From be7bd910782d6d6dcf0317dbfbe78206b8ea8abb Mon Sep 17 00:00:00 2001 From: Todd Wolff Date: Mon, 6 Jul 2026 15:02:54 -0700 Subject: [PATCH] feat(hypershift-operator): add ACR pull identity configured metric Add hypershift_cluster_acr_pull_identity_configured gauge metric that indicates whether a HostedCluster has an ACR pull managed identity configured. Emitted only for Azure platform clusters; value is 1 when a ManagedIdentity ResourceID is set, 0 otherwise. Signed-off-by: Todd Wolff Commit-Message-Assisted-by: Claude (via Claude Code) --- .../hostedcluster/metrics/metrics.go | 24 +++++ .../hostedcluster/metrics/metrics_test.go | 98 +++++++++++++++++++ 2 files changed, 122 insertions(+) diff --git a/hypershift-operator/controllers/hostedcluster/metrics/metrics.go b/hypershift-operator/controllers/hostedcluster/metrics/metrics.go index 898d55fa4236..97b897436709 100644 --- a/hypershift-operator/controllers/hostedcluster/metrics/metrics.go +++ b/hypershift-operator/controllers/hostedcluster/metrics/metrics.go @@ -96,6 +96,9 @@ const ( HostedClusterAzureInfoMetricName = "hosted_cluster_azure_info" HostedClusterAzureInfoMetricHelp = "Reports Azure information about the given HostedCluster" + + AcrPullIdentityConfiguredMetricName = "hypershift_cluster_acr_pull_identity_configured" + acrPullIdentityConfiguredMetricHelp = "Indicates whether a HostedCluster has an ACR pull managed identity configured (1=configured, 0=not configured). Only emitted for Azure platform clusters." ) // semantically constant - not supposed to be changed at runtime @@ -205,6 +208,10 @@ var ( "location", "microsoft_subscription_id", "microsoft_resource_group_name"), nil) + + acrPullIdentityConfiguredMetricDesc = prometheus.NewDesc( + AcrPullIdentityConfiguredMetricName, acrPullIdentityConfiguredMetricHelp, + hclusterLabels, nil) ) type hostedClustersMetricsCollector struct { @@ -369,6 +376,7 @@ func (c *hostedClustersMetricsCollector) collectPerClusterMetrics(ch chan<- prom c.collectProxyMetrics(ch, hcluster, hclusterLabelValues) collectRosaMetrics(ch, hcluster, hclusterLabelValues) collectAzureInfoMetrics(ch, hcluster, hclusterLabelValues) + collectAcrPullIdentityMetric(ch, hcluster, hclusterLabelValues) collectAwsCredsMetric(ch, hcluster, hclusterLabelValues) collectDeletingMetrics(ch, c.clock, hcluster, hclusterLabelValues) } @@ -563,6 +571,22 @@ func collectAzureInfoMetrics(ch chan<- prometheus.Metric, hcluster *hyperv1.Host } } +func collectAcrPullIdentityMetric(ch chan<- prometheus.Metric, hcluster *hyperv1.HostedCluster, hclusterLabelValues []string) { + if hcluster.Spec.Platform.Azure == nil { + return + } + var value float64 + if hcluster.Spec.Platform.Azure.ContainerRegistry.Credentials.Type != "" { + value = 1 + } + ch <- prometheus.MustNewConstMetric( + acrPullIdentityConfiguredMetricDesc, + prometheus.GaugeValue, + value, + hclusterLabelValues..., + ) +} + // Use detailed credential status: 0=valid, 1=invalid, 2=unknown func collectAwsCredsMetric(ch chan<- prometheus.Metric, hcluster *hyperv1.HostedCluster, hclusterLabelValues []string) { credStatus := platformaws.GetCredentialStatus(hcluster) diff --git a/hypershift-operator/controllers/hostedcluster/metrics/metrics_test.go b/hypershift-operator/controllers/hostedcluster/metrics/metrics_test.go index 34fce2ccbfa8..f6ee01445419 100644 --- a/hypershift-operator/controllers/hostedcluster/metrics/metrics_test.go +++ b/hypershift-operator/controllers/hostedcluster/metrics/metrics_test.go @@ -1280,6 +1280,104 @@ func TestHostedClusterAzureInfo(t *testing.T) { } } +func TestAcrPullIdentityConfigured(t *testing.T) { + const ( + zero float64 = 0 + one float64 = 1 + ) + testCases := []struct { + name string + platformType hyperv1.PlatformType + azureSpec *hyperv1.AzurePlatformSpec + expectedMetricName string + expected *dto.MetricFamily + }{ + { + name: "When platform is not Azure it should not emit a metric", + platformType: hyperv1.AWSPlatform, + expectedMetricName: AcrPullIdentityConfiguredMetricName, + }, + { + name: "When Azure has no containerRegistry configured it should emit 0", + platformType: hyperv1.AzurePlatform, + azureSpec: &hyperv1.AzurePlatformSpec{ + Cloud: "AzureCloud", + Location: "eastus", + ResourceGroupName: "myRG", + SubscriptionID: "mySub", + }, + expectedMetricName: AcrPullIdentityConfiguredMetricName, + expected: &dto.MetricFamily{ + Name: ptr.To(AcrPullIdentityConfiguredMetricName), + Help: ptr.To(acrPullIdentityConfiguredMetricHelp), + Type: func() *dto.MetricType { v := dto.MetricType(1); return &v }(), + Metric: []*dto.Metric{{ + Label: []*dto.LabelPair{ + {Name: ptr.To("_id"), Value: ptr.To("this-is-the-clusterID")}, + {Name: ptr.To("name"), Value: ptr.To("hc-name")}, + {Name: ptr.To("namespace"), Value: ptr.To("hc-ns")}, + }, + Gauge: &dto.Gauge{Value: ptr.To(zero)}, + }}, + }, + }, + { + name: "When Azure has a containerRegistry configured it should emit 1", + platformType: hyperv1.AzurePlatform, + azureSpec: &hyperv1.AzurePlatformSpec{ + Cloud: "AzureCloud", + Location: "eastus", + ResourceGroupName: "myRG", + SubscriptionID: "mySub", + ContainerRegistry: hyperv1.AzureContainerRegistryConfig{ + Credentials: hyperv1.AzureContainerRegistryCredentialConfig{ + Type: hyperv1.AzureContainerRegistryCredentialManagedIdentity, + ManagedIdentity: hyperv1.UserAssignedManagedIdentity{ + ResourceID: "/subscriptions/mySub/resourceGroups/myRG/providers/Microsoft.ManagedIdentity/userAssignedIdentities/acr-pull-mi", + }, + }, + }, + }, + expectedMetricName: AcrPullIdentityConfiguredMetricName, + expected: &dto.MetricFamily{ + Name: ptr.To(AcrPullIdentityConfiguredMetricName), + Help: ptr.To(acrPullIdentityConfiguredMetricHelp), + Type: func() *dto.MetricType { v := dto.MetricType(1); return &v }(), + Metric: []*dto.Metric{{ + Label: []*dto.LabelPair{ + {Name: ptr.To("_id"), Value: ptr.To("this-is-the-clusterID")}, + {Name: ptr.To("name"), Value: ptr.To("hc-name")}, + {Name: ptr.To("namespace"), Value: ptr.To("hc-ns")}, + }, + Gauge: &dto.Gauge{Value: ptr.To(one)}, + }}, + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + hostedCluster := &hyperv1.HostedCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "hc-name", + Namespace: "hc-ns", + }, + Spec: hyperv1.HostedClusterSpec{ + ClusterID: "this-is-the-clusterID", + Platform: hyperv1.PlatformSpec{ + Type: tc.platformType, + Azure: tc.azureSpec, + }, + }, + } + checkMetric(t, + fake.NewClientBuilder().WithScheme(api.Scheme).WithObjects(hostedCluster).Build(), + clocktesting.NewFakeClock(now), + tc.expectedMetricName, + tc.expected) + }) + } +} + func TestReportTransitionDurationForAWSEndpointConditions(t *testing.T) { testCases := []struct { name string