diff --git a/cli/cmd/image_extraction_test.go b/cli/cmd/image_extraction_test.go index b323c1d20..ca9962431 100644 --- a/cli/cmd/image_extraction_test.go +++ b/cli/cmd/image_extraction_test.go @@ -91,7 +91,8 @@ func TestExtractImagesFromConfig_ChartWithRequiredValues_WithMatchingHelmChartMa } func TestExtractImagesFromConfig_ChartWithRequiredValues_NoHelmChartManifest(t *testing.T) { - // Test that extraction fails when manifests are not configured + // Test that discovery returns empty map when manifests are not configured (lenient) + // Validation happens at a higher level (in lint command) chartPath := getAbsTestDataPath(t, filepath.Join("testdata", "image-extraction", "chart-with-required-values-test", "chart")) config := &tools.Config{ @@ -101,18 +102,19 @@ func TestExtractImagesFromConfig_ChartWithRequiredValues_NoHelmChartManifest(t * Manifests: []string{}, // No manifests configured } - // Try to extract HelmChart manifests - should fail because manifests are required - _, err := lint2.DiscoverHelmChartManifests(config.Manifests) - - // Should fail because manifests are required - if err == nil { - t.Fatal("Expected error when manifests not configured, got nil") + // Discovery is lenient - returns empty map instead of error + helmCharts, err := lint2.DiscoverHelmChartManifests(config.Manifests) + if err != nil { + t.Fatalf("DiscoverHelmChartManifests should not error on empty manifests: %v", err) } - // Error should mention manifests configuration - if !strings.Contains(err.Error(), "no manifests configured") { - t.Errorf("Expected error about manifests configuration, got: %v", err) + // Should return empty map (lenient discovery) + if len(helmCharts) != 0 { + t.Errorf("Expected empty map, got %d HelmCharts", len(helmCharts)) } + + // Note: Validation that charts need manifests happens in the lint command + // This tests only the discovery layer behavior } @@ -247,7 +249,8 @@ func TestExtractImagesFromConfig_NoCharts_ReturnsError(t *testing.T) { } func TestExtractImagesFromConfig_NoManifests_ReturnsError(t *testing.T) { - // Test that manifests are required for image extraction + // Test that discovery is lenient when manifests array is empty + // Validation happens at lint command level chartPath := getAbsTestDataPath(t, filepath.Join("testdata", "image-extraction", "simple-chart-test", "chart")) config := &tools.Config{ @@ -257,18 +260,19 @@ func TestExtractImagesFromConfig_NoManifests_ReturnsError(t *testing.T) { Manifests: []string{}, // No manifests configured } - // Try to extract HelmChart manifests - should fail because manifests are required - _, err := lint2.DiscoverHelmChartManifests(config.Manifests) - - // Should fail because manifests are required - if err == nil { - t.Fatal("Expected error when manifests not configured, got nil") + // Discovery is lenient - returns empty map instead of error + helmCharts, err := lint2.DiscoverHelmChartManifests(config.Manifests) + if err != nil { + t.Fatalf("DiscoverHelmChartManifests should not error on empty manifests: %v", err) } - // Error should mention manifests configuration - if !strings.Contains(err.Error(), "no manifests configured") { - t.Errorf("Expected error about manifests configuration, got: %v", err) + // Should return empty map (lenient discovery) + if len(helmCharts) != 0 { + t.Errorf("Expected empty map, got %d HelmCharts", len(helmCharts)) } + + // Note: Validation that charts require manifests happens in runLint() + // This tests only the discovery layer behavior (which is now lenient) } @@ -316,7 +320,8 @@ func TestExtractImagesFromConfig_EmptyBuilder_FailsToRender(t *testing.T) { } func TestExtractImagesFromConfig_NoHelmChartInManifests_FailsDiscovery(t *testing.T) { - // Test that manifests with other K8s resources but no HelmChart kind fail discovery + // Test that discovery is lenient when manifests contain no HelmCharts + // Validation happens at lint command level chartPath := getAbsTestDataPath(t, filepath.Join("testdata", "image-extraction", "no-helmchart-test", "chart")) manifestGlob := getAbsTestDataPath(t, filepath.Join("testdata", "image-extraction", "no-helmchart-test", "manifests")) + "/*.yaml" @@ -327,16 +332,17 @@ func TestExtractImagesFromConfig_NoHelmChartInManifests_FailsDiscovery(t *testin Manifests: []string{manifestGlob}, } - // Try to extract HelmChart manifests - should fail because manifests don't contain HelmCharts - _, err := lint2.DiscoverHelmChartManifests(config.Manifests) - - // Should fail because manifests are configured but contain no HelmCharts - if err == nil { - t.Fatal("Expected error when manifests configured but no HelmCharts found, got nil") + // Discovery is lenient - returns empty map when no HelmCharts found + helmCharts, err := lint2.DiscoverHelmChartManifests(config.Manifests) + if err != nil { + t.Fatalf("DiscoverHelmChartManifests should not error when no HelmCharts found: %v", err) } - // Error should mention no HelmChart resources found - if !strings.Contains(err.Error(), "no HelmChart resources found") { - t.Errorf("Expected error about no HelmCharts, got: %v", err) + // Should return empty map (lenient discovery) + if len(helmCharts) != 0 { + t.Errorf("Expected empty map when no HelmCharts in manifests, got %d", len(helmCharts)) } + + // Note: Validation that charts need HelmChart manifests happens in ValidateChartToHelmChartMapping() + // Discovery layer is lenient and allows mixed directories } diff --git a/cli/cmd/lint.go b/cli/cmd/lint.go index ae112b809..e6518e4dd 100644 --- a/cli/cmd/lint.go +++ b/cli/cmd/lint.go @@ -81,20 +81,18 @@ func extractAllPathsAndMetadata(ctx context.Context, config *tools.Config, verbo return nil, err } result.Preflights = preflights + } - // Get HelmChart manifests (used for v1beta3 preflight validation) - // HelmChart manifests are optional - only required for v1beta3 preflights + // Discover HelmChart manifests ONCE (used by preflight rendering, support bundle analysis, image extraction, validation) + if len(config.Manifests) > 0 { helmChartManifests, err := lint2.DiscoverHelmChartManifests(config.Manifests) if err != nil { - // Only error if it's not a "no manifests found" error - if !strings.Contains(err.Error(), "no HelmChart resources found") { - return nil, err - } - // No manifests found is OK - set empty map - result.HelmChartManifests = make(map[string]*lint2.HelmChartManifest) - } else { - result.HelmChartManifests = helmChartManifests + return nil, fmt.Errorf("failed to discover HelmChart manifests: %w", err) } + result.HelmChartManifests = helmChartManifests + } else { + // No manifests configured - return empty map (validation will check if needed) + result.HelmChartManifests = make(map[string]*lint2.HelmChartManifest) } // Discover support bundles @@ -104,29 +102,10 @@ func extractAllPathsAndMetadata(ctx context.Context, config *tools.Config, verbo return nil, err } result.SupportBundles = sbPaths - - // Get HelmChart manifests if not already extracted - if result.HelmChartManifests == nil { - helmChartManifests, err := lint2.DiscoverHelmChartManifests(config.Manifests) - if err != nil { - // Support bundles don't require HelmChart manifests - only error if manifests are explicitly configured - // but fail to parse. If no HelmChart manifests exist, that's fine (return empty map). - if len(config.Manifests) > 0 { - // Check if error is "no HelmChart resources found" - that's acceptable - if err != nil && !strings.Contains(err.Error(), "no HelmChart resources found") { - return nil, err - } - } - // Set empty map so we don't try to extract again - result.HelmChartManifests = make(map[string]*lint2.HelmChartManifest) - } else { - result.HelmChartManifests = helmChartManifests - } - } } - // Extract charts with metadata (ONLY for verbose mode) - if verbose && len(config.Charts) > 0 { + // Extract charts with metadata (needed for validation and image extraction) + if len(config.Charts) > 0 { chartsWithMetadata, err := lint2.GetChartsWithMetadataFromConfig(config) if err != nil { return nil, err @@ -197,11 +176,19 @@ func (r *runners) runLint(cmd *cobra.Command, args []string) error { // Convert to manifests glob patterns for compatibility config.Manifests = append(config.Manifests, sbPaths...) + // Auto-discover HelmChart manifests (needed for chart validation) + helmChartPaths, err := lint2.DiscoverHelmChartPaths(filepath.Join(".", "**")) + if err != nil { + return errors.Wrap(err, "failed to discover HelmChart manifests") + } + config.Manifests = append(config.Manifests, helmChartPaths...) + // Print what was discovered fmt.Fprintf(r.w, "Discovered resources:\n") fmt.Fprintf(r.w, " - %d Helm chart(s)\n", len(chartPaths)) fmt.Fprintf(r.w, " - %d Preflight spec(s)\n", len(preflightPaths)) - fmt.Fprintf(r.w, " - %d Support Bundle spec(s)\n\n", len(sbPaths)) + fmt.Fprintf(r.w, " - %d Support Bundle spec(s)\n", len(sbPaths)) + fmt.Fprintf(r.w, " - %d HelmChart manifest(s)\n\n", len(helmChartPaths)) r.w.Flush() // If nothing was found, exit early @@ -231,6 +218,37 @@ func (r *runners) runLint(cmd *cobra.Command, args []string) error { return errors.Wrap(err, "failed to extract paths and metadata") } + // Validate chart-to-HelmChart mapping if charts are configured + if len(config.Charts) > 0 { + // Charts configured but no manifests - error early + if len(config.Manifests) == 0 { + return errors.New("charts are configured but no manifests paths provided\n\n" + + "HelmChart manifests (kind: HelmChart) are required for each chart.\n" + + "Add manifest paths to your .replicated config:\n\n" + + "manifests:\n" + + " - \"./manifests/**/*.yaml\"") + } + + // Validate mapping using already-extracted metadata + validationResult, err := lint2.ValidateChartToHelmChartMapping( + extracted.ChartsWithMetadata, // Already populated in extraction + extracted.HelmChartManifests, + ) + if err != nil { + // Hard error - stop before linting + return errors.Wrap(err, "chart validation failed") + } + + // Display warnings (orphaned HelmChart manifests) + if r.outputFormat == "table" && len(validationResult.Warnings) > 0 { + for _, warning := range validationResult.Warnings { + fmt.Fprintf(r.w, "Warning: %s\n", warning) + } + fmt.Fprintln(r.w) + r.w.Flush() + } + } + // Extract and display images if verbose mode is enabled if r.args.lintVerbose && len(extracted.ChartsWithMetadata) > 0 { imageResults, err := r.extractImagesFromCharts(cmd.Context(), extracted.ChartsWithMetadata, extracted.HelmChartManifests) diff --git a/cli/cmd/lint_test.go b/cli/cmd/lint_test.go index d8cea9983..89fb4850e 100644 --- a/cli/cmd/lint_test.go +++ b/cli/cmd/lint_test.go @@ -784,3 +784,293 @@ func isValidSemVer(version string) bool { return true } + +// TestLint_ChartValidationError tests that lint fails when a chart is missing its HelmChart manifest +func TestLint_ChartValidationError(t *testing.T) { + tmpDir := t.TempDir() + + // Create a chart + chartDir := filepath.Join(tmpDir, "test-chart") + if err := os.MkdirAll(chartDir, 0755); err != nil { + t.Fatal(err) + } + + chartYaml := filepath.Join(chartDir, "Chart.yaml") + chartContent := `apiVersion: v2 +name: test-app +version: 1.0.0 +` + if err := os.WriteFile(chartYaml, []byte(chartContent), 0644); err != nil { + t.Fatal(err) + } + + // Create empty manifests directory (no HelmChart manifest) + manifestsDir := filepath.Join(tmpDir, "manifests") + if err := os.MkdirAll(manifestsDir, 0755); err != nil { + t.Fatal(err) + } + + // Create config with chart but no matching HelmChart manifest + configPath := filepath.Join(tmpDir, ".replicated") + configContent := `charts: + - path: ` + chartDir + ` +manifests: + - ` + manifestsDir + `/*.yaml +repl-lint: + linters: + helm: + disabled: true + preflight: + disabled: true +` + if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil { + t.Fatal(err) + } + + // Change to temp directory + oldWd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + defer os.Chdir(oldWd) + if err := os.Chdir(tmpDir); err != nil { + t.Fatal(err) + } + + // Create output buffer + buf := new(bytes.Buffer) + w := tabwriter.NewWriter(buf, 0, 8, 4, ' ', 0) + + r := &runners{ + w: w, + outputFormat: "table", + args: runnerArgs{ + lintVerbose: false, + }, + } + + // Create a mock command with context + cmd := &cobra.Command{} + cmd.SetContext(context.Background()) + + // Run the lint command - should fail + err = r.runLint(cmd, []string{}) + if err == nil { + t.Fatal("expected validation error, got nil") + } + + // Error should mention the missing HelmChart manifest + errMsg := err.Error() + if !strings.Contains(errMsg, "chart validation failed") { + t.Errorf("error should mention 'chart validation failed': %s", errMsg) + } + if !strings.Contains(errMsg, "test-app") { + t.Errorf("error should contain chart name 'test-app': %s", errMsg) + } + if !strings.Contains(errMsg, "1.0.0") { + t.Errorf("error should contain version '1.0.0': %s", errMsg) + } + if !strings.Contains(errMsg, "HelmChart manifest") { + t.Errorf("error should mention HelmChart manifest: %s", errMsg) + } +} + +// TestLint_ChartValidationWarning tests that lint succeeds but shows warning for orphaned HelmChart manifest +func TestLint_ChartValidationWarning(t *testing.T) { + tmpDir := t.TempDir() + + // Create a chart + chartDir := filepath.Join(tmpDir, "test-chart") + if err := os.MkdirAll(chartDir, 0755); err != nil { + t.Fatal(err) + } + + chartYaml := filepath.Join(chartDir, "Chart.yaml") + chartContent := `apiVersion: v2 +name: current-app +version: 1.0.0 +` + if err := os.WriteFile(chartYaml, []byte(chartContent), 0644); err != nil { + t.Fatal(err) + } + + // Create manifests directory with matching HelmChart + orphaned one + manifestsDir := filepath.Join(tmpDir, "manifests") + if err := os.MkdirAll(manifestsDir, 0755); err != nil { + t.Fatal(err) + } + + // Matching HelmChart manifest + currentHelmChart := filepath.Join(manifestsDir, "current-app.yaml") + currentContent := `apiVersion: kots.io/v1beta2 +kind: HelmChart +metadata: + name: current-app +spec: + chart: + name: current-app + chartVersion: 1.0.0 + builder: {} +` + if err := os.WriteFile(currentHelmChart, []byte(currentContent), 0644); err != nil { + t.Fatal(err) + } + + // Orphaned HelmChart manifest + oldHelmChart := filepath.Join(manifestsDir, "old-app.yaml") + oldContent := `apiVersion: kots.io/v1beta2 +kind: HelmChart +metadata: + name: old-app +spec: + chart: + name: old-app + chartVersion: 1.0.0 + builder: {} +` + if err := os.WriteFile(oldHelmChart, []byte(oldContent), 0644); err != nil { + t.Fatal(err) + } + + // Create config + configPath := filepath.Join(tmpDir, ".replicated") + configContent := `charts: + - path: ` + chartDir + ` +manifests: + - ` + manifestsDir + `/*.yaml +repl-lint: + linters: + helm: + disabled: true + preflight: + disabled: true +` + if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil { + t.Fatal(err) + } + + // Change to temp directory + oldWd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + defer os.Chdir(oldWd) + if err := os.Chdir(tmpDir); err != nil { + t.Fatal(err) + } + + // Create output buffer + buf := new(bytes.Buffer) + w := tabwriter.NewWriter(buf, 0, 8, 4, ' ', 0) + + r := &runners{ + w: w, + outputFormat: "table", + args: runnerArgs{ + lintVerbose: false, + }, + } + + // Create a mock command with context + cmd := &cobra.Command{} + cmd.SetContext(context.Background()) + + // Run the lint command - should succeed + err = r.runLint(cmd, []string{}) + // Note: err might be non-nil due to disabled linters not running + // We care about the output showing the warning + + w.Flush() + output := buf.String() + + // Output should contain warning about orphaned manifest + if !strings.Contains(output, "Warning") { + t.Error("expected warning message in output") + } + if !strings.Contains(output, "old-app") { + t.Errorf("warning should mention orphaned chart 'old-app': %s", output) + } + if !strings.Contains(output, "no corresponding chart") { + t.Errorf("warning should explain the issue: %s", output) + } +} + +// TestLint_NoManifestsConfig tests error when charts configured but manifests section missing +func TestLint_NoManifestsConfig(t *testing.T) { + tmpDir := t.TempDir() + + // Create a chart + chartDir := filepath.Join(tmpDir, "test-chart") + if err := os.MkdirAll(chartDir, 0755); err != nil { + t.Fatal(err) + } + + chartYaml := filepath.Join(chartDir, "Chart.yaml") + chartContent := `apiVersion: v2 +name: test-app +version: 1.0.0 +` + if err := os.WriteFile(chartYaml, []byte(chartContent), 0644); err != nil { + t.Fatal(err) + } + + // Create config WITHOUT manifests section + configPath := filepath.Join(tmpDir, ".replicated") + configContent := `charts: + - path: ` + chartDir + ` +repl-lint: + linters: + helm: + disabled: true +` + if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil { + t.Fatal(err) + } + + // Change to temp directory + oldWd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + defer os.Chdir(oldWd) + if err := os.Chdir(tmpDir); err != nil { + t.Fatal(err) + } + + // Create output buffer + buf := new(bytes.Buffer) + w := tabwriter.NewWriter(buf, 0, 8, 4, ' ', 0) + + r := &runners{ + w: w, + outputFormat: "table", + args: runnerArgs{ + lintVerbose: false, + }, + } + + // Create a mock command with context + cmd := &cobra.Command{} + cmd.SetContext(context.Background()) + + // Run the lint command - should fail early + err = r.runLint(cmd, []string{}) + if err == nil { + t.Fatal("expected error when charts configured but no manifests, got nil") + } + + // Error should explain the problem clearly + errMsg := err.Error() + if !strings.Contains(errMsg, "charts are configured") { + t.Errorf("error should mention charts are configured: %s", errMsg) + } + if !strings.Contains(errMsg, "no manifests") { + t.Errorf("error should mention missing manifests: %s", errMsg) + } + if !strings.Contains(errMsg, "HelmChart manifest") { + t.Errorf("error should mention HelmChart manifest requirement: %s", errMsg) + } + if !strings.Contains(errMsg, ".replicated") || !strings.Contains(errMsg, "manifests:") { + t.Errorf("error should provide actionable guidance: %s", errMsg) + } +} diff --git a/docs/lint-format.md b/docs/lint-format.md index 23195b596..3d5ed8b0a 100644 --- a/docs/lint-format.md +++ b/docs/lint-format.md @@ -193,3 +193,107 @@ my-chart/ ``` **Note:** Custom values file paths are not currently supported. Values files must be named `values.yaml` or `values.yml` and located in the chart root directory. + +## HelmChart Manifest Requirements + +Every Helm chart configured in your `.replicated` file requires a corresponding `HelmChart` manifest (custom resource with `kind: HelmChart`). This manifest is essential for: + +- **Preflight template rendering**: Charts are rendered with builder values before running preflight checks +- **Image extraction**: Images are extracted from chart templates for air gap bundle creation +- **Air gap bundle building**: Charts are packaged with specific values for offline installations + +### Configuration + +When charts are configured, you must also specify where to find their HelmChart manifests: + +```yaml +charts: + - path: "./charts/my-app" + - path: "./charts/my-api" + +manifests: + - "./manifests/**/*.yaml" # HelmChart manifests must be in these paths +``` + +**Important:** The `manifests` section is required whenever `charts` are configured. If you configure charts but omit manifests, linting will fail with a clear error message. + +### HelmChart Manifest Structure + +Each HelmChart manifest must specify the chart name and version that match the corresponding chart's `Chart.yaml`: + +```yaml +apiVersion: kots.io/v1beta2 +kind: HelmChart +metadata: + name: my-app +spec: + chart: + name: my-app # Must match Chart.yaml name + chartVersion: 1.0.0 # Must match Chart.yaml version + builder: {} # Values for rendering (can be empty) +``` + +The `spec.chart.name` and `spec.chart.chartVersion` fields must exactly match the `name` and `version` in your Helm chart's `Chart.yaml` file. + +### Validation Behavior + +The linter validates chart-to-HelmChart mapping before running other checks: + +- **✅ Success**: All charts have matching HelmChart manifests +- **❌ Error**: One or more charts are missing HelmChart manifests (batch reports all missing) +- **⚠️ Warning**: HelmChart manifest exists but no corresponding chart is configured (orphaned manifest) + +#### Error Example + +``` +Error: chart validation failed: Chart validation failed - 2 charts missing HelmChart manifests: + - ./charts/frontend (frontend:1.0.0) + - ./charts/database (database:1.5.0) + +Each Helm chart requires a corresponding HelmChart manifest (kind: HelmChart). +Ensure the manifests are in paths specified in the 'manifests' section of .replicated config. +``` + +#### Warning Example + +``` +Warning: HelmChart manifest at "./manifests/old-app.yaml" (old-app:1.0.0) has no corresponding chart configured +``` + +### Auto-Discovery + +When no `.replicated` config file exists, the linter automatically discovers all resources including: +- Helm charts (by finding `Chart.yaml` files) +- HelmChart manifests (by finding `kind: HelmChart` in YAML files) +- Preflights and Support Bundles + +Auto-discovery validates that all discovered charts have corresponding HelmChart manifests. + +### Troubleshooting + +**Problem:** "charts are configured but no manifests paths provided" + +**Solution:** Add a `manifests` section to your `.replicated` config: +```yaml +manifests: + - "./manifests/**/*.yaml" +``` + +**Problem:** "Missing HelmChart manifest for chart" + +**Solution:** Create a HelmChart manifest with matching name and version: +```yaml +apiVersion: kots.io/v1beta2 +kind: HelmChart +metadata: + name: +spec: + chart: + name: + chartVersion: + builder: {} +``` + +**Problem:** Warning about orphaned HelmChart manifest + +**Solution:** Either add the corresponding chart to your configuration or remove the unused HelmChart manifest. Warnings are informational and won't cause linting to fail. diff --git a/pkg/lint2/discovery.go b/pkg/lint2/discovery.go index 3db9f9e9b..744311bbe 100644 --- a/pkg/lint2/discovery.go +++ b/pkg/lint2/discovery.go @@ -146,6 +146,18 @@ func DiscoverPreflightPaths(pattern string) ([]string, error) { return discoverYAMLsByKind(pattern, "Preflight", "preflight spec") } +// DiscoverHelmChartPaths discovers HelmChart manifest files from a glob pattern. +// This is a thin wrapper around discoverYAMLsByKind for backward compatibility. +// +// Supports patterns like: +// - "./manifests/**" (finds all HelmChart manifests recursively) +// - "./manifests/**/*.yaml" (explicit YAML extension) +// - "./k8s/{dev,prod}/**/*.yaml" (environment-specific) +// - "./helmchart.yaml" (explicit file path - validated strictly) +func DiscoverHelmChartPaths(pattern string) ([]string, error) { + return discoverYAMLsByKind(pattern, "HelmChart", "HelmChart manifest") +} + // (duplicate isPreflightSpec removed) // (duplicate isSupportBundleSpec removed) diff --git a/pkg/lint2/helmchart.go b/pkg/lint2/helmchart.go index 2925801d6..70a606d8d 100644 --- a/pkg/lint2/helmchart.go +++ b/pkg/lint2/helmchart.go @@ -63,8 +63,8 @@ func (e *DuplicateHelmChartError) Error() string { // - Hidden directories (.git, .github, etc.) func DiscoverHelmChartManifests(manifestGlobs []string) (map[string]*HelmChartManifest, error) { if len(manifestGlobs) == 0 { - // Error when no manifest patterns provided - caller needs at least one pattern to search - return nil, fmt.Errorf("no manifests configured - cannot discover HelmChart resources") + // Return empty map - validation layer will handle this + return make(map[string]*HelmChartManifest), nil } helmCharts := make(map[string]*HelmChartManifest) @@ -121,12 +121,10 @@ func DiscoverHelmChartManifests(manifestGlobs []string) (map[string]*HelmChartMa } } - // Fail-fast if no HelmCharts found - // Both preflight linting and image extraction require HelmCharts when manifests are configured + // 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 { - return nil, fmt.Errorf("no HelmChart resources found in manifests\n"+ - "At least one HelmChart manifest is required when manifests are configured.\n"+ - "Checked patterns: %v", manifestGlobs) + return make(map[string]*HelmChartManifest), nil } return helmCharts, nil diff --git a/pkg/lint2/helmchart_test.go b/pkg/lint2/helmchart_test.go index 7d8e12177..447526dd2 100644 --- a/pkg/lint2/helmchart_test.go +++ b/pkg/lint2/helmchart_test.go @@ -7,13 +7,16 @@ import ( ) func TestDiscoverHelmChartManifests(t *testing.T) { - t.Run("empty manifests list returns error", func(t *testing.T) { - _, err := DiscoverHelmChartManifests([]string{}) - if err == nil { - t.Fatal("expected error for empty manifests list, got nil") + t.Run("empty manifests list returns empty map", func(t *testing.T) { + manifests, err := DiscoverHelmChartManifests([]string{}) + if err != nil { + t.Fatalf("unexpected error for empty manifests list: %v", err) } - if err.Error() != "no manifests configured - cannot discover HelmChart resources" { - t.Errorf("unexpected error message: %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)) } }) @@ -314,17 +317,13 @@ spec: pattern := filepath.Join(tmpDir, "*.yaml") manifests, err := DiscoverHelmChartManifests([]string{pattern}) - // With fail-fast validation, we expect an error when no valid HelmCharts found - if err == nil { - t.Fatal("expected error when all HelmCharts are invalid (fail-fast), got nil") - } - - if !contains(err.Error(), "no HelmChart resources found") { - t.Errorf("expected error about no HelmCharts found, got: %v", err) + // Discovery is now lenient - returns empty map when no valid HelmCharts found + if err != nil { + t.Fatalf("unexpected error: %v", err) } - if manifests != nil { - t.Errorf("expected nil manifests on error, got %d manifests", len(manifests)) + if len(manifests) != 0 { + t.Errorf("expected empty map (invalid HelmCharts skipped), got %d manifests", len(manifests)) } }) @@ -345,17 +344,13 @@ spec: pattern := filepath.Join(tmpDir, "*.yaml") manifests, err := DiscoverHelmChartManifests([]string{pattern}) - // With fail-fast validation, we expect an error when no valid HelmCharts found - if err == nil { - t.Fatal("expected error when all files are invalid (fail-fast), got nil") - } - - if !contains(err.Error(), "no HelmChart resources found") { - t.Errorf("expected error about no HelmCharts found, got: %v", err) + // Discovery is now lenient - returns empty map when no valid HelmCharts found + if err != nil { + t.Fatalf("unexpected error: %v", err) } - if manifests != nil { - t.Errorf("expected nil manifests on error, got %d manifests", len(manifests)) + if len(manifests) != 0 { + t.Errorf("expected empty map (invalid YAML skipped), got %d manifests", len(manifests)) } }) diff --git a/pkg/lint2/preflight_integration_test.go b/pkg/lint2/preflight_integration_test.go index c0cd40dd8..2e983cbfc 100644 --- a/pkg/lint2/preflight_integration_test.go +++ b/pkg/lint2/preflight_integration_test.go @@ -748,30 +748,26 @@ func TestLintPreflight_Integration(t *testing.T) { }) t.Run("manifests without HelmChart kind", func(t *testing.T) { - // This test verifies the fail-fast error path when manifests are configured but don't contain any kind: HelmChart. + // This test verifies the lenient discovery behavior when manifests are configured but don't contain any kind: HelmChart. // Scenario: User has Deployment, Service, ConfigMap manifests, but forgot the HelmChart custom resource. - // Expected: DiscoverHelmChartManifests() fails immediately with helpful error (fail-fast behavior) + // Expected: DiscoverHelmChartManifests() returns empty map (lenient discovery) + // Validation: Error happens at validation layer (in lint command), not discovery layer // Manifests directory contains Deployment, Service, ConfigMap - but NO HelmChart - _, err := DiscoverHelmChartManifests([]string{"testdata/preflights/no-helmchart-test/manifests/*.yaml"}) + helmCharts, err := DiscoverHelmChartManifests([]string{"testdata/preflights/no-helmchart-test/manifests/*.yaml"}) - // Should fail-fast during discovery (not delay error until linting) - if err == nil { - t.Fatal("Expected error when no HelmChart found in manifests (fail-fast), got nil") + // Discovery is lenient - returns empty map instead of error + if err != nil { + t.Fatalf("DiscoverHelmChartManifests should not error when no HelmCharts found: %v", err) } - // Verify error message is helpful - expectedPhrases := []string{ - "no HelmChart resources found", - "At least one HelmChart manifest is required", - } - for _, phrase := range expectedPhrases { - if !contains(err.Error(), phrase) { - t.Errorf("Error message should contain %q, got: %v", phrase, err) - } + // Should return empty map (lenient discovery) + if len(helmCharts) != 0 { + t.Errorf("Expected empty map when no HelmCharts in manifests, got %d", len(helmCharts)) } - t.Logf("✓ Fail-fast error when manifests configured but no HelmChart found: %v", err) + t.Logf("✓ Discovery is lenient: returns empty map when manifests configured but no HelmChart found") + t.Logf(" (Validation that charts need HelmChart manifests happens in ValidateChartToHelmChartMapping)") }) t.Run("advanced template features - Sprig functions", func(t *testing.T) { diff --git a/pkg/lint2/testdata/validation/scenario-1-success/.replicated b/pkg/lint2/testdata/validation/scenario-1-success/.replicated new file mode 100644 index 000000000..41bcca854 --- /dev/null +++ b/pkg/lint2/testdata/validation/scenario-1-success/.replicated @@ -0,0 +1,4 @@ +charts: + - path: ./chart +manifests: + - ./manifests/*.yaml diff --git a/pkg/lint2/testdata/validation/scenario-1-success/chart/Chart.yaml b/pkg/lint2/testdata/validation/scenario-1-success/chart/Chart.yaml new file mode 100644 index 000000000..f6e716632 --- /dev/null +++ b/pkg/lint2/testdata/validation/scenario-1-success/chart/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v2 +name: success-app +version: 1.0.0 +description: Test chart for validation success scenario diff --git a/pkg/lint2/testdata/validation/scenario-1-success/chart/values.yaml b/pkg/lint2/testdata/validation/scenario-1-success/chart/values.yaml new file mode 100644 index 000000000..a2ebabc01 --- /dev/null +++ b/pkg/lint2/testdata/validation/scenario-1-success/chart/values.yaml @@ -0,0 +1,4 @@ +replicaCount: 1 +image: + repository: nginx + tag: "1.21" diff --git a/pkg/lint2/testdata/validation/scenario-1-success/manifests/helmchart.yaml b/pkg/lint2/testdata/validation/scenario-1-success/manifests/helmchart.yaml new file mode 100644 index 000000000..94667f0e2 --- /dev/null +++ b/pkg/lint2/testdata/validation/scenario-1-success/manifests/helmchart.yaml @@ -0,0 +1,9 @@ +apiVersion: kots.io/v1beta2 +kind: HelmChart +metadata: + name: success-app +spec: + chart: + name: success-app + chartVersion: 1.0.0 + builder: {} diff --git a/pkg/lint2/testdata/validation/scenario-2-missing-helmchart/.replicated b/pkg/lint2/testdata/validation/scenario-2-missing-helmchart/.replicated new file mode 100644 index 000000000..41bcca854 --- /dev/null +++ b/pkg/lint2/testdata/validation/scenario-2-missing-helmchart/.replicated @@ -0,0 +1,4 @@ +charts: + - path: ./chart +manifests: + - ./manifests/*.yaml diff --git a/pkg/lint2/testdata/validation/scenario-2-missing-helmchart/chart/Chart.yaml b/pkg/lint2/testdata/validation/scenario-2-missing-helmchart/chart/Chart.yaml new file mode 100644 index 000000000..5cce2a018 --- /dev/null +++ b/pkg/lint2/testdata/validation/scenario-2-missing-helmchart/chart/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v2 +name: missing-manifest-app +version: 2.0.0 +description: Test chart with no HelmChart manifest diff --git a/pkg/lint2/testdata/validation/scenario-2-missing-helmchart/chart/values.yaml b/pkg/lint2/testdata/validation/scenario-2-missing-helmchart/chart/values.yaml new file mode 100644 index 000000000..ebf6be86b --- /dev/null +++ b/pkg/lint2/testdata/validation/scenario-2-missing-helmchart/chart/values.yaml @@ -0,0 +1 @@ +replicaCount: 1 diff --git a/pkg/lint2/testdata/validation/scenario-2-missing-helmchart/manifests/.gitkeep b/pkg/lint2/testdata/validation/scenario-2-missing-helmchart/manifests/.gitkeep new file mode 100644 index 000000000..8db678caf --- /dev/null +++ b/pkg/lint2/testdata/validation/scenario-2-missing-helmchart/manifests/.gitkeep @@ -0,0 +1 @@ +# Empty manifests directory - no HelmChart manifest \ No newline at end of file diff --git a/pkg/lint2/testdata/validation/scenario-3-multiple-charts/.replicated b/pkg/lint2/testdata/validation/scenario-3-multiple-charts/.replicated new file mode 100644 index 000000000..b69ad3593 --- /dev/null +++ b/pkg/lint2/testdata/validation/scenario-3-multiple-charts/.replicated @@ -0,0 +1,6 @@ +charts: + - path: ./chart1 + - path: ./chart2 + - path: ./chart3 +manifests: + - ./manifests/*.yaml diff --git a/pkg/lint2/testdata/validation/scenario-3-multiple-charts/chart1/Chart.yaml b/pkg/lint2/testdata/validation/scenario-3-multiple-charts/chart1/Chart.yaml new file mode 100644 index 000000000..6f13eed05 --- /dev/null +++ b/pkg/lint2/testdata/validation/scenario-3-multiple-charts/chart1/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v2 +name: frontend +version: 1.0.0 +description: Frontend chart diff --git a/pkg/lint2/testdata/validation/scenario-3-multiple-charts/chart1/values.yaml b/pkg/lint2/testdata/validation/scenario-3-multiple-charts/chart1/values.yaml new file mode 100644 index 000000000..ebf6be86b --- /dev/null +++ b/pkg/lint2/testdata/validation/scenario-3-multiple-charts/chart1/values.yaml @@ -0,0 +1 @@ +replicaCount: 1 diff --git a/pkg/lint2/testdata/validation/scenario-3-multiple-charts/chart2/Chart.yaml b/pkg/lint2/testdata/validation/scenario-3-multiple-charts/chart2/Chart.yaml new file mode 100644 index 000000000..90325a367 --- /dev/null +++ b/pkg/lint2/testdata/validation/scenario-3-multiple-charts/chart2/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v2 +name: backend +version: 2.1.0 +description: Backend chart diff --git a/pkg/lint2/testdata/validation/scenario-3-multiple-charts/chart2/values.yaml b/pkg/lint2/testdata/validation/scenario-3-multiple-charts/chart2/values.yaml new file mode 100644 index 000000000..5ef7832ca --- /dev/null +++ b/pkg/lint2/testdata/validation/scenario-3-multiple-charts/chart2/values.yaml @@ -0,0 +1 @@ +replicaCount: 2 diff --git a/pkg/lint2/testdata/validation/scenario-3-multiple-charts/chart3/Chart.yaml b/pkg/lint2/testdata/validation/scenario-3-multiple-charts/chart3/Chart.yaml new file mode 100644 index 000000000..e5d09d67f --- /dev/null +++ b/pkg/lint2/testdata/validation/scenario-3-multiple-charts/chart3/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v2 +name: database +version: 1.5.0 +description: Database chart diff --git a/pkg/lint2/testdata/validation/scenario-3-multiple-charts/chart3/values.yaml b/pkg/lint2/testdata/validation/scenario-3-multiple-charts/chart3/values.yaml new file mode 100644 index 000000000..ebf6be86b --- /dev/null +++ b/pkg/lint2/testdata/validation/scenario-3-multiple-charts/chart3/values.yaml @@ -0,0 +1 @@ +replicaCount: 1 diff --git a/pkg/lint2/testdata/validation/scenario-3-multiple-charts/manifests/backend.yaml b/pkg/lint2/testdata/validation/scenario-3-multiple-charts/manifests/backend.yaml new file mode 100644 index 000000000..3f64a2696 --- /dev/null +++ b/pkg/lint2/testdata/validation/scenario-3-multiple-charts/manifests/backend.yaml @@ -0,0 +1,9 @@ +apiVersion: kots.io/v1beta2 +kind: HelmChart +metadata: + name: backend +spec: + chart: + name: backend + chartVersion: 2.1.0 + builder: {} diff --git a/pkg/lint2/testdata/validation/scenario-3-multiple-charts/manifests/frontend.yaml b/pkg/lint2/testdata/validation/scenario-3-multiple-charts/manifests/frontend.yaml new file mode 100644 index 000000000..068f9137f --- /dev/null +++ b/pkg/lint2/testdata/validation/scenario-3-multiple-charts/manifests/frontend.yaml @@ -0,0 +1,9 @@ +apiVersion: kots.io/v1beta2 +kind: HelmChart +metadata: + name: frontend +spec: + chart: + name: frontend + chartVersion: 1.0.0 + builder: {} diff --git a/pkg/lint2/testdata/validation/scenario-4-orphaned-manifest/.replicated b/pkg/lint2/testdata/validation/scenario-4-orphaned-manifest/.replicated new file mode 100644 index 000000000..41bcca854 --- /dev/null +++ b/pkg/lint2/testdata/validation/scenario-4-orphaned-manifest/.replicated @@ -0,0 +1,4 @@ +charts: + - path: ./chart +manifests: + - ./manifests/*.yaml diff --git a/pkg/lint2/testdata/validation/scenario-4-orphaned-manifest/chart/Chart.yaml b/pkg/lint2/testdata/validation/scenario-4-orphaned-manifest/chart/Chart.yaml new file mode 100644 index 000000000..34886c6ab --- /dev/null +++ b/pkg/lint2/testdata/validation/scenario-4-orphaned-manifest/chart/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v2 +name: current-app +version: 1.0.0 +description: Current active chart diff --git a/pkg/lint2/testdata/validation/scenario-4-orphaned-manifest/chart/values.yaml b/pkg/lint2/testdata/validation/scenario-4-orphaned-manifest/chart/values.yaml new file mode 100644 index 000000000..ebf6be86b --- /dev/null +++ b/pkg/lint2/testdata/validation/scenario-4-orphaned-manifest/chart/values.yaml @@ -0,0 +1 @@ +replicaCount: 1 diff --git a/pkg/lint2/testdata/validation/scenario-4-orphaned-manifest/manifests/current-app.yaml b/pkg/lint2/testdata/validation/scenario-4-orphaned-manifest/manifests/current-app.yaml new file mode 100644 index 000000000..cdb467bbd --- /dev/null +++ b/pkg/lint2/testdata/validation/scenario-4-orphaned-manifest/manifests/current-app.yaml @@ -0,0 +1,9 @@ +apiVersion: kots.io/v1beta2 +kind: HelmChart +metadata: + name: current-app +spec: + chart: + name: current-app + chartVersion: 1.0.0 + builder: {} diff --git a/pkg/lint2/testdata/validation/scenario-4-orphaned-manifest/manifests/old-app.yaml b/pkg/lint2/testdata/validation/scenario-4-orphaned-manifest/manifests/old-app.yaml new file mode 100644 index 000000000..b644370a2 --- /dev/null +++ b/pkg/lint2/testdata/validation/scenario-4-orphaned-manifest/manifests/old-app.yaml @@ -0,0 +1,9 @@ +apiVersion: kots.io/v1beta2 +kind: HelmChart +metadata: + name: old-app +spec: + chart: + name: old-app + chartVersion: 1.0.0 + builder: {} diff --git a/pkg/lint2/testdata/validation/scenario-5-auto-discovery/chart/Chart.yaml b/pkg/lint2/testdata/validation/scenario-5-auto-discovery/chart/Chart.yaml new file mode 100644 index 000000000..c8a5f0745 --- /dev/null +++ b/pkg/lint2/testdata/validation/scenario-5-auto-discovery/chart/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v2 +name: auto-discovered-app +version: 1.0.0 +description: Chart for auto-discovery testing diff --git a/pkg/lint2/testdata/validation/scenario-5-auto-discovery/chart/values.yaml b/pkg/lint2/testdata/validation/scenario-5-auto-discovery/chart/values.yaml new file mode 100644 index 000000000..ebf6be86b --- /dev/null +++ b/pkg/lint2/testdata/validation/scenario-5-auto-discovery/chart/values.yaml @@ -0,0 +1 @@ +replicaCount: 1 diff --git a/pkg/lint2/testdata/validation/scenario-5-auto-discovery/manifests/helmchart.yaml b/pkg/lint2/testdata/validation/scenario-5-auto-discovery/manifests/helmchart.yaml new file mode 100644 index 000000000..46217d278 --- /dev/null +++ b/pkg/lint2/testdata/validation/scenario-5-auto-discovery/manifests/helmchart.yaml @@ -0,0 +1,9 @@ +apiVersion: kots.io/v1beta2 +kind: HelmChart +metadata: + name: auto-discovered-app +spec: + chart: + name: auto-discovered-app + chartVersion: 1.0.0 + builder: {} diff --git a/pkg/lint2/testdata/validation/scenario-6-no-manifests-config/.replicated b/pkg/lint2/testdata/validation/scenario-6-no-manifests-config/.replicated new file mode 100644 index 000000000..d8227649e --- /dev/null +++ b/pkg/lint2/testdata/validation/scenario-6-no-manifests-config/.replicated @@ -0,0 +1,3 @@ +charts: + - path: ./chart +# manifests section missing - should error diff --git a/pkg/lint2/testdata/validation/scenario-6-no-manifests-config/chart/Chart.yaml b/pkg/lint2/testdata/validation/scenario-6-no-manifests-config/chart/Chart.yaml new file mode 100644 index 000000000..0ab58f054 --- /dev/null +++ b/pkg/lint2/testdata/validation/scenario-6-no-manifests-config/chart/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v2 +name: test-app +version: 1.0.0 +description: Chart without manifests configured diff --git a/pkg/lint2/testdata/validation/scenario-6-no-manifests-config/chart/values.yaml b/pkg/lint2/testdata/validation/scenario-6-no-manifests-config/chart/values.yaml new file mode 100644 index 000000000..ebf6be86b --- /dev/null +++ b/pkg/lint2/testdata/validation/scenario-6-no-manifests-config/chart/values.yaml @@ -0,0 +1 @@ +replicaCount: 1 diff --git a/pkg/lint2/validation.go b/pkg/lint2/validation.go new file mode 100644 index 000000000..b5a144a3d --- /dev/null +++ b/pkg/lint2/validation.go @@ -0,0 +1,112 @@ +package lint2 + +import ( + "fmt" + "strings" +) + +// ChartMissingHelmChartInfo contains information about a chart missing its HelmChart manifest. +type ChartMissingHelmChartInfo struct { + ChartPath string // Absolute path to chart directory + ChartName string // Chart name from Chart.yaml + ChartVersion string // Chart version from Chart.yaml +} + +// MultipleChartsMissingHelmChartsError is returned when multiple charts are missing HelmChart manifests. +type MultipleChartsMissingHelmChartsError struct { + MissingCharts []ChartMissingHelmChartInfo +} + +func (e *MultipleChartsMissingHelmChartsError) Error() string { + var b strings.Builder + + if len(e.MissingCharts) == 1 { + // Single chart - simple message + c := e.MissingCharts[0] + fmt.Fprintf(&b, "Missing HelmChart manifest for chart at %q (%s:%s)\n\n", + c.ChartPath, c.ChartName, c.ChartVersion) + } else { + // Multiple charts - list all + fmt.Fprintf(&b, "Chart validation failed - %d charts missing HelmChart manifests:\n", len(e.MissingCharts)) + for _, c := range e.MissingCharts { + fmt.Fprintf(&b, " - %s (%s:%s)\n", c.ChartPath, c.ChartName, c.ChartVersion) + } + fmt.Fprintln(&b) + } + + fmt.Fprint(&b, "Each Helm chart requires a corresponding HelmChart manifest (kind: HelmChart).\n") + fmt.Fprint(&b, "Ensure the manifests are in paths specified in the 'manifests' section of .replicated config.") + + return b.String() +} + +// ChartToHelmChartValidationResult contains validation results and warnings +type ChartToHelmChartValidationResult struct { + Warnings []string // Non-fatal issues (orphaned HelmChart manifests) +} + +// ValidateChartToHelmChartMapping validates that every chart has a corresponding HelmChart manifest. +// +// Requirements: +// - Every chart in charts must have a matching HelmChart manifest in helmChartManifests +// - Matching key format: "chartName:chartVersion" +// +// Returns: +// - result: Contains warnings (orphaned HelmChart manifests) +// - error: Hard error if any chart is missing its HelmChart manifest (batch reports all missing) +// +// Behavior: +// - Hard error: Chart exists but no matching HelmChart manifest (collects ALL missing, reports together) +// - Warning: HelmChart manifest exists but no matching chart configured +// - Duplicate HelmChart manifests are detected in DiscoverHelmChartManifests() +func ValidateChartToHelmChartMapping( + charts []ChartWithMetadata, + helmChartManifests map[string]*HelmChartManifest, +) (*ChartToHelmChartValidationResult, error) { + result := &ChartToHelmChartValidationResult{ + Warnings: []string{}, + } + + // Track which HelmChart manifests are matched (to detect orphans) + matchedManifests := make(map[string]bool) + + // Collect all missing charts (batch reporting) + var missingCharts []ChartMissingHelmChartInfo + + // Check each chart has a corresponding HelmChart manifest + for _, chart := range charts { + key := fmt.Sprintf("%s:%s", chart.Name, chart.Version) + + if _, found := helmChartManifests[key]; !found { + // Collect missing chart info (don't return yet) + missingCharts = append(missingCharts, ChartMissingHelmChartInfo{ + ChartPath: chart.Path, + ChartName: chart.Name, + ChartVersion: chart.Version, + }) + } else { + // Mark manifest as matched + matchedManifests[key] = true + } + } + + // Return all missing charts at once + if len(missingCharts) > 0 { + return nil, &MultipleChartsMissingHelmChartsError{ + MissingCharts: missingCharts, + } + } + + // Check for orphaned HelmChart manifests (warn but don't error) + for key, manifest := range helmChartManifests { + if !matchedManifests[key] { + warning := fmt.Sprintf( + "HelmChart manifest at %q (%s) has no corresponding chart configured", + manifest.FilePath, key, + ) + result.Warnings = append(result.Warnings, warning) + } + } + + return result, nil +} diff --git a/pkg/lint2/validation_integration_test.go b/pkg/lint2/validation_integration_test.go new file mode 100644 index 000000000..6c59d9485 --- /dev/null +++ b/pkg/lint2/validation_integration_test.go @@ -0,0 +1,376 @@ +//go:build integration +// +build integration + +package lint2 + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/replicatedhq/replicated/pkg/tools" +) + +// TestLintValidation_Success tests successful validation with matching HelmChart manifests +func TestLintValidation_Success(t *testing.T) { + testDir := filepath.Join("testdata", "validation", "scenario-1-success") + + // Change to test directory + 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) + } + + // Load config + parser := tools.NewConfigParser() + config, err := parser.FindAndParseConfig(".") + if err != nil { + t.Fatalf("failed to load config: %v", err) + } + + // Extract charts with metadata + charts, err := GetChartsWithMetadataFromConfig(config) + if err != nil { + t.Fatalf("GetChartsWithMetadataFromConfig failed: %v", err) + } + + // Discover HelmChart manifests + helmCharts, err := DiscoverHelmChartManifests(config.Manifests) + if err != nil { + t.Fatalf("DiscoverHelmChartManifests failed: %v", err) + } + + // Validate + result, err := ValidateChartToHelmChartMapping(charts, helmCharts) + if err != nil { + t.Fatalf("validation failed: %v", err) + } + + // Should have no warnings + if len(result.Warnings) != 0 { + t.Errorf("expected no warnings, got %d: %v", len(result.Warnings), result.Warnings) + } +} + +// TestLintValidation_MissingHelmChart tests error when HelmChart manifest is missing +func TestLintValidation_MissingHelmChart(t *testing.T) { + testDir := filepath.Join("testdata", "validation", "scenario-2-missing-helmchart") + + // Change to test directory + 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) + } + + // Load config + parser := tools.NewConfigParser() + config, err := parser.FindAndParseConfig(".") + if err != nil { + t.Fatalf("failed to load config: %v", err) + } + + // Extract charts with metadata + charts, err := GetChartsWithMetadataFromConfig(config) + if err != nil { + t.Fatalf("GetChartsWithMetadataFromConfig failed: %v", err) + } + + // Discover HelmChart manifests (should be empty) + helmCharts, err := DiscoverHelmChartManifests(config.Manifests) + if err != nil { + t.Fatalf("DiscoverHelmChartManifests failed: %v", err) + } + + // Validate - should fail + result, err := ValidateChartToHelmChartMapping(charts, helmCharts) + if err == nil { + t.Fatal("expected validation error, got nil") + } + + // Should be a MultipleChartsMissingHelmChartsError + multiErr, ok := err.(*MultipleChartsMissingHelmChartsError) + if !ok { + t.Fatalf("expected *MultipleChartsMissingHelmChartsError, got %T", err) + } + + // Should report 1 missing chart + if len(multiErr.MissingCharts) != 1 { + t.Errorf("expected 1 missing chart, got %d", len(multiErr.MissingCharts)) + } + + // Verify chart name + if multiErr.MissingCharts[0].ChartName != "missing-manifest-app" { + t.Errorf("expected chart name 'missing-manifest-app', got %s", multiErr.MissingCharts[0].ChartName) + } + + // Result should be nil on error + if result != nil { + t.Error("expected nil result on error") + } +} + +// TestLintValidation_MultipleCharts tests batch error reporting for multiple missing charts +func TestLintValidation_MultipleCharts(t *testing.T) { + testDir := filepath.Join("testdata", "validation", "scenario-3-multiple-charts") + + // Change to test directory + 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) + } + + // Load config + parser := tools.NewConfigParser() + config, err := parser.FindAndParseConfig(".") + if err != nil { + t.Fatalf("failed to load config: %v", err) + } + + // Extract charts with metadata + charts, err := GetChartsWithMetadataFromConfig(config) + if err != nil { + t.Fatalf("GetChartsWithMetadataFromConfig failed: %v", err) + } + + // Should have 3 charts + if len(charts) != 3 { + t.Fatalf("expected 3 charts, got %d", len(charts)) + } + + // Discover HelmChart manifests (only 2 exist) + helmCharts, err := DiscoverHelmChartManifests(config.Manifests) + if err != nil { + t.Fatalf("DiscoverHelmChartManifests failed: %v", err) + } + + // Should have 2 HelmChart manifests + if len(helmCharts) != 2 { + t.Fatalf("expected 2 HelmChart manifests, got %d", len(helmCharts)) + } + + // Validate - should fail with batch error + result, err := ValidateChartToHelmChartMapping(charts, helmCharts) + if err == nil { + t.Fatal("expected validation error, got nil") + } + + // Should be a MultipleChartsMissingHelmChartsError + multiErr, ok := err.(*MultipleChartsMissingHelmChartsError) + if !ok { + t.Fatalf("expected *MultipleChartsMissingHelmChartsError, got %T", err) + } + + // Should report 1 missing chart (database) + if len(multiErr.MissingCharts) != 1 { + t.Errorf("expected 1 missing chart, got %d", len(multiErr.MissingCharts)) + } + + // Verify the missing chart is 'database' + if multiErr.MissingCharts[0].ChartName != "database" { + t.Errorf("expected missing chart 'database', got %s", multiErr.MissingCharts[0].ChartName) + } + + // Error message should mention batch reporting + errMsg := err.Error() + if !strings.Contains(errMsg, "database") { + t.Errorf("error message should contain 'database': %s", errMsg) + } + + // Result should be nil on error + if result != nil { + t.Error("expected nil result on error") + } +} + +// TestLintValidation_OrphanedManifest tests warning for orphaned HelmChart manifests +func TestLintValidation_OrphanedManifest(t *testing.T) { + testDir := filepath.Join("testdata", "validation", "scenario-4-orphaned-manifest") + + // Change to test directory + 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) + } + + // Load config + parser := tools.NewConfigParser() + config, err := parser.FindAndParseConfig(".") + if err != nil { + t.Fatalf("failed to load config: %v", err) + } + + // Extract charts with metadata + charts, err := GetChartsWithMetadataFromConfig(config) + if err != nil { + t.Fatalf("GetChartsWithMetadataFromConfig failed: %v", err) + } + + // Should have 1 chart + if len(charts) != 1 { + t.Fatalf("expected 1 chart, got %d", len(charts)) + } + + // Discover HelmChart manifests (2 exist, 1 orphaned) + helmCharts, err := DiscoverHelmChartManifests(config.Manifests) + if err != nil { + t.Fatalf("DiscoverHelmChartManifests failed: %v", err) + } + + // Should have 2 HelmChart manifests + if len(helmCharts) != 2 { + t.Fatalf("expected 2 HelmChart manifests, got %d", len(helmCharts)) + } + + // Validate - should succeed with warning + result, err := ValidateChartToHelmChartMapping(charts, helmCharts) + if err != nil { + t.Fatalf("validation failed: %v", err) + } + + // Should have 1 warning + if len(result.Warnings) != 1 { + t.Fatalf("expected 1 warning, got %d: %v", len(result.Warnings), result.Warnings) + } + + // Warning should mention old-app + warning := result.Warnings[0] + if !strings.Contains(warning, "old-app") { + t.Errorf("warning should contain 'old-app': %s", warning) + } + if !strings.Contains(warning, "no corresponding chart") { + t.Errorf("warning should explain issue: %s", warning) + } +} + +// TestLintValidation_NoManifestsConfig tests error when manifests section is missing +func TestLintValidation_NoManifestsConfig(t *testing.T) { + testDir := filepath.Join("testdata", "validation", "scenario-6-no-manifests-config") + + // Change to test directory + 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) + } + + // Load config + parser := tools.NewConfigParser() + config, err := parser.FindAndParseConfig(".") + if err != nil { + t.Fatalf("failed to load config: %v", err) + } + + // Charts should be configured + if len(config.Charts) == 0 { + t.Fatal("expected charts to be configured") + } + + // Manifests should be empty + if len(config.Manifests) != 0 { + t.Fatalf("expected no manifests configured, got %d", len(config.Manifests)) + } + + // This simulates the error check in runLint() + // Should error early before extraction + if len(config.Charts) > 0 && len(config.Manifests) == 0 { + // Expected error path - this is what runLint() checks + t.Log("Correctly detected charts with no manifests config") + } else { + t.Error("should have charts but no manifests") + } +} + +// TestLintValidation_AutoDiscovery tests auto-discovery mode +func TestLintValidation_AutoDiscovery(t *testing.T) { + testDir := filepath.Join("testdata", "validation", "scenario-5-auto-discovery") + + // Change to test directory + 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) + } + + // Simulate auto-discovery (no .replicated file exists) + // Auto-discover charts + chartPaths, err := DiscoverChartPaths(filepath.Join(".", "**")) + if err != nil { + t.Fatalf("failed to auto-discover charts: %v", err) + } + + if len(chartPaths) != 1 { + t.Fatalf("expected 1 chart discovered, got %d", len(chartPaths)) + } + + // Auto-discover HelmChart manifests + helmChartPaths, err := DiscoverHelmChartPaths(filepath.Join(".", "**")) + if err != nil { + t.Fatalf("failed to auto-discover HelmChart manifests: %v", err) + } + + if len(helmChartPaths) != 1 { + t.Fatalf("expected 1 HelmChart manifest discovered, got %d", len(helmChartPaths)) + } + + // Create a temporary config with discovered resources + config := &tools.Config{ + Charts: []tools.ChartConfig{ + {Path: chartPaths[0]}, + }, + Manifests: helmChartPaths, + } + + // Extract charts with metadata + charts, err := GetChartsWithMetadataFromConfig(config) + if err != nil { + t.Fatalf("GetChartsWithMetadataFromConfig failed: %v", err) + } + + // Discover HelmChart manifests + helmCharts, err := DiscoverHelmChartManifests(config.Manifests) + if err != nil { + t.Fatalf("DiscoverHelmChartManifests failed: %v", err) + } + + // Validate - should succeed + result, err := ValidateChartToHelmChartMapping(charts, helmCharts) + if err != nil { + t.Fatalf("validation failed in auto-discovery mode: %v", err) + } + + // Should have no warnings + if len(result.Warnings) != 0 { + t.Errorf("expected no warnings, got %d: %v", len(result.Warnings), result.Warnings) + } + + t.Log("Auto-discovery successfully found and validated chart with HelmChart manifest") +} diff --git a/pkg/lint2/validation_test.go b/pkg/lint2/validation_test.go new file mode 100644 index 000000000..97bc5500f --- /dev/null +++ b/pkg/lint2/validation_test.go @@ -0,0 +1,420 @@ +package lint2 + +import ( + "strings" + "testing" +) + +func TestValidateChartToHelmChartMapping_AllChartsHaveManifests(t *testing.T) { + // Setup: 3 charts, 3 matching HelmChart manifests + charts := []ChartWithMetadata{ + {Path: "/path/to/chart1", Name: "app1", Version: "1.0.0"}, + {Path: "/path/to/chart2", Name: "app2", Version: "2.0.0"}, + {Path: "/path/to/chart3", Name: "app3", Version: "1.5.0"}, + } + + helmCharts := map[string]*HelmChartManifest{ + "app1:1.0.0": {Name: "app1", ChartVersion: "1.0.0", FilePath: "/manifests/app1.yaml"}, + "app2:2.0.0": {Name: "app2", ChartVersion: "2.0.0", FilePath: "/manifests/app2.yaml"}, + "app3:1.5.0": {Name: "app3", ChartVersion: "1.5.0", FilePath: "/manifests/app3.yaml"}, + } + + result, err := ValidateChartToHelmChartMapping(charts, helmCharts) + + // Should succeed with no errors or warnings + if err != nil { + t.Fatalf("expected no error, got: %v", err) + } + + if result == nil { + t.Fatal("expected non-nil result") + } + + if len(result.Warnings) != 0 { + t.Errorf("expected no warnings, got %d: %v", len(result.Warnings), result.Warnings) + } +} + +func TestValidateChartToHelmChartMapping_SingleChartMissing(t *testing.T) { + // Setup: 1 chart, 0 HelmChart manifests + charts := []ChartWithMetadata{ + {Path: "/path/to/chart", Name: "my-app", Version: "1.0.0"}, + } + + helmCharts := map[string]*HelmChartManifest{} // Empty - no manifests + + result, err := ValidateChartToHelmChartMapping(charts, helmCharts) + + // Should return error + if err == nil { + t.Fatal("expected error for missing HelmChart manifest, got nil") + } + + // Should be a MultipleChartsMissingHelmChartsError + multiErr, ok := err.(*MultipleChartsMissingHelmChartsError) + if !ok { + t.Fatalf("expected *MultipleChartsMissingHelmChartsError, got %T", err) + } + + // Should report exactly 1 missing chart + if len(multiErr.MissingCharts) != 1 { + t.Errorf("expected 1 missing chart, got %d", len(multiErr.MissingCharts)) + } + + // Verify error message contains chart details + errMsg := err.Error() + if !strings.Contains(errMsg, "my-app") { + t.Errorf("error message should contain chart name 'my-app': %s", errMsg) + } + if !strings.Contains(errMsg, "1.0.0") { + t.Errorf("error message should contain version '1.0.0': %s", errMsg) + } + if !strings.Contains(errMsg, "/path/to/chart") { + t.Errorf("error message should contain chart path: %s", errMsg) + } + + // Result should be nil on error + if result != nil { + t.Error("expected nil result on error") + } +} + +func TestValidateChartToHelmChartMapping_MultipleChartsMissing(t *testing.T) { + // Setup: 3 charts, 1 matching HelmChart (2 missing) + charts := []ChartWithMetadata{ + {Path: "/charts/frontend", Name: "frontend", Version: "1.0.0"}, + {Path: "/charts/backend", Name: "backend", Version: "2.1.0"}, + {Path: "/charts/database", Name: "database", Version: "1.5.0"}, + } + + helmCharts := map[string]*HelmChartManifest{ + "backend:2.1.0": {Name: "backend", ChartVersion: "2.1.0", FilePath: "/manifests/backend.yaml"}, + // frontend and database are missing + } + + result, err := ValidateChartToHelmChartMapping(charts, helmCharts) + + // Should return error + if err == nil { + t.Fatal("expected error for missing HelmChart manifests, got nil") + } + + // Should be a MultipleChartsMissingHelmChartsError + multiErr, ok := err.(*MultipleChartsMissingHelmChartsError) + if !ok { + t.Fatalf("expected *MultipleChartsMissingHelmChartsError, got %T", err) + } + + // Should report exactly 2 missing charts + if len(multiErr.MissingCharts) != 2 { + t.Errorf("expected 2 missing charts, got %d", len(multiErr.MissingCharts)) + } + + // Verify batch error message format + errMsg := err.Error() + if !strings.Contains(errMsg, "2 charts missing") { + t.Errorf("error message should mention '2 charts missing': %s", errMsg) + } + + // Should list both missing charts + if !strings.Contains(errMsg, "frontend") { + t.Errorf("error message should contain 'frontend': %s", errMsg) + } + if !strings.Contains(errMsg, "database") { + t.Errorf("error message should contain 'database': %s", errMsg) + } + + // Should NOT mention the chart that has a manifest + if strings.Contains(errMsg, "backend") { + t.Errorf("error message should not contain 'backend' (it has a manifest): %s", errMsg) + } + + // Verify error message has actionable guidance + if !strings.Contains(errMsg, "HelmChart manifest") { + t.Errorf("error message should mention HelmChart manifest: %s", errMsg) + } + + // Result should be nil on error + if result != nil { + t.Error("expected nil result on error") + } +} + +func TestValidateChartToHelmChartMapping_OrphanedHelmChartManifest(t *testing.T) { + // Setup: 1 chart, 2 HelmChart manifests (1 orphaned) + charts := []ChartWithMetadata{ + {Path: "/charts/current-app", Name: "current-app", Version: "1.0.0"}, + } + + helmCharts := map[string]*HelmChartManifest{ + "current-app:1.0.0": {Name: "current-app", ChartVersion: "1.0.0", FilePath: "/manifests/current.yaml"}, + "old-app:1.0.0": {Name: "old-app", ChartVersion: "1.0.0", FilePath: "/manifests/old.yaml"}, + } + + result, err := ValidateChartToHelmChartMapping(charts, helmCharts) + + // Should succeed (orphans are warnings, not errors) + if err != nil { + t.Fatalf("expected no error, got: %v", err) + } + + if result == nil { + t.Fatal("expected non-nil result") + } + + // Should have exactly 1 warning + if len(result.Warnings) != 1 { + t.Fatalf("expected 1 warning, got %d: %v", len(result.Warnings), result.Warnings) + } + + // Verify warning message + warning := result.Warnings[0] + if !strings.Contains(warning, "old-app") { + t.Errorf("warning should contain orphaned chart name 'old-app': %s", warning) + } + if !strings.Contains(warning, "old-app:1.0.0") { + t.Errorf("warning should contain orphaned chart key 'old-app:1.0.0': %s", warning) + } + if !strings.Contains(warning, "/manifests/old.yaml") { + t.Errorf("warning should contain orphaned manifest path: %s", warning) + } + if !strings.Contains(warning, "no corresponding chart") { + t.Errorf("warning should explain the issue: %s", warning) + } +} + +func TestValidateChartToHelmChartMapping_EmptyCharts(t *testing.T) { + // Setup: 0 charts, 2 HelmChart manifests + charts := []ChartWithMetadata{} // Empty + + helmCharts := map[string]*HelmChartManifest{ + "app1:1.0.0": {Name: "app1", ChartVersion: "1.0.0", FilePath: "/manifests/app1.yaml"}, + "app2:2.0.0": {Name: "app2", ChartVersion: "2.0.0", FilePath: "/manifests/app2.yaml"}, + } + + result, err := ValidateChartToHelmChartMapping(charts, helmCharts) + + // Should succeed (no charts to validate) + if err != nil { + t.Fatalf("expected no error with empty charts, got: %v", err) + } + + if result == nil { + t.Fatal("expected non-nil result") + } + + // Should warn about all orphaned manifests (both) + if len(result.Warnings) != 2 { + t.Errorf("expected 2 warnings for orphaned manifests, got %d: %v", len(result.Warnings), result.Warnings) + } +} + +func TestValidateChartToHelmChartMapping_EmptyManifests(t *testing.T) { + // Setup: 2 charts, 0 HelmChart manifests + charts := []ChartWithMetadata{ + {Path: "/charts/app1", Name: "app1", Version: "1.0.0"}, + {Path: "/charts/app2", Name: "app2", Version: "2.0.0"}, + } + + helmCharts := map[string]*HelmChartManifest{} // Empty + + result, err := ValidateChartToHelmChartMapping(charts, helmCharts) + + // Should error (all charts missing manifests) + if err == nil { + t.Fatal("expected error when all charts missing manifests, got nil") + } + + // Should be a batch error + multiErr, ok := err.(*MultipleChartsMissingHelmChartsError) + if !ok { + t.Fatalf("expected *MultipleChartsMissingHelmChartsError, got %T", err) + } + + // Should report both charts + if len(multiErr.MissingCharts) != 2 { + t.Errorf("expected 2 missing charts, got %d", len(multiErr.MissingCharts)) + } + + // Result should be nil on error + if result != nil { + t.Error("expected nil result on error") + } +} + +func TestValidateChartToHelmChartMapping_VersionMismatch(t *testing.T) { + // Setup: Chart "app:1.0.0", HelmChart "app:2.0.0" (version mismatch) + charts := []ChartWithMetadata{ + {Path: "/charts/app", Name: "app", Version: "1.0.0"}, + } + + helmCharts := map[string]*HelmChartManifest{ + "app:2.0.0": {Name: "app", ChartVersion: "2.0.0", FilePath: "/manifests/app.yaml"}, + // app:1.0.0 is missing + } + + result, err := ValidateChartToHelmChartMapping(charts, helmCharts) + + // Should error (version doesn't match, so chart is considered missing) + if err == nil { + t.Fatal("expected error for version mismatch, got nil") + } + + // Should report the chart with correct version as missing + errMsg := err.Error() + if !strings.Contains(errMsg, "app") { + t.Errorf("error message should contain chart name: %s", errMsg) + } + if !strings.Contains(errMsg, "1.0.0") { + t.Errorf("error message should contain chart version 1.0.0: %s", errMsg) + } + + // Result should be nil on error + if result != nil { + t.Error("expected nil result on error") + } + + // There should be a warning about the orphaned 2.0.0 manifest + // (Actually, this won't happen because result is nil on error) + // The validation reports the chart as missing, user needs to fix the version +} + +func TestValidateChartToHelmChartMapping_NameMismatch(t *testing.T) { + // Setup: Chart "my-app:1.0.0", HelmChart "my-app-v2:1.0.0" (name mismatch) + charts := []ChartWithMetadata{ + {Path: "/charts/my-app", Name: "my-app", Version: "1.0.0"}, + } + + helmCharts := map[string]*HelmChartManifest{ + "my-app-v2:1.0.0": {Name: "my-app-v2", ChartVersion: "1.0.0", FilePath: "/manifests/my-app-v2.yaml"}, + // my-app:1.0.0 is missing + } + + result, err := ValidateChartToHelmChartMapping(charts, helmCharts) + + // Should error (name doesn't match) + if err == nil { + t.Fatal("expected error for name mismatch, got nil") + } + + errMsg := err.Error() + if !strings.Contains(errMsg, "my-app") { + t.Errorf("error message should contain chart name 'my-app': %s", errMsg) + } + + // Result should be nil on error + if result != nil { + t.Error("expected nil result on error") + } +} + +func TestValidateChartToHelmChartMapping_MultipleOrphans(t *testing.T) { + // Setup: 1 chart, 3 HelmChart manifests (2 orphaned) + charts := []ChartWithMetadata{ + {Path: "/charts/current", Name: "current", Version: "3.0.0"}, + } + + helmCharts := map[string]*HelmChartManifest{ + "current:3.0.0": {Name: "current", ChartVersion: "3.0.0", FilePath: "/manifests/current.yaml"}, + "old-v1:1.0.0": {Name: "old-v1", ChartVersion: "1.0.0", FilePath: "/manifests/old-v1.yaml"}, + "old-v2:2.0.0": {Name: "old-v2", ChartVersion: "2.0.0", FilePath: "/manifests/old-v2.yaml"}, + } + + result, err := ValidateChartToHelmChartMapping(charts, helmCharts) + + // Should succeed + if err != nil { + t.Fatalf("expected no error, got: %v", err) + } + + if result == nil { + t.Fatal("expected non-nil result") + } + + // Should have 2 warnings + if len(result.Warnings) != 2 { + t.Fatalf("expected 2 warnings, got %d: %v", len(result.Warnings), result.Warnings) + } + + // Both warnings should mention orphaned manifests + warningsStr := strings.Join(result.Warnings, " ") + if !strings.Contains(warningsStr, "old-v1") { + t.Errorf("warnings should contain 'old-v1': %v", result.Warnings) + } + if !strings.Contains(warningsStr, "old-v2") { + t.Errorf("warnings should contain 'old-v2': %v", result.Warnings) + } +} + +func TestMultipleChartsMissingHelmChartsError_SingleChartMessage(t *testing.T) { + // Test the error message format for a single chart + err := &MultipleChartsMissingHelmChartsError{ + MissingCharts: []ChartMissingHelmChartInfo{ + { + ChartPath: "/charts/my-app", + ChartName: "my-app", + ChartVersion: "1.0.0", + }, + }, + } + + msg := err.Error() + + // Should use singular message format + if strings.Contains(msg, "charts missing") { + t.Errorf("single chart error should not use plural format: %s", msg) + } + + // Should contain chart details + if !strings.Contains(msg, "my-app") { + t.Errorf("error should contain chart name: %s", msg) + } + if !strings.Contains(msg, "1.0.0") { + t.Errorf("error should contain version: %s", msg) + } + if !strings.Contains(msg, "/charts/my-app") { + t.Errorf("error should contain path: %s", msg) + } + + // Should contain guidance + if !strings.Contains(msg, "HelmChart manifest") { + t.Errorf("error should mention HelmChart manifest: %s", msg) + } +} + +func TestMultipleChartsMissingHelmChartsError_MultipleChartsMessage(t *testing.T) { + // Test the error message format for multiple charts + err := &MultipleChartsMissingHelmChartsError{ + MissingCharts: []ChartMissingHelmChartInfo{ + {ChartPath: "/charts/app1", ChartName: "app1", ChartVersion: "1.0.0"}, + {ChartPath: "/charts/app2", ChartName: "app2", ChartVersion: "2.0.0"}, + {ChartPath: "/charts/app3", ChartName: "app3", ChartVersion: "3.0.0"}, + }, + } + + msg := err.Error() + + // Should use plural message format + if !strings.Contains(msg, "3 charts missing") { + t.Errorf("multiple charts error should show count: %s", msg) + } + + // Should list all charts + if !strings.Contains(msg, "app1") || !strings.Contains(msg, "1.0.0") { + t.Errorf("error should contain app1: %s", msg) + } + if !strings.Contains(msg, "app2") || !strings.Contains(msg, "2.0.0") { + t.Errorf("error should contain app2: %s", msg) + } + if !strings.Contains(msg, "app3") || !strings.Contains(msg, "3.0.0") { + t.Errorf("error should contain app3: %s", msg) + } + + // Should contain guidance + if !strings.Contains(msg, "HelmChart manifest") { + t.Errorf("error should mention HelmChart manifest: %s", msg) + } + if !strings.Contains(msg, "manifests") && !strings.Contains(msg, ".replicated") { + t.Errorf("error should mention configuration: %s", msg) + } +} diff --git a/pkg/tools/config.go b/pkg/tools/config.go index 0ded178b4..1a014b0c5 100644 --- a/pkg/tools/config.go +++ b/pkg/tools/config.go @@ -322,10 +322,10 @@ func (p *ConfigParser) validateConfig(config *Config) error { } } - // Validate manifest paths + // Validate manifest paths (array can be empty, but elements cannot be empty strings) for i, manifest := range config.Manifests { if manifest == "" { - return fmt.Errorf("manifest[%d]: path is required", i) + return fmt.Errorf("manifest[%d]: path cannot be empty string", i) } } diff --git a/pkg/tools/config_test.go b/pkg/tools/config_test.go index 4fddbadce..615942bc5 100644 --- a/pkg/tools/config_test.go +++ b/pkg/tools/config_test.go @@ -1034,8 +1034,8 @@ repl-lint: if err == nil { t.Error("ParseConfigFile() expected error for empty manifest path, got nil") } - if !strings.Contains(err.Error(), "manifest[0]: path is required") { - t.Errorf("Expected 'manifest[0]: path is required' error, got: %v", err) + if !strings.Contains(err.Error(), "manifest[0]: path cannot be empty string") { + t.Errorf("Expected 'manifest[0]: path cannot be empty string' error, got: %v", err) } })