Adding support for Amazon ECR (#19306)

* Adding support for Amazon ECR

This patch adds a new module named ecr, which can create, update or
destroy Amazon EC2 Container Registries. It also handles the management
of ECR policies.

* ecs_ecr: addressed review feeback

 * Renaming ecr to ecs_ecr
 * Fixed docs
   * Removed bad doc about empty string handling
   * Added example of `delete_policy`
 * Removed `policy_text` option; switched policy to `json` type so
   it can accept string or dict
 * Added support for specifying registry_id
 * Added explicit else after returned if clauses
 * Added `force_set_policy` option
 * Improved `set_repository_policy` error handling
 * Fixed policy comparisons when AWS doesn't keep the ordering stable
 * Moved `boto_exception` into the module
This commit is contained in:
David M. Lee 2017-01-17 13:45:43 -06:00 committed by Ryan Brown
parent 3fbbdcd466
commit fde551fa2a
4 changed files with 641 additions and 0 deletions

View file

@ -0,0 +1,377 @@
#!/usr/bin/python
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import print_function
import json
import time
import inspect
from ansible.module_utils.basic import *
from ansible.module_utils.ec2 import *
try:
import boto3
from botocore.exceptions import ClientError
HAS_BOTO3 = True
except ImportError:
HAS_BOTO3 = False
ANSIBLE_METADATA = {'status': ['preview'],
'supported_by': 'community',
'version': '1.0'}
DOCUMENTATION = '''
---
module: ecs_ecr
version_added: "2.3"
short_description: Manage Elastic Container Registry repositories
description:
- Manage Elastic Container Registry repositories
options:
name:
description:
- the name of the repository
required: true
registry_id:
description:
- AWS account id associated with the registry.
- If not specified, the default registry is assumed.
required: false
policy:
description:
- JSON or dict that represents the new policy
required: false
force_set_policy:
description:
- if no, prevents setting a policy that would prevent you from
setting another policy in the future.
required: false
default: false
delete_policy:
description:
- if yes, remove the policy from the repository
required: false
default: false
state:
description:
- create or destroy the repository
required: false
choices: [present, absent]
default: 'present'
author:
- David M. Lee (@leedm777)
extends_documentation_fragment: aws
'''
EXAMPLES = '''
# If the repository does not exist, it is created. If it does exist, would not
# affect any policies already on it.
- name: ecr-repo
ecs_ecr: name=super/cool
- name: destroy-ecr-repo
ecs_ecr: name=old/busted state=absent
- name: Cross account ecr-repo
ecs_ecr: registry_id=999999999999 name=cross/account
- name: set-policy as object
ecs_ecr:
name: needs-policy-object
policy:
Version: '2008-10-17'
Statement:
- Sid: read-only
Effect: Allow
Principal:
AWS: '{{ read_only_arn }}'
Action:
- ecr:GetDownloadUrlForLayer
- ecr:BatchGetImage
- ecr:BatchCheckLayerAvailability
- name: set-policy as string
ecs_ecr:
name: needs-policy-string
policy: "{{ lookup('template', 'policy.json.j2') }}"
- name: delete-policy
ecs_ecr:
name: needs-no-policy
delete_policy: yes
'''
RETURN = '''
state:
type: string
description: The asserted state of the repository (present, absent)
created:
type: boolean
description: If true, the repository was created
name:
type: string
description: The name of the repository
returned: "when state == 'absent'"
repository:
type: dict
description: The created or updated repository
returned: "when state == 'present'"
sample:
createdAt: '2017-01-17T08:41:32-06:00'
registryId: '999999999999'
repositoryArn: arn:aws:ecr:us-east-1:999999999999:repository/ecr-test-1484664090
repositoryName: ecr-test-1484664090
repositoryUri: 999999999999.dkr.ecr.us-east-1.amazonaws.com/ecr-test-1484664090
'''
def boto_exception(err):
'''boto error message handler'''
if hasattr(err, 'error_message'):
error = err.error_message
elif hasattr(err, 'message'):
error = err.message
else:
error = '%s: %s' % (Exception, err)
return error
def build_kwargs(registry_id):
"""
Builds a kwargs dict which may contain the optional registryId.
:param registry_id: Optional string containing the registryId.
:return: kwargs dict with registryId, if given
"""
if not registry_id:
return dict()
else:
return dict(registryId=registry_id)
class EcsEcr:
def __init__(self, module):
region, ec2_url, aws_connect_kwargs = \
get_aws_connection_info(module, boto3=True)
self.ecr = boto3_conn(module, conn_type='client',
resource='ecr', region=region,
endpoint=ec2_url, **aws_connect_kwargs)
self.check_mode = module.check_mode
self.changed = False
self.skipped = False
def get_repository(self, registry_id, name):
try:
res = self.ecr.describe_repositories(
repositoryNames=[name], **build_kwargs(registry_id))
repos = res.get('repositories')
return repos and repos[0]
except ClientError as err:
code = err.response['Error'].get('Code', 'Unknown')
if code == 'RepositoryNotFoundException':
return None
raise
def get_repository_policy(self, registry_id, name):
try:
res = self.ecr.get_repository_policy(
repositoryName=name, **build_kwargs(registry_id))
text = res.get('policyText')
return text and json.loads(text)
except ClientError as err:
code = err.response['Error'].get('Code', 'Unknown')
if code == 'RepositoryPolicyNotFoundException':
return None
raise
def create_repository(self, registry_id, name):
if not self.check_mode:
repo = self.ecr.create_repository(
repositoryName=name, **build_kwargs(registry_id)).get(
'repository')
self.changed = True
return repo
else:
self.skipped = True
return dict(repositoryName=name)
def set_repository_policy(self, registry_id, name, policy_text, force):
if not self.check_mode:
policy = self.ecr.set_repository_policy(
repositoryName=name,
policyText=policy_text,
force=force,
**build_kwargs(registry_id))
self.changed = True
return policy
else:
self.skipped = True
if self.get_repository(registry_id, name) is None:
printable = name
if registry_id:
printable = '{}:{}'.format(registry_id, name)
raise Exception(
'could not find repository {}'.format(printable))
return
def delete_repository(self, registry_id, name):
if not self.check_mode:
repo = self.ecr.delete_repository(
repositoryName=name, **build_kwargs(registry_id))
self.changed = True
return repo
else:
repo = self.get_repository(registry_id, name)
if repo:
self.skipped = True
return repo
return None
def delete_repository_policy(self, registry_id, name):
if not self.check_mode:
policy = self.ecr.delete_repository_policy(
repositoryName=name, **build_kwargs(registry_id))
self.changed = True
return policy
else:
policy = self.get_repository_policy(registry_id, name)
if policy:
self.skipped = True
return policy
return None
def run(ecr, params, verbosity):
# type: (EcsEcr, dict, int) -> Tuple[bool, dict]
result = {}
try:
name = params['name']
state = params['state']
policy_text = params['policy']
delete_policy = params['delete_policy']
registry_id = params['registry_id']
force_set_policy = params['force_set_policy']
# If a policy was given, parse it
policy = policy_text and json.loads(policy_text)
result['state'] = state
result['created'] = False
repo = ecr.get_repository(registry_id, name)
if state == 'present':
result['created'] = False
if not repo:
repo = ecr.create_repository(registry_id, name)
result['changed'] = True
result['created'] = True
result['repository'] = repo
if delete_policy:
original_policy = ecr.get_repository_policy(registry_id, name)
if verbosity >= 2:
result['policy'] = None
if verbosity >= 3:
result['original_policy'] = original_policy
if original_policy:
ecr.delete_repository_policy(registry_id, name)
result['changed'] = True
elif policy_text is not None:
try:
policy = sort_json_policy_dict(policy)
if verbosity >= 2:
result['policy'] = policy
original_policy = ecr.get_repository_policy(
registry_id, name)
if original_policy:
original_policy = sort_json_policy_dict(original_policy)
if verbosity >= 3:
result['original_policy'] = original_policy
if original_policy != policy:
ecr.set_repository_policy(
registry_id, name, policy_text, force_set_policy)
result['changed'] = True
except:
# Some failure w/ the policy. It's helpful to know what the
# policy is.
result['policy'] = policy_text
raise
elif state == 'absent':
result['name'] = name
if repo:
ecr.delete_repository(registry_id, name)
result['changed'] = True
except Exception as err:
msg = str(err)
if isinstance(err, ClientError):
msg = boto_exception(err)
result['msg'] = msg
result['exception'] = traceback.format_exc()
return False, result
if ecr.skipped:
result['skipped'] = True
if ecr.changed:
result['changed'] = True
return True, result
def main():
argument_spec = ec2_argument_spec()
argument_spec.update(dict(
name=dict(required=True),
registry_id=dict(required=False),
state=dict(required=False, choices=['present', 'absent'],
default='present'),
force_set_policy=dict(required=False, type='bool', default=False),
policy=dict(required=False, type='json'),
delete_policy=dict(required=False, type='bool')))
module = AnsibleModule(argument_spec=argument_spec,
supports_check_mode=True,
mutually_exclusive=[
['policy', 'delete_policy']])
if not HAS_BOTO3:
module.fail_json(msg='boto3 required for this module')
ecr = EcsEcr(module)
passed, result = run(ecr, module.params, module._verbosity)
if passed:
module.exit_json(**result)
else:
module.fail_json(**result)
if __name__ == '__main__':
main()

View file

@ -13,6 +13,7 @@
#- { role: test_ec2, tags: test_ec2 } #- { role: test_ec2, tags: test_ec2 }
- { role: test_ec2_asg, tags: test_ec2_asg } - { role: test_ec2_asg, tags: test_ec2_asg }
- { role: test_ec2_vpc_nat_gateway, tags: test_ec2_vpc_nat_gateway } - { role: test_ec2_vpc_nat_gateway, tags: test_ec2_vpc_nat_gateway }
- { role: test_ecs_ecr, tags: test_ecs_ecr }
# complex test for ec2_elb, split up over multiple plays # complex test for ec2_elb, split up over multiple plays
# since there is a setup component as well as the test which # since there is a setup component as well as the test which

View file

@ -0,0 +1,10 @@
policy:
Version: '2008-10-17'
Statement:
- Sid: new statement
Effect: Allow
Principal: "*"
Action:
- ecr:GetDownloadUrlForLayer
- ecr:BatchGetImage
- ecr:BatchCheckLayerAvailability

View file

@ -0,0 +1,253 @@
---
- set_fact:
ecr_name: 'ecr-test-{{ ansible_date_time.epoch }}'
- block:
- name: When creating with check mode
ecs_ecr: name='{{ ecr_name }}' region='{{ ec2_region }}'
register: result
check_mode: yes
- name: it should skip, change and create
assert:
that:
- result|skipped
- result|changed
- result.created
- name: When specifying a registry that is inaccessible
ecs_ecr: registry_id=999999999999 name='{{ ecr_name }}' region='{{ ec2_region }}'
register: result
ignore_errors: true
- name: it should fail with an AccessDeniedException
assert:
that:
- result|failed
- '"AccessDeniedException" in result.msg'
- name: When creating a repository
ecs_ecr: name='{{ ecr_name }}' region='{{ ec2_region }}'
register: result
- name: it should change and create
assert:
that:
- result|changed
- result.created
- name: When creating a repository that already exists in check mode
ecs_ecr: name='{{ ecr_name }}' region='{{ ec2_region }}'
register: result
check_mode: yes
- name: it should not skip, should not change
assert:
that:
- not result|skipped
- not result|changed
- name: When creating a repository that already exists
ecs_ecr: name='{{ ecr_name }}' region='{{ ec2_region }}'
register: result
- name: it should not change
assert:
that:
- not result|changed
- name: When in check mode, and deleting a policy that does not exists
ecs_ecr:
region: '{{ ec2_region }}'
name: '{{ ecr_name }}'
delete_policy: yes
register: result
check_mode: yes
- name: it should not skip and not change
assert:
that:
- not result|skipped
- not result|changed
- name: When in check mode, setting policy on a repository that has no policy
ecs_ecr:
region: '{{ ec2_region }}'
name: '{{ ecr_name }}'
policy: '{{ policy }}'
register: result
check_mode: yes
- name: it should skip, change and not create
assert:
that:
- result|skipped
- result|changed
- not result.created
- name: When setting policy on a repository that has no policy
ecs_ecr:
region: '{{ ec2_region }}'
name: '{{ ecr_name }}'
policy: '{{ policy }}'
register: result
- name: it should change and not create
assert:
that:
- result|changed
- not result.created
- name: When in check mode, and deleting a policy that exists
ecs_ecr:
region: '{{ ec2_region }}'
name: '{{ ecr_name }}'
delete_policy: yes
register: result
check_mode: yes
- name: it should skip, change but not create
assert:
that:
- result|skipped
- result|changed
- not result.created
- name: When deleting a policy that exists
ecs_ecr:
region: '{{ ec2_region }}'
name: '{{ ecr_name }}'
delete_policy: yes
register: result
- name: it should change and not create
assert:
that:
- result|changed
- not result.created
- name: When setting a policy as a string
ecs_ecr:
region: '{{ ec2_region }}'
name: '{{ ecr_name }}'
policy: '{{ policy | to_json }}'
register: result
- name: it should change and not create
assert:
that:
- result|changed
- not result.created
- name: When setting a policy to its current value
ecs_ecr:
region: '{{ ec2_region }}'
name: '{{ ecr_name }}'
policy: '{{ policy }}'
register: result
- name: it should not change
assert:
that:
- not result|changed
- name: When omitting policy on a repository that has a policy
ecs_ecr:
region: '{{ ec2_region }}'
name: '{{ ecr_name }}'
register: result
- name: it should not change
assert:
that:
- not result|changed
- name: When specifying both policy and delete_policy
ecs_ecr:
region: '{{ ec2_region }}'
name: '{{ ecr_name }}'
policy: '{{ policy }}'
delete_policy: yes
register: result
ignore_errors: true
- name: it should fail
assert:
that:
- result|failed
- name: When specifying invalid JSON for policy
ecs_ecr:
region: '{{ ec2_region }}'
name: '{{ ecr_name }}'
policy_text: "Ceci n'est pas une JSON"
register: result
ignore_errors: true
- name: it should fail
assert:
that:
- result|failed
- name: When in check mode, deleting a policy that exists
ecs_ecr: name='{{ ecr_name }}' region='{{ ec2_region }}' state=absent
register: result
check_mode: yes
- name: it should skip, change and not create
assert:
that:
- result|skipped
- result|changed
- not result.created
- name: When deleting a policy that exists
ecs_ecr: name='{{ ecr_name }}' region='{{ ec2_region }}' state=absent
register: result
- name: it should change
assert:
that:
- result|changed
- name: When in check mode, deleting a policy that does not exist
ecs_ecr: name='{{ ecr_name }}' region='{{ ec2_region }}' state=absent
register: result
check_mode: yes
- name: it should not change
assert:
that:
- not result|skipped
- not result|changed
- name: When deleting a policy that does not exist
ecs_ecr: name='{{ ecr_name }}' region='{{ ec2_region }}' state=absent
register: result
- name: it should not change
assert:
that:
- not result|changed
always:
- name: Delete lingering ECR repository
ecs_ecr: name='{{ ecr_name }}' region='{{ ec2_region }}' state=absent