From b3f5c18ea907a47c370c531f1db9754f652f405a Mon Sep 17 00:00:00 2001 From: Evsyukov Denis Date: Mon, 18 Aug 2025 15:55:59 +0300 Subject: [PATCH 01/11] feat: enhance Grafana dashboard validation rules - Updated gjson version in go.mod and go.sum. - Added comprehensive validation rules for Grafana dashboards, including checks for deprecated panel types, intervals, legacy alert rules, and datasource validation. - Introduced tests for the new validation rules to ensure correctness and reliability. --- go.mod | 2 +- go.sum | 2 + pkg/linters/templates/README.md | 29 ++ pkg/linters/templates/rules/grafana.go | 287 ++++++++++++++++++- pkg/linters/templates/rules/grafana_test.go | 295 ++++++++++++++++++++ 5 files changed, 613 insertions(+), 2 deletions(-) create mode 100644 pkg/linters/templates/rules/grafana_test.go diff --git a/go.mod b/go.mod index 5c903bdf..fa0f1648 100644 --- a/go.mod +++ b/go.mod @@ -29,7 +29,7 @@ require ( github.com/spf13/pflag v1.0.6 github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.10.0 - github.com/tidwall/gjson v1.14.4 + github.com/tidwall/gjson v1.18.0 golang.org/x/mod v0.25.0 gopkg.in/ini.v1 v1.67.0 gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum index aab1426a..ebffe182 100644 --- a/go.sum +++ b/go.sum @@ -366,6 +366,8 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= diff --git a/pkg/linters/templates/README.md b/pkg/linters/templates/README.md index b3cb476f..e5137b8e 100644 --- a/pkg/linters/templates/README.md +++ b/pkg/linters/templates/README.md @@ -6,6 +6,7 @@ Lint monitoring rules: - run promtool checks - render prometheus rules +- validate grafana dashboards with comprehensive rules ## Settings example @@ -40,3 +41,31 @@ linters-settings: - d8-system impact: error ``` + +## Grafana Dashboard Validation Rules + +The linter now includes comprehensive validation for Grafana dashboards based on best practices from the Deckhouse project: + +### Deprecated Panel Types + +- **graph** → **timeseries**: The `graph` panel type is deprecated and should be replaced with `timeseries` +- **flant-statusmap-panel** → **state-timeline**: The custom statusmap panel should use the standard `state-timeline` panel + +### Deprecated Intervals + +- **interval_rv**, **interval_sx3**, **interval_sx4**: These custom intervals are deprecated and should be replaced with Grafana's built-in `$__rate_interval` variable + +### Legacy Alert Rules + +- **Built-in alerts**: Panels with embedded alert rules should use external Alertmanager instead of Grafana's built-in alerting + +### Datasource Validation + +- **Legacy format**: Detects old datasource UID formats that need to be resaved with newer Grafana versions +- **Hardcoded UIDs**: Identifies hardcoded datasource UIDs that should use Grafana variables +- **Prometheus UIDs**: Ensures Prometheus datasources use recommended UID patterns (`$ds_prometheus` or `${ds_prometheus}`) + +### Template Variables + +- **Required variable**: Ensures dashboards contain the required `ds_prometheus` variable of type `datasource` +- **Query variables**: Validates that query variables use recommended datasource UIDs diff --git a/pkg/linters/templates/rules/grafana.go b/pkg/linters/templates/rules/grafana.go index 62b7ef1a..f17de7d3 100644 --- a/pkg/linters/templates/rules/grafana.go +++ b/pkg/linters/templates/rules/grafana.go @@ -1,5 +1,5 @@ /* -Copyright 2021 Flant JSC +Copyright 2025 Flant JSC Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,6 +17,9 @@ limitations under the License. package rules import ( + "github.com/tidwall/gjson" + + "github.com/deckhouse/dmt/internal/fsutils" "github.com/deckhouse/dmt/internal/module" "github.com/deckhouse/dmt/pkg" "github.com/deckhouse/dmt/pkg/config" @@ -83,6 +86,8 @@ func (r *GrafanaRule) ValidateGrafanaDashboards(m *module.Module, errorList *err return } + // Validate individual dashboard files + r.validateDashboardFiles(m, errorList) desiredContent := `include "helm_lib_grafana_dashboard_definitions` @@ -97,3 +102,283 @@ func (r *GrafanaRule) ValidateGrafanaDashboards(m *module.Module, errorList *err errorList.WithFilePath(monitoringFilePath). Errorf("The content of the 'templates/monitoring.yaml' should be equal to:\n%s\nGot:\n%s", desiredContent, string(content)) } + +// validateDashboardFiles validates individual grafana dashboard files +func (r *GrafanaRule) validateDashboardFiles(m *module.Module, errorList *errors.LintRuleErrorsList) { + searchPath := filepath.Join(m.GetPath(), "monitoring", "grafana-dashboards") + + entries := fsutils.GetFiles(searchPath, true, fsutils.FilterFileByExtensions(".json", ".tpl")) + + for _, entry := range entries { + r.validateDashboardFile(entry, errorList) + } +} + +// validateDashboardFile validates a single grafana dashboard file +func (r *GrafanaRule) validateDashboardFile(filePath string, errorList *errors.LintRuleErrorsList) { + content, err := os.ReadFile(filePath) + if err != nil { + errorList.WithFilePath(filePath).Errorf("failed to read dashboard file: %s", err) + return + } + + // Parse JSON content + dashboard := gjson.ParseBytes(content) + if !dashboard.IsObject() { + errorList.WithFilePath(filePath).Error("dashboard file is not valid JSON") + return + } + + // Extract panels and templates + panels := r.extractDashboardPanels(&dashboard) + templates := r.extractDashboardTemplates(&dashboard) + + // Validate panels + for _, panel := range panels { + r.validatePanel(&panel, filePath, errorList) + } + + // Validate templates + r.validateTemplates(templates, filePath, errorList) +} + +// extractDashboardPanels extracts all panels from dashboard including nested ones +func (*GrafanaRule) extractDashboardPanels(dashboard *gjson.Result) []gjson.Result { + panels := make([]gjson.Result, 0) + + // Extract panels from rows + rows := dashboard.Get("rows").Array() + for _, row := range rows { + rowPanels := row.Get("panels").Array() + panels = append(panels, rowPanels...) + } + + // Extract direct panels + directPanels := dashboard.Get("panels").Array() + for _, panel := range directPanels { + panelType := panel.Get("type").String() + if panelType == "row" { + // Extract panels from row + rowPanels := panel.Get("panels").Array() + panels = append(panels, rowPanels...) + } else { + panels = append(panels, panel) + } + } + + return panels +} + +// extractDashboardTemplates extracts template variables from dashboard +func (*GrafanaRule) extractDashboardTemplates(dashboard *gjson.Result) []gjson.Result { + templating := dashboard.Get("templating") + if !templating.Exists() { + return []gjson.Result{} + } + + list := templating.Get("list") + if !list.Exists() || !list.IsArray() { + return []gjson.Result{} + } + + return list.Array() +} + +// validatePanel validates a single panel +func (r *GrafanaRule) validatePanel(panel *gjson.Result, filePath string, errorList *errors.LintRuleErrorsList) { + panelTitle := panel.Get("title").String() + if panelTitle == "" { + panelTitle = "unnamed" + } + + // Check deprecated panel types + r.checkDeprecatedPanelTypes(panel, panelTitle, filePath, errorList) + + // Check deprecated intervals + r.checkDeprecatedIntervals(panel, panelTitle, filePath, errorList) + + // Check legacy alert rules + r.checkLegacyAlertRules(panel, panelTitle, filePath, errorList) + + // Check datasource validation + r.checkDatasourceValidation(panel, panelTitle, filePath, errorList) +} + +// checkDeprecatedPanelTypes checks for deprecated panel types +func (*GrafanaRule) checkDeprecatedPanelTypes(panel *gjson.Result, panelTitle, filePath string, errorList *errors.LintRuleErrorsList) { + panelType := panel.Get("type").String() + deprecatedTypes := map[string]string{ + "graph": "timeseries", + "flant-statusmap-panel": "state-timeline", + } + + if replaceWith, isDeprecated := deprecatedTypes[panelType]; isDeprecated { + errorList.WithFilePath(filePath).Errorf( + "Panel '%s' uses deprecated type '%s', consider using '%s'", + panelTitle, panelType, replaceWith, + ) + } +} + +// checkDeprecatedIntervals checks for deprecated intervals in panel queries +func (*GrafanaRule) checkDeprecatedIntervals(panel *gjson.Result, panelTitle, filePath string, errorList *errors.LintRuleErrorsList) { + deprecatedIntervals := []string{"interval_rv", "interval_sx3", "interval_sx4"} + targets := panel.Get("targets").Array() + + for _, target := range targets { + expr := target.Get("expr").String() + for _, deprecatedInterval := range deprecatedIntervals { + if strings.Contains(expr, deprecatedInterval) { + errorList.WithFilePath(filePath).Errorf( + "Panel '%s' contains deprecated interval '%s', consider using '$__rate_interval'", + panelTitle, deprecatedInterval, + ) + } + } + } +} + +// checkLegacyAlertRules checks for legacy alert rules in panels +func (*GrafanaRule) checkLegacyAlertRules(panel *gjson.Result, panelTitle, filePath string, errorList *errors.LintRuleErrorsList) { + alertRule := panel.Get("alert") + if alertRule.Exists() { + alertRuleName := alertRule.Get("name").String() + if alertRuleName == "" { + alertRuleName = "unnamed" + } + errorList.WithFilePath(filePath).Errorf( + "Panel '%s' contains legacy alert rule '%s', consider using external alertmanager", + panelTitle, alertRuleName, + ) + } +} + +// checkDatasourceValidation checks datasource UIDs in panel targets +func (*GrafanaRule) checkDatasourceValidation(panel *gjson.Result, panelTitle, filePath string, errorList *errors.LintRuleErrorsList) { + recommendedPrometheusUIDs := []string{"$ds_prometheus", "${ds_prometheus}"} + targets := panel.Get("targets").Array() + + for _, target := range targets { + datasource := target.Get("datasource") + if !datasource.Exists() { + continue + } + + var uidStr string + uid := datasource.Get("uid") + if uid.Exists() { + uidStr = uid.String() + } else { + // Legacy format - datasource UID is stored as string + uidStr = datasource.String() + errorList.WithFilePath(filePath).Errorf( + "Panel '%s' uses legacy datasource format, consider resaving dashboard using newer Grafana version", + panelTitle, + ) + } + + // Check for hardcoded UIDs + if !strings.HasPrefix(uidStr, "$") { + errorList.WithFilePath(filePath).Errorf( + "Panel '%s' contains hardcoded datasource UID '%s', consider using grafana variable of type 'Datasource'", + panelTitle, uidStr, + ) + } + + // Check Prometheus datasource UIDs + datasourceType := datasource.Get("type") + if datasourceType.Exists() && datasourceType.String() == "prometheus" { + isRecommended := false + for _, recommendedUID := range recommendedPrometheusUIDs { + if uidStr == recommendedUID { + isRecommended = true + break + } + } + + if !isRecommended { + errorList.WithFilePath(filePath).Errorf( + "Panel '%s' datasource should be one of: %s instead of '%s'", + panelTitle, strings.Join(recommendedPrometheusUIDs, ", "), uidStr, + ) + } + } + } +} + +// validateTemplates validates dashboard template variables +func (r *GrafanaRule) validateTemplates(templates []gjson.Result, filePath string, errorList *errors.LintRuleErrorsList) { + hasPrometheusDatasourceVariable := false + recommendedPrometheusUIDs := []string{"$ds_prometheus", "${ds_prometheus}"} + + for _, template := range templates { + // Check for required Prometheus datasource variable + if r.isPrometheusDatasourceTemplateVariable(&template) { + hasPrometheusDatasourceVariable = true + } + + // Check query variables for non-recommended datasource UIDs + if r.isNonRecommendedPrometheusDatasourceQueryVariable(&template, recommendedPrometheusUIDs) { + templateName := template.Get("name").String() + errorList.WithFilePath(filePath).Errorf( + "Dashboard variable '%s' should use one of: %s as its datasource", + templateName, strings.Join(recommendedPrometheusUIDs, ", "), + ) + } + } + + // Check if required Prometheus datasource variable exists + if !hasPrometheusDatasourceVariable { + errorList.WithFilePath(filePath).Errorf( + "Dashboard must contain prometheus variable with query type: 'prometheus' and name: 'ds_prometheus'", + ) + } +} + +// isPrometheusDatasourceTemplateVariable checks if template is the required Prometheus datasource variable +func (*GrafanaRule) isPrometheusDatasourceTemplateVariable(template *gjson.Result) bool { + templateType := template.Get("type") + if !templateType.Exists() || templateType.String() != "datasource" { + return false + } + + queryType := template.Get("query") + if !queryType.Exists() || queryType.String() != "prometheus" { + return false + } + + templateName := template.Get("name") + return templateName.Exists() && templateName.String() == "ds_prometheus" +} + +// isNonRecommendedPrometheusDatasourceQueryVariable checks if query variable uses non-recommended datasource +func (*GrafanaRule) isNonRecommendedPrometheusDatasourceQueryVariable(template *gjson.Result, recommendedUIDs []string) bool { + templateType := template.Get("type") + if !templateType.Exists() || templateType.String() != "query" { + return false + } + + datasource := template.Get("datasource") + if !datasource.Exists() { + return false + } + + datasourceType := datasource.Get("type") + if !datasourceType.Exists() || datasourceType.String() != "prometheus" { + return false + } + + datasourceUID := datasource.Get("uid") + if !datasourceUID.Exists() { + return false + } + + uidStr := datasourceUID.String() + for _, recommendedUID := range recommendedUIDs { + if uidStr == recommendedUID { + return false + } + } + + return true +} diff --git a/pkg/linters/templates/rules/grafana_test.go b/pkg/linters/templates/rules/grafana_test.go new file mode 100644 index 00000000..f2be435f --- /dev/null +++ b/pkg/linters/templates/rules/grafana_test.go @@ -0,0 +1,295 @@ +/* +Copyright 2025 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/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/deckhouse/dmt/pkg/errors" +) + +func TestExtractDashboardPanels(t *testing.T) { + rule := &GrafanaRule{} + + // Test dashboard with direct panels + dashboard := gjson.Parse(`{ + "panels": [ + {"type": "graph", "title": "Panel 1"}, + {"type": "row", "panels": [ + {"type": "timeseries", "title": "Panel 2"} + ]}, + {"type": "stat", "title": "Panel 3"} + ] + }`) + + panels := rule.extractDashboardPanels(&dashboard) + assert.Len(t, panels, 3) + assert.Equal(t, "Panel 1", panels[0].Get("title").String()) + assert.Equal(t, "Panel 2", panels[1].Get("title").String()) + assert.Equal(t, "Panel 3", panels[2].Get("title").String()) +} + +func TestExtractDashboardTemplates(t *testing.T) { + rule := &GrafanaRule{} + + // Test dashboard with templates + dashboard := gjson.Parse(`{ + "templating": { + "list": [ + {"name": "ds_prometheus", "type": "datasource", "query": "prometheus"}, + {"name": "namespace", "type": "query"} + ] + } + }`) + + templates := rule.extractDashboardTemplates(&dashboard) + assert.Len(t, templates, 2) + assert.Equal(t, "ds_prometheus", templates[0].Get("name").String()) + assert.Equal(t, "namespace", templates[1].Get("name").String()) +} + +func TestCheckDeprecatedPanelTypes(t *testing.T) { + tests := []struct { + name string + panelType string + expectedWarns int + }{ + { + name: "deprecated graph panel", + panelType: "graph", + expectedWarns: 1, + }, + { + name: "deprecated flant-statusmap-panel", + panelType: "flant-statusmap-panel", + expectedWarns: 1, + }, + { + name: "modern timeseries panel", + panelType: "timeseries", + expectedWarns: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rule := &GrafanaRule{} + errorList := errors.NewLintRuleErrorsList() + + panel := gjson.Parse(`{"type": "` + tt.panelType + `", "title": "Test Panel"}`) + rule.checkDeprecatedPanelTypes(&panel, "Test Panel", "/test.json", errorList) + + assert.Len(t, errorList.GetErrors(), tt.expectedWarns) + }) + } +} + +func TestCheckDeprecatedIntervals(t *testing.T) { + tests := []struct { + name string + expr string + expectedWarns int + }{ + { + name: "deprecated interval_rv", + expr: "rate(metric{job=\"test\"}[interval_rv])", + expectedWarns: 1, + }, + { + name: "deprecated interval_sx3", + expr: "rate(metric{job=\"test\"}[interval_sx3])", + expectedWarns: 1, + }, + { + name: "modern interval", + expr: "rate(metric{job=\"test\"}[$__rate_interval])", + expectedWarns: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rule := &GrafanaRule{} + errorList := errors.NewLintRuleErrorsList() + + // Create JSON with proper escaping + jsonStr := fmt.Sprintf(`{ + "title": "Test Panel", + "targets": [{"expr": %q}] + }`, tt.expr) + + panel := gjson.Parse(jsonStr) + rule.checkDeprecatedIntervals(&panel, "Test Panel", "/test.json", errorList) + + assert.Len(t, errorList.GetErrors(), tt.expectedWarns) + }) + } +} + +func TestCheckLegacyAlertRules(t *testing.T) { + tests := []struct { + name string + hasAlert bool + alertName string + expectedWarns int + }{ + { + name: "has legacy alert rule", + hasAlert: true, + alertName: "test_alert", + expectedWarns: 1, + }, + { + name: "no alert rule", + hasAlert: false, + alertName: "", + expectedWarns: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rule := &GrafanaRule{} + errorList := errors.NewLintRuleErrorsList() + + var panel gjson.Result + if tt.hasAlert { + panel = gjson.Parse(`{ + "title": "Test Panel", + "alert": {"name": "` + tt.alertName + `"} + }`) + } else { + panel = gjson.Parse(`{"title": "Test Panel"}`) + } + + rule.checkLegacyAlertRules(&panel, "Test Panel", "/test.json", errorList) + + assert.Len(t, errorList.GetErrors(), tt.expectedWarns) + }) + } +} + +func TestCheckDatasourceValidation(t *testing.T) { + tests := []struct { + name string + datasource string + expectedWarns int + }{ + { + name: "recommended prometheus datasource", + datasource: `{"type": "prometheus", "uid": "$ds_prometheus"}`, + expectedWarns: 0, + }, + { + name: "hardcoded datasource UID", + datasource: `{"type": "prometheus", "uid": "prometheus-123"}`, + expectedWarns: 2, // hardcoded UID + non-recommended prometheus UID + }, + { + name: "legacy datasource format", + datasource: `"prometheus-123"`, + expectedWarns: 2, // legacy format + hardcoded UID + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rule := &GrafanaRule{} + errorList := errors.NewLintRuleErrorsList() + + panel := gjson.Parse(`{ + "title": "Test Panel", + "targets": [{"datasource": ` + tt.datasource + `}] + }`) + rule.checkDatasourceValidation(&panel, "Test Panel", "/test.json", errorList) + + assert.Len(t, errorList.GetErrors(), tt.expectedWarns) + }) + } +} + +func TestValidateTemplates(t *testing.T) { + tests := []struct { + name string + templates string + expectedWarns int + }{ + { + name: "has required prometheus datasource variable", + templates: `[{"name": "ds_prometheus", "type": "datasource", "query": "prometheus"}, {"name": "namespace", "type": "query"}]`, + expectedWarns: 0, + }, + { + name: "missing required prometheus datasource variable", + templates: `[{"name": "namespace", "type": "query"}]`, + expectedWarns: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rule := &GrafanaRule{} + errorList := errors.NewLintRuleErrorsList() + + templates := gjson.Parse(`{"templating": {"list": ` + tt.templates + `}}`) + templateList := rule.extractDashboardTemplates(&templates) + rule.validateTemplates(templateList, "/test.json", errorList) + + assert.Len(t, errorList.GetErrors(), tt.expectedWarns) + }) + } +} + +func TestValidateDashboardFile(t *testing.T) { + // Create temporary test directory + tempDir, err := os.MkdirTemp("", "grafana-test") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + // Create test dashboard file + dashboardContent := `{ + "panels": [ + {"type": "graph", "title": "Deprecated Panel"} + ], + "templating": { + "list": [ + {"name": "ds_prometheus", "type": "datasource", "query": "prometheus"} + ] + } + }` + + testFile := filepath.Join(tempDir, "test-dashboard.json") + err = os.WriteFile(testFile, []byte(dashboardContent), 0600) + require.NoError(t, err) + + rule := &GrafanaRule{} + + errorList := errors.NewLintRuleErrorsList() + rule.validateDashboardFile(testFile, errorList) + + // Should have warning about deprecated panel type + errorListResult := errorList.GetErrors() + assert.Len(t, errorListResult, 1) + assert.Contains(t, errorListResult[0].Text, "deprecated type 'graph'") +} From 533f99024e3025d3ceaae95ceed12aa96c6c83a2 Mon Sep 17 00:00:00 2001 From: Evsyukov Denis Date: Mon, 18 Aug 2025 15:58:51 +0300 Subject: [PATCH 02/11] chore: update gjson dependency in go.sum - Removed outdated gjson version 1.14.4. - Updated to the latest gjson version 1.18.0 for improved functionality and security. --- go.sum | 2 -- 1 file changed, 2 deletions(-) diff --git a/go.sum b/go.sum index ebffe182..0dcf64f7 100644 --- a/go.sum +++ b/go.sum @@ -364,8 +364,6 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= -github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= From ce0656a6698fc9f259a97f5e1fa65e8b8b91ae00 Mon Sep 17 00:00:00 2001 From: Sinelnikov Michail Date: Thu, 9 Oct 2025 14:58:39 +0300 Subject: [PATCH 03/11] fix Signed-off-by: Sinelnikov Michail --- pkg/linters/templates/rules/grafana.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/linters/templates/rules/grafana.go b/pkg/linters/templates/rules/grafana.go index c9bddbfc..6fd01319 100644 --- a/pkg/linters/templates/rules/grafana.go +++ b/pkg/linters/templates/rules/grafana.go @@ -21,7 +21,7 @@ import ( "k8s.io/utils/ptr" - "github.com/deckhouse/dmt/internal/fsutils" + "github.com/deckhouse/dmt/internal/fsutils" "github.com/deckhouse/dmt/internal/module" "github.com/deckhouse/dmt/pkg" "github.com/deckhouse/dmt/pkg/errors" From e01374375c6865ff382750c28785f179dc66fc5f Mon Sep 17 00:00:00 2001 From: Sinelnikov Michail Date: Mon, 27 Oct 2025 16:36:38 +0300 Subject: [PATCH 04/11] add feature flag for template Signed-off-by: Sinelnikov Michail --- internal/module/module.go | 32 +++++++++++++++++++----------- pkg/config.go | 1 + pkg/config/global/global.go | 19 +++++++++++++++++- pkg/config/linters_settings.go | 12 +++++++++++ pkg/linters/templates/templates.go | 18 ++++++++--------- 5 files changed, 60 insertions(+), 22 deletions(-) diff --git a/internal/module/module.go b/internal/module/module.go index ce99d750..88c060fd 100644 --- a/internal/module/module.go +++ b/internal/module/module.go @@ -184,6 +184,9 @@ func mapRuleSettings(linterSettings *pkg.LintersSettings, configSettings *config // Module rules (uses global rule config + local fallback) mapModuleRules(linterSettings, configSettings, globalConfig) + // Templates rules (uses global rule config + local fallback) + mapTemplatesRules(linterSettings, configSettings, globalConfig) + // Other linter rules (use local linter-level impact) mapSimpleLinterRules(linterSettings, configSettings) } @@ -232,6 +235,23 @@ func mapModuleRules(linterSettings *pkg.LintersSettings, configSettings *config. rules.LegacyReleaseFileRule.SetLevel(globalRules.LegacyReleaseFileRule.Impact, fallbackImpact) } +// mapTemplatesRules configures Templates linter rules +func mapTemplatesRules(linterSettings *pkg.LintersSettings, configSettings *config.LintersSettings, globalConfig *global.Linters) { + rules := &linterSettings.Templates.Rules + globalRules := &globalConfig.Templates.Rules + fallbackImpact := configSettings.Templates.Impact + + rules.VPARule.SetLevel(globalRules.VPARule.Impact, fallbackImpact) + rules.PDBRule.SetLevel(globalRules.PDBRule.Impact, fallbackImpact) + rules.IngressRule.SetLevel(globalRules.IngressRule.Impact, fallbackImpact) + rules.PrometheusRule.SetLevel(globalRules.PrometheusRule.Impact, fallbackImpact) + rules.GrafanaRule.SetLevel(globalRules.GrafanaRule.Impact, fallbackImpact) + rules.KubeRBACProxyRule.SetLevel(globalRules.KubeRBACProxyRule.Impact, fallbackImpact) + rules.ServicePortRule.SetLevel(globalRules.ServicePortRule.Impact, fallbackImpact) + rules.ClusterDomainRule.SetLevel(globalRules.ClusterDomainRule.Impact, fallbackImpact) + rules.RegistryRule.SetLevel(globalRules.RegistryRule.Impact, fallbackImpact) +} + // mapSimpleLinterRules configures rules that use linter-level impact without global overrides func mapSimpleLinterRules(linterSettings *pkg.LintersSettings, configSettings *config.LintersSettings) { // NoCyrillic rules @@ -244,18 +264,6 @@ func mapSimpleLinterRules(linterSettings *pkg.LintersSettings, configSettings *c linterSettings.OpenAPI.Rules.CRDsRule.SetLevel("", openAPIImpact) linterSettings.OpenAPI.Rules.KeysRule.SetLevel("", openAPIImpact) - // Templates rules - templatesImpact := configSettings.Templates.Impact - templates := &linterSettings.Templates.Rules - templates.VPARule.SetLevel("", templatesImpact) - templates.PDBRule.SetLevel("", templatesImpact) - templates.IngressRule.SetLevel("", templatesImpact) - templates.PrometheusRule.SetLevel("", templatesImpact) - templates.GrafanaRule.SetLevel("", templatesImpact) - templates.KubeRBACProxyRule.SetLevel("", templatesImpact) - templates.ServicePortRule.SetLevel("", templatesImpact) - templates.ClusterDomainRule.SetLevel("", templatesImpact) - // RBAC rules rbacImpact := configSettings.Rbac.Impact rbac := &linterSettings.RBAC.Rules diff --git a/pkg/config.go b/pkg/config.go index e8477d96..ef671362 100644 --- a/pkg/config.go +++ b/pkg/config.go @@ -134,6 +134,7 @@ type TemplatesLinterRules struct { KubeRBACProxyRule RuleConfig ServicePortRule RuleConfig ClusterDomainRule RuleConfig + RegistryRule RuleConfig } type PrometheusRuleSettings struct { diff --git a/pkg/config/global/global.go b/pkg/config/global/global.go index 581092ab..a6c7e0f0 100644 --- a/pkg/config/global/global.go +++ b/pkg/config/global/global.go @@ -31,7 +31,7 @@ type Linters struct { NoCyrillic LinterConfig `mapstructure:"no-cyrillic"` OpenAPI LinterConfig `mapstructure:"openapi"` Rbac LinterConfig `mapstructure:"rbac"` - Templates LinterConfig `mapstructure:"templates"` + Templates TemplatesLinterConfig `mapstructure:"templates"` Documentation DocumentationLinterConfig `mapstructure:"documentation"` } @@ -86,6 +86,23 @@ type ModuleLinterRules struct { LegacyReleaseFileRule RuleConfig `mapstructure:"legacy-release-file"` } +type TemplatesLinterConfig struct { + LinterConfig `mapstructure:",squash"` + Rules TemplatesLinterRules `mapstructure:"rules"` +} + +type TemplatesLinterRules struct { + VPARule RuleConfig `mapstructure:"vpa"` + PDBRule RuleConfig `mapstructure:"pdb"` + IngressRule RuleConfig `mapstructure:"ingress"` + 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"` +} + func (c LinterConfig) IsWarn() bool { return c.Impact == pkg.Warn.String() } diff --git a/pkg/config/linters_settings.go b/pkg/config/linters_settings.go index 71904683..1c955eee 100644 --- a/pkg/config/linters_settings.go +++ b/pkg/config/linters_settings.go @@ -187,10 +187,22 @@ type TemplatesSettings struct { ExcludeRules TemplatesExcludeRules `mapstructure:"exclude-rules"` GrafanaDashboards GrafanaDashboardsExcludeList `mapstructure:"grafana-dashboards"` PrometheusRules PrometheusRulesExcludeList `mapstructure:"prometheus-rules"` + Rules TemplatesLinterRules `mapstructure:"rules"` Impact string `mapstructure:"impact"` } +type TemplatesLinterRules struct { + VPARule RuleConfig `mapstructure:"vpa"` + PDBRule RuleConfig `mapstructure:"pdb"` + IngressRule RuleConfig `mapstructure:"ingress"` + 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"` +} + type TemplatesExcludeRules struct { VPAAbsent KindRuleExcludeList `mapstructure:"vpa"` PDBAbsent KindRuleExcludeList `mapstructure:"pdb"` diff --git a/pkg/linters/templates/templates.go b/pkg/linters/templates/templates.go index 1e21dac8..2a71a22d 100644 --- a/pkg/linters/templates/templates.go +++ b/pkg/linters/templates/templates.go @@ -54,9 +54,9 @@ func (l *Templates) Run(m *module.Module) { errorList := l.ErrorList.WithModule(m.GetName()) // VPA - rules.NewVPARule(l.cfg.ExcludeRules.VPAAbsent.Get()).ControllerMustHaveVPA(m, errorList) + rules.NewVPARule(l.cfg.ExcludeRules.VPAAbsent.Get()).ControllerMustHaveVPA(m, errorList.WithMaxLevel(l.cfg.Rules.VPARule.GetLevel())) // PDB - rules.NewPDBRule(l.cfg.ExcludeRules.PDBAbsent.Get()).ControllerMustHavePDB(m, errorList) + rules.NewPDBRule(l.cfg.ExcludeRules.PDBAbsent.Get()).ControllerMustHavePDB(m, errorList.WithMaxLevel(l.cfg.Rules.PDBRule.GetLevel())) // Ingress ingressRule := rules.NewIngressRule(l.cfg.ExcludeRules.Ingress.Get()) @@ -72,26 +72,26 @@ func (l *Templates) Run(m *module.Module) { } rules.NewKubeRbacProxyRule(l.cfg.ExcludeRules.KubeRBACProxy.Get()). - NamespaceMustContainKubeRBACProxyCA(m.GetObjectStore(), errorList) + NamespaceMustContainKubeRBACProxyCA(m.GetObjectStore(), errorList.WithMaxLevel(l.cfg.Rules.KubeRBACProxyRule.GetLevel())) servicePortRule := rules.NewServicePortRule(l.cfg.ExcludeRules.ServicePort.Get()) for _, object := range m.GetStorage() { - servicePortRule.ObjectServiceTargetPort(object, errorList) - prometheusRule.PromtoolRuleCheck(m, object, errorList) - ingressRule.CheckSnippetsRule(object, errorList) + servicePortRule.ObjectServiceTargetPort(object, errorList.WithMaxLevel(l.cfg.Rules.ServicePortRule.GetLevel())) + prometheusRule.PromtoolRuleCheck(m, object, errorList.WithMaxLevel(l.cfg.Rules.PrometheusRule.GetLevel())) + ingressRule.CheckSnippetsRule(object, errorList.WithMaxLevel(l.cfg.Rules.IngressRule.GetLevel())) } // Cluster domain rule clusterDomainRule := rules.NewClusterDomainRule() - clusterDomainRule.ValidateClusterDomainInTemplates(m, errorList) + clusterDomainRule.ValidateClusterDomainInTemplates(m, errorList.WithMaxLevel(l.cfg.Rules.ClusterDomainRule.GetLevel())) // werf file // The following line is commented out because the Werf rule validation is not currently required. // If needed in the future, uncomment and ensure the rule is properly configured. - // rules.NewWerfRule().ValidateWerfTemplates(m, errorList) + // rules.NewWerfRule().ValidateWerfTemplates(m, errorList.WithMaxLevel(l.cfg.Rules.WerfRule.GetLevel())) - rules.NewRegistryRule().CheckRegistrySecret(m, errorList) + rules.NewRegistryRule().CheckRegistrySecret(m, errorList.WithMaxLevel(l.cfg.Rules.RegistryRule.GetLevel())) } func (l *Templates) Name() string { From dd605212d01adc0c871726387ffc9444b38dbc9a Mon Sep 17 00:00:00 2001 From: Sinelnikov Michail Date: Mon, 27 Oct 2025 16:56:09 +0300 Subject: [PATCH 05/11] fix Signed-off-by: Sinelnikov Michail --- internal/metrics/metrics_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/metrics/metrics_test.go b/internal/metrics/metrics_test.go index 9e3013fb..4ae455e5 100644 --- a/internal/metrics/metrics_test.go +++ b/internal/metrics/metrics_test.go @@ -23,7 +23,7 @@ func Test_SetLinterWarningsMetrics_AddsWarningsForAllLinters(t *testing.T) { NoCyrillic: global.LinterConfig{Impact: pkg.Warn.String()}, OpenAPI: global.LinterConfig{Impact: pkg.Warn.String()}, Rbac: global.LinterConfig{Impact: pkg.Warn.String()}, - Templates: global.LinterConfig{Impact: pkg.Warn.String()}, + Templates: global.TemplatesLinterConfig{}, Documentation: global.DocumentationLinterConfig{}, }, } @@ -32,6 +32,7 @@ func Test_SetLinterWarningsMetrics_AddsWarningsForAllLinters(t *testing.T) { cfg.Linters.Images.Impact = pkg.Warn.String() cfg.Linters.Module.Impact = pkg.Warn.String() cfg.Linters.Documentation.Impact = pkg.Warn.String() + cfg.Linters.Templates.Impact = pkg.Warn.String() SetLinterWarningsMetrics(cfg) num, err := testutil.GatherAndCount(metrics.Gatherer, "dmt_linter_info") From 6eb1524151c5c378093d7c84e501484db79998be Mon Sep 17 00:00:00 2001 From: Sinelnikov Michail Date: Mon, 27 Oct 2025 18:00:20 +0300 Subject: [PATCH 06/11] fix readme Signed-off-by: Sinelnikov Michail --- pkg/linters/container/README.md | 40 --------------------------------- 1 file changed, 40 deletions(-) diff --git a/pkg/linters/container/README.md b/pkg/linters/container/README.md index 5b45e480..e8d373ca 100644 --- a/pkg/linters/container/README.md +++ b/pkg/linters/container/README.md @@ -1367,46 +1367,6 @@ linters-settings: container: # Global impact level for all container rules impact: error - - # Rule-specific severity levels - rules: - recommended-labels: - level: error - namespace-labels: - level: error - api-version: - level: error - priority-class: - level: error - dns-policy: - level: error - controller-security-context: - level: error - revision-history-limit: - level: warning - name-duplicates: - level: error - read-only-root-filesystem: - level: error - host-network-ports: - level: error - env-variables-duplicates: - level: error - image-digest: - level: error - image-pull-policy: - level: error - resources: - level: error - security-context: - level: error - ports: - level: error - liveness-probe: - level: error - readiness-probe: - level: error - # Exclude specific objects/containers from rules exclude-rules: read-only-root-filesystem: From 0169d08c7bde3423536baafe2e6fb2ab98ef6d9c Mon Sep 17 00:00:00 2001 From: Sinelnikov Michail Date: Mon, 27 Oct 2025 18:20:31 +0300 Subject: [PATCH 07/11] fix docs Signed-off-by: Sinelnikov Michail --- pkg/linters/container/README.md | 50 --------------- pkg/linters/docs/README.md | 106 -------------------------------- pkg/linters/images/README.md | 21 ------- pkg/linters/module/README.md | 26 -------- 4 files changed, 203 deletions(-) diff --git a/pkg/linters/container/README.md b/pkg/linters/container/README.md index e8d373ca..4fdf107f 100644 --- a/pkg/linters/container/README.md +++ b/pkg/linters/container/README.md @@ -85,17 +85,6 @@ metadata: heritage: deckhouse ``` -**Configuration:** - -```yaml -# .dmt.yaml -linters-settings: - container: - rules: - recommended-labels: - level: error # error | warning | info | ignored -``` - --- ### namespace-labels @@ -1260,45 +1249,6 @@ linters-settings: ## Configuration -The Container linter can be configured at both the module level and for individual rules. - -### Module-Level Settings - -Configure the overall impact level for the container linter: - -```yaml -# .dmt.yaml -linters-settings: - container: - impact: error # Options: error, warning, info, ignored -``` - -**Impact levels:** -- `error`: Violations fail the validation and return a non-zero exit code -- `warning`: Violations are reported but don't fail the validation -- `info`: Violations are reported as informational messages -- `ignored`: The linter is completely disabled - -### Rule-Level Settings - -Each rule can be individually configured with its own severity level: - -```yaml -# .dmt.yaml -linters-settings: - container: - impact: error - rules: - recommended-labels: - level: error # error | warning | info | ignored - api-version: - level: error - priority-class: - level: error - revision-history-limit: - level: warning -``` - ### Exclude Rules Many rules support excluding specific objects or containers: diff --git a/pkg/linters/docs/README.md b/pkg/linters/docs/README.md index 47f3e128..fdcfb6e8 100644 --- a/pkg/linters/docs/README.md +++ b/pkg/linters/docs/README.md @@ -89,15 +89,6 @@ This module provides... **Configuration:** -```yaml -# .dmt.yaml -linters-settings: - documentation: - rules: - readme: - level: error # error | warning | info | ignored -``` - To disable this rule for specific modules: ```yaml @@ -179,15 +170,6 @@ my-module/ **Configuration:** -```yaml -# .dmt.yaml -linters-settings: - documentation: - rules: - bilingual: - level: error # error | warning | info | ignored -``` - To disable bilingual checks for specific files: ```yaml @@ -303,15 +285,6 @@ Line 42: Check the документация for more details. **Configuration:** -```yaml -# .dmt.yaml -linters-settings: - documentation: - rules: - cyrillic-in-english: - level: error # error | warning | info | ignored -``` - To exclude specific files from this check: ```yaml @@ -326,43 +299,6 @@ linters-settings: ## Configuration -The Documentation linter can be configured at both the module level and for individual rules. - -### Module-Level Settings - -Configure the overall impact level for the documentation linter: - -```yaml -# .dmt.yaml -linters-settings: - documentation: - impact: error # Options: error, warning, info, ignored -``` - -**Impact levels:** -- `error`: Violations fail the validation and return a non-zero exit code -- `warning`: Violations are reported but don't fail the validation -- `info`: Violations are reported as informational messages -- `ignored`: The linter is completely disabled - -### Rule-Level Settings - -Each rule can be individually configured with its own severity level: - -```yaml -# .dmt.yaml -linters-settings: - documentation: - impact: error - rules: - readme: - level: error # error | warning | info | ignored - bilingual: - level: error - cyrillic-in-english: - level: warning # Less strict for this rule -``` - ### Path-Based Exclusions Exclude specific modules or files from validation: @@ -383,48 +319,6 @@ linters-settings: - docs/GLOSSARY.md # Technical terms document ``` -### Complete Configuration Example - -```yaml -# .dmt.yaml -linters-settings: - documentation: - # Global impact level for all documentation rules - impact: error - - # Rule-specific settings - rules: - readme: - level: error - exclude: - - experimental-module - - bilingual: - level: error - exclude: - - docs/INTERNAL.md - - docs/DEVELOPMENT.md - - cyrillic-in-english: - level: warning - exclude: - - docs/GLOSSARY.md -``` - -### Configuration in Module Directory - -You can also place a `.dmt.yaml` configuration file directly in your module directory for module-specific settings: - -```yaml -# modules/my-module/.dmt.yaml -linters-settings: - documentation: - impact: warning # More lenient for this specific module - rules: - bilingual: - level: info # Informational only for translations -``` - ## Common Issues ### Issue: Missing README.md diff --git a/pkg/linters/images/README.md b/pkg/linters/images/README.md index 02d908b0..64b141a3 100644 --- a/pkg/linters/images/README.md +++ b/pkg/linters/images/README.md @@ -356,27 +356,6 @@ linters-settings: werf: disable: false # Enable/disable werf validation - - # Overall impact level - impact: error # Level: error | warning | info -``` - -### Rule-Level Configuration - -Configure individual rule severity: - -```yaml -linters-settings: - images: - rules: - dockerfile: - level: error # error | warning | info | ignored - distroless: - level: error - werf: - level: error - patches: - level: warning ``` --- diff --git a/pkg/linters/module/README.md b/pkg/linters/module/README.md index efd32a92..77050eb0 100644 --- a/pkg/linters/module/README.md +++ b/pkg/linters/module/README.md @@ -372,32 +372,6 @@ linters-settings: - third-party/ - vendor/ - # Overall impact level - impact: error # Level: error | warning | info -``` - -### Rule-Level Configuration - -Configure individual rule severity: - -```yaml -linters-settings: - module: - rules: - definition-file: - level: error # error | warning | info | ignored - oss: - level: error - conversions: - level: warning - helmignore: - level: warning - license: - level: error - requirements: - level: error - legacy-release-file: - level: warning ``` --- From bf4f741afd6cd6f8f1afa85ef8286d4df308ae2b Mon Sep 17 00:00:00 2001 From: Sinelnikov Michail Date: Mon, 27 Oct 2025 18:29:18 +0300 Subject: [PATCH 08/11] fix Signed-off-by: Sinelnikov Michail --- pkg/config/linters_settings.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/config/linters_settings.go b/pkg/config/linters_settings.go index 1c955eee..81e31c42 100644 --- a/pkg/config/linters_settings.go +++ b/pkg/config/linters_settings.go @@ -201,6 +201,7 @@ type TemplatesLinterRules struct { KubeRBACProxyRule RuleConfig `mapstructure:"kube-rbac-proxy"` ServicePortRule RuleConfig `mapstructure:"service-port"` ClusterDomainRule RuleConfig `mapstructure:"cluster-domain"` + RegistryRule RuleConfig `mapstructure:"registry"` } type TemplatesExcludeRules struct { From 88a916ef3de837a9ab37433fa247a069dc5acdf6 Mon Sep 17 00:00:00 2001 From: Sinelnikov Michail Date: Mon, 27 Oct 2025 18:42:52 +0300 Subject: [PATCH 09/11] fix Signed-off-by: Sinelnikov Michail --- pkg/linters/container/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/linters/container/README.md b/pkg/linters/container/README.md index 4fdf107f..e6d42b12 100644 --- a/pkg/linters/container/README.md +++ b/pkg/linters/container/README.md @@ -87,6 +87,8 @@ metadata: --- +**Configuration:** + ### namespace-labels **Purpose:** Ensures Deckhouse namespaces (prefixed with `d8-`) have the Prometheus rules watcher label enabled. This allows Prometheus to automatically discover and apply monitoring rules for the namespace. From f9ce6a7b00b9103816747d4df685462e093d5e34 Mon Sep 17 00:00:00 2001 From: Sinelnikov Michail Date: Mon, 27 Oct 2025 18:45:25 +0300 Subject: [PATCH 10/11] fix Signed-off-by: Sinelnikov Michail --- pkg/linters/container/README.md | 19 +++++++++++++++++++ pkg/linters/docs/README.md | 19 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/pkg/linters/container/README.md b/pkg/linters/container/README.md index e6d42b12..bcb2f57f 100644 --- a/pkg/linters/container/README.md +++ b/pkg/linters/container/README.md @@ -1251,6 +1251,25 @@ linters-settings: ## Configuration +The Container linter can be configured at both the module level and for individual rules. + +### Module-Level Settings + +Configure the overall impact level for the container linter: + +```yaml +# .dmt.yaml +linters-settings: + container: + impact: error # Options: error, warning, info, ignored +``` + +**Impact levels:** +- `error`: Violations fail the validation and return a non-zero exit code +- `warning`: Violations are reported but don't fail the validation +- `info`: Violations are reported as informational messages +- `ignored`: The linter is completely disabled + ### Exclude Rules Many rules support excluding specific objects or containers: diff --git a/pkg/linters/docs/README.md b/pkg/linters/docs/README.md index fdcfb6e8..6c98d14b 100644 --- a/pkg/linters/docs/README.md +++ b/pkg/linters/docs/README.md @@ -299,6 +299,25 @@ linters-settings: ## Configuration +The Documentation linter can be configured at both the module level and for individual rules. + +### Module-Level Settings + +Configure the overall impact level for the documentation linter: + +```yaml +# .dmt.yaml +linters-settings: + documentation: + impact: error # Options: error, warning, info, ignored +``` + +**Impact levels:** +- `error`: Violations fail the validation and return a non-zero exit code +- `warning`: Violations are reported but don't fail the validation +- `info`: Violations are reported as informational messages +- `ignored`: The linter is completely disabled + ### Path-Based Exclusions Exclude specific modules or files from validation: From 9fbace774472e1fcc9520407f2c21d561464638d Mon Sep 17 00:00:00 2001 From: Sinelnikov Michail Date: Mon, 27 Oct 2025 18:48:44 +0300 Subject: [PATCH 11/11] back Signed-off-by: Sinelnikov Michail --- pkg/linters/images/README.md | 3 +++ pkg/linters/module/README.md | 2 ++ 2 files changed, 5 insertions(+) diff --git a/pkg/linters/images/README.md b/pkg/linters/images/README.md index 64b141a3..9cd0868b 100644 --- a/pkg/linters/images/README.md +++ b/pkg/linters/images/README.md @@ -356,6 +356,9 @@ linters-settings: werf: disable: false # Enable/disable werf validation + + # Overall impact level + impact: error # Level: error | warning | info ``` --- diff --git a/pkg/linters/module/README.md b/pkg/linters/module/README.md index 77050eb0..594e374d 100644 --- a/pkg/linters/module/README.md +++ b/pkg/linters/module/README.md @@ -372,6 +372,8 @@ linters-settings: - third-party/ - vendor/ + # Overall impact level + impact: error # Level: error | warning | info ``` ---