diff --git a/cmd/troubleshoot/cli/run.go b/cmd/troubleshoot/cli/run.go index d569e05e..e1a0191c 100644 --- a/cmd/troubleshoot/cli/run.go +++ b/cmd/troubleshoot/cli/run.go @@ -1,20 +1,14 @@ package cli import ( - "archive/tar" - "bytes" - "compress/gzip" - "context" "crypto/tls" "encoding/json" "fmt" - "io" - "io/ioutil" "net/http" + "net/url" "os" "path/filepath" - "sort" "strings" "time" @@ -23,47 +17,50 @@ import ( "github.com/manifoldco/promptui" "github.com/mattn/go-isatty" "github.com/pkg/errors" - "github.com/replicatedhq/troubleshoot/cmd/util" - analyzer "github.com/replicatedhq/troubleshoot/pkg/analyze" troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" "github.com/replicatedhq/troubleshoot/pkg/client/troubleshootclientset/scheme" troubleshootclientsetscheme "github.com/replicatedhq/troubleshoot/pkg/client/troubleshootclientset/scheme" - "github.com/replicatedhq/troubleshoot/pkg/collect" "github.com/replicatedhq/troubleshoot/pkg/convert" "github.com/replicatedhq/troubleshoot/pkg/docrewrite" + "github.com/replicatedhq/troubleshoot/pkg/httputil" "github.com/replicatedhq/troubleshoot/pkg/k8sutil" - "github.com/replicatedhq/troubleshoot/pkg/redact" - "github.com/replicatedhq/troubleshoot/pkg/specs" + "github.com/replicatedhq/troubleshoot/pkg/supportbundle" "github.com/spf13/viper" spin "github.com/tj/go-spin" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -var ( - httpClient *http.Client ) func runTroubleshoot(v *viper.Viper, arg string) error { fmt.Print(cursor.Hide()) defer fmt.Print(cursor.Show()) - if v.GetBool("allow-insecure-connections") || v.GetBool("insecure-skip-tls-verify") { - httpClient = &http.Client{Transport: &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - }} - } else { - httpClient = http.DefaultClient + k8sConfig, err := k8sutil.GetRESTConfig() + if err != nil { + return errors.Wrap(err, "failed to convert kube flags to rest config") } - collectorContent, err := loadSupportBundleSpec(v, arg) + var sinceTime *time.Time + if v.GetString("since-time") != "" || v.GetString("since") != "" { + sinceTime, err = parseTimeFlags(v) + if err != nil { + return errors.Wrap(err, "failed parse since time") + } + } + + if v.GetBool("allow-insecure-connections") || v.GetBool("insecure-skip-tls-verify") { + httputil.AddTransport(&http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + }) + } + + collectorContent, err := supportbundle.LoadSupportBundleSpec(arg) if err != nil { return errors.Wrap(err, "failed to load collector spec") } multidocs := strings.Split(string(collectorContent), "\n---\n") - // we suppory both raw collector kinds and supportbundle kinds here - supportBundleSpec, err := parseSupportBundleFromDoc([]byte(multidocs[0])) + // we support both raw collector kinds and supportbundle kinds here + supportBundle, err := supportbundle.ParseSupportBundleFromDoc([]byte(multidocs[0])) if err != nil { return errors.Wrap(err, "failed to parse collector") } @@ -73,24 +70,13 @@ func runTroubleshoot(v *viper.Viper, arg string) error { additionalRedactors := &troubleshootv1beta2.Redactor{} for idx, redactor := range v.GetStringSlice("redactors") { - redactorContent, err := loadRedactorSpec(v, redactor) + redactorObj, err := supportbundle.GetRedactorFromURI(redactor) if err != nil { - return errors.Wrapf(err, "failed to load redactor spec #%d", idx) + return errors.Wrapf(err, "failed to get redactor spec %s, #%d", redactor, idx) } - redactorContent, err = docrewrite.ConvertToV1Beta2(redactorContent) - if err != nil { - return errors.Wrap(err, "failed to convert to v1beta2") - } - obj, _, err := decode([]byte(redactorContent), nil, nil) - if err != nil { - return errors.Wrapf(err, "failed to parse redactors %s", redactor) - } - loopRedactors, ok := obj.(*troubleshootv1beta2.Redactor) - if !ok { - return fmt.Errorf("%s is not a troubleshootv1beta2 redactor type", redactor) - } - if loopRedactors != nil { - additionalRedactors.Spec.Redactors = append(additionalRedactors.Spec.Redactors, loopRedactors.Spec.Redactors...) + + if redactorObj != nil { + additionalRedactors.Spec.Redactors = append(additionalRedactors.Spec.Redactors, redactorObj.Spec.Redactors...) } } @@ -115,7 +101,7 @@ func runTroubleshoot(v *viper.Viper, arg string) error { s := spin.New() finishedCh := make(chan bool, 1) - progressChan := make(chan interface{}, 0) // non-zero buffer can result in missed messages + progressChan := make(chan interface{}) // non-zero buffer can result in missed messages isFinishedChClosed := false go func() { currentDir := "" @@ -147,58 +133,40 @@ func runTroubleshoot(v *viper.Viper, arg string) error { } }() - archivePath, err := runCollectors(v, supportBundleSpec.Spec.Collectors, additionalRedactors, progressChan) + collectorCB := func(c chan interface{}, msg string) { + c <- fmt.Sprintf("%s", msg) + } + + createOpts := supportbundle.SupportBundleCreateOpts{ + CollectorProgressCallback: collectorCB, + CollectWithoutPermissions: v.GetBool("collect-without-permissions"), + KubernetesRestConfig: k8sConfig, + Namespace: v.GetString("namespace"), + ProgressChan: progressChan, + SinceTime: sinceTime, + } + + archivePath, err := supportbundle.CollectSupportBundleFromSpec(&supportBundle.Spec, additionalRedactors, createOpts) if err != nil { return errors.Wrap(err, "run collectors") } - fmt.Printf("\r%s\r", cursor.ClearEntireLine()) - - // upload if needed - fileUploaded := false - if len(supportBundleSpec.Spec.AfterCollection) > 0 { - for _, ac := range supportBundleSpec.Spec.AfterCollection { - if ac.UploadResultsTo != nil { - if err := uploadSupportBundle(ac.UploadResultsTo, archivePath); err != nil { - c := color.New(color.FgHiRed) - c.Printf("%s\r * Failed to upload support bundle: %v\n", cursor.ClearEntireLine(), err) - } else { - fileUploaded = true - } - } else if ac.Callback != nil { - if err := callbackSupportBundleAPI(ac.Callback, archivePath); err != nil { - c := color.New(color.FgHiRed) - c.Printf("%s\r * Failed to notify API that support bundle has been uploaded: %v\n", cursor.ClearEntireLine(), err) - } - } - } + c := color.New() + c.Println(fmt.Sprintf("\r%s\r", cursor.ClearEntireLine())) + fileUploaded, err := supportbundle.ProcessSupportBundleAfterCollection(&supportBundle.Spec, archivePath) + if err != nil { + c := color.New(color.FgHiRed) + c.Printf("%s\r * %v\n", cursor.ClearEntireLine(), err) + return errors.Wrap(err, "failed to process bundle after collection") } - // perform analysis, if possible - if len(supportBundleSpec.Spec.Analyzers) > 0 { - tmpDir, err := ioutil.TempDir("", "troubleshoot") - if err != nil { - c := color.New(color.FgHiRed) - c.Printf("%s\r * Failed to make directory for analysis: %v\n", cursor.ClearEntireLine(), err) - } - - f, err := os.Open(archivePath) - if err != nil { - c := color.New(color.FgHiRed) - c.Printf("%s\r * Failed to open support bundle for analysis: %v\n", cursor.ClearEntireLine(), err) - - } - if err := analyzer.ExtractTroubleshootBundle(f, tmpDir); err != nil { - c := color.New(color.FgHiRed) - c.Printf("%s\r * Failed to extract support bundle for analysis: %v\n", cursor.ClearEntireLine(), err) - } - - analyzeResults, err := analyzer.AnalyzeLocal(tmpDir, supportBundleSpec.Spec.Analyzers) - if err != nil { - c := color.New(color.FgHiRed) - c.Printf("%s\r * Failed to analyze support bundle: %v\n", cursor.ClearEntireLine(), err) - } + analyzeResults, err := supportbundle.AnalyzeAndExtractSupportBundle(&supportBundle.Spec, archivePath) + if err != nil { + c := color.New(color.FgHiRed) + c.Printf("%s\r * %v\n", cursor.ClearEntireLine(), err) + // Don't die + } else if len(analyzeResults) > 0 { interactive := v.GetBool("interactive") && isatty.IsTerminal(os.Stdout.Fd()) @@ -206,12 +174,10 @@ func runTroubleshoot(v *viper.Viper, arg string) error { close(finishedCh) // this removes the spinner isFinishedChClosed = true - if err := showInteractiveResults(supportBundleSpec.Name, analyzeResults); err != nil { + if err := showInteractiveResults(supportBundle.Name, analyzeResults); err != nil { interactive = false } - } - - if !interactive { + } else { data := convert.FromAnalyzerResult(analyzeResults) formatted, err := json.MarshalIndent(data, "", " ") if err != nil { @@ -225,7 +191,7 @@ func runTroubleshoot(v *viper.Viper, arg string) error { if !fileUploaded { msg := archivePath - if appName := supportBundleSpec.Labels["applicationName"]; appName != "" { + if appName := supportBundle.Labels["applicationName"]; appName != "" { f := `A support bundle for %s has been created in this directory named %s. Please upload it on the Troubleshoot page of the %s Admin Console to begin analysis.` @@ -247,144 +213,40 @@ the %s Admin Console to begin analysis.` return nil } -func loadSupportBundleSpec(v *viper.Viper, arg string) ([]byte, error) { - if strings.HasPrefix(arg, "secret/") { - // format secret/namespace-name/secret-name - pathParts := strings.Split(arg, "/") - if len(pathParts) != 3 { - return nil, errors.Errorf("secret path %s must have 3 components", arg) - } - - spec, err := specs.LoadFromSecret(pathParts[1], pathParts[2], "support-bundle-spec") - if err != nil { - return nil, errors.Wrap(err, "failed to get spec from secret") - } - - return spec, nil - } - - return loadSpec(v, arg) -} - -func loadRedactorSpec(v *viper.Viper, arg string) ([]byte, error) { - if strings.HasPrefix(arg, "configmap/") { - // format configmap/namespace-name/configmap-name[/data-key] - pathParts := strings.Split(arg, "/") - if len(pathParts) > 4 { - return nil, errors.Errorf("configmap path %s must have at most 4 components", arg) - } - if len(pathParts) < 3 { - return nil, errors.Errorf("configmap path %s must have at least 3 components", arg) - } - - dataKey := "redactor-spec" - if len(pathParts) == 4 { - dataKey = pathParts[3] - } - - spec, err := specs.LoadFromConfigMap(pathParts[1], pathParts[2], dataKey) - if err != nil { - return nil, errors.Wrap(err, "failed to get spec from configmap") - } - - return spec, nil - } - - spec, err := loadSpec(v, arg) +func getExpectedContentType(uploadURL string) string { + parsedURL, err := url.Parse(uploadURL) if err != nil { - return nil, errors.Wrap(err, "failed to load spec") + return "" } - - return spec, nil + return parsedURL.Query().Get("Content-Type") } -func loadSpec(v *viper.Viper, arg string) ([]byte, error) { - var err error - if _, err = os.Stat(arg); err == nil { - b, err := ioutil.ReadFile(arg) - if err != nil { - return nil, errors.Wrap(err, "read spec file") +func parseTimeFlags(v *viper.Viper) (*time.Time, error) { + var ( + sinceTime time.Time + err error + ) + if v.GetString("since-time") != "" { + if v.GetString("since") != "" { + return nil, errors.Errorf("at most one of `sinceTime` or `since` may be specified") } - - return b, nil - } else if !util.IsURL(arg) { - return nil, fmt.Errorf("%s is not a URL and was not found (err %s)", arg, err) + sinceTime, err = time.Parse(time.RFC3339, v.GetString("since-time")) + if err != nil { + return nil, errors.Wrap(err, "unable to parse --since-time flag") + } + } else { + parsedDuration, err := time.ParseDuration(v.GetString("since")) + if err != nil { + return nil, errors.Wrap(err, "unable to parse --since flag") + } + now := time.Now() + sinceTime = now.Add(0 - parsedDuration) } - spec, err := loadSpecFromURL(v, arg) - if err != nil { - return nil, errors.Wrap(err, "failed to get spec from URL") - } - return spec, nil + return &sinceTime, nil } -func loadSpecFromURL(v *viper.Viper, arg string) ([]byte, error) { - for { - req, err := http.NewRequest("GET", arg, nil) - if err != nil { - return nil, errors.Wrap(err, "make request") - } - req.Header.Set("User-Agent", "Replicated_Troubleshoot/v1beta1") - req.Header.Set("Bundle-Upload-Host", fmt.Sprintf("%s://%s", req.URL.Scheme, req.URL.Host)) - resp, err := httpClient.Do(req) - if err != nil { - if shouldRetryRequest(err) { - continue - } - return nil, errors.Wrap(err, "execute request") - } - defer resp.Body.Close() - - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, errors.Wrap(err, "read responce body") - } - - return body, nil - } -} - -func parseSupportBundleFromDoc(doc []byte) (*troubleshootv1beta2.SupportBundle, error) { - doc, err := docrewrite.ConvertToV1Beta2(doc) - if err != nil { - return nil, errors.Wrap(err, "failed to convert to v1beta2") - } - - troubleshootclientsetscheme.AddToScheme(scheme.Scheme) - decode := scheme.Codecs.UniversalDeserializer().Decode - - obj, _, err := decode(doc, nil, nil) - if err != nil { - return nil, errors.Wrap(err, "failed to parse document") - } - - collector, ok := obj.(*troubleshootv1beta2.Collector) - if ok { - supportBundle := troubleshootv1beta2.SupportBundle{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "troubleshoot.sh/v1beta2", - Kind: "SupportBundle", - }, - ObjectMeta: collector.ObjectMeta, - Spec: troubleshootv1beta2.SupportBundleSpec{ - Collectors: collector.Spec.Collectors, - Analyzers: []*troubleshootv1beta2.Analyze{}, - AfterCollection: collector.Spec.AfterCollection, - }, - } - - return &supportBundle, nil - } - - supportBundle, ok := obj.(*troubleshootv1beta2.SupportBundle) - if ok { - return supportBundle, nil - } - - return nil, errors.New("spec was not parseable as a troubleshoot kind") -} - -func shouldRetryRequest(err error) bool { +func shouldRetryRequest(err error, httpClient *http.Client) bool { if strings.Contains(err.Error(), "x509") && httpClient == http.DefaultClient && canTryInsecure() { httpClient = &http.Client{Transport: &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, @@ -404,408 +266,5 @@ func canTryInsecure() bool { } _, err := prompt.Run() - if err != nil { - return false - } - - return true -} - -func runCollectors(v *viper.Viper, collectors []*troubleshootv1beta2.Collect, additionalRedactors *troubleshootv1beta2.Redactor, progressChan chan interface{}) (string, error) { - tmpDir, err := ioutil.TempDir("", "troubleshoot") - if err != nil { - return "", errors.Wrap(err, "create temp dir") - } - defer os.RemoveAll(tmpDir) - - filename, err := findFileName("support-bundle-"+time.Now().Format("2006-01-02T15_04_05"), "tar.gz") - if err != nil { - return "", errors.Wrap(err, "find file name") - } - - bundlePath := filepath.Join(tmpDir, strings.TrimSuffix(filename, ".tar.gz")) - if err := os.MkdirAll(bundlePath, 0777); err != nil { - return "", errors.Wrap(err, "create bundle dir") - } - - if err = writeVersionFile(bundlePath); err != nil { - return "", errors.Wrap(err, "write version file") - } - - collectSpecs := make([]*troubleshootv1beta2.Collect, 0, 0) - collectSpecs = append(collectSpecs, collectors...) - collectSpecs = ensureCollectorInList(collectSpecs, troubleshootv1beta2.Collect{ClusterInfo: &troubleshootv1beta2.ClusterInfo{}}) - collectSpecs = ensureCollectorInList(collectSpecs, troubleshootv1beta2.Collect{ClusterResources: &troubleshootv1beta2.ClusterResources{}}) - - config, err := k8sutil.GetRESTConfig() - if err != nil { - return "", errors.Wrap(err, "failed to convert kube flags to rest config") - } - - var cleanedCollectors collect.Collectors - for _, desiredCollector := range collectSpecs { - collector := collect.Collector{ - Redact: true, - Collect: desiredCollector, - ClientConfig: config, - Namespace: v.GetString("namespace"), - } - cleanedCollectors = append(cleanedCollectors, &collector) - } - - if err := cleanedCollectors.CheckRBAC(context.Background()); err != nil { - return "", errors.Wrap(err, "failed to check RBAC for collectors") - } - - foundForbidden := false - for _, c := range cleanedCollectors { - for _, e := range c.RBACErrors { - foundForbidden = true - progressChan <- e - } - } - - if foundForbidden && !v.GetBool("collect-without-permissions") { - return "", errors.New("insufficient permissions to run all collectors") - } - - globalRedactors := []*troubleshootv1beta2.Redact{} - if additionalRedactors != nil { - globalRedactors = additionalRedactors.Spec.Redactors - } - if v.GetString("since-time") != "" || v.GetString("since") != "" { - err := parseTimeFlags(v, progressChan, &cleanedCollectors) - if err != nil { - return "", err - } - } - - // Run preflights collectors synchronously - for _, collector := range cleanedCollectors { - if len(collector.RBACErrors) > 0 { - // don't skip clusterResources collector due to RBAC issues - if collector.Collect.ClusterResources == nil { - progressChan <- fmt.Sprintf("skipping collector %s with insufficient RBAC permissions", collector.GetDisplayName()) - continue - } - } - - progressChan <- collector.GetDisplayName() - - result, err := collector.RunCollectorSync(globalRedactors) - if err != nil { - progressChan <- fmt.Errorf("failed to run collector %q: %v", collector.GetDisplayName(), err) - continue - } - - if result != nil { - err = saveCollectorOutput(result, bundlePath, collector) - if err != nil { - progressChan <- fmt.Errorf("failed to parse collector spec %q: %v", collector.GetDisplayName(), err) - continue - } - } - } - - if err := tarSupportBundleDir(bundlePath, filename); err != nil { - return "", errors.Wrap(err, "create bundle file") - } - - return filename, nil -} - -func saveCollectorOutput(output map[string][]byte, bundlePath string, c *collect.Collector) error { - for filename, maybeContents := range output { - if c.Collect.Copy != nil { - err := untarAndSave(maybeContents, filepath.Join(bundlePath, filepath.Dir(filename))) - if err != nil { - return errors.Wrap(err, "extract copied files") - } - continue - } - fileDir, fileName := filepath.Split(filename) - outPath := filepath.Join(bundlePath, fileDir) - - if err := os.MkdirAll(outPath, 0777); err != nil { - return errors.Wrap(err, "create output file") - } - - if err := writeFile(filepath.Join(outPath, fileName), maybeContents); err != nil { - return errors.Wrap(err, "write collector output") - } - } - - return nil -} - -func untarAndSave(tarFile []byte, bundlePath string) error { - keys := make([]string, 0) - dirs := make(map[string]*tar.Header) - files := make(map[string][]byte) - fileHeaders := make(map[string]*tar.Header) - tarReader := tar.NewReader(bytes.NewBuffer(tarFile)) - //Extract and separate tar contentes in file and folders, keeping header info from each one. - for { - header, err := tarReader.Next() - if err != nil { - if err != io.EOF { - return err - } - break - } - switch header.Typeflag { - case tar.TypeDir: - dirs[header.Name] = header - case tar.TypeReg: - file := new(bytes.Buffer) - _, err = io.Copy(file, tarReader) - if err != nil { - return err - } - files[header.Name] = file.Bytes() - fileHeaders[header.Name] = header - default: - return fmt.Errorf("Tar file entry %s contained unsupported file type %v", header.Name, header.FileInfo().Mode()) - } - } - //Create directories from base path: //containerPath - if err := os.MkdirAll(filepath.Join(bundlePath), 0777); err != nil { - return errors.Wrap(err, "create output file") - } - //Order folders stored in variable keys to start always by parent folder. That way folder info is preserved. - for k := range dirs { - keys = append(keys, k) - } - sort.Strings(keys) - //Orderly create folders. - for _, k := range keys { - if err := os.Mkdir(filepath.Join(bundlePath, k), dirs[k].FileInfo().Mode().Perm()); err != nil { - return errors.Wrap(err, "create output file") - } - } - //Populate folders with respective files and its permissions stored in the header. - for k, v := range files { - if err := ioutil.WriteFile(filepath.Join(bundlePath, k), v, fileHeaders[k].FileInfo().Mode().Perm()); err != nil { - return err - } - } - return nil -} -func uploadSupportBundle(r *troubleshootv1beta2.ResultRequest, archivePath string) error { - contentType := getExpectedContentType(r.URI) - if contentType != "" && contentType != "application/tar+gzip" { - return fmt.Errorf("cannot upload content type %s", contentType) - } - - for { - f, err := os.Open(archivePath) - if err != nil { - return errors.Wrap(err, "open file") - } - defer f.Close() - - fileStat, err := f.Stat() - if err != nil { - return errors.Wrap(err, "stat file") - } - - req, err := http.NewRequest(r.Method, r.URI, f) - if err != nil { - return errors.Wrap(err, "create request") - } - req.ContentLength = fileStat.Size() - if contentType != "" { - req.Header.Set("Content-Type", contentType) - } - - resp, err := httpClient.Do(req) - if err != nil { - if shouldRetryRequest(err) { - continue - } - return errors.Wrap(err, "execute request") - } - - if resp.StatusCode >= 300 { - return fmt.Errorf("unexpected status code %d", resp.StatusCode) - } - - break - } - - // send redaction report - if r.RedactURI != "" { - type PutSupportBundleRedactions struct { - Redactions redact.RedactionList `json:"redactions"` - } - - redactBytes, err := json.Marshal(PutSupportBundleRedactions{Redactions: redact.GetRedactionList()}) - if err != nil { - return errors.Wrap(err, "get redaction report") - } - - for { - req, err := http.NewRequest("PUT", r.RedactURI, bytes.NewReader(redactBytes)) - if err != nil { - return errors.Wrap(err, "create redaction report request") - } - req.ContentLength = int64(len(redactBytes)) - - resp, err := httpClient.Do(req) - if err != nil { - if shouldRetryRequest(err) { - continue - } - return errors.Wrap(err, "execute redaction request") - } - - if resp.StatusCode >= 300 { - return fmt.Errorf("unexpected redaction status code %d", resp.StatusCode) - } - - break - } - } - - return nil -} - -func getExpectedContentType(uploadURL string) string { - parsedURL, err := url.Parse(uploadURL) - if err != nil { - return "" - } - return parsedURL.Query().Get("Content-Type") -} - -func callbackSupportBundleAPI(r *troubleshootv1beta2.ResultRequest, archivePath string) error { - for { - req, err := http.NewRequest(r.Method, r.URI, nil) - if err != nil { - return errors.Wrap(err, "create request") - } - - resp, err := httpClient.Do(req) - if err != nil { - if shouldRetryRequest(err) { - continue - } - return errors.Wrap(err, "execute request") - } - - if resp.StatusCode >= 300 { - return fmt.Errorf("unexpected status code %d", resp.StatusCode) - } - - break - } - return nil -} - -func tarSupportBundleDir(inputDir, outputFilename string) error { - fileWriter, err := os.Create(outputFilename) - if err != nil { - return errors.Wrap(err, "failed to create output file") - } - defer fileWriter.Close() - - gzipWriter := gzip.NewWriter(fileWriter) - defer gzipWriter.Close() - - tarWriter := tar.NewWriter(gzipWriter) - defer tarWriter.Close() - - err = filepath.Walk(inputDir, func(filename string, info os.FileInfo, err error) error { - if err != nil { - return err - } - - fileMode := info.Mode() - if !fileMode.IsRegular() { // support bundle can have only files - return nil - } - - parentDirName := filepath.Dir(inputDir) // this is to have the files inside a subdirectory - nameInArchive, err := filepath.Rel(parentDirName, filename) - if err != nil { - return errors.Wrap(err, "failed to create relative file name") - } - - // tar.FileInfoHeader call causes a crash in static builds - // https://github.com/golang/go/issues/24787 - hdr := &tar.Header{ - Name: nameInArchive, - ModTime: info.ModTime(), - Mode: int64(fileMode.Perm()), - Typeflag: tar.TypeReg, - Size: info.Size(), - } - - err = tarWriter.WriteHeader(hdr) - if err != nil { - return errors.Wrap(err, "failed to write tar header") - } - - err = func() error { - fileReader, err := os.Open(filename) - if err != nil { - return errors.Wrap(err, "failed to open source file") - } - defer fileReader.Close() - - _, err = io.Copy(tarWriter, fileReader) - if err != nil { - return errors.Wrap(err, "failed to copy file into archive") - } - - return nil - }() - if err != nil { - return err - } - - return nil - }) - if err != nil { - return errors.Wrap(err, "failed to walk source dir") - } - - return nil -} - -type CollectorFailure struct { - Collector *troubleshootv1beta2.Collect - Failure string -} - -func parseTimeFlags(v *viper.Viper, progressChan chan interface{}, collectors *collect.Collectors) error { - var ( - sinceTime time.Time - err error - ) - if v.GetString("since-time") != "" { - if v.GetString("since") != "" { - return errors.Errorf("at most one of `sinceTime` or `since` may be specified") - } - sinceTime, err = time.Parse(time.RFC3339, v.GetString("since-time")) - if err != nil { - return errors.Wrap(err, "unable to parse --since-time flag") - } - } else { - parsedDuration, err := time.ParseDuration(v.GetString("since")) - if err != nil { - return errors.Wrap(err, "unable to parse --since flag") - } - now := time.Now() - sinceTime = now.Add(0 - parsedDuration) - } - for _, collector := range *collectors { - if collector.Collect.Logs != nil { - if collector.Collect.Logs.Limits == nil { - collector.Collect.Logs.Limits = new(troubleshootv1beta2.LogLimits) - } - collector.Collect.Logs.Limits.SinceTime = metav1.NewTime(sinceTime) - } - } - return nil + return err == nil } diff --git a/pkg/httputil/config.go b/pkg/httputil/config.go new file mode 100644 index 00000000..95fbdfd9 --- /dev/null +++ b/pkg/httputil/config.go @@ -0,0 +1,24 @@ +package httputil + +import ( + "net/http" +) + +var ( + httpTransport *http.Transport + httpClient = &http.Client{} +) + +func AddTransport(transport *http.Transport) { + httpTransport = transport +} + +func GetHttpClient() *http.Client { + + if httpTransport != nil { + httpClient.Transport = httpTransport + return httpClient + } + + return http.DefaultClient +} diff --git a/pkg/supportbundle/aftercollection.go b/pkg/supportbundle/aftercollection.go new file mode 100644 index 00000000..5d8df393 --- /dev/null +++ b/pkg/supportbundle/aftercollection.go @@ -0,0 +1,157 @@ +package supportbundle + +import ( + "bytes" + "crypto/tls" + "encoding/json" + "fmt" + "net/http" + "net/url" + "os" + "strings" + + "github.com/manifoldco/promptui" + "github.com/mattn/go-isatty" + "github.com/pkg/errors" + troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" + "github.com/replicatedhq/troubleshoot/pkg/httputil" + "github.com/replicatedhq/troubleshoot/pkg/redact" +) + +func uploadSupportBundle(r *troubleshootv1beta2.ResultRequest, archivePath string) error { + contentType := getExpectedContentType(r.URI) + if contentType != "" && contentType != "application/tar+gzip" { + return fmt.Errorf("cannot upload content type %s", contentType) + } + + for { + f, err := os.Open(archivePath) + if err != nil { + return errors.Wrap(err, "open file") + } + defer f.Close() + + fileStat, err := f.Stat() + if err != nil { + return errors.Wrap(err, "stat file") + } + + req, err := http.NewRequest(r.Method, r.URI, f) + if err != nil { + return errors.Wrap(err, "create request") + } + req.ContentLength = fileStat.Size() + if contentType != "" { + req.Header.Set("Content-Type", contentType) + } + + httpClient := httputil.GetHttpClient() + resp, err := httpClient.Do(req) + if err != nil { + if shouldRetryRequest(err, httpClient) { + continue + } + return errors.Wrap(err, "execute request") + } + + if resp.StatusCode >= 300 { + return fmt.Errorf("unexpected status code %d", resp.StatusCode) + } + + break + } + + // send redaction report + if r.RedactURI != "" { + type PutSupportBundleRedactions struct { + Redactions redact.RedactionList `json:"redactions"` + } + + redactBytes, err := json.Marshal(PutSupportBundleRedactions{Redactions: redact.GetRedactionList()}) + if err != nil { + return errors.Wrap(err, "get redaction report") + } + + for { + req, err := http.NewRequest("PUT", r.RedactURI, bytes.NewReader(redactBytes)) + if err != nil { + return errors.Wrap(err, "create redaction report request") + } + req.ContentLength = int64(len(redactBytes)) + + httpClient := httputil.GetHttpClient() + resp, err := httpClient.Do(req) + if err != nil { + if shouldRetryRequest(err, httpClient) { + continue + } + return errors.Wrap(err, "execute redaction request") + } + + if resp.StatusCode >= 300 { + return fmt.Errorf("unexpected redaction status code %d", resp.StatusCode) + } + + break + } + } + + return nil +} + +func callbackSupportBundleAPI(r *troubleshootv1beta2.ResultRequest, archivePath string) error { + for { + req, err := http.NewRequest(r.Method, r.URI, nil) + if err != nil { + return errors.Wrap(err, "create request") + } + + httpClient := httputil.GetHttpClient() + resp, err := httpClient.Do(req) + if err != nil { + if shouldRetryRequest(err, httpClient) { + continue + } + return errors.Wrap(err, "execute request") + } + defer resp.Body.Close() + + if resp.StatusCode >= 300 { + return fmt.Errorf("unexpected status code %d", resp.StatusCode) + } + + break + } + return nil +} + +func getExpectedContentType(uploadURL string) string { + parsedURL, err := url.Parse(uploadURL) + if err != nil { + return "" + } + return parsedURL.Query().Get("Content-Type") +} + +func shouldRetryRequest(err error, httpClient *http.Client) bool { + if strings.Contains(err.Error(), "x509") && httpClient == http.DefaultClient && canTryInsecure() { + httpClient = &http.Client{Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + }} + return true + } + return false +} + +func canTryInsecure() bool { + if !isatty.IsTerminal(os.Stdout.Fd()) { + return false + } + prompt := promptui.Prompt{ + Label: "Connection appears to be insecure. Would you like to attempt to create a support bundle anyway?", + IsConfirm: true, + } + + _, err := prompt.Run() + return err == nil +} diff --git a/pkg/supportbundle/collect.go b/pkg/supportbundle/collect.go new file mode 100644 index 00000000..c497075f --- /dev/null +++ b/pkg/supportbundle/collect.go @@ -0,0 +1,320 @@ +package supportbundle + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "context" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "sort" + "time" + + "github.com/pkg/errors" + troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" + "github.com/replicatedhq/troubleshoot/pkg/collect" + "github.com/replicatedhq/troubleshoot/pkg/version" + "gopkg.in/yaml.v2" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// TODO (dan): This is VERY similar to the Preflight collect package and should be refactored. +func runCollectors(collectors []*troubleshootv1beta2.Collect, additionalRedactors *troubleshootv1beta2.Redactor, filename string, bundlePath string, opts SupportBundleCreateOpts) error { + + collectSpecs := make([]*troubleshootv1beta2.Collect, 0) + collectSpecs = append(collectSpecs, collectors...) + collectSpecs = ensureCollectorInList(collectSpecs, troubleshootv1beta2.Collect{ClusterInfo: &troubleshootv1beta2.ClusterInfo{}}) + collectSpecs = ensureCollectorInList(collectSpecs, troubleshootv1beta2.Collect{ClusterResources: &troubleshootv1beta2.ClusterResources{}}) + + var cleanedCollectors collect.Collectors + for _, desiredCollector := range collectSpecs { + collector := collect.Collector{ + Redact: true, + Collect: desiredCollector, + ClientConfig: opts.KubernetesRestConfig, + Namespace: opts.Namespace, + } + cleanedCollectors = append(cleanedCollectors, &collector) + } + + if err := cleanedCollectors.CheckRBAC(context.Background()); err != nil { + return errors.Wrap(err, "failed to check RBAC for collectors") + } + + foundForbidden := false + for _, c := range cleanedCollectors { + for _, e := range c.RBACErrors { + foundForbidden = true + opts.ProgressChan <- e + } + } + + if foundForbidden && !opts.CollectWithoutPermissions { + return errors.New("insufficient permissions to run all collectors") + } + + globalRedactors := []*troubleshootv1beta2.Redact{} + if additionalRedactors != nil { + globalRedactors = additionalRedactors.Spec.Redactors + } + + if opts.SinceTime != nil { + applyLogSinceTime(*opts.SinceTime, &cleanedCollectors) + } + + // Run preflights collectors synchronously + for _, collector := range cleanedCollectors { + if len(collector.RBACErrors) > 0 { + // don't skip clusterResources collector due to RBAC issues + if collector.Collect.ClusterResources == nil { + msg := fmt.Sprintf("skipping collector %s with insufficient RBAC permissions", collector.GetDisplayName()) + opts.CollectorProgressCallback(opts.ProgressChan, msg) + continue + } + } + + opts.CollectorProgressCallback(opts.ProgressChan, collector.GetDisplayName()) + + result, err := collector.RunCollectorSync(globalRedactors) + if err != nil { + opts.ProgressChan <- fmt.Errorf("failed to run collector %q: %v", collector.GetDisplayName(), err) + continue + } + + if result != nil { + // results already contain the bundle dir name in their paths + err = saveCollectorOutput(result, bundlePath, collector) + if err != nil { + opts.ProgressChan <- fmt.Errorf("failed to parse collector spec %q: %v", collector.GetDisplayName(), err) + continue + } + } + } + + return nil +} + +func findFileName(basename, extension string) (string, error) { + n := 1 + name := basename + for { + filename := name + "." + extension + if _, err := os.Stat(filename); os.IsNotExist(err) { + return filename, nil + } else if err != nil { + return "", errors.Wrap(err, "check file exists") + } + + name = fmt.Sprintf("%s (%d)", basename, n) + n = n + 1 + } +} + +func ensureCollectorInList(list []*troubleshootv1beta2.Collect, collector troubleshootv1beta2.Collect) []*troubleshootv1beta2.Collect { + for _, inList := range list { + if collector.ClusterResources != nil && inList.ClusterResources != nil { + return list + } + if collector.ClusterInfo != nil && inList.ClusterInfo != nil { + return list + } + } + + return append(list, &collector) +} + +const VersionFilename = "version.yaml" + +func writeVersionFile(path string) error { + version := troubleshootv1beta2.SupportBundleVersion{ + ApiVersion: "troubleshoot.sh/v1beta2", + Kind: "SupportBundle", + Spec: troubleshootv1beta2.SupportBundleVersionSpec{ + VersionNumber: version.Version(), + }, + } + b, err := yaml.Marshal(version) + if err != nil { + return err + } + + filename := filepath.Join(path, VersionFilename) + err = ioutil.WriteFile(filename, b, 0644) + if err != nil { + return err + } + + return nil +} + +func applyLogSinceTime(sinceTime time.Time, collectors *collect.Collectors) { + + for _, collector := range *collectors { + if collector.Collect.Logs != nil { + if collector.Collect.Logs.Limits == nil { + collector.Collect.Logs.Limits = new(troubleshootv1beta2.LogLimits) + } + collector.Collect.Logs.Limits.SinceTime = metav1.NewTime(sinceTime) + } + } +} + +func saveCollectorOutput(output map[string][]byte, bundlePath string, c *collect.Collector) error { + for filename, maybeContents := range output { + if c.Collect.Copy != nil { + err := untarAndSave(maybeContents, filepath.Join(bundlePath, filepath.Dir(filename))) + if err != nil { + return errors.Wrap(err, "extract copied files") + } + continue + } + fileDir, fileName := filepath.Split(filename) + outPath := filepath.Join(bundlePath, fileDir) + + if err := os.MkdirAll(outPath, 0777); err != nil { + return errors.Wrap(err, "create output file") + } + + if err := writeFile(filepath.Join(outPath, fileName), maybeContents); err != nil { + return errors.Wrap(err, "write collector output") + } + } + + return nil +} + +func untarAndSave(tarFile []byte, bundlePath string) error { + keys := make([]string, 0) + dirs := make(map[string]*tar.Header) + files := make(map[string][]byte) + fileHeaders := make(map[string]*tar.Header) + tarReader := tar.NewReader(bytes.NewBuffer(tarFile)) + //Extract and separate tar contents in file and folders, keeping header info from each one. + for { + header, err := tarReader.Next() + if err != nil { + if err != io.EOF { + return err + } + break + } + switch header.Typeflag { + case tar.TypeDir: + dirs[header.Name] = header + case tar.TypeReg: + file := new(bytes.Buffer) + _, err = io.Copy(file, tarReader) + if err != nil { + return err + } + files[header.Name] = file.Bytes() + fileHeaders[header.Name] = header + default: + return fmt.Errorf("tar file entry %s contained unsupported file type %v", header.Name, header.FileInfo().Mode()) + } + } + //Create directories from base path: //containerPath + if err := os.MkdirAll(filepath.Join(bundlePath), 0777); err != nil { + return errors.Wrap(err, "create output file") + } + //Order folders stored in variable keys to start always by parent folder. That way folder info is preserved. + for k := range dirs { + keys = append(keys, k) + } + sort.Strings(keys) + //Orderly create folders. + for _, k := range keys { + if err := os.Mkdir(filepath.Join(bundlePath, k), dirs[k].FileInfo().Mode().Perm()); err != nil { + return errors.Wrap(err, "create output file") + } + } + //Populate folders with respective files and its permissions stored in the header. + for k, v := range files { + if err := ioutil.WriteFile(filepath.Join(bundlePath, k), v, fileHeaders[k].FileInfo().Mode().Perm()); err != nil { + return err + } + } + return nil +} + +func writeFile(filename string, contents []byte) error { + if err := ioutil.WriteFile(filename, contents, 0644); err != nil { + return err + } + + return nil +} + +func tarSupportBundleDir(inputDir, outputFilename string) error { + fileWriter, err := os.Create(outputFilename) + if err != nil { + return errors.Wrap(err, "failed to create output file") + } + defer fileWriter.Close() + + gzipWriter := gzip.NewWriter(fileWriter) + defer gzipWriter.Close() + + tarWriter := tar.NewWriter(gzipWriter) + defer tarWriter.Close() + + err = filepath.Walk(inputDir, func(filename string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + fileMode := info.Mode() + if !fileMode.IsRegular() { // support bundle can have only files + return nil + } + + parentDirName := filepath.Dir(inputDir) // this is to have the files inside a subdirectory + nameInArchive, err := filepath.Rel(parentDirName, filename) + if err != nil { + return errors.Wrap(err, "failed to create relative file name") + } + + // tar.FileInfoHeader call causes a crash in static builds + // https://github.com/golang/go/issues/24787 + hdr := &tar.Header{ + Name: nameInArchive, + ModTime: info.ModTime(), + Mode: int64(fileMode.Perm()), + Typeflag: tar.TypeReg, + Size: info.Size(), + } + + err = tarWriter.WriteHeader(hdr) + if err != nil { + return errors.Wrap(err, "failed to write tar header") + } + + err = func() error { + fileReader, err := os.Open(filename) + if err != nil { + return errors.Wrap(err, "failed to open source file") + } + defer fileReader.Close() + + _, err = io.Copy(tarWriter, fileReader) + if err != nil { + return errors.Wrap(err, "failed to copy file into archive") + } + + return nil + }() + if err != nil { + return err + } + + return nil + }) + if err != nil { + return errors.Wrap(err, "failed to walk source dir") + } + + return nil +} diff --git a/pkg/supportbundle/load.go b/pkg/supportbundle/load.go new file mode 100644 index 00000000..4dde64d6 --- /dev/null +++ b/pkg/supportbundle/load.go @@ -0,0 +1,199 @@ +package supportbundle + +import ( + "fmt" + "io/ioutil" + "net/http" + "os" + "strings" + + "github.com/pkg/errors" + "github.com/replicatedhq/troubleshoot/cmd/util" + troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" + "github.com/replicatedhq/troubleshoot/pkg/client/troubleshootclientset/scheme" + troubleshootclientsetscheme "github.com/replicatedhq/troubleshoot/pkg/client/troubleshootclientset/scheme" + "github.com/replicatedhq/troubleshoot/pkg/docrewrite" + "github.com/replicatedhq/troubleshoot/pkg/httputil" + "github.com/replicatedhq/troubleshoot/pkg/specs" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func GetSupportBundleFromURI(bundleURI string) (*troubleshootv1beta2.SupportBundle, error) { + collectorContent, err := LoadSupportBundleSpec(bundleURI) + if err != nil { + return nil, errors.Wrap(err, "failed to load collector spec") + } + + multidocs := strings.Split(string(collectorContent), "\n---\n") + + supportbundle, err := ParseSupportBundleFromDoc([]byte(multidocs[0])) + if err != nil { + return nil, errors.Wrap(err, "failed to parse collector") + } + + return supportbundle, nil +} + +func ParseSupportBundleFromDoc(doc []byte) (*troubleshootv1beta2.SupportBundle, error) { + doc, err := docrewrite.ConvertToV1Beta2(doc) + if err != nil { + return nil, errors.Wrap(err, "failed to convert to v1beta2") + } + + troubleshootclientsetscheme.AddToScheme(scheme.Scheme) + decode := scheme.Codecs.UniversalDeserializer().Decode + + obj, _, err := decode(doc, nil, nil) + if err != nil { + return nil, errors.Wrap(err, "failed to parse document") + } + + collector, ok := obj.(*troubleshootv1beta2.Collector) + if ok { + supportBundle := troubleshootv1beta2.SupportBundle{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "troubleshoot.sh/v1beta2", + Kind: "SupportBundle", + }, + ObjectMeta: collector.ObjectMeta, + Spec: troubleshootv1beta2.SupportBundleSpec{ + Collectors: collector.Spec.Collectors, + Analyzers: []*troubleshootv1beta2.Analyze{}, + AfterCollection: collector.Spec.AfterCollection, + }, + } + + return &supportBundle, nil + } + + supportBundle, ok := obj.(*troubleshootv1beta2.SupportBundle) + if ok { + return supportBundle, nil + } + + return nil, errors.New("spec was not parseable as a troubleshoot kind") +} + +func GetRedactorFromURI(redactorURI string) (*troubleshootv1beta2.Redactor, error) { + decode := scheme.Codecs.UniversalDeserializer().Decode + + redactorContent, err := LoadRedactorSpec(redactorURI) + if err != nil { + return nil, errors.Wrapf(err, "failed to load redactor spec %s", redactorURI) + } + + redactorContent, err = docrewrite.ConvertToV1Beta2(redactorContent) + if err != nil { + return nil, errors.Wrap(err, "failed to convert to v1beta2") + } + + obj, _, err := decode([]byte(redactorContent), nil, nil) + if err != nil { + return nil, errors.Wrapf(err, "failed to parse redactors %s", redactorURI) + } + + redactor, ok := obj.(*troubleshootv1beta2.Redactor) + if !ok { + return nil, fmt.Errorf("%s is not a troubleshootv1beta2 redactor type", redactorURI) + } + + return redactor, nil +} + +func LoadSupportBundleSpec(arg string) ([]byte, error) { + if strings.HasPrefix(arg, "secret/") { + // format secret/namespace-name/secret-name + pathParts := strings.Split(arg, "/") + if len(pathParts) != 3 { + return nil, errors.Errorf("secret path %s must have 3 components", arg) + } + + spec, err := specs.LoadFromSecret(pathParts[1], pathParts[2], "support-bundle-spec") + if err != nil { + return nil, errors.Wrap(err, "failed to get spec from secret") + } + + return spec, nil + } + + return loadSpec(arg) +} + +func LoadRedactorSpec(arg string) ([]byte, error) { + if strings.HasPrefix(arg, "configmap/") { + // format configmap/namespace-name/configmap-name[/data-key] + pathParts := strings.Split(arg, "/") + if len(pathParts) > 4 { + return nil, errors.Errorf("configmap path %s must have at most 4 components", arg) + } + if len(pathParts) < 3 { + return nil, errors.Errorf("configmap path %s must have at least 3 components", arg) + } + + dataKey := "redactor-spec" + if len(pathParts) == 4 { + dataKey = pathParts[3] + } + + spec, err := specs.LoadFromConfigMap(pathParts[1], pathParts[2], dataKey) + if err != nil { + return nil, errors.Wrap(err, "failed to get spec from configmap") + } + + return spec, nil + } + + spec, err := loadSpec(arg) + if err != nil { + return nil, errors.Wrap(err, "failed to load spec") + } + + return spec, nil +} + +func loadSpec(arg string) ([]byte, error) { + var err error + if _, err = os.Stat(arg); err == nil { + b, err := ioutil.ReadFile(arg) + if err != nil { + return nil, errors.Wrap(err, "read spec file") + } + + return b, nil + } else if !util.IsURL(arg) { + return nil, fmt.Errorf("%s is not a URL and was not found (err %s)", arg, err) + } + + spec, err := loadSpecFromURL(arg) + if err != nil { + return nil, errors.Wrap(err, "failed to get spec from URL") + } + return spec, nil +} + +func loadSpecFromURL(arg string) ([]byte, error) { + for { + req, err := http.NewRequest("GET", arg, nil) + if err != nil { + return nil, errors.Wrap(err, "make request") + } + req.Header.Set("User-Agent", "Replicated_Troubleshoot/v1beta1") + req.Header.Set("Bundle-Upload-Host", fmt.Sprintf("%s://%s", req.URL.Scheme, req.URL.Host)) + httpClient := httputil.GetHttpClient() + resp, err := httpClient.Do(req) + if err != nil { + if shouldRetryRequest(err, httpClient) { + continue + } + return nil, errors.Wrap(err, "execute request") + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, errors.Wrap(err, "read responce body") + } + + return body, nil + } +} diff --git a/pkg/supportbundle/supportbundle.go b/pkg/supportbundle/supportbundle.go new file mode 100644 index 00000000..87d3e10d --- /dev/null +++ b/pkg/supportbundle/supportbundle.go @@ -0,0 +1,231 @@ +package supportbundle + +import ( + "io/ioutil" + "net/http" + "os" + "path/filepath" + "strings" + "time" + + "github.com/pkg/errors" + analyzer "github.com/replicatedhq/troubleshoot/pkg/analyze" + troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" + "k8s.io/client-go/rest" +) + +type SupportBundleCreateOpts struct { + CollectorProgressCallback func(chan interface{}, string) + CollectWithoutPermissions bool + HttpClient *http.Client + KubernetesRestConfig *rest.Config + Namespace string + ProgressChan chan interface{} + SinceTime *time.Time +} + +type SupportBundleResponse struct { + AnalyzerResults []*analyzer.AnalyzeResult + ArchivePath string + fileUploaded bool +} + +// SupportBundleCollectAnalyzeProcess collects support bundle from start to finish, including running +// collectors, analyzers and after collection steps. Input arguments are specifications. +func SupportBundleCollectAnalyzeProcess(spec *troubleshootv1beta2.SupportBundleSpec, additionalRedactors *troubleshootv1beta2.Redactor, opts SupportBundleCreateOpts) (*SupportBundleResponse, error) { + + resultsResponse := SupportBundleResponse{} + + if opts.KubernetesRestConfig == nil { + return nil, errors.New("did not receive kube rest config") + } + + if opts.ProgressChan == nil { + return nil, errors.New("did not receive collector progress chan") + } + + tmpDir, err := ioutil.TempDir("", "supportbundle") + if err != nil { + return nil, errors.Wrap(err, "create temp dir") + } + defer os.RemoveAll(tmpDir) + + // Do we need to put this in some kind of swap space? + filename, err := findFileName("support-bundle-"+time.Now().Format("2006-01-02T15_04_05"), "tar.gz") + if err != nil { + return nil, errors.Wrap(err, "find file name") + } + resultsResponse.ArchivePath = filename + + bundlePath := filepath.Join(tmpDir, strings.TrimSuffix(filename, ".tar.gz")) + if err := os.MkdirAll(bundlePath, 0777); err != nil { + return nil, errors.Wrap(err, "create bundle dir") + } + + if err = writeVersionFile(bundlePath); err != nil { + return nil, errors.Wrap(err, "write version file") + } + + // Run collectors + err = runCollectors(spec.Collectors, additionalRedactors, filename, bundlePath, opts) + if err != nil { + return nil, errors.Wrap(err, "failed to run collectors") + } + + // Run Analyzers + analyzeResults, err := AnalyzeSupportBundle(spec, tmpDir) + if err != nil { + return nil, errors.Wrap(err, "failed to run analysis") + } + resultsResponse.AnalyzerResults = analyzeResults + + if err := tarSupportBundleDir(bundlePath, filename); err != nil { + return nil, errors.Wrap(err, "create bundle file") + } + + fileUploaded, err := ProcessSupportBundleAfterCollection(spec, filename) + if err != nil { + return nil, errors.Wrap(err, "failed to process bundle after collection") + } + resultsResponse.fileUploaded = fileUploaded + + return &resultsResponse, nil +} + +// CollectSupportBundleFromURI collects support bundle from start to finish, including running +// collectors, analyzers and after collection steps. Input arguments are the URIs of the support bundle and redactor specs. +func CollectSupportBundleFromURI(specURI string, redactorURIs []string, opts SupportBundleCreateOpts) (*SupportBundleResponse, error) { + + supportbundle, err := GetSupportBundleFromURI(specURI) + if err != nil { + return nil, errors.Wrap(err, "could not bundle from URI") + } + + additionalRedactors := &troubleshootv1beta2.Redactor{} + for _, redactor := range redactorURIs { + redactorObj, err := GetRedactorFromURI(redactor) + if err != nil { + return nil, errors.Wrapf(err, "failed to get redactor spec %s", redactor) + } + + if redactorObj != nil { + additionalRedactors.Spec.Redactors = append(additionalRedactors.Spec.Redactors, redactorObj.Spec.Redactors...) + } + } + + return SupportBundleCollectAnalyzeProcess(&supportbundle.Spec, additionalRedactors, opts) +} + +// CollectSupportBundleFromSpec run the support bundle collectors and creates an archive. The output is the name of the archive on disk +// (the caller must remove) +func CollectSupportBundleFromSpec(spec *troubleshootv1beta2.SupportBundleSpec, additionalRedactors *troubleshootv1beta2.Redactor, opts SupportBundleCreateOpts) (string, error) { + + if opts.KubernetesRestConfig == nil { + return "", errors.New("did not receive kube rest config") + } + + if opts.ProgressChan == nil { + return "", errors.New("did not receive collector progress chan") + } + + tmpDir, err := ioutil.TempDir("", "supportbundle") + if err != nil { + return "", errors.Wrap(err, "create temp dir") + } + defer os.RemoveAll(tmpDir) + + // Do we need to put this in some kind of swap space? + filename, err := findFileName("support-bundle-"+time.Now().Format("2006-01-02T15_04_05"), "tar.gz") + if err != nil { + return "", errors.Wrap(err, "find file name") + } + + bundlePath := filepath.Join(tmpDir, strings.TrimSuffix(filename, ".tar.gz")) + if err := os.MkdirAll(bundlePath, 0777); err != nil { + return "", errors.Wrap(err, "create bundle dir") + } + + if err = writeVersionFile(bundlePath); err != nil { + return "", errors.Wrap(err, "write version file") + } + + // Run collectors + err = runCollectors(spec.Collectors, additionalRedactors, filename, bundlePath, opts) + if err != nil { + return "", errors.Wrap(err, "run collectors") + } + + if err := tarSupportBundleDir(bundlePath, filename); err != nil { + return "", errors.Wrap(err, "create bundle file") + } + + return filename, nil +} + +// ProcessSupportBundleAfterCollection performs the after collection actions, like Callbacks and sending the archive to a remote server. +func ProcessSupportBundleAfterCollection(spec *troubleshootv1beta2.SupportBundleSpec, archivePath string) (bool, error) { + fileUploaded := false + if len(spec.AfterCollection) > 0 { + for _, ac := range spec.AfterCollection { + if ac.UploadResultsTo != nil { + if err := uploadSupportBundle(ac.UploadResultsTo, archivePath); err != nil { + return false, errors.Wrap(err, "failed to upload support bundle") + } else { + fileUploaded = true + } + } else if ac.Callback != nil { + if err := callbackSupportBundleAPI(ac.Callback, archivePath); err != nil { + return false, errors.Wrap(err, "failed to notify API that support bundle has been uploaded") + } + } + } + } + return fileUploaded, nil +} + +// AnalyzeAndExtractSupportBundle performs analysis on a support bundle using the archive and spec. +func AnalyzeAndExtractSupportBundle(spec *troubleshootv1beta2.SupportBundleSpec, archivePath string) ([]*analyzer.AnalyzeResult, error) { + + var analyzeResults []*analyzer.AnalyzeResult + + if len(spec.Analyzers) > 0 { + + tmpDir, err := ioutil.TempDir("", "troubleshoot") + if err != nil { + return analyzeResults, errors.Wrap(err, "failed to make directory for analysis") + } + defer os.RemoveAll(tmpDir) + + f, err := os.Open(archivePath) + if err != nil { + return analyzeResults, errors.Wrap(err, "failed to open support bundle for analysis") + } + defer f.Close() + + if err := analyzer.ExtractTroubleshootBundle(f, tmpDir); err != nil { + return analyzeResults, errors.Wrap(err, "failed to extract support bundle for analysis") + } + + analyzeResults, err = analyzer.AnalyzeLocal(tmpDir, spec.Analyzers) + if err != nil { + return analyzeResults, errors.Wrap(err, "failed to analyze support bundle") + } + } + return analyzeResults, nil +} + +// AnalyzeSupportBundle performs analysis on a support bundle using the support bundle spec and an already unpacked support +// bundle on disk +func AnalyzeSupportBundle(spec *troubleshootv1beta2.SupportBundleSpec, tmpDir string) ([]*analyzer.AnalyzeResult, error) { + + var analyzeResults []*analyzer.AnalyzeResult + + if len(spec.Analyzers) > 0 { + + analyzeResults, err := analyzer.AnalyzeLocal(tmpDir, spec.Analyzers) + if err != nil { + return analyzeResults, errors.Wrap(err, "failed to analyze support bundle") + } + } + return analyzeResults, nil +}