Skip to content

ECR

Utilities for working with Amazon Elastic Container Registry.

Overview

Module Description
Core Core ECR utilities
Image Replicator Container image replication

ECRImage dataclass

ECRImage(
    account_id,
    region,
    repository_name,
    image_digest,
    image_manifest=None,
    client=None,
)

Bases: ECRMixins, DataClassModel

Source code in src/aibs_informatics_aws_utils/ecr/core.py
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
def __init__(
    self,
    account_id: str,
    region: str,
    repository_name: str,
    image_digest: str,
    image_manifest: Optional[str] = None,
    client: Optional[ECRClient] = None,
):
    super().__init__(account_id=account_id, region=region, client=client)
    self.repository_name = repository_name
    self.image_digest = image_digest
    if image_manifest is None:
        response = self.client.batch_get_image(
            repositoryName=self.repository_name,
            registryId=self.account_id,
            imageIds=[ImageIdentifierTypeDef(imageDigest=self.image_digest)],
        )
        if len(response["images"]) == 0 or "imageManifest" not in response["images"][0]:
            raise ResourceNotFoundError(f"Could not resolve image manifest for {self.uri}")

        self.image_manifest = response["images"][0]["imageManifest"]
    else:
        self.image_manifest = image_manifest

add_image_tags

add_image_tags(*image_tags)

Add tags to image.

Parameters:

Name Type Description Default
*image_tags str

Tags to add to image.

()
Source code in src/aibs_informatics_aws_utils/ecr/core.py
490
491
492
493
494
495
496
497
498
def add_image_tags(self, *image_tags: str):
    """Add tags to image.

    Args:
        *image_tags: Tags to add to image.
    """
    self.logger.info(f"Adding tags={image_tags} to {self.uri}")
    for tag in image_tags:
        self.put_image(image_tag=tag)

get_image_config

get_image_config()

Get ECR or docker image configuration json metadata.

Returns:

Type Description
Dict[str, Any]

Dictionary with image configuration.

Source code in src/aibs_informatics_aws_utils/ecr/core.py
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
def get_image_config(self) -> Dict[str, Any]:
    """Get ECR or docker image configuration json metadata.

    Returns:
        Dictionary with image configuration.
    """
    # get image_manifest (sha256: hash)
    config_digest = json.loads(self.image_manifest)["config"]["digest"]
    registry = ECRRegistry(account_id=self.account_id, region=self.region)
    ecr_login = registry.get_ecr_login()

    response = requests.get(
        url=f"https://{registry.uri}/v2/{self.repository_name}/blobs/{config_digest}",
        headers={"Authorization": f"Basic {ecr_login.auth_token}"},
    )
    response.raise_for_status()
    return response.json()

get_image_config_layer

get_image_config_layer()

Get the image config layer from image manifest.

The schema of the image manifest config layer is defined here: https://distribution.github.io/distribution/spec/manifest-v2-2/#image-manifest-field-descriptions

Note

While docker image manifests can have multiple formats, ECR only supports the schema defined in the link above, a v2 single image manifest. There is a manifest list, that describes multiple architectures, but ECR does not support this. This method assumes the image manifest is in the correct format.

Returns:

Type Description
LayerTypeDef

Layer Type dict of the config object.

Source code in src/aibs_informatics_aws_utils/ecr/core.py
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
def get_image_config_layer(self) -> LayerTypeDef:
    """Get the image config layer from image manifest.

    The schema of the image manifest config layer is defined here:
    https://distribution.github.io/distribution/spec/manifest-v2-2/#image-manifest-field-descriptions

    Note:
        While docker image manifests can have multiple formats, ECR only supports
        the schema defined in the link above, a v2 single image manifest. There is
        a manifest list, that describes multiple architectures, but ECR does not support
        this. This method assumes the image manifest is in the correct format.

    Returns:
        Layer Type dict of the config object.
    """
    image_manifest = json.loads(self.image_manifest)
    layer = image_manifest["config"]
    return LayerTypeDef(
        layerDigest=layer["digest"],
        layerAvailability="AVAILABLE",
        layerSize=layer["size"],
        mediaType=layer["mediaType"],
    )

