From 41d22d73d826dd49b716eef861e270dbecef5f5f Mon Sep 17 00:00:00 2001 From: Cesar Gray <52045802+cegraybl@users.noreply.github.com> Date: Tue, 15 Sep 2026 11:15:33 -0700 Subject: [PATCH 1/4] fix: support split ACR tasks SDK (#72) fix: support split ACR tasks SDK Copilot-Session: a4f5e94e-df29-4504-b784-0477f1067512 --- src/acrcssc/HISTORY.rst | 4 + src/acrcssc/azext_acrcssc/_client_factory.py | 37 ++++-- .../azext_acrcssc/helper/_taskoperations.py | 54 +++++--- .../tests/latest/test_client_factory.py | 124 ++++++++++++++++++ .../tests/latest/test_cssc_scenario.py | 5 +- .../latest/test_helper_taskoperations.py | 46 +++++-- src/acrcssc/setup.py | 6 +- 7 files changed, 234 insertions(+), 42 deletions(-) create mode 100644 src/acrcssc/azext_acrcssc/tests/latest/test_client_factory.py diff --git a/src/acrcssc/HISTORY.rst b/src/acrcssc/HISTORY.rst index abe3756c403..464dd479444 100644 --- a/src/acrcssc/HISTORY.rst +++ b/src/acrcssc/HISTORY.rst @@ -3,6 +3,10 @@ Release History =============== +1.0.0b8 +++++++++ +* Restore task operations compatibility with current Azure CLI versions. + 1.0.0b7 ++++++++ * Unpin cssc image version so that the latest cached image in task infra can be automatically picked for workflow runs. This allows us to push patch updates to cssc image without needing to update the task definition and release a new version of the extension. diff --git a/src/acrcssc/azext_acrcssc/_client_factory.py b/src/acrcssc/azext_acrcssc/_client_factory.py index 508e4f1a006..b8198164f15 100644 --- a/src/acrcssc/azext_acrcssc/_client_factory.py +++ b/src/acrcssc/azext_acrcssc/_client_factory.py @@ -3,7 +3,7 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- from azure.cli.core.commands.client_factory import get_mgmt_service_client -from azure.cli.core.profiles import ResourceType +from azure.cli.core.profiles import get_sdk, ResourceType from azure.mgmt.containerregistry import ContainerRegistryManagementClient from .helper._constants import ( ACR_API_VERSION_2023_01_01_PREVIEW, @@ -13,6 +13,25 @@ from azure.mgmt.authorization import AuthorizationManagementClient +def _get_acr_tasks_client(cli_ctx): + tasks_resource_type = getattr(ResourceType, "MGMT_CONTAINERREGISTRYTASKS", None) + if tasks_resource_type is not None: + return get_mgmt_service_client(cli_ctx, tasks_resource_type) + + return get_mgmt_service_client( + cli_ctx, + ResourceType.MGMT_CONTAINERREGISTRY, + api_version=ACR_API_VERSION_2019_06_01_PREVIEW) + + +def get_acr_tasks_models(cli_ctx): + tasks_resource_type = getattr( + ResourceType, + "MGMT_CONTAINERREGISTRYTASKS", + ResourceType.MGMT_CONTAINERREGISTRY) + return get_sdk(cli_ctx, tasks_resource_type, "models") + + def cf_acr(cli_ctx, *_) -> ContainerRegistryManagementClient: return get_mgmt_service_client(cli_ctx, ResourceType.MGMT_CONTAINERREGISTRY, @@ -26,27 +45,19 @@ def cf_acr_registries(cli_ctx, *_) -> ContainerRegistryManagementClient: def cf_acr_tasks(cli_ctx, *_): - return get_mgmt_service_client(cli_ctx, - ResourceType.MGMT_CONTAINERREGISTRY, - api_version=ACR_API_VERSION_2019_06_01_PREVIEW).tasks + return _get_acr_tasks_client(cli_ctx).tasks def cf_acr_registries_tasks(cli_ctx, *_): - return get_mgmt_service_client(cli_ctx, - ResourceType.MGMT_CONTAINERREGISTRY, - api_version=ACR_API_VERSION_2019_06_01_PREVIEW).registries + return _get_acr_tasks_client(cli_ctx).registries def cf_acr_taskruns(cli_ctx, *_): - return get_mgmt_service_client(cli_ctx, - ResourceType.MGMT_CONTAINERREGISTRY, - api_version=ACR_API_VERSION_2019_06_01_PREVIEW).task_runs + return _get_acr_tasks_client(cli_ctx).task_runs def cf_acr_runs(cli_ctx, *_): - return get_mgmt_service_client(cli_ctx, - ResourceType.MGMT_CONTAINERREGISTRY, - api_version=ACR_API_VERSION_2019_06_01_PREVIEW).runs + return _get_acr_tasks_client(cli_ctx).runs def cf_resources(cli_ctx, subscription_id=None): diff --git a/src/acrcssc/azext_acrcssc/helper/_taskoperations.py b/src/acrcssc/azext_acrcssc/helper/_taskoperations.py index e8f93e068e4..12848e0c2d0 100644 --- a/src/acrcssc/azext_acrcssc/helper/_taskoperations.py +++ b/src/acrcssc/azext_acrcssc/helper/_taskoperations.py @@ -38,7 +38,12 @@ from azure.cli.command_modules.acr._archive_utils import logger as acr_archive_utils_logger from azure.core.exceptions import HttpResponseError from azure.mgmt.core.tools import parse_resource_id -from azext_acrcssc._client_factory import cf_acr_tasks, cf_authorization, cf_acr_registries_tasks, cf_acr_runs +from azext_acrcssc._client_factory import ( + cf_acr_tasks, + cf_authorization, + cf_acr_registries_tasks, + cf_acr_runs, + get_acr_tasks_models) from azext_acrcssc.helper._deployment import validate_and_deploy_template from azext_acrcssc._validators import check_continuous_task_exists, check_continuous_task_config_exists from datetime import datetime, timezone, timedelta @@ -128,15 +133,22 @@ def _update_cssc_workflow(cmd, registry, schedule_cron_expression, resource_grou # if we need to update the tasks, we will update the cron expression from it # if not we just update the cron expression from the given parameter acr_task_client = cf_acr_tasks(cmd.cli_ctx) + acr_tasks_models = get_acr_tasks_models(cmd.cli_ctx) for task in task_list: deployed_task = task.step.encoded_task_content extension_task = _create_encoded_task(CONTINUOUSPATCH_TASK_DEFINITION[task.name]["template_file"]) if deployed_task != extension_task: logger.debug(f"Task {task.name} is different from the extension task, updating the task") - _update_task_yaml(acr_task_client, registry, resource_group, task, extension_task) + _update_task_yaml(acr_task_client, acr_tasks_models, registry, resource_group, task, extension_task) if schedule_cron_expression: - _update_task_schedule(acr_task_client, registry, resource_group, schedule_cron_expression, dry_run) + _update_task_schedule( + acr_task_client, + acr_tasks_models, + registry, + resource_group, + schedule_cron_expression, + dry_run) def _eval_trigger_run(cmd, registry, resource_group, run_immediately): @@ -223,6 +235,7 @@ def acr_cssc_dry_run(cmd, registry, config_file_path, is_create=True, remove_int resource_group_name = parse_resource_id(registry.id)[RESOURCE_GROUP] acr_registries_task_client = cf_acr_registries_tasks(cmd.cli_ctx) acr_run_client = cf_acr_runs(cmd.cli_ctx) + acr_tasks_models = get_acr_tasks_models(cmd.cli_ctx) # This removes the internal logging from the acr module, reenables it after the setup is completed. # Because it is an external logger, the only way to control the output is by changing the level @@ -243,8 +256,8 @@ def acr_cssc_dry_run(cmd, registry, config_file_path, is_create=True, remove_int if remove_internal_statements: acr_archive_utils_logger.setLevel(acr_archive_utils_logger_level) - OS = acr_run_client.models.OS - Architecture = acr_run_client.models.Architecture + OS = acr_tasks_models.OS + Architecture = acr_tasks_models.Architecture # TODO: when the extension merges back into the acr module, we need to reuse the 'get_validate_platform()' from ACR modules (src\azure-cli\azure\cli\command_modules\acr\_utils.py) platform_os = OS.linux.value @@ -252,13 +265,13 @@ def acr_cssc_dry_run(cmd, registry, config_file_path, is_create=True, remove_int platform_variant = None value_pair = [{"name": "CONFIGPATH", "value": f"{file_name}"}] - request = acr_registries_task_client.models.FileTaskRunRequest( + request = acr_tasks_models.FileTaskRunRequest( task_file_path=TMP_DRY_RUN_FILE_NAME, values_file_path=None, values=value_pair, source_location=source_location, timeout=None, - platform=acr_registries_task_client.models.PlatformProperties( + platform=acr_tasks_models.PlatformProperties( os=platform_os, architecture=platform_arch, variant=platform_variant @@ -376,7 +389,8 @@ def _retrieve_logs_for_image(cmd, registry, resource_group_name, schedule, workf def _trigger_task_run(cmd, registry, resource_group, task_name): acr_task_registries_client = cf_acr_registries_tasks(cmd.cli_ctx) - request = acr_task_registries_client.models.TaskRunRequest( + acr_tasks_models = get_acr_tasks_models(cmd.cli_ctx) + request = acr_tasks_models.TaskRunRequest( task_id=f"{registry.id}/tasks/{task_name}") queued_run = LongRunningOperation(cmd.cli_ctx)( acr_task_registries_client.begin_schedule_run( @@ -400,11 +414,11 @@ def _create_encoded_task(task_file): return base64_content.decode('utf-8') -def _update_task_yaml(acr_task_client, registry, resource_group_name, task, encoded_task): +def _update_task_yaml(acr_task_client, acr_tasks_models, registry, resource_group_name, task, encoded_task): logger.debug("Entering update_task_yaml for task %s", task.name) try: - taskUpdateParameters = acr_task_client.models.TaskUpdateParameters( - step=acr_task_client.models.EncodedTaskStepUpdateParameters( + taskUpdateParameters = acr_tasks_models.TaskUpdateParameters( + step=acr_tasks_models.EncodedTaskStepUpdateParameters( encoded_task_content=encoded_task)) acr_task_client.begin_update(resource_group_name, @@ -417,12 +431,18 @@ def _update_task_yaml(acr_task_client, registry, resource_group_name, task, enco logger.warning(f"Failed to update task {task.name} in registry {registry.name}: {exception}") -def _update_task_schedule(acr_task_client, registry, resource_group_name, cron_expression, dryrun): +def _update_task_schedule( + acr_task_client, + acr_tasks_models, + registry, + resource_group_name, + cron_expression, + dryrun): logger.debug(f"Using cron_expression: {cron_expression}") - taskUpdateParameters = acr_task_client.models.TaskUpdateParameters( - trigger=acr_task_client.models.TriggerUpdateParameters( + taskUpdateParameters = acr_tasks_models.TaskUpdateParameters( + trigger=acr_tasks_models.TriggerUpdateParameters( timer_triggers=[ - acr_task_client.models.TimerTriggerUpdateParameters( + acr_tasks_models.TimerTriggerUpdateParameters( name='azcli_defined_schedule', schedule=cron_expression) ])) @@ -527,8 +547,8 @@ def _transform_task_list(tasks): def _get_custom_registry_credentials(cmd): - acr_tasks_client = cf_acr_tasks(cmd.cli_ctx) - return acr_tasks_client.models.Credentials( + acr_tasks_models = get_acr_tasks_models(cmd.cli_ctx) + return acr_tasks_models.Credentials( source_registry=None, custom_registries=None ) diff --git a/src/acrcssc/azext_acrcssc/tests/latest/test_client_factory.py b/src/acrcssc/azext_acrcssc/tests/latest/test_client_factory.py new file mode 100644 index 00000000000..0f6bf1edd60 --- /dev/null +++ b/src/acrcssc/azext_acrcssc/tests/latest/test_client_factory.py @@ -0,0 +1,124 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +import unittest +from types import SimpleNamespace +from unittest import mock + +from azext_acrcssc import _client_factory +from azext_acrcssc.helper._constants import ACR_API_VERSION_2019_06_01_PREVIEW + + +class TestAcrTasksClientFactory(unittest.TestCase): + + def test_task_clients_use_split_resource_type_when_available(self): + resource_types = SimpleNamespace( + MGMT_CONTAINERREGISTRY=mock.sentinel.legacy_resource_type, + MGMT_CONTAINERREGISTRYTASKS=mock.sentinel.tasks_resource_type) + service_client = SimpleNamespace( + tasks=mock.sentinel.tasks, + registries=mock.sentinel.registries, + task_runs=mock.sentinel.task_runs, + runs=mock.sentinel.runs) + + with mock.patch.object(_client_factory, "ResourceType", resource_types), \ + mock.patch.object( + _client_factory, + "get_mgmt_service_client", + return_value=service_client) as get_client: + clients = { + "tasks": _client_factory.cf_acr_tasks(mock.sentinel.cli_ctx), + "registries": _client_factory.cf_acr_registries_tasks(mock.sentinel.cli_ctx), + "task_runs": _client_factory.cf_acr_taskruns(mock.sentinel.cli_ctx), + "runs": _client_factory.cf_acr_runs(mock.sentinel.cli_ctx), + } + + self.assertEqual( + clients, + { + "tasks": mock.sentinel.tasks, + "registries": mock.sentinel.registries, + "task_runs": mock.sentinel.task_runs, + "runs": mock.sentinel.runs, + }) + self.assertEqual( + get_client.call_args_list, + [mock.call(mock.sentinel.cli_ctx, mock.sentinel.tasks_resource_type)] * 4) + + def test_task_clients_fall_back_to_legacy_resource_type_and_api_version(self): + resource_types = SimpleNamespace( + MGMT_CONTAINERREGISTRY=mock.sentinel.legacy_resource_type) + service_client = SimpleNamespace( + tasks=mock.sentinel.tasks, + registries=mock.sentinel.registries, + task_runs=mock.sentinel.task_runs, + runs=mock.sentinel.runs) + + with mock.patch.object(_client_factory, "ResourceType", resource_types), \ + mock.patch.object( + _client_factory, + "get_mgmt_service_client", + return_value=service_client) as get_client: + clients = { + "tasks": _client_factory.cf_acr_tasks(mock.sentinel.cli_ctx), + "registries": _client_factory.cf_acr_registries_tasks(mock.sentinel.cli_ctx), + "task_runs": _client_factory.cf_acr_taskruns(mock.sentinel.cli_ctx), + "runs": _client_factory.cf_acr_runs(mock.sentinel.cli_ctx), + } + + self.assertEqual( + clients, + { + "tasks": mock.sentinel.tasks, + "registries": mock.sentinel.registries, + "task_runs": mock.sentinel.task_runs, + "runs": mock.sentinel.runs, + }) + self.assertEqual( + get_client.call_args_list, + [ + mock.call( + mock.sentinel.cli_ctx, + mock.sentinel.legacy_resource_type, + api_version=ACR_API_VERSION_2019_06_01_PREVIEW) + ] * 4) + + def test_task_models_use_split_resource_type_when_available(self): + resource_types = SimpleNamespace( + MGMT_CONTAINERREGISTRY=mock.sentinel.legacy_resource_type, + MGMT_CONTAINERREGISTRYTASKS=mock.sentinel.tasks_resource_type) + + with mock.patch.object(_client_factory, "ResourceType", resource_types), \ + mock.patch.object( + _client_factory, + "get_sdk", + create=True, + return_value=mock.sentinel.models) as get_sdk: + models = _client_factory.get_acr_tasks_models(mock.sentinel.cli_ctx) + + self.assertIs(models, mock.sentinel.models) + get_sdk.assert_called_once_with( + mock.sentinel.cli_ctx, + mock.sentinel.tasks_resource_type, + "models") + + def test_task_models_fall_back_to_legacy_resource_type(self): + resource_types = SimpleNamespace( + MGMT_CONTAINERREGISTRY=mock.sentinel.legacy_resource_type) + + with mock.patch.object(_client_factory, "ResourceType", resource_types), \ + mock.patch.object( + _client_factory, + "get_sdk", + create=True, + return_value=mock.sentinel.models) as get_sdk: + models = _client_factory.get_acr_tasks_models(mock.sentinel.cli_ctx) + + self.assertIs(models, mock.sentinel.models) + get_sdk.assert_called_once_with( + mock.sentinel.cli_ctx, + mock.sentinel.legacy_resource_type, + "models") + diff --git a/src/acrcssc/azext_acrcssc/tests/latest/test_cssc_scenario.py b/src/acrcssc/azext_acrcssc/tests/latest/test_cssc_scenario.py index a2d6ac6044b..4024e62a771 100644 --- a/src/acrcssc/azext_acrcssc/tests/latest/test_cssc_scenario.py +++ b/src/acrcssc/azext_acrcssc/tests/latest/test_cssc_scenario.py @@ -24,7 +24,10 @@ def test_acrcssc_workflow(self, resource_group): self.cmd('acr create -n {registry_name} -g {rg} --sku Standard --location {location}', checks=[self.check('name', '{registry_name}'), self.check('provisioningState', 'Succeeded')]) - + + self.cmd('acr supply-chain workflow create -r {registry_name} -g {rg} -t {workflow_type} ' + '--config "{config}" --schedule {schedule} --dry-run') + self.cmd('acr supply-chain workflow create -r {registry_name} -g {rg} -t {workflow_type} --config "{config}" --schedule {schedule}') self.cmd('acr supply-chain workflow list -r {registry_name} -g {rg} -t {workflow_type}') diff --git a/src/acrcssc/azext_acrcssc/tests/latest/test_helper_taskoperations.py b/src/acrcssc/azext_acrcssc/tests/latest/test_helper_taskoperations.py index 63cfe5e265e..c0f7b031b4f 100644 --- a/src/acrcssc/azext_acrcssc/tests/latest/test_helper_taskoperations.py +++ b/src/acrcssc/azext_acrcssc/tests/latest/test_helper_taskoperations.py @@ -5,6 +5,7 @@ import tempfile import unittest +from types import SimpleNamespace from unittest import mock from azure.cli.core.mock import DummyCli from azext_acrcssc.helper._taskoperations import (create_update_continuous_patch_v1, delete_continuous_patch_v1, list_continuous_patch_v1, acr_cssc_dry_run, cancel_continuous_patch_runs, track_scan_progress) @@ -209,17 +210,28 @@ def test_list_continuous_patch_v1(self, mock_transform_task_list, mock_check_con @mock.patch("azext_acrcssc.helper._taskoperations.cf_acr_runs") @mock.patch('azext_acrcssc._validators.cf_acr_tasks') @mock.patch('azext_acrcssc.helper._taskoperations.cf_acr_tasks') + @mock.patch("azext_acrcssc.helper._taskoperations.get_acr_tasks_models", create=True) @mock.patch("azext_acrcssc.helper._taskoperations.LongRunningOperation") - def test_acr_cssc_dry_run(self, mock_LongRunningOperation, mock_cf_acr_tasks_taskoperations, mock_cf_acr_tasks_validator, mock_cf_acr_runs, mock_cf_acr_registries_tasks, mock_prepare_source_location, mock_delete_temporary_dry_run_file, mock_create_temporary_dry_run_file, mock_generate_logs, mock_check_continuous_task_exists): + def test_acr_cssc_dry_run(self, mock_LongRunningOperation, mock_get_acr_tasks_models, mock_cf_acr_tasks_taskoperations, mock_cf_acr_tasks_validator, mock_cf_acr_runs, mock_cf_acr_registries_tasks, mock_prepare_source_location, mock_delete_temporary_dry_run_file, mock_create_temporary_dry_run_file, mock_generate_logs, mock_check_continuous_task_exists): # Mock the necessary dependencies config_file_path = "test_config_file_path" - mock_acr_registries_task_client = mock.MagicMock() - mock_cf_acr_registries_tasks.return_value = mock_acr_registries_task_client - mock_acr_run_client = mock.MagicMock() - mock_cf_acr_runs.return_value = mock_acr_run_client - mock_acr_task_client = mock.MagicMock() - mock_cf_acr_tasks_validator.return_value = mock_acr_task_client - mock_cf_acr_tasks_taskoperations.return_value = mock_acr_task_client + mock_acr_registries_task_client = mock.Mock(spec_set=["begin_schedule_run"]) + mock_cf_acr_registries_tasks.return_value = mock_acr_registries_task_client + mock_acr_run_client = mock.Mock(spec_set=[]) + mock_cf_acr_runs.return_value = mock_acr_run_client + mock_acr_task_client = mock.Mock(spec_set=[]) + mock_cf_acr_tasks_validator.return_value = mock_acr_task_client + mock_cf_acr_tasks_taskoperations.return_value = mock_acr_task_client + task_run_request = mock.sentinel.task_run_request + platform = mock.sentinel.platform + credentials = mock.sentinel.credentials + task_models = SimpleNamespace( + OS=SimpleNamespace(linux=SimpleNamespace(value="linux")), + Architecture=SimpleNamespace(amd64=SimpleNamespace(value="amd64")), + FileTaskRunRequest=mock.Mock(return_value=task_run_request), + PlatformProperties=mock.Mock(return_value=platform), + Credentials=mock.Mock(return_value=credentials)) + mock_get_acr_tasks_models.return_value = task_models mock_LongRunningOperation.return_value.return_value.run_id = "test_run_id" mock_generate_logs.return_value = "mock_logs" mock_check_continuous_task_exists.return_value = False, [] @@ -233,6 +245,24 @@ def test_acr_cssc_dry_run(self, mock_LongRunningOperation, mock_cf_acr_tasks_tas mock_LongRunningOperation.assert_called_once() mock_delete_temporary_dry_run_file.assert_called_once() mock_generate_logs.assert_called_once() + mock_get_acr_tasks_models.assert_called_with(self.cmd.cli_ctx) + task_models.PlatformProperties.assert_called_once_with( + os="linux", + architecture="amd64", + variant=None) + task_models.Credentials.assert_called_once_with( + source_registry=None, + custom_registries=None) + task_models.FileTaskRunRequest.assert_called_once_with( + task_file_path="tmp_dry_run_template.yaml", + values_file_path=None, + values=[{"name": "CONFIGPATH", "value": "test_config_file_path"}], + source_location=mock_prepare_source_location.return_value, + timeout=None, + platform=platform, + credentials=credentials, + agent_pool_name=None, + log_template=None) self.assertIsNotNone(result) @mock.patch("azext_acrcssc.helper._taskoperations.WorkflowTaskStatus.get_taskruns_with_filter") diff --git a/src/acrcssc/setup.py b/src/acrcssc/setup.py index 22cc262d3a4..08a4180b8dc 100644 --- a/src/acrcssc/setup.py +++ b/src/acrcssc/setup.py @@ -6,14 +6,14 @@ # -------------------------------------------------------------------------------------------- from codecs import open +import logging from setuptools import setup, find_packages try: from azure_bdist_wheel import cmdclass except ImportError: - from distutils import log as logger - logger.warn("Wheel is not available, disabling bdist_wheel hook") + logging.warning("Wheel is not available, disabling bdist_wheel hook") -VERSION = '1.0.0b7' +VERSION = '1.0.0b8' # The full list of classifiers is available at # https://pypi.python.org/pypi?%3Aaction=list_classifiers From e01339b0bee6bd59f41f1d4f8b6ec61b51affc97 Mon Sep 17 00:00:00 2001 From: Cesar Gray Blanco Date: Tue, 15 Sep 2026 12:49:19 -0700 Subject: [PATCH 2/4] fix: support current ACR task operations Use synchronous operation names from the split ACR tasks SDK while retaining legacy begin_* compatibility and waiting for legacy pollers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f0ccdfbb-37f1-4ea4-96ae-5de9168b489e --- src/acrcssc/HISTORY.rst | 4 + .../azext_acrcssc/helper/_taskoperations.py | 105 ++++++--- .../test_task_operation_compatibility.py | 208 ++++++++++++++++++ src/acrcssc/setup.py | 2 +- 4 files changed, 292 insertions(+), 27 deletions(-) create mode 100644 src/acrcssc/azext_acrcssc/tests/latest/test_task_operation_compatibility.py diff --git a/src/acrcssc/HISTORY.rst b/src/acrcssc/HISTORY.rst index 464dd479444..bdb21309a3a 100644 --- a/src/acrcssc/HISTORY.rst +++ b/src/acrcssc/HISTORY.rst @@ -3,6 +3,10 @@ Release History =============== +1.0.0b9 +++++++++ +* Support current and legacy ACR task operation method names. + 1.0.0b8 ++++++++ * Restore task operations compatibility with current Azure CLI versions. diff --git a/src/acrcssc/azext_acrcssc/helper/_taskoperations.py b/src/acrcssc/azext_acrcssc/helper/_taskoperations.py index 12848e0c2d0..34edcad6195 100644 --- a/src/acrcssc/azext_acrcssc/helper/_taskoperations.py +++ b/src/acrcssc/azext_acrcssc/helper/_taskoperations.py @@ -54,6 +54,14 @@ logger = get_logger(__name__) +def _get_task_operation(client, operation_name): + legacy_operation = getattr(client, f"begin_{operation_name}", None) + if legacy_operation is not None: + return legacy_operation, True + + return getattr(client, operation_name), False + + def create_update_continuous_patch_v1(cmd, registry, cssc_config_file, @@ -139,10 +147,18 @@ def _update_cssc_workflow(cmd, registry, schedule_cron_expression, resource_grou extension_task = _create_encoded_task(CONTINUOUSPATCH_TASK_DEFINITION[task.name]["template_file"]) if deployed_task != extension_task: logger.debug(f"Task {task.name} is different from the extension task, updating the task") - _update_task_yaml(acr_task_client, acr_tasks_models, registry, resource_group, task, extension_task) + _update_task_yaml( + cmd, + acr_task_client, + acr_tasks_models, + registry, + resource_group, + task, + extension_task) if schedule_cron_expression: _update_task_schedule( + cmd, acr_task_client, acr_tasks_models, registry, @@ -177,7 +193,7 @@ def delete_continuous_patch_v1(cmd, registry, yes): if running_tasks: from knack.prompting import prompt_y_n if yes or prompt_y_n("There are currently running tasks for this workflow. Do you want to cancel their execution?"): - _cancel_task_runs(acr_run_client, registry.name, resource_group_name, running_tasks) + _cancel_task_runs(cmd, acr_run_client, registry.name, resource_group_name, running_tasks) if cssc_tasks_exists: cssc_tasks = ', '.join(task_list_names) @@ -280,10 +296,17 @@ def acr_cssc_dry_run(cmd, registry, config_file_path, is_create=True, remove_int agent_pool_name=None, log_template=None ) - queued = LongRunningOperation(cmd.cli_ctx, start_msg=WORKFLOW_VALIDATION_MESSAGE)(acr_registries_task_client.begin_schedule_run( + schedule_run, is_long_running = _get_task_operation( + acr_registries_task_client, + "schedule_run") + queued = schedule_run( resource_group_name=resource_group_name, registry_name=registry.name, - run_request=request)) + run_request=request) + if is_long_running: + queued = LongRunningOperation( + cmd.cli_ctx, + start_msg=WORKFLOW_VALIDATION_MESSAGE)(queued) run_id = queued.run_id logger.info("Performing dry-run check for filter policy using acr task run id: %s", run_id) return WorkflowTaskStatus.remove_internal_acr_statements( @@ -309,16 +332,24 @@ def cancel_continuous_patch_runs(cmd, resource_group_name, registry_name): status_filter=[TaskRunStatus.Running.value, TaskRunStatus.Queued.value, TaskRunStatus.Started.value], taskname_filter=[CONTINUOUSPATCH_TASK_SCANREGISTRY_NAME, CONTINUOUSPATCH_TASK_SCANIMAGE_NAME, CONTINUOUSPATCH_TASK_PATCHIMAGE_NAME]) - _cancel_task_runs(acr_task_run_client, registry_name, resource_group_name, running_tasks) + _cancel_task_runs(cmd, acr_task_run_client, registry_name, resource_group_name, running_tasks) logger.warning("All active running workflow tasks have been cancelled.") -def _cancel_task_runs(acr_task_run_client, registry_name, resource_group_name, running_tasks): +def _cancel_task_runs(cmd, acr_task_run_client, registry_name, resource_group_name, running_tasks): for task in running_tasks: try: logger.warning("Sending request to cancel task %s", task.name) logger.debug("Cancel Task run, name %s run id: %s", task.name, task.run_id) - acr_task_run_client.begin_cancel(resource_group_name, registry_name, task.name) + cancel, is_long_running = _get_task_operation( + acr_task_run_client, + "cancel") + cancel_result = cancel( + resource_group_name, + registry_name, + task.run_id) + if is_long_running: + LongRunningOperation(cmd.cli_ctx)(cancel_result) except Exception as exception: logger.error(f"Failed to cancel task {task.name} from registry {registry_name}: {exception}") @@ -392,11 +423,15 @@ def _trigger_task_run(cmd, registry, resource_group, task_name): acr_tasks_models = get_acr_tasks_models(cmd.cli_ctx) request = acr_tasks_models.TaskRunRequest( task_id=f"{registry.id}/tasks/{task_name}") - queued_run = LongRunningOperation(cmd.cli_ctx)( - acr_task_registries_client.begin_schedule_run( - resource_group, - registry.name, - request)) + schedule_run, is_long_running = _get_task_operation( + acr_task_registries_client, + "schedule_run") + queued_run = schedule_run( + resource_group, + registry.name, + request) + if is_long_running: + queued_run = LongRunningOperation(cmd.cli_ctx)(queued_run) run_id = queued_run.run_id print(f"Queued {CONTINUOUS_PATCHING_WORKFLOW_NAME} workflow task '{task_name}' with run ID: {run_id}. Use 'az acr task logs --registry {registry.name} --run-id {run_id}' to view the logs.") @@ -414,17 +449,28 @@ def _create_encoded_task(task_file): return base64_content.decode('utf-8') -def _update_task_yaml(acr_task_client, acr_tasks_models, registry, resource_group_name, task, encoded_task): +def _update_task_yaml( + cmd, + acr_task_client, + acr_tasks_models, + registry, + resource_group_name, + task, + encoded_task): logger.debug("Entering update_task_yaml for task %s", task.name) try: taskUpdateParameters = acr_tasks_models.TaskUpdateParameters( step=acr_tasks_models.EncodedTaskStepUpdateParameters( encoded_task_content=encoded_task)) - acr_task_client.begin_update(resource_group_name, - registry.name, - task.name, - taskUpdateParameters) + update, is_long_running = _get_task_operation(acr_task_client, "update") + update_result = update( + resource_group_name, + registry.name, + task.name, + taskUpdateParameters) + if is_long_running: + LongRunningOperation(cmd.cli_ctx)(update_result) logger.debug(f"Task {task.name} updated successfully") except HttpResponseError as exception: @@ -432,6 +478,7 @@ def _update_task_yaml(acr_task_client, acr_tasks_models, registry, resource_grou def _update_task_schedule( + cmd, acr_task_client, acr_tasks_models, registry, @@ -451,10 +498,14 @@ def _update_task_schedule( logger.debug("Dry run, skipping the update of the task schedule") return try: - acr_task_client.begin_update(resource_group_name, - registry.name, - CONTINUOUSPATCH_TASK_SCANREGISTRY_NAME, - taskUpdateParameters) + update, is_long_running = _get_task_operation(acr_task_client, "update") + update_result = update( + resource_group_name, + registry.name, + CONTINUOUSPATCH_TASK_SCANREGISTRY_NAME, + taskUpdateParameters) + if is_long_running: + LongRunningOperation(cmd.cli_ctx)(update_result) print("Schedule has been successfully updated.") except HttpResponseError as exception: raise AzCLIError(f"Failed to update the task schedule: {exception}") @@ -469,11 +520,13 @@ def _delete_task(cmd, registry, task_name): _delete_task_role_assignment(cmd.cli_ctx, acr_tasks_client, registry, resource_group, task_name) logger.debug(f"Deleting task {task_name}") - LongRunningOperation(cmd.cli_ctx)( - acr_tasks_client.begin_delete( - resource_group, - registry.name, - task_name)) + delete, is_long_running = _get_task_operation(acr_tasks_client, "delete") + delete_result = delete( + resource_group, + registry.name, + task_name) + if is_long_running: + LongRunningOperation(cmd.cli_ctx)(delete_result) logger.debug(f"Task {task_name} deleted successfully") except AzCLIError as exception: diff --git a/src/acrcssc/azext_acrcssc/tests/latest/test_task_operation_compatibility.py b/src/acrcssc/azext_acrcssc/tests/latest/test_task_operation_compatibility.py new file mode 100644 index 00000000000..f24df176b06 --- /dev/null +++ b/src/acrcssc/azext_acrcssc/tests/latest/test_task_operation_compatibility.py @@ -0,0 +1,208 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +import unittest +from types import SimpleNamespace +from unittest import mock + +from azure.cli.core.mock import DummyCli + +from azext_acrcssc.helper._taskoperations import ( + _cancel_task_runs, + _delete_task, + _get_task_operation, + _update_task_schedule, + _update_task_yaml) + + +class TestTaskOperationCompatibility(unittest.TestCase): + def setUp(self): + self.cmd = SimpleNamespace(cli_ctx=DummyCli()) + self.registry = SimpleNamespace( + name="mockregistry", + id=( + "/subscriptions/00000000-0000-0000-0000-000000000000/" + "resourceGroups/mockrg/providers/Microsoft.ContainerRegistry/" + "registries/mockregistry")) + + def test_get_task_operation_uses_current_sdk_methods(self): + for operation_name in ("cancel", "delete", "schedule_run", "update"): + with self.subTest(operation_name=operation_name): + client = mock.Mock(spec_set=[operation_name]) + + operation, is_long_running = _get_task_operation( + client, + operation_name) + + self.assertIs(operation, getattr(client, operation_name)) + self.assertFalse(is_long_running) + + def test_get_task_operation_uses_legacy_sdk_methods(self): + for operation_name in ("cancel", "delete", "schedule_run", "update"): + with self.subTest(operation_name=operation_name): + legacy_operation_name = f"begin_{operation_name}" + client = mock.Mock(spec_set=[legacy_operation_name]) + + operation, is_long_running = _get_task_operation( + client, + operation_name) + + self.assertIs(operation, getattr(client, legacy_operation_name)) + self.assertTrue(is_long_running) + + @mock.patch("azext_acrcssc.helper._taskoperations.LongRunningOperation") + @mock.patch("azext_acrcssc.helper._taskoperations._delete_task_role_assignment") + @mock.patch("azext_acrcssc.helper._taskoperations.cf_acr_tasks") + def test_delete_task_uses_current_sdk_delete( + self, + mock_cf_acr_tasks, + _, + mock_long_running_operation): + client = mock.Mock(spec_set=["delete"]) + mock_cf_acr_tasks.return_value = client + + _delete_task(self.cmd, self.registry, "mocktask") + + client.delete.assert_called_once_with( + "mockrg", + "mockregistry", + "mocktask") + mock_long_running_operation.assert_not_called() + + @mock.patch("azext_acrcssc.helper._taskoperations.LongRunningOperation") + @mock.patch("azext_acrcssc.helper._taskoperations._delete_task_role_assignment") + @mock.patch("azext_acrcssc.helper._taskoperations.cf_acr_tasks") + def test_delete_task_uses_legacy_sdk_begin_delete( + self, + mock_cf_acr_tasks, + _, + mock_long_running_operation): + client = mock.Mock(spec_set=["begin_delete"]) + client.begin_delete.return_value = mock.sentinel.delete_poller + mock_cf_acr_tasks.return_value = client + + _delete_task(self.cmd, self.registry, "mocktask") + + client.begin_delete.assert_called_once_with( + "mockrg", + "mockregistry", + "mocktask") + mock_long_running_operation.assert_called_once_with(self.cmd.cli_ctx) + mock_long_running_operation.return_value.assert_called_once_with( + mock.sentinel.delete_poller) + + @mock.patch("azext_acrcssc.helper._taskoperations.LongRunningOperation") + def test_legacy_update_operations_wait_for_completion( + self, + mock_long_running_operation): + client = mock.Mock(spec_set=["begin_update"]) + client.begin_update.side_effect = ( + mock.sentinel.yaml_poller, + mock.sentinel.schedule_poller) + task = SimpleNamespace(name="mocktask") + models = SimpleNamespace( + EncodedTaskStepUpdateParameters=mock.Mock(), + TaskUpdateParameters=mock.Mock(), + TimerTriggerUpdateParameters=mock.Mock(), + TriggerUpdateParameters=mock.Mock()) + + _update_task_yaml( + self.cmd, + client, + models, + self.registry, + "mockrg", + task, + "encoded-task") + _update_task_schedule( + self.cmd, + client, + models, + self.registry, + "mockrg", + "0 0 * * *", + False) + + self.assertEqual( + mock_long_running_operation.return_value.call_args_list, + [ + mock.call(mock.sentinel.yaml_poller), + mock.call(mock.sentinel.schedule_poller), + ]) + + @mock.patch("azext_acrcssc.helper._taskoperations.LongRunningOperation") + def test_current_update_operations_do_not_use_lro( + self, + mock_long_running_operation): + client = mock.Mock(spec_set=["update"]) + task = SimpleNamespace(name="mocktask") + models = SimpleNamespace( + EncodedTaskStepUpdateParameters=mock.Mock(), + TaskUpdateParameters=mock.Mock(), + TimerTriggerUpdateParameters=mock.Mock(), + TriggerUpdateParameters=mock.Mock()) + + _update_task_yaml( + self.cmd, + client, + models, + self.registry, + "mockrg", + task, + "encoded-task") + _update_task_schedule( + self.cmd, + client, + models, + self.registry, + "mockrg", + "0 0 * * *", + False) + + self.assertEqual(client.update.call_count, 2) + mock_long_running_operation.assert_not_called() + + @mock.patch("azext_acrcssc.helper._taskoperations.LongRunningOperation") + def test_legacy_cancel_uses_run_id_and_waits( + self, + mock_long_running_operation): + client = mock.Mock(spec_set=["begin_cancel"]) + client.begin_cancel.return_value = mock.sentinel.cancel_poller + task = SimpleNamespace(name="mocktask", run_id="mock-run-id") + + _cancel_task_runs( + self.cmd, + client, + "mockregistry", + "mockrg", + [task]) + + client.begin_cancel.assert_called_once_with( + "mockrg", + "mockregistry", + "mock-run-id") + mock_long_running_operation.assert_called_once_with(self.cmd.cli_ctx) + mock_long_running_operation.return_value.assert_called_once_with( + mock.sentinel.cancel_poller) + + @mock.patch("azext_acrcssc.helper._taskoperations.LongRunningOperation") + def test_current_cancel_uses_run_id_without_lro( + self, + mock_long_running_operation): + client = mock.Mock(spec_set=["cancel"]) + task = SimpleNamespace(name="mocktask", run_id="mock-run-id") + + _cancel_task_runs( + self.cmd, + client, + "mockregistry", + "mockrg", + [task]) + + client.cancel.assert_called_once_with( + "mockrg", + "mockregistry", + "mock-run-id") + mock_long_running_operation.assert_not_called() diff --git a/src/acrcssc/setup.py b/src/acrcssc/setup.py index 08a4180b8dc..22d7de0cdc4 100644 --- a/src/acrcssc/setup.py +++ b/src/acrcssc/setup.py @@ -13,7 +13,7 @@ except ImportError: logging.warning("Wheel is not available, disabling bdist_wheel hook") -VERSION = '1.0.0b8' +VERSION = '1.0.0b9' # The full list of classifiers is available at # https://pypi.python.org/pypi?%3Aaction=list_classifiers From 961271fa507390c81bbb40df8b79eb4f4cc0b774 Mon Sep 17 00:00:00 2001 From: Cesar Gray Blanco Date: Tue, 15 Sep 2026 13:33:32 -0700 Subject: [PATCH 3/4] fix: support current task run request values Select values_property for the split ACR tasks SDK and retain values for the legacy model. Keep the combined compatibility release at 1.0.0b8. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f0ccdfbb-37f1-4ea4-96ae-5de9168b489e --- src/acrcssc/HISTORY.rst | 5 +--- .../azext_acrcssc/helper/_taskoperations.py | 17 ++++++++++- .../latest/test_helper_taskoperations.py | 6 ++-- .../test_task_operation_compatibility.py | 28 +++++++++++++++++++ src/acrcssc/setup.py | 2 +- 5 files changed, 50 insertions(+), 8 deletions(-) diff --git a/src/acrcssc/HISTORY.rst b/src/acrcssc/HISTORY.rst index bdb21309a3a..cfda947665b 100644 --- a/src/acrcssc/HISTORY.rst +++ b/src/acrcssc/HISTORY.rst @@ -3,13 +3,10 @@ Release History =============== -1.0.0b9 -++++++++ -* Support current and legacy ACR task operation method names. - 1.0.0b8 ++++++++ * Restore task operations compatibility with current Azure CLI versions. +* Support current and legacy ACR task operation method names. 1.0.0b7 ++++++++ diff --git a/src/acrcssc/azext_acrcssc/helper/_taskoperations.py b/src/acrcssc/azext_acrcssc/helper/_taskoperations.py index 34edcad6195..1509bac7d27 100644 --- a/src/acrcssc/azext_acrcssc/helper/_taskoperations.py +++ b/src/acrcssc/azext_acrcssc/helper/_taskoperations.py @@ -62,6 +62,19 @@ def _get_task_operation(client, operation_name): return getattr(client, operation_name), False +def _get_file_task_run_values_parameter(file_task_run_request): + annotations = getattr(file_task_run_request, "__annotations__", {}) + if isinstance(annotations, dict) and "values_property" in annotations: + return "values_property" + + attribute_map = getattr(file_task_run_request, "_attribute_map", {}) + if isinstance(attribute_map, dict) and "values" in attribute_map: + return "values" + + raise TypeError( + "FileTaskRunRequest defines neither values_property nor values") + + def create_update_continuous_patch_v1(cmd, registry, cssc_config_file, @@ -281,10 +294,12 @@ def acr_cssc_dry_run(cmd, registry, config_file_path, is_create=True, remove_int platform_variant = None value_pair = [{"name": "CONFIGPATH", "value": f"{file_name}"}] + values_parameter = _get_file_task_run_values_parameter( + acr_tasks_models.FileTaskRunRequest) request = acr_tasks_models.FileTaskRunRequest( task_file_path=TMP_DRY_RUN_FILE_NAME, values_file_path=None, - values=value_pair, + **{values_parameter: value_pair}, source_location=source_location, timeout=None, platform=acr_tasks_models.PlatformProperties( diff --git a/src/acrcssc/azext_acrcssc/tests/latest/test_helper_taskoperations.py b/src/acrcssc/azext_acrcssc/tests/latest/test_helper_taskoperations.py index c0f7b031b4f..8bd73b8d20e 100644 --- a/src/acrcssc/azext_acrcssc/tests/latest/test_helper_taskoperations.py +++ b/src/acrcssc/azext_acrcssc/tests/latest/test_helper_taskoperations.py @@ -225,10 +225,12 @@ def test_acr_cssc_dry_run(self, mock_LongRunningOperation, mock_get_acr_tasks_mo task_run_request = mock.sentinel.task_run_request platform = mock.sentinel.platform credentials = mock.sentinel.credentials + file_task_run_request = mock.Mock(return_value=task_run_request) + file_task_run_request.__annotations__ = {"values_property": list} task_models = SimpleNamespace( OS=SimpleNamespace(linux=SimpleNamespace(value="linux")), Architecture=SimpleNamespace(amd64=SimpleNamespace(value="amd64")), - FileTaskRunRequest=mock.Mock(return_value=task_run_request), + FileTaskRunRequest=file_task_run_request, PlatformProperties=mock.Mock(return_value=platform), Credentials=mock.Mock(return_value=credentials)) mock_get_acr_tasks_models.return_value = task_models @@ -256,7 +258,7 @@ def test_acr_cssc_dry_run(self, mock_LongRunningOperation, mock_get_acr_tasks_mo task_models.FileTaskRunRequest.assert_called_once_with( task_file_path="tmp_dry_run_template.yaml", values_file_path=None, - values=[{"name": "CONFIGPATH", "value": "test_config_file_path"}], + values_property=[{"name": "CONFIGPATH", "value": "test_config_file_path"}], source_location=mock_prepare_source_location.return_value, timeout=None, platform=platform, diff --git a/src/acrcssc/azext_acrcssc/tests/latest/test_task_operation_compatibility.py b/src/acrcssc/azext_acrcssc/tests/latest/test_task_operation_compatibility.py index f24df176b06..9c099335993 100644 --- a/src/acrcssc/azext_acrcssc/tests/latest/test_task_operation_compatibility.py +++ b/src/acrcssc/azext_acrcssc/tests/latest/test_task_operation_compatibility.py @@ -12,6 +12,7 @@ from azext_acrcssc.helper._taskoperations import ( _cancel_task_runs, _delete_task, + _get_file_task_run_values_parameter, _get_task_operation, _update_task_schedule, _update_task_yaml) @@ -71,6 +72,33 @@ def test_delete_task_uses_current_sdk_delete( "mocktask") mock_long_running_operation.assert_not_called() + def test_file_task_run_values_parameter_uses_current_sdk_name(self): + model = SimpleNamespace( + __annotations__={"values_property": list}) + + self.assertEqual( + _get_file_task_run_values_parameter(model), + "values_property") + + def test_file_task_run_values_parameter_uses_legacy_sdk_name(self): + model = SimpleNamespace( + __annotations__={}, + _attribute_map={"values": {}}) + + self.assertEqual( + _get_file_task_run_values_parameter(model), + "values") + + def test_file_task_run_values_parameter_rejects_unknown_model(self): + model = SimpleNamespace( + __annotations__={}, + _attribute_map={}) + + with self.assertRaisesRegex( + TypeError, + "defines neither values_property nor values"): + _get_file_task_run_values_parameter(model) + @mock.patch("azext_acrcssc.helper._taskoperations.LongRunningOperation") @mock.patch("azext_acrcssc.helper._taskoperations._delete_task_role_assignment") @mock.patch("azext_acrcssc.helper._taskoperations.cf_acr_tasks") diff --git a/src/acrcssc/setup.py b/src/acrcssc/setup.py index 22d7de0cdc4..08a4180b8dc 100644 --- a/src/acrcssc/setup.py +++ b/src/acrcssc/setup.py @@ -13,7 +13,7 @@ except ImportError: logging.warning("Wheel is not available, disabling bdist_wheel hook") -VERSION = '1.0.0b9' +VERSION = '1.0.0b8' # The full list of classifiers is available at # https://pypi.python.org/pypi?%3Aaction=list_classifiers From dd9b8437479622d6742d1b9732baee1f6b5b3174 Mon Sep 17 00:00:00 2001 From: cegraybl_microsoft Date: Thu, 17 Sep 2026 18:40:30 +0000 Subject: [PATCH 4/4] legacy fallback now resolves models with pinned version while split-SDK path still uses get_sdk() --- src/acrcssc/azext_acrcssc/_client_factory.py | 8 ++++++-- .../tests/latest/test_client_factory.py | 15 ++++++--------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/acrcssc/azext_acrcssc/_client_factory.py b/src/acrcssc/azext_acrcssc/_client_factory.py index b8198164f15..1b0deec42da 100644 --- a/src/acrcssc/azext_acrcssc/_client_factory.py +++ b/src/acrcssc/azext_acrcssc/_client_factory.py @@ -28,8 +28,12 @@ def get_acr_tasks_models(cli_ctx): tasks_resource_type = getattr( ResourceType, "MGMT_CONTAINERREGISTRYTASKS", - ResourceType.MGMT_CONTAINERREGISTRY) - return get_sdk(cli_ctx, tasks_resource_type, "models") + None) + if tasks_resource_type is not None: + return get_sdk(cli_ctx, tasks_resource_type, "models") + + return ContainerRegistryManagementClient.models( + ACR_API_VERSION_2019_06_01_PREVIEW) def cf_acr(cli_ctx, *_) -> ContainerRegistryManagementClient: diff --git a/src/acrcssc/azext_acrcssc/tests/latest/test_client_factory.py b/src/acrcssc/azext_acrcssc/tests/latest/test_client_factory.py index 0f6bf1edd60..b73910c164a 100644 --- a/src/acrcssc/azext_acrcssc/tests/latest/test_client_factory.py +++ b/src/acrcssc/azext_acrcssc/tests/latest/test_client_factory.py @@ -104,21 +104,18 @@ def test_task_models_use_split_resource_type_when_available(self): mock.sentinel.tasks_resource_type, "models") - def test_task_models_fall_back_to_legacy_resource_type(self): + def test_task_models_match_legacy_client_api_version(self): resource_types = SimpleNamespace( MGMT_CONTAINERREGISTRY=mock.sentinel.legacy_resource_type) with mock.patch.object(_client_factory, "ResourceType", resource_types), \ mock.patch.object( - _client_factory, - "get_sdk", + _client_factory.ContainerRegistryManagementClient, + "models", create=True, - return_value=mock.sentinel.models) as get_sdk: + return_value=mock.sentinel.models) as get_models: models = _client_factory.get_acr_tasks_models(mock.sentinel.cli_ctx) self.assertIs(models, mock.sentinel.models) - get_sdk.assert_called_once_with( - mock.sentinel.cli_ctx, - mock.sentinel.legacy_resource_type, - "models") - + get_models.assert_called_once_with( + ACR_API_VERSION_2019_06_01_PREVIEW)