diff --git a/src/aks-preview/HISTORY.rst b/src/aks-preview/HISTORY.rst index f80108b39ce..90ad1c6d46e 100644 --- a/src/aks-preview/HISTORY.rst +++ b/src/aks-preview/HISTORY.rst @@ -11,16 +11,23 @@ To release a new version, please select a new version number (usually plus 1 to Pending +++++++ + +0.5.90 +++++++ + +* Fix compatibility issue when enabling Microsoft Defender via aks-preview. + * az aks create + * az aks update + 0.5.89 ++++++ * Fix for the az aks addon list command to return enable:true, if virtual-node addon is enabled for the AKS cluster. -+++++++ 0.5.88 ++++++ -* AKS Monitoring MSI Auth related code imported from Azure CLI to reuse the code between aks-preview and Azure CLI +* AKS Monitoring MSI Auth related code imported from Azure CLI to reuse the code between aks-preview and Azure CLI. 0.5.87 ++++++ 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 a98c745cd39..47d8226a230 100644 --- a/src/aks-preview/azext_aks_preview/managed_cluster_decorator.py +++ b/src/aks-preview/azext_aks_preview/managed_cluster_decorator.py @@ -75,6 +75,7 @@ ManagedClusterStorageProfileBlobCSIDriver = TypeVar('ManagedClusterStorageProfileBlobCSIDriver') ManagedClusterStorageProfileSnapshotController = TypeVar('ManagedClusterStorageProfileSnapshotController') ManagedClusterIngressProfileWebAppRouting = TypeVar("ManagedClusterIngressProfileWebAppRouting") +ManagedClusterSecurityProfileDefender = TypeVar("ManagedClusterSecurityProfileDefender") # pylint: disable=too-few-public-methods @@ -1333,6 +1334,53 @@ def get_disable_keda(self) -> bool: """ return self._get_disable_keda(enable_validation=True) + def get_defender_config(self) -> Union[ManagedClusterSecurityProfileDefender, None]: + """Obtain the value of defender. + + Note: Overwritten in aks-preview to adapt to v2 defender structure. + + :return: ManagedClusterSecurityProfileDefender or None + """ + disable_defender = self.raw_param.get("disable_defender") + if disable_defender: + return self.models.ManagedClusterSecurityProfileDefender( + security_monitoring=self.models.ManagedClusterSecurityProfileDefenderSecurityMonitoring( + enabled=False + ) + ) + + enable_defender = self.raw_param.get("enable_defender") + + if not enable_defender: + return None + + workspace = "" + config_file_path = self.raw_param.get("defender_config") + if config_file_path: + if not os.path.isfile(config_file_path): + raise InvalidArgumentValueError( + "{} is not valid file, or not accessable.".format( + config_file_path + ) + ) + defender_config = get_file_json(config_file_path) + if "logAnalyticsWorkspaceResourceId" in defender_config: + workspace = defender_config["logAnalyticsWorkspaceResourceId"] + + if workspace == "": + workspace = self.external_functions.ensure_default_log_analytics_workspace_for_monitoring( + self.cmd, + self.get_subscription_id(), + self.get_resource_group_name()) + + azure_defender = self.models.ManagedClusterSecurityProfileDefender( + log_analytics_workspace_resource_id=workspace, + security_monitoring=self.models.ManagedClusterSecurityProfileDefenderSecurityMonitoring( + enabled=enable_defender + ), + ) + return azure_defender + class AKSPreviewManagedClusterCreateDecorator(AKSManagedClusterCreateDecorator): def __init__( @@ -1621,6 +1669,24 @@ def set_up_workload_auto_scaler_profile(self, mc: ManagedCluster) -> ManagedClus return mc + def set_up_defender(self, mc: ManagedCluster) -> ManagedCluster: + """Set up defender for the ManagedCluster object. + + Note: Overwritten in aks-preview to adapt to v2 defender structure. + + :return: the ManagedCluster object + """ + self._ensure_mc(mc) + + defender = self.context.get_defender_config() + if defender: + if mc.security_profile is None: + mc.security_profile = self.models.ManagedClusterSecurityProfile() + + mc.security_profile.defender = defender + + return mc + def construct_mc_profile_preview(self, bypass_restore_defaults: bool = False) -> ManagedCluster: """The overall controller used to construct the default ManagedCluster profile. @@ -1930,6 +1996,24 @@ def update_workload_auto_scaler_profile(self, mc: ManagedCluster) -> ManagedClus return mc + def update_defender(self, mc: ManagedCluster) -> ManagedCluster: + """Update defender for the ManagedCluster object. + + Note: Overwritten in aks-preview to adapt to v2 defender structure. + + :return: the ManagedCluster object + """ + self._ensure_mc(mc) + + defender = self.context.get_defender_config() + if defender: + if mc.security_profile is None: + mc.security_profile = self.models.ManagedClusterSecurityProfile() + + mc.security_profile.defender = defender + + return mc + def update_mc_profile_preview(self) -> ManagedCluster: """The overall controller used to update the preview ManagedCluster profile. diff --git a/src/aks-preview/azext_aks_preview/tests/latest/data/defenderconfig.json b/src/aks-preview/azext_aks_preview/tests/latest/data/defenderconfig.json new file mode 100644 index 00000000000..cd1da4e0442 --- /dev/null +++ b/src/aks-preview/azext_aks_preview/tests/latest/data/defenderconfig.json @@ -0,0 +1,3 @@ +{ + "logAnalyticsWorkspaceResourceId": "test_workspace_resource_id" +} \ No newline at end of file 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 6402482e33a..7cc5c6e12a4 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 @@ -4917,3 +4917,60 @@ def test_aks_availability_zones(self, resource_group, resource_group_location): # delete self.cmd( 'aks delete -g {resource_group} -n {name} --yes --no-wait', checks=[self.is_empty()]) + + # live only due to workspace is not mocked correctly + @live_only() + @AllowLargeResponse() + @AKSCustomResourceGroupPreparer(random_name_length=17, name_prefix='clitest', location='westus2') + def test_aks_create_with_defender(self, resource_group, resource_group_location): + aks_name = self.create_random_name('cliakstest', 16) + self.kwargs.update({ + 'name': aks_name, + 'resource_group': resource_group, + 'ssh_key_value': self.generate_ssh_keys() + }) + + # create + create_cmd = 'aks create --resource-group={resource_group} --name={name} ' \ + '--ssh-key-value={ssh_key_value} --enable-defender' + self.cmd(create_cmd, checks=[ + self.check('provisioningState', 'Succeeded'), + self.check('securityProfile.defender.securityMonitoring.enabled', True) + ]) + + # delete + self.cmd( + 'aks delete -g {resource_group} -n {name} --yes --no-wait', checks=[self.is_empty()]) + + # live only due to workspace is not mocked correctly + @live_only() + @AllowLargeResponse() + @AKSCustomResourceGroupPreparer(random_name_length=17, name_prefix='clitest', location='westus2') + def test_aks_update_with_defender(self, resource_group, resource_group_location): + aks_name = self.create_random_name('cliakstest', 16) + self.kwargs.update({ + 'resource_group': resource_group, + 'name': aks_name, + 'ssh_key_value': self.generate_ssh_keys() + }) + + create_cmd = 'aks create --resource-group={resource_group} --name={name} --ssh-key-value={ssh_key_value}' + self.cmd(create_cmd, checks=[ + self.check('provisioningState', 'Succeeded'), + ]) + + # update to enable defender + self.cmd('aks update --resource-group={resource_group} --name={name} --enable-defender', checks=[ + self.check('provisioningState', 'Succeeded'), + self.check('securityProfile.defender.securityMonitoring.enabled', True) + ]) + + # update to disable defender + self.cmd('aks update --resource-group={resource_group} --name={name} --disable-defender', checks=[ + self.check('provisioningState', 'Succeeded'), + self.check('securityProfile.defender.securityMonitoring.enabled', False) + ]) + + # delete + self.cmd( + 'aks delete -g {resource_group} -n {name} --yes --no-wait', checks=[self.is_empty()]) 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 4db8c1ab14c..2ead4fcb183 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 @@ -2248,6 +2248,78 @@ def test_get_disable_keda(self): with self.assertRaises(MutuallyExclusiveArgumentError): ctx_5.get_disable_keda() + def test_get_defender_config(self): + ctx_1 = AKSPreviewManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict( + { + "enable_defender": True, + "defender_config": get_test_data_file_path( + "defenderconfig.json" + ), + } + ), + self.models, + DecoratorMode.CREATE, + ) + defender_config_1 = ctx_1.get_defender_config() + ground_truth_defender_config_1 = self.models.ManagedClusterSecurityProfileDefender( + log_analytics_workspace_resource_id="test_workspace_resource_id", + security_monitoring=self.models.ManagedClusterSecurityProfileDefenderSecurityMonitoring( + enabled=True + ), + ) + self.assertEqual(defender_config_1, ground_truth_defender_config_1) + + # custom value + ctx_2 = AKSPreviewManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict( + {"enable_defender": True, "defender_config": "fake-path"} + ), + self.models, + DecoratorMode.CREATE, + ) + # fail on invalid file path + with self.assertRaises(InvalidArgumentValueError): + ctx_2.get_defender_config() + + # custom + ctx_3 = AKSPreviewManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict({"disable_defender": True}), + self.models, + DecoratorMode.UPDATE, + ) + defender_config_3 = ctx_3.get_defender_config() + ground_truth_defender_config_3 = self.models.ManagedClusterSecurityProfileDefender( + security_monitoring=self.models.ManagedClusterSecurityProfileDefenderSecurityMonitoring( + enabled=False, + ), + ) + self.assertEqual(defender_config_3, ground_truth_defender_config_3) + + # custom + ctx_4 = AKSPreviewManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict({"enable_defender": True}), + self.models, + DecoratorMode.UPDATE, + ) + ctx_4.set_intermediate("subscription_id", "test_subscription_id") + with patch( + "azure.cli.command_modules.acs.managed_cluster_decorator.ensure_default_log_analytics_workspace_for_monitoring", + return_value="test_workspace_resource_id", + ): + defender_config_4 = ctx_4.get_defender_config() + ground_truth_defender_config_4 = self.models.ManagedClusterSecurityProfileDefender( + log_analytics_workspace_resource_id="test_workspace_resource_id", + security_monitoring=self.models.ManagedClusterSecurityProfileDefenderSecurityMonitoring( + enabled=True, + ), + ) + self.assertEqual(defender_config_4, ground_truth_defender_config_4) + class AKSPreviewManagedClusterCreateDecoratorTestCase(unittest.TestCase): def setUp(self): @@ -2913,6 +2985,36 @@ def test_set_up_workload_auto_scaler_profile(self): self.assertIsNotNone(mc_out.workload_auto_scaler_profile.keda) self.assertTrue(mc_out.workload_auto_scaler_profile.keda.enabled) + def test_set_up_defender(self): + dec_1 = AKSPreviewManagedClusterCreateDecorator( + self.cmd, + self.client, + {"enable_defender": True}, + CUSTOM_MGMT_AKS_PREVIEW, + ) + mc_1 = self.models.ManagedCluster(location="test_location") + dec_1.context.attach_mc(mc_1) + dec_1.context.set_intermediate("subscription_id", "test_subscription_id") + + with patch( + "azure.cli.command_modules.acs.managed_cluster_decorator.ensure_default_log_analytics_workspace_for_monitoring", + return_value="test_workspace_resource_id", + ): + dec_mc_1 = dec_1.set_up_defender(mc_1) + + ground_truth_mc_1 = self.models.ManagedCluster( + location="test_location", + security_profile=self.models.ManagedClusterSecurityProfile( + defender=self.models.ManagedClusterSecurityProfileDefender( + log_analytics_workspace_resource_id="test_workspace_resource_id", + security_monitoring=self.models.ManagedClusterSecurityProfileDefenderSecurityMonitoring( + enabled=True + ), + ) + ), + ) + self.assertEqual(dec_mc_1, ground_truth_mc_1) + def test_construct_mc_profile_preview(self): import inspect @@ -3990,6 +4092,77 @@ def test_update_workload_auto_scaler_profile(self): with self.assertRaises(MutuallyExclusiveArgumentError): mc_out = dec_9.update_workload_auto_scaler_profile(mc_in) + def test_update_defender(self): + # enable + dec_1 = AKSPreviewManagedClusterUpdateDecorator( + self.cmd, + self.client, + { + "enable_defender": True, + "defender_config": get_test_data_file_path( + "defenderconfig.json" + ), + }, + CUSTOM_MGMT_AKS_PREVIEW, + ) + mc_1 = self.models.ManagedCluster(location="test_location") + dec_1.context.attach_mc(mc_1) + dec_1.context.set_intermediate( + "subscription_id", "test_subscription_id" + ) + + dec_mc_1 = dec_1.update_defender(mc_1) + + ground_truth_mc_1 = self.models.ManagedCluster( + location="test_location", + security_profile=self.models.ManagedClusterSecurityProfile( + defender=self.models.ManagedClusterSecurityProfileDefender( + log_analytics_workspace_resource_id="test_workspace_resource_id", + security_monitoring=self.models.ManagedClusterSecurityProfileDefenderSecurityMonitoring( + enabled=True + ), + ) + ), + ) + self.assertEqual(dec_mc_1, ground_truth_mc_1) + + # disable + dec_2 = AKSPreviewManagedClusterUpdateDecorator( + self.cmd, + self.client, + {"disable_defender": True}, + CUSTOM_MGMT_AKS_PREVIEW, + ) + mc_2 = self.models.ManagedCluster( + location="test_location", + security_profile=self.models.ManagedClusterSecurityProfile( + defender=self.models.ManagedClusterSecurityProfileDefender( + log_analytics_workspace_resource_id="test_workspace_resource_id", + security_monitoring=self.models.ManagedClusterSecurityProfileDefenderSecurityMonitoring( + enabled=True + ), + ) + ), + ) + dec_2.context.attach_mc(mc_2) + dec_2.context.set_intermediate( + "subscription_id", "test_subscription_id" + ) + + dec_mc_2 = dec_2.update_defender(mc_2) + + ground_truth_mc_2 = self.models.ManagedCluster( + location="test_location", + security_profile=self.models.ManagedClusterSecurityProfile( + defender=self.models.ManagedClusterSecurityProfileDefender( + security_monitoring=self.models.ManagedClusterSecurityProfileDefenderSecurityMonitoring( + enabled=False + ), + ) + ), + ) + self.assertEqual(dec_mc_2, ground_truth_mc_2) + def test_update_mc_profile_preview(self): import inspect diff --git a/src/aks-preview/setup.py b/src/aks-preview/setup.py index 5272a66fb50..5d5444cb3fb 100644 --- a/src/aks-preview/setup.py +++ b/src/aks-preview/setup.py @@ -9,7 +9,7 @@ from setuptools import setup, find_packages -VERSION = "0.5.89" +VERSION = "0.5.90" CLASSIFIERS = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers",