From 1c8fcdfc0beb381ae13448d3affc2bb4e5ab4516 Mon Sep 17 00:00:00 2001 From: Soumya Date: Wed, 13 Aug 2025 02:37:01 +0530 Subject: [PATCH 1/6] Add pod affinity/anti-affinity metrics - Add kube_deployment_spec_pod_affinity_required_rules metric - Add kube_deployment_spec_pod_affinity_preferred_rules metric - Add kube_deployment_spec_pod_anti_affinity_required_rules metric - Add kube_deployment_spec_pod_anti_affinity_preferred_rules metric - Update deployment metrics documentation - Add comprehensive test coverage for all scenarios --- docs/metrics/workload/deployment-metrics.md | 8 +- internal/store/deployment.go | 80 ++++++++ internal/store/deployment_test.go | 198 ++++++++++++++++---- 3 files changed, 249 insertions(+), 37 deletions(-) diff --git a/docs/metrics/workload/deployment-metrics.md b/docs/metrics/workload/deployment-metrics.md index 21fb9fbc3..f4ed289d0 100644 --- a/docs/metrics/workload/deployment-metrics.md +++ b/docs/metrics/workload/deployment-metrics.md @@ -12,10 +12,14 @@ | kube_deployment_status_condition | Gauge | The current status conditions of a deployment. | `deployment`=<deployment-name>
`namespace`=<deployment-namespace>
`reason`=<deployment-transition-reason>
`condition`=<deployment-condition>
`status`=<true\|false\|unknown> | STABLE | | kube_deployment_spec_replicas | Gauge | Number of desired pods for a deployment. | `deployment`=<deployment-name>
`namespace`=<deployment-namespace> | STABLE | | kube_deployment_spec_paused | Gauge | Whether the deployment is paused and will not be processed by the deployment controller. | `deployment`=<deployment-name>
`namespace`=<deployment-namespace> | STABLE | +| kube_deployment_spec_pod_affinity_preferred_rules | Gauge | Number of preferred pod affinity rules in the deployment's pod template. | `deployment`=<deployment-name>
`namespace`=<deployment-namespace> | ALPHA | +| kube_deployment_spec_pod_affinity_required_rules | Gauge | Number of required pod affinity rules in the deployment's pod template. | `deployment`=<deployment-name>
`namespace`=<deployment-namespace> | ALPHA | +| kube_deployment_spec_pod_anti_affinity_preferred_rules | Gauge | Number of preferred pod anti-affinity rules in the deployment's pod template. | `deployment`=<deployment-name>
`namespace`=<deployment-namespace> | ALPHA | +| kube_deployment_spec_pod_anti_affinity_required_rules | Gauge | Number of required pod anti-affinity rules in the deployment's pod template. | `deployment`=<deployment-name>
`namespace`=<deployment-namespace> | ALPHA | | kube_deployment_spec_strategy_rollingupdate_max_unavailable | Gauge | Maximum number of unavailable replicas during a rolling update of a deployment. | `deployment`=<deployment-name>
`namespace`=<deployment-namespace> | STABLE | | kube_deployment_spec_strategy_rollingupdate_max_surge | Gauge | Maximum number of replicas that can be scheduled above the desired number of replicas during a rolling update of a deployment. | `deployment`=<deployment-name>
`namespace`=<deployment-namespace> | STABLE | | kube_deployment_metadata_generation | Gauge | Sequence number representing a specific generation of the desired state. | `deployment`=<deployment-name>
`namespace`=<deployment-namespace> | STABLE | | kube_deployment_labels | Gauge | Kubernetes labels converted to Prometheus labels controlled via [--metric-labels-allowlist](../../developer/cli-arguments.md) | `deployment`=<deployment-name>
`namespace`=<deployment-namespace>
`label_DEPLOYMENT_LABEL`=<DEPLOYMENT_LABEL> | STABLE | | kube_deployment_created | Gauge | Unix creation timestamp | `deployment`=<deployment-name>
`namespace`=<deployment-namespace> | STABLE | -| kube_deployment_deletion_timestamp | Gauge | Unix deletion timestamp | `deployment`=<deployment-name>
`namespace`=<deployment-namespace> | EXPIREMENTAL | -| kube_deployment_owner | Gauge | Information about the Deployment's owner. | `deployment`=<deployment-name>
`namespace`=<deployment-namespace>
`owner_kind`=<owner-kind>
`owner_name`=<owner-name> | ALPHA | +| kube_deployment_deletion_timestamp | Gauge | Unix deletion timestamp | `deployment`=<deployment-name>
`namespace`=<deployment-namespace> | EXPERIMENTAL | +| kube_deployment_owner | Gauge | Information about the Deployment's owner. | `deployment`=<deployment-name>
`namespace`=<deployment-namespace>
`owner_kind`=<owner-kind>
`owner_name`=<owner-name> | ALPHA | diff --git a/internal/store/deployment.go b/internal/store/deployment.go index b3ab3749b..22c350bc5 100644 --- a/internal/store/deployment.go +++ b/internal/store/deployment.go @@ -326,6 +326,86 @@ func deploymentMetricFamilies(allowAnnotationsList, allowLabelsList []string) [] } }), ), + *generator.NewFamilyGeneratorWithStability( + "kube_deployment_spec_pod_affinity_required_rules", + "Number of required pod affinity rules in the deployment's pod template.", + metric.Gauge, + basemetrics.ALPHA, + "", + wrapDeploymentFunc(func(d *v1.Deployment) *metric.Family { + count := 0 + if d.Spec.Template.Spec.Affinity != nil && d.Spec.Template.Spec.Affinity.PodAffinity != nil { + count = len(d.Spec.Template.Spec.Affinity.PodAffinity.RequiredDuringSchedulingIgnoredDuringExecution) + } + return &metric.Family{ + Metrics: []*metric.Metric{ + { + Value: float64(count), + }, + }, + } + }), + ), + *generator.NewFamilyGeneratorWithStability( + "kube_deployment_spec_pod_affinity_preferred_rules", + "Number of preferred pod affinity rules in the deployment's pod template.", + metric.Gauge, + basemetrics.ALPHA, + "", + wrapDeploymentFunc(func(d *v1.Deployment) *metric.Family { + count := 0 + if d.Spec.Template.Spec.Affinity != nil && d.Spec.Template.Spec.Affinity.PodAffinity != nil { + count = len(d.Spec.Template.Spec.Affinity.PodAffinity.PreferredDuringSchedulingIgnoredDuringExecution) + } + return &metric.Family{ + Metrics: []*metric.Metric{ + { + Value: float64(count), + }, + }, + } + }), + ), + *generator.NewFamilyGeneratorWithStability( + "kube_deployment_spec_pod_anti_affinity_required_rules", + "Number of required pod anti-affinity rules in the deployment's pod template.", + metric.Gauge, + basemetrics.ALPHA, + "", + wrapDeploymentFunc(func(d *v1.Deployment) *metric.Family { + count := 0 + if d.Spec.Template.Spec.Affinity != nil && d.Spec.Template.Spec.Affinity.PodAntiAffinity != nil { + count = len(d.Spec.Template.Spec.Affinity.PodAntiAffinity.RequiredDuringSchedulingIgnoredDuringExecution) + } + return &metric.Family{ + Metrics: []*metric.Metric{ + { + Value: float64(count), + }, + }, + } + }), + ), + *generator.NewFamilyGeneratorWithStability( + "kube_deployment_spec_pod_anti_affinity_preferred_rules", + "Number of preferred pod anti-affinity rules in the deployment's pod template.", + metric.Gauge, + basemetrics.ALPHA, + "", + wrapDeploymentFunc(func(d *v1.Deployment) *metric.Family { + count := 0 + if d.Spec.Template.Spec.Affinity != nil && d.Spec.Template.Spec.Affinity.PodAntiAffinity != nil { + count = len(d.Spec.Template.Spec.Affinity.PodAntiAffinity.PreferredDuringSchedulingIgnoredDuringExecution) + } + return &metric.Family{ + Metrics: []*metric.Metric{ + { + Value: float64(count), + }, + }, + } + }), + ), *generator.NewFamilyGeneratorWithStability( "kube_deployment_metadata_generation", "Sequence number representing a specific generation of the desired state.", diff --git a/internal/store/deployment_test.go b/internal/store/deployment_test.go index e3e381dec..5b4604054 100644 --- a/internal/store/deployment_test.go +++ b/internal/store/deployment_test.go @@ -45,41 +45,50 @@ func TestDeploymentStore(t *testing.T) { // Fixed metadata on type and help text. We prepend this to every expected // output so we only have to modify a single place when doing adjustments. const metadata = ` - # HELP kube_deployment_owner Information about the Deployment's owner. - # TYPE kube_deployment_owner gauge - # HELP kube_deployment_annotations Kubernetes annotations converted to Prometheus labels. - # TYPE kube_deployment_annotations gauge - # HELP kube_deployment_created [STABLE] Unix creation timestamp - # TYPE kube_deployment_created gauge - # HELP kube_deployment_metadata_generation [STABLE] Sequence number representing a specific generation of the desired state. - # TYPE kube_deployment_metadata_generation gauge - # HELP kube_deployment_spec_paused [STABLE] Whether the deployment is paused and will not be processed by the deployment controller. - # TYPE kube_deployment_spec_paused gauge - # HELP kube_deployment_spec_replicas [STABLE] Number of desired pods for a deployment. - # TYPE kube_deployment_spec_replicas gauge - # HELP kube_deployment_status_replicas [STABLE] The number of replicas per deployment. - # TYPE kube_deployment_status_replicas gauge - # HELP kube_deployment_status_replicas_ready [STABLE] The number of ready replicas per deployment. - # TYPE kube_deployment_status_replicas_ready gauge - # HELP kube_deployment_status_replicas_available [STABLE] The number of available replicas per deployment. - # TYPE kube_deployment_status_replicas_available gauge - # HELP kube_deployment_status_replicas_unavailable [STABLE] The number of unavailable replicas per deployment. - # TYPE kube_deployment_status_replicas_unavailable gauge - # HELP kube_deployment_status_replicas_updated [STABLE] The number of updated replicas per deployment. - # TYPE kube_deployment_status_replicas_updated gauge - # HELP kube_deployment_status_observed_generation [STABLE] The generation observed by the deployment controller. - # TYPE kube_deployment_status_observed_generation gauge - # HELP kube_deployment_status_condition [STABLE] The current status conditions of a deployment. - # TYPE kube_deployment_status_condition gauge - # HELP kube_deployment_spec_strategy_rollingupdate_max_unavailable [STABLE] Maximum number of unavailable replicas during a rolling update of a deployment. - # TYPE kube_deployment_spec_strategy_rollingupdate_max_unavailable gauge - # HELP kube_deployment_spec_strategy_rollingupdate_max_surge [STABLE] Maximum number of replicas that can be scheduled above the desired number of replicas during a rolling update of a deployment. - # TYPE kube_deployment_spec_strategy_rollingupdate_max_surge gauge - # HELP kube_deployment_labels [STABLE] Kubernetes labels converted to Prometheus labels. - # TYPE kube_deployment_labels gauge - # HELP kube_deployment_deletion_timestamp Unix deletion timestamp - # TYPE kube_deployment_deletion_timestamp gauge - ` + # HELP kube_deployment_owner Information about the Deployment's owner. + # TYPE kube_deployment_owner gauge + # HELP kube_deployment_annotations Kubernetes annotations converted to Prometheus labels. + # TYPE kube_deployment_annotations gauge + # HELP kube_deployment_created [STABLE] Unix creation timestamp + # TYPE kube_deployment_created gauge + # HELP kube_deployment_metadata_generation [STABLE] Sequence number representing a specific generation of the desired state. + # TYPE kube_deployment_metadata_generation gauge + # HELP kube_deployment_spec_paused [STABLE] Whether the deployment is paused and will not be processed by the deployment controller. + # TYPE kube_deployment_spec_paused gauge + # HELP kube_deployment_spec_pod_affinity_preferred_rules Number of preferred pod affinity rules in the deployment's pod template. + # TYPE kube_deployment_spec_pod_affinity_preferred_rules gauge + # HELP kube_deployment_spec_pod_affinity_required_rules Number of required pod affinity rules in the deployment's pod template. + # TYPE kube_deployment_spec_pod_affinity_required_rules gauge + # HELP kube_deployment_spec_pod_anti_affinity_preferred_rules Number of preferred pod anti-affinity rules in the deployment's pod template. + # TYPE kube_deployment_spec_pod_anti_affinity_preferred_rules gauge + # HELP kube_deployment_spec_pod_anti_affinity_required_rules Number of required pod anti-affinity rules in the deployment's pod template. + # TYPE kube_deployment_spec_pod_anti_affinity_required_rules gauge + # HELP kube_deployment_spec_replicas [STABLE] Number of desired pods for a deployment. + # TYPE kube_deployment_spec_replicas gauge + # HELP kube_deployment_status_replicas [STABLE] The number of replicas per deployment. + # TYPE kube_deployment_status_replicas gauge + # HELP kube_deployment_status_replicas_ready [STABLE] The number of ready replicas per deployment. + # TYPE kube_deployment_status_replicas_ready gauge + # HELP kube_deployment_status_replicas_available [STABLE] The number of available replicas per deployment. + # TYPE kube_deployment_status_replicas_available gauge + # HELP kube_deployment_status_replicas_unavailable [STABLE] The number of unavailable replicas per deployment. + # TYPE kube_deployment_status_replicas_unavailable gauge + # HELP kube_deployment_status_replicas_updated [STABLE] The number of updated replicas per deployment. + # TYPE kube_deployment_status_replicas_updated gauge + # HELP kube_deployment_status_observed_generation [STABLE] The generation observed by the deployment controller. + # TYPE kube_deployment_status_observed_generation gauge + # HELP kube_deployment_status_condition [STABLE] The current status conditions of a deployment. + # TYPE kube_deployment_status_condition gauge + # HELP kube_deployment_spec_strategy_rollingupdate_max_unavailable [STABLE] Maximum number of unavailable replicas during a rolling update of a deployment. + # TYPE kube_deployment_spec_strategy_rollingupdate_max_unavailable gauge + # HELP kube_deployment_spec_strategy_rollingupdate_max_surge [STABLE] Maximum number of replicas that can be scheduled above the desired number of replicas during a rolling update of a deployment. + # TYPE kube_deployment_spec_strategy_rollingupdate_max_surge gauge + # HELP kube_deployment_labels [STABLE] Kubernetes labels converted to Prometheus labels. + # TYPE kube_deployment_labels gauge + # HELP kube_deployment_deletion_timestamp Unix deletion timestamp + # TYPE kube_deployment_deletion_timestamp gauge + ` + cases := []generateMetricsTestCase{ { AllowAnnotationsList: []string{"company.io/team"}, @@ -124,6 +133,10 @@ func TestDeploymentStore(t *testing.T) { kube_deployment_owner{deployment="depl1",namespace="ns1",owner_kind="",owner_name=""} 1 kube_deployment_metadata_generation{deployment="depl1",namespace="ns1"} 21 kube_deployment_spec_paused{deployment="depl1",namespace="ns1"} 0 + kube_deployment_spec_pod_affinity_preferred_rules{deployment="depl1",namespace="ns1"} 0 + kube_deployment_spec_pod_affinity_required_rules{deployment="depl1",namespace="ns1"} 0 + kube_deployment_spec_pod_anti_affinity_preferred_rules{deployment="depl1",namespace="ns1"} 0 + kube_deployment_spec_pod_anti_affinity_required_rules{deployment="depl1",namespace="ns1"} 0 kube_deployment_spec_replicas{deployment="depl1",namespace="ns1"} 200 kube_deployment_spec_strategy_rollingupdate_max_surge{deployment="depl1",namespace="ns1"} 10 kube_deployment_spec_strategy_rollingupdate_max_unavailable{deployment="depl1",namespace="ns1"} 10 @@ -139,6 +152,117 @@ func TestDeploymentStore(t *testing.T) { kube_deployment_status_condition{condition="Progressing",deployment="depl1",namespace="ns1",reason="NewReplicaSetAvailable",status="true"} 1 kube_deployment_status_condition{condition="Progressing",deployment="depl1",namespace="ns1",reason="NewReplicaSetAvailable",status="false"} 0 kube_deployment_status_condition{condition="Progressing",deployment="depl1",namespace="ns1",reason="NewReplicaSetAvailable",status="unknown"} 0 +`, + }, + { + Obj: &v1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "depl-with-affinity", + Namespace: "ns1", + Generation: 1, + }, + Status: v1.DeploymentStatus{ + Replicas: 3, + ReadyReplicas: 3, + AvailableReplicas: 3, + UpdatedReplicas: 3, + ObservedGeneration: 1, + }, + Spec: v1.DeploymentSpec{ + Replicas: func() *int32 { r := int32(3); return &r }(), + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Affinity: &corev1.Affinity{ + PodAffinity: &corev1.PodAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: []corev1.PodAffinityTerm{ + { + LabelSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "cache"}, + }, + TopologyKey: "kubernetes.io/zone", + }, + }, + PreferredDuringSchedulingIgnoredDuringExecution: []corev1.WeightedPodAffinityTerm{ + { + Weight: 100, + PodAffinityTerm: corev1.PodAffinityTerm{ + LabelSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "web"}, + }, + TopologyKey: "kubernetes.io/hostname", + }, + }, + }, + }, + PodAntiAffinity: &corev1.PodAntiAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: []corev1.PodAffinityTerm{ + { + LabelSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "depl-with-affinity"}, + }, + TopologyKey: "kubernetes.io/hostname", + }, + }, + }, + }, + }, + }, + }, + }, + Want: metadata + ` + kube_deployment_metadata_generation{deployment="depl-with-affinity",namespace="ns1"} 1 + kube_deployment_spec_paused{deployment="depl-with-affinity",namespace="ns1"} 0 + kube_deployment_spec_pod_affinity_preferred_rules{deployment="depl-with-affinity",namespace="ns1"} 1 + kube_deployment_spec_pod_affinity_required_rules{deployment="depl-with-affinity",namespace="ns1"} 1 + kube_deployment_spec_pod_anti_affinity_preferred_rules{deployment="depl-with-affinity",namespace="ns1"} 0 + kube_deployment_spec_pod_anti_affinity_required_rules{deployment="depl-with-affinity",namespace="ns1"} 1 + kube_deployment_spec_replicas{deployment="depl-with-affinity",namespace="ns1"} 3 + kube_deployment_status_observed_generation{deployment="depl-with-affinity",namespace="ns1"} 1 + kube_deployment_status_replicas_available{deployment="depl-with-affinity",namespace="ns1"} 3 + kube_deployment_status_replicas_unavailable{deployment="depl-with-affinity",namespace="ns1"} 0 + kube_deployment_status_replicas_updated{deployment="depl-with-affinity",namespace="ns1"} 3 + kube_deployment_status_replicas{deployment="depl-with-affinity",namespace="ns1"} 3 + kube_deployment_status_replicas_ready{deployment="depl-with-affinity",namespace="ns1"} 3 +`, + }, + // Test case for deployment without any affinity rules + { + Obj: &v1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "depl-no-affinity", + Namespace: "ns1", + Generation: 1, + }, + Status: v1.DeploymentStatus{ + Replicas: 2, + ReadyReplicas: 2, + AvailableReplicas: 2, + UpdatedReplicas: 2, + ObservedGeneration: 1, + }, + Spec: v1.DeploymentSpec{ + Replicas: func() *int32 { r := int32(2); return &r }(), + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + // No affinity specified + }, + }, + }, + }, + Want: metadata + ` + kube_deployment_metadata_generation{deployment="depl-no-affinity",namespace="ns1"} 1 + kube_deployment_spec_paused{deployment="depl-no-affinity",namespace="ns1"} 0 + kube_deployment_spec_pod_affinity_preferred_rules{deployment="depl-no-affinity",namespace="ns1"} 0 + kube_deployment_spec_pod_affinity_required_rules{deployment="depl-no-affinity",namespace="ns1"} 0 + kube_deployment_spec_pod_anti_affinity_preferred_rules{deployment="depl-no-affinity",namespace="ns1"} 0 + kube_deployment_spec_pod_anti_affinity_required_rules{deployment="depl-no-affinity",namespace="ns1"} 0 + kube_deployment_spec_replicas{deployment="depl-no-affinity",namespace="ns1"} 2 + kube_deployment_status_observed_generation{deployment="depl-no-affinity",namespace="ns1"} 1 + kube_deployment_status_replicas_available{deployment="depl-no-affinity",namespace="ns1"} 2 + kube_deployment_status_replicas_unavailable{deployment="depl-no-affinity",namespace="ns1"} 0 + kube_deployment_status_replicas_updated{deployment="depl-no-affinity",namespace="ns1"} 2 + kube_deployment_status_replicas{deployment="depl-no-affinity",namespace="ns1"} 2 + kube_deployment_status_replicas_ready{deployment="depl-no-affinity",namespace="ns1"} 2 `, }, { @@ -179,6 +303,10 @@ func TestDeploymentStore(t *testing.T) { kube_deployment_metadata_generation{deployment="depl2",namespace="ns2"} 14 kube_deployment_owner{deployment="depl2",namespace="ns2",owner_kind="",owner_name=""} 1 kube_deployment_spec_paused{deployment="depl2",namespace="ns2"} 1 + kube_deployment_spec_pod_affinity_preferred_rules{deployment="depl2",namespace="ns2"} 0 + kube_deployment_spec_pod_affinity_required_rules{deployment="depl2",namespace="ns2"} 0 + kube_deployment_spec_pod_anti_affinity_preferred_rules{deployment="depl2",namespace="ns2"} 0 + kube_deployment_spec_pod_anti_affinity_required_rules{deployment="depl2",namespace="ns2"} 0 kube_deployment_spec_replicas{deployment="depl2",namespace="ns2"} 5 kube_deployment_spec_strategy_rollingupdate_max_surge{deployment="depl2",namespace="ns2"} 1 kube_deployment_spec_strategy_rollingupdate_max_unavailable{deployment="depl2",namespace="ns2"} 1 From b7ff9daaa3cddffba6fc419cdf455afdb6ea0574 Mon Sep 17 00:00:00 2001 From: Soumya Date: Fri, 15 Aug 2025 01:54:40 +0530 Subject: [PATCH 2/6] refactor: replace count-based with explicit rule-based pod affinity metrics - Replace 4 count-based metrics with single kube_deployment_spec_affinity metric - Add granular labels: affinity, type, topology_key, label_selector - Enable individual rule visibility and flexible querying - Update tests and documentation for new metric structure --- docs/metrics/workload/deployment-metrics.md | 5 +- internal/store/deployment.go | 141 ++++++++++---------- internal/store/deployment_test.go | 29 +--- 3 files changed, 74 insertions(+), 101 deletions(-) diff --git a/docs/metrics/workload/deployment-metrics.md b/docs/metrics/workload/deployment-metrics.md index f4ed289d0..1aab229c4 100644 --- a/docs/metrics/workload/deployment-metrics.md +++ b/docs/metrics/workload/deployment-metrics.md @@ -12,10 +12,7 @@ | kube_deployment_status_condition | Gauge | The current status conditions of a deployment. | `deployment`=<deployment-name>
`namespace`=<deployment-namespace>
`reason`=<deployment-transition-reason>
`condition`=<deployment-condition>
`status`=<true\|false\|unknown> | STABLE | | kube_deployment_spec_replicas | Gauge | Number of desired pods for a deployment. | `deployment`=<deployment-name>
`namespace`=<deployment-namespace> | STABLE | | kube_deployment_spec_paused | Gauge | Whether the deployment is paused and will not be processed by the deployment controller. | `deployment`=<deployment-name>
`namespace`=<deployment-namespace> | STABLE | -| kube_deployment_spec_pod_affinity_preferred_rules | Gauge | Number of preferred pod affinity rules in the deployment's pod template. | `deployment`=<deployment-name>
`namespace`=<deployment-namespace> | ALPHA | -| kube_deployment_spec_pod_affinity_required_rules | Gauge | Number of required pod affinity rules in the deployment's pod template. | `deployment`=<deployment-name>
`namespace`=<deployment-namespace> | ALPHA | -| kube_deployment_spec_pod_anti_affinity_preferred_rules | Gauge | Number of preferred pod anti-affinity rules in the deployment's pod template. | `deployment`=<deployment-name>
`namespace`=<deployment-namespace> | ALPHA | -| kube_deployment_spec_pod_anti_affinity_required_rules | Gauge | Number of required pod anti-affinity rules in the deployment's pod template. | `deployment`=<deployment-name>
`namespace`=<deployment-namespace> | ALPHA | +| kube_deployment_spec_affinity | Gauge | Pod affinity and anti-affinity rules defined in the deployment's pod template specification. | `deployment`=<deployment-name>
`namespace`=<deployment-namespace>
`affinity`=<podaffinity\|podantiaffinity>
`type`=<requiredDuringSchedulingIgnoredDuringExecution\|preferredDuringSchedulingIgnoredDuringExecution>
`topology_key`=<topology-key>
`label_selector`=<selector-string> | ALPHA | | kube_deployment_spec_strategy_rollingupdate_max_unavailable | Gauge | Maximum number of unavailable replicas during a rolling update of a deployment. | `deployment`=<deployment-name>
`namespace`=<deployment-namespace> | STABLE | | kube_deployment_spec_strategy_rollingupdate_max_surge | Gauge | Maximum number of replicas that can be scheduled above the desired number of replicas during a rolling update of a deployment. | `deployment`=<deployment-name>
`namespace`=<deployment-namespace> | STABLE | | kube_deployment_metadata_generation | Gauge | Sequence number representing a specific generation of the desired state. | `deployment`=<deployment-name>
`namespace`=<deployment-namespace> | STABLE | diff --git a/internal/store/deployment.go b/internal/store/deployment.go index 22c350bc5..7e72e33d8 100644 --- a/internal/store/deployment.go +++ b/internal/store/deployment.go @@ -327,85 +327,16 @@ func deploymentMetricFamilies(allowAnnotationsList, allowLabelsList []string) [] }), ), *generator.NewFamilyGeneratorWithStability( - "kube_deployment_spec_pod_affinity_required_rules", - "Number of required pod affinity rules in the deployment's pod template.", + "kube_deployment_spec_affinity", + "Pod affinity and anti-affinity rules defined in the deployment's pod template specification.", metric.Gauge, basemetrics.ALPHA, "", wrapDeploymentFunc(func(d *v1.Deployment) *metric.Family { - count := 0 - if d.Spec.Template.Spec.Affinity != nil && d.Spec.Template.Spec.Affinity.PodAffinity != nil { - count = len(d.Spec.Template.Spec.Affinity.PodAffinity.RequiredDuringSchedulingIgnoredDuringExecution) - } - return &metric.Family{ - Metrics: []*metric.Metric{ - { - Value: float64(count), - }, - }, - } - }), - ), - *generator.NewFamilyGeneratorWithStability( - "kube_deployment_spec_pod_affinity_preferred_rules", - "Number of preferred pod affinity rules in the deployment's pod template.", - metric.Gauge, - basemetrics.ALPHA, - "", - wrapDeploymentFunc(func(d *v1.Deployment) *metric.Family { - count := 0 - if d.Spec.Template.Spec.Affinity != nil && d.Spec.Template.Spec.Affinity.PodAffinity != nil { - count = len(d.Spec.Template.Spec.Affinity.PodAffinity.PreferredDuringSchedulingIgnoredDuringExecution) - } - return &metric.Family{ - Metrics: []*metric.Metric{ - { - Value: float64(count), - }, - }, - } - }), - ), - *generator.NewFamilyGeneratorWithStability( - "kube_deployment_spec_pod_anti_affinity_required_rules", - "Number of required pod anti-affinity rules in the deployment's pod template.", - metric.Gauge, - basemetrics.ALPHA, - "", - wrapDeploymentFunc(func(d *v1.Deployment) *metric.Family { - count := 0 - if d.Spec.Template.Spec.Affinity != nil && d.Spec.Template.Spec.Affinity.PodAntiAffinity != nil { - count = len(d.Spec.Template.Spec.Affinity.PodAntiAffinity.RequiredDuringSchedulingIgnoredDuringExecution) - } - return &metric.Family{ - Metrics: []*metric.Metric{ - { - Value: float64(count), - }, - }, - } - }), - ), - *generator.NewFamilyGeneratorWithStability( - "kube_deployment_spec_pod_anti_affinity_preferred_rules", - "Number of preferred pod anti-affinity rules in the deployment's pod template.", - metric.Gauge, - basemetrics.ALPHA, - "", - wrapDeploymentFunc(func(d *v1.Deployment) *metric.Family { - count := 0 - if d.Spec.Template.Spec.Affinity != nil && d.Spec.Template.Spec.Affinity.PodAntiAffinity != nil { - count = len(d.Spec.Template.Spec.Affinity.PodAntiAffinity.PreferredDuringSchedulingIgnoredDuringExecution) - } - return &metric.Family{ - Metrics: []*metric.Metric{ - { - Value: float64(count), - }, - }, - } + return generateDeploymentAffinityMetrics(d) }), ), + *generator.NewFamilyGeneratorWithStability( "kube_deployment_metadata_generation", "Sequence number representing a specific generation of the desired state.", @@ -515,3 +446,67 @@ func createDeploymentListWatch(kubeClient clientset.Interface, ns string, fieldS }, } } +func generateDeploymentAffinityMetrics(d *v1.Deployment) *metric.Family { + var metrics []*metric.Metric + + if d.Spec.Template.Spec.Affinity == nil { + return &metric.Family{Metrics: metrics} + } + + // Handle pod affinity rules + if d.Spec.Template.Spec.Affinity.PodAffinity != nil { + // Required affinity rules + for _, rule := range d.Spec.Template.Spec.Affinity.PodAffinity.RequiredDuringSchedulingIgnoredDuringExecution { + labelSelector := formatLabelSelector(rule.LabelSelector) + metrics = append(metrics, &metric.Metric{ + LabelKeys: []string{"affinity", "type", "topology_key", "label_selector"}, + LabelValues: []string{"podaffinity", "requiredDuringSchedulingIgnoredDuringExecution", rule.TopologyKey, labelSelector}, + Value: 1, + }) + } + + // Preferred affinity rules + for _, rule := range d.Spec.Template.Spec.Affinity.PodAffinity.PreferredDuringSchedulingIgnoredDuringExecution { + labelSelector := formatLabelSelector(rule.PodAffinityTerm.LabelSelector) + metrics = append(metrics, &metric.Metric{ + LabelKeys: []string{"affinity", "type", "topology_key", "label_selector"}, + LabelValues: []string{"podaffinity", "preferredDuringSchedulingIgnoredDuringExecution", rule.PodAffinityTerm.TopologyKey, labelSelector}, + Value: 1, + }) + } + } + + // Handle pod anti-affinity rules + if d.Spec.Template.Spec.Affinity.PodAntiAffinity != nil { + // Required anti-affinity rules + for _, rule := range d.Spec.Template.Spec.Affinity.PodAntiAffinity.RequiredDuringSchedulingIgnoredDuringExecution { + labelSelector := formatLabelSelector(rule.LabelSelector) + metrics = append(metrics, &metric.Metric{ + LabelKeys: []string{"affinity", "type", "topology_key", "label_selector"}, + LabelValues: []string{"podantiaffinity", "requiredDuringSchedulingIgnoredDuringExecution", rule.TopologyKey, labelSelector}, + Value: 1, + }) + } + + // Preferred anti-affinity rules + for _, rule := range d.Spec.Template.Spec.Affinity.PodAntiAffinity.PreferredDuringSchedulingIgnoredDuringExecution { + labelSelector := formatLabelSelector(rule.PodAffinityTerm.LabelSelector) + metrics = append(metrics, &metric.Metric{ + LabelKeys: []string{"affinity", "type", "topology_key", "label_selector"}, + LabelValues: []string{"podantiaffinity", "preferredDuringSchedulingIgnoredDuringExecution", rule.PodAffinityTerm.TopologyKey, labelSelector}, + Value: 1, + }) + } + } + + return &metric.Family{Metrics: metrics} +} + +// formatLabelSelector converts a LabelSelector to a string representation +func formatLabelSelector(selector *metav1.LabelSelector) string { + if selector == nil { + return "" + } + // Use Kubernetes helper function as suggested by @mrueg + return metav1.FormatLabelSelector(selector) +} diff --git a/internal/store/deployment_test.go b/internal/store/deployment_test.go index 5b4604054..d78a692af 100644 --- a/internal/store/deployment_test.go +++ b/internal/store/deployment_test.go @@ -55,14 +55,8 @@ func TestDeploymentStore(t *testing.T) { # TYPE kube_deployment_metadata_generation gauge # HELP kube_deployment_spec_paused [STABLE] Whether the deployment is paused and will not be processed by the deployment controller. # TYPE kube_deployment_spec_paused gauge - # HELP kube_deployment_spec_pod_affinity_preferred_rules Number of preferred pod affinity rules in the deployment's pod template. - # TYPE kube_deployment_spec_pod_affinity_preferred_rules gauge - # HELP kube_deployment_spec_pod_affinity_required_rules Number of required pod affinity rules in the deployment's pod template. - # TYPE kube_deployment_spec_pod_affinity_required_rules gauge - # HELP kube_deployment_spec_pod_anti_affinity_preferred_rules Number of preferred pod anti-affinity rules in the deployment's pod template. - # TYPE kube_deployment_spec_pod_anti_affinity_preferred_rules gauge - # HELP kube_deployment_spec_pod_anti_affinity_required_rules Number of required pod anti-affinity rules in the deployment's pod template. - # TYPE kube_deployment_spec_pod_anti_affinity_required_rules gauge + # HELP kube_deployment_spec_affinity Pod affinity and anti-affinity rules defined in the deployment's pod template specification. + # TYPE kube_deployment_spec_affinity gauge # HELP kube_deployment_spec_replicas [STABLE] Number of desired pods for a deployment. # TYPE kube_deployment_spec_replicas gauge # HELP kube_deployment_status_replicas [STABLE] The number of replicas per deployment. @@ -133,10 +127,6 @@ func TestDeploymentStore(t *testing.T) { kube_deployment_owner{deployment="depl1",namespace="ns1",owner_kind="",owner_name=""} 1 kube_deployment_metadata_generation{deployment="depl1",namespace="ns1"} 21 kube_deployment_spec_paused{deployment="depl1",namespace="ns1"} 0 - kube_deployment_spec_pod_affinity_preferred_rules{deployment="depl1",namespace="ns1"} 0 - kube_deployment_spec_pod_affinity_required_rules{deployment="depl1",namespace="ns1"} 0 - kube_deployment_spec_pod_anti_affinity_preferred_rules{deployment="depl1",namespace="ns1"} 0 - kube_deployment_spec_pod_anti_affinity_required_rules{deployment="depl1",namespace="ns1"} 0 kube_deployment_spec_replicas{deployment="depl1",namespace="ns1"} 200 kube_deployment_spec_strategy_rollingupdate_max_surge{deployment="depl1",namespace="ns1"} 10 kube_deployment_spec_strategy_rollingupdate_max_unavailable{deployment="depl1",namespace="ns1"} 10 @@ -212,10 +202,9 @@ func TestDeploymentStore(t *testing.T) { Want: metadata + ` kube_deployment_metadata_generation{deployment="depl-with-affinity",namespace="ns1"} 1 kube_deployment_spec_paused{deployment="depl-with-affinity",namespace="ns1"} 0 - kube_deployment_spec_pod_affinity_preferred_rules{deployment="depl-with-affinity",namespace="ns1"} 1 - kube_deployment_spec_pod_affinity_required_rules{deployment="depl-with-affinity",namespace="ns1"} 1 - kube_deployment_spec_pod_anti_affinity_preferred_rules{deployment="depl-with-affinity",namespace="ns1"} 0 - kube_deployment_spec_pod_anti_affinity_required_rules{deployment="depl-with-affinity",namespace="ns1"} 1 + kube_deployment_spec_affinity{deployment="depl-with-affinity",namespace="ns1",affinity="podaffinity",type="requiredDuringSchedulingIgnoredDuringExecution",topology_key="kubernetes.io/zone",label_selector="app=cache"} 1 + kube_deployment_spec_affinity{deployment="depl-with-affinity",namespace="ns1",affinity="podaffinity",type="preferredDuringSchedulingIgnoredDuringExecution",topology_key="kubernetes.io/hostname",label_selector="app=web"} 1 + kube_deployment_spec_affinity{deployment="depl-with-affinity",namespace="ns1",affinity="podantiaffinity",type="requiredDuringSchedulingIgnoredDuringExecution",topology_key="kubernetes.io/hostname",label_selector="app=depl-with-affinity"} 1 kube_deployment_spec_replicas{deployment="depl-with-affinity",namespace="ns1"} 3 kube_deployment_status_observed_generation{deployment="depl-with-affinity",namespace="ns1"} 1 kube_deployment_status_replicas_available{deployment="depl-with-affinity",namespace="ns1"} 3 @@ -252,10 +241,6 @@ func TestDeploymentStore(t *testing.T) { Want: metadata + ` kube_deployment_metadata_generation{deployment="depl-no-affinity",namespace="ns1"} 1 kube_deployment_spec_paused{deployment="depl-no-affinity",namespace="ns1"} 0 - kube_deployment_spec_pod_affinity_preferred_rules{deployment="depl-no-affinity",namespace="ns1"} 0 - kube_deployment_spec_pod_affinity_required_rules{deployment="depl-no-affinity",namespace="ns1"} 0 - kube_deployment_spec_pod_anti_affinity_preferred_rules{deployment="depl-no-affinity",namespace="ns1"} 0 - kube_deployment_spec_pod_anti_affinity_required_rules{deployment="depl-no-affinity",namespace="ns1"} 0 kube_deployment_spec_replicas{deployment="depl-no-affinity",namespace="ns1"} 2 kube_deployment_status_observed_generation{deployment="depl-no-affinity",namespace="ns1"} 1 kube_deployment_status_replicas_available{deployment="depl-no-affinity",namespace="ns1"} 2 @@ -303,10 +288,6 @@ func TestDeploymentStore(t *testing.T) { kube_deployment_metadata_generation{deployment="depl2",namespace="ns2"} 14 kube_deployment_owner{deployment="depl2",namespace="ns2",owner_kind="",owner_name=""} 1 kube_deployment_spec_paused{deployment="depl2",namespace="ns2"} 1 - kube_deployment_spec_pod_affinity_preferred_rules{deployment="depl2",namespace="ns2"} 0 - kube_deployment_spec_pod_affinity_required_rules{deployment="depl2",namespace="ns2"} 0 - kube_deployment_spec_pod_anti_affinity_preferred_rules{deployment="depl2",namespace="ns2"} 0 - kube_deployment_spec_pod_anti_affinity_required_rules{deployment="depl2",namespace="ns2"} 0 kube_deployment_spec_replicas{deployment="depl2",namespace="ns2"} 5 kube_deployment_spec_strategy_rollingupdate_max_surge{deployment="depl2",namespace="ns2"} 1 kube_deployment_spec_strategy_rollingupdate_max_unavailable{deployment="depl2",namespace="ns2"} 1 From c03e9757ddce8a59c505551e2c6e9baf1de58259 Mon Sep 17 00:00:00 2001 From: Rishab87 Date: Mon, 1 Sep 2025 15:42:18 +0530 Subject: [PATCH 3/6] chore: prep v2.17.0 --- data.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/data.yaml b/data.yaml index 3517a46e8..9d2464572 100644 --- a/data.yaml +++ b/data.yaml @@ -15,5 +15,7 @@ compat: kubernetes: "1.32" - version: "v2.17.0" kubernetes: "1.33" + - version: "main" + kubernetes: "1.33" - version: "main" kubernetes: "1.34" From d454223357c8c57aa499f04be49fc84c8b14b9cd Mon Sep 17 00:00:00 2001 From: SoumyaRaikwar Date: Sat, 13 Sep 2025 23:46:07 +0530 Subject: [PATCH 4/6] fix: resolve gocritic unlambda and formatting issues --- internal/store/deployment.go | 4 +--- internal/store/deployment_test.go | 6 +++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/internal/store/deployment.go b/internal/store/deployment.go index 7e72e33d8..60ce024b7 100644 --- a/internal/store/deployment.go +++ b/internal/store/deployment.go @@ -332,9 +332,7 @@ func deploymentMetricFamilies(allowAnnotationsList, allowLabelsList []string) [] metric.Gauge, basemetrics.ALPHA, "", - wrapDeploymentFunc(func(d *v1.Deployment) *metric.Family { - return generateDeploymentAffinityMetrics(d) - }), + wrapDeploymentFunc(generateDeploymentAffinityMetrics), ), *generator.NewFamilyGeneratorWithStability( diff --git a/internal/store/deployment_test.go b/internal/store/deployment_test.go index d78a692af..958bdff04 100644 --- a/internal/store/deployment_test.go +++ b/internal/store/deployment_test.go @@ -30,15 +30,15 @@ import ( var ( depl1Replicas int32 = 200 - depl2Replicas int32 = 5 - depl3Replicas int32 = 1 + depl2Replicas int32 = 5 + depl3Replicas int32 = 1 depl4Replicas int32 = 10 depl1MaxUnavailable = intstr.FromInt(10) depl2MaxUnavailable = intstr.FromString("25%") depl1MaxSurge = intstr.FromInt(10) - depl2MaxSurge = intstr.FromString("20%") + depl2MaxSurge = intstr.FromString("20%") ) func TestDeploymentStore(t *testing.T) { From 74da1d601e595f0bd0d7dd9b017305f1555661d6 Mon Sep 17 00:00:00 2001 From: SoumyaRaikwar Date: Fri, 19 Sep 2025 16:34:39 +0530 Subject: [PATCH 5/6] fix: clean up unused variables in deployment test --- internal/store/deployment_test.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/internal/store/deployment_test.go b/internal/store/deployment_test.go index 958bdff04..84b883580 100644 --- a/internal/store/deployment_test.go +++ b/internal/store/deployment_test.go @@ -30,15 +30,11 @@ import ( var ( depl1Replicas int32 = 200 - depl2Replicas int32 = 5 - depl3Replicas int32 = 1 depl4Replicas int32 = 10 depl1MaxUnavailable = intstr.FromInt(10) - depl2MaxUnavailable = intstr.FromString("25%") depl1MaxSurge = intstr.FromInt(10) - depl2MaxSurge = intstr.FromString("20%") ) func TestDeploymentStore(t *testing.T) { From 3c88746d1052f806781581f092127d633da2c2f9 Mon Sep 17 00:00:00 2001 From: SoumyaRaikwar Date: Sat, 20 Sep 2025 17:20:13 +0530 Subject: [PATCH 6/6] affinity metrics: add namespaceSelector and namespaces labels; remove stray comments; keep header spacing; docs+tests updated --- docs/metrics/workload/deployment-metrics.md | 2 +- internal/store/deployment.go | 26 ++++++++++++++------- internal/store/deployment_test.go | 6 ++--- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/docs/metrics/workload/deployment-metrics.md b/docs/metrics/workload/deployment-metrics.md index 1aab229c4..61fc473ab 100644 --- a/docs/metrics/workload/deployment-metrics.md +++ b/docs/metrics/workload/deployment-metrics.md @@ -12,7 +12,7 @@ | kube_deployment_status_condition | Gauge | The current status conditions of a deployment. | `deployment`=<deployment-name>
`namespace`=<deployment-namespace>
`reason`=<deployment-transition-reason>
`condition`=<deployment-condition>
`status`=<true\|false\|unknown> | STABLE | | kube_deployment_spec_replicas | Gauge | Number of desired pods for a deployment. | `deployment`=<deployment-name>
`namespace`=<deployment-namespace> | STABLE | | kube_deployment_spec_paused | Gauge | Whether the deployment is paused and will not be processed by the deployment controller. | `deployment`=<deployment-name>
`namespace`=<deployment-namespace> | STABLE | -| kube_deployment_spec_affinity | Gauge | Pod affinity and anti-affinity rules defined in the deployment's pod template specification. | `deployment`=<deployment-name>
`namespace`=<deployment-namespace>
`affinity`=<podaffinity\|podantiaffinity>
`type`=<requiredDuringSchedulingIgnoredDuringExecution\|preferredDuringSchedulingIgnoredDuringExecution>
`topology_key`=<topology-key>
`label_selector`=<selector-string> | ALPHA | +| kube_deployment_spec_affinity | Gauge | Pod affinity and anti-affinity rules defined in the deployment's pod template specification. | `deployment`=<deployment-name>
`namespace`=<deployment-namespace>
`affinity`=<podaffinity\|podantiaffinity>
`type`=<requiredDuringSchedulingIgnoredDuringExecution\|preferredDuringSchedulingIgnoredDuringExecution>
`topology_key`=<topology-key>
`label_selector`=<selector-string>
`namespace_selector`=<namespace-selector-string>
`namespaces`=<comma-separated-namespaces> | ALPHA | | kube_deployment_spec_strategy_rollingupdate_max_unavailable | Gauge | Maximum number of unavailable replicas during a rolling update of a deployment. | `deployment`=<deployment-name>
`namespace`=<deployment-namespace> | STABLE | | kube_deployment_spec_strategy_rollingupdate_max_surge | Gauge | Maximum number of replicas that can be scheduled above the desired number of replicas during a rolling update of a deployment. | `deployment`=<deployment-name>
`namespace`=<deployment-namespace> | STABLE | | kube_deployment_metadata_generation | Gauge | Sequence number representing a specific generation of the desired state. | `deployment`=<deployment-name>
`namespace`=<deployment-namespace> | STABLE | diff --git a/internal/store/deployment.go b/internal/store/deployment.go index 60ce024b7..c45eedda6 100644 --- a/internal/store/deployment.go +++ b/internal/store/deployment.go @@ -18,6 +18,7 @@ package store import ( "context" + "strings" basemetrics "k8s.io/component-base/metrics" @@ -456,9 +457,11 @@ func generateDeploymentAffinityMetrics(d *v1.Deployment) *metric.Family { // Required affinity rules for _, rule := range d.Spec.Template.Spec.Affinity.PodAffinity.RequiredDuringSchedulingIgnoredDuringExecution { labelSelector := formatLabelSelector(rule.LabelSelector) + namespaceSelector := formatLabelSelector(rule.NamespaceSelector) + namespaces := strings.Join(rule.Namespaces, ",") metrics = append(metrics, &metric.Metric{ - LabelKeys: []string{"affinity", "type", "topology_key", "label_selector"}, - LabelValues: []string{"podaffinity", "requiredDuringSchedulingIgnoredDuringExecution", rule.TopologyKey, labelSelector}, + LabelKeys: []string{"affinity", "type", "topology_key", "label_selector", "namespace_selector", "namespaces"}, + LabelValues: []string{"podaffinity", "requiredDuringSchedulingIgnoredDuringExecution", rule.TopologyKey, labelSelector, namespaceSelector, namespaces}, Value: 1, }) } @@ -466,9 +469,11 @@ func generateDeploymentAffinityMetrics(d *v1.Deployment) *metric.Family { // Preferred affinity rules for _, rule := range d.Spec.Template.Spec.Affinity.PodAffinity.PreferredDuringSchedulingIgnoredDuringExecution { labelSelector := formatLabelSelector(rule.PodAffinityTerm.LabelSelector) + namespaceSelector := formatLabelSelector(rule.PodAffinityTerm.NamespaceSelector) + namespaces := strings.Join(rule.PodAffinityTerm.Namespaces, ",") metrics = append(metrics, &metric.Metric{ - LabelKeys: []string{"affinity", "type", "topology_key", "label_selector"}, - LabelValues: []string{"podaffinity", "preferredDuringSchedulingIgnoredDuringExecution", rule.PodAffinityTerm.TopologyKey, labelSelector}, + LabelKeys: []string{"affinity", "type", "topology_key", "label_selector", "namespace_selector", "namespaces"}, + LabelValues: []string{"podaffinity", "preferredDuringSchedulingIgnoredDuringExecution", rule.PodAffinityTerm.TopologyKey, labelSelector, namespaceSelector, namespaces}, Value: 1, }) } @@ -479,9 +484,11 @@ func generateDeploymentAffinityMetrics(d *v1.Deployment) *metric.Family { // Required anti-affinity rules for _, rule := range d.Spec.Template.Spec.Affinity.PodAntiAffinity.RequiredDuringSchedulingIgnoredDuringExecution { labelSelector := formatLabelSelector(rule.LabelSelector) + namespaceSelector := formatLabelSelector(rule.NamespaceSelector) + namespaces := strings.Join(rule.Namespaces, ",") metrics = append(metrics, &metric.Metric{ - LabelKeys: []string{"affinity", "type", "topology_key", "label_selector"}, - LabelValues: []string{"podantiaffinity", "requiredDuringSchedulingIgnoredDuringExecution", rule.TopologyKey, labelSelector}, + LabelKeys: []string{"affinity", "type", "topology_key", "label_selector", "namespace_selector", "namespaces"}, + LabelValues: []string{"podantiaffinity", "requiredDuringSchedulingIgnoredDuringExecution", rule.TopologyKey, labelSelector, namespaceSelector, namespaces}, Value: 1, }) } @@ -489,9 +496,11 @@ func generateDeploymentAffinityMetrics(d *v1.Deployment) *metric.Family { // Preferred anti-affinity rules for _, rule := range d.Spec.Template.Spec.Affinity.PodAntiAffinity.PreferredDuringSchedulingIgnoredDuringExecution { labelSelector := formatLabelSelector(rule.PodAffinityTerm.LabelSelector) + namespaceSelector := formatLabelSelector(rule.PodAffinityTerm.NamespaceSelector) + namespaces := strings.Join(rule.PodAffinityTerm.Namespaces, ",") metrics = append(metrics, &metric.Metric{ - LabelKeys: []string{"affinity", "type", "topology_key", "label_selector"}, - LabelValues: []string{"podantiaffinity", "preferredDuringSchedulingIgnoredDuringExecution", rule.PodAffinityTerm.TopologyKey, labelSelector}, + LabelKeys: []string{"affinity", "type", "topology_key", "label_selector", "namespace_selector", "namespaces"}, + LabelValues: []string{"podantiaffinity", "preferredDuringSchedulingIgnoredDuringExecution", rule.PodAffinityTerm.TopologyKey, labelSelector, namespaceSelector, namespaces}, Value: 1, }) } @@ -505,6 +514,5 @@ func formatLabelSelector(selector *metav1.LabelSelector) string { if selector == nil { return "" } - // Use Kubernetes helper function as suggested by @mrueg return metav1.FormatLabelSelector(selector) } diff --git a/internal/store/deployment_test.go b/internal/store/deployment_test.go index 84b883580..97bde7c2d 100644 --- a/internal/store/deployment_test.go +++ b/internal/store/deployment_test.go @@ -198,9 +198,9 @@ func TestDeploymentStore(t *testing.T) { Want: metadata + ` kube_deployment_metadata_generation{deployment="depl-with-affinity",namespace="ns1"} 1 kube_deployment_spec_paused{deployment="depl-with-affinity",namespace="ns1"} 0 - kube_deployment_spec_affinity{deployment="depl-with-affinity",namespace="ns1",affinity="podaffinity",type="requiredDuringSchedulingIgnoredDuringExecution",topology_key="kubernetes.io/zone",label_selector="app=cache"} 1 - kube_deployment_spec_affinity{deployment="depl-with-affinity",namespace="ns1",affinity="podaffinity",type="preferredDuringSchedulingIgnoredDuringExecution",topology_key="kubernetes.io/hostname",label_selector="app=web"} 1 - kube_deployment_spec_affinity{deployment="depl-with-affinity",namespace="ns1",affinity="podantiaffinity",type="requiredDuringSchedulingIgnoredDuringExecution",topology_key="kubernetes.io/hostname",label_selector="app=depl-with-affinity"} 1 + kube_deployment_spec_affinity{deployment="depl-with-affinity",namespace="ns1",affinity="podaffinity",type="requiredDuringSchedulingIgnoredDuringExecution",topology_key="kubernetes.io/zone",label_selector="app=cache",namespace_selector="",namespaces=""} 1 + kube_deployment_spec_affinity{deployment="depl-with-affinity",namespace="ns1",affinity="podaffinity",type="preferredDuringSchedulingIgnoredDuringExecution",topology_key="kubernetes.io/hostname",label_selector="app=web",namespace_selector="",namespaces=""} 1 + kube_deployment_spec_affinity{deployment="depl-with-affinity",namespace="ns1",affinity="podantiaffinity",type="requiredDuringSchedulingIgnoredDuringExecution",topology_key="kubernetes.io/hostname",label_selector="app=depl-with-affinity",namespace_selector="",namespaces=""} 1 kube_deployment_spec_replicas{deployment="depl-with-affinity",namespace="ns1"} 3 kube_deployment_status_observed_generation{deployment="depl-with-affinity",namespace="ns1"} 1 kube_deployment_status_replicas_available{deployment="depl-with-affinity",namespace="ns1"} 3