Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion src/acrcache/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@

Release History
===============
1.0.2
++++++
* **BREAKING**: Migrated sync command to dedicated sync API endpoint
* Updated `az acr cache sync` command to use new cacheRuleSyncParameter endpoint instead of importImage
* Changed from Long Running Operation (LRO) pattern to fire-and-forget queuing model
* Added messaging to inform users that sync requests are queued and will complete asynchronously

* **FEATURE**: Added `--sync-tag-if-deleted` parameter for enhanced sync control
* New optional flag for `az acr cache sync` command to control behavior with deleted tags
* When enabled, allows syncing tags that have been deleted from the source registry
* sync_tag_if_deleted parameter defaults to false when omitted

* **ENHANCEMENT**: Improved parameter handling and validation
* Removed unused imports (ImportSource, ImportImageParameters) from legacy implementation

1.0.1
++++++
Expand Down Expand Up @@ -63,4 +77,4 @@ Release History

1.0.0
++++++
* Initial release.
* Initial release.
1 change: 1 addition & 0 deletions src/acrcache/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ Manual Sync Parameters
~~~~~~~~~~~~~~~~~~~~~~

- ``--tag``: The name of the tag to sync immediately (requires existing cache rule with artifact sync enabled)
- ``--sync-tag-if-deleted``: Flag to indicate whether the tag should be synced if it has already been deleted from the source registry (defaults to false)

Installation
------------
Expand Down
1 change: 1 addition & 0 deletions src/acrcache/azext_acrcache/_client_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ def cf_acrcache(cli_ctx, *_):
subscription_id = get_subscription_id(cli_ctx)
return get_mgmt_service_client(cli_ctx, ContainerRegistryManagementClient, subscription_id=subscription_id).cache_rules


def cf_acrreg(cli_ctx, *_):
from azure.cli.core.commands.client_factory import get_mgmt_service_client, get_subscription_id
from azext_acrcache.vendored_sdks.containerregistry.v2025_09_01_preview.generated.container_registry_management_client import ContainerRegistryManagementClient
Expand Down
10 changes: 5 additions & 5 deletions src/acrcache/azext_acrcache/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@
- 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.
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.
Expand Down Expand Up @@ -71,8 +71,8 @@
- 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.
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.
Expand All @@ -83,7 +83,7 @@
- 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}
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 --tag-starts-with v1 --tag-ends-with beta
- name: Enable artifact sync, set a tag filter, and specify platforms and sync referrers.
Expand Down
3 changes: 2 additions & 1 deletion src/acrcache/azext_acrcache/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,5 @@ def load_arguments(self, _):
c.argument('exclude_image_types', help='A list of image types to exclude from the sync. e.g. application/vnd.docker.distribution.manifest.v2+json')

with self.argument_context('acr cache sync') as c:
c.argument('tag', help='The name of the tag you want to sync immediately. Can only be used if artifact sync is enabled and the tag is within any specified tag filter.', deprecate_info=None)
c.argument('tag', help='The name of the tag you want to sync immediately. Can only be used if artifact sync is enabled and the tag is within any specified tag filter.', deprecate_info=None)
c.argument('sync_tag_if_deleted', help='Flag to indicate whether the tag should be sync\'d if it has already been deleted. Defaults to false.', action='store_true')
117 changes: 59 additions & 58 deletions src/acrcache/azext_acrcache/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,27 @@
# pylint: disable=line-too-long

import re
from azure.cli.core.util import user_confirmation

from knack.util import CLIError
from knack.log import get_logger
from azure.core.serialization import NULL as AzureCoreNull
from azure.cli.core.util import user_confirmation
from azure.cli.command_modules.acr._utils import get_resource_group_name_by_registry_name, get_registry_by_name
from azure.core.serialization import NULL as AzureCoreNull
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,
IdentityProperties, UserIdentityProperties
CacheRule, CacheRuleProperties, CacheRuleUpdateParameters, CacheRuleUpdateProperties,
PlatformFilter, ArtifactTypeFilter, TagFilter, ArtifactSyncFilterProperties, IdentityProperties, UserIdentityProperties,
CacheRuleSyncParameters
)


# Constants for managed identity resource validation
MANAGED_IDENTITY_RESOURCE_PROVIDER = "Microsoft.ManagedIdentity"
USER_ASSIGNED_IDENTITY_RESOURCE_TYPE = "userAssignedIdentities"

logger = get_logger(__name__)


