diff --git a/.doc_gen/metadata/ecr_metadata.yaml b/.doc_gen/metadata/ecr_metadata.yaml index 8604aa43d0f..bf19cf32e3b 100644 --- a/.doc_gen/metadata/ecr_metadata.yaml +++ b/.doc_gen/metadata/ecr_metadata.yaml @@ -23,6 +23,14 @@ ecr_Hello: - description: snippet_tags: - ecr.java2_hello.main + Python: + versions: + - sdk_version: 3 + github: python/example_code/ecr + excerpts: + - description: + snippet_tags: + - python.example_code.ecr.Hello services: ecr: {listImages} ecr_PushImageCmd: @@ -67,6 +75,18 @@ ecr_SetRepositoryPolicy: - description: snippet_tags: - ecr.java2.set.repo.policy.main + Python: + versions: + - sdk_version: 3 + github: python/example_code/ecr + excerpts: + - description: + snippet_tags: + - python.example_code.ecr.ECRWrapper.decl + - python.example_code.ecr.ECRWrapper.SetRepositoryPolicy + - description: Example that grants an IAM role download access. + snippet_tags: + - python.example_code.ecr.grant_role_download_access services: ecr: {SetRepositoryPolicy} ecr_GetRepositoryPolicy: @@ -89,6 +109,15 @@ ecr_GetRepositoryPolicy: - description: snippet_tags: - ecr.java2.get.repo.policy.main + Python: + versions: + - sdk_version: 3 + github: python/example_code/ecr + excerpts: + - description: + snippet_tags: + - python.example_code.ecr.ECRWrapper.decl + - python.example_code.ecr.ECRWrapper.GetRepositoryPolicy services: ecr: {GetRepositoryPolicy} ecr_GetAuthorizationToken: @@ -111,6 +140,15 @@ ecr_GetAuthorizationToken: - description: snippet_tags: - ecr.java2.get.token.main + Python: + versions: + - sdk_version: 3 + github: python/example_code/ecr + excerpts: + - description: + snippet_tags: + - python.example_code.ecr.ECRWrapper.decl + - python.example_code.ecr.ECRWrapper.GetAuthorizationToken services: ecr: {GetAuthorizationToken} ecr_StartLifecyclePolicyPreview: @@ -135,6 +173,22 @@ ecr_StartLifecyclePolicyPreview: - ecr.java2.verify.image.main services: ecr: {StartLifecyclePolicyPreview} +ecr_PutLifeCyclePolicy: + languages: + Python: + versions: + - sdk_version: 3 + github: python/example_code/ecr + excerpts: + - description: + snippet_tags: + - python.example_code.ecr.ECRWrapper.decl + - python.example_code.ecr.ECRWrapper.PutLifeCyclePolicy + - description: Example that puts an expiration date policy. + snippet_tags: + - python.example_code.ecr.put_expiration_policy + services: + ecr: {PutLifeCyclePolicy} ecr_DescribeImages: languages: Kotlin: @@ -155,6 +209,15 @@ ecr_DescribeImages: - description: snippet_tags: - ecr.java2.verify.image.main + Python: + versions: + - sdk_version: 3 + github: python/example_code/ecr + excerpts: + - description: + snippet_tags: + - python.example_code.ecr.ECRWrapper.decl + - python.example_code.ecr.ECRWrapper.DescribeImages services: ecr: {DescribeImages} ecr_DeleteRepository: @@ -177,6 +240,15 @@ ecr_DeleteRepository: - description: snippet_tags: - ecr.java2.delete.repo.main + Python: + versions: + - sdk_version: 3 + github: python/example_code/ecr + excerpts: + - description: + snippet_tags: + - python.example_code.ecr.ECRWrapper.decl + - python.example_code.ecr.ECRWrapper.DeleteRepository services: ecr: {DeleteRepository} ecr_CreateRepository: @@ -199,6 +271,15 @@ ecr_CreateRepository: - description: snippet_tags: - ecr.java2.create.repo.main + Python: + versions: + - sdk_version: 3 + github: python/example_code/ecr + excerpts: + - description: + snippet_tags: + - python.example_code.ecr.ECRWrapper.decl + - python.example_code.ecr.ECRWrapper.CreateRepository services: ecr: {CreateRepository} ecr_DescribeRepositories: @@ -230,6 +311,15 @@ ecr_DescribeRepositories: - description: snippet_tags: - ecr.rust.describe-repos + Python: + versions: + - sdk_version: 3 + github: python/example_code/ecr + excerpts: + - description: + snippet_tags: + - python.example_code.ecr.ECRWrapper.decl + - python.example_code.ecr.ECRWrapper.DescribeRepositories services: ecr: {DescribeRepositories} ecr_ListImages: @@ -282,5 +372,19 @@ ecr_Scenario_RepositoryManagement: - description: A wrapper class for &ECR; SDK methods. snippet_tags: - ecr.java2_scenario.main + Python: + versions: + - sdk_version: 3 + github: python/example_code/ecr + sdkguide: + excerpts: + - description: Run an interactive scenario at a command prompt. + genai: some + snippet_tags: + - python.example_code.ecr.BasicsScenario + - description: ECRWrapper class that wraps &ECR; actions. + genai: some + snippet_tags: + - python.example_code.ecr.ECRWrapper.class services: ecr: {CreateRepository, DescribeRepositories, DeleteRepository, DescribeImages, StartLifecyclePolicyPreview, GetAuthorizationToken, GetRepositoryPolicy, SetRepositoryPolicy} diff --git a/python/example_code/ecr/README.md b/python/example_code/ecr/README.md new file mode 100644 index 00000000000..3d4cf16a97c --- /dev/null +++ b/python/example_code/ecr/README.md @@ -0,0 +1,136 @@ +# Amazon ECR code examples for the SDK for Python + +## Overview + +Shows how to use the AWS SDK for Python (Boto3) to work with Amazon Elastic Container Registry (Amazon ECR). + + + + +_Amazon ECR is a fully managed Docker container registry that makes it easy for developers to store, manage, and deploy Docker container images._ + +## ⚠ Important + +* Running this code might result in charges to your AWS account. For more details, see [AWS Pricing](https://aws.amazon.com/pricing/) and [Free Tier](https://aws.amazon.com/free/). +* Running the tests might result in charges to your AWS account. +* We recommend that you grant your code least privilege. At most, grant only the minimum permissions required to perform the task. For more information, see [Grant least privilege](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege). +* This code is not tested in every AWS Region. For more information, see [AWS Regional Services](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services). + + + + +## Code examples + +### Prerequisites + +For prerequisites, see the [README](../../README.md#Prerequisites) in the `python` folder. + +Install the packages required by these examples by running the following in a virtual environment: + +``` +python -m pip install -r requirements.txt +``` + + + + +### Get started + +- [Hello Amazon ECR](hello/hello_ecr.py#L4) (`listImages`) + + +### Basics + +Code examples that show you how to perform the essential operations within a service. + +- [Learn the basics](ecr_getting_started.py) + + +### Single actions + +Code excerpts that show you how to call individual service functions. + +- [CreateRepository](ecr_wrapper.py#L38) +- [DeleteRepository](ecr_wrapper.py#L66) +- [DescribeImages](ecr_wrapper.py#L207) +- [DescribeRepositories](ecr_wrapper.py#L161) +- [GetAuthorizationToken](ecr_wrapper.py#L142) +- [GetRepositoryPolicy](ecr_wrapper.py#L115) +- [PutLifeCyclePolicy](ecr_wrapper.py#L183) +- [SetRepositoryPolicy](ecr_wrapper.py#L88) + + + + + +## Run the examples + +### Instructions + + + + + +#### Hello Amazon ECR + +This example shows you how to get started using Amazon ECR. + +``` +python hello/hello_ecr.py +``` + +#### Learn the basics + +This example shows you how to do the following: + +- Create an Amazon ECR repository. +- Set repository policies. +- Retrieve repository URIs. +- Get Amazon ECR authorization tokens. +- Set lifecycle policies for Amazon ECR repositories. +- Push a Docker image to an Amazon ECR repository. +- Verify the existence of an image in an Amazon ECR repository. +- List Amazon ECR repositories for your account and get details about them. +- Delete Amazon ECR repositories. + + + + +Start the example by running the following at a command prompt: + +``` +python ecr_getting_started.py +``` + + + + + + +### Tests + +⚠ Running tests might result in charges to your AWS account. + + +To find instructions for running these tests, see the [README](../../README.md#Tests) +in the `python` folder. + + + + + + +## Additional resources + +- [Amazon ECR User Guide](https://docs.aws.amazon.com/AmazonECR/latest/userguide/what-is-ecr.html) +- [Amazon ECR API Reference](https://docs.aws.amazon.com/AmazonECR/latest/APIReference/Welcome.html) +- [SDK for Python Amazon ECR reference](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/scheduler.html) + + + + +--- + +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 diff --git a/python/example_code/ecr/docker_files/Dockerfile b/python/example_code/ecr/docker_files/Dockerfile new file mode 100644 index 00000000000..000282a3aa3 --- /dev/null +++ b/python/example_code/ecr/docker_files/Dockerfile @@ -0,0 +1,14 @@ +# Use the official Alpine Linux image as the base image +FROM alpine:latest + +# Set the working directory to /app +WORKDIR /app + +# Copy the "hello.sh" script into the container +COPY hello.sh . + +# Make the "hello.sh" script executable +RUN chmod +x hello.sh + +# Define the command to run the "hello.sh" script +CMD ["./hello.sh"] \ No newline at end of file diff --git a/python/example_code/ecr/docker_files/hello.sh b/python/example_code/ecr/docker_files/hello.sh new file mode 100644 index 00000000000..0df1913c98c --- /dev/null +++ b/python/example_code/ecr/docker_files/hello.sh @@ -0,0 +1,2 @@ +#!/bin/sh +echo "Hello There!" \ No newline at end of file diff --git a/python/example_code/ecr/ecr_getting_started.py b/python/example_code/ecr/ecr_getting_started.py new file mode 100644 index 00000000000..e3e6e6b12ea --- /dev/null +++ b/python/example_code/ecr/ecr_getting_started.py @@ -0,0 +1,410 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Purpose + +Shows how to use the AWS SDK for Python (Boto3) with Amazon Elastic Container Registry (Amazon ECR) to perform +basic operations. + +To demonstrate granting permissions with a policy, an AWS Identity and Access Management (IAM) role Amazon Resource Name (ARN) +can be passed as a script argument. + +To create an IAM role, see: + +https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create.html +""" + +import argparse +import base64 +import json +import logging +import os +import sys + +import docker + +script_dir = os.path.dirname(os.path.abspath(__file__)) + +# Add relative path to include ecr_wrapper. +sys.path.append(os.path.dirname(script_dir)) +from ecr_wrapper import ECRWrapper + +# Add relative path to include demo_tools in this code example without need for setup. +sys.path.append(os.path.join(script_dir, "../..")) +import demo_tools.question as q + +logger = logging.getLogger(__name__) + +no_art = False # 'no_art' suppresses 'art' to improve accessibility. + + +def print_dashes(): + """ + Print a line of dashes to separate sections of the output. + """ + if not no_art: + print("-" * 80) + + +use_press_enter_to_continue = True + + +def press_enter_to_continue(): + if use_press_enter_to_continue: + q.ask("Press Enter to continue...") + + +# snippet-start:[python.example_code.ecr.BasicsScenario] +class ECRGettingStarted: + """ + A scenario that demonstrates how to use Boto3 to perform basic operations using + Amazon ECR. + """ + + def __init__( + self, + ecr_wrapper: ECRWrapper, + docker_client: docker.DockerClient, + ): + self.ecr_wrapper = ecr_wrapper + self.docker_client = docker_client + self.tag = "echo-text" + self.repository_name = "ecr-basics" + self.docker_image = None + self.full_tag_name = None + self.repository = None + + def run(self, role_arn: str) -> None: + """ + Runs the scenario. + """ + print( + """ +The Amazon Elastic Container Registry (ECR) is a fully-managed Docker container registry +service provided by AWS. It allows developers and organizations to securely +store, manage, and deploy Docker container images. +ECR provides a simple and scalable way to manage container images throughout their lifecycle, +from building and testing to production deployment. + +The `ECRWrapper' class is a wrapper for the Boto3 'ecr' client. The 'ecr' client provides a set of methods to +programmatically interact with the Amazon ECR service. This allows developers to +automate the storage, retrieval, and management of container images as part of their application +deployment pipelines. With ECR, teams can focus on building and deploying their +applications without having to worry about the underlying infrastructure required to +host and manage a container registry. + +This scenario walks you through how to perform key operations for this service. +Let's get started... + """ + ) + press_enter_to_continue() + print_dashes() + print( + f""" +* Create an ECR repository. + +An ECR repository is a private Docker container repository provided +by Amazon Web Services (AWS). It is a managed service that makes it easy +to store, manage, and deploy Docker container images. + """ + ) + print(f"Creating a repository named {self.repository_name}") + self.repository = self.ecr_wrapper.create_repository(self.repository_name) + print(f"The ARN of the ECR repository is {self.repository['repositoryArn']}") + repository_uri = self.repository["repositoryUri"] + press_enter_to_continue() + print_dashes() + + print( + f""" +* Build a Docker image. + +Create a local Docker image if it does not already exist. +A Python Docker client is used to execute Docker commands. +You must have Docker installed and running. + """ + ) + print(f"Building a docker image from 'docker_files/Dockerfile'") + self.full_tag_name = f"{repository_uri}:{self.tag}" + self.docker_image = self.docker_client.images.build( + path="docker_files", tag=self.full_tag_name + )[0] + print(f"Docker image {self.full_tag_name} successfully built.") + press_enter_to_continue() + print_dashes() + + if role_arn is None: + print( + """ +* Because an IAM role ARN was not provided, a role policy will not be set for this repository. + """ + ) + else: + print( + """ +* Set an ECR repository policy. + +Setting an ECR repository policy using the `setRepositoryPolicy` function is crucial for maintaining +the security and integrity of your container images. The repository policy allows you to +define specific rules and restrictions for accessing and managing the images stored within your ECR +repository. + """ + ) + + self.grant_role_download_access(role_arn) + print(f"Download access granted to the IAM role ARN {role_arn}") + press_enter_to_continue() + print_dashes() + + print( + """ +* Display ECR repository policy. + +Now we will retrieve the ECR policy to ensure it was successfully set. + """ + ) + + policy_text = self.ecr_wrapper.get_repository_policy(self.repository_name) + print("Policy Text:") + print(f"{policy_text}") + press_enter_to_continue() + print_dashes() + + print( + """ +* Retrieve an ECR authorization token. + +You need an authorization token to securely access and interact with the Amazon ECR registry. +The `get_authorization_token` method of the `ecr` client is responsible for securely accessing +and interacting with an Amazon ECR repository. This operation is responsible for obtaining a +valid authorization token, which is required to authenticate your requests to the ECR service. + +Without a valid authorization token, you would not be able to perform any operations on the +ECR repository, such as pushing, pulling, or managing your Docker images. + """ + ) + + authorization_token = self.ecr_wrapper.get_authorization_token() + print("Authorization token retrieved.") + press_enter_to_continue() + print_dashes() + print( + """ +* Get the ECR Repository URI. + +The URI of an Amazon ECR repository is important. When you want to deploy a container image to +a container orchestration platform like Amazon Elastic Kubernetes Service (EKS) +or Amazon Elastic Container Service (ECS), you need to specify the full image URI, +which includes the ECR repository URI. This allows the container runtime to pull the +correct container image from the ECR repository. + """ + ) + repository_descriptions = self.ecr_wrapper.describe_repositories( + [self.repository_name] + ) + repository_uri = repository_descriptions[0]["repositoryUri"] + print(f"Repository URI found: {repository_uri}") + press_enter_to_continue() + print_dashes() + + print( + """ +* Set an ECR Lifecycle Policy. + +An ECR Lifecycle Policy is used to manage the lifecycle of Docker images stored in your ECR repositories. +These policies allow you to automatically remove old or unused Docker images from your repositories, +freeing up storage space and reducing costs. + +This example policy helps to maintain the size and efficiency of the container registry +by automatically removing older and potentially unused images, ensuring that the +storage is optimized and the registry remains up-to-date. + """ + ) + press_enter_to_continue() + self.put_expiration_policy() + print(f"An expiration policy was added to the repository.") + print_dashes() + + print( + """ +* Push a docker image to the Amazon ECR Repository. + +The Docker client uses the authorization token is used to authenticate the when pushing the image to the +ECR repository. + """ + ) + decoded_authorization = base64.b64decode(authorization_token).decode("utf-8") + username, password = decoded_authorization.split(":") + + resp = self.docker_client.api.push( + repository=repository_uri, + auth_config={"username": username, "password": password}, + tag=self.tag, + stream=True, + decode=True, + ) + for line in resp: + print(line) + + print_dashes() + + print("* Verify if the image is in the ECR Repository.") + image_descriptions = self.ecr_wrapper.describe_images( + self.repository_name, [self.tag] + ) + if len(image_descriptions) > 0: + print("Image found in ECR Repository.") + else: + print("Image not found in ECR Repository.") + press_enter_to_continue() + print_dashes() + + print( + "* As an optional step, you can interact with the image in Amazon ECR by using the CLI." + ) + if q.ask( + "Would you like to view instructions on how to use the CLI to run the image? (y/n)", + q.is_yesno, + ): + print( + f""" +1. Authenticate with ECR - Before you can pull the image from Amazon ECR, you need to authenticate with the registry. You can do this using the AWS CLI: + + aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin {repository_uri.split("/")[0]} + +2. Describe the image using this command: + + aws ecr describe-images --repository-name {self.repository_name} --image-ids imageTag={self.tag} + +3. Run the Docker container and view the output using this command: + + docker run --rm {self.full_tag_name} +""" + ) + + self.cleanup(True) + + def cleanup(self, ask: bool): + """ + Deletes the resources created in this scenario. + :param ask: If True, prompts the user to confirm before deleting the resources. + """ + if self.repository is not None and ( + not ask + or q.ask( + f"Would you like to delete the ECR repository '{self.repository_name}? (y/n) " + ) + ): + print(f"Deleting the ECR repository '{self.repository_name}'.") + self.ecr_wrapper.delete_repository(self.repository_name) + + if self.full_tag_name is not None and ( + not ask + or q.ask( + f"Would you like to delete the local Docker image '{self.full_tag_name}? (y/n) " + ) + ): + print(f"Deleting the docker image '{self.full_tag_name}'.") + self.docker_client.images.remove(self.full_tag_name) + + # snippet-start:[python.example_code.ecr.grant_role_download_access] + def grant_role_download_access(self, role_arn: str): + """ + Grants the specified role access to download images from the ECR repository. + + :param role_arn: The ARN of the role to grant access to. + """ + policy_json = { + "Version": "2008-10-17", + "Statement": [ + { + "Sid": "AllowDownload", + "Effect": "Allow", + "Principal": {"AWS": role_arn}, + "Action": ["ecr:BatchGetImage"], + } + ], + } + + self.ecr_wrapper.set_repository_policy( + self.repository_name, json.dumps(policy_json) + ) + + # snippet-end:[python.example_code.ecr.grant_role_download_access] + + # snippet-start:[python.example_code.ecr.put_expiration_policy] + def put_expiration_policy(self): + """ + Puts an expiration policy on the ECR repository. + """ + policy_json = { + "rules": [ + { + "rulePriority": 1, + "description": "Expire images older than 14 days", + "selection": { + "tagStatus": "any", + "countType": "sinceImagePushed", + "countUnit": "days", + "countNumber": 14, + }, + "action": {"type": "expire"}, + } + ] + } + + self.ecr_wrapper.put_lifecycle_policy( + self.repository_name, json.dumps(policy_json) + ) + + # snippet-end:[python.example_code.ecr.put_expiration_policy] + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Run Amazon ECR getting started scenario." + ) + parser.add_argument( + "--iam-role-arn", + type=str, + default=None, + help="an optional IAM role ARN that will be granted access to download images from a repository.", + required=False, + ) + parser.add_argument( + "--no-art", + action="store_true", + help="accessibility setting that suppresses art in the console output.", + ) + args = parser.parse_args() + no_art = args.no_art + iam_role_arn = args.iam_role_arn + demo = None + a_docker_client = None + try: + a_docker_client = docker.from_env() + if not a_docker_client.ping(): + raise docker.errors.DockerException("Docker is not running.") + except docker.errors.DockerException as err: + logging.error( + """ + The Python Docker client could not be created. + Do you have Docker installed and running? + Here is the error message: + %s + """, + err, + ) + sys.exit("Error with Docker.") + try: + an_ecr_wrapper = ECRWrapper.from_client() + demo = ECRGettingStarted(an_ecr_wrapper, a_docker_client) + demo.run(iam_role_arn) + + except Exception as exception: + logging.exception("Something went wrong with the demo!") + if demo is not None: + demo.cleanup(False) + +# snippet-end:[python.example_code.ecr.BasicsScenario] diff --git a/python/example_code/ecr/ecr_wrapper.py b/python/example_code/ecr/ecr_wrapper.py new file mode 100644 index 00000000000..4378f5f4bb3 --- /dev/null +++ b/python/example_code/ecr/ecr_wrapper.py @@ -0,0 +1,246 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Purpose + +Shows how to use the AWS SDK for Python (Boto3) with Amazon Elastic Container Registry (Amazon ECR) API to perform +actions. +""" + +import logging + +import boto3 +from boto3 import client +from botocore.exceptions import ClientError + +logger = logging.getLogger(__name__) + + +# snippet-start:[python.example_code.ecr.ECRWrapper.class] +# snippet-start:[python.example_code.ecr.ECRWrapper.decl] +class ECRWrapper: + def __init__(self, ecr_client: client): + self.ecr_client = ecr_client + + @classmethod + def from_client(cls) -> "ECRWrapper": + """ + Creates a ECRWrapper instance with a default Amazon ECR client. + + :return: An instance of ECRWrapper initialized with the default Amazon ECR client. + """ + ecr_client = boto3.client("ecr") + return cls(ecr_client) + + # snippet-end:[python.example_code.ecr.ECRWrapper.decl] + + # snippet-start:[python.example_code.ecr.ECRWrapper.CreateRepository] + def create_repository(self, repository_name: str) -> dict[str, any]: + """ + Creates an ECR repository. + + :param repository_name: The name of the repository to create. + :return: A dictionary of the created repository. + """ + try: + response = self.ecr_client.create_repository(repositoryName=repository_name) + return response["repository"] + except ClientError as err: + if err.response["Error"]["Code"] == "RepositoryAlreadyExistsException": + print(f"Repository {repository_name} already exists.") + response = self.ecr_client.describe_repositories( + repositoryNames=[repository_name] + ) + return self.describe_repositories([repository_name])[0] + else: + logger.error( + "Error creating repository %s. Here's why %s", + repository_name, + err.response["Error"]["Message"], + ) + raise + + # snippet-end:[python.example_code.ecr.ECRWrapper.CreateRepository] + + # snippet-start:[python.example_code.ecr.ECRWrapper.DeleteRepository] + def delete_repository(self, repository_name: str): + """ + Deletes an ECR repository. + + :param repository_name: The name of the repository to delete. + """ + try: + self.ecr_client.delete_repository( + repositoryName=repository_name, force=True + ) + print(f"Deleted repository {repository_name}.") + except ClientError as err: + logger.error( + "Couldn't delete repository %s.. Here's why %s", + repository_name, + err.response["Error"]["Message"], + ) + raise + + # snippet-end:[python.example_code.ecr.ECRWrapper.DeleteRepository] + + # snippet-start:[python.example_code.ecr.ECRWrapper.SetRepositoryPolicy] + def set_repository_policy(self, repository_name: str, policy_text: str): + """ + Sets the policy for an ECR repository. + + :param repository_name: The name of the repository to set the policy for. + :param policy_text: The policy text to set. + """ + try: + self.ecr_client.set_repository_policy( + repositoryName=repository_name, policyText=policy_text + ) + print(f"Set repository policy for repository {repository_name}.") + except ClientError as err: + if err.response["Error"]["Code"] == "RepositoryPolicyNotFoundException": + logger.error("Repository does not exist. %s.", repository_name) + raise + else: + logger.error( + "Couldn't set repository policy for repository %s. Here's why %s", + repository_name, + err.response["Error"]["Message"], + ) + raise + + # snippet-end:[python.example_code.ecr.ECRWrapper.SetRepositoryPolicy] + + # snippet-start:[python.example_code.ecr.ECRWrapper.GetRepositoryPolicy] + def get_repository_policy(self, repository_name: str) -> str: + """ + Gets the policy for an ECR repository. + + :param repository_name: The name of the repository to get the policy for. + :return: The policy text. + """ + try: + response = self.ecr_client.get_repository_policy( + repositoryName=repository_name + ) + return response["policyText"] + except ClientError as err: + if err.response["Error"]["Code"] == "RepositoryPolicyNotFoundException": + logger.error("Repository does not exist. %s.", repository_name) + raise + else: + logger.error( + "Couldn't get repository policy for repository %s. Here's why %s", + repository_name, + err.response["Error"]["Message"], + ) + raise + + # snippet-end:[python.example_code.ecr.ECRWrapper.GetRepositoryPolicy] + + # snippet-start:[python.example_code.ecr.ECRWrapper.GetAuthorizationToken] + def get_authorization_token(self) -> str: + """ + Gets an authorization token for an ECR repository. + + :return: The authorization token. + """ + try: + response = self.ecr_client.get_authorization_token() + return response["authorizationData"][0]["authorizationToken"] + except ClientError as err: + logger.error( + "Couldn't get authorization token. Here's why %s", + err.response["Error"]["Message"], + ) + raise + + # snippet-end:[python.example_code.ecr.ECRWrapper.GetAuthorizationToken] + + # snippet-start:[python.example_code.ecr.ECRWrapper.DescribeRepositories] + def describe_repositories(self, repository_names: list[str]) -> list[dict]: + """ + Describes ECR repositories. + + :param repository_names: The names of the repositories to describe. + :return: The list of repository descriptions. + """ + try: + response = self.ecr_client.describe_repositories( + repositoryNames=repository_names + ) + return response["repositories"] + except ClientError as err: + logger.error( + "Couldn't describe repositories. Here's why %s", + err.response["Error"]["Message"], + ) + raise + + # snippet-end:[python.example_code.ecr.ECRWrapper.DescribeRepositories] + + # snippet-start:[python.example_code.ecr.ECRWrapper.PutLifeCyclePolicy] + def put_lifecycle_policy(self, repository_name: str, lifecycle_policy_text: str): + """ + Puts a lifecycle policy for an ECR repository. + + :param repository_name: The name of the repository to put the lifecycle policy for. + :param lifecycle_policy_text: The lifecycle policy text to put. + """ + try: + self.ecr_client.put_lifecycle_policy( + repositoryName=repository_name, + lifecyclePolicyText=lifecycle_policy_text, + ) + print(f"Put lifecycle policy for repository {repository_name}.") + except ClientError as err: + logger.error( + "Couldn't put lifecycle policy for repository %s. Here's why %s", + repository_name, + err.response["Error"]["Message"], + ) + raise + + # snippet-end:[python.example_code.ecr.ECRWrapper.PutLifeCyclePolicy] + + # snippet-start:[python.example_code.ecr.ECRWrapper.DescribeImages] + def describe_images( + self, repository_name: str, image_ids: list[str] = None + ) -> list[dict]: + """ + Describes ECR images. + + :param repository_name: The name of the repository to describe images for. + :param image_ids: The optional IDs of images to describe. + :return: The list of image descriptions. + """ + try: + params = { + "repositoryName": repository_name, + } + if image_ids is not None: + params["imageIds"] = [{"imageTag": tag} for tag in image_ids] + + paginator = self.ecr_client.get_paginator("describe_images") + image_descriptions = [] + for page in paginator.paginate(**params): + image_descriptions.extend(page["imageDetails"]) + return image_descriptions + except ClientError as err: + logger.error( + "Couldn't describe images. Here's why %s", + err.response["Error"]["Message"], + ) + raise + + # snippet-end:[python.example_code.ecr.ECRWrapper.DescribeImages] + + +# snippet-end:[python.example_code.ecr.ECRWrapper.class] + +if __name__ == "__main__": + try: + ecr_wrapper = ECRWrapper.from_client() + except Exception: + logging.exception("Something went wrong creating a client!") diff --git a/python/example_code/ecr/hello/hello_ecr.py b/python/example_code/ecr/hello/hello_ecr.py new file mode 100644 index 00000000000..24d69f1503d --- /dev/null +++ b/python/example_code/ecr/hello/hello_ecr.py @@ -0,0 +1,50 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +# snippet-start:[python.example_code.ecr.Hello] +import boto3 +import argparse +from boto3 import client + + +def hello_ecr(ecr_client: client, repository_name: str) -> None: + """ + Use the AWS SDK for Python (Boto3) to create an Amazon Elastic Container Registry (Amazon ECR) + client and list the images in a repository. + This example uses the default settings specified in your shared credentials + and config files. + + :param ecr_client: A Boto3 Amazon ECR Client object. This object wraps + the low-level Amazon ECR service API. + :param repository_name: The name of an Amazon ECR repository in your account. + """ + print( + f"Hello, Amazon ECR! Let's list some images in the repository '{repository_name}':\n" + ) + paginator = ecr_client.get_paginator("list_images") + page_iterator = paginator.paginate( + repositoryName=repository_name, PaginationConfig={"MaxItems": 10} + ) + + image_names: [str] = [] + for page in page_iterator: + for schedule in page["imageIds"]: + image_names.append(schedule["imageTag"]) + + print(f"{len(image_names)} image(s) retrieved.") + for schedule_name in image_names: + print(f"\t{schedule_name}") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Run hello Amazon ECR.") + parser.add_argument( + "--repository-name", + type=str, + help="the name of an Amazon ECR repository in your account.", + required=True, + ) + args = parser.parse_args() + + hello_ecr(boto3.client("ecr"), args.repository_name) +# snippet-end:[python.example_code.ecr.Hello] diff --git a/python/example_code/ecr/hello/requirements.txt b/python/example_code/ecr/hello/requirements.txt new file mode 100644 index 00000000000..e32a2fb0fa9 --- /dev/null +++ b/python/example_code/ecr/hello/requirements.txt @@ -0,0 +1 @@ +boto3>=1.35.38 \ No newline at end of file diff --git a/python/example_code/ecr/requirements.txt b/python/example_code/ecr/requirements.txt new file mode 100644 index 00000000000..4314a32f437 --- /dev/null +++ b/python/example_code/ecr/requirements.txt @@ -0,0 +1,4 @@ +boto3>=1.35.38 +pytest>=8.3.3 +botocore>=1.35.38 +docker~=7.1.0 \ No newline at end of file diff --git a/python/example_code/ecr/test/conftest.py b/python/example_code/ecr/test/conftest.py new file mode 100644 index 00000000000..c5d7e6664b6 --- /dev/null +++ b/python/example_code/ecr/test/conftest.py @@ -0,0 +1,107 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Contains common test fixtures used to run unit tests. +""" + +import sys + +import boto3 +import pytest +import os + + +script_dir = os.path.dirname(os.path.abspath(__file__)) + +sys.path.append(script_dir) +import ecr_getting_started +from ecr_wrapper import ECRWrapper + +# Add relative path to include demo_tools in this code example without need for setup. +sys.path.append(os.path.join(script_dir, "../..")) + +from test_tools.fixtures.common import * + + +class MockDockerImage: + def __init__(self): + self.repository = None + self.image_tag = (None,) + self.path = None + + def build(self, path: str, tag: str) -> tuple["MockDockerImage", None]: + assert path == self.path, "MockDockerImage.build" + assert tag == f"{self.repository}:{self.image_tag}", "MockDockerImage.build" + return self, None + + def remove(self, tag: str) -> None: + assert tag == f"{self.repository}:{self.image_tag}", "MockDockerImage.remove" + pass + + +class MockDockerAPI: + def __init__(self): + self.repository = None + self.tag = (None,) + self.username = None + self.password = None + + def push( + self, + repository: str, + auth_config: dict[str, str], + tag: str, + stream: bool, + decode: bool, + ) -> list[str]: + assert repository == self.repository, "MockDockerAPI.push" + assert auth_config["username"] == self.username, "MockDockerAPI.push" + assert auth_config["password"] == self.password, "MockDockerAPI.push" + assert tag == self.tag, "MockDockerAPI.push" + assert stream == True, "MockDockerAPI.push" + assert decode == True, "MockDockerAPI.push" + return ["pushing", "push complete"] + + +class MockDockerClient: + def __init__(self): + self.images = MockDockerImage() + self.api = MockDockerAPI() + + def set_mocking_values(self, repository, tag, path, username, password): + self.images.repository = repository + self.images.image_tag = tag + self.images.path = path + self.api.repository = repository + self.api.tag = tag + self.api.username = username + self.api.password = password + + +class ScenarioData: + def __init__( + self, + ecr_client, + mock_docker_client, + ecr_stubber, + ): + self.ecr_client = ecr_client + self.mock_docker_client = mock_docker_client + self.ecr_stubber = ecr_stubber + self.scenario = ecr_getting_started.ECRGettingStarted( + ecr_wrapper=ECRWrapper(self.ecr_client), + docker_client=self.mock_docker_client, + ) + + +@pytest.fixture +def scenario_data(make_stubber): + ecr_client = boto3.client("ecr") + mock_docker_client = MockDockerClient() + ecr_stubber = make_stubber(ecr_client) + return ScenarioData( + ecr_client, + mock_docker_client, + ecr_stubber, + ) diff --git a/python/example_code/ecr/test/test_ecr_getting_started.py b/python/example_code/ecr/test/test_ecr_getting_started.py new file mode 100644 index 00000000000..c9de6a6e711 --- /dev/null +++ b/python/example_code/ecr/test/test_ecr_getting_started.py @@ -0,0 +1,172 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Unit tests for prepare_application in ecr_getting_started.py. +""" + +import pytest +from botocore.exceptions import ClientError +import ecr_getting_started +import json +import base64 + + +class MockManager: + def __init__(self, stub_runner, scenario_data, input_mocker): + self.scenario_data = scenario_data + ecr_getting_started.use_press_enter_to_continue = False + self.role_arn = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" + self.repository_name = "ecr-basics" + self.tag = "echo-text" + self.repository_uri = ( + f"123456789012.dkr.ecr.us-east-1.amazonaws.com/{self.repository_name}" + ) + self.policy_str = json.dumps( + { + "Version": "2008-10-17", + "Statement": [ + { + "Sid": "AllowDownload", + "Effect": "Allow", + "Principal": {"AWS": self.role_arn}, + "Action": ["ecr:BatchGetImage"], + } + ], + } + ) + self.user = "AWS" + self.password = "password" + self.authorization_token = base64.b64encode( + f"{self.user}:{self.password}".encode("utf-8") + ).decode("utf-8") + self.repositories = [ + { + "registryId": "012345678910", + "repositoryName": self.repository_name, + "repositoryArn": "arn:aws:ecr:us-west-2:012345678910:repository/ubuntu", + "repositoryUri": self.repository_uri, + } + ] + self.lifecycle_policy = json.dumps( + { + "rules": [ + { + "rulePriority": 1, + "description": "Expire images older than 14 days", + "selection": { + "tagStatus": "any", + "countType": "sinceImagePushed", + "countUnit": "days", + "countNumber": 14, + }, + "action": {"type": "expire"}, + } + ] + } + ) + self.image_details = [ + { + "registryId": "012345678910", + "repositoryName": self.repository_name, + "imageDigest": "sha256:4a1c6567c38904384ebc64e35b7eeddd8451110c299e3368d2210066487d97e5", + "imageTags": [self.tag], + "imageSizeInBytes": 48318255, + "imagePushedAt": 1565128275.0, + } + ] + + scenario_data.mock_docker_client.set_mocking_values( + self.repository_uri, self.tag, "docker_files", self.user, self.password + ) + + answers = ["y", "y", "y"] + input_mocker.mock_answers(answers) + self.stub_runner = stub_runner + + def setup_stubs( + self, + error, + stop_on, + ecr_stubber, + ): + with self.stub_runner(error, stop_on) as runner: + runner.add( + ecr_stubber.stub_create_repository, + self.repository_name, + ) + runner.add( + ecr_stubber.stub_set_repository_policy, + self.repository_name, + self.policy_str, + ) + runner.add( + ecr_stubber.stub_get_repository_policy, + self.repository_name, + self.policy_str, + ) + runner.add( + ecr_stubber.stub_get_authorization_token, + self.authorization_token, + ) + runner.add( + ecr_stubber.stub_describe_repositories, + [self.repository_name], + self.repositories, + ) + runner.add( + ecr_stubber.stub_put_lifecycle_policy, + self.repository_name, + self.lifecycle_policy, + ) + runner.add( + ecr_stubber.stub_describe_images, + self.repository_name, + [self.tag], + self.image_details, + ) + runner.add( + ecr_stubber.stub_delete_repository, + self.repository_name, + ) + + +@pytest.fixture +def mock_mgr(stub_runner, scenario_data, input_mocker): + return MockManager(stub_runner, scenario_data, input_mocker) + + +@pytest.mark.integ +def test_ecr_getting_started(mock_mgr): + mock_mgr.setup_stubs( + None, + None, + mock_mgr.scenario_data.ecr_stubber, + ) + + mock_mgr.scenario_data.scenario.run(mock_mgr.role_arn) + + +@pytest.mark.parametrize( + "error, stop_on_index", + [ + ("TESTERROR-stub_create_repository", 0), + ("TESTERROR-stub_set_repository_policy", 1), + ("TESTERROR-stub_get_repository_policy", 2), + ("TESTERROR-stub_get_authorization_token", 3), + ("TESTERROR-stub_describe_repositories", 4), + ("TESTERROR-stub_put_lifecycle_policy", 5), + ("TESTERROR-stub_describe_images", 6), + ("TESTERROR-stub_delete_repository", 7), + ], +) +@pytest.mark.integ +def test_ecr_getting_started_error(mock_mgr, error, stop_on_index): + mock_mgr.setup_stubs( + error, + stop_on_index, + mock_mgr.scenario_data.ecr_stubber, + ) + + with pytest.raises(ClientError): + mock_mgr.scenario_data.scenario.run(mock_mgr.role_arn) diff --git a/python/test_tools/ecr_stubber.py b/python/test_tools/ecr_stubber.py new file mode 100644 index 00000000000..074fd80f9d4 --- /dev/null +++ b/python/test_tools/ecr_stubber.py @@ -0,0 +1,124 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Stub functions that are used by the Amazon Elastic Container Registry (Amazon ECR) +unit tests. + +When tests are run against an actual AWS account, the stubber class does not +set up stubs and passes all calls through to the Boto3 client. +""" + +from botocore.stub import ANY +from test_tools.example_stubber import ExampleStubber + + +class EcrStubber(ExampleStubber): + """ + A class that implements a variety of stub functions that are used by the + ECR unit tests. + + The stubbed functions all expect certain parameters to be passed to them as + part of the tests, and will raise errors when the actual parameters differ from + the expected. + """ + + def __init__(self, client, use_stubs=True): + """ + Initializes the object with a specific client and configures it for + stubbing or AWS passthrough. + + :param client: A Boto3 ECR client. + :param use_stubs: When True, use stubs to intercept requests. Otherwise, + pass requests through to AWS. + """ + super().__init__(client, use_stubs) + + def stub_create_repository(self, repository_name, error_code=None): + expected_params = {"repositoryName": repository_name} + response = { + "repository": { + "repositoryArn": f"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "registryId": "XXXXXXXXXXXX", + "repositoryName": repository_name, + "repositoryUri": f"123456789012.dkr.ecr.us-east-1.amazonaws.com/{repository_name}", + "createdAt": "2022-01-01 00:00:00.000000000", + "imageTagMutability": "MUTABLE", + "imageScanningConfiguration": {"scanOnPush": False}, + } + } + self._stub_bifurcator( + "create_repository", expected_params, response, error_code=error_code + ) + + def stub_set_repository_policy(self, repository_name, policy, error_code=None): + expected_params = { + "repositoryName": repository_name, + "policyText": policy, + } + response = {} + self._stub_bifurcator( + "set_repository_policy", expected_params, response, error_code=error_code + ) + + def stub_get_repository_policy(self, repository_name, policy, error_code=None): + expected_params = {"repositoryName": repository_name} + response = {"policyText": policy} + self._stub_bifurcator( + "get_repository_policy", expected_params, response, error_code=error_code + ) + + def stub_get_authorization_token(self, authorization_token, error_code=None): + expected_params = {} + response = { + "authorizationData": [ + { + "authorizationToken": authorization_token, + "proxyEndpoint": "https://123456789012.dkr.ecr.us-east-1.amazonaws.com", + } + ] + } + self._stub_bifurcator( + "get_authorization_token", expected_params, response, error_code=error_code + ) + + def stub_describe_repositories( + self, repository_names, repositories, error_code=None + ): + expected_params = {"repositoryNames": repository_names} + response = {"repositories": repositories} + self._stub_bifurcator( + "describe_repositories", expected_params, response, error_code=error_code + ) + + def stub_put_lifecycle_policy(self, repository_name, policy, error_code=None): + expected_params = { + "repositoryName": repository_name, + "lifecyclePolicyText": policy, + } + response = {} + self._stub_bifurcator( + "put_lifecycle_policy", expected_params, response, error_code=error_code + ) + + def stub_describe_images(self, repository_name, image_ids, images, error_code=None): + expected_params = {"repositoryName": repository_name} + if image_ids is not None: + expected_params["imageIds"] = [{"imageTag": tag} for tag in image_ids] + response = {"imageDetails": images} + self._stub_bifurcator( + "describe_images", expected_params, response, error_code=error_code + ) + + def stub_delete_repository(self, repository_name, error_code=None): + expected_params = {"repositoryName": repository_name, "force": True} + response = { + "repository": { + "registryId": "123456789012", + "repositoryName": "ubuntu", + "repositoryArn": "arn:aws:ecr:us-west-2:123456789012:repository/ubuntu", + } + } + self._stub_bifurcator( + "delete_repository", expected_params, response, error_code=error_code + ) diff --git a/python/test_tools/stubber_factory.py b/python/test_tools/stubber_factory.py index 79ea96ef526..a761d3ce545 100644 --- a/python/test_tools/stubber_factory.py +++ b/python/test_tools/stubber_factory.py @@ -27,6 +27,7 @@ from test_tools.config_stubber import ConfigStubber from test_tools.dynamodb_stubber import DynamoStubber from test_tools.ec2_stubber import Ec2Stubber +from test_tools.ecr_stubber import EcrStubber from test_tools.elbv2_stubber import ELBv2Stubber from test_tools.emr_stubber import EmrStubber from test_tools.eventbridge_stubber import EventBridgeStubber @@ -111,6 +112,8 @@ def stubber_factory(service_name): return DynamoStubber elif service_name == "ec2": return Ec2Stubber + elif service_name == "ecr": + return EcrStubber elif service_name == "elbv2": return ELBv2Stubber elif service_name == "emr":