diff --git a/src/acrcache/HISTORY.rst b/src/acrcache/HISTORY.rst index ab7933b2e88..1cc6fb8020f 100644 --- a/src/acrcache/HISTORY.rst +++ b/src/acrcache/HISTORY.rst @@ -2,6 +2,45 @@ Release History =============== +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.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 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.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/README.rst b/src/acrcache/README.rst index 033e38b4180..3456329f9b4 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,67 @@ 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 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 +- **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 +``` +/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 e545eedcf02..5a7b6615e73 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,11 +31,23 @@ 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 - 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 (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 - name: Create a cache rule with artifact sync enabled, set a tag filter, and specify platforms and sync referrers. @@ -55,15 +64,24 @@ 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 - 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..ad2c58a2482 100644 --- a/src/acrcache/azext_acrcache/cache.py +++ b/src/acrcache/azext_acrcache/cache.py @@ -4,16 +4,23 @@ # -------------------------------------------------------------------------------------------- # 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 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, - PlatformFilter, ArtifactTypeFilter, TagFilter, ArtifactSyncFilterProperties + PlatformFilter, ArtifactTypeFilter, TagFilter, ArtifactSyncFilterProperties, + 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" @@ -50,6 +57,49 @@ 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): + """ + 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, @@ -96,6 +146,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, @@ -127,8 +178,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.") @@ -145,6 +198,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 +269,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 +290,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, @@ -263,7 +321,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 @@ -308,6 +367,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 +442,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..9e9bb5f72ee 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""" @@ -219,5 +235,215 @@ 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/abcdefab-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", + "", + # 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): + """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..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.0c6' +VERSION = '1.0.0c8' # The full list of classifiers is available at # https://pypi.python.org/pypi?%3Aaction=list_classifiers