feat: Optionally save preflight bundles to disk (#1612)

* feat: Optionally save preflight bundles to disk

Signed-off-by: Evans Mungai <evans@replicated.com>

* Add e2e test of saving preflight bundle

Signed-off-by: Evans Mungai <evans@replicated.com>

* Update cli docs

Signed-off-by: Evans Mungai <evans@replicated.com>

* Expose GetVersionFile function publicly

Signed-off-by: Evans Mungai <evans@replicated.com>

* Store analysis.json file in preflight bundle

Signed-off-by: Evans Mungai <evans@replicated.com>

* Run go fmt when running lint fixers

Signed-off-by: Evans Mungai <evans@replicated.com>

* Always generate a preflight bundle in CLI

Signed-off-by: Evans Mungai <evans@replicated.com>

* Print saving bundle message to stderr

Signed-off-by: Evans Mungai <evans@replicated.com>

* Revert changes in docs directory

Signed-off-by: Evans Mungai <evans@replicated.com>

* Use NewResult constructor

Signed-off-by: Evans Mungai <evans@replicated.com>

* Log always when preflight bundle is saved to disk

Signed-off-by: Evans Mungai <evans@replicated.com>

---------

Signed-off-by: Evans Mungai <evans@replicated.com>
This commit is contained in:
Evans Mungai
2024-09-16 23:36:52 +01:00
committed by GitHub
parent 05dcae2388
commit aea4f7c87c
12 changed files with 187 additions and 43 deletions

View File

@@ -239,7 +239,7 @@ scan:
lint:
golangci-lint run --new -c .golangci.yaml ${BUILDPATHS}
.PHONY: lint-and-fix
.PHONY: fmt lint-and-fix
lint-and-fix:
golangci-lint run --new --fix -c .golangci.yaml ${BUILDPATHS}

View File

@@ -50,7 +50,7 @@ that a cluster meets the requirements to run an application.`,
err = preflight.RunPreflights(v.GetBool("interactive"), v.GetString("output"), v.GetString("format"), args)
if !v.GetBool("dry-run") && (v.GetBool("debug") || v.IsSet("v")) {
fmt.Printf("\n%s", traces.GetExporterInstance().GetSummary())
fmt.Fprintf(os.Stderr, "\n%s", traces.GetExporterInstance().GetSummary())
}
return err

View File

@@ -64,7 +64,7 @@ For more information on redactors visit https://troubleshoot.sh/docs/redact/
if output == "" {
output = fmt.Sprintf("redacted-support-bundle-%s.tar.gz", time.Now().Format("2006-01-02T15_04_05"))
}
err = collectorResult.ArchiveSupportBundle(bundleDir, output)
err = collectorResult.ArchiveBundle(bundleDir, output)
if err != nil {
return errors.Wrap(err, "failed to create support bundle archive")
}

View File

@@ -54,7 +54,7 @@ If no arguments are provided, specs are automatically loaded from the cluster by
err = runTroubleshoot(v, args)
if !v.IsSet("dry-run") && (v.GetBool("debug") || v.IsSet("v")) {
fmt.Printf("\n%s", traces.GetExporterInstance().GetSummary())
fmt.Fprintf(os.Stderr, "\n%s", traces.GetExporterInstance().GetSummary())
}
return err

View File

@@ -270,7 +270,14 @@ func (r CollectorResult) CloseWriter(bundlePath string, relativePath string, wri
return errors.Errorf("cannot close writer of type %T", writer)
}
// ArchiveSupportBundle creates an archive of the files in the bundle directory
// Deprecated: Use better named ArchiveBundle since this method is used to archive any directory
func (r CollectorResult) ArchiveSupportBundle(bundlePath string, outputFilename string) error {
return r.ArchiveBundle(bundlePath, outputFilename)
}
// ArchiveBundle creates an archive of the files in the bundle directory
func (r CollectorResult) ArchiveBundle(bundlePath string, outputFilename string) error {
fileWriter, err := os.Create(outputFilename)
if err != nil {
return errors.Wrap(err, "failed to create output file")
@@ -404,5 +411,5 @@ func CollectorResultFromBundle(bundleDir string) (CollectorResult, error) {
// Deprecated: Remove in a future version (v1.0)
func TarSupportBundleDir(bundlePath string, input CollectorResult, outputFilename string) error {
// Is this used anywhere external anyway?
return input.ArchiveSupportBundle(bundlePath, outputFilename)
return input.ArchiveBundle(bundlePath, outputFilename)
}

View File

@@ -20,6 +20,7 @@ const (
LIB_TRACER_NAME = "github.com/replicatedhq/troubleshoot"
TROUBLESHOOT_ROOT_SPAN_NAME = "ReplicatedTroubleshootRootSpan"
EXCLUDED = "excluded"
ANALYSIS_FILENAME = "analysis.json"
// Cluster Resources Collector Directories
CLUSTER_RESOURCES_DIR = "cluster-resources"

View File

@@ -30,6 +30,9 @@ type CollectOpts struct {
LabelSelector string
Timeout time.Duration
ProgressChan chan interface{}
// Optional path to the bundle directory to store the collected data
BundlePath string
}
type CollectProgress struct {
@@ -96,7 +99,7 @@ func CollectHost(opts CollectOpts, p *troubleshootv1beta2.HostPreflight) (Collec
func CollectHostWithContext(
ctx context.Context, opts CollectOpts, p *troubleshootv1beta2.HostPreflight,
) (CollectResult, error) {
collectSpecs := make([]*troubleshootv1beta2.HostCollect, 0, 0)
collectSpecs := make([]*troubleshootv1beta2.HostCollect, 0)
if p != nil && p.Spec.Collectors != nil {
collectSpecs = append(collectSpecs, p.Spec.Collectors...)
}
@@ -105,7 +108,7 @@ func CollectHostWithContext(
var collectors []collect.HostCollector
for _, desiredCollector := range collectSpecs {
collector, ok := collect.GetHostCollector(desiredCollector, "")
collector, ok := collect.GetHostCollector(desiredCollector, opts.BundlePath)
if ok {
collectors = append(collectors, collector)
}
@@ -140,6 +143,7 @@ func CollectHostWithContext(
span.End()
}
// The values of map entries will contain the collected data in bytes if the data was not stored to disk
collectResult.AllCollectedData = allCollectedData
return collectResult, nil
@@ -154,7 +158,7 @@ func CollectWithContext(ctx context.Context, opts CollectOpts, p *troubleshootv1
var allCollectors []collect.Collector
var foundForbidden bool
collectSpecs := make([]*troubleshootv1beta2.Collect, 0, 0)
collectSpecs := make([]*troubleshootv1beta2.Collect, 0)
if p != nil && p.Spec.Collectors != nil {
collectSpecs = append(collectSpecs, p.Spec.Collectors...)
}
@@ -180,7 +184,7 @@ func CollectWithContext(ctx context.Context, opts CollectOpts, p *troubleshootv1
allCollectedData := make(map[string][]byte)
for _, desiredCollector := range collectSpecs {
if collectorInterface, ok := collect.GetCollector(desiredCollector, "", opts.Namespace, opts.KubernetesRestConfig, k8sClient, nil); ok {
if collectorInterface, ok := collect.GetCollector(desiredCollector, opts.BundlePath, opts.Namespace, opts.KubernetesRestConfig, k8sClient, nil); ok {
if collector, ok := collectorInterface.(collect.Collector); ok {
err := collector.CheckRBAC(ctx, collector, desiredCollector, opts.KubernetesRestConfig, opts.Namespace)
if err != nil {
@@ -305,6 +309,7 @@ func CollectWithContext(ctx context.Context, opts CollectOpts, p *troubleshootv1
span.End()
}
// The values of map entries will contain the collected data in bytes if the data was not stored to disk
collectResult.AllCollectedData = allCollectedData
return collectResult, nil

View File

@@ -1,10 +1,13 @@
package preflight
import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"os/signal"
"path/filepath"
"time"
cursor "github.com/ahmetalpbalkan/go-cursor"
@@ -13,15 +16,19 @@ import (
"github.com/replicatedhq/troubleshoot/internal/util"
analyzer "github.com/replicatedhq/troubleshoot/pkg/analyze"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/replicatedhq/troubleshoot/pkg/collect"
"github.com/replicatedhq/troubleshoot/pkg/constants"
"github.com/replicatedhq/troubleshoot/pkg/convert"
"github.com/replicatedhq/troubleshoot/pkg/k8sutil"
"github.com/replicatedhq/troubleshoot/pkg/types"
"github.com/replicatedhq/troubleshoot/pkg/version"
"github.com/spf13/viper"
spin "github.com/tj/go-spin"
"go.opentelemetry.io/otel"
"golang.org/x/sync/errgroup"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/klog/v2"
)
func RunPreflights(interactive bool, output string, format string, args []string) error {
@@ -77,6 +84,21 @@ func RunPreflights(interactive bool, output string, format string, args []string
var uploadCollectResults []CollectResult
preflightSpecName := ""
// Create a temporary directory to save the preflight bundle
tmpDir, err := os.MkdirTemp("", "preflightbundle-")
if err != nil {
return errors.Wrap(err, "create temp dir for preflightbundle")
}
defer os.RemoveAll(tmpDir)
bundleFileName := fmt.Sprintf("preflightbundle-%s", time.Now().Format("2006-01-02T15_04_05"))
bundlePath := filepath.Join(tmpDir, bundleFileName)
if err := os.MkdirAll(bundlePath, 0777); err != nil {
return errors.Wrap(err, "failed to create preflight bundle dir")
}
archivePath := fmt.Sprintf("%s.tar.gz", bundleFileName)
klog.V(2).Infof("Preflight data collected in temporary directory: %s", tmpDir)
progressCh := make(chan interface{})
defer close(progressCh)
@@ -92,12 +114,21 @@ func RunPreflights(interactive bool, output string, format string, args []string
}
uploadResultsMap := make(map[string][]CollectResult)
collectorResults := collect.NewResult()
analyzers := []*troubleshootv1beta2.Analyze{}
hostAnalyzers := []*troubleshootv1beta2.HostAnalyze{}
for _, spec := range specs.PreflightsV1Beta2 {
r, err := collectInCluster(ctx, &spec, progressCh)
r, err := collectInCluster(ctx, &spec, progressCh, bundlePath)
if err != nil {
return types.NewExitCodeError(constants.EXIT_CODE_CATCH_ALL, errors.Wrap(err, "failed to collect in cluster"))
}
collectorResult, ok := (*r).(ClusterCollectResult)
if !ok {
return errors.Errorf("unexpected result type: %T", collectResults)
}
collectorResults.AddResult(collect.CollectorResult(collectorResult.AllCollectedData))
if spec.Spec.UploadResultsTo != "" {
uploadResultsMap[spec.Spec.UploadResultsTo] = append(uploadResultsMap[spec.Spec.UploadResultsTo], *r)
uploadCollectResults = append(collectResults, *r)
@@ -106,15 +137,21 @@ func RunPreflights(interactive bool, output string, format string, args []string
}
// TODO: This spec name will be overwritten by the next spec. Is this intentional?
preflightSpecName = spec.Name
analyzers = append(analyzers, spec.Spec.Analyzers...)
}
for _, spec := range specs.HostPreflightsV1Beta2 {
if len(spec.Spec.Collectors) > 0 {
r, err := collectHost(ctx, &spec, progressCh)
r, err := collectHost(ctx, &spec, progressCh, bundlePath)
if err != nil {
return types.NewExitCodeError(constants.EXIT_CODE_CATCH_ALL, errors.Wrap(err, "failed to collect from host"))
}
collectResults = append(collectResults, *r)
collectorResult, ok := (*r).(HostCollectResult)
if !ok {
return errors.Errorf("unexpected result type: %T", collectResults)
}
collectorResults.AddResult(collect.CollectorResult(collectorResult.AllCollectedData))
}
if len(spec.Spec.RemoteCollectors) > 0 {
r, err := collectRemote(ctx, &spec, progressCh)
@@ -122,17 +159,32 @@ func RunPreflights(interactive bool, output string, format string, args []string
return types.NewExitCodeError(constants.EXIT_CODE_CATCH_ALL, errors.Wrap(err, "failed to collect remotely"))
}
collectResults = append(collectResults, *r)
collectorResult, ok := (*r).(RemoteCollectResult)
if !ok {
return errors.Errorf("unexpected result type: %T", collectResults)
}
collectorResults.AddResult(collect.CollectorResult(collectorResult.AllCollectedData))
}
preflightSpecName = spec.Name
hostAnalyzers = append(hostAnalyzers, spec.Spec.Analyzers...)
}
if len(collectResults) == 0 && len(uploadCollectResults) == 0 {
return types.NewExitCodeError(constants.EXIT_CODE_CATCH_ALL, errors.New("no data was collected"))
}
analyzeResults := []*analyzer.AnalyzeResult{}
for _, res := range collectResults {
analyzeResults = append(analyzeResults, res.Analyze()...)
err = saveTSVersionToBundle(collectorResults, bundlePath)
if err != nil {
return errors.Wrap(err, "failed to save version file")
}
analyzeResults, err := analyzer.AnalyzeLocal(ctx, bundlePath, analyzers, hostAnalyzers)
if err != nil {
return errors.Wrap(err, "failed to analyze support bundle")
}
err = saveAnalysisResultsToBundle(collectorResults, analyzeResults, bundlePath)
if err != nil {
return errors.Wrap(err, "failed to save analysis results to bundle")
}
uploadAnalyzeResultsMap := make(map[string][]*analyzer.AnalyzeResult)
@@ -150,6 +202,12 @@ func RunPreflights(interactive bool, output string, format string, args []string
}
}
// Archive preflight bundle
if err := collectorResults.ArchiveBundle(bundlePath, archivePath); err != nil {
return errors.Wrapf(err, "failed to create %s archive", archivePath)
}
defer fmt.Fprintf(os.Stderr, "\nSaving preflight bundle to %s\n", archivePath)
stopProgressCollection()
progressCollection.Wait()
@@ -176,6 +234,37 @@ func RunPreflights(interactive bool, output string, format string, args []string
return types.NewExitCodeError(exitCode, errors.New("preflights failed with warnings or errors"))
}
func saveAnalysisResultsToBundle(
results collect.CollectorResult, analyzeResults []*analyzer.AnalyzeResult, bundlePath string,
) error {
data := convert.FromAnalyzerResult(analyzeResults)
analysis, err := json.MarshalIndent(data, "", " ")
if err != nil {
return err
}
err = results.SaveResult(bundlePath, "analysis.json", bytes.NewBuffer(analysis))
if err != nil {
return err
}
return nil
}
func saveTSVersionToBundle(results collect.CollectorResult, bundlePath string) error {
version, err := version.GetVersionFile()
if err != nil {
return err
}
err = results.SaveResult(bundlePath, constants.VERSION_FILENAME, bytes.NewBuffer([]byte(version)))
if err != nil {
return err
}
return nil
}
// Determine if any preflight checks passed vs failed vs warned
// If all checks passed: 0
// If 1 or more checks failed: 3
@@ -250,7 +339,9 @@ func collectNonInteractiveProgess(ctx context.Context, progressCh <-chan interfa
}
}
func collectInCluster(ctx context.Context, preflightSpec *troubleshootv1beta2.Preflight, progressCh chan interface{}) (*CollectResult, error) {
func collectInCluster(
ctx context.Context, preflightSpec *troubleshootv1beta2.Preflight, progressCh chan interface{}, bundlePath string,
) (*CollectResult, error) {
v := viper.GetViper()
restConfig, err := k8sutil.GetRESTConfig()
@@ -263,6 +354,7 @@ func collectInCluster(ctx context.Context, preflightSpec *troubleshootv1beta2.Pr
IgnorePermissionErrors: v.GetBool("collect-without-permissions"),
ProgressChan: progressCh,
KubernetesRestConfig: restConfig,
BundlePath: bundlePath,
}
if v.GetString("since") != "" || v.GetString("since-time") != "" {
@@ -289,7 +381,7 @@ func collectInCluster(ctx context.Context, preflightSpec *troubleshootv1beta2.Pr
return &collectResults, nil
}
func collectRemote(ctx context.Context, preflightSpec *troubleshootv1beta2.HostPreflight, progressCh chan interface{}) (*CollectResult, error) {
func collectRemote(_ context.Context, preflightSpec *troubleshootv1beta2.HostPreflight, progressCh chan interface{}) (*CollectResult, error) {
v := viper.GetViper()
restConfig, err := k8sutil.GetRESTConfig()
@@ -331,9 +423,12 @@ func collectRemote(ctx context.Context, preflightSpec *troubleshootv1beta2.HostP
return &collectResults, nil
}
func collectHost(ctx context.Context, hostPreflightSpec *troubleshootv1beta2.HostPreflight, progressCh chan interface{}) (*CollectResult, error) {
func collectHost(
_ context.Context, hostPreflightSpec *troubleshootv1beta2.HostPreflight, progressCh chan interface{}, bundlePath string,
) (*CollectResult, error) {
collectOpts := CollectOpts{
ProgressChan: progressCh,
BundlePath: bundlePath,
}
collectResults, err := CollectHost(collectOpts, hostPreflightSpec)

View File

@@ -19,7 +19,6 @@ import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"gopkg.in/yaml.v2"
"k8s.io/client-go/kubernetes"
)
@@ -221,24 +220,6 @@ func findFileName(basename, extension string) (string, error) {
}
}
func getVersionFile() (io.Reader, 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 nil, errors.Wrap(err, "failed to marshal version data")
}
return bytes.NewBuffer(b), nil
}
const AnalysisFilename = "analysis.json"
func getAnalysisFile(analyzeResults []*analyze.AnalyzeResult) (io.Reader, error) {
data := convert.FromAnalyzerResult(analyzeResults)
analysis, err := json.MarshalIndent(data, "", " ")

View File

@@ -20,6 +20,7 @@ import (
"github.com/replicatedhq/troubleshoot/pkg/collect"
"github.com/replicatedhq/troubleshoot/pkg/constants"
"github.com/replicatedhq/troubleshoot/pkg/convert"
"github.com/replicatedhq/troubleshoot/pkg/version"
"go.opentelemetry.io/otel"
"k8s.io/client-go/rest"
"k8s.io/klog/v2"
@@ -144,12 +145,12 @@ func CollectSupportBundleFromSpec(
return nil, fmt.Errorf("failed to generate support bundle")
}
version, err := getVersionFile()
version, err := version.GetVersionFile()
if err != nil {
return nil, errors.Wrap(err, "failed to get version file")
}
err = result.SaveResult(bundlePath, constants.VERSION_FILENAME, version)
err = result.SaveResult(bundlePath, constants.VERSION_FILENAME, bytes.NewBuffer([]byte(version)))
if err != nil {
return nil, errors.Wrap(err, "failed to write version")
}
@@ -172,7 +173,7 @@ func CollectSupportBundleFromSpec(
return nil, errors.Wrap(err, "failed to get analysis file")
}
err = result.SaveResult(bundlePath, AnalysisFilename, analysis)
err = result.SaveResult(bundlePath, constants.ANALYSIS_FILENAME, analysis)
if err != nil {
return nil, errors.Wrap(err, "failed to write analysis")
}
@@ -188,7 +189,7 @@ func CollectSupportBundleFromSpec(
}
// Archive Support Bundle
if err := result.ArchiveSupportBundle(bundlePath, filename); err != nil {
if err := result.ArchiveBundle(bundlePath, filename); err != nil {
return nil, errors.Wrap(err, "create bundle file")
}

View File

@@ -5,6 +5,10 @@ import (
"runtime"
"runtime/debug"
"time"
"github.com/pkg/errors"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"gopkg.in/yaml.v2"
)
var (
@@ -96,3 +100,21 @@ func getGoInfo() GoInfo {
func GetUserAgent() string {
return fmt.Sprintf("Replicated_Troubleshoot/%s", Version())
}
func GetVersionFile() (string, error) {
// TODO: Should this type be agnostic to the tool?
// i.e should it be a TroubleshootVersion instead?
version := troubleshootv1beta2.SupportBundleVersion{
ApiVersion: "troubleshoot.sh/v1beta2",
Kind: "SupportBundle",
Spec: troubleshootv1beta2.SupportBundleVersionSpec{
VersionNumber: Version(),
},
}
b, err := yaml.Marshal(version)
if err != nil {
return "", errors.Wrap(err, "failed to marshal version data")
}
return string(b), nil
}

View File

@@ -2,10 +2,21 @@
set -euo pipefail
PREFLIGHT_BIN=$(pwd)/bin/preflight
tmpdir="$(mktemp -d)"
trap cleanup SIGHUP SIGINT SIGTERM EXIT
cleanup() {
rm -rf $tmpdir
}
reset_tmp() {
rm -rf "$tmpdir"
tmpdir="$(mktemp -d)"
}
echo -e "\n========= Running preflights from e2e spec and checking results ========="
./bin/preflight --debug --interactive=false --format=json examples/preflight/e2e.yaml > "$tmpdir/result.json"
$PREFLIGHT_BIN --debug --interactive=false --format=json examples/preflight/e2e.yaml > "$tmpdir/result.json"
if [ $? -ne 0 ]; then
echo "preflight command failed"
exit $EXIT_STATUS
@@ -35,13 +46,34 @@ EXIT_STATUS=1
fi
echo -e "\n========= Running preflights from stdin using e2e spec ========="
cat examples/preflight/e2e.yaml | ./bin/preflight --debug --interactive=false --format=json - > "$tmpdir/result.json"
cat examples/preflight/e2e.yaml | $PREFLIGHT_BIN --debug --interactive=false --format=json - > "$tmpdir/result.json"
EXIT_STATUS=$?
if [ $EXIT_STATUS -ne 0 ]; then
echo "preflight command failed"
exit $EXIT_STATUS
fi
rm -rf "$tmpdir"
echo -e "\n========= Running preflights and storing bundle in current working directory ========="
E2E_PREFLIGHT=$(pwd)/examples/preflight/e2e.yaml
# We need a clean slate
reset_tmp
pushd $tmpdir >/dev/null
echo $E2E_PREFLIGHT
cat $E2E_PREFLIGHT | $PREFLIGHT_BIN --debug --interactive=false -
EXIT_STATUS=$?
popd >/dev/null
if [ $EXIT_STATUS -ne 0 ]; then
echo "preflight command failed"
exit $EXIT_STATUS
fi
if ls $tmpdir/preflightbundle-*.tar.gz; then
echo "preflight bundle exists"
else
echo "Failed to find collected preflight bundle"
exit 1
fi
exit $EXIT_STATUS