get_image_detail

get_image_detail()

Get image detail of this image from ECR.

Returns:

Type Description
ImageDetailTypeDef

Image detail dictionary.

Source code in src/aibs_informatics_aws_utils/ecr/core.py
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
def get_image_detail(self) -> ImageDetailTypeDef:
    """Get image detail of this image from ECR.

    Returns:
        Image detail dictionary.
    """
    response = self.client.describe_images(
        repositoryName=self.repository_name,
        registryId=self.account_id,
        imageIds=[ImageIdentifierTypeDef(imageDigest=self.image_digest)],
    )
    image_details = response["imageDetails"]
    if len(image_details) == 0:
        raise ResourceNotFoundError(
            f"Could not resolve image detail for {self}"
        )  # pragma: no cover
    return image_details[0]

get_image_layers

get_image_layers()

Get layers from image manifest into ECR Layer objects.

The schema of the image manifest layers is defined here: https://distribution.github.io/distribution/spec/manifest-v2-2/#image-manifest-field-descriptions

Note

While docker image manifests can have multiple formats, ECR only supports the schema defined in the link above, a v2 single image manifest. There is a manifest list, that describes multiple architectures, but ECR does not support this. This method assumes the image manifest is in the correct format.

Returns:

Type Description
List[LayerTypeDef]

List of ECR Image layers.

Source code in src/aibs_informatics_aws_utils/ecr/core.py
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
def get_image_layers(self) -> List[LayerTypeDef]:
    """Get layers from image manifest into ECR Layer objects.

    The schema of the image manifest layers is defined here:
    https://distribution.github.io/distribution/spec/manifest-v2-2/#image-manifest-field-descriptions

    Note:
        While docker image manifests can have multiple formats, ECR only supports
        the schema defined in the link above, a v2 single image manifest. There is
        a manifest list, that describes multiple architectures, but ECR does not support
        this. This method assumes the image manifest is in the correct format.

    Returns:
        List of ECR Image layers.
    """
    image_manifest = json.loads(self.image_manifest)

    return [
        LayerTypeDef(
            layerDigest=layer["digest"],
            layerAvailability="AVAILABLE",
            layerSize=layer["size"],
            mediaType=layer["mediaType"],
        )
        for layer in image_manifest["layers"]
    ]

put_image

put_image(image_tag)

Make a call to put_image API to add image to ECR repository.

This method will add an image to the ECR repository. If the image already exists, it will not raise an error. Instead, it will log that the image already exists with the given tag.

Note

This operation does not push an image to the repository. It only adds the image manifest to the repository.

Parameters:

Name Type Description Default
image_tag Optional[str]

Tag to associate with image. If None, image is untagged.

required
Source code in src/aibs_informatics_aws_utils/ecr/core.py
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
def put_image(self, image_tag: Optional[str]):
    """Make a call to put_image API to add image to ECR repository.

    This method will add an image to the ECR repository. If the image already exists,
    it will not raise an error. Instead, it will log that the image already
    exists with the given tag.

    Note:
        This operation does not push an image to the repository. It only adds
        the image manifest to the repository.

    Args:
        image_tag: Tag to associate with image. If None, image is untagged.
    """
    try:
        if image_tag is None:
            self.client.put_image(
                registryId=self.account_id,
                repositoryName=self.repository_name,
                imageManifest=self.image_manifest,
                imageDigest=self.image_digest,
            )
        else:
            self.client.put_image(
                registryId=self.account_id,
                repositoryName=self.repository_name,
                imageManifest=self.image_manifest,
                imageDigest=self.image_digest,
                imageTag=image_tag,
            )
    except ClientError as e:
        # IF we receive a ImageAlreadyExistsException,
        # then we have nothing to worry about. Otherwise, raise error.
        if get_client_error_code(e) != "ImageAlreadyExistsException":
            raise e
        self.logger.info(f"Image already exists with tag={image_tag}")
    else:
        self.logger.info(f"Added new image with tag={image_tag}")

ECRImageReplicator

Bases: LoggingMixin

put_image

put_image(
    source_image,
    destination_repository,
    destination_image_tags=None,
)

