Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/generated/checks.md
Original file line number Diff line number Diff line change
Expand Up @@ -357,9 +357,9 @@ minReplicas: 3

**Enabled by default**: Yes

**Description**: Indicates when deployments with multiple replicas fail to specify inter-pod anti-affinity, to ensure that the orchestrator attempts to schedule replicas on different nodes.
**Description**: Indicates when deployments with multiple replicas fail to specify inter-pod anti-affinity or topology spread constraints, to ensure that the orchestrator attempts to schedule replicas on different nodes.

**Remediation**: Specify anti-affinity in your pod specification to ensure that the orchestrator attempts to schedule replicas on different nodes. Using podAntiAffinity, specify a labelSelector that matches pods for the deployment, and set the topologyKey to kubernetes.io/hostname. Refer to https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#inter-pod-affinity-and-anti-affinity for details.
**Remediation**: Specify anti-affinity or topology spread constraints in your pod specification to ensure that the orchestrator attempts to schedule replicas on different nodes. Using podAntiAffinity, specify a labelSelector that matches pods for the deployment, and set the topologyKey to kubernetes.io/hostname. Using topologySpreadConstraints, specify a labelSelector that matches pods for the deployment, and set the topologyKey to kubernetes.io/hostname. Refer to https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#inter-pod-affinity-and-anti-affinity and https://kubernetes.io/docs/concepts/scheduling-eviction/topology-spread-constraints/ for details.

**Template**: [anti-affinity](templates.md#anti-affinity-not-specified)

Expand Down
9 changes: 6 additions & 3 deletions pkg/builtinchecks/yamls/no-anti-affinity.yaml
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
name: "no-anti-affinity"
description: "Indicates when deployments with multiple replicas fail to specify inter-pod anti-affinity, to ensure that the orchestrator attempts to schedule replicas on different nodes."
description: "Indicates when deployments with multiple replicas fail to specify inter-pod anti-affinity or topology spread constraints, to ensure that the orchestrator attempts to schedule replicas on different nodes."
remediation: >-
Specify anti-affinity in your pod specification to ensure that the orchestrator attempts to schedule replicas on different nodes.
Specify anti-affinity or topology spread constraints in your pod specification to ensure that the orchestrator attempts to schedule replicas on different nodes.
Using podAntiAffinity, specify a labelSelector that matches pods for the deployment,
and set the topologyKey to kubernetes.io/hostname.
Refer to https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#inter-pod-affinity-and-anti-affinity for details.
Using topologySpreadConstraints, specify a labelSelector that matches pods for the deployment,
and set the topologyKey to kubernetes.io/hostname.
Refer to https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#inter-pod-affinity-and-anti-affinity
and https://kubernetes.io/docs/concepts/scheduling-eviction/topology-spread-constraints/ for details.
scope:
objectKinds:
- DeploymentLike
Expand Down
18 changes: 18 additions & 0 deletions pkg/templates/antiaffinity/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ func init() {
return nil
}
namespace := object.K8sObject.GetNamespace()
if topologySpreadConstraintsMatchAgainstNodes(podTemplateSpec.Spec.TopologySpreadConstraints,
podTemplateSpec.Labels, topologyKeyMatcher) {
return nil
}
affinity := podTemplateSpec.Spec.Affinity
// Short-circuit if no affinity rule is specified within the pod spec.
if affinity == nil || affinity.PodAntiAffinity == nil {
Expand Down Expand Up @@ -108,6 +112,20 @@ func init() {
})
}

func topologySpreadConstraintsMatchAgainstNodes(topologySpreadConstraints []coreV1.TopologySpreadConstraint,
podLabels map[string]string, topologyKeyMatcher func(string) bool) bool {
for _, constraint := range topologySpreadConstraints {
labelSelector, err := metaV1.LabelSelectorAsSelector(constraint.LabelSelector)
if err != nil {
continue
}
if topologyKeyMatcher(constraint.TopologyKey) && labelSelector.Matches(labels.Set(podLabels)) {
return true
}
}
return false
}
Comment thread
mwozniakk marked this conversation as resolved.

func validateAffinityTermMatchesAgainstNodes(affinityTerm coreV1.PodAffinityTerm, podNamespace string,
podLabels map[string]string, topologyKeyMatcher func(string) bool) error {
// If namespaces is not specified in the affinity term, that means the affinity term implicitly applies to
Expand Down
113 changes: 113 additions & 0 deletions pkg/templates/antiaffinity/template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,24 @@ func (s *AntiAffinityTestSuite) addDeploymentWithAntiAffinity(name string, repli
})
}

func (s *AntiAffinityTestSuite) addDeploymentWithTopologySpreadConstraint(name string, replicas int32, topologyKey string,
labelName string, whenUnsatisfiable v1.UnsatisfiableConstraintAction) {
s.addDeploymentWithReplicas(name, replicas)
s.ctx.ModifyDeployment(s.T(), name, func(deployment *appsV1.Deployment) {
deployment.Spec.Template.Labels = map[string]string{"app": name}
deployment.Spec.Template.Spec.TopologySpreadConstraints = []v1.TopologySpreadConstraint{
{
MaxSkew: 1,
TopologyKey: topologyKey,
WhenUnsatisfiable: whenUnsatisfiable,
LabelSelector: &metaV1.LabelSelector{
MatchLabels: map[string]string{"app": labelName},
},
},
}
})
}

