Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
e9c13be
Add stack-whatif commands.
kalbert312 Feb 20, 2026
a7ba6d2
Add no pretty print argument.
kalbert312 Feb 20, 2026
5202227
Add boilerplate for print deployment stack what-if results.
kalbert312 Feb 20, 2026
c3bb51b
Implement legend output.
kalbert312 Feb 23, 2026
fdcea30
Progress on what-if formatting.
kalbert312 Feb 23, 2026
04b91ef
Add --no-color parameter.
kalbert312 Feb 23, 2026
7a08708
What-if output progress.
kalbert312 Feb 24, 2026
098994f
What-if resource change progress.
kalbert312 Feb 24, 2026
bbea709
What-if potential resource change progress.
kalbert312 Feb 25, 2026
31db7ab
What-if resource deletion summary progress.
kalbert312 Feb 25, 2026
2cb3c3b
Add indents automatically.
kalbert312 Feb 25, 2026
2018396
Type hints.
kalbert312 Feb 25, 2026
158e79b
Diagnostics formatting progress.
kalbert312 Feb 25, 2026
9243a11
Fix type logic.
kalbert312 Feb 25, 2026
9d9d64f
Fix count.
kalbert312 Feb 25, 2026
376f3ac
Split into methods.
kalbert312 Feb 26, 2026
cd5c818
Extensible resource output support.
kalbert312 Feb 26, 2026
38a237c
Array children rendering fixes and misc refactors.
kalbert312 Feb 26, 2026
ddc1813
Array children rendering fixes and misc refactors.
kalbert312 Feb 26, 2026
1683673
Remove redundant "change" label.
kalbert312 Feb 27, 2026
0870961
Add exclusions for wait command + rg. Is consistent with stack commands.
kalbert312 Feb 27, 2026
8a460a7
Fix lint errors. Add resource class formatting method.
kalbert312 Feb 27, 2026
679778c
Update extensible resource display.
kalbert312 Feb 27, 2026
a3b1e91
Lint fixes.
kalbert312 Feb 27, 2026
11a4e68
Add unit test for stacks what-if formatter.
kalbert312 Mar 2, 2026
d75a2d1
Add live test class for deployment stack what-ifs.
kalbert312 Mar 2, 2026
5fd01a6
Add stack-whatif to service_name.json
kalbert312 Mar 2, 2026
5cbbac2
Add missing required params in the stack-whatif help examples.
kalbert312 Mar 2, 2026
7f68947
Add missing stack-whatif help entries and adjust examples & descripti…
kalbert312 Mar 2, 2026
5c867c3
Adjust help entries. Fix some params on delete.
kalbert312 Mar 3, 2026
9718945
Style fixes.
kalbert312 Mar 3, 2026
0077f9a
Merge branch 'dev' into kylea/stacks-what-if
kalbert312 Mar 3, 2026
40112c2
Fix management group ID parameter.
kalbert312 Mar 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 55 additions & 5 deletions src/azure-cli/azure/cli/command_modules/resource/_color.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -26,11 +27,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 self._should_indent():
self._contents.append(''.join(self._indents))

if color:
self._push_color(color)

Expand All @@ -41,14 +46,55 @@ def append(self, value, color=None):

return self

def append_line(self, value="", color=None):
self.append(f"{str(value)}\n", color)

return self
def append_line(self, value="", color=None, no_indent=False):
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, no_indent=False):
if color and self._enable_color:
self._contents.insert(index, str(Color.RESET))

self._contents.insert(index, str(value))

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, 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)

def push_indent(self, indent):
self._indents.append(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 = []

def _push_color(self, color):
if not self._enable_color:
return
Expand All @@ -63,6 +109,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"))

# pylint: disable=protected-access
class ColorScope:
def __init__(self, color_string_builder, color):
Expand Down
191 changes: 189 additions & 2 deletions src/azure-cli/azure/cli/command_modules/resource/_help.py

Large diffs are not rendered by default.

51 changes: 28 additions & 23 deletions src/azure-cli/azure/cli/command_modules/resource/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -618,29 +620,32 @@ 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.')

# 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'):
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 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)
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)
Expand All @@ -662,30 +667,30 @@ 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)
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')
if scope == 'group':
c.argument('resource_group', arg_type=resource_group_name_type, help='The resource group where the deployment stack exists')
c.argument('yes', help='Do not prompt for confirmation.')
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')
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
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')

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.")
Expand Down
Loading