Skip to content

Commit aea4f7c

Browse files
authored
feat: Optionally save preflight bundles to disk (#1612)
* feat: Optionally save preflight bundles to disk Signed-off-by: Evans Mungai <[email protected]> * Add e2e test of saving preflight bundle Signed-off-by: Evans Mungai <[email protected]> * Update cli docs Signed-off-by: Evans Mungai <[email protected]> * Expose GetVersionFile function publicly Signed-off-by: Evans Mungai <[email protected]> * Store analysis.json file in preflight bundle Signed-off-by: Evans Mungai <[email protected]> * Run go fmt when running lint fixers Signed-off-by: Evans Mungai <[email protected]> * Always generate a preflight bundle in CLI Signed-off-by: Evans Mungai <[email protected]> * Print saving bundle message to stderr Signed-off-by: Evans Mungai <[email protected]> * Revert changes in docs directory Signed-off-by: Evans Mungai <[email protected]> * Use NewResult constructor Signed-off-by: Evans Mungai <[email protected]> * Log always when preflight bundle is saved to disk Signed-off-by: Evans Mungai <[email protected]> --------- Signed-off-by: Evans Mungai <[email protected]>
1 parent 05dcae2 commit aea4f7c

File tree

12 files changed

+187
-43
lines changed

12 files changed

+187
-43
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ scan:
239239
lint:
240240
golangci-lint run --new -c .golangci.yaml ${BUILDPATHS}
241241

242-
.PHONY: lint-and-fix
242+
.PHONY: fmt lint-and-fix
243243
lint-and-fix:
244244
golangci-lint run --new --fix -c .golangci.yaml ${BUILDPATHS}
245245

cmd/preflight/cli/root.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ that a cluster meets the requirements to run an application.`,
5050

5151
err = preflight.RunPreflights(v.GetBool("interactive"), v.GetString("output"), v.GetString("format"), args)
5252
if !v.GetBool("dry-run") && (v.GetBool("debug") || v.IsSet("v")) {
53-
fmt.Printf("\n%s", traces.GetExporterInstance().GetSummary())
53+
fmt.Fprintf(os.Stderr, "\n%s", traces.GetExporterInstance().GetSummary())
5454
}
5555

5656
return err

cmd/troubleshoot/cli/redact.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ For more information on redactors visit https://troubleshoot.sh/docs/redact/
6464
if output == "" {
6565
output = fmt.Sprintf("redacted-support-bundle-%s.tar.gz", time.Now().Format("2006-01-02T15_04_05"))
6666
}
67-
err = collectorResult.ArchiveSupportBundle(bundleDir, output)
67+
err = collectorResult.ArchiveBundle(bundleDir, output)
6868
if err != nil {
6969
return errors.Wrap(err, "failed to create support bundle archive")
7070
}

cmd/troubleshoot/cli/root.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ If no arguments are provided, specs are automatically loaded from the cluster by
5454

5555
err = runTroubleshoot(v, args)
5656
if !v.IsSet("dry-run") && (v.GetBool("debug") || v.IsSet("v")) {
57-
fmt.Printf("\n%s", traces.GetExporterInstance().GetSummary())
57+
fmt.Fprintf(os.Stderr, "\n%s", traces.GetExporterInstance().GetSummary())
5858
}
5959

6060
return err

pkg/collect/result.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,14 @@ func (r CollectorResult) CloseWriter(bundlePath string, relativePath string, wri
270270
return errors.Errorf("cannot close writer of type %T", writer)
271271
}
272272

273+
// ArchiveSupportBundle creates an archive of the files in the bundle directory
274+
// Deprecated: Use better named ArchiveBundle since this method is used to archive any directory
273275
func (r CollectorResult) ArchiveSupportBundle(bundlePath string, outputFilename string) error {
276+
return r.ArchiveBundle(bundlePath, outputFilename)
277+
}
278+
279+
// ArchiveBundle creates an archive of the files in the bundle directory
280+
func (r CollectorResult) ArchiveBundle(bundlePath string, outputFilename string) error {
274281
fileWriter, err := os.Create(outputFilename)
275282
if err != nil {
276283
return errors.Wrap(err, "failed to create output file")
@@ -404,5 +411,5 @@ func CollectorResultFromBundle(bundleDir string) (CollectorResult, error) {
404411
// Deprecated: Remove in a future version (v1.0)
405412
func TarSupportBundleDir(bundlePath string, input CollectorResult, outputFilename string) error {
406413
// Is this used anywhere external anyway?
407-
return input.ArchiveSupportBundle(bundlePath, outputFilename)
414+
return input.ArchiveBundle(bundlePath, outputFilename)
408415
}

pkg/constants/constants.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const (
2020
LIB_TRACER_NAME = "github.com/replicatedhq/troubleshoot"
2121
TROUBLESHOOT_ROOT_SPAN_NAME = "ReplicatedTroubleshootRootSpan"
2222
EXCLUDED = "excluded"
23+
ANALYSIS_FILENAME = "analysis.json"
2324

2425
// Cluster Resources Collector Directories
2526
CLUSTER_RESOURCES_DIR = "cluster-resources"

pkg/preflight/collect.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ type CollectOpts struct {
3030
LabelSelector string
3131
Timeout time.Duration
3232
ProgressChan chan interface{}
33+
34+
// Optional path to the bundle directory to store the collected data
35+
BundlePath string
3336
}
3437

3538
type CollectProgress struct {
@@ -96,7 +99,7 @@ func CollectHost(opts CollectOpts, p *troubleshootv1beta2.HostPreflight) (Collec
9699
func CollectHostWithContext(
97100
ctx context.Context, opts CollectOpts, p *troubleshootv1beta2.HostPreflight,
98101
) (CollectResult, error) {
99-
collectSpecs := make([]*troubleshootv1beta2.HostCollect, 0, 0)
102+
collectSpecs := make([]*troubleshootv1beta2.HostCollect, 0)
100103
if p != nil && p.Spec.Collectors != nil {
101104
collectSpecs = append(collectSpecs, p.Spec.Collectors...)
102105
}
@@ -105,7 +108,7 @@ func CollectHostWithContext(
105108

106109
var collectors []collect.HostCollector
107110
for _, desiredCollector := range collectSpecs {
108-
collector, ok := collect.GetHostCollector(desiredCollector, "")
111+
collector, ok := collect.GetHostCollector(desiredCollector, opts.BundlePath)
109112
if ok {
110113
collectors = append(collectors, collector)
111114
}
@@ -140,6 +143,7 @@ func CollectHostWithContext(
140143
span.End()
141144
}
142145

146+
// The values of map entries will contain the collected data in bytes if the data was not stored to disk
143147
collectResult.AllCollectedData = allCollectedData
144148

145149
return collectResult, nil
@@ -154,7 +158,7 @@ func CollectWithContext(ctx context.Context, opts CollectOpts, p *troubleshootv1
154158
var allCollectors []collect.Collector
155159
var foundForbidden bool
156160

157-
collectSpecs := make([]*troubleshootv1beta2.Collect, 0, 0)
161+
collectSpecs := make([]*troubleshootv1beta2.Collect, 0)
158162
if p != nil && p.Spec.Collectors != nil {
159163
collectSpecs = append(collectSpecs, p.Spec.Collectors...)
160164
}
@@ -180,7 +184,7 @@ func CollectWithContext(ctx context.Context, opts CollectOpts, p *troubleshootv1
180184
allCollectedData := make(map[string][]byte)
181185

182186
for _, desiredCollector := range collectSpecs {
183-
if collectorInterface, ok := collect.GetCollector(desiredCollector, "", opts.Namespace, opts.KubernetesRestConfig, k8sClient, nil); ok {
187+
if collectorInterface, ok := collect.GetCollector(desiredCollector, opts.BundlePath, opts.Namespace, opts.KubernetesRestConfig, k8sClient, nil); ok {
184188
if collector, ok := collectorInterface.(collect.Collector); ok {
185189
err := collector.CheckRBAC(ctx, collector, desiredCollector, opts.KubernetesRestConfig, opts.Namespace)
186190
if err != nil {
@@ -305,6 +309,7 @@ func CollectWithContext(ctx context.Context, opts CollectOpts, p *troubleshootv1
305309
span.End()
306310
}
307311

312+
// The values of map entries will contain the collected data in bytes if the data was not stored to disk
308313
collectResult.AllCollectedData = allCollectedData
309314

310315
return collectResult, nil

pkg/preflight/run.go

Lines changed: 103 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
package preflight
22

33
import (
4+
"bytes"
45
"context"
6+
"encoding/json"
57
"fmt"
68
"os"
79
"os/signal"
10+
"path/filepath"
811
"time"
912

1013
cursor "github.com/ahmetalpbalkan/go-cursor"
@@ -13,15 +16,19 @@ import (
1316
"github.com/replicatedhq/troubleshoot/internal/util"
1417
analyzer "github.com/replicatedhq/troubleshoot/pkg/analyze"
1518
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
19+
"github.com/replicatedhq/troubleshoot/pkg/collect"
1620
"github.com/replicatedhq/troubleshoot/pkg/constants"
21+
"github.com/replicatedhq/troubleshoot/pkg/convert"
1722
"github.com/replicatedhq/troubleshoot/pkg/k8sutil"
1823
"github.com/replicatedhq/troubleshoot/pkg/types"
24+
"github.com/replicatedhq/troubleshoot/pkg/version"
1925
"github.com/spf13/viper"
2026
spin "github.com/tj/go-spin"
2127
"go.opentelemetry.io/otel"
2228
"golang.org/x/sync/errgroup"
2329
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2430
"k8s.io/apimachinery/pkg/labels"
31+
"k8s.io/klog/v2"
2532
)
2633

2734
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
7784
var uploadCollectResults []CollectResult
7885
preflightSpecName := ""
7986

87+
// Create a temporary directory to save the preflight bundle
88+
tmpDir, err := os.MkdirTemp("", "preflightbundle-")
89+
if err != nil {
90+
return errors.Wrap(err, "create temp dir for preflightbundle")
91+
}
92+
defer os.RemoveAll(tmpDir)
93+
bundleFileName := fmt.Sprintf("preflightbundle-%s", time.Now().Format("2006-01-02T15_04_05"))
94+
bundlePath := filepath.Join(tmpDir, bundleFileName)
95+
if err := os.MkdirAll(bundlePath, 0777); err != nil {
96+
return errors.Wrap(err, "failed to create preflight bundle dir")
97+
}
98+
99+
archivePath := fmt.Sprintf("%s.tar.gz", bundleFileName)
100+
klog.V(2).Infof("Preflight data collected in temporary directory: %s", tmpDir)
101+
80102
progressCh := make(chan interface{})
81103
defer close(progressCh)
82104

@@ -92,12 +114,21 @@ func RunPreflights(interactive bool, output string, format string, args []string
92114
}
93115

94116
uploadResultsMap := make(map[string][]CollectResult)
117+
collectorResults := collect.NewResult()
118+
analyzers := []*troubleshootv1beta2.Analyze{}
119+
hostAnalyzers := []*troubleshootv1beta2.HostAnalyze{}
95120

96121
for _, spec := range specs.PreflightsV1Beta2 {
97-
r, err := collectInCluster(ctx, &spec, progressCh)
122+
r, err := collectInCluster(ctx, &spec, progressCh, bundlePath)
98123
if err != nil {
99124
return types.NewExitCodeError(constants.EXIT_CODE_CATCH_ALL, errors.Wrap(err, "failed to collect in cluster"))
100125
}
126+
collectorResult, ok := (*r).(ClusterCollectResult)
127+
if !ok {
128+
return errors.Errorf("unexpected result type: %T", collectResults)
129+
}
130+
collectorResults.AddResult(collect.CollectorResult(collectorResult.AllCollectedData))
131+
101132
if spec.Spec.UploadResultsTo != "" {
102133
uploadResultsMap[spec.Spec.UploadResultsTo] = append(uploadResultsMap[spec.Spec.UploadResultsTo], *r)
103134
uploadCollectResults = append(collectResults, *r)
@@ -106,33 +137,54 @@ func RunPreflights(interactive bool, output string, format string, args []string
106137
}
107138
// TODO: This spec name will be overwritten by the next spec. Is this intentional?
108139
preflightSpecName = spec.Name
140+
analyzers = append(analyzers, spec.Spec.Analyzers...)
109141
}
110142

111143
for _, spec := range specs.HostPreflightsV1Beta2 {
112144
if len(spec.Spec.Collectors) > 0 {
113-
r, err := collectHost(ctx, &spec, progressCh)
145+
r, err := collectHost(ctx, &spec, progressCh, bundlePath)
114146
if err != nil {
115147
return types.NewExitCodeError(constants.EXIT_CODE_CATCH_ALL, errors.Wrap(err, "failed to collect from host"))
116148
}
117149
collectResults = append(collectResults, *r)
150+
collectorResult, ok := (*r).(HostCollectResult)
151+
if !ok {
152+
return errors.Errorf("unexpected result type: %T", collectResults)
153+
}
154+
collectorResults.AddResult(collect.CollectorResult(collectorResult.AllCollectedData))
118155
}
119156
if len(spec.Spec.RemoteCollectors) > 0 {
120157
r, err := collectRemote(ctx, &spec, progressCh)
121158
if err != nil {
122159
return types.NewExitCodeError(constants.EXIT_CODE_CATCH_ALL, errors.Wrap(err, "failed to collect remotely"))
123160
}
124161
collectResults = append(collectResults, *r)
162+
collectorResult, ok := (*r).(RemoteCollectResult)
163+
if !ok {
164+
return errors.Errorf("unexpected result type: %T", collectResults)
165+
}
166+
collectorResults.AddResult(collect.CollectorResult(collectorResult.AllCollectedData))
125167
}
126168
preflightSpecName = spec.Name
169+
hostAnalyzers = append(hostAnalyzers, spec.Spec.Analyzers...)
127170
}
128171

129172
if len(collectResults) == 0 && len(uploadCollectResults) == 0 {
130173
return types.NewExitCodeError(constants.EXIT_CODE_CATCH_ALL, errors.New("no data was collected"))
131174
}
132175

133-
analyzeResults := []*analyzer.AnalyzeResult{}
134-
for _, res := range collectResults {
135-
analyzeResults = append(analyzeResults, res.Analyze()...)
176+
err = saveTSVersionToBundle(collectorResults, bundlePath)
177+
if err != nil {
178+
return errors.Wrap(err, "failed to save version file")
179+
}
180+
181+
analyzeResults, err := analyzer.AnalyzeLocal(ctx, bundlePath, analyzers, hostAnalyzers)
182+
if err != nil {
183+
return errors.Wrap(err, "failed to analyze support bundle")
184+
}
185+
err = saveAnalysisResultsToBundle(collectorResults, analyzeResults, bundlePath)
186+
if err != nil {
187+
return errors.Wrap(err, "failed to save analysis results to bundle")
136188
}
137189

138190
uploadAnalyzeResultsMap := make(map[string][]*analyzer.AnalyzeResult)
@@ -150,6 +202,12 @@ func RunPreflights(interactive bool, output string, format string, args []string
150202
}
151203
}
152204

205+
// Archive preflight bundle
206+
if err := collectorResults.ArchiveBundle(bundlePath, archivePath); err != nil {
207+
return errors.Wrapf(err, "failed to create %s archive", archivePath)
208+
}
209+
defer fmt.Fprintf(os.Stderr, "\nSaving preflight bundle to %s\n", archivePath)
210+
153211
stopProgressCollection()
154212
progressCollection.Wait()
155213

@@ -176,6 +234,37 @@ func RunPreflights(interactive bool, output string, format string, args []string
176234
return types.NewExitCodeError(exitCode, errors.New("preflights failed with warnings or errors"))
177235
}
178236

237+
func saveAnalysisResultsToBundle(
238+
results collect.CollectorResult, analyzeResults []*analyzer.AnalyzeResult, bundlePath string,
239+
) error {
240+
data := convert.FromAnalyzerResult(analyzeResults)
241+
analysis, err := json.MarshalIndent(data, "", " ")
242+
if err != nil {
243+
return err
244+
}
245+
246+
err = results.SaveResult(bundlePath, "analysis.json", bytes.NewBuffer(analysis))
247+
if err != nil {
248+
return err
249+
}
250+
251+
return nil
252+
}
253+
254+
func saveTSVersionToBundle(results collect.CollectorResult, bundlePath string) error {
255+
version, err := version.GetVersionFile()
256+
if err != nil {
257+
return err
258+
}
259+
260+
err = results.SaveResult(bundlePath, constants.VERSION_FILENAME, bytes.NewBuffer([]byte(version)))
261+
if err != nil {
262+
return err
263+
}
264+
265+
return nil
266+
}
267+
179268
// Determine if any preflight checks passed vs failed vs warned
180269
// If all checks passed: 0
181270
// If 1 or more checks failed: 3
@@ -250,7 +339,9 @@ func collectNonInteractiveProgess(ctx context.Context, progressCh <-chan interfa
250339
}
251340
}
252341

253-
func collectInCluster(ctx context.Context, preflightSpec *troubleshootv1beta2.Preflight, progressCh chan interface{}) (*CollectResult, error) {
342+
func collectInCluster(
343+
ctx context.Context, preflightSpec *troubleshootv1beta2.Preflight, progressCh chan interface{}, bundlePath string,
344+
) (*CollectResult, error) {
254345
v := viper.GetViper()
255346

256347
restConfig, err := k8sutil.GetRESTConfig()
@@ -263,6 +354,7 @@ func collectInCluster(ctx context.Context, preflightSpec *troubleshootv1beta2.Pr
263354
IgnorePermissionErrors: v.GetBool("collect-without-permissions"),
264355
ProgressChan: progressCh,
265356
KubernetesRestConfig: restConfig,
357+
BundlePath: bundlePath,
266358
}
267359

268360
if v.GetString("since") != "" || v.GetString("since-time") != "" {
@@ -289,7 +381,7 @@ func collectInCluster(ctx context.Context, preflightSpec *troubleshootv1beta2.Pr
289381
return &collectResults, nil
290382
}
291383

292-
func collectRemote(ctx context.Context, preflightSpec *troubleshootv1beta2.HostPreflight, progressCh chan interface{}) (*CollectResult, error) {
384+
func collectRemote(_ context.Context, preflightSpec *troubleshootv1beta2.HostPreflight, progressCh chan interface{}) (*CollectResult, error) {
293385
v := viper.GetViper()
294386

295387
restConfig, err := k8sutil.GetRESTConfig()
@@ -331,9 +423,12 @@ func collectRemote(ctx context.Context, preflightSpec *troubleshootv1beta2.HostP
331423
return &collectResults, nil
332424
}
333425

334-
func collectHost(ctx context.Context, hostPreflightSpec *troubleshootv1beta2.HostPreflight, progressCh chan interface{}) (*CollectResult, error) {
426+
func collectHost(
427+
_ context.Context, hostPreflightSpec *troubleshootv1beta2.HostPreflight, progressCh chan interface{}, bundlePath string,
428+
) (*CollectResult, error) {
335429
collectOpts := CollectOpts{
336430
ProgressChan: progressCh,
431+
BundlePath: bundlePath,
337432
}
338433

339434
collectResults, err := CollectHost(collectOpts, hostPreflightSpec)

pkg/supportbundle/collect.go

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import (
1919
"go.opentelemetry.io/otel"
2020
"go.opentelemetry.io/otel/attribute"
2121
"go.opentelemetry.io/otel/codes"
22-
"gopkg.in/yaml.v2"
2322
"k8s.io/client-go/kubernetes"
2423
)
2524

@@ -221,24 +220,6 @@ func findFileName(basename, extension string) (string, error) {
221220
}
222221
}
223222

224-
func getVersionFile() (io.Reader, error) {
225-
version := troubleshootv1beta2.SupportBundleVersion{
226-
ApiVersion: "troubleshoot.sh/v1beta2",
227-
Kind: "SupportBundle",
228-
Spec: troubleshootv1beta2.SupportBundleVersionSpec{
229-
VersionNumber: version.Version(),
230-
},
231-
}
232-
b, err := yaml.Marshal(version)
233-
if err != nil {
234-
return nil, errors.Wrap(err, "failed to marshal version data")
235-
}
236-
237-
return bytes.NewBuffer(b), nil
238-
}
239-
240-
const AnalysisFilename = "analysis.json"
241-
242223
func getAnalysisFile(analyzeResults []*analyze.AnalyzeResult) (io.Reader, error) {
243224
data := convert.FromAnalyzerResult(analyzeResults)
244225
analysis, err := json.MarshalIndent(data, "", " ")

0 commit comments

Comments
 (0)