diff --git a/docs/generated/checks.md b/docs/generated/checks.md index 914abb9aa..759376d7c 100644 --- a/docs/generated/checks.md +++ b/docs/generated/checks.md @@ -531,6 +531,15 @@ value: '[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+' ```yaml key: owner ``` +## restart-policy + +**Enabled by default**: No + +**Description**: Indicates when a deployment-like object does not use a restart policy + +**Remediation**: Set up the restart policy for your object to 'Always' or 'OnFailure' to increase the fault tolerance. + +**Template**: [restart-policy](templates.md#restart-policy) ## run-as-non-root **Enabled by default**: Yes diff --git a/docs/generated/templates.md b/docs/generated/templates.md index 416a71f75..f246cf1b9 100644 --- a/docs/generated/templates.md +++ b/docs/generated/templates.md @@ -712,6 +712,15 @@ KubeLinter supports the following templates: type: string ``` +## Restart policy + +**Key**: `restart-policy` + +**Description**: Flag applications running without the restart policy. + +**Supported Objects**: DeploymentLike + + ## Run as non-root user **Key**: `run-as-non-root` diff --git a/e2etests/bats-tests.sh b/e2etests/bats-tests.sh index f0a2d8ca6..cacc1ea41 100755 --- a/e2etests/bats-tests.sh +++ b/e2etests/bats-tests.sh @@ -785,6 +785,30 @@ get_value_from() { [[ "${count}" == "2" ]] } +@test "restart-policy" { + tmp="tests/checks/restart-policy.yaml" + cmd="${KUBE_LINTER_BIN} lint --include restart-policy --do-not-auto-add-defaults --format json ${tmp}" + run ${cmd} + + message1=$(get_value_from "${lines[0]}" '.Reports[0] | .Object.K8sObject.GroupVersionKind.Kind + " " + .Object.K8sObject.Name + ": " + .Diagnostic.Message') + message2=$(get_value_from "${lines[0]}" '.Reports[1] | .Object.K8sObject.GroupVersionKind.Kind + " " + .Object.K8sObject.Name + ": " + .Diagnostic.Message') + message3=$(get_value_from "${lines[0]}" '.Reports[2] | .Object.K8sObject.GroupVersionKind.Kind + " " + .Object.K8sObject.Name + ": " + .Diagnostic.Message') + message4=$(get_value_from "${lines[0]}" '.Reports[3] | .Object.K8sObject.GroupVersionKind.Kind + " " + .Object.K8sObject.Name + ": " + .Diagnostic.Message') + message5=$(get_value_from "${lines[0]}" '.Reports[4] | .Object.K8sObject.GroupVersionKind.Kind + " " + .Object.K8sObject.Name + ": " + .Diagnostic.Message') + message6=$(get_value_from "${lines[0]}" '.Reports[5] | .Object.K8sObject.GroupVersionKind.Kind + " " + .Object.K8sObject.Name + ": " + .Diagnostic.Message') + message7=$(get_value_from "${lines[0]}" '.Reports[6] | .Object.K8sObject.GroupVersionKind.Kind + " " + .Object.K8sObject.Name + ": " + .Diagnostic.Message') + count=$(get_value_from "${lines[0]}" '.Reports | length') + + [[ "${message1}" == "Deployment fire-deployment-never: object has a restart policy defined with 'Never' but the only accepted restart policies are '[Always OnFailure]'" ]] + [[ "${message2}" == "Pod fire-pod-never: object has a restart policy defined with 'Never' but the only accepted restart policies are '[Always OnFailure]'" ]] + [[ "${message3}" == "DaemonSet fire-daemonset-never: object has a restart policy defined with 'Never' but the only accepted restart policies are '[Always OnFailure]'" ]] + [[ "${message4}" == "ReplicaSet fire-replicaset-never: object has a restart policy defined with 'Never' but the only accepted restart policies are '[Always OnFailure]'" ]] + [[ "${message5}" == "ReplicationController fire-replicationcontroller-never: object has a restart policy defined with 'Never' but the only accepted restart policies are '[Always OnFailure]'" ]] + [[ "${message6}" == "Job fire-job-never: object has a restart policy defined with 'Never' but the only accepted restart policies are '[Always OnFailure]'" ]] + [[ "${message7}" == "CronJob fire-cronjob-never: object has a restart policy defined with 'Never' but the only accepted restart policies are '[Always OnFailure]'" ]] + [[ "${count}" == "7" ]] +} + @test "run-as-non-root" { tmp="tests/checks/run-as-non-root.yml" cmd="${KUBE_LINTER_BIN} lint --include run-as-non-root --do-not-auto-add-defaults --format json ${tmp}" diff --git a/pkg/builtinchecks/yamls/restart-policy.yaml b/pkg/builtinchecks/yamls/restart-policy.yaml new file mode 100644 index 000000000..cb9e9cb3c --- /dev/null +++ b/pkg/builtinchecks/yamls/restart-policy.yaml @@ -0,0 +1,8 @@ +name: "restart-policy" +description: "Indicates when a deployment-like object does not use a restart policy" +remediation: >- + Set up the restart policy for your object to 'Always' or 'OnFailure' to increase the fault tolerance. +scope: + objectKinds: + - DeploymentLike +template: "restart-policy" \ No newline at end of file diff --git a/pkg/templates/all/all.go b/pkg/templates/all/all.go index 8b92d3cb9..e941d98fb 100644 --- a/pkg/templates/all/all.go +++ b/pkg/templates/all/all.go @@ -48,6 +48,7 @@ import ( _ "golang.stackrox.io/kube-linter/pkg/templates/replicas" _ "golang.stackrox.io/kube-linter/pkg/templates/requiredannotation" _ "golang.stackrox.io/kube-linter/pkg/templates/requiredlabel" + _ "golang.stackrox.io/kube-linter/pkg/templates/restartpolicy" _ "golang.stackrox.io/kube-linter/pkg/templates/runasnonroot" _ "golang.stackrox.io/kube-linter/pkg/templates/sccdenypriv" _ "golang.stackrox.io/kube-linter/pkg/templates/serviceaccount" diff --git a/pkg/templates/restartpolicy/internal/params/gen-params.go b/pkg/templates/restartpolicy/internal/params/gen-params.go new file mode 100644 index 000000000..21bb3aa5c --- /dev/null +++ b/pkg/templates/restartpolicy/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/restartpolicy/internal/params/params.go b/pkg/templates/restartpolicy/internal/params/params.go new file mode 100644 index 000000000..578cc3aa8 --- /dev/null +++ b/pkg/templates/restartpolicy/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/restartpolicy/template.go b/pkg/templates/restartpolicy/template.go new file mode 100644 index 000000000..b3d4c20a6 --- /dev/null +++ b/pkg/templates/restartpolicy/template.go @@ -0,0 +1,50 @@ +package restartpolicy + +import ( + "fmt" + + "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/extract" + "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/restartpolicy/internal/params" + coreV1 "k8s.io/api/core/v1" +) + +const ( + templateKey = "restart-policy" +) + +var acceptedRestartPolicies = []coreV1.RestartPolicy{coreV1.RestartPolicyAlways, coreV1.RestartPolicyOnFailure} + +func init() { + templates.Register(check.Template{ + HumanName: "Restart policy", + Key: templateKey, + Description: "Flag applications running without the restart policy.", + SupportedObjectKinds: config.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 { + spec, found := extract.PodSpec(object.K8sObject) + if !found { + return nil + } + for _, policy := range acceptedRestartPolicies { + if spec.RestartPolicy == policy { + return nil + } + } + return []diagnostic.Diagnostic{ + {Message: fmt.Sprintf("object has a restart policy defined with '%s' but the only accepted restart policies are '%s'", spec.RestartPolicy, acceptedRestartPolicies)}, + } + }, nil + }), + }) +} diff --git a/pkg/templates/restartpolicy/template_test.go b/pkg/templates/restartpolicy/template_test.go new file mode 100644 index 000000000..41d655700 --- /dev/null +++ b/pkg/templates/restartpolicy/template_test.go @@ -0,0 +1,118 @@ +package restartpolicy + +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/restartpolicy/internal/params" + appsV1 "k8s.io/api/apps/v1" + coreV1 "k8s.io/api/core/v1" +) + +func TestRestartPolicy(t *testing.T) { + suite.Run(t, new(RestartPolicyTestSuite)) +} + +type RestartPolicyTestSuite struct { + templates.TemplateTestSuite + + ctx *mocks.MockLintContext +} + +func (s *RestartPolicyTestSuite) SetupTest() { + s.Init(templateKey) + s.ctx = mocks.NewMockContext() +} + +func (s *RestartPolicyTestSuite) addDeploymentWithRestartPolicy(name string, policy coreV1.RestartPolicy) { + s.ctx.AddMockDeployment(s.T(), name) + s.ctx.ModifyDeployment(s.T(), name, func(deployment *appsV1.Deployment) { + deployment.Spec.Template.Spec.RestartPolicy = policy + }) +} + +func (s *RestartPolicyTestSuite) addDeploymentWithEmptyRestartPolicy(name string) { + s.ctx.AddMockDeployment(s.T(), name) + s.ctx.ModifyDeployment(s.T(), name, func(deployment *appsV1.Deployment) { + deployment.Spec.Template.Spec.RestartPolicy = "" + }) +} + +func (s *RestartPolicyTestSuite) addDeploymentWithoutRestartPolicy(name string) { + s.ctx.AddMockDeployment(s.T(), name) +} + +func (s *RestartPolicyTestSuite) addObjectWithoutPodSpec(name string) { + s.ctx.AddMockService(s.T(), name) +} + +func (s *RestartPolicyTestSuite) TestInvalidRestartPolicies() { + const ( + withoutRestartPolicy = "without-restart-policy" + emptyRestartPolicy = "empty-restart-policy" + restartPolicyNever = "restart-policy-never" + ) + + s.addDeploymentWithoutRestartPolicy(withoutRestartPolicy) + s.addDeploymentWithEmptyRestartPolicy(emptyRestartPolicy) + s.addDeploymentWithRestartPolicy(restartPolicyNever, coreV1.RestartPolicyNever) + + s.Validate(s.ctx, []templates.TestCase{ + { + Param: params.Params{}, + Diagnostics: map[string][]diagnostic.Diagnostic{ + withoutRestartPolicy: { + {Message: "object has a restart policy defined with '' but the only accepted restart policies are '[Always OnFailure]'"}, + }, + emptyRestartPolicy: { + {Message: "object has a restart policy defined with '' but the only accepted restart policies are '[Always OnFailure]'"}, + }, + restartPolicyNever: { + {Message: "object has a restart policy defined with 'Never' but the only accepted restart policies are '[Always OnFailure]'"}, + }, + }, + ExpectInstantiationError: false, + }, + }) +} + +func (s *RestartPolicyTestSuite) TestAcceptableRestartPolicy() { + const ( + alwaysRestartPolicy = "restart-policy-always" + onFailureRestartPolicy = "restart-policy-on-failure" + ) + s.addDeploymentWithRestartPolicy(alwaysRestartPolicy, coreV1.RestartPolicyAlways) + s.addDeploymentWithRestartPolicy(onFailureRestartPolicy, coreV1.RestartPolicyOnFailure) + + s.Validate(s.ctx, []templates.TestCase{ + { + Param: params.Params{}, + Diagnostics: map[string][]diagnostic.Diagnostic{ + alwaysRestartPolicy: nil, + onFailureRestartPolicy: nil, + }, + ExpectInstantiationError: false, + }, + }) +} + +func (s *RestartPolicyTestSuite) TestObjectWithoutPodSpec() { + const ( + objectWithoutPodSpec = "object-without-pod-spec" + ) + + s.addObjectWithoutPodSpec(objectWithoutPodSpec) + + s.Validate(s.ctx, []templates.TestCase{ + { + Param: params.Params{}, + Diagnostics: map[string][]diagnostic.Diagnostic{ + objectWithoutPodSpec: nil, + }, + ExpectInstantiationError: false, + }, + }) +} diff --git a/tests/checks/restart-policy.yaml b/tests/checks/restart-policy.yaml new file mode 100644 index 000000000..7e4ecb9b3 --- /dev/null +++ b/tests/checks/restart-policy.yaml @@ -0,0 +1,189 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dont-fire-deployment-always +spec: + template: + spec: + restartPolicy: Always +--- +apiVersion: v1 +kind: Pod +metadata: + name: dont-fire-pod-always +spec: + restartPolicy: Always +--- +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: dont-fire-daemonset-always +spec: + template: + spec: + restartPolicy: Always +--- +apiVersion: apps/v1 +kind: ReplicaSet +metadata: + name: dont-fire-replicaset-always +spec: + template: + spec: + restartPolicy: Always +--- +apiVersion: v1 +kind: ReplicationController +metadata: + name: dont-fire-replicationcontroller-always +spec: + template: + spec: + restartPolicy: Always +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: dont-fire-job-always +spec: + template: + spec: + restartPolicy: Always +--- +apiVersion: batch/v1 +kind: CronJob +metadata: + name: dont-fire-cronjob-always +spec: + jobTemplate: + spec: + template: + spec: + restartPolicy: Always +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dont-fire-deployment-on-failure +spec: + template: + spec: + restartPolicy: OnFailure +--- +apiVersion: v1 +kind: Pod +metadata: + name: dont-fire-pod-on-failure +spec: + restartPolicy: OnFailure +--- +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: dont-fire-daemonset-on-failure +spec: + template: + spec: + restartPolicy: OnFailure +--- +apiVersion: apps/v1 +kind: ReplicaSet +metadata: + name: dont-fire-replicaset-on-failure +spec: + template: + spec: + restartPolicy: OnFailure +--- +apiVersion: v1 +kind: ReplicationController +metadata: + name: dont-fire-replicationcontroller-on-failure +spec: + template: + spec: + restartPolicy: OnFailure +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: dont-fire-job-on-failure +spec: + template: + spec: + restartPolicy: OnFailure +--- +apiVersion: batch/v1 +kind: CronJob +metadata: + name: dont-fire-cronjob-on-failure +spec: + jobTemplate: + spec: + template: + spec: + restartPolicy: OnFailure +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: fire-deployment-never +spec: + template: + spec: + restartPolicy: Never +--- +apiVersion: v1 +kind: Pod +metadata: + name: fire-pod-never +spec: + restartPolicy: Never +--- +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: fire-daemonset-never +spec: + template: + spec: + restartPolicy: Never +--- +apiVersion: apps/v1 +kind: ReplicaSet +metadata: + name: fire-replicaset-never +spec: + template: + spec: + restartPolicy: Never +--- +apiVersion: v1 +kind: ReplicationController +metadata: + name: fire-replicationcontroller-never +spec: + template: + spec: + restartPolicy: Never +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: fire-job-never +spec: + template: + spec: + restartPolicy: Never +--- +apiVersion: batch/v1 +kind: CronJob +metadata: + name: fire-cronjob-never +spec: + jobTemplate: + spec: + template: + spec: + restartPolicy: Never \ No newline at end of file