Odyssey
ecr.py
1 #!/usr/bin/env python
2 '''ECR Commands'''
3 
4 import itertools
5 import os
6 import sys
7 import boto3
8 import humanize
9 import shellish
10 from . import base
11 from . import cmd
12 from . import IMAGES
13 from . import REGISTRIES
14 from . import REPOSITORIES
15 from concurrent import futures
16 
17 AWS_REGIONS = [
18  'us-east-2',
19  'us-west-2',
20 ]
21 AWS_REGION = os.environ.get('AWS_REGION', 'us-east-2')
22 
23 
25  '''Manage ECR Images and Tags'''
26  name = 'ecr'
27 
28  def setup_args(self, parser):
29  self.add_subcommand(PushCommand)
30  self.add_subcommand(CatalogCommand)
31  self.add_subcommand(ListImagesCommand)
32  self.add_subcommand(RemoveTagCommand)
33 
34 
36  '''Push Current Images to AWS ECR'''
37 
38  name = 'push'
39 
40  def setup_args(self, paser):
41  self.add_argument('--image-tag', help='Override Image Tag')
42  self.add_argument('--verbose', '-v',
43  action='store_true',
44  help='Turn on verbose output')
45 
46  def run(self, args):
47  git_sha = os.environ.get('GIT_SHORT_REV')
48  image_tags = [('g-%s' % git_sha)]
49  if args.image_tag:
50  image_tags.append(args.image_tag)
51 
52  for region in AWS_REGIONS:
53  for tag in image_tags:
54  push_odyssey_images(tag, region, args.verbose)
55 
56 
58  '''Display List of Images and Tags in ECR'''
59 
60  name = 'catalog'
61 
62  def setup_args(self, parser):
63  self.add_argument('--region', help='Only list images from this region')
64 
65  def run(self, args):
66  display_image_catalog(args.region)
67 
68 
70  '''Display List of Images and Tags in ECR'''
71 
72  name = 'list'
73 
74  def setup_args(self, parser):
75  self.add_argument('--region', help='Only list images from this region')
76 
77  def run(self, args):
78  display_image_catalog(args.region)
79 
80 
82  '''Remove Docker tag from images in ECR'''
83 
84  name = 'rm'
85 
86  def setup_args(self, parser):
87  self.add_argument('tag_name', help='Name of tag to delete')
88  self.add_argument('--verbose', '-v', action='store_true',
89  help='Print verbose output')
90 
91  def confirm_action(self, message):
92  confirm = input(message)
93  if confirm != 'yes':
94  raise SystemExit('Aborted')
95 
96  def run(self, args):
97  self.confirm_action('Are you sure you want to remove image tag "%s" '
98  '[yes|NO]: ' % args.tag_name)
99 
100  remove_image_tag(args.tag_name, verbose=args.verbose)
101 
102 
103 def display_image_catalog(region=None):
104  regions = AWS_REGIONS
105  if region in AWS_REGIONS:
106  regions = [region]
107  for region in regions:
108  print_image_catalog_table(region)
109 
110 
111 def print_image_catalog_table(region):
112  catalog = build_catalog_table(region)
113  for repository in catalog.keys():
114  table_config = {
115  'headers': [
116  'Image Tags',
117  'Pushed At',
118  'Digest',
119  'Size',
120  ],
121  'columns': [
122  0.25,
123  0.1,
124  None,
125  0.1,
126  ],
127  'title': 'Images from %s/%s' % (region, repository)
128  }
129  t = shellish.Table(**table_config)
130  rows = []
131  for image in catalog[repository]:
132  rows.append((
133  ','.join(image['imageTags']),
134  humanize.naturaldate(image['imagePushedAt']),
135  image['imageDigest'],
136  humanize.naturalsize(image['imageSizeInBytes']),
137  ))
138  t.print(rows)
139  t.print_footer('')
140  t.close()
141 
142 
144  '''build table of images for each repository'''
145  table = {}
146  for repository in REPOSITORIES:
147  table[repository] = list(get_ecr_images(region, repository))
148 
149  return table
150 
151 
152 def get_ecr_images(region, repository_name):
153  def _get_ecr_images_(region, repository_name):
154  client = boto3.client('ecr', region_name=region)
155  paginator = client.get_paginator('describe_images')
156  pages = paginator.paginate(
157  repositoryName=repository_name,
158  filter={
159  'tagStatus': 'TAGGED'
160  }
161  )
162  for page in pages:
163  yield from page['imageDetails']
164 
165  return itertools.chain(_get_ecr_images_(region, repository_name))
166 
167 
168 def push_odyssey_images(tag, region, verbose):
169  registry = REGISTRIES[region]
170  cmd('$(aws --region %s ecr get-login --no-include-email)' % region,
171  verbose=verbose)
172  with futures.ThreadPoolExecutor(max_workers=10) as pool:
173  push_futures = []
174  for image, remote_image in IMAGES:
175  def fn(_image=image, _remote_image=remote_image):
176  ecr_tag = '%s/%s:%s' % (registry,
177  _remote_image,
178  tag)
179  cmd('docker tag', _image, ecr_tag, verbose=verbose)
180  try:
181  cmd('docker push', ecr_tag, verbose=verbose)
182  finally:
183  cmd('docker rmi', ecr_tag, verbose=verbose)
184  push_futures.append(pool.submit(fn))
185  for f in futures.as_completed(push_futures):
186  try:
187  f.result() # check for exceptions
188  except Exception as e:
189  raise SystemExit(str(e))
190 
191 
192 def remove_image_tag(tag_name, **kwargs):
193  def __remove_tag_from_ecr__(tag_name, region_name):
194  client = boto3.client('ecr', region_name=region_name)
195  for repository in REPOSITORIES:
196  response = client.batch_delete_image(
197  repositoryName=repository,
198  imageIds=[
199  {
200  'imageTag': tag_name,
201  }
202  ]
203  )
204  for image_id in response['imageIds']:
205  shellish.vtmlprint(
206  '<red>Deleted image: %s</red>' % (
207  image_id['imageTag']))
208  for failure in response['failures']:
209  shellish.vtmlprint(
210  '<red>Unable to delete: "%s": %s</red>' % (
211  failure['imageId']['imageTag'],
212  failure['failureReason']))
213 
214  verbose = kwargs.get('verbose', False)
215  with futures.ThreadPoolExecutor(max_workers=10) as pool:
216  push_futures = []
217 
218  for region in AWS_REGIONS:
219  push_futures.append(pool.submit(__remove_tag_from_ecr__,
220  tag_name,
221  region))
222  for f in futures.as_completed(push_futures):
223  try:
224  f.result()
225  except Exception as ex:
226  if verbose:
227  print(str(ex), file=sys.stderr)
def confirm_action(self, message)
Definition: ecr.py:91
def build_catalog_table(region)
Definition: ecr.py:143