From 9834b600bfea506d319f7915ea59db120db1bcf9 Mon Sep 17 00:00:00 2001
From: AlejandroMorgante <62363051+AlejandroMorgante@users.noreply.github.com>
Date: Tue, 14 Jul 2026 19:27:59 -0300
Subject: [PATCH 1/2] Add Amazon ECR repository operators
Container-based AWS workflows need reusable repository setup and cleanup. AgentCore deployments share this lifecycle, and the SageMaker system tests currently implement it with direct SDK calls.
---
providers/amazon/docs/operators/ecr.rst | 87 +++++++
providers/amazon/provider.yaml | 5 +
.../providers/amazon/aws/operators/ecr.py | 233 ++++++++++++++++++
.../providers/amazon/get_provider_info.py | 5 +
.../tests/system/amazon/aws/example_ecr.py | 99 ++++++++
.../unit/amazon/aws/operators/test_ecr.py | 208 ++++++++++++++++
6 files changed, 637 insertions(+)
create mode 100644 providers/amazon/docs/operators/ecr.rst
create mode 100644 providers/amazon/src/airflow/providers/amazon/aws/operators/ecr.py
create mode 100644 providers/amazon/tests/system/amazon/aws/example_ecr.py
create mode 100644 providers/amazon/tests/unit/amazon/aws/operators/test_ecr.py
diff --git a/providers/amazon/docs/operators/ecr.rst b/providers/amazon/docs/operators/ecr.rst
new file mode 100644
index 0000000000000..6c35efb869c28
--- /dev/null
+++ b/providers/amazon/docs/operators/ecr.rst
@@ -0,0 +1,87 @@
+ .. Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ .. http://www.apache.org/licenses/LICENSE-2.0
+
+ .. Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+
+=======================================
+Amazon Elastic Container Registry (ECR)
+=======================================
+
+`Amazon Elastic Container Registry (ECR) `__ is a managed container registry
+for storing, sharing, and deploying container images and artifacts.
+
+Prerequisite Tasks
+------------------
+
+.. include:: ../_partials/prerequisite_tasks.rst
+
+Generic Parameters
+------------------
+
+.. include:: ../_partials/generic_parameters.rst
+
+Operators
+---------
+
+.. _howto/operator:EcrCreateRepositoryOperator:
+
+Create an ECR repository
+========================
+
+To create an ECR repository, use
+:class:`~airflow.providers.amazon.aws.operators.ecr.EcrCreateRepositoryOperator`.
+The operator returns the complete response from the Boto3 ``create_repository`` API operation.
+
+.. exampleinclude:: /../../amazon/tests/system/amazon/aws/example_ecr.py
+ :language: python
+ :dedent: 4
+ :start-after: [START howto_operator_ecr_create_repository]
+ :end-before: [END howto_operator_ecr_create_repository]
+
+.. _howto/operator:EcrSetRepositoryPolicyOperator:
+
+Set an ECR repository policy
+============================
+
+To set the repository policy for an ECR repository, use
+:class:`~airflow.providers.amazon.aws.operators.ecr.EcrSetRepositoryPolicyOperator`.
+The operator returns the complete response from the Boto3 ``set_repository_policy`` API operation.
+
+.. exampleinclude:: /../../amazon/tests/system/amazon/aws/example_ecr.py
+ :language: python
+ :dedent: 4
+ :start-after: [START howto_operator_ecr_set_repository_policy]
+ :end-before: [END howto_operator_ecr_set_repository_policy]
+
+.. _howto/operator:EcrDeleteRepositoryOperator:
+
+Delete an ECR repository
+========================
+
+To delete an ECR repository, use
+:class:`~airflow.providers.amazon.aws.operators.ecr.EcrDeleteRepositoryOperator`.
+Set ``force=True`` to delete a repository that contains images. The operator returns the complete response
+from the Boto3 ``delete_repository`` API operation.
+
+.. exampleinclude:: /../../amazon/tests/system/amazon/aws/example_ecr.py
+ :language: python
+ :dedent: 4
+ :start-after: [START howto_operator_ecr_delete_repository]
+ :end-before: [END howto_operator_ecr_delete_repository]
+
+Reference
+---------
+
+* `AWS Boto3 library documentation for ECR `__
diff --git a/providers/amazon/provider.yaml b/providers/amazon/provider.yaml
index 4fa06f211212d..afc766814820a 100644
--- a/providers/amazon/provider.yaml
+++ b/providers/amazon/provider.yaml
@@ -182,6 +182,8 @@ integrations:
- integration-name: Amazon Elastic Container Registry (ECR)
external-doc-url: https://aws.amazon.com/ecr/
logo: /docs/integration-logos/Amazon-Elastic-Container-Registry_light-bg@4x.png
+ how-to-guide:
+ - /docs/apache-airflow-providers-amazon/operators/ecr.rst
tags: [aws]
- integration-name: Amazon ECS
external-doc-url: https://aws.amazon.com/ecs/
@@ -447,6 +449,9 @@ operators:
- integration-name: Amazon EC2
python-modules:
- airflow.providers.amazon.aws.operators.ec2
+ - integration-name: Amazon Elastic Container Registry (ECR)
+ python-modules:
+ - airflow.providers.amazon.aws.operators.ecr
- integration-name: Amazon ECS
python-modules:
- airflow.providers.amazon.aws.operators.ecs
diff --git a/providers/amazon/src/airflow/providers/amazon/aws/operators/ecr.py b/providers/amazon/src/airflow/providers/amazon/aws/operators/ecr.py
new file mode 100644
index 0000000000000..738fafde86ad5
--- /dev/null
+++ b/providers/amazon/src/airflow/providers/amazon/aws/operators/ecr.py
@@ -0,0 +1,233 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""Amazon Elastic Container Registry (ECR) operators."""
+
+from __future__ import annotations
+
+from collections.abc import Sequence
+from typing import TYPE_CHECKING, Any, ClassVar
+
+from airflow.providers.amazon.aws.hooks.ecr import EcrHook
+from airflow.providers.amazon.aws.operators.base_aws import AwsBaseOperator
+from airflow.providers.amazon.aws.utils.mixins import aws_template_fields
+from airflow.utils.helpers import prune_dict
+
+if TYPE_CHECKING:
+ from airflow.sdk import Context
+
+
+class EcrCreateRepositoryOperator(AwsBaseOperator[EcrHook]):
+ """
+ Create an Amazon ECR repository.
+
+ .. seealso::
+ For more information on how to use this operator, take a look at the guide:
+ :ref:`howto/operator:EcrCreateRepositoryOperator`
+
+ :param repository_name: The name of the repository to create. (templated)
+ :param registry_id: The AWS account ID associated with the registry. (templated)
+ :param tags: Metadata to apply to the repository. (templated)
+ :param image_tag_mutability: The tag mutability setting for the repository. (templated)
+ :param image_tag_mutability_exclusion_filters: Filters that override the repository's
+ image tag mutability setting. (templated)
+ :param image_scanning_configuration: The image scanning configuration for the repository. (templated)
+ :param encryption_configuration: The encryption configuration for the repository. (templated)
+ :param aws_conn_id: The Airflow connection used for AWS credentials.
+ If this is ``None`` or empty then the default boto3 behaviour is used. If
+ running Airflow in a distributed manner and aws_conn_id is None or
+ empty, then default boto3 configuration would be used (and must be
+ maintained on each worker node).
+ :param region_name: AWS region_name. If not specified then the default boto3 behaviour is used.
+ :param verify: Whether or not to verify SSL certificates. See:
+ https://boto3.amazonaws.com/v1/documentation/api/latest/reference/core/session.html
+ :param botocore_config: Configuration dictionary (key-values) for botocore client. See:
+ https://botocore.amazonaws.com/v1/documentation/api/latest/reference/config.html
+ """
+
+ aws_hook_class = EcrHook
+ template_fields: Sequence[str] = aws_template_fields(
+ "repository_name",
+ "registry_id",
+ "tags",
+ "image_tag_mutability",
+ "image_tag_mutability_exclusion_filters",
+ "image_scanning_configuration",
+ "encryption_configuration",
+ )
+ template_fields_renderers: ClassVar[dict[str, str]] = {
+ "tags": "json",
+ "image_tag_mutability_exclusion_filters": "json",
+ "image_scanning_configuration": "json",
+ "encryption_configuration": "json",
+ }
+
+ def __init__(
+ self,
+ *,
+ repository_name: str,
+ registry_id: str | None = None,
+ tags: list[dict[str, str]] | None = None,
+ image_tag_mutability: str | None = None,
+ image_tag_mutability_exclusion_filters: list[dict[str, str]] | None = None,
+ image_scanning_configuration: dict[str, bool] | None = None,
+ encryption_configuration: dict[str, str] | None = None,
+ aws_conn_id: str | None = "aws_default",
+ **kwargs,
+ ):
+ super().__init__(aws_conn_id=aws_conn_id, **kwargs)
+ self.repository_name = repository_name
+ self.registry_id = registry_id
+ self.tags = tags
+ self.image_tag_mutability = image_tag_mutability
+ self.image_tag_mutability_exclusion_filters = image_tag_mutability_exclusion_filters
+ self.image_scanning_configuration = image_scanning_configuration
+ self.encryption_configuration = encryption_configuration
+
+ def execute(self, context: Context) -> dict[str, Any]:
+ self.log.info("Creating Amazon ECR repository %s", self.repository_name)
+ response = self.hook.conn.create_repository(
+ **prune_dict(
+ {
+ "registryId": self.registry_id,
+ "repositoryName": self.repository_name,
+ "tags": self.tags,
+ "imageTagMutability": self.image_tag_mutability,
+ "imageTagMutabilityExclusionFilters": self.image_tag_mutability_exclusion_filters,
+ "imageScanningConfiguration": self.image_scanning_configuration,
+ "encryptionConfiguration": self.encryption_configuration,
+ }
+ )
+ )
+ self.log.info("Created Amazon ECR repository %s", self.repository_name)
+ return response
+
+
+class EcrSetRepositoryPolicyOperator(AwsBaseOperator[EcrHook]):
+ """
+ Set the repository policy for an Amazon ECR repository.
+
+ .. seealso::
+ For more information on how to use this operator, take a look at the guide:
+ :ref:`howto/operator:EcrSetRepositoryPolicyOperator`
+
+ :param repository_name: The name of the repository to receive the policy. (templated)
+ :param policy_text: The JSON repository policy text to apply. (templated)
+ :param registry_id: The AWS account ID associated with the registry. (templated)
+ :param force: Whether to replace an existing policy that prevents setting a new policy. (templated)
+ :param aws_conn_id: The Airflow connection used for AWS credentials.
+ If this is ``None`` or empty then the default boto3 behaviour is used. If
+ running Airflow in a distributed manner and aws_conn_id is None or
+ empty, then default boto3 configuration would be used (and must be
+ maintained on each worker node).
+ :param region_name: AWS region_name. If not specified then the default boto3 behaviour is used.
+ :param verify: Whether or not to verify SSL certificates. See:
+ https://boto3.amazonaws.com/v1/documentation/api/latest/reference/core/session.html
+ :param botocore_config: Configuration dictionary (key-values) for botocore client. See:
+ https://botocore.amazonaws.com/v1/documentation/api/latest/reference/config.html
+ """
+
+ aws_hook_class = EcrHook
+ template_fields: Sequence[str] = aws_template_fields(
+ "repository_name", "policy_text", "registry_id", "force"
+ )
+ template_fields_renderers: ClassVar[dict[str, str]] = {"policy_text": "json"}
+
+ def __init__(
+ self,
+ *,
+ repository_name: str,
+ policy_text: str,
+ registry_id: str | None = None,
+ force: bool = False,
+ aws_conn_id: str | None = "aws_default",
+ **kwargs,
+ ):
+ super().__init__(aws_conn_id=aws_conn_id, **kwargs)
+ self.repository_name = repository_name
+ self.policy_text = policy_text
+ self.registry_id = registry_id
+ self.force = force
+
+ def execute(self, context: Context) -> dict[str, Any]:
+ self.log.info("Setting repository policy for Amazon ECR repository %s", self.repository_name)
+ response = self.hook.conn.set_repository_policy(
+ **prune_dict(
+ {
+ "registryId": self.registry_id,
+ "repositoryName": self.repository_name,
+ "policyText": self.policy_text,
+ "force": self.force,
+ }
+ )
+ )
+ self.log.info("Set repository policy for Amazon ECR repository %s", self.repository_name)
+ return response
+
+
+class EcrDeleteRepositoryOperator(AwsBaseOperator[EcrHook]):
+ """
+ Delete an Amazon ECR repository.
+
+ .. seealso::
+ For more information on how to use this operator, take a look at the guide:
+ :ref:`howto/operator:EcrDeleteRepositoryOperator`
+
+ :param repository_name: The name of the repository to delete. (templated)
+ :param registry_id: The AWS account ID associated with the registry. (templated)
+ :param force: Whether to delete the repository when it contains images. (templated)
+ :param aws_conn_id: The Airflow connection used for AWS credentials.
+ If this is ``None`` or empty then the default boto3 behaviour is used. If
+ running Airflow in a distributed manner and aws_conn_id is None or
+ empty, then default boto3 configuration would be used (and must be
+ maintained on each worker node).
+ :param region_name: AWS region_name. If not specified then the default boto3 behaviour is used.
+ :param verify: Whether or not to verify SSL certificates. See:
+ https://boto3.amazonaws.com/v1/documentation/api/latest/reference/core/session.html
+ :param botocore_config: Configuration dictionary (key-values) for botocore client. See:
+ https://botocore.amazonaws.com/v1/documentation/api/latest/reference/config.html
+ """
+
+ aws_hook_class = EcrHook
+ template_fields: Sequence[str] = aws_template_fields("repository_name", "registry_id", "force")
+
+ def __init__(
+ self,
+ *,
+ repository_name: str,
+ registry_id: str | None = None,
+ force: bool = False,
+ aws_conn_id: str | None = "aws_default",
+ **kwargs,
+ ):
+ super().__init__(aws_conn_id=aws_conn_id, **kwargs)
+ self.repository_name = repository_name
+ self.registry_id = registry_id
+ self.force = force
+
+ def execute(self, context: Context) -> dict[str, Any]:
+ self.log.info("Deleting Amazon ECR repository %s", self.repository_name)
+ response = self.hook.conn.delete_repository(
+ **prune_dict(
+ {
+ "registryId": self.registry_id,
+ "repositoryName": self.repository_name,
+ "force": self.force,
+ }
+ )
+ )
+ self.log.info("Deleted Amazon ECR repository %s", self.repository_name)
+ return response
diff --git a/providers/amazon/src/airflow/providers/amazon/get_provider_info.py b/providers/amazon/src/airflow/providers/amazon/get_provider_info.py
index a5a5c532e7e31..35fdd5756a6d1 100644
--- a/providers/amazon/src/airflow/providers/amazon/get_provider_info.py
+++ b/providers/amazon/src/airflow/providers/amazon/get_provider_info.py
@@ -94,6 +94,7 @@ def get_provider_info():
"integration-name": "Amazon Elastic Container Registry (ECR)",
"external-doc-url": "https://aws.amazon.com/ecr/",
"logo": "/docs/integration-logos/Amazon-Elastic-Container-Registry_light-bg@4x.png",
+ "how-to-guide": ["/docs/apache-airflow-providers-amazon/operators/ecr.rst"],
"tags": ["aws"],
},
{
@@ -424,6 +425,10 @@ def get_provider_info():
"integration-name": "Amazon EC2",
"python-modules": ["airflow.providers.amazon.aws.operators.ec2"],
},
+ {
+ "integration-name": "Amazon Elastic Container Registry (ECR)",
+ "python-modules": ["airflow.providers.amazon.aws.operators.ecr"],
+ },
{
"integration-name": "Amazon ECS",
"python-modules": ["airflow.providers.amazon.aws.operators.ecs"],
diff --git a/providers/amazon/tests/system/amazon/aws/example_ecr.py b/providers/amazon/tests/system/amazon/aws/example_ecr.py
new file mode 100644
index 0000000000000..64230657cb01e
--- /dev/null
+++ b/providers/amazon/tests/system/amazon/aws/example_ecr.py
@@ -0,0 +1,99 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+from datetime import datetime
+
+from airflow.providers.amazon.aws.operators.ecr import (
+ EcrCreateRepositoryOperator,
+ EcrDeleteRepositoryOperator,
+ EcrSetRepositoryPolicyOperator,
+)
+from airflow.providers.common.compat.sdk import DAG, chain
+
+from system.amazon.aws.utils import ENV_ID_KEY, SystemTestContextBuilder
+from tests_common.test_utils.version_compat import AIRFLOW_V_3_0_PLUS
+
+if AIRFLOW_V_3_0_PLUS:
+ from airflow.sdk import TriggerRule
+else:
+ from airflow.utils.trigger_rule import TriggerRule # type: ignore[no-redef,attr-defined]
+
+DAG_ID = "example_ecr"
+
+sys_test_context_task = SystemTestContextBuilder().build()
+
+
+with DAG(
+ dag_id=DAG_ID,
+ schedule=None,
+ start_date=datetime(2024, 1, 1),
+ catchup=False,
+) as dag:
+ test_context = sys_test_context_task()
+ repository_name = f"{test_context[ENV_ID_KEY]}-test-repository"
+
+ # [START howto_operator_ecr_create_repository]
+ create_repository = EcrCreateRepositoryOperator(
+ task_id="create_repository",
+ repository_name=repository_name,
+ )
+ # [END howto_operator_ecr_create_repository]
+
+ # [START howto_operator_ecr_set_repository_policy]
+ set_repository_policy = EcrSetRepositoryPolicyOperator(
+ task_id="set_repository_policy",
+ repository_name=repository_name,
+ policy_text="""
+ {
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Sid": "AllowAccountPull",
+ "Effect": "Allow",
+ "Principal": {
+ "AWS": "arn:aws:iam::{{ task_instance.xcom_pull(task_ids='create_repository')['repository']['registryId'] }}:root"
+ },
+ "Action": [
+ "ecr:BatchGetImage",
+ "ecr:GetDownloadUrlForLayer"
+ ]
+ }
+ ]
+ }
+ """,
+ )
+ # [END howto_operator_ecr_set_repository_policy]
+
+ # [START howto_operator_ecr_delete_repository]
+ delete_repository = EcrDeleteRepositoryOperator(
+ task_id="delete_repository",
+ repository_name=repository_name,
+ force=True,
+ trigger_rule=TriggerRule.ALL_DONE,
+ )
+ # [END howto_operator_ecr_delete_repository]
+
+ chain(test_context, create_repository, set_repository_policy, delete_repository)
+
+ from tests_common.test_utils.watcher import watcher
+
+ list(dag.tasks) >> watcher()
+
+from tests_common.test_utils.system_tests import get_test_run # noqa: E402
+
+test_run = get_test_run(dag)
diff --git a/providers/amazon/tests/unit/amazon/aws/operators/test_ecr.py b/providers/amazon/tests/unit/amazon/aws/operators/test_ecr.py
new file mode 100644
index 0000000000000..ba485ba4016d2
--- /dev/null
+++ b/providers/amazon/tests/unit/amazon/aws/operators/test_ecr.py
@@ -0,0 +1,208 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+from unittest import mock
+
+import pytest
+
+from airflow.providers.amazon.aws.hooks.ecr import EcrHook
+from airflow.providers.amazon.aws.operators.ecr import (
+ EcrCreateRepositoryOperator,
+ EcrDeleteRepositoryOperator,
+ EcrSetRepositoryPolicyOperator,
+)
+
+from unit.amazon.aws.utils.test_template_fields import validate_template_fields
+
+REPOSITORY_NAME = "test-repository"
+REGISTRY_ID = "123456789012"
+REPOSITORY_RESPONSE = {
+ "repository": {
+ "repositoryArn": f"arn:aws:ecr:us-east-1:{REGISTRY_ID}:repository/{REPOSITORY_NAME}",
+ "registryId": REGISTRY_ID,
+ "repositoryName": REPOSITORY_NAME,
+ "repositoryUri": f"{REGISTRY_ID}.dkr.ecr.us-east-1.amazonaws.com/{REPOSITORY_NAME}",
+ },
+ "ResponseMetadata": {"HTTPStatusCode": 200},
+}
+POLICY_TEXT = '{"Version":"2012-10-17","Statement":[]}'
+POLICY_RESPONSE = {
+ "registryId": REGISTRY_ID,
+ "repositoryName": REPOSITORY_NAME,
+ "policyText": POLICY_TEXT,
+ "ResponseMetadata": {"HTTPStatusCode": 200},
+}
+
+
+class TestEcrCreateRepositoryOperator:
+ @pytest.mark.parametrize(
+ ("operator_parameters", "boto3_parameters"),
+ [
+ pytest.param(
+ {},
+ {
+ "repositoryName": REPOSITORY_NAME,
+ },
+ id="required-parameters",
+ ),
+ pytest.param(
+ {
+ "registry_id": REGISTRY_ID,
+ "tags": [{"Key": "environment", "Value": "test"}],
+ "image_tag_mutability": "IMMUTABLE_WITH_EXCLUSION",
+ "image_tag_mutability_exclusion_filters": [
+ {"filterType": "WILDCARD", "filter": "latest"}
+ ],
+ "image_scanning_configuration": {"scanOnPush": True},
+ "encryption_configuration": {
+ "encryptionType": "KMS",
+ "kmsKey": "arn:aws:kms:us-east-1:123456789012:key/test-key",
+ },
+ },
+ {
+ "repositoryName": REPOSITORY_NAME,
+ "registryId": REGISTRY_ID,
+ "tags": [{"Key": "environment", "Value": "test"}],
+ "imageTagMutability": "IMMUTABLE_WITH_EXCLUSION",
+ "imageTagMutabilityExclusionFilters": [{"filterType": "WILDCARD", "filter": "latest"}],
+ "imageScanningConfiguration": {"scanOnPush": True},
+ "encryptionConfiguration": {
+ "encryptionType": "KMS",
+ "kmsKey": "arn:aws:kms:us-east-1:123456789012:key/test-key",
+ },
+ },
+ id="all-parameters",
+ ),
+ ],
+ )
+ @mock.patch.object(EcrHook, "conn", new_callable=mock.PropertyMock)
+ def test_execute(self, mock_conn, operator_parameters, boto3_parameters):
+ mock_client = mock.MagicMock(spec=["create_repository"])
+ mock_client.create_repository.return_value = REPOSITORY_RESPONSE
+ mock_conn.return_value = mock_client
+ operator = EcrCreateRepositoryOperator(
+ task_id="create_repository",
+ repository_name=REPOSITORY_NAME,
+ **operator_parameters,
+ )
+
+ result = operator.execute({})
+
+ mock_client.create_repository.assert_called_once_with(**boto3_parameters)
+ assert result == REPOSITORY_RESPONSE
+
+ def test_template_fields(self):
+ operator = EcrCreateRepositoryOperator(
+ task_id="create_repository",
+ repository_name=REPOSITORY_NAME,
+ )
+
+ validate_template_fields(operator)
+
+
+class TestEcrSetRepositoryPolicyOperator:
+ @pytest.mark.parametrize(
+ ("operator_parameters", "boto3_parameters"),
+ [
+ pytest.param(
+ {},
+ {
+ "repositoryName": REPOSITORY_NAME,
+ "policyText": POLICY_TEXT,
+ "force": False,
+ },
+ id="defaults",
+ ),
+ pytest.param(
+ {"registry_id": REGISTRY_ID, "force": True},
+ {
+ "registryId": REGISTRY_ID,
+ "repositoryName": REPOSITORY_NAME,
+ "policyText": POLICY_TEXT,
+ "force": True,
+ },
+ id="registry-and-force",
+ ),
+ ],
+ )
+ @mock.patch.object(EcrHook, "conn", new_callable=mock.PropertyMock)
+ def test_execute(self, mock_conn, operator_parameters, boto3_parameters):
+ mock_client = mock.MagicMock(spec=["set_repository_policy"])
+ mock_client.set_repository_policy.return_value = POLICY_RESPONSE
+ mock_conn.return_value = mock_client
+ operator = EcrSetRepositoryPolicyOperator(
+ task_id="set_repository_policy",
+ repository_name=REPOSITORY_NAME,
+ policy_text=POLICY_TEXT,
+ **operator_parameters,
+ )
+
+ result = operator.execute({})
+
+ mock_client.set_repository_policy.assert_called_once_with(**boto3_parameters)
+ assert result == POLICY_RESPONSE
+
+ def test_template_fields(self):
+ operator = EcrSetRepositoryPolicyOperator(
+ task_id="set_repository_policy",
+ repository_name=REPOSITORY_NAME,
+ policy_text=POLICY_TEXT,
+ )
+
+ validate_template_fields(operator)
+
+
+class TestEcrDeleteRepositoryOperator:
+ @pytest.mark.parametrize(
+ ("operator_parameters", "boto3_parameters"),
+ [
+ pytest.param(
+ {},
+ {"repositoryName": REPOSITORY_NAME, "force": False},
+ id="defaults",
+ ),
+ pytest.param(
+ {"registry_id": REGISTRY_ID, "force": True},
+ {"repositoryName": REPOSITORY_NAME, "registryId": REGISTRY_ID, "force": True},
+ id="registry-and-force",
+ ),
+ ],
+ )
+ @mock.patch.object(EcrHook, "conn", new_callable=mock.PropertyMock)
+ def test_execute(self, mock_conn, operator_parameters, boto3_parameters):
+ mock_client = mock.MagicMock(spec=["delete_repository"])
+ mock_client.delete_repository.return_value = REPOSITORY_RESPONSE
+ mock_conn.return_value = mock_client
+ operator = EcrDeleteRepositoryOperator(
+ task_id="delete_repository",
+ repository_name=REPOSITORY_NAME,
+ **operator_parameters,
+ )
+
+ result = operator.execute({})
+
+ mock_client.delete_repository.assert_called_once_with(**boto3_parameters)
+ assert result == REPOSITORY_RESPONSE
+
+ def test_template_fields(self):
+ operator = EcrDeleteRepositoryOperator(
+ task_id="delete_repository",
+ repository_name=REPOSITORY_NAME,
+ )
+
+ validate_template_fields(operator)
From 9a02caaf354f8998cd21aa319d86a3858d2a74c4 Mon Sep 17 00:00:00 2001
From: AlejandroMorgante <62363051+AlejandroMorgante@users.noreply.github.com>
Date: Thu, 16 Jul 2026 18:01:04 -0300
Subject: [PATCH 2/2] Clarify ECR operator use cases
---
providers/amazon/docs/operators/ecr.rst | 3 +++
.../amazon/src/airflow/providers/amazon/aws/operators/ecr.py | 2 --
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/providers/amazon/docs/operators/ecr.rst b/providers/amazon/docs/operators/ecr.rst
index 6c35efb869c28..466e3c72d5794 100644
--- a/providers/amazon/docs/operators/ecr.rst
+++ b/providers/amazon/docs/operators/ecr.rst
@@ -42,6 +42,7 @@ Create an ECR repository
To create an ECR repository, use
:class:`~airflow.providers.amazon.aws.operators.ecr.EcrCreateRepositoryOperator`.
+This operator can provision a repository before a workflow builds or pushes container images.
The operator returns the complete response from the Boto3 ``create_repository`` API operation.
.. exampleinclude:: /../../amazon/tests/system/amazon/aws/example_ecr.py
@@ -57,6 +58,7 @@ Set an ECR repository policy
To set the repository policy for an ECR repository, use
:class:`~airflow.providers.amazon.aws.operators.ecr.EcrSetRepositoryPolicyOperator`.
+This operator can configure permissions such as cross-account access to images stored in the repository.
The operator returns the complete response from the Boto3 ``set_repository_policy`` API operation.
.. exampleinclude:: /../../amazon/tests/system/amazon/aws/example_ecr.py
@@ -72,6 +74,7 @@ Delete an ECR repository
To delete an ECR repository, use
:class:`~airflow.providers.amazon.aws.operators.ecr.EcrDeleteRepositoryOperator`.
+This operator can clean up temporary repositories or repositories that are no longer needed.
Set ``force=True`` to delete a repository that contains images. The operator returns the complete response
from the Boto3 ``delete_repository`` API operation.
diff --git a/providers/amazon/src/airflow/providers/amazon/aws/operators/ecr.py b/providers/amazon/src/airflow/providers/amazon/aws/operators/ecr.py
index 738fafde86ad5..dec75ede9b582 100644
--- a/providers/amazon/src/airflow/providers/amazon/aws/operators/ecr.py
+++ b/providers/amazon/src/airflow/providers/amazon/aws/operators/ecr.py
@@ -14,8 +14,6 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
-"""Amazon Elastic Container Registry (ECR) operators."""
-
from __future__ import annotations
from collections.abc import Sequence