Put image manifest and tags for an image.

This is the final step in copying an ECR image. It must be done once all the layers of the image have been uploaded.

If the put_image request fails with a 'LayersNotFoundException', this will attempt to upload the missing layers and retry the put_image request.

Parameters:

Name Type Description Default
source_image ECRImage

Source image that has been copied.

required
destination_repository ECRRepository

Destination repository.

required
destination_image_tags Optional[List[str]]

Destination image tags. Optional. If not provided, source image's tags are added.

None

Returns:

Type Description
ECRImage

Destination ECR Image.

Source code in src/aibs_informatics_aws_utils/ecr/image_replicator.py
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
def put_image(
    self,
    source_image: ECRImage,
    destination_repository: ECRRepository,
    destination_image_tags: Optional[List[str]] = None,
) -> ECRImage:
    """Put image manifest and tags for an image.

    This is the final step in copying an ECR image. It must be done
    once all the layers of the image have been uploaded.

    If the put_image request fails with a 'LayersNotFoundException',
    this will attempt to upload the missing layers and retry
    the put_image request.

    Args:
        source_image: Source image that has been copied.
        destination_repository: Destination repository.
        destination_image_tags: Destination image tags. Optional.
            If not provided, source image's tags are added.

    Returns:
        Destination ECR Image.
    """
    tags = (
        source_image.image_tags if destination_image_tags is None else destination_image_tags
    )

    self.logger.info(f"putting image to {destination_repository.uri} with {tags} tags")
    dest_image = ECRImage(
        account_id=destination_repository.account_id,
        region=destination_repository.region,
        repository_name=destination_repository.repository_name,
        image_digest=source_image.image_digest,
        image_manifest=source_image.image_manifest,
        client=destination_repository.client,
    )
    dest_image.client = destination_repository.client
    try:
        dest_image.put_image(None)
        if tags:
            dest_image.add_image_tags(*tags)

    except ClientError as e:
        # IF we have a LayersNotFoundException, then we will retry to upload layers
        if get_client_error_code(e) != "LayersNotFoundException":
            self.log_stacktrace(
                f"Error putting image to {destination_repository.uri} with {tags}",
                e,
            )
            raise e
        self.logger.warning(
            f"Received 'LayerNotFoundException' while putting image. "
            f"Resolving missing layers of {source_image} specified in {e}"
        )
        missing_layers = self._get_missing_layers(
            client=source_image.client,
            repository_name=source_image.repository_name,
            put_image_error=e,
        )
        self.logger.info(f"Identified {len(missing_layers)} missing layers. Uploading layers")
        self._upload_layers(
            source_repository=source_image.get_repository(),
            destination_repository=destination_repository,
            layers=missing_layers,
            check_if_exists=False,
        )

        self.logger.info("uploaded all missing layers, retrying put_image request")
        self.put_image(source_image, destination_repository, destination_image_tags)
    return dest_image

replicate

replicate(
    source_image,
    destination_repository,
    destination_image_tags=None,
)

Copies the source image into the destination repository.

This allows the user to facilitate replication:

  • between AWS accounts
  • to a new repository
  • with new tags
Note

This has exactly the same effect as docker pull; docker tag; docker push, but is done with the AWS ECR SDK so we can run this in a lambda when our stack updates.

Parameters:

Name Type Description Default
source_image ECRImage

Configuration of image to pull.

required
destination_repository ECRRepository

Repository to push to.

required
destination_image_tags Optional[List[str]]

Optional list of tags to apply to the destination image.

None

Returns:

Type Description
ECRImage

Configuration of destination image pushed.

