diff --git a/cmd/preflight/cli/root.go b/cmd/preflight/cli/root.go index 225784b1f..d6b84d632 100644 --- a/cmd/preflight/cli/root.go +++ b/cmd/preflight/cli/root.go @@ -66,6 +66,7 @@ that a cluster meets the requirements to run an application.`, cmd.AddCommand(VersionCmd()) cmd.AddCommand(OciFetchCmd()) + cmd.AddCommand(util.DumpSpecCmd()) preflight.AddFlags(cmd.PersistentFlags()) k8sutil.AddFlags(cmd.Flags()) diff --git a/cmd/troubleshoot/cli/root.go b/cmd/troubleshoot/cli/root.go index 6c670fcd1..9fb05539e 100644 --- a/cmd/troubleshoot/cli/root.go +++ b/cmd/troubleshoot/cli/root.go @@ -62,6 +62,7 @@ from a server that can be used to assist when troubleshooting a Kubernetes clust cmd.AddCommand(Analyze()) cmd.AddCommand(Redact()) + cmd.AddCommand(util.DumpSpecCmd()) cmd.AddCommand(VersionCmd()) cmd.Flags().StringSlice("redactors", []string{}, "names of the additional redactors to use") diff --git a/cmd/util/dump_spec.go b/cmd/util/dump_spec.go new file mode 100644 index 000000000..c97cc186b --- /dev/null +++ b/cmd/util/dump_spec.go @@ -0,0 +1,134 @@ +package util + +import ( + "context" + "fmt" + "io" + "net/http" + "net/url" + "os" + "strings" + + "github.com/pkg/errors" + "github.com/replicatedhq/troubleshoot/internal/util" + "github.com/replicatedhq/troubleshoot/pkg/constants" + "github.com/replicatedhq/troubleshoot/pkg/loader" + "github.com/replicatedhq/troubleshoot/pkg/oci" + "github.com/replicatedhq/troubleshoot/pkg/specs" + "github.com/replicatedhq/troubleshoot/pkg/types" + "github.com/spf13/cobra" + "gopkg.in/yaml.v2" +) + +func DumpSpecCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "dump [URI]", + Args: cobra.MinimumNArgs(1), + Short: "BLAH BLAH BLAH", + RunE: func(cmd *cobra.Command, args []string) error { + return printSpecs(args) + }, + } + return cmd +} + +func printSpecs(args []string) error { + var theSpec []byte + var err error + ctx := context.Background() + + kinds := loader.NewTroubleshootKinds() + + // TODO: Earmarked for cleanup in favour of loader.LoadFromArgs(args []string) + for _, v := range args { + if strings.HasPrefix(v, "secret/") { + // format secret/namespace-name/secret-name + pathParts := strings.Split(v, "/") + if len(pathParts) != 3 { + return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, errors.Errorf("path %s must have 3 components", v)) + } + + spec, err := specs.LoadFromSecret(pathParts[1], pathParts[2], "preflight-spec") + if err != nil { + return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, errors.Wrap(err, "failed to get spec from secret")) + } + + theSpec = spec + } else if _, err = os.Stat(v); err == nil { + b, err := os.ReadFile(v) + if err != nil { + return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, err) + } + + theSpec = b + } else if v == "-" { + b, err := io.ReadAll(os.Stdin) + if err != nil { + return types.NewExitCodeError(constants.EXIT_CODE_CATCH_ALL, err) + } + theSpec = b + } else { + u, err := url.Parse(v) + if err != nil { + return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, err) + } + + if u.Scheme == "oci" { + content, err := oci.PullPreflightFromOCI(v) + if err != nil { + if err == oci.ErrNoRelease { + return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, errors.Errorf("no release found for %s.\nCheck the oci:// uri for errors or contact the application vendor for support.", v)) + } + + return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, err) + } + + theSpec = content + } else { + if !util.IsURL(v) { + return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, fmt.Errorf("%s is not a URL and was not found (err %s)", v, err)) + } + + req, err := http.NewRequest("GET", v, nil) + if err != nil { + // exit code: should this be catch all or spec issues...? + return types.NewExitCodeError(constants.EXIT_CODE_CATCH_ALL, err) + } + req.Header.Set("User-Agent", "Replicated_Preflight/v1beta2") + resp, err := http.DefaultClient.Do(req) + if err != nil { + // exit code: should this be catch all or spec issues...? + return types.NewExitCodeError(constants.EXIT_CODE_CATCH_ALL, err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, err) + } + + theSpec = body + } + } + + k, err := loader.LoadSpecs(ctx, loader.LoadOptions{ + RawSpec: string(theSpec), + }) + if err != nil { + return err + } + + kinds.Add(k) + } + + // TODO: Choose format i.e yaml or json + // TODO: Merge the specs first, maybe? or print them all out as a multi-doc yaml? How about JSON? + yamlOut, err := yaml.Marshal(kinds) + if err != nil { + return err + } + + fmt.Println(string(yamlOut)) + + return nil +} diff --git a/pkg/loader/loader.go b/pkg/loader/loader.go index 3b9289e59..471643365 100644 --- a/pkg/loader/loader.go +++ b/pkg/loader/loader.go @@ -73,6 +73,17 @@ func (kinds *TroubleshootKinds) IsEmpty() bool { len(kinds.SupportBundlesV1Beta2) == 0 } +func (kinds *TroubleshootKinds) Add(other *TroubleshootKinds) { + kinds.AnalyzersV1Beta2 = append(kinds.AnalyzersV1Beta2, other.AnalyzersV1Beta2...) + kinds.CollectorsV1Beta2 = append(kinds.CollectorsV1Beta2, other.CollectorsV1Beta2...) + kinds.HostCollectorsV1Beta2 = append(kinds.HostCollectorsV1Beta2, other.HostCollectorsV1Beta2...) + kinds.HostPreflightsV1Beta2 = append(kinds.HostPreflightsV1Beta2, other.HostPreflightsV1Beta2...) + kinds.PreflightsV1Beta2 = append(kinds.PreflightsV1Beta2, other.PreflightsV1Beta2...) + kinds.RedactorsV1Beta2 = append(kinds.RedactorsV1Beta2, other.RedactorsV1Beta2...) + kinds.RemoteCollectorsV1Beta2 = append(kinds.RemoteCollectorsV1Beta2, other.RemoteCollectorsV1Beta2...) + kinds.SupportBundlesV1Beta2 = append(kinds.SupportBundlesV1Beta2, other.SupportBundlesV1Beta2...) +} + func NewTroubleshootKinds() *TroubleshootKinds { return &TroubleshootKinds{} }