From e9c13bedb41e32cdba66a2025a9b482fa631ad79 Mon Sep 17 00:00:00 2001 From: Kyle Albert <5498623+kalbert312@users.noreply.github.com> Date: Fri, 20 Feb 2026 12:56:37 -0500 Subject: [PATCH 01/32] Add stack-whatif commands. --- .../cli/command_modules/resource/_help.py | 96 +++++++++++++++++++ .../cli/command_modules/resource/_params.py | 3 +- .../cli/command_modules/resource/commands.py | 47 +++++---- 3 files changed, 119 insertions(+), 27 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/resource/_help.py b/src/azure-cli/azure/cli/command_modules/resource/_help.py index 524c4a48210..2883b915eba 100644 --- a/src/azure-cli/azure/cli/command_modules/resource/_help.py +++ b/src/azure-cli/azure/cli/command_modules/resource/_help.py @@ -2460,6 +2460,12 @@ long-summary: Deployment stacks are defined in ARM as the type Microsoft.Resources/deploymentStacks. """ +helps['stack-whatif'] = """ +type: group +short-summary: A deployment stack What-If is a preview of an operation to be performed on a new or existing deployment stack. +long-summary: Deployment stacks What-Ifs are defined in ARM as the type Microsoft.Resources/deploymentStacksWhatIfResults. +""" + helps['stack mg'] = """ type: group short-summary: Manage Deployment Stacks at management group. @@ -2547,6 +2553,34 @@ text: az stack mg delete --id /providers/Microsoft.Management/managementGroups/myMg/providers/Microsoft.Resources/deploymentStacks/StackName --management-group-id myMg --action-on-unmanage deleteAll """ +helps['stack-whatif mg'] = """ +type: group +short-summary: Manage Deployment Stacks What-Ifs at management group. +""" + +# TODO(kylealbert): params in examples +helps['stack-whatif mg create'] = """ +type: command +short-summary: Preview a deployment stack operation at management group scope. +examples: + - name: Perform a what-if on a deployment stack using template file and detach all resources on unmanage. + text: az stack-whatif mg create --name StackName --management-group-id myMg --template-file simpleTemplate.json --location westus2 --description description --deny-settings-mode None --action-on-unmanage detachAll + - name: Perform a what-if on a deployment stack with parameter file and delete resources on unmanage. + text: az stack-whatif mg create --name StackName --management-group-id myMg --action-on-unmanage deleteResources --template-file simpleTemplate.json --parameters simpleTemplateParams.json --location westus2 --description description --deny-settings-mode None + - name: Perform a what-if on a deployment stack with template spec. + text: az stack-whatif mg create --name StackName --management-group-id myMg --template-spec TemplateSpecResourceIDWithVersion --location westus2 --description description --deny-settings-mode None --action-on-unmanage deleteResources + - name: Perform a what-if on a deployment stack using bicep file and delete all resources on unmanage. + text: az stack-whatif mg create --name StackName --management-group-id myMg --action-on-unmanage deleteAll --template-file simple.bicep --location westus2 --description description --deny-settings-mode None + - name: Perform a what-if on a deployment stack using parameters from key/value pairs. + text: az stack-whatif mg create --name StackName --management-group-id myMg --template-file simpleTemplate.json --location westus --description description --parameters simpleTemplateParams.json value1=foo value2=bar --deny-settings-mode None --action-on-unmanage deleteResources + - name: Perform a what-if on a deployment stack from a local template, using a parameter file, a remote parameter file, and selectively overriding key/value pairs. + text: az stack-whatif mg create --name StackName --management-group-id myMg --template-file azuredeploy.json --parameters @params.json --parameters https://mysite/params.json --parameters MyValue=This MyArray=@array.json --location westus --deny-settings-mode None --action-on-unmanage deleteResources + - name: Perform a what-if on a deployment stack from a local template, using deny settings. + text: az stack-whatif mg create --name StackName --management-group-id myMg --template-file azuredeploy.json --deny-settings-mode denyDelete --deny-settings-excluded-actions Microsoft.Compute/virtualMachines/write --deny-settings-excluded-principals "test1 test2" --location westus --action-on-unmanage deleteResources + - name: Perform a what-if on a deployment stack from a local template, apply deny settings to child scope. + text: az stack-whatif mg create --name StackName --management-group-id myMg --template-file azuredeploy.json --deny-settings-mode denyDelete --deny-settings-excluded-actions Microsoft.Compute/virtualMachines/write --deny-settings-apply-to-child-scopes --location westus --action-on-unmanage deleteResources +""" + helps['stack sub'] = """ type: group short-summary: Manage Deployment Stacks at subscription. @@ -2642,6 +2676,38 @@ text: az stack sub delete --id /subscriptions/111111111111/providers/Microsoft.Resources/deploymentStacks/StackName --action-on-unmanage detachAll """ +helps['stack-whatif sub'] = """ +type: group +short-summary: Manage Deployment Stacks What-Ifs at subscription. +""" + +# TODO(kylealbert): params in examples +helps['stack-whatif sub create'] = """ +type: command +short-summary: Preview a deployment stack operation at subscription scope. +examples: + - name: Perform a what-if on a deployment stack using template file and detach all resources on unmanage. + text: az stack-whatif sub create --name StackName --template-file simpleTemplate.json --location westus2 --description description --deny-settings-mode None --action-on-unmanage detachAll + - name: Perform a what-if on a deployment stack with parameter file and delete resources on unmanage. + text: az stack-whatif sub create --name StackName --action-on-unmanage deleteResources --template-file simpleTemplate.json --parameters simpleTemplateParams.json --location westus2 --description description --deny-settings-mode None + - name: Perform a what-if on a deployment stack with template spec. + text: az stack-whatif sub create --name StackName --template-spec TemplateSpecResourceIDWithVersion --location westus2 --description description --deny-settings-mode None --action-on-unmanage deleteResources + - name: Perform a what-if on a deployment stack using bicep file and delete all resources on unmanage. + text: az stack-whatif sub create --name StackName --action-on-unmanage deleteAll --template-file simple.bicep --location westus2 --description description --deny-settings-mode None + - name: Perform a what-if on a deployment stack at a different subscription. + text: az stack-whatif sub create --name StackName --template-file simpleTemplate.json --location westus2 --description description --subscription subscriptionId --deny-settings-mode None --action-on-unmanage deleteResources + - name: Perform a what-if on a deployment stack and deploy at the resource group scope. + text: az stack-whatif sub create --name StackName --template-file simpleTemplate.json --location westus --deployment-resource-group ResourceGroup --description description --deny-settings-mode None --action-on-unmanage deleteResources + - name: Perform a what-if on a deployment stack using parameters from key/value pairs. + text: az stack-whatif sub create --name StackName --template-file simpleTemplate.json --location westus --description description --parameters simpleTemplateParams.json value1=foo value2=bar --deny-settings-mode None --action-on-unmanage deleteResources + - name: Perform a what-if on a deployment stack from a local template, using a parameter file, a remote parameter file, and selectively overriding key/value pairs. + text: az stack-whatif sub create --name StackName --template-file azuredeploy.json --parameters @params.json --parameters https://mysite/params.json --parameters MyValue=This MyArray=@array.json --location westus --deny-settings-mode None --action-on-unmanage deleteResources + - name: Perform a what-if on a deployment stack from a local template, using deny settings. + text: az stack-whatif sub create --name StackName --template-file azuredeploy.json --deny-settings-mode denyDelete --deny-settings-excluded-actions Microsoft.Compute/virtualMachines/write --deny-settings-excluded-principals "test1 test2" --location westus --action-on-unmanage deleteResources + - name: Perform a what-if on a deployment stack from a local template, apply deny settings to child scopes. + text: az stack-whatif sub create --name StackName --template-file azuredeploy.json --deny-settings-mode denyDelete --deny-settings-excluded-actions Microsoft.Compute/virtualMachines/write --deny-settings-apply-to-child-scopes --location westus --action-on-unmanage deleteResources +""" + helps['stack group'] = """ type: group short-summary: Manage Deployment Stacks at resource group. @@ -2733,6 +2799,36 @@ text: az stack group delete --id /subscriptions/111111111111/resourceGroups/ResourceGroup/providers/Microsoft.Resources/deploymentStacks/StackName --action-on-unmanage detachAll """ +helps['stack-whatif group'] = """ +type: group +short-summary: Manage Deployment Stacks What-Ifs at resource group. +""" + +# TODO(kylealbert): params in examples +helps['stack-whatif group create'] = """ +type: command +short-summary: Preview a deployment stack operation at resource group scope. +examples: + - name: Perform a what-if on a deployment stack using template file and delete resources on unmanage. + text: az stack-whatif group create --name StackName --resource-group ResourceGroup --action-on-unmanage deleteResources --template-file simpleTemplate.json --description description --deny-settings-mode None + - name: Perform a what-if on a deployment stack with parameter file and detach all resources on unmanage. + text: az stack-whatif group create --name StackName --resource-group ResourceGroup --action-on-unmanage detachAll --template-file simpleTemplate.json --parameters simpleTemplateParams.json --description description --deny-settings-mode None + - name: Perform a what-if on a deployment stack with template spec and delete all resources on unmanage. + text: az stack-whatif group create --name StackName --resource-group ResourceGroup --action-on-unmanage deleteAll --template-spec TemplateSpecResourceIDWithVersion --description description --deny-settings-mode None + - name: Perform a what-if on a deployment stack using bicep file. + text: az stack-whatif group create --name StackName --resource-group ResourceGroup --template-file simple.bicep --description description --deny-settings-mode None --action-on-unmanage deleteResources + - name: Perform a what-if on a deployment stack at a different subscription. + text: az stack-whatif group create --name StackName --resource-group ResourceGroup --template-file simpleTemplate.json --description description --subscription subscriptionId --deny-settings-mode None --action-on-unmanage deleteResources + - name: Perform a what-if on a deployment stack using parameters from key/value pairs. + text: az stack-whatif group create --name StackName --template-file simpleTemplate.json --resource-group ResourceGroup --description description --parameters simpleTemplateParams.json value1=foo value2=bar --deny-settings-mode None --action-on-unmanage deleteResources + - name: Perform a what-if on a deployment stack from a local template, using a parameter file, a remote parameter file, and selectively overriding key/value pairs. + text: az stack-whatif group create --name StackName --template-file azuredeploy.json --parameters @params.json --parameters https://mysite/params.json --parameters MyValue=This MyArray=@array.json --resource-group ResourceGroup --deny-settings-mode None --action-on-unmanage deleteResources + - name: Perform a what-if on a deployment stack from a local template, using deny settings. + text: az stack-whatif group create --name StackName --resource-group ResourceGroup --template-file azuredeploy.json --deny-settings-mode denyDelete --deny-settings-excluded-actions Microsoft.Compute/virtualMachines/write --deny-settings-excluded-principals "test1 test2" --action-on-unmanage deleteResources + - name: Perform a what-if on a deployment stack from a local template, apply deny setting to child scopes. + text: az stack-whatif group create --name StackName --resource-group ResourceGroup --template-file azuredeploy.json --deny-settings-mode denyDelete --deny-settings-excluded-actions Microsoft.Compute/virtualMachines/write --deny-settings-apply-to-child-scopes --action-on-unmanage deleteResources +""" + helps['bicep generate-params'] = """ type: command short-summary: Generate parameters file for a Bicep file. diff --git a/src/azure-cli/azure/cli/command_modules/resource/_params.py b/src/azure-cli/azure/cli/command_modules/resource/_params.py index 5540de0295b..2cde36bd47b 100644 --- a/src/azure-cli/azure/cli/command_modules/resource/_params.py +++ b/src/azure-cli/azure/cli/command_modules/resource/_params.py @@ -621,8 +621,7 @@ def load_arguments(self, _): with self.argument_context('stack mg') as c: c.argument('management_group_id', arg_type=management_group_id_type, help='The management group id to create stack at.') - # TODO(kylealbert): add "stack-whatif" - for resource_type in ['stack']: + for resource_type in ['stack', 'stack-whatif']: for scope in ['group', 'sub', 'mg']: for action in ['create', 'validate', 'delete', 'show', 'list', 'export']: if resource_type == 'stack-whatif' and (action == 'validate' or action == 'export'): diff --git a/src/azure-cli/azure/cli/command_modules/resource/commands.py b/src/azure-cli/azure/cli/command_modules/resource/commands.py index 968488eada1..90591d9caac 100644 --- a/src/azure-cli/azure/cli/command_modules/resource/commands.py +++ b/src/azure-cli/azure/cli/command_modules/resource/commands.py @@ -442,15 +442,14 @@ def load_command_table(self, _): 'validate', 'validate_deployment_stack_at_management_group', validator=validate_deployment_stack_files, exception_handler=handle_template_based_exception) - # TODO(kylealbert): not ready for march 2026 - # with self.command_group('stack-whatif mg', resource_deploymentstacks_sdk, resource_type=ResourceType.MGMT_RESOURCE_DEPLOYMENTSTACKS) as g: - # g.custom_show_command('show', 'show_deployment_stack_what_if_at_management_group', table_transformer=transform_stacks) - # g.custom_command('list', 'list_deployment_stack_what_if_at_management_group', table_transformer=transform_stacks_list) - # g.custom_command('delete', 'delete_deployment_stack_what_if_at_management_group') - # g.custom_command( - # 'create', 'create_deployment_stack_what_if_at_management_group', supports_no_wait=True, - # validator=validate_deployment_stack_files, table_transformer=transform_stacks, - # exception_handler=handle_template_based_exception) + with self.command_group('stack-whatif mg', resource_deploymentstacks_sdk, resource_type=ResourceType.MGMT_RESOURCE_DEPLOYMENTSTACKS) as g: + g.custom_show_command('show', 'show_deployment_stack_what_if_at_management_group', table_transformer=transform_stacks) + g.custom_command('list', 'list_deployment_stack_what_if_at_management_group', table_transformer=transform_stacks_list) + g.custom_command('delete', 'delete_deployment_stack_what_if_at_management_group') + g.custom_command( + 'create', 'create_deployment_stack_what_if_at_management_group', supports_no_wait=True, + validator=validate_deployment_stack_files, table_transformer=transform_stacks, + exception_handler=handle_template_based_exception) with self.command_group('stack sub', resource_deploymentstacks_sdk, resource_type=ResourceType.MGMT_RESOURCE_DEPLOYMENTSTACKS) as g: g.custom_show_command('show', 'show_deployment_stack_at_subscription', table_transformer=transform_stacks) @@ -464,14 +463,13 @@ def load_command_table(self, _): 'validate', 'validate_deployment_stack_at_subscription', validator=validate_deployment_stack_files, exception_handler=handle_template_based_exception) - # TODO(kylealbert): not ready for march 2026 - # with self.command_group('stack-whatif sub', resource_deploymentstacks_sdk, resource_type=ResourceType.MGMT_RESOURCE_DEPLOYMENTSTACKS) as g: - # g.custom_show_command('show', 'show_deployment_stack_what_if_at_subscription', table_transformer=transform_stacks) - # g.custom_command('list', 'list_deployment_stack_what_if_at_subscription', table_transformer=transform_stacks_list) - # g.custom_command('delete', 'delete_deployment_stack_what_if_at_subscription') - # g.custom_command( - # 'create', 'create_deployment_stack_what_if_at_subscription', supports_no_wait=True, validator=validate_deployment_stack_files, - # table_transformer=transform_stacks, exception_handler=handle_template_based_exception) + with self.command_group('stack-whatif sub', resource_deploymentstacks_sdk, resource_type=ResourceType.MGMT_RESOURCE_DEPLOYMENTSTACKS) as g: + g.custom_show_command('show', 'show_deployment_stack_what_if_at_subscription', table_transformer=transform_stacks) + g.custom_command('list', 'list_deployment_stack_what_if_at_subscription', table_transformer=transform_stacks_list) + g.custom_command('delete', 'delete_deployment_stack_what_if_at_subscription') + g.custom_command( + 'create', 'create_deployment_stack_what_if_at_subscription', supports_no_wait=True, validator=validate_deployment_stack_files, + table_transformer=transform_stacks, exception_handler=handle_template_based_exception) with self.command_group('stack group', resource_deploymentstacks_sdk, resource_type=ResourceType.MGMT_RESOURCE_DEPLOYMENTSTACKS) as g: g.custom_show_command('show', 'show_deployment_stack_at_resource_group', table_transformer=transform_stacks) @@ -485,14 +483,13 @@ def load_command_table(self, _): 'validate', 'validate_deployment_stack_at_resource_group', validator=validate_deployment_stack_files, exception_handler=handle_template_based_exception) - # TODO(kylealbert): not ready for march 2026 - # with self.command_group('stack-whatif group', resource_deploymentstacks_sdk, resource_type=ResourceType.MGMT_RESOURCE_DEPLOYMENTSTACKS) as g: - # g.custom_show_command('show', 'show_deployment_stack_what_if_at_resource_group', table_transformer=transform_stacks) - # g.custom_command('list', 'list_deployment_stack_what_if_at_resource_group', table_transformer=transform_stacks_list) - # g.custom_command('delete', 'delete_deployment_stack_what_if_at_resource_group') - # g.custom_command( - # 'create', 'create_deployment_stack_what_if_at_resource_group', supports_no_wait=True, validator=validate_deployment_stack_files, - # table_transformer=transform_stacks, exception_handler=handle_template_based_exception) + with self.command_group('stack-whatif group', resource_deploymentstacks_sdk, resource_type=ResourceType.MGMT_RESOURCE_DEPLOYMENTSTACKS) as g: + g.custom_show_command('show', 'show_deployment_stack_what_if_at_resource_group', table_transformer=transform_stacks) + g.custom_command('list', 'list_deployment_stack_what_if_at_resource_group', table_transformer=transform_stacks_list) + g.custom_command('delete', 'delete_deployment_stack_what_if_at_resource_group') + g.custom_command( + 'create', 'create_deployment_stack_what_if_at_resource_group', supports_no_wait=True, validator=validate_deployment_stack_files, + table_transformer=transform_stacks, exception_handler=handle_template_based_exception) # az deployment group with self.command_group('deployment group', resource_deployment_sdk, resource_type=ResourceType.MGMT_RESOURCE_DEPLOYMENTS) as g: From a7ba6d2e51f64aff60c27a1d30d8d894a9f726a0 Mon Sep 17 00:00:00 2001 From: Kyle Albert <5498623+kalbert312@users.noreply.github.com> Date: Fri, 20 Feb 2026 16:04:26 -0500 Subject: [PATCH 02/32] Add no pretty print argument. --- .../azure/cli/command_modules/resource/_params.py | 3 +++ .../azure/cli/command_modules/resource/custom.py | 12 ++++++------ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/resource/_params.py b/src/azure-cli/azure/cli/command_modules/resource/_params.py index 2cde36bd47b..6641cab71b1 100644 --- a/src/azure-cli/azure/cli/command_modules/resource/_params.py +++ b/src/azure-cli/azure/cli/command_modules/resource/_params.py @@ -661,6 +661,7 @@ def load_arguments(self, _): elif resource_type == 'stack-whatif': c.argument('stack_id', arg_type=stacks_whatif_stack_id_type) c.argument('retention_interval', arg_type=stacks_whatif_retention_interval_type) + c.argument('no_pretty_print', arg_type=deployment_what_if_no_pretty_print_type) if action == 'create' and resource_type == 'stack': c.argument('yes', help='Do not prompt for confirmation') elif action == 'delete': @@ -679,6 +680,8 @@ def load_arguments(self, _): c.argument('subscription', arg_type=subscription_type) if scope == 'group': c.argument('resource_group', arg_type=resource_group_name_type, help='The resource group where the deployment stack exists') + if resource_type == 'stack-whatif': + c.argument('no_pretty_print', arg_type=deployment_what_if_no_pretty_print_type) elif action == 'list': if scope == 'sub': continue # only uses global arguments diff --git a/src/azure-cli/azure/cli/command_modules/resource/custom.py b/src/azure-cli/azure/cli/command_modules/resource/custom.py index 67bfae06581..04e0d8c7430 100644 --- a/src/azure-cli/azure/cli/command_modules/resource/custom.py +++ b/src/azure-cli/azure/cli/command_modules/resource/custom.py @@ -2917,7 +2917,7 @@ def create_deployment_stack_what_if_at_resource_group( cmd, name, resource_group, stack_id, deny_settings_mode, action_on_unmanage, retention_interval, template_file=None, template_spec=None, template_uri=None, query_string=None, parameters=None, description=None, deny_settings_excluded_principals=None, deny_settings_excluded_actions=None, deny_settings_apply_to_child_scopes=False, resources_without_delete_support=None, - validation_level=None, tags=None + validation_level=None, tags=None, no_pretty_print=None ): rcf = _resource_deploymentstacks_client_factory(cmd.cli_ctx) @@ -2948,7 +2948,7 @@ def create_deployment_stack_what_if_at_resource_group( return whatif_result -def show_deployment_stack_what_if_at_resource_group(cmd, name=None, resource_group=None, id=None): # pylint: disable=redefined-builtin +def show_deployment_stack_what_if_at_resource_group(cmd, name=None, resource_group=None, id=None, no_pretty_print=None): # pylint: disable=redefined-builtin rcf = _resource_deploymentstacks_client_factory(cmd.cli_ctx) if name and resource_group: return rcf.deployment_stacks_what_if_results_at_resource_group.get(resource_group, name) @@ -3004,7 +3004,7 @@ def create_deployment_stack_what_if_at_subscription( cmd, name, location, stack_id, deny_settings_mode, action_on_unmanage, retention_interval, deployment_resource_group=None, template_file=None, template_spec=None, template_uri=None, query_string=None, parameters=None, description=None, deny_settings_excluded_principals=None, deny_settings_excluded_actions=None, deny_settings_apply_to_child_scopes=False, - resources_without_delete_support=None, validation_level=None, tags=None + resources_without_delete_support=None, validation_level=None, tags=None, no_pretty_print=None ): rcf = _resource_deploymentstacks_client_factory(cmd.cli_ctx) @@ -3034,7 +3034,7 @@ def create_deployment_stack_what_if_at_subscription( return whatif_result -def show_deployment_stack_what_if_at_subscription(cmd, name=None, id=None): # pylint: disable=redefined-builtin +def show_deployment_stack_what_if_at_subscription(cmd, name=None, id=None, no_pretty_print=None): # pylint: disable=redefined-builtin rcf = _resource_deploymentstacks_client_factory(cmd.cli_ctx) if name or id: if name: @@ -3077,7 +3077,7 @@ def create_deployment_stack_what_if_at_management_group( cmd, management_group_id, name, location, stack_id, deny_settings_mode, action_on_unmanage, retention_interval, deployment_subscription=None, template_file=None, template_spec=None, template_uri=None, query_string=None, parameters=None, description=None, deny_settings_excluded_principals=None, deny_settings_excluded_actions=None, - deny_settings_apply_to_child_scopes=False, resources_without_delete_support=None, validation_level=None, tags=None + deny_settings_apply_to_child_scopes=False, resources_without_delete_support=None, validation_level=None, tags=None, no_pretty_print=None ): rcf = _resource_deploymentstacks_client_factory(cmd.cli_ctx) @@ -3108,7 +3108,7 @@ def create_deployment_stack_what_if_at_management_group( return whatif_result -def show_deployment_stack_what_if_at_management_group(cmd, management_group_id, name=None, id=None): # pylint: disable=redefined-builtin +def show_deployment_stack_what_if_at_management_group(cmd, management_group_id, name=None, id=None, no_pretty_print=None): # pylint: disable=redefined-builtin if name or id: rcf = _resource_deploymentstacks_client_factory(cmd.cli_ctx) if name: From 520222734ad4486e10dbed3a11fdd269f62681b9 Mon Sep 17 00:00:00 2001 From: Kyle Albert <5498623+kalbert312@users.noreply.github.com> Date: Fri, 20 Feb 2026 16:29:49 -0500 Subject: [PATCH 03/32] Add boilerplate for print deployment stack what-if results. --- .../cli/command_modules/resource/custom.py | 82 +++++++++++-------- 1 file changed, 46 insertions(+), 36 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/resource/custom.py b/src/azure-cli/azure/cli/command_modules/resource/custom.py index 04e0d8c7430..a4a1dee6b82 100644 --- a/src/azure-cli/azure/cli/command_modules/resource/custom.py +++ b/src/azure-cli/azure/cli/command_modules/resource/custom.py @@ -2913,6 +2913,18 @@ def _prepare_validate_stack_at_scope( return deployment_stack_model +def _print_deployment_stack_what_if_result(what_if_result, no_pretty_print): + # Check for an error result. + if what_if_result and what_if_result.properties and what_if_result.properties.error: + err_message = _build_preflight_error_message(what_if_result.properties.error) + raise_subdivision_deployment_error(err_message) + + if no_pretty_print: # TODO(kylealbert): raw json vs non-colored formatted? + return what_if_result + + print("TODO") + + def create_deployment_stack_what_if_at_resource_group( cmd, name, resource_group, stack_id, deny_settings_mode, action_on_unmanage, retention_interval, template_file=None, template_spec=None, template_uri=None, query_string=None, parameters=None, description=None, deny_settings_excluded_principals=None, @@ -2938,26 +2950,26 @@ def create_deployment_stack_what_if_at_resource_group( err_message = _build_http_response_error_message(err) raise_subdivision_deployment_error(err_message, err.error.code if err.error else None) - whatif_result = LongRunningOperation(cmd.cli_ctx)(whatif_poller) - - if whatif_result and whatif_result.properties and whatif_result.properties.error: - err_message = _build_preflight_error_message(whatif_result.properties.error) - raise_subdivision_deployment_error(err_message) + what_if_result = LongRunningOperation(cmd.cli_ctx)(whatif_poller) - # TODO(kylealbert): Return formatted view - return whatif_result + return _print_deployment_stack_what_if_result(what_if_result, no_pretty_print) def show_deployment_stack_what_if_at_resource_group(cmd, name=None, resource_group=None, id=None, no_pretty_print=None): # pylint: disable=redefined-builtin rcf = _resource_deploymentstacks_client_factory(cmd.cli_ctx) - if name and resource_group: - return rcf.deployment_stacks_what_if_results_at_resource_group.get(resource_group, name) + if id: stack_arr = id.split('/') if len(stack_arr) < 5: raise InvalidArgumentValueError("Please enter a valid id") - return rcf.deployment_stacks_what_if_results_at_resource_group.get(stack_arr[4], stack_arr[-1]) - raise InvalidArgumentValueError("Please enter the (stack what-if result name and resource group) or stack what-if resource id") + + what_if_result = rcf.deployment_stacks_what_if_results_at_resource_group.get(stack_arr[4], stack_arr[-1]) + elif name and resource_group: + what_if_result = rcf.deployment_stacks_what_if_results_at_resource_group.get(resource_group, name) + else: + raise InvalidArgumentValueError("Please enter the (stack what-if result name and resource group) or stack what-if result resource id") + + return _print_deployment_stack_what_if_result(what_if_result, no_pretty_print) def list_deployment_stack_what_if_at_resource_group(cmd, resource_group): @@ -3024,23 +3036,22 @@ def create_deployment_stack_what_if_at_subscription( err_message = _build_http_response_error_message(err) raise_subdivision_deployment_error(err_message, err.error.code if err.error else None) - whatif_result = LongRunningOperation(cmd.cli_ctx)(whatif_poller) - - if whatif_result and whatif_result.properties and whatif_result.properties.error: - err_message = _build_preflight_error_message(whatif_result.properties.error) - raise_subdivision_deployment_error(err_message) + what_if_result = LongRunningOperation(cmd.cli_ctx)(whatif_poller) - # TODO(kylealbert): Return formatted view - return whatif_result + return _print_deployment_stack_what_if_result(what_if_result, no_pretty_print) def show_deployment_stack_what_if_at_subscription(cmd, name=None, id=None, no_pretty_print=None): # pylint: disable=redefined-builtin rcf = _resource_deploymentstacks_client_factory(cmd.cli_ctx) - if name or id: - if name: - return rcf.deployment_stacks_what_if_results_at_subscription.get(name) - return rcf.deployment_stacks_what_if_results_at_subscription.get(id.split('/')[-1]) - raise InvalidArgumentValueError("Please enter the stack what-if result name or stack what-if result resource id.") + + if name: + what_if_result = rcf.deployment_stacks_what_if_results_at_subscription.get(name) + elif id: + what_if_result = rcf.deployment_stacks_what_if_results_at_subscription.get(id.split('/')[-1]) + else: + raise InvalidArgumentValueError("Please enter the stack what-if result name or stack what-if result resource id.") + + return _print_deployment_stack_what_if_result(what_if_result, no_pretty_print) def list_deployment_stack_what_if_at_subscription(cmd): @@ -3098,23 +3109,22 @@ def create_deployment_stack_what_if_at_management_group( err_message = _build_http_response_error_message(err) raise_subdivision_deployment_error(err_message, err.error.code if err.error else None) - whatif_result = LongRunningOperation(cmd.cli_ctx)(whatif_poller) + what_if_result = LongRunningOperation(cmd.cli_ctx)(whatif_poller) - if whatif_result and whatif_result.properties and whatif_result.properties.error: - err_message = _build_preflight_error_message(whatif_result.properties.error) - raise_subdivision_deployment_error(err_message) + return _print_deployment_stack_what_if_result(what_if_result, no_pretty_print) - # TODO(kylealbert): Return formatted view - return whatif_result +def show_deployment_stack_what_if_at_management_group(cmd, management_group_id, name=None, id=None, no_pretty_print=None): + rcf = _resource_deploymentstacks_client_factory(cmd.cli_ctx) -def show_deployment_stack_what_if_at_management_group(cmd, management_group_id, name=None, id=None, no_pretty_print=None): # pylint: disable=redefined-builtin - if name or id: - rcf = _resource_deploymentstacks_client_factory(cmd.cli_ctx) - if name: - return rcf.deployment_stacks_what_if_results_at_management_group.get(management_group_id, name) - return rcf.deployment_stacks_what_if_results_at_management_group.get(management_group_id, id.split('/')[-1]) - raise InvalidArgumentValueError("Please enter the stack what-if result name or stack what-if result resource id.") + if name: + what_if_result = rcf.deployment_stacks_what_if_results_at_management_group.get(management_group_id, name) + elif id: + what_if_result = rcf.deployment_stacks_what_if_results_at_management_group.get(management_group_id, id.split('/')[-1]) + else: + raise InvalidArgumentValueError("Please enter the stack what-if result name or stack what-if result resource id.") + + return _print_deployment_stack_what_if_result(what_if_result, no_pretty_print) def list_deployment_stack_what_if_at_management_group(cmd, management_group_id): From c3bb51b56d6f61f31d833da01effb9897b7982cf Mon Sep 17 00:00:00 2001 From: Kyle Albert <5498623+kalbert312@users.noreply.github.com> Date: Mon, 23 Feb 2026 13:22:11 -0500 Subject: [PATCH 04/32] Implement legend output. --- .../cli/command_modules/resource/_color.py | 3 + .../resource/_stacks_formatters.py | 99 +++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py diff --git a/src/azure-cli/azure/cli/command_modules/resource/_color.py b/src/azure-cli/azure/cli/command_modules/resource/_color.py index 7999530de7f..5715bff23d8 100644 --- a/src/azure-cli/azure/cli/command_modules/resource/_color.py +++ b/src/azure-cli/azure/cli/command_modules/resource/_color.py @@ -49,6 +49,9 @@ def append_line(self, value="", color=None): def new_color_scope(self, color): return self.ColorScope(self, color) + def clear(self): + self._contents = [] + def _push_color(self, color): if not self._enable_color: return diff --git a/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py b/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py new file mode 100644 index 00000000000..f0b09a794cd --- /dev/null +++ b/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py @@ -0,0 +1,99 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +import typing as t + +import azure.mgmt.resource.deploymentstacks.models as StackModels +#from itertools import groupby + +#from azure.mgmt.resource.deployments.models import ChangeType, PropertyChangeType, Level + +from ._color import Color, ColoredStringBuilder +#from ._utils import split_resource_id + +ALL_WHAT_IF_CHANGE_TYPES = [ + StackModels.DeploymentStacksWhatIfChangeType.CREATE, + StackModels.DeploymentStacksWhatIfChangeType.UNSUPPORTED, + StackModels.DeploymentStacksWhatIfChangeType.MODIFY, + StackModels.DeploymentStacksWhatIfChangeType.DELETE, + StackModels.DeploymentStacksWhatIfChangeType.NO_CHANGE, + StackModels.DeploymentStacksWhatIfChangeType.DETACH +] + +class DeploymentStacksWhatIfResultFormatter: + WHAT_IF_CHANGE_TYPE_SYMBOLS = { + StackModels.DeploymentStacksWhatIfChangeType.CREATE: '+', + StackModels.DeploymentStacksWhatIfChangeType.DELETE: '-', + StackModels.DeploymentStacksWhatIfChangeType.DETACH: 'v', + StackModels.DeploymentStacksWhatIfChangeType.MODIFY: '~', + StackModels.DeploymentStacksWhatIfChangeType.NO_CHANGE: '=', + StackModels.DeploymentStacksWhatIfChangeType.UNSUPPORTED: '!' + } + + WHAT_IF_CHANGE_TYPE_COLORS = { + StackModels.DeploymentStacksWhatIfChangeType.CREATE: Color.GREEN, + StackModels.DeploymentStacksWhatIfChangeType.DELETE: Color.RED, + StackModels.DeploymentStacksWhatIfChangeType.DETACH: Color.BLUE, + StackModels.DeploymentStacksWhatIfChangeType.MODIFY: Color.PURPLE, + StackModels.DeploymentStacksWhatIfChangeType.NO_CHANGE: Color.GRAY, + StackModels.DeploymentStacksWhatIfChangeType.UNSUPPORTED: Color.GRAY + } + + def __init__(self, enable_color=True): + self.builder: ColoredStringBuilder = ColoredStringBuilder(enable_color) + self.what_if_result: t.Optional[StackModels.DeploymentStacksWhatIfResult] = None + self.what_if_props: t.Optional[StackModels.DeploymentStacksWhatIfResultProperties] = None + + def format(self, what_if_result: StackModels.DeploymentStacksWhatIfResult): + self.builder.clear() + + self.what_if_result = what_if_result + self.what_if_props = what_if_result.properties + + self._format_change_type_legend() + self._format_stack_changes() + self._format_resource_changes() + self._format_resource_deletions() + self._format_diagnostics() + + result = self.builder.build() + self.what_if_result = self.what_if_props = None + + return result + + def _format_change_type_legend(self): + change_type_max_length = 20 + indent_size = 2 + + self.builder.append_line("Resource and property changes are indicated with these symbols:") + + for i, change_type in enumerate(ALL_WHAT_IF_CHANGE_TYPES): + if i % 2 == 0: + self.builder.append(" " * indent_size) + + change_type_label = change_type[0].upper() + change_type[1:] + + (self.builder.append( + DeploymentStacksWhatIfResultFormatter.WHAT_IF_CHANGE_TYPE_SYMBOLS[change_type], + DeploymentStacksWhatIfResultFormatter.WHAT_IF_CHANGE_TYPE_COLORS[change_type]) + .append(" ").append(change_type_label)) + + if i % 2 == 0: + remaining_indent = max(1, change_type_max_length - len(change_type_label)) + self.builder.append(" " * remaining_indent) + elif i < len(ALL_WHAT_IF_CHANGE_TYPES) - 1: + self.builder.append_line() + + def _format_stack_changes(self): + pass + + def _format_resource_changes(self): + pass + + def _format_resource_deletions(self): + pass + + def _format_diagnostics(self): + pass From fdcea30e7aededcc40c61f0cd27125992e4c9e8c Mon Sep 17 00:00:00 2001 From: Kyle Albert <5498623+kalbert312@users.noreply.github.com> Date: Mon, 23 Feb 2026 16:46:51 -0500 Subject: [PATCH 05/32] Progress on what-if formatting. --- .../cli/command_modules/resource/_color.py | 17 +++ .../resource/_stacks_formatters.py | 109 +++++++++++++++--- 2 files changed, 110 insertions(+), 16 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/resource/_color.py b/src/azure-cli/azure/cli/command_modules/resource/_color.py index 5715bff23d8..451c54ae1d5 100644 --- a/src/azure-cli/azure/cli/command_modules/resource/_color.py +++ b/src/azure-cli/azure/cli/command_modules/resource/_color.py @@ -49,6 +49,23 @@ def append_line(self, value="", color=None): def new_color_scope(self, color): return self.ColorScope(self, color) + def insert(self, index, value="", color=None): + if self._enable_color: + self._contents.insert(index, str(Color.RESET)) + + self._contents.insert(index, f"{str(value)}") + + if self._enable_color: + self._contents.insert(index, str(color)) + + return self + + def insert_line(self, index, value="", color=None): + return self.insert(index, f"{str(value)}\n", color) + + def get_current_index(self): + return len(self._contents) + def clear(self): self._contents = [] diff --git a/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py b/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py index f0b09a794cd..ed8038dd58d 100644 --- a/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py +++ b/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py @@ -4,6 +4,7 @@ # -------------------------------------------------------------------------------------------- import typing as t +from requests.structures import CaseInsensitiveDict import azure.mgmt.resource.deploymentstacks.models as StackModels #from itertools import groupby @@ -23,39 +24,53 @@ ] class DeploymentStacksWhatIfResultFormatter: - WHAT_IF_CHANGE_TYPE_SYMBOLS = { + INDENT_SIZE = 2 + INDENT = " " * INDENT_SIZE + + # NOTE(kylealbert): Some of these overlap with property change types + CHANGE_TYPE_SYMBOLS = CaseInsensitiveDict({ + StackModels.DeploymentStacksWhatIfPropertyChangeType.ARRAY: '~', StackModels.DeploymentStacksWhatIfChangeType.CREATE: '+', StackModels.DeploymentStacksWhatIfChangeType.DELETE: '-', StackModels.DeploymentStacksWhatIfChangeType.DETACH: 'v', StackModels.DeploymentStacksWhatIfChangeType.MODIFY: '~', StackModels.DeploymentStacksWhatIfChangeType.NO_CHANGE: '=', - StackModels.DeploymentStacksWhatIfChangeType.UNSUPPORTED: '!' - } + StackModels.DeploymentStacksWhatIfPropertyChangeType.NO_EFFECT: '=', + StackModels.DeploymentStacksWhatIfChangeType.UNSUPPORTED: '!', + }) - WHAT_IF_CHANGE_TYPE_COLORS = { + CHANGE_TYPE_COLORS = CaseInsensitiveDict({ + StackModels.DeploymentStacksWhatIfPropertyChangeType.ARRAY: Color.PURPLE, StackModels.DeploymentStacksWhatIfChangeType.CREATE: Color.GREEN, StackModels.DeploymentStacksWhatIfChangeType.DELETE: Color.RED, StackModels.DeploymentStacksWhatIfChangeType.DETACH: Color.BLUE, StackModels.DeploymentStacksWhatIfChangeType.MODIFY: Color.PURPLE, StackModels.DeploymentStacksWhatIfChangeType.NO_CHANGE: Color.GRAY, - StackModels.DeploymentStacksWhatIfChangeType.UNSUPPORTED: Color.GRAY - } + StackModels.DeploymentStacksWhatIfPropertyChangeType.NO_EFFECT: Color.GRAY, + StackModels.DeploymentStacksWhatIfChangeType.UNSUPPORTED: Color.GRAY, + }) def __init__(self, enable_color=True): self.builder: ColoredStringBuilder = ColoredStringBuilder(enable_color) self.what_if_result: t.Optional[StackModels.DeploymentStacksWhatIfResult] = None self.what_if_props: t.Optional[StackModels.DeploymentStacksWhatIfResultProperties] = None + self.what_if_changes: t.Optional[StackModels.DeploymentStacksWhatIfChange] = None def format(self, what_if_result: StackModels.DeploymentStacksWhatIfResult): self.builder.clear() self.what_if_result = what_if_result self.what_if_props = what_if_result.properties - - self._format_change_type_legend() - self._format_stack_changes() - self._format_resource_changes() - self._format_resource_deletions() + self.what_if_changes = self.what_if_props and self.what_if_props.changes or None + + if self._format_change_type_legend(): + self._format_new_section() + if self._format_stack_changes(): + self._format_new_section() + if self._format_resource_changes(): + self._format_new_section() + if self._format_resource_deletions(): + self._format_new_section() self._format_diagnostics() result = self.builder.build() @@ -63,21 +78,23 @@ def format(self, what_if_result: StackModels.DeploymentStacksWhatIfResult): return result + def _format_new_section(self): + self.builder.append("\n\n") + def _format_change_type_legend(self): change_type_max_length = 20 - indent_size = 2 self.builder.append_line("Resource and property changes are indicated with these symbols:") for i, change_type in enumerate(ALL_WHAT_IF_CHANGE_TYPES): if i % 2 == 0: - self.builder.append(" " * indent_size) + self.builder.append(" " * DeploymentStacksWhatIfResultFormatter.INDENT_SIZE) change_type_label = change_type[0].upper() + change_type[1:] (self.builder.append( - DeploymentStacksWhatIfResultFormatter.WHAT_IF_CHANGE_TYPE_SYMBOLS[change_type], - DeploymentStacksWhatIfResultFormatter.WHAT_IF_CHANGE_TYPE_COLORS[change_type]) + DeploymentStacksWhatIfResultFormatter.CHANGE_TYPE_SYMBOLS[change_type], + DeploymentStacksWhatIfResultFormatter.CHANGE_TYPE_COLORS[change_type]) .append(" ").append(change_type_label)) if i % 2 == 0: @@ -86,8 +103,22 @@ def _format_change_type_legend(self): elif i < len(ALL_WHAT_IF_CHANGE_TYPES) - 1: self.builder.append_line() + return True + def _format_stack_changes(self): - pass + printed = False + + title_index = self.builder.get_current_index() + + if self._format_primitive_change(self.what_if_changes.deployment_scope_change, "DeploymentScope"): + printed = True + if self._format_object_change(self.what_if_changes.deny_settings_change, "DenySettings"): + printed = True + + if printed: + self.builder.insert_line(title_index, f"Changes to Stack {self.what_if_props.deployment_stack_resource_id}:", Color.DARK_YELLOW) + + return printed def _format_resource_changes(self): pass @@ -97,3 +128,49 @@ def _format_resource_deletions(self): def _format_diagnostics(self): pass + + def _format_object_change( + self, object_change: t.Optional[StackModels.DeploymentStacksChangeDeltaRecord], parent_path: t.Optional[str] = None + ): + if not object_change: + return False + + printed = False + delta = object_change.delta + + for delta in delta or []: + if delta.change_type == StackModels.DeploymentStacksWhatIfPropertyChangeType.MODIFY: + if delta + if self._format_property_change(delta, parent_path): + printed = True + + return printed + + def _format_property_change( + self, property_change: t.Optional[StackModels.DeploymentStacksWhatIfPropertyChange], parent_path: t.Optional[str] = None + ): + if not property_change: + return False + + symbol = DeploymentStacksWhatIfResultFormatter.CHANGE_TYPE_SYMBOLS.get(property_change.change_type, None) + property_color = DeploymentStacksWhatIfResultFormatter.CHANGE_TYPE_COLORS.get(property_change.change_type, None) + property_path = '.'.join([parent_path, property_change.path]) if parent_path else property_change.path + + self.builder.append_line(f"{symbol} {property_path}:", property_color) + self.builder.append_line() + + return True + + def _format_primitive_change(self, primitive_change: t.Optional[StackModels.DeploymentStacksChangeBase], property_path: str): + if not primitive_change: + return False + + self.builder.append( + f"~ {property_path}: ", + DeploymentStacksWhatIfResultFormatter.CHANGE_TYPE_COLORS[StackModels.DeploymentStacksWhatIfPropertyChangeType.MODIFY]) + self.builder.append_line(f"{primitive_change.before} => {primitive_change.after}") # TODO(kylealbert): correct arrow symbol? + + return True + + def _get_type_from_delta(self, delta: StackModels.DeploymentStacksChangeBase): + From 04b91efb97d782c87c07ceb1046c9aac999044c3 Mon Sep 17 00:00:00 2001 From: Kyle Albert <5498623+kalbert312@users.noreply.github.com> Date: Mon, 23 Feb 2026 17:01:18 -0500 Subject: [PATCH 06/32] Add --no-color parameter. --- .../cli/command_modules/resource/_params.py | 4 +++ .../resource/_stacks_formatters.py | 5 +-- .../cli/command_modules/resource/custom.py | 35 +++++++++++-------- 3 files changed, 27 insertions(+), 17 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/resource/_params.py b/src/azure-cli/azure/cli/command_modules/resource/_params.py index 6641cab71b1..ef6526eb398 100644 --- a/src/azure-cli/azure/cli/command_modules/resource/_params.py +++ b/src/azure-cli/azure/cli/command_modules/resource/_params.py @@ -72,6 +72,8 @@ def load_arguments(self, _): min_api='2019-07-01') deployment_what_if_no_pretty_print_type = CLIArgumentType(options_list=['--no-pretty-print'], action='store_true', help='Disable pretty-print for What-If results. When set, the output format type will be used.') + deployment_what_if_no_color_type = CLIArgumentType(options_list=['--no-color'], action='store_true', + help='Disable color in pretty-printed what-if results.') deployment_what_if_confirmation_type = CLIArgumentType(options_list=['--confirm-with-what-if', '-c'], action='store_true', help='Instruct the command to run deployment What-If before executing the deployment. It then prompts you to acknowledge resource changes before it continues.', min_api='2019-07-01') @@ -662,6 +664,7 @@ def load_arguments(self, _): c.argument('stack_id', arg_type=stacks_whatif_stack_id_type) c.argument('retention_interval', arg_type=stacks_whatif_retention_interval_type) c.argument('no_pretty_print', arg_type=deployment_what_if_no_pretty_print_type) + c.argument('no_color', arg_type=deployment_what_if_no_color_type) if action == 'create' and resource_type == 'stack': c.argument('yes', help='Do not prompt for confirmation') elif action == 'delete': @@ -682,6 +685,7 @@ def load_arguments(self, _): c.argument('resource_group', arg_type=resource_group_name_type, help='The resource group where the deployment stack exists') if resource_type == 'stack-whatif': c.argument('no_pretty_print', arg_type=deployment_what_if_no_pretty_print_type) + c.argument('no_color', arg_type=deployment_what_if_no_color_type) elif action == 'list': if scope == 'sub': continue # only uses global arguments diff --git a/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py b/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py index ed8038dd58d..7d34cdd1eb1 100644 --- a/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py +++ b/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py @@ -140,7 +140,8 @@ def _format_object_change( for delta in delta or []: if delta.change_type == StackModels.DeploymentStacksWhatIfPropertyChangeType.MODIFY: - if delta + pass # TODO(kylealbert): call right methods + if self._format_property_change(delta, parent_path): printed = True @@ -173,4 +174,4 @@ def _format_primitive_change(self, primitive_change: t.Optional[StackModels.Depl return True def _get_type_from_delta(self, delta: StackModels.DeploymentStacksChangeBase): - + return None diff --git a/src/azure-cli/azure/cli/command_modules/resource/custom.py b/src/azure-cli/azure/cli/command_modules/resource/custom.py index a4a1dee6b82..f6fd5ac50cd 100644 --- a/src/azure-cli/azure/cli/command_modules/resource/custom.py +++ b/src/azure-cli/azure/cli/command_modules/resource/custom.py @@ -44,6 +44,7 @@ from knack.util import CLIError from ._formatters import format_what_if_operation_result +from ._stacks_formatters import DeploymentStacksWhatIfResultFormatter from ._bicep import ( run_bicep_command, is_bicep_file, @@ -2913,23 +2914,26 @@ def _prepare_validate_stack_at_scope( return deployment_stack_model -def _print_deployment_stack_what_if_result(what_if_result, no_pretty_print): +def _print_deployment_stack_what_if_result(what_if_result, no_pretty_print, no_color): # Check for an error result. if what_if_result and what_if_result.properties and what_if_result.properties.error: err_message = _build_preflight_error_message(what_if_result.properties.error) raise_subdivision_deployment_error(err_message) - if no_pretty_print: # TODO(kylealbert): raw json vs non-colored formatted? + if no_pretty_print: return what_if_result - print("TODO") + formatter = DeploymentStacksWhatIfResultFormatter(enable_color=not no_color) + print(formatter.format(what_if_result)) + + return None def create_deployment_stack_what_if_at_resource_group( cmd, name, resource_group, stack_id, deny_settings_mode, action_on_unmanage, retention_interval, template_file=None, template_spec=None, template_uri=None, query_string=None, parameters=None, description=None, deny_settings_excluded_principals=None, deny_settings_excluded_actions=None, deny_settings_apply_to_child_scopes=False, resources_without_delete_support=None, - validation_level=None, tags=None, no_pretty_print=None + validation_level=None, tags=None, no_pretty_print=None, no_color=None ): rcf = _resource_deploymentstacks_client_factory(cmd.cli_ctx) @@ -2952,10 +2956,10 @@ def create_deployment_stack_what_if_at_resource_group( what_if_result = LongRunningOperation(cmd.cli_ctx)(whatif_poller) - return _print_deployment_stack_what_if_result(what_if_result, no_pretty_print) + return _print_deployment_stack_what_if_result(what_if_result, no_pretty_print, no_color) -def show_deployment_stack_what_if_at_resource_group(cmd, name=None, resource_group=None, id=None, no_pretty_print=None): # pylint: disable=redefined-builtin +def show_deployment_stack_what_if_at_resource_group(cmd, name=None, resource_group=None, id=None, no_pretty_print=None, no_color=None): # pylint: disable=redefined-builtin rcf = _resource_deploymentstacks_client_factory(cmd.cli_ctx) if id: @@ -2969,7 +2973,7 @@ def show_deployment_stack_what_if_at_resource_group(cmd, name=None, resource_gro else: raise InvalidArgumentValueError("Please enter the (stack what-if result name and resource group) or stack what-if result resource id") - return _print_deployment_stack_what_if_result(what_if_result, no_pretty_print) + return _print_deployment_stack_what_if_result(what_if_result, no_pretty_print, no_color) def list_deployment_stack_what_if_at_resource_group(cmd, resource_group): @@ -3016,7 +3020,7 @@ def create_deployment_stack_what_if_at_subscription( cmd, name, location, stack_id, deny_settings_mode, action_on_unmanage, retention_interval, deployment_resource_group=None, template_file=None, template_spec=None, template_uri=None, query_string=None, parameters=None, description=None, deny_settings_excluded_principals=None, deny_settings_excluded_actions=None, deny_settings_apply_to_child_scopes=False, - resources_without_delete_support=None, validation_level=None, tags=None, no_pretty_print=None + resources_without_delete_support=None, validation_level=None, tags=None, no_pretty_print=None, no_color=None ): rcf = _resource_deploymentstacks_client_factory(cmd.cli_ctx) @@ -3038,10 +3042,10 @@ def create_deployment_stack_what_if_at_subscription( what_if_result = LongRunningOperation(cmd.cli_ctx)(whatif_poller) - return _print_deployment_stack_what_if_result(what_if_result, no_pretty_print) + return _print_deployment_stack_what_if_result(what_if_result, no_pretty_print, no_color) -def show_deployment_stack_what_if_at_subscription(cmd, name=None, id=None, no_pretty_print=None): # pylint: disable=redefined-builtin +def show_deployment_stack_what_if_at_subscription(cmd, name=None, id=None, no_pretty_print=None, no_color=None): # pylint: disable=redefined-builtin rcf = _resource_deploymentstacks_client_factory(cmd.cli_ctx) if name: @@ -3051,7 +3055,7 @@ def show_deployment_stack_what_if_at_subscription(cmd, name=None, id=None, no_pr else: raise InvalidArgumentValueError("Please enter the stack what-if result name or stack what-if result resource id.") - return _print_deployment_stack_what_if_result(what_if_result, no_pretty_print) + return _print_deployment_stack_what_if_result(what_if_result, no_pretty_print, no_color) def list_deployment_stack_what_if_at_subscription(cmd): @@ -3088,7 +3092,8 @@ def create_deployment_stack_what_if_at_management_group( cmd, management_group_id, name, location, stack_id, deny_settings_mode, action_on_unmanage, retention_interval, deployment_subscription=None, template_file=None, template_spec=None, template_uri=None, query_string=None, parameters=None, description=None, deny_settings_excluded_principals=None, deny_settings_excluded_actions=None, - deny_settings_apply_to_child_scopes=False, resources_without_delete_support=None, validation_level=None, tags=None, no_pretty_print=None + deny_settings_apply_to_child_scopes=False, resources_without_delete_support=None, validation_level=None, tags=None, + no_pretty_print=None, no_color=None ): rcf = _resource_deploymentstacks_client_factory(cmd.cli_ctx) @@ -3111,10 +3116,10 @@ def create_deployment_stack_what_if_at_management_group( what_if_result = LongRunningOperation(cmd.cli_ctx)(whatif_poller) - return _print_deployment_stack_what_if_result(what_if_result, no_pretty_print) + return _print_deployment_stack_what_if_result(what_if_result, no_pretty_print, no_color) -def show_deployment_stack_what_if_at_management_group(cmd, management_group_id, name=None, id=None, no_pretty_print=None): +def show_deployment_stack_what_if_at_management_group(cmd, management_group_id, name=None, id=None, no_pretty_print=None, no_color=None): rcf = _resource_deploymentstacks_client_factory(cmd.cli_ctx) if name: @@ -3124,7 +3129,7 @@ def show_deployment_stack_what_if_at_management_group(cmd, management_group_id, else: raise InvalidArgumentValueError("Please enter the stack what-if result name or stack what-if result resource id.") - return _print_deployment_stack_what_if_result(what_if_result, no_pretty_print) + return _print_deployment_stack_what_if_result(what_if_result, no_pretty_print, no_color=no_color) def list_deployment_stack_what_if_at_management_group(cmd, management_group_id): From 7a087082c142652164e4db50d33375af4421c81d Mon Sep 17 00:00:00 2001 From: Kyle Albert <5498623+kalbert312@users.noreply.github.com> Date: Tue, 24 Feb 2026 13:11:53 -0500 Subject: [PATCH 07/32] What-if output progress. --- .../resource/_stacks_formatters.py | 188 +++++++++++++----- .../cli/command_modules/resource/_utils.py | 3 + 2 files changed, 140 insertions(+), 51 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py b/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py index 7d34cdd1eb1..2fcfdc61a62 100644 --- a/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py +++ b/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py @@ -7,12 +7,10 @@ from requests.structures import CaseInsensitiveDict import azure.mgmt.resource.deploymentstacks.models as StackModels -#from itertools import groupby - -#from azure.mgmt.resource.deployments.models import ChangeType, PropertyChangeType, Level +# from itertools import groupby from ._color import Color, ColoredStringBuilder -#from ._utils import split_resource_id +from ._utils import str_lower_eq ALL_WHAT_IF_CHANGE_TYPES = [ StackModels.DeploymentStacksWhatIfChangeType.CREATE, @@ -23,32 +21,35 @@ StackModels.DeploymentStacksWhatIfChangeType.DETACH ] + class DeploymentStacksWhatIfResultFormatter: INDENT_SIZE = 2 - INDENT = " " * INDENT_SIZE # NOTE(kylealbert): Some of these overlap with property change types - CHANGE_TYPE_SYMBOLS = CaseInsensitiveDict({ - StackModels.DeploymentStacksWhatIfPropertyChangeType.ARRAY: '~', - StackModels.DeploymentStacksWhatIfChangeType.CREATE: '+', - StackModels.DeploymentStacksWhatIfChangeType.DELETE: '-', - StackModels.DeploymentStacksWhatIfChangeType.DETACH: 'v', - StackModels.DeploymentStacksWhatIfChangeType.MODIFY: '~', - StackModels.DeploymentStacksWhatIfChangeType.NO_CHANGE: '=', - StackModels.DeploymentStacksWhatIfPropertyChangeType.NO_EFFECT: '=', - StackModels.DeploymentStacksWhatIfChangeType.UNSUPPORTED: '!', - }) - - CHANGE_TYPE_COLORS = CaseInsensitiveDict({ - StackModels.DeploymentStacksWhatIfPropertyChangeType.ARRAY: Color.PURPLE, - StackModels.DeploymentStacksWhatIfChangeType.CREATE: Color.GREEN, - StackModels.DeploymentStacksWhatIfChangeType.DELETE: Color.RED, - StackModels.DeploymentStacksWhatIfChangeType.DETACH: Color.BLUE, - StackModels.DeploymentStacksWhatIfChangeType.MODIFY: Color.PURPLE, - StackModels.DeploymentStacksWhatIfChangeType.NO_CHANGE: Color.GRAY, - StackModels.DeploymentStacksWhatIfPropertyChangeType.NO_EFFECT: Color.GRAY, - StackModels.DeploymentStacksWhatIfChangeType.UNSUPPORTED: Color.GRAY, - }) + CHANGE_TYPE_SYMBOLS = CaseInsensitiveDict( + { + StackModels.DeploymentStacksWhatIfPropertyChangeType.ARRAY: '~', + StackModels.DeploymentStacksWhatIfChangeType.CREATE: '+', + StackModels.DeploymentStacksWhatIfChangeType.DELETE: '-', + StackModels.DeploymentStacksWhatIfChangeType.DETACH: 'v', + StackModels.DeploymentStacksWhatIfChangeType.MODIFY: '~', + StackModels.DeploymentStacksWhatIfChangeType.NO_CHANGE: '=', + StackModels.DeploymentStacksWhatIfPropertyChangeType.NO_EFFECT: '=', + StackModels.DeploymentStacksWhatIfChangeType.UNSUPPORTED: '!', + }) + + CHANGE_TYPE_COLORS = CaseInsensitiveDict( + { + StackModels.DeploymentStacksWhatIfPropertyChangeType.ARRAY: Color.PURPLE, + StackModels.DeploymentStacksWhatIfChangeType.CREATE: Color.GREEN, + StackModels.DeploymentStacksWhatIfChangeType.DELETE: Color.RED, + StackModels.DeploymentStacksWhatIfChangeType.DETACH: Color.BLUE, + StackModels.DeploymentStacksWhatIfChangeType.MODIFY: Color.PURPLE, + StackModels.DeploymentStacksWhatIfChangeType.NO_CHANGE: Color.GRAY, + StackModels.DeploymentStacksWhatIfPropertyChangeType.NO_EFFECT: Color.GRAY, + StackModels.DeploymentStacksWhatIfChangeType.UNSUPPORTED: Color.GRAY, + }) + def __init__(self, enable_color=True): self.builder: ColoredStringBuilder = ColoredStringBuilder(enable_color) @@ -56,12 +57,13 @@ def __init__(self, enable_color=True): self.what_if_props: t.Optional[StackModels.DeploymentStacksWhatIfResultProperties] = None self.what_if_changes: t.Optional[StackModels.DeploymentStacksWhatIfChange] = None + def format(self, what_if_result: StackModels.DeploymentStacksWhatIfResult): self.builder.clear() self.what_if_result = what_if_result self.what_if_props = what_if_result.properties - self.what_if_changes = self.what_if_props and self.what_if_props.changes or None + self.what_if_changes = self.what_if_props.changes if self.what_if_props else None if self._format_change_type_legend(): self._format_new_section() @@ -78,9 +80,11 @@ def format(self, what_if_result: StackModels.DeploymentStacksWhatIfResult): return result + def _format_new_section(self): self.builder.append("\n\n") + def _format_change_type_legend(self): change_type_max_length = 20 @@ -105,30 +109,59 @@ def _format_change_type_legend(self): return True + def _format_stack_changes(self): printed = False title_index = self.builder.get_current_index() - if self._format_primitive_change(self.what_if_changes.deployment_scope_change, "DeploymentScope"): - printed = True - if self._format_object_change(self.what_if_changes.deny_settings_change, "DenySettings"): - printed = True + all_stack_changes = { + "DeploymentScope": self.what_if_changes.deployment_scope_change, + "DenySettings": self.what_if_changes.deny_settings_change + } + + for path, change in all_stack_changes.items(): + if self._format_change(change, path): + printed = True if printed: self.builder.insert_line(title_index, f"Changes to Stack {self.what_if_props.deployment_stack_resource_id}:", Color.DARK_YELLOW) return printed + def _format_resource_changes(self): pass + def _format_resource_deletions(self): pass + def _format_diagnostics(self): pass + + def _format_change( + self, change: t.Union[ + StackModels.DeploymentStacksChangeBase, StackModels.DeploymentStacksChangeDeltaRecord, StackModels.DeploymentStacksWhatIfPropertyChange], + parent_path: t.Optional[str] = None + ): + value_type = self._get_value_type_from_change(change) + + if value_type is str or value_type is bool or value_type is int: + if self._format_primitive_change(change, parent_path): + return True + elif value_type is list: + if self._format_array_changes(change, parent_path): + return True + elif value_type is dict: + if self._format_object_change(change, parent_path): + return True + + return False + + def _format_object_change( self, object_change: t.Optional[StackModels.DeploymentStacksChangeDeltaRecord], parent_path: t.Optional[str] = None ): @@ -139,39 +172,92 @@ def _format_object_change( delta = object_change.delta for delta in delta or []: - if delta.change_type == StackModels.DeploymentStacksWhatIfPropertyChangeType.MODIFY: - pass # TODO(kylealbert): call right methods - - if self._format_property_change(delta, parent_path): - printed = True + if self._format_change(delta, parent_path): + printed = True return printed - def _format_property_change( - self, property_change: t.Optional[StackModels.DeploymentStacksWhatIfPropertyChange], parent_path: t.Optional[str] = None - ): - if not property_change: + + def _format_array_changes(self, array_change: StackModels.DeploymentStacksWhatIfPropertyChange, parent_path: t.Optional[str] = None): + if not str_lower_eq(array_change.change_type, StackModels.DeploymentStacksWhatIfPropertyChangeType.ARRAY): return False - symbol = DeploymentStacksWhatIfResultFormatter.CHANGE_TYPE_SYMBOLS.get(property_change.change_type, None) - property_color = DeploymentStacksWhatIfResultFormatter.CHANGE_TYPE_COLORS.get(property_change.change_type, None) - property_path = '.'.join([parent_path, property_change.path]) if parent_path else property_change.path + property_path = self._get_change_path(array_change, parent_path) + color = DeploymentStacksWhatIfResultFormatter.CHANGE_TYPE_COLORS[StackModels.DeploymentStacksWhatIfPropertyChangeType.MODIFY] + + self.builder.append_line(f"~ {property_path}: ", color) - self.builder.append_line(f"{symbol} {property_path}:", property_color) - self.builder.append_line() + for item_change in array_change.children or []: + self._format_array_child_change(item_change) return True - def _format_primitive_change(self, primitive_change: t.Optional[StackModels.DeploymentStacksChangeBase], property_path: str): + + def _format_array_child_change(self, array_change: StackModels.DeploymentStacksWhatIfPropertyChange): + symbol = DeploymentStacksWhatIfResultFormatter.CHANGE_TYPE_SYMBOLS.get(array_change.change_type, None) + color = DeploymentStacksWhatIfResultFormatter.CHANGE_TYPE_COLORS.get(array_change.change_type, None) + indent = self._get_indent(1) + + if str_lower_eq(array_change.change_type, StackModels.DeploymentStacksWhatIfPropertyChangeType.CREATE): + self.builder.append_line(f"{indent}{symbol} {self._format_primitive_value(array_change.after)}", color) + elif str_lower_eq(array_change.change_type, StackModels.DeploymentStacksWhatIfPropertyChangeType.DELETE): + self.builder.append_line(f"{indent}{symbol} {self._format_primitive_value(array_change.before)}", color) + elif str_lower_eq(array_change.change_type, StackModels.DeploymentStacksWhatIfPropertyChangeType.NO_EFFECT): + self.builder.append_line(f"{indent}{symbol} {self._format_primitive_value(array_change.after)}", color) + + + def _format_primitive_change( + self, + primitive_change: t.Optional[t.Union[StackModels.DeploymentStacksChangeBase, StackModels.DeploymentStacksWhatIfPropertyChange]], + parent_path: t.Optional[str] = None + ): if not primitive_change: return False - self.builder.append( - f"~ {property_path}: ", - DeploymentStacksWhatIfResultFormatter.CHANGE_TYPE_COLORS[StackModels.DeploymentStacksWhatIfPropertyChangeType.MODIFY]) - self.builder.append_line(f"{primitive_change.before} => {primitive_change.after}") # TODO(kylealbert): correct arrow symbol? + property_path = self._get_change_path(primitive_change, parent_path) + color = DeploymentStacksWhatIfResultFormatter.CHANGE_TYPE_COLORS[StackModels.DeploymentStacksWhatIfPropertyChangeType.MODIFY] + + self.builder.append(f"~ {property_path}: ", color) + self.builder.append_line( + f"{self._format_primitive_value(primitive_change.before)} => {self._format_primitive_value(primitive_change.after)}") # TODO(kylealbert): correct arrow symbol? return True - def _get_type_from_delta(self, delta: StackModels.DeploymentStacksChangeBase): + + @staticmethod + def _format_primitive_value(value: t.Union[str, bool, int]): + return f'"{value}"' if isinstance(value, str) else str(value) + + + @staticmethod + def _get_change_path(change, parent_path: t.Optional[str] = None): + if hasattr(change, "path"): + return '.'.join([parent_path, change.path]) if parent_path else change.path + return parent_path + + + @staticmethod + def _get_indent(indent_level: int, indent_size: int = INDENT_SIZE): + return " " * indent_size * indent_level + + + @staticmethod + def _get_value_type_from_change( + change: t.Union[ + StackModels.DeploymentStacksChangeBase, StackModels.DeploymentStacksChangeDeltaRecord, StackModels.DeploymentStacksWhatIfPropertyChange] + ): + if hasattr(change, "change_type"): + if str_lower_eq(StackModels.DeploymentStacksWhatIfPropertyChangeType.ARRAY, change.change_type): + return list + elif hasattr(change, "delta"): + return dict + + before_type = type(change.before) + after_type = type(change.after) + + if before_type == after_type: + return before_type + if before_type is type(None) or after_type is type(None): + return before_type or after_type + return None diff --git a/src/azure-cli/azure/cli/command_modules/resource/_utils.py b/src/azure-cli/azure/cli/command_modules/resource/_utils.py index 9feb1d93daa..5a0a769133b 100644 --- a/src/azure-cli/azure/cli/command_modules/resource/_utils.py +++ b/src/azure-cli/azure/cli/command_modules/resource/_utils.py @@ -3,6 +3,7 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- +import typing as t import re import json @@ -13,6 +14,8 @@ _resource_group_pattern = r"^\/resourceGroups\/(?P[-\w\._\(\)]+)" _relative_resource_id_pattern = r"^\/providers/(?P.+$)" +def str_lower_eq(str1: t.Optional[str], str2: t.Optional[str]): + return str1.lower() == str2.lower() if str1 and str2 else False def split_resource_id(resource_id): """Splits a fully qualified resource ID into two parts. From 098994f27a49f59567fdba1ded7009b2ee733f78 Mon Sep 17 00:00:00 2001 From: Kyle Albert <5498623+kalbert312@users.noreply.github.com> Date: Tue, 24 Feb 2026 16:13:12 -0500 Subject: [PATCH 08/32] What-if resource change progress. --- .../cli/command_modules/resource/_color.py | 16 +- .../resource/_stacks_formatters.py | 175 +++++++++++++----- 2 files changed, 140 insertions(+), 51 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/resource/_color.py b/src/azure-cli/azure/cli/command_modules/resource/_color.py index 451c54ae1d5..343f3647f6b 100644 --- a/src/azure-cli/azure/cli/command_modules/resource/_color.py +++ b/src/azure-cli/azure/cli/command_modules/resource/_color.py @@ -26,11 +26,15 @@ def __init__(self, enable_color=True): self._enable_color = enable_color self._contents = [] self._colors = deque() + self._indents = [] def build(self): return "".join(self._contents) - def append(self, value, color=None): + def append(self, value, color=None, no_indent=False): + if not no_indent and len(self._indents) > 0: + self._contents.append(''.join(self._indents)) + if color: self._push_color(color) @@ -41,8 +45,8 @@ def append(self, value, color=None): return self - def append_line(self, value="", color=None): - self.append(f"{str(value)}\n", color) + def append_line(self, value="", color=None, no_indent=False): + self.append(f"{str(value)}\n", color, no_indent) return self @@ -66,6 +70,12 @@ def insert_line(self, index, value="", color=None): def get_current_index(self): return len(self._contents) + def push_indent(self, indent): + self._indents.append(indent) + + def pop_indent(self): + self._indents.pop() + def clear(self): self._contents = [] diff --git a/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py b/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py index 2fcfdc61a62..7afa2277264 100644 --- a/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py +++ b/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py @@ -22,7 +22,7 @@ ] -class DeploymentStacksWhatIfResultFormatter: +class DeploymentStacksWhatIfResultFormatter: # pylint: disable=too-few-public-methods INDENT_SIZE = 2 # NOTE(kylealbert): Some of these overlap with property change types @@ -44,10 +44,7 @@ class DeploymentStacksWhatIfResultFormatter: StackModels.DeploymentStacksWhatIfChangeType.CREATE: Color.GREEN, StackModels.DeploymentStacksWhatIfChangeType.DELETE: Color.RED, StackModels.DeploymentStacksWhatIfChangeType.DETACH: Color.BLUE, - StackModels.DeploymentStacksWhatIfChangeType.MODIFY: Color.PURPLE, - StackModels.DeploymentStacksWhatIfChangeType.NO_CHANGE: Color.GRAY, - StackModels.DeploymentStacksWhatIfPropertyChangeType.NO_EFFECT: Color.GRAY, - StackModels.DeploymentStacksWhatIfChangeType.UNSUPPORTED: Color.GRAY, + StackModels.DeploymentStacksWhatIfChangeType.MODIFY: Color.PURPLE }) @@ -71,7 +68,7 @@ def format(self, what_if_result: StackModels.DeploymentStacksWhatIfResult): self._format_new_section() if self._format_resource_changes(): self._format_new_section() - if self._format_resource_deletions(): + if self._format_resource_deletions_summary(): self._format_new_section() self._format_diagnostics() @@ -89,23 +86,21 @@ def _format_change_type_legend(self): change_type_max_length = 20 self.builder.append_line("Resource and property changes are indicated with these symbols:") + self._push_indent() for i, change_type in enumerate(ALL_WHAT_IF_CHANGE_TYPES): - if i % 2 == 0: - self.builder.append(" " * DeploymentStacksWhatIfResultFormatter.INDENT_SIZE) - change_type_label = change_type[0].upper() + change_type[1:] + symbol, color = self._get_change_type_formatting(change_type) - (self.builder.append( - DeploymentStacksWhatIfResultFormatter.CHANGE_TYPE_SYMBOLS[change_type], - DeploymentStacksWhatIfResultFormatter.CHANGE_TYPE_COLORS[change_type]) - .append(" ").append(change_type_label)) + self.builder.append(symbol, color).append(" ", no_indent=True).append(change_type_label, no_indent=True) if i % 2 == 0: remaining_indent = max(1, change_type_max_length - len(change_type_label)) - self.builder.append(" " * remaining_indent) + self.builder.append(" " * remaining_indent, no_indent=True) elif i < len(ALL_WHAT_IF_CHANGE_TYPES) - 1: - self.builder.append_line() + self.builder.append_line(no_indent=True) + + self._pop_indent() return True @@ -125,16 +120,67 @@ def _format_stack_changes(self): printed = True if printed: - self.builder.insert_line(title_index, f"Changes to Stack {self.what_if_props.deployment_stack_resource_id}:", Color.DARK_YELLOW) + self.builder.insert_line( + title_index, f"Changes to Stack {self.what_if_props.deployment_stack_resource_id}:", Color.DARK_YELLOW) return printed def _format_resource_changes(self): - pass + printed = False + + title_index = self.builder.get_current_index() + + # TODO(kylealbert): sort definite vs potential + for change in self.what_if_changes.resource_changes or []: + if self._format_resource_change(change): + printed = True + + if printed: + self.builder.insert_line(title_index, "Changes to Managed Resources:", Color.DARK_YELLOW) + + return printed + + def _format_resource_change(self, resource_change: StackModels.DeploymentStacksWhatIfResourceChange): + symbol, color = self._get_change_type_formatting(resource_change.change_type) - def _format_resource_deletions(self): + if not resource_change.id: # is an extensible resource + return False # not yet supported + + all_resource_changes = { + "Management Status Change": resource_change.management_status_change, + "Deny Status Change": resource_change.deny_status_change, + } + + self.builder.append_line(f"{symbol} {resource_change.id}", color) + self._push_indent() + + for path, change in all_resource_changes.items(): + self._format_change(change, path) + + self._format_resource_property_changes(resource_change.resource_configuration_changes) + self._pop_indent() + + return True + + + def _format_resource_property_changes( + self, property_changes: t.Optional[StackModels.DeploymentStacksChangeDeltaRecord] + ): + if not property_changes or not property_changes.delta: + return False + + printed = False + + for property_change in property_changes.delta: + if self._format_change(property_change): + printed = True + + return printed + + + def _format_resource_deletions_summary(self): pass @@ -143,10 +189,16 @@ def _format_diagnostics(self): def _format_change( - self, change: t.Union[ - StackModels.DeploymentStacksChangeBase, StackModels.DeploymentStacksChangeDeltaRecord, StackModels.DeploymentStacksWhatIfPropertyChange], + self, + change: t.Optional[t.Union[ + StackModels.DeploymentStacksChangeBase, + StackModels.DeploymentStacksChangeDeltaRecord, + StackModels.DeploymentStacksWhatIfPropertyChange]], parent_path: t.Optional[str] = None ): + if not change: + return False + value_type = self._get_value_type_from_change(change) if value_type is str or value_type is bool or value_type is int: @@ -163,72 +215,102 @@ def _format_change( def _format_object_change( - self, object_change: t.Optional[StackModels.DeploymentStacksChangeDeltaRecord], parent_path: t.Optional[str] = None + self, object_change: t.Optional[StackModels.DeploymentStacksChangeDeltaRecord], + parent_path: t.Optional[str] = None ): - if not object_change: + if not object_change or not object_change.delta: return False printed = False - delta = object_change.delta - for delta in delta or []: + for delta in object_change.delta: if self._format_change(delta, parent_path): printed = True return printed - def _format_array_changes(self, array_change: StackModels.DeploymentStacksWhatIfPropertyChange, parent_path: t.Optional[str] = None): + def _format_array_changes( + self, array_change: StackModels.DeploymentStacksWhatIfPropertyChange, parent_path: t.Optional[str] = None + ): if not str_lower_eq(array_change.change_type, StackModels.DeploymentStacksWhatIfPropertyChangeType.ARRAY): return False property_path = self._get_change_path(array_change, parent_path) - color = DeploymentStacksWhatIfResultFormatter.CHANGE_TYPE_COLORS[StackModels.DeploymentStacksWhatIfPropertyChangeType.MODIFY] + symbol, color = self._get_change_type_formatting(StackModels.DeploymentStacksWhatIfPropertyChangeType.MODIFY) + + self.builder.append_line(f"{symbol} {property_path}: ", color) + self._push_indent() - self.builder.append_line(f"~ {property_path}: ", color) + for i, item_change in enumerate(array_change.children or []): + if self._format_array_child_change(item_change) and i < len(array_change.children) - 1: + self.builder.append_line(no_indent=True) - for item_change in array_change.children or []: - self._format_array_child_change(item_change) + self._pop_indent() return True def _format_array_child_change(self, array_change: StackModels.DeploymentStacksWhatIfPropertyChange): - symbol = DeploymentStacksWhatIfResultFormatter.CHANGE_TYPE_SYMBOLS.get(array_change.change_type, None) - color = DeploymentStacksWhatIfResultFormatter.CHANGE_TYPE_COLORS.get(array_change.change_type, None) - indent = self._get_indent(1) + symbol, color = self._get_change_type_formatting(array_change.change_type) + + if str_lower_eq(array_change.change_type, StackModels.DeploymentStacksWhatIfPropertyChangeType.CREATE) or \ + str_lower_eq(array_change.change_type, StackModels.DeploymentStacksWhatIfPropertyChangeType.NO_EFFECT): + self.builder.append(f"{symbol} {self._format_primitive_value(array_change.after)}", color) + return True + if str_lower_eq(array_change.change_type, StackModels.DeploymentStacksWhatIfPropertyChangeType.DELETE): + self.builder.append(f"{symbol} {self._format_primitive_value(array_change.before)}", color) + return True - if str_lower_eq(array_change.change_type, StackModels.DeploymentStacksWhatIfPropertyChangeType.CREATE): - self.builder.append_line(f"{indent}{symbol} {self._format_primitive_value(array_change.after)}", color) - elif str_lower_eq(array_change.change_type, StackModels.DeploymentStacksWhatIfPropertyChangeType.DELETE): - self.builder.append_line(f"{indent}{symbol} {self._format_primitive_value(array_change.before)}", color) - elif str_lower_eq(array_change.change_type, StackModels.DeploymentStacksWhatIfPropertyChangeType.NO_EFFECT): - self.builder.append_line(f"{indent}{symbol} {self._format_primitive_value(array_change.after)}", color) + return False def _format_primitive_change( self, - primitive_change: t.Optional[t.Union[StackModels.DeploymentStacksChangeBase, StackModels.DeploymentStacksWhatIfPropertyChange]], + primitive_change: t.Optional[ + t.Union[StackModels.DeploymentStacksChangeBase, StackModels.DeploymentStacksWhatIfPropertyChange]], parent_path: t.Optional[str] = None ): if not primitive_change: return False property_path = self._get_change_path(primitive_change, parent_path) - color = DeploymentStacksWhatIfResultFormatter.CHANGE_TYPE_COLORS[StackModels.DeploymentStacksWhatIfPropertyChangeType.MODIFY] + symbol, color = self._get_change_type_formatting( + StackModels.DeploymentStacksWhatIfPropertyChangeType.NO_EFFECT if primitive_change.before == primitive_change.after \ + else StackModels.DeploymentStacksWhatIfPropertyChangeType.MODIFY) - self.builder.append(f"~ {property_path}: ", color) + self.builder.append(f"{symbol} {property_path}: ", color) self.builder.append_line( - f"{self._format_primitive_value(primitive_change.before)} => {self._format_primitive_value(primitive_change.after)}") # TODO(kylealbert): correct arrow symbol? + f"{self._format_primitive_value(primitive_change.before)} => {self._format_primitive_value(primitive_change.after)}", + no_indent=True) return True + def _push_indent(self, indent_size=INDENT_SIZE): + self.builder.push_indent(" " * indent_size) + + + def _pop_indent(self): + self.builder.pop_indent() + + @staticmethod def _format_primitive_value(value: t.Union[str, bool, int]): return f'"{value}"' if isinstance(value, str) else str(value) + @staticmethod + def _get_change_type_formatting( + change_type: t.Union[ + StackModels.DeploymentStacksWhatIfChangeType, StackModels.DeploymentStacksWhatIfPropertyChangeType] + ): + symbol = DeploymentStacksWhatIfResultFormatter.CHANGE_TYPE_SYMBOLS.get(change_type, None) + color = DeploymentStacksWhatIfResultFormatter.CHANGE_TYPE_COLORS.get(change_type, None) + + return symbol, color + + @staticmethod def _get_change_path(change, parent_path: t.Optional[str] = None): if hasattr(change, "path"): @@ -236,15 +318,12 @@ def _get_change_path(change, parent_path: t.Optional[str] = None): return parent_path - @staticmethod - def _get_indent(indent_level: int, indent_size: int = INDENT_SIZE): - return " " * indent_size * indent_level - - @staticmethod def _get_value_type_from_change( change: t.Union[ - StackModels.DeploymentStacksChangeBase, StackModels.DeploymentStacksChangeDeltaRecord, StackModels.DeploymentStacksWhatIfPropertyChange] + StackModels.DeploymentStacksChangeBase, + StackModels.DeploymentStacksChangeDeltaRecord, + StackModels.DeploymentStacksWhatIfPropertyChange] ): if hasattr(change, "change_type"): if str_lower_eq(StackModels.DeploymentStacksWhatIfPropertyChangeType.ARRAY, change.change_type): From bbea70998619d8f141bb9cf5ac9cb30bd71ab8a3 Mon Sep 17 00:00:00 2001 From: Kyle Albert <5498623+kalbert312@users.noreply.github.com> Date: Wed, 25 Feb 2026 10:34:29 -0500 Subject: [PATCH 09/32] What-if potential resource change progress. --- .../cli/command_modules/resource/_color.py | 7 +- .../resource/_stacks_formatters.py | 65 +++++++++++++++---- 2 files changed, 55 insertions(+), 17 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/resource/_color.py b/src/azure-cli/azure/cli/command_modules/resource/_color.py index 343f3647f6b..e4995f0448f 100644 --- a/src/azure-cli/azure/cli/command_modules/resource/_color.py +++ b/src/azure-cli/azure/cli/command_modules/resource/_color.py @@ -12,6 +12,7 @@ class Color(Enum): GREEN = "\033[38;5;77m" PURPLE = "\033[38;5;141m" BLUE = "\033[38;5;39m" + CYAN = "\033[38;5;51m" GRAY = "\033[38;5;246m" RED = "\033[38;5;203m" DARK_YELLOW = "\033[38;5;136m" @@ -54,12 +55,12 @@ def new_color_scope(self, color): return self.ColorScope(self, color) def insert(self, index, value="", color=None): - if self._enable_color: + if color and self._enable_color: self._contents.insert(index, str(Color.RESET)) - self._contents.insert(index, f"{str(value)}") + self._contents.insert(index, str(value)) - if self._enable_color: + if color and self._enable_color: self._contents.insert(index, str(color)) return self diff --git a/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py b/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py index 7afa2277264..3bcbca3623e 100644 --- a/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py +++ b/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py @@ -12,7 +12,7 @@ from ._color import Color, ColoredStringBuilder from ._utils import str_lower_eq -ALL_WHAT_IF_CHANGE_TYPES = [ +ALL_WHAT_IF_TOP_LEVEL_CHANGE_TYPES = [ StackModels.DeploymentStacksWhatIfChangeType.CREATE, StackModels.DeploymentStacksWhatIfChangeType.UNSUPPORTED, StackModels.DeploymentStacksWhatIfChangeType.MODIFY, @@ -47,6 +47,11 @@ class DeploymentStacksWhatIfResultFormatter: # pylint: disable=too-few-public-m StackModels.DeploymentStacksWhatIfChangeType.MODIFY: Color.PURPLE }) + CHANGE_CERTAINTY_WEIGHTS = CaseInsensitiveDict( + { + StackModels.DeploymentStacksWhatIfChangeCertainty.DEFINITE: 0, + StackModels.DeploymentStacksWhatIfChangeCertainty.POTENTIAL: 1 + }) def __init__(self, enable_color=True): self.builder: ColoredStringBuilder = ColoredStringBuilder(enable_color) @@ -88,7 +93,7 @@ def _format_change_type_legend(self): self.builder.append_line("Resource and property changes are indicated with these symbols:") self._push_indent() - for i, change_type in enumerate(ALL_WHAT_IF_CHANGE_TYPES): + for i, change_type in enumerate(ALL_WHAT_IF_TOP_LEVEL_CHANGE_TYPES): change_type_label = change_type[0].upper() + change_type[1:] symbol, color = self._get_change_type_formatting(change_type) @@ -97,7 +102,7 @@ def _format_change_type_legend(self): if i % 2 == 0: remaining_indent = max(1, change_type_max_length - len(change_type_label)) self.builder.append(" " * remaining_indent, no_indent=True) - elif i < len(ALL_WHAT_IF_CHANGE_TYPES) - 1: + elif i < len(ALL_WHAT_IF_TOP_LEVEL_CHANGE_TYPES) - 1: self.builder.append_line(no_indent=True) self._pop_indent() @@ -106,10 +111,11 @@ def _format_change_type_legend(self): def _format_stack_changes(self): - printed = False + if not self.what_if_changes: + return False + printed = False title_index = self.builder.get_current_index() - all_stack_changes = { "DeploymentScope": self.what_if_changes.deployment_scope_change, "DenySettings": self.what_if_changes.deny_settings_change @@ -127,15 +133,29 @@ def _format_stack_changes(self): def _format_resource_changes(self): - printed = False + if not self.what_if_changes or not self.what_if_changes.resource_changes: + return False + printed = False title_index = self.builder.get_current_index() - # TODO(kylealbert): sort definite vs potential - for change in self.what_if_changes.resource_changes or []: + resource_changes_sorted = sorted( + self.what_if_changes.resource_changes, + key=lambda x: (DeploymentStacksWhatIfResultFormatter.CHANGE_CERTAINTY_WEIGHTS.get(x.change_certainty, 1), x.id)) + + first_potential_change_index = None + for change in resource_changes_sorted: + if first_potential_change_index is None and str_lower_eq( + change.change_certainty, StackModels.DeploymentStacksWhatIfChangeCertainty.POTENTIAL): + first_potential_change_index = self.builder.get_current_index() + if self._format_resource_change(change): printed = True + if first_potential_change_index is not None: + self.builder.insert_line(first_potential_change_index, "Potential Resource Changes (Learn more at https://aka.ms/whatIfPotentialChanges)", Color.PURPLE) + self.builder.insert(first_potential_change_index, ">> ") + if printed: self.builder.insert_line(title_index, "Changes to Managed Resources:", Color.DARK_YELLOW) @@ -148,17 +168,30 @@ def _format_resource_change(self, resource_change: StackModels.DeploymentStacksW if not resource_change.id: # is an extensible resource return False # not yet supported + is_potential_change = str_lower_eq( + resource_change.change_certainty, StackModels.DeploymentStacksWhatIfChangeCertainty.POTENTIAL) + + # print the change type and resource ID + if is_potential_change: + self.builder.append("?", Color.CYAN) + self.builder.append(f"{symbol} ", color, no_indent=is_potential_change) + if is_potential_change: + self.builder.append("Potential ?", Color.CYAN, no_indent=True).append(f"{symbol} ", color, no_indent=True) + + api_version_suffix = f" [{resource_change.api_version}]" if resource_change.api_version else "" + self.builder.append_line(f"{resource_change.id}{api_version_suffix}", color, no_indent=True) + + # print stack management related changes + self._push_indent() all_resource_changes = { "Management Status Change": resource_change.management_status_change, "Deny Status Change": resource_change.deny_status_change, } - self.builder.append_line(f"{symbol} {resource_change.id}", color) - self._push_indent() - for path, change in all_resource_changes.items(): self._format_change(change, path) + # print resource property changes self._format_resource_property_changes(resource_change.resource_configuration_changes) self._pop_indent() @@ -181,7 +214,8 @@ def _format_resource_property_changes( def _format_resource_deletions_summary(self): - pass + if not self.what_if_changes or not self.what_if_changes.resource_changes: + return False def _format_diagnostics(self): @@ -201,7 +235,7 @@ def _format_change( value_type = self._get_value_type_from_change(change) - if value_type is str or value_type is bool or value_type is int: + if value_type is str or value_type is bool or value_type is int or value_type is float: if self._format_primitive_change(change, parent_path): return True elif value_type is list: @@ -254,6 +288,7 @@ def _format_array_changes( def _format_array_child_change(self, array_change: StackModels.DeploymentStacksWhatIfPropertyChange): symbol, color = self._get_change_type_formatting(array_change.change_type) + # TODO(kylealbert): handle non-primitive if str_lower_eq(array_change.change_type, StackModels.DeploymentStacksWhatIfPropertyChangeType.CREATE) or \ str_lower_eq(array_change.change_type, StackModels.DeploymentStacksWhatIfPropertyChangeType.NO_EFFECT): self.builder.append(f"{symbol} {self._format_primitive_value(array_change.after)}", color) @@ -296,7 +331,9 @@ def _pop_indent(self): @staticmethod - def _format_primitive_value(value: t.Union[str, bool, int]): + def _format_primitive_value(value: t.Optional[t.Union[str, bool, int, float]]): + if value is None: + return "null" return f'"{value}"' if isinstance(value, str) else str(value) From 31db7abcaa36374ffc89291b423674fd35708610 Mon Sep 17 00:00:00 2001 From: Kyle Albert <5498623+kalbert312@users.noreply.github.com> Date: Wed, 25 Feb 2026 11:00:05 -0500 Subject: [PATCH 10/32] What-if resource deletion summary progress. --- .../resource/_stacks_formatters.py | 63 ++++++++++++++----- 1 file changed, 47 insertions(+), 16 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py b/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py index 3bcbca3623e..f8e66d67c23 100644 --- a/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py +++ b/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py @@ -73,8 +73,6 @@ def format(self, what_if_result: StackModels.DeploymentStacksWhatIfResult): self._format_new_section() if self._format_resource_changes(): self._format_new_section() - if self._format_resource_deletions_summary(): - self._format_new_section() self._format_diagnostics() result = self.builder.build() @@ -143,6 +141,7 @@ def _format_resource_changes(self): self.what_if_changes.resource_changes, key=lambda x: (DeploymentStacksWhatIfResultFormatter.CHANGE_CERTAINTY_WEIGHTS.get(x.change_certainty, 1), x.id)) + # Print the definite resource changes, followed by the potential changes first_potential_change_index = None for change in resource_changes_sorted: if first_potential_change_index is None and str_lower_eq( @@ -159,27 +158,42 @@ def _format_resource_changes(self): if printed: self.builder.insert_line(title_index, "Changes to Managed Resources:", Color.DARK_YELLOW) + # Summarize the deletions, if any + delete_changes = list(filter( + lambda x: str_lower_eq(x.change_type, StackModels.DeploymentStacksWhatIfChangeType.DELETE), + resource_changes_sorted)) + + if len(delete_changes) > 0: + self._format_new_section() + self.builder.append("Deleting - ", Color.RED) + self.builder.append_line(f"Resources Marked for Deletion {len(delete_changes)} total:", no_indent=True) + + first_potential_change_index = None + num_potential_deletions = 0 + for delete_change in delete_changes: + if first_potential_change_index is None and str_lower_eq( + delete_change.change_certainty, StackModels.DeploymentStacksWhatIfChangeCertainty.POTENTIAL): + first_potential_change_index = self.builder.get_current_index() + num_potential_deletions += 1 + + self._format_resource_heading_line(delete_change) + + if first_potential_change_index is not None: + self.builder.insert_line( + first_potential_change_index, + f"Potential Deletions {num_potential_deletions} total (Learn more at https://aka.ms/whatIfPotentialChanges)", + Color.RED) + self.builder.insert(first_potential_change_index, ">> ") + return printed def _format_resource_change(self, resource_change: StackModels.DeploymentStacksWhatIfResourceChange): - symbol, color = self._get_change_type_formatting(resource_change.change_type) - if not resource_change.id: # is an extensible resource return False # not yet supported - is_potential_change = str_lower_eq( - resource_change.change_certainty, StackModels.DeploymentStacksWhatIfChangeCertainty.POTENTIAL) - - # print the change type and resource ID - if is_potential_change: - self.builder.append("?", Color.CYAN) - self.builder.append(f"{symbol} ", color, no_indent=is_potential_change) - if is_potential_change: - self.builder.append("Potential ?", Color.CYAN, no_indent=True).append(f"{symbol} ", color, no_indent=True) - - api_version_suffix = f" [{resource_change.api_version}]" if resource_change.api_version else "" - self.builder.append_line(f"{resource_change.id}{api_version_suffix}", color, no_indent=True) + # print the resource heading line + self._format_resource_heading_line(resource_change) # print stack management related changes self._push_indent() @@ -198,6 +212,23 @@ def _format_resource_change(self, resource_change: StackModels.DeploymentStacksW return True + def _format_resource_heading_line(self, resource_change: StackModels.DeploymentStacksWhatIfResourceChange): + symbol, color = self._get_change_type_formatting(resource_change.change_type) + + is_potential_change = str_lower_eq( + resource_change.change_certainty, StackModels.DeploymentStacksWhatIfChangeCertainty.POTENTIAL) + + # print the change type and resource ID + if is_potential_change: + self.builder.append("?", Color.CYAN) + self.builder.append(f"{symbol} ", color, no_indent=is_potential_change) + if is_potential_change: + self.builder.append("Potential ?", Color.CYAN, no_indent=True).append(f"{symbol} ", color, no_indent=True) + + api_version_suffix = f" [{resource_change.api_version}]" if resource_change.api_version else "" + self.builder.append_line(f"{resource_change.id}{api_version_suffix}", color, no_indent=True) + + def _format_resource_property_changes( self, property_changes: t.Optional[StackModels.DeploymentStacksChangeDeltaRecord] ): From 2cb3c3b9ea52c4700b004fe4cf3c0a611e8e5614 Mon Sep 17 00:00:00 2001 From: Kyle Albert <5498623+kalbert312@users.noreply.github.com> Date: Wed, 25 Feb 2026 11:55:44 -0500 Subject: [PATCH 11/32] Add indents automatically. --- .../cli/command_modules/resource/_color.py | 20 ++++++++++------ .../resource/_stacks_formatters.py | 24 +++++++------------ 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/resource/_color.py b/src/azure-cli/azure/cli/command_modules/resource/_color.py index e4995f0448f..61431a0c29e 100644 --- a/src/azure-cli/azure/cli/command_modules/resource/_color.py +++ b/src/azure-cli/azure/cli/command_modules/resource/_color.py @@ -33,7 +33,7 @@ def build(self): return "".join(self._contents) def append(self, value, color=None, no_indent=False): - if not no_indent and len(self._indents) > 0: + if not no_indent and self._should_indent(): self._contents.append(''.join(self._indents)) if color: @@ -47,14 +47,13 @@ def append(self, value, color=None, no_indent=False): return self def append_line(self, value="", color=None, no_indent=False): - self.append(f"{str(value)}\n", color, no_indent) - - return self + self.append(value, color, no_indent) + return self.append("\n", no_indent=True) def new_color_scope(self, color): return self.ColorScope(self, color) - def insert(self, index, value="", color=None): + def insert(self, index, value="", color=None, no_indent=False): if color and self._enable_color: self._contents.insert(index, str(Color.RESET)) @@ -63,10 +62,14 @@ def insert(self, index, value="", color=None): if color and self._enable_color: self._contents.insert(index, str(color)) + if not no_indent and self._should_indent(index, True): + self._contents.insert(index, ''.join(self._indents)) + return self - def insert_line(self, index, value="", color=None): - return self.insert(index, f"{str(value)}\n", color) + def insert_line(self, index, value="", color=None, no_indent=False): + self.insert(index, "\n", no_indent=no_indent) + return self.insert(index, value, color, no_indent) def get_current_index(self): return len(self._contents) @@ -93,6 +96,9 @@ def _pop_color(self): self._colors.pop() self._contents.append(str(self._colors[-1] if self._colors else Color.RESET)) + + def _should_indent(self, index=-1, is_insert=False): + return len(self._indents) > 0 and (not self._contents or self._contents[max(index - 1, 0) if is_insert else index].endswith("\n")) # pylint: disable=protected-access class ColorScope: diff --git a/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py b/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py index f8e66d67c23..70a6e4eeac1 100644 --- a/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py +++ b/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py @@ -95,13 +95,13 @@ def _format_change_type_legend(self): change_type_label = change_type[0].upper() + change_type[1:] symbol, color = self._get_change_type_formatting(change_type) - self.builder.append(symbol, color).append(" ", no_indent=True).append(change_type_label, no_indent=True) + self.builder.append(symbol, color).append(" ").append(change_type_label) if i % 2 == 0: remaining_indent = max(1, change_type_max_length - len(change_type_label)) - self.builder.append(" " * remaining_indent, no_indent=True) + self.builder.append(" " * remaining_indent) elif i < len(ALL_WHAT_IF_TOP_LEVEL_CHANGE_TYPES) - 1: - self.builder.append_line(no_indent=True) + self.builder.append_line() self._pop_indent() @@ -166,7 +166,7 @@ def _format_resource_changes(self): if len(delete_changes) > 0: self._format_new_section() self.builder.append("Deleting - ", Color.RED) - self.builder.append_line(f"Resources Marked for Deletion {len(delete_changes)} total:", no_indent=True) + self.builder.append_line(f"Resources Marked for Deletion {len(delete_changes)} total:") first_potential_change_index = None num_potential_deletions = 0 @@ -221,12 +221,12 @@ def _format_resource_heading_line(self, resource_change: StackModels.DeploymentS # print the change type and resource ID if is_potential_change: self.builder.append("?", Color.CYAN) - self.builder.append(f"{symbol} ", color, no_indent=is_potential_change) + self.builder.append(f"{symbol} ", color) if is_potential_change: - self.builder.append("Potential ?", Color.CYAN, no_indent=True).append(f"{symbol} ", color, no_indent=True) + self.builder.append("Potential ?", Color.CYAN).append(f"{symbol} ", color) api_version_suffix = f" [{resource_change.api_version}]" if resource_change.api_version else "" - self.builder.append_line(f"{resource_change.id}{api_version_suffix}", color, no_indent=True) + self.builder.append_line(f"{resource_change.id}{api_version_suffix}", color) def _format_resource_property_changes( @@ -244,11 +244,6 @@ def _format_resource_property_changes( return printed - def _format_resource_deletions_summary(self): - if not self.what_if_changes or not self.what_if_changes.resource_changes: - return False - - def _format_diagnostics(self): pass @@ -309,7 +304,7 @@ def _format_array_changes( for i, item_change in enumerate(array_change.children or []): if self._format_array_child_change(item_change) and i < len(array_change.children) - 1: - self.builder.append_line(no_indent=True) + self.builder.append_line() self._pop_indent() @@ -347,8 +342,7 @@ def _format_primitive_change( self.builder.append(f"{symbol} {property_path}: ", color) self.builder.append_line( - f"{self._format_primitive_value(primitive_change.before)} => {self._format_primitive_value(primitive_change.after)}", - no_indent=True) + f"{self._format_primitive_value(primitive_change.before)} => {self._format_primitive_value(primitive_change.after)}") return True From 2018396e05573d0a88944629b1370f895bc4948e Mon Sep 17 00:00:00 2001 From: Kyle Albert <5498623+kalbert312@users.noreply.github.com> Date: Wed, 25 Feb 2026 12:00:20 -0500 Subject: [PATCH 12/32] Type hints. --- .../resource/_stacks_formatters.py | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py b/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py index 70a6e4eeac1..2228add6fe0 100644 --- a/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py +++ b/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py @@ -60,7 +60,7 @@ def __init__(self, enable_color=True): self.what_if_changes: t.Optional[StackModels.DeploymentStacksWhatIfChange] = None - def format(self, what_if_result: StackModels.DeploymentStacksWhatIfResult): + def format(self, what_if_result: StackModels.DeploymentStacksWhatIfResult) -> str: self.builder.clear() self.what_if_result = what_if_result @@ -85,7 +85,7 @@ def _format_new_section(self): self.builder.append("\n\n") - def _format_change_type_legend(self): + def _format_change_type_legend(self) -> bool: change_type_max_length = 20 self.builder.append_line("Resource and property changes are indicated with these symbols:") @@ -108,7 +108,7 @@ def _format_change_type_legend(self): return True - def _format_stack_changes(self): + def _format_stack_changes(self) -> bool: if not self.what_if_changes: return False @@ -130,7 +130,7 @@ def _format_stack_changes(self): return printed - def _format_resource_changes(self): + def _format_resource_changes(self) -> bool: if not self.what_if_changes or not self.what_if_changes.resource_changes: return False @@ -188,7 +188,7 @@ def _format_resource_changes(self): return printed - def _format_resource_change(self, resource_change: StackModels.DeploymentStacksWhatIfResourceChange): + def _format_resource_change(self, resource_change: StackModels.DeploymentStacksWhatIfResourceChange) -> bool: if not resource_change.id: # is an extensible resource return False # not yet supported @@ -231,7 +231,7 @@ def _format_resource_heading_line(self, resource_change: StackModels.DeploymentS def _format_resource_property_changes( self, property_changes: t.Optional[StackModels.DeploymentStacksChangeDeltaRecord] - ): + ) -> bool: if not property_changes or not property_changes.delta: return False @@ -244,7 +244,7 @@ def _format_resource_property_changes( return printed - def _format_diagnostics(self): + def _format_diagnostics(self) -> bool: pass @@ -255,7 +255,7 @@ def _format_change( StackModels.DeploymentStacksChangeDeltaRecord, StackModels.DeploymentStacksWhatIfPropertyChange]], parent_path: t.Optional[str] = None - ): + ) -> bool: if not change: return False @@ -277,7 +277,7 @@ def _format_change( def _format_object_change( self, object_change: t.Optional[StackModels.DeploymentStacksChangeDeltaRecord], parent_path: t.Optional[str] = None - ): + ) -> bool: if not object_change or not object_change.delta: return False @@ -292,7 +292,7 @@ def _format_object_change( def _format_array_changes( self, array_change: StackModels.DeploymentStacksWhatIfPropertyChange, parent_path: t.Optional[str] = None - ): + ) -> bool: if not str_lower_eq(array_change.change_type, StackModels.DeploymentStacksWhatIfPropertyChangeType.ARRAY): return False @@ -311,7 +311,7 @@ def _format_array_changes( return True - def _format_array_child_change(self, array_change: StackModels.DeploymentStacksWhatIfPropertyChange): + def _format_array_child_change(self, array_change: StackModels.DeploymentStacksWhatIfPropertyChange) -> bool: symbol, color = self._get_change_type_formatting(array_change.change_type) # TODO(kylealbert): handle non-primitive @@ -331,7 +331,7 @@ def _format_primitive_change( primitive_change: t.Optional[ t.Union[StackModels.DeploymentStacksChangeBase, StackModels.DeploymentStacksWhatIfPropertyChange]], parent_path: t.Optional[str] = None - ): + ) -> bool: if not primitive_change: return False @@ -366,7 +366,7 @@ def _format_primitive_value(value: t.Optional[t.Union[str, bool, int, float]]): def _get_change_type_formatting( change_type: t.Union[ StackModels.DeploymentStacksWhatIfChangeType, StackModels.DeploymentStacksWhatIfPropertyChangeType] - ): + ) -> t.Tuple[t.Optional[str], t.Optional[Color]]: symbol = DeploymentStacksWhatIfResultFormatter.CHANGE_TYPE_SYMBOLS.get(change_type, None) color = DeploymentStacksWhatIfResultFormatter.CHANGE_TYPE_COLORS.get(change_type, None) @@ -374,7 +374,7 @@ def _get_change_type_formatting( @staticmethod - def _get_change_path(change, parent_path: t.Optional[str] = None): + def _get_change_path(change, parent_path: t.Optional[str] = None) -> str: if hasattr(change, "path"): return '.'.join([parent_path, change.path]) if parent_path else change.path return parent_path @@ -386,7 +386,7 @@ def _get_value_type_from_change( StackModels.DeploymentStacksChangeBase, StackModels.DeploymentStacksChangeDeltaRecord, StackModels.DeploymentStacksWhatIfPropertyChange] - ): + ) -> t.Optional[t.Type]: if hasattr(change, "change_type"): if str_lower_eq(StackModels.DeploymentStacksWhatIfPropertyChangeType.ARRAY, change.change_type): return list From 158e79bcc7909edd5cb33d337c542ba208abfe3f Mon Sep 17 00:00:00 2001 From: Kyle Albert <5498623+kalbert312@users.noreply.github.com> Date: Wed, 25 Feb 2026 14:54:18 -0500 Subject: [PATCH 13/32] Diagnostics formatting progress. --- .../cli/command_modules/resource/_color.py | 13 +++++ .../resource/_stacks_formatters.py | 47 ++++++++++++++++--- 2 files changed, 53 insertions(+), 7 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/resource/_color.py b/src/azure-cli/azure/cli/command_modules/resource/_color.py index 61431a0c29e..bd5c164cf27 100644 --- a/src/azure-cli/azure/cli/command_modules/resource/_color.py +++ b/src/azure-cli/azure/cli/command_modules/resource/_color.py @@ -80,6 +80,19 @@ def push_indent(self, indent): def pop_indent(self): self._indents.pop() + def ensure_num_new_lines(self, num_new_lines): + if len(self._contents) == 0: + self.append("\n" * num_new_lines) + return + + last_entry = self._contents[-1] + existing_newlines = len(last_entry) - len(last_entry.rstrip('\n')) + remaining_newlines = num_new_lines - existing_newlines + + if remaining_newlines > 0: + self._contents.append("\n" * remaining_newlines) + + def clear(self): self._contents = [] diff --git a/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py b/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py index 2228add6fe0..bbe1a2eaf80 100644 --- a/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py +++ b/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py @@ -53,6 +53,19 @@ class DeploymentStacksWhatIfResultFormatter: # pylint: disable=too-few-public-m StackModels.DeploymentStacksWhatIfChangeCertainty.POTENTIAL: 1 }) + DIAGNOSTIC_WEIGHTS = CaseInsensitiveDict( + { + StackModels.DeploymentStacksDiagnosticLevel.INFO: 1, + StackModels.DeploymentStacksDiagnosticLevel.WARNING: 2, + StackModels.DeploymentStacksDiagnosticLevel.ERROR: 3, + }) + + DIAGNOSTIC_COLORS = CaseInsensitiveDict( + { + StackModels.DeploymentStacksDiagnosticLevel.WARNING: Color.DARK_YELLOW, + StackModels.DeploymentStacksDiagnosticLevel.ERROR: Color.RED, + }) + def __init__(self, enable_color=True): self.builder: ColoredStringBuilder = ColoredStringBuilder(enable_color) self.what_if_result: t.Optional[StackModels.DeploymentStacksWhatIfResult] = None @@ -68,11 +81,11 @@ def format(self, what_if_result: StackModels.DeploymentStacksWhatIfResult) -> st self.what_if_changes = self.what_if_props.changes if self.what_if_props else None if self._format_change_type_legend(): - self._format_new_section() + self._format_section_spacer() if self._format_stack_changes(): - self._format_new_section() + self._format_section_spacer() if self._format_resource_changes(): - self._format_new_section() + self._format_section_spacer() self._format_diagnostics() result = self.builder.build() @@ -81,8 +94,8 @@ def format(self, what_if_result: StackModels.DeploymentStacksWhatIfResult) -> st return result - def _format_new_section(self): - self.builder.append("\n\n") + def _format_section_spacer(self): + self.builder.ensure_num_new_lines(2) def _format_change_type_legend(self) -> bool: @@ -164,7 +177,7 @@ def _format_resource_changes(self) -> bool: resource_changes_sorted)) if len(delete_changes) > 0: - self._format_new_section() + self._format_section_spacer() self.builder.append("Deleting - ", Color.RED) self.builder.append_line(f"Resources Marked for Deletion {len(delete_changes)} total:") @@ -245,7 +258,27 @@ def _format_resource_property_changes( def _format_diagnostics(self) -> bool: - pass + if not self.what_if_props or not self.what_if_props.diagnostics or len(self.what_if_props.diagnostics) == 0: + return False + + diagnostics_sorted = sorted( + self.what_if_props.diagnostics, + key=lambda x: (DeploymentStacksWhatIfResultFormatter.DIAGNOSTIC_WEIGHTS.get(x.level, 0), x.code)) + + title_index = self.builder.get_current_index() + + for i, diagnostic in enumerate(diagnostics_sorted): + self._format_diagnostic(diagnostic) + + self.builder.insert_line(title_index, f"Diagnostics ({len(diagnostics_sorted)}):") + + return True + + + def _format_diagnostic(self, diagnostic: StackModels.DeploymentStacksDiagnostic): + self.builder.append_line( + f"{diagnostic.level.upper()}: [{diagnostic.code}] {diagnostic.message}", + DeploymentStacksWhatIfResultFormatter.DIAGNOSTIC_COLORS.get(diagnostic.level, None)) def _format_change( From 9243a118f3f362136fc73df34928fa9f8e951b3e Mon Sep 17 00:00:00 2001 From: Kyle Albert <5498623+kalbert312@users.noreply.github.com> Date: Wed, 25 Feb 2026 15:36:01 -0500 Subject: [PATCH 14/32] Fix type logic. --- .../cli/command_modules/resource/_stacks_formatters.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py b/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py index bbe1a2eaf80..251e74cd932 100644 --- a/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py +++ b/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py @@ -431,7 +431,9 @@ def _get_value_type_from_change( if before_type == after_type: return before_type - if before_type is type(None) or after_type is type(None): - return before_type or after_type + if after_type is not type(None): + return after_type + if before_type is not type(None): + return before_type return None From 9d9d64f2b8b2801a6b96c1db749dffbfdb200560 Mon Sep 17 00:00:00 2001 From: Kyle Albert <5498623+kalbert312@users.noreply.github.com> Date: Wed, 25 Feb 2026 16:31:17 -0500 Subject: [PATCH 15/32] Fix count. --- .../cli/command_modules/resource/_stacks_formatters.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py b/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py index 251e74cd932..bf05047f5a9 100644 --- a/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py +++ b/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py @@ -184,10 +184,10 @@ def _format_resource_changes(self) -> bool: first_potential_change_index = None num_potential_deletions = 0 for delete_change in delete_changes: - if first_potential_change_index is None and str_lower_eq( - delete_change.change_certainty, StackModels.DeploymentStacksWhatIfChangeCertainty.POTENTIAL): - first_potential_change_index = self.builder.get_current_index() + if str_lower_eq(delete_change.change_certainty, StackModels.DeploymentStacksWhatIfChangeCertainty.POTENTIAL): num_potential_deletions += 1 + if first_potential_change_index is None: + first_potential_change_index = self.builder.get_current_index() self._format_resource_heading_line(delete_change) From 376f3ac62dcc1be2a7bae8687ce5bedd70fbfe5d Mon Sep 17 00:00:00 2001 From: Kyle Albert <5498623+kalbert312@users.noreply.github.com> Date: Thu, 26 Feb 2026 11:12:31 -0500 Subject: [PATCH 16/32] Split into methods. --- .../resource/_stacks_formatters.py | 94 ++++++++++++------- 1 file changed, 60 insertions(+), 34 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py b/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py index bf05047f5a9..55903cdcf88 100644 --- a/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py +++ b/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py @@ -4,6 +4,7 @@ # -------------------------------------------------------------------------------------------- import typing as t + from requests.structures import CaseInsensitiveDict import azure.mgmt.resource.deploymentstacks.models as StackModels @@ -84,7 +85,7 @@ def format(self, what_if_result: StackModels.DeploymentStacksWhatIfResult) -> st self._format_section_spacer() if self._format_stack_changes(): self._format_section_spacer() - if self._format_resource_changes(): + if self._format_resource_changes_and_deletion_summary(): self._format_section_spacer() self._format_diagnostics() @@ -143,19 +144,32 @@ def _format_stack_changes(self) -> bool: return printed - def _format_resource_changes(self) -> bool: + def _format_resource_changes_and_deletion_summary(self) -> bool: if not self.what_if_changes or not self.what_if_changes.resource_changes: return False printed = False - title_index = self.builder.get_current_index() - resource_changes_sorted = sorted( self.what_if_changes.resource_changes, - key=lambda x: (DeploymentStacksWhatIfResultFormatter.CHANGE_CERTAINTY_WEIGHTS.get(x.change_certainty, 1), x.id)) + key=lambda x: (DeploymentStacksWhatIfResultFormatter.CHANGE_CERTAINTY_WEIGHTS.get(x.change_certainty, 1), x.id or "")) + if self._format_resource_changes(resource_changes_sorted): + printed = True + if self._format_resource_deletions_summary(resource_changes_sorted): + printed = True + + return printed + + + def _format_resource_changes( + self, resource_changes_sorted: list[StackModels.DeploymentStacksWhatIfResourceChange] + ) -> bool: # Print the definite resource changes, followed by the potential changes + title_index = self.builder.get_current_index() first_potential_change_index = None + + printed = False + for change in resource_changes_sorted: if first_potential_change_index is None and str_lower_eq( change.change_certainty, StackModels.DeploymentStacksWhatIfChangeCertainty.POTENTIAL): @@ -165,39 +179,14 @@ def _format_resource_changes(self) -> bool: printed = True if first_potential_change_index is not None: - self.builder.insert_line(first_potential_change_index, "Potential Resource Changes (Learn more at https://aka.ms/whatIfPotentialChanges)", Color.PURPLE) + self.builder.insert_line( + first_potential_change_index, + "Potential Resource Changes (Learn more at https://aka.ms/whatIfPotentialChanges)", Color.PURPLE) self.builder.insert(first_potential_change_index, ">> ") if printed: self.builder.insert_line(title_index, "Changes to Managed Resources:", Color.DARK_YELLOW) - # Summarize the deletions, if any - delete_changes = list(filter( - lambda x: str_lower_eq(x.change_type, StackModels.DeploymentStacksWhatIfChangeType.DELETE), - resource_changes_sorted)) - - if len(delete_changes) > 0: - self._format_section_spacer() - self.builder.append("Deleting - ", Color.RED) - self.builder.append_line(f"Resources Marked for Deletion {len(delete_changes)} total:") - - first_potential_change_index = None - num_potential_deletions = 0 - for delete_change in delete_changes: - if str_lower_eq(delete_change.change_certainty, StackModels.DeploymentStacksWhatIfChangeCertainty.POTENTIAL): - num_potential_deletions += 1 - if first_potential_change_index is None: - first_potential_change_index = self.builder.get_current_index() - - self._format_resource_heading_line(delete_change) - - if first_potential_change_index is not None: - self.builder.insert_line( - first_potential_change_index, - f"Potential Deletions {num_potential_deletions} total (Learn more at https://aka.ms/whatIfPotentialChanges)", - Color.RED) - self.builder.insert(first_potential_change_index, ">> ") - return printed @@ -225,6 +214,43 @@ def _format_resource_change(self, resource_change: StackModels.DeploymentStacksW return True + def _format_resource_deletions_summary( + self, resource_changes_sorted: list[StackModels.DeploymentStacksWhatIfResourceChange] + ) -> bool: + # Summarize the deletions, if any + printed = False + delete_changes = list( + filter( + lambda x: str_lower_eq(x.change_type, StackModels.DeploymentStacksWhatIfChangeType.DELETE), + resource_changes_sorted)) + + if len(delete_changes) > 0: + self._format_section_spacer() + self.builder.append("Deleting - ", Color.RED) + self.builder.append_line(f"Resources Marked for Deletion {len(delete_changes)} total:") + printed = True + + first_potential_change_index = None + num_potential_deletions = 0 + for delete_change in delete_changes: + if str_lower_eq( + delete_change.change_certainty, StackModels.DeploymentStacksWhatIfChangeCertainty.POTENTIAL): + num_potential_deletions += 1 + if first_potential_change_index is None: + first_potential_change_index = self.builder.get_current_index() + + self._format_resource_heading_line(delete_change) + + if first_potential_change_index is not None: + self.builder.insert_line( + first_potential_change_index, + f"Potential Deletions {num_potential_deletions} total (Learn more at https://aka.ms/whatIfPotentialChanges)", + Color.RED) + self.builder.insert(first_potential_change_index, ">> ") + + return printed + + def _format_resource_heading_line(self, resource_change: StackModels.DeploymentStacksWhatIfResourceChange): symbol, color = self._get_change_type_formatting(resource_change.change_type) @@ -263,7 +289,7 @@ def _format_diagnostics(self) -> bool: diagnostics_sorted = sorted( self.what_if_props.diagnostics, - key=lambda x: (DeploymentStacksWhatIfResultFormatter.DIAGNOSTIC_WEIGHTS.get(x.level, 0), x.code)) + key=lambda x: (DeploymentStacksWhatIfResultFormatter.DIAGNOSTIC_WEIGHTS.get(x.level, 0), x.code or "")) title_index = self.builder.get_current_index() From cd5c81871f60ae0b231e593cc8c2993dff0c5f22 Mon Sep 17 00:00:00 2001 From: Kyle Albert <5498623+kalbert312@users.noreply.github.com> Date: Thu, 26 Feb 2026 12:35:49 -0500 Subject: [PATCH 17/32] Extensible resource output support. --- .../resource/_stacks_formatters.py | 68 ++++++++++++++----- 1 file changed, 50 insertions(+), 18 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py b/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py index 55903cdcf88..f2719f2c5fd 100644 --- a/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py +++ b/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py @@ -3,6 +3,7 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- +import json import typing as t from requests.structures import CaseInsensitiveDict @@ -48,13 +49,13 @@ class DeploymentStacksWhatIfResultFormatter: # pylint: disable=too-few-public-m StackModels.DeploymentStacksWhatIfChangeType.MODIFY: Color.PURPLE }) - CHANGE_CERTAINTY_WEIGHTS = CaseInsensitiveDict( + CHANGE_CERTAINTY_PRIORITIES = CaseInsensitiveDict( { StackModels.DeploymentStacksWhatIfChangeCertainty.DEFINITE: 0, StackModels.DeploymentStacksWhatIfChangeCertainty.POTENTIAL: 1 }) - DIAGNOSTIC_WEIGHTS = CaseInsensitiveDict( + DIAGNOSTIC_LEVEL_PRIORITIES = CaseInsensitiveDict( { StackModels.DeploymentStacksDiagnosticLevel.INFO: 1, StackModels.DeploymentStacksDiagnosticLevel.WARNING: 2, @@ -151,7 +152,20 @@ def _format_resource_changes_and_deletion_summary(self) -> bool: printed = False resource_changes_sorted = sorted( self.what_if_changes.resource_changes, - key=lambda x: (DeploymentStacksWhatIfResultFormatter.CHANGE_CERTAINTY_WEIGHTS.get(x.change_certainty, 1), x.id or "")) + key=lambda x: ( + 0 if x.id else 1, # sort Azure resources before extension resources + DeploymentStacksWhatIfResultFormatter.CHANGE_CERTAINTY_PRIORITIES.get( + x.change_certainty, 1) if x.id else 0, # Azure resources: then by certainty + x.id if x.id else "", # Azure resources: then by ID + x.extension.name if x.extension else "", + # Extension resources: then by (ext name, ext version, config id) + x.extension.version if x.extension else "", + (x.extension.config_id if x.extension else "") or "", + DeploymentStacksWhatIfResultFormatter.CHANGE_CERTAINTY_PRIORITIES.get( + x.change_certainty, 1) if not x.id else 0, # Extension resources: then by certainty + x.type if x.extension else "", # Extension resources: then by type + json.dumps(x.identifiers) if x.extension else "" # Extension resources: then by identifiers + )) if self._format_resource_changes(resource_changes_sorted): printed = True @@ -166,34 +180,40 @@ def _format_resource_changes( ) -> bool: # Print the definite resource changes, followed by the potential changes title_index = self.builder.get_current_index() - first_potential_change_index = None - + first_potential_change_index: t.Optional[int] = None + last_group: t.Optional[str] = None printed = False for change in resource_changes_sorted: + printed = True + + # check if a new section should be started + group = "Azure" if change.id else ( + f"{change.extension.name}@{change.extension.version}" if change.extension else "Unknown") + + if group != last_group: + self._format_section_spacer() + self.builder.append_line(group) + last_group = group + first_potential_change_index = None + if first_potential_change_index is None and str_lower_eq( change.change_certainty, StackModels.DeploymentStacksWhatIfChangeCertainty.POTENTIAL): + self.builder.append(">> ").append_line( + "Potential Resource Changes (Learn more at https://aka.ms/whatIfPotentialChanges)", Color.PURPLE) first_potential_change_index = self.builder.get_current_index() if self._format_resource_change(change): printed = True - if first_potential_change_index is not None: - self.builder.insert_line( - first_potential_change_index, - "Potential Resource Changes (Learn more at https://aka.ms/whatIfPotentialChanges)", Color.PURPLE) - self.builder.insert(first_potential_change_index, ">> ") - if printed: + self.builder.insert_line(title_index) self.builder.insert_line(title_index, "Changes to Managed Resources:", Color.DARK_YELLOW) return printed def _format_resource_change(self, resource_change: StackModels.DeploymentStacksWhatIfResourceChange) -> bool: - if not resource_change.id: # is an extensible resource - return False # not yet supported - # print the resource heading line self._format_resource_heading_line(resource_change) @@ -230,9 +250,20 @@ def _format_resource_deletions_summary( self.builder.append_line(f"Resources Marked for Deletion {len(delete_changes)} total:") printed = True - first_potential_change_index = None + first_potential_change_index: t.Optional[int] = None + last_group: t.Optional[str] = None num_potential_deletions = 0 + for delete_change in delete_changes: + group = "Azure" if delete_change.id else ( + f"{delete_change.extension.name}@{delete_change.extension.version}" if delete_change.extension else "Unknown") + + if group != last_group: + self._format_section_spacer() + self.builder.append_line(group) + last_group = group + first_potential_change_index = None + if str_lower_eq( delete_change.change_certainty, StackModels.DeploymentStacksWhatIfChangeCertainty.POTENTIAL): num_potential_deletions += 1 @@ -262,10 +293,11 @@ def _format_resource_heading_line(self, resource_change: StackModels.DeploymentS self.builder.append("?", Color.CYAN) self.builder.append(f"{symbol} ", color) if is_potential_change: - self.builder.append("Potential ?", Color.CYAN).append(f"{symbol} ", color) + self.builder.append("[Potential] ", Color.CYAN) api_version_suffix = f" [{resource_change.api_version}]" if resource_change.api_version else "" - self.builder.append_line(f"{resource_change.id}{api_version_suffix}", color) + resource_id = resource_change.id if resource_change.id else f"{resource_change.type} {json.dumps(resource_change.identifiers)}" + self.builder.append_line(f"{resource_id}{api_version_suffix}", color) def _format_resource_property_changes( @@ -289,7 +321,7 @@ def _format_diagnostics(self) -> bool: diagnostics_sorted = sorted( self.what_if_props.diagnostics, - key=lambda x: (DeploymentStacksWhatIfResultFormatter.DIAGNOSTIC_WEIGHTS.get(x.level, 0), x.code or "")) + key=lambda x: (DeploymentStacksWhatIfResultFormatter.DIAGNOSTIC_LEVEL_PRIORITIES.get(x.level, 0), x.code or "")) title_index = self.builder.get_current_index() From 38a237c55bff7c3ee40cd750e48da30f70bff464 Mon Sep 17 00:00:00 2001 From: Kyle Albert <5498623+kalbert312@users.noreply.github.com> Date: Thu, 26 Feb 2026 16:12:36 -0500 Subject: [PATCH 18/32] Array children rendering fixes and misc refactors. --- .../resource/_stacks_formatters.py | 162 +++++++++++------- 1 file changed, 96 insertions(+), 66 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py b/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py index f2719f2c5fd..f6f8f9bd784 100644 --- a/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py +++ b/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py @@ -178,15 +178,16 @@ def _format_resource_changes_and_deletion_summary(self) -> bool: def _format_resource_changes( self, resource_changes_sorted: list[StackModels.DeploymentStacksWhatIfResourceChange] ) -> bool: + if resource_changes_sorted is None or len(resource_changes_sorted) == 0: + return False + # Print the definite resource changes, followed by the potential changes - title_index = self.builder.get_current_index() - first_potential_change_index: t.Optional[int] = None last_group: t.Optional[str] = None - printed = False + has_potential_changes = False - for change in resource_changes_sorted: - printed = True + self.builder.append_line("Changes to Managed Resources:", Color.DARK_YELLOW) + for change in resource_changes_sorted: # check if a new section should be started group = "Azure" if change.id else ( f"{change.extension.name}@{change.extension.version}" if change.extension else "Unknown") @@ -195,22 +196,19 @@ def _format_resource_changes( self._format_section_spacer() self.builder.append_line(group) last_group = group - first_potential_change_index = None + has_potential_changes = False - if first_potential_change_index is None and str_lower_eq( - change.change_certainty, StackModels.DeploymentStacksWhatIfChangeCertainty.POTENTIAL): - self.builder.append(">> ").append_line( - "Potential Resource Changes (Learn more at https://aka.ms/whatIfPotentialChanges)", Color.PURPLE) - first_potential_change_index = self.builder.get_current_index() + if str_lower_eq(change.change_certainty, StackModels.DeploymentStacksWhatIfChangeCertainty.POTENTIAL): + if not has_potential_changes: + self.builder.append(">> ").append_line( + "Potential Resource Changes (Learn more at https://aka.ms/whatIfPotentialChanges)", + Color.PURPLE) - if self._format_resource_change(change): - printed = True + has_potential_changes = True - if printed: - self.builder.insert_line(title_index) - self.builder.insert_line(title_index, "Changes to Managed Resources:", Color.DARK_YELLOW) + self._format_resource_change(change) - return printed + return True def _format_resource_change(self, resource_change: StackModels.DeploymentStacksWhatIfResourceChange) -> bool: @@ -250,11 +248,10 @@ def _format_resource_deletions_summary( self.builder.append_line(f"Resources Marked for Deletion {len(delete_changes)} total:") printed = True - first_potential_change_index: t.Optional[int] = None last_group: t.Optional[str] = None - num_potential_deletions = 0 + has_potential_deletions = False - for delete_change in delete_changes: + for i, delete_change in enumerate(delete_changes): group = "Azure" if delete_change.id else ( f"{delete_change.extension.name}@{delete_change.extension.version}" if delete_change.extension else "Unknown") @@ -262,22 +259,19 @@ def _format_resource_deletions_summary( self._format_section_spacer() self.builder.append_line(group) last_group = group - first_potential_change_index = None + has_potential_deletions = False if str_lower_eq( delete_change.change_certainty, StackModels.DeploymentStacksWhatIfChangeCertainty.POTENTIAL): - num_potential_deletions += 1 - if first_potential_change_index is None: - first_potential_change_index = self.builder.get_current_index() - self._format_resource_heading_line(delete_change) + if not has_potential_deletions: + self.builder.append(">> ").append_line( + f"Potential Deletions {self._get_num_potential_resource_changes(delete_changes, i)} total (Learn more at https://aka.ms/whatIfPotentialChanges)", + Color.RED) - if first_potential_change_index is not None: - self.builder.insert_line( - first_potential_change_index, - f"Potential Deletions {num_potential_deletions} total (Learn more at https://aka.ms/whatIfPotentialChanges)", - Color.RED) - self.builder.insert(first_potential_change_index, ">> ") + has_potential_deletions = True + + self._format_resource_heading_line(delete_change) return printed @@ -323,13 +317,11 @@ def _format_diagnostics(self) -> bool: self.what_if_props.diagnostics, key=lambda x: (DeploymentStacksWhatIfResultFormatter.DIAGNOSTIC_LEVEL_PRIORITIES.get(x.level, 0), x.code or "")) - title_index = self.builder.get_current_index() + self.builder.append_line(f"Diagnostics ({len(diagnostics_sorted)}):") for i, diagnostic in enumerate(diagnostics_sorted): self._format_diagnostic(diagnostic) - self.builder.insert_line(title_index, f"Diagnostics ({len(diagnostics_sorted)}):") - return True @@ -345,7 +337,8 @@ def _format_change( StackModels.DeploymentStacksChangeBase, StackModels.DeploymentStacksChangeDeltaRecord, StackModels.DeploymentStacksWhatIfPropertyChange]], - parent_path: t.Optional[str] = None + parent_path: t.Optional[str] = None, + is_array_item: bool = False ) -> bool: if not change: return False @@ -353,7 +346,7 @@ def _format_change( value_type = self._get_value_type_from_change(change) if value_type is str or value_type is bool or value_type is int or value_type is float: - if self._format_primitive_change(change, parent_path): + if self._format_primitive_change(change, parent_path, is_array_item): return True elif value_type is list: if self._format_array_changes(change, parent_path): @@ -366,16 +359,24 @@ def _format_change( def _format_object_change( - self, object_change: t.Optional[StackModels.DeploymentStacksChangeDeltaRecord], + self, + object_change: t.Optional[t.Union[ + StackModels.DeploymentStacksChangeDeltaRecord, StackModels.DeploymentStacksWhatIfPropertyChange]], parent_path: t.Optional[str] = None ) -> bool: - if not object_change or not object_change.delta: + if not object_change: + return False + + children = object_change.delta if hasattr(object_change, "delta") else ( + object_change.children if hasattr(object_change, "children") else None) + + if not children or len(children) == 0: return False printed = False - for delta in object_change.delta: - if self._format_change(delta, parent_path): + for child in children: + if self._format_change(child, parent_path): printed = True return printed @@ -387,53 +388,64 @@ def _format_array_changes( if not str_lower_eq(array_change.change_type, StackModels.DeploymentStacksWhatIfPropertyChangeType.ARRAY): return False + children = array_change.children + + if not children or len(children) == 0: + return False + property_path = self._get_change_path(array_change, parent_path) symbol, color = self._get_change_type_formatting(StackModels.DeploymentStacksWhatIfPropertyChangeType.MODIFY) self.builder.append_line(f"{symbol} {property_path}: ", color) self._push_indent() - for i, item_change in enumerate(array_change.children or []): - if self._format_array_child_change(item_change) and i < len(array_change.children) - 1: - self.builder.append_line() + print_array_indices = all(c.path for c in children) + sorted_children = sorted( + children, + key=lambda x:int(x.path)) if print_array_indices else children - self._pop_indent() + for i, item_change in enumerate(sorted_children): + if print_array_indices: + self.builder.append_line(f"{item_change.path}:") + self._push_indent() - return True + if self._format_change(item_change, is_array_item=True) and i < len(array_change.children) - 1: + self.builder.ensure_num_new_lines(1) + if print_array_indices: + self._pop_indent() - def _format_array_child_change(self, array_change: StackModels.DeploymentStacksWhatIfPropertyChange) -> bool: - symbol, color = self._get_change_type_formatting(array_change.change_type) - - # TODO(kylealbert): handle non-primitive - if str_lower_eq(array_change.change_type, StackModels.DeploymentStacksWhatIfPropertyChangeType.CREATE) or \ - str_lower_eq(array_change.change_type, StackModels.DeploymentStacksWhatIfPropertyChangeType.NO_EFFECT): - self.builder.append(f"{symbol} {self._format_primitive_value(array_change.after)}", color) - return True - if str_lower_eq(array_change.change_type, StackModels.DeploymentStacksWhatIfPropertyChangeType.DELETE): - self.builder.append(f"{symbol} {self._format_primitive_value(array_change.before)}", color) - return True + self._pop_indent() - return False + return True def _format_primitive_change( self, primitive_change: t.Optional[ t.Union[StackModels.DeploymentStacksChangeBase, StackModels.DeploymentStacksWhatIfPropertyChange]], - parent_path: t.Optional[str] = None + parent_path: t.Optional[str] = None, + is_array_item: bool = False ) -> bool: if not primitive_change: return False - property_path = self._get_change_path(primitive_change, parent_path) - symbol, color = self._get_change_type_formatting( - StackModels.DeploymentStacksWhatIfPropertyChangeType.NO_EFFECT if primitive_change.before == primitive_change.after \ - else StackModels.DeploymentStacksWhatIfPropertyChangeType.MODIFY) + change_type = (primitive_change.change_type if hasattr(primitive_change, "change_type") else None) or ( + StackModels.DeploymentStacksWhatIfPropertyChangeType.NO_EFFECT if primitive_change.before == primitive_change.after + else StackModels.DeploymentStacksWhatIfPropertyChangeType.MODIFY) - self.builder.append(f"{symbol} {property_path}: ", color) - self.builder.append_line( - f"{self._format_primitive_value(primitive_change.before)} => {self._format_primitive_value(primitive_change.after)}") + property_path = self._get_change_path(primitive_change, parent_path) + symbol, color = self._get_change_type_formatting(change_type) + + self.builder.append(f"{symbol} " if is_array_item else f"{symbol} {property_path}: ", color) + if str_lower_eq(change_type, StackModels.DeploymentStacksWhatIfPropertyChangeType.MODIFY): + self.builder.append_line( + f"{self._format_primitive_value(primitive_change.before)} => {self._format_primitive_value(primitive_change.after)}") + else: + value = primitive_change.before if str_lower_eq( + change_type, StackModels.DeploymentStacksWhatIfPropertyChangeType.DELETE) else primitive_change.after + self.builder.append_line( + self._format_primitive_value(value), color if is_array_item else None) return True @@ -456,8 +468,11 @@ def _format_primitive_value(value: t.Optional[t.Union[str, bool, int, float]]): @staticmethod def _get_change_type_formatting( change_type: t.Union[ - StackModels.DeploymentStacksWhatIfChangeType, StackModels.DeploymentStacksWhatIfPropertyChangeType] + StackModels.DeploymentStacksWhatIfChangeType, StackModels.DeploymentStacksWhatIfPropertyChangeType, str] ) -> t.Tuple[t.Optional[str], t.Optional[Color]]: + if change_type is None: + return None, None + symbol = DeploymentStacksWhatIfResultFormatter.CHANGE_TYPE_SYMBOLS.get(change_type, None) color = DeploymentStacksWhatIfResultFormatter.CHANGE_TYPE_COLORS.get(change_type, None) @@ -481,6 +496,8 @@ def _get_value_type_from_change( if hasattr(change, "change_type"): if str_lower_eq(StackModels.DeploymentStacksWhatIfPropertyChangeType.ARRAY, change.change_type): return list + if hasattr(change, "children") and change.children and len(change.children) > 0: + return dict elif hasattr(change, "delta"): return dict @@ -495,3 +512,16 @@ def _get_value_type_from_change( return before_type return None + + @staticmethod + def _get_num_potential_resource_changes( + resource_changes: t.List[StackModels.DeploymentStacksWhatIfResourceChange], start_index: int + ) -> int: + count = 0 + for i in range(start_index, len(resource_changes)): + if str_lower_eq( + resource_changes[i].change_certainty, StackModels.DeploymentStacksWhatIfChangeCertainty.POTENTIAL): + count += 1 + else: + break + return count From ddc1813082b4c4fb91827e12255ff635a14f3050 Mon Sep 17 00:00:00 2001 From: Kyle Albert <5498623+kalbert312@users.noreply.github.com> Date: Thu, 26 Feb 2026 16:22:04 -0500 Subject: [PATCH 19/32] Array children rendering fixes and misc refactors. --- .../resource/_stacks_formatters.py | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py b/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py index f6f8f9bd784..033c4550f1d 100644 --- a/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py +++ b/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py @@ -198,12 +198,10 @@ def _format_resource_changes( last_group = group has_potential_changes = False - if str_lower_eq(change.change_certainty, StackModels.DeploymentStacksWhatIfChangeCertainty.POTENTIAL): - if not has_potential_changes: - self.builder.append(">> ").append_line( - "Potential Resource Changes (Learn more at https://aka.ms/whatIfPotentialChanges)", - Color.PURPLE) - + if not has_potential_changes and str_lower_eq(change.change_certainty, StackModels.DeploymentStacksWhatIfChangeCertainty.POTENTIAL): + self.builder.append(">> ").append_line( + "Potential Resource Changes (Learn more at https://aka.ms/whatIfPotentialChanges)", + Color.PURPLE) has_potential_changes = True self._format_resource_change(change) @@ -261,14 +259,11 @@ def _format_resource_deletions_summary( last_group = group has_potential_deletions = False - if str_lower_eq( + if not has_potential_deletions and str_lower_eq( delete_change.change_certainty, StackModels.DeploymentStacksWhatIfChangeCertainty.POTENTIAL): - - if not has_potential_deletions: - self.builder.append(">> ").append_line( - f"Potential Deletions {self._get_num_potential_resource_changes(delete_changes, i)} total (Learn more at https://aka.ms/whatIfPotentialChanges)", - Color.RED) - + self.builder.append(">> ").append_line( + f"Potential Deletions {self._get_num_potential_resource_changes(delete_changes, i)} total (Learn more at https://aka.ms/whatIfPotentialChanges)", + Color.RED) has_potential_deletions = True self._format_resource_heading_line(delete_change) @@ -406,7 +401,8 @@ def _format_array_changes( for i, item_change in enumerate(sorted_children): if print_array_indices: - self.builder.append_line(f"{item_change.path}:") + child_symbol, child_color = self._get_change_type_formatting(item_change.change_type) + self.builder.append(child_symbol, child_color).append_line(f" {item_change.path}:") self._push_indent() if self._format_change(item_change, is_array_item=True) and i < len(array_change.children) - 1: From 168367340981cf6c9fbccf2ec160d2cbb0f36948 Mon Sep 17 00:00:00 2001 From: Kyle Albert <5498623+kalbert312@users.noreply.github.com> Date: Fri, 27 Feb 2026 11:13:40 -0500 Subject: [PATCH 20/32] Remove redundant "change" label. --- .../command_modules/resource/_stacks_formatters.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py b/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py index 033c4550f1d..97a62906867 100644 --- a/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py +++ b/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py @@ -190,13 +190,15 @@ def _format_resource_changes( for change in resource_changes_sorted: # check if a new section should be started group = "Azure" if change.id else ( - f"{change.extension.name}@{change.extension.version}" if change.extension else "Unknown") + f"{change.extension.name}@{change.extension.version}" if change.extension and not change.extension.config else ( + f"{change.extension.name}@{change.extension.version} Config: {json.dumps(change.extension.config)}" if change.extension and change.extension.config else "Unknown" + )) if group != last_group: - self._format_section_spacer() - self.builder.append_line(group) last_group = group has_potential_changes = False + self._format_section_spacer() + self.builder.append_line(group) if not has_potential_changes and str_lower_eq(change.change_certainty, StackModels.DeploymentStacksWhatIfChangeCertainty.POTENTIAL): self.builder.append(">> ").append_line( @@ -216,8 +218,8 @@ def _format_resource_change(self, resource_change: StackModels.DeploymentStacksW # print stack management related changes self._push_indent() all_resource_changes = { - "Management Status Change": resource_change.management_status_change, - "Deny Status Change": resource_change.deny_status_change, + "Management Status": resource_change.management_status_change, + "Deny Status": resource_change.deny_status_change, } for path, change in all_resource_changes.items(): From 08709613f8ddd994889311b5c7c81df3040e47a5 Mon Sep 17 00:00:00 2001 From: Kyle Albert <5498623+kalbert312@users.noreply.github.com> Date: Fri, 27 Feb 2026 11:31:46 -0500 Subject: [PATCH 21/32] Add exclusions for wait command + rg. Is consistent with stack commands. --- .../resource/linter_exclusions.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/azure-cli/azure/cli/command_modules/resource/linter_exclusions.yml b/src/azure-cli/azure/cli/command_modules/resource/linter_exclusions.yml index 363c0602902..148f0235c62 100644 --- a/src/azure-cli/azure/cli/command_modules/resource/linter_exclusions.yml +++ b/src/azure-cli/azure/cli/command_modules/resource/linter_exclusions.yml @@ -27,3 +27,21 @@ stack sub validate: stack mg: rule_exclusions: - require_wait_command_if_no_wait + +stack-whatif group: + rule_exclusions: + - require_wait_command_if_no_wait + +stack-whatif sub: + rule_exclusions: + - require_wait_command_if_no_wait + +stack-whatif sub create: + parameters: + deployment_resource_group: + rule_exclusions: + - parameter_should_not_end_in_resource_group + +stack-whatif mg: + rule_exclusions: + - require_wait_command_if_no_wait From 8a460a70641006296c941a3bbf1986edd54b44af Mon Sep 17 00:00:00 2001 From: Kyle Albert <5498623+kalbert312@users.noreply.github.com> Date: Fri, 27 Feb 2026 12:35:23 -0500 Subject: [PATCH 22/32] Fix lint errors. Add resource class formatting method. --- .../cli/command_modules/resource/_color.py | 6 +- .../resource/_stacks_formatters.py | 64 +++++++++---------- .../cli/command_modules/resource/_utils.py | 2 + .../cli/command_modules/resource/custom.py | 4 +- 4 files changed, 37 insertions(+), 39 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/resource/_color.py b/src/azure-cli/azure/cli/command_modules/resource/_color.py index bd5c164cf27..24e747d353e 100644 --- a/src/azure-cli/azure/cli/command_modules/resource/_color.py +++ b/src/azure-cli/azure/cli/command_modules/resource/_color.py @@ -92,7 +92,6 @@ def ensure_num_new_lines(self, num_new_lines): if remaining_newlines > 0: self._contents.append("\n" * remaining_newlines) - def clear(self): self._contents = [] @@ -109,9 +108,10 @@ def _pop_color(self): self._colors.pop() self._contents.append(str(self._colors[-1] if self._colors else Color.RESET)) - + def _should_indent(self, index=-1, is_insert=False): - return len(self._indents) > 0 and (not self._contents or self._contents[max(index - 1, 0) if is_insert else index].endswith("\n")) + return len(self._indents) > 0 and ( + not self._contents or self._contents[max(index - 1, 0) if is_insert else index].endswith("\n")) # pylint: disable=protected-access class ColorScope: diff --git a/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py b/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py index 97a62906867..5e2d0d15f23 100644 --- a/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py +++ b/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py @@ -74,7 +74,6 @@ def __init__(self, enable_color=True): self.what_if_props: t.Optional[StackModels.DeploymentStacksWhatIfResultProperties] = None self.what_if_changes: t.Optional[StackModels.DeploymentStacksWhatIfChange] = None - def format(self, what_if_result: StackModels.DeploymentStacksWhatIfResult) -> str: self.builder.clear() @@ -95,11 +94,9 @@ def format(self, what_if_result: StackModels.DeploymentStacksWhatIfResult) -> st return result - def _format_section_spacer(self): self.builder.ensure_num_new_lines(2) - def _format_change_type_legend(self) -> bool: change_type_max_length = 20 @@ -122,7 +119,6 @@ def _format_change_type_legend(self) -> bool: return True - def _format_stack_changes(self) -> bool: if not self.what_if_changes: return False @@ -144,7 +140,6 @@ def _format_stack_changes(self) -> bool: return printed - def _format_resource_changes_and_deletion_summary(self) -> bool: if not self.what_if_changes or not self.what_if_changes.resource_changes: return False @@ -174,7 +169,6 @@ def _format_resource_changes_and_deletion_summary(self) -> bool: return printed - def _format_resource_changes( self, resource_changes_sorted: list[StackModels.DeploymentStacksWhatIfResourceChange] ) -> bool: @@ -189,10 +183,7 @@ def _format_resource_changes( for change in resource_changes_sorted: # check if a new section should be started - group = "Azure" if change.id else ( - f"{change.extension.name}@{change.extension.version}" if change.extension and not change.extension.config else ( - f"{change.extension.name}@{change.extension.version} Config: {json.dumps(change.extension.config)}" if change.extension and change.extension.config else "Unknown" - )) + group = self._format_resource_class_header(change) if group != last_group: last_group = group @@ -200,7 +191,8 @@ def _format_resource_changes( self._format_section_spacer() self.builder.append_line(group) - if not has_potential_changes and str_lower_eq(change.change_certainty, StackModels.DeploymentStacksWhatIfChangeCertainty.POTENTIAL): + if not has_potential_changes and str_lower_eq( + change.change_certainty, StackModels.DeploymentStacksWhatIfChangeCertainty.POTENTIAL): self.builder.append(">> ").append_line( "Potential Resource Changes (Learn more at https://aka.ms/whatIfPotentialChanges)", Color.PURPLE) @@ -210,7 +202,6 @@ def _format_resource_changes( return True - def _format_resource_change(self, resource_change: StackModels.DeploymentStacksWhatIfResourceChange) -> bool: # print the resource heading line self._format_resource_heading_line(resource_change) @@ -231,7 +222,6 @@ def _format_resource_change(self, resource_change: StackModels.DeploymentStacksW return True - def _format_resource_deletions_summary( self, resource_changes_sorted: list[StackModels.DeploymentStacksWhatIfResourceChange] ) -> bool: @@ -252,8 +242,7 @@ def _format_resource_deletions_summary( has_potential_deletions = False for i, delete_change in enumerate(delete_changes): - group = "Azure" if delete_change.id else ( - f"{delete_change.extension.name}@{delete_change.extension.version}" if delete_change.extension else "Unknown") + group = self._format_resource_class_header(delete_change) if group != last_group: self._format_section_spacer() @@ -262,7 +251,7 @@ def _format_resource_deletions_summary( has_potential_deletions = False if not has_potential_deletions and str_lower_eq( - delete_change.change_certainty, StackModels.DeploymentStacksWhatIfChangeCertainty.POTENTIAL): + delete_change.change_certainty, StackModels.DeploymentStacksWhatIfChangeCertainty.POTENTIAL): self.builder.append(">> ").append_line( f"Potential Deletions {self._get_num_potential_resource_changes(delete_changes, i)} total (Learn more at https://aka.ms/whatIfPotentialChanges)", Color.RED) @@ -272,7 +261,6 @@ def _format_resource_deletions_summary( return printed - def _format_resource_heading_line(self, resource_change: StackModels.DeploymentStacksWhatIfResourceChange): symbol, color = self._get_change_type_formatting(resource_change.change_type) @@ -290,7 +278,6 @@ def _format_resource_heading_line(self, resource_change: StackModels.DeploymentS resource_id = resource_change.id if resource_change.id else f"{resource_change.type} {json.dumps(resource_change.identifiers)}" self.builder.append_line(f"{resource_id}{api_version_suffix}", color) - def _format_resource_property_changes( self, property_changes: t.Optional[StackModels.DeploymentStacksChangeDeltaRecord] ) -> bool: @@ -305,7 +292,6 @@ def _format_resource_property_changes( return printed - def _format_diagnostics(self) -> bool: if not self.what_if_props or not self.what_if_props.diagnostics or len(self.what_if_props.diagnostics) == 0: return False @@ -321,13 +307,11 @@ def _format_diagnostics(self) -> bool: return True - def _format_diagnostic(self, diagnostic: StackModels.DeploymentStacksDiagnostic): self.builder.append_line( f"{diagnostic.level.upper()}: [{diagnostic.code}] {diagnostic.message}", DeploymentStacksWhatIfResultFormatter.DIAGNOSTIC_COLORS.get(diagnostic.level, None)) - def _format_change( self, change: t.Optional[t.Union[ @@ -354,7 +338,6 @@ def _format_change( return False - def _format_object_change( self, object_change: t.Optional[t.Union[ @@ -378,7 +361,6 @@ def _format_object_change( return printed - def _format_array_changes( self, array_change: StackModels.DeploymentStacksWhatIfPropertyChange, parent_path: t.Optional[str] = None ) -> bool: @@ -399,7 +381,7 @@ def _format_array_changes( print_array_indices = all(c.path for c in children) sorted_children = sorted( children, - key=lambda x:int(x.path)) if print_array_indices else children + key=lambda x: int(x.path)) if print_array_indices else children for i, item_change in enumerate(sorted_children): if print_array_indices: @@ -417,7 +399,6 @@ def _format_array_changes( return True - def _format_primitive_change( self, primitive_change: t.Optional[ @@ -447,22 +428,18 @@ def _format_primitive_change( return True - def _push_indent(self, indent_size=INDENT_SIZE): self.builder.push_indent(" " * indent_size) - def _pop_indent(self): self.builder.pop_indent() - @staticmethod def _format_primitive_value(value: t.Optional[t.Union[str, bool, int, float]]): if value is None: return "null" return f'"{value}"' if isinstance(value, str) else str(value) - @staticmethod def _get_change_type_formatting( change_type: t.Union[ @@ -476,14 +453,12 @@ def _get_change_type_formatting( return symbol, color - @staticmethod def _get_change_path(change, parent_path: t.Optional[str] = None) -> str: if hasattr(change, "path"): return '.'.join([parent_path, change.path]) if parent_path else change.path return parent_path - @staticmethod def _get_value_type_from_change( change: t.Union[ @@ -510,16 +485,37 @@ def _get_value_type_from_change( return before_type return None - + @staticmethod def _get_num_potential_resource_changes( resource_changes: t.List[StackModels.DeploymentStacksWhatIfResourceChange], start_index: int ) -> int: count = 0 for i in range(start_index, len(resource_changes)): - if str_lower_eq( - resource_changes[i].change_certainty, StackModels.DeploymentStacksWhatIfChangeCertainty.POTENTIAL): + if str_lower_eq(resource_changes[i].change_certainty, StackModels.DeploymentStacksWhatIfChangeCertainty.POTENTIAL): count += 1 else: break return count + + @staticmethod + def _format_resource_class_header(change: StackModels.DeploymentStacksWhatIfResourceChange) -> str: + if change.id: + return "Azure" + + result = "Unknown" + if change.extension: + result = f"{change.extension.name}@{change.extension.version}" + + if change.extension.config: + config_items = sorted( + change.extension.config.items(), + key=lambda ci: ci[0]) + + if len(config_items) > 0: + for prop, item in change.extension.config.items(): + result += f" {prop}={item}\n" + + result = result.rstrip("\n") + + return result diff --git a/src/azure-cli/azure/cli/command_modules/resource/_utils.py b/src/azure-cli/azure/cli/command_modules/resource/_utils.py index 5a0a769133b..fcffeea9fb8 100644 --- a/src/azure-cli/azure/cli/command_modules/resource/_utils.py +++ b/src/azure-cli/azure/cli/command_modules/resource/_utils.py @@ -14,9 +14,11 @@ _resource_group_pattern = r"^\/resourceGroups\/(?P[-\w\._\(\)]+)" _relative_resource_id_pattern = r"^\/providers/(?P.+$)" + def str_lower_eq(str1: t.Optional[str], str2: t.Optional[str]): return str1.lower() == str2.lower() if str1 and str2 else False + def split_resource_id(resource_id): """Splits a fully qualified resource ID into two parts. diff --git a/src/azure-cli/azure/cli/command_modules/resource/custom.py b/src/azure-cli/azure/cli/command_modules/resource/custom.py index f6fd5ac50cd..41b87080f30 100644 --- a/src/azure-cli/azure/cli/command_modules/resource/custom.py +++ b/src/azure-cli/azure/cli/command_modules/resource/custom.py @@ -3119,11 +3119,11 @@ def create_deployment_stack_what_if_at_management_group( return _print_deployment_stack_what_if_result(what_if_result, no_pretty_print, no_color) -def show_deployment_stack_what_if_at_management_group(cmd, management_group_id, name=None, id=None, no_pretty_print=None, no_color=None): +def show_deployment_stack_what_if_at_management_group(cmd, management_group_id, name=None, id=None, no_pretty_print=None, no_color=None): # pylint: disable=redefined-builtin rcf = _resource_deploymentstacks_client_factory(cmd.cli_ctx) if name: - what_if_result = rcf.deployment_stacks_what_if_results_at_management_group.get(management_group_id, name) + what_if_result = rcf.deployment_stacks_what_if_results_at_management_group.get(management_group_id, name) elif id: what_if_result = rcf.deployment_stacks_what_if_results_at_management_group.get(management_group_id, id.split('/')[-1]) else: From 679778c050f96012686eeef7a8374b17248eaefa Mon Sep 17 00:00:00 2001 From: Kyle Albert <5498623+kalbert312@users.noreply.github.com> Date: Fri, 27 Feb 2026 13:53:22 -0500 Subject: [PATCH 23/32] Update extensible resource display. --- .../resource/_stacks_formatters.py | 37 +++++++++++++++---- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py b/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py index 5e2d0d15f23..884c503a9ac 100644 --- a/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py +++ b/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py @@ -151,15 +151,15 @@ def _format_resource_changes_and_deletion_summary(self) -> bool: 0 if x.id else 1, # sort Azure resources before extension resources DeploymentStacksWhatIfResultFormatter.CHANGE_CERTAINTY_PRIORITIES.get( x.change_certainty, 1) if x.id else 0, # Azure resources: then by certainty - x.id if x.id else "", # Azure resources: then by ID - x.extension.name if x.extension else "", + x.id.lower() if x.id else "", # Azure resources: then by ID # Extension resources: then by (ext name, ext version, config id) + x.extension.name if x.extension else "", x.extension.version if x.extension else "", (x.extension.config_id if x.extension else "") or "", DeploymentStacksWhatIfResultFormatter.CHANGE_CERTAINTY_PRIORITIES.get( x.change_certainty, 1) if not x.id else 0, # Extension resources: then by certainty x.type if x.extension else "", # Extension resources: then by type - json.dumps(x.identifiers) if x.extension else "" # Extension resources: then by identifiers + self._format_ext_resource_identifiers(x.identifiers) if x.identifiers else "" # Extension resources: then by identifiers )) if self._format_resource_changes(resource_changes_sorted): @@ -275,7 +275,8 @@ def _format_resource_heading_line(self, resource_change: StackModels.DeploymentS self.builder.append("[Potential] ", Color.CYAN) api_version_suffix = f" [{resource_change.api_version}]" if resource_change.api_version else "" - resource_id = resource_change.id if resource_change.id else f"{resource_change.type} {json.dumps(resource_change.identifiers)}" + resource_id = resource_change.id if resource_change.id else\ + f"{resource_change.type} {self._format_ext_resource_identifiers(resource_change.identifiers)}" self.builder.append_line(f"{resource_id}{api_version_suffix}", color) def _format_resource_property_changes( @@ -508,14 +509,34 @@ def _format_resource_class_header(change: StackModels.DeploymentStacksWhatIfReso result = f"{change.extension.name}@{change.extension.version}" if change.extension.config: + # Print the config. Eventually this can be substituted with an optional user-provided "comparison ID" + # for brevity. config_items = sorted( change.extension.config.items(), - key=lambda ci: ci[0]) + key=lambda ci: ((ci[1] or {}).get('keyVaultReference', None) is not None, ci[0])) if len(config_items) > 0: - for prop, item in change.extension.config.items(): - result += f" {prop}={item}\n" + config_parts = [] + + for prop, item in config_items: + if not item: + continue + + if item.get('keyVaultReference', None): + secret_name = item['keyVaultReference'].get('secretName', None) + secret_version = item['keyVaultReference'].get('secretVersion', None) + kv_id = item['keyVaultReference'].get('keyVault', {}).get('id', None) + version_suffix = f"@{secret_version}" if secret_version else "" + config_parts.append(f"{prop}=") + else: + config_parts.append(f"{prop}={json.dumps(item.get('value', None))}") - result = result.rstrip("\n") + result += f" {', '.join(config_parts)}" return result + + @staticmethod + def _format_ext_resource_identifiers(identifiers: dict[str, t.Any]) -> str: + sorted_items = sorted(identifiers.items(), key=lambda x: x[0]) + + return ", ".join(f"{key}={json.dumps(value)}" for key, value in sorted_items) From a3b1e9157c7ddefb800821e402d915fba96a8c62 Mon Sep 17 00:00:00 2001 From: Kyle Albert <5498623+kalbert312@users.noreply.github.com> Date: Fri, 27 Feb 2026 14:43:41 -0500 Subject: [PATCH 24/32] Lint fixes. --- .../cli/command_modules/resource/_color.py | 2 +- .../resource/_stacks_formatters.py | 31 ++++++++++++------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/resource/_color.py b/src/azure-cli/azure/cli/command_modules/resource/_color.py index 24e747d353e..b14eb162525 100644 --- a/src/azure-cli/azure/cli/command_modules/resource/_color.py +++ b/src/azure-cli/azure/cli/command_modules/resource/_color.py @@ -111,7 +111,7 @@ def _pop_color(self): def _should_indent(self, index=-1, is_insert=False): return len(self._indents) > 0 and ( - not self._contents or self._contents[max(index - 1, 0) if is_insert else index].endswith("\n")) + not self._contents or self._contents[max(index - 1, 0) if is_insert else index].endswith("\n")) # pylint: disable=protected-access class ColorScope: diff --git a/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py b/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py index 884c503a9ac..cdc1f642a12 100644 --- a/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py +++ b/src/azure-cli/azure/cli/command_modules/resource/_stacks_formatters.py @@ -159,7 +159,8 @@ def _format_resource_changes_and_deletion_summary(self) -> bool: DeploymentStacksWhatIfResultFormatter.CHANGE_CERTAINTY_PRIORITIES.get( x.change_certainty, 1) if not x.id else 0, # Extension resources: then by certainty x.type if x.extension else "", # Extension resources: then by type - self._format_ext_resource_identifiers(x.identifiers) if x.identifiers else "" # Extension resources: then by identifiers + # Extension resources: then by identifiers + self._format_ext_resource_identifiers(x.identifiers) if x.identifiers else "" )) if self._format_resource_changes(resource_changes_sorted): @@ -253,7 +254,8 @@ def _format_resource_deletions_summary( if not has_potential_deletions and str_lower_eq( delete_change.change_certainty, StackModels.DeploymentStacksWhatIfChangeCertainty.POTENTIAL): self.builder.append(">> ").append_line( - f"Potential Deletions {self._get_num_potential_resource_changes(delete_changes, i)} total (Learn more at https://aka.ms/whatIfPotentialChanges)", + f"Potential Deletions {self._get_num_potential_resource_changes(delete_changes, i)} total" + " (Learn more at https://aka.ms/whatIfPotentialChanges)", Color.RED) has_potential_deletions = True @@ -299,11 +301,13 @@ def _format_diagnostics(self) -> bool: diagnostics_sorted = sorted( self.what_if_props.diagnostics, - key=lambda x: (DeploymentStacksWhatIfResultFormatter.DIAGNOSTIC_LEVEL_PRIORITIES.get(x.level, 0), x.code or "")) + key=lambda x: ( + DeploymentStacksWhatIfResultFormatter.DIAGNOSTIC_LEVEL_PRIORITIES.get(x.level, 0), + x.code or "")) self.builder.append_line(f"Diagnostics ({len(diagnostics_sorted)}):") - for i, diagnostic in enumerate(diagnostics_sorted): + for diagnostic in diagnostics_sorted: self._format_diagnostic(diagnostic) return True @@ -410,9 +414,10 @@ def _format_primitive_change( if not primitive_change: return False - change_type = (primitive_change.change_type if hasattr(primitive_change, "change_type") else None) or ( - StackModels.DeploymentStacksWhatIfPropertyChangeType.NO_EFFECT if primitive_change.before == primitive_change.after - else StackModels.DeploymentStacksWhatIfPropertyChangeType.MODIFY) + change_type = primitive_change.change_type if hasattr(primitive_change, "change_type") else None + change_type = (change_type or (StackModels.DeploymentStacksWhatIfPropertyChangeType.NO_EFFECT + if primitive_change.before == primitive_change.after + else StackModels.DeploymentStacksWhatIfPropertyChangeType.MODIFY)) property_path = self._get_change_path(primitive_change, parent_path) symbol, color = self._get_change_type_formatting(change_type) @@ -420,7 +425,8 @@ def _format_primitive_change( self.builder.append(f"{symbol} " if is_array_item else f"{symbol} {property_path}: ", color) if str_lower_eq(change_type, StackModels.DeploymentStacksWhatIfPropertyChangeType.MODIFY): self.builder.append_line( - f"{self._format_primitive_value(primitive_change.before)} => {self._format_primitive_value(primitive_change.after)}") + f"{self._format_primitive_value(primitive_change.before)}" + f" => {self._format_primitive_value(primitive_change.after)}") else: value = primitive_change.before if str_lower_eq( change_type, StackModels.DeploymentStacksWhatIfPropertyChangeType.DELETE) else primitive_change.after @@ -461,7 +467,7 @@ def _get_change_path(change, parent_path: t.Optional[str] = None) -> str: return parent_path @staticmethod - def _get_value_type_from_change( + def _get_value_type_from_change( # pylint: disable=too-many-return-statements change: t.Union[ StackModels.DeploymentStacksChangeBase, StackModels.DeploymentStacksChangeDeltaRecord, @@ -493,7 +499,8 @@ def _get_num_potential_resource_changes( ) -> int: count = 0 for i in range(start_index, len(resource_changes)): - if str_lower_eq(resource_changes[i].change_certainty, StackModels.DeploymentStacksWhatIfChangeCertainty.POTENTIAL): + if str_lower_eq( + resource_changes[i].change_certainty, StackModels.DeploymentStacksWhatIfChangeCertainty.POTENTIAL): count += 1 else: break @@ -527,7 +534,9 @@ def _format_resource_class_header(change: StackModels.DeploymentStacksWhatIfReso secret_version = item['keyVaultReference'].get('secretVersion', None) kv_id = item['keyVaultReference'].get('keyVault', {}).get('id', None) version_suffix = f"@{secret_version}" if secret_version else "" - config_parts.append(f"{prop}=") + + config_parts.append( + f"{prop}=") else: config_parts.append(f"{prop}={json.dumps(item.get('value', None))}") From 11a4e6888257e809d1e8f7391d02cea55b5e7c64 Mon Sep 17 00:00:00 2001 From: Kyle Albert <5498623+kalbert312@users.noreply.github.com> Date: Mon, 2 Mar 2026 14:12:42 -0500 Subject: [PATCH 25/32] Add unit test for stacks what-if formatter. --- .../latest/data/stacks-what-if/what-if-1.json | 498 ++++++++++++++++++ .../tests/latest/test_stack_formatters.py | 115 ++++ 2 files changed, 613 insertions(+) create mode 100644 src/azure-cli/azure/cli/command_modules/resource/tests/latest/data/stacks-what-if/what-if-1.json create mode 100644 src/azure-cli/azure/cli/command_modules/resource/tests/latest/test_stack_formatters.py diff --git a/src/azure-cli/azure/cli/command_modules/resource/tests/latest/data/stacks-what-if/what-if-1.json b/src/azure-cli/azure/cli/command_modules/resource/tests/latest/data/stacks-what-if/what-if-1.json new file mode 100644 index 00000000000..bacc5457869 --- /dev/null +++ b/src/azure-cli/azure/cli/command_modules/resource/tests/latest/data/stacks-what-if/what-if-1.json @@ -0,0 +1,498 @@ +{ + "properties": { + "deploymentStackResourceId": "/subscriptions/6d41d86d-eb6b-473a-b31d-bbd084e1814d/resourceGroups/503ace4c-9b1c-4059-a3e9-09553d24e9e1/providers/Microsoft.Resources/deploymentStacks/testStack_9ef16884f0dad7d0e5de3d3ec57", + "retentionInterval": "P1D", + "provisioningState": "Succeeded", + "deploymentStackLastModified": "2026-02-23T16:44:05+00:00", + "deploymentExtensions": [ + { + "name": "Contoso", + "version": "2.0.0" + }, + { + "name": "Kubernetes", + "version": "2.0.0", + "configId": "kubeConfigHash", + "config": { + "kubeconfig": { + "keyVaultReference": { + "keyVault": { + "id": "/subscriptions/6d41d86d-eb6b-473a-b31d-bbd084e1814d/resourceGroups/myRg/providers/Microsoft.KeyVault/vaults/kubeConfigVault" + }, + "secretName": "kubeConfigSecretName" + } + }, + "namespace": { + "value": "default" + } + } + } + ], + "changes": { + "resourceChanges": [ + { + "id": "/subscriptions/6d41d86d-eb6b-473a-b31d-bbd084e1814d/resourceGroups/503ace4c-9b1c-4059-a3e9-09553d24e9e1/providers/Microsoft.Test/testA/resourceA", + "type": "Microsoft.Test/testA", + "managementStatusChange": { + "before": "Managed", + "after": "Managed" + }, + "denyStatusChange": { + "before": "None", + "after": "DenyDelete" + }, + "resourceConfigurationChanges": { + "before": { + "apiVersion": "2021-05-01", + "id": "/subscriptions/6d41d86d-eb6b-473a-b31d-bbd084e1814d/resourceGroups/503ace4c-9b1c-4059-a3e9-09553d24e9e1/providers/Microsoft.Test/testA/resourceA", + "location": "westus", + "name": "resourceA", + "type": "Microsoft.Test/testA", + "properties": { + "property1": "resourceA-before" + } + }, + "after": { + "apiVersion": "2021-05-01", + "id": "/subscriptions/6d41d86d-eb6b-473a-b31d-bbd084e1814d/resourceGroups/503ace4c-9b1c-4059-a3e9-09553d24e9e1/providers/Microsoft.Test/testA/resourceA", + "location": "westus", + "name": "resourceA", + "type": "Microsoft.Test/testA", + "properties": { + "property1": "resourceA-after" + } + }, + "delta": [ + { + "path": "properties.properties1", + "changeType": "Modify", + "before": "resourceA-before", + "after": "resourceA-after", + "children": [] + } + ] + }, + "changeType": "Modify", + "changeCertainty": "Definite" + }, + { + "changeType": "Delete", + "changeCertainty": "Definite", + "extension": { + "name": "Contoso", + "version": "2.0.0" + }, + "type": "Contoso/example", + "identifiers": { + "name": "defResource" + }, + "managementStatusChange": { + "before": null, + "after": "Managed" + }, + "denyStatusChange": { + "before": null, + "after": "NotApplicable" + }, + "resourceConfigurationChanges": { + "before": { + "apiVersion": "2021-05-01", + "id": "/subscriptions/6d41d86d-eb6b-473a-b31d-bbd084e1814d/resourceGroups/503ace4c-9b1c-4059-a3e9-09553d24e9e1/providers/Microsoft.Test/testA/resourceA", + "location": "westus", + "name": "resourceA", + "type": "Microsoft.Test/testA", + "properties": { + "property1": "resourceA-before" + } + }, + "after": { + "apiVersion": "2021-05-01", + "id": "/subscriptions/6d41d86d-eb6b-473a-b31d-bbd084e1814d/resourceGroups/503ace4c-9b1c-4059-a3e9-09553d24e9e1/providers/Microsoft.Test/testA/resourceA", + "location": "westus", + "name": "resourceA", + "type": "Microsoft.Test/testA", + "properties": { + "property1": "resourceA-after" + } + }, + "delta": [ + { + "path": "properties.properties1", + "changeType": "Modify", + "before": "resourceA-before", + "after": "resourceA-after", + "children": [] + } + ] + } + }, + { + "changeType": "Modify", + "changeCertainty": "Definite", + "extension": { + "name": "Contoso", + "version": "2.0.0" + }, + "type": "Contoso/example", + "identifiers": { + "name": "abcResource" + }, + "managementStatusChange": { + "before": null, + "after": "Managed" + }, + "denyStatusChange": { + "before": null, + "after": "NotApplicable" + }, + "resourceConfigurationChanges": { + "before": { + "apiVersion": "2021-05-01", + "id": "/subscriptions/6d41d86d-eb6b-473a-b31d-bbd084e1814d/resourceGroups/503ace4c-9b1c-4059-a3e9-09553d24e9e1/providers/Microsoft.Test/testA/resourceA", + "location": "westus", + "name": "resourceA", + "type": "Microsoft.Test/testA", + "properties": { + "property1": "resourceA-before" + } + }, + "after": { + "apiVersion": "2021-05-01", + "id": "/subscriptions/6d41d86d-eb6b-473a-b31d-bbd084e1814d/resourceGroups/503ace4c-9b1c-4059-a3e9-09553d24e9e1/providers/Microsoft.Test/testA/resourceA", + "location": "westus", + "name": "resourceA", + "type": "Microsoft.Test/testA", + "properties": { + "property1": "resourceA-after" + } + }, + "delta": [ + { + "path": "properties.properties1", + "changeType": "Modify", + "before": "resourceA-before", + "after": "resourceA-after", + "children": [] + } + ] + } + }, + { + "id": "/subscriptions/6d41d86d-eb6b-473a-b31d-bbd084e1814d/resourceGroups/503ace4c-9b1c-4059-a3e9-09553d24e9e1/providers/Microsoft.Test/testB/resourceB", + "type": "Microsoft.Test/testB", + "managementStatusChange": { + "before": "Managed", + "after": "Managed" + }, + "denyStatusChange": { + "before": "None", + "after": "DenyDelete" + }, + "resourceConfigurationChanges": { + "before": { + "apiVersion": "2021-05-01", + "id": "/subscriptions/6d41d86d-eb6b-473a-b31d-bbd084e1814d/resourceGroups/503ace4c-9b1c-4059-a3e9-09553d24e9e1/providers/Microsoft.Test/testB/resourceB", + "location": "westus", + "name": "resourceB", + "type": "Microsoft.Test/testB", + "properties": { + "property1": "resourceB" + } + }, + "after": { + "apiVersion": "2021-05-01", + "id": "/subscriptions/6d41d86d-eb6b-473a-b31d-bbd084e1814d/resourceGroups/503ace4c-9b1c-4059-a3e9-09553d24e9e1/providers/Microsoft.Test/testB/resourceB", + "location": "westus", + "name": "resourceB", + "type": "Microsoft.Test/testB", + "properties": { + "property1": "resourceB" + } + }, + "delta": [] + }, + "changeType": "NoChange", + "changeCertainty": "Definite" + }, + { + "id": "/subscriptions/6d41d86d-eb6b-473a-b31d-bbd084e1814d/resourceGroups/503ace4c-9b1c-4059-a3e9-09553d24e9e1/providers/Microsoft.Test/testD/resourceD", + "type": "Microsoft.Test/testD", + "managementStatusChange": { + "before": "NotManaged", + "after": "Managed" + }, + "denyStatusChange": { + "before": "None", + "after": "DenyDelete" + }, + "resourceConfigurationChanges": { + "after": { + "apiVersion": "2021-05-01", + "id": "/subscriptions/6d41d86d-eb6b-473a-b31d-bbd084e1814d/resourceGroups/503ace4c-9b1c-4059-a3e9-09553d24e9e1/providers/Microsoft.Test/testD/resourceD", + "location": "westus", + "name": "resourceD", + "type": "Microsoft.Test/testD", + "properties": { + "property1": "resourceD" + } + }, + "delta": [] + }, + "changeType": "Create", + "changeCertainty": "Definite" + }, + { + "id": "/subscriptions/6d41d86d-eb6b-473a-b31d-bbd084e1814d/resourceGroups/503ace4c-9b1c-4059-a3e9-09553d24e9e1/providers/Microsoft.Test/testC/resourceC", + "type": "Microsoft.Test/testC", + "managementStatusChange": { + "before": "Managed", + "after": "Managed" + }, + "denyStatusChange": { + "before": "None", + "after": "DenyDelete" + }, + "resourceConfigurationChanges": { + "before": { + "apiVersion": "2021-05-01", + "id": "/subscriptions/6d41d86d-eb6b-473a-b31d-bbd084e1814d/resourceGroups/503ace4c-9b1c-4059-a3e9-09553d24e9e1/providers/Microsoft.Test/testC/resourceC", + "location": "westus", + "name": "resourceC", + "type": "Microsoft.Test/testC", + "properties": { + "property1": "resourceC-before" + } + }, + "after": { + "apiVersion": "2021-05-01", + "id": "/subscriptions/6d41d86d-eb6b-473a-b31d-bbd084e1814d/resourceGroups/503ace4c-9b1c-4059-a3e9-09553d24e9e1/providers/Microsoft.Test/testC/resourceC", + "location": "westus", + "name": "resourceC", + "type": "Microsoft.Test/testC", + "properties": { + "property1": "resourceC-potential-after" + } + }, + "delta": [ + { + "path": "properties.properties1", + "changeType": "Modify", + "before": "resourceC-before", + "after": "resourceC-potential-after", + "children": [] + } + ] + }, + "changeType": "Modify", + "changeCertainty": "Potential" + }, + { + "id": "/subscriptions/6d41d86d-eb6b-473a-b31d-bbd084e1814d/resourceGroups/503ace4c-9b1c-4059-a3e9-09553d24e9e1/providers/Microsoft.Test/testC/resourceC", + "type": "Microsoft.Test/testC", + "managementStatusChange": { + "before": "Managed", + "after": "NotManaged" + }, + "denyStatusChange": { + "before": "None", + "after": "None" + }, + "changeType": "Delete", + "changeCertainty": "Potential" + }, + { + "changeType": "Create", + "changeCertainty": "Definite", + "extension": { + "name": "Kubernetes", + "version": "2.0.0", + "configId": "kubeConfigHash", + "config": { + "namespace": { + "value": "myNs" + }, + "kubeconfig": { + "keyVaultReference": { + "keyVault": { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myResourceGroup/providers/Microsoft.KeyVault/vaults/myKeyVault" + }, + "secretName": "mySecret" + } + } + } + }, + "type": "app/Deployment", + "identifiers": { + "name": "abcResource" + }, + "managementStatusChange": { + "before": null, + "after": "Managed" + }, + "denyStatusChange": { + "before": null, + "after": "NotApplicable" + }, + "resourceConfigurationChanges": { + "before": { + "apiVersion": "2021-05-01", + "id": "/subscriptions/6d41d86d-eb6b-473a-b31d-bbd084e1814d/resourceGroups/503ace4c-9b1c-4059-a3e9-09553d24e9e1/providers/Microsoft.Test/testA/resourceA", + "location": "westus", + "name": "resourceA", + "type": "Microsoft.Test/testA", + "properties": { + "property1": "resourceA-before" + } + }, + "after": { + "apiVersion": "2021-05-01", + "id": "/subscriptions/6d41d86d-eb6b-473a-b31d-bbd084e1814d/resourceGroups/503ace4c-9b1c-4059-a3e9-09553d24e9e1/providers/Microsoft.Test/testA/resourceA", + "location": "westus", + "name": "resourceA", + "type": "Microsoft.Test/testA", + "properties": { + "property1": "resourceA-after" + } + }, + "delta": [ + { + "path": "properties.properties1", + "changeType": "Modify", + "before": "resourceA-before", + "after": "resourceA-after", + "children": [] + } + ] + } + } + ], + "deploymentScopeChange": { + "before": "ThisIsBefore", + "after": "ThisIsAfter" + }, + "denySettingsChange": { + "before": { + "mode": "None", + "excludedPrincipals": [], + "excludedActions": [], + "applyToChildScopes": false + }, + "after": { + "mode": "DenyDelete", + "excludedPrincipals": [ + "004afc20-146e-4932-a8b5-3098461c46a5", + "e6a513a0-b872-4355-82b9-47645fb30d3a" + ], + "excludedActions": [], + "applyToChildScopes": true + }, + "delta": [ + { + "path": "Mode", + "changeType": "Modify", + "before": "None", + "after": "DenyDelete" + }, + { + "path": "ApplyToChildScopes", + "changeType": "Modify", + "before": "False", + "after": "True" + }, + { + "path": "ExcludedPrincipals", + "changeType": "Array", + "children": [ + { + "changeType": "Create", + "after": "004afc20-146e-4932-a8b5-3098461c46a5" + }, + { + "changeType": "Create", + "after": "e6a513a0-b872-4355-82b9-47645fb30d3a" + } + ] + }, + { + "path": "ArrayOfMixed", + "changeType": "Array", + "children": [ + { + "path": "0", + "changeType": "Modify", + "children": [ + { + "path": "properties.something", + "changeType": "Modify", + "before": "B4", + "after": "Now" + } + ] + }, + { + "path": "1", + "changeType": "Create", + "after": "now", + "before": "b4" + }, + { + "path": "2", + "changeType": "Delete", + "before": "iWasDeleted" + } + ] + } + ] + } + }, + "diagnostics": [ + { + "level": "Warning", + "code": "NoSupportForExtensibleResources", + "message": "Extensible resources are currently not supported" + }, + { + "level": "Warning", + "code": "Abc", + "message": "Xyz" + }, + { + "level": "Info", + "code": "InfoCode", + "message": "InfoMessage" + }, + { + "level": "Error", + "code": "ErrorCode", + "message": "ErrorMessage" + } + ], + "correlationId": "14958d2b-08d9-4b9a-a9ce-7fcfa172fda5", + "actionOnUnmanage": { + "resources": "Delete", + "resourceGroups": "Detach", + "managementGroups": "Detach", + "resourcesWithoutDeleteSupport": "Fail" + }, + "deploymentScope": "/subscriptions/6d41d86d-eb6b-473a-b31d-bbd084e1814d/resourceGroups/503ace4c-9b1c-4059-a3e9-09553d24e9e1", + "denySettings": { + "mode": "DenyDelete", + "applyToChildScopes": true, + "excludedPrincipals": [ + "004afc20-146e-4932-a8b5-3098461c46a5", + "e6a513a0-b872-4355-82b9-47645fb30d3a" + ], + "excludedActions": [] + }, + "parametersLink": { + "uri": "https://MyParameters.json" + }, + "templateLink": { + "uri": "https://MyTemplate.json" + }, + "bypassStackOutOfSyncError": false + }, + "id": "/subscriptions/6d41d86d-eb6b-473a-b31d-bbd084e1814d/resourceGroups/503ace4c-9b1c-4059-a3e9-09553d24e9e1/providers/Microsoft.Resources/deploymentStacksWhatIfResults/testWhatIf_c04b8bc4f3cbe14fb0670419042", + "type": "Microsoft.Resources/deploymentStacksWhatIfResults", + "name": "testWhatIf_c04b8bc4f3cbe14fb0670419042" +} \ No newline at end of file diff --git a/src/azure-cli/azure/cli/command_modules/resource/tests/latest/test_stack_formatters.py b/src/azure-cli/azure/cli/command_modules/resource/tests/latest/test_stack_formatters.py new file mode 100644 index 00000000000..1a047ed88ee --- /dev/null +++ b/src/azure-cli/azure/cli/command_modules/resource/tests/latest/test_stack_formatters.py @@ -0,0 +1,115 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +import json +import os +import unittest + +import azure.mgmt.resource.deploymentstacks.models as StackModels + +from azure.cli.command_modules.resource._color import Color +from azure.cli.command_modules.resource._stacks_formatters import DeploymentStacksWhatIfResultFormatter + + +class TestStacksWhatIfResultFormatter(unittest.TestCase): + + def test_what_if_1(self): + what_if_result = self._get_stacks_what_if_result("what-if-1.json") + + self.assertEqual(self.EXPECTED_STACKS_WHAT_IF_1, DeploymentStacksWhatIfResultFormatter().format(what_if_result)) + + expected_no_color_result = self.EXPECTED_STACKS_WHAT_IF_1 + for color in list(Color): + expected_no_color_result = expected_no_color_result.replace(str(color), '') + + self.assertEqual( + expected_no_color_result, DeploymentStacksWhatIfResultFormatter(enable_color=False).format(what_if_result)) + + def _get_stacks_what_if_result(self, file_name: str): + return StackModels.DeploymentStacksWhatIfResult(self._get_stacks_what_if_json(file_name)) + + @staticmethod + def _get_stacks_what_if_json(file_name: str): + with open(TestStacksWhatIfResultFormatter._get_stacks_what_if_test_file_path(file_name), 'r') as f: + return json.load(f) + + @staticmethod + def _get_stacks_what_if_test_file_path(file_name: str): + curr_dir = os.path.dirname(os.path.realpath(__file__)) + return os.path.join(os.path.join(curr_dir, 'data\\stacks-what-if'), file_name).replace('\\', '\\\\') + + EXPECTED_STACKS_WHAT_IF_1 = f"""Resource and property changes are indicated with these symbols: + {Color.GREEN}+{Color.RESET} Create ! Unsupported + {Color.PURPLE}~{Color.RESET} Modify {Color.RED}-{Color.RESET} Delete + = NoChange {Color.BLUE}v{Color.RESET} Detach + +{Color.DARK_YELLOW}Changes to Stack /subscriptions/6d41d86d-eb6b-473a-b31d-bbd084e1814d/resourceGroups/503ace4c-9b1c-4059-a3e9-09553d24e9e1/providers/Microsoft.Resources/deploymentStacks/testStack_9ef16884f0dad7d0e5de3d3ec57:{Color.RESET} +{Color.PURPLE}~ DeploymentScope: {Color.RESET}"ThisIsBefore" => "ThisIsAfter" +{Color.PURPLE}~ DenySettings.Mode: {Color.RESET}"None" => "DenyDelete" +{Color.PURPLE}~ DenySettings.ApplyToChildScopes: {Color.RESET}"False" => "True" +{Color.PURPLE}~ DenySettings.ExcludedPrincipals: {Color.RESET} + {Color.GREEN}+ {Color.RESET}{Color.GREEN}"004afc20-146e-4932-a8b5-3098461c46a5"{Color.RESET} + {Color.GREEN}+ {Color.RESET}{Color.GREEN}"e6a513a0-b872-4355-82b9-47645fb30d3a"{Color.RESET} +{Color.PURPLE}~ DenySettings.ArrayOfMixed: {Color.RESET} + {Color.PURPLE}~{Color.RESET} 0: + {Color.PURPLE}~ properties.something: {Color.RESET}"B4" => "Now" + {Color.GREEN}+{Color.RESET} 1: + {Color.GREEN}+ {Color.RESET}{Color.GREEN}"now"{Color.RESET} + {Color.RED}-{Color.RESET} 2: + {Color.RED}- {Color.RESET}{Color.RED}"iWasDeleted"{Color.RESET} + +{Color.DARK_YELLOW}Changes to Managed Resources:{Color.RESET} + +Azure +{Color.PURPLE}~ {Color.RESET}{Color.PURPLE}/subscriptions/6d41d86d-eb6b-473a-b31d-bbd084e1814d/resourceGroups/503ace4c-9b1c-4059-a3e9-09553d24e9e1/providers/Microsoft.Test/testA/resourceA{Color.RESET} + = Management Status: "Managed" + {Color.PURPLE}~ Deny Status: {Color.RESET}"None" => "DenyDelete" + {Color.PURPLE}~ properties.properties1: {Color.RESET}"resourceA-before" => "resourceA-after" += /subscriptions/6d41d86d-eb6b-473a-b31d-bbd084e1814d/resourceGroups/503ace4c-9b1c-4059-a3e9-09553d24e9e1/providers/Microsoft.Test/testB/resourceB + = Management Status: "Managed" + {Color.PURPLE}~ Deny Status: {Color.RESET}"None" => "DenyDelete" +{Color.GREEN}+ {Color.RESET}{Color.GREEN}/subscriptions/6d41d86d-eb6b-473a-b31d-bbd084e1814d/resourceGroups/503ace4c-9b1c-4059-a3e9-09553d24e9e1/providers/Microsoft.Test/testD/resourceD{Color.RESET} + {Color.PURPLE}~ Management Status: {Color.RESET}"NotManaged" => "Managed" + {Color.PURPLE}~ Deny Status: {Color.RESET}"None" => "DenyDelete" +>> {Color.PURPLE}Potential Resource Changes (Learn more at https://aka.ms/whatIfPotentialChanges){Color.RESET} +{Color.CYAN}?{Color.RESET}{Color.PURPLE}~ {Color.RESET}{Color.CYAN}[Potential] {Color.RESET}{Color.PURPLE}/subscriptions/6d41d86d-eb6b-473a-b31d-bbd084e1814d/resourceGroups/503ace4c-9b1c-4059-a3e9-09553d24e9e1/providers/Microsoft.Test/testC/resourceC{Color.RESET} + = Management Status: "Managed" + {Color.PURPLE}~ Deny Status: {Color.RESET}"None" => "DenyDelete" + {Color.PURPLE}~ properties.properties1: {Color.RESET}"resourceC-before" => "resourceC-potential-after" +{Color.CYAN}?{Color.RESET}{Color.RED}- {Color.RESET}{Color.CYAN}[Potential] {Color.RESET}{Color.RED}/subscriptions/6d41d86d-eb6b-473a-b31d-bbd084e1814d/resourceGroups/503ace4c-9b1c-4059-a3e9-09553d24e9e1/providers/Microsoft.Test/testC/resourceC{Color.RESET} + {Color.PURPLE}~ Management Status: {Color.RESET}"Managed" => "NotManaged" + = Deny Status: "None" + +Contoso@2.0.0 +{Color.PURPLE}~ {Color.RESET}{Color.PURPLE}Contoso/example name="abcResource"{Color.RESET} + {Color.PURPLE}~ Management Status: {Color.RESET}null => "Managed" + {Color.PURPLE}~ Deny Status: {Color.RESET}null => "NotApplicable" + {Color.PURPLE}~ properties.properties1: {Color.RESET}"resourceA-before" => "resourceA-after" +{Color.RED}- {Color.RESET}{Color.RED}Contoso/example name="defResource"{Color.RESET} + {Color.PURPLE}~ Management Status: {Color.RESET}null => "Managed" + {Color.PURPLE}~ Deny Status: {Color.RESET}null => "NotApplicable" + {Color.PURPLE}~ properties.properties1: {Color.RESET}"resourceA-before" => "resourceA-after" + +Kubernetes@2.0.0 namespace="myNs", kubeconfig= +{Color.GREEN}+ {Color.RESET}{Color.GREEN}app/Deployment name="abcResource"{Color.RESET} + {Color.PURPLE}~ Management Status: {Color.RESET}null => "Managed" + {Color.PURPLE}~ Deny Status: {Color.RESET}null => "NotApplicable" + {Color.PURPLE}~ properties.properties1: {Color.RESET}"resourceA-before" => "resourceA-after" + +{Color.RED}Deleting - {Color.RESET}Resources Marked for Deletion 2 total: + +Azure +>> {Color.RED}Potential Deletions 1 total (Learn more at https://aka.ms/whatIfPotentialChanges){Color.RESET} +{Color.CYAN}?{Color.RESET}{Color.RED}- {Color.RESET}{Color.CYAN}[Potential] {Color.RESET}{Color.RED}/subscriptions/6d41d86d-eb6b-473a-b31d-bbd084e1814d/resourceGroups/503ace4c-9b1c-4059-a3e9-09553d24e9e1/providers/Microsoft.Test/testC/resourceC{Color.RESET} + +Contoso@2.0.0 +{Color.RED}- {Color.RESET}{Color.RED}Contoso/example name="defResource"{Color.RESET} + +Diagnostics (4): +INFO: [InfoCode] InfoMessage +{Color.DARK_YELLOW}WARNING: [Abc] Xyz{Color.RESET} +{Color.DARK_YELLOW}WARNING: [NoSupportForExtensibleResources] Extensible resources are currently not supported{Color.RESET} +{Color.RED}ERROR: [ErrorCode] ErrorMessage{Color.RESET} +""" From d75a2d17cb4691f98485cc25b1be93e19006c13a Mon Sep 17 00:00:00 2001 From: Kyle Albert <5498623+kalbert312@users.noreply.github.com> Date: Mon, 2 Mar 2026 15:19:09 -0500 Subject: [PATCH 26/32] Add live test class for deployment stack what-ifs. --- .../tests/latest/test_resource_stacks.py | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 src/azure-cli/azure/cli/command_modules/resource/tests/latest/test_resource_stacks.py diff --git a/src/azure-cli/azure/cli/command_modules/resource/tests/latest/test_resource_stacks.py b/src/azure-cli/azure/cli/command_modules/resource/tests/latest/test_resource_stacks.py new file mode 100644 index 00000000000..7acee83ebb9 --- /dev/null +++ b/src/azure-cli/azure/cli/command_modules/resource/tests/latest/test_resource_stacks.py @@ -0,0 +1,98 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +import os + +from azure.cli.testsdk import ScenarioTest, ResourceGroupPreparer + + +class DeploymentStacksWhatIfTest(ScenarioTest): + LOCATION = "westcentralus" + MGMT_GROUP_NAME = "AzBlueprintAssignTest" + + @ResourceGroupPreparer(name_prefix='cli_test_stacks_what_if', location=LOCATION) + def test_deployment_stack_what_if_at_resource_group(self, resource_group): + stack_what_if_name = self.create_random_name('cli-test-create-rg-stack-what-if', 60) + stack_name = self.create_random_name('cli-test-create-rg-stack-for-what-if', 60) + + self.kwargs.update({ + 'name': stack_what_if_name, + 'location': DeploymentStacksWhatIfTest.LOCATION, + 'resource-group': resource_group, + 'template-file': self._get_test_file('simple_template.json'), + 'parameter-file': self._get_test_file('simple_template_params.json'), + 'stack-id': f'/subscriptions/{self.get_subscription_id()}/resourceGroups/{resource_group}/providers/Microsoft.Resources/deploymentStacks/{stack_name}', + }) + + self.cmd( + 'stack-whatif group create --name {name} --resource-group {resource-group} --template-file "{template-file}" --deny-settings-mode denYdeletE --parameters "{parameter-file}" --yes --description "stack deployment" --aou deleteAll --deny-settings-excluded-principals "principal1 principal2" --deny-settings-excluded-actions "action1 action2" --deny-settings-apply-to-child-scopes --vl ProviderNoRbac --ri P1D --stack "{stack-id}"', + checks=self.check('provisioningState', 'succeeded')) + + self.cmd( + 'stack-whatif group show --name {name} --resource-group {resource-group}', + checks=self.check('provisioningState', 'succeeded')) + + self.cmd( + 'stack-whatif group list --resource-group {resource-group}', + checks=self.check(f"length([?name=='{stack_what_if_name}']) > `0`", True)) + + self.cmd('stack-whatif group delete --name {name} --resource-group {resource-group} --yes') + + def test_deployment_stack_what_if_at_subscription(self): + stack_what_if_name = self.create_random_name('cli-test-create-sub-stack-what-if', 60) + stack_name = self.create_random_name('cli-test-create-sub-stack-for-what-if', 60) + + self.kwargs.update({ + 'name': stack_what_if_name, + 'location': DeploymentStacksWhatIfTest.LOCATION, + 'template-file': self._get_test_file('template_sub_validate.json'), + 'parameter-file': self._get_test_file('template_sub_validate_parameters_valid.json'), + 'stack-id': f'/subscriptions/{self.get_subscription_id()}/providers/Microsoft.Resources/deploymentStacks/{stack_name}', + }) + + self.cmd( + 'stack-whatif sub create --name {name} --location {location} --template-file "{template-file}" --dm denyDelete --parameters "{parameter-file}" --yes --description "stack deployment" --aou deleteAll --deny-settings-excluded-principals "principal1 principal2" --deny-settings-excluded-actions "action1 action2" --deny-settings-apply-to-child-scopes --vl ProviderNoRbac --ri P1D --stack "{stack-id}"', + checks=self.check('provisioningState', 'succeeded')) + + self.cmd( + 'stack-whatif sub show --name {name}', + checks=self.check('provisioningState', 'succeeded')) + + self.cmd( + 'stack-whatif sub list', + checks=self.check(f"length([?name=='{stack_what_if_name}']) > `0`", True)) + + self.cmd('stack-whatif sub delete --name {name} --yes') + + def test_deployment_stack_what_if_at_management_group(self): + stack_what_if_name = self.create_random_name('cli-test-create-mg-stack-what-if', 60) + stack_name = self.create_random_name('cli-test-create-mg-stack-for-what-if', 60) + + self.kwargs.update({ + 'name': stack_what_if_name, + 'location': DeploymentStacksWhatIfTest.LOCATION, + 'management-group': DeploymentStacksWhatIfTest.MGMT_GROUP_NAME, + 'template-file': self._get_test_file('template_mg_validate.json'), + 'parameter-file': self._get_test_file('template_mg_validate_parameters_valid.json'), + 'stack-id': f'/providers/Microsoft.Management/managementGroups/{DeploymentStacksWhatIfTest.MGMT_GROUP_NAME}/providers/Microsoft.Resources/deploymentStacks/{stack_name}', + }) + + self.cmd( + 'stack-whatif mg create --name {name} --location {location} --management-group-id {management-group} --template-file "{template-file}" --dm denyDelete --parameters "{parameter-file}" --yes --description "stack deployment" --aou deleteAll --deny-settings-excluded-principals "principal1 principal2" --deny-settings-excluded-actions "action1 action2" --deny-settings-apply-to-child-scopes --vl ProviderNoRbac --ri P1D --stack "{stack-id}"', + checks=self.check('provisioningState', 'succeeded')) + + self.cmd( + 'stack-whatif mg show --name {name} --management-group-id {management-group}', + checks=self.check('provisioningState', 'succeeded')) + + self.cmd( + 'stack-whatif mg list --management-group-id {management-group}', + checks=self.check(f"length([?name=='{stack_what_if_name}']) > `0`", True)) + + self.cmd('stack-whatif mg delete --name {name} --management-group-id {management-group} --yes') + + @staticmethod + def _get_test_file(file_path: str): + return os.path.join(os.path.dirname(os.path.realpath(__file__)), file_path).replace('\\', '\\\\') \ No newline at end of file From 5fd01a611e426488b36f8cda501d5df8f21a9ceb Mon Sep 17 00:00:00 2001 From: Kyle Albert <5498623+kalbert312@users.noreply.github.com> Date: Mon, 2 Mar 2026 15:42:29 -0500 Subject: [PATCH 27/32] Add stack-whatif to service_name.json --- src/azure-cli/service_name.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/azure-cli/service_name.json b/src/azure-cli/service_name.json index e260e066dc8..adf2d01e4f1 100644 --- a/src/azure-cli/service_name.json +++ b/src/azure-cli/service_name.json @@ -194,6 +194,11 @@ "AzureServiceName": "Resource Manager", "URL": "https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/deployment-stacks" }, + { + "Command": "az stack-whatif", + "AzureServiceName": "Resource Manager", + "URL": "https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/deployment-stacks" + }, { "Command": "az devops", "AzureServiceName": "DevOps", From 5cbbac27e32a9e1b97324bb603cb833381528c9d Mon Sep 17 00:00:00 2001 From: Kyle Albert <5498623+kalbert312@users.noreply.github.com> Date: Mon, 2 Mar 2026 17:24:17 -0500 Subject: [PATCH 28/32] Add missing required params in the stack-whatif help examples. --- .../cli/command_modules/resource/_help.py | 59 +++++++++---------- 1 file changed, 27 insertions(+), 32 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/resource/_help.py b/src/azure-cli/azure/cli/command_modules/resource/_help.py index 2883b915eba..74736d9d4e6 100644 --- a/src/azure-cli/azure/cli/command_modules/resource/_help.py +++ b/src/azure-cli/azure/cli/command_modules/resource/_help.py @@ -2558,27 +2558,24 @@ short-summary: Manage Deployment Stacks What-Ifs at management group. """ -# TODO(kylealbert): params in examples helps['stack-whatif mg create'] = """ type: command short-summary: Preview a deployment stack operation at management group scope. examples: - - name: Perform a what-if on a deployment stack using template file and detach all resources on unmanage. - text: az stack-whatif mg create --name StackName --management-group-id myMg --template-file simpleTemplate.json --location westus2 --description description --deny-settings-mode None --action-on-unmanage detachAll - - name: Perform a what-if on a deployment stack with parameter file and delete resources on unmanage. - text: az stack-whatif mg create --name StackName --management-group-id myMg --action-on-unmanage deleteResources --template-file simpleTemplate.json --parameters simpleTemplateParams.json --location westus2 --description description --deny-settings-mode None + - name: Perform a what-if on a deployment stack using template file and detach all unmanaged resources. + text: az stack-whatif mg create --name StackName --management-group-id myMg --template-file simpleTemplate.json --parameters simpleTemplateParams.json --location westus2 --description description --deny-settings-mode None --action-on-unmanage detachAll --ri P5D --stack /providers/Microsoft.Management/myMg/providers/Microsoft.Resources/deploymentStacks/myMgStack - name: Perform a what-if on a deployment stack with template spec. - text: az stack-whatif mg create --name StackName --management-group-id myMg --template-spec TemplateSpecResourceIDWithVersion --location westus2 --description description --deny-settings-mode None --action-on-unmanage deleteResources + text: az stack-whatif mg create --name StackName --management-group-id myMg --template-spec TemplateSpecResourceIDWithVersion --location westus2 --description description --deny-settings-mode None --action-on-unmanage deleteResources --ri P5D --stack /providers/Microsoft.Management/myMg/providers/Microsoft.Resources/deploymentStacks/myMgStack - name: Perform a what-if on a deployment stack using bicep file and delete all resources on unmanage. - text: az stack-whatif mg create --name StackName --management-group-id myMg --action-on-unmanage deleteAll --template-file simple.bicep --location westus2 --description description --deny-settings-mode None + text: az stack-whatif mg create --name StackName --management-group-id myMg --action-on-unmanage deleteAll --template-file simple.bicep --location westus2 --description description --deny-settings-mode None --ri P5D --stack /providers/Microsoft.Management/myMg/providers/Microsoft.Resources/deploymentStacks/myMgStack - name: Perform a what-if on a deployment stack using parameters from key/value pairs. - text: az stack-whatif mg create --name StackName --management-group-id myMg --template-file simpleTemplate.json --location westus --description description --parameters simpleTemplateParams.json value1=foo value2=bar --deny-settings-mode None --action-on-unmanage deleteResources + text: az stack-whatif mg create --name StackName --management-group-id myMg --template-file simpleTemplate.json --location westus --description description --parameters simpleTemplateParams.json value1=foo value2=bar --deny-settings-mode None --action-on-unmanage deleteResources --ri P5D --stack /providers/Microsoft.Management/myMg/providers/Microsoft.Resources/deploymentStacks/myMgStack - name: Perform a what-if on a deployment stack from a local template, using a parameter file, a remote parameter file, and selectively overriding key/value pairs. - text: az stack-whatif mg create --name StackName --management-group-id myMg --template-file azuredeploy.json --parameters @params.json --parameters https://mysite/params.json --parameters MyValue=This MyArray=@array.json --location westus --deny-settings-mode None --action-on-unmanage deleteResources + text: az stack-whatif mg create --name StackName --management-group-id myMg --template-file azuredeploy.json --parameters @params.json --parameters https://mysite/params.json --parameters MyValue=This MyArray=@array.json --location westus --deny-settings-mode None --action-on-unmanage deleteResources --ri P5D --stack /providers/Microsoft.Management/myMg/providers/Microsoft.Resources/deploymentStacks/myMgStack - name: Perform a what-if on a deployment stack from a local template, using deny settings. - text: az stack-whatif mg create --name StackName --management-group-id myMg --template-file azuredeploy.json --deny-settings-mode denyDelete --deny-settings-excluded-actions Microsoft.Compute/virtualMachines/write --deny-settings-excluded-principals "test1 test2" --location westus --action-on-unmanage deleteResources + text: az stack-whatif mg create --name StackName --management-group-id myMg --template-file azuredeploy.json --deny-settings-mode denyDelete --deny-settings-excluded-actions Microsoft.Compute/virtualMachines/write --deny-settings-excluded-principals "test1 test2" --location westus --action-on-unmanage deleteResources --ri P5D --stack /providers/Microsoft.Management/myMg/providers/Microsoft.Resources/deploymentStacks/myMgStack - name: Perform a what-if on a deployment stack from a local template, apply deny settings to child scope. - text: az stack-whatif mg create --name StackName --management-group-id myMg --template-file azuredeploy.json --deny-settings-mode denyDelete --deny-settings-excluded-actions Microsoft.Compute/virtualMachines/write --deny-settings-apply-to-child-scopes --location westus --action-on-unmanage deleteResources + text: az stack-whatif mg create --name StackName --management-group-id myMg --template-file azuredeploy.json --deny-settings-mode denyDelete --deny-settings-excluded-actions Microsoft.Compute/virtualMachines/write --deny-settings-apply-to-child-scopes --location westus --action-on-unmanage deleteResources --ri P5D --stack /providers/Microsoft.Management/myMg/providers/Microsoft.Resources/deploymentStacks/myMgStack """ helps['stack sub'] = """ @@ -2681,31 +2678,30 @@ short-summary: Manage Deployment Stacks What-Ifs at subscription. """ -# TODO(kylealbert): params in examples helps['stack-whatif sub create'] = """ type: command short-summary: Preview a deployment stack operation at subscription scope. examples: - name: Perform a what-if on a deployment stack using template file and detach all resources on unmanage. - text: az stack-whatif sub create --name StackName --template-file simpleTemplate.json --location westus2 --description description --deny-settings-mode None --action-on-unmanage detachAll + text: az stack-whatif sub create --name StackName --template-file simpleTemplate.json --location westus2 --description description --deny-settings-mode None --action-on-unmanage detachAll --ri P5D --stack /subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Resources/deploymentStacks/mySubStack - name: Perform a what-if on a deployment stack with parameter file and delete resources on unmanage. - text: az stack-whatif sub create --name StackName --action-on-unmanage deleteResources --template-file simpleTemplate.json --parameters simpleTemplateParams.json --location westus2 --description description --deny-settings-mode None + text: az stack-whatif sub create --name StackName --action-on-unmanage deleteResources --template-file simpleTemplate.json --parameters simpleTemplateParams.json --location westus2 --description description --deny-settings-mode None --ri P5D --stack /subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Resources/deploymentStacks/mySubStack - name: Perform a what-if on a deployment stack with template spec. - text: az stack-whatif sub create --name StackName --template-spec TemplateSpecResourceIDWithVersion --location westus2 --description description --deny-settings-mode None --action-on-unmanage deleteResources + text: az stack-whatif sub create --name StackName --template-spec TemplateSpecResourceIDWithVersion --location westus2 --description description --deny-settings-mode None --action-on-unmanage deleteResources --ri P5D --stack /subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Resources/deploymentStacks/mySubStack - name: Perform a what-if on a deployment stack using bicep file and delete all resources on unmanage. - text: az stack-whatif sub create --name StackName --action-on-unmanage deleteAll --template-file simple.bicep --location westus2 --description description --deny-settings-mode None + text: az stack-whatif sub create --name StackName --action-on-unmanage deleteAll --template-file simple.bicep --location westus2 --description description --deny-settings-mode None --ri P5D --stack /subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Resources/deploymentStacks/mySubStack - name: Perform a what-if on a deployment stack at a different subscription. - text: az stack-whatif sub create --name StackName --template-file simpleTemplate.json --location westus2 --description description --subscription subscriptionId --deny-settings-mode None --action-on-unmanage deleteResources + text: az stack-whatif sub create --name StackName --template-file simpleTemplate.json --location westus2 --description description --subscription subscriptionId --deny-settings-mode None --action-on-unmanage deleteResources --ri P5D --stack /subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Resources/deploymentStacks/mySubStack - name: Perform a what-if on a deployment stack and deploy at the resource group scope. - text: az stack-whatif sub create --name StackName --template-file simpleTemplate.json --location westus --deployment-resource-group ResourceGroup --description description --deny-settings-mode None --action-on-unmanage deleteResources + text: az stack-whatif sub create --name StackName --template-file simpleTemplate.json --location westus --deployment-resource-group ResourceGroup --description description --deny-settings-mode None --action-on-unmanage deleteResources --ri P5D --stack /subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Resources/deploymentStacks/mySubStack - name: Perform a what-if on a deployment stack using parameters from key/value pairs. - text: az stack-whatif sub create --name StackName --template-file simpleTemplate.json --location westus --description description --parameters simpleTemplateParams.json value1=foo value2=bar --deny-settings-mode None --action-on-unmanage deleteResources + text: az stack-whatif sub create --name StackName --template-file simpleTemplate.json --location westus --description description --parameters simpleTemplateParams.json value1=foo value2=bar --deny-settings-mode None --action-on-unmanage deleteResources --ri P5D --stack /subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Resources/deploymentStacks/mySubStack - name: Perform a what-if on a deployment stack from a local template, using a parameter file, a remote parameter file, and selectively overriding key/value pairs. - text: az stack-whatif sub create --name StackName --template-file azuredeploy.json --parameters @params.json --parameters https://mysite/params.json --parameters MyValue=This MyArray=@array.json --location westus --deny-settings-mode None --action-on-unmanage deleteResources + text: az stack-whatif sub create --name StackName --template-file azuredeploy.json --parameters @params.json --parameters https://mysite/params.json --parameters MyValue=This MyArray=@array.json --location westus --deny-settings-mode None --action-on-unmanage deleteResources --ri P5D --stack /subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Resources/deploymentStacks/mySubStack - name: Perform a what-if on a deployment stack from a local template, using deny settings. - text: az stack-whatif sub create --name StackName --template-file azuredeploy.json --deny-settings-mode denyDelete --deny-settings-excluded-actions Microsoft.Compute/virtualMachines/write --deny-settings-excluded-principals "test1 test2" --location westus --action-on-unmanage deleteResources + text: az stack-whatif sub create --name StackName --template-file azuredeploy.json --deny-settings-mode denyDelete --deny-settings-excluded-actions Microsoft.Compute/virtualMachines/write --deny-settings-excluded-principals "test1 test2" --location westus --action-on-unmanage deleteResources --ri P5D --stack /subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Resources/deploymentStacks/mySubStack - name: Perform a what-if on a deployment stack from a local template, apply deny settings to child scopes. - text: az stack-whatif sub create --name StackName --template-file azuredeploy.json --deny-settings-mode denyDelete --deny-settings-excluded-actions Microsoft.Compute/virtualMachines/write --deny-settings-apply-to-child-scopes --location westus --action-on-unmanage deleteResources + text: az stack-whatif sub create --name StackName --template-file azuredeploy.json --deny-settings-mode denyDelete --deny-settings-excluded-actions Microsoft.Compute/virtualMachines/write --deny-settings-apply-to-child-scopes --location westus --action-on-unmanage deleteResources --ri P5D --stack /subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Resources/deploymentStacks/mySubStack """ helps['stack group'] = """ @@ -2804,29 +2800,28 @@ short-summary: Manage Deployment Stacks What-Ifs at resource group. """ -# TODO(kylealbert): params in examples helps['stack-whatif group create'] = """ type: command short-summary: Preview a deployment stack operation at resource group scope. examples: - name: Perform a what-if on a deployment stack using template file and delete resources on unmanage. - text: az stack-whatif group create --name StackName --resource-group ResourceGroup --action-on-unmanage deleteResources --template-file simpleTemplate.json --description description --deny-settings-mode None + text: az stack-whatif group create --name StackName --resource-group ResourceGroup --action-on-unmanage deleteResources --template-file simpleTemplate.json --description description --deny-settings-mode None --ri P5D --stack /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/ResourceGroup/providers/Microsoft.Resources/deploymentStacks/mySubStack - name: Perform a what-if on a deployment stack with parameter file and detach all resources on unmanage. - text: az stack-whatif group create --name StackName --resource-group ResourceGroup --action-on-unmanage detachAll --template-file simpleTemplate.json --parameters simpleTemplateParams.json --description description --deny-settings-mode None + text: az stack-whatif group create --name StackName --resource-group ResourceGroup --action-on-unmanage detachAll --template-file simpleTemplate.json --parameters simpleTemplateParams.json --description description --deny-settings-mode None --ri P5D --stack /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/ResourceGroup/providers/Microsoft.Resources/deploymentStacks/mySubStack - name: Perform a what-if on a deployment stack with template spec and delete all resources on unmanage. - text: az stack-whatif group create --name StackName --resource-group ResourceGroup --action-on-unmanage deleteAll --template-spec TemplateSpecResourceIDWithVersion --description description --deny-settings-mode None + text: az stack-whatif group create --name StackName --resource-group ResourceGroup --action-on-unmanage deleteAll --template-spec TemplateSpecResourceIDWithVersion --description description --deny-settings-mode None --ri P5D --stack /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/ResourceGroup/providers/Microsoft.Resources/deploymentStacks/mySubStack - name: Perform a what-if on a deployment stack using bicep file. - text: az stack-whatif group create --name StackName --resource-group ResourceGroup --template-file simple.bicep --description description --deny-settings-mode None --action-on-unmanage deleteResources + text: az stack-whatif group create --name StackName --resource-group ResourceGroup --template-file simple.bicep --description description --deny-settings-mode None --action-on-unmanage deleteResources --ri P5D --stack /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/ResourceGroup/providers/Microsoft.Resources/deploymentStacks/mySubStack - name: Perform a what-if on a deployment stack at a different subscription. - text: az stack-whatif group create --name StackName --resource-group ResourceGroup --template-file simpleTemplate.json --description description --subscription subscriptionId --deny-settings-mode None --action-on-unmanage deleteResources + text: az stack-whatif group create --name StackName --resource-group ResourceGroup --template-file simpleTemplate.json --description description --subscription subscriptionId --deny-settings-mode None --action-on-unmanage deleteResources --ri P5D --stack /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/ResourceGroup/providers/Microsoft.Resources/deploymentStacks/mySubStack - name: Perform a what-if on a deployment stack using parameters from key/value pairs. - text: az stack-whatif group create --name StackName --template-file simpleTemplate.json --resource-group ResourceGroup --description description --parameters simpleTemplateParams.json value1=foo value2=bar --deny-settings-mode None --action-on-unmanage deleteResources + text: az stack-whatif group create --name StackName --template-file simpleTemplate.json --resource-group ResourceGroup --description description --parameters simpleTemplateParams.json value1=foo value2=bar --deny-settings-mode None --action-on-unmanage deleteResources --ri P5D --stack /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/ResourceGroup/providers/Microsoft.Resources/deploymentStacks/mySubStack - name: Perform a what-if on a deployment stack from a local template, using a parameter file, a remote parameter file, and selectively overriding key/value pairs. - text: az stack-whatif group create --name StackName --template-file azuredeploy.json --parameters @params.json --parameters https://mysite/params.json --parameters MyValue=This MyArray=@array.json --resource-group ResourceGroup --deny-settings-mode None --action-on-unmanage deleteResources + text: az stack-whatif group create --name StackName --template-file azuredeploy.json --parameters @params.json --parameters https://mysite/params.json --parameters MyValue=This MyArray=@array.json --resource-group ResourceGroup --deny-settings-mode None --action-on-unmanage deleteResources --ri P5D --stack /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/ResourceGroup/providers/Microsoft.Resources/deploymentStacks/mySubStack - name: Perform a what-if on a deployment stack from a local template, using deny settings. - text: az stack-whatif group create --name StackName --resource-group ResourceGroup --template-file azuredeploy.json --deny-settings-mode denyDelete --deny-settings-excluded-actions Microsoft.Compute/virtualMachines/write --deny-settings-excluded-principals "test1 test2" --action-on-unmanage deleteResources + text: az stack-whatif group create --name StackName --resource-group ResourceGroup --template-file azuredeploy.json --deny-settings-mode denyDelete --deny-settings-excluded-actions Microsoft.Compute/virtualMachines/write --deny-settings-excluded-principals "test1 test2" --action-on-unmanage deleteResources --ri P5D --stack /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/ResourceGroup/providers/Microsoft.Resources/deploymentStacks/mySubStack - name: Perform a what-if on a deployment stack from a local template, apply deny setting to child scopes. - text: az stack-whatif group create --name StackName --resource-group ResourceGroup --template-file azuredeploy.json --deny-settings-mode denyDelete --deny-settings-excluded-actions Microsoft.Compute/virtualMachines/write --deny-settings-apply-to-child-scopes --action-on-unmanage deleteResources + text: az stack-whatif group create --name StackName --resource-group ResourceGroup --template-file azuredeploy.json --deny-settings-mode denyDelete --deny-settings-excluded-actions Microsoft.Compute/virtualMachines/write --deny-settings-apply-to-child-scopes --action-on-unmanage deleteResources --ri P5D --stack /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/ResourceGroup/providers/Microsoft.Resources/deploymentStacks/mySubStack """ helps['bicep generate-params'] = """ From 7f68947ed276822cc3c30044d3d776dbac8c0e62 Mon Sep 17 00:00:00 2001 From: Kyle Albert <5498623+kalbert312@users.noreply.github.com> Date: Mon, 2 Mar 2026 17:55:27 -0500 Subject: [PATCH 29/32] Add missing stack-whatif help entries and adjust examples & descriptions. --- .../cli/command_modules/resource/_help.py | 140 +++++++++++++++--- 1 file changed, 118 insertions(+), 22 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/resource/_help.py b/src/azure-cli/azure/cli/command_modules/resource/_help.py index 74736d9d4e6..acf0475fbff 100644 --- a/src/azure-cli/azure/cli/command_modules/resource/_help.py +++ b/src/azure-cli/azure/cli/command_modules/resource/_help.py @@ -2555,19 +2555,19 @@ helps['stack-whatif mg'] = """ type: group -short-summary: Manage Deployment Stacks What-Ifs at management group. +short-summary: Manage Deployment stacks what-if results at management group scope. """ helps['stack-whatif mg create'] = """ type: command -short-summary: Preview a deployment stack operation at management group scope. +short-summary: Create a deployment stack what-if result at management group scope. examples: - name: Perform a what-if on a deployment stack using template file and detach all unmanaged resources. text: az stack-whatif mg create --name StackName --management-group-id myMg --template-file simpleTemplate.json --parameters simpleTemplateParams.json --location westus2 --description description --deny-settings-mode None --action-on-unmanage detachAll --ri P5D --stack /providers/Microsoft.Management/myMg/providers/Microsoft.Resources/deploymentStacks/myMgStack - name: Perform a what-if on a deployment stack with template spec. text: az stack-whatif mg create --name StackName --management-group-id myMg --template-spec TemplateSpecResourceIDWithVersion --location westus2 --description description --deny-settings-mode None --action-on-unmanage deleteResources --ri P5D --stack /providers/Microsoft.Management/myMg/providers/Microsoft.Resources/deploymentStacks/myMgStack - - name: Perform a what-if on a deployment stack using bicep file and delete all resources on unmanage. - text: az stack-whatif mg create --name StackName --management-group-id myMg --action-on-unmanage deleteAll --template-file simple.bicep --location westus2 --description description --deny-settings-mode None --ri P5D --stack /providers/Microsoft.Management/myMg/providers/Microsoft.Resources/deploymentStacks/myMgStack + - name: Perform a what-if on a deployment stack using bicep file, delete all resources on unmanage, and remove color from the output. + text: az stack-whatif mg create --name StackName --management-group-id myMg --no-color --action-on-unmanage deleteAll --template-file simple.bicep --location westus2 --description description --deny-settings-mode None --ri P5D --stack /providers/Microsoft.Management/myMg/providers/Microsoft.Resources/deploymentStacks/myMgStack - name: Perform a what-if on a deployment stack using parameters from key/value pairs. text: az stack-whatif mg create --name StackName --management-group-id myMg --template-file simpleTemplate.json --location westus --description description --parameters simpleTemplateParams.json value1=foo value2=bar --deny-settings-mode None --action-on-unmanage deleteResources --ri P5D --stack /providers/Microsoft.Management/myMg/providers/Microsoft.Resources/deploymentStacks/myMgStack - name: Perform a what-if on a deployment stack from a local template, using a parameter file, a remote parameter file, and selectively overriding key/value pairs. @@ -2578,9 +2578,41 @@ text: az stack-whatif mg create --name StackName --management-group-id myMg --template-file azuredeploy.json --deny-settings-mode denyDelete --deny-settings-excluded-actions Microsoft.Compute/virtualMachines/write --deny-settings-apply-to-child-scopes --location westus --action-on-unmanage deleteResources --ri P5D --stack /providers/Microsoft.Management/myMg/providers/Microsoft.Resources/deploymentStacks/myMgStack """ +helps['stack-whatif mg list'] = """ +type: command +short-summary: List all deployment stacks what-if results in a management group. +examples: + - name: List all stack what-if results in management group. + text: az stack-whatif mg list --management-group-id myMg +""" + +helps['stack-whatif mg show'] = """ +type: command +short-summary: Get a deployment stack what-if result from management group scope. +examples: + - name: Get a stack what-if result by name. + text: az stack-whatif mg show --name ResultName --management-group-id myMg + - name: Get a stack what-if result by name without color. + text: az stack-whatif mg show --name ResultName --management-group-id myMg --no-color + - name: Get JSON for a stack what-if result by name. + text: az stack-whatif mg show --name ResultName --management-group-id myMg --no-pretty-print + - name: Get a stack what-if result by resource id. + text: az stack-whatif mg show --id /providers/Microsoft.Management/managementGroups/myMg/providers/Microsoft.Resources/deploymentStacksWhatIfResults/ResultName --management-group-id myMg +""" + +helps['stack-whatif mg delete'] = """ +type: command +short-summary: Delete a deployment stack what-if result from management group scope. +examples: + - name: Delete a stack what-if result by name. + text: az stack-whatif mg delete --name ResultName --management-group-id myMg + - name: Delete a stack what-if result by resource id. + text: az stack-whatif mg delete --id /providers/Microsoft.Management/managementGroups/myMg/providers/Microsoft.Resources/deploymentStacksWhatIfResults/ResultName --management-group-id myMg +""" + helps['stack sub'] = """ type: group -short-summary: Manage Deployment Stacks at subscription. +short-summary: Manage Deployment stacks at subscription scope. """ helps['stack sub create'] = """ @@ -2675,17 +2707,17 @@ helps['stack-whatif sub'] = """ type: group -short-summary: Manage Deployment Stacks What-Ifs at subscription. +short-summary: Manage Deployment stacks what-if results at subscription scope. """ helps['stack-whatif sub create'] = """ type: command -short-summary: Preview a deployment stack operation at subscription scope. +short-summary: Create a deployment stack what-if result at subscription scope. examples: - name: Perform a what-if on a deployment stack using template file and detach all resources on unmanage. text: az stack-whatif sub create --name StackName --template-file simpleTemplate.json --location westus2 --description description --deny-settings-mode None --action-on-unmanage detachAll --ri P5D --stack /subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Resources/deploymentStacks/mySubStack - - name: Perform a what-if on a deployment stack with parameter file and delete resources on unmanage. - text: az stack-whatif sub create --name StackName --action-on-unmanage deleteResources --template-file simpleTemplate.json --parameters simpleTemplateParams.json --location westus2 --description description --deny-settings-mode None --ri P5D --stack /subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Resources/deploymentStacks/mySubStack + - name: Perform a what-if on a deployment stack with parameter file, delete resources on unmanage, and remove color from the output. + text: az stack-whatif sub create --name StackName --action-on-unmanage deleteResources --no-color --template-file simpleTemplate.json --parameters simpleTemplateParams.json --location westus2 --description description --deny-settings-mode None --ri P5D --stack /subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Resources/deploymentStacks/mySubStack - name: Perform a what-if on a deployment stack with template spec. text: az stack-whatif sub create --name StackName --template-spec TemplateSpecResourceIDWithVersion --location westus2 --description description --deny-settings-mode None --action-on-unmanage deleteResources --ri P5D --stack /subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Resources/deploymentStacks/mySubStack - name: Perform a what-if on a deployment stack using bicep file and delete all resources on unmanage. @@ -2704,9 +2736,41 @@ text: az stack-whatif sub create --name StackName --template-file azuredeploy.json --deny-settings-mode denyDelete --deny-settings-excluded-actions Microsoft.Compute/virtualMachines/write --deny-settings-apply-to-child-scopes --location westus --action-on-unmanage deleteResources --ri P5D --stack /subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Resources/deploymentStacks/mySubStack """ +helps['stack-whatif sub list'] = """ +type: command +short-summary: List all deployment stack what-if results in a subscription. +examples: + - name: List all stack what-if results the current subscription. + text: az stack-whatif sub list +""" + +helps['stack-whatif sub show'] = """ +type: command +short-summary: Get a deployment stack what-if result from subscription scope. +examples: + - name: Get a stack what-if result by name. + text: az stack-whatif sub show --name ResultName + - name: Get a stack what-if result by name without color. + text: az stack-whatif sub show --name ResultName --no-color + - name: Get JSON for a stack what-if result by name. + text: az stack-whatif sub show --name ResultName --no-pretty-print + - name: Get a stack what-if result by resource id. + text: az stack-whatif sub show --id /subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Resources/deploymentStacksWhatIfResults/ResultName +""" + +helps['stack-whatif sub delete'] = """ +type: command +short-summary: Delete a deployment stack what-if result from subscription scope. +examples: + - name: Delete a stack what-if result by name. + text: az stack-whatif sub delete --name StackName + - name: Delete a stack what-if result by resource id. + text: az stack-whatif sub delete --id /subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Resources/deploymentStacksWhatIfResults/ResultName +""" + helps['stack group'] = """ type: group -short-summary: Manage Deployment Stacks at resource group. +short-summary: Manage Deployment stacks at resource group scope. """ helps['stack group create'] = """ @@ -2797,31 +2861,63 @@ helps['stack-whatif group'] = """ type: group -short-summary: Manage Deployment Stacks What-Ifs at resource group. +short-summary: Manage Deployment stacks what-if results at resource group scope. """ helps['stack-whatif group create'] = """ type: command -short-summary: Preview a deployment stack operation at resource group scope. +short-summary: Create a deployment stack what-if result at resource group scope. examples: - name: Perform a what-if on a deployment stack using template file and delete resources on unmanage. - text: az stack-whatif group create --name StackName --resource-group ResourceGroup --action-on-unmanage deleteResources --template-file simpleTemplate.json --description description --deny-settings-mode None --ri P5D --stack /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/ResourceGroup/providers/Microsoft.Resources/deploymentStacks/mySubStack - - name: Perform a what-if on a deployment stack with parameter file and detach all resources on unmanage. - text: az stack-whatif group create --name StackName --resource-group ResourceGroup --action-on-unmanage detachAll --template-file simpleTemplate.json --parameters simpleTemplateParams.json --description description --deny-settings-mode None --ri P5D --stack /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/ResourceGroup/providers/Microsoft.Resources/deploymentStacks/mySubStack + text: az stack-whatif group create --name ResultName --resource-group ResourceGroup --action-on-unmanage deleteResources --template-file simpleTemplate.json --description description --deny-settings-mode None --ri P5D --stack /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/ResourceGroup/providers/Microsoft.Resources/deploymentStacks/mySubStack + - name: Perform a what-if on a deployment stack with parameter file, detach all resources on unmanage, and remove color from the output. + text: az stack-whatif group create --name ResultName --resource-group ResourceGroup --no-color --action-on-unmanage detachAll --template-file simpleTemplate.json --parameters simpleTemplateParams.json --description description --deny-settings-mode None --ri P5D --stack /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/ResourceGroup/providers/Microsoft.Resources/deploymentStacks/mySubStack - name: Perform a what-if on a deployment stack with template spec and delete all resources on unmanage. - text: az stack-whatif group create --name StackName --resource-group ResourceGroup --action-on-unmanage deleteAll --template-spec TemplateSpecResourceIDWithVersion --description description --deny-settings-mode None --ri P5D --stack /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/ResourceGroup/providers/Microsoft.Resources/deploymentStacks/mySubStack + text: az stack-whatif group create --name ResultName --resource-group ResourceGroup --action-on-unmanage deleteAll --template-spec TemplateSpecResourceIDWithVersion --description description --deny-settings-mode None --ri P5D --stack /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/ResourceGroup/providers/Microsoft.Resources/deploymentStacks/mySubStack - name: Perform a what-if on a deployment stack using bicep file. - text: az stack-whatif group create --name StackName --resource-group ResourceGroup --template-file simple.bicep --description description --deny-settings-mode None --action-on-unmanage deleteResources --ri P5D --stack /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/ResourceGroup/providers/Microsoft.Resources/deploymentStacks/mySubStack + text: az stack-whatif group create --name ResultName --resource-group ResourceGroup --template-file simple.bicep --description description --deny-settings-mode None --action-on-unmanage deleteResources --ri P5D --stack /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/ResourceGroup/providers/Microsoft.Resources/deploymentStacks/mySubStack - name: Perform a what-if on a deployment stack at a different subscription. - text: az stack-whatif group create --name StackName --resource-group ResourceGroup --template-file simpleTemplate.json --description description --subscription subscriptionId --deny-settings-mode None --action-on-unmanage deleteResources --ri P5D --stack /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/ResourceGroup/providers/Microsoft.Resources/deploymentStacks/mySubStack + text: az stack-whatif group create --name ResultName --resource-group ResourceGroup --template-file simpleTemplate.json --description description --subscription subscriptionId --deny-settings-mode None --action-on-unmanage deleteResources --ri P5D --stack /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/ResourceGroup/providers/Microsoft.Resources/deploymentStacks/mySubStack - name: Perform a what-if on a deployment stack using parameters from key/value pairs. - text: az stack-whatif group create --name StackName --template-file simpleTemplate.json --resource-group ResourceGroup --description description --parameters simpleTemplateParams.json value1=foo value2=bar --deny-settings-mode None --action-on-unmanage deleteResources --ri P5D --stack /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/ResourceGroup/providers/Microsoft.Resources/deploymentStacks/mySubStack + text: az stack-whatif group create --name ResultName --template-file simpleTemplate.json --resource-group ResourceGroup --description description --parameters simpleTemplateParams.json value1=foo value2=bar --deny-settings-mode None --action-on-unmanage deleteResources --ri P5D --stack /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/ResourceGroup/providers/Microsoft.Resources/deploymentStacks/mySubStack - name: Perform a what-if on a deployment stack from a local template, using a parameter file, a remote parameter file, and selectively overriding key/value pairs. - text: az stack-whatif group create --name StackName --template-file azuredeploy.json --parameters @params.json --parameters https://mysite/params.json --parameters MyValue=This MyArray=@array.json --resource-group ResourceGroup --deny-settings-mode None --action-on-unmanage deleteResources --ri P5D --stack /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/ResourceGroup/providers/Microsoft.Resources/deploymentStacks/mySubStack + text: az stack-whatif group create --name ResultName --template-file azuredeploy.json --parameters @params.json --parameters https://mysite/params.json --parameters MyValue=This MyArray=@array.json --resource-group ResourceGroup --deny-settings-mode None --action-on-unmanage deleteResources --ri P5D --stack /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/ResourceGroup/providers/Microsoft.Resources/deploymentStacks/mySubStack - name: Perform a what-if on a deployment stack from a local template, using deny settings. - text: az stack-whatif group create --name StackName --resource-group ResourceGroup --template-file azuredeploy.json --deny-settings-mode denyDelete --deny-settings-excluded-actions Microsoft.Compute/virtualMachines/write --deny-settings-excluded-principals "test1 test2" --action-on-unmanage deleteResources --ri P5D --stack /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/ResourceGroup/providers/Microsoft.Resources/deploymentStacks/mySubStack + text: az stack-whatif group create --name ResultName --resource-group ResourceGroup --template-file azuredeploy.json --deny-settings-mode denyDelete --deny-settings-excluded-actions Microsoft.Compute/virtualMachines/write --deny-settings-excluded-principals "test1 test2" --action-on-unmanage deleteResources --ri P5D --stack /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/ResourceGroup/providers/Microsoft.Resources/deploymentStacks/mySubStack - name: Perform a what-if on a deployment stack from a local template, apply deny setting to child scopes. - text: az stack-whatif group create --name StackName --resource-group ResourceGroup --template-file azuredeploy.json --deny-settings-mode denyDelete --deny-settings-excluded-actions Microsoft.Compute/virtualMachines/write --deny-settings-apply-to-child-scopes --action-on-unmanage deleteResources --ri P5D --stack /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/ResourceGroup/providers/Microsoft.Resources/deploymentStacks/mySubStack + text: az stack-whatif group create --name ResultName --resource-group ResourceGroup --template-file azuredeploy.json --deny-settings-mode denyDelete --deny-settings-excluded-actions Microsoft.Compute/virtualMachines/write --deny-settings-apply-to-child-scopes --action-on-unmanage deleteResources --ri P5D --stack /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/ResourceGroup/providers/Microsoft.Resources/deploymentStacks/mySubStack +""" + +helps['stack-whatif group list'] = """ +type: command +short-summary: List all deployment stack what-if results in a resource group. +examples: + - name: List all stack what-if results in a resource group. + text: az stack-whatif group list --resource-group ResourceGroup +""" + +helps['stack-whatif group show'] = """ +type: command +short-summary: Get a deployment stack what-if result from resource group scope. +examples: + - name: Get a stack what-if result by name. + text: az stack-whatif group show --name ResultName --resource-group ResourceGroup + - name: Get a stack what-if result by name without color. + text: az stack-whatif group show --name ResultName --resource-group ResourceGroup --no-color + - name: Get JSON for a stack what-if result by name. + text: az stack-whatif group show --name ResultName --resource-group ResourceGroup --no-pretty-print + - name: Get a stack what-if result by resource id. + text: az stack-whatif group show --id /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/ResourceGroup/providers/Microsoft.Resources/deploymentStacksWhatIfResults/ResultName +""" + +helps['stack-whatif group delete'] = """ +type: command +short-summary: Delete a deployment stack what-if result from resource group scope. +examples: + - name: Delete stack what-if result by name. + text: az stack-whatif group delete --name ResultName --resource-group ResourceGroup + - name: Delete stack what-if result by resource id. + text: az stack-whatif group delete --id /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/ResourceGroup/providers/Microsoft.Resources/deploymentStacksWhatIfResults/ResultName """ helps['bicep generate-params'] = """ From 5c867c32525b102651f0e48463d366c8d869066b Mon Sep 17 00:00:00 2001 From: Kyle Albert <5498623+kalbert312@users.noreply.github.com> Date: Tue, 3 Mar 2026 11:40:31 -0500 Subject: [PATCH 30/32] Adjust help entries. Fix some params on delete. --- .../cli/command_modules/resource/_params.py | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/resource/_params.py b/src/azure-cli/azure/cli/command_modules/resource/_params.py index ef6526eb398..d5bbb4be0df 100644 --- a/src/azure-cli/azure/cli/command_modules/resource/_params.py +++ b/src/azure-cli/azure/cli/command_modules/resource/_params.py @@ -630,18 +630,20 @@ def load_arguments(self, _): continue with self.argument_context(f'{resource_type} {scope} {action}') as c: + entity_type = "deployment stack what-if result" if resource_type == 'stack-whatif' else "deployment stack" + if action == 'create' or action == 'validate': - c.argument('name', arg_type=stacks_name_type) + c.argument('name', arg_type=stacks_name_type, help=f'The name of the {entity_type}.') if scope == 'group': - c.argument('resource_group', arg_type=resource_group_name_type, help='The resource group where the deployment stack will be created.') + c.argument('resource_group', arg_type=resource_group_name_type, help=f'The resource group where the {entity_type} will be created.') elif scope == 'sub': c.argument('deployment_resource_group', arg_type=stacks_stack_deployment_resource_group) elif scope == 'mg': c.argument('deployment_subscription', arg_type=stacks_stack_deployment_subscription) if scope != 'group': - c.argument('location', arg_type=get_location_type(self.cli_ctx), help='The location to store deployment stack.') + c.argument('location', arg_type=get_location_type(self.cli_ctx), help=f'The location to store the {entity_type}.') c.argument('template_file', arg_type=deployment_template_file_type) c.argument('template_spec', arg_type=deployment_template_spec_type) @@ -666,23 +668,24 @@ def load_arguments(self, _): c.argument('no_pretty_print', arg_type=deployment_what_if_no_pretty_print_type) c.argument('no_color', arg_type=deployment_what_if_no_color_type) if action == 'create' and resource_type == 'stack': - c.argument('yes', help='Do not prompt for confirmation') + c.argument('yes', help='Do not prompt for confirmation.') elif action == 'delete': - c.argument('name', options_list=['--name', '-n'], arg_type=stacks_stack_name_type) - c.argument('id', arg_type=stacks_stack_type) + c.argument('name', options_list=['--name', '-n'], arg_type=stacks_stack_name_type, help=f'The name of the {entity_type}.') + c.argument('id', arg_type=stacks_stack_type, help=f'The {entity_type} resource ID.') c.argument('subscription', arg_type=subscription_type) - c.argument('action_on_unmanage', arg_type=stacks_action_on_unmanage_type) - c.argument('resources_without_delete_support', arg_type=stacks_resources_without_delete_support_type) - c.argument('bypass_stack_out_of_sync_error', arg_type=stacks_bypass_stack_out_of_sync_error_type) - c.argument('yes', help='Do not prompt for confirmation') + c.argument('yes', help='Do not prompt for confirmation.') if scope == 'group': - c.argument('resource_group', arg_type=resource_group_name_type, help='The resource group where the deployment stack exists') + c.argument('resource_group', arg_type=resource_group_name_type, help=f'The resource group where the {entity_type} exists.') + if resource_type == 'stack': + c.argument('action_on_unmanage', arg_type=stacks_action_on_unmanage_type) + c.argument('resources_without_delete_support', arg_type=stacks_resources_without_delete_support_type) + c.argument('bypass_stack_out_of_sync_error', arg_type=stacks_bypass_stack_out_of_sync_error_type) elif action == 'show' or action == 'export': - c.argument('name', options_list=['--name', '-n'], arg_type=stacks_stack_name_type) - c.argument('id', arg_type=stacks_stack_type) + c.argument('name', options_list=['--name', '-n'], arg_type=stacks_stack_name_type, help=f'The name of the {entity_type}.') + c.argument('id', arg_type=stacks_stack_type, help=f'The {entity_type} resource ID.') c.argument('subscription', arg_type=subscription_type) if scope == 'group': - c.argument('resource_group', arg_type=resource_group_name_type, help='The resource group where the deployment stack exists') + c.argument('resource_group', arg_type=resource_group_name_type, help=f'The resource group where the {entity_type} exists.') if resource_type == 'stack-whatif': c.argument('no_pretty_print', arg_type=deployment_what_if_no_pretty_print_type) c.argument('no_color', arg_type=deployment_what_if_no_color_type) @@ -691,7 +694,7 @@ def load_arguments(self, _): continue # only uses global arguments c.argument('subscription', arg_type=subscription_type) if scope == 'group': - c.argument('resource_group', arg_type=resource_group_name_type, help='The resource group where the deployment stack exists') + c.argument('resource_group', arg_type=resource_group_name_type, help=f'The resource group where the {entity_type} exists.') with self.argument_context('bicep build') as c: c.argument('file', arg_type=bicep_file_type, help="The path to the Bicep file to build in the file system.") From 971894551d5481a22ab28da711135258aeef04af Mon Sep 17 00:00:00 2001 From: Kyle Albert <5498623+kalbert312@users.noreply.github.com> Date: Tue, 3 Mar 2026 12:13:24 -0500 Subject: [PATCH 31/32] Style fixes. --- .../azure/cli/command_modules/resource/_help.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/resource/_help.py b/src/azure-cli/azure/cli/command_modules/resource/_help.py index acf0475fbff..37db5459af8 100644 --- a/src/azure-cli/azure/cli/command_modules/resource/_help.py +++ b/src/azure-cli/azure/cli/command_modules/resource/_help.py @@ -2563,19 +2563,19 @@ short-summary: Create a deployment stack what-if result at management group scope. examples: - name: Perform a what-if on a deployment stack using template file and detach all unmanaged resources. - text: az stack-whatif mg create --name StackName --management-group-id myMg --template-file simpleTemplate.json --parameters simpleTemplateParams.json --location westus2 --description description --deny-settings-mode None --action-on-unmanage detachAll --ri P5D --stack /providers/Microsoft.Management/myMg/providers/Microsoft.Resources/deploymentStacks/myMgStack + text: az stack-whatif mg create --name StackName --management-group-id myMg --template-file simpleTemplate.json --parameters simpleTemplateParams.json --location westus2 --description description --deny-settings-mode None --action-on-unmanage detachAll --ri P5D --stack /providers/Microsoft.Management/myMg/providers/Microsoft.Resources/deploymentStacks/myMgStack - name: Perform a what-if on a deployment stack with template spec. - text: az stack-whatif mg create --name StackName --management-group-id myMg --template-spec TemplateSpecResourceIDWithVersion --location westus2 --description description --deny-settings-mode None --action-on-unmanage deleteResources --ri P5D --stack /providers/Microsoft.Management/myMg/providers/Microsoft.Resources/deploymentStacks/myMgStack + text: az stack-whatif mg create --name StackName --management-group-id myMg --template-spec TemplateSpecResourceIDWithVersion --location westus2 --description description --deny-settings-mode None --action-on-unmanage deleteResources --ri P5D --stack /providers/Microsoft.Management/myMg/providers/Microsoft.Resources/deploymentStacks/myMgStack - name: Perform a what-if on a deployment stack using bicep file, delete all resources on unmanage, and remove color from the output. - text: az stack-whatif mg create --name StackName --management-group-id myMg --no-color --action-on-unmanage deleteAll --template-file simple.bicep --location westus2 --description description --deny-settings-mode None --ri P5D --stack /providers/Microsoft.Management/myMg/providers/Microsoft.Resources/deploymentStacks/myMgStack + text: az stack-whatif mg create --name StackName --management-group-id myMg --no-color --action-on-unmanage deleteAll --template-file simple.bicep --location westus2 --description description --deny-settings-mode None --ri P5D --stack /providers/Microsoft.Management/myMg/providers/Microsoft.Resources/deploymentStacks/myMgStack - name: Perform a what-if on a deployment stack using parameters from key/value pairs. - text: az stack-whatif mg create --name StackName --management-group-id myMg --template-file simpleTemplate.json --location westus --description description --parameters simpleTemplateParams.json value1=foo value2=bar --deny-settings-mode None --action-on-unmanage deleteResources --ri P5D --stack /providers/Microsoft.Management/myMg/providers/Microsoft.Resources/deploymentStacks/myMgStack + text: az stack-whatif mg create --name StackName --management-group-id myMg --template-file simpleTemplate.json --location westus --description description --parameters simpleTemplateParams.json value1=foo value2=bar --deny-settings-mode None --action-on-unmanage deleteResources --ri P5D --stack /providers/Microsoft.Management/myMg/providers/Microsoft.Resources/deploymentStacks/myMgStack - name: Perform a what-if on a deployment stack from a local template, using a parameter file, a remote parameter file, and selectively overriding key/value pairs. - text: az stack-whatif mg create --name StackName --management-group-id myMg --template-file azuredeploy.json --parameters @params.json --parameters https://mysite/params.json --parameters MyValue=This MyArray=@array.json --location westus --deny-settings-mode None --action-on-unmanage deleteResources --ri P5D --stack /providers/Microsoft.Management/myMg/providers/Microsoft.Resources/deploymentStacks/myMgStack + text: az stack-whatif mg create --name StackName --management-group-id myMg --template-file azuredeploy.json --parameters @params.json --parameters https://mysite/params.json --parameters MyValue=This MyArray=@array.json --location westus --deny-settings-mode None --action-on-unmanage deleteResources --ri P5D --stack /providers/Microsoft.Management/myMg/providers/Microsoft.Resources/deploymentStacks/myMgStack - name: Perform a what-if on a deployment stack from a local template, using deny settings. - text: az stack-whatif mg create --name StackName --management-group-id myMg --template-file azuredeploy.json --deny-settings-mode denyDelete --deny-settings-excluded-actions Microsoft.Compute/virtualMachines/write --deny-settings-excluded-principals "test1 test2" --location westus --action-on-unmanage deleteResources --ri P5D --stack /providers/Microsoft.Management/myMg/providers/Microsoft.Resources/deploymentStacks/myMgStack + text: az stack-whatif mg create --name StackName --management-group-id myMg --template-file azuredeploy.json --deny-settings-mode denyDelete --deny-settings-excluded-actions Microsoft.Compute/virtualMachines/write --deny-settings-excluded-principals "test1 test2" --location westus --action-on-unmanage deleteResources --ri P5D --stack /providers/Microsoft.Management/myMg/providers/Microsoft.Resources/deploymentStacks/myMgStack - name: Perform a what-if on a deployment stack from a local template, apply deny settings to child scope. - text: az stack-whatif mg create --name StackName --management-group-id myMg --template-file azuredeploy.json --deny-settings-mode denyDelete --deny-settings-excluded-actions Microsoft.Compute/virtualMachines/write --deny-settings-apply-to-child-scopes --location westus --action-on-unmanage deleteResources --ri P5D --stack /providers/Microsoft.Management/myMg/providers/Microsoft.Resources/deploymentStacks/myMgStack + text: az stack-whatif mg create --name StackName --management-group-id myMg --template-file azuredeploy.json --deny-settings-mode denyDelete --deny-settings-excluded-actions Microsoft.Compute/virtualMachines/write --deny-settings-apply-to-child-scopes --location westus --action-on-unmanage deleteResources --ri P5D --stack /providers/Microsoft.Management/myMg/providers/Microsoft.Resources/deploymentStacks/myMgStack """ helps['stack-whatif mg list'] = """ @@ -2905,7 +2905,7 @@ - name: Get a stack what-if result by name without color. text: az stack-whatif group show --name ResultName --resource-group ResourceGroup --no-color - name: Get JSON for a stack what-if result by name. - text: az stack-whatif group show --name ResultName --resource-group ResourceGroup --no-pretty-print + text: az stack-whatif group show --name ResultName --resource-group ResourceGroup --no-pretty-print - name: Get a stack what-if result by resource id. text: az stack-whatif group show --id /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/ResourceGroup/providers/Microsoft.Resources/deploymentStacksWhatIfResults/ResultName """ From 40112c268087bd87cd6116e7e5f23b2d1090092b Mon Sep 17 00:00:00 2001 From: Kyle Albert <5498623+kalbert312@users.noreply.github.com> Date: Tue, 3 Mar 2026 12:52:00 -0500 Subject: [PATCH 32/32] Fix management group ID parameter. --- .../azure/cli/command_modules/resource/_params.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/resource/_params.py b/src/azure-cli/azure/cli/command_modules/resource/_params.py index d5bbb4be0df..d6d96ca4684 100644 --- a/src/azure-cli/azure/cli/command_modules/resource/_params.py +++ b/src/azure-cli/azure/cli/command_modules/resource/_params.py @@ -620,9 +620,6 @@ def load_arguments(self, _): with self.argument_context('ts list') as c: c.argument('resource_group', arg_type=resource_group_name_type) - with self.argument_context('stack mg') as c: - c.argument('management_group_id', arg_type=management_group_id_type, help='The management group id to create stack at.') - for resource_type in ['stack', 'stack-whatif']: for scope in ['group', 'sub', 'mg']: for action in ['create', 'validate', 'delete', 'show', 'list', 'export']: @@ -632,6 +629,11 @@ def load_arguments(self, _): with self.argument_context(f'{resource_type} {scope} {action}') as c: entity_type = "deployment stack what-if result" if resource_type == 'stack-whatif' else "deployment stack" + if scope == 'group': + c.argument('resource_group', arg_type=resource_group_name_type, help=f'The resource group where the {entity_type} exists.') + elif scope == 'mg': + c.argument('management_group_id', arg_type=management_group_id_type, help=f'The management group ID to create a {entity_type} in.') + if action == 'create' or action == 'validate': c.argument('name', arg_type=stacks_name_type, help=f'The name of the {entity_type}.') @@ -674,8 +676,6 @@ def load_arguments(self, _): c.argument('id', arg_type=stacks_stack_type, help=f'The {entity_type} resource ID.') c.argument('subscription', arg_type=subscription_type) c.argument('yes', help='Do not prompt for confirmation.') - if scope == 'group': - c.argument('resource_group', arg_type=resource_group_name_type, help=f'The resource group where the {entity_type} exists.') if resource_type == 'stack': c.argument('action_on_unmanage', arg_type=stacks_action_on_unmanage_type) c.argument('resources_without_delete_support', arg_type=stacks_resources_without_delete_support_type) @@ -684,8 +684,6 @@ def load_arguments(self, _): c.argument('name', options_list=['--name', '-n'], arg_type=stacks_stack_name_type, help=f'The name of the {entity_type}.') c.argument('id', arg_type=stacks_stack_type, help=f'The {entity_type} resource ID.') c.argument('subscription', arg_type=subscription_type) - if scope == 'group': - c.argument('resource_group', arg_type=resource_group_name_type, help=f'The resource group where the {entity_type} exists.') if resource_type == 'stack-whatif': c.argument('no_pretty_print', arg_type=deployment_what_if_no_pretty_print_type) c.argument('no_color', arg_type=deployment_what_if_no_color_type) @@ -693,8 +691,6 @@ def load_arguments(self, _): if scope == 'sub': continue # only uses global arguments c.argument('subscription', arg_type=subscription_type) - if scope == 'group': - c.argument('resource_group', arg_type=resource_group_name_type, help=f'The resource group where the {entity_type} exists.') with self.argument_context('bicep build') as c: c.argument('file', arg_type=bicep_file_type, help="The path to the Bicep file to build in the file system.")