def _create_kql(starts_with=None, ends_with=None, contains=None, tag=None):
if not starts_with and not ends_with and not contains and not tag:
return "Tags"
Expand All @@ -44,7 +46,6 @@ def _create_kql(starts_with=None, ends_with=None, contains=None, tag=None):
return query



def process_tag_filters(tag, starts_with, ends_with, contains,
tag_equals, tag_starts_with, tag_ends_with, tag_contains):
"""
Expand All @@ -55,9 +56,10 @@ def process_tag_filters(tag, starts_with, ends_with, contains,
final_starts_with = tag_starts_with if tag_starts_with is not None else starts_with
final_ends_with = tag_ends_with if tag_ends_with is not None else ends_with
final_contains = tag_contains if tag_contains is not None else contains

return final_tag, final_starts_with, final_ends_with, final_contains


def process_assign_identity_parameter(assign_identity: str) -> IdentityProperties:
"""Process assign identity parameter and return IdentityProperties object.

Expand All @@ -67,7 +69,7 @@ def process_assign_identity_parameter(assign_identity: str) -> IdentityPropertie

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}")

Expand All @@ -78,17 +80,18 @@ def process_assign_identity_parameter(assign_identity: str) -> IdentityPropertie
}
)
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
Expand All @@ -102,6 +105,7 @@ def is_valid_user_assigned_managed_identity_resource_id(resource_id):
except Exception:
return False


