From 8d689190b7dcd63a8bbb50d05e1739273dbc1e9a Mon Sep 17 00:00:00 2001 From: Wissam Abu Ahmad Date: Thu, 31 Oct 2024 12:11:02 +0100 Subject: [PATCH] Add new check pdb-unhealthy-pod-eviction-policy (#855) (#856) --- docs/generated/checks.md | 9 +++ docs/generated/templates.md | 9 +++ e2etests/bats-tests.sh | 14 +++++ internal/defaultchecks/default_checks.go | 1 + .../pdb-unhealthy-pod-eviction-policy.yaml | 7 +++ pkg/templates/all/all.go | 1 + .../internal/params/gen-params.go | 52 ++++++++++++++++ .../internal/params/params.go | 5 ++ .../pdbunhealthypodevictionpolicy/template.go | 42 +++++++++++++ .../template_test.go | 60 +++++++++++++++++++ .../pdb-unhealthy-pod-eviction-policy.yaml | 32 ++++++++++ 11 files changed, 232 insertions(+) create mode 100644 pkg/builtinchecks/yamls/pdb-unhealthy-pod-eviction-policy.yaml create mode 100644 pkg/templates/pdbunhealthypodevictionpolicy/internal/params/gen-params.go create mode 100644 pkg/templates/pdbunhealthypodevictionpolicy/internal/params/params.go create mode 100644 pkg/templates/pdbunhealthypodevictionpolicy/template.go create mode 100644 pkg/templates/pdbunhealthypodevictionpolicy/template_test.go create mode 100644 tests/checks/pdb-unhealthy-pod-eviction-policy.yaml diff --git a/docs/generated/checks.md b/docs/generated/checks.md index e5a966404..10601c6ea 100644 --- a/docs/generated/checks.md +++ b/docs/generated/checks.md @@ -446,6 +446,15 @@ strategyTypeRegex: ^(RollingUpdate|Rolling)$ **Remediation**: Change the PodDisruptionBudget to have minAvailable set to a number lower than the number of replicas in the related deployment-like objects. Refer to https://kubernetes.io/docs/tasks/run-application/configure-pdb/ for more information. **Template**: [pdb-min-available](templates.md#no-pod-disruptions-allowed---minavailable) +## pdb-unhealthy-pod-eviction-policy + +**Enabled by default**: Yes + +**Description**: Indicates when a PodDisruptionBudget does not explicitly set the unhealthyPodEvictionPolicy field. + +**Remediation**: Set unhealthyPodEvictionPolicy to AlwaysAllow. Refer to https://kubernetes.io/docs/tasks/run-application/configure-pdb/#unhealthy-pod-eviction-policy for more information. + +**Template**: [pdb-unhealthy-pod-eviction-policy](templates.md#.spec.unhealthypodevictionpolicy-in-pdb-is-set-to-default) ## privilege-escalation-container **Enabled by default**: Yes diff --git a/docs/generated/templates.md b/docs/generated/templates.md index 5bb23a908..416a71f75 100644 --- a/docs/generated/templates.md +++ b/docs/generated/templates.md @@ -564,6 +564,15 @@ KubeLinter supports the following templates: **Supported Objects**: PodDisruptionBudget +## .spec.unhealthyPodEvictionPolicy in PDB is set to default + +**Key**: `pdb-unhealthy-pod-eviction-policy` + +**Description**: Flag PodDisruptionBudget objects that do not explicitly set unhealthyPodEvictionPolicy. + +**Supported Objects**: PodDisruptionBudget + + ## Ports **Key**: `ports` diff --git a/e2etests/bats-tests.sh b/e2etests/bats-tests.sh index e4ef523d4..f0a2d8ca6 100755 --- a/e2etests/bats-tests.sh +++ b/e2etests/bats-tests.sh @@ -648,6 +648,20 @@ get_value_from() { [[ "${count}" == "3" ]] } +@test "pdb-unhealthy-pod-eviction-policy" { + + tmp="tests/checks/pdb-unhealthy-pod-eviction-policy.yaml" + cmd="${KUBE_LINTER_BIN} lint --include pdb-unhealthy-pod-eviction-policy --do-not-auto-add-defaults --format json ${tmp}" + run ${cmd} + + message1=$(get_value_from "${lines[0]}" '.Reports[0].Object.K8sObject.GroupVersionKind.Kind + ": " + .Reports[0].Diagnostic.Message') + + [[ "${message1}" == "PodDisruptionBudget: unhealthyPodEvictionPolicy is not explicitly set" ]] + count=$(get_value_from "${lines[0]}" '.Reports | length') + [[ "${count}" == "1" ]] + +} + @test "privilege-escalation-container" { tmp="tests/checks/privilege-escalation-container.yml" cmd="${KUBE_LINTER_BIN} lint --include privilege-escalation-container --do-not-auto-add-defaults --format json ${tmp}" diff --git a/internal/defaultchecks/default_checks.go b/internal/defaultchecks/default_checks.go index 4e1243c64..f3b0d9a6a 100644 --- a/internal/defaultchecks/default_checks.go +++ b/internal/defaultchecks/default_checks.go @@ -36,5 +36,6 @@ var ( "unsafe-sysctls", "unset-cpu-requirements", "unset-memory-requirements", + "pdb-unhealthy-pod-eviction-policy", ) ) diff --git a/pkg/builtinchecks/yamls/pdb-unhealthy-pod-eviction-policy.yaml b/pkg/builtinchecks/yamls/pdb-unhealthy-pod-eviction-policy.yaml new file mode 100644 index 000000000..775529dfd --- /dev/null +++ b/pkg/builtinchecks/yamls/pdb-unhealthy-pod-eviction-policy.yaml @@ -0,0 +1,7 @@ +name: "pdb-unhealthy-pod-eviction-policy" +description: "Indicates when a PodDisruptionBudget does not explicitly set the unhealthyPodEvictionPolicy field." +remediation: "Set unhealthyPodEvictionPolicy to AlwaysAllow. Refer to https://kubernetes.io/docs/tasks/run-application/configure-pdb/#unhealthy-pod-eviction-policy for more information." +scope: + objectKinds: + - PodDisruptionBudget +template: "pdb-unhealthy-pod-eviction-policy" diff --git a/pkg/templates/all/all.go b/pkg/templates/all/all.go index 01f8db3e7..8b92d3cb9 100644 --- a/pkg/templates/all/all.go +++ b/pkg/templates/all/all.go @@ -36,6 +36,7 @@ import ( _ "golang.stackrox.io/kube-linter/pkg/templates/nonisolatedpod" _ "golang.stackrox.io/kube-linter/pkg/templates/pdbmaxunavailable" _ "golang.stackrox.io/kube-linter/pkg/templates/pdbminavailable" + _ "golang.stackrox.io/kube-linter/pkg/templates/pdbunhealthypodevictionpolicy" _ "golang.stackrox.io/kube-linter/pkg/templates/ports" _ "golang.stackrox.io/kube-linter/pkg/templates/privileged" _ "golang.stackrox.io/kube-linter/pkg/templates/privilegedports" diff --git a/pkg/templates/pdbunhealthypodevictionpolicy/internal/params/gen-params.go b/pkg/templates/pdbunhealthypodevictionpolicy/internal/params/gen-params.go new file mode 100644 index 000000000..21bb3aa5c --- /dev/null +++ b/pkg/templates/pdbunhealthypodevictionpolicy/internal/params/gen-params.go @@ -0,0 +1,52 @@ +// Code generated by kube-linter template codegen. DO NOT EDIT. +// +build !templatecodegen + +package params + +import ( + "fmt" + "strings" + + "github.com/pkg/errors" + "golang.stackrox.io/kube-linter/pkg/check" + "golang.stackrox.io/kube-linter/pkg/templates/util" +) + +var ( + // Use some imports in case they don't get used otherwise. + _ = util.MustParseParameterDesc + _ = fmt.Sprintf + + ParamDescs = []check.ParameterDesc{ + } +) + +func (p *Params) Validate() error { + var validationErrors []string + if len(validationErrors) > 0 { + return errors.Errorf("invalid parameters: %s", strings.Join(validationErrors, ", ")) + } + return nil +} + +// ParseAndValidate instantiates a Params object out of the passed map[string]interface{}, +// validates it, and returns it. +// The return type is interface{} to satisfy the type in the Template struct. +func ParseAndValidate(m map[string]interface{}) (interface{}, error) { + var p Params + if err := util.DecodeMapStructure(m, &p); err != nil { + return nil, err + } + if err := p.Validate(); err != nil { + return nil, err + } + return p, nil +} + +// WrapInstantiateFunc is a convenience wrapper that wraps an untyped instantiate function +// into a typed one. +func WrapInstantiateFunc(f func(p Params) (check.Func, error)) func (interface{}) (check.Func, error) { + return func(paramsInt interface{}) (check.Func, error) { + return f(paramsInt.(Params)) + } +} diff --git a/pkg/templates/pdbunhealthypodevictionpolicy/internal/params/params.go b/pkg/templates/pdbunhealthypodevictionpolicy/internal/params/params.go new file mode 100644 index 000000000..578cc3aa8 --- /dev/null +++ b/pkg/templates/pdbunhealthypodevictionpolicy/internal/params/params.go @@ -0,0 +1,5 @@ +package params + +// Params represents the params accepted by this template. +type Params struct { +} diff --git a/pkg/templates/pdbunhealthypodevictionpolicy/template.go b/pkg/templates/pdbunhealthypodevictionpolicy/template.go new file mode 100644 index 000000000..37517be15 --- /dev/null +++ b/pkg/templates/pdbunhealthypodevictionpolicy/template.go @@ -0,0 +1,42 @@ +package pdbunhealthypodevictionpolicy + +import ( + "golang.stackrox.io/kube-linter/pkg/check" + "golang.stackrox.io/kube-linter/pkg/config" + "golang.stackrox.io/kube-linter/pkg/diagnostic" + "golang.stackrox.io/kube-linter/pkg/lintcontext" + "golang.stackrox.io/kube-linter/pkg/objectkinds" + "golang.stackrox.io/kube-linter/pkg/templates" + "golang.stackrox.io/kube-linter/pkg/templates/pdbunhealthypodevictionpolicy/internal/params" + pdbV1 "k8s.io/api/policy/v1" +) + +const ( + templateKey = "pdb-unhealthy-pod-eviction-policy" +) + +func init() { + templates.Register(check.Template{ + HumanName: ".spec.unhealthyPodEvictionPolicy in PDB is set to default", + Key: templateKey, + Description: "Flag PodDisruptionBudget objects that do not explicitly set unhealthyPodEvictionPolicy.", + SupportedObjectKinds: config.ObjectKindsDesc{ + ObjectKinds: []string{ + objectkinds.PodDisruptionBudget}, + }, + Parameters: params.ParamDescs, + ParseAndValidateParams: params.ParseAndValidate, + Instantiate: params.WrapInstantiateFunc(func(_ params.Params) (check.Func, error) { + return func(_ lintcontext.LintContext, object lintcontext.Object) []diagnostic.Diagnostic { + pdb, ok := object.K8sObject.(*pdbV1.PodDisruptionBudget) + if !ok { + return nil + } + if pdb.Spec.UnhealthyPodEvictionPolicy == nil { + return []diagnostic.Diagnostic{{Message: "unhealthyPodEvictionPolicy is not explicitly set"}} + } + return nil + }, nil + }), + }) +} diff --git a/pkg/templates/pdbunhealthypodevictionpolicy/template_test.go b/pkg/templates/pdbunhealthypodevictionpolicy/template_test.go new file mode 100644 index 000000000..1bba6c141 --- /dev/null +++ b/pkg/templates/pdbunhealthypodevictionpolicy/template_test.go @@ -0,0 +1,60 @@ +package pdbunhealthypodevictionpolicy + +import ( + "testing" + + "github.com/stretchr/testify/suite" + "golang.stackrox.io/kube-linter/pkg/diagnostic" + "golang.stackrox.io/kube-linter/pkg/lintcontext/mocks" + "golang.stackrox.io/kube-linter/pkg/templates" + "golang.stackrox.io/kube-linter/pkg/templates/pdbunhealthypodevictionpolicy/internal/params" + pdbv1 "k8s.io/api/policy/v1" +) + +func TestUnhealthyPodEvictionPolicyPDB(t *testing.T) { + suite.Run(t, new(UnhealthyPodEvictionPolicyPDBTestSuite)) +} + +type UnhealthyPodEvictionPolicyPDBTestSuite struct { + templates.TemplateTestSuite + + ctx *mocks.MockLintContext +} + +func (s *UnhealthyPodEvictionPolicyPDBTestSuite) SetupTest() { + s.Init(templateKey) + s.ctx = mocks.NewMockContext() +} + +func (s *UnhealthyPodEvictionPolicyPDBTestSuite) TestNoUnhealthyPodEvictionPolicy() { + s.ctx.AddMockPodDisruptionBudget(s.T(), "test-pdb-no-unhealthy-pod-eviction-policy") + s.ctx.ModifyPodDisruptionBudget(s.T(), "test-pdb-no-unhealthy-pod-eviction-policy", func(pdb *pdbv1.PodDisruptionBudget) { + pdb.Spec.UnhealthyPodEvictionPolicy = nil + }) + s.Validate(s.ctx, []templates.TestCase{ + { + Param: params.Params{}, + Diagnostics: map[string][]diagnostic.Diagnostic{ + "test-pdb-no-unhealthy-pod-eviction-policy": {{Message: "unhealthyPodEvictionPolicy is not explicitly set"}}, + }, + ExpectInstantiationError: false, + }, + }) +} + +func (s *UnhealthyPodEvictionPolicyPDBTestSuite) TestUnhealthyPodEvictionPolicyIsSet() { + s.ctx.AddMockPodDisruptionBudget(s.T(), "test-pdb-unhealthy-pod-eviction-policy-is-set") + s.ctx.ModifyPodDisruptionBudget(s.T(), "test-pdb-unhealthy-pod-eviction-policy-is-set", func(pdb *pdbv1.PodDisruptionBudget) { + var policy pdbv1.UnhealthyPodEvictionPolicyType = "AlwaysAllow" + pdb.Spec.UnhealthyPodEvictionPolicy = &policy + }) + s.Validate(s.ctx, []templates.TestCase{ + { + Param: params.Params{}, + Diagnostics: map[string][]diagnostic.Diagnostic{ + "test-pdb-unhealthy-pod-eviction-policy-is-set": nil, + }, + ExpectInstantiationError: false, + }, + }) +} diff --git a/tests/checks/pdb-unhealthy-pod-eviction-policy.yaml b/tests/checks/pdb-unhealthy-pod-eviction-policy.yaml new file mode 100644 index 000000000..e67a7131c --- /dev/null +++ b/tests/checks/pdb-unhealthy-pod-eviction-policy.yaml @@ -0,0 +1,32 @@ +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: fire +spec: + selector: + matchLabels: + app: app1 + maxUnavailable: 1 +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: dont-fire-1 +spec: + selector: + matchLabels: + app: app2 + maxUnavailable: 1 + unhealthyPodEvictionPolicy: AlwaysAllow +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: dont-fire-2 +spec: + selector: + matchLabels: + app: app2 + maxUnavailable: 1 + unhealthyPodEvictionPolicy: IfHealthyBudget