From abb6a8ba40e8ba43432ace2d8840eb0af7b0c195 Mon Sep 17 00:00:00 2001 From: diyliv Date: Wed, 17 Jun 2026 12:46:30 +0300 Subject: [PATCH 01/11] add mount points validation rule Signed-off-by: diyliv --- internal/module/module.go | 1 + pkg/config.go | 1 + pkg/config/global/global.go | 1 + pkg/linters/templates/rules/mount_points.go | 150 ++++++ .../templates/rules/mount_points_test.go | 429 ++++++++++++++++++ pkg/linters/templates/templates.go | 3 + 6 files changed, 585 insertions(+) create mode 100644 pkg/linters/templates/rules/mount_points.go create mode 100644 pkg/linters/templates/rules/mount_points_test.go diff --git a/internal/module/module.go b/internal/module/module.go index 7756ffa0..70e6a266 100644 --- a/internal/module/module.go +++ b/internal/module/module.go @@ -341,6 +341,7 @@ func mapTemplatesRules(linterSettings *pkg.LintersSettings, configSettings *conf rules.ClusterDomainRule.SetLevel(globalRules.ClusterDomainRule.Impact, fallbackImpact) rules.RegistryRule.SetLevel(globalRules.RegistryRule.Impact, fallbackImpact) rules.EnabledModulesRule.SetLevel(globalRules.EnabledModulesRule.Impact, fallbackImpact) + rules.MountPointsRule.SetLevel(globalRules.MountPointsRule.Impact, fallbackImpact) } // mapOpenAPIRules configures OpenAPI linter rules diff --git a/pkg/config.go b/pkg/config.go index b84bded0..4ea920b6 100644 --- a/pkg/config.go +++ b/pkg/config.go @@ -141,6 +141,7 @@ type TemplatesLinterRules struct { ClusterDomainRule RuleConfig RegistryRule RuleConfig EnabledModulesRule RuleConfig + MountPointsRule RuleConfig } type PrometheusRuleSettings struct { diff --git a/pkg/config/global/global.go b/pkg/config/global/global.go index d6c969a9..be88f1f1 100644 --- a/pkg/config/global/global.go +++ b/pkg/config/global/global.go @@ -139,6 +139,7 @@ type TemplatesLinterRules struct { ClusterDomainRule RuleConfig `mapstructure:"cluster-domain"` RegistryRule RuleConfig `mapstructure:"registry"` EnabledModulesRule RuleConfig `mapstructure:"enabled-modules"` + MountPointsRule RuleConfig `mapstructure:"mount-points"` } func (c LinterConfig) IsWarn() bool { diff --git a/pkg/linters/templates/rules/mount_points.go b/pkg/linters/templates/rules/mount_points.go new file mode 100644 index 00000000..61edcc81 --- /dev/null +++ b/pkg/linters/templates/rules/mount_points.go @@ -0,0 +1,150 @@ +/* +Copyright 2026 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rules + +import ( + "os" + "path/filepath" + "strings" + + "gopkg.in/yaml.v3" + + "github.com/deckhouse/dmt/pkg" + "github.com/deckhouse/dmt/pkg/errors" +) + +const ( + MountPointsRuleName = "mount-points" +) + +type mountPointsFile struct { + Dirs []string `yaml:"dirs"` +} + +func NewMountPointsRule() *MountPointsRule { + return &MountPointsRule{ + RuleMeta: pkg.RuleMeta{ + Name: MountPointsRuleName, + }, + } +} + +type MountPointsRule struct { + pkg.RuleMeta +} + +func (r *MountPointsRule) ValidateMountPoints(m pkg.Module, errorList *errors.LintRuleErrorsList) { + errorList = errorList.WithRule(r.GetName()) + + dirsByFile := collectMountPointsDirs(m, errorList) + if len(dirsByFile) == 0 { + return + } + + templateMountPaths := collectTemplateMountPaths(m, errorList) + if len(templateMountPaths) == 0 { + return + } + + for filePath, dirs := range dirsByFile { + for _, dir := range dirs { + normalizedDir := strings.TrimRight(dir, "/") + if !templateMountPaths[normalizedDir] { + errorList.WithFilePath(filePath). + Errorf("mount-points.yaml references dir %q which is not used as a mountPath in any pod controller", dir) + } + } + } +} + +func collectMountPointsDirs(m pkg.Module, errorList *errors.LintRuleErrorsList) map[string][]string { + dirsByFile := make(map[string][]string) + + modulePath := m.GetPath() + if modulePath == "" { + return dirsByFile + } + + searchDir := filepath.Join(modulePath, "images") + if _, err := os.Stat(searchDir); os.IsNotExist(err) { + return dirsByFile + } + + err := filepath.Walk(modulePath, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if info.IsDir() { + return nil + } + + if filepath.Base(path) != "mount-points.yaml" { + return nil + } + + data, err := os.ReadFile(path) + if err != nil { + errorList.Errorf("failed to read %s: %s", path, err) + return nil + } + + var mpf mountPointsFile + if err := yaml.Unmarshal(data, &mpf); err != nil { + errorList.Errorf("failed to parse %s: %s", path, err) + return nil + } + + if len(mpf.Dirs) > 0 { + dirsByFile[path] = mpf.Dirs + } + + return nil + }) + + if err != nil { + errorList.Errorf("failed to walk module directory: %s", err) + } + + return dirsByFile +} + +func collectTemplateMountPaths(m pkg.Module, errorList *errors.LintRuleErrorsList) map[string]bool { + mountPaths := make(map[string]bool) + + for _, object := range m.GetStorage() { + if !IsPodController(object.Unstructured.GetKind()) { + continue + } + + containers, err := object.GetAllContainers() + if err != nil { + errorList.WithObjectID(object.Identity()). + Errorf("failed to get containers for object: %s", err) + continue + } + + for _, container := range containers { + for _, vm := range container.VolumeMounts { + normalizedPath := strings.TrimRight(vm.MountPath, "/") + mountPaths[normalizedPath] = true + } + } + } + + return mountPaths +} diff --git a/pkg/linters/templates/rules/mount_points_test.go b/pkg/linters/templates/rules/mount_points_test.go new file mode 100644 index 00000000..ca2dfbda --- /dev/null +++ b/pkg/linters/templates/rules/mount_points_test.go @@ -0,0 +1,429 @@ +/* +Copyright 2026 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rules + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "helm.sh/helm/v3/pkg/chart" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + + "github.com/deckhouse/dmt/internal/storage" + "github.com/deckhouse/dmt/pkg/errors" +) + +func deploymentWithMounts(name string, mountPaths ...string) storage.StoreObject { + volumeMounts := []any{} + for _, mp := range mountPaths { + volumeMounts = append(volumeMounts, map[string]any{ + "name": "vol-" + name, + "mountPath": mp, + }) + } + + return storage.StoreObject{ + Unstructured: unstructured.Unstructured{ + Object: map[string]any{ + "kind": "Deployment", + "apiVersion": "apps/v1", + "metadata": map[string]any{ + "name": name, + "namespace": "default", + }, + "spec": map[string]any{ + "template": map[string]any{ + "spec": map[string]any{ + "containers": []map[string]any{ + { + "name": "main", + "image": "test:latest", + "volumeMounts": volumeMounts, + }, + }, + }, + }, + }, + }, + }, + } +} + +func TestNewMountPointsRule(t *testing.T) { + rule := NewMountPointsRule() + assert.Equal(t, MountPointsRuleName, rule.Name) +} + +func TestMountPointsRule_AllDirsMatched(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "mount-points-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + imagesDir := filepath.Join(tmpDir, "images", "app") + if err := os.MkdirAll(imagesDir, 0755); err != nil { + t.Fatal(err) + } + + mountPointsYAML := `dirs: + - /etc/app + - /etc/app/certs +` + if err := os.WriteFile(filepath.Join(imagesDir, "mount-points.yaml"), []byte(mountPointsYAML), 0600); err != nil { + t.Fatal(err) + } + + storageMap := map[storage.ResourceIndex]storage.StoreObject{ + {Kind: "Deployment", Name: "app", Namespace: "default"}: deploymentWithMounts("app", "/etc/app", "/etc/app/certs"), + } + + errorList := errors.NewLintRuleErrorsList() + rule := NewMountPointsRule() + rule.ValidateMountPoints(&mockMountPointsModule{path: tmpDir, storage: storageMap}, errorList) + + errs := errorList.GetErrors() + assert.Len(t, errs, 0) +} + +func TestMountPointsRule_MissingDir(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "mount-points-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + imagesDir := filepath.Join(tmpDir, "images", "app") + if err := os.MkdirAll(imagesDir, 0755); err != nil { + t.Fatal(err) + } + + mountPointsYAML := `dirs: + - /etc/app + - /etc/not-mounted +` + if err := os.WriteFile(filepath.Join(imagesDir, "mount-points.yaml"), []byte(mountPointsYAML), 0600); err != nil { + t.Fatal(err) + } + + storageMap := map[storage.ResourceIndex]storage.StoreObject{ + {Kind: "Deployment", Name: "app", Namespace: "default"}: deploymentWithMounts("app", "/etc/app"), + } + + errorList := errors.NewLintRuleErrorsList() + rule := NewMountPointsRule() + rule.ValidateMountPoints(&mockMountPointsModule{path: tmpDir, storage: storageMap}, errorList) + + errs := errorList.GetErrors() + assert.Len(t, errs, 1) + assert.Contains(t, errs[0].Text, `"/etc/not-mounted"`) +} + +func TestMountPointsRule_MultipleFiles(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "mount-points-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + for _, img := range []string{"app1", "app2"} { + imgDir := filepath.Join(tmpDir, "images", img) + if err := os.MkdirAll(imgDir, 0755); err != nil { + t.Fatal(err) + } + + mountPointsYAML := `dirs: + - /etc/` + img + ` +` + if err := os.WriteFile(filepath.Join(imgDir, "mount-points.yaml"), []byte(mountPointsYAML), 0600); err != nil { + t.Fatal(err) + } + } + + storageMap := map[storage.ResourceIndex]storage.StoreObject{ + {Kind: "Deployment", Name: "app1", Namespace: "default"}: deploymentWithMounts("app1", "/etc/app1"), + {Kind: "Deployment", Name: "app2", Namespace: "default"}: deploymentWithMounts("app2", "/etc/app2"), + } + + errorList := errors.NewLintRuleErrorsList() + rule := NewMountPointsRule() + rule.ValidateMountPoints(&mockMountPointsModule{path: tmpDir, storage: storageMap}, errorList) + + errs := errorList.GetErrors() + assert.Len(t, errs, 0) +} + +func TestMountPointsRule_NoMountPointsFile(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "mount-points-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + imagesDir := filepath.Join(tmpDir, "images", "app") + if err := os.MkdirAll(imagesDir, 0755); err != nil { + t.Fatal(err) + } + + storageMap := map[storage.ResourceIndex]storage.StoreObject{ + {Kind: "Deployment", Name: "app", Namespace: "default"}: deploymentWithMounts("app", "/etc/app"), + } + + errorList := errors.NewLintRuleErrorsList() + rule := NewMountPointsRule() + rule.ValidateMountPoints(&mockMountPointsModule{path: tmpDir, storage: storageMap}, errorList) + + errs := errorList.GetErrors() + assert.Len(t, errs, 0) +} + +func TestMountPointsRule_EmptyMountPointsFile(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "mount-points-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + imagesDir := filepath.Join(tmpDir, "images", "app") + if err := os.MkdirAll(imagesDir, 0755); err != nil { + t.Fatal(err) + } + + mountPointsYAML := `dirs: [] +` + if err := os.WriteFile(filepath.Join(imagesDir, "mount-points.yaml"), []byte(mountPointsYAML), 0600); err != nil { + t.Fatal(err) + } + + storageMap := map[storage.ResourceIndex]storage.StoreObject{ + {Kind: "Deployment", Name: "app", Namespace: "default"}: deploymentWithMounts("app", "/etc/app"), + } + + errorList := errors.NewLintRuleErrorsList() + rule := NewMountPointsRule() + rule.ValidateMountPoints(&mockMountPointsModule{path: tmpDir, storage: storageMap}, errorList) + + errs := errorList.GetErrors() + assert.Len(t, errs, 0) +} + +func TestMountPointsRule_NoPodControllers(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "mount-points-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + imagesDir := filepath.Join(tmpDir, "images", "app") + if err := os.MkdirAll(imagesDir, 0755); err != nil { + t.Fatal(err) + } + + mountPointsYAML := `dirs: + - /etc/app +` + if err := os.WriteFile(filepath.Join(imagesDir, "mount-points.yaml"), []byte(mountPointsYAML), 0600); err != nil { + t.Fatal(err) + } + + errorList := errors.NewLintRuleErrorsList() + rule := NewMountPointsRule() + rule.ValidateMountPoints(&mockMountPointsModule{path: tmpDir, storage: nil}, errorList) + + errs := errorList.GetErrors() + assert.Len(t, errs, 0) +} + +func TestMountPointsRule_DaemonSetAndStatefulSet(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "mount-points-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + imagesDir := filepath.Join(tmpDir, "images", "app") + if err := os.MkdirAll(imagesDir, 0755); err != nil { + t.Fatal(err) + } + + mountPointsYAML := `dirs: + - /etc/daemon + - /etc/sts +` + if err := os.WriteFile(filepath.Join(imagesDir, "mount-points.yaml"), []byte(mountPointsYAML), 0600); err != nil { + t.Fatal(err) + } + + dsMounts := []any{ + map[string]any{"name": "vol-ds", "mountPath": "/etc/daemon"}, + } + dsObj := storage.StoreObject{ + Unstructured: unstructured.Unstructured{ + Object: map[string]any{ + "kind": "DaemonSet", + "apiVersion": "apps/v1", + "metadata": map[string]any{"name": "ds", "namespace": "default"}, + "spec": map[string]any{ + "template": map[string]any{ + "spec": map[string]any{ + "containers": []map[string]any{ + {"name": "main", "image": "test:latest", "volumeMounts": dsMounts}, + }, + }, + }, + }, + }, + }, + } + + stsMounts := []any{ + map[string]any{"name": "vol-sts", "mountPath": "/etc/sts"}, + } + stsObj := storage.StoreObject{ + Unstructured: unstructured.Unstructured{ + Object: map[string]any{ + "kind": "StatefulSet", + "apiVersion": "apps/v1", + "metadata": map[string]any{"name": "sts", "namespace": "default"}, + "spec": map[string]any{ + "template": map[string]any{ + "spec": map[string]any{ + "containers": []map[string]any{ + {"name": "main", "image": "test:latest", "volumeMounts": stsMounts}, + }, + }, + }, + }, + }, + }, + } + + storageMap := map[storage.ResourceIndex]storage.StoreObject{ + {Kind: "DaemonSet", Name: "ds", Namespace: "default"}: dsObj, + {Kind: "StatefulSet", Name: "sts", Namespace: "default"}: stsObj, + } + + errorList := errors.NewLintRuleErrorsList() + rule := NewMountPointsRule() + rule.ValidateMountPoints(&mockMountPointsModule{path: tmpDir, storage: storageMap}, errorList) + + errs := errorList.GetErrors() + assert.Len(t, errs, 0) +} + +func TestMountPointsRule_TrailingSlashMatch(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "mount-points-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + imagesDir := filepath.Join(tmpDir, "images", "app") + if err := os.MkdirAll(imagesDir, 0755); err != nil { + t.Fatal(err) + } + + mountPointsYAML := `dirs: + - /etc/app/ +` + if err := os.WriteFile(filepath.Join(imagesDir, "mount-points.yaml"), []byte(mountPointsYAML), 0600); err != nil { + t.Fatal(err) + } + + storageMap := map[storage.ResourceIndex]storage.StoreObject{ + {Kind: "Deployment", Name: "app", Namespace: "default"}: deploymentWithMounts("app", "/etc/app"), + } + + errorList := errors.NewLintRuleErrorsList() + rule := NewMountPointsRule() + rule.ValidateMountPoints(&mockMountPointsModule{path: tmpDir, storage: storageMap}, errorList) + + errs := errorList.GetErrors() + assert.Len(t, errs, 0) +} + +func TestMountPointsRule_InitContainers(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "mount-points-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + imagesDir := filepath.Join(tmpDir, "images", "app") + if err := os.MkdirAll(imagesDir, 0755); err != nil { + t.Fatal(err) + } + + mountPointsYAML := `dirs: + - /etc/init +` + if err := os.WriteFile(filepath.Join(imagesDir, "mount-points.yaml"), []byte(mountPointsYAML), 0600); err != nil { + t.Fatal(err) + } + + storageMap := map[storage.ResourceIndex]storage.StoreObject{ + {Kind: "Deployment", Name: "app", Namespace: "default"}: { + Unstructured: unstructured.Unstructured{ + Object: map[string]any{ + "kind": "Deployment", + "apiVersion": "apps/v1", + "metadata": map[string]any{"name": "app", "namespace": "default"}, + "spec": map[string]any{ + "template": map[string]any{ + "spec": map[string]any{ + "containers": []map[string]any{ + {"name": "main", "image": "test:latest"}, + }, + "initContainers": []map[string]any{ + { + "name": "init", + "image": "init:latest", + "volumeMounts": []map[string]any{ + {"name": "vol-init", "mountPath": "/etc/init"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + errorList := errors.NewLintRuleErrorsList() + rule := NewMountPointsRule() + rule.ValidateMountPoints(&mockMountPointsModule{path: tmpDir, storage: storageMap}, errorList) + + errs := errorList.GetErrors() + assert.Len(t, errs, 0) +} + +type mockMountPointsModule struct { + path string + storage map[storage.ResourceIndex]storage.StoreObject +} + +func (m *mockMountPointsModule) GetName() string { return "test-module" } +func (m *mockMountPointsModule) GetNamespace() string { return "default" } +func (m *mockMountPointsModule) GetPath() string { return m.path } +func (m *mockMountPointsModule) GetWerfFile() string { return "" } +func (m *mockMountPointsModule) GetChart() *chart.Chart { return nil } +func (m *mockMountPointsModule) GetObjectStore() *storage.UnstructuredObjectStore { return nil } +func (m *mockMountPointsModule) GetStorage() map[storage.ResourceIndex]storage.StoreObject { return m.storage } diff --git a/pkg/linters/templates/templates.go b/pkg/linters/templates/templates.go index 3ed5261b..d931f77b 100644 --- a/pkg/linters/templates/templates.go +++ b/pkg/linters/templates/templates.go @@ -98,6 +98,9 @@ func (l *Templates) Run(m *module.Module) { l.cfg.ExcludeRules.EnabledModules.Files.Get(), l.cfg.ExcludeRules.EnabledModules.Directories.Get(), ).CheckEnabledModules(m, errorList.WithMaxLevel(l.cfg.Rules.EnabledModulesRule.GetLevel())) + + // MountPoints rule + rules.NewMountPointsRule().ValidateMountPoints(m, errorList.WithMaxLevel(l.cfg.Rules.MountPointsRule.GetLevel())) } func (l *Templates) Name() string { From 2913fc08ba52d43375496cf7103150a8368a07be Mon Sep 17 00:00:00 2001 From: diyliv Date: Wed, 17 Jun 2026 12:54:10 +0300 Subject: [PATCH 02/11] add mount points exclude and docs Signed-off-by: diyliv --- internal/module/module.go | 1 + pkg/config.go | 1 + pkg/config/linters_settings.go | 2 + pkg/linters/templates/README.md | 71 +++++++++++++++++++ pkg/linters/templates/rules/mount_points.go | 10 ++- .../templates/rules/mount_points_test.go | 53 +++++++++++--- pkg/linters/templates/templates.go | 2 +- 7 files changed, 128 insertions(+), 12 deletions(-) diff --git a/internal/module/module.go b/internal/module/module.go index 70e6a266..c5361a43 100644 --- a/internal/module/module.go +++ b/internal/module/module.go @@ -455,6 +455,7 @@ func mapTemplatesExclusionsAndSettings(linterSettings *pkg.LintersSettings, conf excludes.Ingress = configExcludes.Ingress.Get() excludes.EnabledModules.Files = pkg.StringRuleExcludeList(configExcludes.EnabledModules.Files) excludes.EnabledModules.Directories = pkg.PrefixRuleExcludeList(configExcludes.EnabledModules.Directories) + excludes.MountPoints = pkg.StringRuleExcludeList(configExcludes.MountPoints) // Additional settings linterSettings.Templates.PrometheusRuleSettings.Disable = configSettings.Templates.PrometheusRules.Disable diff --git a/pkg/config.go b/pkg/config.go index 4ea920b6..e865e31c 100644 --- a/pkg/config.go +++ b/pkg/config.go @@ -158,6 +158,7 @@ type TemplatesExcludeRules struct { KubeRBACProxy StringRuleExcludeList Ingress KindRuleExcludeList EnabledModules EnabledModulesExcludeRule + MountPoints StringRuleExcludeList } type EnabledModulesExcludeRule struct { diff --git a/pkg/config/linters_settings.go b/pkg/config/linters_settings.go index 1469b27e..0a03aebe 100644 --- a/pkg/config/linters_settings.go +++ b/pkg/config/linters_settings.go @@ -227,6 +227,7 @@ type TemplatesLinterRules struct { ClusterDomainRule RuleConfig `mapstructure:"cluster-domain"` RegistryRule RuleConfig `mapstructure:"registry"` EnabledModulesRule RuleConfig `mapstructure:"enabled-modules"` + MountPointsRule RuleConfig `mapstructure:"mount-points"` } type TemplatesExcludeRules struct { @@ -236,6 +237,7 @@ type TemplatesExcludeRules struct { KubeRBACProxy StringRuleExcludeList `mapstructure:"kube-rbac-proxy"` Ingress KindRuleExcludeList `mapstructure:"ingress"` EnabledModules EnabledModulesExcludeRule `mapstructure:"enabled-modules"` + MountPoints StringRuleExcludeList `mapstructure:"mount-points"` } type EnabledModulesExcludeRule struct { diff --git a/pkg/linters/templates/README.md b/pkg/linters/templates/README.md index f1e4c27e..3cb4a1df 100644 --- a/pkg/linters/templates/README.md +++ b/pkg/linters/templates/README.md @@ -21,6 +21,7 @@ Proper template validation prevents runtime issues, ensures applications are pro | [registry](#registry) | Validates registry secret configuration | ❌ | enabled | | [werf](#werf) | Validates image names in `werf.yaml` do not contain underscores | ❌ | enabled | | [enabled-modules](#enabled-modules) | Detects usage of `.Values.global.enabledModules` in templates | ✅ | enabled | +| [mount-points](#mount-points) | Validates that mount-points.yaml directories are used as volumeMounts in pod controllers | ✅ | enabled | ## Rule Details @@ -1642,6 +1643,76 @@ linters-settings: - templates/vendor/ # Exclude entire directory ``` +--- + +### mount-points + +**Purpose:** Ensures that all directories listed in `mount-points.yaml` files are actually used as `volumeMount.mountPath` in at least one pod controller (Deployment, StatefulSet, or DaemonSet). This prevents containerd v2 from crashing when trying to mount into a non-existent directory. + +**Description:** + +Recursively searches the module directory for `mount-points.yaml` files (typically located under `images//`). Each file declares a list of directories that the container expects to have available for mounting. The rule verifies that every declared directory appears as a `mountPath` in at least one pod controller's volume mount (including init containers). + +**What it checks:** + +1. All `mount-points.yaml` files found recursively in the module directory +2. Every directory listed under `dirs:` is present as `volumeMounts[].mountPath` in at least one Deployment, StatefulSet, or DaemonSet +3. Both main containers and init containers are checked +4. Trailing slashes are normalized for comparison + +**Why it matters:** + +containerd v2 fails critically if a directory specified as a mount point has not been created and something attempts to mount into it. Keeping `mount-points.yaml` in sync with actual template usage prevents runtime container crashes. + +**Examples:** + +mount-points.yaml (`images/app/mount-points.yaml`): +```yaml +dirs: + - /etc/app + - /etc/app/certs +``` + +❌ **Incorrect** - Directory not referenced in any template: + +`/etc/app/certs` is declared in `mount-points.yaml` but no pod controller uses it as a mountPath. + +**Error:** +``` +mount-points.yaml references dir "/etc/app/certs" which is not used as a mountPath in any pod controller +``` + +✅ **Correct** - All directories used in templates: + +```yaml +# templates/deployment.yaml +apiVersion: apps/v1 +kind: Deployment +spec: + template: + spec: + containers: + - name: app + volumeMounts: + - name: config + mountPath: /etc/app + - name: certs + mountPath: /etc/app/certs +``` + +**Configuration:** + +The rule supports excluding specific directories from the check: + +```yaml +# .dmt.yaml +linters-settings: + templates: + exclude-rules: + mount-points: + - /etc/ignore-this-dir # Exclude specific directory +``` + ## Configuration The Templates linter can be configured at the module level with rule-specific settings and exclusions. diff --git a/pkg/linters/templates/rules/mount_points.go b/pkg/linters/templates/rules/mount_points.go index 61edcc81..67a2fa70 100644 --- a/pkg/linters/templates/rules/mount_points.go +++ b/pkg/linters/templates/rules/mount_points.go @@ -35,16 +35,20 @@ type mountPointsFile struct { Dirs []string `yaml:"dirs"` } -func NewMountPointsRule() *MountPointsRule { +func NewMountPointsRule(excludeRules []pkg.StringRuleExclude) *MountPointsRule { return &MountPointsRule{ RuleMeta: pkg.RuleMeta{ Name: MountPointsRuleName, }, + StringRule: pkg.StringRule{ + ExcludeRules: excludeRules, + }, } } type MountPointsRule struct { pkg.RuleMeta + pkg.StringRule } func (r *MountPointsRule) ValidateMountPoints(m pkg.Module, errorList *errors.LintRuleErrorsList) { @@ -62,6 +66,10 @@ func (r *MountPointsRule) ValidateMountPoints(m pkg.Module, errorList *errors.Li for filePath, dirs := range dirsByFile { for _, dir := range dirs { + if !r.Enabled(dir) { + continue + } + normalizedDir := strings.TrimRight(dir, "/") if !templateMountPaths[normalizedDir] { errorList.WithFilePath(filePath). diff --git a/pkg/linters/templates/rules/mount_points_test.go b/pkg/linters/templates/rules/mount_points_test.go index ca2dfbda..ed4d5bb6 100644 --- a/pkg/linters/templates/rules/mount_points_test.go +++ b/pkg/linters/templates/rules/mount_points_test.go @@ -26,6 +26,7 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "github.com/deckhouse/dmt/internal/storage" + "github.com/deckhouse/dmt/pkg" "github.com/deckhouse/dmt/pkg/errors" ) @@ -66,7 +67,7 @@ func deploymentWithMounts(name string, mountPaths ...string) storage.StoreObject } func TestNewMountPointsRule(t *testing.T) { - rule := NewMountPointsRule() + rule := NewMountPointsRule(nil) assert.Equal(t, MountPointsRuleName, rule.Name) } @@ -95,7 +96,7 @@ func TestMountPointsRule_AllDirsMatched(t *testing.T) { } errorList := errors.NewLintRuleErrorsList() - rule := NewMountPointsRule() + rule := NewMountPointsRule(nil) rule.ValidateMountPoints(&mockMountPointsModule{path: tmpDir, storage: storageMap}, errorList) errs := errorList.GetErrors() @@ -127,7 +128,7 @@ func TestMountPointsRule_MissingDir(t *testing.T) { } errorList := errors.NewLintRuleErrorsList() - rule := NewMountPointsRule() + rule := NewMountPointsRule(nil) rule.ValidateMountPoints(&mockMountPointsModule{path: tmpDir, storage: storageMap}, errorList) errs := errorList.GetErrors() @@ -162,7 +163,7 @@ func TestMountPointsRule_MultipleFiles(t *testing.T) { } errorList := errors.NewLintRuleErrorsList() - rule := NewMountPointsRule() + rule := NewMountPointsRule(nil) rule.ValidateMountPoints(&mockMountPointsModule{path: tmpDir, storage: storageMap}, errorList) errs := errorList.GetErrors() @@ -186,7 +187,7 @@ func TestMountPointsRule_NoMountPointsFile(t *testing.T) { } errorList := errors.NewLintRuleErrorsList() - rule := NewMountPointsRule() + rule := NewMountPointsRule(nil) rule.ValidateMountPoints(&mockMountPointsModule{path: tmpDir, storage: storageMap}, errorList) errs := errorList.GetErrors() @@ -216,7 +217,7 @@ func TestMountPointsRule_EmptyMountPointsFile(t *testing.T) { } errorList := errors.NewLintRuleErrorsList() - rule := NewMountPointsRule() + rule := NewMountPointsRule(nil) rule.ValidateMountPoints(&mockMountPointsModule{path: tmpDir, storage: storageMap}, errorList) errs := errorList.GetErrors() @@ -243,7 +244,7 @@ func TestMountPointsRule_NoPodControllers(t *testing.T) { } errorList := errors.NewLintRuleErrorsList() - rule := NewMountPointsRule() + rule := NewMountPointsRule(nil) rule.ValidateMountPoints(&mockMountPointsModule{path: tmpDir, storage: nil}, errorList) errs := errorList.GetErrors() @@ -320,7 +321,7 @@ func TestMountPointsRule_DaemonSetAndStatefulSet(t *testing.T) { } errorList := errors.NewLintRuleErrorsList() - rule := NewMountPointsRule() + rule := NewMountPointsRule(nil) rule.ValidateMountPoints(&mockMountPointsModule{path: tmpDir, storage: storageMap}, errorList) errs := errorList.GetErrors() @@ -351,7 +352,7 @@ func TestMountPointsRule_TrailingSlashMatch(t *testing.T) { } errorList := errors.NewLintRuleErrorsList() - rule := NewMountPointsRule() + rule := NewMountPointsRule(nil) rule.ValidateMountPoints(&mockMountPointsModule{path: tmpDir, storage: storageMap}, errorList) errs := errorList.GetErrors() @@ -408,7 +409,39 @@ func TestMountPointsRule_InitContainers(t *testing.T) { } errorList := errors.NewLintRuleErrorsList() - rule := NewMountPointsRule() + rule := NewMountPointsRule(nil) + rule.ValidateMountPoints(&mockMountPointsModule{path: tmpDir, storage: storageMap}, errorList) + + errs := errorList.GetErrors() + assert.Len(t, errs, 0) +} + +func TestMountPointsRule_ExcludeDir(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "mount-points-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + imagesDir := filepath.Join(tmpDir, "images", "app") + if err := os.MkdirAll(imagesDir, 0755); err != nil { + t.Fatal(err) + } + + mountPointsYAML := `dirs: + - /etc/app + - /etc/not-mounted +` + if err := os.WriteFile(filepath.Join(imagesDir, "mount-points.yaml"), []byte(mountPointsYAML), 0600); err != nil { + t.Fatal(err) + } + + storageMap := map[storage.ResourceIndex]storage.StoreObject{ + {Kind: "Deployment", Name: "app", Namespace: "default"}: deploymentWithMounts("app", "/etc/app"), + } + + errorList := errors.NewLintRuleErrorsList() + rule := NewMountPointsRule([]pkg.StringRuleExclude{"/etc/not-mounted"}) rule.ValidateMountPoints(&mockMountPointsModule{path: tmpDir, storage: storageMap}, errorList) errs := errorList.GetErrors() diff --git a/pkg/linters/templates/templates.go b/pkg/linters/templates/templates.go index d931f77b..a95638ed 100644 --- a/pkg/linters/templates/templates.go +++ b/pkg/linters/templates/templates.go @@ -100,7 +100,7 @@ func (l *Templates) Run(m *module.Module) { ).CheckEnabledModules(m, errorList.WithMaxLevel(l.cfg.Rules.EnabledModulesRule.GetLevel())) // MountPoints rule - rules.NewMountPointsRule().ValidateMountPoints(m, errorList.WithMaxLevel(l.cfg.Rules.MountPointsRule.GetLevel())) + rules.NewMountPointsRule(l.cfg.ExcludeRules.MountPoints.Get()).ValidateMountPoints(m, errorList.WithMaxLevel(l.cfg.Rules.MountPointsRule.GetLevel())) } func (l *Templates) Name() string { From 944f8e52cba0c13b6d585638d5c772f0a393fa56 Mon Sep 17 00:00:00 2001 From: diyliv Date: Wed, 17 Jun 2026 14:53:45 +0300 Subject: [PATCH 03/11] fix mount points early return and exclude normalization Signed-off-by: diyliv --- pkg/linters/templates/rules/mount_points.go | 9 +- .../templates/rules/mount_points_test.go | 114 ++++++++++++++++-- 2 files changed, 106 insertions(+), 17 deletions(-) diff --git a/pkg/linters/templates/rules/mount_points.go b/pkg/linters/templates/rules/mount_points.go index 67a2fa70..b0e09001 100644 --- a/pkg/linters/templates/rules/mount_points.go +++ b/pkg/linters/templates/rules/mount_points.go @@ -60,17 +60,14 @@ func (r *MountPointsRule) ValidateMountPoints(m pkg.Module, errorList *errors.Li } templateMountPaths := collectTemplateMountPaths(m, errorList) - if len(templateMountPaths) == 0 { - return - } for filePath, dirs := range dirsByFile { for _, dir := range dirs { - if !r.Enabled(dir) { + normalizedDir := strings.TrimRight(dir, "/") + if !r.Enabled(normalizedDir) { continue } - normalizedDir := strings.TrimRight(dir, "/") if !templateMountPaths[normalizedDir] { errorList.WithFilePath(filePath). Errorf("mount-points.yaml references dir %q which is not used as a mountPath in any pod controller", dir) @@ -123,7 +120,6 @@ func collectMountPointsDirs(m pkg.Module, errorList *errors.LintRuleErrorsList) return nil }) - if err != nil { errorList.Errorf("failed to walk module directory: %s", err) } @@ -143,6 +139,7 @@ func collectTemplateMountPaths(m pkg.Module, errorList *errors.LintRuleErrorsLis if err != nil { errorList.WithObjectID(object.Identity()). Errorf("failed to get containers for object: %s", err) + continue } diff --git a/pkg/linters/templates/rules/mount_points_test.go b/pkg/linters/templates/rules/mount_points_test.go index ed4d5bb6..9d31289a 100644 --- a/pkg/linters/templates/rules/mount_points_test.go +++ b/pkg/linters/templates/rules/mount_points_test.go @@ -31,7 +31,7 @@ import ( ) func deploymentWithMounts(name string, mountPaths ...string) storage.StoreObject { - volumeMounts := []any{} + volumeMounts := make([]any, 0, len(mountPaths)) for _, mp := range mountPaths { volumeMounts = append(volumeMounts, map[string]any{ "name": "vol-" + name, @@ -224,7 +224,7 @@ func TestMountPointsRule_EmptyMountPointsFile(t *testing.T) { assert.Len(t, errs, 0) } -func TestMountPointsRule_NoPodControllers(t *testing.T) { +func TestMountPointsRule_NoPodControllers_ReportsUnusedDir(t *testing.T) { tmpDir, err := os.MkdirTemp("", "mount-points-test") if err != nil { t.Fatal(err) @@ -248,7 +248,8 @@ func TestMountPointsRule_NoPodControllers(t *testing.T) { rule.ValidateMountPoints(&mockMountPointsModule{path: tmpDir, storage: nil}, errorList) errs := errorList.GetErrors() - assert.Len(t, errs, 0) + assert.Len(t, errs, 1) + assert.Contains(t, errs[0].Text, `"/etc/app"`) } func TestMountPointsRule_DaemonSetAndStatefulSet(t *testing.T) { @@ -316,7 +317,7 @@ func TestMountPointsRule_DaemonSetAndStatefulSet(t *testing.T) { } storageMap := map[storage.ResourceIndex]storage.StoreObject{ - {Kind: "DaemonSet", Name: "ds", Namespace: "default"}: dsObj, + {Kind: "DaemonSet", Name: "ds", Namespace: "default"}: dsObj, {Kind: "StatefulSet", Name: "sts", Namespace: "default"}: stsObj, } @@ -448,15 +449,106 @@ func TestMountPointsRule_ExcludeDir(t *testing.T) { assert.Len(t, errs, 0) } +func TestMountPointsRule_ExcludeDirWithTrailingSlash(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "mount-points-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + imagesDir := filepath.Join(tmpDir, "images", "app") + if err := os.MkdirAll(imagesDir, 0755); err != nil { + t.Fatal(err) + } + + mountPointsYAML := `dirs: + - /etc/app + - /etc/not-mounted/ +` + if err := os.WriteFile(filepath.Join(imagesDir, "mount-points.yaml"), []byte(mountPointsYAML), 0600); err != nil { + t.Fatal(err) + } + + storageMap := map[storage.ResourceIndex]storage.StoreObject{ + {Kind: "Deployment", Name: "app", Namespace: "default"}: deploymentWithMounts("app", "/etc/app"), + } + + errorList := errors.NewLintRuleErrorsList() + rule := NewMountPointsRule([]pkg.StringRuleExclude{"/etc/not-mounted"}) + rule.ValidateMountPoints(&mockMountPointsModule{path: tmpDir, storage: storageMap}, errorList) + + errs := errorList.GetErrors() + assert.Len(t, errs, 0) +} + +func TestMountPointsRule_ControllerWithoutVolumeMounts(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "mount-points-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + imagesDir := filepath.Join(tmpDir, "images", "app") + if err := os.MkdirAll(imagesDir, 0755); err != nil { + t.Fatal(err) + } + + mountPointsYAML := `dirs: + - /etc/app +` + if err := os.WriteFile(filepath.Join(imagesDir, "mount-points.yaml"), []byte(mountPointsYAML), 0600); err != nil { + t.Fatal(err) + } + + deploymentWithNoMounts := storage.StoreObject{ + Unstructured: unstructured.Unstructured{ + Object: map[string]any{ + "kind": "Deployment", + "apiVersion": "apps/v1", + "metadata": map[string]any{ + "name": "app", + "namespace": "default", + }, + "spec": map[string]any{ + "template": map[string]any{ + "spec": map[string]any{ + "containers": []map[string]any{ + { + "name": "main", + "image": "test:latest", + }, + }, + }, + }, + }, + }, + }, + } + + storageMap := map[storage.ResourceIndex]storage.StoreObject{ + {Kind: "Deployment", Name: "app", Namespace: "default"}: deploymentWithNoMounts, + } + + errorList := errors.NewLintRuleErrorsList() + rule := NewMountPointsRule(nil) + rule.ValidateMountPoints(&mockMountPointsModule{path: tmpDir, storage: storageMap}, errorList) + + errs := errorList.GetErrors() + assert.Len(t, errs, 1) + assert.Contains(t, errs[0].Text, `"/etc/app"`) +} + type mockMountPointsModule struct { path string storage map[storage.ResourceIndex]storage.StoreObject } -func (m *mockMountPointsModule) GetName() string { return "test-module" } -func (m *mockMountPointsModule) GetNamespace() string { return "default" } -func (m *mockMountPointsModule) GetPath() string { return m.path } -func (m *mockMountPointsModule) GetWerfFile() string { return "" } -func (m *mockMountPointsModule) GetChart() *chart.Chart { return nil } -func (m *mockMountPointsModule) GetObjectStore() *storage.UnstructuredObjectStore { return nil } -func (m *mockMountPointsModule) GetStorage() map[storage.ResourceIndex]storage.StoreObject { return m.storage } +func (m *mockMountPointsModule) GetName() string { return "test-module" } +func (m *mockMountPointsModule) GetNamespace() string { return "default" } +func (m *mockMountPointsModule) GetPath() string { return m.path } +func (m *mockMountPointsModule) GetWerfFile() string { return "" } +func (m *mockMountPointsModule) GetChart() *chart.Chart { return nil } +func (m *mockMountPointsModule) GetObjectStore() *storage.UnstructuredObjectStore { return nil } +func (m *mockMountPointsModule) GetStorage() map[storage.ResourceIndex]storage.StoreObject { + return m.storage +} From 57afb3f8e3ad1795c71ffbfba6a917a32e9daf1f Mon Sep 17 00:00:00 2001 From: diyliv Date: Wed, 17 Jun 2026 19:34:48 +0300 Subject: [PATCH 04/11] warn missing mount points dir Signed-off-by: diyliv --- pkg/linters/templates/rules/mount_points.go | 2 +- pkg/linters/templates/rules/mount_points_test.go | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/linters/templates/rules/mount_points.go b/pkg/linters/templates/rules/mount_points.go index b0e09001..9bdd9013 100644 --- a/pkg/linters/templates/rules/mount_points.go +++ b/pkg/linters/templates/rules/mount_points.go @@ -70,7 +70,7 @@ func (r *MountPointsRule) ValidateMountPoints(m pkg.Module, errorList *errors.Li if !templateMountPaths[normalizedDir] { errorList.WithFilePath(filePath). - Errorf("mount-points.yaml references dir %q which is not used as a mountPath in any pod controller", dir) + Warnf("mount-points.yaml references dir %q which is not used as a mountPath in any pod controller", dir) } } } diff --git a/pkg/linters/templates/rules/mount_points_test.go b/pkg/linters/templates/rules/mount_points_test.go index 9d31289a..4ec8a35b 100644 --- a/pkg/linters/templates/rules/mount_points_test.go +++ b/pkg/linters/templates/rules/mount_points_test.go @@ -133,6 +133,7 @@ func TestMountPointsRule_MissingDir(t *testing.T) { errs := errorList.GetErrors() assert.Len(t, errs, 1) + assert.Equal(t, pkg.Warn, errs[0].Level) assert.Contains(t, errs[0].Text, `"/etc/not-mounted"`) } @@ -249,6 +250,7 @@ func TestMountPointsRule_NoPodControllers_ReportsUnusedDir(t *testing.T) { errs := errorList.GetErrors() assert.Len(t, errs, 1) + assert.Equal(t, pkg.Warn, errs[0].Level) assert.Contains(t, errs[0].Text, `"/etc/app"`) } @@ -535,6 +537,7 @@ func TestMountPointsRule_ControllerWithoutVolumeMounts(t *testing.T) { errs := errorList.GetErrors() assert.Len(t, errs, 1) + assert.Equal(t, pkg.Warn, errs[0].Level) assert.Contains(t, errs[0].Text, `"/etc/app"`) } From 0ba8af96daf1ca6e959bc42348e102adedc24fb0 Mon Sep 17 00:00:00 2001 From: diyliv Date: Wed, 17 Jun 2026 19:36:05 +0300 Subject: [PATCH 05/11] document exclude paths in readme Signed-off-by: diyliv --- pkg/linters/templates/README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/linters/templates/README.md b/pkg/linters/templates/README.md index 3cb4a1df..e23e5d50 100644 --- a/pkg/linters/templates/README.md +++ b/pkg/linters/templates/README.md @@ -1711,8 +1711,13 @@ linters-settings: exclude-rules: mount-points: - /etc/ignore-this-dir # Exclude specific directory + - /run/secrets/ # Exclude entire tree (pod managed outside Helm) + - /var/run/secrets/istiod # Exclude single path ``` +**When to exclude:** Pods managed outside Helm (operators, mutating webhooks, static pods, bashible) have `volumeMounts` that are not present in Helm templates. Directories from `mount-points.yaml` for these containers will produce false positives — exclude them with the corresponding paths. + + ## Configuration The Templates linter can be configured at the module level with rule-specific settings and exclusions. From e745fc0e42b7f197c6ef91999a4ccd2e88930e34 Mon Sep 17 00:00:00 2001 From: diyliv Date: Wed, 17 Jun 2026 22:34:35 +0300 Subject: [PATCH 06/11] add container mount points reverse rule Signed-off-by: diyliv --- internal/module/module.go | 6 + pkg/config.go | 4 +- pkg/config/global/global.go | 1 + pkg/config/linters_settings.go | 3 +- pkg/linters/container/container.go | 4 + pkg/linters/container/rules.go | 4 + pkg/linters/container/rules/mount_points.go | 141 ++++++++++ .../container/rules/mount_points_test.go | 258 ++++++++++++++++++ .../mount-points-missing/expected.yaml | 9 + .../module/images/app/mount-points.yaml | 3 + .../mount-points-missing/module/module.yaml | 2 + .../module/openapi/config-values.yaml | 2 + .../module/openapi/values.yaml | 4 + .../module/templates/deployment.yaml | 27 ++ 14 files changed, 466 insertions(+), 2 deletions(-) create mode 100644 pkg/linters/container/rules/mount_points.go create mode 100644 pkg/linters/container/rules/mount_points_test.go create mode 100644 test/e2e/testdata/container/mount-points-missing/expected.yaml create mode 100644 test/e2e/testdata/container/mount-points-missing/module/images/app/mount-points.yaml create mode 100644 test/e2e/testdata/container/mount-points-missing/module/module.yaml create mode 100644 test/e2e/testdata/container/mount-points-missing/module/openapi/config-values.yaml create mode 100644 test/e2e/testdata/container/mount-points-missing/module/openapi/values.yaml create mode 100644 test/e2e/testdata/container/mount-points-missing/module/templates/deployment.yaml diff --git a/internal/module/module.go b/internal/module/module.go index c5361a43..d4b4935f 100644 --- a/internal/module/module.go +++ b/internal/module/module.go @@ -284,6 +284,10 @@ func mapContainerRules(linterSettings *pkg.LintersSettings, configSettings *conf globalConfig.Container.Rules.ReadinessRule.Impact, configSettings.Container.Impact, ) + linterSettings.Container.Rules.MountPointsRule.SetLevel( + globalConfig.Container.Rules.MountPointsRule.Impact, + configSettings.Container.Impact, + ) } // mapImageRules configures Image linter rules @@ -407,7 +411,9 @@ func mapContainerExclusions(linterSettings *pkg.LintersSettings, configSettings excludes.Readiness = configExcludes.Readiness.Get() excludes.SeccompProfile = configExcludes.SeccompProfile.Get() excludes.NoNewPrivileges = configExcludes.NoNewPrivileges.Get() + excludes.ImageDigest = configExcludes.ImageDigest.Get() excludes.Description = pkg.StringRuleExcludeList(configExcludes.Description) + excludes.MountPoints = pkg.StringRuleExcludeList(configExcludes.MountPoints) } // mapImageExclusionsAndSettings maps Image linter exclusions and additional settings diff --git a/pkg/config.go b/pkg/config.go index e865e31c..1e4f1a1e 100644 --- a/pkg/config.go +++ b/pkg/config.go @@ -331,6 +331,7 @@ type ContainerLinterRules struct { PortsRule RuleConfig LivenessRule RuleConfig ReadinessRule RuleConfig + MountPointsRule RuleConfig } type ContainerExcludeRules struct { @@ -349,7 +350,8 @@ type ContainerExcludeRules struct { Liveness ContainerRuleExcludeList Readiness ContainerRuleExcludeList - Description StringRuleExcludeList + Description StringRuleExcludeList + MountPoints StringRuleExcludeList } type StringRuleExcludeList []string diff --git a/pkg/config/global/global.go b/pkg/config/global/global.go index be88f1f1..c8d1ab63 100644 --- a/pkg/config/global/global.go +++ b/pkg/config/global/global.go @@ -67,6 +67,7 @@ type ContainerRules struct { PortsRule RuleConfig `mapstructure:"ports"` LivenessRule RuleConfig `mapstructure:"liveness-probe"` ReadinessRule RuleConfig `mapstructure:"readiness-probe"` + MountPointsRule RuleConfig `mapstructure:"mount-points"` } type ImagesLinterConfig struct { diff --git a/pkg/config/linters_settings.go b/pkg/config/linters_settings.go index 0a03aebe..3559fe8d 100644 --- a/pkg/config/linters_settings.go +++ b/pkg/config/linters_settings.go @@ -70,7 +70,8 @@ type ContainerExcludeRules struct { Liveness ContainerRuleExcludeList `mapstructure:"liveness-probe"` Readiness ContainerRuleExcludeList `mapstructure:"readiness-probe"` - Description StringRuleExcludeList `mapstructure:"description"` + Description StringRuleExcludeList `mapstructure:"description"` + MountPoints StringRuleExcludeList `mapstructure:"mount-points"` } type HooksSettings struct { diff --git a/pkg/linters/container/container.go b/pkg/linters/container/container.go index bc24e951..b6a1a88b 100644 --- a/pkg/linters/container/container.go +++ b/pkg/linters/container/container.go @@ -31,6 +31,8 @@ type Container struct { name, desc string cfg *pkg.ContainerLinterConfig ErrorList *errors.LintRuleErrorsList + + modulePath string } func New(containerCfg *pkg.ContainerLinterConfig, errorList *errors.LintRuleErrorsList) *Container { @@ -49,6 +51,8 @@ func (l *Container) Run(m *module.Module) { errorList := l.ErrorList.WithModule(m.GetName()) + l.modulePath = m.GetPath() + storage := m.GetStorage() for _, object := range storage { l.applyContainerRules(object, storage, errorList) diff --git a/pkg/linters/container/rules.go b/pkg/linters/container/rules.go index ba5e4241..e0e79bf1 100644 --- a/pkg/linters/container/rules.go +++ b/pkg/linters/container/rules.go @@ -91,6 +91,10 @@ func (l *Container) applyContainerRules(object storage.StoreObject, storageMap m func(object storage.StoreObject, containers []corev1.Container, errorList *errors.LintRuleErrorsList) { rules.NewPortsRule(l.cfg.ExcludeRules.Ports.Get()).ContainerPorts(object, containers, errorList.WithMaxLevel(l.cfg.Rules.PortsRule.GetLevel())) }, + func(object storage.StoreObject, containers []corev1.Container, errorList *errors.LintRuleErrorsList) { + rules.NewMountPointsRule(l.cfg.ExcludeRules.MountPoints.Get(), l.modulePath). + CheckMountPaths(object, containers, errorList.WithMaxLevel(l.cfg.Rules.MountPointsRule.GetLevel())) + }, } for _, rule := range containerRules { diff --git a/pkg/linters/container/rules/mount_points.go b/pkg/linters/container/rules/mount_points.go new file mode 100644 index 00000000..91788246 --- /dev/null +++ b/pkg/linters/container/rules/mount_points.go @@ -0,0 +1,141 @@ +/* +Copyright 2026 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rules + +import ( + "os" + "path/filepath" + "strings" + "sync" + + "gopkg.in/yaml.v3" + corev1 "k8s.io/api/core/v1" + + "github.com/deckhouse/dmt/internal/storage" + "github.com/deckhouse/dmt/pkg" + "github.com/deckhouse/dmt/pkg/errors" +) + +const ( + MountPointsRuleName = "mount-points" +) + +type mountPointsFile struct { + Dirs []string `yaml:"dirs"` +} + +func NewMountPointsRule(excludeRules []pkg.StringRuleExclude, modulePath string) *MountPointsRule { + return &MountPointsRule{ + RuleMeta: pkg.RuleMeta{ + Name: MountPointsRuleName, + }, + StringRule: pkg.StringRule{ + ExcludeRules: excludeRules, + }, + mountPointsDirs: collectMountPointsDirs(modulePath), + } +} + +type MountPointsRule struct { + pkg.RuleMeta + pkg.StringRule + mountPointsDirs map[string]bool +} + +// CheckMountPaths verifies that every volumeMount.mountPath in pod controllers +// is declared in at least one mount-points.yaml file in the module. +// +// Direction: templates → mount-points.yaml (reverse of the existing templates rule). +func (r *MountPointsRule) CheckMountPaths(object storage.StoreObject, containers []corev1.Container, errorList *errors.LintRuleErrorsList) { + errorList = errorList.WithRule(r.GetName()).WithFilePath(object.ShortPath()) + + if len(r.mountPointsDirs) == 0 { + return + } + + switch object.Unstructured.GetKind() { + case "Deployment", "DaemonSet", "StatefulSet": + default: + return + } + + for _, container := range containers { + for _, vm := range container.VolumeMounts { + normalizedPath := strings.TrimRight(vm.MountPath, "/") + if !r.Enabled(normalizedPath) { + continue + } + + if !r.mountPointsDirs[normalizedPath] { + errorList.WithObjectID(object.Identity()). + Warnf("Container %q mountPath %q is not declared in any mount-points.yaml", container.Name, vm.MountPath) + } + } + } +} + +var mpDirsCache sync.Map // map[string]map[string]bool + +// collectMountPointsDirs walks the module directory and collects all dirs +// from mount-points.yaml files into a set keyed by normalized path. +// Results are cached per module path. +func collectMountPointsDirs(modulePath string) map[string]bool { + if cached, ok := mpDirsCache.Load(modulePath); ok { + return cached.(map[string]bool) + } + + dirs := make(map[string]bool) + + searchDir := filepath.Join(modulePath, "images") + if _, err := os.Stat(searchDir); os.IsNotExist(err) { + mpDirsCache.Store(modulePath, dirs) + return dirs + } + + _ = filepath.Walk(modulePath, func(path string, info os.FileInfo, err error) error { + if err != nil { + return nil + } + + if info.IsDir() { + return nil + } + + if filepath.Base(path) != "mount-points.yaml" { + return nil + } + + data, err := os.ReadFile(path) + if err != nil { + return nil + } + + var mpf mountPointsFile + if err := yaml.Unmarshal(data, &mpf); err != nil { + return nil + } + + for _, dir := range mpf.Dirs { + dirs[strings.TrimRight(dir, "/")] = true + } + + return nil + }) + + mpDirsCache.Store(modulePath, dirs) + return dirs +} diff --git a/pkg/linters/container/rules/mount_points_test.go b/pkg/linters/container/rules/mount_points_test.go new file mode 100644 index 00000000..7f16a19a --- /dev/null +++ b/pkg/linters/container/rules/mount_points_test.go @@ -0,0 +1,258 @@ +/* +Copyright 2026 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rules + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + + "github.com/deckhouse/dmt/internal/storage" + "github.com/deckhouse/dmt/pkg" + "github.com/deckhouse/dmt/pkg/errors" +) + +func objectWithMounts(kind, name string, mountPaths ...string) storage.StoreObject { + volumeMounts := make([]corev1.VolumeMount, 0, len(mountPaths)) + for _, mp := range mountPaths { + volumeMounts = append(volumeMounts, corev1.VolumeMount{ + Name: "vol", + MountPath: mp, + }) + } + + return storage.StoreObject{ + Unstructured: unstructured.Unstructured{ + Object: map[string]any{ + "kind": kind, + "apiVersion": "apps/v1", + "metadata": map[string]any{ + "name": name, + "namespace": "default", + }, + "spec": map[string]any{ + "template": map[string]any{ + "spec": map[string]any{ + "containers": []map[string]any{ + { + "name": "main", + "image": "test:latest", + "volumeMounts": func() []any { + var mounts []any + for _, vm := range volumeMounts { + mounts = append(mounts, map[string]any{ + "name": vm.Name, + "mountPath": vm.MountPath, + }) + } + return mounts + }(), + }, + }, + }, + }, + }, + }, + }, + } +} + +func writeMountPointsFile(t *testing.T, dir, content string) { + t.Helper() + + imagesDir := filepath.Join(dir, "images", "app") + if err := os.MkdirAll(imagesDir, 0755); err != nil { + t.Fatal(err) + } + + if err := os.WriteFile(filepath.Join(imagesDir, "mount-points.yaml"), []byte(content), 0600); err != nil { + t.Fatal(err) + } +} + +func TestMountPointsContainerRule_AllDeclared(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "mpcr-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + writeMountPointsFile(t, tmpDir, `dirs: + - /etc/app + - /etc/app/certs +`) + + obj := objectWithMounts("Deployment", "app", "/etc/app", "/etc/app/certs") + + containers, err := obj.GetAllContainers() + assert.NoError(t, err) + + errorList := errors.NewLintRuleErrorsList() + rule := NewMountPointsRule(nil, tmpDir) + rule.CheckMountPaths(obj, containers, errorList) + + assert.Len(t, errorList.GetErrors(), 0) +} + +func TestMountPointsContainerRule_MissingDeclared(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "mpcr-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + writeMountPointsFile(t, tmpDir, `dirs: + - /etc/app +`) + + obj := objectWithMounts("Deployment", "app", "/etc/app", "/etc/missing") + + containers, err := obj.GetAllContainers() + assert.NoError(t, err) + + errorList := errors.NewLintRuleErrorsList() + rule := NewMountPointsRule(nil, tmpDir) + rule.CheckMountPaths(obj, containers, errorList) + + errs := errorList.GetErrors() + assert.Len(t, errs, 1) + assert.Equal(t, pkg.Warn, errs[0].Level) + assert.Contains(t, errs[0].Text, "not declared in any mount-points.yaml") +} + +func TestMountPointsContainerRule_NoMountPointsFiles(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "mpcr-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + obj := objectWithMounts("Deployment", "app", "/etc/app", "/etc/missing") + + containers, err := obj.GetAllContainers() + assert.NoError(t, err) + + errorList := errors.NewLintRuleErrorsList() + rule := NewMountPointsRule(nil, tmpDir) + rule.CheckMountPaths(obj, containers, errorList) + + assert.Len(t, errorList.GetErrors(), 0) +} + +func TestMountPointsContainerRule_ExcludedPath(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "mpcr-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + writeMountPointsFile(t, tmpDir, `dirs: + - /etc/app +`) + + obj := objectWithMounts("Deployment", "app", "/etc/app", "/etc/excluded") + + containers, err := obj.GetAllContainers() + assert.NoError(t, err) + + errorList := errors.NewLintRuleErrorsList() + rule := NewMountPointsRule([]pkg.StringRuleExclude{"/etc/excluded"}, tmpDir) + rule.CheckMountPaths(obj, containers, errorList) + + assert.Len(t, errorList.GetErrors(), 0) +} + +func TestMountPointsContainerRule_NonPodController(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "mpcr-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + writeMountPointsFile(t, tmpDir, `dirs: + - /etc/app +`) + + obj := objectWithMounts("ConfigMap", "app", "/etc/not-checked") + + containers, err := obj.GetAllContainers() + assert.NoError(t, err) + + errorList := errors.NewLintRuleErrorsList() + rule := NewMountPointsRule(nil, tmpDir) + rule.CheckMountPaths(obj, containers, errorList) + + assert.Len(t, errorList.GetErrors(), 0) +} + +func TestMountPointsContainerRule_TrailingSlash(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "mpcr-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + writeMountPointsFile(t, tmpDir, `dirs: + - /etc/app/ +`) + + obj := objectWithMounts("Deployment", "app", "/etc/app") + + containers, err := obj.GetAllContainers() + assert.NoError(t, err) + + errorList := errors.NewLintRuleErrorsList() + rule := NewMountPointsRule(nil, tmpDir) + rule.CheckMountPaths(obj, containers, errorList) + + assert.Len(t, errorList.GetErrors(), 0) +} + +func TestMountPointsContainerRule_DaemonSetAndStatefulSet(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "mpcr-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + writeMountPointsFile(t, tmpDir, `dirs: + - /etc/daemon + - /etc/sts +`) + + dsObj := objectWithMounts("DaemonSet", "ds", "/etc/daemon") + dsContainers, err := dsObj.GetAllContainers() + assert.NoError(t, err) + + errorList := errors.NewLintRuleErrorsList() + rule := NewMountPointsRule(nil, tmpDir) + rule.CheckMountPaths(dsObj, dsContainers, errorList) + assert.Len(t, errorList.GetErrors(), 0) + + stsObj := objectWithMounts("StatefulSet", "sts", "/etc/sts") + stsContainers, err := stsObj.GetAllContainers() + assert.NoError(t, err) + + errorList = errors.NewLintRuleErrorsList() + rule = NewMountPointsRule(nil, tmpDir) + rule.CheckMountPaths(stsObj, stsContainers, errorList) + assert.Len(t, errorList.GetErrors(), 0) +} diff --git a/test/e2e/testdata/container/mount-points-missing/expected.yaml b/test/e2e/testdata/container/mount-points-missing/expected.yaml new file mode 100644 index 00000000..be1bfef0 --- /dev/null +++ b/test/e2e/testdata/container/mount-points-missing/expected.yaml @@ -0,0 +1,9 @@ +description: > + Reverse mount-points rule warns when a volumeMount in templates + is not declared in any mount-points.yaml file. +module: module +expect: + - linter: container + rule: mount-points + level: warn + textContains: 'mountPath "/etc/missing" is not declared' diff --git a/test/e2e/testdata/container/mount-points-missing/module/images/app/mount-points.yaml b/test/e2e/testdata/container/mount-points-missing/module/images/app/mount-points.yaml new file mode 100644 index 00000000..e335f82f --- /dev/null +++ b/test/e2e/testdata/container/mount-points-missing/module/images/app/mount-points.yaml @@ -0,0 +1,3 @@ +dirs: + - /etc/app + - /var/data diff --git a/test/e2e/testdata/container/mount-points-missing/module/module.yaml b/test/e2e/testdata/container/mount-points-missing/module/module.yaml new file mode 100644 index 00000000..c29509f1 --- /dev/null +++ b/test/e2e/testdata/container/mount-points-missing/module/module.yaml @@ -0,0 +1,2 @@ +name: e2e-mount-points-missing +namespace: e2e-mount-points-missing diff --git a/test/e2e/testdata/container/mount-points-missing/module/openapi/config-values.yaml b/test/e2e/testdata/container/mount-points-missing/module/openapi/config-values.yaml new file mode 100644 index 00000000..03b0d8bf --- /dev/null +++ b/test/e2e/testdata/container/mount-points-missing/module/openapi/config-values.yaml @@ -0,0 +1,2 @@ +type: object +properties: {} diff --git a/test/e2e/testdata/container/mount-points-missing/module/openapi/values.yaml b/test/e2e/testdata/container/mount-points-missing/module/openapi/values.yaml new file mode 100644 index 00000000..47180da5 --- /dev/null +++ b/test/e2e/testdata/container/mount-points-missing/module/openapi/values.yaml @@ -0,0 +1,4 @@ +x-extend: + schema: config-values.yaml +type: object +properties: {} diff --git a/test/e2e/testdata/container/mount-points-missing/module/templates/deployment.yaml b/test/e2e/testdata/container/mount-points-missing/module/templates/deployment.yaml new file mode 100644 index 00000000..5230a671 --- /dev/null +++ b/test/e2e/testdata/container/mount-points-missing/module/templates/deployment.yaml @@ -0,0 +1,27 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: test-app + namespace: e2e-mount-points-missing +spec: + selector: + matchLabels: + app: test-app + template: + metadata: + labels: + app: test-app + spec: + containers: + - name: app + image: test:latest + volumeMounts: + - name: config + mountPath: /etc/app + - name: missing + mountPath: /etc/missing + - name: sidecar + image: sidecar:latest + volumeMounts: + - name: data + mountPath: /var/data From 191b6f720ab5ea5c402d38ffee78b50c2064f49d Mon Sep 17 00:00:00 2001 From: diyliv Date: Wed, 17 Jun 2026 22:35:59 +0300 Subject: [PATCH 07/11] fix lint issues in mount points Signed-off-by: diyliv --- pkg/linters/container/rules/mount_points.go | 1 + pkg/linters/container/rules/mount_points_test.go | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/linters/container/rules/mount_points.go b/pkg/linters/container/rules/mount_points.go index 91788246..2639a4a1 100644 --- a/pkg/linters/container/rules/mount_points.go +++ b/pkg/linters/container/rules/mount_points.go @@ -137,5 +137,6 @@ func collectMountPointsDirs(modulePath string) map[string]bool { }) mpDirsCache.Store(modulePath, dirs) + return dirs } diff --git a/pkg/linters/container/rules/mount_points_test.go b/pkg/linters/container/rules/mount_points_test.go index 7f16a19a..868d8b59 100644 --- a/pkg/linters/container/rules/mount_points_test.go +++ b/pkg/linters/container/rules/mount_points_test.go @@ -56,13 +56,14 @@ func objectWithMounts(kind, name string, mountPaths ...string) storage.StoreObje "name": "main", "image": "test:latest", "volumeMounts": func() []any { - var mounts []any + mounts := make([]any, 0, len(volumeMounts)) for _, vm := range volumeMounts { mounts = append(mounts, map[string]any{ "name": vm.Name, "mountPath": vm.MountPath, }) } + return mounts }(), }, From e8ad9923f8f251d219bdda0cded271bc46fe3b59 Mon Sep 17 00:00:00 2001 From: diyliv Date: Wed, 17 Jun 2026 22:38:26 +0300 Subject: [PATCH 08/11] fix gofmt struct alignment Signed-off-by: diyliv --- pkg/config.go | 4 ++-- pkg/config/linters_settings.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/config.go b/pkg/config.go index 1e4f1a1e..6d6839dc 100644 --- a/pkg/config.go +++ b/pkg/config.go @@ -350,8 +350,8 @@ type ContainerExcludeRules struct { Liveness ContainerRuleExcludeList Readiness ContainerRuleExcludeList - Description StringRuleExcludeList - MountPoints StringRuleExcludeList + Description StringRuleExcludeList + MountPoints StringRuleExcludeList } type StringRuleExcludeList []string diff --git a/pkg/config/linters_settings.go b/pkg/config/linters_settings.go index 3559fe8d..fbaacd9f 100644 --- a/pkg/config/linters_settings.go +++ b/pkg/config/linters_settings.go @@ -70,8 +70,8 @@ type ContainerExcludeRules struct { Liveness ContainerRuleExcludeList `mapstructure:"liveness-probe"` Readiness ContainerRuleExcludeList `mapstructure:"readiness-probe"` - Description StringRuleExcludeList `mapstructure:"description"` - MountPoints StringRuleExcludeList `mapstructure:"mount-points"` + Description StringRuleExcludeList `mapstructure:"description"` + MountPoints StringRuleExcludeList `mapstructure:"mount-points"` } type HooksSettings struct { From 6f2e391662776bea088d0bf988e17fb57247f2f3 Mon Sep 17 00:00:00 2001 From: diyliv Date: Fri, 19 Jun 2026 12:36:14 +0300 Subject: [PATCH 09/11] replace .dmt.yaml with dmtlint.yaml in mount-points docs Signed-off-by: diyliv --- pkg/linters/templates/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/linters/templates/README.md b/pkg/linters/templates/README.md index 53aa8840..da52e196 100644 --- a/pkg/linters/templates/README.md +++ b/pkg/linters/templates/README.md @@ -1705,7 +1705,7 @@ spec: The rule supports excluding specific directories from the check: ```yaml -# .dmt.yaml +# dmtlint.yaml linters-settings: templates: exclude-rules: @@ -1727,7 +1727,7 @@ The Templates linter can be configured at the module level with rule-specific se Configure the overall impact level and individual rule toggles: ```yaml -# .dmt.yaml +# dmtlint.yaml linters-settings: templates: # Overall impact level From aa5216a6cd472a335cfe464e4db473323480cf91 Mon Sep 17 00:00:00 2001 From: diyliv Date: Thu, 2 Jul 2026 13:24:52 +0300 Subject: [PATCH 10/11] fix copilot remarks Signed-off-by: diyliv --- internal/module/module.go | 1 - pkg/linters/templates/README.md | 2 +- pkg/linters/templates/rules/mount_points.go | 11 ++++++++--- pkg/linters/templates/rules/mount_points_test.go | 6 ++---- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/internal/module/module.go b/internal/module/module.go index 81636a67..0a913571 100644 --- a/internal/module/module.go +++ b/internal/module/module.go @@ -412,7 +412,6 @@ func mapContainerExclusions(linterSettings *pkg.LintersSettings, configSettings excludes.Readiness = configExcludes.Readiness.Get() excludes.SeccompProfile = configExcludes.SeccompProfile.Get() excludes.NoNewPrivileges = configExcludes.NoNewPrivileges.Get() - excludes.ImageDigest = configExcludes.ImageDigest.Get() excludes.Description = pkg.StringRuleExcludeList(configExcludes.Description) excludes.MountPoints = pkg.StringRuleExcludeList(configExcludes.MountPoints) } diff --git a/pkg/linters/templates/README.md b/pkg/linters/templates/README.md index c00d2fba..ce9259c4 100644 --- a/pkg/linters/templates/README.md +++ b/pkg/linters/templates/README.md @@ -1885,7 +1885,7 @@ dirs: `/etc/app/certs` is declared in `mount-points.yaml` but no pod controller uses it as a mountPath. -**Error:** +**Warning:** ``` mount-points.yaml references dir "/etc/app/certs" which is not used as a mountPath in any pod controller ``` diff --git a/pkg/linters/templates/rules/mount_points.go b/pkg/linters/templates/rules/mount_points.go index 9bdd9013..9bb26826 100644 --- a/pkg/linters/templates/rules/mount_points.go +++ b/pkg/linters/templates/rules/mount_points.go @@ -59,7 +59,10 @@ func (r *MountPointsRule) ValidateMountPoints(m pkg.Module, errorList *errors.Li return } - templateMountPaths := collectTemplateMountPaths(m, errorList) + templateMountPaths, hasPodControllers := collectTemplateMountPaths(m, errorList) + if !hasPodControllers { + return + } for filePath, dirs := range dirsByFile { for _, dir := range dirs { @@ -127,13 +130,15 @@ func collectMountPointsDirs(m pkg.Module, errorList *errors.LintRuleErrorsList) return dirsByFile } -func collectTemplateMountPaths(m pkg.Module, errorList *errors.LintRuleErrorsList) map[string]bool { +func collectTemplateMountPaths(m pkg.Module, errorList *errors.LintRuleErrorsList) (map[string]bool, bool) { mountPaths := make(map[string]bool) + hasPodControllers := false for _, object := range m.GetStorage() { if !IsPodController(object.Unstructured.GetKind()) { continue } + hasPodControllers = true containers, err := object.GetAllContainers() if err != nil { @@ -151,5 +156,5 @@ func collectTemplateMountPaths(m pkg.Module, errorList *errors.LintRuleErrorsLis } } - return mountPaths + return mountPaths, hasPodControllers } diff --git a/pkg/linters/templates/rules/mount_points_test.go b/pkg/linters/templates/rules/mount_points_test.go index 4ec8a35b..129ccea0 100644 --- a/pkg/linters/templates/rules/mount_points_test.go +++ b/pkg/linters/templates/rules/mount_points_test.go @@ -225,7 +225,7 @@ func TestMountPointsRule_EmptyMountPointsFile(t *testing.T) { assert.Len(t, errs, 0) } -func TestMountPointsRule_NoPodControllers_ReportsUnusedDir(t *testing.T) { +func TestMountPointsRule_NoPodControllers_Skips(t *testing.T) { tmpDir, err := os.MkdirTemp("", "mount-points-test") if err != nil { t.Fatal(err) @@ -249,9 +249,7 @@ func TestMountPointsRule_NoPodControllers_ReportsUnusedDir(t *testing.T) { rule.ValidateMountPoints(&mockMountPointsModule{path: tmpDir, storage: nil}, errorList) errs := errorList.GetErrors() - assert.Len(t, errs, 1) - assert.Equal(t, pkg.Warn, errs[0].Level) - assert.Contains(t, errs[0].Text, `"/etc/app"`) + assert.Len(t, errs, 0) } func TestMountPointsRule_DaemonSetAndStatefulSet(t *testing.T) { From 3fe2e2499e0259e07dc488d2a965aa53f87aaddf Mon Sep 17 00:00:00 2001 From: diyliv Date: Thu, 2 Jul 2026 13:26:59 +0300 Subject: [PATCH 11/11] fix wsl lint Signed-off-by: diyliv --- pkg/linters/templates/rules/mount_points.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/linters/templates/rules/mount_points.go b/pkg/linters/templates/rules/mount_points.go index 9bb26826..6344245a 100644 --- a/pkg/linters/templates/rules/mount_points.go +++ b/pkg/linters/templates/rules/mount_points.go @@ -138,6 +138,7 @@ func collectTemplateMountPaths(m pkg.Module, errorList *errors.LintRuleErrorsLis if !IsPodController(object.Unstructured.GetKind()) { continue } + hasPodControllers = true containers, err := object.GetAllContainers()