diff --git a/src/aks-preview/HISTORY.rst b/src/aks-preview/HISTORY.rst index 31404ddcd7d..c36fe8b94ca 100644 --- a/src/aks-preview/HISTORY.rst +++ b/src/aks-preview/HISTORY.rst @@ -11,11 +11,15 @@ To release a new version, please select a new version number (usually plus 1 to Pending +++++++ + +21.0.0b7 +++++++++ * `az aks create`: Add `--node-disruption-policy` (preview) to set the node disruption policy at cluster creation time. Requires AFEC registration `Microsoft.ContainerService/NodeDisruptionProfile`. This is a cluster-level property that applies to all node pools in the cluster. -* `az aks maintenancewindow`: Add CRUD commands (`create`, `show`, `list`, `update`, `delete`, `wait`) for the new MaintenanceWindow peer ARM resource. Available with API version `2026-04-02-preview`. Requires the `Microsoft.ContainerService/AKSSharedMaintenanceWindowPreview` feature to be registered on the subscription (auto-approval). * `az aks bastion`: Fix failure when the bastion host is in a different subscription than the cluster by using the subscription from the bastion resource ID for the internal `az network bastion tunnel` command. -* Set `principalType` when creating role assignments to avoid `PrincipalNotFound` failures caused by Microsoft Entra ID replication delay for freshly created identities. +* `az aks create` and `az aks update`: Add `--enable-backup` (preview) to configure Azure Backup for the AKS cluster in a single command. Supports `--backup-strategy` presets (Week, Month, DisasterRecovery, Custom) and an optional `--backup-configuration` for bring-your-own vault/policy/storage. Requires the `dataprotection` CLI extension. +* `az aks maintenancewindow`: Add CRUD commands (`create`, `show`, `list`, `update`, `delete`, `wait`) for the new MaintenanceWindow peer ARM resource. Available with API version `2026-04-02-preview`. Requires the `Microsoft.ContainerService/AKSSharedMaintenanceWindowPreview` feature to be registered on the subscription (auto-approval). * `az aks update`: Fix misleading error when updating outbound type to `userDefinedRouting` or `userAssignedNATGateway`. For managed VNet clusters (unsupported), a clear error message is now shown instead of asking for `--vnet-subnet-id`. For BYO VNet clusters, the update works correctly without requiring the user to re-specify the subnet. +* Set `principalType` when creating role assignments to avoid `PrincipalNotFound` failures caused by Microsoft Entra ID replication delay for freshly created identities. 21.0.0b6 ++++++++ diff --git a/src/aks-preview/azext_aks_preview/_help.py b/src/aks-preview/azext_aks_preview/_help.py index d5e72bd3ad2..6d04154ca5d 100644 --- a/src/aks-preview/azext_aks_preview/_help.py +++ b/src/aks-preview/azext_aks_preview/_help.py @@ -843,6 +843,8 @@ text: az aks create -g MyResourceGroup -n MyManagedCluster --control-plane-scaling-size H4 - name: Create an automatic cluster with hosted system components enabled. text: az aks create -g MyResourceGroup -n MyManagedCluster --sku automatic --enable-hosted-system + - name: Create a kubernetes cluster with Azure Backup enabled (default Week strategy). Requires the 'dataprotection' extension. Implicitly waits for cluster creation. + text: az aks create -g MyResourceGroup -n MyManagedCluster --generate-ssh-keys --enable-backup --yes """ @@ -1613,6 +1615,10 @@ text: az aks update -g MyResourceGroup -n MyManagedCluster --safeguards-level Warning --safeguards-excluded-ns ns1,ns2 - name: Enable Azure Monitor logs for a kubernetes cluster text: az aks update -g MyResourceGroup -n MyManagedCluster --enable-azure-monitor-logs + - name: Enable Azure Backup for a kubernetes cluster (default Week strategy). Requires the 'dataprotection' extension. + text: az aks update -g MyResourceGroup -n MyManagedCluster --enable-backup --yes + - name: Enable Azure Backup with a custom strategy using an existing vault and policy + text: az aks update -g MyResourceGroup -n MyManagedCluster --enable-backup --backup-strategy Custom --backup-configuration @config.json --yes - name: Disable Azure Monitor logs for a kubernetes cluster text: az aks update -g MyResourceGroup -n MyManagedCluster --disable-azure-monitor-logs - name: Update a kubernetes cluster to clear any namespaces excluded from safeguards. Assumes azure policy addon is already enabled diff --git a/src/aks-preview/azext_aks_preview/_params.py b/src/aks-preview/azext_aks_preview/_params.py index de14295c279..c91f3c11139 100644 --- a/src/aks-preview/azext_aks_preview/_params.py +++ b/src/aks-preview/azext_aks_preview/_params.py @@ -568,6 +568,10 @@ CONST_UPGRADE_STRATEGY_BLUE_GREEN, ] +# AKS backup strategy presets exposed by --backup-strategy. +# NOTE: must mirror CONST_AKS_BACKUP_STRATEGIES in azext_dataprotection.manual._consts. +aks_backup_strategies = ["Week", "Month", "DisasterRecovery", "Custom"] + node_disruption_policies = [ CONST_NODE_DISRUPTION_POLICY_ALLOW, CONST_NODE_DISRUPTION_POLICY_BLOCK, @@ -1318,6 +1322,34 @@ def load_arguments(self, _): is_preview=True, help="Enable continuous control plane and addon monitor for the cluster.", ) + # Backup (delegates to the dataprotection extension) + c.argument( + "enable_backup", + action="store_true", + is_preview=True, + help="Enable Azure Backup for this AKS cluster. Orchestrates the same flow as " + "'az dataprotection enable-backup trigger' (requires the 'dataprotection' extension). " + "Implicitly waits for cluster creation to complete (ignores --no-wait).", + ) + c.argument( + "backup_strategy", + arg_type=get_enum_type(aks_backup_strategies), + is_preview=True, + help="Backup strategy preset. Week (default, 7-day operational retention), Month " + "(30-day operational retention), DisasterRecovery (7-day operational + 90-day vault " + "retention), Custom (bring your own vault and policy via --backup-configuration). " + "Only valid with --enable-backup.", + ) + c.argument( + "backup_configuration_file", + options_list=["--backup-configuration"], + type=validate_file_or_dict, + is_preview=True, + help="Backup configuration as inline JSON string or @file.json. " + "Supports storageAccountResourceId, blobContainerName, backupResourceGroupId, " + "backupVaultId, backupPolicyId, tags. backupVaultId and backupPolicyId are required " + "for Custom strategy. Only valid with --enable-backup.", + ) # prepared image specification c.argument( 'prepared_image_specification_id', @@ -1989,6 +2021,33 @@ def load_arguments(self, _): is_preview=True, help="Disable continuous control plane and addon monitor for the cluster.", ) + # Backup (delegates to the dataprotection extension) + c.argument( + "enable_backup", + action="store_true", + is_preview=True, + help="Enable Azure Backup for this AKS cluster. Orchestrates the same flow as " + "'az dataprotection enable-backup trigger' (requires the 'dataprotection' extension).", + ) + c.argument( + "backup_strategy", + arg_type=get_enum_type(aks_backup_strategies), + is_preview=True, + help="Backup strategy preset. Week (default, 7-day operational retention), Month " + "(30-day operational retention), DisasterRecovery (7-day operational + 90-day vault " + "retention), Custom (bring your own vault and policy via --backup-configuration). " + "Only valid with --enable-backup.", + ) + c.argument( + "backup_configuration_file", + options_list=["--backup-configuration"], + type=validate_file_or_dict, + is_preview=True, + help="Backup configuration as inline JSON string or @file.json. " + "Supports storageAccountResourceId, blobContainerName, backupResourceGroupId, " + "backupVaultId, backupPolicyId, tags. backupVaultId and backupPolicyId are required " + "for Custom strategy. Only valid with --enable-backup.", + ) c.argument( "control_plane_scaling_size", options_list=["--control-plane-scaling-size", "--cp-scaling-size"], diff --git a/src/aks-preview/azext_aks_preview/aks_backup.py b/src/aks-preview/azext_aks_preview/aks_backup.py new file mode 100644 index 00000000000..845ca013c45 --- /dev/null +++ b/src/aks-preview/azext_aks_preview/aks_backup.py @@ -0,0 +1,84 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +"""Helpers that delegate AKS backup enablement to the dataprotection extension.""" + +from knack.log import get_logger +from knack.prompting import prompt_y_n, NoTTYException + +from azure.cli.core.azclierror import RequiredArgumentMissingError + +DATAPROTECTION_EXTENSION_NAME = "dataprotection" + +logger = get_logger(__name__) + + +def _ensure_dataprotection_extension(cmd, yes): + """Make ``azext_dataprotection.manual.aks.aks_helper`` importable. + + If the ``dataprotection`` extension is not installed, prompt the user to + install it (or install silently when ``yes`` is True). Raises + ``RequiredArgumentMissingError`` if the user declines or the install fails. + """ + from azure.cli.core.extension.operations import add_extension_to_path + + try: + add_extension_to_path(DATAPROTECTION_EXTENSION_NAME) + from azext_dataprotection.manual.aks.aks_helper import ( # pylint: disable=unused-import,import-error + dataprotection_enable_backup_helper, + ) + return + except Exception: # pylint: disable=broad-except + pass + + install_msg = ( + f"The '{DATAPROTECTION_EXTENSION_NAME}' extension is required for " + "--enable-backup but is not installed. Install it now?" + ) + proceed = yes + if not proceed: + try: + proceed = prompt_y_n(install_msg, default="y") + except NoTTYException: + proceed = False + if not proceed: + raise RequiredArgumentMissingError( + f"The '{DATAPROTECTION_EXTENSION_NAME}' extension is required for " + "--enable-backup with 'az aks create' / 'az aks update'.\n" + f"Run `az extension add --name {DATAPROTECTION_EXTENSION_NAME}` " + "and retry, or rerun with --yes to auto-install." + ) + + logger.warning("Installing extension '%s'...", DATAPROTECTION_EXTENSION_NAME) + from azure.cli.core.extension.operations import add_extension + add_extension(cmd=cmd, extension_name=DATAPROTECTION_EXTENSION_NAME) + add_extension_to_path(DATAPROTECTION_EXTENSION_NAME) + + +def enable_aks_backup(cmd, resource_group_name, cluster_name, # pylint: disable=too-many-positional-arguments + backup_strategy, backup_configuration_file, yes): + """Enable Azure Backup for an AKS cluster by delegating to the + ``dataprotection`` extension. + """ + from azure.cli.core.commands.client_factory import get_subscription_id + + _ensure_dataprotection_extension(cmd, yes) + + from azext_dataprotection.manual.aks.aks_helper import ( # pylint: disable=import-error + dataprotection_enable_backup_helper, + ) + + subscription_id = get_subscription_id(cmd.cli_ctx) + datasource_id = ( + f"/subscriptions/{subscription_id}/resourceGroups/{resource_group_name}" + f"/providers/Microsoft.ContainerService/managedClusters/{cluster_name}" + ) + dataprotection_enable_backup_helper( + cmd, + datasource_id, + backup_strategy or "Week", + backup_configuration_file or {}, + yes=yes, + ) diff --git a/src/aks-preview/azext_aks_preview/custom.py b/src/aks-preview/azext_aks_preview/custom.py index 368292c71b9..9e823d00871 100644 --- a/src/aks-preview/azext_aks_preview/custom.py +++ b/src/aks-preview/azext_aks_preview/custom.py @@ -1440,6 +1440,10 @@ def aks_create( control_plane_scaling_size=None, # health monitor enable_continuous_control_plane_and_addon_monitor=False, + # backup (delegates to the dataprotection extension) + enable_backup=False, + backup_strategy=None, + backup_configuration_file=None, # prepared image specification prepared_image_specification_id=None, ): @@ -1702,6 +1706,10 @@ def aks_update( # health monitor enable_continuous_control_plane_and_addon_monitor=False, disable_continuous_control_plane_and_addon_monitor=False, + # backup (delegates to the dataprotection extension) + enable_backup=False, + backup_strategy=None, + backup_configuration_file=None, # node disruption policy node_disruption_policy=None, # control plane scaling diff --git a/src/aks-preview/azext_aks_preview/managed_cluster_decorator.py b/src/aks-preview/azext_aks_preview/managed_cluster_decorator.py index cb636f95e32..016d9aca659 100644 --- a/src/aks-preview/azext_aks_preview/managed_cluster_decorator.py +++ b/src/aks-preview/azext_aks_preview/managed_cluster_decorator.py @@ -296,6 +296,9 @@ def external_functions(self) -> SimpleNamespace: external_functions["ensure_container_insights_for_monitoring"] = ( ensure_container_insights_for_monitoring_preview ) + # AKS backup (delegates to the dataprotection extension) + from azext_aks_preview.aks_backup import enable_aks_backup + external_functions["enable_aks_backup"] = enable_aks_backup self.__external_functions = SimpleNamespace(**external_functions) return self.__external_functions @@ -5516,6 +5519,7 @@ def check_is_postprocessing_required(self, mc: ManagedCluster) -> bool: "enable_azure_container_storage", default_value=False ) + enable_backup = self.context.raw_param.get("enable_backup", False) # pylint: disable=too-many-boolean-expressions if ( @@ -5525,7 +5529,8 @@ def check_is_postprocessing_required(self, mc: ManagedCluster) -> bool: azuremonitormetrics_addon_enabled or (enable_managed_identity and attach_acr) or need_grant_vnet_permission_to_cluster_identity or - enable_azure_container_storage + enable_azure_container_storage or + enable_backup ): return True return False @@ -5787,6 +5792,17 @@ def postprocessing_after_mc_created(self, cluster: ManagedCluster) -> None: assignee_principal_type="User", ) + # Enable Azure Backup for the AKS cluster (delegates to dataprotection extension) + if self.context.raw_param.get("enable_backup", False): + self.context.external_functions.enable_aks_backup( + self.cmd, + self.context.get_resource_group_name(), + self.context.get_name(), + self.context.raw_param.get("backup_strategy"), + self.context.raw_param.get("backup_configuration_file"), + self.context.raw_param.get("yes", False), + ) + def put_mc(self, mc: ManagedCluster) -> ManagedCluster: etag, match_condition = _get_etag_match_condition( self.context.get_if_match(), self.context.get_if_none_match() @@ -8512,10 +8528,13 @@ def check_is_postprocessing_required(self, mc: ManagedCluster) -> bool: monitoring_addon_postprocessing_required = self.context.get_intermediate( "monitoring_addon_postprocessing_required", default_value=False ) + enable_backup = self.context.raw_param.get("enable_backup", False) # Note: monitoring_addon_disable_postprocessing_required is no longer used - cleanup is done upfront + # pylint: disable=too-many-boolean-expressions if (enable_azure_container_storage or disable_azure_container_storage) or \ (keyvault_id and enable_azure_keyvault_secrets_provider_addon) or \ - (monitoring_addon_postprocessing_required): + (monitoring_addon_postprocessing_required) or \ + enable_backup: return True return postprocessing_required @@ -8748,6 +8767,17 @@ def postprocessing_after_mc_created(self, cluster: ManagedCluster) -> None: else: raise CLIError('Keyvault secrets provider addon must be enabled to attach keyvault.\n') + # Enable Azure Backup for the AKS cluster (delegates to dataprotection extension) + if self.context.raw_param.get("enable_backup", False): + self.context.external_functions.enable_aks_backup( + self.cmd, + self.context.get_resource_group_name(), + self.context.get_name(), + self.context.raw_param.get("backup_strategy"), + self.context.raw_param.get("backup_configuration_file"), + self.context.raw_param.get("yes", False), + ) + def put_mc(self, mc: ManagedCluster) -> ManagedCluster: etag, match_condition = _get_etag_match_condition( self.context.get_if_match(), self.context.get_if_none_match() diff --git a/src/aks-preview/azext_aks_preview/tests/latest/test_aks_commands.py b/src/aks-preview/azext_aks_preview/tests/latest/test_aks_commands.py index 5e10d234788..9391421fe81 100644 --- a/src/aks-preview/azext_aks_preview/tests/latest/test_aks_commands.py +++ b/src/aks-preview/azext_aks_preview/tests/latest/test_aks_commands.py @@ -14886,6 +14886,271 @@ def test_aks_create_with_azuremonitorappmonitoring( ], ) + # ------------------------------------------------------------------ + # --enable-backup helpers + tests + # ------------------------------------------------------------------ + + def _setup_backup_test(self, resource_group, resource_group_location, aks_name): + """Common setup: install sibling extensions used by --enable-backup + orchestration (`dataprotection`, `k8s-extension`) and seed kwargs.""" + self.test_resources_count = 0 + node_vm_size = "standard_d2s_v3" + self.kwargs.update( + { + "resource_group": resource_group, + "name": aks_name, + "location": resource_group_location, + "ssh_key_value": self.generate_ssh_keys(), + "node_vm_size": node_vm_size, + } + ) + self.cmd("extension add --name dataprotection") + self.cmd("extension add --name k8s-extension") + + def _validate_backup(self, resource_group, aks_name, resource_group_location): + """Validate k8s extension, vault, policy, backup instance. + Returns (vault_name, instance_name, policy_name) for cleanup. + """ + vault_name = None + instance_name = None + policy_name = None + + # 1. k8s extension + self.cmd( + "k8s-extension show --resource-group {resource_group} " + "--cluster-name {name} --cluster-type managedClusters " + "--name azure-aks-backup", + checks=[self.check("provisioningState", "Succeeded")], + ) + + sub_id = self.cmd("account show --query id -o tsv").output.strip() + cluster_id = ( + f"/subscriptions/{sub_id}/resourceGroups/{resource_group}" + f"/providers/Microsoft.ContainerService/managedClusters/" + f"{aks_name}" + ) + backup_rg = f"AKSAzureBackup_{resource_group_location}" + self.kwargs["backup_rg"] = backup_rg + + # 2. vault exists + vaults = self.cmd( + "dataprotection backup-vault list -g {backup_rg}" + ).get_output_in_json() + self.assertGreaterEqual( + len(vaults), 1, + f"Expected at least one backup vault in {backup_rg}" + ) + + # 3. find the vault hosting OUR backup instance + policy + backup_vault_name = None + backup_policy_count = 0 + for vault in vaults: + v_name = vault["name"] + self.kwargs["vault_name"] = v_name + instances = self.cmd( + "dataprotection backup-instance list " + "-g {backup_rg} --vault-name {vault_name}" + ).get_output_in_json() + for inst in instances: + ds_id = ( + inst.get("properties", {}) + .get("dataSourceInfo", {}) + .get("resourceID", "") + ) + if ds_id.lower() == cluster_id.lower(): + backup_vault_name = v_name + instance_name = inst["name"] + vault_name = v_name + break + if backup_vault_name: + policies = self.cmd( + "dataprotection backup-policy list " + "-g {backup_rg} --vault-name {vault_name}" + ).get_output_in_json() + backup_policy_count = len(policies) + if policies: + policy_id = ( + inst.get("properties", {}) + .get("policyInfo", {}) + .get("policyId", "") + ) + for pol in policies: + if pol["id"].lower() == policy_id.lower(): + policy_name = pol["name"] + break + if policy_name is None: + policy_name = policies[0]["name"] + break + self.assertIsNotNone( + backup_vault_name, + f"No backup vault hosting a backup instance for " + f"cluster {aks_name} was found in {backup_rg}" + ) + self.assertGreaterEqual( + backup_policy_count, 1, + f"Expected at least one backup policy on vault " + f"{backup_vault_name}" + ) + + # 4. poll until ProtectionConfigured + self.kwargs["vault_name"] = backup_vault_name + self.kwargs["instance_name"] = instance_name + final_status = None + for _ in range(8): + inst = self.cmd( + "dataprotection backup-instance show " + "-g {backup_rg} --vault-name {vault_name} " + "--backup-instance-name {instance_name}" + ).get_output_in_json() + final_status = ( + inst.get("properties", {}) + .get("protectionStatus", {}) + .get("status") + ) + if final_status == "ProtectionConfigured": + break + time.sleep(30) + self.assertEqual( + final_status, "ProtectionConfigured", + f"Backup instance {instance_name} did not reach " + f"ProtectionConfigured (last status: {final_status})" + ) + + return vault_name, instance_name, policy_name + + def _cleanup_backup(self, vault_name): + """Best-effort cleanup of the shared regional backup vault: drain + all backup instances and policies, then delete the vault. The + cluster + cluster RG are reaped by `AKSCustomResourceGroupPreparer`. + """ + backup_rg = self.kwargs.get("backup_rg") + if not (vault_name and backup_rg): + return + + # 1. Disable immutability + soft-delete on the vault so that BIs + # with active recovery points can be force-deleted. + try: + self.cmd( + "dataprotection backup-vault update " + "-g {backup_rg} --vault-name {vault_name} " + "--set properties.securitySettings.immutabilitySettings.state=Disabled " + "properties.securitySettings.softDeleteSettings.state=Off" + ) + except Exception: # pylint: disable=broad-except + pass + + # 2. Delete ALL backup instances on the vault. + try: + all_instances = self.cmd( + "dataprotection backup-instance list " + "-g {backup_rg} --vault-name {vault_name}" + ).get_output_in_json() + for bi in all_instances: + self.kwargs["_bi"] = bi["name"] + try: + self.cmd( + "dataprotection backup-instance delete " + "-g {backup_rg} --vault-name {vault_name} " + "--backup-instance-name {_bi} --yes" + ) + except Exception: # pylint: disable=broad-except + pass + except Exception: # pylint: disable=broad-except + pass + + # 3. Delete ALL backup policies on the vault. + try: + all_policies = self.cmd( + "dataprotection backup-policy list " + "-g {backup_rg} --vault-name {vault_name}" + ).get_output_in_json() + for pol in all_policies: + self.kwargs["_pol"] = pol["name"] + try: + self.cmd( + "dataprotection backup-policy delete " + "-g {backup_rg} --vault-name {vault_name} " + "--name {_pol} --yes" + ) + except Exception: # pylint: disable=broad-except + pass + except Exception: # pylint: disable=broad-except + pass + + # 4. Delete the vault (now empty). + try: + self.cmd( + "dataprotection backup-vault delete " + "-g {backup_rg} --vault-name {vault_name} --yes" + ) + except Exception: # pylint: disable=broad-except + pass + + # ---- aks create --enable-backup ---- + + @live_only() + @AllowLargeResponse(999999) + @AKSCustomResourceGroupPreparer( + random_name_length=17, name_prefix="clitest", location="westcentralus" + ) + def test_aks_create_with_enable_backup(self, resource_group, resource_group_location): + """aks create --enable-backup: full 8-step orchestration after + cluster LRO, then validate extension / vault / policy / BI.""" + aks_name = self.create_random_name("cliakstest", 16) + self._setup_backup_test(resource_group, resource_group_location, aks_name) + vault_name = None + try: + self.cmd( + "aks create --resource-group={resource_group} --name={name} " + "--location={location} --ssh-key-value={ssh_key_value} " + "--node-vm-size={node_vm_size} --node-count 3 " + "--enable-managed-identity " + "--enable-backup --backup-strategy Week " + "--yes --output=json", + checks=[self.check("provisioningState", "Succeeded")], + ) + vault_name, _, _ = self._validate_backup( + resource_group, aks_name, resource_group_location + ) + finally: + self._cleanup_backup(vault_name) + + # ---- aks update --enable-backup ---- + + @live_only() + @AllowLargeResponse(999999) + @AKSCustomResourceGroupPreparer( + random_name_length=17, name_prefix="clitest", location="westcentralus" + ) + def test_aks_update_with_enable_backup(self, resource_group, resource_group_location): + """aks update --enable-backup on a brownfield cluster: create the + cluster first without backup, then update with --enable-backup.""" + aks_name = self.create_random_name("cliakstest", 16) + self._setup_backup_test(resource_group, resource_group_location, aks_name) + vault_name = None + try: + # 1. Create a plain cluster (no backup). + self.cmd( + "aks create --resource-group={resource_group} --name={name} " + "--location={location} --ssh-key-value={ssh_key_value} " + "--node-vm-size={node_vm_size} --node-count 3 " + "--enable-managed-identity " + "--yes --output=json", + checks=[self.check("provisioningState", "Succeeded")], + ) + # 2. Update: enable backup on the existing cluster. + self.cmd( + "aks update --resource-group={resource_group} --name={name} " + "--enable-backup --backup-strategy Week " + "--yes --output=json", + checks=[self.check("provisioningState", "Succeeded")], + ) + vault_name, _, _ = self._validate_backup( + resource_group, aks_name, resource_group_location + ) + finally: + self._cleanup_backup(vault_name) + # live only due to downloading k8s-extension extension @live_only() @AllowLargeResponse(999999) diff --git a/src/aks-preview/setup.py b/src/aks-preview/setup.py index 0229e9ebd47..31c52a1dfc0 100644 --- a/src/aks-preview/setup.py +++ b/src/aks-preview/setup.py @@ -9,7 +9,7 @@ from setuptools import find_packages, setup -VERSION = "21.0.0b6" +VERSION = "21.0.0b7" CLASSIFIERS = [ "Development Status :: 4 - Beta",