Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 21 additions & 7 deletions cli/cmd/lint.go
Original file line number Diff line number Diff line change
Expand Up @@ -340,27 +340,41 @@ func (r *runners) lintPreflightSpecs(cmd *cobra.Command, config *tools.Config) (
}
}

// Check if there are any preflight specs configured
preflightPaths, err := lint2.GetPreflightPathsFromConfig(config)
// Discover HelmChart manifests once (needed for templated preflights)
helmChartManifests, err := lint2.DiscoverHelmChartManifests(config.Manifests)
if err != nil {
return nil, errors.Wrap(err, "failed to discover HelmChart manifests")
}

// Get preflight paths with values information
preflights, err := lint2.GetPreflightWithValuesFromConfig(config)
if err != nil {
return nil, errors.Wrap(err, "failed to expand preflight paths")
}

results := &PreflightLintResults{
Enabled: true,
Specs: make([]PreflightLintResult, 0, len(preflightPaths)),
Specs: make([]PreflightLintResult, 0, len(preflights)),
}

// Lint all preflight specs and collect results
for _, specPath := range preflightPaths {
lint2Result, err := lint2.LintPreflight(cmd.Context(), specPath, preflightVersion)
for _, pf := range preflights {
lint2Result, err := lint2.LintPreflight(
cmd.Context(),
pf.SpecPath,
pf.ValuesPath,
pf.ChartName,
pf.ChartVersion,
helmChartManifests,
preflightVersion,
)
if err != nil {
return nil, errors.Wrapf(err, "failed to lint preflight spec: %s", specPath)
return nil, errors.Wrapf(err, "failed to lint preflight spec: %s", pf.SpecPath)
}

// Convert to structured format
preflightResult := PreflightLintResult{
Path: specPath,
Path: pf.SpecPath,
Success: lint2Result.Success,
Messages: convertLint2Messages(lint2Result.Messages),
Summary: calculateResourceSummary(lint2Result.Messages),
Expand Down
108 changes: 108 additions & 0 deletions pkg/lint2/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"path/filepath"

"github.com/replicatedhq/replicated/pkg/tools"
"gopkg.in/yaml.v3"
)

// GetChartPathsFromConfig extracts and expands chart paths from config
Expand Down Expand Up @@ -152,3 +153,110 @@ func validatePreflightPath(path string) error {

return nil
}

// PreflightWithValues contains preflight spec path and associated chart/values information
// All fields are required - every preflight must have an associated chart structure
type PreflightWithValues struct {
SpecPath string // Path to the preflight spec file
ValuesPath string // Path to values.yaml for template rendering (required)
ChartName string // Chart name from Chart.yaml (required)
ChartVersion string // Chart version from Chart.yaml (required)
}

// ChartMetadata represents the minimal Chart.yaml structure needed for matching
type ChartMetadata struct {
Name string `yaml:"name"`
Version string `yaml:"version"`
}

// parseChartYaml reads and parses a Chart.yaml file
func parseChartYaml(path string) (*ChartMetadata, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read Chart.yaml: %w", err)
}

var chart ChartMetadata
if err := yaml.Unmarshal(data, &chart); err != nil {
return nil, fmt.Errorf("failed to parse Chart.yaml: %w", err)
}

if chart.Name == "" {
return nil, fmt.Errorf("Chart.yaml missing required field: name")
}
if chart.Version == "" {
return nil, fmt.Errorf("Chart.yaml missing required field: version")
}

return &chart, nil
}

// GetPreflightWithValuesFromConfig extracts preflight paths with associated chart/values information
func GetPreflightWithValuesFromConfig(config *tools.Config) ([]PreflightWithValues, error) {
if len(config.Preflights) == 0 {
return nil, fmt.Errorf("no preflights found in .replicated config")
}

var results []PreflightWithValues

for _, preflightConfig := range config.Preflights {
// Handle glob patterns in preflight path
var specPaths []string
if containsGlob(preflightConfig.Path) {
matches, err := discoverPreflightPaths(preflightConfig.Path)
if err != nil {
return nil, fmt.Errorf("failed to discover preflights from pattern %s: %w", preflightConfig.Path, err)
}
if len(matches) == 0 {
return nil, fmt.Errorf("no preflight specs found matching pattern: %s", preflightConfig.Path)
}
specPaths = matches
} else {
if err := validatePreflightPath(preflightConfig.Path); err != nil {
return nil, fmt.Errorf("invalid preflight spec path %s: %w", preflightConfig.Path, err)
}
specPaths = []string{preflightConfig.Path}
}

// Create PreflightWithValues for each discovered spec
for _, specPath := range specPaths {
// valuesPath is REQUIRED - error if missing
if preflightConfig.ValuesPath == "" {
return nil, fmt.Errorf("preflight (%s) missing required field 'valuesPath'\n"+
"All preflights must specify a valuesPath pointing to chart values.yaml", specPath)
}

result := PreflightWithValues{
SpecPath: specPath,
ValuesPath: preflightConfig.ValuesPath,
}

// Extract chart metadata (always required)
valuesDir := filepath.Dir(preflightConfig.ValuesPath)
chartYamlPath := filepath.Join(valuesDir, "Chart.yaml")

// Try Chart.yml as fallback
if _, err := os.Stat(chartYamlPath); err != nil {
chartYmlPath := filepath.Join(valuesDir, "Chart.yml")
if _, err := os.Stat(chartYmlPath); err == nil {
chartYamlPath = chartYmlPath
} else {
return nil, fmt.Errorf("Chart.yaml not found for preflight\nExpected at: %s\nPreflight: %s", chartYamlPath, specPath)
}
}

// Parse Chart.yaml to get name and version
chart, err := parseChartYaml(chartYamlPath)
if err != nil {
return nil, fmt.Errorf("failed to parse Chart.yaml for preflight %s: %w", specPath, err)
}

result.ChartName = chart.Name
result.ChartVersion = chart.Version

results = append(results, result)
}
}

return results, nil
}
51 changes: 51 additions & 0 deletions pkg/lint2/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1135,3 +1135,54 @@ spec:
t.Errorf("DiscoverSupportBundlesFromManifests() returned path %q, want %q", paths[0], bundlePath)
}
}

