diff --git a/src/aks-preview/HISTORY.rst b/src/aks-preview/HISTORY.rst index 92bce607137..3ceb447f0e2 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.0b2 +++++++ +* `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 in API version `2026-02-02-preview` (replaces the AFEC-gated preview). + 21.0.0b1 ++++++ * [BREAKING CHANGE] `az aks create/update`: Remove `--disk-driver-version` option as the `version` field has been removed from the API spec for `ManagedClusterStorageProfileDiskCSIDriver`. diff --git a/src/aks-preview/azext_aks_preview/_help.py b/src/aks-preview/azext_aks_preview/_help.py index a06b6051ae8..5ffa3ff4418 100644 --- a/src/aks-preview/azext_aks_preview/_help.py +++ b/src/aks-preview/azext_aks_preview/_help.py @@ -580,6 +580,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 (kube-apiserver, etcd, etc). Requires Azure Monitor metrics to be enabled (already enabled or via --enable-azure-monitor-metrics). See aka.ms/aks/controlplane-metrics. - name: --azure-monitor-workspace-resource-id type: string short-summary: Resource ID of the Azure Monitor Workspace @@ -1285,6 +1288,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 (kube-apiserver, etcd, etc). Requires Azure Monitor metrics to be enabled (already enabled or via --enable-azure-monitor-metrics). See aka.ms/aks/controlplane-metrics. - name: --azure-monitor-workspace-resource-id type: string short-summary: Resource ID of the Azure Monitor Workspace @@ -1306,6 +1312,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. See aka.ms/aks/controlplane-metrics. - 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 f3dfa8af56b..03252fe6e81 100644 --- a/src/aks-preview/azext_aks_preview/_params.py +++ b/src/aks-preview/azext_aks_preview/_params.py @@ -1102,6 +1102,16 @@ 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 (kube-apiserver, etcd, etc). Requires Azure Monitor metrics to be enabled " + "(already enabled or via --enable-azure-monitor-metrics). See aka.ms/aks/controlplane-metrics." + ), + ) c.argument("enable_azure_monitor_app_monitoring", is_preview=True, action="store_true" @@ -1596,6 +1606,25 @@ def load_arguments(self, _): hide=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 (kube-apiserver, etcd, etc). Requires Azure Monitor metrics to be enabled " + "(already enabled or via --enable-azure-monitor-metrics). See aka.ms/aks/controlplane-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/custom.py b/src/aks-preview/azext_aks_preview/custom.py index d73fd9b8b71..507d6c5a236 100644 --- a/src/aks-preview/azext_aks_preview/custom.py +++ b/src/aks-preview/azext_aks_preview/custom.py @@ -1136,6 +1136,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 @@ -1361,6 +1362,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 60f22700aaa..3d287826171 100644 --- a/src/aks-preview/azext_aks_preview/managed_cluster_decorator.py +++ b/src/aks-preview/azext_aks_preview/managed_cluster_decorator.py @@ -2596,6 +2596,91 @@ def get_disable_azure_monitor_metrics(self) -> bool: """ return self._get_disable_azure_monitor_metrics(enable_validation=True) + def _get_enable_control_plane_metrics(self, enable_validation: bool = False) -> bool: + """Internal function to obtain the value of enable_control_plane_metrics. + This function supports the option of enable_validation. When enabled, if both + enable_control_plane_metrics and disable_control_plane_metrics are specified, raise a + MutuallyExclusiveArgumentError. Additionally, --enable-control-plane-metrics requires Azure + Monitor metrics to either already be enabled on the cluster or to be enabled in the same + command via --enable-azure-monitor-metrics. + + :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 + # This parameter does not need dynamic completion. + if enable_validation: + if enable_control_plane_metrics and self._get_disable_control_plane_metrics(False): + raise MutuallyExclusiveArgumentError( + "Cannot specify --enable-control-plane-metrics and --disable-control-plane-metrics " + "at the same time." + ) + if enable_control_plane_metrics: + # 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." + ) + return 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. + This function supports the option of enable_validation. When enabled, if both + enable_control_plane_metrics and disable_control_plane_metrics are specified, raise a + MutuallyExclusiveArgumentError. + :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: + if disable_control_plane_metrics and self._get_enable_control_plane_metrics(False): + raise MutuallyExclusiveArgumentError( + "Cannot specify --enable-control-plane-metrics and --disable-control-plane-metrics " + "at the same time." + ) + return 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 @@ -4622,6 +4707,13 @@ 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 + + # Enable control plane metrics if requested. + if self.context.get_enable_control_plane_metrics(): + mc.azure_monitor_profile.metrics.control_plane = ( + self.models.ManagedClusterAzureMonitorProfileMetricsControlPlane(enabled=True) + ) + self.context.set_intermediate("azuremonitormetrics_addon_enabled", True, overwrite_exists=True) def _setup_azure_monitor_app_monitoring(self, mc: ManagedCluster) -> None: @@ -4741,6 +4833,11 @@ def set_up_azure_monitor_profile(self, mc: ManagedCluster) -> ManagedCluster: """ self._ensure_mc(mc) + # Trigger control-plane-metrics validation even if the parent metrics flag was + # not specified, so users get a clear error instead of silent ignore when they + # pass --enable-control-plane-metrics on its own. + self.context.get_enable_control_plane_metrics() + if self.context.get_enable_azure_monitor_metrics(): self._setup_azure_monitor_metrics(mc) @@ -6908,6 +7005,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_managed_cluster_decorator.py b/src/aks-preview/azext_aks_preview/tests/latest/test_managed_cluster_decorator.py index fcf4fff548c..ffb4316887e 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 @@ -12200,6 +12200,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 c11d2f3317a..7bb3308c98d 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.0b1" +VERSION = "21.0.0b2" CLASSIFIERS = [ "Development Status :: 4 - Beta",