Comment thread
coderabbitai[bot] marked this conversation as resolved.
func (s *AntiAffinityTestSuite) addDeploymentWithEmptyAntiAffinity(name string, replicas int32) {
s.addDeploymentWithReplicas(name, replicas)
s.ctx.ModifyDeployment(s.T(), name, func(deployment *appsV1.Deployment) {
Expand Down Expand Up @@ -223,3 +241,98 @@ func (s *AntiAffinityTestSuite) TestWithAntiAffinity() {
},
})
}

func (s *AntiAffinityTestSuite) TestWithTopologySpreadConstraints() {
const (
kubernetesIOHostnameDepName = "topology-spread-kubernetes-io-hostname"
otherValidKeyDepName = "topology-spread-other-valid-key"
weirdKeyDepName = "topology-spread-weird-key"
nonMatchingLabelSelectors = "topology-spread-non-matching-label-selector"
scheduleAnywayDepName = "topology-spread-schedule-anyway"
invalidLabelSelectorDepName = "topology-spread-invalid-label-selector"
)
s.addDeploymentWithTopologySpreadConstraint(kubernetesIOHostnameDepName, 2, "kubernetes.io/hostname",
kubernetesIOHostnameDepName, v1.DoNotSchedule)
s.addDeploymentWithTopologySpreadConstraint(otherValidKeyDepName, 3, "other.valid/key", otherValidKeyDepName,
v1.DoNotSchedule)
s.addDeploymentWithTopologySpreadConstraint(weirdKeyDepName, 4, "weird/key", weirdKeyDepName, v1.DoNotSchedule)
s.addDeploymentWithTopologySpreadConstraint(nonMatchingLabelSelectors, 4, "kubernetes.io/hostname",
"non-matching", v1.DoNotSchedule)
s.addDeploymentWithTopologySpreadConstraint(scheduleAnywayDepName, 4, "kubernetes.io/hostname", scheduleAnywayDepName,
v1.ScheduleAnyway)
s.addDeploymentWithTopologySpreadConstraint(invalidLabelSelectorDepName, 4, "kubernetes.io/hostname",
invalidLabelSelectorDepName, v1.DoNotSchedule)
s.ctx.ModifyDeployment(s.T(), invalidLabelSelectorDepName, func(deployment *appsV1.Deployment) {
deployment.Spec.Template.Spec.TopologySpreadConstraints[0].LabelSelector = &metaV1.LabelSelector{
MatchExpressions: []metaV1.LabelSelectorRequirement{
{
Key: "app",
Operator: metaV1.LabelSelectorOperator("InvalidOperator"),
Values: []string{invalidLabelSelectorDepName},
},
},
}
})

s.Validate(s.ctx, []templates.TestCase{
{
Param: params.Params{
MinReplicas: 2,
},
Diagnostics: map[string][]diagnostic.Diagnostic{
otherValidKeyDepName: {
{Message: "object has 3 replicas but does not specify inter pod anti-affinity"},
},
weirdKeyDepName: {
{Message: "object has 4 replicas but does not specify inter pod anti-affinity"},
},
nonMatchingLabelSelectors: {
{Message: "object has 4 replicas but does not specify inter pod anti-affinity"},
},
invalidLabelSelectorDepName: {
{Message: "object has 4 replicas but does not specify inter pod anti-affinity"},
},
},
ExpectInstantiationError: false,
},
{
Param: params.Params{
MinReplicas: 2,
TopologyKey: "other.valid/key",
},
Diagnostics: map[string][]diagnostic.Diagnostic{
kubernetesIOHostnameDepName: {
{Message: "object has 2 replicas but does not specify inter pod anti-affinity"},
},
weirdKeyDepName: {
{Message: "object has 4 replicas but does not specify inter pod anti-affinity"},
},
nonMatchingLabelSelectors: {
{Message: "object has 4 replicas but does not specify inter pod anti-affinity"},
},
scheduleAnywayDepName: {
{Message: "object has 4 replicas but does not specify inter pod anti-affinity"},
},
invalidLabelSelectorDepName: {
{Message: "object has 4 replicas but does not specify inter pod anti-affinity"},
},
},
ExpectInstantiationError: false,
},
{
Param: params.Params{
MinReplicas: 2,
TopologyKey: ".+",
},
Diagnostics: map[string][]diagnostic.Diagnostic{
nonMatchingLabelSelectors: {
{Message: "object has 4 replicas but does not specify inter pod anti-affinity"},
},
invalidLabelSelectorDepName: {
{Message: "object has 4 replicas but does not specify inter pod anti-affinity"},
},
},
ExpectInstantiationError: false,
},
})
}
Loading