Source code in src/aibs_informatics_aws_utils/ecr/image_replicator.py
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
def replicate(
    self,
    source_image: ECRImage,
    destination_repository: ECRRepository,
    destination_image_tags: Optional[List[str]] = None,
) -> ECRImage:
    """Copies the source image into the destination repository.

    This allows the user to facilitate replication:

    - **between AWS accounts**
    - **to a new repository**
    - **with new tags**

    Note:
        This has exactly the same effect as `docker pull; docker tag; docker push`,
        but is done with the AWS ECR SDK so we can run this in a lambda when our
        stack updates.

    Args:
        source_image: Configuration of image to pull.
        destination_repository: Repository to push to.
        destination_image_tags: Optional list of tags to apply to the destination image.

    Returns:
        Configuration of destination image pushed.
    """
    self.log.info(
        f"Starting Image Replication "
        f"[source={source_image.uri}, destination={destination_repository.uri}] "
        f"with tags={destination_image_tags}"
    )

    self.upload_layers(
        source_image=source_image, destination_repository=destination_repository
    )

    self.logger.info(f"Putting image to {destination_repository.uri}")

    self.put_image(
        source_image=source_image,
        destination_repository=destination_repository,
        destination_image_tags=destination_image_tags,
    )
    self.logger.info(f"Completed putting image to {destination_repository.uri}.")

    return ECRImage(
        account_id=destination_repository.account_id,
        region=destination_repository.region,
        repository_name=destination_repository.repository_name,
        image_digest=source_image.image_digest,
        client=destination_repository.client,
    )

upload_layers

upload_layers(source_image, destination_repository)

Upload image layers of the source image to the destination repository.

Parameters:

Name Type Description Default
source_image ECRImage

Source image that has been copied.

required
destination_repository ECRRepository

Destination repository.

required
Source code in src/aibs_informatics_aws_utils/ecr/image_replicator.py
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
def upload_layers(self, source_image: ECRImage, destination_repository: ECRRepository):
    """Upload image layers of the source image to the destination repository.

    Args:
        source_image: Source image that has been copied.
        destination_repository: Destination repository.
    """
    self.logger.info(
        f"Uploading layers from {source_image.uri} to {destination_repository.uri}."
    )

    layers = source_image.get_image_layers()
    config_layer = source_image.get_image_config_layer()

    all_layers = layers + [config_layer]

    self.logger.info(
        f"Source image {source_image.uri} has {len(layers)} layers and "
        f"1 config layer totalling {len(all_layers)} layers"
    )

    self._upload_layers(
        source_repository=source_image.get_repository(),
        destination_repository=destination_repository,
        layers=all_layers,
        check_if_exists=True,
    )

ECRImageUri

Bases: ECRRepositoryUri

from_components classmethod

from_components(
    repository_name,
    image_tag=None,
    image_digest=None,
    account_id=None,
    region=None,
)

Generate an Image URI.

If account ID not provided, account Id of credentials is used. If region is not provided, region of credentials is used.

Parameters:

Name Type Description Default
repository_name str

Name of ECR repository.

required
image_tag Optional[str]

Tag associated with image. Defaults to None.

None
image_digest Optional[str]

The image digest. Defaults to None.

None
account_id Optional[str]

The registry ID. Defaults to None.

None
region Optional[str]

AWS region. Defaults to None.

None

Raises:

Type Description
ValueError

If both or neither image tag / image digest are provided.

Returns:

Type Description
ECRImageUri

ECR Image URI.

Source code in src/aibs_informatics_aws_utils/ecr/core.py
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
@classmethod
def from_components(  # type: ignore[override]
    cls,
    repository_name: str,
    image_tag: Optional[str] = None,
    image_digest: Optional[str] = None,
    account_id: Optional[str] = None,
    region: Optional[str] = None,
) -> "ECRImageUri":
    """Generate an Image URI.

    If account ID not provided, account Id of credentials is used.
    If region is not provided, region of credentials is used.

    Args:
        repository_name: Name of ECR repository.
        image_tag: Tag associated with image. Defaults to None.
        image_digest: The image digest. Defaults to None.
        account_id: The registry ID. Defaults to None.
        region: AWS region. Defaults to None.

    Raises:
        ValueError: If both or neither image tag / image digest are provided.

    Returns:
        ECR Image URI.
    """
    if (image_tag and image_digest) or (not image_tag and not image_digest):
        raise ValueError(
            "Must provide EITHER image tag OR image digest. "
            f"image_tag={image_tag}, image_digest={image_digest}"
        )
    repo_uri = ECRRepositoryUri.from_components(
        repository_name=repository_name, account_id=account_id, region=region
    )
    image_id = f"{'@' if image_digest else ':'}{image_digest or image_tag}"
    return ECRImageUri(f"{repo_uri}{image_id}")

