From 32b4ea2c91a7cf292f6ee6b1b43474ad8bdbffdb Mon Sep 17 00:00:00 2001 From: Andrew Westgarth Date: Mon, 21 Sep 2026 19:27:19 +0100 Subject: [PATCH 1/2] Fix Windows defaults for App Service managed instance plans Default App Service plans to Windows when --is-managed-instance is specified without --is-linux. Reject unsupported argument combinations: - --is-managed-instance with --is-linux true - --hyper-v with --is-managed-instance Update parameter help and add unit tests covering the managed instance Windows default and mutual-exclusion validation. --- .../cli/command_modules/appservice/_params.py | 11 ++++-- .../cli/command_modules/appservice/custom.py | 15 ++++++-- .../latest/test_webapp_commands_thru_mock.py | 37 +++++++++++++++++++ 3 files changed, 56 insertions(+), 7 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/appservice/_params.py b/src/azure-cli/azure/cli/command_modules/appservice/_params.py index 675d17ee4de..d629465e75b 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/_params.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/_params.py @@ -135,9 +135,12 @@ def load_arguments(self, _): local_context_attribute=LocalContextAttribute(name='ase_name', actions=[LocalContextAction.GET])) c.argument('sku', arg_type=sku_arg_type) c.argument('is_linux', arg_type=get_three_state_flag(), default=None, required=False, - help='Host web app on Linux worker. Defaults to true unless --hyper-v is specified. ' + help='Host web app on Linux worker. Defaults to true unless --hyper-v or ' + '--is-managed-instance is specified. ' 'Use "--is-linux false" to create a Windows plan.') - c.argument('hyper_v', action='store_true', required=False, help='Host Windows Container Web App on Hyper-V worker.') + c.argument('hyper_v', action='store_true', required=False, + help='Host Windows Container Web App on Hyper-V worker. Cannot be used with ' + '--is-managed-instance.') c.argument('per_site_scaling', action='store_true', required=False, help='Enable per-app scaling at the ' 'App Service plan level to allow for ' 'scaling an app independently from ' @@ -145,7 +148,9 @@ def load_arguments(self, _): c.argument('zone_redundant', options_list=['--zone-redundant', '-z'], help='Enable zone redundancy for high availability. Minimum instance count is 2.') c.argument('tags', arg_type=tags_type) c.argument('async_scaling_enabled', arg_type=get_three_state_flag(), help='Enables async scaling for the app service plan. Set to "true" to create an async operation if there are insufficient workers to scale synchronously. The SKU must be Dedicated.') - c.argument('is_managed_instance', action='store_true', help='host web app on managed instance') + c.argument('is_managed_instance', action='store_true', + help='Host web app on Managed Instance. Managed Instance supports Windows plans only and ' + 'cannot be used with --hyper-v.') c.argument('mi_system_assigned', arg_type=get_three_state_flag(), help="Enable system-assigned managed identity for this app service plan.") diff --git a/src/azure-cli/azure/cli/command_modules/appservice/custom.py b/src/azure-cli/azure/cli/command_modules/appservice/custom.py index 9601d262676..45f0b862775 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -5462,11 +5462,18 @@ def create_app_service_plan(cmd, resource_group_name, name, is_linux, hyper_v, p default_identity=None, rdp_enabled=None, vnet=None, subnet=None, registry_adapters=None, install_scripts=None, storage_mounts=None, enriched_errors=False): + if hyper_v and is_managed_instance: + raise MutuallyExclusiveArgumentError( + '--hyper-v and --is-managed-instance cannot be used together because Windows Containers ' + 'are not supported in Managed Instance on Azure App Service.') + if is_linux is None: - is_linux = not hyper_v - elif is_linux and hyper_v: - raise MutuallyExclusiveArgumentError('--hyper-v creates a Windows container plan and cannot be combined ' - 'with --is-linux true. Omit --is-linux or use "--is-linux false".') + is_linux = not (hyper_v or is_managed_instance) + elif is_linux and (hyper_v or is_managed_instance): + windows_plan_argument = '--hyper-v' if hyper_v else '--is-managed-instance' + raise MutuallyExclusiveArgumentError( + '{} creates a Windows plan and cannot be combined with --is-linux true. ' + 'Omit --is-linux or use "--is-linux false".'.format(windows_plan_argument)) if sku is None: sku = 'P0V3' if is_linux else 'B1' 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 317283d8839..3ca6428f9c2 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 @@ -1767,6 +1767,43 @@ def test_is_linux_false_creates_windows_plan(self, mock_location, mock_client_fa call_kwargs = mock_app_service_plan_cls.call_args self.assertIn('reserved=False', str(call_kwargs)) + @mock.patch('azure.cli.command_modules.appservice.custom.web_client_factory') + @mock.patch('azure.cli.command_modules.appservice.custom._get_location_from_resource_group', return_value='eastus') + def test_managed_instance_defaults_to_windows(self, mock_location, mock_client_factory): + """When --is-managed-instance is specified, an omitted --is-linux defaults to false.""" + from azure.cli.command_modules.appservice.custom import create_app_service_plan + mock_cmd = mock.MagicMock() + mock_app_service_plan_cls = mock.MagicMock() + mock_cmd.get_models.return_value = (mock.MagicMock(), mock.MagicMock(), mock_app_service_plan_cls) + mock_cmd.cli_ctx = mock.MagicMock() + mock_client_factory.return_value = mock.MagicMock() + + try: + create_app_service_plan(mock_cmd, 'rg', 'plan', is_linux=None, hyper_v=False, + is_managed_instance=True) + except Exception: + pass + + mock_app_service_plan_cls.assert_called() + self.assertIn('reserved=False', str(mock_app_service_plan_cls.call_args)) + + def test_managed_instance_rejects_linux(self): + """Managed Instance on App Service supports only Windows plans.""" + from azure.cli.command_modules.appservice.custom import create_app_service_plan + + with self.assertRaisesRegex(MutuallyExclusiveArgumentError, '--is-managed-instance creates a Windows plan'): + create_app_service_plan(mock.MagicMock(), 'rg', 'plan', is_linux=True, hyper_v=False, + is_managed_instance=True) + + def test_managed_instance_rejects_hyper_v(self): + """Managed Instance on App Service does not support Windows Containers.""" + from azure.cli.command_modules.appservice.custom import create_app_service_plan + + with self.assertRaisesRegex(MutuallyExclusiveArgumentError, + '--hyper-v and --is-managed-instance cannot be used together'): + create_app_service_plan(mock.MagicMock(), 'rg', 'plan', is_linux=None, hyper_v=True, + is_managed_instance=True) + class TestOneDeployScmCache(unittest.TestCase): """Tests for the per-invocation SCM URL / SCM headers cache on OneDeployParams. From 4a15ce0c4a26f9c8f96c15831b0fb4b8672d4ea3 Mon Sep 17 00:00:00 2001 From: Andrew Westgarth Date: Mon, 21 Sep 2026 20:14:59 +0100 Subject: [PATCH 2/2] Fix OS validation for managed instance App Service plans Default App Service plans to Windows when --is-managed-instance is specified without --is-linux. Update both the command validator and create handler to reject unsupported argument combinations: - --is-managed-instance with --is-linux true - --hyper-v with --is-managed-instance - --hyper-v with --is-linux true Update parameter help and add focused validator and handler unit tests for the Windows defaults and mutual-exclusion rules. --- .../command_modules/appservice/_validators.py | 15 +++++-- .../test_appservice_validators_thru_mock.py | 39 ++++++++++++++++++- 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/appservice/_validators.py b/src/azure-cli/azure/cli/command_modules/appservice/_validators.py index ba623cbb0c0..849f6f9cf04 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/_validators.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/_validators.py @@ -113,11 +113,18 @@ def _validate_asp_sku(sku, app_service_environment, zone_redundant): def validate_asp_create(namespace): validate_tags(namespace) # is_linux is None when not explicitly provided by the user (default). - # Resolve the default: Linux unless --hyper-v is specified. + # Resolve the default: Linux unless a Windows-only plan type is specified. + if namespace.hyper_v and namespace.is_managed_instance: + raise MutuallyExclusiveArgumentError( + '--hyper-v and --is-managed-instance cannot be used together because Windows Containers ' + 'are not supported in Managed Instance on Azure App Service.') if namespace.is_linux is None: - namespace.is_linux = not namespace.hyper_v - elif namespace.is_linux and namespace.hyper_v: - raise MutuallyExclusiveArgumentError('Usage error: --is-linux true and --hyper-v cannot be used together.') + namespace.is_linux = not (namespace.hyper_v or namespace.is_managed_instance) + elif namespace.is_linux and (namespace.hyper_v or namespace.is_managed_instance): + windows_plan_argument = '--hyper-v' if namespace.hyper_v else '--is-managed-instance' + raise MutuallyExclusiveArgumentError( + '{} creates a Windows plan and cannot be combined with --is-linux true. ' + 'Omit --is-linux or use "--is-linux false".'.format(windows_plan_argument)) if namespace.sku is None: if namespace.is_linux: namespace.sku = 'P0V3' diff --git a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_appservice_validators_thru_mock.py b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_appservice_validators_thru_mock.py index b1ad47f6eb8..e37a28cc3d7 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_appservice_validators_thru_mock.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_appservice_validators_thru_mock.py @@ -5,8 +5,9 @@ import unittest from unittest import mock +from types import SimpleNamespace -from azure.cli.core.azclierror import ArgumentUsageError, ValidationError +from azure.cli.core.azclierror import ArgumentUsageError, MutuallyExclusiveArgumentError, ValidationError from azure.cli.command_modules.appservice._constants import ISOLATED_V4_SKUS from azure.cli.command_modules.appservice._validators import ( @@ -15,10 +16,46 @@ _validate_asp_sku, _validate_ip_address_existence, _validate_service_tag_existence, + validate_asp_create, ) from azure.cli.command_modules.appservice.utils import get_sku_tier +class ValidateAppServicePlanCreateTest(unittest.TestCase): + @staticmethod + def _make_namespace(is_linux=None, hyper_v=False, is_managed_instance=False): + return SimpleNamespace( + tags=None, + is_linux=is_linux, + hyper_v=hyper_v, + is_managed_instance=is_managed_instance, + sku='P1V4', + app_service_environment=None, + zone_redundant=False, + ) + + def test_managed_instance_defaults_to_windows(self): + namespace = self._make_namespace(is_managed_instance=True) + + validate_asp_create(namespace) + + self.assertFalse(namespace.is_linux) + + def test_managed_instance_rejects_linux(self): + namespace = self._make_namespace(is_linux=True, is_managed_instance=True) + + with self.assertRaisesRegex(MutuallyExclusiveArgumentError, + '--is-managed-instance creates a Windows plan'): + validate_asp_create(namespace) + + def test_managed_instance_rejects_hyper_v(self): + namespace = self._make_namespace(hyper_v=True, is_managed_instance=True) + + with self.assertRaisesRegex(MutuallyExclusiveArgumentError, + '--hyper-v and --is-managed-instance cannot be used together'): + validate_asp_create(namespace) + + class ValidateAppServicePlanSkuTest(unittest.TestCase): def test_isolated_v4_skus_are_supported_for_ase_plans(self): for sku in ISOLATED_V4_SKUS: