From cd581b878dfeed2edf72647f6bdc4b03f1f3bba5 Mon Sep 17 00:00:00 2001 From: Shreyas Gopalakrishna Date: Tue, 29 Sep 2026 11:25:05 -0700 Subject: [PATCH] [AppService] Fix #33732: Restore appServicePlanId in webapp output Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../command_modules/appservice/commands.py | 62 ++++++++++++++++--- .../tests/latest/test_webapp_commands.py | 7 ++- .../latest/test_webapp_commands_thru_mock.py | 58 ++++++++++++++++- 3 files changed, 118 insertions(+), 9 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/appservice/commands.py b/src/azure-cli/azure/cli/command_modules/appservice/commands.py index 3f14d6735b0..cd4aac51c11 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/commands.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/commands.py @@ -32,7 +32,9 @@ def transform_web_output(web): props = ['name', 'state', 'location', 'resourceGroup', 'defaultHostName', 'appServicePlanId', 'ftpPublishingUrl'] result = {k: web[k] for k in web if k in props} # to get width under control, also the plan usually is in the same RG - result['appServicePlan'] = result.pop('appServicePlanId').split('/')[-1] + app_service_plan_id = result.pop('appServicePlanId', None) + if app_service_plan_id is not None: + result['appServicePlan'] = app_service_plan_id.split('/')[-1] return result @@ -40,6 +42,46 @@ def transform_web_list_output(webs): return [transform_web_output(w) for w in webs] +def _transform_site_output(result, remove_server_farm_id): + """Restore appServicePlanId after serializing azure-mgmt-web Site models.""" + from azure.cli.core.util import todict + from azure.cli.core.commands import AzCliCommandInvoker + + result = todict(result, AzCliCommandInvoker.remove_additional_prop_layer) + sites = result if isinstance(result, list) else [result] + for site in sites: + if not isinstance(site, dict): + continue + + app_service_plan_id = site.get('appServicePlanId') + if app_service_plan_id is None: + app_service_plan_id = site.get('serverFarmId') + + properties = site.get('properties') + if app_service_plan_id is None and isinstance(properties, dict): + app_service_plan_id = properties.get('appServicePlanId') + if app_service_plan_id is None: + app_service_plan_id = properties.get('serverFarmId') + + if app_service_plan_id is not None: + site['appServicePlanId'] = app_service_plan_id + + if remove_server_farm_id: + site.pop('serverFarmId', None) + if isinstance(properties, dict): + properties.pop('serverFarmId', None) + + return result + + +def transform_webapp_site_output(result): + return _transform_site_output(result, remove_server_farm_id=True) + + +def transform_site_output(result): + return _transform_site_output(result, remove_server_farm_id=False) + + def transform_runtime_list_output(result): from collections import OrderedDict return [OrderedDict([ @@ -278,8 +320,10 @@ def load_command_table(self, _): deprecate_info=g.deprecate(redirect='webapp create and webapp deploy')) g.custom_command('ssh', 'ssh_webapp', exception_handler=ex_handler_factory(), is_preview=True) g.custom_command('exec', 'webapp_exec', custom_command_type=webapp_exec_custom, exception_handler=ex_handler_factory(), is_preview=True) - g.custom_command('list', 'list_webapp', table_transformer=transform_web_list_output) - g.custom_show_command('show', 'show_app', table_transformer=transform_web_output) + g.custom_command('list', 'list_webapp', transform=transform_webapp_site_output, + table_transformer=transform_web_list_output) + g.custom_show_command('show', 'show_app', transform=transform_webapp_site_output, + table_transformer=transform_web_output) g.custom_command('delete', 'delete_webapp') g.custom_command('stop', 'stop_webapp') g.custom_command('start', 'start_webapp') @@ -533,8 +577,10 @@ def load_command_table(self, _): validator=validate_functionapp) g.custom_command('list-runtimes', 'list_function_app_runtimes') g.custom_command('list-flexconsumption-runtimes', 'list_flex_function_app_runtimes') - g.custom_command('list', 'list_function_app', table_transformer=transform_web_list_output) - g.custom_show_command('show', 'show_functionapp', table_transformer=transform_web_output) + g.custom_command('list', 'list_function_app', transform=transform_site_output, + table_transformer=transform_web_list_output) + g.custom_show_command('show', 'show_functionapp', transform=transform_site_output, + table_transformer=transform_web_output) g.custom_command('delete', 'delete_function_app') g.custom_command('stop', 'stop_webapp') g.custom_command('start', 'start_webapp') @@ -758,8 +804,10 @@ def load_command_table(self, _): with self.command_group('logicapp', custom_command_type=logicapp_custom) as g: g.custom_command('create', 'create_logicapp', exception_handler=ex_handler_factory()) - g.custom_command('list', 'list_logicapp', table_transformer=transform_web_list_output) - g.custom_show_command('show', 'show_logicapp', table_transformer=transform_web_output) + g.custom_command('list', 'list_logicapp', transform=transform_site_output, + table_transformer=transform_web_list_output) + g.custom_show_command('show', 'show_logicapp', transform=transform_site_output, + table_transformer=transform_web_output) g.custom_command('scale', 'scale_logicapp', exception_handler=ex_handler_factory()) with self.command_group('logicapp config appsettings', custom_command_type=logicapp_custom) as g: diff --git a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands.py b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands.py index 33be88cfa99..b0386d71b55 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands.py @@ -48,7 +48,8 @@ def test_webapp_e2e(self, resource_group): webapp_name = self.create_random_name(prefix='webapp-e2e', length=24) plan = self.create_random_name(prefix='webapp-e2e-plan', length=24) - self.cmd('appservice plan create -g {} -n {}'.format(resource_group, plan)) + plan_id = self.cmd( + 'appservice plan create -g {} -n {}'.format(resource_group, plan)).get_output_in_json()['id'] self.cmd('appservice plan list -g {}'.format(resource_group), checks=[ JMESPathCheck('length(@)', 1), JMESPathCheck('[0].name', plan), @@ -81,11 +82,15 @@ def test_webapp_e2e(self, resource_group): self.cmd('webapp list -g {}'.format(resource_group), checks=[ JMESPathCheck('length(@)', 1), JMESPathCheck('[0].name', webapp_name), + JMESPathCheck('[0].appServicePlanId', plan_id), + JMESPathCheckNotExists('[0].serverFarmId'), JMESPathCheck('[0].hostNames[0]', webapp_name + '.azurewebsites.net') ]) self.cmd('webapp show -g {} -n {}'.format(resource_group, webapp_name), checks=[ JMESPathCheck('name', webapp_name), + JMESPathCheck('appServicePlanId', plan_id), + JMESPathCheckNotExists('serverFarmId'), JMESPathCheck('hostNames[0]', webapp_name + '.azurewebsites.net') ]) result = self.cmd('webapp deployment source config-local-git -g {} -n {}'.format( diff --git a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py index ec0e1b13668..2d6bfbec30f 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py @@ -12,6 +12,7 @@ from azure.core.exceptions import HttpResponseError from azure.mgmt.web import WebSiteManagementClient +from azure.mgmt.web.models import Site from knack.output import format_table from knack.util import CLIError, CommandResultItem from azure.cli.core.azclierror import (InvalidArgumentValueError, @@ -56,7 +57,10 @@ create_webapp) from azure.cli.command_modules.appservice.commands import (transform_troubleshoot_config_output, transform_secure_build_output, - transform_troubleshoot_deployment_output) + transform_troubleshoot_deployment_output, + transform_site_output, + transform_webapp_site_output, + transform_web_output) from azure.cli.command_modules.appservice._deployment_context_engine import EnrichedDeploymentError # pylint: disable=line-too-long @@ -75,6 +79,58 @@ def _get_test_cmd(): return cmd +class TestWebappOutputTransformers(unittest.TestCase): + + def test_transform_webapp_site_output_renames_server_farm_id_for_sdk_model(self): + farm_id = '/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Web/serverfarms/plan' + site = Site(location='westus', server_farm_id=farm_id) + + result = transform_webapp_site_output(site) + + self.assertEqual(result['appServicePlanId'], farm_id) + self.assertNotIn('serverFarmId', result) + + def test_transform_webapp_site_output_renames_server_farm_id_from_properties(self): + farm_id = '/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Web/serverfarms/plan' + site = { + 'location': 'westus', + 'properties': { + 'serverFarmId': farm_id, + 'name': 'myapp', + }, + } + + result = transform_webapp_site_output(site) + + self.assertEqual(result['appServicePlanId'], farm_id) + self.assertNotIn('serverFarmId', result['properties']) + + def test_transform_site_output_preserves_server_farm_id_for_other_app_types(self): + farm_id = '/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Web/serverfarms/plan' + sites = [ + {'serverFarmId': farm_id, 'name': 'app1'}, + {'appServicePlanId': farm_id, 'name': 'app2'}, + ] + + result = transform_site_output(sites) + + self.assertEqual([site['appServicePlanId'] for site in result], [farm_id, farm_id]) + self.assertEqual(result[0]['serverFarmId'], farm_id) + + def test_transform_web_output_uses_app_service_plan_name(self): + farm_id = '/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Web/serverfarms/plan' + + result = transform_web_output({'name': 'app', 'appServicePlanId': farm_id}) + + self.assertEqual(result['appServicePlan'], 'plan') + self.assertNotIn('appServicePlanId', result) + + def test_transform_web_output_tolerates_missing_app_service_plan_id(self): + result = transform_web_output({'name': 'app', 'state': 'Running'}) + + self.assertEqual(result, {'name': 'app', 'state': 'Running'}) + + class TestSecureBuildMocked(unittest.TestCase): @mock.patch('azure.cli.core.util.should_disable_connection_verify', return_value=False)