ECRRegistry dataclass

ECRRegistry(account_id, region, client=None)

Bases: ECRResource

Source code in src/aibs_informatics_aws_utils/ecr/core.py
322
323
324
325
def __init__(self, account_id: str, region: str, client: Optional[ECRClient] = None):
    self.account_id = account_id
    self.region = region
    self._client = client

get_repositories

get_repositories(
    repository_name=None, repository_tags=None
)

Filter repositories based on resource tags specified.

Parameters:

Name Type Description Default
repository_name Optional[Union[str, Pattern]]

Repository name or pattern to filter by.

None
repository_tags Optional[List[ResourceTag]]

List of resource tags to filter by.

None

Returns:

Type Description
List[ECRRepository]

Filtered list of repositories with resource tags.

Source code in src/aibs_informatics_aws_utils/ecr/core.py
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
def get_repositories(
    self,
    repository_name: Optional[Union[str, re.Pattern]] = None,
    repository_tags: Optional[List[ResourceTag]] = None,
) -> List[ECRRepository]:
    """Filter repositories based on resource tags specified.

    Args:
        repository_name: Repository name or pattern to filter by.
        repository_tags: List of resource tags to filter by.

    Returns:
        Filtered list of repositories with resource tags.
    """
    repositories = self.list_repositories()
    filtered_repos = []
    for repo in repositories:
        if repository_tags:
            repo_tags = repo.get_resource_tags()
            if not all([filter_tag in repo_tags for filter_tag in repository_tags]):
                continue
        if repository_name:
            if isinstance(repository_name, re.Pattern):
                if not repository_name.match(repo.repository_name):
                    continue
            elif repository_name not in repo.repository_name:
                continue
        filtered_repos.append(repo)
    return filtered_repos

list_repositories

list_repositories()

List all repositories in the Registry.

Returns:

Type Description
List[ECRRepository]

List of repositories in the registry.

Source code in src/aibs_informatics_aws_utils/ecr/core.py
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
def list_repositories(self) -> List[ECRRepository]:
    """List all repositories in the Registry.

    Returns:
        List of repositories in the registry.
    """

    paginator = self.client.get_paginator("describe_repositories")
    repositories: List[ECRRepository] = []
    for describe_repos_response in paginator.paginate(registryId=self.account_id):
        for repository in describe_repos_response["repositories"]:
            assert "repositoryName" in repository
            repositories.append(
                ECRRepository(
                    account_id=self.account_id,
                    region=self.region,
                    repository_name=repository["repositoryName"],
                )
            )
    return repositories

ECRRegistryUri

Bases: ValidatedStr

from_components classmethod

from_components(account_id=None, region=None, **kwargs)

Generate a Registry URI.

If account ID not provided, account Id of credentials is used. If region is not provided, region of credentials is used.

Parameters:

Name Type Description Default
account_id Optional[str]

The registry ID. Defaults to None.

None
region Optional[str]

AWS region. Defaults to None.

None
**kwargs

Additional keyword arguments (unused).

{}

Returns:

Type Description
ECRRegistryUri

AWS Registry URI.

Source code in src/aibs_informatics_aws_utils/ecr/core.py
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
@classmethod
def from_components(
    cls, account_id: Optional[str] = None, region: Optional[str] = None, **kwargs
) -> "ECRRegistryUri":
    """Generate a Registry URI.

    If account ID not provided, account Id of credentials is used.
    If region is not provided, region of credentials is used.

    Args:
        account_id: The registry ID. Defaults to None.
        region: AWS region. Defaults to None.
        **kwargs: Additional keyword arguments (unused).

    Returns:
        AWS Registry URI.
    """
    account_id = account_id or get_account_id()
    region = get_region(region)
    return ECRRegistryUri(f"{account_id}.dkr.ecr.{region}.amazonaws.com")

ECRRepository dataclass

ECRRepository(
    account_id, region, repository_name, client=None
)

