diff --git a/src/aks-preview/azcli_aks_live_test/configs/ext_matrix_default.json b/src/aks-preview/azcli_aks_live_test/configs/ext_matrix_default.json index 425b1c5af84..96f17666206 100644 --- a/src/aks-preview/azcli_aks_live_test/configs/ext_matrix_default.json +++ b/src/aks-preview/azcli_aks_live_test/configs/ext_matrix_default.json @@ -24,7 +24,8 @@ "test_aks_nodepool_add_with_gpu_instance_profile", "test_aks_create_with_crg_id", "test_aks_create_and_update_with_http_proxy_config", - "test_aks_snapshot" + "test_aks_snapshot", + "test_aks_custom_kubelet_identity" ] } } \ No newline at end of file diff --git a/src/aks-preview/azext_aks_preview/_client_factory.py b/src/aks-preview/azext_aks_preview/_client_factory.py index 6eb8fe35d44..a39b2994e30 100644 --- a/src/aks-preview/azext_aks_preview/_client_factory.py +++ b/src/aks-preview/azext_aks_preview/_client_factory.py @@ -14,48 +14,46 @@ 'ContainerServiceClient') -def cf_storage(cli_ctx, subscription_id=None): - return get_mgmt_service_client(cli_ctx, ResourceType.MGMT_STORAGE, subscription_id=subscription_id) - - -def cf_compute_service(cli_ctx, *_): - return get_mgmt_service_client(cli_ctx, ResourceType.MGMT_COMPUTE) +def get_container_service_client(cli_ctx, **_): + return get_mgmt_service_client(cli_ctx, CUSTOM_MGMT_AKS_PREVIEW) def cf_container_services(cli_ctx, *_): return get_container_service_client(cli_ctx).container_services -def get_container_service_client(cli_ctx, **_): - return get_mgmt_service_client(cli_ctx, CUSTOM_MGMT_AKS_PREVIEW) - - def cf_managed_clusters(cli_ctx, *_): - return get_mgmt_service_client(cli_ctx, CUSTOM_MGMT_AKS_PREVIEW).managed_clusters + return get_container_service_client(cli_ctx).managed_clusters def cf_agent_pools(cli_ctx, *_): - return get_mgmt_service_client(cli_ctx, CUSTOM_MGMT_AKS_PREVIEW).agent_pools + return get_container_service_client(cli_ctx).agent_pools -def cf_mc_snapshots_client(cli_ctx, subscription_id=None): - return get_mgmt_service_client(cli_ctx, CUSTOM_MGMT_AKS_PREVIEW, subscription_id=subscription_id).managed_cluster_snapshots +def cf_maintenance_configurations(cli_ctx, *_): + return get_container_service_client(cli_ctx).maintenance_configurations -def cf_mc_snapshots(cli_ctx, *_): - return get_mgmt_service_client(cli_ctx, CUSTOM_MGMT_AKS_PREVIEW).managed_cluster_snapshots +def cf_nodepool_snapshots(cli_ctx, *_): + return get_container_service_client(cli_ctx).snapshots +# TODO: remove this def cf_nodepool_snapshots_client(cli_ctx, subscription_id=None): return get_mgmt_service_client(cli_ctx, CUSTOM_MGMT_AKS_PREVIEW, subscription_id=subscription_id).snapshots -def cf_nodepool_snapshots(cli_ctx, *_): - return get_mgmt_service_client(cli_ctx, CUSTOM_MGMT_AKS_PREVIEW).snapshots +def cf_mc_snapshots(cli_ctx, *_): + return get_container_service_client(cli_ctx).managed_cluster_snapshots -def cf_maintenance_configurations(cli_ctx, *_): - return get_mgmt_service_client(cli_ctx, CUSTOM_MGMT_AKS_PREVIEW).maintenance_configurations +# TODO: remove this +def cf_mc_snapshots_client(cli_ctx, subscription_id=None): + return get_mgmt_service_client(cli_ctx, CUSTOM_MGMT_AKS_PREVIEW, subscription_id=subscription_id).managed_cluster_snapshots + + +def cf_compute_service(cli_ctx, *_): + return get_mgmt_service_client(cli_ctx, ResourceType.MGMT_COMPUTE) def cf_resource_groups(cli_ctx, subscription_id=None): @@ -73,6 +71,10 @@ def cf_container_registry_service(cli_ctx, subscription_id=None): subscription_id=subscription_id) +def cf_storage(cli_ctx, subscription_id=None): + return get_mgmt_service_client(cli_ctx, ResourceType.MGMT_STORAGE, subscription_id=subscription_id) + + def get_auth_management_client(cli_ctx, scope=None, **_): import re diff --git a/src/aks-preview/azext_aks_preview/_help.py b/src/aks-preview/azext_aks_preview/_help.py index 49c9180115b..e6c556a763e 100644 --- a/src/aks-preview/azext_aks_preview/_help.py +++ b/src/aks-preview/azext_aks_preview/_help.py @@ -30,6 +30,9 @@ type: bool short-summary: Skip role assignment for subnet (advanced networking). long-summary: If specified, please make sure your service principal has the access to your subnet. + - name: --zones -z + type: string array + short-summary: Space-separated list of availability zones where agent nodes will be placed. - name: --client-secret type: string short-summary: Secret associated with the service principal. This argument is required if @@ -990,9 +993,9 @@ type: int short-summary: The maximum number of pods deployable to a node. long-summary: If not specified, defaults based on network-plugin. 30 for "azure", 110 for "kubenet", or 250 for "none". - - name: --node-zones --zones -z + - name: --zones -z type: string array - short-summary: (will be deprecated, use --zones) Availability zones where agent nodes will be placed. + short-summary: Space-separated list of availability zones where agent nodes will be placed. - name: --vnet-subnet-id type: string short-summary: The ID of a subnet in an existing VNet into which to deploy the cluster. diff --git a/src/aks-preview/azext_aks_preview/_helpers.py b/src/aks-preview/azext_aks_preview/_helpers.py index de46221daff..c21f9c8e4dd 100644 --- a/src/aks-preview/azext_aks_preview/_helpers.py +++ b/src/aks-preview/azext_aks_preview/_helpers.py @@ -2,7 +2,14 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -from ._consts import CONST_CONTAINER_NAME_MAX_LENGTH +import re + +from azure.cli.command_modules.acs._helpers import map_azure_error_to_cli_error +from azure.cli.core.azclierror import InvalidArgumentValueError, ResourceNotFoundError +from azure.core.exceptions import AzureError + +from azext_aks_preview._client_factory import cf_nodepool_snapshots +from azext_aks_preview._consts import CONST_CONTAINER_NAME_MAX_LENGTH def _trim_fqdn_name_containing_hcp(normalized_fqdn: str) -> str: @@ -65,3 +72,29 @@ def similar_word(a, b): matches.append(word) return matches + + +def get_nodepool_snapshot_by_snapshot_id(cli_ctx, snapshot_id): + _re_snapshot_resource_id = re.compile( + r"/subscriptions/(.*?)/resourcegroups/(.*?)/providers/microsoft.containerservice/snapshots/(.*)", + flags=re.IGNORECASE, + ) + snapshot_id = snapshot_id.lower() + match = _re_snapshot_resource_id.search(snapshot_id) + if match: + resource_group_name = match.group(2) + snapshot_name = match.group(3) + return get_nodepool_snapshot(cli_ctx, resource_group_name, snapshot_name) + raise InvalidArgumentValueError("Cannot parse snapshot name from provided resource id '{}'.".format(snapshot_id)) + + +def get_nodepool_snapshot(cli_ctx, resource_group_name, snapshot_name): + snapshot_client = cf_nodepool_snapshots(cli_ctx) + try: + snapshot = snapshot_client.get(resource_group_name, snapshot_name) + # track 2 sdk raise exception from azure.core.exceptions + except AzureError as ex: + if "not found" in ex.message: + raise ResourceNotFoundError("Snapshot '{}' not found.".format(snapshot_name)) + raise map_azure_error_to_cli_error(ex) + return snapshot diff --git a/src/aks-preview/azext_aks_preview/_params.py b/src/aks-preview/azext_aks_preview/_params.py index ecb8f7a69fa..a237a3ceb0c 100644 --- a/src/aks-preview/azext_aks_preview/_params.py +++ b/src/aks-preview/azext_aks_preview/_params.py @@ -174,10 +174,8 @@ def load_arguments(self, _): c.argument('enable_rbac', action='store_true', options_list=['--enable-rbac', '-r'], deprecate_info=c.deprecate(redirect="--disable-rbac", hide="2.0.45")) 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('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'), completer=FilesCompleter(), validator=validate_ssh_key) c.argument('no_ssh_key', options_list=['--no-ssh-key', '-x']) @@ -185,22 +183,15 @@ def load_arguments(self, _): c.argument('docker_bridge_address') c.argument('pod_cidrs') c.argument('service_cidrs') - c.argument('load_balancer_sku', arg_type=get_enum_type(load_balancer_skus), - validator=validate_load_balancer_sku) + c.argument('load_balancer_sku', arg_type=get_enum_type(load_balancer_skus), validator=validate_load_balancer_sku) c.argument('load_balancer_managed_outbound_ip_count', type=int) c.argument('load_balancer_managed_outbound_ipv6_count', type=int) - c.argument('load_balancer_outbound_ips', type=str, - validator=validate_load_balancer_outbound_ips) - c.argument('load_balancer_outbound_ip_prefixes', type=str, - validator=validate_load_balancer_outbound_ip_prefixes) - c.argument('load_balancer_outbound_ports', type=int, - validator=validate_load_balancer_outbound_ports) - c.argument('load_balancer_idle_timeout', type=int, - validator=validate_load_balancer_idle_timeout) - c.argument('nat_gateway_managed_outbound_ip_count', type=int, - validator=validate_nat_gateway_managed_outbound_ip_count) - c.argument('nat_gateway_idle_timeout', type=int, - validator=validate_nat_gateway_idle_timeout) + c.argument('load_balancer_outbound_ips', validator=validate_load_balancer_outbound_ips) + c.argument('load_balancer_outbound_ip_prefixes', validator=validate_load_balancer_outbound_ip_prefixes) + c.argument('load_balancer_outbound_ports', type=int, validator=validate_load_balancer_outbound_ports) + c.argument('load_balancer_idle_timeout', type=int, validator=validate_load_balancer_idle_timeout) + c.argument('nat_gateway_managed_outbound_ip_count', type=int, validator=validate_nat_gateway_managed_outbound_ip_count) + c.argument('nat_gateway_idle_timeout', type=int, validator=validate_nat_gateway_idle_timeout) c.argument('outbound_type', arg_type=get_enum_type(outbound_types)) c.argument('network_plugin', arg_type=get_enum_type(network_plugins)) c.argument('network_policy') @@ -208,18 +199,15 @@ def load_arguments(self, _): c.argument('cluster_autoscaler_profile', nargs='+') c.argument('uptime_sla', action='store_true') c.argument('fqdn_subdomain') - c.argument('api_server_authorized_ip_ranges', - type=str, validator=validate_ip_ranges) + c.argument('api_server_authorized_ip_ranges', validator=validate_ip_ranges) c.argument('enable_private_cluster', action='store_true') c.argument('private_dns_zone') c.argument('disable_public_fqdn', action='store_true') c.argument('service_principal') c.argument('client_secret') c.argument('enable_managed_identity', action='store_true') - c.argument('assign_identity', type=str, - validator=validate_assign_identity) - c.argument('assign_kubelet_identity', type=str, - validator=validate_assign_kubelet_identity) + c.argument('assign_identity', validator=validate_assign_identity) + c.argument('assign_kubelet_identity', validator=validate_assign_kubelet_identity) c.argument('enable_aad', action='store_true') c.argument('enable_azure_rbac', action='store_true') c.argument('aad_client_app_id') @@ -227,55 +215,41 @@ def load_arguments(self, _): c.argument('aad_server_app_secret') c.argument('aad_tenant_id') c.argument('aad_admin_group_object_ids') - c.argument('windows_admin_username', options_list=[ - '--windows-admin-username']) - c.argument('windows_admin_password', options_list=[ - '--windows-admin-password']) - c.argument('enable_ahub', options_list=['--enable-ahub']) - c.argument('disable_ahub', options_list=['--disable-ahub']) - c.argument('gmsa_dns_server', options_list=['--gmsa-dns-server']) - c.argument('gmsa_root_domain_name', options_list=[ - '--gmsa-root-domain-name']) + c.argument('windows_admin_username') + c.argument('windows_admin_password') + c.argument('enable_ahub') + c.argument('disable_ahub') + c.argument('gmsa_dns_server') + c.argument('gmsa_root_domain_name') c.argument('attach_acr', acr_arg_type) c.argument('skip_subnet_role_assignment', action='store_true') # addons - c.argument('enable_addons', options_list=[ - '--enable-addons', '-a'], validator=validate_addons) + c.argument('enable_addons', options_list=['--enable-addons', '-a'], validator=validate_addons) c.argument('workspace_resource_id') - c.argument('enable_msi_auth_for_monitoring', - arg_type=get_three_state_flag(), is_preview=True) - c.argument('aci_subnet_name', type=str) - c.argument('appgw_name', options_list=[ - '--appgw-name'], arg_group='Application Gateway') - c.argument('appgw_subnet_prefix', options_list=[ - '--appgw-subnet-prefix'], arg_group='Application Gateway', deprecate_info=c.deprecate(redirect='--appgw-subnet-cidr', hide=True)) - c.argument('appgw_subnet_cidr', options_list=[ - '--appgw-subnet-cidr'], arg_group='Application Gateway') - c.argument('appgw_id', options_list=[ - '--appgw-id'], arg_group='Application Gateway') - c.argument('appgw_subnet_id', options_list=[ - '--appgw-subnet-id'], arg_group='Application Gateway') - c.argument('appgw_watch_namespace', options_list=[ - '--appgw-watch-namespace'], arg_group='Application Gateway') + c.argument('enable_msi_auth_for_monitoring', arg_type=get_three_state_flag(), is_preview=True) + c.argument('aci_subnet_name') + c.argument('appgw_name', arg_group='Application Gateway') + c.argument('appgw_subnet_prefix', arg_group='Application Gateway', deprecate_info=c.deprecate(redirect='--appgw-subnet-cidr', hide=True)) + c.argument('appgw_subnet_cidr', arg_group='Application Gateway') + c.argument('appgw_id', arg_group='Application Gateway') + c.argument('appgw_subnet_id', arg_group='Application Gateway') + c.argument('appgw_watch_namespace', arg_group='Application Gateway') c.argument('enable_secret_rotation', action='store_true') - c.argument('rotation_poll_interval', type=str) + c.argument('rotation_poll_interval') c.argument('enable_sgxquotehelper', action='store_true') # nodepool paramerters - c.argument('nodepool_name', type=str, default='nodepool1', + c.argument('nodepool_name', default='nodepool1', help='Node pool name, upto 12 alphanumeric characters', validator=validate_nodepool_name) c.argument('node_vm_size', options_list=[ '--node-vm-size', '-s'], completer=get_vm_size_completion_list) - c.argument('os_sku', type=str, options_list=[ - '--os-sku'], completer=get_ossku_completion_list) - c.argument('vnet_subnet_id', type=str, - validator=validate_vnet_subnet_id) - c.argument('pod_subnet_id', type=str, validator=validate_pod_subnet_id) + c.argument('os_sku', completer=get_ossku_completion_list) + c.argument('vnet_subnet_id', validator=validate_vnet_subnet_id) + c.argument('pod_subnet_id', validator=validate_pod_subnet_id) c.argument('enable_node_public_ip', action='store_true') - c.argument('node_public_ip_prefix_id', type=str) + c.argument('node_public_ip_prefix_id') c.argument('enable_cluster_autoscaler', action='store_true') c.argument('min_count', type=int, validator=validate_nodes_count) c.argument('max_count', type=int, validator=validate_nodes_count) - c.argument('nodepool_tags', nargs='*', validator=validate_nodepool_tags, help='space-separated tags: key[=value] [key[=value] ...]. Use "" to clear existing tags.') c.argument('nodepool_labels', nargs='*', validator=validate_nodepool_labels, @@ -283,19 +257,17 @@ def load_arguments(self, _): c.argument('node_osdisk_type', arg_type=get_enum_type(node_os_disk_types)) c.argument('node_osdisk_size', type=int) c.argument('max_pods', type=int, options_list=['--max-pods', '-m']) - c.argument('vm_set_type', type=str, validator=validate_vm_set_type) - c.argument('enable_vmss', action='store_true', - help='To be deprecated. Use vm_set_type instead.') - c.argument('node_zones', zones_type, options_list=[ - '--node-zones', '--zones', '-z'], help='(--node-zones will be deprecated, use --zones) Space-separated list of availability zones where agent nodes will be placed.') + c.argument('vm_set_type', validator=validate_vm_set_type) + c.argument('enable_vmss', action='store_true', help='To be deprecated. Use vm_set_type instead.', deprecate_info=c.deprecate(redirect='--vm-set-type', hide=True)) + c.argument('node_zones', zones_type, options_list=['--node-zones'], help='(--node-zones will be deprecated) Space-separated list of availability zones where agent nodes will be placed.', deprecate_info=c.deprecate(redirect='--zones', hide='2.37.0')) + c.argument('zones', zones_type, options_list=['--zones', '-z'], help='Space-separated list of availability zones where agent nodes will be placed.') c.argument('ppg') - c.argument('enable_encryption_at_host', arg_type=get_three_state_flag( - ), help='Enable EncryptionAtHost.') + c.argument('enable_encryption_at_host', arg_type=get_three_state_flag(), help='Enable EncryptionAtHost.') c.argument('enable_ultra_ssd', action='store_true') c.argument('enable_fips_image', action='store_true') c.argument('snapshot_id', validator=validate_snapshot_id) - c.argument('kubelet_config', type=str) - c.argument('linux_os_config', type=str) + c.argument('kubelet_config') + c.argument('linux_os_config') c.argument('yes', options_list=[ '--yes', '-y'], help='Do not prompt for confirmation.', action='store_true') c.argument('aks_custom_headers') @@ -303,26 +275,20 @@ def load_arguments(self, _): # managed cluster c.argument('node_resource_group') c.argument('ip_families') - c.argument('http_proxy_config', options_list=[ - '--http-proxy-config'], type=str) + c.argument('http_proxy_config') c.argument('enable_pod_security_policy', action='store_true') c.argument('enable_pod_identity', action='store_true') c.argument('enable_workload_identity', arg_type=get_three_state_flag(), is_preview=True) 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('azure_keyvault_kms_key_id', - validator=validate_azure_keyvault_kms_key_id, is_preview=True) - c.argument('cluster_snapshot_id', - validator=validate_cluster_snapshot_id, is_preview=True) + c.argument('enable_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('cluster_snapshot_id', validator=validate_cluster_snapshot_id, is_preview=True) # nodepool - c.argument('host_group_id', - validator=validate_host_group_id, is_preview=True) + c.argument('host_group_id', validator=validate_host_group_id, is_preview=True) c.argument('crg_id', validator=validate_crg_id, is_preview=True) # no validation for aks create because it already only supports Linux. - c.argument('message_of_the_day', type=str) - c.argument('gpu_instance_profile', - arg_type=get_enum_type(gpu_instance_profiles)) + c.argument('message_of_the_day') + c.argument('gpu_instance_profile', arg_type=get_enum_type(gpu_instance_profiles)) c.argument('workload_runtime', arg_type=get_enum_type(workload_runtimes), default=CONST_WORKLOAD_RUNTIME_OCI_CONTAINER) with self.argument_context('aks update') as c: @@ -331,51 +297,39 @@ def load_arguments(self, _): c.argument('enable_local_accounts', action='store_true') c.argument('load_balancer_managed_outbound_ip_count', type=int) c.argument('load_balancer_managed_outbound_ipv6_count', type=int) - c.argument('load_balancer_outbound_ips', type=str, - validator=validate_load_balancer_outbound_ips) - c.argument('load_balancer_outbound_ip_prefixes', type=str, - validator=validate_load_balancer_outbound_ip_prefixes) - c.argument('load_balancer_outbound_ports', type=int, - validator=validate_load_balancer_outbound_ports) - c.argument('load_balancer_idle_timeout', type=int, - validator=validate_load_balancer_idle_timeout) - c.argument('nat_gateway_managed_outbound_ip_count', type=int, - validator=validate_nat_gateway_managed_outbound_ip_count) - c.argument('nat_gateway_idle_timeout', type=int, - validator=validate_nat_gateway_idle_timeout) + c.argument('load_balancer_outbound_ips', validator=validate_load_balancer_outbound_ips) + c.argument('load_balancer_outbound_ip_prefixes', validator=validate_load_balancer_outbound_ip_prefixes) + c.argument('load_balancer_outbound_ports', type=int, validator=validate_load_balancer_outbound_ports) + c.argument('load_balancer_idle_timeout', type=int, validator=validate_load_balancer_idle_timeout) + c.argument('nat_gateway_managed_outbound_ip_count', type=int, validator=validate_nat_gateway_managed_outbound_ip_count) + c.argument('nat_gateway_idle_timeout', type=int, validator=validate_nat_gateway_idle_timeout) c.argument('auto_upgrade_channel', arg_type=get_enum_type(auto_upgrade_channels)) c.argument('cluster_autoscaler_profile', nargs='+') c.argument('uptime_sla', action='store_true') c.argument('no_uptime_sla', action='store_true') - c.argument('api_server_authorized_ip_ranges', - type=str, validator=validate_ip_ranges) + c.argument('api_server_authorized_ip_ranges', validator=validate_ip_ranges) c.argument('enable_public_fqdn', action='store_true') c.argument('disable_public_fqdn', action='store_true') c.argument('enable_managed_identity', action='store_true') - c.argument('assign_identity', type=str, - validator=validate_assign_identity) - c.argument('assign_kubelet_identity', - validator=validate_assign_kubelet_identity) + c.argument('assign_identity', validator=validate_assign_identity) + c.argument('assign_kubelet_identity', validator=validate_assign_kubelet_identity) c.argument('enable_aad', action='store_true') c.argument('enable_azure_rbac', action='store_true') c.argument('disable_azure_rbac', action='store_true') c.argument('aad_tenant_id') c.argument('aad_admin_group_object_ids') - c.argument('windows_admin_password', options_list=[ - '--windows-admin-password']) - c.argument('enable_ahub', options_list=['--enable-ahub']) - c.argument('disable_ahub', options_list=['--disable-ahub']) - c.argument('enable_windows_gmsa', action='store_true', - options_list=['--enable-windows-gmsa']) - c.argument('gmsa_dns_server', options_list=['--gmsa-dns-server']) - c.argument('gmsa_root_domain_name', options_list=[ - '--gmsa-root-domain-name']) + c.argument('windows_admin_password') + c.argument('enable_ahub') + c.argument('disable_ahub') + c.argument('enable_windows_gmsa', action='store_true') + c.argument('gmsa_dns_server') + c.argument('gmsa_root_domain_name') c.argument('attach_acr', acr_arg_type, validator=validate_acr) c.argument('detach_acr', acr_arg_type, validator=validate_acr) # addons c.argument('enable_secret_rotation', action='store_true') c.argument('disable_secret_rotation', action='store_true') - c.argument('rotation_poll_interval', type=str) + c.argument('rotation_poll_interval') # nodepool paramerters c.argument('enable_cluster_autoscaler', options_list=[ "--enable-cluster-autoscaler", "-e"], action='store_true') @@ -392,7 +346,7 @@ def load_arguments(self, _): c.argument('aks_custom_headers') # extensions # managed cluster - c.argument('http_proxy_config', type=str) + c.argument('http_proxy_config') c.argument('enable_pod_security_policy', action='store_true') c.argument('disable_pod_security_policy', action='store_true') c.argument('enable_pod_identity', action='store_true') @@ -400,13 +354,11 @@ def load_arguments(self, _): c.argument('enable_workload_identity', arg_type=get_three_state_flag(), is_preview=True) c.argument('disable_workload_identity', arg_type=get_three_state_flag(), is_preview=True) 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('azure_keyvault_kms_key_id', - validator=validate_azure_keyvault_kms_key_id, is_preview=True) + c.argument('enable_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) with self.argument_context('aks scale') as c: - c.argument('nodepool_name', type=str, + c.argument('nodepool_name', help='Node pool name, upto 12 alphanumeric characters', validator=validate_nodepool_name) with self.argument_context('aks upgrade') as c: @@ -416,42 +368,42 @@ def load_arguments(self, _): '--yes', '-y'], help='Do not prompt for confirmation.', action='store_true') with self.argument_context('aks maintenanceconfiguration') as c: - c.argument('cluster_name', type=str, help='The cluster name.') + c.argument('cluster_name', help='The cluster name.') for scope in ['aks maintenanceconfiguration add', 'aks maintenanceconfiguration update']: with self.argument_context(scope) as c: - c.argument('config_name', type=str, options_list=[ + c.argument('config_name', options_list=[ '--name', '-n'], help='The config name.') - c.argument('config_file', type=str, options_list=[ + c.argument('config_file', options_list=[ '--config-file'], help='The config json file.', required=False) - c.argument('weekday', type=str, options_list=[ + c.argument('weekday', options_list=[ '--weekday'], help='weekday on which maintenance can happen. e.g. Monday', required=False) c.argument('start_hour', type=int, options_list=[ '--start-hour'], help='maintenance start hour of 1 hour window on the weekday. e.g. 1 means 1:00am - 2:00am', required=False) for scope in ['aks maintenanceconfiguration show', 'aks maintenanceconfiguration delete']: with self.argument_context(scope) as c: - c.argument('config_name', type=str, options_list=[ + c.argument('config_name', options_list=[ '--name', '-n'], help='The config name.') with self.argument_context('aks nodepool') as c: - c.argument('cluster_name', type=str, help='The cluster name.') + c.argument('cluster_name', help='The cluster name.') for scope in ['aks nodepool add']: with self.argument_context(scope) as c: - c.argument('nodepool_name', type=str, options_list=[ + c.argument('nodepool_name', options_list=[ '--name', '-n'], validator=validate_nodepool_name, help='The node pool name.') c.argument('node_vm_size', options_list=[ '--node-vm-size', '-s'], completer=get_vm_size_completion_list) - c.argument('os_type', type=str) - c.argument('os_sku', type=str, options_list=[ + c.argument('os_type') + c.argument('os_sku', options_list=[ '--os-sku'], completer=get_ossku_completion_list) - c.argument('vnet_subnet_id', type=str, + c.argument('vnet_subnet_id', validator=validate_vnet_subnet_id) - c.argument('pod_subnet_id', type=str, + c.argument('pod_subnet_id', validator=validate_pod_subnet_id) c.argument('enable_node_public_ip', action='store_true') - c.argument('node_public_ip_prefix_id', type=str) + c.argument('node_public_ip_prefix_id') c.argument('enable_cluster_autoscaler', options_list=[ "--enable-cluster-autoscaler", "-e"], action='store_true') c.argument('min_count', type=int, validator=validate_nodes_count) @@ -467,37 +419,35 @@ def load_arguments(self, _): c.argument('node_osdisk_size', type=int) c.argument('mode', arg_type=get_enum_type(node_mode_types)) c.argument('scale_down_mode', arg_type=get_enum_type(scale_down_modes)) - c.argument('max_surge', type=str, validator=validate_max_surge) + c.argument('max_surge', validator=validate_max_surge) c.argument('max_pods', type=int, options_list=['--max-pods', '-m']) - c.argument('node_zones', zones_type, options_list=[ - '--node-zones', '--zones', '-z'], help='(--node-zones will be deprecated) Space-separated list of availability zones where agent nodes will be placed.') + c.argument('node_zones', zones_type, options_list=['--node-zones'], help='(--node-zones will be deprecated) Space-separated list of availability zones where agent nodes will be placed.', deprecate_info=c.deprecate(redirect='--zones', hide='2.37.0')) + c.argument('zones', zones_type, options_list=['--zones', '-z'], help='Space-separated list of availability zones where agent nodes will be placed.') c.argument('ppg') c.argument('enable_encryption_at_host', options_list=[ '--enable-encryption-at-host'], action='store_true') c.argument('enable_ultra_ssd', action='store_true') c.argument('enable_fips_image', action='store_true') - c.argument('snapshot_id', type=str, validator=validate_snapshot_id) - c.argument('kubelet_config', type=str) - c.argument('linux_os_config', type=str) + c.argument('snapshot_id', validator=validate_snapshot_id) + c.argument('kubelet_config') + c.argument('linux_os_config') c.argument('aks_custom_headers') # extensions - c.argument('host_group_id', - validator=validate_host_group_id, is_preview=True) + c.argument('host_group_id', validator=validate_host_group_id, is_preview=True) c.argument('crg_id', validator=validate_crg_id, is_preview=True) - c.argument('message_of_the_day', type=str, - validator=validate_message_of_the_day) + c.argument('message_of_the_day', validator=validate_message_of_the_day) c.argument('workload_runtime', arg_type=get_enum_type(workload_runtimes), default=CONST_WORKLOAD_RUNTIME_OCI_CONTAINER) c.argument('gpu_instance_profile', arg_type=get_enum_type(gpu_instance_profiles)) for scope in ['aks nodepool show', 'aks nodepool delete', 'aks nodepool scale', 'aks nodepool upgrade', 'aks nodepool update']: with self.argument_context(scope) as c: - c.argument('nodepool_name', type=str, options_list=[ + c.argument('nodepool_name', options_list=[ '--name', '-n'], validator=validate_nodepool_name, help='The node pool name.') with self.argument_context('aks nodepool upgrade') as c: - c.argument('max_surge', type=str, validator=validate_max_surge) + c.argument('max_surge', validator=validate_max_surge) c.argument('aks_custom_headers') - c.argument('snapshot_id', type=str, validator=validate_snapshot_id) + c.argument('snapshot_id', validator=validate_snapshot_id) with self.argument_context('aks nodepool update') as c: c.argument('enable_cluster_autoscaler', options_list=[ @@ -513,7 +463,7 @@ def load_arguments(self, _): c.argument('node_taints', validator=validate_taints) c.argument('mode', arg_type=get_enum_type(node_mode_types)) c.argument('scale_down_mode', arg_type=get_enum_type(scale_down_modes)) - c.argument('max_surge', type=str, validator=validate_max_surge) + c.argument('max_surge', validator=validate_max_surge) with self.argument_context('aks addon show') as c: c.argument('addon', options_list=[ @@ -538,7 +488,7 @@ def load_arguments(self, _): c.argument('appgw_watch_namespace', options_list=[ '--appgw-watch-namespace'], arg_group='Application Gateway') c.argument('enable_secret_rotation', action='store_true') - c.argument('rotation_poll_interval', type=str) + c.argument('rotation_poll_interval') c.argument('workspace_resource_id') c.argument('enable_msi_auth_for_monitoring', arg_type=get_three_state_flag(), is_preview=True) @@ -566,7 +516,7 @@ def load_arguments(self, _): c.argument('appgw_watch_namespace', options_list=[ '--appgw-watch-namespace'], arg_group='Application Gateway') c.argument('enable_secret_rotation', action='store_true') - c.argument('rotation_poll_interval', type=str) + c.argument('rotation_poll_interval') c.argument('workspace_resource_id') c.argument('enable_msi_auth_for_monitoring', arg_type=get_three_state_flag(), is_preview=True) @@ -594,7 +544,7 @@ def load_arguments(self, _): c.argument('appgw_watch_namespace', options_list=[ '--appgw-watch-namespace'], arg_group='Application Gateway') c.argument('enable_secret_rotation', action='store_true') - c.argument('rotation_poll_interval', type=str) + c.argument('rotation_poll_interval') c.argument('workspace_resource_id') c.argument('enable_msi_auth_for_monitoring', arg_type=get_three_state_flag(), is_preview=True) @@ -611,31 +561,31 @@ def load_arguments(self, _): c.argument('credential_format', options_list=['--format'], arg_type=get_enum_type(credential_formats)) with self.argument_context('aks pod-identity') as c: - c.argument('cluster_name', type=str, help='The cluster name.') + c.argument('cluster_name', help='The cluster name.') with self.argument_context('aks pod-identity add') as c: - c.argument('identity_name', type=str, options_list=['--name', '-n'], default=None, required=False, + c.argument('identity_name', options_list=['--name', '-n'], default=None, required=False, help='The pod identity name. Generate if not specified.', validator=validate_pod_identity_resource_name('identity_name', required=False)) - c.argument('identity_namespace', type=str, options_list=[ + c.argument('identity_namespace', options_list=[ '--namespace'], help='The pod identity namespace.') - c.argument('identity_resource_id', type=str, options_list=[ + c.argument('identity_resource_id', options_list=[ '--identity-resource-id'], help='Resource id of the identity to use.') - c.argument('binding_selector', type=str, options_list=[ + c.argument('binding_selector', options_list=[ '--binding-selector'], help='Optional binding selector to use.') with self.argument_context('aks pod-identity delete') as c: - c.argument('identity_name', type=str, options_list=['--name', '-n'], default=None, required=True, + c.argument('identity_name', options_list=['--name', '-n'], default=None, required=True, help='The pod identity name.', validator=validate_pod_identity_resource_name('identity_name', required=True)) - c.argument('identity_namespace', type=str, options_list=[ + c.argument('identity_namespace', options_list=[ '--namespace'], help='The pod identity namespace.') with self.argument_context('aks pod-identity exception add') as c: - c.argument('exc_name', type=str, options_list=['--name', '-n'], default=None, required=False, + c.argument('exc_name', options_list=['--name', '-n'], default=None, required=False, help='The pod identity exception name. Generate if not specified.', validator=validate_pod_identity_resource_name('exc_name', required=False)) - c.argument('exc_namespace', type=str, options_list=['--namespace'], required=True, + c.argument('exc_namespace', options_list=['--namespace'], required=True, help='The pod identity exception namespace.', validator=validate_pod_identity_resource_namespace) c.argument('pod_labels', nargs='*', required=True, @@ -643,18 +593,18 @@ def load_arguments(self, _): validator=validate_pod_identity_pod_labels) with self.argument_context('aks pod-identity exception delete') as c: - c.argument('exc_name', type=str, options_list=['--name', '-n'], required=True, + c.argument('exc_name', options_list=['--name', '-n'], required=True, help='The pod identity exception name to remove.', validator=validate_pod_identity_resource_name('exc_name', required=True)) - c.argument('exc_namespace', type=str, options_list=['--namespace'], required=True, + c.argument('exc_namespace', options_list=['--namespace'], required=True, help='The pod identity exception namespace to remove.', validator=validate_pod_identity_resource_namespace) with self.argument_context('aks pod-identity exception update') as c: - c.argument('exc_name', type=str, options_list=['--name', '-n'], required=True, + c.argument('exc_name', options_list=['--name', '-n'], required=True, help='The pod identity exception name to remove.', validator=validate_pod_identity_resource_name('exc_name', required=True)) - c.argument('exc_namespace', type=str, options_list=['--namespace'], required=True, + c.argument('exc_namespace', options_list=['--namespace'], required=True, help='The pod identity exception namespace to remove.', validator=validate_pod_identity_resource_namespace) c.argument('pod_labels', nargs='*', required=True, @@ -663,32 +613,32 @@ def load_arguments(self, _): for scope in ['aks nodepool snapshot create']: with self.argument_context(scope) as c: - c.argument('snapshot_name', type=str, options_list=[ + c.argument('snapshot_name', options_list=[ '--name', '-n'], required=True, help='The nodepool snapshot name.', validator=validate_snapshot_name) c.argument('tags', tags_type) - c.argument('nodepool_id', type=str, required=True, + c.argument('nodepool_id', required=True, help='The nodepool id.', validator=validate_nodepool_id) c.argument('aks_custom_headers') for scope in ['aks nodepool snapshot show', 'aks nodepool snapshot delete']: with self.argument_context(scope) as c: - c.argument('snapshot_name', type=str, options_list=[ + c.argument('snapshot_name', options_list=[ '--name', '-n'], required=True, help='The nodepool snapshot name.', validator=validate_snapshot_name) c.argument('yes', options_list=[ '--yes', '-y'], help='Do not prompt for confirmation.', action='store_true') for scope in ['aks snapshot create']: with self.argument_context(scope) as c: - c.argument('snapshot_name', type=str, options_list=[ + c.argument('snapshot_name', options_list=[ '--name', '-n'], required=True, help='The cluster snapshot name.', validator=validate_snapshot_name) c.argument('tags', tags_type) - c.argument('cluster_id', type=str, required=True, + c.argument('cluster_id', required=True, validator=validate_cluster_id, help='The cluster id.') c.argument('aks_custom_headers') for scope in ['aks snapshot show', 'aks snapshot delete']: with self.argument_context(scope) as c: - c.argument('snapshot_name', type=str, options_list=[ + c.argument('snapshot_name', options_list=[ '--name', '-n'], required=True, help='The cluster snapshot name.', validator=validate_snapshot_name) c.argument('yes', options_list=[ '--yes', '-y'], help='Do not prompt for confirmation.', action='store_true') diff --git a/src/aks-preview/azext_aks_preview/agentpool_decorator.py b/src/aks-preview/azext_aks_preview/agentpool_decorator.py new file mode 100644 index 00000000000..a228c6bc244 --- /dev/null +++ b/src/aks-preview/azext_aks_preview/agentpool_decorator.py @@ -0,0 +1,303 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +import base64 +import os +from types import SimpleNamespace +from typing import Dict, TypeVar, Union, List + +from azure.cli.command_modules.acs._consts import AgentPoolDecoratorMode, DecoratorMode +from azure.cli.command_modules.acs.agentpool_decorator import ( + AKSAgentPoolAddDecorator, + AKSAgentPoolContext, + AKSAgentPoolModels, + AKSAgentPoolParamDict, + AKSAgentPoolUpdateDecorator, +) +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 read_file_content +from knack.log import get_logger + +from azext_aks_preview._client_factory import cf_agent_pools +from azext_aks_preview._consts import CONST_WORKLOAD_RUNTIME_OCI_CONTAINER +from azext_aks_preview._helpers import get_nodepool_snapshot_by_snapshot_id + +logger = get_logger(__name__) + +# type variables +AgentPool = TypeVar("AgentPool") +AgentPoolsOperations = TypeVar("AgentPoolsOperations") + + +# pylint: disable=too-few-public-methods +class AKSPreviewAgentPoolModels(AKSAgentPoolModels): + """Store the models used in aks agentpool series of commands. + + The api version of the class corresponding to a model is determined by resource_type. + """ + + +class AKSPreviewAgentPoolContext(AKSAgentPoolContext): + def __init__( + self, + cmd: AzCliCommand, + raw_parameters: AKSAgentPoolParamDict, + models: AKSAgentPoolModels, + decorator_mode: DecoratorMode, + agentpool_decorator_mode: AgentPoolDecoratorMode, + ): + super().__init__(cmd, raw_parameters, models, decorator_mode, agentpool_decorator_mode) + self.__external_functions = None + + @property + def external_functions(self) -> SimpleNamespace: + if self.__external_functions is None: + external_functions = vars(super().external_functions) + external_functions["cf_agent_pools"] = cf_agent_pools + external_functions["get_snapshot_by_snapshot_id"] = get_nodepool_snapshot_by_snapshot_id + self.__external_functions = SimpleNamespace(**external_functions) + return self.__external_functions + + def get_zones(self) -> Union[List[str], None]: + """Obtain the value of zones. + + Note: Inherited and extended in aks-preview to add support for a different parameter name (node_zones). + + :return: list of strings or None + """ + zones = super().get_zones() + if zones is not None: + return zones + # read the original value passed by the command + return self.raw_param.get("node_zones") + + def get_host_group_id(self) -> Union[str, None]: + """Obtain the value of host_group_id. + + :return: string or None + """ + # read the original value passed by the command + host_group_id = self.raw_param.get("host_group_id") + # try to read the property value corresponding to the parameter from the `agentpool` object + if ( + self.agentpool and + self.agentpool.host_group_id is not None + ): + host_group_id = self.agentpool.host_group_id + + # this parameter does not need dynamic completion + # this parameter does not need validation + return host_group_id + + def get_crg_id(self) -> Union[str, None]: + """Obtain the value of crg_id. + + :return: string or None + """ + # read the original value passed by the command + crg_id = self.raw_param.get("crg_id") + # try to read the property value corresponding to the parameter from the `agentpool` object + if ( + self.agentpool and + self.agentpool.capacity_reservation_group_id is not None + ): + crg_id = self.agentpool.capacity_reservation_group_id + + # this parameter does not need dynamic completion + # this parameter does not need validation + return crg_id + + def get_message_of_the_day(self) -> Union[str, None]: + """Obtain the value of message_of_the_day. + + :return: string or None + """ + # read the original value passed by the command + message_of_the_day = None + message_of_the_day_file_path = self.raw_param.get("message_of_the_day") + + if message_of_the_day_file_path: + if not os.path.isfile(message_of_the_day_file_path): + raise InvalidArgumentValueError( + "{} is not valid file, or not accessable.".format( + message_of_the_day_file_path + ) + ) + message_of_the_day = read_file_content( + message_of_the_day_file_path) + message_of_the_day = base64.b64encode( + bytes(message_of_the_day, 'ascii')).decode('ascii') + + # try to read the property value corresponding to the parameter from the `mc` object + if self.agentpool and self.agentpool.message_of_the_day is not None: + message_of_the_day = self.agentpool.message_of_the_day + + # this parameter does not need dynamic completion + # this parameter does not need validation + return message_of_the_day + + 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 + gpu_instance_profile = self.raw_param.get("gpu_instance_profile") + # try to read the property value corresponding to the parameter from the `mc` object + if self.agentpool and self.agentpool.gpu_instance_profile is not None: + gpu_instance_profile = self.agentpool.gpu_instance_profile + + # this parameter does not need dynamic completion + # this parameter does not need validation + return gpu_instance_profile + + def get_workload_runtime(self) -> Union[str, None]: + """Obtain the value of workload_runtime, default value is CONST_WORKLOAD_RUNTIME_OCI_CONTAINER. + + :return: string or None + """ + # read the original value passed by the command + workload_runtime = self.raw_param.get("workload_runtime", CONST_WORKLOAD_RUNTIME_OCI_CONTAINER) + # try to read the property value corresponding to the parameter from the `mc` object + if self.agentpool and self.agentpool.workload_runtime is not None: + workload_runtime = self.agentpool.workload_runtime + + # this parameter does not need dynamic completion + # this parameter does not need validation + return workload_runtime + + +class AKSPreviewAgentPoolAddDecorator(AKSAgentPoolAddDecorator): + def __init__( + self, + cmd: AzCliCommand, + client: AgentPoolsOperations, + raw_parameters: Dict, + resource_type: ResourceType, + agentpool_decorator_mode: AgentPoolDecoratorMode, + ): + self.__raw_parameters = raw_parameters + super().__init__(cmd, client, raw_parameters, resource_type, agentpool_decorator_mode) + + def init_models(self) -> None: + """Initialize an AKSPreviewAgentPoolModels object to store the models. + + :return: None + """ + self.models = AKSPreviewAgentPoolModels(self.cmd, self.resource_type, self.agentpool_decorator_mode) + + def init_context(self) -> None: + """Initialize an AKSPreviewAgentPoolContext object to store the context in the process of assemble the + AgentPool object. + + :return: None + """ + self.context = AKSPreviewAgentPoolContext( + self.cmd, + AKSAgentPoolParamDict(self.__raw_parameters), + self.models, + DecoratorMode.CREATE, + self.agentpool_decorator_mode, + ) + + def set_up_preview_vm_properties(self, agentpool: AgentPool) -> AgentPool: + """Set up preview vm related properties for the AgentPool object. + + :return: the AgentPool object + """ + self._ensure_agentpool(agentpool) + + agentpool.host_group_id = self.context.get_host_group_id() + agentpool.capacity_reservation_group_id = self.context.get_crg_id() + return agentpool + + def set_up_motd(self, agentpool: AgentPool) -> AgentPool: + """Set up message of the day for the AgentPool object. + + :return: the AgentPool object + """ + self._ensure_agentpool(agentpool) + + agentpool.message_of_the_day = self.context.get_message_of_the_day() + return agentpool + + def set_up_gpu_propertes(self, agentpool: AgentPool) -> AgentPool: + """Set up gpu related properties for the AgentPool object. + + :return: the AgentPool object + """ + self._ensure_agentpool(agentpool) + + agentpool.gpu_instance_profile = self.context.get_gpu_instance_profile() + agentpool.workload_runtime = self.context.get_workload_runtime() + return agentpool + + def construct_agentpool_profile_preview(self) -> AgentPool: + """The overall controller used to construct the preview AgentPool profile. + + The completely constructed AgentPool object will later be passed as a parameter to the underlying SDK + (mgmt-containerservice) to send the actual request. + + :return: the AgentPool object + """ + # construct the default AgentPool profile + agentpool = self.construct_agentpool_profile_default(bypass_restore_defaults=True) + # set up preview vm properties + agentpool = self.set_up_preview_vm_properties(agentpool) + # set up message of the day + agentpool = self.set_up_motd(agentpool) + # set up gpu profiles + agentpool = self.set_up_gpu_propertes(agentpool) + # restore defaults + agentpool = self._restore_defaults_in_agentpool(agentpool) + return agentpool + + +class AKSPreviewAgentPoolUpdateDecorator(AKSAgentPoolUpdateDecorator): + def __init__( + self, + cmd: AzCliCommand, + client: AgentPoolsOperations, + raw_parameters: Dict, + resource_type: ResourceType, + agentpool_decorator_mode: AgentPoolDecoratorMode, + ): + self.__raw_parameters = raw_parameters + super().__init__(cmd, client, raw_parameters, resource_type, agentpool_decorator_mode) + + def init_models(self) -> None: + """Initialize an AKSPreviewAgentPoolModels object to store the models. + + :return: None + """ + self.models = AKSPreviewAgentPoolModels(self.cmd, self.resource_type, self.agentpool_decorator_mode) + + def init_context(self) -> None: + """Initialize an AKSPreviewAgentPoolContext object to store the context in the process of assemble the + AgentPool object. + + :return: None + """ + self.context = AKSPreviewAgentPoolContext( + self.cmd, + AKSAgentPoolParamDict(self.__raw_parameters), + self.models, + DecoratorMode.UPDATE, + self.agentpool_decorator_mode, + ) + + def update_agentpool_profile_preview(self, agentpools: List[AgentPool] = None) -> AgentPool: + """The overall controller used to update the preview AgentPool profile. + + The completely constructed AgentPool object will later be passed as a parameter to the underlying SDK + (mgmt-containerservice) to send the actual request. + + :return: the AgentPool object + """ + # fetch and update the default AgentPool profile + agentpool = self.update_agentpool_profile_default(agentpools) + return agentpool diff --git a/src/aks-preview/azext_aks_preview/custom.py b/src/aks-preview/azext_aks_preview/custom.py index 0bf35b9704e..18debed75dc 100644 --- a/src/aks-preview/azext_aks_preview/custom.py +++ b/src/aks-preview/azext_aks_preview/custom.py @@ -742,6 +742,7 @@ def aks_create(cmd, aad_tenant_id=None, tags=None, node_zones=None, + zones=None, enable_node_public_ip=False, node_public_ip_prefix_id=None, generate_ssh_keys=False, # pylint: disable=unused-argument @@ -1597,6 +1598,7 @@ def aks_agentpool_add(cmd, # pylint: disable=unused-argument,too-many-local tags=None, kubernetes_version=None, node_zones=None, + zones=None, enable_node_public_ip=False, node_public_ip_prefix_id=None, node_vm_size=None, diff --git a/src/aks-preview/azext_aks_preview/tests/latest/test_agentpool_decorator.py b/src/aks-preview/azext_aks_preview/tests/latest/test_agentpool_decorator.py new file mode 100644 index 00000000000..aed8866b68c --- /dev/null +++ b/src/aks-preview/azext_aks_preview/tests/latest/test_agentpool_decorator.py @@ -0,0 +1,697 @@ +# -------------------------------------------------------------------------------------------- +# 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 unittest.mock import Mock, patch + +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._consts import CONST_WORKLOAD_RUNTIME_OCI_CONTAINER +from azext_aks_preview.agentpool_decorator import ( + AKSPreviewAgentPoolAddDecorator, + AKSPreviewAgentPoolContext, + AKSPreviewAgentPoolModels, + AKSPreviewAgentPoolUpdateDecorator, +) +from azext_aks_preview.tests.latest.utils import get_test_data_file_path +from azure.cli.command_modules.acs._consts import ( + CONST_DEFAULT_NODE_OS_TYPE, + CONST_DEFAULT_NODE_VM_SIZE, + CONST_NODEPOOL_MODE_SYSTEM, + CONST_NODEPOOL_MODE_USER, + CONST_SCALE_DOWN_MODE_DELETE, + CONST_VIRTUAL_MACHINE_SCALE_SETS, + AgentPoolDecoratorMode, + DecoratorMode, +) +from azure.cli.command_modules.acs.agentpool_decorator import AKSAgentPoolParamDict +from azure.cli.command_modules.acs.tests.latest.mocks import MockCLI, MockClient, MockCmd +from azure.cli.core.azclierror import CLIInternalError, InvalidArgumentValueError + + +class AKSPreviewAgentPoolContextCommonTestCase(unittest.TestCase): + def _remove_defaults_in_agentpool(self, agentpool): + self.defaults_in_agentpool = {} + for attr_name, attr_value in vars(agentpool).items(): + if not attr_name.startswith("_") and attr_name != "name" and attr_value is not None: + self.defaults_in_agentpool[attr_name] = attr_value + setattr(agentpool, attr_name, None) + return agentpool + + def _restore_defaults_in_agentpool(self, agentpool): + for key, value in self.defaults_in_agentpool.items(): + if getattr(agentpool, key, None) is None: + setattr(agentpool, key, value) + return agentpool + + def create_initialized_agentpool_instance( + self, nodepool_name="nodepool1", remove_defaults=True, restore_defaults=True, **kwargs + ): + """Helper function to create a properly initialized agentpool instance. + + :return: the AgentPool object + """ + if self.agentpool_decorator_mode == AgentPoolDecoratorMode.MANAGED_CLUSTER: + agentpool = self.models.UnifiedAgentPoolModel(name=nodepool_name) + else: + agentpool = self.models.UnifiedAgentPoolModel() + agentpool.name = nodepool_name + + # remove defaults + if remove_defaults: + self._remove_defaults_in_agentpool(agentpool) + + # set properties + for key, value in kwargs.items(): + setattr(agentpool, key, value) + + # resote defaults + if restore_defaults: + self._restore_defaults_in_agentpool(agentpool) + return agentpool + + def common_get_zones(self): + # default + ctx_1 = AKSPreviewAgentPoolContext( + self.cmd, + AKSAgentPoolParamDict({"zone": None, "node_zones": "test_node_zones"}), + self.models, + DecoratorMode.CREATE, + self.agentpool_decorator_mode, + ) + self.assertEqual(ctx_1.get_zones(), "test_node_zones") + agentpool_1 = self.create_initialized_agentpool_instance( + availability_zones=["test_mc_zones1", "test_mc_zones2"] + ) + ctx_1.attach_agentpool(agentpool_1) + self.assertEqual(ctx_1.get_zones(), ["test_mc_zones1", "test_mc_zones2"]) + + # custom value + ctx_2 = AKSPreviewAgentPoolContext( + self.cmd, + AKSAgentPoolParamDict({"zones": "test_zones", "node_zones": "test_node_zones"}), + self.models, + DecoratorMode.CREATE, + self.agentpool_decorator_mode, + ) + self.assertEqual(ctx_2.get_zones(), "test_zones") + + def common_get_host_group_id(self): + # default + ctx_1 = AKSPreviewAgentPoolContext( + self.cmd, + AKSAgentPoolParamDict({"host_group_id": None}), + self.models, + DecoratorMode.CREATE, + self.agentpool_decorator_mode, + ) + self.assertEqual(ctx_1.get_host_group_id(), None) + agentpool_1 = self.create_initialized_agentpool_instance(host_group_id="test_host_group_id") + ctx_1.attach_agentpool(agentpool_1) + self.assertEqual(ctx_1.get_host_group_id(), "test_host_group_id") + + def common_get_crg_id(self): + # default + ctx_1 = AKSPreviewAgentPoolContext( + self.cmd, + AKSAgentPoolParamDict({"crg_id": None}), + self.models, + DecoratorMode.CREATE, + self.agentpool_decorator_mode, + ) + self.assertEqual(ctx_1.get_crg_id(), None) + agentpool_1 = self.create_initialized_agentpool_instance( + capacity_reservation_group_id="test_capacity_reservation_group_id" + ) + ctx_1.attach_agentpool(agentpool_1) + self.assertEqual(ctx_1.get_crg_id(), "test_capacity_reservation_group_id") + + def common_get_message_of_the_day(self): + # default + ctx_1 = AKSPreviewAgentPoolContext( + self.cmd, + AKSAgentPoolParamDict({"message_of_the_day": None}), + self.models, + DecoratorMode.CREATE, + self.agentpool_decorator_mode, + ) + self.assertEqual(ctx_1.get_message_of_the_day(), None) + agentpool_1 = self.create_initialized_agentpool_instance(message_of_the_day="test_message_of_the_day") + ctx_1.attach_agentpool(agentpool_1) + self.assertEqual(ctx_1.get_message_of_the_day(), "test_message_of_the_day") + + # custom + ctx_2 = AKSPreviewAgentPoolContext( + self.cmd, + AKSAgentPoolParamDict({"message_of_the_day": get_test_data_file_path("motd.txt")}), + self.models, + DecoratorMode.CREATE, + self.agentpool_decorator_mode, + ) + self.assertEqual( + ctx_2.get_message_of_the_day(), + "VU5BVVRIT1JJWkVEIEFDQ0VTUyBUTyBUSElTIERFVklDRSBJUyBQUk9ISUJJVEVECgpZb3UgbXVzdCBoYXZlIGV4cGxpY2l0LCBhdXRob3JpemVkIHBlcm1pc3Npb24gdG8gYWNjZXNzIG9yIGNvbmZpZ3VyZSB0aGlzIGRldmljZS4gVW5hdXRob3JpemVkIGF0dGVtcHRzIGFuZCBhY3Rpb25zIHRvIGFjY2VzcyBvciB1c2UgdGhpcyBzeXN0ZW0gbWF5IHJlc3VsdCBpbiBjaXZpbCBhbmQvb3IgY3JpbWluYWwgcGVuYWx0aWVzLiBBbGwgYWN0aXZpdGllcyBwZXJmb3JtZWQgb24gdGhpcyBkZXZpY2UgYXJlIGxvZ2dlZCBhbmQgbW9uaXRvcmVkLgo=", + ) + + # custom + ctx_3 = AKSPreviewAgentPoolContext( + self.cmd, + AKSAgentPoolParamDict({"message_of_the_day": "fake-path"}), + self.models, + DecoratorMode.CREATE, + self.agentpool_decorator_mode, + ) + with self.assertRaises(InvalidArgumentValueError): + ctx_3.get_message_of_the_day() + + def common_get_gpu_instance_profile(self): + # default + ctx_1 = AKSPreviewAgentPoolContext( + self.cmd, + AKSAgentPoolParamDict({"gpu_instance_profile": None}), + self.models, + DecoratorMode.CREATE, + self.agentpool_decorator_mode, + ) + self.assertEqual(ctx_1.get_gpu_instance_profile(), None) + agentpool_1 = self.create_initialized_agentpool_instance(gpu_instance_profile="test_gpu_instance_profile") + ctx_1.attach_agentpool(agentpool_1) + self.assertEqual(ctx_1.get_gpu_instance_profile(), "test_gpu_instance_profile") + + def common_get_workload_runtime(self): + # default + ctx_1 = AKSPreviewAgentPoolContext( + self.cmd, + AKSAgentPoolParamDict({"workload_runtime": None}), + self.models, + DecoratorMode.CREATE, + self.agentpool_decorator_mode, + ) + self.assertEqual(ctx_1.get_workload_runtime(), CONST_WORKLOAD_RUNTIME_OCI_CONTAINER) + agentpool_1 = self.create_initialized_agentpool_instance(workload_runtime="test_workload_runtime") + ctx_1.attach_agentpool(agentpool_1) + self.assertEqual(ctx_1.get_workload_runtime(), "test_workload_runtime") + + +class AKSPreviewAgentPoolContextStandaloneModeTestCase(AKSPreviewAgentPoolContextCommonTestCase): + 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.resource_type = CUSTOM_MGMT_AKS_PREVIEW + self.agentpool_decorator_mode = AgentPoolDecoratorMode.STANDALONE + self.models = AKSPreviewAgentPoolModels(self.cmd, self.resource_type, self.agentpool_decorator_mode) + + def test_get_zones(self): + self.common_get_zones() + + def test_get_host_group_id(self): + self.common_get_host_group_id() + + def test_get_crg_id(self): + self.common_get_crg_id() + + def test_get_message_of_the_day(self): + self.common_get_message_of_the_day() + + def test_get_gpu_instance_profile(self): + self.common_get_gpu_instance_profile() + + def test_get_workload_runtime(self): + self.common_get_workload_runtime() + + +class AKSPreviewAgentPoolContextManagedClusterModeTestCase(AKSPreviewAgentPoolContextCommonTestCase): + 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.resource_type = CUSTOM_MGMT_AKS_PREVIEW + self.agentpool_decorator_mode = AgentPoolDecoratorMode.MANAGED_CLUSTER + self.models = AKSPreviewAgentPoolModels(self.cmd, self.resource_type, self.agentpool_decorator_mode) + + def test_get_zones(self): + self.common_get_zones() + + def test_get_host_group_id(self): + self.common_get_host_group_id() + + def test_get_crg_id(self): + self.common_get_crg_id() + + def test_get_message_of_the_day(self): + self.common_get_message_of_the_day() + + def test_get_gpu_instance_profile(self): + self.common_get_gpu_instance_profile() + + def test_get_workload_runtime(self): + self.common_get_workload_runtime() + + +class AKSPreviewAgentPoolAddDecoratorCommonTestCase(unittest.TestCase): + def _remove_defaults_in_agentpool(self, agentpool): + self.defaults_in_agentpool = {} + for attr_name, attr_value in vars(agentpool).items(): + if not attr_name.startswith("_") and attr_name != "name" and attr_value is not None: + self.defaults_in_agentpool[attr_name] = attr_value + setattr(agentpool, attr_name, None) + return agentpool + + def _restore_defaults_in_agentpool(self, agentpool): + for key, value in self.defaults_in_agentpool.items(): + if getattr(agentpool, key, None) is None: + setattr(agentpool, key, value) + return agentpool + + def create_initialized_agentpool_instance( + self, nodepool_name="nodepool1", remove_defaults=True, restore_defaults=True, **kwargs + ): + """Helper function to create a properly initialized agentpool instance. + + :return: the AgentPool object + """ + if self.agentpool_decorator_mode == AgentPoolDecoratorMode.MANAGED_CLUSTER: + agentpool = self.models.UnifiedAgentPoolModel(name=nodepool_name) + else: + agentpool = self.models.UnifiedAgentPoolModel() + agentpool.name = nodepool_name + + # remove defaults + if remove_defaults: + self._remove_defaults_in_agentpool(agentpool) + + # set properties + for key, value in kwargs.items(): + setattr(agentpool, key, value) + + # resote defaults + if restore_defaults: + self._restore_defaults_in_agentpool(agentpool) + return agentpool + + def common_set_up_preview_vm_properties(self): + dec_1 = AKSPreviewAgentPoolAddDecorator( + self.cmd, + self.client, + {"host_group_id": "test_host_group_id", "crg_id": "test_crg_id"}, + self.resource_type, + self.agentpool_decorator_mode, + ) + # fail on passing the wrong agentpool object + with self.assertRaises(CLIInternalError): + dec_1.set_up_preview_vm_properties(None) + agentpool_1 = self.create_initialized_agentpool_instance(restore_defaults=False) + dec_1.context.attach_agentpool(agentpool_1) + dec_agentpool_1 = dec_1.set_up_preview_vm_properties(agentpool_1) + dec_agentpool_1 = self._restore_defaults_in_agentpool(dec_agentpool_1) + ground_truth_agentpool_1 = self.create_initialized_agentpool_instance( + host_group_id="test_host_group_id", capacity_reservation_group_id="test_crg_id" + ) + self.assertEqual(dec_agentpool_1, ground_truth_agentpool_1) + + def common_set_up_motd(self): + dec_1 = AKSPreviewAgentPoolAddDecorator( + self.cmd, + self.client, + {"message_of_the_day": get_test_data_file_path("motd.txt")}, + self.resource_type, + self.agentpool_decorator_mode, + ) + # fail on passing the wrong agentpool object + with self.assertRaises(CLIInternalError): + dec_1.set_up_motd(None) + agentpool_1 = self.create_initialized_agentpool_instance(restore_defaults=False) + dec_1.context.attach_agentpool(agentpool_1) + dec_agentpool_1 = dec_1.set_up_motd(agentpool_1) + dec_agentpool_1 = self._restore_defaults_in_agentpool(dec_agentpool_1) + ground_truth_agentpool_1 = self.create_initialized_agentpool_instance( + message_of_the_day="VU5BVVRIT1JJWkVEIEFDQ0VTUyBUTyBUSElTIERFVklDRSBJUyBQUk9ISUJJVEVECgpZb3UgbXVzdCBoYXZlIGV4cGxpY2l0LCBhdXRob3JpemVkIHBlcm1pc3Npb24gdG8gYWNjZXNzIG9yIGNvbmZpZ3VyZSB0aGlzIGRldmljZS4gVW5hdXRob3JpemVkIGF0dGVtcHRzIGFuZCBhY3Rpb25zIHRvIGFjY2VzcyBvciB1c2UgdGhpcyBzeXN0ZW0gbWF5IHJlc3VsdCBpbiBjaXZpbCBhbmQvb3IgY3JpbWluYWwgcGVuYWx0aWVzLiBBbGwgYWN0aXZpdGllcyBwZXJmb3JtZWQgb24gdGhpcyBkZXZpY2UgYXJlIGxvZ2dlZCBhbmQgbW9uaXRvcmVkLgo=", + ) + self.assertEqual(dec_agentpool_1, ground_truth_agentpool_1) + + def common_set_up_gpu_propertes(self): + dec_1 = AKSPreviewAgentPoolAddDecorator( + self.cmd, + self.client, + {"gpu_instance_profile": "test_gpu_instance_profile", "workload_runtime": "test_workload_runtime"}, + self.resource_type, + self.agentpool_decorator_mode, + ) + # fail on passing the wrong agentpool object + with self.assertRaises(CLIInternalError): + dec_1.set_up_gpu_propertes(None) + agentpool_1 = self.create_initialized_agentpool_instance(restore_defaults=False) + dec_1.context.attach_agentpool(agentpool_1) + dec_agentpool_1 = dec_1.set_up_gpu_propertes(agentpool_1) + dec_agentpool_1 = self._restore_defaults_in_agentpool(dec_agentpool_1) + ground_truth_agentpool_1 = self.create_initialized_agentpool_instance( + gpu_instance_profile="test_gpu_instance_profile", + workload_runtime="test_workload_runtime", + ) + self.assertEqual(dec_agentpool_1, ground_truth_agentpool_1) + + +class AKSPreviewAgentPoolAddDecoratorStandaloneModeTestCase(AKSPreviewAgentPoolAddDecoratorCommonTestCase): + 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.resource_type = CUSTOM_MGMT_AKS_PREVIEW + self.agentpool_decorator_mode = AgentPoolDecoratorMode.STANDALONE + self.models = AKSPreviewAgentPoolModels(self.cmd, self.resource_type, self.agentpool_decorator_mode) + self.client = MockClient() + + def test_set_up_preview_vm_properties(self): + self.common_set_up_preview_vm_properties() + + def test_set_up_motd(self): + self.common_set_up_motd() + + def test_set_up_gpu_propertes(self): + self.common_set_up_gpu_propertes() + + def test_construct_agentpool_profile_preview(self): + import inspect + + from azext_aks_preview.custom import aks_agentpool_add + + optional_params = {} + positional_params = [] + for _, v in inspect.signature(aks_agentpool_add).parameters.items(): + if v.default != v.empty: + optional_params[v.name] = v.default + else: + positional_params.append(v.name) + ground_truth_positional_params = [ + "cmd", + "client", + "resource_group_name", + "cluster_name", + "nodepool_name", + ] + self.assertEqual(positional_params, ground_truth_positional_params) + + # prepare a dictionary of default parameters + raw_param_dict = { + "resource_group_name": "test_rg_name", + "cluster_name": "test_cluster_name", + "nodepool_name": "test_nodepool_name", + } + raw_param_dict.update(optional_params) + + # default value in `aks nodepool add` + dec_1 = AKSPreviewAgentPoolAddDecorator( + self.cmd, + self.client, + raw_param_dict, + self.resource_type, + self.agentpool_decorator_mode, + ) + + with patch( + "azext_aks_preview.agentpool_decorator.cf_agent_pools", + return_value=Mock(list=Mock(return_value=[])), + ): + dec_agentpool_1 = dec_1.construct_agentpool_profile_preview() + + ground_truth_upgrade_settings_1 = self.models.AgentPoolUpgradeSettings() + ground_truth_agentpool_1 = self.create_initialized_agentpool_instance( + nodepool_name="test_nodepool_name", + upgrade_settings=ground_truth_upgrade_settings_1, + os_disk_size_gb=0, + enable_auto_scaling=False, + count=3, + os_type=CONST_DEFAULT_NODE_OS_TYPE, + vm_size=CONST_DEFAULT_NODE_VM_SIZE, + node_taints=[], + enable_node_public_ip=False, + type_properties_type=CONST_VIRTUAL_MACHINE_SCALE_SETS, + enable_encryption_at_host=False, + enable_ultra_ssd=False, + enable_fips=False, + mode=CONST_NODEPOOL_MODE_USER, + scale_down_mode=CONST_SCALE_DOWN_MODE_DELETE, + workload_runtime=CONST_WORKLOAD_RUNTIME_OCI_CONTAINER, + ) + self.assertEqual(dec_agentpool_1, ground_truth_agentpool_1) + + dec_1.context.raw_param.print_usage_statistics() + + +class AKSPreviewAgentPoolAddDecoratorManagedClusterModeTestCase(AKSPreviewAgentPoolAddDecoratorCommonTestCase): + 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.resource_type = CUSTOM_MGMT_AKS_PREVIEW + self.agentpool_decorator_mode = AgentPoolDecoratorMode.MANAGED_CLUSTER + self.models = AKSPreviewAgentPoolModels(self.cmd, self.resource_type, self.agentpool_decorator_mode) + self.client = MockClient() + + def test_set_up_preview_vm_properties(self): + self.common_set_up_preview_vm_properties() + + def test_set_up_motd(self): + self.common_set_up_motd() + + def test_set_up_gpu_propertes(self): + self.common_set_up_gpu_propertes() + + def test_construct_agentpool_profile_preview(self): + import inspect + + from azext_aks_preview.custom import aks_create + + optional_params = {} + positional_params = [] + for _, v in inspect.signature(aks_create).parameters.items(): + if v.default != v.empty: + optional_params[v.name] = v.default + else: + positional_params.append(v.name) + ground_truth_positional_params = [ + "cmd", + "client", + "resource_group_name", + "name", + "ssh_key_value", + ] + self.assertEqual(positional_params, ground_truth_positional_params) + + # prepare a dictionary of default parameters + raw_param_dict = { + "resource_group_name": "test_rg_name", + "name": "test_cluster_name", + "ssh_key_value": None, + } + raw_param_dict.update(optional_params) + + # default value in `aks_create` + dec_1 = AKSPreviewAgentPoolAddDecorator( + self.cmd, + self.client, + raw_param_dict, + self.resource_type, + self.agentpool_decorator_mode, + ) + + with patch( + "azure.cli.command_modules.acs.agentpool_decorator.cf_agent_pools", + return_value=Mock(list=Mock(return_value=[])), + ): + dec_agentpool_1 = dec_1.construct_agentpool_profile_preview() + + upgrade_settings_1 = self.models.AgentPoolUpgradeSettings() + ground_truth_agentpool_1 = self.create_initialized_agentpool_instance( + nodepool_name="nodepool1", + upgrade_settings=upgrade_settings_1, + os_disk_size_gb=0, + enable_auto_scaling=False, + count=3, + orchestrator_version="", + os_type=CONST_DEFAULT_NODE_OS_TYPE, + vm_size=CONST_DEFAULT_NODE_VM_SIZE, + node_taints=[], + enable_node_public_ip=False, + type=CONST_VIRTUAL_MACHINE_SCALE_SETS, + enable_encryption_at_host=False, + enable_ultra_ssd=False, + enable_fips=False, + mode=CONST_NODEPOOL_MODE_SYSTEM, + workload_runtime=CONST_WORKLOAD_RUNTIME_OCI_CONTAINER, + ) + self.assertEqual(dec_agentpool_1, ground_truth_agentpool_1) + + dec_1.context.raw_param.print_usage_statistics() + + +class AKSPreviewAgentPoolUpdateDecoratorCommonTestCase(unittest.TestCase): + def _remove_defaults_in_agentpool(self, agentpool): + self.defaults_in_agentpool = {} + for attr_name, attr_value in vars(agentpool).items(): + if not attr_name.startswith("_") and attr_name != "name" and attr_value is not None: + self.defaults_in_agentpool[attr_name] = attr_value + setattr(agentpool, attr_name, None) + return agentpool + + def _restore_defaults_in_agentpool(self, agentpool): + for key, value in self.defaults_in_agentpool.items(): + if getattr(agentpool, key, None) is None: + setattr(agentpool, key, value) + return agentpool + + def create_initialized_agentpool_instance( + self, nodepool_name="nodepool1", remove_defaults=True, restore_defaults=True, **kwargs + ): + """Helper function to create a properly initialized agentpool instance. + + :return: the AgentPool object + """ + if self.agentpool_decorator_mode == AgentPoolDecoratorMode.MANAGED_CLUSTER: + agentpool = self.models.UnifiedAgentPoolModel(name=nodepool_name) + else: + agentpool = self.models.UnifiedAgentPoolModel() + agentpool.name = nodepool_name + + # remove defaults + if remove_defaults: + self._remove_defaults_in_agentpool(agentpool) + + # set properties + for key, value in kwargs.items(): + setattr(agentpool, key, value) + + # resote defaults + if restore_defaults: + self._restore_defaults_in_agentpool(agentpool) + return agentpool + + +class AKSPreviewAgentPoolUpdateDecoratorStandaloneModeTestCase(AKSPreviewAgentPoolUpdateDecoratorCommonTestCase): + 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.resource_type = CUSTOM_MGMT_AKS_PREVIEW + self.agentpool_decorator_mode = AgentPoolDecoratorMode.STANDALONE + self.models = AKSPreviewAgentPoolModels(self.cmd, self.resource_type, self.agentpool_decorator_mode) + self.client = MockClient() + + def test_update_agentpool_profile_preview(self): + import inspect + + from azext_aks_preview.custom import aks_agentpool_update + + optional_params = {} + positional_params = [] + for _, v in inspect.signature(aks_agentpool_update).parameters.items(): + if v.default != v.empty: + optional_params[v.name] = v.default + else: + positional_params.append(v.name) + ground_truth_positional_params = [ + "cmd", + "client", + "resource_group_name", + "cluster_name", + "nodepool_name", + ] + self.assertEqual(positional_params, ground_truth_positional_params) + + # prepare a dictionary of default parameters + raw_param_dict = { + "resource_group_name": "test_rg_name", + "cluster_name": "test_cluster_name", + "nodepool_name": "test_nodepool_name", + } + raw_param_dict.update(optional_params) + + # default value in `aks_create` + dec_1 = AKSPreviewAgentPoolUpdateDecorator( + self.cmd, + self.client, + raw_param_dict, + self.resource_type, + self.agentpool_decorator_mode, + ) + self.client.get = Mock( + return_value=self.create_initialized_agentpool_instance(nodepool_name="test_nodepool_name") + ) + dec_agentpool_1 = dec_1.update_agentpool_profile_preview() + ground_truth_agentpool_1 = self.create_initialized_agentpool_instance( + nodepool_name="test_nodepool_name", + ) + self.assertEqual(dec_agentpool_1, ground_truth_agentpool_1) + + dec_1.context.raw_param.print_usage_statistics() + + +class AKSPreviewAgentPoolUpdateDecoratorManagedClusterModeTestCase(AKSPreviewAgentPoolUpdateDecoratorCommonTestCase): + 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.resource_type = CUSTOM_MGMT_AKS_PREVIEW + self.agentpool_decorator_mode = AgentPoolDecoratorMode.MANAGED_CLUSTER + self.models = AKSPreviewAgentPoolModels(self.cmd, self.resource_type, self.agentpool_decorator_mode) + self.client = MockClient() + + def test_update_agentpool_profile_preview(self): + import inspect + + from azure.cli.command_modules.acs.custom import aks_update + + optional_params = {} + positional_params = [] + for _, v in inspect.signature(aks_update).parameters.items(): + if v.default != v.empty: + optional_params[v.name] = v.default + else: + positional_params.append(v.name) + ground_truth_positional_params = [ + "cmd", + "client", + "resource_group_name", + "name", + ] + self.assertEqual(positional_params, ground_truth_positional_params) + + # prepare a dictionary of default parameters + raw_param_dict = { + "resource_group_name": "test_rg_name", + "name": "test_cluster_name", + } + raw_param_dict.update(optional_params) + + # default value in `aks_create` + dec_1 = AKSPreviewAgentPoolUpdateDecorator( + self.cmd, + self.client, + raw_param_dict, + self.resource_type, + self.agentpool_decorator_mode, + ) + agentpools = [ + self.create_initialized_agentpool_instance(nodepool_name="test_nodepool_1"), + self.create_initialized_agentpool_instance(nodepool_name="test_nodepool_2"), + ] + dec_agentpool_1 = dec_1.update_agentpool_profile_preview(agentpools) + ground_truth_agentpool_1 = self.create_initialized_agentpool_instance( + nodepool_name="test_nodepool_1", + ) + self.assertEqual(dec_agentpool_1, ground_truth_agentpool_1) + + dec_1.context.raw_param.print_usage_statistics() + + +if __name__ == "__main__": + unittest.main() diff --git a/src/aks-preview/azext_aks_preview/tests/latest/utils.py b/src/aks-preview/azext_aks_preview/tests/latest/utils.py new file mode 100644 index 00000000000..5871a469b84 --- /dev/null +++ b/src/aks-preview/azext_aks_preview/tests/latest/utils.py @@ -0,0 +1,14 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +import os +import platform +import shutil +import tempfile + + +def get_test_data_file_path(filename): + curr_dir = os.path.dirname(os.path.realpath(__file__)) + return os.path.join(curr_dir, "data", filename)