mirror of
https://github.com/replicatedhq/troubleshoot.git
synced 2026-02-14 10:19:54 +00:00
Revert unintended commits on main
This commit is contained in:
@@ -1,98 +0,0 @@
|
|||||||
package cli
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/replicatedhq/troubleshoot/pkg/constants"
|
|
||||||
"github.com/replicatedhq/troubleshoot/pkg/lint"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
func LintCmd() *cobra.Command {
|
|
||||||
cmd := &cobra.Command{
|
|
||||||
Use: "lint [spec-files...]",
|
|
||||||
Args: cobra.MinimumNArgs(1),
|
|
||||||
Short: "Lint preflight specs for syntax and structural errors",
|
|
||||||
Long: `Lint preflight specs for syntax and structural errors.
|
|
||||||
|
|
||||||
This command validates troubleshoot specs and checks for:
|
|
||||||
- YAML syntax errors (missing colons, invalid structure)
|
|
||||||
- Missing required fields (apiVersion, kind, metadata, spec)
|
|
||||||
- Invalid template syntax ({{ .Values.* }}, {{ .Release.* }}, etc.)
|
|
||||||
- Missing analyzers or collectors
|
|
||||||
- Common structural issues
|
|
||||||
- Missing docStrings (warning)
|
|
||||||
|
|
||||||
Both v1beta2 and v1beta3 apiVersions are supported. Use 'convert' if you need a full structural conversion between schema versions.
|
|
||||||
|
|
||||||
The --fix flag can automatically repair:
|
|
||||||
- Missing colons in YAML (e.g., "metadata" → "metadata:")
|
|
||||||
- Missing or malformed apiVersion line. If templating ({{ }}) or docString fields are detected, apiVersion is set to v1beta3; otherwise v1beta2.
|
|
||||||
- Template expressions missing leading dot (e.g., "{{ Values.x }}" → "{{ .Values.x }}")
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
# Lint a single spec file
|
|
||||||
preflight lint my-preflight.yaml
|
|
||||||
|
|
||||||
# Lint multiple spec files
|
|
||||||
preflight lint spec1.yaml spec2.yaml spec3.yaml
|
|
||||||
|
|
||||||
# Lint with automatic fixes (may need to run multiple times for complex issues)
|
|
||||||
preflight lint --fix my-preflight.yaml
|
|
||||||
|
|
||||||
# Lint and output as JSON for CI/CD integration
|
|
||||||
preflight lint --format json my-preflight.yaml
|
|
||||||
|
|
||||||
Exit codes:
|
|
||||||
0 - No errors found
|
|
||||||
2 - Validation errors found`,
|
|
||||||
PreRun: func(cmd *cobra.Command, args []string) {
|
|
||||||
viper.BindPFlags(cmd.Flags())
|
|
||||||
},
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
|
||||||
v := viper.GetViper()
|
|
||||||
|
|
||||||
opts := lint.LintOptions{
|
|
||||||
FilePaths: args,
|
|
||||||
Fix: v.GetBool("fix"),
|
|
||||||
Format: v.GetString("format"),
|
|
||||||
}
|
|
||||||
|
|
||||||
return runLint(opts)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Flags().Bool("fix", false, "Automatically fix issues where possible")
|
|
||||||
cmd.Flags().String("format", "text", "Output format: text or json")
|
|
||||||
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
func runLint(opts lint.LintOptions) error {
|
|
||||||
// Validate file paths exist
|
|
||||||
for _, filePath := range opts.FilePaths {
|
|
||||||
if _, err := os.Stat(filePath); err != nil {
|
|
||||||
return errors.Wrapf(err, "file not found: %s", filePath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run linting
|
|
||||||
results, err := lint.LintFiles(opts)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "failed to lint files")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format and print results
|
|
||||||
output := lint.FormatResults(results, opts.Format)
|
|
||||||
fmt.Print(output)
|
|
||||||
|
|
||||||
// Return appropriate exit code
|
|
||||||
if lint.HasErrors(results) {
|
|
||||||
os.Exit(constants.EXIT_CODE_SPEC_ISSUES)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -89,7 +89,6 @@ that a cluster meets the requirements to run an application.`,
|
|||||||
cmd.AddCommand(TemplateCmd())
|
cmd.AddCommand(TemplateCmd())
|
||||||
cmd.AddCommand(DocsCmd())
|
cmd.AddCommand(DocsCmd())
|
||||||
cmd.AddCommand(ConvertCmd())
|
cmd.AddCommand(ConvertCmd())
|
||||||
cmd.AddCommand(LintCmd())
|
|
||||||
|
|
||||||
preflight.AddFlags(cmd.PersistentFlags())
|
preflight.AddFlags(cmd.PersistentFlags())
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/replicatedhq/troubleshoot/cmd/internal/util"
|
"github.com/replicatedhq/troubleshoot/cmd/internal/util"
|
||||||
preflightcli "github.com/replicatedhq/troubleshoot/cmd/preflight/cli"
|
|
||||||
"github.com/replicatedhq/troubleshoot/internal/traces"
|
"github.com/replicatedhq/troubleshoot/internal/traces"
|
||||||
"github.com/replicatedhq/troubleshoot/pkg/k8sutil"
|
"github.com/replicatedhq/troubleshoot/pkg/k8sutil"
|
||||||
"github.com/replicatedhq/troubleshoot/pkg/logger"
|
"github.com/replicatedhq/troubleshoot/pkg/logger"
|
||||||
@@ -110,7 +109,6 @@ If no arguments are provided, specs are automatically loaded from the cluster by
|
|||||||
cmd.AddCommand(Schedule())
|
cmd.AddCommand(Schedule())
|
||||||
cmd.AddCommand(UploadCmd())
|
cmd.AddCommand(UploadCmd())
|
||||||
cmd.AddCommand(util.VersionCmd())
|
cmd.AddCommand(util.VersionCmd())
|
||||||
cmd.AddCommand(preflightcli.LintCmd())
|
|
||||||
|
|
||||||
cmd.Flags().StringSlice("redactors", []string{}, "names of the additional redactors to use")
|
cmd.Flags().StringSlice("redactors", []string{}, "names of the additional redactors to use")
|
||||||
cmd.Flags().Bool("redact", true, "enable/disable default redactions")
|
cmd.Flags().Bool("redact", true, "enable/disable default redactions")
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
apiVersion: troubleshoot.sh/v1beta3
|
|
||||||
kind: Preflight
|
|
||||||
metadata:
|
|
||||||
name: helm-builtins-example
|
|
||||||
labels:
|
|
||||||
release: {{ .Release.Name }}
|
|
||||||
spec:
|
|
||||||
analyzers:
|
|
||||||
- docString: |
|
|
||||||
Title: Example using Helm builtin objects
|
|
||||||
Requirement: Demonstrates .Values, .Release, .Chart, etc.
|
|
||||||
|
|
||||||
Supported Helm builtin objects:
|
|
||||||
- .Values.* - User-provided values
|
|
||||||
- .Release.Name - Release name (default: "preflight")
|
|
||||||
- .Release.Namespace - Release namespace (default: "default")
|
|
||||||
- .Release.IsInstall - Whether this is an install (true)
|
|
||||||
- .Release.IsUpgrade - Whether this is an upgrade (false)
|
|
||||||
- .Release.Revision - Release revision (1)
|
|
||||||
- .Chart.Name - Chart name
|
|
||||||
- .Chart.Version - Chart version
|
|
||||||
- .Capabilities.KubeVersion - Kubernetes version capabilities
|
|
||||||
clusterVersion:
|
|
||||||
checkName: Kubernetes version check in {{ .Release.Namespace }}
|
|
||||||
outcomes:
|
|
||||||
- fail:
|
|
||||||
when: '< {{ .Values.minVersion | default "1.19.0" }}'
|
|
||||||
message: |
|
|
||||||
Release {{ .Release.Name }} requires Kubernetes {{ .Values.minVersion | default "1.19.0" }} or later.
|
|
||||||
Chart: {{ .Chart.Name }}
|
|
||||||
- pass:
|
|
||||||
when: '>= {{ .Values.minVersion | default "1.19.0" }}'
|
|
||||||
message: Kubernetes version is supported for release {{ .Release.Name }}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
apiVersion: troubleshoot.sh/v1beta3
|
|
||||||
kind: Preflight
|
|
||||||
metadata
|
|
||||||
name: invalid-yaml
|
|
||||||
spec:
|
|
||||||
analyzers:
|
|
||||||
- clusterVersion:
|
|
||||||
checkName: Kubernetes version
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
kind: Preflight
|
|
||||||
metadata:
|
|
||||||
name: missing-apiversion
|
|
||||||
spec:
|
|
||||||
analyzers:
|
|
||||||
- clusterVersion:
|
|
||||||
checkName: Kubernetes version
|
|
||||||
outcomes:
|
|
||||||
- pass:
|
|
||||||
when: '>= 1.19.0'
|
|
||||||
message: Kubernetes version is supported
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
apiVersion: troubleshoot.sh/v1beta3
|
|
||||||
kind: Preflight
|
|
||||||
spec:
|
|
||||||
analyzers:
|
|
||||||
- clusterVersion:
|
|
||||||
checkName: Kubernetes version
|
|
||||||
outcomes:
|
|
||||||
- pass:
|
|
||||||
when: '>= 1.19.0'
|
|
||||||
message: Kubernetes version is supported
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
apiVersion: troubleshoot.sh/v1beta3
|
|
||||||
kind: Preflight
|
|
||||||
metadata:
|
|
||||||
name: no-analyzers
|
|
||||||
spec:
|
|
||||||
collectors:
|
|
||||||
- clusterInfo: {}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
apiVersion: troubleshoot.sh/v1beta3
|
|
||||||
kind: Preflight
|
|
||||||
metadata:
|
|
||||||
name: simple-no-template
|
|
||||||
spec:
|
|
||||||
analyzers:
|
|
||||||
- docString: |
|
|
||||||
Title: Kubernetes Version Check
|
|
||||||
Requirement: Kubernetes 1.19.0 or later
|
|
||||||
clusterVersion:
|
|
||||||
checkName: Kubernetes version
|
|
||||||
outcomes:
|
|
||||||
- fail:
|
|
||||||
when: '< 1.19.0'
|
|
||||||
message: Kubernetes version must be at least 1.19.0
|
|
||||||
- pass:
|
|
||||||
when: '>= 1.19.0'
|
|
||||||
message: Kubernetes version is supported
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
apiVersion: troubleshoot.sh/v1beta3
|
|
||||||
kind: SupportBundle
|
|
||||||
metadata:
|
|
||||||
name: no-collectors
|
|
||||||
spec:
|
|
||||||
analyzers:
|
|
||||||
- clusterVersion:
|
|
||||||
checkName: Kubernetes version
|
|
||||||
outcomes:
|
|
||||||
- pass:
|
|
||||||
when: '>= 1.19.0'
|
|
||||||
message: Kubernetes version is supported
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
apiVersion: troubleshoot.sh/v1beta3
|
|
||||||
kind: SupportBundle
|
|
||||||
metadata:
|
|
||||||
name: valid-support-bundle
|
|
||||||
spec:
|
|
||||||
collectors:
|
|
||||||
- clusterInfo: {}
|
|
||||||
- clusterResources: {}
|
|
||||||
analyzers:
|
|
||||||
- clusterVersion:
|
|
||||||
checkName: Kubernetes version
|
|
||||||
outcomes:
|
|
||||||
- pass:
|
|
||||||
when: '>= 1.19.0'
|
|
||||||
message: Kubernetes version is supported
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
apiVersion: troubleshoot.sh/v1beta3
|
|
||||||
kind: Preflight
|
|
||||||
metadata:
|
|
||||||
name: valid-preflight
|
|
||||||
spec:
|
|
||||||
analyzers:
|
|
||||||
- docString: |
|
|
||||||
Title: Test Analyzer
|
|
||||||
Requirement: Test requirement
|
|
||||||
clusterVersion:
|
|
||||||
checkName: Kubernetes version
|
|
||||||
outcomes:
|
|
||||||
- pass:
|
|
||||||
when: '>= 1.19.0'
|
|
||||||
message: Kubernetes version is supported
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
apiVersion: troubleshoot.sh/v1beta2
|
|
||||||
kind: Preflight
|
|
||||||
metadata:
|
|
||||||
name: wrong-version
|
|
||||||
spec:
|
|
||||||
analyzers:
|
|
||||||
- clusterVersion:
|
|
||||||
checkName: Kubernetes version
|
|
||||||
outcomes:
|
|
||||||
- pass:
|
|
||||||
when: '>= 1.19.0'
|
|
||||||
message: Kubernetes version is supported
|
|
||||||
768
pkg/lint/lint.go
768
pkg/lint/lint.go
@@ -1,768 +0,0 @@
|
|||||||
package lint
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/replicatedhq/troubleshoot/pkg/constants"
|
|
||||||
"sigs.k8s.io/yaml"
|
|
||||||
)
|
|
||||||
|
|
||||||
type LintResult struct {
|
|
||||||
FilePath string
|
|
||||||
Errors []LintError
|
|
||||||
Warnings []LintWarning
|
|
||||||
}
|
|
||||||
|
|
||||||
type LintError struct {
|
|
||||||
Line int
|
|
||||||
Column int
|
|
||||||
Message string
|
|
||||||
Field string
|
|
||||||
}
|
|
||||||
|
|
||||||
type LintWarning struct {
|
|
||||||
Line int
|
|
||||||
Column int
|
|
||||||
Message string
|
|
||||||
Field string
|
|
||||||
}
|
|
||||||
|
|
||||||
type LintOptions struct {
|
|
||||||
FilePaths []string
|
|
||||||
Fix bool
|
|
||||||
Format string // "text" or "json"
|
|
||||||
}
|
|
||||||
|
|
||||||
// LintFiles validates v1beta3 troubleshoot specs for syntax and structural errors
|
|
||||||
func LintFiles(opts LintOptions) ([]LintResult, error) {
|
|
||||||
results := []LintResult{}
|
|
||||||
|
|
||||||
for _, filePath := range opts.FilePaths {
|
|
||||||
result, err := lintFile(filePath, opts.Fix)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
results = append(results, result)
|
|
||||||
}
|
|
||||||
|
|
||||||
return results, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func lintFile(filePath string, fix bool) (LintResult, error) {
|
|
||||||
result := LintResult{
|
|
||||||
FilePath: filePath,
|
|
||||||
Errors: []LintError{},
|
|
||||||
Warnings: []LintWarning{},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read file
|
|
||||||
content, err := os.ReadFile(filePath)
|
|
||||||
if err != nil {
|
|
||||||
return result, errors.Wrapf(err, "failed to read file %s", filePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if file contains template expressions
|
|
||||||
hasTemplates := strings.Contains(string(content), "{{") && strings.Contains(string(content), "}}")
|
|
||||||
|
|
||||||
// Validate YAML syntax (but be lenient with templated files)
|
|
||||||
var parsed map[string]interface{}
|
|
||||||
yamlParseErr := yaml.Unmarshal(content, &parsed)
|
|
||||||
if yamlParseErr != nil {
|
|
||||||
// If the file has templates, YAML parsing may fail - that's expected
|
|
||||||
// We'll still try to validate what we can
|
|
||||||
if !hasTemplates {
|
|
||||||
result.Errors = append(result.Errors, LintError{
|
|
||||||
Line: extractLineFromError(yamlParseErr),
|
|
||||||
Message: fmt.Sprintf("YAML syntax error: %s", yamlParseErr.Error()),
|
|
||||||
})
|
|
||||||
// Don't return yet - we want to try to fix this error
|
|
||||||
// Continue to applyFixes at the end
|
|
||||||
// Try to surface apiVersion issues even if YAML failed to parse
|
|
||||||
// Detect via simple textual scan
|
|
||||||
avLine, avValue := findAPIVersionLineAndValue(string(content))
|
|
||||||
if avLine == 0 {
|
|
||||||
result.Errors = append(result.Errors, LintError{
|
|
||||||
Line: 0,
|
|
||||||
Field: "apiVersion",
|
|
||||||
Message: "Missing or empty 'apiVersion' field",
|
|
||||||
})
|
|
||||||
} else if avValue != constants.Troubleshootv1beta2Kind && avValue != constants.Troubleshootv1beta3Kind {
|
|
||||||
result.Errors = append(result.Errors, LintError{
|
|
||||||
Line: avLine,
|
|
||||||
Field: "apiVersion",
|
|
||||||
Message: fmt.Sprintf("Invalid 'apiVersion' value %q; expected %s or %s", avValue, constants.Troubleshootv1beta2Kind, constants.Troubleshootv1beta3Kind),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if fix {
|
|
||||||
fixed, err := applyFixes(filePath, string(content), result)
|
|
||||||
if err != nil {
|
|
||||||
return result, err
|
|
||||||
}
|
|
||||||
if fixed {
|
|
||||||
// Re-lint to verify fixes
|
|
||||||
return lintFile(filePath, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
// For templated files, we can't parse YAML strictly, so just check template syntax
|
|
||||||
result.Errors = append(result.Errors, checkTemplateSyntax(string(content))...)
|
|
||||||
// Surface apiVersion issues via textual scan for templated files
|
|
||||||
avLine, avValue := findAPIVersionLineAndValue(string(content))
|
|
||||||
if avLine == 0 {
|
|
||||||
result.Errors = append(result.Errors, LintError{
|
|
||||||
Line: 0,
|
|
||||||
Field: "apiVersion",
|
|
||||||
Message: "Missing or empty 'apiVersion' field",
|
|
||||||
})
|
|
||||||
} else if avValue != constants.Troubleshootv1beta2Kind && avValue != constants.Troubleshootv1beta3Kind {
|
|
||||||
result.Errors = append(result.Errors, LintError{
|
|
||||||
Line: avLine,
|
|
||||||
Field: "apiVersion",
|
|
||||||
Message: fmt.Sprintf("Invalid 'apiVersion' value %q; expected %s or %s", avValue, constants.Troubleshootv1beta2Kind, constants.Troubleshootv1beta3Kind),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// Continue to applyFixes for templates too
|
|
||||||
if fix {
|
|
||||||
fixed, err := applyFixes(filePath, string(content), result)
|
|
||||||
if err != nil {
|
|
||||||
return result, err
|
|
||||||
}
|
|
||||||
if fixed {
|
|
||||||
// Re-lint to verify fixes
|
|
||||||
return lintFile(filePath, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check required fields
|
|
||||||
result.Errors = append(result.Errors, checkRequiredFields(parsed, string(content))...)
|
|
||||||
|
|
||||||
// Check template syntax
|
|
||||||
result.Errors = append(result.Errors, checkTemplateSyntax(string(content))...)
|
|
||||||
|
|
||||||
// Check for kind-specific requirements
|
|
||||||
if kind, ok := parsed["kind"].(string); ok {
|
|
||||||
switch kind {
|
|
||||||
case "Preflight":
|
|
||||||
result.Errors = append(result.Errors, checkPreflightSpec(parsed, string(content))...)
|
|
||||||
case "SupportBundle":
|
|
||||||
result.Errors = append(result.Errors, checkSupportBundleSpec(parsed, string(content))...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate apiVersion value if present
|
|
||||||
if apiVersion, ok := parsed["apiVersion"].(string); ok && apiVersion != "" {
|
|
||||||
if apiVersion != constants.Troubleshootv1beta2Kind && apiVersion != constants.Troubleshootv1beta3Kind {
|
|
||||||
result.Errors = append(result.Errors, LintError{
|
|
||||||
Line: findLineNumber(string(content), "apiVersion"),
|
|
||||||
Field: "apiVersion",
|
|
||||||
Message: fmt.Sprintf("Invalid 'apiVersion' value %q; expected %s or %s", apiVersion, constants.Troubleshootv1beta2Kind, constants.Troubleshootv1beta3Kind),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for common issues
|
|
||||||
result.Warnings = append(result.Warnings, checkCommonIssues(parsed, string(content))...)
|
|
||||||
|
|
||||||
// Apply fixes if requested
|
|
||||||
if fix && (len(result.Errors) > 0 || len(result.Warnings) > 0) {
|
|
||||||
fixed, err := applyFixes(filePath, string(content), result)
|
|
||||||
if err != nil {
|
|
||||||
return result, err
|
|
||||||
}
|
|
||||||
if fixed {
|
|
||||||
// Re-lint to verify fixes
|
|
||||||
return lintFile(filePath, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkRequiredFields(parsed map[string]interface{}, content string) []LintError {
|
|
||||||
errors := []LintError{}
|
|
||||||
|
|
||||||
// Check apiVersion
|
|
||||||
if apiVersion, ok := parsed["apiVersion"].(string); !ok || apiVersion == "" {
|
|
||||||
errors = append(errors, LintError{
|
|
||||||
Line: findLineNumber(content, "apiVersion"),
|
|
||||||
Field: "apiVersion",
|
|
||||||
Message: "Missing or empty 'apiVersion' field",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check kind
|
|
||||||
if kind, ok := parsed["kind"].(string); !ok || kind == "" {
|
|
||||||
errors = append(errors, LintError{
|
|
||||||
Line: findLineNumber(content, "kind"),
|
|
||||||
Field: "kind",
|
|
||||||
Message: "Missing or empty 'kind' field",
|
|
||||||
})
|
|
||||||
} else if kind != "Preflight" && kind != "SupportBundle" {
|
|
||||||
errors = append(errors, LintError{
|
|
||||||
Line: findLineNumber(content, "kind"),
|
|
||||||
Field: "kind",
|
|
||||||
Message: fmt.Sprintf("Invalid kind '%s'. Must be 'Preflight' or 'SupportBundle'", kind),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check metadata
|
|
||||||
if _, ok := parsed["metadata"]; !ok {
|
|
||||||
errors = append(errors, LintError{
|
|
||||||
Line: findLineNumber(content, "metadata"),
|
|
||||||
Field: "metadata",
|
|
||||||
Message: "Missing 'metadata' section",
|
|
||||||
})
|
|
||||||
} else if metadata, ok := parsed["metadata"].(map[string]interface{}); ok {
|
|
||||||
if name, ok := metadata["name"].(string); !ok || name == "" {
|
|
||||||
errors = append(errors, LintError{
|
|
||||||
Line: findLineNumber(content, "name"),
|
|
||||||
Field: "metadata.name",
|
|
||||||
Message: "Missing or empty 'metadata.name' field",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check spec
|
|
||||||
if _, ok := parsed["spec"]; !ok {
|
|
||||||
errors = append(errors, LintError{
|
|
||||||
Line: findLineNumber(content, "spec"),
|
|
||||||
Field: "spec",
|
|
||||||
Message: "Missing 'spec' section",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkTemplateSyntax(content string) []LintError {
|
|
||||||
errors := []LintError{}
|
|
||||||
lines := strings.Split(content, "\n")
|
|
||||||
|
|
||||||
// Check for unmatched braces
|
|
||||||
for i, line := range lines {
|
|
||||||
// Count opening and closing braces
|
|
||||||
opening := strings.Count(line, "{{")
|
|
||||||
closing := strings.Count(line, "}}")
|
|
||||||
|
|
||||||
if opening != closing {
|
|
||||||
errors = append(errors, LintError{
|
|
||||||
Line: i + 1,
|
|
||||||
Message: fmt.Sprintf("Unmatched template braces: %d opening, %d closing", opening, closing),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for common template syntax issues
|
|
||||||
// Look for templates that might be missing the leading dot
|
|
||||||
if strings.Contains(line, "{{") && strings.Contains(line, "}}") {
|
|
||||||
// Extract template expressions
|
|
||||||
templateExpr := extractTemplateBetweenBraces(line)
|
|
||||||
for _, expr := range templateExpr {
|
|
||||||
trimmed := strings.TrimSpace(expr)
|
|
||||||
|
|
||||||
// Skip empty expressions
|
|
||||||
if trimmed == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip control structures (if, else, end, range, with, etc.)
|
|
||||||
if isControlStructure(trimmed) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip comments: {{/* ... */}}
|
|
||||||
if strings.HasPrefix(trimmed, "/*") || strings.HasPrefix(trimmed, "*/") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip template variables (start with $)
|
|
||||||
if strings.HasPrefix(trimmed, "$") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip expressions that start with a dot (valid references)
|
|
||||||
if strings.HasPrefix(trimmed, ".") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip string literals
|
|
||||||
if strings.HasPrefix(trimmed, "\"") || strings.HasPrefix(trimmed, "'") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip numeric literals
|
|
||||||
if regexp.MustCompile(`^[0-9]+$`).MatchString(trimmed) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip function calls (contain parentheses or pipes)
|
|
||||||
if strings.Contains(trimmed, "(") || strings.Contains(trimmed, "|") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip known Helm functions/keywords
|
|
||||||
helmFunctions := []string{"toYaml", "toJson", "include", "required", "default", "quote", "nindent", "indent", "upper", "lower", "trim"}
|
|
||||||
isFunction := false
|
|
||||||
for _, fn := range helmFunctions {
|
|
||||||
if strings.HasPrefix(trimmed, fn+" ") || trimmed == fn {
|
|
||||||
isFunction = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if isFunction {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we got here, it might be missing a leading dot
|
|
||||||
errors = append(errors, LintError{
|
|
||||||
Line: i + 1,
|
|
||||||
Message: fmt.Sprintf("Template expression may be missing leading dot: {{ %s }}", expr),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkPreflightSpec(parsed map[string]interface{}, content string) []LintError {
|
|
||||||
errors := []LintError{}
|
|
||||||
|
|
||||||
spec, ok := parsed["spec"].(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
return errors
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for analyzers
|
|
||||||
analyzers, hasAnalyzers := spec["analyzers"]
|
|
||||||
if !hasAnalyzers {
|
|
||||||
errors = append(errors, LintError{
|
|
||||||
Line: findLineNumber(content, "spec:"),
|
|
||||||
Field: "spec.analyzers",
|
|
||||||
Message: "Preflight spec must contain 'analyzers'",
|
|
||||||
})
|
|
||||||
} else if analyzersList, ok := analyzers.([]interface{}); ok {
|
|
||||||
if len(analyzersList) == 0 {
|
|
||||||
errors = append(errors, LintError{
|
|
||||||
Line: findLineNumber(content, "analyzers"),
|
|
||||||
Field: "spec.analyzers",
|
|
||||||
Message: "Preflight spec must have at least one analyzer",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkSupportBundleSpec(parsed map[string]interface{}, content string) []LintError {
|
|
||||||
errors := []LintError{}
|
|
||||||
|
|
||||||
spec, ok := parsed["spec"].(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
return errors
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for collectors
|
|
||||||
collectors, hasCollectors := spec["collectors"]
|
|
||||||
_, hasHostCollectors := spec["hostCollectors"]
|
|
||||||
|
|
||||||
if !hasCollectors && !hasHostCollectors {
|
|
||||||
errors = append(errors, LintError{
|
|
||||||
Line: findLineNumber(content, "spec:"),
|
|
||||||
Field: "spec.collectors",
|
|
||||||
Message: "SupportBundle spec must contain 'collectors' or 'hostCollectors'",
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// Check if collectors list is empty
|
|
||||||
if hasCollectors {
|
|
||||||
if collectorsList, ok := collectors.([]interface{}); ok && len(collectorsList) == 0 {
|
|
||||||
errors = append(errors, LintError{
|
|
||||||
Line: findLineNumber(content, "collectors"),
|
|
||||||
Field: "spec.collectors",
|
|
||||||
Message: "Collectors list is empty",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkCommonIssues(parsed map[string]interface{}, content string) []LintWarning {
|
|
||||||
warnings := []LintWarning{}
|
|
||||||
|
|
||||||
// Check for missing docStrings in analyzers
|
|
||||||
spec, ok := parsed["spec"].(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
return warnings
|
|
||||||
}
|
|
||||||
|
|
||||||
if analyzers, ok := spec["analyzers"].([]interface{}); ok {
|
|
||||||
for i, analyzer := range analyzers {
|
|
||||||
if analyzerMap, ok := analyzer.(map[string]interface{}); ok {
|
|
||||||
if _, hasDocString := analyzerMap["docString"]; !hasDocString {
|
|
||||||
warnings = append(warnings, LintWarning{
|
|
||||||
Line: findAnalyzerLine(content, i),
|
|
||||||
Field: fmt.Sprintf("spec.analyzers[%d].docString", i),
|
|
||||||
Message: "Analyzer missing docString (recommended for v1beta3)",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return warnings
|
|
||||||
}
|
|
||||||
|
|
||||||
func applyFixes(filePath, content string, result LintResult) (bool, error) {
|
|
||||||
fixed := false
|
|
||||||
newContent := content
|
|
||||||
lines := strings.Split(newContent, "\n")
|
|
||||||
|
|
||||||
// Determine desired apiVersion for fixes
|
|
||||||
hasTemplates := strings.Contains(content, "{{") && strings.Contains(content, "}}")
|
|
||||||
hasDocStrings := strings.Contains(content, "docString:")
|
|
||||||
desiredAPIVersion := constants.Troubleshootv1beta2Kind
|
|
||||||
if hasTemplates || hasDocStrings {
|
|
||||||
desiredAPIVersion = constants.Troubleshootv1beta3Kind
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort errors by line number (descending) to avoid line number shifts when editing
|
|
||||||
errorsByLine := make(map[int][]LintError)
|
|
||||||
for _, err := range result.Errors {
|
|
||||||
if err.Line > 0 {
|
|
||||||
errorsByLine[err.Line] = append(errorsByLine[err.Line], err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process errors line by line
|
|
||||||
for lineNum, errs := range errorsByLine {
|
|
||||||
if lineNum > len(lines) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
line := lines[lineNum-1]
|
|
||||||
originalLine := line
|
|
||||||
|
|
||||||
for _, err := range errs {
|
|
||||||
// Fix 1: Add missing colon
|
|
||||||
// YAML parsers often report the error on the line AFTER the actual problem
|
|
||||||
if strings.Contains(err.Message, "could not find expected ':'") {
|
|
||||||
// Check current line first
|
|
||||||
if !strings.Contains(line, ":") {
|
|
||||||
trimmed := strings.TrimSpace(line)
|
|
||||||
indent := line[:len(line)-len(strings.TrimLeft(line, " \t"))]
|
|
||||||
line = indent + trimmed + ":"
|
|
||||||
fixed = true
|
|
||||||
} else if lineNum > 1 {
|
|
||||||
// Check previous line (where the colon is likely missing)
|
|
||||||
prevLine := lines[lineNum-2]
|
|
||||||
if !strings.Contains(prevLine, ":") && strings.TrimSpace(prevLine) != "" {
|
|
||||||
trimmed := strings.TrimSpace(prevLine)
|
|
||||||
indent := prevLine[:len(prevLine)-len(strings.TrimLeft(prevLine, " \t"))]
|
|
||||||
lines[lineNum-2] = indent + trimmed + ":"
|
|
||||||
fixed = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fix 2: Add missing leading dot in template expressions
|
|
||||||
if strings.Contains(err.Message, "Template expression may be missing leading dot:") {
|
|
||||||
// Extract the expression from the error message
|
|
||||||
re := regexp.MustCompile(`Template expression may be missing leading dot: \{\{ (.+?) \}\}`)
|
|
||||||
matches := re.FindStringSubmatch(err.Message)
|
|
||||||
if len(matches) > 1 {
|
|
||||||
badExpr := matches[1]
|
|
||||||
// Add the leading dot
|
|
||||||
fixedExpr := "." + badExpr
|
|
||||||
// Replace in the line
|
|
||||||
line = strings.Replace(line, "{{ "+badExpr+" }}", "{{ "+fixedExpr+" }}", 1)
|
|
||||||
line = strings.Replace(line, "{{"+badExpr+"}}", "{{"+fixedExpr+"}}", 1)
|
|
||||||
line = strings.Replace(line, "{{- "+badExpr+" }}", "{{- "+fixedExpr+" }}", 1)
|
|
||||||
line = strings.Replace(line, "{{- "+badExpr+" -}}", "{{- "+fixedExpr+" -}}", 1)
|
|
||||||
fixed = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fix 3: Replace invalid apiVersion value with desiredAPIVersion
|
|
||||||
if strings.Contains(err.Message, "Invalid 'apiVersion' value") && err.Field == "apiVersion" {
|
|
||||||
if strings.Contains(line, "apiVersion:") {
|
|
||||||
indent := line[:len(line)-len(strings.TrimLeft(line, " \t"))]
|
|
||||||
line = indent + "apiVersion: " + desiredAPIVersion
|
|
||||||
fixed = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the line if it changed
|
|
||||||
if line != originalLine {
|
|
||||||
lines[lineNum-1] = line
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fix 4: Add missing required top-level fields
|
|
||||||
for _, err := range result.Errors {
|
|
||||||
if err.Field == "apiVersion" && strings.Contains(err.Message, "Missing or empty 'apiVersion'") {
|
|
||||||
// Replace existing empty apiVersion line if present; otherwise prepend
|
|
||||||
if avLine, avVal := findAPIVersionLineAndValue(newContent); avLine > 0 && strings.TrimSpace(avVal) == "" {
|
|
||||||
line := lines[avLine-1]
|
|
||||||
indent := line[:len(line)-len(strings.TrimLeft(line, " \t"))]
|
|
||||||
lines[avLine-1] = indent + "apiVersion: " + desiredAPIVersion
|
|
||||||
} else {
|
|
||||||
lines = append([]string{"apiVersion: " + desiredAPIVersion}, lines...)
|
|
||||||
}
|
|
||||||
fixed = true
|
|
||||||
} else if err.Field == "kind" && strings.Contains(err.Message, "Missing or empty 'kind'") {
|
|
||||||
// Try to determine if it should be Preflight or SupportBundle based on filename
|
|
||||||
kind := "Preflight"
|
|
||||||
if strings.Contains(strings.ToLower(filePath), "bundle") {
|
|
||||||
kind = "SupportBundle"
|
|
||||||
}
|
|
||||||
// Add kind after apiVersion
|
|
||||||
insertIndex := 0
|
|
||||||
for i, line := range lines {
|
|
||||||
if strings.Contains(line, "apiVersion:") {
|
|
||||||
insertIndex = i + 1
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
newLines := make([]string, 0, len(lines)+1)
|
|
||||||
newLines = append(newLines, lines[:insertIndex]...)
|
|
||||||
newLines = append(newLines, "kind: "+kind)
|
|
||||||
newLines = append(newLines, lines[insertIndex:]...)
|
|
||||||
lines = newLines
|
|
||||||
fixed = true
|
|
||||||
} else if err.Field == "metadata" && strings.Contains(err.Message, "Missing 'metadata'") {
|
|
||||||
// Add metadata section after kind
|
|
||||||
insertIndex := 0
|
|
||||||
for i, line := range lines {
|
|
||||||
if strings.Contains(line, "kind:") {
|
|
||||||
insertIndex = i + 1
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
newLines := make([]string, 0, len(lines)+2)
|
|
||||||
newLines = append(newLines, lines[:insertIndex]...)
|
|
||||||
newLines = append(newLines, "metadata:")
|
|
||||||
newLines = append(newLines, " name: my-spec")
|
|
||||||
newLines = append(newLines, lines[insertIndex:]...)
|
|
||||||
lines = newLines
|
|
||||||
fixed = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write fixed content back to file if changes were made
|
|
||||||
if fixed {
|
|
||||||
newContent = strings.Join(lines, "\n")
|
|
||||||
if err := os.WriteFile(filePath, []byte(newContent), 0644); err != nil {
|
|
||||||
return false, errors.Wrapf(err, "failed to write fixed content to %s", filePath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return fixed, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func findLineNumber(content, search string) int {
|
|
||||||
lines := strings.Split(content, "\n")
|
|
||||||
for i, line := range lines {
|
|
||||||
if strings.Contains(line, search) {
|
|
||||||
return i + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// findAPIVersionLineAndValue locates the first line that declares apiVersion and returns its
|
|
||||||
// 1-based line number and the trimmed value to the right of the colon. Returns (0, "") if not found.
|
|
||||||
func findAPIVersionLineAndValue(content string) (int, string) {
|
|
||||||
lines := strings.Split(content, "\n")
|
|
||||||
for i, line := range lines {
|
|
||||||
trimmed := strings.TrimSpace(line)
|
|
||||||
if strings.HasPrefix(trimmed, "apiVersion:") {
|
|
||||||
// extract value after the first colon
|
|
||||||
parts := strings.SplitN(trimmed, ":", 2)
|
|
||||||
if len(parts) == 2 {
|
|
||||||
value := strings.TrimSpace(parts[1])
|
|
||||||
return i + 1, value
|
|
||||||
}
|
|
||||||
return i + 1, ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0, ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func findAnalyzerLine(content string, index int) int {
|
|
||||||
lines := strings.Split(content, "\n")
|
|
||||||
analyzerCount := 0
|
|
||||||
inAnalyzers := false
|
|
||||||
|
|
||||||
for i, line := range lines {
|
|
||||||
if strings.Contains(line, "analyzers:") {
|
|
||||||
inAnalyzers = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if inAnalyzers && strings.HasPrefix(strings.TrimSpace(line), "- ") {
|
|
||||||
if analyzerCount == index {
|
|
||||||
return i + 1
|
|
||||||
}
|
|
||||||
analyzerCount++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func extractLineFromError(err error) int {
|
|
||||||
// Try to extract line number from YAML error message
|
|
||||||
re := regexp.MustCompile(`line (\d+)`)
|
|
||||||
matches := re.FindStringSubmatch(err.Error())
|
|
||||||
if len(matches) > 1 {
|
|
||||||
var line int
|
|
||||||
fmt.Sscanf(matches[1], "%d", &line)
|
|
||||||
return line
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// extractTemplateBetweenBraces extracts template expressions from a line
|
|
||||||
func extractTemplateBetweenBraces(line string) []string {
|
|
||||||
var expressions []string
|
|
||||||
// Match {{ ... }} with optional whitespace trimming (-), including comments {{/* */}}
|
|
||||||
re := regexp.MustCompile(`\{\{-?\s*(.+?)\s*-?\}\}`)
|
|
||||||
matches := re.FindAllStringSubmatch(line, -1)
|
|
||||||
for _, match := range matches {
|
|
||||||
if len(match) > 1 {
|
|
||||||
// Clean up the expression
|
|
||||||
expr := match[1]
|
|
||||||
// Remove */ at the end if it's part of a comment
|
|
||||||
expr = strings.TrimSuffix(strings.TrimSpace(expr), "*/")
|
|
||||||
expressions = append(expressions, expr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return expressions
|
|
||||||
}
|
|
||||||
|
|
||||||
// isControlStructure checks if a template expression is a control structure
|
|
||||||
func isControlStructure(expr string) bool {
|
|
||||||
trimmed := strings.TrimSpace(expr)
|
|
||||||
controlKeywords := []string{"if", "else", "end", "range", "with", "define", "template", "block", "include"}
|
|
||||||
for _, keyword := range controlKeywords {
|
|
||||||
if strings.HasPrefix(trimmed, keyword+" ") || trimmed == keyword {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// FormatResults formats lint results for output
|
|
||||||
func FormatResults(results []LintResult, format string) string {
|
|
||||||
if format == "json" {
|
|
||||||
return formatJSON(results)
|
|
||||||
}
|
|
||||||
return formatText(results)
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatText(results []LintResult) string {
|
|
||||||
var output strings.Builder
|
|
||||||
totalErrors := 0
|
|
||||||
totalWarnings := 0
|
|
||||||
|
|
||||||
for _, result := range results {
|
|
||||||
if len(result.Errors) == 0 && len(result.Warnings) == 0 {
|
|
||||||
output.WriteString(fmt.Sprintf("✓ %s: No issues found\n", result.FilePath))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
output.WriteString(fmt.Sprintf("\n%s:\n", result.FilePath))
|
|
||||||
|
|
||||||
for _, err := range result.Errors {
|
|
||||||
output.WriteString(fmt.Sprintf(" ✗ Error (line %d): %s\n", err.Line, err.Message))
|
|
||||||
if err.Field != "" {
|
|
||||||
output.WriteString(fmt.Sprintf(" Field: %s\n", err.Field))
|
|
||||||
}
|
|
||||||
totalErrors++
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, warn := range result.Warnings {
|
|
||||||
output.WriteString(fmt.Sprintf(" ⚠ Warning (line %d): %s\n", warn.Line, warn.Message))
|
|
||||||
if warn.Field != "" {
|
|
||||||
output.WriteString(fmt.Sprintf(" Field: %s\n", warn.Field))
|
|
||||||
}
|
|
||||||
totalWarnings++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
output.WriteString(fmt.Sprintf("\nSummary: %d error(s), %d warning(s) across %d file(s)\n", totalErrors, totalWarnings, len(results)))
|
|
||||||
|
|
||||||
return output.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatJSON(results []LintResult) string {
|
|
||||||
// Simple JSON formatting without importing encoding/json
|
|
||||||
var output strings.Builder
|
|
||||||
output.WriteString("{\n")
|
|
||||||
output.WriteString(" \"results\": [\n")
|
|
||||||
|
|
||||||
for i, result := range results {
|
|
||||||
output.WriteString(" {\n")
|
|
||||||
output.WriteString(fmt.Sprintf(" \"filePath\": %q,\n", result.FilePath))
|
|
||||||
output.WriteString(" \"errors\": [\n")
|
|
||||||
|
|
||||||
for j, err := range result.Errors {
|
|
||||||
output.WriteString(" {\n")
|
|
||||||
output.WriteString(fmt.Sprintf(" \"line\": %d,\n", err.Line))
|
|
||||||
output.WriteString(fmt.Sprintf(" \"column\": %d,\n", err.Column))
|
|
||||||
output.WriteString(fmt.Sprintf(" \"message\": %q,\n", err.Message))
|
|
||||||
output.WriteString(fmt.Sprintf(" \"field\": %q\n", err.Field))
|
|
||||||
output.WriteString(" }")
|
|
||||||
if j < len(result.Errors)-1 {
|
|
||||||
output.WriteString(",")
|
|
||||||
}
|
|
||||||
output.WriteString("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
output.WriteString(" ],\n")
|
|
||||||
output.WriteString(" \"warnings\": [\n")
|
|
||||||
|
|
||||||
for j, warn := range result.Warnings {
|
|
||||||
output.WriteString(" {\n")
|
|
||||||
output.WriteString(fmt.Sprintf(" \"line\": %d,\n", warn.Line))
|
|
||||||
output.WriteString(fmt.Sprintf(" \"column\": %d,\n", warn.Column))
|
|
||||||
output.WriteString(fmt.Sprintf(" \"message\": %q,\n", warn.Message))
|
|
||||||
output.WriteString(fmt.Sprintf(" \"field\": %q\n", warn.Field))
|
|
||||||
output.WriteString(" }")
|
|
||||||
if j < len(result.Warnings)-1 {
|
|
||||||
output.WriteString(",")
|
|
||||||
}
|
|
||||||
output.WriteString("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
output.WriteString(" ]\n")
|
|
||||||
output.WriteString(" }")
|
|
||||||
if i < len(results)-1 {
|
|
||||||
output.WriteString(",")
|
|
||||||
}
|
|
||||||
output.WriteString("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
output.WriteString(" ]\n")
|
|
||||||
output.WriteString("}\n")
|
|
||||||
|
|
||||||
return output.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// HasErrors returns true if any of the results contain errors
|
|
||||||
func HasErrors(results []LintResult) bool {
|
|
||||||
for _, result := range results {
|
|
||||||
if len(result.Errors) > 0 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user