Repository: ansible-collections/community.aws

Issue:
ecs_ecr - support for specifying KMS key
### Summary

Unless I'm missing something, I don't see a way to specify KMS key usage in this module.  Is it possible to integrate this? 

### Issue Type

Feature Idea

### Component Name

ecs_ecr

### Additional Information

<!--- Paste example playbooks or commands between quotes below -->
```yaml (paste below)

```


### Code of Conduct

- [X] I agree to follow the Ansible Code of Conduct


Discussion:
Files identified in the description:
None

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
Yes. It's possible.  
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ecr.html

But only on create. It's not changeable once the ECR is created.
Files identified in the description:
* [`plugins/modules/ecs_ecr.py`](https://github.com/['ansible-collections/amazon.aws', 'ansible-collections/community.aws', 'ansible-collections/community.vmware']/blob/main/plugins/modules/ecs_ecr.py)

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
cc @jillr @leedm777 @s-hertel @tremble
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
@markuman 

> Yes. It's possible. https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ecr.html
> 
> But only on create. It's not changeable once the ECR is created.

I have this working to some extent, but is it possible to pass variables to it, in an Ansible role?  Currently, it only works if I hard code the values in the client.create_repository function within the module.  I would like to do something like this:

```
- name: create repo from custom module
   ecr_kms_module:
      repositoryName: "{{ repo_name }}"
      encryptionConfiguration.kmsKey: "{{ kms_key }}"
```
> @markuman
> 
> > Yes. It's possible. https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ecr.html
> > But only on create. It's not changeable once the ECR is created.
> 
> I have this working to some extent, but is it possible to pass variables to it, in an Ansible role? Currently, it only works if I hard code the values in the client.create_repository function within the module. I would like to do something like this:
> 
> ```
> - name: create repo from custom module
>    ecr_kms_module:
>       repositoryName: "{{ repo_name }}"
>       encryptionConfiguration.kmsKey: "{{ kms_key }}"
> ```

Yes, it should work just out of the box.  
Maybe something is wrong with your key-name decision and handling.  
I suggest not to create a new module for this task. Instead patch the existing `ecs_ecr` module and add just the missing `kms_key` parameter. If the ecr already exists, the `kms_key` parameter can be ignored, because it's not changeable. Maybe through a warning if the requestes key differs from the existing key.  
Are you willing to prepare a PR @GreNIX ?

Resolving patch:
diff --git a/plugins/modules/ecs_ecr.py b/plugins/modules/ecs_ecr.py
--- 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'],


Test patch:
diff --git a/tests/integration/targets/ecs_ecr/tasks/main.yml b/tests/integration/targets/ecs_ecr/tasks/main.yml
--- 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
--- /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"
+                }
+            }
+      }
+  ]
+}