diff --git a/src/workload-orchestration/HISTORY.rst b/src/workload-orchestration/HISTORY.rst index 2b72d000c45..f80c37473ad 100644 --- a/src/workload-orchestration/HISTORY.rst +++ b/src/workload-orchestration/HISTORY.rst @@ -2,6 +2,11 @@ Release History =============== +5.1.0 +++++++ +* Added new target solution management command: + * ``az workload-orchestration target unstage`` - Unstage a solution version from a target +* Added double confirmation before ``az workload-orchestration target remove-revision`` to prevent accidental deletions 5.0.0 ++++++ diff --git a/src/workload-orchestration/azext_workload_orchestration/aaz/latest/workload_orchestration/solution_template/_delete_version.py b/src/workload-orchestration/azext_workload_orchestration/aaz/latest/workload_orchestration/solution_template/_delete_version.py index 2311d34bdb8..1a3f3db43c0 100644 --- a/src/workload-orchestration/azext_workload_orchestration/aaz/latest/workload_orchestration/solution_template/_delete_version.py +++ b/src/workload-orchestration/azext_workload_orchestration/aaz/latest/workload_orchestration/solution_template/_delete_version.py @@ -13,6 +13,7 @@ @register_command( "workload-orchestration solution-template delete-version", + confirmation="Are you sure you want to perform this operation?", ) class DeleteVersion(AAZCommand): """Delete Solution Template Version Resource diff --git a/src/workload-orchestration/azext_workload_orchestration/aaz/latest/workload_orchestration/target/__init__.py b/src/workload-orchestration/azext_workload_orchestration/aaz/latest/workload_orchestration/target/__init__.py index 13c2517b178..119ad79b298 100644 --- a/src/workload-orchestration/azext_workload_orchestration/aaz/latest/workload_orchestration/target/__init__.py +++ b/src/workload-orchestration/azext_workload_orchestration/aaz/latest/workload_orchestration/target/__init__.py @@ -20,6 +20,7 @@ from ._update_external_validation_status import * from ._show import * from ._uninstall import * +from ._unstage import * from ._update import * from ._wait import * from ._solution_instance_list import * diff --git a/src/workload-orchestration/azext_workload_orchestration/aaz/latest/workload_orchestration/target/_unstage.py b/src/workload-orchestration/azext_workload_orchestration/aaz/latest/workload_orchestration/target/_unstage.py new file mode 100644 index 00000000000..2fe80b92d0e --- /dev/null +++ b/src/workload-orchestration/azext_workload_orchestration/aaz/latest/workload_orchestration/target/_unstage.py @@ -0,0 +1,577 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# +# Code generated by aaz-dev-tools +# -------------------------------------------------------------------------------------------- + +# pylint: skip-file +# flake8: noqa + +from azure.cli.core.aaz import * + + +@register_command( + "workload-orchestration target unstage", +) +class Unstage(AAZCommand): + """Unstage a Solution Version Resource + :example: Unstage a solution version from a target + az workload-orchestration target unstage -n myTarget -g myResourceGroup --solution-version-id /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg/providers/Microsoft.Edge/targets/test/solutions/AppA/versions/1.0.0 + + """ + + _aaz_info = { + "version": "2025-08-01", + "resources": [ + ["mgmt-plane", "/subscriptions/{}/resourcegroups/{}/providers/Microsoft.Edge/targets/{}/unstageSolutionVersion", "2025-08-01"], + ] + } + + AZ_SUPPORT_NO_WAIT = True + + def _handler(self, command_args): + super()._handler(command_args) + return self.build_lro_poller(self._execute_operations, self._output) + + _args_schema = None + + @classmethod + def _build_arguments_schema(cls, *args, **kwargs): + if cls._args_schema is not None: + return cls._args_schema + cls._args_schema = super()._build_arguments_schema(*args, **kwargs) + + # define Arg Group "" + + _args_schema = cls._args_schema + _args_schema.resource_group = AAZResourceGroupNameArg( + required=True, + ) + _args_schema.target_name = AAZStrArg( + options=["--target-name", "--name", "-n"], + help="Name of the target", + required=True, + id_part="name", + fmt=AAZStrArgFormat( + pattern="^[a-zA-Z0-9]([-a-zA-Z0-9]*[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([-a-zA-Z0-9]*[a-zA-Z0-9])?)*$", + max_length=61, + min_length=3, + ), + ) + + # define Arg Group "Body" + + _args_schema = cls._args_schema + _args_schema.solution_version_id = AAZStrArg( + options=["--solution-version-id"], + arg_group="Body", + help="Solution Version ARM Id", + required=True, + ) + return cls._args_schema + + def _execute_operations(self): + self.pre_operations() + # Step 1: Trigger unstage LRO + yield self.TargetsUnstageSolutionVersion(ctx=self.ctx)() + # Step 2: Fetch the updated SolutionVersion after LRO completes + if not getattr(self.ctx.args, "no_wait", False): + self.SolutionVersionsGet(ctx=self.ctx, command_instance=self)() + self.post_operations() + + @register_callback + def pre_operations(self): + from azure.cli.core.azclierror import ValidationError + from knack.log import get_logger + + # Validate solution-version-id format before making any API calls + solution_version_id = self.ctx.args.solution_version_id.to_serialized_data() + parts = solution_version_id.split('/') + solution_name = None + solution_version_name = None + for i, part in enumerate(parts): + if part.lower() == 'solutions' and i + 1 < len(parts): + solution_name = parts[i + 1] + if part.lower() == 'versions' and i + 1 < len(parts): + solution_version_name = parts[i + 1] + + # Validate that required segments were found in the ARM ID + if solution_name is None or solution_version_name is None: + raise ValidationError( + f"Invalid solution-version-id format: '{solution_version_id}'. " + f"Expected format: '/subscriptions/{{subscriptionId}}/resourceGroups/{{resourceGroupName}}/" + f"providers/Microsoft.Edge/targets/{{targetName}}/solutions/{{solutionName}}/versions/{{versionName}}'" + ) + + # Store parsed values as instance variables for reuse in SolutionVersionsGet + self._solution_name = solution_name + self._solution_version_name = solution_version_name + + logger = get_logger(__name__) + logger.warning("WARNING: Unstaging this solution version will remove associated images from the connected registry within next 24 hours.") + + @register_callback + def post_operations(self): + pass + + def _output(self, *args, **kwargs): + result = self.deserialize_output(self.ctx.vars.instance, client_flatten=True) + return result + + class TargetsUnstageSolutionVersion(AAZHttpOperation): + CLIENT_TYPE = "MgmtClient" + + def __call__(self, *args, **kwargs): + request = self.make_request() + session = self.client.send_request(request=request, stream=False, **kwargs) + if session.http_response.status_code in [202]: + return self.client.build_lro_polling( + self.ctx.args.no_wait, + session, + self.on_200, + self.on_error, + lro_options={"final-state-via": "location"}, + path_format_arguments=self.url_parameters, + ) + + return self.on_error(session.http_response) + + @property + def url(self): + return self.client.format_url( + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Edge/targets/{targetName}/unstageSolutionVersion", + **self.url_parameters + ) + + @property + def method(self): + return "POST" + + @property + def error_format(self): + return "MgmtErrorFormat" + + @property + def url_parameters(self): + parameters = { + **self.serialize_url_param( + "resourceGroupName", self.ctx.args.resource_group, + required=True, + ), + **self.serialize_url_param( + "subscriptionId", self.ctx.subscription_id, + required=True, + ), + **self.serialize_url_param( + "targetName", self.ctx.args.target_name, + required=True, + ), + } + return parameters + + @property + def query_parameters(self): + parameters = { + **self.serialize_query_param( + "api-version", "2025-08-01", + required=True, + ), + } + return parameters + + @property + def header_parameters(self): + parameters = { + **self.serialize_header_param( + "Content-Type", "application/json", + ), + } + return parameters + + @property + def content(self): + _content_value, _builder = self.new_content_builder( + self.ctx.args, + typ=AAZObjectType, + typ_kwargs={"flags": {"required": True, "client_flatten": True}} + ) + _builder.set_prop("solutionVersionId", AAZStrType, ".solution_version_id", typ_kwargs={"flags": {"required": True}}) + + return self.serialize_content(_content_value) + + def on_200(self, session): + # LRO completed - operation status returned, not the SolutionVersion + # The actual SolutionVersion will be fetched by SolutionVersionsGet + pass + + class SolutionVersionsGet(AAZHttpOperation): + """GET the SolutionVersion after LRO completes""" + CLIENT_TYPE = "MgmtClient" + + def __init__(self, ctx, command_instance): + super().__init__(ctx) + self.command_instance = command_instance + + def __call__(self, *args, **kwargs): + request = self.make_request() + session = self.client.send_request(request=request, stream=False, **kwargs) + if session.http_response.status_code in [200]: + return self.on_200(session) + return self.on_error(session.http_response) + + @property + def url(self): + return self.client.format_url( + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Edge/targets/{targetName}/solutions/{solutionName}/versions/{solutionVersionName}", + **self.url_parameters + ) + + @property + def method(self): + return "GET" + + @property + def error_format(self): + return "MgmtErrorFormat" + + @property + def url_parameters(self): + parameters = { + **self.serialize_url_param( + "resourceGroupName", self.ctx.args.resource_group, + required=True, + ), + **self.serialize_url_param( + "subscriptionId", self.ctx.subscription_id, + required=True, + ), + **self.serialize_url_param( + "targetName", self.ctx.args.target_name, + required=True, + ), + **self.serialize_url_param( + "solutionName", self.command_instance._solution_name, + required=True, + ), + **self.serialize_url_param( + "solutionVersionName", self.command_instance._solution_version_name, + required=True, + ), + } + return parameters + + @property + def query_parameters(self): + parameters = { + **self.serialize_query_param( + "api-version", "2025-08-01", + required=True, + ), + } + return parameters + + @property + def header_parameters(self): + parameters = { + **self.serialize_header_param( + "Accept", "application/json", + ), + } + return parameters + + def on_200(self, session): + data = self.deserialize_http_content(session) + self.ctx.set_var( + "instance", + data, + schema_builder=self._build_schema_on_200 + ) + + _schema_on_200 = None + + @classmethod + def _build_schema_on_200(cls): + if cls._schema_on_200 is not None: + return cls._schema_on_200 + + cls._schema_on_200 = AAZObjectType() + + _schema_on_200 = cls._schema_on_200 + _schema_on_200.e_tag = AAZStrType( + serialized_name="eTag", + flags={"read_only": True}, + ) + _schema_on_200.extended_location = AAZObjectType( + serialized_name="extendedLocation", + ) + _schema_on_200.id = AAZStrType( + flags={"read_only": True}, + ) + _schema_on_200.name = AAZStrType( + flags={"read_only": True}, + ) + _schema_on_200.properties = AAZObjectType() + _schema_on_200.system_data = AAZObjectType( + serialized_name="systemData", + flags={"read_only": True}, + ) + _schema_on_200.type = AAZStrType( + flags={"read_only": True}, + ) + + extended_location = _schema_on_200.extended_location + extended_location.name = AAZStrType() + extended_location.type = AAZStrType() + + properties = _schema_on_200.properties + properties.action_type = AAZStrType( + serialized_name="actionType", + flags={"read_only": True}, + ) + properties.configuration = AAZStrType( + flags={"read_only": True}, + ) + properties.current_stage = AAZObjectType( + serialized_name="currentStage", + flags={"read_only": True}, + ) + properties.error_details = AAZObjectType( + serialized_name="errorDetails", + flags={"read_only": True}, + ) + properties.external_validation_id = AAZStrType( + serialized_name="externalValidationId", + flags={"read_only": True}, + ) + properties.latest_action_tracking_uri = AAZStrType( + serialized_name="latestActionTrackingUri", + flags={"read_only": True}, + ) + properties.latest_action_triggered_by = AAZStrType( + serialized_name="latestActionTriggeredBy", + flags={"read_only": True}, + ) + properties.provisioning_state = AAZStrType( + serialized_name="provisioningState", + flags={"read_only": True}, + ) + properties.review_id = AAZStrType( + serialized_name="reviewId", + flags={"read_only": True}, + ) + properties.revision = AAZIntType( + flags={"read_only": True}, + ) + properties.solution_dependencies = AAZListType( + serialized_name="solutionDependencies", + flags={"read_only": True}, + ) + properties.solution_instance_name = AAZStrType( + serialized_name="solutionInstanceName", + flags={"read_only": True}, + ) + properties.solution_template_version_id = AAZStrType( + serialized_name="solutionTemplateVersionId", + flags={"read_only": True}, + ) + properties.specification = AAZFreeFormDictType() + properties.stages = AAZListType( + flags={"read_only": True}, + ) + properties.state = AAZStrType( + flags={"read_only": True}, + ) + properties.target_display_name = AAZStrType( + serialized_name="targetDisplayName", + flags={"read_only": True}, + ) + properties.target_level_configuration = AAZStrType( + serialized_name="targetLevelConfiguration", + flags={"read_only": True}, + ) + + current_stage = _schema_on_200.properties.current_stage + current_stage.description = AAZStrType( + flags={"read_only": True}, + ) + current_stage.display_state = AAZStrType( + serialized_name="displayState", + flags={"read_only": True}, + ) + current_stage.reason = AAZStrType( + flags={"read_only": True}, + ) + current_stage.stage = AAZStrType() + current_stage.updated_time = AAZStrType( + serialized_name="updatedTime", + flags={"read_only": True}, + ) + + error_details = _schema_on_200.properties.error_details + error_details.additional_info = AAZListType( + serialized_name="additionalInfo", + flags={"read_only": True}, + ) + error_details.code = AAZStrType( + flags={"read_only": True}, + ) + error_details.details = AAZListType( + flags={"read_only": True}, + ) + error_details.message = AAZStrType( + flags={"read_only": True}, + ) + error_details.target = AAZStrType( + flags={"read_only": True}, + ) + + additional_info = _schema_on_200.properties.error_details.additional_info + additional_info.Element = AAZObjectType() + additional_info.Element.info = AAZFreeFormDictType( + flags={"read_only": True}, + ) + additional_info.Element.type = AAZStrType( + flags={"read_only": True}, + ) + + details = _schema_on_200.properties.error_details.details + details.Element = AAZObjectType() + _UnstageHelper._build_schema_error_detail_read(details.Element) + + solution_dependencies = _schema_on_200.properties.solution_dependencies + solution_dependencies.Element = AAZObjectType() + _UnstageHelper._build_schema_solution_dependency_read(solution_dependencies.Element) + + stages = _schema_on_200.properties.stages + stages.Element = AAZObjectType() + stages.Element.description = AAZStrType( + flags={"read_only": True}, + ) + stages.Element.display_state = AAZStrType( + serialized_name="displayState", + flags={"read_only": True}, + ) + stages.Element.reason = AAZStrType( + flags={"read_only": True}, + ) + stages.Element.stage = AAZStrType() + stages.Element.updated_time = AAZStrType( + serialized_name="updatedTime", + flags={"read_only": True}, + ) + + system_data = _schema_on_200.system_data + system_data.created_at = AAZStrType( + serialized_name="createdAt", + ) + system_data.created_by = AAZStrType( + serialized_name="createdBy", + ) + system_data.created_by_type = AAZStrType( + serialized_name="createdByType", + ) + system_data.last_modified_at = AAZStrType( + serialized_name="lastModifiedAt", + ) + system_data.last_modified_by = AAZStrType( + serialized_name="lastModifiedBy", + ) + system_data.last_modified_by_type = AAZStrType( + serialized_name="lastModifiedByType", + ) + + return cls._schema_on_200 + + +class _UnstageHelper: + """Helper class for Unstage""" + + _schema_error_detail_read = None + + @classmethod + def _build_schema_error_detail_read(cls, _schema): + if cls._schema_error_detail_read is not None: + _schema.additional_info = cls._schema_error_detail_read.additional_info + _schema.code = cls._schema_error_detail_read.code + _schema.details = cls._schema_error_detail_read.details + _schema.message = cls._schema_error_detail_read.message + _schema.target = cls._schema_error_detail_read.target + return + + cls._schema_error_detail_read = _schema_error_detail_read = AAZObjectType() + + _schema_error_detail_read.additional_info = AAZListType( + serialized_name="additionalInfo", + flags={"read_only": True}, + ) + _schema_error_detail_read.code = AAZStrType( + flags={"read_only": True}, + ) + _schema_error_detail_read.details = AAZListType( + flags={"read_only": True}, + ) + _schema_error_detail_read.message = AAZStrType( + flags={"read_only": True}, + ) + _schema_error_detail_read.target = AAZStrType( + flags={"read_only": True}, + ) + + additional_info = _schema_error_detail_read.additional_info + additional_info.Element = AAZObjectType() + additional_info.Element.info = AAZFreeFormDictType( + flags={"read_only": True}, + ) + additional_info.Element.type = AAZStrType( + flags={"read_only": True}, + ) + + details = _schema_error_detail_read.details + details.Element = AAZObjectType() + cls._build_schema_error_detail_read(details.Element) + + _schema.additional_info = cls._schema_error_detail_read.additional_info + _schema.code = cls._schema_error_detail_read.code + _schema.details = cls._schema_error_detail_read.details + _schema.message = cls._schema_error_detail_read.message + _schema.target = cls._schema_error_detail_read.target + + _schema_solution_dependency_read = None + + @classmethod + def _build_schema_solution_dependency_read(cls, _schema): + if cls._schema_solution_dependency_read is not None: + _schema.dependencies = cls._schema_solution_dependency_read.dependencies + _schema.solution_template_version_id = cls._schema_solution_dependency_read.solution_template_version_id + _schema.solution_version_id = cls._schema_solution_dependency_read.solution_version_id + _schema.target_id = cls._schema_solution_dependency_read.target_id + return + + cls._schema_solution_dependency_read = _schema_solution_dependency_read = AAZObjectType() + + _schema_solution_dependency_read.dependencies = AAZListType() + _schema_solution_dependency_read.solution_template_version_id = AAZStrType( + serialized_name="solutionTemplateVersionId", + flags={"required": True}, + ) + _schema_solution_dependency_read.solution_version_id = AAZStrType( + serialized_name="solutionVersionId", + flags={"required": True}, + ) + _schema_solution_dependency_read.target_id = AAZStrType( + serialized_name="targetId", + flags={"required": True}, + ) + + dependencies = _schema_solution_dependency_read.dependencies + dependencies.Element = AAZObjectType() + cls._build_schema_solution_dependency_read(dependencies.Element) + + _schema.dependencies = cls._schema_solution_dependency_read.dependencies + _schema.solution_template_version_id = cls._schema_solution_dependency_read.solution_template_version_id + _schema.solution_version_id = cls._schema_solution_dependency_read.solution_version_id + _schema.target_id = cls._schema_solution_dependency_read.target_id + + +__all__ = ["Unstage"] diff --git a/src/workload-orchestration/setup.py b/src/workload-orchestration/setup.py index 06f8a0c6372..32448955dc4 100644 --- a/src/workload-orchestration/setup.py +++ b/src/workload-orchestration/setup.py @@ -10,7 +10,7 @@ # HISTORY.rst entry. -VERSION = '5.0.0' +VERSION = '5.1.0' # The full list of classifiers is available at # https://pypi.python.org/pypi?%3Aaction=list_classifiers