diff --git a/src/aks-preview/HISTORY.rst b/src/aks-preview/HISTORY.rst index b58406fdad7..0a72b6ba3c6 100644 --- a/src/aks-preview/HISTORY.rst +++ b/src/aks-preview/HISTORY.rst @@ -12,6 +12,8 @@ To release a new version, please select a new version number (usually plus 1 to Pending +++++++ +* Support disabling Azure KeyVault KMS. + 0.5.91 ++++++ diff --git a/src/aks-preview/azext_aks_preview/_help.py b/src/aks-preview/azext_aks_preview/_help.py index 5a6d9b7e0ba..4255dff9454 100644 --- a/src/aks-preview/azext_aks_preview/_help.py +++ b/src/aks-preview/azext_aks_preview/_help.py @@ -771,6 +771,9 @@ - name: --enable-azure-keyvault-kms type: bool short-summary: Enable Azure KeyVault Key Management Service. + - name: --disable-azure-keyvault-kms + type: bool + short-summary: Disable Azure KeyVault Key Management Service. - name: --azure-keyvault-kms-key-id type: string short-summary: Identifier of Azure Key Vault key. diff --git a/src/aks-preview/azext_aks_preview/_params.py b/src/aks-preview/azext_aks_preview/_params.py index 1c0569e6a2d..bb9f97f5297 100644 --- a/src/aks-preview/azext_aks_preview/_params.py +++ b/src/aks-preview/azext_aks_preview/_params.py @@ -389,6 +389,7 @@ def load_arguments(self, _): c.argument('enable_workload_identity', arg_type=get_three_state_flag()) c.argument('enable_oidc_issuer', action='store_true', is_preview=True) c.argument('enable_azure_keyvault_kms', action='store_true', is_preview=True) + c.argument('disable_azure_keyvault_kms', action='store_true', is_preview=True) c.argument('azure_keyvault_kms_key_id', validator=validate_azure_keyvault_kms_key_id, is_preview=True) c.argument('azure_keyvault_kms_key_vault_network_access', arg_type=get_enum_type(keyvault_network_access_types), is_preview=True) c.argument('azure_keyvault_kms_key_vault_resource_id', validator=validate_azure_keyvault_kms_key_vault_resource_id, is_preview=True) diff --git a/src/aks-preview/azext_aks_preview/custom.py b/src/aks-preview/azext_aks_preview/custom.py index 3489a94a3a1..bd893840ce1 100644 --- a/src/aks-preview/azext_aks_preview/custom.py +++ b/src/aks-preview/azext_aks_preview/custom.py @@ -766,6 +766,7 @@ def aks_update( enable_workload_identity=None, enable_oidc_issuer=False, enable_azure_keyvault_kms=False, + disable_azure_keyvault_kms=False, azure_keyvault_kms_key_id=None, azure_keyvault_kms_key_vault_network_access=None, azure_keyvault_kms_key_vault_resource_id=None, diff --git a/src/aks-preview/azext_aks_preview/managed_cluster_decorator.py b/src/aks-preview/azext_aks_preview/managed_cluster_decorator.py index 47d8226a230..1188827cf55 100644 --- a/src/aks-preview/azext_aks_preview/managed_cluster_decorator.py +++ b/src/aks-preview/azext_aks_preview/managed_cluster_decorator.py @@ -680,6 +680,37 @@ def get_enable_azure_keyvault_kms(self) -> bool: """ return self._get_enable_azure_keyvault_kms(enable_validation=True) + def _get_disable_azure_keyvault_kms(self, enable_validation: bool = False) -> bool: + """Internal function to obtain the value of disable_azure_keyvault_kms. + + This function supports the option of enable_validation. When enabled, if both enable_azure_keyvault_kms and disable_azure_keyvault_kms are + specified, raise a MutuallyExclusiveArgumentError. + + :return: bool + """ + # Read the original value passed by the command. + disable_azure_keyvault_kms = self.raw_param.get("disable_azure_keyvault_kms") + + # This option is not supported in create mode, hence we do not read the property value from the `mc` object. + # This parameter does not need dynamic completion. + if enable_validation: + if disable_azure_keyvault_kms and self._get_enable_azure_keyvault_kms(enable_validation=False): + raise MutuallyExclusiveArgumentError( + "Cannot specify --enable-azure-keyvault-kms and --disable-azure-keyvault-kms at the same time." + ) + + return disable_azure_keyvault_kms + + def get_disable_azure_keyvault_kms(self) -> bool: + """Obtain the value of disable_azure_keyvault_kms. + + This function will verify the parameter by default. If both enable_azure_keyvault_kms and disable_azure_keyvault_kms are specified, raise a + MutuallyExclusiveArgumentError. + + :return: bool + """ + return self._get_disable_azure_keyvault_kms(enable_validation=True) + def _get_azure_keyvault_kms_key_id(self, enable_validation: bool = False) -> Union[str, None]: """Internal function to obtain the value of azure_keyvault_kms_key_id according to the context. @@ -1964,6 +1995,18 @@ def update_azure_keyvault_kms(self, mc: ManagedCluster) -> ManagedCluster: self.context.get_azure_keyvault_kms_key_vault_resource_id() ) + if self.context.get_disable_azure_keyvault_kms(): + # get kms profile + if mc.security_profile is None: + mc.security_profile = self.models.ManagedClusterSecurityProfile() + azure_key_vault_kms_profile = mc.security_profile.azure_key_vault_kms + if azure_key_vault_kms_profile is None: + azure_key_vault_kms_profile = self.models.AzureKeyVaultKms() + mc.security_profile.azure_key_vault_kms = azure_key_vault_kms_profile + + # set enabled to False + azure_key_vault_kms_profile.enabled = False + return mc def update_storage_profile(self, mc: ManagedCluster) -> ManagedCluster: diff --git a/src/aks-preview/azext_aks_preview/tests/latest/test_aks_commands.py b/src/aks-preview/azext_aks_preview/tests/latest/test_aks_commands.py index 3be96c0bd03..2b9dae28350 100644 --- a/src/aks-preview/azext_aks_preview/tests/latest/test_aks_commands.py +++ b/src/aks-preview/azext_aks_preview/tests/latest/test_aks_commands.py @@ -4291,6 +4291,80 @@ def test_aks_update_with_azurekeyvaultkms_private_key_vault(self, resource_group self.is_empty(), ]) + @live_only() + @AllowLargeResponse() + @AKSCustomResourceGroupPreparer(random_name_length=17, name_prefix='clitest', location='centraluseuap') + def test_aks_disable_azurekeyvaultkms(self, resource_group, resource_group_location): + aks_name = self.create_random_name('cliakstest', 16) + kv_name = self.create_random_name('cliakstestkv', 16) + identity_name = self.create_random_name('cliakstestidentity', 24) + self.kwargs.update({ + 'resource_group': resource_group, + 'name': aks_name, + "kv_name": kv_name, + "identity_name": identity_name, + 'ssh_key_value': self.generate_ssh_keys() + }) + + # create user-assigned identity + create_identity = 'identity create --resource-group={resource_group} --name={identity_name} -o json' + identity = self.cmd(create_identity).get_output_in_json() + identity_id = identity['id'] + identity_object_id = identity['principalId'] + assert identity_id is not None + assert identity_object_id is not None + self.kwargs.update({ + 'identity_id': identity_id, + 'identity_object_id': identity_object_id, + }) + + # create key vault and key + create_keyvault = 'keyvault create --resource-group={resource_group} --name={kv_name} -o json' + kv = self.cmd(create_keyvault, checks=[ + self.check('properties.provisioningState', 'Succeeded') + ]).get_output_in_json() + + create_key = 'keyvault key create -n kms --vault-name {kv_name} -o json' + key = self.cmd(create_key, checks=[ + self.check('attributes.enabled', True) + ]).get_output_in_json() + key_id = key['key']['kid'] + assert key_id is not None + self.kwargs.update({ + 'key_id': key_id, + }) + + # assign access policy + set_policy = 'keyvault set-policy --resource-group={resource_group} --name={kv_name} ' \ + '--object-id {identity_object_id} --key-permissions encrypt decrypt -o json' + policy = self.cmd(set_policy, checks=[ + self.check('properties.provisioningState', 'Succeeded') + ]).get_output_in_json() + + create_cmd = 'aks create --resource-group={resource_group} --name={name} ' \ + '--assign-identity {identity_id} ' \ + '--enable-azure-keyvault-kms --azure-keyvault-kms-key-id={key_id} --aks-custom-headers AKSHTTPCustomFeatures=Microsoft.ContainerService/AzureKeyVaultKmsPreview ' \ + '--ssh-key-value={ssh_key_value} -o json' + self.cmd(create_cmd, checks=[ + self.check('provisioningState', 'Succeeded'), + self.check('securityProfile.azureKeyVaultKms.enabled', True), + self.check('securityProfile.azureKeyVaultKms.keyId', key_id) + ]) + + update_cmd = 'aks update --resource-group={resource_group} --name={name} ' \ + '--disable-azure-keyvault-kms --aks-custom-headers AKSHTTPCustomFeatures=Microsoft.ContainerService/AzureKeyVaultKmsPreview ' \ + '-o json' + self.cmd(update_cmd, checks=[ + self.check('provisioningState', 'Succeeded'), + self.check('securityProfile.azureKeyVaultKms.enabled', False), + ]) + + # delete + cmd = 'aks delete --resource-group={resource_group} --name={name} --yes --no-wait' + self.cmd(cmd, checks=[ + self.is_empty(), + ]) + @AllowLargeResponse() @AKSCustomResourceGroupPreparer(random_name_length=17, name_prefix='clitest', location='westcentralus', preserve_default_location=True) def test_aks_create_and_update_with_csi_drivers_extensibility(self, resource_group, resource_group_location): diff --git a/src/aks-preview/azext_aks_preview/tests/latest/test_managed_cluster_decorator.py b/src/aks-preview/azext_aks_preview/tests/latest/test_managed_cluster_decorator.py index 2ead4fcb183..d8df7507f2b 100644 --- a/src/aks-preview/azext_aks_preview/tests/latest/test_managed_cluster_decorator.py +++ b/src/aks-preview/azext_aks_preview/tests/latest/test_managed_cluster_decorator.py @@ -998,6 +998,53 @@ def test_get_enable_azure_keyvault_kms(self): with self.assertRaises(RequiredArgumentMissingError): ctx_5.get_enable_azure_keyvault_kms() + def test_get_disable_azure_keyvault_kms(self): + ctx_0 = AKSPreviewManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict({}), + self.models, + decorator_mode=DecoratorMode.UPDATE, + ) + self.assertIsNone(ctx_0.get_enable_azure_keyvault_kms()) + + ctx_1 = AKSPreviewManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict( + { + "disable_azure_keyvault_kms": True, + } + ), + self.models, + decorator_mode=DecoratorMode.UPDATE, + ) + self.assertEqual(ctx_1.get_disable_azure_keyvault_kms(), True) + + ctx_2 = AKSPreviewManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict( + { + "disable_azure_keyvault_kms": False, + } + ), + self.models, + decorator_mode=DecoratorMode.UPDATE, + ) + self.assertEqual(ctx_2.get_disable_azure_keyvault_kms(), False) + + ctx_3 = AKSPreviewManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict( + { + "enable_azure_keyvault_kms": True, + "disable_azure_keyvault_kms": True, + } + ), + self.models, + decorator_mode=DecoratorMode.UPDATE, + ) + with self.assertRaises(MutuallyExclusiveArgumentError): + ctx_3.get_disable_azure_keyvault_kms() + def test_get_azure_keyvault_kms_key_id(self): ctx_0 = AKSPreviewManagedClusterContext( self.cmd, @@ -3959,6 +4006,67 @@ def test_update_azure_keyvault_kms(self): ) self.assertEqual(dec_mc_4, ground_truth_mc_4) + dec_5 = AKSPreviewManagedClusterUpdateDecorator( + self.cmd, + self.client, + { + "disable_azure_keyvault_kms": True, + }, + CUSTOM_MGMT_AKS_PREVIEW, + ) + azure_keyvault_kms_profile_5 = self.models.AzureKeyVaultKms( + enabled=True, + key_id=key_id_1, + key_vault_network_access="Public", + ) + security_profile_5 = self.models.ManagedClusterSecurityProfile( + azure_key_vault_kms=azure_keyvault_kms_profile_5, + ) + mc_5 = self.models.ManagedCluster( + location="test_location", + security_profile=security_profile_5, + ) + dec_5.context.attach_mc(mc_5) + dec_mc_5 = dec_5.update_azure_keyvault_kms(mc_5) + + ground_truth_azure_keyvault_kms_profile_5 = self.models.AzureKeyVaultKms( + enabled=False, + key_id=key_id_1, + key_vault_network_access="Public", + ) + ground_truth_security_profile_5 = self.models.ManagedClusterSecurityProfile( + azure_key_vault_kms=ground_truth_azure_keyvault_kms_profile_5, + ) + ground_truth_mc_5 = self.models.ManagedCluster( + location="test_location", + security_profile=ground_truth_security_profile_5, + ) + self.assertEqual(dec_mc_5, ground_truth_mc_5) + + dec_6 = AKSPreviewManagedClusterUpdateDecorator( + self.cmd, + self.client, + { + "disable_azure_keyvault_kms": True, + }, + CUSTOM_MGMT_AKS_PREVIEW, + ) + mc_6 = self.models.ManagedCluster( + location="test_location", + ) + dec_6.context.attach_mc(mc_6) + dec_mc_6 = dec_6.update_azure_keyvault_kms(mc_6) + + ground_truth_azure_keyvault_kms_profile_6 = self.models.AzureKeyVaultKms() + ground_truth_azure_keyvault_kms_profile_6.enabled=False + ground_truth_security_profile_6 = self.models.ManagedClusterSecurityProfile() + ground_truth_security_profile_6.azure_key_vault_kms=ground_truth_azure_keyvault_kms_profile_6 + ground_truth_mc_6 = self.models.ManagedCluster( + location="test_location", + security_profile=ground_truth_security_profile_6, + ) + self.assertEqual(dec_mc_6, ground_truth_mc_6) + def test_update_storage_profile(self): dec_1 = AKSPreviewManagedClusterUpdateDecorator( diff --git a/src/aks-preview/linter_exclusions.yml b/src/aks-preview/linter_exclusions.yml index 97bfe676ed0..124ec74af89 100644 --- a/src/aks-preview/linter_exclusions.yml +++ b/src/aks-preview/linter_exclusions.yml @@ -48,6 +48,9 @@ aks update: enable_azure_keyvault_kms: rule_exclusions: - option_length_too_long + disable_azure_keyvault_kms: + rule_exclusions: + - option_length_too_long azure_keyvault_kms_key_id: rule_exclusions: - option_length_too_long