From 567881418616e68c9e0977f67ac11bee9df88e81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20BUISSON?= Date: Mon, 31 Aug 2026 09:49:20 +0200 Subject: [PATCH] fix: resolve env-value-from references within their namespace --- pkg/templates/envvarvaluefrom/template.go | 83 +++++++++------- .../envvarvaluefrom/template_test.go | 95 +++++++++++++++++++ 2 files changed, 144 insertions(+), 34 deletions(-) diff --git a/pkg/templates/envvarvaluefrom/template.go b/pkg/templates/envvarvaluefrom/template.go index be9a4265d..0e23b3fdf 100644 --- a/pkg/templates/envvarvaluefrom/template.go +++ b/pkg/templates/envvarvaluefrom/template.go @@ -36,11 +36,29 @@ const ( type resourceChecker struct { objType string - objMap map[string]interface{} + objects map[string][]namespacedObject getKeys func(interface{}) []string ignoredRegex []*regexp.Regexp } +// namespacedObject pairs a secret or config map with its namespace: names repeat across namespaces. +type namespacedObject struct { + namespace string + object interface{} +} + +// inNamespace returns the objects a reference from namespace resolves against. +// An empty namespace matches any: manifests get one when they are applied. +func (c *resourceChecker) inNamespace(namespace, name string) []interface{} { + var matches []interface{} + for _, candidate := range c.objects[name] { + if candidate.namespace == namespace || candidate.namespace == "" || namespace == "" { + matches = append(matches, candidate.object) + } + } + return matches +} + func init() { templates.Register(check.Template{ HumanName: "Env references", @@ -61,14 +79,14 @@ func init() { 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) + secrets := make(map[string][]namespacedObject) + configmaps := make(map[string][]namespacedObject) for _, obj := range lintCtx.Objects() { if secret, found := obj.K8sObject.(*v1.Secret); found { - secrets[secret.Name] = secret // Fix: Remove ObjectMeta + secrets[secret.Name] = append(secrets[secret.Name], namespacedObject{namespace: secret.Namespace, object: secret}) } if configmap, found := obj.K8sObject.(*v1.ConfigMap); found { - configmaps[configmap.Name] = configmap // Fix: Remove ObjectMeta + configmaps[configmap.Name] = append(configmaps[configmap.Name], namespacedObject{namespace: configmap.Namespace, object: configmap}) } } return lintForEachContainer(lintCtx, object, ignoredSecrets, ignoredConfigMaps, secrets, configmaps) @@ -77,7 +95,23 @@ func init() { }) } -func lintForEachContainer(lintCtx lintcontext.LintContext, object lintcontext.Object, ignoredSecrets, ignoredConfigMaps []*regexp.Regexp, secrets map[string]*v1.Secret, configmaps map[string]*v1.ConfigMap) []diagnostic.Diagnostic { +func lintForEachContainer(lintCtx lintcontext.LintContext, object lintcontext.Object, ignoredSecrets, ignoredConfigMaps []*regexp.Regexp, secrets, configmaps map[string][]namespacedObject) []diagnostic.Diagnostic { + namespace := object.K8sObject.GetNamespace() + + secretChecker := &resourceChecker{ + objType: "secret", + objects: secrets, + getKeys: getSecretKeys, + ignoredRegex: ignoredSecrets, + } + + configMapChecker := &resourceChecker{ + objType: "config map", + objects: configmaps, + getKeys: getConfigMapKeys, + ignoredRegex: ignoredConfigMaps, + } + return util.PerContainerCheck(func(container *v1.Container) []diagnostic.Diagnostic { var results []diagnostic.Diagnostic var envRefs []struct { @@ -120,26 +154,6 @@ func lintForEachContainer(lintCtx lintcontext.LintContext, object lintcontext.Ob } } - secretChecker := &resourceChecker{ - objType: "secret", - objMap: make(map[string]interface{}), - getKeys: getSecretKeys, - ignoredRegex: ignoredSecrets, - } - for k, v := range secrets { - secretChecker.objMap[k] = v - } - - configMapChecker := &resourceChecker{ - objType: "config map", - objMap: make(map[string]interface{}), - getKeys: getConfigMapKeys, - ignoredRegex: ignoredConfigMaps, - } - for k, v := range configmaps { - configMapChecker.objMap[k] = v - } - for _, envRef := range envRefs { var checker *resourceChecker switch envRef.typ { @@ -149,7 +163,7 @@ func lintForEachContainer(lintCtx lintcontext.LintContext, object lintcontext.Ob checker = configMapChecker } - if msg := checkResourceReference(container.Name, envRef.info, checker); msg != "" { + if msg := checkResourceReference(container.Name, namespace, envRef.info, checker); msg != "" { results = append(results, diagnostic.Diagnostic{Message: msg}) } } @@ -157,7 +171,7 @@ func lintForEachContainer(lintCtx lintcontext.LintContext, object lintcontext.Ob })(lintCtx, object) } -func checkResourceReference(containerName string, ref resourceInfo, checker *resourceChecker) string { +func checkResourceReference(containerName, namespace string, ref resourceInfo, checker *resourceChecker) string { if ref.optional != nil && *ref.optional { return "" } @@ -166,17 +180,18 @@ func checkResourceReference(containerName string, ref resourceInfo, checker *res return "" } - obj, ok := checker.objMap[ref.name] - if !ok { + objs := checker.inNamespace(namespace, ref.name) + if len(objs) == 0 { return fmt.Sprintf("The container %q is referring to an unknown %s %q", containerName, checker.objType, ref.name) } - keys := checker.getKeys(obj) - if !isInList(keys, ref.key) { - return fmt.Sprintf("The container %q is referring to an unknown key %q in %s %q", containerName, ref.key, checker.objType, ref.name) + for _, obj := range objs { + if isInList(checker.getKeys(obj), ref.key) { + return "" + } } - return "" + return fmt.Sprintf("The container %q is referring to an unknown key %q in %s %q", containerName, ref.key, checker.objType, ref.name) } func isInRegexList(regexlist []*regexp.Regexp, name string) bool { diff --git a/pkg/templates/envvarvaluefrom/template_test.go b/pkg/templates/envvarvaluefrom/template_test.go index 68042e2fe..c6b9eff37 100644 --- a/pkg/templates/envvarvaluefrom/template_test.go +++ b/pkg/templates/envvarvaluefrom/template_test.go @@ -10,6 +10,7 @@ import ( "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" + appsV1 "k8s.io/api/apps/v1" coreV1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -397,6 +398,100 @@ func (s *EnVarValueFromTestSuite) TestMultipleIgnoredSecrets() { }) } +func (s *EnVarValueFromTestSuite) TestSecretOfAnotherNamespaceDoesNotShadow() { + s.ctx.AddMockDeployment(s.T(), targetDeploymentName) + s.ctx.ModifyDeployment(s.T(), targetDeploymentName, func(deployment *appsV1.Deployment) { + deployment.Namespace = "alpha" + }) + alpha := &coreV1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "shared", Namespace: "alpha"}, + Data: map[string][]byte{"key-alpha": []byte("value")}, + } + beta := &coreV1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "shared", Namespace: "beta"}, + Data: map[string][]byte{"key-beta": []byte("value")}, + } + s.ctx.AddObject("alpha/shared", alpha) + s.ctx.AddObject("beta/shared", beta) + s.addContainerWithEnvFromSecret(envReference{ + Name: "my-secret", + Kind: "secret", + Source: sourceReference{ + Name: "shared", + Key: "key-alpha", + Optional: pointers.Bool(false), + }, + }) + s.Validate(s.ctx, []templates.TestCase{ + { + Param: params.Params{}, + Diagnostics: map[string][]diagnostic.Diagnostic{ + targetDeploymentName: {}, + }, + }, + }) +} + +func (s *EnVarValueFromTestSuite) TestSecretOfAnotherNamespaceIsUnknown() { + s.ctx.AddMockDeployment(s.T(), targetDeploymentName) + s.ctx.ModifyDeployment(s.T(), targetDeploymentName, func(deployment *appsV1.Deployment) { + deployment.Namespace = "alpha" + }) + beta := &coreV1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "shared", Namespace: "beta"}, + Data: map[string][]byte{"key-beta": []byte("value")}, + } + s.ctx.AddObject("beta/shared", beta) + s.addContainerWithEnvFromSecret(envReference{ + Name: "my-secret", + Kind: "secret", + Source: sourceReference{ + Name: "shared", + Key: "key-beta", + 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 \"shared\"", + }}, + }, + }, + }) +} + +func (s *EnVarValueFromTestSuite) TestSecretWithoutNamespaceStillResolves() { + s.ctx.AddMockDeployment(s.T(), targetDeploymentName) + s.ctx.ModifyDeployment(s.T(), targetDeploymentName, func(deployment *appsV1.Deployment) { + deployment.Namespace = "alpha" + }) + secret := &coreV1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "shared"}, + Data: map[string][]byte{"key": []byte("value")}, + } + s.ctx.AddObject("shared", secret) + s.addContainerWithEnvFromSecret(envReference{ + Name: "my-secret", + Kind: "secret", + Source: sourceReference{ + Name: "shared", + Key: "key", + Optional: pointers.Bool(false), + }, + }) + s.Validate(s.ctx, []templates.TestCase{ + { + Param: params.Params{}, + Diagnostics: map[string][]diagnostic.Diagnostic{ + targetDeploymentName: {}, + }, + }, + }) +} + func (s *EnVarValueFromTestSuite) TestEmptyObjectList() { s.ctx.AddMockDeployment(s.T(), targetDeploymentName) s.addContainerWithEnvFromSecret(envReference{