Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
abb2f17
az what-if (save)
MoChilia Sep 11, 2025
46cf88d
az what-if
MoChilia Sep 14, 2025
7bc0289
Add what if feature
wangzelin007 Sep 17, 2025
df108e2
Update src/azure-cli-core/azure/cli/core/commands/__init__.py
wangzelin007 Sep 17, 2025
f126b42
update
MoChilia Sep 18, 2025
08cc6c1
add progress bar and support pretty output
MoChilia Sep 19, 2025
6a1b839
fix style issue
MoChilia Sep 19, 2025
6a52aac
add a mock test
MoChilia Sep 19, 2025
b4aae27
use get_raw_token
MoChilia Sep 19, 2025
b764741
def PropertyChange
MoChilia Sep 22, 2025
c3caf10
update mock test
MoChilia Sep 22, 2025
2b61732
minor fix
wangzelin007 Sep 22, 2025
3f86e44
Update what_if.py
wangzelin007 Sep 22, 2025
499e477
move call what-if to core
MoChilia Sep 22, 2025
e11f6aa
Merge remote-tracking branch 'upstream/dev' into whatif
MoChilia Sep 22, 2025
8d394ba
Merge branch 'whatif' into wzl/add-what-if
MoChilia Sep 22, 2025
6393d35
remove what if command
wangzelin007 Nov 5, 2025
2d75315
minor fix
wangzelin007 Nov 5, 2025
31dab53
minor fix
wangzelin007 Nov 5, 2025
4207bca
add --export-bicep
wangzelin007 Nov 5, 2025
8402c51
minor fix
wangzelin007 Nov 6, 2025
6c3d3bc
Merge branch 'dev' into wzl/add-what-if
wangzelin007 Nov 6, 2025
9c44270
add whitelist
wangzelin007 Nov 11, 2025
7d89ee3
minor fix
wangzelin007 Nov 11, 2025
6bdefb9
minor fix
wangzelin007 Nov 11, 2025
6fd0a35
resource: update What-If noise notice link to GitHub issues
wangzelin007 Nov 17, 2025
45f3dc9
resource: update What-If issue link to the new GitHub issue template
wangzelin007 Nov 19, 2025
0cf60fd
minor fix
wangzelin007 Dec 1, 2025
7cf3c0a
minor fix
wangzelin007 Dec 1, 2025
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
195 changes: 194 additions & 1 deletion src/azure-cli-core/azure/cli/core/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,7 @@ class AzCliCommandInvoker(CommandInvoker):

# pylint: disable=too-many-statements,too-many-locals,too-many-branches
def execute(self, args):
args_copy = args[:]
from knack.events import (EVENT_INVOKER_PRE_CMD_TBL_CREATE, EVENT_INVOKER_POST_CMD_TBL_CREATE,
EVENT_INVOKER_CMD_TBL_LOADED, EVENT_INVOKER_PRE_PARSE_ARGS,
EVENT_INVOKER_POST_PARSE_ARGS,
Expand Down Expand Up @@ -586,7 +587,12 @@ def execute(self, args):
args[0] = '--help'

self.parser.enable_autocomplete()

if '--what-if' in (args_copy):
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

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

The conditional check is redundant. The check if '--what-if' in (args_copy) doesn't need the extra parentheses around args_copy, and this check is performed again inside the _what_if method at line 702. Consider simplifying:

if '--what-if' in args_copy:
    return self._what_if(args_copy)
Suggested change
if '--what-if' in (args_copy):
if '--what-if' in args_copy:

Copilot uses AI. Check for mistakes.
return self._what_if(args_copy)
elif '--export-bicep' in (args_copy):
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

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

The conditional check has redundant parentheses. Should be:

elif '--export-bicep' in args_copy:
Suggested change
elif '--export-bicep' in (args_copy):
elif '--export-bicep' in args_copy:

Copilot uses AI. Check for mistakes.
# --export-bicep must be used with --what-if
logger.error("The --export-bicep parameter must be used together with --what-if")
return CommandResultItem(None, exit_code=1, error=CLIError('The --export-bicep parameter must be used together with --what-if'))
self.cli_ctx.raise_event(EVENT_INVOKER_PRE_PARSE_ARGS, args=args)
parsed_args = self.parser.parse_args(args)
self.cli_ctx.raise_event(EVENT_INVOKER_POST_PARSE_ARGS, command=parsed_args.command, args=parsed_args)
Expand Down Expand Up @@ -691,6 +697,193 @@ def execute(self, args):
table_transformer=self.commands_loader.command_table[parsed_args.command].table_transformer,
is_query_active=self.data['query_active'])

def _what_if(self, args):
logger.debug("_what_if called with command: %s", args)
if '--what-if' in args:
logger.debug("Entering what-if mode")

