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"
diff --git a/docs/metrics/workload/deployment-metrics.md b/docs/metrics/workload/deployment-metrics.md
index 21fb9fbc3..61fc473ab 100644
--- a/docs/metrics/workload/deployment-metrics.md
+++ b/docs/metrics/workload/deployment-metrics.md
@@ -12,10 +12,11 @@
| 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>
`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 |
| 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..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"
@@ -326,6 +327,15 @@ func deploymentMetricFamilies(allowAnnotationsList, allowLabelsList []string) []
}
}),
),
+ *generator.NewFamilyGeneratorWithStability(
+ "kube_deployment_spec_affinity",
+ "Pod affinity and anti-affinity rules defined in the deployment's pod template specification.",
+ metric.Gauge,
+ basemetrics.ALPHA,
+ "",
+ wrapDeploymentFunc(generateDeploymentAffinityMetrics),
+ ),
+
*generator.NewFamilyGeneratorWithStability(
"kube_deployment_metadata_generation",
"Sequence number representing a specific generation of the desired state.",
@@ -435,3 +445,74 @@ 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)
+ namespaceSelector := formatLabelSelector(rule.NamespaceSelector)
+ namespaces := strings.Join(rule.Namespaces, ",")
+ metrics = append(metrics, &metric.Metric{
+ LabelKeys: []string{"affinity", "type", "topology_key", "label_selector", "namespace_selector", "namespaces"},
+ LabelValues: []string{"podaffinity", "requiredDuringSchedulingIgnoredDuringExecution", rule.TopologyKey, labelSelector, namespaceSelector, namespaces},
+ Value: 1,
+ })
+ }
+
+ // 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", "namespace_selector", "namespaces"},
+ LabelValues: []string{"podaffinity", "preferredDuringSchedulingIgnoredDuringExecution", rule.PodAffinityTerm.TopologyKey, labelSelector, namespaceSelector, namespaces},
+ 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)
+ namespaceSelector := formatLabelSelector(rule.NamespaceSelector)
+ namespaces := strings.Join(rule.Namespaces, ",")
+ metrics = append(metrics, &metric.Metric{
+ LabelKeys: []string{"affinity", "type", "topology_key", "label_selector", "namespace_selector", "namespaces"},
+ LabelValues: []string{"podantiaffinity", "requiredDuringSchedulingIgnoredDuringExecution", rule.TopologyKey, labelSelector, namespaceSelector, namespaces},
+ Value: 1,
+ })
+ }
+
+ // 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", "namespace_selector", "namespaces"},
+ LabelValues: []string{"podantiaffinity", "preferredDuringSchedulingIgnoredDuringExecution", rule.PodAffinityTerm.TopologyKey, labelSelector, namespaceSelector, namespaces},
+ 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 ""
+ }
+ return metav1.FormatLabelSelector(selector)
+}
diff --git a/internal/store/deployment_test.go b/internal/store/deployment_test.go
index e3e381dec..97bde7c2d 100644
--- a/internal/store/deployment_test.go
+++ b/internal/store/deployment_test.go
@@ -30,56 +30,55 @@ 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) {
// 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_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.
+ # 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"},
@@ -139,6 +138,112 @@ 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_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
+ 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_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
`,
},
{