diff --git a/.circleci/config.yml b/.circleci/config.yml
index 7979824ad..db946359d 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -22,9 +22,9 @@ jobs:
make lint
- run:
- name: Ensure generated docs are up-to-date
+ name: Ensure generated files are up-to-date
command: |
- make generated-docs
+ make generated-srcs
git diff --exit-code HEAD
test:
diff --git a/Makefile b/Makefile
index 0cb17e777..bf06cb8a4 100644
--- a/Makefile
+++ b/Makefile
@@ -70,11 +70,18 @@ lint: golangci-lint staticcheck
## Code generation #
####################
+.PHONY: go-generated-srcs
+go-generated-srcs: deps
+ go generate ./...
+
.PHONY: generated-docs
-generated-docs: build
+generated-docs: go-generated-srcs build
kube-linter templates list --format markdown > docs/generated/templates.md
kube-linter checks list --format markdown > docs/generated/checks.md
+.PHONY: generated-srcs
+generated-srcs: go-generated-srcs generated-docs
+
.PHONY: packr
packr: $(PACKR_BIN)
packr
diff --git a/docs/generated/checks.md b/docs/generated/checks.md
index 39d949f86..8867b253d 100644
--- a/docs/generated/checks.md
+++ b/docs/generated/checks.md
@@ -2,8 +2,9 @@ The following table enumerates built-in checks:
| Name | Enabled by default | Description | Template | Parameters |
| ---- | ------------------ | ----------- | -------- | ---------- |
- | env-var-secret | Yes | Alert on objects using a secret in an environment variable | env-var |- `name`: `.*secret.*`
|
- | no-read-only-root-fs | Yes | Alert on containers not running with a read-only root filesystem | read-only-root-fs | none |
- | privileged-container | Yes | Alert on deployments with containers running in privileged mode | privileged | none |
- | required-label-owner | No | Alert on objects without the 'owner' label | required-label |- `key`: `owner`
|
- | run-as-non-root | Yes | Alert on containers not set to runAsNonRoot | run-as-non-root | none |
+ | env-var-secret | Yes | Alert on objects using a secret in an environment variable | env-var | `{"name":".*secret.*"}` |
+ | no-extensions-v1beta | Yes | Alert on objects using deprecated API versions under extensions v1beta | disallowed-api-obj | `{"group":"extensions","version":"v1beta.+"}` |
+ | no-read-only-root-fs | Yes | Alert on containers not running with a read-only root filesystem | read-only-root-fs | `{}` |
+ | privileged-container | Yes | Alert on deployments with containers running in privileged mode | privileged | `{}` |
+ | required-label-owner | No | Alert on objects without the 'owner' label | required-label | `{"key":"owner"}` |
+ | run-as-non-root | Yes | Alert on containers not set to runAsNonRoot | run-as-non-root | `{}` |
diff --git a/docs/generated/templates.md b/docs/generated/templates.md
index 627a4d7c8..a8194c29d 100644
--- a/docs/generated/templates.md
+++ b/docs/generated/templates.md
@@ -1,9 +1,156 @@
-The following table enumerates supported check templates:
-
-| Name | Description | Supported Objects | Parameters |
-| ---- | ----------- | ----------------- | ---------- |
- | env-var | Flag environment variables that match the provided patterns | DeploymentLike |- `name` (required): A regex for the env var name
- `value`: A regex for the env var value
|
- | privileged | Flag privileged containers | DeploymentLike | none |
- | read-only-root-fs | Flag containers without read-only root file systems | DeploymentLike | none |
- | required-label | Flag objects not carrying at least one label matching the provided patterns | Any |- `key` (required): A regex for the key of the required label
- `value`: A regex for the value of the required label
|
- | run-as-non-root | Flag containers set to run as a root user | DeploymentLike | none |
+This page lists supported check templates.
+
+## Disallowed API Objects
+
+**Key**: `disallowed-api-obj`
+
+**Description**: Flag disallowed API object kinds
+
+**Supported Objects**: Any
+
+**Parameters**:
+```
+[
+ {
+ "name": "group",
+ "type": "string",
+ "description": "The disallowed object group.",
+ "required": false,
+ "examples": [
+ "apps"
+ ],
+ "regexAllowed": true,
+ "negationAllowed": true
+ },
+ {
+ "name": "version",
+ "type": "string",
+ "description": "The disallowed object API version.",
+ "required": false,
+ "examples": [
+ "v1",
+ "v1beta1"
+ ],
+ "regexAllowed": true,
+ "negationAllowed": true
+ },
+ {
+ "name": "kind",
+ "type": "string",
+ "description": "The disallowed kind.",
+ "required": false,
+ "examples": [
+ "Deployment",
+ "DaemonSet"
+ ],
+ "regexAllowed": true,
+ "negationAllowed": true
+ }
+]
+
+```
+
+## Environment Variables
+
+**Key**: `env-var`
+
+**Description**: Flag environment variables that match the provided patterns
+
+**Supported Objects**: DeploymentLike
+
+**Parameters**:
+```
+[
+ {
+ "name": "name",
+ "type": "string",
+ "description": "The name of the environment variable.",
+ "required": true,
+ "regexAllowed": true,
+ "negationAllowed": true
+ },
+ {
+ "name": "value",
+ "type": "string",
+ "description": "The value of the environment variable.",
+ "required": false,
+ "regexAllowed": true,
+ "negationAllowed": true
+ }
+]
+
+```
+
+## Privileged Containers
+
+**Key**: `privileged`
+
+**Description**: Flag privileged containers
+
+**Supported Objects**: DeploymentLike
+
+**Parameters**:
+```
+[]
+
+```
+
+## Read-only Root Filesystems
+
+**Key**: `read-only-root-fs`
+
+**Description**: Flag containers without read-only root file systems
+
+**Supported Objects**: DeploymentLike
+
+**Parameters**:
+```
+[]
+
+```
+
+## Required Label
+
+**Key**: `required-label`
+
+**Description**: Flag objects not carrying at least one label matching the provided patterns
+
+**Supported Objects**: Any
+
+**Parameters**:
+```
+[
+ {
+ "name": "key",
+ "type": "string",
+ "description": "Key of the required label.",
+ "required": true,
+ "regexAllowed": true,
+ "negationAllowed": true
+ },
+ {
+ "name": "value",
+ "type": "string",
+ "description": "Value of the required label.",
+ "required": false,
+ "regexAllowed": true,
+ "negationAllowed": true
+ }
+]
+
+```
+
+## Run as non-root user
+
+**Key**: `run-as-non-root`
+
+**Description**: Flag containers set to run as a root user
+
+**Supported Objects**: DeploymentLike
+
+**Parameters**:
+```
+[]
+
+```
+
diff --git a/go.mod b/go.mod
index e49d881da..a66a1129e 100644
--- a/go.mod
+++ b/go.mod
@@ -8,6 +8,7 @@ require (
github.com/ghodss/yaml v1.0.0
github.com/gobuffalo/packr v1.30.1
github.com/golangci/golangci-lint v1.30.0
+ github.com/mitchellh/mapstructure v1.1.2
github.com/pkg/errors v0.9.1
github.com/spf13/cobra v1.0.0
github.com/stretchr/objx v0.2.0 // indirect
@@ -18,4 +19,5 @@ require (
k8s.io/api v0.19.1
k8s.io/apimachinery v0.19.1
k8s.io/client-go v0.19.0
+ k8s.io/gengo v0.0.0-20200728071708-7794989d0000
)
diff --git a/go.sum b/go.sum
index ab0ed8b89..780a0e81e 100644
--- a/go.sum
+++ b/go.sum
@@ -671,6 +671,7 @@ golang.org/x/tools v0.0.0-20200321224714-0d839f3cf2ed/go.mod h1:Sl4aGygMT6LrqrWc
golang.org/x/tools v0.0.0-20200324003944-a576cf524670/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200414032229-332987a829c3/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200422022333-3d57cf2e726e/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200519015757-0d0afa43d58a/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200625211823-6506e20df31f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200626171337-aa94e735be7f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
@@ -765,10 +766,13 @@ k8s.io/apimachinery v0.19.1/go.mod h1:DnPGDnARWFvYa3pMHgSxtbZb7gpzzAZ1pTfaUNDVlm
k8s.io/client-go v0.19.0 h1:1+0E0zfWFIWeyRhQYWzimJOyAk2UT7TiARaLNwJCf7k=
k8s.io/client-go v0.19.0/go.mod h1:H9E/VT95blcFQnlyShFgnFT9ZnJOAceiUHM3MlRC+mU=
k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
+k8s.io/gengo v0.0.0-20200728071708-7794989d0000 h1:XgICMZutMLbopSVIJJrhUun6Hbuh1NTZBv2sd0lvypU=
+k8s.io/gengo v0.0.0-20200728071708-7794989d0000/go.mod h1:aG2eeomYfcUw8sE3fa7YdkjgnGtyY56TjZlaJJ0ZoWo=
k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=
k8s.io/klog/v2 v2.2.0 h1:XRvcwJozkgZ1UQJmfMGpvRthQHOvihEhYtDfAaxMz/A=
k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6/go.mod h1:UuqjUnNftUyPE5H64/qeyjQoUZhGpeFDVdxjTeEVN2o=
+k8s.io/utils v0.0.0-20200729134348-d5654de09c73 h1:uJmqzgNWG7XyClnU/mLPBWwfKKF1K8Hf8whTseBgJcg=
k8s.io/utils v0.0.0-20200729134348-d5654de09c73/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
mvdan.cc/gofumpt v0.0.0-20200709182408-4fd085cb6d5f h1:gi7cb8HTDZ6q8VqsUpkdoFi3vxwHMneQ6+Q5Ap5hjPE=
mvdan.cc/gofumpt v0.0.0-20200709182408-4fd085cb6d5f/go.mod h1:9VQ397fNXEnF84t90W4r4TRCQK+pg9f8ugVfyj+S26w=
diff --git a/internal/builtinchecks/yamls/no-extensions-v1beta.yaml b/internal/builtinchecks/yamls/no-extensions-v1beta.yaml
new file mode 100644
index 000000000..f7b7b46c9
--- /dev/null
+++ b/internal/builtinchecks/yamls/no-extensions-v1beta.yaml
@@ -0,0 +1,9 @@
+name: "no-extensions-v1beta"
+description: "Alert on objects using deprecated API versions under extensions v1beta"
+scope:
+ objectKinds:
+ - Any
+template: "disallowed-api-obj"
+params:
+ group: "extensions"
+ version: "v1beta.+"
diff --git a/internal/check/check.go b/internal/check/check.go
index 062b7555e..55d22aa86 100644
--- a/internal/check/check.go
+++ b/internal/check/check.go
@@ -2,9 +2,9 @@ package check
// A Check represents a single check. It is serializable.
type Check struct {
- Name string `json:"name"`
- Description string `json:"description"`
- Scope *ObjectKindsDesc `json:"scope"`
- Template string `json:"template"`
- Params map[string]string `json:"params,omitempty"`
+ Name string `json:"name"`
+ Description string `json:"description"`
+ Scope *ObjectKindsDesc `json:"scope"`
+ Template string `json:"template"`
+ Params map[string]interface{} `json:"params,omitempty"`
}
diff --git a/internal/check/parameter_desc.go b/internal/check/parameter_desc.go
new file mode 100644
index 000000000..41eadcfaa
--- /dev/null
+++ b/internal/check/parameter_desc.go
@@ -0,0 +1,85 @@
+package check
+
+import (
+ "golang.stackrox.io/kube-linter/internal/pointers"
+)
+
+// ParameterType represents the expected type of a particular parameter.
+type ParameterType string
+
+// This block enumerates all known type names.
+// These type names are chosen to be aligned with OpenAPI/JSON schema.
+const (
+ StringType ParameterType = "string"
+ IntegerType ParameterType = "integer"
+ BooleanType ParameterType = "boolean"
+ NumberType ParameterType = "number"
+ ObjectType ParameterType = "object"
+)
+
+// ParameterDesc describes a parameter.
+type ParameterDesc struct {
+ Name string
+ Type ParameterType
+ Description string
+
+ Examples []string
+
+ // SubParameters are the child parameters of the given parameter.
+ // Only relevant if Type is "object".
+ SubParameters []ParameterDesc
+
+ // Required denotes whether the parameter is required.
+ Required bool
+
+ // NoRegex is set if the parameter does not support regexes.
+ // Only relevant if Type is "string".
+ NoRegex bool
+
+ // NotNegatable is set if the parameter does not support negation via a leading !.
+ // OnlyRelevant if Type is "string".
+ NotNegatable bool
+
+ // Fields below are for internal use only.
+
+ XXXStructFieldName string
+}
+
+// HumanReadableParamDesc is a human-friendly representation of a ParameterDesc.
+// It is intended only for API documentation/JSON marshaling, and must NOT be used for
+// any business logic.
+type HumanReadableParamDesc struct {
+ Name string `json:"name"`
+ Type ParameterType `json:"type"`
+ Description string `json:"description"`
+ Required bool `json:"required"`
+ Examples []string `json:"examples,omitempty"`
+ RegexAllowed *bool `json:"regexAllowed,omitempty"`
+ NegationAllowed *bool `json:"negationAllowed,omitempty"`
+ SubParameters []HumanReadableParamDesc `json:"subParameters,omitempty"`
+}
+
+// HumanReadableFields returns a human-friendly representation of this ParameterDesc.
+func (p *ParameterDesc) HumanReadableFields() HumanReadableParamDesc {
+ out := HumanReadableParamDesc{
+ Name: p.Name,
+ Type: p.Type,
+ Description: p.Description,
+ Required: p.Required,
+ Examples: p.Examples,
+ }
+
+ if p.Type == StringType {
+ out.RegexAllowed = pointers.Bool(!p.NoRegex)
+ out.NegationAllowed = pointers.Bool(!p.NotNegatable)
+ }
+
+ if len(p.SubParameters) > 0 {
+ subParamFields := make([]HumanReadableParamDesc, 0, len(p.SubParameters))
+ for _, subParam := range p.SubParameters {
+ subParamFields = append(subParamFields, subParam.HumanReadableFields())
+ }
+ out.SubParameters = subParamFields
+ }
+ return out
+}
diff --git a/internal/check/template.go b/internal/check/template.go
index 15b44c088..d92278da0 100644
--- a/internal/check/template.go
+++ b/internal/check/template.go
@@ -10,13 +10,6 @@ import (
// object passed in the second argument.
type Func func(lintCtx *lintcontext.LintContext, object lintcontext.Object) []diagnostic.Diagnostic
-// A ParameterDesc describes a parameter to a check template.
-type ParameterDesc struct {
- ParamName string
- Required bool
- Description string
-}
-
// ObjectKindsDesc describes a list of supported object kinds for a check template.
type ObjectKindsDesc struct {
ObjectKinds []string `json:"objectKinds"`
@@ -24,9 +17,15 @@ type ObjectKindsDesc struct {
// A Template is a template for a check.
type Template struct {
- Name string
+ // HumanName is a human-friendly name for the template.
+ // It is to be used ONLY for documentation, and has no
+ // semantic relevance.
+ HumanName string
+ Key string
Description string
SupportedObjectKinds ObjectKindsDesc
- Parameters []ParameterDesc
- Instantiate func(params map[string]string) (Func, error)
+
+ Parameters []ParameterDesc
+ ParseAndValidateParams func(params map[string]interface{}) (interface{}, error)
+ Instantiate func(parsedParams interface{}) (Func, error)
}
diff --git a/internal/command/checks/command.go b/internal/command/checks/command.go
index d0ae7093f..3eb351364 100644
--- a/internal/command/checks/command.go
+++ b/internal/command/checks/command.go
@@ -40,17 +40,13 @@ const (
| Name | Enabled by default | Description | Template | Parameters |
| ---- | ------------------ | ----------- | -------- | ---------- |
-{{ range . }} | {{ .Check.Name}} | {{ if .Default }}Yes{{ else }}No{{ end }} | {{.Check.Description}} | {{.Check.Template}} |
-{{- range $key, $value := .Check.Params -}}
-- {{backtick}}{{$key}}{{backtick}}: {{backtick}}{{$value}}{{backtick}}
-{{- else }} none {{ end -}}
-|
+{{ range . }} | {{ .Check.Name}} | {{ if .Default }}Yes{{ else }}No{{ end }} | {{.Check.Description}} | {{.Check.Template}} | {{ backtick }}{{ mustToJson (default (dict) .Check.Params ) }}{{ backtick }} |
{{ end -}}
`
)
var (
- markDownTemplate = common.MustInstantiateTemplate(markDownTemplateStr)
+ markDownTemplate = common.MustInstantiateTemplate(markDownTemplateStr, nil)
)
func renderMarkdown(checks []check.Check, out io.Writer) error {
diff --git a/internal/command/common/markdown.go b/internal/command/common/markdown.go
index fe0b3d085..e5e865dc8 100644
--- a/internal/command/common/markdown.go
+++ b/internal/command/common/markdown.go
@@ -9,14 +9,14 @@ import (
// MustInstantiateTemplate instanties the given go template with a common list of
// functions. It panics if there is an error.
-func MustInstantiateTemplate(templateStr string) *template.Template {
+func MustInstantiateTemplate(templateStr string, customFuncMap template.FuncMap) *template.Template {
tpl, err := template.New("").Funcs(sprig.TxtFuncMap()).Funcs(
template.FuncMap{
"backtick": func() string {
return "`"
},
},
- ).Parse(templateStr)
+ ).Funcs(customFuncMap).Parse(templateStr)
utils.Must(err)
return tpl
diff --git a/internal/command/templates/command.go b/internal/command/templates/command.go
index 5b591aee2..8120db05f 100644
--- a/internal/command/templates/command.go
+++ b/internal/command/templates/command.go
@@ -1,9 +1,13 @@
package templates
import (
+ "bytes"
+ "encoding/json"
"fmt"
"io"
"os"
+ "strings"
+ "text/template"
"github.com/pkg/errors"
"github.com/spf13/cobra"
@@ -23,33 +27,70 @@ var (
)
const (
- markDownTemplateStr = `The following table enumerates supported check templates:
-
-| Name | Description | Supported Objects | Parameters |
-| ---- | ----------- | ----------------- | ---------- |
-{{ range . }} | {{ .Name}} | {{ .Description }} | {{ join "," .SupportedObjectKinds.ObjectKinds }} |
-{{- range .Parameters -}}
-- {{backtick}}{{.ParamName}}{{backtick}}{{ if .Required }} (required){{ end }}: {{ .Description }}
-{{- else }} none {{ end -}}
-|
+ markDownTemplateStr = `This page lists supported check templates.
+
+{{ range . -}}
+## {{ .HumanName }}
+
+**Key**: {{ backtick }}{{ .Key }}{{ backtick }}
+
+**Description**: {{ .Description }}
+
+**Supported Objects**: {{ join "," .SupportedObjectKinds.ObjectKinds }}
+
+**Parameters**:
+{{ backtick }}{{ backtick }}{{ backtick }}
+{{ getParametersJSON .Parameters }}
+{{ backtick }}{{ backtick }}{{ backtick }}
+
{{ end -}}
`
)
var (
- markDownTemplate = common.MustInstantiateTemplate(markDownTemplateStr)
+ markDownTemplate = common.MustInstantiateTemplate(markDownTemplateStr, template.FuncMap{
+ "getParametersJSON": func(params []check.ParameterDesc) (string, error) {
+ out := make([]check.HumanReadableParamDesc, 0, len(params))
+ for _, param := range params {
+ out = append(out, param.HumanReadableFields())
+ }
+ var buf bytes.Buffer
+ enc := json.NewEncoder(&buf)
+ enc.SetIndent("", "\t")
+ if err := enc.Encode(out); err != nil {
+ return "", err
+ }
+ return buf.String(), nil
+ },
+ })
)
+func renderParameters(numTabs int, params []check.ParameterDesc, out io.Writer) {
+ tabs := stringutils.Repeat("\t", numTabs)
+ for _, param := range params {
+ fmt.Fprintf(out, "%s%s:\n%s\tDescription: %s\n%s\tRequired: %v\n", tabs, param.Name, tabs, param.Description, tabs, param.Required)
+ if len(param.Examples) > 0 {
+ quotedExamples := make([]string, 0, len(param.Examples))
+ for _, ex := range param.Examples {
+ quotedExamples = append(quotedExamples, fmt.Sprintf(`"%s"`, ex))
+ }
+ fmt.Fprintf(out, "%s\tExample values: %s\n", tabs, strings.Join(quotedExamples, ", "))
+ }
+ if len(param.SubParameters) > 0 {
+ fmt.Fprintf(out, "%s\tSub-parameters:\n", tabs)
+ renderParameters(numTabs+1, param.SubParameters, out)
+ }
+ }
+}
+
func renderPlain(templates []check.Template, out io.Writer) error { //nolint:unparam // The function signature is required to match formatToRenderFuncs
for i, template := range templates {
- fmt.Fprintf(out, "Name: %s\nDescription: %s\nSupported Objects: %v\n", template.Name, template.Description, template.SupportedObjectKinds.ObjectKinds)
+ fmt.Fprintf(out, "Name: %s\nKey: %s\nDescription: %s\nSupported Objects: %v\n", template.HumanName, template.Key, template.Description, template.SupportedObjectKinds.ObjectKinds)
if len(template.Parameters) == 0 {
fmt.Fprintln(out, "Parameters: none")
} else {
fmt.Fprintf(out, "Parameters:\n")
- for _, param := range template.Parameters {
- fmt.Fprintf(out, "\t%s:\n\t\tDescription: %s\n\t\tRequired: %v\n", param.ParamName, param.Description, param.Required)
- }
+ renderParameters(1, template.Parameters, out)
}
if i != len(templates)-1 {
fmt.Fprintf(out, "\n%s\n\n", dashes)
diff --git a/internal/defaultchecks/default_checks.go b/internal/defaultchecks/default_checks.go
index 53ec130e9..04d351465 100644
--- a/internal/defaultchecks/default_checks.go
+++ b/internal/defaultchecks/default_checks.go
@@ -11,5 +11,6 @@ var (
"env-var-secret",
"no-read-only-root-fs",
"run-as-non-root",
+ "no-extensions-v1beta",
)
)
diff --git a/internal/extract/gvk.go b/internal/extract/gvk.go
new file mode 100644
index 000000000..47da02d2f
--- /dev/null
+++ b/internal/extract/gvk.go
@@ -0,0 +1,11 @@
+package extract
+
+import (
+ "golang.stackrox.io/kube-linter/internal/k8sutil"
+ "k8s.io/apimachinery/pkg/runtime/schema"
+)
+
+// GVK extracts the GroupVersionKind of an object.
+func GVK(object k8sutil.Object) schema.GroupVersionKind {
+ return object.GetObjectKind().GroupVersionKind()
+}
diff --git a/internal/instantiatedcheck/instantiated_check.go b/internal/instantiatedcheck/instantiated_check.go
index f7ffe28d6..fb10d2967 100644
--- a/internal/instantiatedcheck/instantiated_check.go
+++ b/internal/instantiatedcheck/instantiated_check.go
@@ -5,7 +5,6 @@ import (
"golang.stackrox.io/kube-linter/internal/check"
"golang.stackrox.io/kube-linter/internal/errorhelpers"
"golang.stackrox.io/kube-linter/internal/objectkinds"
- "golang.stackrox.io/kube-linter/internal/set"
"golang.stackrox.io/kube-linter/internal/templates"
)
@@ -30,20 +29,11 @@ func ValidateAndInstantiate(c *check.Check) (*InstantiatedCheck, error) {
return nil, validationErrs.ToError()
}
- supportedParams := set.NewStringSet()
- for _, param := range template.Parameters {
- if param.Required {
- if _, found := c.Params[param.ParamName]; !found {
- validationErrs.AddStringf("required param %q not specified", param.ParamName)
- }
- }
- supportedParams.Add(param.ParamName)
- }
- for passedParam := range c.Params {
- if !supportedParams.Contains(passedParam) {
- validationErrs.AddStringf("unknown param %q passed", passedParam)
- }
+ params, err := template.ParseAndValidateParams(c.Params)
+ if err != nil {
+ return nil, errors.Wrap(err, "validating and instantiating params")
}
+
if err := validationErrs.ToError(); err != nil {
return nil, err
}
@@ -60,7 +50,7 @@ func ValidateAndInstantiate(c *check.Check) (*InstantiatedCheck, error) {
return nil, err
}
i.Matcher = matcher
- checkFunc, err := template.Instantiate(c.Params)
+ checkFunc, err := template.Instantiate(params)
if err != nil {
return nil, errors.Wrap(err, "instantiating check")
}
diff --git a/internal/pointers/pointers.go b/internal/pointers/pointers.go
new file mode 100644
index 000000000..867285240
--- /dev/null
+++ b/internal/pointers/pointers.go
@@ -0,0 +1,6 @@
+package pointers
+
+// Bool returns a pointer to a bool.
+func Bool(b bool) *bool {
+ return &b
+}
diff --git a/internal/stringutils/split.go b/internal/stringutils/split.go
new file mode 100644
index 000000000..47996cc9c
--- /dev/null
+++ b/internal/stringutils/split.go
@@ -0,0 +1,16 @@
+package stringutils
+
+import (
+ "strings"
+)
+
+// Split2 splits the given string at the given separator, returning the part before and after the separator as two
+// separate return values.
+// If the string does not contain `sep`, the entire string is returned as the first return value.
+func Split2(str, sep string) (string, string) {
+ splitIdx := strings.Index(str, sep)
+ if splitIdx == -1 {
+ return str, ""
+ }
+ return str[:splitIdx], str[splitIdx+len(sep):]
+}
diff --git a/internal/templates/all/all.go b/internal/templates/all/all.go
index bb2b978bc..80f55bcda 100644
--- a/internal/templates/all/all.go
+++ b/internal/templates/all/all.go
@@ -2,6 +2,7 @@ package all
import (
// Import all check templates.
+ _ "golang.stackrox.io/kube-linter/internal/templates/disallowedgvk"
_ "golang.stackrox.io/kube-linter/internal/templates/envvar"
_ "golang.stackrox.io/kube-linter/internal/templates/privileged"
_ "golang.stackrox.io/kube-linter/internal/templates/readonlyrootfs"
diff --git a/internal/templates/all/all_test.go b/internal/templates/all/all_test.go
new file mode 100644
index 000000000..2d1fdf7fd
--- /dev/null
+++ b/internal/templates/all/all_test.go
@@ -0,0 +1,21 @@
+package all
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "golang.stackrox.io/kube-linter/internal/templates"
+)
+
+func TestTemplatesAreValid(t *testing.T) {
+ for _, template := range templates.List() {
+ t.Run(template.HumanName, func(t *testing.T) {
+ assert.NotEmpty(t, template.HumanName, "human name")
+ assert.NotEmpty(t, template.Key, "name")
+ assert.NotEmpty(t, template.Description, "description")
+ assert.NotNil(t, template.ParseAndValidateParams, "parse and validate params")
+ assert.NotNil(t, template.Parameters, "params") // We want people to use the generated code and explicitly set it to an empty list.
+ assert.NotNil(t, template.Instantiate, "instantiate")
+ })
+ }
+}
diff --git a/internal/templates/codegen/main.go b/internal/templates/codegen/main.go
new file mode 100644
index 000000000..327cb968c
--- /dev/null
+++ b/internal/templates/codegen/main.go
@@ -0,0 +1,288 @@
+package main
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "reflect"
+ "strings"
+ "text/template"
+
+ "github.com/Masterminds/sprig/v3"
+ "github.com/pkg/errors"
+ "golang.stackrox.io/kube-linter/internal/check"
+ "golang.stackrox.io/kube-linter/internal/set"
+ "golang.stackrox.io/kube-linter/internal/stringutils"
+ "golang.stackrox.io/kube-linter/internal/utils"
+ "k8s.io/gengo/parser"
+ "k8s.io/gengo/types"
+)
+
+var (
+ knownNonTemplateDirs = set.NewFrozenStringSet("all", "codegen", "util")
+)
+
+const (
+ metadataMarker = "+"
+
+ paramsStructName = "Params"
+)
+
+type templateElem struct {
+ ParamDesc check.ParameterDesc
+ ParamJSON string
+}
+
+const (
+ fileTemplateStr = `// Code generated by kube-linter template codegen. DO NOT EDIT.
+// +build !templatecodegen
+
+package params
+
+import (
+ "github.com/pkg/errors"
+ "golang.stackrox.io/kube-linter/internal/check"
+ "golang.stackrox.io/kube-linter/internal/templates/util"
+)
+
+var (
+ // Use this in case it doesn't get used otherwise.
+ _ = util.MustParseParameterDesc
+
+{{ range . }}
+ {{ .ParamDesc.Name}}ParamDesc = util.MustParseParameterDesc({{backtick}}
+{{- .ParamJSON -}}
+{{backtick}})
+{{- end }}
+
+ ParamDescs = []check.ParameterDesc{
+ {{- range . }}
+ {{ .ParamDesc.Name}}ParamDesc,
+ {{- end }}
+ }
+)
+
+func (p *Params) Validate() error {
+ var missingRequiredParams []string
+ {{- range . }}
+ {{- if eq .ParamDesc.Type "object" }}
+ return errors.Errorf("parameter validation not yet supported for object type \"{{ .ParamDesc.Key }}\"")
+ {{- end }}
+ {{- if .ParamDesc.Required }}
+ {{- if ne .ParamDesc.Type "string" }}
+ return errors.Errorf("required parameter validation is currently only supported for strings, but {{ .ParamDesc.Key }} is not")
+ {{- end }}
+ if p.{{ .ParamDesc.XXXStructFieldName }} == "" {
+ missingRequiredParams = append(missingRequiredParams, "{{.ParamDesc.Name}}")
+ }
+ {{- end }}
+ {{- end }}
+ if len(missingRequiredParams) > 0 {
+ return errors.Errorf("required params %v not found", missingRequiredParams)
+ }
+ 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))
+ }
+}
+`
+)
+
+var (
+ fileTemplate = template.Must(template.New("gen").Funcs(sprig.TxtFuncMap()).Funcs(template.FuncMap{
+ "backtick": func() string {
+ return "`"
+ },
+ }).Parse(fileTemplateStr))
+)
+
+func lowerCaseFirstLetter(s string) string {
+ return strings.ToLower(s[:1]) + s[1:]
+}
+
+func getName(member types.Member) string {
+ if jsonTag := reflect.StructTag(member.Tags).Get("json"); jsonTag != "" {
+ name, _ := stringutils.Split2(jsonTag, ",")
+ if name != "" {
+ return name
+ }
+ }
+ return lowerCaseFirstLetter(member.Name)
+}
+
+func getDescription(member types.Member) string {
+ firstCommentLineWithMetadata := len(member.CommentLines)
+ for i, commentLine := range member.CommentLines {
+ if strings.HasPrefix(commentLine, metadataMarker) {
+ firstCommentLineWithMetadata = i
+ break
+ }
+ }
+ return strings.Join(member.CommentLines[:firstCommentLineWithMetadata], " ")
+}
+
+func setBoolBasedOnPresenceOfTag(valToSet *bool, tag string, extractedTags map[string][]string) error {
+ if val, exists := extractedTags[tag]; exists {
+ if len(val) > 1 || (len(val) == 0 && val[0] != "") {
+ return errors.Errorf("invalid value for tag %s: %v; tag is only supported WITHOUT values", tag, val)
+ }
+ *valToSet = true
+ }
+ return nil
+}
+
+func constructParameterDescsFromStruct(typeSpec *types.Type) ([]check.ParameterDesc, error) {
+ var paramDescs []check.ParameterDesc
+ for _, member := range typeSpec.Members {
+ if member.Embedded {
+ return nil, errors.Errorf("cannot handle embedded member %s in %+v", member.Name, typeSpec)
+ }
+
+ desc := check.ParameterDesc{
+ Name: getName(member),
+ Description: getDescription(member),
+ XXXStructFieldName: member.Name,
+ }
+ switch kind := member.Type.Kind; kind {
+ case types.Builtin:
+ switch member.Type {
+ case types.String:
+ desc.Type = check.StringType
+ case types.Int:
+ desc.Type = check.IntegerType
+ case types.Float32, types.Float64:
+ desc.Type = check.NumberType
+ case types.Bool:
+ desc.Type = check.BooleanType
+ default:
+ return nil, errors.Errorf("currently unsupported type %v", member.Type)
+ }
+ case types.Struct:
+ desc.Type = check.ObjectType
+ subParams, err := constructParameterDescsFromStruct(member.Type)
+ if err != nil {
+ return nil, errors.Wrapf(err, "handling field %v", member.Name)
+ }
+ desc.SubParameters = subParams
+ }
+
+ extractedTags := types.ExtractCommentTags(metadataMarker, member.CommentLines)
+ desc.Examples = extractedTags["example"]
+ if err := setBoolBasedOnPresenceOfTag(&desc.Required, "required", extractedTags); err != nil {
+ return nil, err
+ }
+ if err := setBoolBasedOnPresenceOfTag(&desc.NoRegex, "noregex", extractedTags); err != nil {
+ return nil, err
+ }
+ if err := setBoolBasedOnPresenceOfTag(&desc.NotNegatable, "notnegatable", extractedTags); err != nil {
+ return nil, err
+ }
+ paramDescs = append(paramDescs, desc)
+ }
+ return paramDescs, nil
+}
+
+func processTemplate(dir string) error {
+ b := parser.New()
+ // This avoids parsing generated files in the package (since we add +build !templatecodegen to them,
+ // which makes the parsing much quicker since the parser doesn't have to load any imported packages).
+ b.AddBuildTags("templatecodegen")
+ if err := b.AddDir(fmt.Sprintf("./%s/internal/params", dir)); err != nil {
+ return err
+ }
+ typeUniverse, err := b.FindTypes()
+ if err != nil {
+ return err
+ }
+ pkgNames := b.FindPackages()
+ if len(pkgNames) != 1 {
+ return errors.Errorf("found unexpected number of packages in %+v: %d", pkgNames, len(pkgNames))
+ }
+
+ pkg := typeUniverse.Package(pkgNames[0])
+ paramsType := pkg.Type(paramsStructName)
+
+ if paramsType.Kind != types.Struct {
+ return errors.Errorf("unexpected param type: %+v", paramsType)
+ }
+ paramDescs, err := constructParameterDescsFromStruct(paramsType)
+ if err != nil {
+ return err
+ }
+
+ var templateObj []templateElem
+
+ for _, paramDesc := range paramDescs {
+ buf := bytes.NewBuffer(nil)
+ enc := json.NewEncoder(buf)
+ enc.SetIndent("", "\t")
+ if err := enc.Encode(paramDesc); err != nil {
+ return errors.Wrapf(err, "couldn't marshal param %v", paramDesc)
+ }
+
+ templateObj = append(templateObj, templateElem{
+ ParamDesc: paramDesc,
+ ParamJSON: buf.String(),
+ })
+ }
+
+ outFileName := filepath.Join(dir, "internal", "params", "gen-params.go")
+ outF, err := os.Create(outFileName)
+ if err != nil {
+ return errors.Wrap(err, "creating output file")
+ }
+ defer utils.IgnoreError(outF.Close)
+ if err := fileTemplate.Execute(outF, templateObj); err != nil {
+ return err
+ }
+ return nil
+}
+
+func mainCmd() error {
+ fileInfos, err := ioutil.ReadDir(".")
+ if err != nil {
+ return err
+ }
+ for _, fileInfo := range fileInfos {
+ if !fileInfo.IsDir() {
+ continue
+ }
+ if knownNonTemplateDirs.Contains(fileInfo.Name()) {
+ continue
+ }
+ if err := processTemplate(fileInfo.Name()); err != nil {
+ return errors.Wrapf(err, "processing dir %v", fileInfo.Name())
+ }
+ }
+ return nil
+}
+
+func main() {
+ if err := mainCmd(); err != nil {
+ fmt.Printf("Error executing command: %v", err)
+ os.Exit(1)
+ }
+
+}
diff --git a/internal/templates/disallowedgvk/internal/params/gen-params.go b/internal/templates/disallowedgvk/internal/params/gen-params.go
new file mode 100644
index 000000000..b1a787305
--- /dev/null
+++ b/internal/templates/disallowedgvk/internal/params/gen-params.go
@@ -0,0 +1,97 @@
+// Code generated by kube-linter template codegen. DO NOT EDIT.
+// +build !templatecodegen
+
+package params
+
+import (
+ "github.com/pkg/errors"
+ "golang.stackrox.io/kube-linter/internal/check"
+ "golang.stackrox.io/kube-linter/internal/templates/util"
+)
+
+var (
+ // Use this in case it doesn't get used otherwise.
+ _ = util.MustParseParameterDesc
+
+
+ groupParamDesc = util.MustParseParameterDesc(`{
+ "Name": "group",
+ "Type": "string",
+ "Description": "The disallowed object group.",
+ "Examples": [
+ "apps"
+ ],
+ "SubParameters": null,
+ "Required": false,
+ "NoRegex": false,
+ "NotNegatable": false,
+ "XXXStructFieldName": "Group"
+}
+`)
+ versionParamDesc = util.MustParseParameterDesc(`{
+ "Name": "version",
+ "Type": "string",
+ "Description": "The disallowed object API version.",
+ "Examples": [
+ "v1",
+ "v1beta1"
+ ],
+ "SubParameters": null,
+ "Required": false,
+ "NoRegex": false,
+ "NotNegatable": false,
+ "XXXStructFieldName": "Version"
+}
+`)
+ kindParamDesc = util.MustParseParameterDesc(`{
+ "Name": "kind",
+ "Type": "string",
+ "Description": "The disallowed kind.",
+ "Examples": [
+ "Deployment",
+ "DaemonSet"
+ ],
+ "SubParameters": null,
+ "Required": false,
+ "NoRegex": false,
+ "NotNegatable": false,
+ "XXXStructFieldName": "Kind"
+}
+`)
+
+ ParamDescs = []check.ParameterDesc{
+ groupParamDesc,
+ versionParamDesc,
+ kindParamDesc,
+ }
+)
+
+func (p *Params) Validate() error {
+ var missingRequiredParams []string
+ if len(missingRequiredParams) > 0 {
+ return errors.Errorf("required params %v not found", missingRequiredParams)
+ }
+ 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/internal/templates/disallowedgvk/internal/params/params.go b/internal/templates/disallowedgvk/internal/params/params.go
new file mode 100644
index 000000000..6190bbf92
--- /dev/null
+++ b/internal/templates/disallowedgvk/internal/params/params.go
@@ -0,0 +1,19 @@
+package params
+
+// Params represents the params accepted by this template.
+type Params struct {
+
+ // The disallowed object group.
+ // +example=apps
+ Group string `json:"group"`
+
+ // The disallowed object API version.
+ // +example=v1
+ // +example=v1beta1
+ Version string
+
+ // The disallowed kind.
+ // +example=Deployment
+ // +example=DaemonSet
+ Kind string
+}
diff --git a/internal/templates/disallowedgvk/template.go b/internal/templates/disallowedgvk/template.go
new file mode 100644
index 000000000..5725e0b7a
--- /dev/null
+++ b/internal/templates/disallowedgvk/template.go
@@ -0,0 +1,49 @@
+package disallowedgvk
+
+import (
+ "fmt"
+
+ "github.com/pkg/errors"
+ "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/matcher"
+ "golang.stackrox.io/kube-linter/internal/objectkinds"
+ "golang.stackrox.io/kube-linter/internal/templates"
+ "golang.stackrox.io/kube-linter/internal/templates/disallowedgvk/internal/params"
+)
+
+func init() {
+ templates.Register(check.Template{
+ HumanName: "Disallowed API Objects",
+ Key: "disallowed-api-obj",
+ Description: "Flag disallowed API object kinds",
+ SupportedObjectKinds: check.ObjectKindsDesc{
+ ObjectKinds: []string{objectkinds.Any},
+ },
+ Parameters: params.ParamDescs,
+ ParseAndValidateParams: params.ParseAndValidate,
+ Instantiate: params.WrapInstantiateFunc(func(p params.Params) (check.Func, error) {
+ groupMatcher, err := matcher.ForString(p.Group)
+ if err != nil {
+ return nil, errors.Wrap(err, "invalid group")
+ }
+ versionMatcher, err := matcher.ForString(p.Version)
+ if err != nil {
+ return nil, errors.Wrap(err, "invalid version")
+ }
+ kindMatcher, err := matcher.ForString(p.Kind)
+ if err != nil {
+ return nil, errors.Wrap(err, "invalid kind")
+ }
+ return func(_ *lintcontext.LintContext, object lintcontext.Object) []diagnostic.Diagnostic {
+ gvk := extract.GVK(object.K8sObject)
+ if groupMatcher(gvk.Group) && versionMatcher(gvk.Version) && kindMatcher(gvk.Kind) {
+ return []diagnostic.Diagnostic{{Message: fmt.Sprintf("disallowed API object found: %s", gvk)}}
+ }
+ return nil
+ }, nil
+ }),
+ })
+}
diff --git a/internal/templates/envvar/internal/params/gen-params.go b/internal/templates/envvar/internal/params/gen-params.go
new file mode 100644
index 000000000..5f739a63e
--- /dev/null
+++ b/internal/templates/envvar/internal/params/gen-params.go
@@ -0,0 +1,79 @@
+// Code generated by kube-linter template codegen. DO NOT EDIT.
+// +build !templatecodegen
+
+package params
+
+import (
+ "github.com/pkg/errors"
+ "golang.stackrox.io/kube-linter/internal/check"
+ "golang.stackrox.io/kube-linter/internal/templates/util"
+)
+
+var (
+ // Use this in case it doesn't get used otherwise.
+ _ = util.MustParseParameterDesc
+
+
+ nameParamDesc = util.MustParseParameterDesc(`{
+ "Name": "name",
+ "Type": "string",
+ "Description": "The name of the environment variable.",
+ "Examples": null,
+ "SubParameters": null,
+ "Required": true,
+ "NoRegex": false,
+ "NotNegatable": false,
+ "XXXStructFieldName": "Name"
+}
+`)
+ valueParamDesc = util.MustParseParameterDesc(`{
+ "Name": "value",
+ "Type": "string",
+ "Description": "The value of the environment variable.",
+ "Examples": null,
+ "SubParameters": null,
+ "Required": false,
+ "NoRegex": false,
+ "NotNegatable": false,
+ "XXXStructFieldName": "Value"
+}
+`)
+
+ ParamDescs = []check.ParameterDesc{
+ nameParamDesc,
+ valueParamDesc,
+ }
+)
+
+func (p *Params) Validate() error {
+ var missingRequiredParams []string
+ if p.Name == "" {
+ missingRequiredParams = append(missingRequiredParams, "name")
+ }
+ if len(missingRequiredParams) > 0 {
+ return errors.Errorf("required params %v not found", missingRequiredParams)
+ }
+ 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/internal/templates/envvar/internal/params/params.go b/internal/templates/envvar/internal/params/params.go
new file mode 100644
index 000000000..69bb6184e
--- /dev/null
+++ b/internal/templates/envvar/internal/params/params.go
@@ -0,0 +1,12 @@
+package params
+
+// Params represents the params accepted by this template.
+type Params struct {
+
+ // The name of the environment variable.
+ // +required
+ Name string
+
+ // The value of the environment variable.
+ Value string
+}
diff --git a/internal/templates/envvar/template.go b/internal/templates/envvar/template.go
index 5d60f7d33..891176f1b 100644
--- a/internal/templates/envvar/template.go
+++ b/internal/templates/envvar/template.go
@@ -11,30 +11,25 @@ import (
"golang.stackrox.io/kube-linter/internal/matcher"
"golang.stackrox.io/kube-linter/internal/objectkinds"
"golang.stackrox.io/kube-linter/internal/templates"
-)
-
-const (
- nameParamName = "name"
- valueParamName = "value"
+ "golang.stackrox.io/kube-linter/internal/templates/envvar/internal/params"
)
func init() {
templates.Register(check.Template{
- Name: "env-var",
+ HumanName: "Environment Variables",
+ Key: "env-var",
Description: "Flag environment variables that match the provided patterns",
SupportedObjectKinds: check.ObjectKindsDesc{
ObjectKinds: []string{objectkinds.DeploymentLike},
},
- Parameters: []check.ParameterDesc{
- {ParamName: nameParamName, Required: true, Description: "A regex for the env var name"},
- {ParamName: valueParamName, Description: "A regex for the env var value"},
- },
- Instantiate: func(params map[string]string) (check.Func, error) {
- nameMatcher, err := matcher.ForString(params[nameParamName])
+ Parameters: params.ParamDescs,
+ ParseAndValidateParams: params.ParseAndValidate,
+ Instantiate: params.WrapInstantiateFunc(func(p params.Params) (check.Func, error) {
+ nameMatcher, err := matcher.ForString(p.Name)
if err != nil {
- return nil, errors.Wrap(err, "invalid key")
+ return nil, errors.Wrap(err, "invalid name")
}
- valueMatcher, err := matcher.ForString(params[valueParamName])
+ valueMatcher, err := matcher.ForString(p.Value)
if err != nil {
return nil, errors.Wrap(err, "invalid value")
}
@@ -56,6 +51,6 @@ func init() {
}
return results
}, nil
- },
+ }),
})
}
diff --git a/internal/templates/gen.go b/internal/templates/gen.go
new file mode 100644
index 000000000..3c739a572
--- /dev/null
+++ b/internal/templates/gen.go
@@ -0,0 +1,3 @@
+package templates
+
+//go:generate go run ./codegen
diff --git a/internal/templates/privileged/internal/params/gen-params.go b/internal/templates/privileged/internal/params/gen-params.go
new file mode 100644
index 000000000..8ca851fde
--- /dev/null
+++ b/internal/templates/privileged/internal/params/gen-params.go
@@ -0,0 +1,50 @@
+// Code generated by kube-linter template codegen. DO NOT EDIT.
+// +build !templatecodegen
+
+package params
+
+import (
+ "github.com/pkg/errors"
+ "golang.stackrox.io/kube-linter/internal/check"
+ "golang.stackrox.io/kube-linter/internal/templates/util"
+)
+
+var (
+ // Use this in case it doesn't get used otherwise.
+ _ = util.MustParseParameterDesc
+
+
+
+ ParamDescs = []check.ParameterDesc{
+ }
+)
+
+func (p *Params) Validate() error {
+ var missingRequiredParams []string
+ if len(missingRequiredParams) > 0 {
+ return errors.Errorf("required params %v not found", missingRequiredParams)
+ }
+ 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/internal/templates/privileged/internal/params/params.go b/internal/templates/privileged/internal/params/params.go
new file mode 100644
index 000000000..578cc3aa8
--- /dev/null
+++ b/internal/templates/privileged/internal/params/params.go
@@ -0,0 +1,5 @@
+package params
+
+// Params represents the params accepted by this template.
+type Params struct {
+}
diff --git a/internal/templates/privileged/template.go b/internal/templates/privileged/template.go
index 96518ed85..09b806859 100644
--- a/internal/templates/privileged/template.go
+++ b/internal/templates/privileged/template.go
@@ -9,17 +9,20 @@ import (
"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/privileged/internal/params"
)
func init() {
templates.Register(check.Template{
- Name: "privileged",
+ HumanName: "Privileged Containers",
+ Key: "privileged",
Description: "Flag privileged containers",
SupportedObjectKinds: check.ObjectKindsDesc{
ObjectKinds: []string{objectkinds.DeploymentLike},
},
- Parameters: nil,
- Instantiate: func(_ map[string]string) (check.Func, error) {
+ Parameters: params.ParamDescs,
+ ParseAndValidateParams: params.ParseAndValidate,
+ Instantiate: params.WrapInstantiateFunc(func(_ params.Params) (check.Func, error) {
return func(_ *lintcontext.LintContext, object lintcontext.Object) []diagnostic.Diagnostic {
podSpec, found := extract.PodSpec(object.K8sObject)
if !found {
@@ -36,6 +39,6 @@ func init() {
}
return results
}, nil
- },
+ }),
})
}
diff --git a/internal/templates/readonlyrootfs/internal/params/gen-params.go b/internal/templates/readonlyrootfs/internal/params/gen-params.go
new file mode 100644
index 000000000..8ca851fde
--- /dev/null
+++ b/internal/templates/readonlyrootfs/internal/params/gen-params.go
@@ -0,0 +1,50 @@
+// Code generated by kube-linter template codegen. DO NOT EDIT.
+// +build !templatecodegen
+
+package params
+
+import (
+ "github.com/pkg/errors"
+ "golang.stackrox.io/kube-linter/internal/check"
+ "golang.stackrox.io/kube-linter/internal/templates/util"
+)
+
+var (
+ // Use this in case it doesn't get used otherwise.
+ _ = util.MustParseParameterDesc
+
+
+
+ ParamDescs = []check.ParameterDesc{
+ }
+)
+
+func (p *Params) Validate() error {
+ var missingRequiredParams []string
+ if len(missingRequiredParams) > 0 {
+ return errors.Errorf("required params %v not found", missingRequiredParams)
+ }
+ 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/internal/templates/readonlyrootfs/internal/params/params.go b/internal/templates/readonlyrootfs/internal/params/params.go
new file mode 100644
index 000000000..578cc3aa8
--- /dev/null
+++ b/internal/templates/readonlyrootfs/internal/params/params.go
@@ -0,0 +1,5 @@
+package params
+
+// Params represents the params accepted by this template.
+type Params struct {
+}
diff --git a/internal/templates/readonlyrootfs/template.go b/internal/templates/readonlyrootfs/template.go
index 370cbc03a..37abf7e37 100644
--- a/internal/templates/readonlyrootfs/template.go
+++ b/internal/templates/readonlyrootfs/template.go
@@ -9,17 +9,20 @@ import (
"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/readonlyrootfs/internal/params"
)
func init() {
templates.Register(check.Template{
- Name: "read-only-root-fs",
+ HumanName: "Read-only Root Filesystems",
+ Key: "read-only-root-fs",
Description: "Flag containers without read-only root file systems",
SupportedObjectKinds: check.ObjectKindsDesc{
ObjectKinds: []string{objectkinds.DeploymentLike},
},
- Parameters: nil,
- Instantiate: func(_ map[string]string) (check.Func, error) {
+ 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 {
podSpec, found := extract.PodSpec(object.K8sObject)
if !found {
@@ -34,6 +37,6 @@ func init() {
}
return results
}, nil
- },
+ }),
})
}
diff --git a/internal/templates/registry.go b/internal/templates/registry.go
index 6d1fa24d3..470397146 100644
--- a/internal/templates/registry.go
+++ b/internal/templates/registry.go
@@ -14,10 +14,10 @@ var (
// Register registers a template with the given name.
// Intended to be called at program init time.
func Register(t check.Template) {
- if _, ok := allTemplates[t.Name]; ok {
- panic(fmt.Sprintf("duplicate template: %v", t.Name))
+ if _, ok := allTemplates[t.Key]; ok {
+ panic(fmt.Sprintf("duplicate template: %v", t.Key))
}
- allTemplates[t.Name] = t
+ allTemplates[t.Key] = t
}
// Get gets a template by name, returning a boolean indicating whether it was found.
@@ -33,7 +33,7 @@ func List() []check.Template {
out = append(out, t)
}
sort.Slice(out, func(i, j int) bool {
- return out[i].Name < out[j].Name
+ return out[i].Key < out[j].Key
})
return out
}
diff --git a/internal/templates/requiredlabel/internal/params/gen-params.go b/internal/templates/requiredlabel/internal/params/gen-params.go
new file mode 100644
index 000000000..6b2d04d81
--- /dev/null
+++ b/internal/templates/requiredlabel/internal/params/gen-params.go
@@ -0,0 +1,79 @@
+// Code generated by kube-linter template codegen. DO NOT EDIT.
+// +build !templatecodegen
+
+package params
+
+import (
+ "github.com/pkg/errors"
+ "golang.stackrox.io/kube-linter/internal/check"
+ "golang.stackrox.io/kube-linter/internal/templates/util"
+)
+
+var (
+ // Use this in case it doesn't get used otherwise.
+ _ = util.MustParseParameterDesc
+
+
+ keyParamDesc = util.MustParseParameterDesc(`{
+ "Name": "key",
+ "Type": "string",
+ "Description": "Key of the required label.",
+ "Examples": null,
+ "SubParameters": null,
+ "Required": true,
+ "NoRegex": false,
+ "NotNegatable": false,
+ "XXXStructFieldName": "Key"
+}
+`)
+ valueParamDesc = util.MustParseParameterDesc(`{
+ "Name": "value",
+ "Type": "string",
+ "Description": "Value of the required label.",
+ "Examples": null,
+ "SubParameters": null,
+ "Required": false,
+ "NoRegex": false,
+ "NotNegatable": false,
+ "XXXStructFieldName": "Value"
+}
+`)
+
+ ParamDescs = []check.ParameterDesc{
+ keyParamDesc,
+ valueParamDesc,
+ }
+)
+
+func (p *Params) Validate() error {
+ var missingRequiredParams []string
+ if p.Key == "" {
+ missingRequiredParams = append(missingRequiredParams, "key")
+ }
+ if len(missingRequiredParams) > 0 {
+ return errors.Errorf("required params %v not found", missingRequiredParams)
+ }
+ 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/internal/templates/requiredlabel/internal/params/params.go b/internal/templates/requiredlabel/internal/params/params.go
new file mode 100644
index 000000000..c0187b32c
--- /dev/null
+++ b/internal/templates/requiredlabel/internal/params/params.go
@@ -0,0 +1,12 @@
+package params
+
+// Params represents the params accepted by this template.
+type Params struct {
+
+ // Key of the required label.
+ // +required
+ Key string
+
+ // Value of the required label.
+ Value string
+}
diff --git a/internal/templates/requiredlabel/template.go b/internal/templates/requiredlabel/template.go
index 5d4b70003..b83d44f0f 100644
--- a/internal/templates/requiredlabel/template.go
+++ b/internal/templates/requiredlabel/template.go
@@ -12,30 +12,25 @@ import (
"golang.stackrox.io/kube-linter/internal/objectkinds"
"golang.stackrox.io/kube-linter/internal/stringutils"
"golang.stackrox.io/kube-linter/internal/templates"
-)
-
-const (
- keyParamName = "key"
- valueParamName = "value"
+ "golang.stackrox.io/kube-linter/internal/templates/requiredlabel/internal/params"
)
func init() {
templates.Register(check.Template{
- Name: "required-label",
+ HumanName: "Required Label",
+ Key: "required-label",
Description: "Flag objects not carrying at least one label matching the provided patterns",
SupportedObjectKinds: check.ObjectKindsDesc{
ObjectKinds: []string{objectkinds.Any},
},
- Parameters: []check.ParameterDesc{
- {ParamName: keyParamName, Required: true, Description: "A regex for the key of the required label"},
- {ParamName: valueParamName, Required: false, Description: "A regex for the value of the required label"},
- },
- Instantiate: func(params map[string]string) (check.Func, error) {
- keyMatcher, err := matcher.ForString(params[keyParamName])
+ Parameters: params.ParamDescs,
+ ParseAndValidateParams: params.ParseAndValidate,
+ Instantiate: params.WrapInstantiateFunc(func(p params.Params) (check.Func, error) {
+ keyMatcher, err := matcher.ForString(p.Key)
if err != nil {
return nil, errors.Wrap(err, "invalid key")
}
- valueMatcher, err := matcher.ForString(params[valueParamName])
+ valueMatcher, err := matcher.ForString(p.Value)
if err != nil {
return nil, errors.Wrap(err, "invalid value")
}
@@ -48,9 +43,9 @@ func init() {
}
}
return []diagnostic.Diagnostic{{
- Message: fmt.Sprintf("no label matching \"%s=%s\" found", params[keyParamName], stringutils.OrDefault(params[valueParamName], "")),
+ Message: fmt.Sprintf("no label matching \"%s=%s\" found", p.Key, stringutils.OrDefault(p.Value, "")),
}}
}, nil
- },
+ }),
})
}
diff --git a/internal/templates/runasnonroot/internal/params/gen-params.go b/internal/templates/runasnonroot/internal/params/gen-params.go
new file mode 100644
index 000000000..8ca851fde
--- /dev/null
+++ b/internal/templates/runasnonroot/internal/params/gen-params.go
@@ -0,0 +1,50 @@
+// Code generated by kube-linter template codegen. DO NOT EDIT.
+// +build !templatecodegen
+
+package params
+
+import (
+ "github.com/pkg/errors"
+ "golang.stackrox.io/kube-linter/internal/check"
+ "golang.stackrox.io/kube-linter/internal/templates/util"
+)
+
+var (
+ // Use this in case it doesn't get used otherwise.
+ _ = util.MustParseParameterDesc
+
+
+
+ ParamDescs = []check.ParameterDesc{
+ }
+)
+
+func (p *Params) Validate() error {
+ var missingRequiredParams []string
+ if len(missingRequiredParams) > 0 {
+ return errors.Errorf("required params %v not found", missingRequiredParams)
+ }
+ 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/internal/templates/runasnonroot/internal/params/params.go b/internal/templates/runasnonroot/internal/params/params.go
new file mode 100644
index 000000000..578cc3aa8
--- /dev/null
+++ b/internal/templates/runasnonroot/internal/params/params.go
@@ -0,0 +1,5 @@
+package params
+
+// Params represents the params accepted by this template.
+type Params struct {
+}
diff --git a/internal/templates/runasnonroot/template.go b/internal/templates/runasnonroot/template.go
index 0c13e8ddb..6c2b7dc60 100644
--- a/internal/templates/runasnonroot/template.go
+++ b/internal/templates/runasnonroot/template.go
@@ -9,6 +9,7 @@ import (
"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/runasnonroot/internal/params"
v1 "k8s.io/api/core/v1"
)
@@ -34,13 +35,15 @@ func effectiveRunAsUser(podSC *v1.PodSecurityContext, containerSC *v1.SecurityCo
func init() {
templates.Register(check.Template{
- Name: "run-as-non-root",
+ HumanName: "Run as non-root user",
+ Key: "run-as-non-root",
Description: "Flag containers set to run as a root user",
SupportedObjectKinds: check.ObjectKindsDesc{
ObjectKinds: []string{objectkinds.DeploymentLike},
},
- Parameters: nil,
- Instantiate: func(_ map[string]string) (check.Func, error) {
+ Parameters: params.ParamDescs,
+ ParseAndValidateParams: params.ParseAndValidate,
+ Instantiate: params.WrapInstantiateFunc(func(_ params.Params) (check.Func, error) {
return func(_ *lintcontext.LintContext, object lintcontext.Object) []diagnostic.Diagnostic {
podSpec, found := extract.PodSpec(object.K8sObject)
if !found {
@@ -67,6 +70,6 @@ func init() {
}
return results
}, nil
- },
+ }),
})
}
diff --git a/internal/templates/util/json.go b/internal/templates/util/json.go
new file mode 100644
index 000000000..b5b762fa2
--- /dev/null
+++ b/internal/templates/util/json.go
@@ -0,0 +1,20 @@
+package util
+
+import (
+ "encoding/json"
+ "strings"
+
+ "golang.stackrox.io/kube-linter/internal/check"
+)
+
+// MustParseParameterDesc unmarshals the given JSON into a templates.ParameterDesc.
+func MustParseParameterDesc(asJSON string) check.ParameterDesc {
+ var out check.ParameterDesc
+
+ decoder := json.NewDecoder(strings.NewReader(asJSON))
+ decoder.DisallowUnknownFields()
+ if err := decoder.Decode(&out); err != nil {
+ panic(err)
+ }
+ return out
+}
diff --git a/internal/templates/util/map_structure.go b/internal/templates/util/map_structure.go
new file mode 100644
index 000000000..ab1ac05b5
--- /dev/null
+++ b/internal/templates/util/map_structure.go
@@ -0,0 +1,19 @@
+package util
+
+import (
+ "github.com/mitchellh/mapstructure"
+)
+
+// DecodeMapStructure decodes the given map[string]interface{} into the given out variable, typically
+// a pointer to a struct.
+func DecodeMapStructure(m map[string]interface{}, out interface{}) error {
+ dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
+ ErrorUnused: true,
+ TagName: "json",
+ Result: out,
+ })
+ if err != nil {
+ return err
+ }
+ return dec.Decode(m)
+}
diff --git a/internal/utils/ignore_error.go b/internal/utils/ignore_error.go
new file mode 100644
index 000000000..c38c0b38d
--- /dev/null
+++ b/internal/utils/ignore_error.go
@@ -0,0 +1,7 @@
+package utils
+
+// IgnoreError is useful when you want to defer a func that returns an error,
+// but ignore the error without having the linter complain.
+func IgnoreError(f func() error) {
+ _ = f()
+}