From 45887c10badac02b54d4a4ddc6a03c07551edfb3 Mon Sep 17 00:00:00 2001 From: Mikhail Scherba Date: Sun, 6 Sep 2026 10:40:44 +0300 Subject: [PATCH 1/5] add ingress/gw-api linters Signed-off-by: Mikhail Scherba --- README.md | 2 +- pkg/config.go | 69 ++-- pkg/linters/templates/README.md | 308 ++++++++++++++++++ .../templates/rules/deprecated_annotations.go | 141 ++++++++ .../rules/deprecated_annotations_test.go | 218 +++++++++++++ .../templates/rules/enablement_helpers.go | 126 +++++++ .../templates/rules/gateway_enablement.go | 92 ++++++ .../rules/gateway_enablement_test.go | 175 ++++++++++ .../templates/rules/ingress_enablement.go | 93 ++++++ .../rules/ingress_enablement_test.go | 174 ++++++++++ pkg/linters/templates/templates.go | 12 + pkg/scopes/static.go | 3 + .../deprecated-annotations/expected.yaml | 11 + .../deprecated-annotations/module/module.yaml | 2 + .../module/openapi/config-values.yaml | 2 + .../module/openapi/values.yaml | 4 + .../module/templates/httproute.yaml | 14 + .../gateway-enablement-missing/expected.yaml | 11 + .../module/module.yaml | 2 + .../module/openapi/config-values.yaml | 2 + .../module/openapi/values.yaml | 4 + .../module/templates/httproute.yaml | 12 + .../ingress-enablement-missing/expected.yaml | 10 + .../module/module.yaml | 2 + .../module/openapi/config-values.yaml | 2 + .../module/openapi/values.yaml | 4 + .../module/templates/ingress.yaml | 17 + .../no-ingress-gateway-objects/expected.yaml | 14 + .../module/module.yaml | 2 + .../module/openapi/config-values.yaml | 2 + .../module/openapi/values.yaml | 4 + .../module/templates/configmap.yaml | 9 + 32 files changed, 1514 insertions(+), 29 deletions(-) create mode 100644 pkg/linters/templates/rules/deprecated_annotations.go create mode 100644 pkg/linters/templates/rules/deprecated_annotations_test.go create mode 100644 pkg/linters/templates/rules/enablement_helpers.go create mode 100644 pkg/linters/templates/rules/gateway_enablement.go create mode 100644 pkg/linters/templates/rules/gateway_enablement_test.go create mode 100644 pkg/linters/templates/rules/ingress_enablement.go create mode 100644 pkg/linters/templates/rules/ingress_enablement_test.go create mode 100644 test/e2e/testdata/templates/deprecated-annotations/expected.yaml create mode 100644 test/e2e/testdata/templates/deprecated-annotations/module/module.yaml create mode 100644 test/e2e/testdata/templates/deprecated-annotations/module/openapi/config-values.yaml create mode 100644 test/e2e/testdata/templates/deprecated-annotations/module/openapi/values.yaml create mode 100644 test/e2e/testdata/templates/deprecated-annotations/module/templates/httproute.yaml create mode 100644 test/e2e/testdata/templates/gateway-enablement-missing/expected.yaml create mode 100644 test/e2e/testdata/templates/gateway-enablement-missing/module/module.yaml create mode 100644 test/e2e/testdata/templates/gateway-enablement-missing/module/openapi/config-values.yaml create mode 100644 test/e2e/testdata/templates/gateway-enablement-missing/module/openapi/values.yaml create mode 100644 test/e2e/testdata/templates/gateway-enablement-missing/module/templates/httproute.yaml create mode 100644 test/e2e/testdata/templates/ingress-enablement-missing/expected.yaml create mode 100644 test/e2e/testdata/templates/ingress-enablement-missing/module/module.yaml create mode 100644 test/e2e/testdata/templates/ingress-enablement-missing/module/openapi/config-values.yaml create mode 100644 test/e2e/testdata/templates/ingress-enablement-missing/module/openapi/values.yaml create mode 100644 test/e2e/testdata/templates/ingress-enablement-missing/module/templates/ingress.yaml create mode 100644 test/e2e/testdata/templates/no-ingress-gateway-objects/expected.yaml create mode 100644 test/e2e/testdata/templates/no-ingress-gateway-objects/module/module.yaml create mode 100644 test/e2e/testdata/templates/no-ingress-gateway-objects/module/openapi/config-values.yaml create mode 100644 test/e2e/testdata/templates/no-ingress-gateway-objects/module/openapi/values.yaml create mode 100644 test/e2e/testdata/templates/no-ingress-gateway-objects/module/templates/configmap.yaml diff --git a/README.md b/README.md index 18f12631e..23a662d58 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ DMT includes **9 specialized linters** to validate different aspects of your Dec | [**NoCyrillic**](pkg/linters/no-cyrillic/README.md) | Character encoding | Cyrillic characters in code/config files | | [**OpenAPI**](pkg/linters/openapi/README.md) | OpenAPI schemas | Schema validation, CRD definitions, naming conventions | | [**RBAC**](pkg/linters/rbac/README.md) | Security policies | Role bindings, service accounts, wildcards | -| [**Templates**](pkg/linters/templates/README.md) | Kubernetes templates | VPA/PDB settings, Prometheus rules, Grafana dashboards, service ports, mount-points | +| [**Templates**](pkg/linters/templates/README.md) | Kubernetes templates | VPA/PDB settings, Prometheus rules, Grafana dashboards, service ports, mount-points, Ingress/Gateway API enablement, deprecated annotations | ### 🚀 Module Bootstrapping diff --git a/pkg/config.go b/pkg/config.go index a98025a7f..644d377ad 100644 --- a/pkg/config.go +++ b/pkg/config.go @@ -137,23 +137,26 @@ type TemplatesLinterConfig struct { GrafanaDashboardsSettings GrafanaDashboardsSettings } type TemplatesLinterRules struct { - VPARule RuleConfig - PDBRule RuleConfig - IngressRule RuleConfig - PrometheusRule RuleConfig - GrafanaRule RuleConfig - KubeRBACProxyRule RuleConfig - ServicePortRule RuleConfig - ClusterDomainRule RuleConfig - RegistryRule RuleConfig - HTTPRouteRule RuleConfig - EnabledModulesRule RuleConfig - CRDEnabledModulesRule RuleConfig - WebhookConfigurationRule RuleConfig - MountPointsRule RuleConfig - HelmRenderRule RuleConfig - OpenAPIValuesQuoteRule RuleConfig - SchemaValidationRule RuleConfig + VPARule RuleConfig + PDBRule RuleConfig + IngressRule RuleConfig + PrometheusRule RuleConfig + GrafanaRule RuleConfig + KubeRBACProxyRule RuleConfig + ServicePortRule RuleConfig + ClusterDomainRule RuleConfig + RegistryRule RuleConfig + HTTPRouteRule RuleConfig + EnabledModulesRule RuleConfig + CRDEnabledModulesRule RuleConfig + WebhookConfigurationRule RuleConfig + MountPointsRule RuleConfig + HelmRenderRule RuleConfig + OpenAPIValuesQuoteRule RuleConfig + SchemaValidationRule RuleConfig + DeprecatedAnnotationsRule RuleConfig + IngressEnablementRule RuleConfig + GatewayEnablementRule RuleConfig } type PrometheusRuleSettings struct { @@ -164,17 +167,27 @@ type GrafanaDashboardsSettings struct { Disable bool } type TemplatesExcludeRules struct { - VPAAbsent KindRuleExcludeList - PDBAbsent KindRuleExcludeList - ServicePort ServicePortExcludeList - KubeRBACProxy StringRuleExcludeList - Ingress KindRuleExcludeList - HTTPRoute KindRuleExcludeList - EnabledModules EnabledModulesExcludeRule - WebhookConfiguration KindRuleExcludeList - MountPoints StringRuleExcludeList - OpenAPIValuesQuote StringRuleExcludeList - SchemaValidation KindRuleExcludeList + VPAAbsent KindRuleExcludeList + PDBAbsent KindRuleExcludeList + ServicePort ServicePortExcludeList + KubeRBACProxy StringRuleExcludeList + Ingress KindRuleExcludeList + HTTPRoute KindRuleExcludeList + EnabledModules EnabledModulesExcludeRule + WebhookConfiguration KindRuleExcludeList + MountPoints StringRuleExcludeList + OpenAPIValuesQuote StringRuleExcludeList + SchemaValidation KindRuleExcludeList + DeprecatedAnnotations PathRuleExclude + IngressEnablement PathRuleExclude + GatewayEnablement PathRuleExclude +} + +// PathRuleExclude excludes specific files and whole directories (both relative +// to the module root) from a rule that scans template source files. +type PathRuleExclude struct { + Files StringRuleExcludeList + Directories DirectoryRuleExcludeList } type EnabledModulesExcludeRule struct { diff --git a/pkg/linters/templates/README.md b/pkg/linters/templates/README.md index e5f530497..2a6a7d9d6 100644 --- a/pkg/linters/templates/README.md +++ b/pkg/linters/templates/README.md @@ -27,6 +27,9 @@ Proper template validation prevents runtime issues, ensures applications are pro | [mount-points](#mount-points) | Validates that mount-points.yaml directories are used as volumeMounts in pod controllers | ✅ | enabled | | [openapi-values-quote](#openapi-values-quote) | Requires templates to quote OpenAPI string values that have no `pattern`/`enum`/`format` | ✅ | enabled | | [schema-validation](#schema-validation) | Strictly decodes every rendered standard Kubernetes resource against its API type | ✅ | enabled | +| [deprecated-annotations](#deprecated-annotations) | Flags deprecated annotation keys (e.g. `alb.network.deckhouse.io/response-headers-to-add`) | ✅ | enabled | +| [ingress-enablement](#ingress-enablement) | Requires Ingress creation to be gated by `helm_lib_module_ingress_enabled` | ✅ | enabled | +| [gateway-enablement](#gateway-enablement) | Requires HTTPRoute/ListenerSet creation to be gated by `helm_lib_module_gateway_enabled` | ✅ | enabled | "Configurable" means that this rule can be configured using the `.dmtlint.yaml` file, including customizing the rule's parameters and/or disabling the rule. @@ -2936,3 +2939,308 @@ linters-settings: Whichever `k8s.io/api` is in `go.mod`. Bumping that dependency is the whole of updating this rule — there is nothing else to regenerate. + +--- + +### deprecated-annotations + +**Purpose:** Flags module-specific annotation keys that have been superseded by +a native Kubernetes/Gateway API mechanism, so authors migrate off them instead +of copying the pattern into new templates. + +**Description:** + +Scans all template files (`.yaml`, `.yml`, `.tpl`) for a small built-in list of +banned annotation keys and reports every occurrence, together with a concrete +workaround snippet for the replacement. Today the list has one entry: +`alb.network.deckhouse.io/response-headers-to-add`, deprecated in favor of the +native Gateway API `ResponseHeaderModifier` HTTPRoute filter. + +**What it checks:** + +1. Only runs when the module actually renders an `Ingress`, `HTTPRoute`, or + `ListenerSet` — every banned annotation is specific to those resources, so a + module with none of them is skipped entirely +2. All files in the `templates/` directory +3. Presence of a banned annotation key, as plain text — it does not matter + whether the key appears as a YAML annotation or inside a Helm expression + +**Why it matters:** + +`alb.network.deckhouse.io/response-headers-to-add` predates Gateway API's own +`ResponseHeaderModifier` filter and applies to every rule of the HTTPRoute +object uniformly (there is no way to target one rule). The native filter is +per-rule, standards-based, and portable to any Gateway API implementation — +the annotation is a legacy-only escape hatch. + +**Examples:** + +❌ **Incorrect** - Setting the deprecated annotation: + +```yaml +# templates/httproute.yaml +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: dashboard + annotations: + alb.network.deckhouse.io/response-headers-to-add: '{"Strict-Transport-Security":"max-age=31536000; includeSubDomains"}' +spec: + rules: + - backendRefs: + - name: dashboard + port: 443 +``` + +**Error:** +``` +Error: Annotation "alb.network.deckhouse.io/response-headers-to-add" must not be used: deprecated in favor of the native Gateway API HTTPRoute ResponseHeaderModifier filter. Add this filter to the relevant HTTPRoute rule instead: + rules: + - backendRefs: [...] + matches: [...] + filters: + - type: ResponseHeaderModifier + responseHeaderModifier: + add: + - name: Strict-Transport-Security + value: max-age=31536000; includeSubDomains +``` + +✅ **Correct** - Using the native HTTPRoute filter: + +```yaml +# templates/httproute.yaml +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: dashboard +spec: + rules: + - backendRefs: + - name: dashboard + port: 443 + filters: + - type: ResponseHeaderModifier + responseHeaderModifier: + add: + - name: Strict-Transport-Security + value: max-age=31536000; includeSubDomains +``` + +**Configuration:** + +The rule supports excluding specific files and directories (paths are relative +to the module root): + +```yaml +# .dmtlint.yaml +linters-settings: + templates: + exclude-rules: + deprecated-annotations: + files: + - templates/legacy-ingress.yaml + directories: + - templates/vendor/ +``` + +--- + +### ingress-enablement + +**Purpose:** Ensures an Ingress's creation can be turned off the same way every +other module's can: via `global.modules.ingress.enabled` or the module's own +`.ingress.enabled` override. + +**Description:** + +Scans every template file that emits a `kind: Ingress` manifest and reports the +ones that never reference `helm_lib_module_ingress_enabled` — the shared +`helm_lib` helper that checks the module override first, then the global +setting, defaulting to enabled when neither is set — anywhere in the same +file. + +**What it checks:** + +1. Only runs when the module actually renders an `Ingress`: a module with none + has nothing for this check to say +2. Every file in `templates/` whose rendered output would contain + `kind: Ingress` +3. That the same file also references `helm_lib_module_ingress_enabled` + +**This is a same-file, textual heuristic, not a template-scope analysis.** It +does not verify that the helper actually gates the specific manifest it +found — only that both the `kind: Ingress` line and the helper name appear +somewhere in the same file. In every module observed so far the guard and the +manifest it protects live in the same file (`{{- if eq (include +"helm_lib_module_ingress_enabled" .) "true" }}` wrapping the whole +document), so this catches the case that actually matters — an Ingress with no +enablement check at all — without needing a real Helm control-flow parser. + +**Why it matters:** + +An Ingress that never checks the shared helper renders unconditionally: it +cannot be disabled by an operator who sets `ingress.enabled: false` at either +the global or the module level, and every module is expected to honor that +knob the same way. + +**Examples:** + +❌ **Incorrect** - Ingress with no enablement check: + +```yaml +# templates/ingress.yaml +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: dashboard + namespace: d8-my-module +spec: + rules: + - host: dashboard.example.com +``` + +**Error:** +``` +Error: File creates a Ingress object but never checks "helm_lib_module_ingress_enabled", so its creation cannot be controlled via global.modules.ingress.enabled or myModule.ingress.enabled. Guard the manifest with {{- if eq (include "helm_lib_module_ingress_enabled" .) "true" }} ... {{- end }} so it can be disabled the same way every other module's Ingress does. +``` + +The exact `.Values` path named in the finding is computed from the module's own +name (`myModule` above is `my-module` converted to camelCase), so it always +matches what that module's own values.yaml actually calls it. + +✅ **Correct** - Guarded by the shared helper: + +```yaml +# templates/ingress.yaml +{{- if eq (include "helm_lib_module_ingress_enabled" .) "true" }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: dashboard + namespace: d8-my-module +spec: + rules: + - host: dashboard.example.com +{{- end }} +``` + +**Configuration:** + +```yaml +# .dmtlint.yaml +linters-settings: + templates: + exclude-rules: + ingress-enablement: + files: + - templates/ingress.yaml # module has a documented reason to skip the helper + directories: + - templates/vendor/ +``` + +--- + +### gateway-enablement + +**Purpose:** The Gateway API counterpart of [ingress-enablement](#ingress-enablement): +ensures HTTPRoute and ListenerSet creation can be turned off via +`global.modules.gatewayAPI.enabled` or the module's own +`.gatewayAPI.enabled` override. + +**Description:** + +Scans every template file that emits a `kind: HTTPRoute` or `kind: +ListenerSet` manifest and reports the ones that never reference +`helm_lib_module_gateway_enabled` — the shared helper that requires both an +enabled flag and a resolvable Gateway (module, then global, then +`global.discovery.gatewayAPIDefaultGateway`) — anywhere in the same file. + +**What it checks:** + +1. Only runs when the module actually renders an `HTTPRoute` or `ListenerSet`: + a module with neither has nothing for this check to say +2. Every file in `templates/` whose rendered output would contain + `kind: HTTPRoute` or `kind: ListenerSet` +3. That the same file also references `helm_lib_module_gateway_enabled` + +Same same-file heuristic and trade-off as `ingress-enablement` — see that +rule's description for the reasoning. + +**Why it matters:** + +Unlike Ingress, Gateway API has no safe default: a module cannot assume a +Gateway exists the way it can assume an `nginx` IngressClass exists. A +HTTPRoute/ListenerSet pair that skips `helm_lib_module_gateway_enabled` will +either render with no usable parent Gateway, or fail to respect an operator's +explicit `gatewayAPI.enabled: false`. + +**Examples:** + +❌ **Incorrect** - HTTPRoute with no enablement check: + +```yaml +# templates/httproute.yaml +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: dashboard + namespace: d8-my-module +spec: + hostnames: + - dashboard.example.com +``` + +**Error:** +``` +Error: File creates a Gateway API (HTTPRoute/ListenerSet) object but never checks "helm_lib_module_gateway_enabled", so its creation cannot be controlled via global.modules.gatewayAPI.enabled or myModule.gatewayAPI.enabled, with a Gateway resolvable via global.discovery.gatewayAPIDefaultGateway, global.modules.gatewayAPI.gateway, or myModule.gatewayAPI.gateway. Guard the manifest with {{- if eq (include "helm_lib_module_gateway_enabled" .) "true" }} ... {{- end }} so it can be disabled the same way every other module's Gateway API (HTTPRoute/ListenerSet) does. +``` + +As with `ingress-enablement`, the `.Values` paths named in the finding are +computed from the module's own name. + +✅ **Correct** - Guarded by the shared helper: + +```yaml +# templates/httproute.yaml +{{- $moduleGateway := dict }} +{{- include "helm_lib_module_gateway" (list . $moduleGateway) }} +{{- if and (eq (include "helm_lib_module_gateway_enabled" .) "true") .Values.global.modules.publicDomainTemplate }} +apiVersion: gateway.networking.k8s.io/v1 +kind: ListenerSet +metadata: + name: dashboard + namespace: d8-my-module +spec: + parentRef: + name: {{ $moduleGateway.name }} + namespace: {{ $moduleGateway.namespace }} + listeners: + - name: http + protocol: HTTP + port: 80 +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: dashboard + namespace: d8-my-module +spec: + hostnames: + - dashboard.example.com +{{- end }} +``` + +**Configuration:** + +```yaml +# .dmtlint.yaml +linters-settings: + templates: + exclude-rules: + gateway-enablement: + files: + - templates/multicluster/api-proxy/httproute.yaml + directories: + - templates/vendor/ +``` diff --git a/pkg/linters/templates/rules/deprecated_annotations.go b/pkg/linters/templates/rules/deprecated_annotations.go new file mode 100644 index 000000000..5e562d779 --- /dev/null +++ b/pkg/linters/templates/rules/deprecated_annotations.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 ( + "context" + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/deckhouse/dmt/internal/fsutils" + "github.com/deckhouse/dmt/pkg" + "github.com/deckhouse/dmt/pkg/errors" +) + +const ( + DeprecatedAnnotationsRuleName = "deprecated-annotations" +) + +// deprecatedAnnotation is one annotation key that must no longer appear in +// module templates, along with the reason it was banned and a workaround +// snippet demonstrating the replacement, both surfaced in the finding. +type deprecatedAnnotation struct { + Key string + Reason string + Workaround string +} + +// deprecatedAnnotations is the list of annotations this rule flags. Add an entry +// here to ban another annotation; the scan and reporting are shared. +var deprecatedAnnotations = []deprecatedAnnotation{ + { + Key: "alb.network.deckhouse.io/response-headers-to-add", + Reason: "deprecated in favor of the native Gateway API HTTPRoute ResponseHeaderModifier filter", + Workaround: ` rules: + - backendRefs: [...] + matches: [...] + filters: + - type: ResponseHeaderModifier + responseHeaderModifier: + add: + - name: Strict-Transport-Security + value: max-age=31536000; includeSubDomains`, + }, +} + +type DeprecatedAnnotationsRule struct { + pkg.RuleMeta + pkg.PathRule + + module pkg.Module + errorList *errors.LintRuleErrorsList +} + +func NewDeprecatedAnnotationsRule(excludeFileRules []pkg.StringRuleExclude, + excludeDirectoryRules []pkg.DirectoryRuleExclude, + m pkg.Module, errorList *errors.LintRuleErrorsList) *DeprecatedAnnotationsRule { + return &DeprecatedAnnotationsRule{ + RuleMeta: pkg.RuleMeta{ + Name: DeprecatedAnnotationsRuleName, + }, + PathRule: pkg.PathRule{ + ExcludeStringRules: excludeFileRules, + ExcludeDirectoryRules: excludeDirectoryRules, + }, + module: m, + errorList: errorList.WithRule(DeprecatedAnnotationsRuleName), + } +} + +var _ pkg.Rule = (*DeprecatedAnnotationsRule)(nil) + +// Check scans every template file for the annotation keys in deprecatedAnnotations +// and reports each occurrence, regardless of whether the key appears as a plain +// YAML annotation or inside a Helm expression — the key text itself is what must +// no longer be used. +// +// The rule only runs when the module actually renders an Ingress, HTTPRoute, or +// ListenerSet: every entry in deprecatedAnnotations is specific to those +// resources, so a module with none of them has nothing for this check to say. +func (r *DeprecatedAnnotationsRule) Check(_ context.Context) { + m := r.module + + if !storageHasKind(m, "Ingress", "HTTPRoute", "ListenerSet") { + return + } + + templatesPath := filepath.Join(m.GetPath(), "templates") + if _, err := os.Stat(templatesPath); os.IsNotExist(err) { + return + } + + files := fsutils.GetFiles(templatesPath, true, fsutils.FilterFileByExtensions(".yaml", ".yml", ".tpl")) + + for _, filePath := range files { + relPath := fsutils.Rel(m.GetPath(), filePath) + + if !r.Enabled(relPath) { + continue + } + + content, err := os.ReadFile(filePath) + if err != nil { + r.errorList.WithFilePath(relPath).Errorf("Failed to read file: %v", err) + continue + } + + r.checkContent(relPath, content) + } +} + +func (r *DeprecatedAnnotationsRule) checkContent(relPath string, content []byte) { + for _, annotation := range deprecatedAnnotations { + re := regexp.MustCompile(regexp.QuoteMeta(annotation.Key)) + + for _, loc := range re.FindAllIndex(content, -1) { + line := strings.Count(string(content[:loc[0]]), "\n") + 1 + + r.errorList.WithFilePath(relPath). + WithLineNumber(line). + WithValue(annotation.Key). + Errorf("Annotation %q must not be used: %s. Add this filter to the relevant HTTPRoute rule instead:\n%s", + annotation.Key, annotation.Reason, annotation.Workaround) + } + } +} diff --git a/pkg/linters/templates/rules/deprecated_annotations_test.go b/pkg/linters/templates/rules/deprecated_annotations_test.go new file mode 100644 index 000000000..6ded7c28a --- /dev/null +++ b/pkg/linters/templates/rules/deprecated_annotations_test.go @@ -0,0 +1,218 @@ +/* +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 ( + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/gojuno/minimock/v3" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + + "github.com/deckhouse/dmt/internal/mocks" + "github.com/deckhouse/dmt/internal/storage" + "github.com/deckhouse/dmt/pkg" + "github.com/deckhouse/dmt/pkg/errors" +) + +// writeTemplatesModule builds a temporary module directory containing the given +// template files (keyed by path relative to the module root) and returns the +// module path. +func writeTemplatesModule(t *testing.T, templateFiles map[string]string) string { + t.Helper() + + modulePath := filepath.Join(t.TempDir(), "module") + require.NoError(t, os.MkdirAll(modulePath, 0o755)) + + for relPath, content := range templateFiles { + fullPath := filepath.Join(modulePath, relPath) + require.NoError(t, os.MkdirAll(filepath.Dir(fullPath), 0o755)) + require.NoError(t, os.WriteFile(fullPath, []byte(content), 0o600)) + } + + return modulePath +} + +// kindOnlyStorage builds a minimal rendered-object store containing one bare +// object per kind given — enough for storageHasKind to see them, which is all +// these rules read from GetStorage(). +func kindOnlyStorage(kinds ...string) map[storage.ResourceIndex]storage.StoreObject { + out := make(map[storage.ResourceIndex]storage.StoreObject, len(kinds)) + + for i, kind := range kinds { + u := unstructured.Unstructured{Object: map[string]any{ + "apiVersion": "v1", + "kind": kind, + "metadata": map[string]any{"name": fmt.Sprintf("obj-%d", i)}, + }} + + idx := storage.ResourceIndex{Kind: u.GetKind(), Name: u.GetName(), Namespace: u.GetNamespace()} + out[idx] = storage.StoreObject{Unstructured: u} + } + + return out +} + +// templatesMockModule builds a Module mock rooted at modulePath whose rendered +// storage contains one bare object per kind in storageKinds. +func templatesMockModule(t *testing.T, modulePath string, storageKinds ...string) *mocks.ModuleMock { + t.Helper() + + m := mocks.NewModuleMock(minimock.NewController(t)) + // Optional: the storage gate in each rule's Check may return before GetPath + // or GetName is ever called (see the "does not run at all" test cases). + m.GetPathMock.Optional().Return(modulePath) + m.GetNameMock.Optional().Return("my-module") + m.GetStorageMock.Return(kindOnlyStorage(storageKinds...)) + + return m +} + +func TestDeprecatedAnnotationsRule_Check(t *testing.T) { + const httprouteWithAnnotation = `apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: x + annotations: + alb.network.deckhouse.io/response-headers-to-add: '{"Strict-Transport-Security":"max-age=31536000"}' +` + + tests := []struct { + name string + templateFiles map[string]string + storageKinds []string + exclude []pkg.StringRuleExclude + wantCount int + wantContains []string + wantLines []int + }{ + { + name: "flags the deprecated response-headers-to-add annotation", + templateFiles: map[string]string{ + "templates/httproute.yaml": httprouteWithAnnotation, + }, + storageKinds: []string{"HTTPRoute"}, + wantCount: 1, + wantContains: []string{ + `alb.network.deckhouse.io/response-headers-to-add`, + "ResponseHeaderModifier", + "Strict-Transport-Security", + "responseHeaderModifier", + }, + wantLines: []int{6}, + }, + { + name: "flags multiple occurrences across files", + templateFiles: map[string]string{ + "templates/a.yaml": `metadata: + annotations: + alb.network.deckhouse.io/response-headers-to-add: '{}' +`, + "templates/b.yaml": `metadata: + annotations: + alb.network.deckhouse.io/response-headers-to-add: '{}' +`, + }, + storageKinds: []string{"HTTPRoute"}, + wantCount: 2, + }, + { + name: "ignores files that never use the annotation", + templateFiles: map[string]string{ + "templates/httproute.yaml": `apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: x + annotations: + alb.network.deckhouse.io/backend-tls-settings: '{"mode": "SIMPLE"}' +`, + }, + storageKinds: []string{"HTTPRoute"}, + wantCount: 0, + }, + { + name: "does not run at all when the module ships no Ingress/HTTPRoute/ListenerSet", + templateFiles: map[string]string{ + "templates/httproute.yaml": httprouteWithAnnotation, + }, + storageKinds: nil, // e.g. a module whose only resources are a Deployment and a Service + wantCount: 0, + }, + { + name: "an excluded file is skipped even though the module ships HTTPRoute", + templateFiles: map[string]string{ + "templates/httproute.yaml": httprouteWithAnnotation, + }, + storageKinds: []string{"HTTPRoute"}, + exclude: []pkg.StringRuleExclude{"templates/httproute.yaml"}, + wantCount: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + modulePath := writeTemplatesModule(t, tt.templateFiles) + + errorList := errors.NewLintRuleErrorsList() + NewDeprecatedAnnotationsRule(tt.exclude, nil, templatesMockModule(t, modulePath, tt.storageKinds...), errorList).Check(t.Context()) + + errs := errorList.GetErrors() + require.Len(t, errs, tt.wantCount) + + for _, want := range tt.wantContains { + found := false + + for i := range errs { + if containsStr(errs[i].Text, want) { + found = true + break + } + } + + require.Truef(t, found, "expected a finding containing %q, got %+v", want, errs) + } + + for i, wantLine := range tt.wantLines { + if i < len(errs) { + require.Equalf(t, wantLine, errs[i].LineNumber, "unexpected line for finding %d: %s", i, errs[i].Text) + } + } + }) + } +} + +func TestDeprecatedAnnotationsRule_DirectoryExclusion(t *testing.T) { + modulePath := writeTemplatesModule(t, map[string]string{ + "templates/vendor/httproute.yaml": `metadata: + annotations: + alb.network.deckhouse.io/response-headers-to-add: '{}' +`, + }) + + errorList := errors.NewLintRuleErrorsList() + NewDeprecatedAnnotationsRule( + nil, + []pkg.DirectoryRuleExclude{"templates/vendor/"}, + templatesMockModule(t, modulePath, "HTTPRoute"), + errorList, + ).Check(t.Context()) + + require.Empty(t, errorList.GetErrors()) +} diff --git a/pkg/linters/templates/rules/enablement_helpers.go b/pkg/linters/templates/rules/enablement_helpers.go new file mode 100644 index 000000000..96df378fb --- /dev/null +++ b/pkg/linters/templates/rules/enablement_helpers.go @@ -0,0 +1,126 @@ +/* +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 ( + "bytes" + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/deckhouse/dmt/internal/fsutils" + "github.com/deckhouse/dmt/pkg" + "github.com/deckhouse/dmt/pkg/errors" +) + +// kindLineRe returns a regexp matching a manifest's `kind:` field on its own +// line for any of the given kinds — the same shape a rendered Kubernetes YAML +// document uses, regardless of the Helm expressions around it. It is used to +// test "does this template file ever emit an object of this kind" without +// rendering the chart. +func kindLineRe(kinds ...string) *regexp.Regexp { + escaped := make([]string, len(kinds)) + for i, k := range kinds { + escaped[i] = regexp.QuoteMeta(k) + } + + return regexp.MustCompile(`(?m)^kind:\s*(` + strings.Join(escaped, "|") + `)\s*$`) +} + +// storageHasKind reports whether module's rendered objects include at least one +// of the given kinds. It gates the enablement/annotation rules so they only run +// on modules that actually ship the kind of resource they check — a module with +// no Ingress has nothing for ingress-enablement to say, and likewise for Gateway +// API and HTTPRoute/ListenerSet. +func storageHasKind(m pkg.Module, kinds ...string) bool { + for _, object := range m.GetStorage() { + kind := object.Unstructured.GetKind() + + for _, k := range kinds { + if kind == k { + return true + } + } + } + + return false +} + +// checkKindGatedByHelper scans every template file of module for kindRe and +// reports each file that matches it but never mentions helperName anywhere in +// the same file. kindLabel names the resource kind(s) in the finding text, +// and valuesHint names the concrete `.Values` path(s) an author would set to +// control it, so the finding says exactly what to change, not just which +// helper to call. +// +// This is a textual, same-file heuristic: it does not verify that helperName +// actually gates the specific manifest kindRe matched, only that both appear +// somewhere in the same file. See IngressEnablementRule.Check for why that +// trade-off was chosen over a full Helm-template control-flow parser. +func checkKindGatedByHelper( + m pkg.Module, + errorList *errors.LintRuleErrorsList, + pathRule pkg.PathRule, + kindRe *regexp.Regexp, + helperName string, + kindLabel string, + valuesHint string, +) { + templatesPath := filepath.Join(m.GetPath(), "templates") + if _, err := os.Stat(templatesPath); os.IsNotExist(err) { + return + } + + files := fsutils.GetFiles(templatesPath, true, fsutils.FilterFileByExtensions(".yaml", ".yml", ".tpl")) + helperBytes := []byte(helperName) + + for _, filePath := range files { + relPath := fsutils.Rel(m.GetPath(), filePath) + + if !pathRule.Enabled(relPath) { + continue + } + + content, err := os.ReadFile(filePath) + if err != nil { + errorList.WithFilePath(relPath).Errorf("Failed to read file: %v", err) + continue + } + + loc := kindRe.FindIndex(content) + if loc == nil { + continue + } + + if bytes.Contains(content, helperBytes) { + continue + } + + line := bytes.Count(content[:loc[0]], []byte("\n")) + 1 + + errorList.WithFilePath(relPath). + WithLineNumber(line). + Errorf( + "File creates a %s object but never checks %q, so its creation cannot be "+ + "controlled via %s. Guard the manifest with "+ + "{{- if eq (include %q .) \"true\" }} ... {{- end }} so it can be disabled the "+ + "same way every other module's %s does.", + kindLabel, helperName, valuesHint, helperName, kindLabel, + ) + } +} diff --git a/pkg/linters/templates/rules/gateway_enablement.go b/pkg/linters/templates/rules/gateway_enablement.go new file mode 100644 index 000000000..e4092a07c --- /dev/null +++ b/pkg/linters/templates/rules/gateway_enablement.go @@ -0,0 +1,92 @@ +/* +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 ( + "context" + "fmt" + + "github.com/deckhouse/dmt/internal/modules" + "github.com/deckhouse/dmt/pkg" + "github.com/deckhouse/dmt/pkg/errors" +) + +const ( + GatewayEnablementRuleName = "gateway-enablement" + + // gatewayEnabledHelper is the shared helm_lib helper that decides whether a + // module's Gateway API resources (HTTPRoute, ListenerSet) should be created: + // it checks the module's own `.gatewayAPI.enabled` override (or the + // global `global.modules.gatewayAPI.enabled`), AND requires that a Gateway + // actually resolves (module, then global, then + // global.discovery.gatewayAPIDefaultGateway) — unlike Ingress there is no + // safe default gateway, so both conditions matter. + gatewayEnabledHelper = "helm_lib_module_gateway_enabled" +) + +type GatewayEnablementRule struct { + pkg.RuleMeta + pkg.PathRule + + module pkg.Module + errorList *errors.LintRuleErrorsList +} + +func NewGatewayEnablementRule(excludeFileRules []pkg.StringRuleExclude, + excludeDirectoryRules []pkg.DirectoryRuleExclude, + m pkg.Module, errorList *errors.LintRuleErrorsList) *GatewayEnablementRule { + return &GatewayEnablementRule{ + RuleMeta: pkg.RuleMeta{ + Name: GatewayEnablementRuleName, + }, + PathRule: pkg.PathRule{ + ExcludeStringRules: excludeFileRules, + ExcludeDirectoryRules: excludeDirectoryRules, + }, + module: m, + errorList: errorList.WithRule(GatewayEnablementRuleName), + } +} + +var _ pkg.Rule = (*GatewayEnablementRule)(nil) + +// Check scans every template file that emits a `kind: HTTPRoute` or +// `kind: ListenerSet` manifest and reports the ones that never reference +// helm_lib_module_gateway_enabled anywhere in the same file. See +// IngressEnablementRule.Check for the same-file heuristic this shares and why it +// was chosen. +// +// The rule only runs when the module actually renders an HTTPRoute or +// ListenerSet: a module with neither has nothing for this check to say. +func (r *GatewayEnablementRule) Check(_ context.Context) { + if !storageHasKind(r.module, "HTTPRoute", "ListenerSet") { + return + } + + camelModuleName := modules.ToLowerCamel(r.module.GetName()) + valuesHint := fmt.Sprintf( + "global.modules.gatewayAPI.enabled or %[1]s.gatewayAPI.enabled, with a Gateway resolvable "+ + "via global.discovery.gatewayAPIDefaultGateway, global.modules.gatewayAPI.gateway, or %[1]s.gatewayAPI.gateway", + camelModuleName, + ) + + checkKindGatedByHelper( + r.module, r.errorList, r.PathRule, + kindLineRe("HTTPRoute", "ListenerSet"), gatewayEnabledHelper, + "Gateway API (HTTPRoute/ListenerSet)", valuesHint, + ) +} diff --git a/pkg/linters/templates/rules/gateway_enablement_test.go b/pkg/linters/templates/rules/gateway_enablement_test.go new file mode 100644 index 000000000..64a895880 --- /dev/null +++ b/pkg/linters/templates/rules/gateway_enablement_test.go @@ -0,0 +1,175 @@ +/* +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 ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/deckhouse/dmt/pkg" + "github.com/deckhouse/dmt/pkg/errors" +) + +func TestGatewayEnablementRule_Check(t *testing.T) { + const ungatedHTTPRoute = `apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: dashboard + namespace: d8-my-module +` + const ungatedListenerSet = `apiVersion: gateway.networking.k8s.io/v1 +kind: ListenerSet +metadata: + name: dashboard + namespace: d8-my-module +` + + tests := []struct { + name string + templateFiles map[string]string + storageKinds []string + exclude []pkg.StringRuleExclude + wantCount int + wantContains []string + wantLines []int + }{ + { + name: "flags an HTTPRoute with no enablement check at all", + templateFiles: map[string]string{ + "templates/httproute.yaml": ungatedHTTPRoute, + }, + storageKinds: []string{"HTTPRoute"}, + wantCount: 1, + wantContains: []string{ + "helm_lib_module_gateway_enabled", "Gateway API", + "global.modules.gatewayAPI.enabled", "myModule.gatewayAPI.enabled", + "global.discovery.gatewayAPIDefaultGateway", + }, + wantLines: []int{2}, + }, + { + name: "flags a ListenerSet with no enablement check at all", + templateFiles: map[string]string{ + "templates/listenerset.yaml": ungatedListenerSet, + }, + storageKinds: []string{"ListenerSet"}, + wantCount: 1, + }, + { + name: "passes a ListenerSet and HTTPRoute guarded by helm_lib_module_gateway_enabled in the same file", + templateFiles: map[string]string{ + "templates/httproute.yaml": `{{- if and (eq (include "helm_lib_module_gateway_enabled" .) "true") .Values.global.modules.publicDomainTemplate }} +apiVersion: gateway.networking.k8s.io/v1 +kind: ListenerSet +metadata: + name: dashboard + namespace: d8-my-module +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: dashboard + namespace: d8-my-module +{{- end }} +`, + }, + storageKinds: []string{"HTTPRoute", "ListenerSet"}, + wantCount: 0, + }, + { + name: "ignores files that never create Gateway API objects", + templateFiles: map[string]string{ + "templates/ingress.yaml": `apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: dashboard +`, + }, + storageKinds: []string{"HTTPRoute"}, // module has one, just not from this file + wantCount: 0, + }, + { + name: "does not run at all when the module ships neither HTTPRoute nor ListenerSet", + templateFiles: map[string]string{ + "templates/httproute.yaml": ungatedHTTPRoute, + }, + storageKinds: nil, // e.g. a module whose HTTPRoute never actually rendered + wantCount: 0, + }, + { + name: "an excluded file is skipped even though the module ships an HTTPRoute", + templateFiles: map[string]string{ + "templates/httproute.yaml": ungatedHTTPRoute, + }, + storageKinds: []string{"HTTPRoute"}, + exclude: []pkg.StringRuleExclude{"templates/httproute.yaml"}, + wantCount: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + modulePath := writeTemplatesModule(t, tt.templateFiles) + + errorList := errors.NewLintRuleErrorsList() + NewGatewayEnablementRule(tt.exclude, nil, templatesMockModule(t, modulePath, tt.storageKinds...), errorList).Check(t.Context()) + + errs := errorList.GetErrors() + require.Len(t, errs, tt.wantCount) + + for _, want := range tt.wantContains { + found := false + + for i := range errs { + if containsStr(errs[i].Text, want) { + found = true + break + } + } + + require.Truef(t, found, "expected a finding containing %q, got %+v", want, errs) + } + + for i, wantLine := range tt.wantLines { + if i < len(errs) { + require.Equalf(t, wantLine, errs[i].LineNumber, "unexpected line for finding %d: %s", i, errs[i].Text) + } + } + }) + } +} + +func TestGatewayEnablementRule_DirectoryExclusion(t *testing.T) { + modulePath := writeTemplatesModule(t, map[string]string{ + "templates/vendor/httproute.yaml": `apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: dashboard +`, + }) + + errorList := errors.NewLintRuleErrorsList() + NewGatewayEnablementRule( + nil, + []pkg.DirectoryRuleExclude{"templates/vendor/"}, + templatesMockModule(t, modulePath, "HTTPRoute"), + errorList, + ).Check(t.Context()) + + require.Empty(t, errorList.GetErrors()) +} diff --git a/pkg/linters/templates/rules/ingress_enablement.go b/pkg/linters/templates/rules/ingress_enablement.go new file mode 100644 index 000000000..224d6059b --- /dev/null +++ b/pkg/linters/templates/rules/ingress_enablement.go @@ -0,0 +1,93 @@ +/* +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 ( + "context" + "fmt" + + "github.com/deckhouse/dmt/internal/modules" + "github.com/deckhouse/dmt/pkg" + "github.com/deckhouse/dmt/pkg/errors" +) + +const ( + IngressEnablementRuleName = "ingress-enablement" + + // ingressEnabledHelper is the shared helm_lib helper that decides whether a + // module's Ingress should be created: it checks the module's own + // `.ingress.enabled` override first, then falls back to the global + // `global.modules.ingress.enabled`, defaulting to true when neither is set. + ingressEnabledHelper = "helm_lib_module_ingress_enabled" +) + +type IngressEnablementRule struct { + pkg.RuleMeta + pkg.PathRule + + module pkg.Module + errorList *errors.LintRuleErrorsList +} + +func NewIngressEnablementRule(excludeFileRules []pkg.StringRuleExclude, + excludeDirectoryRules []pkg.DirectoryRuleExclude, + m pkg.Module, errorList *errors.LintRuleErrorsList) *IngressEnablementRule { + return &IngressEnablementRule{ + RuleMeta: pkg.RuleMeta{ + Name: IngressEnablementRuleName, + }, + PathRule: pkg.PathRule{ + ExcludeStringRules: excludeFileRules, + ExcludeDirectoryRules: excludeDirectoryRules, + }, + module: m, + errorList: errorList.WithRule(IngressEnablementRuleName), + } +} + +var _ pkg.Rule = (*IngressEnablementRule)(nil) + +// Check scans every template file that emits a `kind: Ingress` manifest and +// reports the ones that never reference helm_lib_module_ingress_enabled anywhere +// in the same file. Without that helper (or an equivalent check on the same +// values), the Ingress renders unconditionally and cannot be turned off via +// either global.modules.ingress.enabled or the module's own ingress.enabled +// override — the two supported ways to disable it. +// +// This is a textual, same-file heuristic, not a template-scope analysis: a file +// that emits several Ingress manifests but only guards one of them with the +// helper will not be flagged. In every module observed so far the guard and the +// manifest it protects live in the same file, so this trade-off catches the +// common and important case — an Ingress with no enablement check at all — +// without the cost of a real Helm-template control-flow parser. +// +// The rule only runs when the module actually renders an Ingress object: a +// module with none has nothing for this check to say. +func (r *IngressEnablementRule) Check(_ context.Context) { + if !storageHasKind(r.module, "Ingress") { + return + } + + camelModuleName := modules.ToLowerCamel(r.module.GetName()) + valuesHint := fmt.Sprintf("global.modules.ingress.enabled or %s.ingress.enabled", camelModuleName) + + checkKindGatedByHelper( + r.module, r.errorList, r.PathRule, + kindLineRe("Ingress"), ingressEnabledHelper, + "Ingress", valuesHint, + ) +} diff --git a/pkg/linters/templates/rules/ingress_enablement_test.go b/pkg/linters/templates/rules/ingress_enablement_test.go new file mode 100644 index 000000000..6591e3f51 --- /dev/null +++ b/pkg/linters/templates/rules/ingress_enablement_test.go @@ -0,0 +1,174 @@ +/* +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 ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/deckhouse/dmt/pkg" + "github.com/deckhouse/dmt/pkg/errors" +) + +func TestIngressEnablementRule_Check(t *testing.T) { + const ungatedIngress = `apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: dashboard + namespace: d8-my-module +spec: + rules: + - host: dashboard.example.com +` + + tests := []struct { + name string + templateFiles map[string]string + storageKinds []string + exclude []pkg.StringRuleExclude + wantCount int + wantContains []string + wantLines []int + }{ + { + name: "flags an Ingress with no enablement check at all", + templateFiles: map[string]string{ + "templates/ingress.yaml": ungatedIngress, + }, + storageKinds: []string{"Ingress"}, + wantCount: 1, + wantContains: []string{ + "helm_lib_module_ingress_enabled", "Ingress", + "global.modules.ingress.enabled", "myModule.ingress.enabled", + }, + wantLines: []int{2}, + }, + { + name: "passes an Ingress guarded by helm_lib_module_ingress_enabled in the same file", + templateFiles: map[string]string{ + "templates/ingress.yaml": `{{- if eq (include "helm_lib_module_ingress_enabled" .) "true" }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: dashboard + namespace: d8-my-module +spec: + rules: + - host: dashboard.example.com +{{- end }} +`, + }, + storageKinds: []string{"Ingress"}, + wantCount: 0, + }, + { + name: "ignores files that never create an Ingress", + templateFiles: map[string]string{ + "templates/deployment.yaml": `apiVersion: apps/v1 +kind: Deployment +metadata: + name: my-app +`, + }, + storageKinds: []string{"Ingress"}, // module has one, just not from this file + wantCount: 0, + }, + { + name: "does not confuse an unrelated kind field with Ingress", + templateFiles: map[string]string{ + "templates/configmap.yaml": `apiVersion: v1 +kind: ConfigMap +metadata: + name: my-config +data: + note: "this is not an IngressClass" +`, + }, + storageKinds: []string{"Ingress"}, + wantCount: 0, + }, + { + name: "does not run at all when the module ships no Ingress", + templateFiles: map[string]string{ + "templates/ingress.yaml": ungatedIngress, + }, + storageKinds: nil, // e.g. a module whose Ingress never actually rendered + wantCount: 0, + }, + { + name: "an excluded file is skipped even though the module ships an Ingress", + templateFiles: map[string]string{ + "templates/ingress.yaml": ungatedIngress, + }, + storageKinds: []string{"Ingress"}, + exclude: []pkg.StringRuleExclude{"templates/ingress.yaml"}, + wantCount: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + modulePath := writeTemplatesModule(t, tt.templateFiles) + + errorList := errors.NewLintRuleErrorsList() + NewIngressEnablementRule(tt.exclude, nil, templatesMockModule(t, modulePath, tt.storageKinds...), errorList).Check(t.Context()) + + errs := errorList.GetErrors() + require.Len(t, errs, tt.wantCount) + + for _, want := range tt.wantContains { + found := false + + for i := range errs { + if containsStr(errs[i].Text, want) { + found = true + break + } + } + + require.Truef(t, found, "expected a finding containing %q, got %+v", want, errs) + } + + for i, wantLine := range tt.wantLines { + if i < len(errs) { + require.Equalf(t, wantLine, errs[i].LineNumber, "unexpected line for finding %d: %s", i, errs[i].Text) + } + } + }) + } +} + +func TestIngressEnablementRule_DirectoryExclusion(t *testing.T) { + modulePath := writeTemplatesModule(t, map[string]string{ + "templates/vendor/ingress.yaml": `apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: dashboard +`, + }) + + errorList := errors.NewLintRuleErrorsList() + NewIngressEnablementRule( + nil, + []pkg.DirectoryRuleExclude{"templates/vendor/"}, + templatesMockModule(t, modulePath, "Ingress"), + errorList, + ).Check(t.Context()) + + require.Empty(t, errorList.GetErrors()) +} diff --git a/pkg/linters/templates/templates.go b/pkg/linters/templates/templates.go index 285e30057..2597e2f6e 100644 --- a/pkg/linters/templates/templates.go +++ b/pkg/linters/templates/templates.go @@ -111,6 +111,18 @@ func (l *Templates) rules() []pkg.Rule { rules.NewHelmRenderRule(m, level(cfg.Rules.HelmRenderRule)), rules.NewOpenAPIValuesQuoteRule(cfg.ExcludeRules.OpenAPIValuesQuote.Get(), m, level(cfg.Rules.OpenAPIValuesQuoteRule)), rules.NewSchemaValidationRule(cfg.ExcludeRules.SchemaValidation.Get(), m, level(cfg.Rules.SchemaValidationRule)), + rules.NewDeprecatedAnnotationsRule( + cfg.ExcludeRules.DeprecatedAnnotations.Files.Get(), + cfg.ExcludeRules.DeprecatedAnnotations.Directories.Get(), + m, level(cfg.Rules.DeprecatedAnnotationsRule)), + rules.NewIngressEnablementRule( + cfg.ExcludeRules.IngressEnablement.Files.Get(), + cfg.ExcludeRules.IngressEnablement.Directories.Get(), + m, level(cfg.Rules.IngressEnablementRule)), + rules.NewGatewayEnablementRule( + cfg.ExcludeRules.GatewayEnablement.Files.Get(), + cfg.ExcludeRules.GatewayEnablement.Directories.Get(), + m, level(cfg.Rules.GatewayEnablementRule)), ) } diff --git a/pkg/scopes/static.go b/pkg/scopes/static.go index 670aeb63b..ddc605735 100644 --- a/pkg/scopes/static.go +++ b/pkg/scopes/static.go @@ -130,10 +130,13 @@ var staticRules = map[string]set.Set{ templates.ID: set.New( templatesrules.CRDEnabledModulesRuleName, templatesrules.ClusterDomainRuleName, + templatesrules.DeprecatedAnnotationsRuleName, templatesrules.EnabledModulesRuleName, + templatesrules.GatewayEnablementRuleName, templatesrules.GrafanaRuleName, templatesrules.HTTPRouteRuleName, templatesrules.HelmRenderRuleName, + templatesrules.IngressEnablementRuleName, templatesrules.IngressRuleName, templatesrules.KubeRbacProxyRuleName, templatesrules.MountPointsRuleName, diff --git a/test/e2e/testdata/templates/deprecated-annotations/expected.yaml b/test/e2e/testdata/templates/deprecated-annotations/expected.yaml new file mode 100644 index 000000000..df2477ccb --- /dev/null +++ b/test/e2e/testdata/templates/deprecated-annotations/expected.yaml @@ -0,0 +1,11 @@ +description: > + A template that still sets the deprecated + `alb.network.deckhouse.io/response-headers-to-add` annotation must be flagged + by the deprecated-annotations rule instead of the native Gateway API + ResponseHeaderModifier filter. +module: module +expect: + - linter: templates + rule: deprecated-annotations + level: error + textContains: "alb.network.deckhouse.io/response-headers-to-add" diff --git a/test/e2e/testdata/templates/deprecated-annotations/module/module.yaml b/test/e2e/testdata/templates/deprecated-annotations/module/module.yaml new file mode 100644 index 000000000..0467c65f1 --- /dev/null +++ b/test/e2e/testdata/templates/deprecated-annotations/module/module.yaml @@ -0,0 +1,2 @@ +name: e2e-deprecated-annotations +namespace: e2e-deprecated-annotations diff --git a/test/e2e/testdata/templates/deprecated-annotations/module/openapi/config-values.yaml b/test/e2e/testdata/templates/deprecated-annotations/module/openapi/config-values.yaml new file mode 100644 index 000000000..03b0d8bfe --- /dev/null +++ b/test/e2e/testdata/templates/deprecated-annotations/module/openapi/config-values.yaml @@ -0,0 +1,2 @@ +type: object +properties: {} diff --git a/test/e2e/testdata/templates/deprecated-annotations/module/openapi/values.yaml b/test/e2e/testdata/templates/deprecated-annotations/module/openapi/values.yaml new file mode 100644 index 000000000..47180da56 --- /dev/null +++ b/test/e2e/testdata/templates/deprecated-annotations/module/openapi/values.yaml @@ -0,0 +1,4 @@ +x-extend: + schema: config-values.yaml +type: object +properties: {} diff --git a/test/e2e/testdata/templates/deprecated-annotations/module/templates/httproute.yaml b/test/e2e/testdata/templates/deprecated-annotations/module/templates/httproute.yaml new file mode 100644 index 000000000..c584cab77 --- /dev/null +++ b/test/e2e/testdata/templates/deprecated-annotations/module/templates/httproute.yaml @@ -0,0 +1,14 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: e2e-deprecated-annotations + namespace: e2e-deprecated-annotations + annotations: + alb.network.deckhouse.io/response-headers-to-add: '{"Strict-Transport-Security":"max-age=31536000; includeSubDomains"}' +spec: + hostnames: + - e2e.example.com + rules: + - backendRefs: + - name: e2e + port: 80 diff --git a/test/e2e/testdata/templates/gateway-enablement-missing/expected.yaml b/test/e2e/testdata/templates/gateway-enablement-missing/expected.yaml new file mode 100644 index 000000000..2fb632867 --- /dev/null +++ b/test/e2e/testdata/templates/gateway-enablement-missing/expected.yaml @@ -0,0 +1,11 @@ +description: > + A module that creates an HTTPRoute without ever checking + helm_lib_module_gateway_enabled cannot have that HTTPRoute disabled via + global or module configuration, and must be flagged by the + gateway-enablement rule. +module: module +expect: + - linter: templates + rule: gateway-enablement + level: error + textContains: "helm_lib_module_gateway_enabled" diff --git a/test/e2e/testdata/templates/gateway-enablement-missing/module/module.yaml b/test/e2e/testdata/templates/gateway-enablement-missing/module/module.yaml new file mode 100644 index 000000000..86e7b2d6c --- /dev/null +++ b/test/e2e/testdata/templates/gateway-enablement-missing/module/module.yaml @@ -0,0 +1,2 @@ +name: e2e-gateway-enablement-missing +namespace: e2e-gateway-enablement-missing diff --git a/test/e2e/testdata/templates/gateway-enablement-missing/module/openapi/config-values.yaml b/test/e2e/testdata/templates/gateway-enablement-missing/module/openapi/config-values.yaml new file mode 100644 index 000000000..03b0d8bfe --- /dev/null +++ b/test/e2e/testdata/templates/gateway-enablement-missing/module/openapi/config-values.yaml @@ -0,0 +1,2 @@ +type: object +properties: {} diff --git a/test/e2e/testdata/templates/gateway-enablement-missing/module/openapi/values.yaml b/test/e2e/testdata/templates/gateway-enablement-missing/module/openapi/values.yaml new file mode 100644 index 000000000..47180da56 --- /dev/null +++ b/test/e2e/testdata/templates/gateway-enablement-missing/module/openapi/values.yaml @@ -0,0 +1,4 @@ +x-extend: + schema: config-values.yaml +type: object +properties: {} diff --git a/test/e2e/testdata/templates/gateway-enablement-missing/module/templates/httproute.yaml b/test/e2e/testdata/templates/gateway-enablement-missing/module/templates/httproute.yaml new file mode 100644 index 000000000..df2e2de3f --- /dev/null +++ b/test/e2e/testdata/templates/gateway-enablement-missing/module/templates/httproute.yaml @@ -0,0 +1,12 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: e2e-gateway-enablement-missing + namespace: e2e-gateway-enablement-missing +spec: + hostnames: + - e2e.example.com + rules: + - backendRefs: + - name: e2e + port: 80 diff --git a/test/e2e/testdata/templates/ingress-enablement-missing/expected.yaml b/test/e2e/testdata/templates/ingress-enablement-missing/expected.yaml new file mode 100644 index 000000000..b9f1014da --- /dev/null +++ b/test/e2e/testdata/templates/ingress-enablement-missing/expected.yaml @@ -0,0 +1,10 @@ +description: > + A module that creates an Ingress without ever checking + helm_lib_module_ingress_enabled cannot have that Ingress disabled via global + or module configuration, and must be flagged by the ingress-enablement rule. +module: module +expect: + - linter: templates + rule: ingress-enablement + level: error + textContains: "helm_lib_module_ingress_enabled" diff --git a/test/e2e/testdata/templates/ingress-enablement-missing/module/module.yaml b/test/e2e/testdata/templates/ingress-enablement-missing/module/module.yaml new file mode 100644 index 000000000..d2dc361ab --- /dev/null +++ b/test/e2e/testdata/templates/ingress-enablement-missing/module/module.yaml @@ -0,0 +1,2 @@ +name: e2e-ingress-enablement-missing +namespace: e2e-ingress-enablement-missing diff --git a/test/e2e/testdata/templates/ingress-enablement-missing/module/openapi/config-values.yaml b/test/e2e/testdata/templates/ingress-enablement-missing/module/openapi/config-values.yaml new file mode 100644 index 000000000..03b0d8bfe --- /dev/null +++ b/test/e2e/testdata/templates/ingress-enablement-missing/module/openapi/config-values.yaml @@ -0,0 +1,2 @@ +type: object +properties: {} diff --git a/test/e2e/testdata/templates/ingress-enablement-missing/module/openapi/values.yaml b/test/e2e/testdata/templates/ingress-enablement-missing/module/openapi/values.yaml new file mode 100644 index 000000000..47180da56 --- /dev/null +++ b/test/e2e/testdata/templates/ingress-enablement-missing/module/openapi/values.yaml @@ -0,0 +1,4 @@ +x-extend: + schema: config-values.yaml +type: object +properties: {} diff --git a/test/e2e/testdata/templates/ingress-enablement-missing/module/templates/ingress.yaml b/test/e2e/testdata/templates/ingress-enablement-missing/module/templates/ingress.yaml new file mode 100644 index 000000000..22d477f66 --- /dev/null +++ b/test/e2e/testdata/templates/ingress-enablement-missing/module/templates/ingress.yaml @@ -0,0 +1,17 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: e2e-ingress-enablement-missing + namespace: e2e-ingress-enablement-missing +spec: + rules: + - host: e2e.example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: e2e + port: + number: 80 diff --git a/test/e2e/testdata/templates/no-ingress-gateway-objects/expected.yaml b/test/e2e/testdata/templates/no-ingress-gateway-objects/expected.yaml new file mode 100644 index 000000000..af2adc52d --- /dev/null +++ b/test/e2e/testdata/templates/no-ingress-gateway-objects/expected.yaml @@ -0,0 +1,14 @@ +description: > + A module that ships no Ingress, HTTPRoute, or ListenerSet at all must not + trigger deprecated-annotations, ingress-enablement, or gateway-enablement — + even though the module's ConfigMap contains the literal banned annotation + string, which would otherwise trip deprecated-annotations. All three rules + gate on the module actually rendering one of those kinds. +module: module +expectAbsent: + - linter: templates + rule: deprecated-annotations + - linter: templates + rule: ingress-enablement + - linter: templates + rule: gateway-enablement diff --git a/test/e2e/testdata/templates/no-ingress-gateway-objects/module/module.yaml b/test/e2e/testdata/templates/no-ingress-gateway-objects/module/module.yaml new file mode 100644 index 000000000..2bf1fa278 --- /dev/null +++ b/test/e2e/testdata/templates/no-ingress-gateway-objects/module/module.yaml @@ -0,0 +1,2 @@ +name: e2e-no-ingress-gateway-objects +namespace: e2e-no-ingress-gateway-objects diff --git a/test/e2e/testdata/templates/no-ingress-gateway-objects/module/openapi/config-values.yaml b/test/e2e/testdata/templates/no-ingress-gateway-objects/module/openapi/config-values.yaml new file mode 100644 index 000000000..03b0d8bfe --- /dev/null +++ b/test/e2e/testdata/templates/no-ingress-gateway-objects/module/openapi/config-values.yaml @@ -0,0 +1,2 @@ +type: object +properties: {} diff --git a/test/e2e/testdata/templates/no-ingress-gateway-objects/module/openapi/values.yaml b/test/e2e/testdata/templates/no-ingress-gateway-objects/module/openapi/values.yaml new file mode 100644 index 000000000..47180da56 --- /dev/null +++ b/test/e2e/testdata/templates/no-ingress-gateway-objects/module/openapi/values.yaml @@ -0,0 +1,4 @@ +x-extend: + schema: config-values.yaml +type: object +properties: {} diff --git a/test/e2e/testdata/templates/no-ingress-gateway-objects/module/templates/configmap.yaml b/test/e2e/testdata/templates/no-ingress-gateway-objects/module/templates/configmap.yaml new file mode 100644 index 000000000..946197fe2 --- /dev/null +++ b/test/e2e/testdata/templates/no-ingress-gateway-objects/module/templates/configmap.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: e2e-no-ingress-gateway-objects + namespace: e2e-no-ingress-gateway-objects +data: + # This string would trip the deprecated-annotations rule if it ran, but the + # module has no Ingress/HTTPRoute/ListenerSet at all, so the rule must not run. + note: "alb.network.deckhouse.io/response-headers-to-add" From 6e2b3e897769b523f8190261dde7db8703c73462 Mon Sep 17 00:00:00 2001 From: Mikhail Scherba Date: Sun, 6 Sep 2026 10:59:34 +0300 Subject: [PATCH 2/5] add ingress/gw-api linters Signed-off-by: Mikhail Scherba --- pkg/linters/templates/rules/gateway_enablement_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/linters/templates/rules/gateway_enablement_test.go b/pkg/linters/templates/rules/gateway_enablement_test.go index 64a895880..45b0e66a5 100644 --- a/pkg/linters/templates/rules/gateway_enablement_test.go +++ b/pkg/linters/templates/rules/gateway_enablement_test.go @@ -32,6 +32,7 @@ metadata: name: dashboard namespace: d8-my-module ` + const ungatedListenerSet = `apiVersion: gateway.networking.k8s.io/v1 kind: ListenerSet metadata: From 2b1ddd6c1871114c107dea53341a31d864adaa3b Mon Sep 17 00:00:00 2001 From: Mikhail Scherba Date: Sun, 6 Sep 2026 11:43:15 +0300 Subject: [PATCH 3/5] add excludes Signed-off-by: Mikhail Scherba --- internal/modules/module.go | 9 +++ pkg/config/global/global.go | 37 +++++----- pkg/config/linters_settings.go | 69 +++++++++++-------- .../expected.yaml | 11 +++ .../module/.dmtlint.yaml | 6 ++ .../module/module.yaml | 2 + .../module/openapi/config-values.yaml | 2 + .../module/openapi/values.yaml | 4 ++ .../module/templates/httproute.yaml | 14 ++++ 9 files changed, 109 insertions(+), 45 deletions(-) create mode 100644 test/e2e/testdata/templates/deprecated-annotations-excluded/expected.yaml create mode 100644 test/e2e/testdata/templates/deprecated-annotations-excluded/module/.dmtlint.yaml create mode 100644 test/e2e/testdata/templates/deprecated-annotations-excluded/module/module.yaml create mode 100644 test/e2e/testdata/templates/deprecated-annotations-excluded/module/openapi/config-values.yaml create mode 100644 test/e2e/testdata/templates/deprecated-annotations-excluded/module/openapi/values.yaml create mode 100644 test/e2e/testdata/templates/deprecated-annotations-excluded/module/templates/httproute.yaml diff --git a/internal/modules/module.go b/internal/modules/module.go index e1a0d1a00..61f14d907 100644 --- a/internal/modules/module.go +++ b/internal/modules/module.go @@ -418,6 +418,9 @@ func mapTemplatesRules(linterSettings *pkg.LintersSettings, configSettings *conf rules.HelmRenderRule.SetLevel(globalRules.HelmRenderRule.Impact, fallbackImpact) rules.OpenAPIValuesQuoteRule.SetLevel(globalRules.OpenAPIValuesQuoteRule.Impact, fallbackImpact) rules.SchemaValidationRule.SetLevel(globalRules.SchemaValidationRule.Impact, fallbackImpact) + rules.DeprecatedAnnotationsRule.SetLevel(globalRules.DeprecatedAnnotationsRule.Impact, fallbackImpact) + rules.IngressEnablementRule.SetLevel(globalRules.IngressEnablementRule.Impact, fallbackImpact) + rules.GatewayEnablementRule.SetLevel(globalRules.GatewayEnablementRule.Impact, fallbackImpact) } // mapOpenAPIRules configures OpenAPI linter rules @@ -552,6 +555,12 @@ func mapTemplatesExclusionsAndSettings(linterSettings *pkg.LintersSettings, conf excludes.MountPoints = pkg.StringRuleExcludeList(configExcludes.MountPoints) excludes.OpenAPIValuesQuote = pkg.StringRuleExcludeList(configExcludes.OpenAPIValuesQuote) excludes.SchemaValidation = configExcludes.SchemaValidation.Get() + excludes.DeprecatedAnnotations.Files = pkg.StringRuleExcludeList(configExcludes.DeprecatedAnnotations.Files) + excludes.DeprecatedAnnotations.Directories = pkg.DirectoryRuleExcludeList(configExcludes.DeprecatedAnnotations.Directories) + excludes.IngressEnablement.Files = pkg.StringRuleExcludeList(configExcludes.IngressEnablement.Files) + excludes.IngressEnablement.Directories = pkg.DirectoryRuleExcludeList(configExcludes.IngressEnablement.Directories) + excludes.GatewayEnablement.Files = pkg.StringRuleExcludeList(configExcludes.GatewayEnablement.Files) + excludes.GatewayEnablement.Directories = pkg.DirectoryRuleExcludeList(configExcludes.GatewayEnablement.Directories) // Additional settings linterSettings.Templates.PrometheusRuleSettings.Disable = configSettings.Templates.PrometheusRules.Disable diff --git a/pkg/config/global/global.go b/pkg/config/global/global.go index a5b2fca78..d052f863b 100644 --- a/pkg/config/global/global.go +++ b/pkg/config/global/global.go @@ -140,23 +140,26 @@ type TemplatesLinterConfig struct { } type TemplatesLinterRules struct { - VPARule RuleConfig `mapstructure:"vpa"` - PDBRule RuleConfig `mapstructure:"pdb"` - IngressRule RuleConfig `mapstructure:"ingress"` - HTTPRouteRule RuleConfig `mapstructure:"httproute"` - PrometheusRule RuleConfig `mapstructure:"prometheus-rules"` - GrafanaRule RuleConfig `mapstructure:"grafana-dashboards"` - KubeRBACProxyRule RuleConfig `mapstructure:"kube-rbac-proxy"` - ServicePortRule RuleConfig `mapstructure:"service-port"` - ClusterDomainRule RuleConfig `mapstructure:"cluster-domain"` - RegistryRule RuleConfig `mapstructure:"registry"` - EnabledModulesRule RuleConfig `mapstructure:"enabled-modules"` - CRDEnabledModulesRule RuleConfig `mapstructure:"crd-enabled-modules"` - WebhookConfigurationRule RuleConfig `mapstructure:"webhook-configuration-annotations"` - MountPointsRule RuleConfig `mapstructure:"mount-points"` - HelmRenderRule RuleConfig `mapstructure:"helm-render"` - OpenAPIValuesQuoteRule RuleConfig `mapstructure:"openapi-values-quote"` - SchemaValidationRule RuleConfig `mapstructure:"schema-validation"` + VPARule RuleConfig `mapstructure:"vpa"` + PDBRule RuleConfig `mapstructure:"pdb"` + IngressRule RuleConfig `mapstructure:"ingress"` + HTTPRouteRule RuleConfig `mapstructure:"httproute"` + PrometheusRule RuleConfig `mapstructure:"prometheus-rules"` + GrafanaRule RuleConfig `mapstructure:"grafana-dashboards"` + KubeRBACProxyRule RuleConfig `mapstructure:"kube-rbac-proxy"` + ServicePortRule RuleConfig `mapstructure:"service-port"` + ClusterDomainRule RuleConfig `mapstructure:"cluster-domain"` + RegistryRule RuleConfig `mapstructure:"registry"` + EnabledModulesRule RuleConfig `mapstructure:"enabled-modules"` + CRDEnabledModulesRule RuleConfig `mapstructure:"crd-enabled-modules"` + WebhookConfigurationRule RuleConfig `mapstructure:"webhook-configuration-annotations"` + MountPointsRule RuleConfig `mapstructure:"mount-points"` + HelmRenderRule RuleConfig `mapstructure:"helm-render"` + OpenAPIValuesQuoteRule RuleConfig `mapstructure:"openapi-values-quote"` + SchemaValidationRule RuleConfig `mapstructure:"schema-validation"` + DeprecatedAnnotationsRule RuleConfig `mapstructure:"deprecated-annotations"` + IngressEnablementRule RuleConfig `mapstructure:"ingress-enablement"` + GatewayEnablementRule RuleConfig `mapstructure:"gateway-enablement"` } func (c LinterConfig) IsWarn() bool { diff --git a/pkg/config/linters_settings.go b/pkg/config/linters_settings.go index db57c4804..346dc24f0 100644 --- a/pkg/config/linters_settings.go +++ b/pkg/config/linters_settings.go @@ -234,37 +234,43 @@ type TemplatesSettings struct { } type TemplatesLinterRules struct { - VPARule RuleConfig `mapstructure:"vpa"` - PDBRule RuleConfig `mapstructure:"pdb"` - IngressRule RuleConfig `mapstructure:"ingress"` - HTTPRouteRule RuleConfig `mapstructure:"httproute"` - PrometheusRule RuleConfig `mapstructure:"prometheus-rules"` - GrafanaRule RuleConfig `mapstructure:"grafana-dashboards"` - KubeRBACProxyRule RuleConfig `mapstructure:"kube-rbac-proxy"` - ServicePortRule RuleConfig `mapstructure:"service-port"` - ClusterDomainRule RuleConfig `mapstructure:"cluster-domain"` - RegistryRule RuleConfig `mapstructure:"registry"` - EnabledModulesRule RuleConfig `mapstructure:"enabled-modules"` - CRDEnabledModulesRule RuleConfig `mapstructure:"crd-enabled-modules"` - WebhookConfigurationRule RuleConfig `mapstructure:"webhook-configuration-annotations"` - MountPointsRule RuleConfig `mapstructure:"mount-points"` - HelmRenderRule RuleConfig `mapstructure:"helm-render"` - OpenAPIValuesQuoteRule RuleConfig `mapstructure:"openapi-values-quote"` - SchemaValidationRule RuleConfig `mapstructure:"schema-validation"` + VPARule RuleConfig `mapstructure:"vpa"` + PDBRule RuleConfig `mapstructure:"pdb"` + IngressRule RuleConfig `mapstructure:"ingress"` + HTTPRouteRule RuleConfig `mapstructure:"httproute"` + PrometheusRule RuleConfig `mapstructure:"prometheus-rules"` + GrafanaRule RuleConfig `mapstructure:"grafana-dashboards"` + KubeRBACProxyRule RuleConfig `mapstructure:"kube-rbac-proxy"` + ServicePortRule RuleConfig `mapstructure:"service-port"` + ClusterDomainRule RuleConfig `mapstructure:"cluster-domain"` + RegistryRule RuleConfig `mapstructure:"registry"` + EnabledModulesRule RuleConfig `mapstructure:"enabled-modules"` + CRDEnabledModulesRule RuleConfig `mapstructure:"crd-enabled-modules"` + WebhookConfigurationRule RuleConfig `mapstructure:"webhook-configuration-annotations"` + MountPointsRule RuleConfig `mapstructure:"mount-points"` + HelmRenderRule RuleConfig `mapstructure:"helm-render"` + OpenAPIValuesQuoteRule RuleConfig `mapstructure:"openapi-values-quote"` + SchemaValidationRule RuleConfig `mapstructure:"schema-validation"` + DeprecatedAnnotationsRule RuleConfig `mapstructure:"deprecated-annotations"` + IngressEnablementRule RuleConfig `mapstructure:"ingress-enablement"` + GatewayEnablementRule RuleConfig `mapstructure:"gateway-enablement"` } type TemplatesExcludeRules struct { - VPAAbsent KindRuleExcludeList `mapstructure:"vpa"` - PDBAbsent KindRuleExcludeList `mapstructure:"pdb"` - ServicePort ServicePortExcludeList `mapstructure:"service-port"` - KubeRBACProxy StringRuleExcludeList `mapstructure:"kube-rbac-proxy"` - Ingress KindRuleExcludeList `mapstructure:"ingress"` - HTTPRoute KindRuleExcludeList `mapstructure:"httproute"` - EnabledModules EnabledModulesExcludeRule `mapstructure:"enabled-modules"` - WebhookConfiguration KindRuleExcludeList `mapstructure:"webhook-configuration-annotations"` - MountPoints StringRuleExcludeList `mapstructure:"mount-points"` - OpenAPIValuesQuote StringRuleExcludeList `mapstructure:"openapi-values-quote"` - SchemaValidation KindRuleExcludeList `mapstructure:"schema-validation"` + VPAAbsent KindRuleExcludeList `mapstructure:"vpa"` + PDBAbsent KindRuleExcludeList `mapstructure:"pdb"` + ServicePort ServicePortExcludeList `mapstructure:"service-port"` + KubeRBACProxy StringRuleExcludeList `mapstructure:"kube-rbac-proxy"` + Ingress KindRuleExcludeList `mapstructure:"ingress"` + HTTPRoute KindRuleExcludeList `mapstructure:"httproute"` + EnabledModules EnabledModulesExcludeRule `mapstructure:"enabled-modules"` + WebhookConfiguration KindRuleExcludeList `mapstructure:"webhook-configuration-annotations"` + MountPoints StringRuleExcludeList `mapstructure:"mount-points"` + OpenAPIValuesQuote StringRuleExcludeList `mapstructure:"openapi-values-quote"` + SchemaValidation KindRuleExcludeList `mapstructure:"schema-validation"` + DeprecatedAnnotations PathRuleExclude `mapstructure:"deprecated-annotations"` + IngressEnablement PathRuleExclude `mapstructure:"ingress-enablement"` + GatewayEnablement PathRuleExclude `mapstructure:"gateway-enablement"` } type EnabledModulesExcludeRule struct { @@ -272,6 +278,13 @@ type EnabledModulesExcludeRule struct { Directories DirectoryRuleExcludeList `mapstructure:"directories"` } +// PathRuleExclude excludes specific files and whole directories (both relative +// to the module root) from a rule that scans template source files. +type PathRuleExclude struct { + Files StringRuleExcludeList `mapstructure:"files"` + Directories DirectoryRuleExcludeList `mapstructure:"directories"` +} + type GrafanaDashboardsExcludeList struct { Disable bool `mapstructure:"disable"` } diff --git a/test/e2e/testdata/templates/deprecated-annotations-excluded/expected.yaml b/test/e2e/testdata/templates/deprecated-annotations-excluded/expected.yaml new file mode 100644 index 000000000..61b2ec1ce --- /dev/null +++ b/test/e2e/testdata/templates/deprecated-annotations-excluded/expected.yaml @@ -0,0 +1,11 @@ +description: > + A module whose .dmtlint.yaml excludes templates/httproute.yaml from the + deprecated-annotations rule via exclude-rules.deprecated-annotations.files + must not be flagged for that file, even though it still sets the deprecated + alb.network.deckhouse.io/response-headers-to-add annotation. This proves the + exclude-rules configuration is actually wired from .dmtlint.yaml through to + the rule, not just reachable via a direct Go constructor call. +module: module +expectAbsent: + - linter: templates + rule: deprecated-annotations diff --git a/test/e2e/testdata/templates/deprecated-annotations-excluded/module/.dmtlint.yaml b/test/e2e/testdata/templates/deprecated-annotations-excluded/module/.dmtlint.yaml new file mode 100644 index 000000000..2f89325e6 --- /dev/null +++ b/test/e2e/testdata/templates/deprecated-annotations-excluded/module/.dmtlint.yaml @@ -0,0 +1,6 @@ +linters-settings: + templates: + exclude-rules: + deprecated-annotations: + files: + - templates/httproute.yaml diff --git a/test/e2e/testdata/templates/deprecated-annotations-excluded/module/module.yaml b/test/e2e/testdata/templates/deprecated-annotations-excluded/module/module.yaml new file mode 100644 index 000000000..0467c65f1 --- /dev/null +++ b/test/e2e/testdata/templates/deprecated-annotations-excluded/module/module.yaml @@ -0,0 +1,2 @@ +name: e2e-deprecated-annotations +namespace: e2e-deprecated-annotations diff --git a/test/e2e/testdata/templates/deprecated-annotations-excluded/module/openapi/config-values.yaml b/test/e2e/testdata/templates/deprecated-annotations-excluded/module/openapi/config-values.yaml new file mode 100644 index 000000000..03b0d8bfe --- /dev/null +++ b/test/e2e/testdata/templates/deprecated-annotations-excluded/module/openapi/config-values.yaml @@ -0,0 +1,2 @@ +type: object +properties: {} diff --git a/test/e2e/testdata/templates/deprecated-annotations-excluded/module/openapi/values.yaml b/test/e2e/testdata/templates/deprecated-annotations-excluded/module/openapi/values.yaml new file mode 100644 index 000000000..47180da56 --- /dev/null +++ b/test/e2e/testdata/templates/deprecated-annotations-excluded/module/openapi/values.yaml @@ -0,0 +1,4 @@ +x-extend: + schema: config-values.yaml +type: object +properties: {} diff --git a/test/e2e/testdata/templates/deprecated-annotations-excluded/module/templates/httproute.yaml b/test/e2e/testdata/templates/deprecated-annotations-excluded/module/templates/httproute.yaml new file mode 100644 index 000000000..c584cab77 --- /dev/null +++ b/test/e2e/testdata/templates/deprecated-annotations-excluded/module/templates/httproute.yaml @@ -0,0 +1,14 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: e2e-deprecated-annotations + namespace: e2e-deprecated-annotations + annotations: + alb.network.deckhouse.io/response-headers-to-add: '{"Strict-Transport-Security":"max-age=31536000; includeSubDomains"}' +spec: + hostnames: + - e2e.example.com + rules: + - backendRefs: + - name: e2e + port: 80 From 3e96226459fbd4a4d95d71254c59a4ba95435d6e Mon Sep 17 00:00:00 2001 From: Mikhail Scherba Date: Sun, 6 Sep 2026 12:14:40 +0300 Subject: [PATCH 4/5] add excludes Signed-off-by: Mikhail Scherba --- internal/modules/module.go | 6 +- pkg/config.go | 68 +++++++++---------- pkg/config/global/global.go | 40 +++++------ pkg/config/linters_settings.go | 68 +++++++++---------- pkg/linters/templates/README.md | 14 ++-- ...go => deprecated_httproute_annotations.go} | 20 +++--- ... deprecated_httproute_annotations_test.go} | 8 +-- pkg/linters/templates/templates.go | 8 +-- pkg/scopes/static.go | 2 +- .../module/module.yaml | 2 - .../deprecated-annotations/module/module.yaml | 2 - .../expected.yaml | 4 +- .../module/.dmtlint.yaml | 2 +- .../module/module.yaml | 2 + .../module/openapi/config-values.yaml | 0 .../module/openapi/values.yaml | 0 .../module/templates/httproute.yaml | 4 +- .../expected.yaml | 4 +- .../module/module.yaml | 2 + .../module/openapi/config-values.yaml | 0 .../module/openapi/values.yaml | 0 .../module/templates/httproute.yaml | 4 +- .../no-ingress-gateway-objects/expected.yaml | 6 +- .../module/templates/configmap.yaml | 2 +- 24 files changed, 135 insertions(+), 133 deletions(-) rename pkg/linters/templates/rules/{deprecated_annotations.go => deprecated_httproute_annotations.go} (85%) rename pkg/linters/templates/rules/{deprecated_annotations_test.go => deprecated_httproute_annotations_test.go} (95%) delete mode 100644 test/e2e/testdata/templates/deprecated-annotations-excluded/module/module.yaml delete mode 100644 test/e2e/testdata/templates/deprecated-annotations/module/module.yaml rename test/e2e/testdata/templates/{deprecated-annotations-excluded => deprecated-httproute-annotations-excluded}/expected.yaml (75%) rename test/e2e/testdata/templates/{deprecated-annotations-excluded => deprecated-httproute-annotations-excluded}/module/.dmtlint.yaml (71%) create mode 100644 test/e2e/testdata/templates/deprecated-httproute-annotations-excluded/module/module.yaml rename test/e2e/testdata/templates/{deprecated-annotations-excluded => deprecated-httproute-annotations-excluded}/module/openapi/config-values.yaml (100%) rename test/e2e/testdata/templates/{deprecated-annotations-excluded => deprecated-httproute-annotations-excluded}/module/openapi/values.yaml (100%) rename test/e2e/testdata/templates/{deprecated-annotations-excluded => deprecated-httproute-annotations-excluded}/module/templates/httproute.yaml (76%) rename test/e2e/testdata/templates/{deprecated-annotations => deprecated-httproute-annotations}/expected.yaml (70%) create mode 100644 test/e2e/testdata/templates/deprecated-httproute-annotations/module/module.yaml rename test/e2e/testdata/templates/{deprecated-annotations => deprecated-httproute-annotations}/module/openapi/config-values.yaml (100%) rename test/e2e/testdata/templates/{deprecated-annotations => deprecated-httproute-annotations}/module/openapi/values.yaml (100%) rename test/e2e/testdata/templates/{deprecated-annotations => deprecated-httproute-annotations}/module/templates/httproute.yaml (76%) diff --git a/internal/modules/module.go b/internal/modules/module.go index 61f14d907..29062413c 100644 --- a/internal/modules/module.go +++ b/internal/modules/module.go @@ -418,7 +418,7 @@ func mapTemplatesRules(linterSettings *pkg.LintersSettings, configSettings *conf rules.HelmRenderRule.SetLevel(globalRules.HelmRenderRule.Impact, fallbackImpact) rules.OpenAPIValuesQuoteRule.SetLevel(globalRules.OpenAPIValuesQuoteRule.Impact, fallbackImpact) rules.SchemaValidationRule.SetLevel(globalRules.SchemaValidationRule.Impact, fallbackImpact) - rules.DeprecatedAnnotationsRule.SetLevel(globalRules.DeprecatedAnnotationsRule.Impact, fallbackImpact) + rules.DeprecatedHTTPRouteAnnotationsRule.SetLevel(globalRules.DeprecatedHTTPRouteAnnotationsRule.Impact, fallbackImpact) rules.IngressEnablementRule.SetLevel(globalRules.IngressEnablementRule.Impact, fallbackImpact) rules.GatewayEnablementRule.SetLevel(globalRules.GatewayEnablementRule.Impact, fallbackImpact) } @@ -555,8 +555,8 @@ func mapTemplatesExclusionsAndSettings(linterSettings *pkg.LintersSettings, conf excludes.MountPoints = pkg.StringRuleExcludeList(configExcludes.MountPoints) excludes.OpenAPIValuesQuote = pkg.StringRuleExcludeList(configExcludes.OpenAPIValuesQuote) excludes.SchemaValidation = configExcludes.SchemaValidation.Get() - excludes.DeprecatedAnnotations.Files = pkg.StringRuleExcludeList(configExcludes.DeprecatedAnnotations.Files) - excludes.DeprecatedAnnotations.Directories = pkg.DirectoryRuleExcludeList(configExcludes.DeprecatedAnnotations.Directories) + excludes.DeprecatedHTTPRouteAnnotations.Files = pkg.StringRuleExcludeList(configExcludes.DeprecatedHTTPRouteAnnotations.Files) + excludes.DeprecatedHTTPRouteAnnotations.Directories = pkg.DirectoryRuleExcludeList(configExcludes.DeprecatedHTTPRouteAnnotations.Directories) excludes.IngressEnablement.Files = pkg.StringRuleExcludeList(configExcludes.IngressEnablement.Files) excludes.IngressEnablement.Directories = pkg.DirectoryRuleExcludeList(configExcludes.IngressEnablement.Directories) excludes.GatewayEnablement.Files = pkg.StringRuleExcludeList(configExcludes.GatewayEnablement.Files) diff --git a/pkg/config.go b/pkg/config.go index 644d377ad..91951cd6f 100644 --- a/pkg/config.go +++ b/pkg/config.go @@ -137,26 +137,26 @@ type TemplatesLinterConfig struct { GrafanaDashboardsSettings GrafanaDashboardsSettings } type TemplatesLinterRules struct { - VPARule RuleConfig - PDBRule RuleConfig - IngressRule RuleConfig - PrometheusRule RuleConfig - GrafanaRule RuleConfig - KubeRBACProxyRule RuleConfig - ServicePortRule RuleConfig - ClusterDomainRule RuleConfig - RegistryRule RuleConfig - HTTPRouteRule RuleConfig - EnabledModulesRule RuleConfig - CRDEnabledModulesRule RuleConfig - WebhookConfigurationRule RuleConfig - MountPointsRule RuleConfig - HelmRenderRule RuleConfig - OpenAPIValuesQuoteRule RuleConfig - SchemaValidationRule RuleConfig - DeprecatedAnnotationsRule RuleConfig - IngressEnablementRule RuleConfig - GatewayEnablementRule RuleConfig + VPARule RuleConfig + PDBRule RuleConfig + IngressRule RuleConfig + PrometheusRule RuleConfig + GrafanaRule RuleConfig + KubeRBACProxyRule RuleConfig + ServicePortRule RuleConfig + ClusterDomainRule RuleConfig + RegistryRule RuleConfig + HTTPRouteRule RuleConfig + EnabledModulesRule RuleConfig + CRDEnabledModulesRule RuleConfig + WebhookConfigurationRule RuleConfig + MountPointsRule RuleConfig + HelmRenderRule RuleConfig + OpenAPIValuesQuoteRule RuleConfig + SchemaValidationRule RuleConfig + DeprecatedHTTPRouteAnnotationsRule RuleConfig + IngressEnablementRule RuleConfig + GatewayEnablementRule RuleConfig } type PrometheusRuleSettings struct { @@ -167,20 +167,20 @@ type GrafanaDashboardsSettings struct { Disable bool } type TemplatesExcludeRules struct { - VPAAbsent KindRuleExcludeList - PDBAbsent KindRuleExcludeList - ServicePort ServicePortExcludeList - KubeRBACProxy StringRuleExcludeList - Ingress KindRuleExcludeList - HTTPRoute KindRuleExcludeList - EnabledModules EnabledModulesExcludeRule - WebhookConfiguration KindRuleExcludeList - MountPoints StringRuleExcludeList - OpenAPIValuesQuote StringRuleExcludeList - SchemaValidation KindRuleExcludeList - DeprecatedAnnotations PathRuleExclude - IngressEnablement PathRuleExclude - GatewayEnablement PathRuleExclude + VPAAbsent KindRuleExcludeList + PDBAbsent KindRuleExcludeList + ServicePort ServicePortExcludeList + KubeRBACProxy StringRuleExcludeList + Ingress KindRuleExcludeList + HTTPRoute KindRuleExcludeList + EnabledModules EnabledModulesExcludeRule + WebhookConfiguration KindRuleExcludeList + MountPoints StringRuleExcludeList + OpenAPIValuesQuote StringRuleExcludeList + SchemaValidation KindRuleExcludeList + DeprecatedHTTPRouteAnnotations PathRuleExclude + IngressEnablement PathRuleExclude + GatewayEnablement PathRuleExclude } // PathRuleExclude excludes specific files and whole directories (both relative diff --git a/pkg/config/global/global.go b/pkg/config/global/global.go index d052f863b..4a216ea38 100644 --- a/pkg/config/global/global.go +++ b/pkg/config/global/global.go @@ -140,26 +140,26 @@ type TemplatesLinterConfig struct { } type TemplatesLinterRules struct { - VPARule RuleConfig `mapstructure:"vpa"` - PDBRule RuleConfig `mapstructure:"pdb"` - IngressRule RuleConfig `mapstructure:"ingress"` - HTTPRouteRule RuleConfig `mapstructure:"httproute"` - PrometheusRule RuleConfig `mapstructure:"prometheus-rules"` - GrafanaRule RuleConfig `mapstructure:"grafana-dashboards"` - KubeRBACProxyRule RuleConfig `mapstructure:"kube-rbac-proxy"` - ServicePortRule RuleConfig `mapstructure:"service-port"` - ClusterDomainRule RuleConfig `mapstructure:"cluster-domain"` - RegistryRule RuleConfig `mapstructure:"registry"` - EnabledModulesRule RuleConfig `mapstructure:"enabled-modules"` - CRDEnabledModulesRule RuleConfig `mapstructure:"crd-enabled-modules"` - WebhookConfigurationRule RuleConfig `mapstructure:"webhook-configuration-annotations"` - MountPointsRule RuleConfig `mapstructure:"mount-points"` - HelmRenderRule RuleConfig `mapstructure:"helm-render"` - OpenAPIValuesQuoteRule RuleConfig `mapstructure:"openapi-values-quote"` - SchemaValidationRule RuleConfig `mapstructure:"schema-validation"` - DeprecatedAnnotationsRule RuleConfig `mapstructure:"deprecated-annotations"` - IngressEnablementRule RuleConfig `mapstructure:"ingress-enablement"` - GatewayEnablementRule RuleConfig `mapstructure:"gateway-enablement"` + VPARule RuleConfig `mapstructure:"vpa"` + PDBRule RuleConfig `mapstructure:"pdb"` + IngressRule RuleConfig `mapstructure:"ingress"` + HTTPRouteRule RuleConfig `mapstructure:"httproute"` + PrometheusRule RuleConfig `mapstructure:"prometheus-rules"` + GrafanaRule RuleConfig `mapstructure:"grafana-dashboards"` + KubeRBACProxyRule RuleConfig `mapstructure:"kube-rbac-proxy"` + ServicePortRule RuleConfig `mapstructure:"service-port"` + ClusterDomainRule RuleConfig `mapstructure:"cluster-domain"` + RegistryRule RuleConfig `mapstructure:"registry"` + EnabledModulesRule RuleConfig `mapstructure:"enabled-modules"` + CRDEnabledModulesRule RuleConfig `mapstructure:"crd-enabled-modules"` + WebhookConfigurationRule RuleConfig `mapstructure:"webhook-configuration-annotations"` + MountPointsRule RuleConfig `mapstructure:"mount-points"` + HelmRenderRule RuleConfig `mapstructure:"helm-render"` + OpenAPIValuesQuoteRule RuleConfig `mapstructure:"openapi-values-quote"` + SchemaValidationRule RuleConfig `mapstructure:"schema-validation"` + DeprecatedHTTPRouteAnnotationsRule RuleConfig `mapstructure:"deprecated-httproute-annotations"` + IngressEnablementRule RuleConfig `mapstructure:"ingress-enablement"` + GatewayEnablementRule RuleConfig `mapstructure:"gateway-enablement"` } func (c LinterConfig) IsWarn() bool { diff --git a/pkg/config/linters_settings.go b/pkg/config/linters_settings.go index 346dc24f0..3605ee760 100644 --- a/pkg/config/linters_settings.go +++ b/pkg/config/linters_settings.go @@ -234,43 +234,43 @@ type TemplatesSettings struct { } type TemplatesLinterRules struct { - VPARule RuleConfig `mapstructure:"vpa"` - PDBRule RuleConfig `mapstructure:"pdb"` - IngressRule RuleConfig `mapstructure:"ingress"` - HTTPRouteRule RuleConfig `mapstructure:"httproute"` - PrometheusRule RuleConfig `mapstructure:"prometheus-rules"` - GrafanaRule RuleConfig `mapstructure:"grafana-dashboards"` - KubeRBACProxyRule RuleConfig `mapstructure:"kube-rbac-proxy"` - ServicePortRule RuleConfig `mapstructure:"service-port"` - ClusterDomainRule RuleConfig `mapstructure:"cluster-domain"` - RegistryRule RuleConfig `mapstructure:"registry"` - EnabledModulesRule RuleConfig `mapstructure:"enabled-modules"` - CRDEnabledModulesRule RuleConfig `mapstructure:"crd-enabled-modules"` - WebhookConfigurationRule RuleConfig `mapstructure:"webhook-configuration-annotations"` - MountPointsRule RuleConfig `mapstructure:"mount-points"` - HelmRenderRule RuleConfig `mapstructure:"helm-render"` - OpenAPIValuesQuoteRule RuleConfig `mapstructure:"openapi-values-quote"` - SchemaValidationRule RuleConfig `mapstructure:"schema-validation"` - DeprecatedAnnotationsRule RuleConfig `mapstructure:"deprecated-annotations"` - IngressEnablementRule RuleConfig `mapstructure:"ingress-enablement"` - GatewayEnablementRule RuleConfig `mapstructure:"gateway-enablement"` + VPARule RuleConfig `mapstructure:"vpa"` + PDBRule RuleConfig `mapstructure:"pdb"` + IngressRule RuleConfig `mapstructure:"ingress"` + HTTPRouteRule RuleConfig `mapstructure:"httproute"` + PrometheusRule RuleConfig `mapstructure:"prometheus-rules"` + GrafanaRule RuleConfig `mapstructure:"grafana-dashboards"` + KubeRBACProxyRule RuleConfig `mapstructure:"kube-rbac-proxy"` + ServicePortRule RuleConfig `mapstructure:"service-port"` + ClusterDomainRule RuleConfig `mapstructure:"cluster-domain"` + RegistryRule RuleConfig `mapstructure:"registry"` + EnabledModulesRule RuleConfig `mapstructure:"enabled-modules"` + CRDEnabledModulesRule RuleConfig `mapstructure:"crd-enabled-modules"` + WebhookConfigurationRule RuleConfig `mapstructure:"webhook-configuration-annotations"` + MountPointsRule RuleConfig `mapstructure:"mount-points"` + HelmRenderRule RuleConfig `mapstructure:"helm-render"` + OpenAPIValuesQuoteRule RuleConfig `mapstructure:"openapi-values-quote"` + SchemaValidationRule RuleConfig `mapstructure:"schema-validation"` + DeprecatedHTTPRouteAnnotationsRule RuleConfig `mapstructure:"deprecated-httproute-annotations"` + IngressEnablementRule RuleConfig `mapstructure:"ingress-enablement"` + GatewayEnablementRule RuleConfig `mapstructure:"gateway-enablement"` } type TemplatesExcludeRules struct { - VPAAbsent KindRuleExcludeList `mapstructure:"vpa"` - PDBAbsent KindRuleExcludeList `mapstructure:"pdb"` - ServicePort ServicePortExcludeList `mapstructure:"service-port"` - KubeRBACProxy StringRuleExcludeList `mapstructure:"kube-rbac-proxy"` - Ingress KindRuleExcludeList `mapstructure:"ingress"` - HTTPRoute KindRuleExcludeList `mapstructure:"httproute"` - EnabledModules EnabledModulesExcludeRule `mapstructure:"enabled-modules"` - WebhookConfiguration KindRuleExcludeList `mapstructure:"webhook-configuration-annotations"` - MountPoints StringRuleExcludeList `mapstructure:"mount-points"` - OpenAPIValuesQuote StringRuleExcludeList `mapstructure:"openapi-values-quote"` - SchemaValidation KindRuleExcludeList `mapstructure:"schema-validation"` - DeprecatedAnnotations PathRuleExclude `mapstructure:"deprecated-annotations"` - IngressEnablement PathRuleExclude `mapstructure:"ingress-enablement"` - GatewayEnablement PathRuleExclude `mapstructure:"gateway-enablement"` + VPAAbsent KindRuleExcludeList `mapstructure:"vpa"` + PDBAbsent KindRuleExcludeList `mapstructure:"pdb"` + ServicePort ServicePortExcludeList `mapstructure:"service-port"` + KubeRBACProxy StringRuleExcludeList `mapstructure:"kube-rbac-proxy"` + Ingress KindRuleExcludeList `mapstructure:"ingress"` + HTTPRoute KindRuleExcludeList `mapstructure:"httproute"` + EnabledModules EnabledModulesExcludeRule `mapstructure:"enabled-modules"` + WebhookConfiguration KindRuleExcludeList `mapstructure:"webhook-configuration-annotations"` + MountPoints StringRuleExcludeList `mapstructure:"mount-points"` + OpenAPIValuesQuote StringRuleExcludeList `mapstructure:"openapi-values-quote"` + SchemaValidation KindRuleExcludeList `mapstructure:"schema-validation"` + DeprecatedHTTPRouteAnnotations PathRuleExclude `mapstructure:"deprecated-httproute-annotations"` + IngressEnablement PathRuleExclude `mapstructure:"ingress-enablement"` + GatewayEnablement PathRuleExclude `mapstructure:"gateway-enablement"` } type EnabledModulesExcludeRule struct { diff --git a/pkg/linters/templates/README.md b/pkg/linters/templates/README.md index 2a6a7d9d6..e02608300 100644 --- a/pkg/linters/templates/README.md +++ b/pkg/linters/templates/README.md @@ -27,7 +27,7 @@ Proper template validation prevents runtime issues, ensures applications are pro | [mount-points](#mount-points) | Validates that mount-points.yaml directories are used as volumeMounts in pod controllers | ✅ | enabled | | [openapi-values-quote](#openapi-values-quote) | Requires templates to quote OpenAPI string values that have no `pattern`/`enum`/`format` | ✅ | enabled | | [schema-validation](#schema-validation) | Strictly decodes every rendered standard Kubernetes resource against its API type | ✅ | enabled | -| [deprecated-annotations](#deprecated-annotations) | Flags deprecated annotation keys (e.g. `alb.network.deckhouse.io/response-headers-to-add`) | ✅ | enabled | +| [deprecated-httproute-annotations](#deprecated-httproute-annotations) | Flags deprecated annotation keys (e.g. `alb.network.deckhouse.io/response-headers-to-add`) | ✅ | enabled | | [ingress-enablement](#ingress-enablement) | Requires Ingress creation to be gated by `helm_lib_module_ingress_enabled` | ✅ | enabled | | [gateway-enablement](#gateway-enablement) | Requires HTTPRoute/ListenerSet creation to be gated by `helm_lib_module_gateway_enabled` | ✅ | enabled | @@ -2942,11 +2942,13 @@ updating this rule — there is nothing else to regenerate. --- -### deprecated-annotations +### deprecated-httproute-annotations -**Purpose:** Flags module-specific annotation keys that have been superseded by -a native Kubernetes/Gateway API mechanism, so authors migrate off them instead -of copying the pattern into new templates. +**Purpose:** Flags annotation keys used to work around a missing native +HTTPRoute setting — an ALB-specific annotation standing in for a field +Gateway API's HTTPRoute now exposes directly — so authors migrate to the +native field instead of copying the annotation-based workaround into new +templates. **Description:** @@ -3037,7 +3039,7 @@ to the module root): linters-settings: templates: exclude-rules: - deprecated-annotations: + deprecated-httproute-annotations: files: - templates/legacy-ingress.yaml directories: diff --git a/pkg/linters/templates/rules/deprecated_annotations.go b/pkg/linters/templates/rules/deprecated_httproute_annotations.go similarity index 85% rename from pkg/linters/templates/rules/deprecated_annotations.go rename to pkg/linters/templates/rules/deprecated_httproute_annotations.go index 5e562d779..5d47221ff 100644 --- a/pkg/linters/templates/rules/deprecated_annotations.go +++ b/pkg/linters/templates/rules/deprecated_httproute_annotations.go @@ -29,7 +29,7 @@ import ( ) const ( - DeprecatedAnnotationsRuleName = "deprecated-annotations" + DeprecatedHTTPRouteAnnotationsRuleName = "deprecated-httproute-annotations" ) // deprecatedAnnotation is one annotation key that must no longer appear in @@ -59,7 +59,7 @@ var deprecatedAnnotations = []deprecatedAnnotation{ }, } -type DeprecatedAnnotationsRule struct { +type DeprecatedHTTPRouteAnnotationsRule struct { pkg.RuleMeta pkg.PathRule @@ -67,23 +67,23 @@ type DeprecatedAnnotationsRule struct { errorList *errors.LintRuleErrorsList } -func NewDeprecatedAnnotationsRule(excludeFileRules []pkg.StringRuleExclude, +func NewDeprecatedHTTPRouteAnnotationsRule(excludeFileRules []pkg.StringRuleExclude, excludeDirectoryRules []pkg.DirectoryRuleExclude, - m pkg.Module, errorList *errors.LintRuleErrorsList) *DeprecatedAnnotationsRule { - return &DeprecatedAnnotationsRule{ + m pkg.Module, errorList *errors.LintRuleErrorsList) *DeprecatedHTTPRouteAnnotationsRule { + return &DeprecatedHTTPRouteAnnotationsRule{ RuleMeta: pkg.RuleMeta{ - Name: DeprecatedAnnotationsRuleName, + Name: DeprecatedHTTPRouteAnnotationsRuleName, }, PathRule: pkg.PathRule{ ExcludeStringRules: excludeFileRules, ExcludeDirectoryRules: excludeDirectoryRules, }, module: m, - errorList: errorList.WithRule(DeprecatedAnnotationsRuleName), + errorList: errorList.WithRule(DeprecatedHTTPRouteAnnotationsRuleName), } } -var _ pkg.Rule = (*DeprecatedAnnotationsRule)(nil) +var _ pkg.Rule = (*DeprecatedHTTPRouteAnnotationsRule)(nil) // Check scans every template file for the annotation keys in deprecatedAnnotations // and reports each occurrence, regardless of whether the key appears as a plain @@ -93,7 +93,7 @@ var _ pkg.Rule = (*DeprecatedAnnotationsRule)(nil) // The rule only runs when the module actually renders an Ingress, HTTPRoute, or // ListenerSet: every entry in deprecatedAnnotations is specific to those // resources, so a module with none of them has nothing for this check to say. -func (r *DeprecatedAnnotationsRule) Check(_ context.Context) { +func (r *DeprecatedHTTPRouteAnnotationsRule) Check(_ context.Context) { m := r.module if !storageHasKind(m, "Ingress", "HTTPRoute", "ListenerSet") { @@ -124,7 +124,7 @@ func (r *DeprecatedAnnotationsRule) Check(_ context.Context) { } } -func (r *DeprecatedAnnotationsRule) checkContent(relPath string, content []byte) { +func (r *DeprecatedHTTPRouteAnnotationsRule) checkContent(relPath string, content []byte) { for _, annotation := range deprecatedAnnotations { re := regexp.MustCompile(regexp.QuoteMeta(annotation.Key)) diff --git a/pkg/linters/templates/rules/deprecated_annotations_test.go b/pkg/linters/templates/rules/deprecated_httproute_annotations_test.go similarity index 95% rename from pkg/linters/templates/rules/deprecated_annotations_test.go rename to pkg/linters/templates/rules/deprecated_httproute_annotations_test.go index 6ded7c28a..0713655f0 100644 --- a/pkg/linters/templates/rules/deprecated_annotations_test.go +++ b/pkg/linters/templates/rules/deprecated_httproute_annotations_test.go @@ -85,7 +85,7 @@ func templatesMockModule(t *testing.T, modulePath string, storageKinds ...string return m } -func TestDeprecatedAnnotationsRule_Check(t *testing.T) { +func TestDeprecatedHTTPRouteAnnotationsRule_Check(t *testing.T) { const httprouteWithAnnotation = `apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: @@ -171,7 +171,7 @@ metadata: modulePath := writeTemplatesModule(t, tt.templateFiles) errorList := errors.NewLintRuleErrorsList() - NewDeprecatedAnnotationsRule(tt.exclude, nil, templatesMockModule(t, modulePath, tt.storageKinds...), errorList).Check(t.Context()) + NewDeprecatedHTTPRouteAnnotationsRule(tt.exclude, nil, templatesMockModule(t, modulePath, tt.storageKinds...), errorList).Check(t.Context()) errs := errorList.GetErrors() require.Len(t, errs, tt.wantCount) @@ -198,7 +198,7 @@ metadata: } } -func TestDeprecatedAnnotationsRule_DirectoryExclusion(t *testing.T) { +func TestDeprecatedHTTPRouteAnnotationsRule_DirectoryExclusion(t *testing.T) { modulePath := writeTemplatesModule(t, map[string]string{ "templates/vendor/httproute.yaml": `metadata: annotations: @@ -207,7 +207,7 @@ func TestDeprecatedAnnotationsRule_DirectoryExclusion(t *testing.T) { }) errorList := errors.NewLintRuleErrorsList() - NewDeprecatedAnnotationsRule( + NewDeprecatedHTTPRouteAnnotationsRule( nil, []pkg.DirectoryRuleExclude{"templates/vendor/"}, templatesMockModule(t, modulePath, "HTTPRoute"), diff --git a/pkg/linters/templates/templates.go b/pkg/linters/templates/templates.go index 2597e2f6e..70a688744 100644 --- a/pkg/linters/templates/templates.go +++ b/pkg/linters/templates/templates.go @@ -111,10 +111,10 @@ func (l *Templates) rules() []pkg.Rule { rules.NewHelmRenderRule(m, level(cfg.Rules.HelmRenderRule)), rules.NewOpenAPIValuesQuoteRule(cfg.ExcludeRules.OpenAPIValuesQuote.Get(), m, level(cfg.Rules.OpenAPIValuesQuoteRule)), rules.NewSchemaValidationRule(cfg.ExcludeRules.SchemaValidation.Get(), m, level(cfg.Rules.SchemaValidationRule)), - rules.NewDeprecatedAnnotationsRule( - cfg.ExcludeRules.DeprecatedAnnotations.Files.Get(), - cfg.ExcludeRules.DeprecatedAnnotations.Directories.Get(), - m, level(cfg.Rules.DeprecatedAnnotationsRule)), + rules.NewDeprecatedHTTPRouteAnnotationsRule( + cfg.ExcludeRules.DeprecatedHTTPRouteAnnotations.Files.Get(), + cfg.ExcludeRules.DeprecatedHTTPRouteAnnotations.Directories.Get(), + m, level(cfg.Rules.DeprecatedHTTPRouteAnnotationsRule)), rules.NewIngressEnablementRule( cfg.ExcludeRules.IngressEnablement.Files.Get(), cfg.ExcludeRules.IngressEnablement.Directories.Get(), diff --git a/pkg/scopes/static.go b/pkg/scopes/static.go index ddc605735..17c8587f2 100644 --- a/pkg/scopes/static.go +++ b/pkg/scopes/static.go @@ -130,7 +130,7 @@ var staticRules = map[string]set.Set{ templates.ID: set.New( templatesrules.CRDEnabledModulesRuleName, templatesrules.ClusterDomainRuleName, - templatesrules.DeprecatedAnnotationsRuleName, + templatesrules.DeprecatedHTTPRouteAnnotationsRuleName, templatesrules.EnabledModulesRuleName, templatesrules.GatewayEnablementRuleName, templatesrules.GrafanaRuleName, diff --git a/test/e2e/testdata/templates/deprecated-annotations-excluded/module/module.yaml b/test/e2e/testdata/templates/deprecated-annotations-excluded/module/module.yaml deleted file mode 100644 index 0467c65f1..000000000 --- a/test/e2e/testdata/templates/deprecated-annotations-excluded/module/module.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: e2e-deprecated-annotations -namespace: e2e-deprecated-annotations diff --git a/test/e2e/testdata/templates/deprecated-annotations/module/module.yaml b/test/e2e/testdata/templates/deprecated-annotations/module/module.yaml deleted file mode 100644 index 0467c65f1..000000000 --- a/test/e2e/testdata/templates/deprecated-annotations/module/module.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: e2e-deprecated-annotations -namespace: e2e-deprecated-annotations diff --git a/test/e2e/testdata/templates/deprecated-annotations-excluded/expected.yaml b/test/e2e/testdata/templates/deprecated-httproute-annotations-excluded/expected.yaml similarity index 75% rename from test/e2e/testdata/templates/deprecated-annotations-excluded/expected.yaml rename to test/e2e/testdata/templates/deprecated-httproute-annotations-excluded/expected.yaml index 61b2ec1ce..adac35ef4 100644 --- a/test/e2e/testdata/templates/deprecated-annotations-excluded/expected.yaml +++ b/test/e2e/testdata/templates/deprecated-httproute-annotations-excluded/expected.yaml @@ -1,6 +1,6 @@ description: > A module whose .dmtlint.yaml excludes templates/httproute.yaml from the - deprecated-annotations rule via exclude-rules.deprecated-annotations.files + deprecated-httproute-annotations rule via exclude-rules.deprecated-httproute-annotations.files must not be flagged for that file, even though it still sets the deprecated alb.network.deckhouse.io/response-headers-to-add annotation. This proves the exclude-rules configuration is actually wired from .dmtlint.yaml through to @@ -8,4 +8,4 @@ description: > module: module expectAbsent: - linter: templates - rule: deprecated-annotations + rule: deprecated-httproute-annotations diff --git a/test/e2e/testdata/templates/deprecated-annotations-excluded/module/.dmtlint.yaml b/test/e2e/testdata/templates/deprecated-httproute-annotations-excluded/module/.dmtlint.yaml similarity index 71% rename from test/e2e/testdata/templates/deprecated-annotations-excluded/module/.dmtlint.yaml rename to test/e2e/testdata/templates/deprecated-httproute-annotations-excluded/module/.dmtlint.yaml index 2f89325e6..0e506f116 100644 --- a/test/e2e/testdata/templates/deprecated-annotations-excluded/module/.dmtlint.yaml +++ b/test/e2e/testdata/templates/deprecated-httproute-annotations-excluded/module/.dmtlint.yaml @@ -1,6 +1,6 @@ linters-settings: templates: exclude-rules: - deprecated-annotations: + deprecated-httproute-annotations: files: - templates/httproute.yaml diff --git a/test/e2e/testdata/templates/deprecated-httproute-annotations-excluded/module/module.yaml b/test/e2e/testdata/templates/deprecated-httproute-annotations-excluded/module/module.yaml new file mode 100644 index 000000000..891ac0d51 --- /dev/null +++ b/test/e2e/testdata/templates/deprecated-httproute-annotations-excluded/module/module.yaml @@ -0,0 +1,2 @@ +name: e2e-deprecated-httproute-annotations +namespace: e2e-deprecated-httproute-annotations diff --git a/test/e2e/testdata/templates/deprecated-annotations-excluded/module/openapi/config-values.yaml b/test/e2e/testdata/templates/deprecated-httproute-annotations-excluded/module/openapi/config-values.yaml similarity index 100% rename from test/e2e/testdata/templates/deprecated-annotations-excluded/module/openapi/config-values.yaml rename to test/e2e/testdata/templates/deprecated-httproute-annotations-excluded/module/openapi/config-values.yaml diff --git a/test/e2e/testdata/templates/deprecated-annotations-excluded/module/openapi/values.yaml b/test/e2e/testdata/templates/deprecated-httproute-annotations-excluded/module/openapi/values.yaml similarity index 100% rename from test/e2e/testdata/templates/deprecated-annotations-excluded/module/openapi/values.yaml rename to test/e2e/testdata/templates/deprecated-httproute-annotations-excluded/module/openapi/values.yaml diff --git a/test/e2e/testdata/templates/deprecated-annotations-excluded/module/templates/httproute.yaml b/test/e2e/testdata/templates/deprecated-httproute-annotations-excluded/module/templates/httproute.yaml similarity index 76% rename from test/e2e/testdata/templates/deprecated-annotations-excluded/module/templates/httproute.yaml rename to test/e2e/testdata/templates/deprecated-httproute-annotations-excluded/module/templates/httproute.yaml index c584cab77..a362a6ae3 100644 --- a/test/e2e/testdata/templates/deprecated-annotations-excluded/module/templates/httproute.yaml +++ b/test/e2e/testdata/templates/deprecated-httproute-annotations-excluded/module/templates/httproute.yaml @@ -1,8 +1,8 @@ apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: - name: e2e-deprecated-annotations - namespace: e2e-deprecated-annotations + name: e2e-deprecated-httproute-annotations + namespace: e2e-deprecated-httproute-annotations annotations: alb.network.deckhouse.io/response-headers-to-add: '{"Strict-Transport-Security":"max-age=31536000; includeSubDomains"}' spec: diff --git a/test/e2e/testdata/templates/deprecated-annotations/expected.yaml b/test/e2e/testdata/templates/deprecated-httproute-annotations/expected.yaml similarity index 70% rename from test/e2e/testdata/templates/deprecated-annotations/expected.yaml rename to test/e2e/testdata/templates/deprecated-httproute-annotations/expected.yaml index df2477ccb..aadec274e 100644 --- a/test/e2e/testdata/templates/deprecated-annotations/expected.yaml +++ b/test/e2e/testdata/templates/deprecated-httproute-annotations/expected.yaml @@ -1,11 +1,11 @@ description: > A template that still sets the deprecated `alb.network.deckhouse.io/response-headers-to-add` annotation must be flagged - by the deprecated-annotations rule instead of the native Gateway API + by the deprecated-httproute-annotations rule instead of the native Gateway API ResponseHeaderModifier filter. module: module expect: - linter: templates - rule: deprecated-annotations + rule: deprecated-httproute-annotations level: error textContains: "alb.network.deckhouse.io/response-headers-to-add" diff --git a/test/e2e/testdata/templates/deprecated-httproute-annotations/module/module.yaml b/test/e2e/testdata/templates/deprecated-httproute-annotations/module/module.yaml new file mode 100644 index 000000000..891ac0d51 --- /dev/null +++ b/test/e2e/testdata/templates/deprecated-httproute-annotations/module/module.yaml @@ -0,0 +1,2 @@ +name: e2e-deprecated-httproute-annotations +namespace: e2e-deprecated-httproute-annotations diff --git a/test/e2e/testdata/templates/deprecated-annotations/module/openapi/config-values.yaml b/test/e2e/testdata/templates/deprecated-httproute-annotations/module/openapi/config-values.yaml similarity index 100% rename from test/e2e/testdata/templates/deprecated-annotations/module/openapi/config-values.yaml rename to test/e2e/testdata/templates/deprecated-httproute-annotations/module/openapi/config-values.yaml diff --git a/test/e2e/testdata/templates/deprecated-annotations/module/openapi/values.yaml b/test/e2e/testdata/templates/deprecated-httproute-annotations/module/openapi/values.yaml similarity index 100% rename from test/e2e/testdata/templates/deprecated-annotations/module/openapi/values.yaml rename to test/e2e/testdata/templates/deprecated-httproute-annotations/module/openapi/values.yaml diff --git a/test/e2e/testdata/templates/deprecated-annotations/module/templates/httproute.yaml b/test/e2e/testdata/templates/deprecated-httproute-annotations/module/templates/httproute.yaml similarity index 76% rename from test/e2e/testdata/templates/deprecated-annotations/module/templates/httproute.yaml rename to test/e2e/testdata/templates/deprecated-httproute-annotations/module/templates/httproute.yaml index c584cab77..a362a6ae3 100644 --- a/test/e2e/testdata/templates/deprecated-annotations/module/templates/httproute.yaml +++ b/test/e2e/testdata/templates/deprecated-httproute-annotations/module/templates/httproute.yaml @@ -1,8 +1,8 @@ apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: - name: e2e-deprecated-annotations - namespace: e2e-deprecated-annotations + name: e2e-deprecated-httproute-annotations + namespace: e2e-deprecated-httproute-annotations annotations: alb.network.deckhouse.io/response-headers-to-add: '{"Strict-Transport-Security":"max-age=31536000; includeSubDomains"}' spec: diff --git a/test/e2e/testdata/templates/no-ingress-gateway-objects/expected.yaml b/test/e2e/testdata/templates/no-ingress-gateway-objects/expected.yaml index af2adc52d..c3016a126 100644 --- a/test/e2e/testdata/templates/no-ingress-gateway-objects/expected.yaml +++ b/test/e2e/testdata/templates/no-ingress-gateway-objects/expected.yaml @@ -1,13 +1,13 @@ description: > A module that ships no Ingress, HTTPRoute, or ListenerSet at all must not - trigger deprecated-annotations, ingress-enablement, or gateway-enablement — + trigger deprecated-httproute-annotations, ingress-enablement, or gateway-enablement — even though the module's ConfigMap contains the literal banned annotation - string, which would otherwise trip deprecated-annotations. All three rules + string, which would otherwise trip deprecated-httproute-annotations. All three rules gate on the module actually rendering one of those kinds. module: module expectAbsent: - linter: templates - rule: deprecated-annotations + rule: deprecated-httproute-annotations - linter: templates rule: ingress-enablement - linter: templates diff --git a/test/e2e/testdata/templates/no-ingress-gateway-objects/module/templates/configmap.yaml b/test/e2e/testdata/templates/no-ingress-gateway-objects/module/templates/configmap.yaml index 946197fe2..19fff124a 100644 --- a/test/e2e/testdata/templates/no-ingress-gateway-objects/module/templates/configmap.yaml +++ b/test/e2e/testdata/templates/no-ingress-gateway-objects/module/templates/configmap.yaml @@ -4,6 +4,6 @@ metadata: name: e2e-no-ingress-gateway-objects namespace: e2e-no-ingress-gateway-objects data: - # This string would trip the deprecated-annotations rule if it ran, but the + # This string would trip the deprecated-httproute-annotations rule if it ran, but the # module has no Ingress/HTTPRoute/ListenerSet at all, so the rule must not run. note: "alb.network.deckhouse.io/response-headers-to-add" From 6db20c6f03aa7aed309b03a46ee53c6b05609324 Mon Sep 17 00:00:00 2001 From: Mikhail Scherba Date: Mon, 7 Sep 2026 17:26:39 +0300 Subject: [PATCH 5/5] improve description Signed-off-by: Mikhail Scherba --- pkg/linters/templates/README.md | 4 ++-- pkg/linters/templates/rules/enablement_helpers.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/linters/templates/README.md b/pkg/linters/templates/README.md index e02608300..c41fec0e1 100644 --- a/pkg/linters/templates/README.md +++ b/pkg/linters/templates/README.md @@ -3104,7 +3104,7 @@ spec: **Error:** ``` -Error: File creates a Ingress object but never checks "helm_lib_module_ingress_enabled", so its creation cannot be controlled via global.modules.ingress.enabled or myModule.ingress.enabled. Guard the manifest with {{- if eq (include "helm_lib_module_ingress_enabled" .) "true" }} ... {{- end }} so it can be disabled the same way every other module's Ingress does. +Error: File creates a Ingress object but never checks "helm_lib_module_ingress_enabled", so its creation cannot be controlled via global.modules.ingress.enabled or myModule.ingress.enabled. Guard the manifest with {{- if eq (include "helm_lib_module_ingress_enabled" .) "true" }} ... {{- end }} (requires lib_helm v1.72.21+) so it can be disabled the same way every other module's Ingress does. ``` The exact `.Values` path named in the finding is computed from the module's own @@ -3195,7 +3195,7 @@ spec: **Error:** ``` -Error: File creates a Gateway API (HTTPRoute/ListenerSet) object but never checks "helm_lib_module_gateway_enabled", so its creation cannot be controlled via global.modules.gatewayAPI.enabled or myModule.gatewayAPI.enabled, with a Gateway resolvable via global.discovery.gatewayAPIDefaultGateway, global.modules.gatewayAPI.gateway, or myModule.gatewayAPI.gateway. Guard the manifest with {{- if eq (include "helm_lib_module_gateway_enabled" .) "true" }} ... {{- end }} so it can be disabled the same way every other module's Gateway API (HTTPRoute/ListenerSet) does. +Error: File creates a Gateway API (HTTPRoute/ListenerSet) object but never checks "helm_lib_module_gateway_enabled", so its creation cannot be controlled via global.modules.gatewayAPI.enabled or myModule.gatewayAPI.enabled, with a Gateway resolvable via global.discovery.gatewayAPIDefaultGateway, global.modules.gatewayAPI.gateway, or myModule.gatewayAPI.gateway. Guard the manifest with {{- if eq (include "helm_lib_module_gateway_enabled" .) "true" }} ... {{- end }} (requires lib_helm v1.72.21+) so it can be disabled the same way every other module's Gateway API (HTTPRoute/ListenerSet) does. ``` As with `ingress-enablement`, the `.Values` paths named in the finding are diff --git a/pkg/linters/templates/rules/enablement_helpers.go b/pkg/linters/templates/rules/enablement_helpers.go index 96df378fb..9d30d9a46 100644 --- a/pkg/linters/templates/rules/enablement_helpers.go +++ b/pkg/linters/templates/rules/enablement_helpers.go @@ -118,8 +118,8 @@ func checkKindGatedByHelper( Errorf( "File creates a %s object but never checks %q, so its creation cannot be "+ "controlled via %s. Guard the manifest with "+ - "{{- if eq (include %q .) \"true\" }} ... {{- end }} so it can be disabled the "+ - "same way every other module's %s does.", + "{{- if eq (include %q .) \"true\" }} ... {{- end }} (requires lib_helm "+ + "v1.72.21+) so it can be disabled the same way every other module's %s does.", kindLabel, helperName, valuesHint, helperName, kindLabel, ) }