func TestGetPreflightWithValuesFromConfig_MissingChartYaml(t *testing.T) {
// Test that GetPreflightWithValuesFromConfig errors when valuesPath is set but Chart.yaml is missing
tmpDir := t.TempDir()

// Create a preflight spec
preflightPath := filepath.Join(tmpDir, "preflight.yaml")
preflightContent := `apiVersion: troubleshoot.sh/v1beta2
kind: Preflight
metadata:
name: test
spec:
collectors: []
`
if err := os.WriteFile(preflightPath, []byte(preflightContent), 0644); err != nil {
t.Fatal(err)
}

// Create a values.yaml file WITHOUT adjacent Chart.yaml
valuesDir := filepath.Join(tmpDir, "chart")
if err := os.MkdirAll(valuesDir, 0755); err != nil {
t.Fatal(err)
}
valuesPath := filepath.Join(valuesDir, "values.yaml")
valuesContent := `database:
enabled: true
`
if err := os.WriteFile(valuesPath, []byte(valuesContent), 0644); err != nil {
t.Fatal(err)
}

// Config with valuesPath but no Chart.yaml
config := &tools.Config{
Preflights: []tools.PreflightConfig{
{
Path: preflightPath,
ValuesPath: valuesPath,
},
},
}

_, err := GetPreflightWithValuesFromConfig(config)
if err == nil {
t.Fatal("GetPreflightWithValuesFromConfig() should error when Chart.yaml is missing, got nil")
}

// Error should mention Chart.yaml not found
if !contains(err.Error(), "Chart.yaml not found") {
t.Errorf("Error should mention Chart.yaml not found, got: %v", err)
}
}
8 changes: 8 additions & 0 deletions pkg/lint2/helmchart.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,14 @@ func DiscoverHelmChartManifests(manifestGlobs []string) (map[string]*HelmChartMa
}
}

// Fail-fast if no HelmCharts found - prevents confusing "no HelmChart manifest found for chart X" error later
// This is required because preflights with valuesPath always need builder values from a HelmChart manifest
if len(helmCharts) == 0 {
return nil, fmt.Errorf("no HelmChart resources found in manifests\n"+
"At least one HelmChart manifest is required for preflight linting\n"+
"Checked patterns: %v", manifestGlobs)
}

return helmCharts, nil
}

Expand Down
28 changes: 20 additions & 8 deletions pkg/lint2/helmchart_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -313,12 +313,18 @@ spec:

pattern := filepath.Join(tmpDir, "*.yaml")
manifests, err := DiscoverHelmChartManifests([]string{pattern})
if err != nil {
t.Fatalf("unexpected error: %v", err)

// With fail-fast validation, we now 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)
}

if len(manifests) != 0 {
t.Fatalf("expected 0 manifests (invalid files skipped), got %d", len(manifests))
if manifests != nil {
t.Errorf("expected nil manifests on error, got %d manifests", len(manifests))
}
})

Expand All @@ -338,12 +344,18 @@ spec:

pattern := filepath.Join(tmpDir, "*.yaml")
manifests, err := DiscoverHelmChartManifests([]string{pattern})
if err != nil {
t.Fatalf("unexpected error (should skip invalid YAML): %v", err)

// With fail-fast validation, we now 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)
}

if len(manifests) != 0 {
t.Fatalf("expected 0 manifests (invalid YAML skipped), got %d", len(manifests))
if manifests != nil {
t.Errorf("expected nil manifests on error, got %d manifests", len(manifests))
}
})

Expand Down
Loading