diff --git a/hacking/aws_config/testing_policies/devops-policy.json b/hacking/aws_config/testing_policies/devops-policy.json index fa7359b6d08..b07ce88542d 100644 --- a/hacking/aws_config/testing_policies/devops-policy.json +++ b/hacking/aws_config/testing_policies/devops-policy.json @@ -12,6 +12,28 @@ "Resource": [ "*" ] + }, + { + "Sid": "AllowCloudformationTests", + "Effect": "Allow", + "Action": [ + "cloudformation:CreateChangeSet", + "cloudformation:CreateStack", + "cloudformation:DeleteChangeSet", + "cloudformation:DeleteStack", + "cloudformation:DescribeChangeSet", + "cloudformation:DescribeStackEvents", + "cloudformation:DescribeStacks", + "cloudformation:GetStackPolicy", + "cloudformation:GetTemplate", + "cloudformation:ListChangeSets", + "cloudformation:ListStackResources", + "cloudformation:UpdateStack", + "cloudformation:UpdateTerminationProtection" + ], + "Resource": [ + "*" + ] } ] } diff --git a/test/integration/targets/cloudformation/aliases b/test/integration/targets/cloudformation/aliases new file mode 100644 index 00000000000..55555be7899 --- /dev/null +++ b/test/integration/targets/cloudformation/aliases @@ -0,0 +1,3 @@ +cloud/aws +shippable/aws/group2 +cloudformation_info diff --git a/test/integration/targets/cloudformation/defaults/main.yml b/test/integration/targets/cloudformation/defaults/main.yml new file mode 100644 index 00000000000..aaf0ca7e616 --- /dev/null +++ b/test/integration/targets/cloudformation/defaults/main.yml @@ -0,0 +1,8 @@ +stack_name: "{{ resource_prefix }}" + +vpc_name: '{{ resource_prefix }}-vpc' +vpc_seed: '{{ resource_prefix }}' +vpc_cidr: '10.{{ 256 | random(seed=vpc_seed) }}.0.0/16' +subnet_cidr: '10.{{ 256 | random(seed=vpc_seed) }}.32.0/24' + +ec2_ami_name: 'amzn2-ami-hvm-2.*-x86_64-gp2' diff --git a/test/integration/targets/cloudformation/files/cf_template.json b/test/integration/targets/cloudformation/files/cf_template.json new file mode 100644 index 00000000000..ff4c5693b0b --- /dev/null +++ b/test/integration/targets/cloudformation/files/cf_template.json @@ -0,0 +1,37 @@ +{ + "AWSTemplateFormatVersion" : "2010-09-09", + + "Description" : "Create an Amazon EC2 instance.", + + "Parameters" : { + "InstanceType" : { + "Description" : "EC2 instance type", + "Type" : "String", + "Default" : "t3.nano", + "AllowedValues" : [ "t3.micro", "t3.nano"] + }, + "ImageId" : { + "Type" : "String" + }, + "SubnetId" : { + "Type" : "String" + } + }, + + "Resources" : { + "EC2Instance" : { + "Type" : "AWS::EC2::Instance", + "Properties" : { + "InstanceType" : { "Ref" : "InstanceType" }, + "ImageId" : { "Ref" : "ImageId" }, + "SubnetId": { "Ref" : "SubnetId" } + } + } + }, + + "Outputs" : { + "InstanceId" : { + "Value" : { "Ref" : "EC2Instance" } + } + } +} diff --git a/test/integration/targets/cloudformation/tasks/main.yml b/test/integration/targets/cloudformation/tasks/main.yml new file mode 100644 index 00000000000..e7c21f96555 --- /dev/null +++ b/test/integration/targets/cloudformation/tasks/main.yml @@ -0,0 +1,365 @@ +--- + +- module_defaults: + group/aws: + aws_access_key: '{{ aws_access_key | default(omit) }}' + aws_secret_key: '{{ aws_secret_key | default(omit) }}' + security_token: '{{ security_token | default(omit) }}' + region: '{{ aws_region | default(omit) }}' + + block: + + # ==== Env setup ========================================================== + + - name: Create a test VPC + ec2_vpc_net: + name: "{{ vpc_name }}" + cidr_block: "{{ vpc_cidr }}" + tags: + Name: Cloudformation testing + register: testing_vpc + + - name: Create a test subnet + ec2_vpc_subnet: + vpc_id: "{{ testing_vpc.vpc.id }}" + cidr: "{{ subnet_cidr }}" + register: testing_subnet + + - name: Find AMI to use + ec2_ami_info: + owners: 'amazon' + filters: + name: '{{ ec2_ami_name }}' + register: ec2_amis + + - name: Set fact with latest AMI + vars: + latest_ami: '{{ ec2_amis.images | sort(attribute="creation_date") | last }}' + set_fact: + ec2_ami_image: '{{ latest_ami.image_id }}' + + # ==== Cloudformation tests =============================================== + + # 1. Basic stack creation (check mode, actual run and idempotency) + # 2. Tags + # 3. cloudformation_info tests (basic + all_facts) + # 4. termination_protection + # 5. create_changeset + changeset_name + + # There is still scope to add tests for - + # 1. capabilities + # 2. stack_policy + # 3. on_create_failure (covered in unit tests) + # 4. Passing in a role + # 5. nested stacks? + + + - name: create a cloudformation stack (check mode) + cloudformation: + stack_name: "{{ stack_name }}" + template_body: "{{ lookup('file','cf_template.json') }}" + template_parameters: + InstanceType: "t3.nano" + ImageId: "{{ ec2_ami_image }}" + SubnetId: "{{ testing_subnet.subnet.id }}" + tags: + Stack: "{{ stack_name }}" + test: "{{ resource_prefix }}" + register: cf_stack + check_mode: yes + + - name: check task return attributes + assert: + that: + - cf_stack.changed + - "'msg' in cf_stack and 'New stack would be created' in cf_stack.msg" + + - name: create a cloudformation stack + cloudformation: + stack_name: "{{ stack_name }}" + template_body: "{{ lookup('file','cf_template.json') }}" + template_parameters: + InstanceType: "t3.nano" + ImageId: "{{ ec2_ami_image }}" + SubnetId: "{{ testing_subnet.subnet.id }}" + tags: + Stack: "{{ stack_name }}" + test: "{{ resource_prefix }}" + register: cf_stack + + - name: check task return attributes + assert: + that: + - cf_stack.changed + - "'events' in cf_stack" + - "'output' in cf_stack and 'Stack CREATE complete' in cf_stack.output" + - "'stack_outputs' in cf_stack and 'InstanceId' in cf_stack.stack_outputs" + - "'stack_resources' in cf_stack" + + - name: create a cloudformation stack (check mode) (idempotent) + cloudformation: + stack_name: "{{ stack_name }}" + template_body: "{{ lookup('file','cf_template.json') }}" + template_parameters: + InstanceType: "t3.nano" + ImageId: "{{ ec2_ami_image }}" + SubnetId: "{{ testing_subnet.subnet.id }}" + tags: + Stack: "{{ stack_name }}" + test: "{{ resource_prefix }}" + register: cf_stack + check_mode: yes + + - name: check task return attributes + assert: + that: + - not cf_stack.changed + + - name: create a cloudformation stack (idempotent) + cloudformation: + stack_name: "{{ stack_name }}" + template_body: "{{ lookup('file','cf_template.json') }}" + template_parameters: + InstanceType: "t3.nano" + ImageId: "{{ ec2_ami_image }}" + SubnetId: "{{ testing_subnet.subnet.id }}" + tags: + Stack: "{{ stack_name }}" + test: "{{ resource_prefix }}" + register: cf_stack + + - name: check task return attributes + assert: + that: + - not cf_stack.changed + - "'output' in cf_stack and 'Stack is already up-to-date.' in cf_stack.output" + - "'stack_outputs' in cf_stack and 'InstanceId' in cf_stack.stack_outputs" + - "'stack_resources' in cf_stack" + + - name: get stack details + cloudformation_info: + stack_name: "{{ stack_name }}" + register: stack_info + + - name: assert stack info + assert: + that: + - "'cloudformation' in stack_info" + - "stack_info.cloudformation | length == 1" + - "stack_name in stack_info.cloudformation" + - "'stack_description' in stack_info.cloudformation[stack_name]" + - "'stack_outputs' in stack_info.cloudformation[stack_name]" + - "'stack_parameters' in stack_info.cloudformation[stack_name]" + - "'stack_tags' in stack_info.cloudformation[stack_name]" + - "stack_info.cloudformation[stack_name].stack_tags.Stack == stack_name" + + - name: get stack details (all_facts) + cloudformation_info: + stack_name: "{{ stack_name }}" + all_facts: yes + register: stack_info + + - name: assert stack info + assert: + that: + - "'stack_events' in stack_info.cloudformation[stack_name]" + - "'stack_policy' in stack_info.cloudformation[stack_name]" + - "'stack_resource_list' in stack_info.cloudformation[stack_name]" + - "'stack_resources' in stack_info.cloudformation[stack_name]" + - "'stack_template' in stack_info.cloudformation[stack_name]" + + # ==== Cloudformation tests (create changeset) ============================ + + # try to create a changeset by changing instance type + - name: create a changeset + cloudformation: + stack_name: "{{ stack_name }}" + create_changeset: yes + changeset_name: "test-changeset" + template_body: "{{ lookup('file','cf_template.json') }}" + template_parameters: + InstanceType: "t3.micro" + ImageId: "{{ ec2_ami_image }}" + SubnetId: "{{ testing_subnet.subnet.id }}" + tags: + Stack: "{{ stack_name }}" + test: "{{ resource_prefix }}" + register: create_changeset_result + + - name: assert changeset created + assert: + that: + - "create_changeset_result.changed" + - "'change_set_id' in create_changeset_result" + - "'Stack CREATE_CHANGESET complete' in create_changeset_result.output" + + # try to create an empty changeset by passing in unchanged template + - name: create a changeset + cloudformation: + stack_name: "{{ stack_name }}" + create_changeset: yes + template_body: "{{ lookup('file','cf_template.json') }}" + template_parameters: + InstanceType: "t3.nano" + ImageId: "{{ ec2_ami_image }}" + SubnetId: "{{ testing_subnet.subnet.id }}" + tags: + Stack: "{{ stack_name }}" + test: "{{ resource_prefix }}" + register: create_changeset_result + + - name: assert changeset created + assert: + that: + - "not create_changeset_result.changed" + - "'The created Change Set did not contain any changes to this stack and was deleted.' in create_changeset_result.output" + + # ==== Cloudformation tests (termination_protection) ====================== + + - name: set termination protection to true + cloudformation: + stack_name: "{{ stack_name }}" + termination_protection: yes + template_body: "{{ lookup('file','cf_template.json') }}" + template_parameters: + InstanceType: "t3.nano" + ImageId: "{{ ec2_ami_image }}" + SubnetId: "{{ testing_subnet.subnet.id }}" + tags: + Stack: "{{ stack_name }}" + test: "{{ resource_prefix }}" + register: cf_stack + +# This fails - #65592 +# - name: check task return attributes +# assert: +# that: +# - cf_stack.changed + + - name: get stack details + cloudformation_info: + stack_name: "{{ stack_name }}" + register: stack_info + + - name: assert stack info + assert: + that: + - "stack_info.cloudformation[stack_name].stack_description.enable_termination_protection" + + - name: set termination protection to false + cloudformation: + stack_name: "{{ stack_name }}" + termination_protection: no + template_body: "{{ lookup('file','cf_template.json') }}" + template_parameters: + InstanceType: "t3.nano" + ImageId: "{{ ec2_ami_image }}" + SubnetId: "{{ testing_subnet.subnet.id }}" + tags: + Stack: "{{ stack_name }}" + test: "{{ resource_prefix }}" + register: cf_stack + +# This fails - #65592 +# - name: check task return attributes +# assert: +# that: +# - cf_stack.changed + + - name: get stack details + cloudformation_info: + stack_name: "{{ stack_name }}" + register: stack_info + + - name: assert stack info + assert: + that: + - "not stack_info.cloudformation[stack_name].stack_description.enable_termination_protection" + + # ==== Cloudformation tests (delete stack tests) ========================== + + - name: delete cloudformation stack (check mode) + cloudformation: + stack_name: "{{ stack_name }}" + state: absent + check_mode: yes + register: cf_stack + + - name: check task return attributes + assert: + that: + - cf_stack.changed + - "'msg' in cf_stack and 'Stack would be deleted' in cf_stack.msg" + + - name: delete cloudformation stack + cloudformation: + stack_name: "{{ stack_name }}" + state: absent + register: cf_stack + + - name: check task return attributes + assert: + that: + - cf_stack.changed + - "'output' in cf_stack and 'Stack Deleted' in cf_stack.output" + + - name: delete cloudformation stack (check mode) (idempotent) + cloudformation: + stack_name: "{{ stack_name }}" + state: absent + check_mode: yes + register: cf_stack + + - name: check task return attributes + assert: + that: + - not cf_stack.changed + - "'msg' in cf_stack" + - >- + "Stack doesn't exist" in cf_stack.msg + + - name: delete cloudformation stack (idempotent) + cloudformation: + stack_name: "{{ stack_name }}" + state: absent + register: cf_stack + + - name: check task return attributes + assert: + that: + - not cf_stack.changed + - "'output' in cf_stack and 'Stack not found.' in cf_stack.output" + + - name: get stack details + cloudformation_info: + stack_name: "{{ stack_name }}" + register: stack_info + + - name: assert stack info + assert: + that: + - "not stack_info.cloudformation" + + # ==== Cleanup ============================================================ + + always: + + - name: delete stack + cloudformation: + stack_name: "{{ stack_name }}" + state: absent + ignore_errors: yes + + - name: Delete test subnet + ec2_vpc_subnet: + vpc_id: "{{ testing_vpc.vpc.id }}" + cidr: "{{ subnet_cidr }}" + state: absent + ignore_errors: yes + + - name: Delete test VPC + ec2_vpc_net: + name: "{{ vpc_name }}" + cidr_block: "{{ vpc_cidr }}" + state: absent + ignore_errors: yes