Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 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
2c9307c
Add some coverage for printing arguments.
kalbert312 Mar 3, 2026
1b0f8b0
Fix test issues. Add stack whatif RG recording.
kalbert312 Mar 4, 2026
a24faaa
Merge branch 'dev' into kylea/stacks-what-if
kalbert312 Mar 17, 2026
c7ef104
Merge branch 'dev' into kylea/stacks-what-if
kalbert312 Mar 23, 2026
429de5f
Minimal changes to ignore extensible resources.
kalbert312 Mar 26, 2026
f94a995
Merge branch 'dev' into kylea/stacks-what-if
kalbert312 Mar 27, 2026
e63bbea
Run update index script.
kalbert312 Mar 27, 2026
2829a1a
Merge branch 'dev' into kylea/stacks-what-if
kalbert312 Apr 6, 2026
ce7ce8c
Merge branch 'dev' into kylea/stacks-what-if
kalbert312 Apr 7, 2026
d54ba8e
Change to --stack-id per bug bash.
kalbert312 Apr 7, 2026
b7f1edb
Rerecord RG live test.
kalbert312 Apr 7, 2026
e34539a
Record sub live test.
kalbert312 Apr 7, 2026
463dd5b
Record mg live test.
kalbert312 Apr 7, 2026
c1eaa4e
Apply suggestions from code review
kalbert312 Apr 7, 2026
d6f9e7a
Consistent identifiers printing.
kalbert312 Apr 7, 2026
c5d4d90
Fix style issues.
kalbert312 Apr 9, 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
6 changes: 4 additions & 2 deletions src/azure-cli-core/azure/cli/core/commandIndex.latest.json
Original file line number Diff line number Diff line change
Expand Up @@ -212,8 +212,7 @@
"azure.cli.command_modules.netappfiles"
],
"network": [
"azure.cli.command_modules.network",
"azure.cli.command_modules.privatedns"
"azure.cli.command_modules.network"
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

commandIndex.latest.json drops azure.cli.command_modules.privatedns from the network command group. Since azure/cli/command_modules/privatedns/ still exists, this likely breaks az network private-dns ... command loading under the optimized loader. Please regenerate the command index or re-add the privatedns module to the network entry.

Suggested change
"azure.cli.command_modules.network"
"azure.cli.command_modules.network",
"azure.cli.command_modules.privatedns"

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how this happened. I ran the script to update this.

],
"policy": [
"azure.cli.command_modules.policyinsights",
Expand Down Expand Up @@ -322,6 +321,9 @@
"webapp": [
"azure.cli.command_modules.appservice",
"azure.cli.command_modules.serviceconnector"
],
"stack-whatif": [
"azure.cli.command_modules.resource"
]
}
}
4 changes: 4 additions & 0 deletions src/azure-cli-core/azure/cli/core/helpIndex.latest.json
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,10 @@
"webapp": {
"summary": "Manage web apps.",
"tags": ""
},
"stack-whatif": {
"summary": "A deployment stack What-If is a preview of an operation to be performed on a new or existing deployment stack.",
"tags": ""
}
},
"commands": {
Expand Down
62 changes: 57 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,57 @@ 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.clear()
self._colors.clear()
self._indents.clear()

def _push_color(self, color):
if not self._enable_color:
return
Expand All @@ -63,6 +111,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
66 changes: 51 additions & 15 deletions src/azure-cli/azure/cli/command_modules/resource/_formatters.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,20 @@
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

import typing as t
import json
from itertools import groupby

from azure.mgmt.resource.deployments.models import ChangeType, PropertyChangeType, Level
import azure.mgmt.resource.deployments.models as DeploymentModels

from ._symbol import Symbol
from ._color import Color, ColoredStringBuilder
from ._utils import split_resource_id

ChangeType = DeploymentModels.ChangeType
PropertyChangeType = DeploymentModels.PropertyChangeType
Level = DeploymentModels.Level

_change_type_to_color = {
ChangeType.create: Color.GREEN,
ChangeType.delete: Color.ORANGE,
Expand Down Expand Up @@ -75,7 +81,10 @@
}


def format_what_if_operation_result(what_if_operation_result, enable_color=True):
def format_what_if_operation_result(
what_if_operation_result: DeploymentModels.WhatIfOperationResult,
enable_color=True
):
builder = ColoredStringBuilder(enable_color)
_format_noise_notice(builder)
_format_change_type_legend(builder, what_if_operation_result.changes)
Expand Down Expand Up @@ -106,7 +115,7 @@ def _format_noise_notice(builder):
builder.append_line()


def _format_change_type_legend(builder, resource_changes):
def _format_change_type_legend(builder, resource_changes: t.Optional[list[DeploymentModels.WhatIfChange]]):
if not resource_changes:
return

Expand Down Expand Up @@ -140,7 +149,11 @@ def populate_change_type_set(property_changes):
builder.append_line(change_type.title())


def _format_resource_changes_stats(builder, resource_changes, definite_changes=True):
def _format_resource_changes_stats(
builder,
resource_changes: t.Optional[list[DeploymentModels.WhatIfChange]],
definite_changes=True
):
if definite_changes:
builder.append_line().append("Resource changes: ")

Expand Down Expand Up @@ -208,7 +221,11 @@ def _format_diagnostics(builder, resource_changes, potential_changes, diagnostic
builder.append_line()


def _format_resource_changes(builder, resource_changes, definite_changes=True):
def _format_resource_changes(
builder,
resource_changes: t.Optional[list[DeploymentModels.WhatIfChange]],
definite_changes=True
):
if not resource_changes:
return

Expand Down Expand Up @@ -245,9 +262,12 @@ def _format_resource_changes_in_scope(builder, scope, resource_changes_in_scope)
_format_resource_change(builder, resource_change, is_last)


def _format_resource_change(builder, resource_change, is_last):
change_type = resource_change.change_type
def _format_resource_change(builder, resource_change: DeploymentModels.WhatIfChange, is_last):
relative_resource_id = _get_relative_resource_id(resource_change)
if relative_resource_id is None:
return

change_type = resource_change.change_type
api_version = _get_api_version(resource_change)

builder.append_line()
Expand Down Expand Up @@ -437,26 +457,33 @@ def _format_property_array_change(builder, parent_property_change, property_chan
builder.append(Symbol.RIGHT_SQUARE_BRACKET)


def _get_api_version(resource_change):
def _get_api_version(resource_change: DeploymentModels.WhatIfChange):
if resource_change.before:
return resource_change.before.get("apiVersion")
if resource_change.after:
return resource_change.after.get("apiVersion")
return None


def _get_scope(resource_change):
scope, _ = split_resource_id(resource_change.resource_id)
return scope
def _get_scope(resource_change: DeploymentModels.WhatIfChange):
if resource_change.resource_id:
return split_resource_id(resource_change.resource_id)[0]
if resource_change.extension:
# TODO: eventually this should include the extension config with key=value pairs
return f"{resource_change.extension.name}@{resource_change.extension.version}"
return "Unknown"


def _get_scope_uppercase(resource_change):
def _get_scope_uppercase(resource_change: DeploymentModels.WhatIfChange):
return _get_scope(resource_change).upper()


def _get_relative_resource_id(resource_change):
_, relative_resource_id = split_resource_id(resource_change.resource_id)
return relative_resource_id
def _get_relative_resource_id(resource_change: DeploymentModels.WhatIfChange):
if resource_change.resource_id:
return split_resource_id(resource_change.resource_id)[1]
if resource_change.identifiers:
return _format_ext_resource_identifiers(resource_change.identifiers)
return None


def _get_max_path_length_from_property_changes(property_changes):
Expand Down Expand Up @@ -639,3 +666,12 @@ def _is_non_empty_array(value):

def _is_non_empty_object(value):
return isinstance(value, dict) and value


def _format_ext_resource_identifiers(identifiers: t.Optional[dict[str, t.Any]]) -> str:
if not identifiers:
return ""

sorted_items = sorted(identifiers.items(), key=lambda x: x[0])

return ", ".join(f"{key}={json.dumps(value)}" for key, value in sorted_items)
Loading
Loading