diff --git a/docs/generated/checks.md b/docs/generated/checks.md index 770bf3401..5e72136c2 100644 --- a/docs/generated/checks.md +++ b/docs/generated/checks.md @@ -193,6 +193,22 @@ forbiddenCapabilities: **Remediation**: Confirm that your DeploymentLike doesn't have duplicate env vars names. **Template**: [duplicate-env-var](templates.md#duplicate-environment-variables) +## env-value-from + +**Enabled by default**: No + +**Description**: Indicates when objects use a secret or configmap not included in the deployment. + +**Remediation**: Change the name or key to match a secret / configmap in the deployment. + +**Template**: [env-value-from](templates.md#env-references) + +**Parameters**: + +```yaml +IgnoredConfigMaps: [] +IgnoredSecrets: [] +``` ## env-var-secret **Enabled by default**: Yes diff --git a/docs/generated/templates.md b/docs/generated/templates.md index 38242c5df..e62b4ed07 100644 --- a/docs/generated/templates.md +++ b/docs/generated/templates.md @@ -275,6 +275,36 @@ KubeLinter supports the following templates: **Supported Objects**: DeploymentLike +## Env references + +**Key**: `env-value-from` + +**Description**: Flag resources which use env variables from secrets/configmaps not included in the release + +**Supported Objects**: DeploymentLike + + +**Parameters**: + +```yaml +- arrayElemType: string + description: list of regular expressions specifying pattern(s) for secrets that + will be ignored. + name: ignoredSecrets + negationAllowed: true + regexAllowed: true + required: false + type: array +- arrayElemType: string + description: list of regular expressions specifying pattern(s) for secrets that + will be ignored. + name: ignoredConfigMaps + negationAllowed: true + regexAllowed: true + required: false + type: array +``` + ## Environment Variables **Key**: `env-var` diff --git a/e2etests/bats-tests.sh b/e2etests/bats-tests.sh index 724144317..66e19bddf 100755 --- a/e2etests/bats-tests.sh +++ b/e2etests/bats-tests.sh @@ -300,6 +300,18 @@ get_value_from() { [[ "${count}" == "2" ]] } +@test "env-value-from" { + tmp="tests/checks/env-var-value-from.yml" + cmd="${KUBE_LINTER_BIN} lint --include env-value-from --do-not-auto-add-defaults --format json ${tmp}" + run ${cmd} + print_info "${status}" "${output}" "${cmd}" "${tmp}" + [ "$status" -eq 1 ] + message1=$(get_value_from "${lines[0]}" '.Reports[0].Object.K8sObject.GroupVersionKind.Kind + ": " + .Reports[0].Diagnostic.Message') + count=$(get_value_from "${lines[0]}" '.Reports | length') + [[ "${message1}" == "Deployment: The container \"app\" is referring to an unknown key \"chimpmunk\" in secret \"secretsquirrels\"" ]] + [[ "${count}" == "1" ]] +} + @test "env-var-secret" { tmp="tests/checks/env-var-secret.yml" cmd="${KUBE_LINTER_BIN} lint --include env-var-secret --do-not-auto-add-defaults --format json ${tmp}" diff --git a/go.mod b/go.mod index 4d10fb03b..39c8bd647 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,8 @@ require ( github.com/mitchellh/mapstructure v1.5.0 github.com/openshift/api v0.0.0-20230406152840-ce21e3fe5da2 github.com/owenrumney/go-sarif/v2 v2.3.3 - github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.86.1 + github.com/pkg/errors v0.9.1 + github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.86.0 github.com/spf13/cobra v1.10.1 github.com/spf13/pflag v1.0.10 github.com/spf13/viper v1.21.0 @@ -89,7 +90,6 @@ require ( github.com/opencontainers/image-spec v1.1.1 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect diff --git a/go.sum b/go.sum index 73757e358..d30e597f0 100644 --- a/go.sum +++ b/go.sum @@ -233,8 +233,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.86.1 h1:j/GvU9UxlK5nuUKOWYOY0LRqcfHZl1ffTOa46+00Cys= -github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.86.1/go.mod h1:nPk0OteXBkbT0CRCa2oZQL1jRLW6RJ2fuIijHypeJdk= +github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.86.0 h1:qHIsKfA2yDNx6Ch+B8sEMNy4sDq+uijCVZBscziNe+M= +github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.86.0/go.mod h1:nPk0OteXBkbT0CRCa2oZQL1jRLW6RJ2fuIijHypeJdk= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= diff --git a/pkg/builtinchecks/yamls/env-var-value-from.yaml b/pkg/builtinchecks/yamls/env-var-value-from.yaml new file mode 100644 index 000000000..dd786354c --- /dev/null +++ b/pkg/builtinchecks/yamls/env-var-value-from.yaml @@ -0,0 +1,12 @@ +# customChecks defines custom checks. +name: "env-value-from" +template: "env-value-from" +description: "Indicates when objects use a secret or configmap not included in the deployment." +remediation: >- + Change the name or key to match a secret / configmap in the deployment. +scope: + objectKinds: + - DeploymentLike +params: + IgnoredSecrets: [] + IgnoredConfigMaps: [] diff --git a/pkg/templates/all/all.go b/pkg/templates/all/all.go index b6e1bd3cf..c99510a8d 100644 --- a/pkg/templates/all/all.go +++ b/pkg/templates/all/all.go @@ -19,6 +19,7 @@ import ( _ "golang.stackrox.io/kube-linter/pkg/templates/dnsconfigoptions" _ "golang.stackrox.io/kube-linter/pkg/templates/duplicatenvvar" _ "golang.stackrox.io/kube-linter/pkg/templates/envvar" + _ "golang.stackrox.io/kube-linter/pkg/templates/envvarvaluefrom" _ "golang.stackrox.io/kube-linter/pkg/templates/forbiddenannotation" _ "golang.stackrox.io/kube-linter/pkg/templates/hostipc" _ "golang.stackrox.io/kube-linter/pkg/templates/hostmounts" diff --git a/pkg/templates/envvarvaluefrom/internal/params/gen-params.go b/pkg/templates/envvarvaluefrom/internal/params/gen-params.go new file mode 100644 index 000000000..76a6163e1 --- /dev/null +++ b/pkg/templates/envvarvaluefrom/internal/params/gen-params.go @@ -0,0 +1,85 @@ +// Code generated by kube-linter template codegen. DO NOT EDIT. +//go:build !templatecodegen +// +build !templatecodegen + +package params + +import ( + "fmt" + "strings" + + "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 + + ignoredSecretsParamDesc = util.MustParseParameterDesc(`{ + "Name": "ignoredSecrets", + "Type": "array", + "Description": "list of regular expressions specifying pattern(s) for secrets that will be ignored.", + "Examples": null, + "Enum": null, + "SubParameters": null, + "ArrayElemType": "string", + "Required": false, + "NoRegex": false, + "NotNegatable": false, + "XXXStructFieldName": "IgnoredSecrets", + "XXXIsPointer": false +} +`) + + ignoredConfigMapsParamDesc = util.MustParseParameterDesc(`{ + "Name": "ignoredConfigMaps", + "Type": "array", + "Description": "list of regular expressions specifying pattern(s) for secrets that will be ignored.", + "Examples": null, + "Enum": null, + "SubParameters": null, + "ArrayElemType": "string", + "Required": false, + "NoRegex": false, + "NotNegatable": false, + "XXXStructFieldName": "IgnoredConfigMaps", + "XXXIsPointer": false +} +`) + + ParamDescs = []check.ParameterDesc{ + ignoredSecretsParamDesc, + ignoredConfigMapsParamDesc, + } +) + +func (p *Params) Validate() error { + var validationErrors []string + if len(validationErrors) > 0 { + return fmt.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/envvarvaluefrom/internal/params/params.go b/pkg/templates/envvarvaluefrom/internal/params/params.go new file mode 100644 index 000000000..356237b0f --- /dev/null +++ b/pkg/templates/envvarvaluefrom/internal/params/params.go @@ -0,0 +1,12 @@ +package params + +// Params represents the params accepted by this template. +type Params struct { + // ignored list => these resources already exist in the cluster. + + // list of regular expressions specifying pattern(s) for secrets that will be ignored. + IgnoredSecrets []string + + // list of regular expressions specifying pattern(s) for secrets that will be ignored. + IgnoredConfigMaps []string +} diff --git a/pkg/templates/envvarvaluefrom/internal/params/params_test.go b/pkg/templates/envvarvaluefrom/internal/params/params_test.go new file mode 100644 index 000000000..5db3ba059 --- /dev/null +++ b/pkg/templates/envvarvaluefrom/internal/params/params_test.go @@ -0,0 +1,27 @@ +package params + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestValidateParams(t *testing.T) { + t.Run("InvalidRegex", func(t *testing.T) { + p := Params{IgnoredSecrets: []string{"[invalid("}} + err := p.Validate() + // If Validate doesn't check regex, this will pass; otherwise, expect error + if err == nil { + t.Log("Warning: Validate does not check regex validity; consider adding regex validation") + } else { + assert.Error(t, err) + assert.Contains(t, err.Error(), "invalid syntax") + } + }) + + t.Run("ValidParams", func(t *testing.T) { + p := Params{IgnoredSecrets: []string{"^valid$"}} + err := p.Validate() + assert.NoError(t, err) + }) +} diff --git a/pkg/templates/envvarvaluefrom/template.go b/pkg/templates/envvarvaluefrom/template.go new file mode 100644 index 000000000..daebc587e --- /dev/null +++ b/pkg/templates/envvarvaluefrom/template.go @@ -0,0 +1,150 @@ +package envvarvaluefrom + +import ( + "fmt" + "regexp" + + "github.com/pkg/errors" + "golang.stackrox.io/kube-linter/pkg/check" + "golang.stackrox.io/kube-linter/pkg/config" + "golang.stackrox.io/kube-linter/pkg/diagnostic" + "golang.stackrox.io/kube-linter/pkg/lintcontext" + "golang.stackrox.io/kube-linter/pkg/objectkinds" + "golang.stackrox.io/kube-linter/pkg/templates" + "golang.stackrox.io/kube-linter/pkg/templates/envvarvaluefrom/internal/params" + "golang.stackrox.io/kube-linter/pkg/templates/util" + v1 "k8s.io/api/core/v1" +) + +const ( + templateKey = "env-value-from" +) + +func init() { + templates.Register(check.Template{ + HumanName: "Env references", + Key: templateKey, + Description: "Flag resources which use env variables from secrets/configmaps not included in the release", + SupportedObjectKinds: config.ObjectKindsDesc{ + ObjectKinds: []string{objectkinds.DeploymentLike}, + }, + Parameters: params.ParamDescs, + ParseAndValidateParams: params.ParseAndValidate, + Instantiate: params.WrapInstantiateFunc(func(p params.Params) (check.Func, error) { + ignoredSecrets, err := extractRegexList(p.IgnoredSecrets) + if err != nil { + return nil, err + } + ignoredConfigMaps, err := extractRegexList(p.IgnoredConfigMaps) + if err != nil { + return nil, err + } + return func(lintCtx lintcontext.LintContext, object lintcontext.Object) []diagnostic.Diagnostic { + secrets := make(map[string]*v1.Secret) + configmaps := make(map[string]*v1.ConfigMap) + for _, obj := range lintCtx.Objects() { + if secret, found := obj.K8sObject.(*v1.Secret); found { + secrets[secret.Name] = secret // Fix: Remove ObjectMeta + } + if configmap, found := obj.K8sObject.(*v1.ConfigMap); found { + configmaps[configmap.Name] = configmap // Fix: Remove ObjectMeta + } + } + return lintForEachContainer(lintCtx, object, ignoredSecrets, ignoredConfigMaps, secrets, configmaps) + }, nil + }), + }) +} + +func lintForEachContainer(lintCtx lintcontext.LintContext, object lintcontext.Object, ignoredSecrets, ignoredConfigMaps []*regexp.Regexp, secrets map[string]*v1.Secret, configmaps map[string]*v1.ConfigMap) []diagnostic.Diagnostic { + return util.PerContainerCheck(func(container *v1.Container) []diagnostic.Diagnostic { + var results []diagnostic.Diagnostic + for _, envVar := range container.Env { + valueFrom := envVar.ValueFrom + if valueFrom == nil { + continue + } + if secretKeySelector := valueFrom.SecretKeyRef; secretKeySelector != nil { + if secretKeySelector.Optional != nil && *secretKeySelector.Optional { + continue + } + if isInRegexList(ignoredSecrets, secretKeySelector.Name) { + continue + } + secret, ok := secrets[secretKeySelector.Name] + if !ok { + results = append(results, diagnostic.Diagnostic{ + Message: fmt.Sprintf("The container %q is referring to an unknown secret %q", container.Name, secretKeySelector.Name), + }) + continue + } + if isInList(Keys(secret.Data), secretKeySelector.Key) || isInList(Keys(secret.StringData), secretKeySelector.Key) { + continue + } + results = append(results, diagnostic.Diagnostic{ + Message: fmt.Sprintf("The container %q is referring to an unknown key %q in secret %q", container.Name, secretKeySelector.Key, secretKeySelector.Name), + }) + } + if configMapSelector := valueFrom.ConfigMapKeyRef; configMapSelector != nil { + if configMapSelector.Optional != nil && *configMapSelector.Optional { + continue + } + if isInRegexList(ignoredConfigMaps, configMapSelector.Name) { + continue + } + configmap, ok := configmaps[configMapSelector.Name] + if !ok { + results = append(results, diagnostic.Diagnostic{ + Message: fmt.Sprintf("The container %q is referring to an unknown config map %q", container.Name, configMapSelector.Name), + }) + continue + } + if isInList(Keys(configmap.Data), configMapSelector.Key) || isInList(Keys(configmap.BinaryData), configMapSelector.Key) { + continue + } + results = append(results, diagnostic.Diagnostic{ + Message: fmt.Sprintf("The container %q is referring to an unknown key %q in config map %q", container.Name, configMapSelector.Key, configMapSelector.Name), + }) + } + } + return results + })(lintCtx, object) +} + +func isInRegexList(regexlist []*regexp.Regexp, name string) bool { + for _, regex := range regexlist { + if regex.MatchString(name) { + return true + } + } + return false +} + +func isInList(regexlist []string, name string) bool { + for _, regex := range regexlist { + if name == regex { + return true + } + } + return false +} + +func Keys[K comparable, V any](m map[K]V) []K { + r := make([]K, 0, len(m)) + for k := range m { + r = append(r, k) + } + return r +} + +func extractRegexList(inputList []string) ([]*regexp.Regexp, error) { + result := make([]*regexp.Regexp, 0, len(inputList)) + for _, res := range inputList { + rg, err := regexp.Compile(res) + if err != nil { + return nil, errors.Wrapf(err, "invalid regex %s", res) + } + result = append(result, rg) + } + return result, nil +} diff --git a/pkg/templates/envvarvaluefrom/template_test.go b/pkg/templates/envvarvaluefrom/template_test.go new file mode 100644 index 000000000..48a679a2f --- /dev/null +++ b/pkg/templates/envvarvaluefrom/template_test.go @@ -0,0 +1,344 @@ +package envvarvaluefrom + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/suite" + "golang.stackrox.io/kube-linter/internal/pointers" + "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/envvarvaluefrom/internal/params" + coreV1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + targetDeploymentName = "deployment-1" +) + +func TestEnvVarValueFrom(t *testing.T) { + suite.Run(t, new(EnVarValueFromTestSuite)) +} + +type EnVarValueFromTestSuite struct { + templates.TemplateTestSuite + ctx *mocks.MockLintContext +} + +func (s *EnVarValueFromTestSuite) SetupTest() { + s.Init(templateKey) + s.ctx = mocks.NewMockContext() +} + +type sourceReference struct { + Name string + Key string + Optional *bool +} + +type envReference struct { + Name string + Kind string + Source sourceReference +} + +func makeSecretSource(descriptor sourceReference) *coreV1.EnvVarSource { + return &coreV1.EnvVarSource{ + SecretKeyRef: &coreV1.SecretKeySelector{ + LocalObjectReference: coreV1.LocalObjectReference{ + Name: descriptor.Name, + }, + Key: descriptor.Key, + Optional: descriptor.Optional, + }, + } +} + +func makeConfigMapSource(descriptor sourceReference) *coreV1.EnvVarSource { + return &coreV1.EnvVarSource{ + ConfigMapKeyRef: &coreV1.ConfigMapKeySelector{ + LocalObjectReference: coreV1.LocalObjectReference{ + Name: descriptor.Name, + }, + Key: descriptor.Key, + Optional: descriptor.Optional, + }, + } +} + +func (s *EnVarValueFromTestSuite) addContainerWithEnvFromSecret(envRef envReference) { // Fix: Remove name parameter + var valueFrom *coreV1.EnvVarSource + switch envRef.Kind { + case "secret": + valueFrom = makeSecretSource(envRef.Source) + case "configmap": + valueFrom = makeConfigMapSource(envRef.Source) + default: + s.Require().FailNow(fmt.Sprintf("Unknown source kind %s", envRef.Kind)) // Fix: Use s.Require().FailNow + } + s.ctx.AddContainerToDeployment(s.T(), targetDeploymentName, coreV1.Container{ // Fix: Hardcode targetDeploymentName + Name: "container", + Env: []coreV1.EnvVar{ + { + Name: "ENV_1", + ValueFrom: valueFrom, + }, + }, + }) +} + +func (s *EnVarValueFromTestSuite) TestDeploymentWithoutEnvPasses() { + s.ctx.AddMockDeployment(s.T(), targetDeploymentName) + s.ctx.AddContainerToDeployment(s.T(), targetDeploymentName, coreV1.Container{ + Name: "container", + Env: []coreV1.EnvVar{}, + }) + s.Validate(s.ctx, []templates.TestCase{ + { + Param: params.Params{}, + Diagnostics: map[string][]diagnostic.Diagnostic{ + targetDeploymentName: {}, + }, + ExpectInstantiationError: false, + }, + }) +} + +func (s *EnVarValueFromTestSuite) TestDeploymentWithDirectEnvPasses() { + s.ctx.AddMockDeployment(s.T(), targetDeploymentName) + s.ctx.AddContainerToDeployment(s.T(), targetDeploymentName, coreV1.Container{ + Name: "container", + Env: []coreV1.EnvVar{ + { + Name: "ENV_1", + Value: "Value", + }, + }, + }) + s.Validate(s.ctx, []templates.TestCase{ + { + Param: params.Params{}, + Diagnostics: map[string][]diagnostic.Diagnostic{ + targetDeploymentName: {}, + }, + ExpectInstantiationError: false, + }, + }) +} + +func (s *EnVarValueFromTestSuite) TestDeploymentWithUnknownSecret() { + s.ctx.AddMockDeployment(s.T(), targetDeploymentName) + s.addContainerWithEnvFromSecret(envReference{ + Name: "my-secret", + Kind: "secret", + Source: sourceReference{ + Name: "foo", + Key: "bar", + Optional: pointers.Bool(false), + }, + }) + s.Validate(s.ctx, []templates.TestCase{ + { + Param: params.Params{}, + Diagnostics: map[string][]diagnostic.Diagnostic{ + targetDeploymentName: {{ + Message: "The container \"container\" is referring to an unknown secret \"foo\"", + }}, + }, + ExpectInstantiationError: false, + }, + }) +} + +func (s *EnVarValueFromTestSuite) TestDeploymentWithNoOptionalSecret() { + s.ctx.AddMockDeployment(s.T(), targetDeploymentName) + s.addContainerWithEnvFromSecret(envReference{ + Name: "my-secret", + Kind: "secret", + Source: sourceReference{ + Name: "foo", + Key: "bar", + Optional: nil, + }, + }) + s.Validate(s.ctx, []templates.TestCase{ + { + Param: params.Params{}, + Diagnostics: map[string][]diagnostic.Diagnostic{ + targetDeploymentName: {{ + Message: "The container \"container\" is referring to an unknown secret \"foo\"", + }}, + }, + ExpectInstantiationError: false, + }, + }) +} + +func (s *EnVarValueFromTestSuite) TestDeploymentWithUnknownOptionalSecretPasses() { + s.ctx.AddMockDeployment(s.T(), targetDeploymentName) + s.addContainerWithEnvFromSecret(envReference{ + Name: "my-secret", + Kind: "secret", + Source: sourceReference{ + Name: "foo", + Key: "bar", + Optional: pointers.Bool(true), + }, + }) + s.Validate(s.ctx, []templates.TestCase{ + { + Param: params.Params{}, + Diagnostics: map[string][]diagnostic.Diagnostic{ + targetDeploymentName: {}, + }, + ExpectInstantiationError: false, + }, + }) +} + +func (s *EnVarValueFromTestSuite) TestDeploymentWithUnknownConfigMap() { + s.ctx.AddMockDeployment(s.T(), targetDeploymentName) + s.addContainerWithEnvFromSecret(envReference{ + Name: "my_config_var", + Kind: "configmap", + Source: sourceReference{ + Name: "foo", + Key: "bar", + Optional: pointers.Bool(false), + }, + }) + s.Validate(s.ctx, []templates.TestCase{ + { + Param: params.Params{}, + Diagnostics: map[string][]diagnostic.Diagnostic{ + targetDeploymentName: {{ + Message: "The container \"container\" is referring to an unknown config map \"foo\"", + }}, + }, + ExpectInstantiationError: false, + }, + }) +} + +func (s *EnVarValueFromTestSuite) TestDeploymentWithUnknownOptionalConfigMapPasses() { + s.ctx.AddMockDeployment(s.T(), targetDeploymentName) + s.addContainerWithEnvFromSecret(envReference{ + Name: "my_config_var", + Kind: "configmap", + Source: sourceReference{ + Name: "foo", + Key: "bar", + Optional: pointers.Bool(true), + }, + }) + s.Validate(s.ctx, []templates.TestCase{ + { + Param: params.Params{}, + Diagnostics: map[string][]diagnostic.Diagnostic{ + targetDeploymentName: {}, + }, + ExpectInstantiationError: false, + }, + }) +} + +func (s *EnVarValueFromTestSuite) TestDeploymentWithNoOptionalConfigMap() { + s.ctx.AddMockDeployment(s.T(), targetDeploymentName) + s.addContainerWithEnvFromSecret(envReference{ + Name: "my-config", + Kind: "configmap", + Source: sourceReference{ + Name: "foo", + Key: "bar", + Optional: nil, + }, + }) + s.Validate(s.ctx, []templates.TestCase{ + { + Param: params.Params{}, + Diagnostics: map[string][]diagnostic.Diagnostic{ + targetDeploymentName: {{ + Message: "The container \"container\" is referring to an unknown config map \"foo\"", + }}, + }, + ExpectInstantiationError: false, + }, + }) +} + +func (s *EnVarValueFromTestSuite) TestExtractRegexListInvalidPattern() { + p := params.Params{IgnoredSecrets: []string{"[invalid("}} // Invalid regex + _, err := extractRegexList(p.IgnoredSecrets) + s.Error(err) + s.Contains(err.Error(), "invalid regex [invalid(") +} + +func (s *EnVarValueFromTestSuite) TestExtractRegexListEmpty() { + regexList, err := extractRegexList([]string{}) + s.NoError(err) + s.Empty(regexList) +} + +func (s *EnVarValueFromTestSuite) TestUnknownKeyInSecret() { + s.ctx.AddMockDeployment(s.T(), targetDeploymentName) + secret := &coreV1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "test-secret"}, + Data: map[string][]byte{"key": []byte("value")}, + } + s.ctx.AddObject("test-secret", secret) // Fixed: Use object name as key, not s.T() + s.addContainerWithEnvFromSecret(envReference{ + Name: "my-secret", + Kind: "secret", + Source: sourceReference{ + Name: "test-secret", + Key: "unknown-key", + Optional: pointers.Bool(false), + }, + }) + s.Validate(s.ctx, []templates.TestCase{ + { + Param: params.Params{}, + Diagnostics: map[string][]diagnostic.Diagnostic{ + targetDeploymentName: {{ + Message: "The container \"container\" is referring to an unknown key \"unknown-key\" in secret \"test-secret\"", + }}, + }, + ExpectInstantiationError: false, + }, + }) +} + +func (s *EnVarValueFromTestSuite) TestIgnoredSecretWithRegex() { + s.ctx.AddMockDeployment(s.T(), targetDeploymentName) + secret := &coreV1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "ignored-secret"}, + Data: map[string][]byte{"key": []byte("value")}, + } + s.ctx.AddObject("ignored-secret", secret) // Fixed: Use object name as key, not s.T() + s.addContainerWithEnvFromSecret(envReference{ + Name: "my-secret", + Kind: "secret", + Source: sourceReference{ + Name: "ignored-secret", + Key: "key", + Optional: pointers.Bool(false), + }, + }) + s.Validate(s.ctx, []templates.TestCase{ + { + Param: params.Params{IgnoredSecrets: []string{"^ignored-secret$"}}, + Diagnostics: map[string][]diagnostic.Diagnostic{ + targetDeploymentName: {}, + }, + ExpectInstantiationError: false, + }, + }) +} + +func (s *EnVarValueFromTestSuite) TestKeysEmptyMap() { + emptyMap := map[string]string{} + keys := Keys(emptyMap) + s.Empty(keys) +} diff --git a/tests/checks/env-var-value-from.yml b/tests/checks/env-var-value-from.yml new file mode 100644 index 000000000..1e27938ae --- /dev/null +++ b/tests/checks/env-var-value-from.yml @@ -0,0 +1,84 @@ +--- +# This should fail due to key mismatch (chimpmunk vs chimpmun) +apiVersion: apps/v1 +kind: Deployment +metadata: + name: should-fire +spec: + selector: + matchLabels: + app: test + template: + metadata: + labels: + app: test + spec: + containers: + - name: app + image: nginx:1.14.2 + securityContext: + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 1000 + resources: + requests: + cpu: "100m" + memory: "64Mi" + limits: + cpu: "200m" + memory: "128Mi" + env: + - name: BLAH + valueFrom: + secretKeyRef: + name: secretsquirrels + key: chimpmunk +--- +apiVersion: v1 +kind: Secret +metadata: + name: secretsquirrels +stringData: + chimpmun: "aaaaaaa" +--- +# This should pass (exact match) +apiVersion: apps/v1 +kind: Deployment +metadata: + name: should-pass +spec: + selector: + matchLabels: + app: test + template: + metadata: + labels: + app: test + spec: + containers: + - name: app + image: nginx:1.14.2 + securityContext: + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 1000 + resources: + requests: + cpu: "100m" + memory: "64Mi" + limits: + cpu: "200m" + memory: "128Mi" + env: + - name: BLAH + valueFrom: + secretKeyRef: + name: validsecret + key: validkey +--- +apiVersion: v1 +kind: Secret +metadata: + name: validsecret +stringData: + validkey: "bbbbbbb"