def acr_cache_show(cmd,
client,
registry_name,
Expand Down Expand Up @@ -169,11 +173,11 @@ def acr_cache_create(cmd,

registry, _ = get_registry_by_name(cmd.cli_ctx, registry_name, resource_group_name)

#extract resource group from registry id
# extract resource group from registry id
if resource_group_name:
rg = resource_group_name
else:
#extract resource group from registry id
# extract resource group from registry id

match = re.search(r'/resourceGroups/([^/]+)/', registry.id)
rg = match.group(1) if match else None
Expand Down Expand Up @@ -204,14 +208,14 @@ def acr_cache_create(cmd,
if include_image_types and exclude_image_types:
raise CLIError("You cannot specify both include_image_types and exclude_image_types. Please choose one.")

#validate that filter parameters require sync to be enabled
if sync and sync.lower() != 'activesync' and (include_artifact_types or exclude_artifact_types or
include_image_types or exclude_image_types or
platforms or processed_starts_with or processed_ends_with or processed_contains or processed_tag):
# validate that filter parameters require sync to be enabled
if sync and sync.lower() != 'activesync' and (include_artifact_types or exclude_artifact_types or
include_image_types or exclude_image_types or
platforms or processed_starts_with or processed_ends_with or processed_contains or processed_tag):
raise CLIError("Artifact sync filters (--include-artifact-types, --exclude-artifact-types, "
"--include-image-types, --exclude-image-types, --platforms, "
"--starts-with, --ends-with, --contains, --tag, "
"--tag-equals, --tag-starts-with, --tag-ends-with, --tag-contains) require --sync activesync.")
"--include-image-types, --exclude-image-types, --platforms, "
"--starts-with, --ends-with, --contains, --tag, "
"--tag-equals, --tag-starts-with, --tag-ends-with, --tag-contains) require --sync activesync.")

cred_set_id = AzureCoreNull if not cred_set else f'{registry.id}/credentialSets/{cred_set}'

Expand All @@ -221,7 +225,7 @@ def acr_cache_create(cmd,
if ':' in source_repo:
raise CLIError("Source repository should not include a tag. Please use the --tag-equals parameter to specify tag for filtering.")

#create artifact sync filters object
# create artifact sync filters object
artifact_sync_filters = None

if sync and sync.lower() == 'activesync':
Expand Down Expand Up @@ -271,7 +275,7 @@ def acr_cache_create(cmd,
query=kql_str
)

#create cacheRuleProperties object
# create cacheRuleProperties object
properties = CacheRuleProperties(
credential_set_resource_id=cred_set_id,
source_repository=source_repo,
Expand Down Expand Up @@ -329,18 +333,18 @@ def acr_cache_update_custom(cmd,

registry, rg = get_registry_by_name(cmd.cli_ctx, registry_name, resource_group_name)

#fetch existing cacheRule
# fetch existing cacheRule
cache_rule = client.get(resource_group_name=rg,
registry_name=registry_name,
cache_rule_name=name)

#extract existing properties
# extract existing properties
properties = cache_rule.properties
sync_mode = properties.sync_mode
sync_referrers_status = properties.sync_referrers

#check if activesync is enabled
#check both existing sync mode (when not changing sync) AND new sync value (when updating sync)
# check if activesync is enabled
# check both existing sync mode (when not changing sync) AND new sync value (when updating sync)
is_active_sync = (sync is None and sync_mode and sync_mode.lower() == 'activesync') or (sync and sync.lower() == 'activesync')

# Validate sync_referrers requires activesync
Expand All @@ -358,14 +362,14 @@ def acr_cache_update_custom(cmd,
tag, starts_with, ends_with, contains,
tag_equals, tag_starts_with, tag_ends_with, tag_contains
)

# Initialize filter objects
updated_tags = None
updated_platforms = None
updated_artifact_types = None
updated_image_types = None

#preserve old artifact sync filters
# preserve old artifact sync filters
preserve_filters = (properties.artifact_sync_filters is not None and is_active_sync)

if preserve_filters:
Expand All @@ -385,7 +389,7 @@ def acr_cache_update_custom(cmd,
if properties.artifact_sync_filters.image_types and not include_image_types and not exclude_image_types:
updated_image_types = properties.artifact_sync_filters.image_types

#Handle credential sets update
# Handle credential sets update
cred_set_id = properties.credential_set_resource_id
if remove_cred_set:
cred_set_id = AzureCoreNull
Expand All @@ -405,7 +409,7 @@ def acr_cache_update_custom(cmd,
if sync_referrers is not None:
sync_referrers_status = "Enabled" if sync_referrers and sync_referrers.lower() == 'enabled' else "Disabled"

#update artifact sync filters object
# update artifact sync filters object
updated_artifact_sync_filters = None
if is_active_sync:
if processed_starts_with or processed_ends_with or processed_contains or processed_tag:
Expand Down Expand Up @@ -447,18 +451,18 @@ def acr_cache_update_custom(cmd,
values=exclude_image_list
)

#create artifactSyncFilterProperties object if any filter is set
# create artifactSyncFilterProperties object if any filter is set
updated_artifact_sync_filters = ArtifactSyncFilterProperties(
tags=updated_tags,
platforms=updated_platforms,
artifact_types=updated_artifact_types,
image_types=updated_image_types
)

#create updated cache rule properties
# create updated cache rule properties
updated_properties = CacheRuleUpdateProperties(
credential_set_resource_id= cred_set_id,
sync_mode= sync_mode,
credential_set_resource_id=cred_set_id,
sync_mode=sync_mode,
sync_referrers=sync_referrers_status,
artifact_sync_filters=updated_artifact_sync_filters
)
Expand All @@ -471,44 +475,41 @@ def acr_cache_update_custom(cmd,
registry_name=registry_name,
cache_rule_name=name,
cache_rule_create_parameters=CacheRuleUpdateParameters(
properties=updated_properties,
identity=identity_properties))
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,
identity=identity_properties))
properties=updated_properties,
identity=identity_properties))


def acr_cache_sync(cmd,
client,
registry_name,
name,
tag,
sync_tag_if_deleted=None,
resource_group_name=None):

rg = get_resource_group_name_by_registry_name(cmd.cli_ctx, registry_name, resource_group_name)

rule = client.cache_rules.get(resource_group_name=rg,
registry_name=registry_name,
cache_rule_name=name)

rule_id = rule.id
source_repo = rule.properties.source_repository
target_repo = rule.properties.target_repository
source_image_str = source_repo[source_repo.find('/') + 1:] + ":" + tag

import_source = ImportSource(source_image=source_image_str,
cache_rule_resource_id=rule_id)
# Create sync parameters for the new cacheRuleSyncParameter endpoint
sync_params = CacheRuleSyncParameters(
tag_name=tag,
sync_tag_if_deleted=sync_tag_if_deleted if sync_tag_if_deleted is not None else False
)

params = ImportImageParameters(source=import_source,
# import tag with force to override existing tags
mode="Force",
target_tags=[target_repo + ":" + tag])
# Call the sync API
result = client.cache_rules.sync_cache_rule(
resource_group_name=rg,
registry_name=registry_name,
cache_rule_name=name,
sync_tag_parameters=sync_params
)

return client.registries.begin_import_image(resource_group_name=rg,
registry_name=registry_name,
parameters=params)

# Inform user that sync request has been queued
logger.warning("Sync request queued successfully. The repository/tag will appear when sync completes.")
return result
Loading