diff --git a/src/aks-preview/HISTORY.rst b/src/aks-preview/HISTORY.rst index 16968a8a68b..1f72874429c 100644 --- a/src/aks-preview/HISTORY.rst +++ b/src/aks-preview/HISTORY.rst @@ -11,6 +11,7 @@ To release a new version, please select a new version number (usually plus 1 to Pending +++++++ +* `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. 20.0.0b2 +++++++ 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 d0ccbfb0389..845fa9f7ea5 100644 --- a/src/aks-preview/azext_aks_preview/managed_cluster_decorator.py +++ b/src/aks-preview/azext_aks_preview/managed_cluster_decorator.py @@ -510,7 +510,7 @@ def _get_disable_local_accounts(self, enable_validation: bool = False) -> bool: ) return disable_local_accounts - def _get_outbound_type( + def _get_outbound_type( # pylint: disable=too-many-branches self, enable_validation: bool = False, read_only: bool = False, @@ -594,9 +594,17 @@ def _get_outbound_type( CONST_OUTBOUND_TYPE_USER_ASSIGNED_NAT_GATEWAY, ]: if self.get_vnet_subnet_id() in ["", None]: - raise RequiredArgumentMissingError( - "--vnet-subnet-id must be specified for userDefinedRouting and it must " - "be pre-configured with a route table with egress rules" + if self.decorator_mode == DecoratorMode.CREATE: + raise RequiredArgumentMissingError( + "--vnet-subnet-id must be specified for userDefinedRouting and it must " + "be pre-configured with a route table with egress rules" + ) + raise InvalidArgumentValueError( + f"Updating outbound type to {outbound_type} is only supported for " + "clusters using a custom (BYO) virtual network. Managed VNet clusters " + f"cannot be updated to {outbound_type}. Please refer to " + "https://learn.microsoft.com/en-us/azure/aks/egress-outboundtype" + "#updating-outboundtype-after-cluster-creation for supported migration paths." ) if ( diff --git a/src/aks-preview/azext_aks_preview/tests/latest/test_managed_cluster_decorator.py b/src/aks-preview/azext_aks_preview/tests/latest/test_managed_cluster_decorator.py index f84639fab49..05f814fb3af 100644 --- a/src/aks-preview/azext_aks_preview/tests/latest/test_managed_cluster_decorator.py +++ b/src/aks-preview/azext_aks_preview/tests/latest/test_managed_cluster_decorator.py @@ -99,6 +99,8 @@ from azure.cli.command_modules.acs._consts import ( CONST_OUTBOUND_TYPE_LOAD_BALANCER, CONST_OUTBOUND_TYPE_MANAGED_NAT_GATEWAY, + CONST_OUTBOUND_TYPE_USER_DEFINED_ROUTING, + CONST_OUTBOUND_TYPE_USER_ASSIGNED_NAT_GATEWAY, DecoratorEarlyExitException, DecoratorMode, ) @@ -4705,6 +4707,96 @@ def test_get_outbound_type(self): CONST_OUTBOUND_TYPE_MANAGED_NAT_GATEWAY_V2, ) + def test_get_outbound_type_update_udr_byo_vnet(self): + """Test that updating to UDR succeeds when the cluster has a BYO VNet (vnet_subnet_id is set on agentpool).""" + ctx = AKSPreviewManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict({"outbound_type": "userDefinedRouting"}), + self.models, + decorator_mode=DecoratorMode.UPDATE, + ) + self.create_attach_agentpool_context(ctx) + # Simulate a BYO VNet cluster: agentpool has vnet_subnet_id set + agentpool = self.models.ManagedClusterAgentPoolProfile( + name="nodepool1", + vnet_subnet_id="/subscriptions/test/resourceGroups/rg/providers/Microsoft.Network/virtualNetworks/vnet/subnets/subnet", + ) + mc = self.models.ManagedCluster( + location="test_location", + agent_pool_profiles=[agentpool], + network_profile=self.models.ContainerServiceNetworkProfile( + load_balancer_sku="standard", + ), + ) + ctx.attach_mc(mc) + ctx.agentpool_context.attach_agentpool(agentpool) + # Should succeed — BYO VNet cluster can update to UDR + outbound_type = ctx._get_outbound_type(enable_validation=True) + self.assertEqual(outbound_type, CONST_OUTBOUND_TYPE_USER_DEFINED_ROUTING) + + def test_get_outbound_type_update_udr_managed_vnet(self): + """Test that updating to UDR fails with clear error when the cluster uses managed VNet (no vnet_subnet_id).""" + ctx = AKSPreviewManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict({"outbound_type": "userDefinedRouting"}), + self.models, + decorator_mode=DecoratorMode.UPDATE, + ) + self.create_attach_agentpool_context(ctx) + # Simulate a managed VNet cluster: agentpool has no vnet_subnet_id + agentpool = self.models.ManagedClusterAgentPoolProfile( + name="nodepool1", + ) + mc = self.models.ManagedCluster( + location="test_location", + agent_pool_profiles=[agentpool], + network_profile=self.models.ContainerServiceNetworkProfile( + load_balancer_sku="standard", + ), + ) + ctx.attach_mc(mc) + ctx.agentpool_context.attach_agentpool(agentpool) + # Should fail with InvalidArgumentValueError for managed VNet clusters + with self.assertRaises(InvalidArgumentValueError): + ctx._get_outbound_type(enable_validation=True) + + def test_get_outbound_type_update_user_assigned_nat_gw_managed_vnet(self): + """Test that updating to userAssignedNATGateway fails with clear error when using managed VNet.""" + ctx = AKSPreviewManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict({"outbound_type": "userAssignedNATGateway"}), + self.models, + decorator_mode=DecoratorMode.UPDATE, + ) + self.create_attach_agentpool_context(ctx) + agentpool = self.models.ManagedClusterAgentPoolProfile( + name="nodepool1", + ) + mc = self.models.ManagedCluster( + location="test_location", + agent_pool_profiles=[agentpool], + network_profile=self.models.ContainerServiceNetworkProfile( + load_balancer_sku="standard", + ), + ) + ctx.attach_mc(mc) + ctx.agentpool_context.attach_agentpool(agentpool) + with self.assertRaises(InvalidArgumentValueError): + ctx._get_outbound_type(enable_validation=True) + + def test_get_outbound_type_create_udr_no_subnet(self): + """Test that creating with UDR but no vnet_subnet_id raises RequiredArgumentMissingError.""" + ctx = AKSPreviewManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict({"outbound_type": "userDefinedRouting"}), + self.models, + decorator_mode=DecoratorMode.CREATE, + ) + self.create_attach_agentpool_context(ctx) + # Should fail with RequiredArgumentMissingError during create + with self.assertRaises(RequiredArgumentMissingError): + ctx._get_outbound_type(enable_validation=True) + def test_get_enable_gateway_api(self): # default value ctx_1 = AKSPreviewManagedClusterContext(