From 967f580d6e0742b1d311ea73b4f44fa1e18543b5 Mon Sep 17 00:00:00 2001 From: mabelegba Date: Fri, 17 Oct 2025 13:31:28 -0400 Subject: [PATCH 1/3] Add user-assigned managed identity support to cache rule create and update operations Add user-assigned managed identity support to cache rule create and update operations --- src/acrcache/HISTORY.rst | 33 +++ src/acrcache/azext_acrcache/_help.py | 4 + src/acrcache/azext_acrcache/_params.py | 1 + src/acrcache/azext_acrcache/cache.py | 57 ++++- .../azext_acrcache/tests/latest/test_cache.py | 195 ++++++++++++++++++ src/acrcache/setup.py | 2 +- 6 files changed, 287 insertions(+), 5 deletions(-) diff --git a/src/acrcache/HISTORY.rst b/src/acrcache/HISTORY.rst index ab7933b2e88..9348829979d 100644 --- a/src/acrcache/HISTORY.rst +++ b/src/acrcache/HISTORY.rst @@ -2,6 +2,39 @@ Release History =============== + +1.2.0 - version 1.0.0c7 +++++++ +* **FEATURE**: Added `--assign-identity` parameter support for cache rules + * `az acr cache create --assign-identity` - Create cache rules with user-assigned managed identities + * `az acr cache update --assign-identity` - Update existing cache rules with managed identities + * Enables secure authentication for ACR-to-ACR caching across subscriptions and tenants + * Supports Azure resource ID format validation for managed identity resources +* **ENHANCEMENT**: Improved error handling and validation for identity parameters +* **TESTING**: Added comprehensive unit test coverage for identity processing functionality + +1.10 - version 1.0.0c6 +++++++ +* **BREAKING**: Migrated to Container Registry SDK v2025-09-01-preview + * Updated SDK imports from v2025_07_01_preview to v2025_09_01_preview + * Updated SDK client factory to support new API version +* **ENHANCEMENT**: Standardized enum values for sync and referrer status + * Sync parameter now uses ActiveSync/PassiveSync values + * Referrer status now uses Enabled/Disabled values + * Added case-insensitive comparisons and improved None handling +* **REFACTOR**: Improved validation and state logic + * Refactored input validation logic in cache.py for sync/referrer options + * Modified CLI argument definitions in _params.py to reflect new enum values + * Enhanced error handling and parameter validation +* **DOCUMENTATION**: Updated help examples for clarity + * Rewrote help examples in _help.py for alignment with new conventions + * Improved CLI documentation and usage examples +* **TESTING**: Expanded test coverage + * Added comprehensive unit tests for cache operations and validation logic + * Updated test coverage to support the new API version + * Enhanced reliability testing under new SDK +* **COMPATIBILITY**: No breaking changes to CLI interface, only behavioral improvements + 1.0.0 ++++++ * Initial release. \ No newline at end of file diff --git a/src/acrcache/azext_acrcache/_help.py b/src/acrcache/azext_acrcache/_help.py index e545eedcf02..043bd8a5819 100644 --- a/src/acrcache/azext_acrcache/_help.py +++ b/src/acrcache/azext_acrcache/_help.py @@ -39,6 +39,8 @@ text: az acr cache create -r myregistry -n MyRule -s docker.io/library/ubuntu -t ubuntu - name: Create a cache rule with a credential set. text: az acr cache create -r myregistry -n MyRule -s docker.io/library/ubuntu -t ubuntu -c MyCredSet + - name: Create a cache rule with a user-assigned managed identity. + text: az acr cache create -r myregistry -n MyRule -s upstreamacrregistry.azurecr-test.io -t acr-to-acr-cacherule --assign-identity /subscriptions/{subscription-id}/resourceGroups/{resource-group}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{identity-name} - name: Create a cache rule with artifact sync enabled and set a tag filter. text: az acr cache create -r myregistry -n MyRule -s docker.io/library/ubuntu -t ubuntu --sync activesync --starts-with v1 --ends-with beta - name: Create a cache rule with artifact sync enabled, set a tag filter, and specify platforms and sync referrers. @@ -64,6 +66,8 @@ text: az acr cache update -r myregistry -n MyRule -c NewCredSet - name: Remove a credential set from an existing cache rule. text: az acr cache update -r myregistry -n MyRule --remove-cred-set + - name: Update a cache rule with a user-assigned managed identity. + text: az acr cache update -r myregistry -n MyRule --assign-identity /subscriptions/{subscription-id}/resourceGroups/{resource-group}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{identity-name} - name: Enable artifact sync and set a tag filter. text: az acr cache update -r myregistry -n MyRule --sync activesync --starts-with v1 --ends-with beta - name: Enable artifact sync, set a tag filter, and specify platforms and sync referrers. diff --git a/src/acrcache/azext_acrcache/_params.py b/src/acrcache/azext_acrcache/_params.py index e8902a98607..679e03981a8 100644 --- a/src/acrcache/azext_acrcache/_params.py +++ b/src/acrcache/azext_acrcache/_params.py @@ -12,6 +12,7 @@ def load_arguments(self, _): c.argument('registry_name', options_list=['--registry', '-r'], help='The name of the container registry. It should be specified in lower case. You can configure the default registry name using `az configure --defaults acr=`') c.argument('name', options_list=['--name', '-n'], help='The name of the cache rule.') c.argument('cred_set', options_list=['--cred-set', '-c'], help='The name of the credential set.') + c.argument('assign_identity', options_list=['--assign-identity', '-i'], help='Resource ID of the user-assigned managed identity for authenticating with the ACR source registry. Must be in the same tenant as both registries. Format: /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{identityName}') c.argument('source_repo', options_list=['--source-repo', '-s'], help="The full source repository path such as 'docker.io/library/ubuntu'.") c.argument('target_repo', options_list=['--target-repo', '-t'], help="The target repository namespace such as 'ubuntu'.") c.argument('remove_cred_set', action="store_true", help='Optional boolean indicating whether to remove the credential set from the cache rule. False by default.') diff --git a/src/acrcache/azext_acrcache/cache.py b/src/acrcache/azext_acrcache/cache.py index 754512e30ae..383140eee78 100644 --- a/src/acrcache/azext_acrcache/cache.py +++ b/src/acrcache/azext_acrcache/cache.py @@ -4,6 +4,7 @@ # -------------------------------------------------------------------------------------------- # pylint: disable=line-too-long +import re from azure.cli.core.util import user_confirmation from knack.util import CLIError from azure.core.serialization import NULL as AzureCoreNull @@ -11,7 +12,8 @@ from .vendored_sdks.containerregistry.v2025_09_01_preview.generated.container_registry_management_client.models._models import ( CacheRule, CacheRuleProperties, CacheRuleUpdateParameters, CacheRuleUpdateProperties, ImportSource, ImportImageParameters, - PlatformFilter, ArtifactTypeFilter, TagFilter, ArtifactSyncFilterProperties + PlatformFilter, ArtifactTypeFilter, TagFilter, ArtifactSyncFilterProperties, + IdentityProperties, UserIdentityProperties ) def _create_kql(starts_with=None, ends_with=None, contains=None): @@ -50,6 +52,40 @@ def _separate_params(query): return starts_with, ends_with, contains +def process_assign_identity_parameter(assign_identity: str) -> IdentityProperties: + """ + Process assign identity parameter and return IdentityProperties object. + + :param assign_identity: User-assigned managed identity resource ID + :return: IdentityProperties object or None + """ + + if not assign_identity: + return None + + if not is_valid_user_assigned_managed_identity_resource_id(assign_identity): + raise CLIError(f"Invalid user-assigned managed identity resource ID: {assign_identity}") + + + identity_properties = IdentityProperties( + type="UserAssigned", + user_assigned_identities={ + assign_identity: UserIdentityProperties() + } + ) + return identity_properties + +def is_valid_user_assigned_managed_identity_resource_id(resource_id): + # format Validation logic for user-assigned managed identity resource ID + # include the full pattern of Microsoft.ManagedIdentity. + # check GUID format for subscription ID + # https://docs.microsoft.com/azure/azure-resource-manager/management/resource-name-rules#microsoftmanagedidentity + pattern = ( + r"^/subscriptions/[0-9a-zA-Z\-]{36}" + r"/resourceGroups/[^/]+" + r"/providers/Microsoft\.ManagedIdentity/userAssignedIdentities/[^/]+$" + ) + return bool(re.match(pattern, resource_id, re.IGNORECASE)) def acr_cache_show(cmd, client, @@ -96,6 +132,7 @@ def acr_cache_create(cmd, target_repo, resource_group_name=None, cred_set=None, + assign_identity=None, sync=False, starts_with=None, ends_with=None, @@ -145,6 +182,9 @@ def acr_cache_create(cmd, "--starts-with, --ends-with, --contains) require --sync activesync.") cred_set_id = AzureCoreNull if not cred_set else f'{registry.id}/credentialSets/{cred_set}' + + identity_properties = process_assign_identity_parameter(assign_identity) + tag = None if ':' in source_repo: @@ -213,7 +253,8 @@ def acr_cache_create(cmd, # Create cache rule with properties cache_rule = CacheRule( name=name, - properties=properties + properties=properties, + identity=identity_properties ) if tag is None and sync and not dry_run: @@ -233,6 +274,7 @@ def acr_cache_update_custom(cmd, name, resource_group_name=None, cred_set=None, + assign_identity=None, remove_cred_set=False, sync=None, starts_with=None, @@ -308,6 +350,9 @@ def acr_cache_update_custom(cmd, if cred_set is None and not remove_cred_set: cred_set_id = AzureCoreNull + # Process identity parameter + identity_properties = process_assign_identity_parameter(assign_identity) + # Handle artifact sync status - only change if explicitly provided if sync is not None: sync_mode = "ActiveSync" if sync.lower() == 'activesync' else "PassiveSync" @@ -380,12 +425,16 @@ def acr_cache_update_custom(cmd, return client.begin_create(resource_group_name=rg, registry_name=registry_name, cache_rule_name=name, - cache_rule_create_parameters=CacheRuleUpdateParameters(properties=updated_properties)) + cache_rule_create_parameters=CacheRuleUpdateParameters( + properties=updated_properties, + identity=identity_properties)) return client.begin_update(resource_group_name=rg, registry_name=registry_name, cache_rule_name=name, - cache_rule_update_parameters=CacheRuleUpdateParameters(properties=updated_properties)) + cache_rule_update_parameters=CacheRuleUpdateParameters( + properties=updated_properties, + identity=identity_properties)) def acr_cache_sync(cmd, diff --git a/src/acrcache/azext_acrcache/tests/latest/test_cache.py b/src/acrcache/azext_acrcache/tests/latest/test_cache.py index 853591b547c..75e5b5c853e 100644 --- a/src/acrcache/azext_acrcache/tests/latest/test_cache.py +++ b/src/acrcache/azext_acrcache/tests/latest/test_cache.py @@ -219,5 +219,200 @@ def test_acr_cache_sync_calls_import_image(self, mock_get_rg): self.assertEqual(params.target_tags, ["repo/target:tag1"]) self.assertEqual(params.source.cache_rule_resource_id, "ruleid") +class TestIdentityProcessing(unittest.TestCase): + """Test identity parameter processing functionality""" + + def test_process_assign_identity_parameter_none(self): + """Test process_assign_identity_parameter returns None when no identity provided""" + result = cache.process_assign_identity_parameter(None) + self.assertIsNone(result) + + result = cache.process_assign_identity_parameter("") + self.assertIsNone(result) + + def test_process_assign_identity_parameter_valid(self): + """Test process_assign_identity_parameter with valid resource ID""" + resource_id = "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/myRG/providers/Microsoft.ManagedIdentity/userAssignedIdentities/myIdentity" + + result = cache.process_assign_identity_parameter(resource_id) + + self.assertIsNotNone(result) + self.assertEqual(result.type, "UserAssigned") + self.assertIn(resource_id, result.user_assigned_identities) + self.assertIsInstance(result.user_assigned_identities[resource_id], cache.UserIdentityProperties) + + def test_process_assign_identity_parameter_invalid(self): + """Test process_assign_identity_parameter raises error for invalid resource ID""" + invalid_ids = [ + "invalid-resource-id", + "/resourceGroups/myRG/providers/Microsoft.ManagedIdentity/userAssignedIdentities/myIdentity", + "subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/myRG/providers/Microsoft.ManagedIdentity/userAssignedIdentities/myIdentity" + ] + + for invalid_id in invalid_ids: + with self.assertRaises(CLIError) as context: + cache.process_assign_identity_parameter(invalid_id) + self.assertIn("Invalid user-assigned managed identity resource ID", str(context.exception)) + + def test_is_valid_user_assigned_managed_identity_resource_id(self): + """Test resource ID validation function""" + # Valid resource IDs + valid_ids = [ + "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/myRG/providers/Microsoft.ManagedIdentity/userAssignedIdentities/myIdentity", + "/subscriptions/abcdefgh-1234-5678-9012-123456789012/resourceGroups/test-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/test-identity" + ] + + for valid_id in valid_ids: + self.assertTrue(cache.is_valid_user_assigned_managed_identity_resource_id(valid_id)) + + # Invalid resource IDs + invalid_ids = [ + "invalid-resource-id", + "/resourceGroups/myRG/providers/Microsoft.ManagedIdentity/userAssignedIdentities/myIdentity", + "subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/myRG/providers/Microsoft.ManagedIdentity/userAssignedIdentities/myIdentity", + "" + ] + + for invalid_id in invalid_ids: + self.assertFalse(cache.is_valid_user_assigned_managed_identity_resource_id(invalid_id)) + + +class TestCacheCreateWithIdentity(unittest.TestCase): + """Test cache creation with identity parameter""" + + def setUp(self): + """Set up test fixtures""" + self.cmd = mock.Mock() + self.cmd.cli_ctx = mock.Mock() + self.client = mock.Mock() + self.valid_identity_id = "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/myRG/providers/Microsoft.ManagedIdentity/userAssignedIdentities/myIdentity" + + @mock.patch('azext_acrcache.cache.get_registry_by_name') + @mock.patch('azext_acrcache.cache.user_confirmation') + def test_acr_cache_create_with_valid_identity(self, mock_confirmation, mock_get_registry): + """Test cache creation with valid identity""" + mock_registry = mock.Mock() + mock_registry.id = "/subscriptions/xxx/resourceGroups/rg1/providers/Microsoft.ContainerRegistry/registries/registry1" + mock_get_registry.return_value = (mock_registry, None) + + cache.acr_cache_create( + self.cmd, self.client, "mockRegistry", "mockCacheRule", "source/repo", "target/repo", + resource_group_name="mockrg", + assign_identity=self.valid_identity_id, + sync="activesync", + yes=True + ) + + # Verify client.begin_create was called + self.client.begin_create.assert_called_once() + + # Extract the cache rule from the call + call_args = self.client.begin_create.call_args[1] + cache_rule = call_args['cache_rule_create_parameters'] + + # Verify identity was set + self.assertIsNotNone(cache_rule.identity) + self.assertEqual(cache_rule.identity.type, "UserAssigned") + self.assertIn(self.valid_identity_id, cache_rule.identity.user_assigned_identities) + + @mock.patch('azext_acrcache.cache.get_registry_by_name') + def test_acr_cache_create_with_invalid_identity(self, mock_get_registry): + """Test cache creation with invalid identity raises error""" + mock_registry = mock.Mock() + mock_registry.id = "/subscriptions/xxx/resourceGroups/rg1/providers/Microsoft.ContainerRegistry/registries/registry1" + mock_get_registry.return_value = (mock_registry, None) + + with self.assertRaises(CLIError): + cache.acr_cache_create( + self.cmd, self.client, "mockRegistry", "mockCacheRule", "source/repo", "target/repo", + resource_group_name="mockrg", + assign_identity="invalid-identity-id" + ) + + @mock.patch('azext_acrcache.cache.get_registry_by_name') + @mock.patch('azext_acrcache.cache.user_confirmation') + def test_acr_cache_create_without_identity(self, mock_confirmation, mock_get_registry): + """Test cache creation without identity works normally""" + mock_registry = mock.Mock() + mock_registry.id = "/subscriptions/xxx/resourceGroups/rg1/providers/Microsoft.ContainerRegistry/registries/registry1" + mock_get_registry.return_value = (mock_registry, None) + + cache.acr_cache_create( + self.cmd, self.client, "mockRegistry", "mockCacheRule", "source/repo", "target/repo", + resource_group_name="mockrg", + sync="activesync", + yes=True + ) + + # Verify client.begin_create was called + self.client.begin_create.assert_called_once() + + # Extract the cache rule from the call + call_args = self.client.begin_create.call_args[1] + cache_rule = call_args['cache_rule_create_parameters'] + + # Verify identity is None + self.assertIsNone(cache_rule.identity) + +class TestCacheUpdateWithIdentity(unittest.TestCase): + """Test cache update with identity parameter""" + + def setUp(self): + """Set up test fixtures""" + self.cmd = mock.Mock() + self.cmd.cli_ctx = mock.Mock() + self.client = mock.Mock() + self.valid_identity_id = "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/myRG/providers/Microsoft.ManagedIdentity/userAssignedIdentities/myIdentity" + + # Set up mock cache rule + self.dummy_rule = mock.Mock() + self.dummy_rule.properties = mock.Mock() + self.dummy_rule.properties.sync_mode = "ActiveSync" + self.dummy_rule.properties.sync_referrers = "Disabled" + self.dummy_rule.properties.artifact_sync_filters = None + self.dummy_rule.properties.credential_set_resource_id = None + + @mock.patch('azext_acrcache.cache.get_registry_by_name') + @mock.patch('azext_acrcache.cache.user_confirmation') + def test_acr_cache_update_with_valid_identity(self, mock_confirmation, mock_get_registry): + """Test cache update with valid identity""" + mock_registry = mock.Mock() + mock_registry.id = "/subscriptions/xxx/resourceGroups/rg1/providers/Microsoft.ContainerRegistry/registries/registry1" + mock_get_registry.return_value = (mock_registry, "mockrg") + self.client.get.return_value = self.dummy_rule + + cache.acr_cache_update_custom( + self.cmd, self.client, "mockRegistry", "mockCacheRule", + assign_identity=self.valid_identity_id, + yes=True + ) + + # Verify client.begin_update was called + self.client.begin_update.assert_called_once() + + # Extract the update parameters from the call + call_args = self.client.begin_update.call_args[1] + update_params = call_args['cache_rule_update_parameters'] + + # Verify identity was set + self.assertIsNotNone(update_params.identity) + self.assertEqual(update_params.identity.type, "UserAssigned") + self.assertIn(self.valid_identity_id, update_params.identity.user_assigned_identities) + + @mock.patch('azext_acrcache.cache.get_registry_by_name') + def test_acr_cache_update_with_invalid_identity(self, mock_get_registry): + """Test cache update with invalid identity raises error""" + mock_registry = mock.Mock() + mock_registry.id = "/subscriptions/xxx/resourceGroups/rg1/providers/Microsoft.ContainerRegistry/registries/registry1" + mock_get_registry.return_value = (mock_registry, "mockrg") + self.client.get.return_value = self.dummy_rule + + with self.assertRaises(CLIError): + cache.acr_cache_update_custom( + self.cmd, self.client, "mockRegistry", "mockCacheRule", + assign_identity="invalid-identity-id" + ) + + if __name__ == '__main__': unittest.main() \ No newline at end of file diff --git a/src/acrcache/setup.py b/src/acrcache/setup.py index 28cff359c5e..48085d16ac0 100644 --- a/src/acrcache/setup.py +++ b/src/acrcache/setup.py @@ -14,7 +14,7 @@ logger.warn("Wheel is not available, disabling bdist_wheel hook") # HISTORY.rst entry. -VERSION = '1.0.0c6' +VERSION = '1.0.0c7' # The full list of classifiers is available at # https://pypi.python.org/pypi?%3Aaction=list_classifiers From ce87429e4cade119588484126ceb35e422961dd3 Mon Sep 17 00:00:00 2001 From: mabelegba Date: Tue, 25 Nov 2025 09:17:17 -0500 Subject: [PATCH 2/3] Validate sync_referrers requires activesync for cache rule create and update --- src/acrcache/HISTORY.rst | 10 +- src/acrcache/README.rst | 99 +++++++++++++++++-- src/acrcache/azext_acrcache/_help.py | 28 ++++-- src/acrcache/azext_acrcache/cache.py | 9 +- .../azext_acrcache/tests/latest/test_cache.py | 16 +++ src/acrcache/setup.py | 2 +- 6 files changed, 141 insertions(+), 23 deletions(-) diff --git a/src/acrcache/HISTORY.rst b/src/acrcache/HISTORY.rst index 9348829979d..ebb32056bd0 100644 --- a/src/acrcache/HISTORY.rst +++ b/src/acrcache/HISTORY.rst @@ -2,8 +2,14 @@ Release History =============== +1.1.2 - version 1.0.0c8 +++++++ +* **BUGFIX**: Resolved issue with sync-referrers enabled without sync activesync + * Added validation to ensure `--sync-referrers` can only be used with `--sync activesync` + * Ensured proper validation and assignment of managed identities in `az acr cache create` and `az acr cache update` commands + -1.2.0 - version 1.0.0c7 +1.1.1 - version 1.0.0c7 ++++++ * **FEATURE**: Added `--assign-identity` parameter support for cache rules * `az acr cache create --assign-identity` - Create cache rules with user-assigned managed identities @@ -13,7 +19,7 @@ Release History * **ENHANCEMENT**: Improved error handling and validation for identity parameters * **TESTING**: Added comprehensive unit test coverage for identity processing functionality -1.10 - version 1.0.0c6 +1.1.0 - version 1.0.0c6 ++++++ * **BREAKING**: Migrated to Container Registry SDK v2025-09-01-preview * Updated SDK imports from v2025_07_01_preview to v2025_09_01_preview diff --git a/src/acrcache/README.rst b/src/acrcache/README.rst index 033e38b4180..24b040d95cf 100644 --- a/src/acrcache/README.rst +++ b/src/acrcache/README.rst @@ -8,6 +8,8 @@ The `acrcache` extension adds support for managing Azure Container Registry (ACR ## Features - Create, update, list, show, and delete cache rules for ACR. +- **Managed Identity Authentication**: Configure user-assigned managed identities for secure cross-registry authentication. + - **--assign-identity**: Specify a user-assigned managed identity for authenticating with source registries. - Configure artifact sync with flexible filters: - **--platforms**: Filter which platforms to sync (e.g., `linux/amd64`, `linux/arm64`). - **--sync-referrers**: Enable or disable syncing of referrers. @@ -115,28 +117,51 @@ az acr cache list -r ### Create a cache rule +#### Basic cache rule (pull-through cache) ```sh -az acr cache create -r -n -s -t \ - --sync true --platforms linux/amd64,linux/arm64 --sync-referrers enabled \ - --include-artifact-types images,notary-project-signature --exclude-image-types +az acr cache create -r -n -s -t +``` + +#### Cache rule with credential set ```sh -az acr cache list -r -az acr cache list -r +az acr cache create -r -n -s -t -c ``` -### Create a cache rule +#### Cache rule with user-assigned managed identity (ACR-to-ACR authentication) +```sh +az acr cache create -r -n -s .azurecr.io/ -t \ + --assign-identity /subscriptions//resourceGroups//providers/Microsoft.ManagedIdentity/userAssignedIdentities/ +``` +#### Advanced cache rule with artifact sync and filtering ```sh az acr cache create -r -n -s -t \ - --sync true --platforms linux/amd64,linux/arm64 --sync-referrers enabled \ - --include-artifact-types images,notary-project-signature --exclude-image-types + --sync activesync --platforms linux/amd64,linux/arm64 --sync-referrers enabled \ + --include-artifact-types images,notary-project-signature ``` ### Update a cache rule +#### Update credential set ```sh -az acr cache update -r -n --platforms linux/amd64 --sync-referrers disabled \ - --include-artifact-types images --exclude-artifact-types +az acr cache update -r -n -c +``` + +#### Update with managed identity +```sh +az acr cache update -r -n \ + --assign-identity /subscriptions//resourceGroups//providers/Microsoft.ManagedIdentity/userAssignedIdentities/ +``` + +#### Update sync settings and filters +```sh +az acr cache update -r -n --sync activesync --platforms linux/amd64 \ + --sync-referrers enabled --include-artifact-types images +``` + +#### Remove credential set +```sh +az acr cache update -r -n --remove-cred-set ``` ### Show a cache rule @@ -145,12 +170,66 @@ az acr cache update -r -n --platforms linux/amd64 -- az acr cache show -r -n ``` +### Sync a specific tag immediately + +```sh +az acr cache sync -r -n --image +``` + ### Delete a cache rule ```sh az acr cache delete -r -n ``` +## Authentication Methods + +### User-Assigned Managed Identity + +The `--assign-identity` parameter enables secure authentication between Azure Container Registries using user-assigned managed identities. This is particularly useful for: + +- **Cross-subscription ACR caching**: Cache images from ACRs in different subscriptions +- **Cross-tenant scenarios**: Secure authentication across Azure AD tenants +- **Enhanced security**: Eliminates the need for credential sets in many scenarios + +#### Requirements +- Both source and target registries must be in the same Azure AD tenant +- The managed identity must have `AcrPull` permissions on the source registry +- The managed identity must be in the same subscription as the target registry + +#### Resource ID Format +``` +/subscriptions/{subscription-id}/resourceGroups/{resource-group}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{identity-name} +``` + +#### Example Setup +1. Create a user-assigned managed identity +2. Assign `AcrPull` role to the identity on the source registry +3. Create cache rule with the identity resource ID + +```sh +# Create managed identity +az identity create -g -n + +# Assign AcrPull permissions to source registry +az role assignment create --assignee --role AcrPull --scope + +# Create cache rule with managed identity +az acr cache create -r -n -s .azurecr.io/ -t \ + --assign-identity /subscriptions//resourceGroups//providers/Microsoft.ManagedIdentity/userAssignedIdentities/ +``` + +## Artifact Sync and Filtering + +### Sync Modes +- **PassiveSync** (default): Pull-through cache behavior - images are cached when pulled +- **ActiveSync**: Proactive synchronization - images are automatically pulled and cached + +### Important Notes +- **--sync-referrers enabled** requires **--sync activesync** +- Artifact filtering parameters (--platforms, --include-artifact-types, etc.) require **--sync activesync** +- Tag filters (--starts-with, --ends-with, --contains) require **--sync activesync** + ## Minimum Azure CLI Version This extension requires Azure CLI version **2.57.0** or higher. diff --git a/src/acrcache/azext_acrcache/_help.py b/src/acrcache/azext_acrcache/_help.py index 043bd8a5819..4c265a185b9 100644 --- a/src/acrcache/azext_acrcache/_help.py +++ b/src/acrcache/azext_acrcache/_help.py @@ -23,9 +23,6 @@ helps['acr cache list'] = """ type: command short-summary: List the cache rules in an Azure Container Registry. -long-summary: | - NOTE: The parameters --platforms, --sync-referrers, --include-artifact-types, and --exclude-artifact-types are not yet implemented. Using any of these parameters will return a 'not implemented' error message. - examples: - name: List the cache rules in an Azure Container Registry. text: az acr cache list -r myregistry @@ -34,6 +31,16 @@ helps['acr cache create'] = """ type: command short-summary: Create a cache rule. +parameters: + - name: --assign-identity + short-summary: Resource ID of the user-assigned managed identity. + long-summary: | + Resource ID of the user-assigned managed identity for authenticating with the ACR source registry. + Must be in the same tenant as both registries. + Format: /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{identityName} + - name: --sync-referrers + short-summary: Enable or disable sync referrers. + long-summary: Requires --sync activesync to be enabled. examples: - name: Create a cache rule without a credential set. text: az acr cache create -r myregistry -n MyRule -s docker.io/library/ubuntu -t ubuntu @@ -57,10 +64,17 @@ helps['acr cache update'] = """ type: command -short-summary: Update the credential set on a cache rule. -long-summary: | - NOTE: The parameters --platforms, --sync-referrers, --include-artifact-types, and --exclude-artifact-types are not yet implemented. Using any of these parameters will return a 'not implemented' error message. - +short-summary: Update a cache rule. +parameters: + - name: --assign-identity + short-summary: Resource ID of the user-assigned managed identity. + long-summary: | + Resource ID of the user-assigned managed identity for authenticating with the ACR source registry. + Must be in the same tenant as both registries. + Format: /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{identityName} + - name: --sync-referrers + short-summary: Enable or disable syncing of referrers. + long-summary: Requires --sync activesync to be enabled. examples: - name: Change or add a credential set to an existing cache rule. text: az acr cache update -r myregistry -n MyRule -c NewCredSet diff --git a/src/acrcache/azext_acrcache/cache.py b/src/acrcache/azext_acrcache/cache.py index 383140eee78..3e4862d760f 100644 --- a/src/acrcache/azext_acrcache/cache.py +++ b/src/acrcache/azext_acrcache/cache.py @@ -164,8 +164,10 @@ def acr_cache_create(cmd, sync_str = sync if sync else None sync_referrers_str = "Enabled" if sync_referrers and sync_referrers.lower() == 'enabled' else "Disabled" - if sync_referrers and sync_referrers.lower() == 'enabled' and sync and sync.lower() != 'activesync': - raise CLIError("Syncing referrers requires sync to be set to 'activesync'. Please update your cache rule configuration.") + # Validate sync_referrers requires activesync - check both when sync is provided and when it's not + if sync_referrers and sync_referrers.lower() == 'enabled': + if not sync or sync.lower() != 'activesync': + raise CLIError("Syncing referrers requires sync to be set to 'activesync'. Please update your cache rule configuration.") if include_artifact_types and exclude_artifact_types: raise CLIError("You cannot specify both include_artifact_types and exclude_artifact_types. Please choose one.") @@ -305,7 +307,8 @@ def acr_cache_update_custom(cmd, #check both existing sync mode (when not changing sync) AND new sync value (when updating sync) isActiveSync = (sync is None and sync_mode and sync_mode.lower() == 'activesync') or (sync and sync.lower() == 'activesync') - if sync_referrers and sync_referrers.lower() == 'enabled' and not isActiveSync and sync and sync.lower() != 'activesync': + # Validate sync_referrers requires activesync + if sync_referrers and sync_referrers.lower() == 'enabled' and not isActiveSync: raise CLIError("Syncing referrers requires sync to be set to 'activesync'. Please update your cache rule configuration.") # Warn if mutually exclusive parameters are provided diff --git a/src/acrcache/azext_acrcache/tests/latest/test_cache.py b/src/acrcache/azext_acrcache/tests/latest/test_cache.py index 75e5b5c853e..819700b8c8e 100644 --- a/src/acrcache/azext_acrcache/tests/latest/test_cache.py +++ b/src/acrcache/azext_acrcache/tests/latest/test_cache.py @@ -138,6 +138,22 @@ def test_acr_cache_create_sync_referrers_requires_activesync(self, mock_get_regi sync_referrers="enabled" ) + @mock.patch('azext_acrcache.cache.get_registry_by_name') + def test_acr_cache_create_sync_referrers_without_sync_parameter_fails(self, mock_get_registry): + """Test that sync referrers fails when no sync parameter is provided""" + mock_registry = mock.Mock() + mock_registry.id = "/subscriptions/xxx/resourceGroups/rg1/providers/xxx" + mock_get_registry.return_value = (mock_registry, None) + + with self.assertRaises(CLIError) as cm: + cache.acr_cache_create( + self.cmd, self.client, "mockRegistry", "mockCacheRule1", "mockRepo1", "mockRepo2", + resource_group_name="mockrg", + sync_referrers="enabled" # No sync parameter provided + ) + + self.assertIn("Syncing referrers requires sync to be set to 'activesync'", str(cm.exception)) + class TestCacheUpdateValidation(unittest.TestCase): """Test cache update validation logic""" diff --git a/src/acrcache/setup.py b/src/acrcache/setup.py index 48085d16ac0..993af0cf6fe 100644 --- a/src/acrcache/setup.py +++ b/src/acrcache/setup.py @@ -14,7 +14,7 @@ logger.warn("Wheel is not available, disabling bdist_wheel hook") # HISTORY.rst entry. -VERSION = '1.0.0c7' +VERSION = '1.0.0c8' # The full list of classifiers is available at # https://pypi.python.org/pypi?%3Aaction=list_classifiers From a6c9a398f1e8b8b6cb8d8bb671fd31e19b598435 Mon Sep 17 00:00:00 2001 From: mabelegba Date: Wed, 26 Nov 2025 16:35:58 -0500 Subject: [PATCH 3/3] bug fixes and update readme and test --- src/acrcache/HISTORY.rst | 8 ++-- src/acrcache/README.rst | 11 ++--- src/acrcache/azext_acrcache/_help.py | 2 +- src/acrcache/azext_acrcache/cache.py | 40 +++++++++++++------ .../azext_acrcache/tests/latest/test_cache.py | 19 ++++++++- 5 files changed, 55 insertions(+), 25 deletions(-) diff --git a/src/acrcache/HISTORY.rst b/src/acrcache/HISTORY.rst index ebb32056bd0..1cc6fb8020f 100644 --- a/src/acrcache/HISTORY.rst +++ b/src/acrcache/HISTORY.rst @@ -2,24 +2,24 @@ Release History =============== -1.1.2 - version 1.0.0c8 +1.0.0c8 ++++++ * **BUGFIX**: Resolved issue with sync-referrers enabled without sync activesync * Added validation to ensure `--sync-referrers` can only be used with `--sync activesync` * Ensured proper validation and assignment of managed identities in `az acr cache create` and `az acr cache update` commands -1.1.1 - version 1.0.0c7 +1.0.0c7 ++++++ * **FEATURE**: Added `--assign-identity` parameter support for cache rules * `az acr cache create --assign-identity` - Create cache rules with user-assigned managed identities * `az acr cache update --assign-identity` - Update existing cache rules with managed identities - * Enables secure authentication for ACR-to-ACR caching across subscriptions and tenants + * Enables secure authentication for ACR-to-ACR caching across subscriptions within the same tenant * Supports Azure resource ID format validation for managed identity resources * **ENHANCEMENT**: Improved error handling and validation for identity parameters * **TESTING**: Added comprehensive unit test coverage for identity processing functionality -1.1.0 - version 1.0.0c6 +1.0.0c6 ++++++ * **BREAKING**: Migrated to Container Registry SDK v2025-09-01-preview * Updated SDK imports from v2025_07_01_preview to v2025_09_01_preview diff --git a/src/acrcache/README.rst b/src/acrcache/README.rst index 24b040d95cf..3456329f9b4 100644 --- a/src/acrcache/README.rst +++ b/src/acrcache/README.rst @@ -188,14 +188,15 @@ az acr cache delete -r -n The `--assign-identity` parameter enables secure authentication between Azure Container Registries using user-assigned managed identities. This is particularly useful for: -- **Cross-subscription ACR caching**: Cache images from ACRs in different subscriptions -- **Cross-tenant scenarios**: Secure authentication across Azure AD tenants +- **Cross-subscription ACR caching**: Cache images from ACRs in different subscriptions within the same tenant - **Enhanced security**: Eliminates the need for credential sets in many scenarios +- **Simplified authentication**: Uses Azure's managed identity infrastructure for secure access #### Requirements -- Both source and target registries must be in the same Azure AD tenant -- The managed identity must have `AcrPull` permissions on the source registry -- The managed identity must be in the same subscription as the target registry +- **Same tenant**: Both source and target registries must be in the same Azure AD tenant +- **Permissions**: The managed identity must have `AcrPull` permissions on the source registry +- **Identity location**: The managed identity must be in the same subscription as the target registry +- **Cross-subscription support**: Registries can be in different subscriptions within the same tenant #### Resource ID Format ``` diff --git a/src/acrcache/azext_acrcache/_help.py b/src/acrcache/azext_acrcache/_help.py index 4c265a185b9..5a7b6615e73 100644 --- a/src/acrcache/azext_acrcache/_help.py +++ b/src/acrcache/azext_acrcache/_help.py @@ -46,7 +46,7 @@ text: az acr cache create -r myregistry -n MyRule -s docker.io/library/ubuntu -t ubuntu - name: Create a cache rule with a credential set. text: az acr cache create -r myregistry -n MyRule -s docker.io/library/ubuntu -t ubuntu -c MyCredSet - - name: Create a cache rule with a user-assigned managed identity. + - name: Create a cache rule with a user-assigned managed identity (using test registry domain). text: az acr cache create -r myregistry -n MyRule -s upstreamacrregistry.azurecr-test.io -t acr-to-acr-cacherule --assign-identity /subscriptions/{subscription-id}/resourceGroups/{resource-group}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{identity-name} - name: Create a cache rule with artifact sync enabled and set a tag filter. text: az acr cache create -r myregistry -n MyRule -s docker.io/library/ubuntu -t ubuntu --sync activesync --starts-with v1 --ends-with beta diff --git a/src/acrcache/azext_acrcache/cache.py b/src/acrcache/azext_acrcache/cache.py index 3e4862d760f..ad2c58a2482 100644 --- a/src/acrcache/azext_acrcache/cache.py +++ b/src/acrcache/azext_acrcache/cache.py @@ -9,6 +9,7 @@ from knack.util import CLIError from azure.core.serialization import NULL as AzureCoreNull from azure.cli.command_modules.acr._utils import get_resource_group_name_by_registry_name, get_registry_by_name +from azure.mgmt.core.tools import parse_resource_id, is_valid_resource_id from .vendored_sdks.containerregistry.v2025_09_01_preview.generated.container_registry_management_client.models._models import ( CacheRule, CacheRuleProperties, CacheRuleUpdateParameters, CacheRuleUpdateProperties, ImportSource, ImportImageParameters, @@ -16,6 +17,10 @@ IdentityProperties, UserIdentityProperties ) +# Constants for managed identity resource validation +MANAGED_IDENTITY_RESOURCE_PROVIDER = "Microsoft.ManagedIdentity" +USER_ASSIGNED_IDENTITY_RESOURCE_TYPE = "userAssignedIdentities" + def _create_kql(starts_with=None, ends_with=None, contains=None): if not starts_with and not ends_with and not contains: return "Tags" @@ -53,8 +58,7 @@ def _separate_params(query): return starts_with, ends_with, contains def process_assign_identity_parameter(assign_identity: str) -> IdentityProperties: - """ - Process assign identity parameter and return IdentityProperties object. + """Process assign identity parameter and return IdentityProperties object. :param assign_identity: User-assigned managed identity resource ID :return: IdentityProperties object or None @@ -66,7 +70,6 @@ def process_assign_identity_parameter(assign_identity: str) -> IdentityPropertie if not is_valid_user_assigned_managed_identity_resource_id(assign_identity): raise CLIError(f"Invalid user-assigned managed identity resource ID: {assign_identity}") - identity_properties = IdentityProperties( type="UserAssigned", user_assigned_identities={ @@ -76,16 +79,27 @@ def process_assign_identity_parameter(assign_identity: str) -> IdentityPropertie return identity_properties def is_valid_user_assigned_managed_identity_resource_id(resource_id): - # format Validation logic for user-assigned managed identity resource ID - # include the full pattern of Microsoft.ManagedIdentity. - # check GUID format for subscription ID - # https://docs.microsoft.com/azure/azure-resource-manager/management/resource-name-rules#microsoftmanagedidentity - pattern = ( - r"^/subscriptions/[0-9a-zA-Z\-]{36}" - r"/resourceGroups/[^/]+" - r"/providers/Microsoft\.ManagedIdentity/userAssignedIdentities/[^/]+$" - ) - return bool(re.match(pattern, resource_id, re.IGNORECASE)) + """ + Validate user-assigned managed identity resource ID using Azure's built-in utilities. + + :param resource_id: Resource ID to validate + :return: True if valid, False otherwise + """ + if not is_valid_resource_id(resource_id): + return False + + try: + parsed = parse_resource_id(resource_id) + # Ensure it's specifically a Microsoft.ManagedIdentity userAssignedIdentities resource + return ( + parsed.get("namespace") == MANAGED_IDENTITY_RESOURCE_PROVIDER and + ( + parsed.get("type") == USER_ASSIGNED_IDENTITY_RESOURCE_TYPE or + parsed.get("resource_type") == USER_ASSIGNED_IDENTITY_RESOURCE_TYPE + ) + ) + except Exception: + return False def acr_cache_show(cmd, client, diff --git a/src/acrcache/azext_acrcache/tests/latest/test_cache.py b/src/acrcache/azext_acrcache/tests/latest/test_cache.py index 819700b8c8e..9e9bb5f72ee 100644 --- a/src/acrcache/azext_acrcache/tests/latest/test_cache.py +++ b/src/acrcache/azext_acrcache/tests/latest/test_cache.py @@ -275,7 +275,7 @@ def test_is_valid_user_assigned_managed_identity_resource_id(self): # Valid resource IDs valid_ids = [ "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/myRG/providers/Microsoft.ManagedIdentity/userAssignedIdentities/myIdentity", - "/subscriptions/abcdefgh-1234-5678-9012-123456789012/resourceGroups/test-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/test-identity" + "/subscriptions/abcdefab-1234-5678-9012-123456789012/resourceGroups/test-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/test-identity" ] for valid_id in valid_ids: @@ -286,11 +286,26 @@ def test_is_valid_user_assigned_managed_identity_resource_id(self): "invalid-resource-id", "/resourceGroups/myRG/providers/Microsoft.ManagedIdentity/userAssignedIdentities/myIdentity", "subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/myRG/providers/Microsoft.ManagedIdentity/userAssignedIdentities/myIdentity", - "" + "", + # Wrong resource provider + "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/myRG/providers/Microsoft.Storage/storageAccounts/mystorage", + # Wrong resource type + "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/myRG/providers/Microsoft.ManagedIdentity/systemAssignedIdentities/myIdentity", ] for invalid_id in invalid_ids: self.assertFalse(cache.is_valid_user_assigned_managed_identity_resource_id(invalid_id)) + + # Edge cases that are technically invalid but accepted by Azure's parsing utilities + potentially_invalid_but_accepted_ids = [ + "/subscriptions/abcdefgh-1234-5678-9012-123456789012/resourceGroups/myRG/providers/Microsoft.ManagedIdentity/userAssignedIdentities/myIdentity", # contains g,h + "/subscriptions/notauuid/resourceGroups/myRG/providers/Microsoft.ManagedIdentity/userAssignedIdentities/myIdentity", # not UUID format + ] + + # These are accepted by Azure's parsing utilities (which is by design) + for accepted_id in potentially_invalid_but_accepted_ids: + result = cache.is_valid_user_assigned_managed_identity_resource_id(accepted_id) + class TestCacheCreateWithIdentity(unittest.TestCase):