311 lines
10 KiB
Bash
311 lines
10 KiB
Bash
#!/usr/bin/env bats
|
|
# CloudFormation tests
|
|
|
|
setup() {
|
|
load 'test_helper/common-setup'
|
|
STACK_NAME="bats-cfn-stack-$(unique_name)"
|
|
TEMPLATE_FILE=$(mktemp /tmp/cfn-bats-XXXXXX.yaml)
|
|
}
|
|
|
|
teardown() {
|
|
aws_cmd cloudformation delete-stack --stack-name "$STACK_NAME" >/dev/null 2>&1 || true
|
|
[ -n "$TEMPLATE_FILE" ] && rm -f "$TEMPLATE_FILE"
|
|
}
|
|
|
|
# ── CreateStack / DescribeStacks ──────────────────────────────────────────────
|
|
|
|
@test "CloudFormation: create stack reaches CREATE_COMPLETE" {
|
|
cat > "$TEMPLATE_FILE" << 'EOF'
|
|
AWSTemplateFormatVersion: '2010-09-09'
|
|
Resources:
|
|
MyQueue:
|
|
Type: AWS::SQS::Queue
|
|
Properties:
|
|
QueueName: bats-cfn-basic-queue
|
|
EOF
|
|
run aws_cmd cloudformation create-stack \
|
|
--stack-name "$STACK_NAME" \
|
|
--template-body "file://$TEMPLATE_FILE"
|
|
assert_success
|
|
|
|
run aws_cmd cloudformation describe-stacks --stack-name "$STACK_NAME"
|
|
assert_success
|
|
local stack_status
|
|
stack_status=$(json_get "$output" '.Stacks[0].StackStatus')
|
|
[ "$stack_status" = "CREATE_COMPLETE" ]
|
|
}
|
|
|
|
@test "CloudFormation: describe-stack-resources lists provisioned resources" {
|
|
cat > "$TEMPLATE_FILE" << 'EOF'
|
|
AWSTemplateFormatVersion: '2010-09-09'
|
|
Resources:
|
|
MyQueue:
|
|
Type: AWS::SQS::Queue
|
|
Properties:
|
|
QueueName: bats-cfn-resources-queue
|
|
EOF
|
|
aws_cmd cloudformation create-stack \
|
|
--stack-name "$STACK_NAME" \
|
|
--template-body "file://$TEMPLATE_FILE" >/dev/null
|
|
|
|
run aws_cmd cloudformation describe-stack-resources --stack-name "$STACK_NAME"
|
|
assert_success
|
|
local count
|
|
count=$(json_get "$output" '.StackResources | length')
|
|
[ "$count" -gt 0 ]
|
|
}
|
|
|
|
@test "CloudFormation: delete stack removes resources" {
|
|
cat > "$TEMPLATE_FILE" << 'EOF'
|
|
AWSTemplateFormatVersion: '2010-09-09'
|
|
Resources:
|
|
MyQueue:
|
|
Type: AWS::SQS::Queue
|
|
Properties:
|
|
QueueName: bats-cfn-delete-queue
|
|
EOF
|
|
aws_cmd cloudformation create-stack \
|
|
--stack-name "$STACK_NAME" \
|
|
--template-body "file://$TEMPLATE_FILE" >/dev/null
|
|
|
|
run aws_cmd cloudformation delete-stack --stack-name "$STACK_NAME"
|
|
assert_success
|
|
|
|
run aws_cmd cloudformation describe-stacks --stack-name "$STACK_NAME"
|
|
# Stack should no longer exist
|
|
[[ "$output" == *"does not exist"* ]]
|
|
STACK_NAME="" # prevent teardown from trying again
|
|
}
|
|
|
|
# ── aws cloudformation deploy (CreateChangeSet + ExecuteChangeSet by ARN) ─────
|
|
#
|
|
# Regression: DescribeChangeSet / ExecuteChangeSet failed when called with the
|
|
# changeset ARN (the AWS CLI always passes the ARN, not the short name).
|
|
# See: https://github.com/floci-io/floci/issues/606
|
|
|
|
@test "CloudFormation: deploy creates stack via changeset" {
|
|
cat > "$TEMPLATE_FILE" << 'EOF'
|
|
AWSTemplateFormatVersion: '2010-09-09'
|
|
Resources:
|
|
MyQueue:
|
|
Type: AWS::SQS::Queue
|
|
Properties:
|
|
QueueName: bats-cfn-deploy-queue
|
|
EOF
|
|
run aws_cmd cloudformation deploy \
|
|
--stack-name "$STACK_NAME" \
|
|
--template-file "$TEMPLATE_FILE"
|
|
assert_success
|
|
|
|
run aws_cmd cloudformation describe-stacks --stack-name "$STACK_NAME"
|
|
assert_success
|
|
local stack_status
|
|
stack_status=$(json_get "$output" '.Stacks[0].StackStatus')
|
|
[ "$stack_status" = "CREATE_COMPLETE" ]
|
|
}
|
|
|
|
@test "CloudFormation: deploy provisions resources correctly" {
|
|
cat > "$TEMPLATE_FILE" << 'EOF'
|
|
AWSTemplateFormatVersion: '2010-09-09'
|
|
Resources:
|
|
MyQueue:
|
|
Type: AWS::SQS::Queue
|
|
Properties:
|
|
QueueName: bats-cfn-deploy-res-queue
|
|
EOF
|
|
aws_cmd cloudformation deploy \
|
|
--stack-name "$STACK_NAME" \
|
|
--template-file "$TEMPLATE_FILE" >/dev/null
|
|
|
|
run aws_cmd cloudformation describe-stack-resources --stack-name "$STACK_NAME"
|
|
assert_success
|
|
local resource_status
|
|
resource_status=$(json_get "$output" '.StackResources[0].ResourceStatus')
|
|
[ "$resource_status" = "CREATE_COMPLETE" ]
|
|
}
|
|
|
|
# ── EC2 VPC/Subnet provisioning (regression: issue #1297) ─────────────────────
|
|
#
|
|
# CloudFormation must create AWS::EC2::VPC / AWS::EC2::Subnet resources for real
|
|
# (in EC2), not stub them. Before the fix, describe-stacks reported CREATE_COMPLETE
|
|
# and exported the subnet ids, but ec2 describe-subnets did not return them and
|
|
# elbv2 create-load-balancer failed with "The subnet ID '...' does not exist".
|
|
# https://github.com/floci-io/floci/issues/1297
|
|
|
|
@test "CloudFormation: VPC/subnet stack exports subnet ids that EC2 and ELBv2 can use" {
|
|
cat > "$TEMPLATE_FILE" << 'EOF'
|
|
{
|
|
"Resources": {
|
|
"Vpc": {
|
|
"Type": "AWS::EC2::VPC",
|
|
"Properties": {"CidrBlock": "10.20.0.0/16"}
|
|
},
|
|
"Subnet1": {
|
|
"Type": "AWS::EC2::Subnet",
|
|
"Properties": {
|
|
"VpcId": {"Ref": "Vpc"},
|
|
"CidrBlock": "10.20.1.0/24",
|
|
"AvailabilityZone": "us-east-1a"
|
|
}
|
|
},
|
|
"Subnet2": {
|
|
"Type": "AWS::EC2::Subnet",
|
|
"Properties": {
|
|
"VpcId": {"Ref": "Vpc"},
|
|
"CidrBlock": "10.20.2.0/24",
|
|
"AvailabilityZone": "us-east-1b"
|
|
}
|
|
}
|
|
},
|
|
"Outputs": {
|
|
"Subnet1Id": {"Value": {"Ref": "Subnet1"}},
|
|
"Subnet2Id": {"Value": {"Ref": "Subnet2"}}
|
|
}
|
|
}
|
|
EOF
|
|
run aws_cmd cloudformation create-stack \
|
|
--stack-name "$STACK_NAME" \
|
|
--template-body "file://$TEMPLATE_FILE"
|
|
assert_success
|
|
|
|
run aws_cmd cloudformation describe-stacks --stack-name "$STACK_NAME"
|
|
assert_success
|
|
local stack_status
|
|
stack_status=$(json_get "$output" '.Stacks[0].StackStatus')
|
|
[ "$stack_status" = "CREATE_COMPLETE" ]
|
|
|
|
# The exported subnet ids must be real subnet-xxxx ids (not the old stub shape).
|
|
local subnet1 subnet2
|
|
subnet1=$(json_get "$output" '.Stacks[0].Outputs[] | select(.OutputKey=="Subnet1Id") | .OutputValue')
|
|
subnet2=$(json_get "$output" '.Stacks[0].Outputs[] | select(.OutputKey=="Subnet2Id") | .OutputValue')
|
|
[[ "$subnet1" == subnet-* ]]
|
|
[[ "$subnet2" == subnet-* ]]
|
|
|
|
# describe-subnets must return the exact subnet ids CloudFormation exported.
|
|
run aws_cmd ec2 describe-subnets --subnet-ids "$subnet1" "$subnet2"
|
|
assert_success
|
|
local count
|
|
count=$(json_get "$output" '.Subnets | length')
|
|
[ "$count" -eq 2 ]
|
|
|
|
# ELBv2 create-load-balancer must accept the exported subnets (no SubnetNotFound).
|
|
run aws_cmd elbv2 create-load-balancer \
|
|
--name "bats-vpc-alb-${RANDOM}" \
|
|
--type application \
|
|
--scheme internal \
|
|
--subnets "$subnet1" "$subnet2"
|
|
assert_success
|
|
local lb_arn
|
|
lb_arn=$(json_get "$output" '.LoadBalancers[0].LoadBalancerArn')
|
|
[ -n "$lb_arn" ]
|
|
[[ "$lb_arn" == *":loadbalancer/"* ]]
|
|
}
|
|
|
|
# ── DeleteStack with managed S3 buckets (regression: issue #1539) ──────────────
|
|
#
|
|
# Real AWS CloudFormation does not report a stack as DELETE_COMPLETE while a
|
|
# managed resource still exists. Deleting a stack that owns a NON-EMPTY
|
|
# AWS::S3::Bucket must put the stack into DELETE_FAILED (S3 DeleteBucket refuses
|
|
# a non-empty bucket) and leave the bucket in place — it must NOT silently report
|
|
# success while the bucket and its objects remain.
|
|
# An EMPTY managed bucket must be deleted as part of the stack delete.
|
|
# https://github.com/floci-io/floci/issues/1539
|
|
|
|
# Poll describe-stacks until the stack reaches the expected status, or report the
|
|
# special value DOES_NOT_EXIST once the stack is gone.
|
|
cfn_wait_status() {
|
|
local stack="$1" expected="$2" tries="${3:-40}"
|
|
local i out status
|
|
for ((i = 0; i < tries; i++)); do
|
|
# `|| true`: describe-stacks exits non-zero once the stack is gone, which
|
|
# would otherwise abort this command substitution under bats' errexit.
|
|
out=$(aws_cmd cloudformation describe-stacks --stack-name "$stack" 2>&1 || true)
|
|
if [[ "$out" == *"does not exist"* ]]; then
|
|
status="DOES_NOT_EXIST"
|
|
else
|
|
status=$(echo "$out" | jq -r '.Stacks[0].StackStatus' 2>/dev/null)
|
|
fi
|
|
if [ "$status" = "$expected" ]; then
|
|
return 0
|
|
fi
|
|
sleep 0.25
|
|
done
|
|
echo "timeout waiting for stack '$stack' to reach '$expected' (last status: '$status')" >&2
|
|
return 1
|
|
}
|
|
|
|
@test "CloudFormation: delete-stack with non-empty S3 bucket fails and keeps the bucket (issue #1539)" {
|
|
local bucket="bats-cfn-nonempty-$(unique_name bkt)"
|
|
cat > "$TEMPLATE_FILE" << EOF
|
|
{
|
|
"AWSTemplateFormatVersion": "2010-09-09",
|
|
"Resources": {
|
|
"Bucket": {
|
|
"Type": "AWS::S3::Bucket",
|
|
"Properties": { "BucketName": "$bucket" }
|
|
}
|
|
}
|
|
}
|
|
EOF
|
|
run aws_cmd cloudformation create-stack \
|
|
--stack-name "$STACK_NAME" \
|
|
--template-body "file://$TEMPLATE_FILE"
|
|
assert_success
|
|
cfn_wait_status "$STACK_NAME" "CREATE_COMPLETE"
|
|
|
|
# Put an object so the managed bucket is non-empty.
|
|
local body_file
|
|
body_file=$(mktemp)
|
|
echo -n "hello floci 1539" > "$body_file"
|
|
run aws_cmd s3api put-object --bucket "$bucket" --key "object.txt" --body "$body_file"
|
|
assert_success
|
|
rm -f "$body_file"
|
|
|
|
run aws_cmd cloudformation delete-stack --stack-name "$STACK_NAME"
|
|
assert_success
|
|
|
|
# AWS leaves the stack in DELETE_FAILED — it must NOT report DELETE_COMPLETE
|
|
# (i.e. the stack must NOT silently disappear).
|
|
cfn_wait_status "$STACK_NAME" "DELETE_FAILED"
|
|
|
|
# The managed bucket and its object must still exist after the failed delete.
|
|
run aws_cmd s3api head-bucket --bucket "$bucket"
|
|
assert_success
|
|
|
|
# Cleanup: empty + remove the leftover bucket, then drop the stack.
|
|
aws_cmd s3 rm "s3://$bucket" --recursive >/dev/null 2>&1 || true
|
|
aws_cmd s3api delete-bucket --bucket "$bucket" >/dev/null 2>&1 || true
|
|
}
|
|
|
|
@test "CloudFormation: delete-stack with empty S3 bucket removes the bucket" {
|
|
local bucket="bats-cfn-empty-$(unique_name bkt)"
|
|
cat > "$TEMPLATE_FILE" << EOF
|
|
{
|
|
"AWSTemplateFormatVersion": "2010-09-09",
|
|
"Resources": {
|
|
"Bucket": {
|
|
"Type": "AWS::S3::Bucket",
|
|
"Properties": { "BucketName": "$bucket" }
|
|
}
|
|
}
|
|
}
|
|
EOF
|
|
run aws_cmd cloudformation create-stack \
|
|
--stack-name "$STACK_NAME" \
|
|
--template-body "file://$TEMPLATE_FILE"
|
|
assert_success
|
|
cfn_wait_status "$STACK_NAME" "CREATE_COMPLETE"
|
|
|
|
run aws_cmd s3api head-bucket --bucket "$bucket"
|
|
assert_success
|
|
|
|
run aws_cmd cloudformation delete-stack --stack-name "$STACK_NAME"
|
|
assert_success
|
|
cfn_wait_status "$STACK_NAME" "DOES_NOT_EXIST"
|
|
STACK_NAME="" # prevent teardown from trying again
|
|
|
|
# The empty managed bucket must be gone.
|
|
run aws_cmd s3api head-bucket --bucket "$bucket"
|
|
assert_failure
|
|
}
|