second first commit
This commit is contained in:
@@ -0,0 +1,310 @@
|
||||
#!/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
|
||||
}
|
||||
Reference in New Issue
Block a user