Bases: ECRResource

Source code in src/aibs_informatics_aws_utils/ecr/core.py
629
630
631
632
633
634
635
636
637
def __init__(
    self,
    account_id: str,
    region: str,
    repository_name: str,
    client: Optional[ECRClient] = None,
):
    super().__init__(account_id=account_id, region=region, client=client)
    self.repository_name = repository_name

create

create(
    tags=None,
    image_tag_mutability="MUTABLE",
    exists_ok=True,
)

Create an ECR Repository.

Parameters:

Name Type Description Default
tags Optional[List[ResourceTag]]

List of repo tags to add. Defaults to None.

None
image_tag_mutability ImageTagMutabilityType

Whether image tag is immutable. Defaults to "MUTABLE".

'MUTABLE'
exists_ok bool

Suppress error if repository already exists. Defaults to True.

True
Source code in src/aibs_informatics_aws_utils/ecr/core.py
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
def create(
    self,
    tags: Optional[List[ResourceTag]] = None,
    image_tag_mutability: ImageTagMutabilityType = "MUTABLE",
    exists_ok: bool = True,
):
    """Create an ECR Repository.

    Args:
        tags: List of repo tags to add. Defaults to None.
        image_tag_mutability: Whether image tag is immutable. Defaults to "MUTABLE".
        exists_ok: Suppress error if repository already exists. Defaults to True.
    """
    if tags is None:
        tags = []

    try:
        self.client.create_repository(
            registryId=self.account_id,
            repositoryName=self.repository_name,
            tags=tags,
            imageTagMutability=image_tag_mutability,
        )
    except ClientError as e:
        # If the repo already exists, just move on.
        if get_client_error_code(e) == "RepositoryAlreadyExistsException" and exists_ok:
            # update tags
            if tags:
                self.update_resource_tags(*tags)
        else:
            raise e

delete

delete(force)

Delete the ECR repository described by this instance.

Parameters:

Name Type Description Default
force bool

Ignore if images in repository.

required
Source code in src/aibs_informatics_aws_utils/ecr/core.py
708
709
710
711
712
713
714
715
716
def delete(self, force: bool):
    """Delete the ECR repository described by this instance.

    Args:
        force: Ignore if images in repository.
    """
    self.client.delete_repository(
        registryId=self.account_id, repositoryName=self.repository_name, force=force
    )

exists

exists()

Check if repository exists.

Returns:

Type Description
bool

True if repository exists.

Source code in src/aibs_informatics_aws_utils/ecr/core.py
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
def exists(self) -> bool:
    """Check if repository exists.

    Returns:
        True if repository exists.
    """
    try:
        self.client.describe_repositories(
            registryId=self.account_id, repositoryNames=[self.repository_name]
        )
    except ClientError as e:
        if get_client_error_code(e) == "RepositoryNotFoundException":
            return False
        else:
            raise e
    else:
        return True

get_image

get_image(image_tag=None, image_digest=None)

Get the image associated with the following tag or digest.

Parameters:

Name Type Description Default
image_tag Optional[str]

Image tag. Defaults to None.

None
image_digest Optional[str]

Image digest. Defaults to None.

None

Returns:

Type Description
ECRImage

The image with the image tag or digest.

Source code in src/aibs_informatics_aws_utils/ecr/core.py
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
def get_image(
    self, image_tag: Optional[str] = None, image_digest: Optional[str] = None
) -> "ECRImage":
    """Get the image associated with the following tag or digest.

    Args:
        image_tag: Image tag. Defaults to None.
        image_digest: Image digest. Defaults to None.

    Returns:
        The image with the image tag or digest.
    """
    if (image_tag is None) == (image_digest is None):
        raise ValueError(
            f"Must provide image tag XOR digest. "
            f"provided image_tag={image_tag}, image_digest={image_digest}"
        )
    images = self.get_images()
    for image in images:
        if image_tag and image_tag in image.image_tags:
            return image
        elif image_digest and image_digest == image.image_digest:
            return image
    else:
        raise ResourceNotFoundError(
            f"Could not find an image in {self.uri} with tag={image_tag}"
        )

get_images

