Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 33 additions & 0 deletions bundle/exec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package bundle

import (
"context"

"github.com/replicatedcom/support-bundle/types"
)

func Exec(ctx context.Context, rootDir string, tasks []types.Task) []*types.Result {
results := make(chan []*types.Result)

for _, task := range tasks {
go func(task types.Task) {
results <- task.Exec(ctx, rootDir)
}(task)
}

pending := len(tasks)
var accm []*types.Result

for {
select {
case r := <-results:
accm = append(accm, r...)
pending--
if pending == 0 {
return accm
}
case <-ctx.Done():
return accm
}
}
}
84 changes: 84 additions & 0 deletions bundle/exec_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package bundle

import (
"context"
"testing"
"time"

"github.com/pkg/errors"
"github.com/replicatedcom/support-bundle/types"
"github.com/stretchr/testify/assert"
)

type taskStub struct {
elapse time.Duration
results []*types.Result
}

func (t taskStub) Exec(ctx context.Context, rootDir string) []*types.Result {
time.Sleep(t.elapse)
return t.results
}

func TestExec(t *testing.T) {
nilResults := taskStub{
elapse: time.Nanosecond,
results: nil,
}
noResults := taskStub{
elapse: time.Nanosecond,
results: []*types.Result{},
}
singleResults := taskStub{
elapse: time.Nanosecond,
results: []*types.Result{
{
Description: "Logs from db container",
Path: "/docker/db.logs",
},
},
}
mixedResults := taskStub{
elapse: time.Nanosecond,
results: []*types.Result{
{
Description: "Stderr from app container",
Path: "/docker/app/stderr.txt",
},
{
Description: "Stdout from app container",
Error: errors.New("Docker API 500"),
},
{
Description: "Logs from app container",
Path: "/docker/app.logs",
Error: errors.New("Timedout"),
},
},
}
slowResults := taskStub{
elapse: time.Second,
results: []*types.Result{
{
Description: "/usr/bin/free",
Path: "/host/commands/free",
},
},
}

ctx, _ := context.WithTimeout(context.Background(), 50*time.Millisecond)
results := Exec(ctx, "/dir", []types.Task{
nilResults,
noResults,
singleResults,
mixedResults,
slowResults,
})

assert.Len(t, results, 4)
assert.Contains(t, results, singleResults.results[0])
assert.Contains(t, results, mixedResults.results[0])
assert.Contains(t, results, mixedResults.results[1])
assert.Contains(t, results, mixedResults.results[2])
assert.NotContains(t, results, slowResults.results[0])
}
114 changes: 22 additions & 92 deletions bundle/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"io/ioutil"
"os"
"path/filepath"
"sync"
"time"

"github.com/pkg/errors"
Expand All @@ -16,121 +15,52 @@ import (
jww "github.com/spf13/jwalterweatherman"
)

type resultInfo struct {
Paths []string `json:"paths"`
Task string `json:"task"`
Args []string `json:"arguments"`
}

type errorInfo struct {
Task string `json:"task"`
Args []string `json:"arguments"`
Error string `json:"error"`
}

