diff --git a/cli/cmd/lint.go b/cli/cmd/lint.go index 6eb9c1945..53c4e9d81 100644 --- a/cli/cmd/lint.go +++ b/cli/cmd/lint.go @@ -2,10 +2,13 @@ package cmd import ( "context" + "encoding/json" "fmt" "io" + "os" "path/filepath" "strings" + "text/tabwriter" "github.com/manifoldco/promptui" "github.com/pkg/errors" @@ -18,13 +21,35 @@ import ( func (r *runners) InitLint(parent *cobra.Command) *cobra.Command { cmd := &cobra.Command{ - Use: "lint", - Short: "Lint Helm charts, Preflight specs, and Support Bundle specs", - Long: `Lint Helm charts, Preflight specs, and Support Bundle specs defined in .replicated config file. This command reads paths from the .replicated config and executes linting locally on each resource. Use --verbose to also display extracted container images.`, + Use: "lint", + Short: "Lint Helm charts, Preflight specs, and Support Bundle specs", + Long: `Lint Helm charts, Preflight specs, and Support Bundle specs defined in .replicated config file. + +This command reads paths from the .replicated config and executes linting locally +on each resource. Use --verbose to also display extracted container images.`, + Example: ` # Lint with default table output + replicated lint + + # Output JSON to stdout + replicated lint --format json + + # Save results to file (writes to both stdout and file) + replicated lint --output results.txt + + # Save JSON results to file + replicated lint --format json --output results.json + + # Use in CI/CD pipelines + replicated lint --format json | jq '.summary.overall_success' + + # Verbose mode with image extraction + replicated lint --verbose --format json`, SilenceUsage: true, } cmd.Flags().BoolVarP(&r.args.lintVerbose, "verbose", "v", false, "Show detailed output including extracted container images") + cmd.Flags().StringVar(&r.outputFormat, "format", "table", "The output format to use. One of: json|table") + cmd.Flags().StringVarP(&r.args.lintOutputFile, "output", "o", "", "Write output to file at specified path") cmd.RunE = r.runLint @@ -33,6 +58,11 @@ func (r *runners) InitLint(parent *cobra.Command) *cobra.Command { } func (r *runners) runLint(cmd *cobra.Command, args []string) error { + // Validate format + if r.outputFormat != "table" && r.outputFormat != "json" { + return errors.Errorf("invalid format: %s. Supported formats: json, table", r.outputFormat) + } + // Load .replicated config using tools parser (supports monorepos) parser := tools.NewConfigParser() config, err := parser.FindAndParseConfig(".") @@ -74,84 +104,142 @@ func (r *runners) runLint(cmd *cobra.Command, args []string) error { } } - hasFailure := false + // Initialize JSON output structure + output := &JSONLintOutput{} + + // Get Helm version from config + helmVersion := tools.DefaultHelmVersion + if config.ReplLint.Tools != nil { + if v, ok := config.ReplLint.Tools[tools.ToolHelm]; ok { + helmVersion = v + } + } + + // Populate metadata + configPath := findConfigFilePath(".") + output.Metadata = newLintMetadata(configPath, helmVersion, "v0.90.0") // TODO: Get actual CLI version // Extract and display images if verbose mode is enabled if r.args.lintVerbose { - if err := r.extractAndDisplayImagesFromConfig(cmd.Context(), config); err != nil { + imageResults, err := r.extractImagesFromConfig(cmd.Context(), config) + if err != nil { // Log warning but don't fail the lint command - fmt.Fprintf(r.w, "Warning: Failed to extract images: %v\n\n", err) - r.w.Flush() + if r.outputFormat == "table" { + fmt.Fprintf(r.w, "Warning: Failed to extract images: %v\n\n", err) + r.w.Flush() + } + } else { + output.Images = imageResults + // Display images (only for table format) + if r.outputFormat == "table" { + r.displayImages(imageResults) + + // Print separator + fmt.Fprintln(r.w, "────────────────────────────────────────────────────────────────────────────") + fmt.Fprintln(r.w, "\nRunning lint checks...") + fmt.Fprintln(r.w) + r.w.Flush() + } } - - // Print separator - fmt.Fprintln(r.w, "────────────────────────────────────────────────────────────────────────────") - fmt.Fprintln(r.w, "\nRunning lint checks...") - fmt.Fprintln(r.w) - r.w.Flush() } // Lint Helm charts if enabled if config.ReplLint.Linters.Helm.IsEnabled() { if len(config.Charts) == 0 { - fmt.Fprintf(r.w, "No Helm charts configured (skipping Helm linting)\n\n") + output.HelmResults = &HelmLintResults{Enabled: true, Charts: []ChartLintResult{}} + if r.outputFormat == "table" { + fmt.Fprintf(r.w, "No Helm charts configured (skipping Helm linting)\n\n") + } } else { - helmFailed, err := r.lintHelmCharts(cmd, config) + helmResults, err := r.lintHelmCharts(cmd, config) if err != nil { return err } - if helmFailed { - hasFailure = true - } + output.HelmResults = helmResults } } else { - fmt.Fprintf(r.w, "Helm linting is disabled in .replicated config\n\n") + output.HelmResults = &HelmLintResults{Enabled: false, Charts: []ChartLintResult{}} + if r.outputFormat == "table" { + fmt.Fprintf(r.w, "Helm linting is disabled in .replicated config\n\n") + } } // Lint Preflight specs if enabled if config.ReplLint.Linters.Preflight.IsEnabled() { if len(config.Preflights) == 0 { - fmt.Fprintf(r.w, "No preflight specs configured (skipping preflight linting)\n\n") + output.PreflightResults = &PreflightLintResults{Enabled: true, Specs: []PreflightLintResult{}} + if r.outputFormat == "table" { + fmt.Fprintf(r.w, "No preflight specs configured (skipping preflight linting)\n\n") + } } else { - preflightFailed, err := r.lintPreflightSpecs(cmd, config) + preflightResults, err := r.lintPreflightSpecs(cmd, config) if err != nil { return err } - if preflightFailed { - hasFailure = true - } + output.PreflightResults = preflightResults } } else { - fmt.Fprintf(r.w, "Preflight linting is disabled in .replicated config\n\n") + output.PreflightResults = &PreflightLintResults{Enabled: false, Specs: []PreflightLintResult{}} + if r.outputFormat == "table" { + fmt.Fprintf(r.w, "Preflight linting is disabled in .replicated config\n\n") + } } // Lint Support Bundle specs if enabled if config.ReplLint.Linters.SupportBundle.IsEnabled() { - sbFailed, err := r.lintSupportBundleSpecs(cmd, config) + sbResults, err := r.lintSupportBundleSpecs(cmd, config) if err != nil { return err } - if sbFailed { - hasFailure = true + output.SupportBundleResults = sbResults + } else { + output.SupportBundleResults = &SupportBundleLintResults{Enabled: false, Specs: []SupportBundleLintResult{}} + if r.outputFormat == "table" { + fmt.Fprintf(r.w, "Support Bundle linting is disabled in .replicated config\n\n") + } + } + + // Calculate overall summary + output.Summary = r.calculateOverallSummary(output) + + // Check if output file already exists + if r.args.lintOutputFile != "" { + if _, err := os.Stat(r.args.lintOutputFile); err == nil { + return errors.Errorf("file already exists: %s. Please specify a different path or remove the existing file", r.args.lintOutputFile) + } else if !os.IsNotExist(err) { + return errors.Wrapf(err, "failed to check if file exists: %s", r.args.lintOutputFile) + } + } + + // Output to stdout + if r.outputFormat == "json" { + if err := print.LintResults(r.outputFormat, r.w, output); err != nil { + return errors.Wrap(err, "failed to print JSON output to stdout") } } else { - fmt.Fprintf(r.w, "Support Bundle linting is disabled in .replicated config\n\n") + // Table format was already displayed by individual display functions + // Just flush the writer + if err := r.w.Flush(); err != nil { + return errors.Wrap(err, "failed to flush output") + } } - // Flush the tab writer - if err := r.w.Flush(); err != nil { - return errors.Wrap(err, "failed to flush output") + // Output to file if specified + if r.args.lintOutputFile != "" { + if err := r.writeOutputToFile(output); err != nil { + return errors.Wrapf(err, "failed to write output to file: %s", r.args.lintOutputFile) + } } // Return error if any linting failed - if hasFailure { + if !output.Summary.OverallSuccess { return errors.New("linting failed") } return nil } -func (r *runners) lintHelmCharts(cmd *cobra.Command, config *tools.Config) (bool, error) { +func (r *runners) lintHelmCharts(cmd *cobra.Command, config *tools.Config) (*HelmLintResults, error) { // Get helm version from config helmVersion := tools.DefaultHelmVersion if config.ReplLint.Tools != nil { @@ -163,37 +251,42 @@ func (r *runners) lintHelmCharts(cmd *cobra.Command, config *tools.Config) (bool // Check if there are any charts configured chartPaths, err := lint2.GetChartPathsFromConfig(config) if err != nil { - return false, errors.Wrap(err, "failed to expand chart paths") + return nil, errors.Wrap(err, "failed to expand chart paths") } - // Lint all charts and collect results - var allResults []*lint2.LintResult - var allPaths []string - hasFailure := false + results := &HelmLintResults{ + Enabled: true, + Charts: make([]ChartLintResult, 0, len(chartPaths)), + } + // Lint all charts and collect results for _, chartPath := range chartPaths { - result, err := lint2.LintChart(cmd.Context(), chartPath, helmVersion) + lint2Result, err := lint2.LintChart(cmd.Context(), chartPath, helmVersion) if err != nil { - return false, errors.Wrapf(err, "failed to lint chart: %s", chartPath) + return nil, errors.Wrapf(err, "failed to lint chart: %s", chartPath) } - allResults = append(allResults, result) - allPaths = append(allPaths, chartPath) - - if !result.Success { - hasFailure = true + // Convert to structured format + chartResult := ChartLintResult{ + Path: chartPath, + Success: lint2Result.Success, + Messages: convertLint2Messages(lint2Result.Messages), + Summary: calculateResourceSummary(lint2Result.Messages), } + results.Charts = append(results.Charts, chartResult) } - // Display results for all charts - if err := displayAllLintResults(r.w, "chart", allPaths, allResults); err != nil { - return false, errors.Wrap(err, "failed to display lint results") + // Display results in table format (only if table output) + if r.outputFormat == "table" { + if err := r.displayHelmResults(results); err != nil { + return nil, errors.Wrap(err, "failed to display helm results") + } } - return hasFailure, nil + return results, nil } -func (r *runners) lintPreflightSpecs(cmd *cobra.Command, config *tools.Config) (bool, error) { +func (r *runners) lintPreflightSpecs(cmd *cobra.Command, config *tools.Config) (*PreflightLintResults, error) { // Get preflight version from config preflightVersion := tools.DefaultPreflightVersion if config.ReplLint.Tools != nil { @@ -205,37 +298,42 @@ func (r *runners) lintPreflightSpecs(cmd *cobra.Command, config *tools.Config) ( // Check if there are any preflight specs configured preflightPaths, err := lint2.GetPreflightPathsFromConfig(config) if err != nil { - return false, errors.Wrap(err, "failed to expand preflight paths") + return nil, errors.Wrap(err, "failed to expand preflight paths") } - // Lint all preflight specs and collect results - var allResults []*lint2.LintResult - var allPaths []string - hasFailure := false + results := &PreflightLintResults{ + Enabled: true, + Specs: make([]PreflightLintResult, 0, len(preflightPaths)), + } + // Lint all preflight specs and collect results for _, specPath := range preflightPaths { - result, err := lint2.LintPreflight(cmd.Context(), specPath, preflightVersion) + lint2Result, err := lint2.LintPreflight(cmd.Context(), specPath, preflightVersion) if err != nil { - return false, errors.Wrapf(err, "failed to lint preflight spec: %s", specPath) + return nil, errors.Wrapf(err, "failed to lint preflight spec: %s", specPath) } - allResults = append(allResults, result) - allPaths = append(allPaths, specPath) - - if !result.Success { - hasFailure = true + // Convert to structured format + preflightResult := PreflightLintResult{ + Path: specPath, + Success: lint2Result.Success, + Messages: convertLint2Messages(lint2Result.Messages), + Summary: calculateResourceSummary(lint2Result.Messages), } + results.Specs = append(results.Specs, preflightResult) } - // Display results for all preflight specs - if err := displayAllLintResults(r.w, "preflight spec", allPaths, allResults); err != nil { - return false, errors.Wrap(err, "failed to display lint results") + // Display results in table format (only if table output) + if r.outputFormat == "table" { + if err := r.displayPreflightResults(results); err != nil { + return nil, errors.Wrap(err, "failed to display preflight results") + } } - return hasFailure, nil + return results, nil } -func (r *runners) lintSupportBundleSpecs(cmd *cobra.Command, config *tools.Config) (bool, error) { +func (r *runners) lintSupportBundleSpecs(cmd *cobra.Command, config *tools.Config) (*SupportBundleLintResults, error) { // Get support-bundle version from config sbVersion := tools.DefaultSupportBundleVersion if config.ReplLint.Tools != nil { @@ -249,39 +347,44 @@ func (r *runners) lintSupportBundleSpecs(cmd *cobra.Command, config *tools.Confi // unlike preflights which are moving to a separate location sbPaths, err := lint2.DiscoverSupportBundlesFromManifests(config.Manifests) if err != nil { - return false, errors.Wrap(err, "failed to discover support bundle specs from manifests") + return nil, errors.Wrap(err, "failed to discover support bundle specs from manifests") + } + + results := &SupportBundleLintResults{ + Enabled: true, + Specs: make([]SupportBundleLintResult, 0, len(sbPaths)), } // If no support bundles found, that's not an error - they're optional if len(sbPaths) == 0 { - return false, nil + return results, nil } // Lint all support bundle specs and collect results - var allResults []*lint2.LintResult - var allPaths []string - hasFailure := false - for _, specPath := range sbPaths { - result, err := lint2.LintSupportBundle(cmd.Context(), specPath, sbVersion) + lint2Result, err := lint2.LintSupportBundle(cmd.Context(), specPath, sbVersion) if err != nil { - return false, errors.Wrapf(err, "failed to lint support bundle spec: %s", specPath) + return nil, errors.Wrapf(err, "failed to lint support bundle spec: %s", specPath) } - allResults = append(allResults, result) - allPaths = append(allPaths, specPath) - - if !result.Success { - hasFailure = true + // Convert to structured format + sbResult := SupportBundleLintResult{ + Path: specPath, + Success: lint2Result.Success, + Messages: convertLint2Messages(lint2Result.Messages), + Summary: calculateResourceSummary(lint2Result.Messages), } + results.Specs = append(results.Specs, sbResult) } - // Display results for all support bundle specs - if err := displayAllLintResults(r.w, "support bundle spec", allPaths, allResults); err != nil { - return false, errors.Wrap(err, "failed to display lint results") + // Display results in table format (only if table output) + if r.outputFormat == "table" { + if err := r.displaySupportBundleResults(results); err != nil { + return nil, errors.Wrap(err, "failed to display support bundle results") + } } - return hasFailure, nil + return results, nil } type resourceSummary struct { @@ -480,39 +583,40 @@ func (r *runners) initConfigForLint(cmd *cobra.Command) error { return nil } -func (r *runners) extractAndDisplayImagesFromConfig(ctx context.Context, config *tools.Config) error { +// extractImagesFromConfig extracts images from charts and returns structured results +func (r *runners) extractImagesFromConfig(ctx context.Context, config *tools.Config) (*ImageExtractResults, error) { extractor := imageextract.NewExtractor() opts := imageextract.Options{ IncludeDuplicates: false, - NoWarnings: true, // Suppress warnings in lint context + NoWarnings: false, } - fmt.Fprintln(r.w, "Extracting images from Helm charts...") - fmt.Fprintln(r.w) - r.w.Flush() - // Get chart paths from config chartPaths, err := lint2.GetChartPathsFromConfig(config) if err != nil { - return errors.Wrap(err, "failed to get chart paths from config") + return nil, errors.Wrap(err, "failed to get chart paths from config") } if len(chartPaths) == 0 { - fmt.Fprintln(r.w, "No Helm charts found in .replicated config") - fmt.Fprintln(r.w) - r.w.Flush() - return nil + return &ImageExtractResults{ + Images: []imageextract.ImageRef{}, + Warnings: []imageextract.Warning{}, + Summary: ImageSummary{TotalImages: 0, UniqueImages: 0}, + }, nil } // Collect all images from all charts - var allImages []imageextract.ImageRef imageMap := make(map[string]imageextract.ImageRef) // For deduplication + var allWarnings []imageextract.Warning for _, chartPath := range chartPaths { result, err := extractor.ExtractFromChart(ctx, chartPath, opts) if err != nil { - fmt.Fprintf(r.w, "Warning: Failed to extract images from %s: %v\n", chartPath, err) + allWarnings = append(allWarnings, imageextract.Warning{ + Image: chartPath, + Message: fmt.Sprintf("Failed to extract images: %v", err), + }) continue } @@ -526,23 +630,487 @@ func (r *runners) extractAndDisplayImagesFromConfig(ctx context.Context, config imageMap[img.Raw] = img } } + + allWarnings = append(allWarnings, result.Warnings...) } // Convert map back to slice + var allImages []imageextract.ImageRef for _, img := range imageMap { allImages = append(allImages, img) } - // Create a result with all images - combinedResult := &imageextract.Result{ - Images: allImages, + return &ImageExtractResults{ + Images: allImages, + Warnings: allWarnings, + Summary: ImageSummary{ + TotalImages: len(allImages), + UniqueImages: len(allImages), + }, + }, nil +} + +// displayImages displays image extraction results +func (r *runners) displayImages(results *ImageExtractResults) { + if results == nil { + return + } + + fmt.Fprintln(r.w, "Extracting images from Helm charts...") + fmt.Fprintln(r.w) + r.w.Flush() + + // Create a result for the print function + printResult := &imageextract.Result{ + Images: results.Images, + Warnings: results.Warnings, } // Print images using existing print function - if err := print.Images("table", r.w, combinedResult); err != nil { - return err + if err := print.Images("table", r.w, printResult); err != nil { + fmt.Fprintf(r.w, "Warning: Failed to display images: %v\n", err) + } + + fmt.Fprintf(r.w, "\nFound %d unique images\n\n", results.Summary.UniqueImages) + r.w.Flush() +} + +// calculateOverallSummary calculates the overall summary from all results +func (r *runners) calculateOverallSummary(output *JSONLintOutput) LintSummary { + summary := LintSummary{} + + // Count from Helm results + if output.HelmResults != nil { + for _, chart := range output.HelmResults.Charts { + summary.TotalResources++ + if chart.Success { + summary.PassedResources++ + } else { + summary.FailedResources++ + } + summary.TotalErrors += chart.Summary.ErrorCount + summary.TotalWarnings += chart.Summary.WarningCount + summary.TotalInfo += chart.Summary.InfoCount + } + } + + // Count from Preflight results + if output.PreflightResults != nil { + for _, spec := range output.PreflightResults.Specs { + summary.TotalResources++ + if spec.Success { + summary.PassedResources++ + } else { + summary.FailedResources++ + } + summary.TotalErrors += spec.Summary.ErrorCount + summary.TotalWarnings += spec.Summary.WarningCount + summary.TotalInfo += spec.Summary.InfoCount + } + } + + // Count from Support Bundle results + if output.SupportBundleResults != nil { + for _, spec := range output.SupportBundleResults.Specs { + summary.TotalResources++ + if spec.Success { + summary.PassedResources++ + } else { + summary.FailedResources++ + } + summary.TotalErrors += spec.Summary.ErrorCount + summary.TotalWarnings += spec.Summary.WarningCount + summary.TotalInfo += spec.Summary.InfoCount + } + } + + summary.OverallSuccess = summary.FailedResources == 0 + + return summary +} + +// displayHelmResults displays Helm lint results in table format +func (r *runners) displayHelmResults(results *HelmLintResults) error { + if results == nil || len(results.Charts) == 0 { + return nil + } + + for _, chart := range results.Charts { + fmt.Fprintf(r.w, "==> Linting chart: %s\n\n", chart.Path) + + if len(chart.Messages) == 0 { + fmt.Fprintf(r.w, "No issues found\n") + } else { + for _, msg := range chart.Messages { + if msg.Path != "" { + fmt.Fprintf(r.w, "[%s] %s: %s\n", msg.Severity, msg.Path, msg.Message) + } else { + fmt.Fprintf(r.w, "[%s] %s\n", msg.Severity, msg.Message) + } + } + } + + fmt.Fprintf(r.w, "\nSummary for %s: %d error(s), %d warning(s), %d info\n", + chart.Path, chart.Summary.ErrorCount, chart.Summary.WarningCount, chart.Summary.InfoCount) + + if chart.Success { + fmt.Fprintf(r.w, "Status: Passed\n\n") + } else { + fmt.Fprintf(r.w, "Status: Failed\n\n") + } + } + + // Print overall summary if multiple charts + if len(results.Charts) > 1 { + totalErrors := 0 + totalWarnings := 0 + totalInfo := 0 + failedCharts := 0 + + for _, chart := range results.Charts { + totalErrors += chart.Summary.ErrorCount + totalWarnings += chart.Summary.WarningCount + totalInfo += chart.Summary.InfoCount + if !chart.Success { + failedCharts++ + } + } + + fmt.Fprintf(r.w, "==> Overall Summary\n") + fmt.Fprintf(r.w, "charts linted: %d\n", len(results.Charts)) + fmt.Fprintf(r.w, "charts passed: %d\n", len(results.Charts)-failedCharts) + fmt.Fprintf(r.w, "charts failed: %d\n", failedCharts) + fmt.Fprintf(r.w, "Total errors: %d\n", totalErrors) + fmt.Fprintf(r.w, "Total warnings: %d\n", totalWarnings) + fmt.Fprintf(r.w, "Total info: %d\n", totalInfo) + + if failedCharts > 0 { + fmt.Fprintf(r.w, "\nOverall Status: Failed\n") + } else { + fmt.Fprintf(r.w, "\nOverall Status: Passed\n") + } } - fmt.Fprintf(r.w, "\nFound %d unique images across %d chart(s)\n\n", len(allImages), len(chartPaths)) - return r.w.Flush() + return nil +} + +// displayPreflightResults displays Preflight lint results in table format +func (r *runners) displayPreflightResults(results *PreflightLintResults) error { + if results == nil || len(results.Specs) == 0 { + return nil + } + + for _, spec := range results.Specs { + fmt.Fprintf(r.w, "==> Linting preflight spec: %s\n\n", spec.Path) + + if len(spec.Messages) == 0 { + fmt.Fprintf(r.w, "No issues found\n") + } else { + for _, msg := range spec.Messages { + if msg.Path != "" { + fmt.Fprintf(r.w, "[%s] %s: %s\n", msg.Severity, msg.Path, msg.Message) + } else { + fmt.Fprintf(r.w, "[%s] %s\n", msg.Severity, msg.Message) + } + } + } + + fmt.Fprintf(r.w, "\nSummary for %s: %d error(s), %d warning(s), %d info\n", + spec.Path, spec.Summary.ErrorCount, spec.Summary.WarningCount, spec.Summary.InfoCount) + + if spec.Success { + fmt.Fprintf(r.w, "Status: Passed\n\n") + } else { + fmt.Fprintf(r.w, "Status: Failed\n\n") + } + } + + // Print overall summary if multiple specs + if len(results.Specs) > 1 { + totalErrors := 0 + totalWarnings := 0 + totalInfo := 0 + failedSpecs := 0 + + for _, spec := range results.Specs { + totalErrors += spec.Summary.ErrorCount + totalWarnings += spec.Summary.WarningCount + totalInfo += spec.Summary.InfoCount + if !spec.Success { + failedSpecs++ + } + } + + fmt.Fprintf(r.w, "==> Overall Summary\n") + fmt.Fprintf(r.w, "preflight specs linted: %d\n", len(results.Specs)) + fmt.Fprintf(r.w, "preflight specs passed: %d\n", len(results.Specs)-failedSpecs) + fmt.Fprintf(r.w, "preflight specs failed: %d\n", failedSpecs) + fmt.Fprintf(r.w, "Total errors: %d\n", totalErrors) + fmt.Fprintf(r.w, "Total warnings: %d\n", totalWarnings) + fmt.Fprintf(r.w, "Total info: %d\n", totalInfo) + + if failedSpecs > 0 { + fmt.Fprintf(r.w, "\nOverall Status: Failed\n") + } else { + fmt.Fprintf(r.w, "\nOverall Status: Passed\n") + } + } + + return nil +} + +// displaySupportBundleResults displays Support Bundle lint results in table format +func (r *runners) displaySupportBundleResults(results *SupportBundleLintResults) error { + if results == nil || len(results.Specs) == 0 { + return nil + } + + for _, spec := range results.Specs { + fmt.Fprintf(r.w, "==> Linting support bundle spec: %s\n\n", spec.Path) + + if len(spec.Messages) == 0 { + fmt.Fprintf(r.w, "No issues found\n") + } else { + for _, msg := range spec.Messages { + if msg.Path != "" { + fmt.Fprintf(r.w, "[%s] %s: %s\n", msg.Severity, msg.Path, msg.Message) + } else { + fmt.Fprintf(r.w, "[%s] %s\n", msg.Severity, msg.Message) + } + } + } + + fmt.Fprintf(r.w, "\nSummary for %s: %d error(s), %d warning(s), %d info\n", + spec.Path, spec.Summary.ErrorCount, spec.Summary.WarningCount, spec.Summary.InfoCount) + + if spec.Success { + fmt.Fprintf(r.w, "Status: Passed\n\n") + } else { + fmt.Fprintf(r.w, "Status: Failed\n\n") + } + } + + // Print overall summary if multiple specs + if len(results.Specs) > 1 { + totalErrors := 0 + totalWarnings := 0 + totalInfo := 0 + failedSpecs := 0 + + for _, spec := range results.Specs { + totalErrors += spec.Summary.ErrorCount + totalWarnings += spec.Summary.WarningCount + totalInfo += spec.Summary.InfoCount + if !spec.Success { + failedSpecs++ + } + } + + fmt.Fprintf(r.w, "==> Overall Summary\n") + fmt.Fprintf(r.w, "support bundle specs linted: %d\n", len(results.Specs)) + fmt.Fprintf(r.w, "support bundle specs passed: %d\n", len(results.Specs)-failedSpecs) + fmt.Fprintf(r.w, "support bundle specs failed: %d\n", failedSpecs) + fmt.Fprintf(r.w, "Total errors: %d\n", totalErrors) + fmt.Fprintf(r.w, "Total warnings: %d\n", totalWarnings) + fmt.Fprintf(r.w, "Total info: %d\n", totalInfo) + + if failedSpecs > 0 { + fmt.Fprintf(r.w, "\nOverall Status: Failed\n") + } else { + fmt.Fprintf(r.w, "\nOverall Status: Passed\n") + } + } + + return nil +} + +// findConfigFilePath finds the .replicated config file path +func findConfigFilePath(startPath string) string { + currentDir := startPath + if currentDir == "" { + var err error + currentDir, err = os.Getwd() + if err != nil { + return ".replicated" + } + } + + for { + // Try .replicated first, then .replicated.yaml + candidates := []string{ + filepath.Join(currentDir, ".replicated"), + filepath.Join(currentDir, ".replicated.yaml"), + } + + for _, configPath := range candidates { + if stat, err := os.Stat(configPath); err == nil && !stat.IsDir() { + return configPath + } + } + + // Move up one directory + parentDir := filepath.Dir(currentDir) + if parentDir == currentDir { + // Reached root, return default + return ".replicated" + } + currentDir = parentDir + } +} + +// writeOutputToFile writes lint output to a file +func (r *runners) writeOutputToFile(output *JSONLintOutput) error { + // Create the file + file, err := os.Create(r.args.lintOutputFile) + if err != nil { + return errors.Wrap(err, "failed to create output file") + } + defer file.Close() + + // For JSON format, write JSON directly + if r.outputFormat == "json" { + encoder := json.NewEncoder(file) + encoder.SetIndent("", " ") + if err := encoder.Encode(output); err != nil { + return errors.Wrap(err, "failed to write JSON to file") + } + return nil + } + + // For table format, we need to recreate the table output + // Create a tabwriter for the file + w := tabwriter.NewWriter(file, minWidth, tabWidth, padding, padChar, tabwriter.TabIndent) + + // Re-display helm results + if output.HelmResults != nil && output.HelmResults.Enabled { + for _, chart := range output.HelmResults.Charts { + fmt.Fprintf(w, "==> Linting chart: %s\n\n", chart.Path) + + if len(chart.Messages) == 0 { + fmt.Fprintf(w, "No issues found\n") + } else { + for _, msg := range chart.Messages { + if msg.Path != "" { + fmt.Fprintf(w, "[%s] %s: %s\n", msg.Severity, msg.Path, msg.Message) + } else { + fmt.Fprintf(w, "[%s] %s\n", msg.Severity, msg.Message) + } + } + } + + fmt.Fprintf(w, "\nSummary for %s: %d error(s), %d warning(s), %d info\n", + chart.Path, chart.Summary.ErrorCount, chart.Summary.WarningCount, chart.Summary.InfoCount) + + if chart.Success { + fmt.Fprintf(w, "Status: Passed\n\n") + } else { + fmt.Fprintf(w, "Status: Failed\n\n") + } + } + + // Print overall summary if multiple charts + if len(output.HelmResults.Charts) > 1 { + totalErrors := 0 + totalWarnings := 0 + totalInfo := 0 + failedCharts := 0 + + for _, chart := range output.HelmResults.Charts { + totalErrors += chart.Summary.ErrorCount + totalWarnings += chart.Summary.WarningCount + totalInfo += chart.Summary.InfoCount + if !chart.Success { + failedCharts++ + } + } + + fmt.Fprintf(w, "==> Overall Summary\n") + fmt.Fprintf(w, "charts linted: %d\n", len(output.HelmResults.Charts)) + fmt.Fprintf(w, "charts passed: %d\n", len(output.HelmResults.Charts)-failedCharts) + fmt.Fprintf(w, "charts failed: %d\n", failedCharts) + fmt.Fprintf(w, "Total errors: %d\n", totalErrors) + fmt.Fprintf(w, "Total warnings: %d\n", totalWarnings) + fmt.Fprintf(w, "Total info: %d\n", totalInfo) + + if failedCharts > 0 { + fmt.Fprintf(w, "\nOverall Status: Failed\n") + } else { + fmt.Fprintf(w, "\nOverall Status: Passed\n") + } + } + } + + // Display preflight results + if output.PreflightResults != nil && output.PreflightResults.Enabled { + for _, spec := range output.PreflightResults.Specs { + fmt.Fprintf(w, "==> Linting preflight spec: %s\n\n", spec.Path) + + if len(spec.Messages) == 0 { + fmt.Fprintf(w, "No issues found\n") + } else { + for _, msg := range spec.Messages { + if msg.Path != "" { + fmt.Fprintf(w, "[%s] %s: %s\n", msg.Severity, msg.Path, msg.Message) + } else { + fmt.Fprintf(w, "[%s] %s\n", msg.Severity, msg.Message) + } + } + } + + fmt.Fprintf(w, "\nSummary for %s: %d error(s), %d warning(s), %d info\n", + spec.Path, spec.Summary.ErrorCount, spec.Summary.WarningCount, spec.Summary.InfoCount) + + if spec.Success { + fmt.Fprintf(w, "Status: Passed\n\n") + } else { + fmt.Fprintf(w, "Status: Failed\n\n") + } + } + } + + // Display support bundle results + if output.SupportBundleResults != nil && output.SupportBundleResults.Enabled { + for _, spec := range output.SupportBundleResults.Specs { + fmt.Fprintf(w, "==> Linting support bundle spec: %s\n\n", spec.Path) + + if len(spec.Messages) == 0 { + fmt.Fprintf(w, "No issues found\n") + } else { + for _, msg := range spec.Messages { + if msg.Path != "" { + fmt.Fprintf(w, "[%s] %s: %s\n", msg.Severity, msg.Path, msg.Message) + } else { + fmt.Fprintf(w, "[%s] %s\n", msg.Severity, msg.Message) + } + } + } + + fmt.Fprintf(w, "\nSummary for %s: %d error(s), %d warning(s), %d info\n", + spec.Path, spec.Summary.ErrorCount, spec.Summary.WarningCount, spec.Summary.InfoCount) + + if spec.Success { + fmt.Fprintf(w, "Status: Passed\n\n") + } else { + fmt.Fprintf(w, "Status: Failed\n\n") + } + } + } + + // Display disabled linters messages + if output.HelmResults != nil && !output.HelmResults.Enabled { + fmt.Fprintf(w, "Helm linting is disabled in .replicated config\n\n") + } + if output.PreflightResults != nil && !output.PreflightResults.Enabled { + fmt.Fprintf(w, "Preflight linting is disabled in .replicated config\n\n") + } + if output.SupportBundleResults != nil && !output.SupportBundleResults.Enabled { + fmt.Fprintf(w, "Support Bundle linting is disabled in .replicated config\n\n") + } + + // Flush and close + if err := w.Flush(); err != nil { + return errors.Wrap(err, "failed to flush output to file") + } + + return nil } diff --git a/cli/cmd/lint_test.go b/cli/cmd/lint_test.go index 55159a058..d05d52a34 100644 --- a/cli/cmd/lint_test.go +++ b/cli/cmd/lint_test.go @@ -112,13 +112,17 @@ repl-lint: t.Fatalf("failed to load config: %v", err) } - // Test extractAndDisplayImagesFromConfig - err = r.extractAndDisplayImagesFromConfig(context.Background(), config) + // Test extractImagesFromConfig + imageResults, err := r.extractImagesFromConfig(context.Background(), config) if err != nil { t.Errorf("unexpected error: %v", err) } + if imageResults != nil { + r.displayImages(imageResults) + } + w.Flush() output := buf.String() @@ -179,7 +183,10 @@ func TestExtractAndDisplayImagesFromConfig_NoCharts(t *testing.T) { } // Should handle no charts gracefully - err = r.extractAndDisplayImagesFromConfig(context.Background(), config) + imageResults, err := r.extractImagesFromConfig(context.Background(), config) + if err == nil && imageResults != nil { + r.displayImages(imageResults) + } // Should get error about no charts if err == nil { @@ -232,21 +239,17 @@ repl-lint: t.Fatalf("failed to load config: %v", err) } - // Should handle errors gracefully - err = r.extractAndDisplayImagesFromConfig(context.Background(), config) + // Should get an error for non-existent chart path (validated by GetChartPathsFromConfig) + _, err = r.extractImagesFromConfig(context.Background(), config) - // Error expected due to invalid chart path + // We expect an error because the chart path doesn't exist if err == nil { - t.Error("expected error for invalid chart path") + t.Error("expected error for non-existent chart path") } - w.Flush() - output := buf.String() - - // Should still have tried to extract - if !strings.Contains(output, "Extracting images") { - t.Error("expected 'Extracting images' message even on error") - } + // Since we got an error, we don't display anything + // This is the correct behavior - fail fast on invalid paths + // The test verified that we correctly return an error for non-existent paths } func TestExtractAndDisplayImagesFromConfig_MultipleCharts(t *testing.T) { @@ -346,7 +349,10 @@ repl-lint: } // Extract images - err = r.extractAndDisplayImagesFromConfig(context.Background(), config) + imageResults, err := r.extractImagesFromConfig(context.Background(), config) + if err == nil && imageResults != nil { + r.displayImages(imageResults) + } if err != nil { t.Errorf("unexpected error: %v", err) } @@ -361,7 +367,8 @@ repl-lint: if !strings.Contains(output, "redis") { t.Error("expected to find redis image from chart2") } - if !strings.Contains(output, "2 chart(s)") { - t.Error("expected message about 2 charts") + // The new implementation shows total unique images instead of chart count + if !strings.Contains(output, "unique images") { + t.Error("expected message about unique images") } } diff --git a/cli/cmd/lint_types.go b/cli/cmd/lint_types.go new file mode 100644 index 000000000..fa516f248 --- /dev/null +++ b/cli/cmd/lint_types.go @@ -0,0 +1,147 @@ +package cmd + +import ( + "time" + + "github.com/replicatedhq/replicated/pkg/imageextract" + "github.com/replicatedhq/replicated/pkg/lint2" +) + +// JSONLintOutput represents the complete JSON output structure for lint results +type JSONLintOutput struct { + Metadata LintMetadata `json:"metadata"` + HelmResults *HelmLintResults `json:"helm_results,omitempty"` + PreflightResults *PreflightLintResults `json:"preflight_results,omitempty"` + SupportBundleResults *SupportBundleLintResults `json:"support_bundle_results,omitempty"` + Summary LintSummary `json:"summary"` + Images *ImageExtractResults `json:"images,omitempty"` // Only if --verbose +} + +// LintMetadata contains execution context and environment information +type LintMetadata struct { + Timestamp string `json:"timestamp"` + ConfigFile string `json:"config_file"` + HelmVersion string `json:"helm_version,omitempty"` + CLIVersion string `json:"cli_version"` +} + +// HelmLintResults contains all Helm chart lint results +type HelmLintResults struct { + Enabled bool `json:"enabled"` + Charts []ChartLintResult `json:"charts"` +} + +// ChartLintResult represents lint results for a single Helm chart +type ChartLintResult struct { + Path string `json:"path"` + Success bool `json:"success"` + Messages []LintMessage `json:"messages"` + Summary ResourceSummary `json:"summary"` +} + +// PreflightLintResults contains all Preflight spec lint results +type PreflightLintResults struct { + Enabled bool `json:"enabled"` + Specs []PreflightLintResult `json:"specs"` +} + +// PreflightLintResult represents lint results for a single Preflight spec +type PreflightLintResult struct { + Path string `json:"path"` + Success bool `json:"success"` + Messages []LintMessage `json:"messages"` + Summary ResourceSummary `json:"summary"` +} + +// SupportBundleLintResults contains all Support Bundle spec lint results +type SupportBundleLintResults struct { + Enabled bool `json:"enabled"` + Specs []SupportBundleLintResult `json:"specs"` +} + +// SupportBundleLintResult represents lint results for a single Support Bundle spec +type SupportBundleLintResult struct { + Path string `json:"path"` + Success bool `json:"success"` + Messages []LintMessage `json:"messages"` + Summary ResourceSummary `json:"summary"` +} + +// LintMessage represents a single lint issue (wraps lint2.LintMessage with JSON tags) +type LintMessage struct { + Severity string `json:"severity"` // ERROR, WARNING, INFO + Path string `json:"path,omitempty"` + Message string `json:"message"` +} + +// ResourceSummary contains counts by severity for a resource +type ResourceSummary struct { + ErrorCount int `json:"error_count"` + WarningCount int `json:"warning_count"` + InfoCount int `json:"info_count"` +} + +// LintSummary contains overall statistics across all linted resources +type LintSummary struct { + TotalResources int `json:"total_resources"` + PassedResources int `json:"passed_resources"` + FailedResources int `json:"failed_resources"` + TotalErrors int `json:"total_errors"` + TotalWarnings int `json:"total_warnings"` + TotalInfo int `json:"total_info"` + OverallSuccess bool `json:"overall_success"` +} + +// ImageExtractResults contains extracted image information +type ImageExtractResults struct { + Images []imageextract.ImageRef `json:"images"` + Warnings []imageextract.Warning `json:"warnings"` + Summary ImageSummary `json:"summary"` +} + +// ImageSummary contains summary statistics for extracted images +type ImageSummary struct { + TotalImages int `json:"total_images"` + UniqueImages int `json:"unique_images"` +} + +// Helper functions to convert between types + +// convertLint2Messages converts lint2.LintMessage slice to LintMessage slice +func convertLint2Messages(messages []lint2.LintMessage) []LintMessage { + result := make([]LintMessage, len(messages)) + for i, msg := range messages { + result[i] = LintMessage{ + Severity: msg.Severity, + Path: msg.Path, + Message: msg.Message, + } + } + return result +} + +// calculateResourceSummary calculates summary from lint messages +func calculateResourceSummary(messages []lint2.LintMessage) ResourceSummary { + summary := ResourceSummary{} + for _, msg := range messages { + switch msg.Severity { + case "ERROR": + summary.ErrorCount++ + case "WARNING": + summary.WarningCount++ + case "INFO": + summary.InfoCount++ + } + } + return summary +} + +// newLintMetadata creates metadata for the lint output +func newLintMetadata(configFile, helmVersion, cliVersion string) LintMetadata { + return LintMetadata{ + Timestamp: time.Now().UTC().Format(time.RFC3339), + ConfigFile: configFile, + HelmVersion: helmVersion, + CLIVersion: cliVersion, + } +} diff --git a/cli/cmd/runner.go b/cli/cmd/runner.go index 15ae15634..320394079 100644 --- a/cli/cmd/runner.go +++ b/cli/cmd/runner.go @@ -66,6 +66,7 @@ type runnerArgs struct { lintReleaseChart string lintReleaseFailOn string lintVerbose bool + lintOutputFile string releaseOptional bool releaseRequired bool releaseNotes string diff --git a/cli/print/lint_results.go b/cli/print/lint_results.go new file mode 100644 index 000000000..1c6192d5b --- /dev/null +++ b/cli/print/lint_results.go @@ -0,0 +1,48 @@ +package print + +import ( + "encoding/json" + "fmt" + "text/tabwriter" + + "github.com/pkg/errors" +) + +// LintOutput represents the complete lint output structure +// This is imported from cli/cmd but redefined here to avoid circular imports +type LintOutput interface{} + +// LintResults formats and prints lint results in the specified format +func LintResults(format string, w *tabwriter.Writer, output interface{}) error { + switch format { + case "table": + // Table format is handled by the display functions in lint.go + // This function is only called for non-table formats + return errors.New("table format should be handled by display functions") + case "json": + return printLintResultsJSON(w, output) + default: + return errors.Errorf("invalid format: %s. Supported formats: json, table", format) + } +} + +// printLintResultsJSON outputs lint results as formatted JSON +func printLintResultsJSON(w *tabwriter.Writer, output interface{}) error { + // Marshal to JSON with pretty printing + jsonBytes, err := json.MarshalIndent(output, "", " ") + if err != nil { + return errors.Wrap(err, "failed to marshal lint results to JSON") + } + + // Write JSON to output + if _, err := fmt.Fprintln(w, string(jsonBytes)); err != nil { + return errors.Wrap(err, "failed to write JSON output") + } + + // Flush the writer + if err := w.Flush(); err != nil { + return errors.Wrap(err, "failed to flush output") + } + + return nil +} diff --git a/examples/.replicated.yaml b/examples/.replicated.yaml index 184171a35..50db189eb 100644 --- a/examples/.replicated.yaml +++ b/examples/.replicated.yaml @@ -4,19 +4,19 @@ promoteToChannelIds: [] promoteToChannelNames: [] charts: [ { - path: "../pkg/imageextract/testdata/helm-chart", + path: "./helm-chart", chartVersion: "", appVersion: "", }, ] preflights: [ { - path: "./preflights/stuff", + path: "./preflights/**", valuesPath: "./chart/something", # directory to corresponding helm chart } ] releaseLabel: "" ## some sort of semver pattern? -manifests: ["replicated/**/*.yaml"] +manifests: ["./support-bundles/**"] repl-lint: version: 1 linters: @@ -26,10 +26,6 @@ repl-lint: disabled: false support-bundle: disabled: false - embedded-cluster: - disabled: true - kots: - disabled: true tools: helm: "3.19.0" preflight: "0.123.9" diff --git a/examples/preflights/helm-chart/Chart.yaml b/examples/helm-chart/Chart.yaml similarity index 100% rename from examples/preflights/helm-chart/Chart.yaml rename to examples/helm-chart/Chart.yaml diff --git a/examples/preflights/helm-chart/templates/deployment.yaml b/examples/helm-chart/templates/deployment.yaml similarity index 100% rename from examples/preflights/helm-chart/templates/deployment.yaml rename to examples/helm-chart/templates/deployment.yaml diff --git a/examples/preflights/helm-chart/values.yaml b/examples/helm-chart/values.yaml similarity index 100% rename from examples/preflights/helm-chart/values.yaml rename to examples/helm-chart/values.yaml diff --git a/examples/output.json b/examples/output.json new file mode 100644 index 000000000..f6d9283c1 --- /dev/null +++ b/examples/output.json @@ -0,0 +1,174 @@ +{ + "metadata": { + "timestamp": "2025-10-21T19:33:49Z", + "config_file": ".replicated.yaml", + "helm_version": "3.19.0", + "cli_version": "v0.90.0" + }, + "helm_results": { + "enabled": true, + "charts": [ + { + "path": "/Users/noah/replicatedhq/replicated/examples/helm-chart", + "success": true, + "messages": [ + { + "severity": "INFO", + "path": "Chart.yaml", + "message": "icon is recommended" + } + ], + "summary": { + "error_count": 0, + "warning_count": 0, + "info_count": 1 + } + } + ] + }, + "preflight_results": { + "enabled": true, + "specs": [ + { + "path": "/Users/noah/replicatedhq/replicated/examples/preflights/all-analyzers-v1beta2.yaml", + "success": false, + "messages": [ + { + "severity": "ERROR", + "path": "/Users/noah/replicatedhq/replicated/examples/preflights/all-analyzers-v1beta2.yaml", + "message": "line 26: Unmatched template braces: 0 opening, 1 closing" + }, + { + "severity": "WARNING", + "path": "/Users/noah/replicatedhq/replicated/examples/preflights/all-analyzers-v1beta2.yaml", + "message": "line 5: Some analyzers and collectors are missing docString (recommended for v1beta3) (field: spec)" + } + ], + "summary": { + "error_count": 1, + "warning_count": 1, + "info_count": 0 + } + }, + { + "path": "/Users/noah/replicatedhq/replicated/examples/preflights/complex-v1beta3.yaml", + "success": true, + "messages": [ + { + "severity": "WARNING", + "path": "/Users/noah/replicatedhq/replicated/examples/preflights/complex-v1beta3.yaml", + "message": "line 1: Template values that must be provided at runtime: cephStatus.enabled, cephStatus.namespace, cephStatus.timeout, certificates.configMaps, certificates.enabled, certificates.secrets, clusterContainerStatuses.enabled, clusterContainerStatuses.namespaces, clusterContainerStatuses.restartCount, clusterPodStatuses.enabled, clusterPodStatuses.namespaces, clusterResource.clusterScoped, clusterResource.enabled, clusterResource.expectedValue, clusterResource.kind, clusterResource.name, clusterResource.namespace, clusterResource.regex, clusterResource.yamlPath, clusterVersion.enabled, clusterVersion.minVersion, clusterVersion.recommendedVersion, configMap.enabled, configMap.key, configMap.name, configMap.namespace, containerRuntime.enabled, crd.enabled, crd.name, databases.mssql.collectorName, databases.mssql.enabled, databases.mssql.uri, databases.mysql.collectorName, databases.mysql.enabled, databases.mysql.uri, databases.postgres.collectorName, databases.postgres.enabled, databases.postgres.tls, databases.postgres.tls.secret, databases.postgres.tls.secret.name, databases.postgres.tls.secret.namespace, databases.postgres.tls.skipVerify, databases.postgres.uri, databases.redis.collectorName, databases.redis.enabled, databases.redis.uri, distribution.enabled, distribution.supported, distribution.unsupported, event.collectorName, event.enabled, event.kind, event.namespace, event.reason, event.regex, goldpinger.collectDelay, goldpinger.collectorName, goldpinger.enabled, goldpinger.filePath, goldpinger.namespace, goldpinger.podLaunch, goldpinger.podLaunch.image, goldpinger.podLaunch.imagePullSecret, goldpinger.podLaunch.imagePullSecret.name, goldpinger.podLaunch.namespace, goldpinger.podLaunch.serviceAccountName, http.collectorName, http.enabled, http.get, http.get.headers, http.get.insecureSkipVerify, http.get.timeout, http.get.url, http.post, http.post.body, http.post.headers, http.post.insecureSkipVerify, http.post.timeout, http.post.url, imagePullSecret.enabled, imagePullSecret.registry, ingress.enabled, ingress.name, ingress.namespace, jsonCompare.enabled, jsonCompare.fileName, jsonCompare.jsonPath, jsonCompare.value, longhorn.enabled, longhorn.namespace, longhorn.timeout, nodeMetrics.collectorName, nodeMetrics.enabled, nodeMetrics.filters.pvc.nameRegex, nodeMetrics.filters.pvc.namespace, nodeMetrics.nodeNames, nodeMetrics.selector, nodeResources.count.enabled, nodeResources.count.min, nodeResources.count.recommended, nodeResources.cpu.enabled, nodeResources.cpu.min, nodeResources.ephemeral.enabled, nodeResources.ephemeral.minGi, nodeResources.ephemeral.recommendedGi, nodeResources.memory.enabled, nodeResources.memory.minGi, nodeResources.memory.recommendedGi, registryImages.collectorName, registryImages.enabled, registryImages.imagePullSecret, registryImages.imagePullSecret.data, registryImages.imagePullSecret.name, registryImages.images, registryImages.namespace, secret.enabled, secret.key, secret.name, secret.namespace, storageClass.className, storageClass.enabled, sysctl.enabled, sysctl.image, sysctl.imagePullPolicy, sysctl.namespace, textAnalyze.enabled, textAnalyze.fileName, textAnalyze.regex, velero.enabled, weaveReport.enabled, weaveReport.reportFileGlob, workloads.deployments.enabled, workloads.deployments.minReady, workloads.deployments.name, workloads.deployments.namespace, workloads.jobs.enabled, workloads.jobs.name, workloads.jobs.namespace, workloads.replicasets.enabled, workloads.replicasets.minReady, workloads.replicasets.name, workloads.replicasets.namespace, workloads.statefulsets.enabled, workloads.statefulsets.minReady, workloads.statefulsets.name, workloads.statefulsets.namespace, yamlCompare.enabled, yamlCompare.fileName, yamlCompare.path, yamlCompare.value (field: template-values)" + } + ], + "summary": { + "error_count": 0, + "warning_count": 1, + "info_count": 0 + } + }, + { + "path": "/Users/noah/replicatedhq/replicated/examples/preflights/missing-metadata-v1beta3.yaml", + "success": false, + "messages": [ + { + "severity": "ERROR", + "path": "/Users/noah/replicatedhq/replicated/examples/preflights/missing-metadata-v1beta3.yaml", + "message": "Missing 'metadata' section (field: metadata)" + }, + { + "severity": "WARNING", + "path": "/Users/noah/replicatedhq/replicated/examples/preflights/missing-metadata-v1beta3.yaml", + "message": "line 4: Some analyzers are missing docString (recommended for v1beta3) (field: spec.analyzers)" + } + ], + "summary": { + "error_count": 1, + "warning_count": 1, + "info_count": 0 + } + } + ] + }, + "support_bundle_results": { + "enabled": true, + "specs": [ + { + "path": "/Users/noah/replicatedhq/replicated/examples/support-bundles/all-collectors.yaml", + "success": true, + "messages": [], + "summary": { + "error_count": 0, + "warning_count": 0, + "info_count": 0 + } + }, + { + "path": "/Users/noah/replicatedhq/replicated/examples/support-bundles/all-kubernetes-collectors.yaml", + "success": true, + "messages": [ + { + "severity": "WARNING", + "path": "/Users/noah/replicatedhq/replicated/examples/support-bundles/all-kubernetes-collectors.yaml", + "message": "line 6: Some collectors are missing docString (recommended for v1beta3) (field: spec.collectors)" + } + ], + "summary": { + "error_count": 0, + "warning_count": 1, + "info_count": 0 + } + }, + { + "path": "/Users/noah/replicatedhq/replicated/examples/support-bundles/invalid-collectors-analyzers.yaml", + "success": false, + "messages": [ + { + "severity": "ERROR", + "path": "/Users/noah/replicatedhq/replicated/examples/support-bundles/invalid-collectors-analyzers.yaml", + "message": "line 12: Expected 'hostCollectors' to be a list (field: spec.hostCollectors)" + }, + { + "severity": "WARNING", + "path": "/Users/noah/replicatedhq/replicated/examples/support-bundles/invalid-collectors-analyzers.yaml", + "message": "line 5: Some analyzers and collectors are missing docString (recommended for v1beta3) (field: spec)" + } + ], + "summary": { + "error_count": 1, + "warning_count": 1, + "info_count": 0 + } + }, + { + "path": "/Users/noah/replicatedhq/replicated/examples/support-bundles/support-bundle-no-collectors-v1beta3.yaml", + "success": false, + "messages": [ + { + "severity": "ERROR", + "path": "/Users/noah/replicatedhq/replicated/examples/support-bundles/support-bundle-no-collectors-v1beta3.yaml", + "message": "line 5: SupportBundle spec must contain 'collectors' or 'hostCollectors' (field: spec.collectors)" + }, + { + "severity": "WARNING", + "path": "/Users/noah/replicatedhq/replicated/examples/support-bundles/support-bundle-no-collectors-v1beta3.yaml", + "message": "line 6: Some analyzers are missing docString (recommended for v1beta3) (field: spec.analyzers)" + } + ], + "summary": { + "error_count": 1, + "warning_count": 1, + "info_count": 0 + } + } + ] + }, + "summary": { + "total_resources": 8, + "passed_resources": 4, + "failed_resources": 4, + "total_errors": 4, + "total_warnings": 6, + "total_info": 1, + "overall_success": false + } +}