diff --git a/changelogs/fragments/1623-ecs_ecr-add-kms-config.yml b/changelogs/fragments/1623-ecs_ecr-add-kms-config.yml new file mode 100644 index 00000000000..b0e194bc32d --- /dev/null +++ b/changelogs/fragments/1623-ecs_ecr-add-kms-config.yml @@ -0,0 +1,2 @@ +minor_changes: +- ecs_ecr - add `encryption_configuration` option (https://github.com/ansible-collections/community.aws/pull/1623). diff --git a/plugins/modules/ecs_ecr.py b/plugins/modules/ecs_ecr.py index 1323bc6c35a..d83d5af2ec1 100644 --- a/plugins/modules/ecs_ecr.py +++ b/plugins/modules/ecs_ecr.py @@ -85,6 +85,24 @@ default: false type: bool version_added: 1.3.0 + encryption_configuration: + description: + - The encryption configuration for the repository. + required: false + suboptions: + encryption_type: + description: + - The encryption type to use. + choices: [AES256, KMS] + default: 'AES256' + type: str + kms_key: + description: + - If I(encryption_type=KMS), specify the KMS key to use for encryption. + - The alias, key ID, or full ARN of the KMS key can be specified. + type: str + type: dict + version_added: 5.2.0 author: - David M. Lee (@leedm777) extends_documentation_fragment: @@ -161,6 +179,13 @@ community.aws.ecs_ecr: name: needs-no-lifecycle-policy purge_lifecycle_policy: true + +- name: set-encryption-configuration + community.aws.ecs_ecr: + name: uses-custom-kms-key + encryption_configuration: + encryption_type: KMS + kms_key: custom-kms-key-alias ''' RETURN = ''' @@ -201,6 +226,7 @@ except ImportError: pass # Handled by AnsibleAWSModule +from ansible.module_utils.common.dict_transformations import snake_dict_to_camel_dict from ansible.module_utils.six import string_types from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule @@ -249,17 +275,21 @@ def get_repository_policy(self, registry_id, name): except is_boto3_error_code(['RepositoryNotFoundException', 'RepositoryPolicyNotFoundException']): return None - def create_repository(self, registry_id, name, image_tag_mutability): + def create_repository(self, registry_id, name, image_tag_mutability, encryption_configuration): if registry_id: default_registry_id = self.sts.get_caller_identity().get('Account') if registry_id != default_registry_id: raise Exception('Cannot create repository in registry {0}.' 'Would be created in {1} instead.'.format(registry_id, default_registry_id)) + if encryption_configuration is None: + encryption_configuration = dict(encryptionType='AES256') + if not self.check_mode: repo = self.ecr.create_repository( repositoryName=name, - imageTagMutability=image_tag_mutability).get('repository') + imageTagMutability=image_tag_mutability, + encryptionConfiguration=encryption_configuration).get('repository') self.changed = True return repo else: @@ -412,6 +442,7 @@ def run(ecr, params): lifecycle_policy_text = params['lifecycle_policy'] purge_lifecycle_policy = params['purge_lifecycle_policy'] scan_on_push = params['scan_on_push'] + encryption_configuration = snake_dict_to_camel_dict(params['encryption_configuration']) # Parse policies, if they are given try: @@ -438,10 +469,16 @@ def run(ecr, params): result['created'] = False if not repo: - repo = ecr.create_repository(registry_id, name, image_tag_mutability) + repo = ecr.create_repository( + registry_id, name, image_tag_mutability, encryption_configuration) result['changed'] = True result['created'] = True else: + if encryption_configuration is not None: + if repo.get('encryptionConfiguration') != encryption_configuration: + result['msg'] = 'Cannot modify repository encryption type' + return False, result + repo = ecr.put_image_tag_mutability(registry_id, name, image_tag_mutability) result['repository'] = repo @@ -557,7 +594,18 @@ def main(): purge_policy=dict(required=False, type='bool'), lifecycle_policy=dict(required=False, type='json'), purge_lifecycle_policy=dict(required=False, type='bool'), - scan_on_push=(dict(required=False, type='bool', default=False)) + scan_on_push=(dict(required=False, type='bool', default=False)), + encryption_configuration=dict( + required=False, + type='dict', + options=dict( + encryption_type=dict(required=False, type='str', default='AES256', choices=['AES256', 'KMS']), + kms_key=dict(required=False, type='str', no_log=False), + ), + required_if=[ + ['encryption_type', 'KMS', ['kms_key']], + ], + ), ) mutually_exclusive = [ ['policy', 'purge_policy'], diff --git a/tests/integration/targets/ecs_ecr/tasks/main.yml b/tests/integration/targets/ecs_ecr/tasks/main.yml index 73fcc3530f1..e0ce4f3f664 100644 --- a/tests/integration/targets/ecs_ecr/tasks/main.yml +++ b/tests/integration/targets/ecs_ecr/tasks/main.yml @@ -10,6 +10,24 @@ - set_fact: ecr_name: '{{ resource_prefix }}-ecr' + - name: get ARN of calling user + aws_caller_info: + register: aws_caller_info + + - name: create KMS key for testing + aws_kms: + alias: "{{ resource_prefix }}-ecr" + description: a key used for testing ECR + state: present + enabled: yes + key_spec: SYMMETRIC_DEFAULT + key_usage: ENCRYPT_DECRYPT + policy: "{{ lookup('template', 'kms_policy.j2') }}" + tags: + Name: "{{ resource_prefix }}-ecr" + AnsibleTest: AnsibleTestVpc + register: kms_test_key + - name: When creating with check mode ecs_ecr: name: '{{ ecr_name }}' @@ -54,6 +72,11 @@ that: - result.repository.imageTagMutability == "MUTABLE" + - name: it should use AES256 encryption by default + assert: + that: + - result.repository.encryptionConfiguration.encryptionType == "AES256" + - name: When pulling an existing repository that has no existing policy ecs_ecr: name: '{{ ecr_name }}' @@ -538,9 +561,52 @@ - result is changed - not result.repository.imageScanningConfiguration.scanOnPush + - name: When modifying the encryption setting of an existing repository + ecs_ecr: + name: '{{ ecr_name }}' + encryption_configuration: + encryption_type: KMS + kms_key: '{{ kms_test_key.key_arn }}' + register: result + ignore_errors: true + + - name: it should fail + assert: + that: + - result is failed + + - name: delete repository + ecs_ecr: + name: '{{ ecr_name }}' + state: absent + + - name: When creating a repo using KMS encryption + ecs_ecr: + name: '{{ ecr_name }}' + encryption_configuration: + encryption_type: KMS + kms_key: '{{ kms_test_key.key_arn }}' + register: result + + - name: it should create the repo and use KMS encryption + assert: + that: + - result is changed + - result.repository.encryptionConfiguration.encryptionType == "KMS" + + - name: it should use the provided KMS key + assert: + that: + - result.repository.encryptionConfiguration.kmsKey == '{{ kms_test_key.key_arn }}' + always: - name: Delete lingering ECR repository ecs_ecr: name: '{{ ecr_name }}' state: absent + + - name: Delete KMS key + aws_kms: + key_id: '{{ kms_test_key.key_arn }}' + state: absent diff --git a/tests/integration/targets/ecs_ecr/templates/kms_policy.j2 b/tests/integration/targets/ecs_ecr/templates/kms_policy.j2 new file mode 100644 index 00000000000..17108e5b367 --- /dev/null +++ b/tests/integration/targets/ecs_ecr/templates/kms_policy.j2 @@ -0,0 +1,72 @@ +{ + "Id": "key-ansible-test-policy-123", + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "Allow access for root user", + "Effect": "Allow", + "Principal": { + "AWS": "arn:aws:iam::{{ aws_caller_info.account }}:root" + }, + "Action": "kms:*", + "Resource": "*" + }, + { + "Sid": "Allow access for calling user", + "Effect": "Allow", + "Principal": { + "AWS": "{{ aws_caller_info.arn }}" + }, + "Action": [ + "kms:Create*", + "kms:Describe*", + "kms:Enable*", + "kms:List*", + "kms:Put*", + "kms:Update*", + "kms:Revoke*", + "kms:Disable*", + "kms:Get*", + "kms:Delete*", + "kms:TagResource", + "kms:UntagResource", + "kms:ScheduleKeyDeletion", + "kms:CancelKeyDeletion" + ], + "Resource": "*" + }, + { + "Sid": "Allow use of the key", + "Effect": "Allow", + "Principal": { + "AWS": "{{ aws_caller_info.arn }}" + }, + "Action": [ + "kms:Encrypt", + "kms:Decrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + "kms:DescribeKey" + ], + "Resource": "*" + }, + { + "Sid": "Allow attachment of persistent resources", + "Effect": "Allow", + "Principal": { + "AWS": "{{ aws_caller_info.arn }}" + }, + "Action": [ + "kms:CreateGrant", + "kms:ListGrants", + "kms:RevokeGrant" + ], + "Resource": "*", + "Condition": { + "Bool": { + "kms:GrantIsForAWSResource": "true" + } + } + } + ] +}