// Generate is called to start a new support bundle generation
func Generate(tasks []Task, timeout time.Duration) (string, error) {
var wg sync.WaitGroup

var resultMutex = &sync.Mutex{}
var allResultInfo []resultInfo
var allErrorInfo []errorInfo

wg.Add(len(tasks))

collectDir, err := ioutil.TempDir("", "support-bundle")
// Generate a new support bundle and write the results as an archive at pathname
func Generate(tasks []types.Task, timeout time.Duration, pathname string) error {
collectDir, err := ioutil.TempDir(filepath.Dir(pathname), "")
if err != nil {
err = errors.Wrap(err, "Creating a temporary directory to store results failed")
jww.ERROR.Print(err)
return "", err
return err
}
defer os.RemoveAll(collectDir)

ctx := context.Background()
defaultCtx, cancel := context.WithTimeout(ctx, timeout)
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()

for _, task := range tasks {
go func(task Task) {
var datas []types.Data
var result types.Result
var err error

if task.Timeout == 0 {
// use the default context for this task
datas, result, err = task.ExecFunc(defaultCtx, task.Args)
} else {
// use a unique context+timeout for this task
uniqueCtx, cancel := context.WithTimeout(ctx, task.Timeout)
defer cancel()
datas, result, err = task.ExecFunc(uniqueCtx, task.Args)
}
results := Exec(ctx, collectDir, tasks)

if err != nil {
jww.ERROR.Printf(err.Error() + "\n")
}
// any result that wrote a file, whether it has an error or not
var resultsWithOutput []*types.Result
// any result with an error, whether or not it wrote a file
var resultsWithError []*types.Result

for _, data := range datas {
dataFile := filepath.Join(collectDir, data.Filename)
if err := os.MkdirAll(filepath.Dir(dataFile), 0700); err != nil {
jww.ERROR.Print(err)
}
if err := ioutil.WriteFile(dataFile, data.Data, 0666); err != nil {
jww.ERROR.Print(err)
}
}

resultMutex.Lock()
allResultInfo = append(allResultInfo, resultInfo{
Paths: result.Filenames,
Task: result.Task,
Args: result.Args,
})
if result.Error != nil {
allErrorInfo = append(allErrorInfo, errorInfo{
Task: result.Task,
Args: result.Args,
Error: result.Error.Error(),
})
}
resultMutex.Unlock()

wg.Done()
}(task)
for _, r := range results {
if r.Path != "" {
resultsWithOutput = append(resultsWithOutput, r)
}
if r.Error != nil {
resultsWithError = append(resultsWithError, r)
}
}

wg.Wait()

//write index and error json files
indexJSON, err := json.MarshalIndent(allResultInfo, "", " ")
indexJSON, err := json.MarshalIndent(resultsWithOutput, "", " ")
if err != nil {
jww.ERROR.Print(err)
}
ioutil.WriteFile(filepath.Join(collectDir, "index.json"), indexJSON, 0666)

errorJSON, err := json.MarshalIndent(allErrorInfo, "", " ")
errorJSON, err := json.MarshalIndent(resultsWithError, "", " ")
if err != nil {
jww.ERROR.Print(err)
}
ioutil.WriteFile(filepath.Join(collectDir, "error.json"), errorJSON, 0666)

// Build the output tar file
archiveFile, err := ioutil.TempFile("", "support-bundle")
if err != nil {
err = errors.Wrap(err, "Creating a temporary file to compress results failed")
jww.ERROR.Print(err)
return "", err
}

comp := compressor.NewTgz()
comp.SetTarConfig(compressor.Tar{TruncateLongFiles: true})
if err := comp.Compress(collectDir, archiveFile.Name()); err != nil {
err = errors.Wrap(err, "Compressing results directory failed")
jww.ERROR.Print(err)
return "", err
if err := comp.Compress(collectDir, pathname); err != nil {
return errors.Wrap(err, "Compressing results directory failed")
}

jww.TRACE.Printf("Created support bundle at %q\n", archiveFile.Name())

return archiveFile.Name(), nil
return nil
}
33 changes: 33 additions & 0 deletions bundle/planner.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package bundle

import (
"strings"

"github.com/replicatedcom/support-bundle/types"
)

type Planner struct {
Plugins map[string]types.Plugin
}

func (p Planner) Plan(specs []types.Spec) []types.Task {
var tasks []types.Task

for _, spec := range specs {
parts := strings.Split(spec.Builtin, ".")
if len(parts) != 2 {
continue
}
plugin, ok := p.Plugins[parts[0]]
if !ok {
continue
}
planner, ok := plugin[parts[1]]
if !ok {
continue
}
tasks = append(tasks, planner(spec)...)
}

return tasks
}
15 changes: 0 additions & 15 deletions bundle/task.go

This file was deleted.

Loading