diff --git a/docs/generated/checks.md b/docs/generated/checks.md index 759376d7c..5dda3a440 100644 --- a/docs/generated/checks.md +++ b/docs/generated/checks.md @@ -455,6 +455,23 @@ strategyTypeRegex: ^(RollingUpdate|Rolling)$ **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) +## priority-class-name + +**Enabled by default**: No + +**Description**: Indicates when a deployment-like object does not use a valid priority class name + +**Remediation**: Set up the priority class name for your object to any accepted values. + +**Template**: [priority-class-name](templates.md#priority-class-name) + +**Parameters**: + +```yaml +acceptedPriorityClassNames: +- system-cluster-critical +- system-node-critical +``` ## privilege-escalation-container **Enabled by default**: Yes diff --git a/docs/generated/templates.md b/docs/generated/templates.md index f246cf1b9..7cffef8fa 100644 --- a/docs/generated/templates.md +++ b/docs/generated/templates.md @@ -597,6 +597,27 @@ KubeLinter supports the following templates: type: string ``` +## Priority class name + +**Key**: `priority-class-name` + +**Description**: Flag applications running with invalid priority class name. + +**Supported Objects**: DeploymentLike + + +**Parameters**: + +```yaml +- arrayElemType: string + description: Array of all priority class names that are accepted. + name: acceptedPriorityClassNames + negationAllowed: false + regexAllowed: false + required: false + type: array +``` + ## Privilege Escalation on Containers **Key**: `privilege-escalation-container` diff --git a/e2etests/bats-tests.sh b/e2etests/bats-tests.sh index cacc1ea41..38e18ce54 100755 --- a/e2etests/bats-tests.sh +++ b/e2etests/bats-tests.sh @@ -662,6 +662,30 @@ get_value_from() { } +@test "priority-class-name" { + tmp="tests/checks/priority-class-name.yaml" + cmd="${KUBE_LINTER_BIN} lint --include priority-class-name --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: object has a priority class name defined with 'fire' but the only accepted priority class names are '[system-cluster-critical system-node-critical]'" ]] + [[ "${message2}" == "Pod fire-pod: object has a priority class name defined with 'fire' but the only accepted priority class names are '[system-cluster-critical system-node-critical]'" ]] + [[ "${message3}" == "DaemonSet fire-daemonset: object has a priority class name defined with 'fire' but the only accepted priority class names are '[system-cluster-critical system-node-critical]'" ]] + [[ "${message4}" == "ReplicaSet fire-replicaset: object has a priority class name defined with 'fire' but the only accepted priority class names are '[system-cluster-critical system-node-critical]'" ]] + [[ "${message5}" == "ReplicationController fire-replicationcontroller: object has a priority class name defined with 'fire' but the only accepted priority class names are '[system-cluster-critical system-node-critical]'" ]] + [[ "${message6}" == "Job fire-job: object has a priority class name defined with 'fire' but the only accepted priority class names are '[system-cluster-critical system-node-critical]'" ]] + [[ "${message7}" == "CronJob fire-cronjob: object has a priority class name defined with 'fire' but the only accepted priority class names are '[system-cluster-critical system-node-critical]'" ]] + [[ "${count}" == "7" ]] +} + @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/pkg/builtinchecks/yamls/priority-class-name.yaml b/pkg/builtinchecks/yamls/priority-class-name.yaml new file mode 100644 index 000000000..5fc5c9b5b --- /dev/null +++ b/pkg/builtinchecks/yamls/priority-class-name.yaml @@ -0,0 +1,10 @@ +name: "priority-class-name" +description: "Indicates when a deployment-like object does not use a valid priority class name" +remediation: >- + Set up the priority class name for your object to any accepted values. +scope: + objectKinds: + - DeploymentLike +template: "priority-class-name" +params: + acceptedPriorityClassNames: ["system-cluster-critical", "system-node-critical"] \ No newline at end of file diff --git a/pkg/templates/all/all.go b/pkg/templates/all/all.go index e941d98fb..964fdcc91 100644 --- a/pkg/templates/all/all.go +++ b/pkg/templates/all/all.go @@ -38,6 +38,7 @@ import ( _ "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/priorityclassname" _ "golang.stackrox.io/kube-linter/pkg/templates/privileged" _ "golang.stackrox.io/kube-linter/pkg/templates/privilegedports" _ "golang.stackrox.io/kube-linter/pkg/templates/privilegeescalation" diff --git a/pkg/templates/priorityclassname/internal/params/gen-params.go b/pkg/templates/priorityclassname/internal/params/gen-params.go new file mode 100644 index 000000000..81a7c43d6 --- /dev/null +++ b/pkg/templates/priorityclassname/internal/params/gen-params.go @@ -0,0 +1,69 @@ +// 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 + + acceptedPriorityClassNamesParamDesc = util.MustParseParameterDesc(`{ + "Name": "acceptedPriorityClassNames", + "Type": "array", + "Description": "Array of all priority class names that are accepted.", + "Examples": null, + "Enum": null, + "SubParameters": null, + "ArrayElemType": "string", + "Required": false, + "NoRegex": true, + "NotNegatable": true, + "XXXStructFieldName": "AcceptedPriorityClassNames", + "XXXIsPointer": false +} +`) + + ParamDescs = []check.ParameterDesc{ + acceptedPriorityClassNamesParamDesc, + } +) + +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/priorityclassname/internal/params/params.go b/pkg/templates/priorityclassname/internal/params/params.go new file mode 100644 index 000000000..9844fec6f --- /dev/null +++ b/pkg/templates/priorityclassname/internal/params/params.go @@ -0,0 +1,9 @@ +package params + +// Params represents the params accepted by this template. +type Params struct { + // Array of all priority class names that are accepted. + // +noregex + // +notnegatable + AcceptedPriorityClassNames []string `json:"acceptedPriorityClassNames"` +} diff --git a/pkg/templates/priorityclassname/template.go b/pkg/templates/priorityclassname/template.go new file mode 100644 index 000000000..9854a6bfa --- /dev/null +++ b/pkg/templates/priorityclassname/template.go @@ -0,0 +1,47 @@ +package priorityclassname + +import ( + "fmt" + "strings" + + "slices" + + "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/priorityclassname/internal/params" +) + +const ( + templateKey = "priority-class-name" +) + +func init() { + templates.Register(check.Template{ + HumanName: "Priority class name", + Key: templateKey, + Description: "Flag applications running with invalid priority class name.", + 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) + isEmpty := strings.TrimSpace(spec.PriorityClassName) == "" + isAccepted := slices.Contains(p.AcceptedPriorityClassNames, spec.PriorityClassName) + if !found || isEmpty || isAccepted { + return nil + } + return []diagnostic.Diagnostic{ + {Message: fmt.Sprintf("object has a priority class name defined with '%s' but the only accepted priority class names are '%s'", spec.PriorityClassName, p.AcceptedPriorityClassNames)}, + } + }, nil + }), + }) +} diff --git a/pkg/templates/priorityclassname/template_test.go b/pkg/templates/priorityclassname/template_test.go new file mode 100644 index 000000000..bd4c8fbc8 --- /dev/null +++ b/pkg/templates/priorityclassname/template_test.go @@ -0,0 +1,115 @@ +package priorityclassname + +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/priorityclassname/internal/params" + appsV1 "k8s.io/api/apps/v1" +) + +func TestPriorityClassName(t *testing.T) { + suite.Run(t, new(PriorityClassNameTestSuite)) +} + +type PriorityClassNameTestSuite struct { + templates.TemplateTestSuite + + ctx *mocks.MockLintContext +} + +func (s *PriorityClassNameTestSuite) SetupTest() { + s.Init(templateKey) + s.ctx = mocks.NewMockContext() +} + +func (s *PriorityClassNameTestSuite) addDeploymentWithPriorityClassName(name, priorityClassName string) { + s.ctx.AddMockDeployment(s.T(), name) + s.ctx.ModifyDeployment(s.T(), name, func(deployment *appsV1.Deployment) { + deployment.Spec.Template.Spec.PriorityClassName = priorityClassName + }) +} + +func (s *PriorityClassNameTestSuite) addDeploymentWithEmptyPriorityClassName(name string) { + s.ctx.AddMockDeployment(s.T(), name) + s.ctx.ModifyDeployment(s.T(), name, func(deployment *appsV1.Deployment) { + deployment.Spec.Template.Spec.PriorityClassName = "" + }) +} + +func (s *PriorityClassNameTestSuite) addDeploymentWithoutPriorityClassName(name string) { + s.ctx.AddMockDeployment(s.T(), name) +} + +func (s *PriorityClassNameTestSuite) addObjectWithoutPodSpec(name string) { + s.ctx.AddMockService(s.T(), name) +} + +func (s *PriorityClassNameTestSuite) TestInvalidPriorityClassName() { + const ( + invalidPriorityClassName = "invalid-priority-class-name" + ) + + s.addDeploymentWithPriorityClassName(invalidPriorityClassName, "system-node-critical") + + s.Validate(s.ctx, []templates.TestCase{ + { + Param: params.Params{ + AcceptedPriorityClassNames: []string{"system-cluster-critical", "custom-priority-class-name"}, + }, + Diagnostics: map[string][]diagnostic.Diagnostic{ + invalidPriorityClassName: { + {Message: "object has a priority class name defined with 'system-node-critical' but the only accepted priority class names are '[system-cluster-critical custom-priority-class-name]'"}, + }, + }, + ExpectInstantiationError: false, + }, + }) +} + +func (s *PriorityClassNameTestSuite) TestAcceptablePriorityClassName() { + const ( + validPriorityClassName = "valid-priority-class-name" + emptyPriorityClassName = "empty-priotity-class-name" + withoutPriorityClassName = "without-piority-class-name" + ) + + s.addDeploymentWithPriorityClassName(validPriorityClassName, "system-cluster-critical") + s.addDeploymentWithEmptyPriorityClassName(emptyPriorityClassName) + s.addDeploymentWithoutPriorityClassName(withoutPriorityClassName) + + s.Validate(s.ctx, []templates.TestCase{ + { + Param: params.Params{ + AcceptedPriorityClassNames: []string{"system-cluster-critical", "custom-priority-class-name"}, + }, + Diagnostics: map[string][]diagnostic.Diagnostic{ + validPriorityClassName: nil, + emptyPriorityClassName: nil, + withoutPriorityClassName: nil, + }, + ExpectInstantiationError: false, + }, + }) +} + +func (s *PriorityClassNameTestSuite) 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/priority-class-name.yaml b/tests/checks/priority-class-name.yaml new file mode 100644 index 000000000..6e5f853cb --- /dev/null +++ b/tests/checks/priority-class-name.yaml @@ -0,0 +1,126 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dont-fire-deployment +spec: + template: + spec: + priorityClassName: system-cluster-critical +--- +apiVersion: v1 +kind: Pod +metadata: + name: dont-fire-pod +spec: + priorityClassName: system-cluster-critical +--- +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: dont-fire-daemonset +spec: + template: + spec: + priorityClassName: system-cluster-critical +--- +apiVersion: apps/v1 +kind: ReplicaSet +metadata: + name: dont-fire-replicaset +spec: + template: + spec: + priorityClassName: system-cluster-critical +--- +apiVersion: v1 +kind: ReplicationController +metadata: + name: dont-fire-replicationcontroller +spec: + template: + spec: + priorityClassName: system-cluster-critical +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: dont-fire-job +spec: + template: + spec: + priorityClassName: system-cluster-critical +--- +apiVersion: batch/v1 +kind: CronJob +metadata: + name: dont-fire-cronjob +spec: + jobTemplate: + spec: + template: + spec: + priorityClassName: system-cluster-critical +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: fire-deployment +spec: + template: + spec: + priorityClassName: fire +--- +apiVersion: v1 +kind: Pod +metadata: + name: fire-pod +spec: + priorityClassName: fire +--- +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: fire-daemonset +spec: + template: + spec: + priorityClassName: fire +--- +apiVersion: apps/v1 +kind: ReplicaSet +metadata: + name: fire-replicaset +spec: + template: + spec: + priorityClassName: fire +--- +apiVersion: v1 +kind: ReplicationController +metadata: + name: fire-replicationcontroller +spec: + template: + spec: + priorityClassName: fire +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: fire-job +spec: + template: + spec: + priorityClassName: fire +--- +apiVersion: batch/v1 +kind: CronJob +metadata: + name: fire-cronjob +spec: + jobTemplate: + spec: + template: + spec: + priorityClassName: fire \ No newline at end of file