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 }