get_images(tag_status='ANY')

Fetches all images in a given repository.

Parameters:

Name Type Description Default
tag_status TagStatusType

Filter non-tagged images. Defaults to "ANY".

'ANY'

Returns:

Type Description
List[ECRImage]

List of images in repository.

Source code in src/aibs_informatics_aws_utils/ecr/core.py
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
def get_images(self, tag_status: TagStatusType = "ANY") -> List["ECRImage"]:
    """Fetches all images in a given repository.

    Args:
        tag_status: Filter non-tagged images. Defaults to "ANY".

    Returns:
        List of images in repository.
    """

    # To fetch necessary image info, we need to make two calls:
    #   1. Call the `list_images` API which will return imageDigest/imageTag.
    #       - This is a lightweight response which gives us digests
    #   2. Call the `batch_get_image` API which returns imageManifest info.
    #       - this is consolidated all in one go

    # Call 1: list_images
    paginator = self.client.get_paginator("list_images")
    list_image_request = ListImagesRequestPaginateTypeDef(
        repositoryName=self.repository_name,
        filter=ListImagesFilterTypeDef(tagStatus=tag_status),
    )
    image_digests: List[str] = sorted(
        list(
            {
                image_id["imageDigest"]
                for list_images_response in paginator.paginate(**list_image_request)
                for image_id in list_images_response["imageIds"]
                if "imageDigest" in image_id
            }
        )
    )
    if len(image_digests) == 0:
        return []

    # Call 2: batch_get_image
    response: BatchGetImageResponseTypeDef = self.client.batch_get_image(
        repositoryName=self.repository_name,
        registryId=self.account_id,
        imageIds=[ImageIdentifierTypeDef(imageDigest=digest) for digest in image_digests],
    )

    # Next we consolidate the results, ensuring that the image manifests
    # are all the same. If an image digest has differing manifests,
    # we should throw an error.
    digest_to_manifest_map: Dict[str, str] = {}
    for image in response["images"]:
        image_digest = image["imageId"]["imageDigest"]  # type: ignore
        image_manifest = image["imageManifest"]  # type: ignore
        if image_digest in digest_to_manifest_map:
            if image_manifest != digest_to_manifest_map[image_digest]:
                raise ValueError(
                    f"Not all image manifests are equivalent for {image_digest} in {self.uri}"
                )
        else:
            digest_to_manifest_map[image_digest] = image_manifest

    return [
        ECRImage(
            account_id=self.account_id,
            region=self.region,
            repository_name=self.repository_name,
            image_digest=image_digest,
            image_manifest=image_manifest,
        )
        for image_digest, image_manifest in digest_to_manifest_map.items()
    ]

ECRRepositoryUri

Bases: ECRRegistryUri

from_components classmethod

from_components(
    repository_name, account_id=None, region=None
)

Generate a Repository URI.

If account ID not provided, account Id of credentials is used. If region is not provided, region of credentials is used.

Parameters:

Name Type Description Default
repository_name str

Name of ECR repository.

required
account_id Optional[str]

The registry ID. Defaults to None.

None
region Optional[str]

AWS region. Defaults to None.

None

Returns:

Type Description
ECRRepositoryUri

AWS Repository URI.

Source code in src/aibs_informatics_aws_utils/ecr/core.py
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
@classmethod
def from_components(  # type: ignore[override]
    cls,
    repository_name: str,
    account_id: Optional[str] = None,
    region: Optional[str] = None,
) -> "ECRRepositoryUri":
    """Generate a Repository URI.

    If account ID not provided, account Id of credentials is used.
    If region is not provided, region of credentials is used.

    Args:
        repository_name: Name of ECR repository.
        account_id: The registry ID. Defaults to None.
        region: AWS region. Defaults to None.

    Returns:
        AWS Repository URI.
    """
    registry_uri = ECRRegistryUri.from_components(account_id=account_id, region=region)
    return ECRRepositoryUri(f"{registry_uri}/{repository_name}")

ECRResource dataclass

ECRResource(account_id, region, client=None)

Bases: ECRMixins, DataClassModel

