Skip to content

Commit 55597dc

Browse files
authored
Add check to enforce that objects with multiple replicas use inter-pod anti affinity (#54)
1 parent 6a27cdf commit 55597dc

File tree

10 files changed

+219
-1
lines changed

10 files changed

+219
-1
lines changed

docs/generated/checks.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ The following table enumerates built-in checks:
77
| deprecated-service-account-field | Yes | Alert on deployments that use the deprecated serviceAccount field | Use the serviceAccountName field instead of the serviceAccount field. | deprecated-service-account-field | `{}` |
88
| env-var-secret | Yes | Alert on objects using a secret in an environment variable | Don't use raw secrets in an environment variable. Instead, either mount the secret as a file or use a secretKeyRef. See https://kubernetes.io/docs/concepts/configuration/secret/#using-secrets for more details. | env-var | `{"name":"(?i).*secret.*","value":".+"}` |
99
| mismatching-selector | Yes | Alert on deployments where the selector doesn't match the pod template labels | Make sure your deployment's selector correctly matches the labels in its pod template. | mismatching-selector | `{}` |
10+
| no-anti-affinity | Yes | Alert on deployments with multiple replicas that don't specify inter pod anti-affinity to ensure that the orchestrator attempts to schedule replicas on different nodes | Specify anti-affinity in your pod spec to ensure that the orchestrator attempts to schedule replicas on different nodes. You can do this by using podAntiAffinity, specifying a labelSelector that matches pods of this deployment, and setting the topologyKey to kubernetes.io/hostname. See https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#inter-pod-affinity-and-anti-affinity for more details. | anti-affinity | `{"minReplicas":2}` |
1011
| no-extensions-v1beta | Yes | Alert on objects using deprecated API versions under extensions v1beta | Migrate to using the apps/v1 API versions for these objects. See https://kubernetes.io/blog/2019/07/18/api-deprecations-in-1-16/ for more details. | disallowed-api-obj | `{"group":"extensions","version":"v1beta.+"}` |
1112
| no-liveness-probe | No | Alert on containers which don't specify a liveness probe | Specify a liveness probe in your container. See https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ for more details. | liveness-probe | `{}` |
1213
| no-read-only-root-fs | Yes | Alert on containers not running with a read-only root filesystem | Set readOnlyRootFilesystem to true in your container's securityContext. | read-only-root-fs | `{}` |

docs/generated/templates.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,26 @@
11
This page lists supported check templates.
22

3+
## Anti affinity not specified
4+
5+
**Key**: `anti-affinity`
6+
7+
**Description**: Flag objects with multiple replicas but inter-pod anti affinity not specified in the pod template spec
8+
9+
**Supported Objects**: DeploymentLike
10+
11+
**Parameters**:
12+
```
13+
[
14+
{
15+
"name": "minReplicas",
16+
"type": "integer",
17+
"description": "The minimum number of replicas a deployment must have before anti-affinity is enforced on it",
18+
"required": false
19+
}
20+
]
21+
22+
```
23+
324
## CPU Requirements
425

526
**Key**: `cpu-requirements`

internal/builtinchecks/built_in_checks.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ func List() ([]check.Check, error) {
4343
}
4444
var chk check.Check
4545
if err := yaml.Unmarshal(contents, &chk); err != nil {
46-
loadErr = errors.Wrapf(err, "unmarshaling default check from %s", fileName)
46+
loadErr = errors.Wrapf(err, "unmarshalling default check from %s", fileName)
4747
return
4848
}
4949
list = append(list, chk)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
name: "no-anti-affinity"
2+
description: "Alert on deployments with multiple replicas that don't specify inter pod anti-affinity to ensure that the orchestrator attempts to schedule replicas on different nodes"
3+
remediation: >-
4+
Specify anti-affinity in your pod spec to ensure that the orchestrator attempts to schedule replicas on different nodes.
5+
You can do this by using podAntiAffinity, specifying a labelSelector that matches pods of this deployment,
6+
and setting the topologyKey to kubernetes.io/hostname.
7+
See https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#inter-pod-affinity-and-anti-affinity for more details.
8+
scope:
9+
objectKinds:
10+
- DeploymentLike
11+
template: "anti-affinity"
12+
params:
13+
minReplicas: 2

internal/defaultchecks/default_checks.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ var (
1111
"deprecated-service-account-field",
1212
"env-var-secret",
1313
"mismatching-selector",
14+
"no-anti-affinity",
1415
"no-extensions-v1beta",
1516
"no-read-only-root-fs",
1617
"non-existent-service-account",

internal/extract/pod_spec.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,25 @@ func Selector(obj k8sutil.Object) (*metaV1.LabelSelector, bool) {
7272
}
7373
return nil, false
7474
}
75+
76+
// Replicas extracts replicas from the given object, if available.
77+
func Replicas(obj k8sutil.Object) (int32, bool) {
78+
objValue := reflect.Indirect(reflect.ValueOf(obj))
79+
spec := objValue.FieldByName("Spec")
80+
if !spec.IsValid() {
81+
return 0, false
82+
}
83+
replicas := spec.FieldByName("Replicas")
84+
if !replicas.IsValid() {
85+
return 0, false
86+
}
87+
numReplicas, ok := replicas.Interface().(*int32)
88+
if ok {
89+
if numReplicas != nil {
90+
return *numReplicas, true
91+
}
92+
// If numReplicas is a `nil` pointer, then it defaults to 1.
93+
return 1, true
94+
}
95+
return 0, false
96+
}

internal/templates/all/all.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package all
22

33
import (
44
// Import all check templates.
5+
_ "golang.stackrox.io/kube-linter/internal/templates/antiaffinity"
56
_ "golang.stackrox.io/kube-linter/internal/templates/cpurequirements"
67
_ "golang.stackrox.io/kube-linter/internal/templates/danglingservice"
78
_ "golang.stackrox.io/kube-linter/internal/templates/deprecatedserviceaccount"

internal/templates/antiaffinity/internal/params/gen-params.go

Lines changed: 68 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package params
2+
3+
// Params represents the params accepted by this template.
4+
type Params struct {
5+
6+
// The minimum number of replicas a deployment must have before anti-affinity is enforced on it
7+
MinReplicas int
8+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package antiaffinity
2+
3+
import (
4+
"fmt"
5+
6+
"golang.stackrox.io/kube-linter/internal/check"
7+
"golang.stackrox.io/kube-linter/internal/diagnostic"
8+
"golang.stackrox.io/kube-linter/internal/extract"
9+
"golang.stackrox.io/kube-linter/internal/lintcontext"
10+
"golang.stackrox.io/kube-linter/internal/objectkinds"
11+
"golang.stackrox.io/kube-linter/internal/templates"
12+
"golang.stackrox.io/kube-linter/internal/templates/antiaffinity/internal/params"
13+
coreV1 "k8s.io/api/core/v1"
14+
metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
15+
"k8s.io/apimachinery/pkg/labels"
16+
)
17+
18+
func init() {
19+
templates.Register(check.Template{
20+
HumanName: "Anti affinity not specified",
21+
Key: "anti-affinity",
22+
Description: "Flag objects with multiple replicas but inter-pod anti affinity not specified in the pod template spec",
23+
SupportedObjectKinds: check.ObjectKindsDesc{
24+
ObjectKinds: []string{objectkinds.DeploymentLike},
25+
},
26+
Parameters: params.ParamDescs,
27+
ParseAndValidateParams: params.ParseAndValidate,
28+
Instantiate: params.WrapInstantiateFunc(func(p params.Params) (check.Func, error) {
29+
return func(_ *lintcontext.LintContext, object lintcontext.Object) []diagnostic.Diagnostic {
30+
replicas, found := extract.Replicas(object.K8sObject)
31+
if !found {
32+
return nil
33+
}
34+
if int(replicas) < p.MinReplicas {
35+
return nil
36+
}
37+
podTemplateSpec, hasPods := extract.PodTemplateSpec(object.K8sObject)
38+
if !hasPods {
39+
return nil
40+
}
41+
if affinity := podTemplateSpec.Spec.Affinity; affinity != nil && affinity.PodAntiAffinity != nil {
42+
preferredAffinity := affinity.PodAntiAffinity.PreferredDuringSchedulingIgnoredDuringExecution
43+
requiredAffinity := affinity.PodAntiAffinity.RequiredDuringSchedulingIgnoredDuringExecution
44+
for _, preferred := range preferredAffinity {
45+
if affinityTermMatchesLabelsAgainstNodes(preferred.PodAffinityTerm, podTemplateSpec.Namespace, podTemplateSpec.Labels) {
46+
return nil
47+
}
48+
}
49+
for _, required := range requiredAffinity {
50+
if affinityTermMatchesLabelsAgainstNodes(required, podTemplateSpec.Namespace, podTemplateSpec.Labels) {
51+
return nil
52+
}
53+
}
54+
}
55+
return []diagnostic.Diagnostic{{Message: fmt.Sprintf("object has %d replicas but does not specify inter pod anti-affinity", replicas)}}
56+
}, nil
57+
}),
58+
})
59+
}
60+
61+
func affinityTermMatchesLabelsAgainstNodes(affinityTerm coreV1.PodAffinityTerm, podNamespace string, podLabels map[string]string) bool {
62+
// If namespaces is not specified in the affinity term, that means the affinity term implicitly applies to the pod's namespace.
63+
if len(affinityTerm.Namespaces) > 0 {
64+
var matchingNSFound bool
65+
for _, ns := range affinityTerm.Namespaces {
66+
if ns == podNamespace {
67+
matchingNSFound = true
68+
break
69+
}
70+
}
71+
if !matchingNSFound {
72+
return false
73+
}
74+
}
75+
labelSelector, err := metaV1.LabelSelectorAsSelector(affinityTerm.LabelSelector)
76+
if err != nil {
77+
return false
78+
}
79+
if affinityTerm.TopologyKey == "kubernetes.io/hostname" && labelSelector.Matches(labels.Set(podLabels)) {
80+
return true
81+
}
82+
return false
83+
}

0 commit comments

Comments
 (0)