diff --git a/pkg/lint2/discovery.go b/pkg/lint2/discovery.go index 639bc1a7e..ad34e30b8 100644 --- a/pkg/lint2/discovery.go +++ b/pkg/lint2/discovery.go @@ -174,6 +174,44 @@ func hasKind(path string, kind string) (bool, error) { return false, nil } +// hasAPIVersionKind checks if a YAML file contains a specific apiVersion and kind. +// Handles multi-document YAML files properly using yaml.NewDecoder. +// For files with syntax errors, falls back to simple regex matching to detect both apiVersion and kind. +func hasAPIVersionKind(path string, apiVersion string, kind string) (bool, error) { + data, err := os.ReadFile(path) + if err != nil { + return false, err + } + + decoder := yaml.NewDecoder(bytes.NewReader(data)) + + for { + var doc struct { + Kind string `yaml:"kind"` + APIVersion string `yaml:"apiVersion"` + } + + err := decoder.Decode(&doc) + if err != nil { + if err == io.EOF { + break + } + // Parse error - fall back to regex matching + kindPattern := fmt.Sprintf(`(?m)^kind:\s+%s\s*$`, regexp.QuoteMeta(kind)) + apiPattern := fmt.Sprintf(`(?m)^apiVersion:\s+%s\s*$`, regexp.QuoteMeta(apiVersion)) + kindMatched, _ := regexp.Match(kindPattern, data) + apiMatched, _ := regexp.Match(apiPattern, data) + return kindMatched && apiMatched, nil + } + + if doc.Kind == kind && doc.APIVersion == apiVersion { + return true, nil + } + } + + return false, nil +} + // discoverPreflightPaths discovers Preflight spec files from a glob pattern. // This is a thin wrapper around discoverYAMLsByKind for backward compatibility. // diff --git a/pkg/lint2/discovery_test.go b/pkg/lint2/discovery_test.go index 25acf7098..303bda5ff 100644 --- a/pkg/lint2/discovery_test.go +++ b/pkg/lint2/discovery_test.go @@ -2676,3 +2676,143 @@ func TestDiscoverSupportBundlePaths_ExplicitBypass(t *testing.T) { t.Errorf("Expected bundle in dist/, got: %s", bundles[0]) } } + +// Phase 9 Tests: hasAPIVersionKind + +func TestHasAPIVersionKind_ECConfigMatch(t *testing.T) { + tmpDir := t.TempDir() + path := filepath.Join(tmpDir, "ec.yaml") + content := `apiVersion: embeddedcluster.replicated.com/v1beta1 +kind: Config +metadata: + name: ec +` + if err := os.WriteFile(path, []byte(content), 0644); err != nil { + t.Fatal(err) + } + + got, err := hasAPIVersionKind(path, "embeddedcluster.replicated.com/v1beta1", "Config") + if err != nil { + t.Fatalf("hasAPIVersionKind() error = %v", err) + } + if !got { + t.Errorf("hasAPIVersionKind() = false, want true for EC Config") + } +} + +func TestHasAPIVersionKind_KOTSConfigMismatch(t *testing.T) { + tmpDir := t.TempDir() + path := filepath.Join(tmpDir, "config.yaml") + content := `apiVersion: kots.io/v1beta1 +kind: Config +metadata: + name: config +` + if err := os.WriteFile(path, []byte(content), 0644); err != nil { + t.Fatal(err) + } + + got, err := hasAPIVersionKind(path, "embeddedcluster.replicated.com/v1beta1", "Config") + if err != nil { + t.Fatalf("hasAPIVersionKind() error = %v", err) + } + if got { + t.Errorf("hasAPIVersionKind() = true, want false for KOTS Config") + } +} + +func TestHasAPIVersionKind_MultiDocumentWithECConfig(t *testing.T) { + tmpDir := t.TempDir() + path := filepath.Join(tmpDir, "multi.yaml") + content := `apiVersion: v1 +kind: ConfigMap +metadata: + name: cm +--- +apiVersion: embeddedcluster.replicated.com/v1beta1 +kind: Config +metadata: + name: ec +` + if err := os.WriteFile(path, []byte(content), 0644); err != nil { + t.Fatal(err) + } + + got, err := hasAPIVersionKind(path, "embeddedcluster.replicated.com/v1beta1", "Config") + if err != nil { + t.Fatalf("hasAPIVersionKind() error = %v", err) + } + if !got { + t.Errorf("hasAPIVersionKind() = false, want true for multi-doc with EC Config") + } +} + +func TestHasAPIVersionKind_MultiDocumentWithoutECConfig(t *testing.T) { + tmpDir := t.TempDir() + path := filepath.Join(tmpDir, "multi.yaml") + content := `apiVersion: v1 +kind: ConfigMap +metadata: + name: cm +--- +apiVersion: kots.io/v1beta1 +kind: Config +metadata: + name: config +` + if err := os.WriteFile(path, []byte(content), 0644); err != nil { + t.Fatal(err) + } + + got, err := hasAPIVersionKind(path, "embeddedcluster.replicated.com/v1beta1", "Config") + if err != nil { + t.Fatalf("hasAPIVersionKind() error = %v", err) + } + if got { + t.Errorf("hasAPIVersionKind() = true, want false for multi-doc without EC Config") + } +} + +func TestHasAPIVersionKind_EmptyFile(t *testing.T) { + tmpDir := t.TempDir() + path := filepath.Join(tmpDir, "empty.yaml") + if err := os.WriteFile(path, []byte(""), 0644); err != nil { + t.Fatal(err) + } + + got, err := hasAPIVersionKind(path, "embeddedcluster.replicated.com/v1beta1", "Config") + if err != nil { + t.Fatalf("hasAPIVersionKind() error = %v", err) + } + if got { + t.Errorf("hasAPIVersionKind() = true, want false for empty file") + } +} + +func TestHasAPIVersionKind_InvalidYAMLRegexFallback(t *testing.T) { + tmpDir := t.TempDir() + path := filepath.Join(tmpDir, "invalid.yaml") + content := `apiVersion: embeddedcluster.replicated.com/v1beta1 +kind: Config +metadata: + name: ec +spec: + version: 3.0.0 + extensions: + helmCharts: + - chart: + name: my-app + chartVersion: 1.2.3 +` + if err := os.WriteFile(path, []byte(content), 0644); err != nil { + t.Fatal(err) + } + + got, err := hasAPIVersionKind(path, "embeddedcluster.replicated.com/v1beta1", "Config") + if err != nil { + t.Fatalf("hasAPIVersionKind() error = %v", err) + } + if !got { + t.Errorf("hasAPIVersionKind() = false, want true for valid EC Config") + } +} diff --git a/pkg/lint2/helmchart.go b/pkg/lint2/helmchart.go index 70a606d8d..01410fce1 100644 --- a/pkg/lint2/helmchart.go +++ b/pkg/lint2/helmchart.go @@ -121,6 +121,22 @@ func DiscoverHelmChartManifests(manifestGlobs []string) (map[string]*HelmChartMa } } + // Discover EC Config helm charts and merge + ecHelmCharts, err := DiscoverECConfigHelmCharts(manifestGlobs) + if err != nil { + return nil, err + } + for key, manifest := range ecHelmCharts { + if existing, found := helmCharts[key]; found { + return nil, &DuplicateHelmChartError{ + ChartKey: key, + FirstFile: existing.FilePath, + SecondFile: manifest.FilePath, + } + } + helmCharts[key] = manifest + } + // Return empty map if no HelmCharts found - validation layer will check if charts need HelmCharts // Discovery is lenient - validation happens later in the flow if len(helmCharts) == 0 { @@ -136,6 +152,71 @@ func isHelmChartManifest(path string) (bool, error) { return hasKind(path, "HelmChart") } +// DiscoverECConfigHelmCharts scans manifest glob patterns and extracts helm chart +// declarations from embeddedcluster.replicated.com/v1beta1 Config manifests. +// It returns a map keyed by "name:chartVersion" for efficient lookup during validation. +// +// Silently skips: +// - Files that can't be read +// - Files that aren't valid YAML +// - Files that don't contain an EC Config with extensions.helmCharts +// - Hidden directories (.git, .github, etc.) +func DiscoverECConfigHelmCharts(manifestGlobs []string) (map[string]*HelmChartManifest, error) { + if len(manifestGlobs) == 0 { + return make(map[string]*HelmChartManifest), nil + } + + helmCharts := make(map[string]*HelmChartManifest) + seenFiles := make(map[string]bool) + + for _, pattern := range manifestGlobs { + matches, err := GlobFiles(pattern) + if err != nil { + return nil, fmt.Errorf("failed to expand manifest pattern %s: %w", pattern, err) + } + + for _, path := range matches { + if isHiddenPath(path) { + continue + } + if seenFiles[path] { + continue + } + seenFiles[path] = true + + // Quick apiVersion+kind check before parsing to avoid matching KOTS Config + isECConfig, err := hasAPIVersionKind(path, ecConfigAPIVersion, "Config") + if err != nil || !isECConfig { + continue + } + + // Parse EC Config helm charts + manifests, err := parseECConfigHelmCharts(path) + if err != nil { + continue + } + + for _, manifest := range manifests { + key := fmt.Sprintf("%s:%s", manifest.Name, manifest.ChartVersion) + if existing, found := helmCharts[key]; found { + return nil, &DuplicateHelmChartError{ + ChartKey: key, + FirstFile: existing.FilePath, + SecondFile: manifest.FilePath, + } + } + helmCharts[key] = manifest + } + } + } + + if len(helmCharts) == 0 { + return make(map[string]*HelmChartManifest), nil + } + + return helmCharts, nil +} + // parseHelmChartManifest parses a HelmChart manifest and extracts the fields needed for preflight rendering. // Accepts any apiVersion (validation happens in the linter). // @@ -150,25 +231,23 @@ func parseHelmChartManifest(path string) (*HelmChartManifest, error) { return nil, fmt.Errorf("failed to read file: %w", err) } - // Parse the full HelmChart structure - // Support both v1beta1 and v1beta2 - they have the same structure for fields we need - var helmChart struct { - APIVersion string `yaml:"apiVersion"` - Kind string `yaml:"kind"` - Spec struct { - Chart struct { - Name string `yaml:"name"` - ChartVersion string `yaml:"chartVersion"` - } `yaml:"chart"` - Builder map[string]interface{} `yaml:"builder"` - } `yaml:"spec"` - } - // Use yaml.NewDecoder to handle multi-document files decoder := yaml.NewDecoder(bytes.NewReader(data)) // Find the first HelmChart document for { + var helmChart struct { + APIVersion string `yaml:"apiVersion"` + Kind string `yaml:"kind"` + Spec struct { + Chart struct { + Name string `yaml:"name"` + ChartVersion string `yaml:"chartVersion"` + } `yaml:"chart"` + Builder map[string]interface{} `yaml:"builder"` + } `yaml:"spec"` + } + err := decoder.Decode(&helmChart) if err != nil { if err == io.EOF { @@ -178,26 +257,79 @@ func parseHelmChartManifest(path string) (*HelmChartManifest, error) { } if helmChart.Kind == "HelmChart" { - break + // Validate required fields + if helmChart.Spec.Chart.Name == "" { + return nil, fmt.Errorf("spec.chart.name is required but not found") + } + if helmChart.Spec.Chart.ChartVersion == "" { + return nil, fmt.Errorf("spec.chart.chartVersion is required but not found") + } + + // Note: We don't validate apiVersion here - discovery is permissive. + // The preflight linter will validate apiVersion when it processes the HelmChart. + // This allows future apiVersions to work without code changes. + + return &HelmChartManifest{ + Name: helmChart.Spec.Chart.Name, + ChartVersion: helmChart.Spec.Chart.ChartVersion, + BuilderValues: helmChart.Spec.Builder, // Can be nil or empty - that's valid + FilePath: path, + }, nil } } +} - // Validate required fields - if helmChart.Spec.Chart.Name == "" { - return nil, fmt.Errorf("spec.chart.name is required but not found") - } - if helmChart.Spec.Chart.ChartVersion == "" { - return nil, fmt.Errorf("spec.chart.chartVersion is required but not found") +// parseECConfigHelmCharts reads a YAML file and extracts helm chart declarations from +// any embeddedcluster.replicated.com/v1beta1 Config documents. +// It returns a slice of HelmChartManifest (without BuilderValues, as EC Config doesn't have them). +func parseECConfigHelmCharts(path string) ([]*HelmChartManifest, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("failed to read file: %w", err) } - // Note: We don't validate apiVersion here - discovery is permissive. - // The preflight linter will validate apiVersion when it processes the HelmChart. - // This allows future apiVersions to work without code changes. + decoder := yaml.NewDecoder(bytes.NewReader(data)) + var manifests []*HelmChartManifest + + for { + var ecConfig struct { + APIVersion string `yaml:"apiVersion"` + Kind string `yaml:"kind"` + Spec struct { + Extensions struct { + HelmCharts []struct { + Chart struct { + Name string `yaml:"name"` + ChartVersion string `yaml:"chartVersion"` + } `yaml:"chart"` + } `yaml:"helmCharts"` + } `yaml:"extensions"` + } `yaml:"spec"` + } + + err := decoder.Decode(&ecConfig) + if err != nil { + if err == io.EOF { + break + } + return nil, fmt.Errorf("failed to parse YAML: %w", err) + } + + if ecConfig.Kind != "Config" || ecConfig.APIVersion != ecConfigAPIVersion { + continue + } + + for _, hc := range ecConfig.Spec.Extensions.HelmCharts { + if hc.Chart.Name == "" || hc.Chart.ChartVersion == "" { + continue + } + manifests = append(manifests, &HelmChartManifest{ + Name: hc.Chart.Name, + ChartVersion: hc.Chart.ChartVersion, + FilePath: path, + }) + } + } - return &HelmChartManifest{ - Name: helmChart.Spec.Chart.Name, - ChartVersion: helmChart.Spec.Chart.ChartVersion, - BuilderValues: helmChart.Spec.Builder, // Can be nil or empty - that's valid - FilePath: path, - }, nil + return manifests, nil } diff --git a/pkg/lint2/helmchart_test.go b/pkg/lint2/helmchart_test.go index 447526dd2..5ac258ec4 100644 --- a/pkg/lint2/helmchart_test.go +++ b/pkg/lint2/helmchart_test.go @@ -754,3 +754,232 @@ spec: } }) } + +func TestDiscoverECConfigHelmCharts(t *testing.T) { + t.Run("empty manifests list returns empty map", func(t *testing.T) { + manifests, err := DiscoverECConfigHelmCharts([]string{}) + if err != nil { + t.Fatalf("unexpected error for empty manifests list: %v", err) + } + if manifests == nil { + t.Fatal("expected non-nil map, got nil") + } + if len(manifests) != 0 { + t.Errorf("expected empty map, got %d manifests", len(manifests)) + } + }) + + t.Run("single valid EC Config helm chart", func(t *testing.T) { + tmpDir := t.TempDir() + ecFile := filepath.Join(tmpDir, "ec.yaml") + content := `apiVersion: embeddedcluster.replicated.com/v1beta1 +kind: Config +metadata: + name: ec +spec: + version: 3.0.0 + extensions: + helmCharts: + - chart: + name: my-app + chartVersion: 1.2.3 +` + if err := os.WriteFile(ecFile, []byte(content), 0644); err != nil { + t.Fatal(err) + } + + pattern := filepath.Join(tmpDir, "*.yaml") + manifests, err := DiscoverECConfigHelmCharts([]string{pattern}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if len(manifests) != 1 { + t.Fatalf("expected 1 manifest, got %d", len(manifests)) + } + + key := "my-app:1.2.3" + manifest, found := manifests[key] + if !found { + t.Fatalf("expected manifest with key %q not found", key) + } + if manifest.Name != "my-app" { + t.Errorf("expected name 'my-app', got %q", manifest.Name) + } + if manifest.ChartVersion != "1.2.3" { + t.Errorf("expected chartVersion '1.2.3', got %q", manifest.ChartVersion) + } + if manifest.FilePath != ecFile { + t.Errorf("expected filePath %q, got %q", ecFile, manifest.FilePath) + } + }) + + t.Run("multiple EC Config helm charts", func(t *testing.T) { + tmpDir := t.TempDir() + ecFile := filepath.Join(tmpDir, "ec.yaml") + content := `apiVersion: embeddedcluster.replicated.com/v1beta1 +kind: Config +metadata: + name: ec +spec: + version: 3.0.0 + extensions: + helmCharts: + - chart: + name: app-one + chartVersion: 1.0.0 + - chart: + name: app-two + chartVersion: 2.0.0 +` + if err := os.WriteFile(ecFile, []byte(content), 0644); err != nil { + t.Fatal(err) + } + + pattern := filepath.Join(tmpDir, "*.yaml") + manifests, err := DiscoverECConfigHelmCharts([]string{pattern}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if len(manifests) != 2 { + t.Fatalf("expected 2 manifests, got %d", len(manifests)) + } + + if _, found := manifests["app-one:1.0.0"]; !found { + t.Error("expected app-one:1.0.0 not found") + } + if _, found := manifests["app-two:2.0.0"]; !found { + t.Error("expected app-two:2.0.0 not found") + } + }) + + t.Run("duplicate EC Config helm charts returns error", func(t *testing.T) { + tmpDir := t.TempDir() + + ecFile1 := filepath.Join(tmpDir, "ec1.yaml") + content1 := `apiVersion: embeddedcluster.replicated.com/v1beta1 +kind: Config +metadata: + name: ec1 +spec: + version: 3.0.0 + extensions: + helmCharts: + - chart: + name: my-app + chartVersion: 1.0.0 +` + if err := os.WriteFile(ecFile1, []byte(content1), 0644); err != nil { + t.Fatal(err) + } + + ecFile2 := filepath.Join(tmpDir, "ec2.yaml") + content2 := `apiVersion: embeddedcluster.replicated.com/v1beta1 +kind: Config +metadata: + name: ec2 +spec: + version: 3.0.0 + extensions: + helmCharts: + - chart: + name: my-app + chartVersion: 1.0.0 +` + if err := os.WriteFile(ecFile2, []byte(content2), 0644); err != nil { + t.Fatal(err) + } + + pattern := filepath.Join(tmpDir, "*.yaml") + _, err := DiscoverECConfigHelmCharts([]string{pattern}) + if err == nil { + t.Fatal("expected error for duplicate EC Config helm chart, got nil") + } + + dupErr, ok := err.(*DuplicateHelmChartError) + if !ok { + t.Fatalf("expected DuplicateHelmChartError, got %T", err) + } + if dupErr.ChartKey != "my-app:1.0.0" { + t.Errorf("expected ChartKey 'my-app:1.0.0', got %q", dupErr.ChartKey) + } + }) + + t.Run("non-EC Config files skipped", func(t *testing.T) { + tmpDir := t.TempDir() + + kotsConfig := filepath.Join(tmpDir, "config.yaml") + kotsContent := `apiVersion: kots.io/v1beta1 +kind: Config +metadata: + name: config +spec: + groups: [] +` + if err := os.WriteFile(kotsConfig, []byte(kotsContent), 0644); err != nil { + t.Fatal(err) + } + + pattern := filepath.Join(tmpDir, "*.yaml") + manifests, err := DiscoverECConfigHelmCharts([]string{pattern}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(manifests) != 0 { + t.Errorf("expected 0 manifests, got %d", len(manifests)) + } + }) + + t.Run("merged with kots HelmCharts in DiscoverHelmChartManifests", func(t *testing.T) { + tmpDir := t.TempDir() + + helmChartFile := filepath.Join(tmpDir, "helmchart.yaml") + helmContent := `apiVersion: kots.io/v1beta1 +kind: HelmChart +metadata: + name: kots-chart +spec: + chart: + name: kots-app + chartVersion: 1.0.0 +` + if err := os.WriteFile(helmChartFile, []byte(helmContent), 0644); err != nil { + t.Fatal(err) + } + + ecFile := filepath.Join(tmpDir, "ec.yaml") + ecContent := `apiVersion: embeddedcluster.replicated.com/v1beta1 +kind: Config +metadata: + name: ec +spec: + version: 3.0.0 + extensions: + helmCharts: + - chart: + name: ec-app + chartVersion: 2.0.0 +` + if err := os.WriteFile(ecFile, []byte(ecContent), 0644); err != nil { + t.Fatal(err) + } + + pattern := filepath.Join(tmpDir, "*.yaml") + manifests, err := DiscoverHelmChartManifests([]string{pattern}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if len(manifests) != 2 { + t.Fatalf("expected 2 manifests (1 kots + 1 EC), got %d", len(manifests)) + } + + if _, found := manifests["kots-app:1.0.0"]; !found { + t.Error("expected kots-app:1.0.0 not found") + } + if _, found := manifests["ec-app:2.0.0"]; !found { + t.Error("expected ec-app:2.0.0 not found") + } + }) +} diff --git a/pkg/lint2/testdata/validation/scenario-7-ec-config-helmcharts/.replicated b/pkg/lint2/testdata/validation/scenario-7-ec-config-helmcharts/.replicated new file mode 100644 index 000000000..41bcca854 --- /dev/null +++ b/pkg/lint2/testdata/validation/scenario-7-ec-config-helmcharts/.replicated @@ -0,0 +1,4 @@ +charts: + - path: ./chart +manifests: + - ./manifests/*.yaml diff --git a/pkg/lint2/testdata/validation/scenario-7-ec-config-helmcharts/chart/Chart.yaml b/pkg/lint2/testdata/validation/scenario-7-ec-config-helmcharts/chart/Chart.yaml new file mode 100644 index 000000000..3dafca10c --- /dev/null +++ b/pkg/lint2/testdata/validation/scenario-7-ec-config-helmcharts/chart/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v2 +name: ec-app +version: 1.0.0 +description: Test chart for EC Config validation scenario diff --git a/pkg/lint2/testdata/validation/scenario-7-ec-config-helmcharts/chart/values.yaml b/pkg/lint2/testdata/validation/scenario-7-ec-config-helmcharts/chart/values.yaml new file mode 100644 index 000000000..d6c93d3e8 --- /dev/null +++ b/pkg/lint2/testdata/validation/scenario-7-ec-config-helmcharts/chart/values.yaml @@ -0,0 +1 @@ +# default values diff --git a/pkg/lint2/testdata/validation/scenario-7-ec-config-helmcharts/manifests/ec.yaml b/pkg/lint2/testdata/validation/scenario-7-ec-config-helmcharts/manifests/ec.yaml new file mode 100644 index 000000000..ff59aba4a --- /dev/null +++ b/pkg/lint2/testdata/validation/scenario-7-ec-config-helmcharts/manifests/ec.yaml @@ -0,0 +1,11 @@ +apiVersion: embeddedcluster.replicated.com/v1beta1 +kind: Config +metadata: + name: ec +spec: + version: 3.0.0 + extensions: + helmCharts: + - chart: + name: ec-app + chartVersion: 1.0.0 diff --git a/pkg/lint2/testdata/validation/scenario-8-ec-config-missing-archive/.replicated b/pkg/lint2/testdata/validation/scenario-8-ec-config-missing-archive/.replicated new file mode 100644 index 000000000..41bcca854 --- /dev/null +++ b/pkg/lint2/testdata/validation/scenario-8-ec-config-missing-archive/.replicated @@ -0,0 +1,4 @@ +charts: + - path: ./chart +manifests: + - ./manifests/*.yaml diff --git a/pkg/lint2/testdata/validation/scenario-8-ec-config-missing-archive/chart/Chart.yaml b/pkg/lint2/testdata/validation/scenario-8-ec-config-missing-archive/chart/Chart.yaml new file mode 100644 index 000000000..986fbf040 --- /dev/null +++ b/pkg/lint2/testdata/validation/scenario-8-ec-config-missing-archive/chart/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v2 +name: other-app +version: 1.0.0 +description: Test chart with matching kots HelmChart manifest diff --git a/pkg/lint2/testdata/validation/scenario-8-ec-config-missing-archive/chart/values.yaml b/pkg/lint2/testdata/validation/scenario-8-ec-config-missing-archive/chart/values.yaml new file mode 100644 index 000000000..d6c93d3e8 --- /dev/null +++ b/pkg/lint2/testdata/validation/scenario-8-ec-config-missing-archive/chart/values.yaml @@ -0,0 +1 @@ +# default values diff --git a/pkg/lint2/testdata/validation/scenario-8-ec-config-missing-archive/manifests/ec.yaml b/pkg/lint2/testdata/validation/scenario-8-ec-config-missing-archive/manifests/ec.yaml new file mode 100644 index 000000000..fa691b67d --- /dev/null +++ b/pkg/lint2/testdata/validation/scenario-8-ec-config-missing-archive/manifests/ec.yaml @@ -0,0 +1,11 @@ +apiVersion: embeddedcluster.replicated.com/v1beta1 +kind: Config +metadata: + name: ec +spec: + version: 3.0.0 + extensions: + helmCharts: + - chart: + name: missing-chart + chartVersion: 1.0.0 diff --git a/pkg/lint2/testdata/validation/scenario-8-ec-config-missing-archive/manifests/helmchart.yaml b/pkg/lint2/testdata/validation/scenario-8-ec-config-missing-archive/manifests/helmchart.yaml new file mode 100644 index 000000000..b1b039190 --- /dev/null +++ b/pkg/lint2/testdata/validation/scenario-8-ec-config-missing-archive/manifests/helmchart.yaml @@ -0,0 +1,9 @@ +apiVersion: kots.io/v1beta1 +kind: HelmChart +metadata: + name: other-app +spec: + chart: + name: other-app + chartVersion: 1.0.0 + builder: {} diff --git a/pkg/lint2/validation_integration_test.go b/pkg/lint2/validation_integration_test.go index 6c59d9485..ee531c414 100644 --- a/pkg/lint2/validation_integration_test.go +++ b/pkg/lint2/validation_integration_test.go @@ -374,3 +374,105 @@ func TestLintValidation_AutoDiscovery(t *testing.T) { t.Log("Auto-discovery successfully found and validated chart with HelmChart manifest") } + +// TestLintValidation_ECConfigHelmCharts tests validation with EC Config helmCharts that match a chart. +func TestLintValidation_ECConfigHelmCharts(t *testing.T) { + testDir := filepath.Join("testdata", "validation", "scenario-7-ec-config-helmcharts") + + originalWd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + defer os.Chdir(originalWd) + + if err := os.Chdir(testDir); err != nil { + t.Fatalf("failed to change to test directory: %v", err) + } + + parser := tools.NewConfigParser() + config, err := parser.FindAndParseConfig(".") + if err != nil { + t.Fatalf("failed to load config: %v", err) + } + + charts, err := GetChartsWithMetadataFromConfig(config) + if err != nil { + t.Fatalf("GetChartsWithMetadataFromConfig failed: %v", err) + } + + if len(charts) != 1 { + t.Fatalf("expected 1 chart, got %d", len(charts)) + } + + helmCharts, err := DiscoverHelmChartManifests(config.Manifests) + if err != nil { + t.Fatalf("DiscoverHelmChartManifests failed: %v", err) + } + + if len(helmCharts) != 1 { + t.Fatalf("expected 1 HelmChart manifest discovered from EC Config, got %d", len(helmCharts)) + } + + result, err := ValidateChartToHelmChartMapping(charts, helmCharts) + if err != nil { + t.Fatalf("validation failed: %v", err) + } + + if len(result.Warnings) != 0 { + t.Errorf("expected no warnings, got %d: %v", len(result.Warnings), result.Warnings) + } +} + +// TestLintValidation_ECConfigMissingArchive tests warning when EC Config helmCharts has no matching chart. +func TestLintValidation_ECConfigMissingArchive(t *testing.T) { + testDir := filepath.Join("testdata", "validation", "scenario-8-ec-config-missing-archive") + + originalWd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + defer os.Chdir(originalWd) + + if err := os.Chdir(testDir); err != nil { + t.Fatalf("failed to change to test directory: %v", err) + } + + parser := tools.NewConfigParser() + config, err := parser.FindAndParseConfig(".") + if err != nil { + t.Fatalf("failed to load config: %v", err) + } + + charts, err := GetChartsWithMetadataFromConfig(config) + if err != nil { + t.Fatalf("GetChartsWithMetadataFromConfig failed: %v", err) + } + + if len(charts) != 1 { + t.Fatalf("expected 1 chart, got %d", len(charts)) + } + + helmCharts, err := DiscoverHelmChartManifests(config.Manifests) + if err != nil { + t.Fatalf("DiscoverHelmChartManifests failed: %v", err) + } + + if len(helmCharts) != 2 { + t.Fatalf("expected 2 manifests (1 kots + 1 EC), got %d", len(helmCharts)) + } + + result, err := ValidateChartToHelmChartMapping(charts, helmCharts) + if err != nil { + t.Fatalf("validation failed: %v", err) + } + + if len(result.Warnings) != 1 { + t.Fatalf("expected 1 warning for orphaned EC Config manifest, got %d: %v", len(result.Warnings), result.Warnings) + } + + // Verify the warning mentions the orphaned EC Config chart + warning := result.Warnings[0] + if !strings.Contains(warning, "missing-chart") { + t.Errorf("warning should mention orphaned chart 'missing-chart': %s", warning) + } +}