Skip to content

Commit

Permalink
Add check to enforce that objects with multiple replicas use inter-po…
Browse files Browse the repository at this point in the history
…d anti affinity (#54)
  • Loading branch information
arghya88 authored Nov 14, 2020
1 parent 6a27cdf commit 55597dc
Show file tree
Hide file tree
Showing 10 changed files with 219 additions and 1 deletion.
1 change: 1 addition & 0 deletions docs/generated/checks.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ The following table enumerates built-in checks:
| 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 | `{}` |
| 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":".+"}` |
| 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 | `{}` |
| 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}` |
| 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.+"}` |
| 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 | `{}` |
| 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 | `{}` |
Expand Down
21 changes: 21 additions & 0 deletions docs/generated/templates.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,26 @@
This page lists supported check templates.

## Anti affinity not specified

**Key**: `anti-affinity`

**Description**: Flag objects with multiple replicas but inter-pod anti affinity not specified in the pod template spec

**Supported Objects**: DeploymentLike

**Parameters**:
```
[
{
"name": "minReplicas",
"type": "integer",
"description": "The minimum number of replicas a deployment must have before anti-affinity is enforced on it",
"required": false
}
]
```

## CPU Requirements

**Key**: `cpu-requirements`
Expand Down
2 changes: 1 addition & 1 deletion internal/builtinchecks/built_in_checks.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func List() ([]check.Check, error) {
}
var chk check.Check
if err := yaml.Unmarshal(contents, &chk); err != nil {
loadErr = errors.Wrapf(err, "unmarshaling default check from %s", fileName)
loadErr = errors.Wrapf(err, "unmarshalling default check from %s", fileName)
return
}
list = append(list, chk)
Expand Down
13 changes: 13 additions & 0 deletions internal/builtinchecks/yamls/no-anti-affinity.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name: "no-anti-affinity"
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"
remediation: >-
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.
scope:
objectKinds:
- DeploymentLike
template: "anti-affinity"
params:
minReplicas: 2
1 change: 1 addition & 0 deletions internal/defaultchecks/default_checks.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ var (
"deprecated-service-account-field",
"env-var-secret",
"mismatching-selector",
"no-anti-affinity",
"no-extensions-v1beta",
"no-read-only-root-fs",
"non-existent-service-account",
Expand Down
22 changes: 22 additions & 0 deletions internal/extract/pod_spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,25 @@ func Selector(obj k8sutil.Object) (*metaV1.LabelSelector, bool) {
}
return nil, false
}

// Replicas extracts replicas from the given object, if available.
func Replicas(obj k8sutil.Object) (int32, bool) {
objValue := reflect.Indirect(reflect.ValueOf(obj))
spec := objValue.FieldByName("Spec")
if !spec.IsValid() {
return 0, false
}
replicas := spec.FieldByName("Replicas")
if !replicas.IsValid() {
return 0, false
}
numReplicas, ok := replicas.Interface().(*int32)
if ok {
if numReplicas != nil {
return *numReplicas, true
}
// If numReplicas is a `nil` pointer, then it defaults to 1.
return 1, true
}
return 0, false
}
1 change: 1 addition & 0 deletions internal/templates/all/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package all

import (
// Import all check templates.
_ "golang.stackrox.io/kube-linter/internal/templates/antiaffinity"
_ "golang.stackrox.io/kube-linter/internal/templates/cpurequirements"
_ "golang.stackrox.io/kube-linter/internal/templates/danglingservice"
_ "golang.stackrox.io/kube-linter/internal/templates/deprecatedserviceaccount"
Expand Down
68 changes: 68 additions & 0 deletions internal/templates/antiaffinity/internal/params/gen-params.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions internal/templates/antiaffinity/internal/params/params.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package params

// Params represents the params accepted by this template.
type Params struct {

// The minimum number of replicas a deployment must have before anti-affinity is enforced on it
MinReplicas int
}
83 changes: 83 additions & 0 deletions internal/templates/antiaffinity/template.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package antiaffinity

import (
"fmt"

"golang.stackrox.io/kube-linter/internal/check"
"golang.stackrox.io/kube-linter/internal/diagnostic"
"golang.stackrox.io/kube-linter/internal/extract"
"golang.stackrox.io/kube-linter/internal/lintcontext"
"golang.stackrox.io/kube-linter/internal/objectkinds"
"golang.stackrox.io/kube-linter/internal/templates"
"golang.stackrox.io/kube-linter/internal/templates/antiaffinity/internal/params"
coreV1 "k8s.io/api/core/v1"
metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
)

func init() {
templates.Register(check.Template{
HumanName: "Anti affinity not specified",
Key: "anti-affinity",
Description: "Flag objects with multiple replicas but inter-pod anti affinity not specified in the pod template spec",
SupportedObjectKinds: check.ObjectKindsDesc{
ObjectKinds: []string{objectkinds.DeploymentLike},
},
Parameters: params.ParamDescs,
ParseAndValidateParams: params.ParseAndValidate,
Instantiate: params.WrapInstantiateFunc(func(p params.Params) (check.Func, error) {
return func(_ *lintcontext.LintContext, object lintcontext.Object) []diagnostic.Diagnostic {
replicas, found := extract.Replicas(object.K8sObject)
if !found {
return nil
}
if int(replicas) < p.MinReplicas {
return nil
}
podTemplateSpec, hasPods := extract.PodTemplateSpec(object.K8sObject)
if !hasPods {
return nil
}
if affinity := podTemplateSpec.Spec.Affinity; affinity != nil && affinity.PodAntiAffinity != nil {
preferredAffinity := affinity.PodAntiAffinity.PreferredDuringSchedulingIgnoredDuringExecution
requiredAffinity := affinity.PodAntiAffinity.RequiredDuringSchedulingIgnoredDuringExecution
for _, preferred := range preferredAffinity {
if affinityTermMatchesLabelsAgainstNodes(preferred.PodAffinityTerm, podTemplateSpec.Namespace, podTemplateSpec.Labels) {
return nil
}
}
for _, required := range requiredAffinity {
if affinityTermMatchesLabelsAgainstNodes(required, podTemplateSpec.Namespace, podTemplateSpec.Labels) {
return nil
}
}
}
return []diagnostic.Diagnostic{{Message: fmt.Sprintf("object has %d replicas but does not specify inter pod anti-affinity", replicas)}}
}, nil
}),
})
}

func affinityTermMatchesLabelsAgainstNodes(affinityTerm coreV1.PodAffinityTerm, podNamespace string, podLabels map[string]string) bool {
// If namespaces is not specified in the affinity term, that means the affinity term implicitly applies to the pod's namespace.
if len(affinityTerm.Namespaces) > 0 {
var matchingNSFound bool
for _, ns := range affinityTerm.Namespaces {
if ns == podNamespace {
matchingNSFound = true
break
}
}
if !matchingNSFound {
return false
}
}
labelSelector, err := metaV1.LabelSelectorAsSelector(affinityTerm.LabelSelector)
if err != nil {
return false
}
if affinityTerm.TopologyKey == "kubernetes.io/hostname" && labelSelector.Matches(labels.Set(podLabels)) {
return true
}
return false
}

0 comments on commit 55597dc

Please sign in to comment.