Source code in src/aibs_informatics_aws_utils/ecr/core.py
322
323
324
325
def __init__(self, account_id: str, region: str, client: Optional[ECRClient] = None):
    self.account_id = account_id
    self.region = region
    self._client = client

get_resource_tags

get_resource_tags()

Gets the tags for this ECR Resource.

Returns:

Type Description
List[ResourceTag]

List of tags.

Source code in src/aibs_informatics_aws_utils/ecr/core.py
586
587
588
589
590
591
592
593
594
595
596
def get_resource_tags(self) -> List[ResourceTag]:
    """Gets the tags for this ECR Resource.

    Returns:
        List of tags.
    """
    return [
        ResourceTag(Key=tag["Key"], Value=tag["Value"])
        for tag in self.client.list_tags_for_resource(resourceArn=self.arn)["tags"]
        if "Key" in tag and "Value" in tag
    ]

update_resource_tags

update_resource_tags(
    *tags, mode=cast(TagMode, TagMode.APPEND)
)

Updates the tags for an ECR Resource.

An update can either append or overwrite the existing tags.

Parameters:

Name Type Description Default
*tags ResourceTag

Resource tags to update.

()
mode TagMode

Either append or overwrite tags of resource. Defaults to TagMode.APPEND.

cast(TagMode, APPEND)
Source code in src/aibs_informatics_aws_utils/ecr/core.py
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
def update_resource_tags(
    self, *tags: ResourceTag, mode: TagMode = cast(TagMode, TagMode.APPEND)
):
    """Updates the tags for an ECR Resource.

    An update can either append or overwrite the existing tags.

    Args:
        *tags: Resource tags to update.
        mode: Either append or overwrite tags of resource.
            Defaults to TagMode.APPEND.
    """
    tag_dict = dict([(tag["Key"], tag["Value"]) for tag in tags])
    if mode == TagMode.OVERWRITE:
        existing_tags = self.get_resource_tags()
        tag_keys_to_remove = [
            existing_tag["Key"]
            for existing_tag in existing_tags
            if existing_tag["Key"] not in tag_dict
        ]
        self.client.untag_resource(resourceArn=self.arn, tagKeys=tag_keys_to_remove)
    self.client.tag_resource(
        resourceArn=self.arn,
        tags=[TagTypeDef(Key=key, Value=value) for key, value in tag_dict.items()],
    )

resolve_image_uri

resolve_image_uri(name, default_tag=None)

Resolve full image URI from input name.

Parameters:

Name Type Description Default
name str

Partial or fully qualified uri, name of image or repository.

required
default_tag Optional[str]

Default tag to use if not specified. Defaults to None.

None

Returns:

Type Description
str

Fully qualified image URI.

Source code in src/aibs_informatics_aws_utils/ecr/core.py
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
def resolve_image_uri(name: str, default_tag: Optional[str] = None) -> str:
    """Resolve full image URI from input name.

    Args:
        name: Partial or fully qualified uri, name of image or repository.
        default_tag: Default tag to use if not specified. Defaults to None.

    Returns:
        Fully qualified image URI.
    """

    try:
        uri = name

        if not ECRRegistryUri.is_prefixed(uri):
            uri = f"{ECRRegistryUri.from_components()}/{uri}"

        if ECRImageUri.is_valid(uri):
            return uri
        elif ECRRepositoryUri.is_valid(uri):
            repo = ECRRepository.from_uri(uri)

            if default_tag:
                image = repo.get_image(image_tag=default_tag)
                return image.uri
            else:
                # Fetch latest tagged
                def get_image_push_time(image: ECRImage) -> datetime:
                    if image.image_pushed_at is None:
                        raise RuntimeError(f"Couldn't get 'image_pushed_at' for: {image}")
                    return image.image_pushed_at

                images = sorted(repo.get_images("TAGGED"), key=get_image_push_time)
                return images[-1].uri
        else:
            raise ValueError(f"Could not resolve full URI for image {uri} (raw={name})")
    except Exception as e:
        msg = f"Couldn't resolve ECR image URI from {name} with error: {e}"
        logger.exception(msg)
        raise ResourceNotFoundError(msg) from e