Files
Noah CampbellandGitHub 8197ddecfe added --values and --set flags to lint command (#1907)
* added --values and --set flags to lint command

* Update lint_test.go
2025-10-23 13:20:21 -05:00

262 lines
8.6 KiB
Go

package preflight
import (
"context"
"os"
"strings"
"github.com/pkg/errors"
"github.com/replicatedhq/troubleshoot/internal/specs"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/replicatedhq/troubleshoot/pkg/constants"
"github.com/replicatedhq/troubleshoot/pkg/k8sutil"
"github.com/replicatedhq/troubleshoot/pkg/loader"
"github.com/spf13/viper"
"helm.sh/helm/v3/pkg/strvals"
"k8s.io/client-go/kubernetes"
yaml "sigs.k8s.io/yaml"
)
func readSpecs(args []string) (*loader.TroubleshootKinds, error) {
config, err := k8sutil.GetRESTConfig()
if err != nil {
return nil, errors.Wrap(err, "failed to convert kube flags to rest config")
}
client, err := kubernetes.NewForConfig(config)
if err != nil {
return nil, errors.Wrap(err, "failed to convert create k8s client")
}
// Pre-process v1beta3 specs with templates if values are provided
processedArgs, tempFiles, err := preprocessV1Beta3Specs(args)
if err != nil {
return nil, errors.Wrap(err, "failed to preprocess v1beta3 specs")
}
// Ensure any temp files created during preprocessing are cleaned up
defer func() {
for _, f := range tempFiles {
_ = os.Remove(f)
}
}()
ctx := context.Background()
kinds, err := specs.LoadFromCLIArgs(ctx, client, processedArgs, viper.GetViper())
if err != nil {
return nil, err
}
// Load additional specs from URIs
// only when no-uri flag is not set
if !viper.GetBool("no-uri") {
specs.LoadAdditionalSpecFromURIs(ctx, kinds)
}
ret := loader.NewTroubleshootKinds()
// Concatenate all preflight inclusterSpecs that don't have an upload destination
inclusterSpecs := []troubleshootv1beta2.Preflight{}
var concatenatedSpec *troubleshootv1beta2.Preflight
for _, v := range kinds.PreflightsV1Beta2 {
v := v // https://golang.org/doc/faq#closures_and_goroutines
if v.Spec.UploadResultsTo == "" {
concatenatedSpec = ConcatPreflightSpec(concatenatedSpec, &v)
} else {
inclusterSpecs = append(inclusterSpecs, v)
}
}
if concatenatedSpec != nil {
inclusterSpecs = append(inclusterSpecs, *concatenatedSpec)
}
ret.PreflightsV1Beta2 = inclusterSpecs
var hostSpec *troubleshootv1beta2.HostPreflight
for _, v := range kinds.HostPreflightsV1Beta2 {
v := v // https://golang.org/doc/faq#closures_and_goroutines
hostSpec = ConcatHostPreflightSpec(hostSpec, &v)
}
if hostSpec != nil {
ret.HostPreflightsV1Beta2 = []troubleshootv1beta2.HostPreflight{*hostSpec}
}
return ret, nil
}
// preprocessV1Beta3Specs processes v1beta3 specs with template rendering if values are provided
func preprocessV1Beta3Specs(args []string) ([]string, []string, error) {
valuesFiles := viper.GetStringSlice("values")
setValues := viper.GetStringSlice("set")
// Check if any args contain v1beta3 specs
hasV1Beta3 := false
for _, arg := range args {
// Skip non-file arguments
if arg == "-" || strings.HasPrefix(arg, "http://") || strings.HasPrefix(arg, "https://") ||
strings.HasPrefix(arg, "secret/") || strings.HasPrefix(arg, "configmap/") {
continue
}
// Check if file exists
if _, err := os.Stat(arg); err != nil {
continue
}
// Read the file
content, err := os.ReadFile(arg)
if err != nil {
continue
}
// Check if it's a v1beta3 spec with templates
contentStr := string(content)
var parsed map[string]interface{}
if err := yaml.Unmarshal(content, &parsed); err == nil {
if apiVersion, ok := parsed["apiVersion"]; ok && apiVersion == constants.Troubleshootv1beta3Kind {
// Only require values if the spec has Helm templates
if strings.Contains(contentStr, "{{") && strings.Contains(contentStr, "}}") {
hasV1Beta3 = true
break
}
}
} else {
// If YAML parsing fails, check raw content for v1beta3 with templates
if strings.Contains(contentStr, "apiVersion: troubleshoot.sh/v1beta3") &&
strings.Contains(contentStr, "{{") && strings.Contains(contentStr, "}}") {
hasV1Beta3 = true
break
}
}
}
// If v1beta3 spec with templates found but no values provided, return error
if hasV1Beta3 && len(valuesFiles) == 0 && len(setValues) == 0 {
return nil, nil, errors.New("v1beta3 specs with Helm templates require a values file. Please provide values using --values or --set flags")
}
// If no values provided, return args unchanged
if len(valuesFiles) == 0 && len(setValues) == 0 {
return args, nil, nil
}
// Load values from files and --set flags
values := make(map[string]interface{})
for _, valuesFile := range valuesFiles {
if valuesFile == "" {
continue
}
data, err := os.ReadFile(valuesFile)
if err != nil {
return nil, nil, errors.Wrapf(err, "failed to read values file %s", valuesFile)
}
var fileValues map[string]interface{}
if err := yaml.Unmarshal(data, &fileValues); err != nil {
return nil, nil, errors.Wrapf(err, "failed to parse values file %s", valuesFile)
}
values = MergeMaps(values, fileValues)
}
// Apply --set values
for _, setValue := range setValues {
if err := strvals.ParseInto(setValue, values); err != nil {
return nil, nil, errors.Wrapf(err, "failed to parse --set value: %s", setValue)
}
}
// Process each arg
processedArgs := make([]string, 0, len(args))
tempFiles := make([]string, 0)
for _, arg := range args {
// Skip non-file arguments (like URLs, stdin, etc.)
if arg == "-" || strings.HasPrefix(arg, "http://") || strings.HasPrefix(arg, "https://") ||
strings.HasPrefix(arg, "secret/") || strings.HasPrefix(arg, "configmap/") {
processedArgs = append(processedArgs, arg)
continue
}
// Check if file exists
if _, err := os.Stat(arg); err != nil {
processedArgs = append(processedArgs, arg)
continue
}
// Read the file
content, err := os.ReadFile(arg)
if err != nil {
return nil, nil, errors.Wrapf(err, "failed to read file %s", arg)
}
// Check if it's a v1beta3 spec with templates
var parsed map[string]interface{}
if err := yaml.Unmarshal(content, &parsed); err != nil {
// Not valid YAML, might be templated - try to detect v1beta3
contentStr := string(content)
if strings.Contains(contentStr, "apiVersion: troubleshoot.sh/v1beta3") &&
strings.Contains(contentStr, "{{") && strings.Contains(contentStr, "}}") {
// It's a v1beta3 template, render it
// Seed default-false for referenced boolean flags and create parent maps for any
// .Values.* paths so missing values behave as empty and blocks can be omitted.
SeedDefaultBooleans(contentStr, values)
SeedParentMapsForValueRefs(contentStr, values)
rendered, err := RenderWithHelmTemplate(contentStr, values)
if err != nil {
return nil, nil, errors.Wrapf(err, "failed to render v1beta3 template %s", arg)
}
// Write to temp file
tmpFile, err := os.CreateTemp("", "preflight-rendered-*.yaml")
if err != nil {
return nil, nil, errors.Wrap(err, "failed to create temp file")
}
if _, err := tmpFile.WriteString(rendered); err != nil {
tmpFile.Close()
os.Remove(tmpFile.Name())
return nil, nil, errors.Wrap(err, "failed to write rendered template")
}
tmpFile.Close()
processedArgs = append(processedArgs, tmpFile.Name())
tempFiles = append(tempFiles, tmpFile.Name())
} else {
processedArgs = append(processedArgs, arg)
}
} else {
// Valid YAML, check if it's v1beta3 with templates
if apiVersion, ok := parsed["apiVersion"]; ok && apiVersion == constants.Troubleshootv1beta3Kind {
contentStr := string(content)
if strings.Contains(contentStr, "{{") && strings.Contains(contentStr, "}}") {
// It's a v1beta3 template, render it
// Seed default-false for referenced boolean flags and create parent maps for .Values.* paths
SeedDefaultBooleans(contentStr, values)
SeedParentMapsForValueRefs(contentStr, values)
rendered, err := RenderWithHelmTemplate(contentStr, values)
if err != nil {
return nil, nil, errors.Wrapf(err, "failed to render v1beta3 template %s", arg)
}
// Write to temp file
tmpFile, err := os.CreateTemp("", "preflight-rendered-*.yaml")
if err != nil {
return nil, nil, errors.Wrap(err, "failed to create temp file")
}
if _, err := tmpFile.WriteString(rendered); err != nil {
tmpFile.Close()
os.Remove(tmpFile.Name())
return nil, nil, errors.Wrap(err, "failed to write rendered template")
}
tmpFile.Close()
processedArgs = append(processedArgs, tmpFile.Name())
tempFiles = append(tempFiles, tmpFile.Name())
} else {
// v1beta3 but no templates
processedArgs = append(processedArgs, arg)
}
} else {
// Not v1beta3
processedArgs = append(processedArgs, arg)
}
}
}
return processedArgs, tempFiles, nil
}