From d9194f080ee517f78b6469006689f8572ae1ac1f Mon Sep 17 00:00:00 2001 From: Mitch Shao Date: Fri, 31 Jul 2026 13:31:25 -0700 Subject: [PATCH 1/3] [AKS] Skip SSH key configuration for Automatic SKU clusters Automatic SKU clusters use a fully managed system node pool that rejects any SSH key configuration, forcing users to pass --no-ssh-key. Skip SSH key reading/generation in validate_ssh_key and skip attaching the linux profile in set_up_linux_profile when --sku automatic is used. If the user explicitly passes --ssh-key-value or --generate-ssh-keys, raise a clear MutuallyExclusiveArgumentError instead of the opaque server error. Co-Authored-By: Claude --- .../cli/command_modules/acs/_validators.py | 18 ++++++ .../acs/managed_cluster_decorator.py | 5 ++ .../latest/test_managed_cluster_decorator.py | 20 +++++++ .../acs/tests/latest/test_validators.py | 60 +++++++++++++++++++ 4 files changed, 103 insertions(+) diff --git a/src/azure-cli/azure/cli/command_modules/acs/_validators.py b/src/azure-cli/azure/cli/command_modules/acs/_validators.py index 8521b2746bf..3ffbb5be449 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/_validators.py +++ b/src/azure-cli/azure/cli/command_modules/acs/_validators.py @@ -10,6 +10,7 @@ from azure.mgmt.containerservice.models import KubernetesSupportPlan from azure.cli.command_modules.acs._consts import ( + CONST_MANAGED_CLUSTER_SKU_NAME_AUTOMATIC, CONST_MANAGED_CLUSTER_SKU_TIER_FREE, CONST_MANAGED_CLUSTER_SKU_TIER_STANDARD, CONST_MANAGED_CLUSTER_SKU_TIER_PREMIUM, @@ -43,6 +44,23 @@ def validate_ssh_key(namespace): if hasattr(namespace, 'no_ssh_key') and namespace.no_ssh_key: return + # Automatic SKU clusters use a fully managed system node pool that rejects any SSH key + # configuration. Skip reading/generating an SSH key so users don't need --no-ssh-key. + if getattr(namespace, 'sku', None) is not None and \ + namespace.sku.lower() == CONST_MANAGED_CLUSTER_SKU_NAME_AUTOMATIC: + # ssh_key_value defaults to "~/.ssh/id_rsa.pub" (expanded to an absolute path by + # the arg's file_type); only treat a non-default value as an explicit user request. + default_ssh_key_value = os.path.expanduser(os.path.join("~", ".ssh", "id_rsa.pub")) + explicit_ssh_key = ( + namespace.ssh_key_value and + os.path.expanduser(namespace.ssh_key_value) != default_ssh_key_value + ) + if namespace.generate_ssh_keys or explicit_ssh_key: + raise MutuallyExclusiveArgumentError( + 'SSH key configuration is not supported for the Automatic SKU. ' + 'Do not specify "--ssh-key-value" or "--generate-ssh-keys" when using "--sku automatic".' + ) + return string_or_file = (namespace.ssh_key_value or os.path.join(os.path.expanduser('~'), '.ssh', 'id_rsa.pub')) content = string_or_file diff --git a/src/azure-cli/azure/cli/command_modules/acs/managed_cluster_decorator.py b/src/azure-cli/azure/cli/command_modules/acs/managed_cluster_decorator.py index d58c6c7e0e0..e86ff6e6da8 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/managed_cluster_decorator.py +++ b/src/azure-cli/azure/cli/command_modules/acs/managed_cluster_decorator.py @@ -6647,6 +6647,11 @@ def set_up_linux_profile(self, mc: ManagedCluster) -> ManagedCluster: """ self._ensure_mc(mc) + # Automatic SKU clusters use a fully managed system node pool that rejects any SSH + # key configuration, so never attach a linux profile for them. + if self.context.get_sku_name() == CONST_MANAGED_CLUSTER_SKU_NAME_AUTOMATIC: + return mc + ssh_key_value, no_ssh_key = self.context.get_ssh_key_value_and_no_ssh_key() if not no_ssh_key: ssh_config = self.models.ContainerServiceSshConfiguration( diff --git a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_managed_cluster_decorator.py b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_managed_cluster_decorator.py index 26e06749272..02f18b2e6d2 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_managed_cluster_decorator.py +++ b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_managed_cluster_decorator.py @@ -7160,6 +7160,26 @@ def test_set_up_linux_profile(self): ground_truth_mc_2 = self.models.ManagedCluster(location="test_location") self.assertEqual(dec_mc_2, ground_truth_mc_2) + def test_set_up_linux_profile_automatic_sku_skips_ssh_key(self): + dec_1 = AKSManagedClusterCreateDecorator( + self.cmd, + self.client, + { + "sku": "automatic", + "admin_username": "azureuser", + "no_ssh_key": False, + "ssh_key_value": "unused-for-managed-system-pool", + }, + ResourceType.MGMT_CONTAINERSERVICE, + ) + mc_1 = self.models.ManagedCluster(location="test_location") + dec_1.context.attach_mc(mc_1) + + dec_mc_1 = dec_1.set_up_linux_profile(mc_1) + + self.assertIs(dec_mc_1, mc_1) + self.assertIsNone(dec_mc_1.linux_profile) + def test_set_up_windows_profile(self): # default value in `aks_create` dec_1 = AKSManagedClusterCreateDecorator( diff --git a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_validators.py b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_validators.py index 6dc41fe4f53..6f5421bc3bf 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_validators.py +++ b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_validators.py @@ -1986,5 +1986,65 @@ def test_fail_if_disable_for_windows(self): ) +class TestValidateSshKey(unittest.TestCase): + def _default_key(self): + import os + return os.path.expanduser(os.path.join("~", ".ssh", "id_rsa.pub")) + + def test_skip_for_automatic_sku(self): + # Default ssh_key_value (expanded like the CLI does) should be treated as + # "not explicitly provided" and skipped without error. + default_key = self._default_key() + namespace = SimpleNamespace( + no_ssh_key=False, + generate_ssh_keys=False, + ssh_key_value=default_key, + sku="automatic", + ) + validators.validate_ssh_key(namespace) + self.assertEqual(namespace.ssh_key_value, default_key) + + def test_skip_for_automatic_sku_case_insensitive(self): + default_key = self._default_key() + namespace = SimpleNamespace( + no_ssh_key=False, + generate_ssh_keys=False, + ssh_key_value=default_key, + sku="Automatic", + ) + validators.validate_ssh_key(namespace) + self.assertEqual(namespace.ssh_key_value, default_key) + + def test_automatic_sku_with_generate_ssh_keys_errors(self): + namespace = SimpleNamespace( + no_ssh_key=False, + generate_ssh_keys=True, + ssh_key_value=self._default_key(), + sku="automatic", + ) + with self.assertRaises(MutuallyExclusiveArgumentError): + validators.validate_ssh_key(namespace) + + def test_automatic_sku_with_explicit_ssh_key_value_errors(self): + namespace = SimpleNamespace( + no_ssh_key=False, + generate_ssh_keys=False, + ssh_key_value="ssh-rsa AAAAB3NzaC1yc2Euser@host", + sku="automatic", + ) + with self.assertRaises(MutuallyExclusiveArgumentError): + validators.validate_ssh_key(namespace) + + def test_no_ssh_key_still_skips(self): + namespace = SimpleNamespace( + no_ssh_key=True, + generate_ssh_keys=False, + ssh_key_value=None, + sku="base", + ) + validators.validate_ssh_key(namespace) + self.assertIsNone(namespace.ssh_key_value) + + if __name__ == "__main__": unittest.main() From 1d2a72310448981b7e37b8f454b09429b77920b5 Mon Sep 17 00:00:00 2001 From: Mitch Shao Date: Fri, 31 Jul 2026 14:07:27 -0700 Subject: [PATCH 2/3] [AKS] Error on any explicit SSH key with Automatic SKU Change --ssh-key-value default from the hardcoded ~/.ssh/id_rsa.pub path to None so the validator can distinguish an explicitly-provided key from the default. For --sku automatic, any --ssh-key-value (including the default path) or --generate-ssh-keys now raises MutuallyExclusiveArgumentError instead of being silently ignored. The non-Automatic path is unchanged: a None value still falls back to the default key location. Co-Authored-By: Claude --- .../azure/cli/command_modules/acs/_params.py | 2 +- .../cli/command_modules/acs/_validators.py | 11 ++----- .../acs/tests/latest/test_validators.py | 33 +++++++++++-------- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/acs/_params.py b/src/azure-cli/azure/cli/command_modules/acs/_params.py index 7db0fe45c9d..80ebca7b7f6 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/_params.py +++ b/src/azure-cli/azure/cli/command_modules/acs/_params.py @@ -408,7 +408,7 @@ def load_arguments(self, _): c.argument('edge_zone', edge_zone_type) c.argument('admin_username', options_list=['--admin-username', '-u'], default='azureuser') c.argument('generate_ssh_keys', action='store_true', validator=validate_create_parameters) - c.argument('ssh_key_value', required=False, type=file_type, default=os.path.join('~', '.ssh', 'id_rsa.pub'), + c.argument('ssh_key_value', required=False, type=file_type, default=None, completer=FilesCompleter(), validator=validate_ssh_key) c.argument('no_ssh_key', options_list=['--no-ssh-key', '-x']) c.argument('dns_service_ip') diff --git a/src/azure-cli/azure/cli/command_modules/acs/_validators.py b/src/azure-cli/azure/cli/command_modules/acs/_validators.py index 3ffbb5be449..293b256c85a 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/_validators.py +++ b/src/azure-cli/azure/cli/command_modules/acs/_validators.py @@ -45,17 +45,10 @@ def validate_ssh_key(namespace): if hasattr(namespace, 'no_ssh_key') and namespace.no_ssh_key: return # Automatic SKU clusters use a fully managed system node pool that rejects any SSH key - # configuration. Skip reading/generating an SSH key so users don't need --no-ssh-key. + # configuration. Do not generate/read a key; if the user explicitly asked for one, error. if getattr(namespace, 'sku', None) is not None and \ namespace.sku.lower() == CONST_MANAGED_CLUSTER_SKU_NAME_AUTOMATIC: - # ssh_key_value defaults to "~/.ssh/id_rsa.pub" (expanded to an absolute path by - # the arg's file_type); only treat a non-default value as an explicit user request. - default_ssh_key_value = os.path.expanduser(os.path.join("~", ".ssh", "id_rsa.pub")) - explicit_ssh_key = ( - namespace.ssh_key_value and - os.path.expanduser(namespace.ssh_key_value) != default_ssh_key_value - ) - if namespace.generate_ssh_keys or explicit_ssh_key: + if namespace.generate_ssh_keys or namespace.ssh_key_value: raise MutuallyExclusiveArgumentError( 'SSH key configuration is not supported for the Automatic SKU. ' 'Do not specify "--ssh-key-value" or "--generate-ssh-keys" when using "--sku automatic".' diff --git a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_validators.py b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_validators.py index 6f5421bc3bf..320797145a7 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_validators.py +++ b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_validators.py @@ -1987,39 +1987,33 @@ def test_fail_if_disable_for_windows(self): class TestValidateSshKey(unittest.TestCase): - def _default_key(self): - import os - return os.path.expanduser(os.path.join("~", ".ssh", "id_rsa.pub")) - def test_skip_for_automatic_sku(self): - # Default ssh_key_value (expanded like the CLI does) should be treated as - # "not explicitly provided" and skipped without error. - default_key = self._default_key() + # When --ssh-key-value is not provided (default None), Automatic SKU skips SSH + # handling without error. namespace = SimpleNamespace( no_ssh_key=False, generate_ssh_keys=False, - ssh_key_value=default_key, + ssh_key_value=None, sku="automatic", ) validators.validate_ssh_key(namespace) - self.assertEqual(namespace.ssh_key_value, default_key) + self.assertIsNone(namespace.ssh_key_value) def test_skip_for_automatic_sku_case_insensitive(self): - default_key = self._default_key() namespace = SimpleNamespace( no_ssh_key=False, generate_ssh_keys=False, - ssh_key_value=default_key, + ssh_key_value=None, sku="Automatic", ) validators.validate_ssh_key(namespace) - self.assertEqual(namespace.ssh_key_value, default_key) + self.assertIsNone(namespace.ssh_key_value) def test_automatic_sku_with_generate_ssh_keys_errors(self): namespace = SimpleNamespace( no_ssh_key=False, generate_ssh_keys=True, - ssh_key_value=self._default_key(), + ssh_key_value=None, sku="automatic", ) with self.assertRaises(MutuallyExclusiveArgumentError): @@ -2035,6 +2029,19 @@ def test_automatic_sku_with_explicit_ssh_key_value_errors(self): with self.assertRaises(MutuallyExclusiveArgumentError): validators.validate_ssh_key(namespace) + def test_automatic_sku_with_default_path_ssh_key_value_errors(self): + # Even the default key path counts as an explicit --ssh-key-value now that the + # arg default is None. + import os + namespace = SimpleNamespace( + no_ssh_key=False, + generate_ssh_keys=False, + ssh_key_value=os.path.expanduser(os.path.join("~", ".ssh", "id_rsa.pub")), + sku="automatic", + ) + with self.assertRaises(MutuallyExclusiveArgumentError): + validators.validate_ssh_key(namespace) + def test_no_ssh_key_still_skips(self): namespace = SimpleNamespace( no_ssh_key=True, From bbfaa5cf3e98f151f16d26490690a746c762e777 Mon Sep 17 00:00:00 2001 From: Mitch Shao Date: Fri, 31 Jul 2026 14:31:53 -0700 Subject: [PATCH 3/3] Revert "[AKS] Error on any explicit SSH key with Automatic SKU" This reverts commit 1d2a72310448981b7e37b8f454b09429b77920b5. --- .../azure/cli/command_modules/acs/_params.py | 2 +- .../cli/command_modules/acs/_validators.py | 11 +++++-- .../acs/tests/latest/test_validators.py | 33 ++++++++----------- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/acs/_params.py b/src/azure-cli/azure/cli/command_modules/acs/_params.py index 80ebca7b7f6..7db0fe45c9d 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/_params.py +++ b/src/azure-cli/azure/cli/command_modules/acs/_params.py @@ -408,7 +408,7 @@ def load_arguments(self, _): c.argument('edge_zone', edge_zone_type) c.argument('admin_username', options_list=['--admin-username', '-u'], default='azureuser') c.argument('generate_ssh_keys', action='store_true', validator=validate_create_parameters) - c.argument('ssh_key_value', required=False, type=file_type, default=None, + c.argument('ssh_key_value', required=False, type=file_type, default=os.path.join('~', '.ssh', 'id_rsa.pub'), completer=FilesCompleter(), validator=validate_ssh_key) c.argument('no_ssh_key', options_list=['--no-ssh-key', '-x']) c.argument('dns_service_ip') diff --git a/src/azure-cli/azure/cli/command_modules/acs/_validators.py b/src/azure-cli/azure/cli/command_modules/acs/_validators.py index 293b256c85a..3ffbb5be449 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/_validators.py +++ b/src/azure-cli/azure/cli/command_modules/acs/_validators.py @@ -45,10 +45,17 @@ def validate_ssh_key(namespace): if hasattr(namespace, 'no_ssh_key') and namespace.no_ssh_key: return # Automatic SKU clusters use a fully managed system node pool that rejects any SSH key - # configuration. Do not generate/read a key; if the user explicitly asked for one, error. + # configuration. Skip reading/generating an SSH key so users don't need --no-ssh-key. if getattr(namespace, 'sku', None) is not None and \ namespace.sku.lower() == CONST_MANAGED_CLUSTER_SKU_NAME_AUTOMATIC: - if namespace.generate_ssh_keys or namespace.ssh_key_value: + # ssh_key_value defaults to "~/.ssh/id_rsa.pub" (expanded to an absolute path by + # the arg's file_type); only treat a non-default value as an explicit user request. + default_ssh_key_value = os.path.expanduser(os.path.join("~", ".ssh", "id_rsa.pub")) + explicit_ssh_key = ( + namespace.ssh_key_value and + os.path.expanduser(namespace.ssh_key_value) != default_ssh_key_value + ) + if namespace.generate_ssh_keys or explicit_ssh_key: raise MutuallyExclusiveArgumentError( 'SSH key configuration is not supported for the Automatic SKU. ' 'Do not specify "--ssh-key-value" or "--generate-ssh-keys" when using "--sku automatic".' diff --git a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_validators.py b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_validators.py index 320797145a7..6f5421bc3bf 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_validators.py +++ b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_validators.py @@ -1987,33 +1987,39 @@ def test_fail_if_disable_for_windows(self): class TestValidateSshKey(unittest.TestCase): + def _default_key(self): + import os + return os.path.expanduser(os.path.join("~", ".ssh", "id_rsa.pub")) + def test_skip_for_automatic_sku(self): - # When --ssh-key-value is not provided (default None), Automatic SKU skips SSH - # handling without error. + # Default ssh_key_value (expanded like the CLI does) should be treated as + # "not explicitly provided" and skipped without error. + default_key = self._default_key() namespace = SimpleNamespace( no_ssh_key=False, generate_ssh_keys=False, - ssh_key_value=None, + ssh_key_value=default_key, sku="automatic", ) validators.validate_ssh_key(namespace) - self.assertIsNone(namespace.ssh_key_value) + self.assertEqual(namespace.ssh_key_value, default_key) def test_skip_for_automatic_sku_case_insensitive(self): + default_key = self._default_key() namespace = SimpleNamespace( no_ssh_key=False, generate_ssh_keys=False, - ssh_key_value=None, + ssh_key_value=default_key, sku="Automatic", ) validators.validate_ssh_key(namespace) - self.assertIsNone(namespace.ssh_key_value) + self.assertEqual(namespace.ssh_key_value, default_key) def test_automatic_sku_with_generate_ssh_keys_errors(self): namespace = SimpleNamespace( no_ssh_key=False, generate_ssh_keys=True, - ssh_key_value=None, + ssh_key_value=self._default_key(), sku="automatic", ) with self.assertRaises(MutuallyExclusiveArgumentError): @@ -2029,19 +2035,6 @@ def test_automatic_sku_with_explicit_ssh_key_value_errors(self): with self.assertRaises(MutuallyExclusiveArgumentError): validators.validate_ssh_key(namespace) - def test_automatic_sku_with_default_path_ssh_key_value_errors(self): - # Even the default key path counts as an explicit --ssh-key-value now that the - # arg default is None. - import os - namespace = SimpleNamespace( - no_ssh_key=False, - generate_ssh_keys=False, - ssh_key_value=os.path.expanduser(os.path.join("~", ".ssh", "id_rsa.pub")), - sku="automatic", - ) - with self.assertRaises(MutuallyExclusiveArgumentError): - validators.validate_ssh_key(namespace) - def test_no_ssh_key_still_skips(self): namespace = SimpleNamespace( no_ssh_key=True,