# Check if command is in whitelist
if not self._is_command_supported_for_what_if(args):
error_msg = ("\"--what-if\" argument is not supported for this command.")
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

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

The error message format is inconsistent. Using f-string formatting with .format() style placeholders mixed in other parts of the code. This error should follow the same pattern as others:

error_msg = "\"--what-if\" argument is not supported for this command."

Or use consistent formatting throughout.

Copilot uses AI. Check for mistakes.
logger.error(error_msg)
telemetry.set_user_fault(summary='what-if-unsupported-command')
telemetry.set_failure(summary='what-if-unsupported-command')
return CommandResultItem(None, exit_code=1, error=CLIError(error_msg))

from azure.cli.core.what_if import show_what_if

# Remove both --what-if and --export-bicep from args for processing
clean_args = [arg for arg in args if arg not in ['--what-if', '--export-bicep']]
command_parts = [arg for arg in clean_args if not arg.startswith('-') and arg != 'az']
command_name = ' '.join(command_parts) if command_parts else 'unknown'

# Check if --export-bicep is present
export_bicep = '--export-bicep' in args
safe_params = AzCliCommandInvoker._extract_parameter_names(args)

telemetry.set_command_details(
command_name + ' --what-if',
self.data.get('output', 'json'),
safe_params
)
telemetry.set_custom_properties('what-if', 'export_bicep', str(export_bicep))

try:
if export_bicep:
logger.debug("Export bicep mode enabled")

# Get subscription ID with priority: --subscription parameter > current login subscription
if '--subscription' in clean_args:
index = clean_args.index('--subscription')
if index + 1 < len(clean_args):
subscription_value = clean_args[index + 1]
subscription_id = subscription_value
else:
from azure.cli.core.commands.client_factory import get_subscription_id
subscription_id = get_subscription_id(self.cli_ctx)
logger.debug("Using current login subscription ID: %s", subscription_id)

clean_args = ["az"] + clean_args if clean_args[0] != 'az' else clean_args
command = " ".join(clean_args)
what_if_result = show_what_if(self.cli_ctx, command, subscription_id=subscription_id, export_bicep=export_bicep)

# Save bicep templates if export_bicep is enabled and bicep_template exists
bicep_files = []
if export_bicep and isinstance(what_if_result, dict) and 'bicep_template' in what_if_result:
bicep_files = self._save_bicep_templates(clean_args, what_if_result['bicep_template'])
what_if_result.pop('bicep_template', None)

# Print bicep file locations if any were saved
if bicep_files:
from azure.cli.core.style import Style, print_styled_text
print_styled_text((Style.WARNING, "\nBicep templates saved to:"))
for file_path in bicep_files:
print_styled_text((Style.WARNING, f" {file_path}"))
print("")
telemetry.set_custom_properties('what-if', 'bicep_files_count', str(len(bicep_files)))

# Ensure output format is set for proper formatting
# Default to 'json' if not already set
if 'output' not in self.cli_ctx.invocation.data or self.cli_ctx.invocation.data['output'] is None:
self.cli_ctx.invocation.data['output'] = 'json'

telemetry.set_success(summary='what-if-completed')

return CommandResultItem(
what_if_result,
table_transformer=None,
is_query_active=self.data.get('query_active', False),
exit_code=0
)
except (CLIError, ValueError, KeyError) as ex:
logger.error("What-if preview failed: %s", str(ex))
telemetry.set_exception(ex, fault_type='what-if-error', summary=str(ex)[:100])
telemetry.set_failure(summary='what-if-failed')
return CommandResultItem(None, exit_code=1,
error=CLIError(f'What-if preview failed: {str(ex)}'))

def _is_command_supported_for_what_if(self, args):
"""Check if the command is in the what-if whitelist

Args:
args: List of command arguments

Returns:
bool: True if command is supported, False otherwise
"""
# Define supported commands for what-if functionality
WHAT_IF_SUPPORTED_COMMANDS = {
'vm create',
'vm update',
'storage account create',
'storage container create',
'storage share create',
'network vnet create',
'network vnet update',
'storage account network-rule add',
'vm disk attach',
'vm disk detach',
'vm nic remove',
'sql server create',
'sql server update',
}
Comment on lines +794 to +809
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

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

The hardcoded list of supported commands should be maintained in a centralized configuration file or constant at the module level rather than buried in this method. This makes it easier to maintain and update the list of supported commands.

Consider moving this to a module-level constant:

# At module level
WHAT_IF_SUPPORTED_COMMANDS = {
    'vm create',
    'vm update',
    # ... rest of commands
}
Suggested change
# Define supported commands for what-if functionality
WHAT_IF_SUPPORTED_COMMANDS = {
'vm create',
'vm update',
'storage account create',
'storage container create',
'storage share create',
'network vnet create',
'network vnet update',
'storage account network-rule add',
'vm disk attach',
'vm disk detach',
'vm nic remove',
'sql server create',
'sql server update',
}
# Supported commands for what-if functionality (module-level constant)
WHAT_IF_SUPPORTED_COMMANDS = {
'vm create',
'vm update',
'storage account create',
'storage container create',
'storage share create',
'network vnet create',
'network vnet update',
'storage account network-rule add',
'vm disk attach',
'vm disk detach',
'vm nic remove',
'sql server create',
'sql server update',
}
# Define supported commands for what-if functionality
# Use module-level WHAT_IF_SUPPORTED_COMMANDS

Copilot uses AI. Check for mistakes.

# Extract command parts (skip 'az' and flags)
command_parts = []
for arg in args:
if arg == 'az':
continue
if arg.startswith('-'):
break
command_parts.append(arg)

# Join command parts to form the command string
if command_parts:
command = ' '.join(command_parts)
logger.debug("Checking what-if support for command: %s", command)
return command in WHAT_IF_SUPPORTED_COMMANDS

return False

def _save_bicep_templates(self, args, bicep_template):
"""Save bicep templates to user's .azure directory
Returns a list of saved file paths
"""
saved_files = []
try:
import os
from datetime import datetime
from azure.cli.core._environment import get_config_dir

# Extract command name (first argument after 'az')
command_parts = [arg for arg in args if not arg.startswith('-') and arg != 'az']
if not command_parts:
logger.warning("Could not determine command name for bicep file naming")
return saved_files

first_command = command_parts[0]
az_command = f"az_{first_command}"

# Get full command for file naming (e.g., az_vm_create)
if len(command_parts) > 1:
full_command = f"az_{command_parts[0]}_{command_parts[1]}"
else:
full_command = az_command + "_command"

# Create timestamp in yyyymmddhhMMss format
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")

# Get .azure config directory
config_dir = get_config_dir()
whatif_dir = os.path.join(config_dir, 'whatif', az_command)

# Create directories if they don't exist
os.makedirs(whatif_dir, exist_ok=True)
logger.debug("Created bicep template directory: %s", whatif_dir)

# Save main template
if 'main_template' in bicep_template:
main_file = os.path.join(whatif_dir, f"{full_command}_main_{timestamp}.bicep")
with open(main_file, 'w', encoding='utf-8') as f:
f.write(bicep_template['main_template'])
logger.debug("Bicep main template saved to: %s", main_file)
saved_files.append(main_file)

# Save module templates if they exist
if 'module_templates' in bicep_template and bicep_template['module_templates']:
for i, module_template in enumerate(bicep_template['module_templates'], 1):
module_suffix = f"module{i}" if i > 1 else "module"
module_file = os.path.join(whatif_dir, f"{full_command}_{module_suffix}_{timestamp}.bicep")
with open(module_file, 'w', encoding='utf-8') as f:
f.write(module_template)
logger.debug("Bicep module template saved to: %s", module_file)
saved_files.append(module_file)

except Exception as ex:
logger.warning("Failed to save bicep templates: %s", str(ex))

return saved_files

@staticmethod
def _extract_parameter_names(args):
# note: name start with more than 2 '-' will be treated as value e.g. certs in PEM format
Expand Down
20 changes: 20 additions & 0 deletions src/azure-cli-core/azure/cli/core/commands/parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,26 @@ def get_location_type(cli_ctx):
return location_type


def get_what_if_type():
what_if_type = CLIArgumentType(
options_list=['--what-if'],
help="Preview the changes that will be made without actually executing the command. "
"This will call the what-if service to compare the current state with the expected state after execution.",
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

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

[nitpick] Documentation issue: The help text doesn't specify the argument type (boolean) or default value. Consider making it more explicit:

help="Preview the changes that will be made without actually executing the command. "
     "This will call the what-if service to compare the current state with the expected state after execution. "
     "Default: False",
Suggested change
"This will call the what-if service to compare the current state with the expected state after execution.",
"This will call the what-if service to compare the current state with the expected state after execution. "
"Type: boolean. Default: False.",

Copilot uses AI. Check for mistakes.
is_preview=True
)
return what_if_type


def get_export_bicep_type():
export_bicep_type = CLIArgumentType(
options_list=['--export-bicep'],
help="Export the Bicep template corresponding to the what-if analysis. "
"This parameter must be used together with --what-if.",
is_preview=True
)
return export_bicep_type


deployment_name_type = CLIArgumentType(
help=argparse.SUPPRESS,
required=False,
Expand Down
Loading
Loading