From 3b8621a04df2ba672701bcdf23a1647fcb2db122 Mon Sep 17 00:00:00 2001 From: Fuming Zhang Date: Mon, 11 Oct 2021 13:47:50 +0800 Subject: [PATCH 1/4] add decorator pattern basic structure --- src/aks-preview/azext_aks_preview/__init__.py | 14 +- .../azext_aks_preview/decorator.py | 206 ++++++++++++++++++ .../azext_aks_preview/tests/latest/mocks.py | 55 +++++ .../tests/latest/test_decorator.py | 199 +++++++++++++++++ 4 files changed, 469 insertions(+), 5 deletions(-) create mode 100644 src/aks-preview/azext_aks_preview/decorator.py create mode 100644 src/aks-preview/azext_aks_preview/tests/latest/mocks.py create mode 100644 src/aks-preview/azext_aks_preview/tests/latest/test_decorator.py diff --git a/src/aks-preview/azext_aks_preview/__init__.py b/src/aks-preview/azext_aks_preview/__init__.py index 06a96a4fdc0..4a109b41064 100644 --- a/src/aks-preview/azext_aks_preview/__init__.py +++ b/src/aks-preview/azext_aks_preview/__init__.py @@ -12,15 +12,19 @@ from azext_aks_preview._client_factory import CUSTOM_MGMT_AKS_PREVIEW +def register_aks_preview_resource_type(): + register_resource_type( + "latest", + CUSTOM_MGMT_AKS_PREVIEW, + SDKProfile("2021-08-01", {"container_services": "2017-07-01"}), + ) + + class ContainerServiceCommandsLoader(AzCommandsLoader): def __init__(self, cli_ctx=None): from azure.cli.core.commands import CliCommandType - register_resource_type( - "latest", - CUSTOM_MGMT_AKS_PREVIEW, - SDKProfile("2021-08-01", {"container_services": "2017-07-01"}), - ) + register_aks_preview_resource_type() acs_custom = CliCommandType(operations_tmpl='azext_aks_preview.custom#{}') super(ContainerServiceCommandsLoader, self).__init__(cli_ctx=cli_ctx, diff --git a/src/aks-preview/azext_aks_preview/decorator.py b/src/aks-preview/azext_aks_preview/decorator.py new file mode 100644 index 00000000000..66560f2562d --- /dev/null +++ b/src/aks-preview/azext_aks_preview/decorator.py @@ -0,0 +1,206 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from typing import Any, Dict, List, Tuple, TypeVar, Union + +from azure.cli.command_modules.acs._consts import ( + DecoratorMode, +) +from azure.cli.command_modules.acs.decorator import ( + AKSModels, + AKSContext, + AKSCreateDecorator, + AKSUpdateDecorator, + safe_list_get, +) +from azure.cli.core import AzCommandsLoader +from azure.cli.core.commands import AzCliCommand +from azure.cli.core.profiles import ResourceType +from knack.log import get_logger + +logger = get_logger(__name__) + +# type variables +ContainerServiceClient = TypeVar("ContainerServiceClient") +Identity = TypeVar("Identity") +ManagedCluster = TypeVar("ManagedCluster") +ManagedClusterLoadBalancerProfile = TypeVar("ManagedClusterLoadBalancerProfile") +ResourceReference = TypeVar("ResourceReference") + + +class AKSPreviewModels(AKSModels): + def __init__(self, cmd: AzCommandsLoader, resource_type: ResourceType = ...): + super().__init__(cmd, resource_type=resource_type) + + +class AKSPreviewContext(AKSContext): + def __init__(self, cmd: AzCliCommand, raw_parameters: Dict, models: AKSPreviewModels, decorator_mode): + super().__init__(cmd, raw_parameters, models, decorator_mode) + + def get_pod_subnet_id(self) -> Union[str, None]: + """Obtain the value of pod_subnet_id. + + :return: bool + """ + # read the original value passed by the command + raw_value = self.raw_param.get("pod_subnet_id") + # try to read the property value corresponding to the parameter from the `mc` object + value_obtained_from_mc = None + if self.mc and self.mc.agent_pool_profiles: + agent_pool_profile = safe_list_get( + self.mc.agent_pool_profiles, 0, None + ) + if agent_pool_profile: + value_obtained_from_mc = agent_pool_profile.pod_subnet_id + + # set default value + if value_obtained_from_mc is not None: + pod_subnet_id = value_obtained_from_mc + else: + pod_subnet_id = raw_value + + # this parameter does not need dynamic completion + # this parameter does not need validation + return pod_subnet_id + + def get_enable_fips_image(self) -> bool: + """Obtain the value of enable_fips_image. + + :return: bool + """ + # read the original value passed by the command + raw_value = self.raw_param.get("enable_fips_image") + # try to read the property value corresponding to the parameter from the `mc` object + value_obtained_from_mc = None + if self.mc and self.mc.agent_pool_profiles: + agent_pool_profile = safe_list_get( + self.mc.agent_pool_profiles, 0, None + ) + if agent_pool_profile: + value_obtained_from_mc = agent_pool_profile.enable_fips + + # set default value + if value_obtained_from_mc is not None: + enable_fips_image = value_obtained_from_mc + else: + enable_fips_image = raw_value + + # this parameter does not need dynamic completion + # this parameter does not need validation + return enable_fips_image + + def get_workload_runtime(self) -> Union[str, None]: + """Obtain the value of workload_runtime. + + :return: string or None + """ + # read the original value passed by the command + raw_value = self.raw_param.get("workload_runtime") + # try to read the property value corresponding to the parameter from the `mc` object + value_obtained_from_mc = None + if self.mc and self.mc.agent_pool_profiles: + agent_pool_profile = safe_list_get( + self.mc.agent_pool_profiles, 0, None + ) + if agent_pool_profile: + value_obtained_from_mc = agent_pool_profile.workload_runtime + + # set default value + if value_obtained_from_mc is not None: + workload_runtime = value_obtained_from_mc + else: + workload_runtime = raw_value + + # this parameter does not need dynamic completion + # this parameter does not need validation + return workload_runtime + + def get_gpu_instance_profile(self) -> Union[str, None]: + """Obtain the value of gpu_instance_profile. + + :return: string or None + """ + # read the original value passed by the command + raw_value = self.raw_param.get("gpu_instance_profile") + # try to read the property value corresponding to the parameter from the `mc` object + value_obtained_from_mc = None + if self.mc and self.mc.agent_pool_profiles: + agent_pool_profile = safe_list_get( + self.mc.agent_pool_profiles, 0, None + ) + if agent_pool_profile: + value_obtained_from_mc = agent_pool_profile.gpu_instance_profile + + # set default value + if value_obtained_from_mc is not None: + gpu_instance_profile = value_obtained_from_mc + else: + gpu_instance_profile = raw_value + + # this parameter does not need dynamic completion + # this parameter does not need validation + return gpu_instance_profile + + +class AKSPreviewCreateDecorator(AKSCreateDecorator): + # pylint: disable=super-init-not-called + def __init__( + self, + cmd: AzCliCommand, + client: ContainerServiceClient, + raw_parameters: Dict, + resource_type: ResourceType, + ): + """Internal controller of aks_create in aks-preview. + + Break down the all-in-one aks_create function into several relatively independent functions (some of them have + a certain order dependency) that only focus on a specific profile or process a specific piece of logic. + In addition, an overall control function is provided. By calling the aforementioned independent functions one + by one, a complete ManagedCluster object is gradually decorated and finally requests are sent to create a + cluster. + """ + self.cmd = cmd + self.client = client + self.models = AKSPreviewModels(cmd, resource_type) + # store the context in the process of assemble the ManagedCluster object + self.context = AKSPreviewContext(cmd, raw_parameters, self.models, decorator_mode=DecoratorMode.CREATE) + + def set_up_agent_pool_profiles(self, mc: ManagedCluster) -> ManagedCluster: + """Set up agent pool profiles for the ManagedCluster object. + + :return: the ManagedCluster object + """ + mc = super().set_up_agent_pool_profiles(mc) + agent_pool_profile = safe_list_get(mc.agent_pool_profiles, 0, None) + + # set up extra parameters supported in aks-preview + agent_pool_profile.pod_subnet_id = self.context.get_pod_subnet_id() + agent_pool_profile.enable_fips = self.context.get_enable_fips_image() + agent_pool_profile.workload_runtime = self.context.get_workload_runtime() + agent_pool_profile.gpu_instance_profile = self.context.get_gpu_instance_profile() + return mc + +class AKSPreviewUpdateDecorator(AKSUpdateDecorator): + # pylint: disable=super-init-not-called + def __init__( + self, + cmd: AzCliCommand, + client: ContainerServiceClient, + raw_parameters: Dict, + resource_type: ResourceType, + ): + """Internal controller of aks_update in aks-preview. + + Break down the all-in-one aks_update function into several relatively independent functions (some of them have + a certain order dependency) that only focus on a specific profile or process a specific piece of logic. + In addition, an overall control function is provided. By calling the aforementioned independent functions one + by one, a complete ManagedCluster object is gradually updated and finally requests are sent to update an + existing cluster. + """ + self.cmd = cmd + self.client = client + self.models = AKSPreviewModels(cmd, resource_type) + # store the context in the process of assemble the ManagedCluster object + self.context = AKSPreviewContext(cmd, raw_parameters, self.models, decorator_mode=DecoratorMode.UPDATE) diff --git a/src/aks-preview/azext_aks_preview/tests/latest/mocks.py b/src/aks-preview/azext_aks_preview/tests/latest/mocks.py new file mode 100644 index 00000000000..14435a8a2e5 --- /dev/null +++ b/src/aks-preview/azext_aks_preview/tests/latest/mocks.py @@ -0,0 +1,55 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from knack import CLI +import tempfile + +from azure.cli.core import AzCommandsLoader +from azure.cli.core.cloud import get_active_cloud +from azure.cli.core.commands import AzCliCommand +from azure.cli.core._config import ENV_VAR_PREFIX + +MOCK_CLI_CONFIG_DIR = tempfile.mkdtemp() +MOCK_CLI_ENV_VAR_PREFIX = "MOCK_" + ENV_VAR_PREFIX + + +class MockClient(object): + def __init__(self): + pass + + +class MockCLI(CLI): + def __init__(self): + super(MockCLI, self).__init__( + cli_name="mock_cli", + config_dir=MOCK_CLI_CONFIG_DIR, + config_env_var_prefix=MOCK_CLI_ENV_VAR_PREFIX, + ) + self.cloud = get_active_cloud(self) + + +class MockCmd(object): + def __init__(self, cli_ctx): + self.cli_ctx = cli_ctx + self.cmd = AzCliCommand(AzCommandsLoader(cli_ctx), "mock-cmd", None) + + def supported_api_version( + self, + resource_type=None, + min_api=None, + max_api=None, + operation_group=None, + parameter_name=None, + ): + return self.cmd.supported_api_version( + resource_type=resource_type, + min_api=min_api, + max_api=max_api, + operation_group=operation_group, + parameter_name=parameter_name, + ) + + def get_models(self, *attr_args, **kwargs): + return self.cmd.get_models(*attr_args, **kwargs) diff --git a/src/aks-preview/azext_aks_preview/tests/latest/test_decorator.py b/src/aks-preview/azext_aks_preview/tests/latest/test_decorator.py new file mode 100644 index 00000000000..f7e58965416 --- /dev/null +++ b/src/aks-preview/azext_aks_preview/tests/latest/test_decorator.py @@ -0,0 +1,199 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +import unittest + +from azure.cli.command_modules.acs._consts import ( + DecoratorMode, +) +from azure.cli.core.azclierror import ( + ArgumentUsageError, + CLIInternalError, + InvalidArgumentValueError, + MutuallyExclusiveArgumentError, + NoTTYError, + RequiredArgumentMissingError, + UnknownError, +) +from azext_aks_preview.decorator import ( + AKSPreviewContext, + AKSPreviewCreateDecorator, + AKSPreviewModels, + AKSPreviewUpdateDecorator, +) +from azext_aks_preview.tests.latest.mocks import ( + MockCLI, + MockClient, + MockCmd, +) +from azext_aks_preview._client_factory import CUSTOM_MGMT_AKS_PREVIEW +from azext_aks_preview.__init__ import register_aks_preview_resource_type + +class AKSPreviewModelsTestCase(unittest.TestCase): + def setUp(self): + self.cli_ctx = MockCLI() + self.cmd = MockCmd(self.cli_ctx) + +class AKSPreviewContextTestCase(unittest.TestCase): + def setUp(self): + # manually register CUSTOM_MGMT_AKS_PREVIEW + register_aks_preview_resource_type() + self.cli_ctx = MockCLI() + self.cmd = MockCmd(self.cli_ctx) + self.models = AKSPreviewModels(self.cmd, CUSTOM_MGMT_AKS_PREVIEW) + + def test_get_pod_subnet_id(self): + # default + ctx_1 = AKSPreviewContext( + self.cmd, + {"pod_subnet_id": None}, + self.models, + decorator_mode=DecoratorMode.CREATE, + ) + self.assertEqual(ctx_1.get_pod_subnet_id(), None) + agent_pool_profile = self.models.ManagedClusterAgentPoolProfile( + name="test_nodepool_name", pod_subnet_id="test_mc_pod_subnet_id" + ) + mc = self.models.ManagedCluster( + location="test_location", agent_pool_profiles=[agent_pool_profile] + ) + ctx_1.attach_mc(mc) + self.assertEqual(ctx_1.get_pod_subnet_id(), "test_mc_pod_subnet_id") + + def test_get_enable_fips_image(self): + # default + ctx_1 = AKSPreviewContext( + self.cmd, + {"enable_fips_image": False}, + self.models, + decorator_mode=DecoratorMode.CREATE, + ) + self.assertEqual(ctx_1.get_enable_fips_image(), False) + agent_pool_profile = self.models.ManagedClusterAgentPoolProfile( + name="test_nodepool_name", enable_fips=True, + ) + mc = self.models.ManagedCluster( + location="test_location", agent_pool_profiles=[agent_pool_profile] + ) + ctx_1.attach_mc(mc) + self.assertEqual(ctx_1.get_enable_fips_image(), True) + + def test_get_workload_runtime(self): + # default + ctx_1 = AKSPreviewContext( + self.cmd, + {"workload_runtime": None}, + self.models, + decorator_mode=DecoratorMode.CREATE, + ) + self.assertEqual(ctx_1.get_workload_runtime(), None) + agent_pool_profile = self.models.ManagedClusterAgentPoolProfile( + name="test_nodepool_name", workload_runtime="test_mc_workload_runtime", + ) + mc = self.models.ManagedCluster( + location="test_location", agent_pool_profiles=[agent_pool_profile] + ) + ctx_1.attach_mc(mc) + self.assertEqual(ctx_1.get_workload_runtime(), "test_mc_workload_runtime") + + def test_get_gpu_instance_profile(self): + # default + ctx_1 = AKSPreviewContext( + self.cmd, + {"gpu_instance_profile": None}, + self.models, + decorator_mode=DecoratorMode.CREATE, + ) + self.assertEqual(ctx_1.get_gpu_instance_profile(), None) + agent_pool_profile = self.models.ManagedClusterAgentPoolProfile( + name="test_nodepool_name", gpu_instance_profile="test_mc_gpu_instance_profile", + ) + mc = self.models.ManagedCluster( + location="test_location", agent_pool_profiles=[agent_pool_profile] + ) + ctx_1.attach_mc(mc) + self.assertEqual(ctx_1.get_gpu_instance_profile(), "test_mc_gpu_instance_profile") + +class AKSPreviewCreateDecoratorTestCase(unittest.TestCase): + def setUp(self): + # manually register CUSTOM_MGMT_AKS_PREVIEW + register_aks_preview_resource_type() + self.cli_ctx = MockCLI() + self.cmd = MockCmd(self.cli_ctx) + self.models = AKSPreviewModels(self.cmd, CUSTOM_MGMT_AKS_PREVIEW) + self.client = MockClient() + + def test_set_up_agent_pool_profiles(self): + # default value in `aks_create` + dec_1 = AKSPreviewCreateDecorator( + self.cmd, + self.client, + { + "nodepool_name": "nodepool1", + "nodepool_tags": None, + "nodepool_labels": None, + "node_count": 3, + "node_vm_size": "Standard_DS2_v2", + "os_sku": None, + "vnet_subnet_id": None, + "pod_subnet_id": None, + "ppg": None, + "zones": None, + "enable_fips_image": False, + "enable_node_public_ip": False, + "node_public_ip_prefix_id": None, + "enable_encryption_at_host": False, + "enable_ultra_ssd": False, + "max_pods": 0, + "node_osdisk_size": 0, + "node_osdisk_type": None, + "enable_cluster_autoscaler": False, + "min_count": None, + "max_count": None, + }, + CUSTOM_MGMT_AKS_PREVIEW, + ) + mc_1 = self.models.ManagedCluster(location="test_location") + # fail on passing the wrong mc object + with self.assertRaises(CLIInternalError): + dec_1.set_up_agent_pool_profiles(None) + dec_mc_1 = dec_1.set_up_agent_pool_profiles(mc_1) + agent_pool_profile_1 = self.models.ManagedClusterAgentPoolProfile( + # Must be 12 chars or less before ACS RP adds to it + name="nodepool1", + tags=None, + node_labels=None, + count=3, + vm_size="Standard_DS2_v2", + os_type="Linux", + vnet_subnet_id=None, + proximity_placement_group_id=None, + availability_zones=None, + enable_node_public_ip=False, + node_public_ip_prefix_id=None, + enable_encryption_at_host=False, + enable_ultra_ssd=False, + max_pods=None, + type="VirtualMachineScaleSets", + mode="System", + os_disk_size_gb=None, + os_disk_type=None, + enable_auto_scaling=False, + min_count=None, + max_count=None, + ) + ground_truth_mc_1 = self.models.ManagedCluster(location="test_location") + ground_truth_mc_1.agent_pool_profiles = [agent_pool_profile_1] + self.assertEqual(dec_mc_1, ground_truth_mc_1) + + +class AKSPreviewUpdateDecoratorTestCase(unittest.TestCase): + def setUp(self): + # manually register CUSTOM_MGMT_AKS_PREVIEW + register_aks_preview_resource_type() + self.cli_ctx = MockCLI() + self.cmd = MockCmd(self.cli_ctx) + self.models = AKSPreviewModels(self.cmd, CUSTOM_MGMT_AKS_PREVIEW) + self.client = MockClient() From ec894093be27d10504e3ab9e262eaaef09ad45f3 Mon Sep 17 00:00:00 2001 From: Fuming Zhang Date: Mon, 11 Oct 2021 16:04:16 +0800 Subject: [PATCH 2/4] fix test --- .../tests/latest/test_decorator.py | 75 ++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/src/aks-preview/azext_aks_preview/tests/latest/test_decorator.py b/src/aks-preview/azext_aks_preview/tests/latest/test_decorator.py index f7e58965416..45377a76d4a 100644 --- a/src/aks-preview/azext_aks_preview/tests/latest/test_decorator.py +++ b/src/aks-preview/azext_aks_preview/tests/latest/test_decorator.py @@ -141,8 +141,8 @@ def test_set_up_agent_pool_profiles(self): "pod_subnet_id": None, "ppg": None, "zones": None, - "enable_fips_image": False, "enable_node_public_ip": False, + "enable_fips_image": False, "node_public_ip_prefix_id": None, "enable_encryption_at_host": False, "enable_ultra_ssd": False, @@ -152,6 +152,8 @@ def test_set_up_agent_pool_profiles(self): "enable_cluster_autoscaler": False, "min_count": None, "max_count": None, + "workload_runtime": None, + "gpu_instance_profile": None, }, CUSTOM_MGMT_AKS_PREVIEW, ) @@ -168,10 +170,13 @@ def test_set_up_agent_pool_profiles(self): count=3, vm_size="Standard_DS2_v2", os_type="Linux", + os_sku=None, vnet_subnet_id=None, + pod_subnet_id=None, proximity_placement_group_id=None, availability_zones=None, enable_node_public_ip=False, + enable_fips=False, node_public_ip_prefix_id=None, enable_encryption_at_host=False, enable_ultra_ssd=False, @@ -183,11 +188,79 @@ def test_set_up_agent_pool_profiles(self): enable_auto_scaling=False, min_count=None, max_count=None, + workload_runtime=None, + gpu_instance_profile=None, ) ground_truth_mc_1 = self.models.ManagedCluster(location="test_location") ground_truth_mc_1.agent_pool_profiles = [agent_pool_profile_1] self.assertEqual(dec_mc_1, ground_truth_mc_1) + # custom value + dec_2 = AKSPreviewCreateDecorator( + self.cmd, + self.client, + { + "nodepool_name": "test_np_name1234", + "nodepool_tags": {"k1": "v1"}, + "nodepool_labels": {"k1": "v1", "k2": "v2"}, + "node_count": 10, + "node_vm_size": "Standard_DSx_vy", + "os_sku": "test_os_sku", + "vnet_subnet_id": "test_vnet_subnet_id", + "pod_subnet_id": "test_pod_subnet_id", + "ppg": "test_ppg_id", + "zones": ["tz1", "tz2"], + "enable_node_public_ip": True, + "enable_fips_image": True, + "node_public_ip_prefix_id": "test_node_public_ip_prefix_id", + "enable_encryption_at_host": True, + "enable_ultra_ssd": True, + "max_pods": 50, + "node_osdisk_size": 100, + "node_osdisk_type": "test_os_disk_type", + "enable_cluster_autoscaler": True, + "min_count": 5, + "max_count": 20, + "workload_runtime": "test_workload_runtime", + "gpu_instance_profile": "test_gpu_instance_profile", + }, + CUSTOM_MGMT_AKS_PREVIEW, + ) + mc_2 = self.models.ManagedCluster(location="test_location") + dec_mc_2 = dec_2.set_up_agent_pool_profiles(mc_2) + agent_pool_profile_2 = self.models.ManagedClusterAgentPoolProfile( + # Must be 12 chars or less before ACS RP adds to it + name="test_np_name", + tags={"k1": "v1"}, + node_labels={"k1": "v1", "k2": "v2"}, + count=10, + vm_size="Standard_DSx_vy", + os_type="Linux", + os_sku= "test_os_sku", + vnet_subnet_id="test_vnet_subnet_id", + pod_subnet_id="test_pod_subnet_id", + proximity_placement_group_id="test_ppg_id", + availability_zones=["tz1", "tz2"], + enable_node_public_ip=True, + enable_fips=True, + node_public_ip_prefix_id="test_node_public_ip_prefix_id", + enable_encryption_at_host=True, + enable_ultra_ssd=True, + max_pods=50, + type="VirtualMachineScaleSets", + mode="System", + os_disk_size_gb=100, + os_disk_type="test_os_disk_type", + enable_auto_scaling=True, + min_count=5, + max_count=20, + workload_runtime="test_workload_runtime", + gpu_instance_profile="test_gpu_instance_profile", + ) + ground_truth_mc_2 = self.models.ManagedCluster(location="test_location") + ground_truth_mc_2.agent_pool_profiles = [agent_pool_profile_2] + self.assertEqual(dec_mc_2, ground_truth_mc_2) + class AKSPreviewUpdateDecoratorTestCase(unittest.TestCase): def setUp(self): From 0f27ddc1f30bde14a2a4cc069fc55ccb768b9696 Mon Sep 17 00:00:00 2001 From: Fuming Zhang Date: Wed, 13 Oct 2021 13:34:26 +0800 Subject: [PATCH 3/4] refactor kubelet_config & linux_os_config --- .../azext_aks_preview/decorator.py | 145 ++++++++++++-- .../tests/latest/data/invalidconfig.json | 1 + .../tests/latest/test_decorator.py | 181 ++++++++++++++++-- 3 files changed, 293 insertions(+), 34 deletions(-) create mode 100644 src/aks-preview/azext_aks_preview/tests/latest/data/invalidconfig.json diff --git a/src/aks-preview/azext_aks_preview/decorator.py b/src/aks-preview/azext_aks_preview/decorator.py index 66560f2562d..a0217dceb1c 100644 --- a/src/aks-preview/azext_aks_preview/decorator.py +++ b/src/aks-preview/azext_aks_preview/decorator.py @@ -3,21 +3,22 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -from typing import Any, Dict, List, Tuple, TypeVar, Union +import os +from typing import Dict, TypeVar, Union -from azure.cli.command_modules.acs._consts import ( - DecoratorMode, -) +from azure.cli.command_modules.acs._consts import DecoratorMode from azure.cli.command_modules.acs.decorator import ( - AKSModels, AKSContext, AKSCreateDecorator, + AKSModels, AKSUpdateDecorator, safe_list_get, ) from azure.cli.core import AzCommandsLoader +from azure.cli.core.azclierror import InvalidArgumentValueError from azure.cli.core.commands import AzCliCommand from azure.cli.core.profiles import ResourceType +from azure.cli.core.util import get_file_json from knack.log import get_logger logger = get_logger(__name__) @@ -28,15 +29,36 @@ ManagedCluster = TypeVar("ManagedCluster") ManagedClusterLoadBalancerProfile = TypeVar("ManagedClusterLoadBalancerProfile") ResourceReference = TypeVar("ResourceReference") +KubeletConfig = TypeVar("KubeletConfig") +LinuxOSConfig = TypeVar("LinuxOSConfig") +# pylint: disable=too-many-instance-attributes,too-few-public-methods class AKSPreviewModels(AKSModels): - def __init__(self, cmd: AzCommandsLoader, resource_type: ResourceType = ...): + def __init__(self, cmd: AzCommandsLoader, resource_type: ResourceType): super().__init__(cmd, resource_type=resource_type) - - + self.__cmd = cmd + self.KubeletConfig = self.__cmd.get_models( + "KubeletConfig", + resource_type=self.resource_type, + operation_group="managed_clusters", + ) + self.LinuxOSConfig = self.__cmd.get_models( + "LinuxOSConfig", + resource_type=self.resource_type, + operation_group="managed_clusters", + ) + + +# pylint: disable=too-many-public-methods class AKSPreviewContext(AKSContext): - def __init__(self, cmd: AzCliCommand, raw_parameters: Dict, models: AKSPreviewModels, decorator_mode): + def __init__( + self, + cmd: AzCliCommand, + raw_parameters: Dict, + models: AKSPreviewModels, + decorator_mode, + ): super().__init__(cmd, raw_parameters, models, decorator_mode) def get_pod_subnet_id(self) -> Union[str, None]: @@ -143,6 +165,86 @@ def get_gpu_instance_profile(self) -> Union[str, None]: # this parameter does not need validation return gpu_instance_profile + def get_kubelet_config(self) -> Union[dict, KubeletConfig, None]: + """Obtain the value of kubelet_config. + + :return: dict, KubeletConfig or None + """ + # read the original value passed by the command + kubelet_config = None + kubelet_config_file_path = self.raw_param.get("kubelet_config") + # validate user input + if kubelet_config_file_path: + if not os.path.isfile(kubelet_config_file_path): + raise InvalidArgumentValueError( + "{} is not valid file, or not accessable.".format( + kubelet_config_file_path + ) + ) + kubelet_config = get_file_json(kubelet_config_file_path) + if not isinstance(kubelet_config, dict): + raise InvalidArgumentValueError( + "Error reading kubelet configuration from {}. " + "Please see https://aka.ms/CustomNodeConfig for correct format.".format( + kubelet_config_file_path + ) + ) + + # try to read the property value corresponding to the parameter from the `mc` object + if self.mc and self.mc.agent_pool_profiles: + agent_pool_profile = safe_list_get( + self.mc.agent_pool_profiles, 0, None + ) + if ( + agent_pool_profile + and agent_pool_profile.kubelet_config is not None + ): + kubelet_config = agent_pool_profile.kubelet_config + + # this parameter does not need dynamic completion + # this parameter does not need validation + return kubelet_config + + def get_linux_os_config(self) -> Union[dict, LinuxOSConfig, None]: + """Obtain the value of linux_os_config. + + :return: dict, LinuxOSConfig or None + """ + # read the original value passed by the command + linux_os_config = None + linux_os_config_file_path = self.raw_param.get("linux_os_config") + # validate user input + if linux_os_config_file_path: + if not os.path.isfile(linux_os_config_file_path): + raise InvalidArgumentValueError( + "{} is not valid file, or not accessable.".format( + linux_os_config_file_path + ) + ) + linux_os_config = get_file_json(linux_os_config_file_path) + if not isinstance(linux_os_config, dict): + raise InvalidArgumentValueError( + "Error reading Linux OS configuration from {}. " + "Please see https://aka.ms/CustomNodeConfig for correct format.".format( + linux_os_config_file_path + ) + ) + + # try to read the property value corresponding to the parameter from the `mc` object + if self.mc and self.mc.agent_pool_profiles: + agent_pool_profile = safe_list_get( + self.mc.agent_pool_profiles, 0, None + ) + if ( + agent_pool_profile + and agent_pool_profile.linux_os_config is not None + ): + linux_os_config = agent_pool_profile.linux_os_config + + # this parameter does not need dynamic completion + # this parameter does not need validation + return linux_os_config + class AKSPreviewCreateDecorator(AKSCreateDecorator): # pylint: disable=super-init-not-called @@ -165,7 +267,12 @@ def __init__( self.client = client self.models = AKSPreviewModels(cmd, resource_type) # store the context in the process of assemble the ManagedCluster object - self.context = AKSPreviewContext(cmd, raw_parameters, self.models, decorator_mode=DecoratorMode.CREATE) + self.context = AKSPreviewContext( + cmd, + raw_parameters, + self.models, + decorator_mode=DecoratorMode.CREATE, + ) def set_up_agent_pool_profiles(self, mc: ManagedCluster) -> ManagedCluster: """Set up agent pool profiles for the ManagedCluster object. @@ -178,10 +285,17 @@ def set_up_agent_pool_profiles(self, mc: ManagedCluster) -> ManagedCluster: # set up extra parameters supported in aks-preview agent_pool_profile.pod_subnet_id = self.context.get_pod_subnet_id() agent_pool_profile.enable_fips = self.context.get_enable_fips_image() - agent_pool_profile.workload_runtime = self.context.get_workload_runtime() - agent_pool_profile.gpu_instance_profile = self.context.get_gpu_instance_profile() + agent_pool_profile.workload_runtime = ( + self.context.get_workload_runtime() + ) + agent_pool_profile.gpu_instance_profile = ( + self.context.get_gpu_instance_profile() + ) + agent_pool_profile.kubelet_config = self.context.get_kubelet_config() + agent_pool_profile.linux_os_config = self.context.get_linux_os_config() return mc + class AKSPreviewUpdateDecorator(AKSUpdateDecorator): # pylint: disable=super-init-not-called def __init__( @@ -203,4 +317,9 @@ def __init__( self.client = client self.models = AKSPreviewModels(cmd, resource_type) # store the context in the process of assemble the ManagedCluster object - self.context = AKSPreviewContext(cmd, raw_parameters, self.models, decorator_mode=DecoratorMode.UPDATE) + self.context = AKSPreviewContext( + cmd, + raw_parameters, + self.models, + decorator_mode=DecoratorMode.UPDATE, + ) diff --git a/src/aks-preview/azext_aks_preview/tests/latest/data/invalidconfig.json b/src/aks-preview/azext_aks_preview/tests/latest/data/invalidconfig.json new file mode 100644 index 00000000000..0637a088a01 --- /dev/null +++ b/src/aks-preview/azext_aks_preview/tests/latest/data/invalidconfig.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/src/aks-preview/azext_aks_preview/tests/latest/test_decorator.py b/src/aks-preview/azext_aks_preview/tests/latest/test_decorator.py index 45377a76d4a..ed9c9a33e0b 100644 --- a/src/aks-preview/azext_aks_preview/tests/latest/test_decorator.py +++ b/src/aks-preview/azext_aks_preview/tests/latest/test_decorator.py @@ -3,11 +3,20 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- +import importlib import unittest -from azure.cli.command_modules.acs._consts import ( - DecoratorMode, +from azext_aks_preview.__init__ import register_aks_preview_resource_type +from azext_aks_preview._client_factory import CUSTOM_MGMT_AKS_PREVIEW +from azext_aks_preview.decorator import ( + AKSPreviewContext, + AKSPreviewCreateDecorator, + AKSPreviewModels, + AKSPreviewUpdateDecorator, ) +from azext_aks_preview.tests.latest.mocks import MockCLI, MockClient, MockCmd +from azext_aks_preview.tests.latest.test_aks_commands import _get_test_data_file +from azure.cli.command_modules.acs._consts import DecoratorMode from azure.cli.core.azclierror import ( ArgumentUsageError, CLIInternalError, @@ -17,25 +26,32 @@ RequiredArgumentMissingError, UnknownError, ) -from azext_aks_preview.decorator import ( - AKSPreviewContext, - AKSPreviewCreateDecorator, - AKSPreviewModels, - AKSPreviewUpdateDecorator, -) -from azext_aks_preview.tests.latest.mocks import ( - MockCLI, - MockClient, - MockCmd, -) -from azext_aks_preview._client_factory import CUSTOM_MGMT_AKS_PREVIEW -from azext_aks_preview.__init__ import register_aks_preview_resource_type + class AKSPreviewModelsTestCase(unittest.TestCase): def setUp(self): + # manually register CUSTOM_MGMT_AKS_PREVIEW + register_aks_preview_resource_type() self.cli_ctx = MockCLI() self.cmd = MockCmd(self.cli_ctx) + def test_models(self): + models = AKSPreviewModels(self.cmd, CUSTOM_MGMT_AKS_PREVIEW) + + # load models directly (instead of through the `get_sdk` method provided by the cli component) + from azure.cli.core.profiles._shared import AZURE_API_PROFILES + + sdk_profile = AZURE_API_PROFILES["latest"][CUSTOM_MGMT_AKS_PREVIEW] + api_version = sdk_profile.default_api_version + module_name = "azext_aks_preview.vendored_sdks.azure_mgmt_preview_aks.v{}.models".format( + api_version.replace("-", "_") + ) + module = importlib.import_module(module_name) + + self.assertEqual(models.KubeletConfig, getattr(module, "KubeletConfig")) + self.assertEqual(models.LinuxOSConfig, getattr(module, "LinuxOSConfig")) + + class AKSPreviewContextTestCase(unittest.TestCase): def setUp(self): # manually register CUSTOM_MGMT_AKS_PREVIEW @@ -72,7 +88,8 @@ def test_get_enable_fips_image(self): ) self.assertEqual(ctx_1.get_enable_fips_image(), False) agent_pool_profile = self.models.ManagedClusterAgentPoolProfile( - name="test_nodepool_name", enable_fips=True, + name="test_nodepool_name", + enable_fips=True, ) mc = self.models.ManagedCluster( location="test_location", agent_pool_profiles=[agent_pool_profile] @@ -90,13 +107,16 @@ def test_get_workload_runtime(self): ) self.assertEqual(ctx_1.get_workload_runtime(), None) agent_pool_profile = self.models.ManagedClusterAgentPoolProfile( - name="test_nodepool_name", workload_runtime="test_mc_workload_runtime", + name="test_nodepool_name", + workload_runtime="test_mc_workload_runtime", ) mc = self.models.ManagedCluster( location="test_location", agent_pool_profiles=[agent_pool_profile] ) ctx_1.attach_mc(mc) - self.assertEqual(ctx_1.get_workload_runtime(), "test_mc_workload_runtime") + self.assertEqual( + ctx_1.get_workload_runtime(), "test_mc_workload_runtime" + ) def test_get_gpu_instance_profile(self): # default @@ -108,13 +128,105 @@ def test_get_gpu_instance_profile(self): ) self.assertEqual(ctx_1.get_gpu_instance_profile(), None) agent_pool_profile = self.models.ManagedClusterAgentPoolProfile( - name="test_nodepool_name", gpu_instance_profile="test_mc_gpu_instance_profile", + name="test_nodepool_name", + gpu_instance_profile="test_mc_gpu_instance_profile", ) mc = self.models.ManagedCluster( location="test_location", agent_pool_profiles=[agent_pool_profile] ) ctx_1.attach_mc(mc) - self.assertEqual(ctx_1.get_gpu_instance_profile(), "test_mc_gpu_instance_profile") + self.assertEqual( + ctx_1.get_gpu_instance_profile(), "test_mc_gpu_instance_profile" + ) + + def test_get_kubelet_config(self): + # default + ctx_1 = AKSPreviewContext( + self.cmd, + {"kubelet_config": None}, + self.models, + decorator_mode=DecoratorMode.CREATE, + ) + self.assertEqual(ctx_1.get_kubelet_config(), None) + agent_pool_profile = self.models.ManagedClusterAgentPoolProfile( + name="test_nodepool_name", + kubelet_config=self.models.KubeletConfig(pod_max_pids=100), + ) + mc = self.models.ManagedCluster( + location="test_location", agent_pool_profiles=[agent_pool_profile] + ) + ctx_1.attach_mc(mc) + self.assertEqual( + ctx_1.get_kubelet_config(), + self.models.KubeletConfig(pod_max_pids=100), + ) + + # custom value + ctx_2 = AKSPreviewContext( + self.cmd, + {"kubelet_config": "fake-path"}, + self.models, + decorator_mode=DecoratorMode.CREATE, + ) + # fail on invalid file path + with self.assertRaises(InvalidArgumentValueError): + ctx_2.get_kubelet_config() + + # custom value + ctx_3 = AKSPreviewContext( + self.cmd, + {"kubelet_config": _get_test_data_file("invalidconfig.json")}, + self.models, + decorator_mode=DecoratorMode.CREATE, + ) + # fail on invalid file path + with self.assertRaises(InvalidArgumentValueError): + ctx_3.get_kubelet_config() + + def test_get_linux_os_config(self): + # default + ctx_1 = AKSPreviewContext( + self.cmd, + {"linux_os_config": None}, + self.models, + decorator_mode=DecoratorMode.CREATE, + ) + self.assertEqual(ctx_1.get_linux_os_config(), None) + agent_pool_profile = self.models.ManagedClusterAgentPoolProfile( + name="test_nodepool_name", + linux_os_config=self.models.LinuxOSConfig(swap_file_size_mb=200), + ) + mc = self.models.ManagedCluster( + location="test_location", agent_pool_profiles=[agent_pool_profile] + ) + ctx_1.attach_mc(mc) + self.assertEqual( + ctx_1.get_linux_os_config(), + self.models.LinuxOSConfig(swap_file_size_mb=200), + ) + + # custom value + ctx_2 = AKSPreviewContext( + self.cmd, + {"linux_os_config": "fake-path"}, + self.models, + decorator_mode=DecoratorMode.CREATE, + ) + # fail on invalid file path + with self.assertRaises(InvalidArgumentValueError): + ctx_2.get_linux_os_config() + + # custom value + ctx_3 = AKSPreviewContext( + self.cmd, + {"linux_os_config": _get_test_data_file("invalidconfig.json")}, + self.models, + decorator_mode=DecoratorMode.CREATE, + ) + # fail on invalid file path + with self.assertRaises(InvalidArgumentValueError): + ctx_3.get_linux_os_config() + class AKSPreviewCreateDecoratorTestCase(unittest.TestCase): def setUp(self): @@ -154,6 +266,7 @@ def test_set_up_agent_pool_profiles(self): "max_count": None, "workload_runtime": None, "gpu_instance_profile": None, + "kubelet_config": None, }, CUSTOM_MGMT_AKS_PREVIEW, ) @@ -190,6 +303,7 @@ def test_set_up_agent_pool_profiles(self): max_count=None, workload_runtime=None, gpu_instance_profile=None, + kubelet_config=None, ) ground_truth_mc_1 = self.models.ManagedCluster(location="test_location") ground_truth_mc_1.agent_pool_profiles = [agent_pool_profile_1] @@ -223,6 +337,8 @@ def test_set_up_agent_pool_profiles(self): "max_count": 20, "workload_runtime": "test_workload_runtime", "gpu_instance_profile": "test_gpu_instance_profile", + "kubelet_config": _get_test_data_file("kubeletconfig.json"), + "linux_os_config": _get_test_data_file("linuxosconfig.json"), }, CUSTOM_MGMT_AKS_PREVIEW, ) @@ -236,7 +352,7 @@ def test_set_up_agent_pool_profiles(self): count=10, vm_size="Standard_DSx_vy", os_type="Linux", - os_sku= "test_os_sku", + os_sku="test_os_sku", vnet_subnet_id="test_vnet_subnet_id", pod_subnet_id="test_pod_subnet_id", proximity_placement_group_id="test_ppg_id", @@ -256,6 +372,29 @@ def test_set_up_agent_pool_profiles(self): max_count=20, workload_runtime="test_workload_runtime", gpu_instance_profile="test_gpu_instance_profile", + kubelet_config={ + "cpuManagerPolicy": "static", + "cpuCfsQuota": True, + "cpuCfsQuotaPeriod": "200ms", + "imageGcHighThreshold": 90, + "imageGcLowThreshold": 70, + "topologyManagerPolicy": "best-effort", + "allowedUnsafeSysctls": ["kernel.msg*", "net.*"], + "failSwapOn": False, + "containerLogMaxFiles": 10, + "podMaxPids": 120, + "containerLogMaxSizeMB": 20, + }, + linux_os_config={ + "transparentHugePageEnabled": "madvise", + "transparentHugePageDefrag": "defer+madvise", + "swapFileSizeMB": 1500, + "sysctls": { + "netCoreSomaxconn": 163849, + "netIpv4TcpTwReuse": True, + "netIpv4IpLocalPortRange": "32000 60000", + }, + }, ) ground_truth_mc_2 = self.models.ManagedCluster(location="test_location") ground_truth_mc_2.agent_pool_profiles = [agent_pool_profile_2] From 8db86b87a215c939629602c1079a513152cd6348 Mon Sep 17 00:00:00 2001 From: Fuming Zhang Date: Wed, 13 Oct 2021 13:51:47 +0800 Subject: [PATCH 4/4] fix lint issue --- .../azext_aks_preview/decorator.py | 72 ++++++++----------- 1 file changed, 28 insertions(+), 44 deletions(-) diff --git a/src/aks-preview/azext_aks_preview/decorator.py b/src/aks-preview/azext_aks_preview/decorator.py index a0217dceb1c..14408f7b8d7 100644 --- a/src/aks-preview/azext_aks_preview/decorator.py +++ b/src/aks-preview/azext_aks_preview/decorator.py @@ -67,21 +67,17 @@ def get_pod_subnet_id(self) -> Union[str, None]: :return: bool """ # read the original value passed by the command - raw_value = self.raw_param.get("pod_subnet_id") + pod_subnet_id = self.raw_param.get("pod_subnet_id") # try to read the property value corresponding to the parameter from the `mc` object - value_obtained_from_mc = None if self.mc and self.mc.agent_pool_profiles: agent_pool_profile = safe_list_get( self.mc.agent_pool_profiles, 0, None ) - if agent_pool_profile: - value_obtained_from_mc = agent_pool_profile.pod_subnet_id - - # set default value - if value_obtained_from_mc is not None: - pod_subnet_id = value_obtained_from_mc - else: - pod_subnet_id = raw_value + if ( + agent_pool_profile and + agent_pool_profile.pod_subnet_id is not None + ): + pod_subnet_id = agent_pool_profile.pod_subnet_id # this parameter does not need dynamic completion # this parameter does not need validation @@ -93,21 +89,17 @@ def get_enable_fips_image(self) -> bool: :return: bool """ # read the original value passed by the command - raw_value = self.raw_param.get("enable_fips_image") + enable_fips_image = self.raw_param.get("enable_fips_image") # try to read the property value corresponding to the parameter from the `mc` object - value_obtained_from_mc = None if self.mc and self.mc.agent_pool_profiles: agent_pool_profile = safe_list_get( self.mc.agent_pool_profiles, 0, None ) - if agent_pool_profile: - value_obtained_from_mc = agent_pool_profile.enable_fips - - # set default value - if value_obtained_from_mc is not None: - enable_fips_image = value_obtained_from_mc - else: - enable_fips_image = raw_value + if ( + agent_pool_profile and + agent_pool_profile.enable_fips is not None + ): + enable_fips_image = agent_pool_profile.enable_fips # this parameter does not need dynamic completion # this parameter does not need validation @@ -119,21 +111,17 @@ def get_workload_runtime(self) -> Union[str, None]: :return: string or None """ # read the original value passed by the command - raw_value = self.raw_param.get("workload_runtime") + workload_runtime = self.raw_param.get("workload_runtime") # try to read the property value corresponding to the parameter from the `mc` object - value_obtained_from_mc = None if self.mc and self.mc.agent_pool_profiles: agent_pool_profile = safe_list_get( self.mc.agent_pool_profiles, 0, None ) - if agent_pool_profile: - value_obtained_from_mc = agent_pool_profile.workload_runtime - - # set default value - if value_obtained_from_mc is not None: - workload_runtime = value_obtained_from_mc - else: - workload_runtime = raw_value + if ( + agent_pool_profile and + agent_pool_profile.workload_runtime is not None + ): + workload_runtime = agent_pool_profile.workload_runtime # this parameter does not need dynamic completion # this parameter does not need validation @@ -145,21 +133,17 @@ def get_gpu_instance_profile(self) -> Union[str, None]: :return: string or None """ # read the original value passed by the command - raw_value = self.raw_param.get("gpu_instance_profile") + gpu_instance_profile = self.raw_param.get("gpu_instance_profile") # try to read the property value corresponding to the parameter from the `mc` object - value_obtained_from_mc = None if self.mc and self.mc.agent_pool_profiles: agent_pool_profile = safe_list_get( self.mc.agent_pool_profiles, 0, None ) - if agent_pool_profile: - value_obtained_from_mc = agent_pool_profile.gpu_instance_profile - - # set default value - if value_obtained_from_mc is not None: - gpu_instance_profile = value_obtained_from_mc - else: - gpu_instance_profile = raw_value + if ( + agent_pool_profile and + agent_pool_profile.gpu_instance_profile is not None + ): + gpu_instance_profile = agent_pool_profile.gpu_instance_profile # this parameter does not need dynamic completion # this parameter does not need validation @@ -196,8 +180,8 @@ def get_kubelet_config(self) -> Union[dict, KubeletConfig, None]: self.mc.agent_pool_profiles, 0, None ) if ( - agent_pool_profile - and agent_pool_profile.kubelet_config is not None + agent_pool_profile and + agent_pool_profile.kubelet_config is not None ): kubelet_config = agent_pool_profile.kubelet_config @@ -236,8 +220,8 @@ def get_linux_os_config(self) -> Union[dict, LinuxOSConfig, None]: self.mc.agent_pool_profiles, 0, None ) if ( - agent_pool_profile - and agent_pool_profile.linux_os_config is not None + agent_pool_profile and + agent_pool_profile.linux_os_config is not None ): linux_os_config = agent_pool_profile.linux_os_config