diff --git a/src/aks-preview/HISTORY.rst b/src/aks-preview/HISTORY.rst index 466e5826678..acf48cd8057 100644 --- a/src/aks-preview/HISTORY.rst +++ b/src/aks-preview/HISTORY.rst @@ -12,6 +12,10 @@ To release a new version, please select a new version number (usually plus 1 to Pending +++++++ +21.0.0b6 +++++++++ +* `az aks create / update`: Add `--enable-control-plane-metrics` and `az aks update`: `--disable-control-plane-metrics` flags to opt clusters into Azure Monitor managed Prometheus control plane metrics (kube-apiserver, etcd, etc.) via the first-class API property `azureMonitorProfile.metrics.controlPlane.enabled` (replaces the AFEC-gated preview). + 21.0.0b5 +++++++ * `az aks prepared-image-specification`: Add new command. diff --git a/src/aks-preview/azext_aks_preview/_help.py b/src/aks-preview/azext_aks_preview/_help.py index 41744109c0e..10d21db102d 100644 --- a/src/aks-preview/azext_aks_preview/_help.py +++ b/src/aks-preview/azext_aks_preview/_help.py @@ -606,6 +606,9 @@ - name: --enable-azure-monitor-metrics type: bool short-summary: Enable Azure Monitor Metrics Profile + - name: --enable-control-plane-metrics --enable-cp-metrics + type: bool + short-summary: Enable collection of Azure Monitor managed Prometheus control plane metrics for managed cluster components (controlplane-apiserver and controlplane-etcd targets by default). Requires Azure Monitor metrics to be enabled (already enabled or via --enable-azure-monitor-metrics). - name: --azure-monitor-workspace-resource-id type: string short-summary: Resource ID of the Azure Monitor Workspace @@ -1332,6 +1335,9 @@ - name: --enable-azure-monitor-metrics type: bool short-summary: Enable Azure Monitor Metrics Profile + - name: --enable-control-plane-metrics --enable-cp-metrics + type: bool + short-summary: Enable collection of Azure Monitor managed Prometheus control plane metrics for managed cluster components (controlplane-apiserver and controlplane-etcd targets by default). Requires Azure Monitor metrics to be enabled (already enabled or via --enable-azure-monitor-metrics). - name: --azure-monitor-workspace-resource-id type: string short-summary: Resource ID of the Azure Monitor Workspace @@ -1353,6 +1359,9 @@ - name: --disable-azure-monitor-metrics type: bool short-summary: Disable Azure Monitor Metrics Profile. This will delete all DCRA's associated with the cluster, any linked DCRs with the data stream = prometheus-stream and the recording rule groups created by the addon for this AKS cluster. + - name: --disable-control-plane-metrics --disable-cp-metrics + type: bool + short-summary: Disable collection of Azure Monitor managed Prometheus control plane metrics. Leaves Azure Monitor metrics enabled. - name: --enable-azure-monitor-app-monitoring type: bool short-summary: Enable Azure Monitor Application Monitoring diff --git a/src/aks-preview/azext_aks_preview/_params.py b/src/aks-preview/azext_aks_preview/_params.py index 8f2d5b7eaa4..6658c5fff74 100644 --- a/src/aks-preview/azext_aks_preview/_params.py +++ b/src/aks-preview/azext_aks_preview/_params.py @@ -1131,6 +1131,17 @@ def load_arguments(self, _): c.argument("ksm_metric_annotations_allow_list") c.argument("grafana_resource_id", validator=validate_grafanaresourceid) c.argument("enable_windows_recording_rules", action="store_true") + c.argument( + "enable_control_plane_metrics", + options_list=["--enable-control-plane-metrics", "--enable-cp-metrics"], + action="store_true", + help=( + "Enable collection of Azure Monitor managed Prometheus control plane metrics for managed " + "cluster components (controlplane-apiserver and controlplane-etcd targets by default). " + "Requires Azure Monitor metrics to be enabled " + "(already enabled or via --enable-azure-monitor-metrics)." + ), + ) c.argument("enable_azure_monitor_app_monitoring", is_preview=True, action="store_true" @@ -1636,6 +1647,27 @@ def load_arguments(self, _): hide=True, ), ) + c.argument("disable_azure_monitor_metrics", action="store_true") + c.argument( + "enable_control_plane_metrics", + options_list=["--enable-control-plane-metrics", "--enable-cp-metrics"], + action="store_true", + help=( + "Enable collection of Azure Monitor managed Prometheus control plane metrics for managed " + "cluster components (controlplane-apiserver and controlplane-etcd targets by default). " + "Requires Azure Monitor metrics to be enabled " + "(already enabled or via --enable-azure-monitor-metrics)." + ), + ) + c.argument( + "disable_control_plane_metrics", + options_list=["--disable-control-plane-metrics", "--disable-cp-metrics"], + action="store_true", + help=( + "Disable collection of Azure Monitor managed Prometheus control plane metrics. " + "Sets azureMonitorProfile.metrics.controlPlane.enabled=false on the cluster." + ), + ) c.argument("enable_azure_monitor_app_monitoring", action="store_true", is_preview=True diff --git a/src/aks-preview/azext_aks_preview/azuremonitormetrics/azuremonitorprofile.py b/src/aks-preview/azext_aks_preview/azuremonitormetrics/azuremonitorprofile.py index 9d7be56cf6d..2cb29cf1a2c 100644 --- a/src/aks-preview/azext_aks_preview/azuremonitormetrics/azuremonitorprofile.py +++ b/src/aks-preview/azext_aks_preview/azuremonitormetrics/azuremonitorprofile.py @@ -18,13 +18,59 @@ check_azuremonitormetrics_profile, rp_registrations ) -from azure.cli.core.azclierror import InvalidArgumentValueError +from azure.cli.command_modules.acs._client_factory import get_container_service_client +from azure.cli.core.azclierror import InvalidArgumentValueError, UnknownError from knack.util import CLIError from knack.log import get_logger logger = get_logger(__name__) +# pylint: disable=line-too-long +def _addon_put_with_control_plane(cmd, cluster_subscription, cluster_resource_group_name, cluster_name): + """Sibling of core ``addon_put`` that ALSO flips ``metrics.controlPlane.enabled=True``. + + Used by the greenfield ``aks create --enable-control-plane-metrics`` path. The + initial cluster PUT intentionally leaves ``control_plane`` unset so the RP does + not schedule the control-plane-metrics collection (CCP) pod before the DCRA is + created in postprocessing. Once the DCRA exists, we issue this PUT so the CCP + pod is scheduled with its DCRA already in place (race-free). + """ + client = get_container_service_client(cmd.cli_ctx, cluster_subscription).managed_clusters + try: + mc = client.get(cluster_resource_group_name, cluster_name) + except CLIError as e: + raise UnknownError(str(e)) from e + # Enable metrics if present and not already enabled (mirrors core addon_put). + if hasattr(mc, "azure_monitor_profile") and mc.azure_monitor_profile: + if hasattr(mc.azure_monitor_profile, "metrics") and mc.azure_monitor_profile.metrics: + if getattr(mc.azure_monitor_profile.metrics, "enabled", None) is False: + mc.azure_monitor_profile.metrics.enabled = True + # Flip control plane now that DCRA exists. + try: + from azure.mgmt.containerservice.models import ( + ManagedClusterAzureMonitorProfileMetricsControlPlane, + ) + mc.azure_monitor_profile.metrics.control_plane = ( + ManagedClusterAzureMonitorProfileMetricsControlPlane(enabled=True) + ) + except ImportError: + # Fallback for SDK versions that don't expose the model directly: + # set a dict that the generated client will serialize as the property. + mc.azure_monitor_profile.metrics.control_plane = {"enabled": True} + # Unlike the sibling ``addon_put`` (where ``metrics.enabled`` is already true on the + # cluster from the initial PUT and the postprocessing PUT is just a safety re-affirm), + # this is the ONLY place where ``controlPlane.enabled`` is set during ``aks create``. + # Wait for the LRO so the CP flip is durably persisted before the create command + # returns; otherwise callers / tests that read the cluster immediately could see the + # pre-flip state. + try: + poller = client.begin_create_or_update(cluster_resource_group_name, cluster_name, mc) + poller.result() + except Exception as e: + raise UnknownError(str(e)) from e + + # pylint: disable=line-too-long def link_azure_monitor_profile_artifacts( cmd, @@ -49,7 +95,15 @@ def link_azure_monitor_profile_artifacts( create_rules(cmd, cluster_subscription, cluster_resource_group_name, cluster_name, azure_monitor_workspace_resource_id, azure_monitor_workspace_location, raw_parameters) # if aks cluster create flow -> do a PUT on the AKS cluster to enable the addon if create_flow: - addon_put(cmd, cluster_subscription, cluster_resource_group_name, cluster_name) + # If --enable-control-plane-metrics was specified on create, flip + # metrics.controlPlane.enabled HERE (after DCRA creation) instead of on + # the initial cluster PUT. This avoids the CCP pod being scheduled before + # its DCRA exists (which would cause CrashLoopBackOff until reconciliation). + enable_cp = bool(raw_parameters and raw_parameters.get("enable_control_plane_metrics")) + if enable_cp: + _addon_put_with_control_plane(cmd, cluster_subscription, cluster_resource_group_name, cluster_name) + else: + addon_put(cmd, cluster_subscription, cluster_resource_group_name, cluster_name) # pylint: disable=line-too-long diff --git a/src/aks-preview/azext_aks_preview/custom.py b/src/aks-preview/azext_aks_preview/custom.py index 92395f939bf..47b797bad7b 100644 --- a/src/aks-preview/azext_aks_preview/custom.py +++ b/src/aks-preview/azext_aks_preview/custom.py @@ -1144,6 +1144,7 @@ def aks_create( ksm_metric_annotations_allow_list=None, grafana_resource_id=None, enable_windows_recording_rules=False, + enable_control_plane_metrics=False, # azure monitor profile - app monitoring enable_azure_monitor_app_monitoring=False, # opentelemetry parameters @@ -1375,6 +1376,8 @@ def aks_update( enable_windows_recording_rules=False, disable_azuremonitormetrics=False, disable_azure_monitor_metrics=False, + enable_control_plane_metrics=False, + disable_control_plane_metrics=False, # azure monitor profile - app monitoring enable_azure_monitor_app_monitoring=False, disable_azure_monitor_app_monitoring=False, 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 795c794596b..e665b2f5b4e 100644 --- a/src/aks-preview/azext_aks_preview/managed_cluster_decorator.py +++ b/src/aks-preview/azext_aks_preview/managed_cluster_decorator.py @@ -2651,6 +2651,109 @@ def get_disable_azure_monitor_metrics(self) -> bool: """ return self._get_disable_azure_monitor_metrics(enable_validation=True) + def validate_control_plane_metrics_params(self) -> None: + """Validate the --enable/--disable-control-plane-metrics flag combo and + its interaction with --enable/--disable-azure-monitor-metrics. + + Raises MutuallyExclusiveArgumentError or RequiredArgumentMissingError on + an invalid combination. Returns nothing — use this when you want to + surface validation errors without consuming a parameter value. + + Reads raw_param directly so the getters can also delegate here from + their enable_validation=True path without recursing. + """ + enable_cp = self.raw_param.get("enable_control_plane_metrics") + disable_cp = self.raw_param.get("disable_control_plane_metrics") + # On create, the property may already be set on the incoming mc object. + if self.decorator_mode == DecoratorMode.CREATE: + if ( + self.mc and + self.mc.azure_monitor_profile and + self.mc.azure_monitor_profile.metrics and + self.mc.azure_monitor_profile.metrics.control_plane + ): + enable_cp = self.mc.azure_monitor_profile.metrics.control_plane.enabled + + if enable_cp and disable_cp: + raise MutuallyExclusiveArgumentError( + "Cannot specify --enable-control-plane-metrics and --disable-control-plane-metrics " + "at the same time." + ) + + if enable_cp: + # Reject combining enable-control-plane-metrics with disable-azure-monitor-metrics + # in the same command — the resulting payload would be inconsistent. + if self._get_disable_azure_monitor_metrics(False): + raise MutuallyExclusiveArgumentError( + "Cannot specify --enable-control-plane-metrics together with " + "--disable-azure-monitor-metrics." + ) + # Must have Azure Monitor metrics enabled (either already or in this command). + already_enabled = ( + self.mc and + self.mc.azure_monitor_profile and + self.mc.azure_monitor_profile.metrics and + self.mc.azure_monitor_profile.metrics.enabled + ) + enabling_now = self._get_enable_azure_monitor_metrics(False) + if not already_enabled and not enabling_now: + raise RequiredArgumentMissingError( + "--enable-control-plane-metrics requires Azure Monitor metrics to be enabled. " + "Specify --enable-azure-monitor-metrics or run on a cluster that already has " + "Azure Monitor metrics enabled." + ) + + def _get_enable_control_plane_metrics(self, enable_validation: bool = False) -> bool: + """Internal function to obtain the value of enable_control_plane_metrics. + When enable_validation is True, the flag combinations are validated via + validate_control_plane_metrics_params before the value is returned. + + :return: bool + """ + # Read the original value passed by the command. + enable_control_plane_metrics = self.raw_param.get("enable_control_plane_metrics") + # In create mode, try to read the property value corresponding to the parameter from the `mc` object. + if self.decorator_mode == DecoratorMode.CREATE: + if ( + self.mc and + self.mc.azure_monitor_profile and + self.mc.azure_monitor_profile.metrics and + self.mc.azure_monitor_profile.metrics.control_plane + ): + enable_control_plane_metrics = self.mc.azure_monitor_profile.metrics.control_plane.enabled + if enable_validation: + self.validate_control_plane_metrics_params() + return bool(enable_control_plane_metrics) + + def get_enable_control_plane_metrics(self) -> bool: + """Obtain the value of enable_control_plane_metrics. + This function will verify the parameter by default. If both enable_control_plane_metrics and + disable_control_plane_metrics are specified, raise a MutuallyExclusiveArgumentError. + :return: bool + """ + return self._get_enable_control_plane_metrics(enable_validation=True) + + def _get_disable_control_plane_metrics(self, enable_validation: bool = False) -> bool: + """Internal function to obtain the value of disable_control_plane_metrics. + When enable_validation is True, the flag combinations are validated via + validate_control_plane_metrics_params before the value is returned. + + :return: bool + """ + # Read the original value passed by the command. + disable_control_plane_metrics = self.raw_param.get("disable_control_plane_metrics") + if enable_validation: + self.validate_control_plane_metrics_params() + return bool(disable_control_plane_metrics) + + def get_disable_control_plane_metrics(self) -> bool: + """Obtain the value of disable_control_plane_metrics. + This function will verify the parameter by default. If both enable_control_plane_metrics and + disable_control_plane_metrics are specified, raise a MutuallyExclusiveArgumentError. + :return: bool + """ + return self._get_disable_control_plane_metrics(enable_validation=True) + def _get_enable_azure_monitor_app_monitoring(self, enable_validation=True) -> bool: """Internal function to obtain the value of enable_azure_monitor_app_monitoring. This function supports the option of enable_validation. When enabled, if both @@ -4693,6 +4796,18 @@ def _setup_azure_monitor_metrics(self, mc: ManagedCluster) -> None: metric_annotations_allow_list=str(ksm_metric_annotations_allow_list) )) mc.azure_monitor_profile.metrics.kube_state_metrics = kube_state_metrics + + # NOTE: control_plane.enabled is intentionally NOT set here on the create flow. + # If we set it on this initial PUT, the RP would schedule the control-plane-metrics + # collection pod (CCP) before the DCRA (Data Collection Rule Association) has been + # created in postprocessing. The CCP would then crash-loop with "DCRA not found" + # until the next reconciliation. + # Instead, we defer the flip to the addon_put step inside + # link_azure_monitor_profile_artifacts (postprocessing_after_mc_created), which + # runs *after* DCRA creation. The validator still runs here to surface flag + # combination errors early. + self.context.validate_control_plane_metrics_params() + self.context.set_intermediate("azuremonitormetrics_addon_enabled", True, overwrite_exists=True) def _setup_azure_monitor_app_monitoring(self, mc: ManagedCluster) -> None: @@ -4812,6 +4927,12 @@ def set_up_azure_monitor_profile(self, mc: ManagedCluster) -> ManagedCluster: """ self._ensure_mc(mc) + # Surface control-plane-metrics flag combination errors even when the + # parent metrics flag was not specified, so users get a clear error + # instead of a silent ignore when they pass --enable-control-plane-metrics + # on its own. + self.context.validate_control_plane_metrics_params() + if self.context.get_enable_azure_monitor_metrics(): self._setup_azure_monitor_metrics(mc) @@ -7006,6 +7127,30 @@ def update_azure_monitor_profile(self, mc: ManagedCluster) -> ManagedCluster: if self.context.get_disable_azure_monitor_metrics(): self._disable_azure_monitor_metrics(mc) + # Handle enable / disable of control plane metrics independently of the parent metrics flag, + # so users can toggle control plane metrics on a cluster that already has metrics enabled. + if self.context.get_enable_control_plane_metrics(): + if mc.azure_monitor_profile is None: + mc.azure_monitor_profile = self.models.ManagedClusterAzureMonitorProfile() # pylint: disable=no-member + if mc.azure_monitor_profile.metrics is None: + # Should not normally happen — validation requires metrics to be enabled — but guard + # against partially-populated profiles to avoid AttributeError. + mc.azure_monitor_profile.metrics = ( + self.models.ManagedClusterAzureMonitorProfileMetrics(enabled=True) # pylint: disable=no-member + ) + mc.azure_monitor_profile.metrics.control_plane = ( + self.models.ManagedClusterAzureMonitorProfileMetricsControlPlane(enabled=True) # pylint: disable=no-member + ) + + if self.context.get_disable_control_plane_metrics(): + if ( + mc.azure_monitor_profile and + mc.azure_monitor_profile.metrics + ): + mc.azure_monitor_profile.metrics.control_plane = ( + self.models.ManagedClusterAzureMonitorProfileMetricsControlPlane(enabled=False) # pylint: disable=no-member + ) + if self.context.get_enable_azure_monitor_app_monitoring(): if mc.azure_monitor_profile is None: mc.azure_monitor_profile = self.models.ManagedClusterAzureMonitorProfile() 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 4859d9338f0..eaafad8c988 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 @@ -15690,6 +15690,211 @@ def test_aks_update_with_azuremonitormetrics( ], ) + @live_only() + @AllowLargeResponse() + @AKSCustomResourceGroupPreparer( + random_name_length=17, name_prefix="clitest", location="westus2" + ) + def test_aks_create_with_control_plane_metrics( + self, resource_group, resource_group_location + ): + # reset the count so in replay mode the random names will start with 0 + self.test_resources_count = 0 + aks_name = self.create_random_name("cliakstest", 16) + 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, + } + ) + + # create: --enable-azure-monitor-metrics + --enable-control-plane-metrics + create_cmd = ( + "aks create --resource-group={resource_group} --name={name} --location={location} " + "--ssh-key-value={ssh_key_value} --node-vm-size={node_vm_size} --enable-managed-identity " + "--enable-azure-monitor-metrics --enable-control-plane-metrics --output=json" + ) + # NOTE: ``--enable-control-plane-metrics`` on create is intentionally deferred to a + # postprocessing PUT (after DCRA creation) to avoid scheduling the CCP pod before its + # DCRA exists. The create response may therefore reflect the pre-flip state; assert + # the final state via ``aks show`` after the cluster settles. + self.cmd( + create_cmd, + checks=[ + self.check("provisioningState", "Succeeded"), + self.check("azureMonitorProfile.metrics.enabled", True), + ], + ) + + wait_cmd = ( + "aks wait --resource-group={resource_group} --name={name} --created " + "--interval 60 --timeout 1800" + ) + self.cmd(wait_cmd, checks=[self.is_empty()]) + + # Verify the deferred control-plane-metrics flip landed on the cluster. + self.cmd( + "aks show --resource-group={resource_group} --name={name} --output=json", + checks=[ + self.check("azureMonitorProfile.metrics.enabled", True), + self.check("azureMonitorProfile.metrics.controlPlane.enabled", True), + ], + ) + + # delete + self.cmd( + "aks delete --resource-group={resource_group} --name={name} --yes --no-wait", + checks=[self.is_empty()], + ) + + @live_only() + @AllowLargeResponse() + @AKSCustomResourceGroupPreparer( + random_name_length=17, name_prefix="clitest", location="westus2" + ) + def test_aks_update_with_control_plane_metrics( + self, resource_group, resource_group_location + ): + aks_name = self.create_random_name("cliakstest", 16) + 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, + } + ) + + # create: with azure monitor metrics but without control plane metrics + create_cmd = ( + "aks create --resource-group={resource_group} --name={name} --location={location} " + "--ssh-key-value={ssh_key_value} --node-vm-size={node_vm_size} --enable-managed-identity " + "--enable-azure-monitor-metrics --output=json" + ) + self.cmd( + create_cmd, + checks=[ + self.check("provisioningState", "Succeeded"), + self.check("azureMonitorProfile.metrics.enabled", True), + ], + ) + + # wait for AMW background setup to complete before issuing update + wait_cmd = "aks wait --resource-group={resource_group} --name={name} --updated --timeout=1800" + self.cmd(wait_cmd, checks=[self.is_empty()]) + + # update: enable-control-plane-metrics on a cluster that already has AM metrics + update_cmd = ( + "aks update --resource-group={resource_group} --name={name} --yes --output=json " + "--enable-control-plane-metrics" + ) + self.cmd( + update_cmd, + checks=[ + self.check("provisioningState", "Succeeded"), + self.check("azureMonitorProfile.metrics.controlPlane.enabled", True), + ], + ) + + wait_cmd = "aks wait --resource-group={resource_group} --name={name} --updated --timeout=1800" + self.cmd(wait_cmd, checks=[self.is_empty()]) + + # update: disable-control-plane-metrics + update_cmd = ( + "aks update --resource-group={resource_group} --name={name} --yes --output=json " + "--disable-control-plane-metrics" + ) + self.cmd( + update_cmd, + checks=[ + self.check("provisioningState", "Succeeded"), + self.check("azureMonitorProfile.metrics.controlPlane.enabled", False), + ], + ) + + self.cmd(wait_cmd, checks=[self.is_empty()]) + + # delete + self.cmd( + "aks delete --resource-group={resource_group} --name={name} --yes --no-wait", + checks=[self.is_empty()], + ) + + @live_only() + @AllowLargeResponse() + @AKSCustomResourceGroupPreparer( + random_name_length=17, name_prefix="clitest", location="westus2" + ) + def test_aks_control_plane_metrics_negative( + self, resource_group, resource_group_location + ): + aks_name = self.create_random_name("cliakstest", 16) + 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, + } + ) + + # negative: --enable-control-plane-metrics without --enable-azure-monitor-metrics on create + create_missing_parent = ( + "aks create --resource-group={resource_group} --name={name} --location={location} " + "--ssh-key-value={ssh_key_value} --node-vm-size={node_vm_size} --enable-managed-identity " + "--enable-control-plane-metrics --output=json" + ) + self.cmd(create_missing_parent, expect_failure=True) + + # create a baseline cluster (no AM metrics) so we can exercise the update-time negatives + create_cmd = ( + "aks create --resource-group={resource_group} --name={name} --location={location} " + "--ssh-key-value={ssh_key_value} --node-vm-size={node_vm_size} --enable-managed-identity " + "--output=json" + ) + self.cmd( + create_cmd, + checks=[ + self.check("provisioningState", "Succeeded"), + self.not_exists("azureMonitorProfile.metrics"), + ], + ) + + # negative: update --enable-control-plane-metrics on a cluster without AM metrics enabled + update_missing_parent = ( + "aks update --resource-group={resource_group} --name={name} --yes --output=json " + "--enable-control-plane-metrics" + ) + self.cmd(update_missing_parent, expect_failure=True) + + # negative: --enable-control-plane-metrics with --disable-azure-monitor-metrics on update + update_conflicting = ( + "aks update --resource-group={resource_group} --name={name} --yes --output=json " + "--enable-azure-monitor-metrics --enable-control-plane-metrics --disable-azure-monitor-metrics" + ) + self.cmd(update_conflicting, expect_failure=True) + + # negative: both --enable-control-plane-metrics and --disable-control-plane-metrics on update + update_both = ( + "aks update --resource-group={resource_group} --name={name} --yes --output=json " + "--enable-azure-monitor-metrics --enable-control-plane-metrics --disable-control-plane-metrics" + ) + self.cmd(update_both, expect_failure=True) + + # delete + self.cmd( + "aks delete --resource-group={resource_group} --name={name} --yes --no-wait", + checks=[self.is_empty()], + ) + @AllowLargeResponse(8192) @AKSCustomResourceGroupPreparer(random_name_length=17, name_prefix='clitest', location='westus2') def test_aks_update_with_azuremonitorappmonitoring(self, resource_group, resource_group_location): 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 6bebf48ee3d..aaed9a88cd8 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 @@ -6748,6 +6748,57 @@ def test_set_up_azure_monitor_profile(self): ) self.assertEqual(dec_mc_1, ground_truth_mc_1) + def test_set_up_azure_monitor_profile_defers_control_plane_on_create(self): + # Greenfield --enable-control-plane-metrics must NOT set control_plane.enabled + # on the initial cluster PUT. It is deferred to the addon_put step in + # postprocessing (after DCRA creation) so the CCP collector pod is only + # scheduled once its DCRA exists. + dec = AKSPreviewManagedClusterCreateDecorator( + self.cmd, + self.client, + { + "enable_azure_monitor_metrics": True, + "enable_control_plane_metrics": True, + }, + CUSTOM_MGMT_AKS_PREVIEW, + ) + mc = self.models.ManagedCluster( + location="test_location", + identity=self.models.ManagedClusterIdentity(type="SystemAssigned"), + ) + dec.context.attach_mc(mc) + dec_mc = dec.set_up_azure_monitor_profile(mc) + + # Parent AMP metrics is enabled on the initial PUT... + self.assertIsNotNone(dec_mc.azure_monitor_profile) + self.assertIsNotNone(dec_mc.azure_monitor_profile.metrics) + self.assertTrue(dec_mc.azure_monitor_profile.metrics.enabled) + # ...but control_plane is deferred and must be None here. + self.assertIsNone(dec_mc.azure_monitor_profile.metrics.control_plane) + # Intermediate flag still set so postprocessing runs the prereqs/addon_put. + self.assertTrue(dec.context.get_intermediate("azuremonitormetrics_addon_enabled")) + + def test_set_up_azure_monitor_profile_create_cp_without_amp_raises(self): + # --enable-control-plane-metrics without --enable-azure-monitor-metrics on create + # must fail validation early. + from azure.cli.core.azclierror import RequiredArgumentMissingError + + dec = AKSPreviewManagedClusterCreateDecorator( + self.cmd, + self.client, + { + "enable_control_plane_metrics": True, + }, + CUSTOM_MGMT_AKS_PREVIEW, + ) + mc = self.models.ManagedCluster( + location="test_location", + identity=self.models.ManagedClusterIdentity(type="SystemAssigned"), + ) + dec.context.attach_mc(mc) + with self.assertRaises(RequiredArgumentMissingError): + dec.set_up_azure_monitor_profile(mc) + def test_set_up_image_cleaner(self): dec_0 = AKSPreviewManagedClusterCreateDecorator( self.cmd, @@ -12346,6 +12397,152 @@ def test_update_disable_azure_monitor_metrics(self): dec_mc_3.azure_monitor_profile.app_monitoring.open_telemetry_metrics ) + def test_update_enable_control_plane_metrics_requires_parent_metrics(self): + # Update path: --enable-control-plane-metrics on a cluster that has neither + # Azure Monitor metrics already enabled nor --enable-azure-monitor-metrics in + # the same command must raise RequiredArgumentMissingError. + dec = AKSPreviewManagedClusterUpdateDecorator( + self.cmd, + self.client, + { + "enable_control_plane_metrics": True, + }, + CUSTOM_MGMT_AKS_PREVIEW, + ) + mc = self.models.ManagedCluster( + location="test_location", + identity=self.models.ManagedClusterIdentity(type="SystemAssigned"), + ) + dec.context.attach_mc(mc) + + with self.assertRaises(RequiredArgumentMissingError): + dec.context.get_enable_control_plane_metrics() + + def test_update_enable_control_plane_metrics_already_enabled_cluster_succeeds(self): + # Update path: --enable-control-plane-metrics on a cluster that already has + # Azure Monitor metrics enabled should succeed without requiring + # --enable-azure-monitor-metrics. + dec = AKSPreviewManagedClusterUpdateDecorator( + self.cmd, + self.client, + { + "enable_control_plane_metrics": True, + }, + CUSTOM_MGMT_AKS_PREVIEW, + ) + mc = self.models.ManagedCluster( + location="test_location", + identity=self.models.ManagedClusterIdentity(type="SystemAssigned"), + azure_monitor_profile=self.models.ManagedClusterAzureMonitorProfile( + metrics=self.models.ManagedClusterAzureMonitorProfileMetrics(enabled=True) + ), + ) + dec.context.attach_mc(mc) + + self.assertTrue(dec.context.get_enable_control_plane_metrics()) + + with patch( + "azext_aks_preview.managed_cluster_decorator.ensure_azure_monitor_profile_prerequisites" + ), patch.object( + dec.context, "get_subscription_id", return_value="test-subscription-id" + ), patch.object( + dec.context, "get_resource_group_name", return_value="test-rg" + ), patch.object( + dec.context, "get_name", return_value="test-cluster" + ): + dec_mc = dec.update_azure_monitor_profile(mc) + + self.assertIsNotNone(dec_mc.azure_monitor_profile.metrics.control_plane) + self.assertTrue(dec_mc.azure_monitor_profile.metrics.control_plane.enabled) + + def test_update_enable_control_plane_metrics_with_disable_metrics_raises(self): + # Update path: --enable-control-plane-metrics combined with + # --disable-azure-monitor-metrics in the same command must be rejected to + # avoid producing an inconsistent payload. + dec = AKSPreviewManagedClusterUpdateDecorator( + self.cmd, + self.client, + { + "enable_control_plane_metrics": True, + "disable_azure_monitor_metrics": True, + }, + CUSTOM_MGMT_AKS_PREVIEW, + ) + mc = self.models.ManagedCluster( + location="test_location", + identity=self.models.ManagedClusterIdentity(type="SystemAssigned"), + azure_monitor_profile=self.models.ManagedClusterAzureMonitorProfile( + metrics=self.models.ManagedClusterAzureMonitorProfileMetrics(enabled=True) + ), + ) + dec.context.attach_mc(mc) + + with self.assertRaises(MutuallyExclusiveArgumentError): + dec.context.get_enable_control_plane_metrics() + + def test_update_enable_control_plane_metrics_with_disable_control_plane_raises(self): + # --enable-control-plane-metrics together with --disable-control-plane-metrics + # in the same command must raise MutuallyExclusiveArgumentError. + dec = AKSPreviewManagedClusterUpdateDecorator( + self.cmd, + self.client, + { + "enable_control_plane_metrics": True, + "disable_control_plane_metrics": True, + }, + CUSTOM_MGMT_AKS_PREVIEW, + ) + mc = self.models.ManagedCluster( + location="test_location", + identity=self.models.ManagedClusterIdentity(type="SystemAssigned"), + azure_monitor_profile=self.models.ManagedClusterAzureMonitorProfile( + metrics=self.models.ManagedClusterAzureMonitorProfileMetrics(enabled=True) + ), + ) + dec.context.attach_mc(mc) + + with self.assertRaises(MutuallyExclusiveArgumentError): + dec.context.get_enable_control_plane_metrics() + + def test_update_disable_control_plane_metrics_sets_enabled_false(self): + # --disable-control-plane-metrics on a cluster that has it enabled should + # produce a payload with control_plane.enabled=False. + dec = AKSPreviewManagedClusterUpdateDecorator( + self.cmd, + self.client, + { + "disable_control_plane_metrics": True, + }, + CUSTOM_MGMT_AKS_PREVIEW, + ) + mc = self.models.ManagedCluster( + location="test_location", + identity=self.models.ManagedClusterIdentity(type="SystemAssigned"), + azure_monitor_profile=self.models.ManagedClusterAzureMonitorProfile( + metrics=self.models.ManagedClusterAzureMonitorProfileMetrics( + enabled=True, + control_plane=self.models.ManagedClusterAzureMonitorProfileMetricsControlPlane( + enabled=True + ), + ) + ), + ) + dec.context.attach_mc(mc) + + with patch( + "azext_aks_preview.managed_cluster_decorator.ensure_azure_monitor_profile_prerequisites" + ), patch.object( + dec.context, "get_subscription_id", return_value="test-subscription-id" + ), patch.object( + dec.context, "get_resource_group_name", return_value="test-rg" + ), patch.object( + dec.context, "get_name", return_value="test-cluster" + ): + dec_mc = dec.update_azure_monitor_profile(mc) + + self.assertIsNotNone(dec_mc.azure_monitor_profile.metrics.control_plane) + self.assertFalse(dec_mc.azure_monitor_profile.metrics.control_plane.enabled) + def test_setup_azure_monitor_logs_with_omsagent_camelcase(self): # Test that _setup_azure_monitor_logs handles existing omsAgent (camelCase) correctly # This simulates what Azure API returns diff --git a/src/aks-preview/setup.py b/src/aks-preview/setup.py index e90b603c1fa..0229e9ebd47 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.0b5" +VERSION = "21.0.0b6" CLASSIFIERS = [ "Development Status :: 4 - Beta",