Merge pull request #360 from replicatedhq/chore-refactor-support-bundle

chore(support-bundle): refactor out support bundle pkg
This commit is contained in:
Dan Stough
2021-05-04 10:53:29 -04:00
committed by GitHub
6 changed files with 1016 additions and 626 deletions
+85 -626
View File
@@ -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: <namespace>/<pod name>/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
}
+24
View File
@@ -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
}
+157
View File
@@ -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
}
+320
View File
@@ -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: <namespace>/<pod name>/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
}
+199
View File
@@ -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
}
}
+231
View File
@@ -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
}