From 91219df0c86038f807126483e1596239aa8af4ed Mon Sep 17 00:00:00 2001 From: diyliv Date: Tue, 19 May 2026 21:46:11 +0300 Subject: [PATCH] require x-config-version in config-values.yaml when conversions exist Signed-off-by: diyliv --- pkg/linters/module/rules/conversions.go | 29 ++- pkg/linters/module/rules/conversions_test.go | 252 +++++++++++++++++++ pkg/testers/conversions/tester.go | 5 +- pkg/testers/conversions/tester_test.go | 30 +++ 4 files changed, 306 insertions(+), 10 deletions(-) create mode 100644 pkg/linters/module/rules/conversions_test.go diff --git a/pkg/linters/module/rules/conversions.go b/pkg/linters/module/rules/conversions.go index ad7d20ed..d33f890d 100644 --- a/pkg/linters/module/rules/conversions.go +++ b/pkg/linters/module/rules/conversions.go @@ -106,10 +106,6 @@ func (r *ConversionsRule) CheckConversions(modulePath string, errorList *errors. return } - if cv.ConfigVersion == 0 { - return - } - folder := filepath.Join(modulePath, conversionsFolder) stat, err := os.Stat(folder) @@ -120,9 +116,11 @@ func (r *ConversionsRule) CheckConversions(modulePath string, errorList *errors. return } - if os.IsNotExist(err) || !stat.IsDir() { - errorList.WithFilePath(conversionsFolder). - Error("Conversions folder is not exist") + if err != nil || !stat.IsDir() { + if cv.ConfigVersion > 0 { + errorList.WithFilePath(conversionsFolder). + Error("Conversions folder is not exist") + } return } @@ -164,13 +162,28 @@ func (r *ConversionsRule) CheckConversions(modulePath string, errorList *errors. }) if len(versions) == 0 { - errorList.Errorf("No versions in folder: %q", folder) + if cv.ConfigVersion > 0 { + errorList.Errorf("No versions in folder: %q", folder) + } + + return + } + + if cv.ConfigVersion == 0 { + errorList.WithFilePath(configValuesFile). + Error("x-config-version is not set in config-values.yaml, but conversions exist") return } slices.Sort(versions) + latestVersion := versions[len(versions)-1] + if cv.ConfigVersion != latestVersion { + errorList.WithFilePath(configValuesFile). + Errorf("x-config-version (%d) does not match latest conversion version (%d)", cv.ConfigVersion, latestVersion) + } + if versions[0] != 2 { errorList.Errorf("You need to start with version number: 2") } diff --git a/pkg/linters/module/rules/conversions_test.go b/pkg/linters/module/rules/conversions_test.go new file mode 100644 index 00000000..d832910e --- /dev/null +++ b/pkg/linters/module/rules/conversions_test.go @@ -0,0 +1,252 @@ +/* +Copyright 2026 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rules + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/deckhouse/dmt/pkg/errors" +) + +func TestNewConversionsRule(t *testing.T) { + tests := []struct { + name string + disable bool + expected bool + }{ + { + name: "enabled rule", + disable: false, + expected: true, + }, + { + name: "disabled rule", + disable: true, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rule := NewConversionsRule(tt.disable) + assert.Equal(t, ConversionsRuleName, rule.GetName()) + assert.Equal(t, tt.expected, rule.Enabled()) + }) + } +} + +func TestConversionsRule_CheckConversions(t *testing.T) { + tests := []struct { + name string + setup func(dir string) error + expectedErrors []string + }{ + { + name: "no config-values.yaml", + setup: func(dir string) error { + return nil + }, + expectedErrors: []string{}, + }, + { + name: "config-values with x-config-version 0 and no conversions folder", + setup: func(dir string) error { + return os.MkdirAll(filepath.Join(dir, "openapi"), 0755) + }, + expectedErrors: []string{}, + }, + { + name: "x-config-version set but no conversions folder", + setup: func(dir string) error { + openapiDir := filepath.Join(dir, "openapi") + + if err := os.MkdirAll(openapiDir, 0755); err != nil { + return err + } + + return os.WriteFile(filepath.Join(openapiDir, "config-values.yaml"), []byte("x-config-version: 2"), 0644) + }, + expectedErrors: []string{ + "Conversions folder is not exist", + }, + }, + { + name: "conversions exist but x-config-version is 0", + setup: func(dir string) error { + openapiDir := filepath.Join(dir, "openapi") + convDir := filepath.Join(openapiDir, "conversions") + + if err := os.MkdirAll(convDir, 0755); err != nil { + return err + } + + if err := os.WriteFile(filepath.Join(openapiDir, "config-values.yaml"), []byte("x-config-version: 0"), 0644); err != nil { + return err + } + + return os.WriteFile(filepath.Join(convDir, "v2.yaml"), []byte(`version: 2 +conversions: + - del(.auth.password) +description: + en: "v2" + ru: "v2 ru"`), 0644) + }, + expectedErrors: []string{ + "x-config-version is not set in config-values.yaml, but conversions exist", + }, + }, + { + name: "x-config-version does not match latest conversion version", + setup: func(dir string) error { + openapiDir := filepath.Join(dir, "openapi") + convDir := filepath.Join(openapiDir, "conversions") + + if err := os.MkdirAll(convDir, 0755); err != nil { + return err + } + + if err := os.WriteFile(filepath.Join(openapiDir, "config-values.yaml"), []byte("x-config-version: 5"), 0644); err != nil { + return err + } + + return os.WriteFile(filepath.Join(convDir, "v2.yaml"), []byte(`version: 2 +conversions: + - del(.auth.password) +description: + en: "v2" + ru: "v2 ru"`), 0644) + }, + expectedErrors: []string{ + "x-config-version (5) does not match latest conversion version (2)", + }, + }, + { + name: "valid conversions with matching x-config-version", + setup: func(dir string) error { + openapiDir := filepath.Join(dir, "openapi") + convDir := filepath.Join(openapiDir, "conversions") + + if err := os.MkdirAll(convDir, 0755); err != nil { + return err + } + + if err := os.WriteFile(filepath.Join(openapiDir, "config-values.yaml"), []byte("x-config-version: 2"), 0644); err != nil { + return err + } + + return os.WriteFile(filepath.Join(convDir, "v2.yaml"), []byte(`version: 2 +conversions: + - del(.auth.password) +description: + en: "v2" + ru: "v2 ru"`), 0644) + }, + expectedErrors: []string{}, + }, + { + name: "conversions not starting from version 2", + setup: func(dir string) error { + openapiDir := filepath.Join(dir, "openapi") + convDir := filepath.Join(openapiDir, "conversions") + + if err := os.MkdirAll(convDir, 0755); err != nil { + return err + } + + if err := os.WriteFile(filepath.Join(openapiDir, "config-values.yaml"), []byte("x-config-version: 3"), 0644); err != nil { + return err + } + + if err := os.WriteFile(filepath.Join(convDir, "v3.yaml"), []byte(`version: 3 +conversions: + - del(.auth.password) +description: + en: "v3" + ru: "v3 ru"`), 0644); err != nil { + return err + } + + return nil + }, + expectedErrors: []string{ + "You need to start with version number: 2", + }, + }, + { + name: "non-sequential conversion versions", + setup: func(dir string) error { + openapiDir := filepath.Join(dir, "openapi") + convDir := filepath.Join(openapiDir, "conversions") + + if err := os.MkdirAll(convDir, 0755); err != nil { + return err + } + + if err := os.WriteFile(filepath.Join(openapiDir, "config-values.yaml"), []byte("x-config-version: 4"), 0644); err != nil { + return err + } + + if err := os.WriteFile(filepath.Join(convDir, "v2.yaml"), []byte(`version: 2 +conversions: + - del(.auth.password) +description: + en: "v2" + ru: "v2 ru"`), 0644); err != nil { + return err + } + + return os.WriteFile(filepath.Join(convDir, "v4.yaml"), []byte(`version: 4 +conversions: + - del(.auth) +description: + en: "v4" + ru: "v4 ru"`), 0644) + }, + expectedErrors: []string{ + "No sequential versions between 4 and 2", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tempDir := t.TempDir() + + err := tt.setup(tempDir) + require.NoError(t, err) + + rule := NewConversionsRule(false) + errorList := errors.NewLintRuleErrorsList() + + rule.CheckConversions(tempDir, errorList) + + errs := errorList.GetErrors() + assert.Len(t, errs, len(tt.expectedErrors), "Expected %d errors, got %d", len(tt.expectedErrors), len(errs)) + + for i, expectedError := range tt.expectedErrors { + if i < len(errs) { + assert.Contains(t, errs[i].Text, expectedError) + } + } + }) + } +} diff --git a/pkg/testers/conversions/tester.go b/pkg/testers/conversions/tester.go index cf1940e5..c8bfa112 100644 --- a/pkg/testers/conversions/tester.go +++ b/pkg/testers/conversions/tester.go @@ -79,7 +79,7 @@ func (t *Tester) Run(modulePath string) bool { return true } - if latestVersion > 0 && configVersion != latestVersion { + if configVersion > 0 && latestVersion > 0 && configVersion != latestVersion { errorList.WithTestName("config values version mismatch"). AddTestResult("x-config-version with latest conversion version mismatch", strconv.Itoa(latestVersion), @@ -113,7 +113,8 @@ func (t *Tester) checkConversions(modulePath string, errorList *pkgerrors.TestEr } if configVersion == 0 { - return "", 0, false + errorList.Errorf("x-config-version is not set in config-values.yaml, but conversions exist") + return convFolder, 0, true } return convFolder, configVersion, true diff --git a/pkg/testers/conversions/tester_test.go b/pkg/testers/conversions/tester_test.go index 149cc13c..6587d5b2 100644 --- a/pkg/testers/conversions/tester_test.go +++ b/pkg/testers/conversions/tester_test.go @@ -360,6 +360,36 @@ description: assert.Contains(t, errorList.GetErrors()[0].Text, "x-config-version") } +func TestTester_ConversionsExistButConfigVersionZero(t *testing.T) { + tempDir := t.TempDir() + + err := os.MkdirAll(filepath.Join(tempDir, "openapi", "conversions"), 0755) + require.NoError(t, err) + + err = os.WriteFile( + filepath.Join(tempDir, "openapi", "config-values.yaml"), + []byte("x-config-version: 0"), + 0644, + ) + require.NoError(t, err) + + v2 := `version: 2 +conversions: + - del(.auth.password) +description: + en: "v2" + ru: "v2 ru"` + err = os.WriteFile(filepath.Join(tempDir, "openapi", "conversions", "v2.yaml"), []byte(v2), 0644) + require.NoError(t, err) + + errorList := pkgerrors.NewTestErrorsList() + tester := New(errorList) + tester.Run(tempDir) + + assert.True(t, errorList.ContainsErrors()) + assert.Contains(t, errorList.GetErrors()[0].Text, "x-config-version is not set") +} + func TestConversionsTester_ReportsTestcaseFailure(t *testing.T) { tmpDir := t.TempDir()