mirror of
https://github.com/replicatedhq/troubleshoot.git
synced 2026-09-03 00:47:17 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e862f0ba2 | ||
|
|
4651478368 |
@@ -94,6 +94,19 @@ jobs:
|
||||
name: support-bundle
|
||||
path: bin/support-bundle
|
||||
|
||||
compile-collect:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
- run: make generate collect
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: collect
|
||||
path: bin/collect
|
||||
|
||||
validate-supportbundle-e2e:
|
||||
runs-on: ubuntu-latest
|
||||
needs: compile-supportbundle
|
||||
@@ -114,7 +127,7 @@ jobs:
|
||||
# Additional e2e tests for support bundle that run in Go, these create a Kind cluster
|
||||
validate-supportbundle-e2e-go:
|
||||
runs-on: ubuntu-latest
|
||||
needs: compile-supportbundle
|
||||
needs: [compile-supportbundle, compile-collect]
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Download support bundle binary
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"syscall"
|
||||
|
||||
"github.com/replicatedhq/troubleshoot/internal/util"
|
||||
)
|
||||
|
||||
func checkAndSetChroot(newroot string) error {
|
||||
if newroot == "" {
|
||||
return nil
|
||||
}
|
||||
if !util.IsRunningAsRoot() {
|
||||
return errors.New("Can only chroot when run as root")
|
||||
}
|
||||
if err := syscall.Chroot(newroot); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"syscall"
|
||||
|
||||
"github.com/replicatedhq/troubleshoot/internal/util"
|
||||
)
|
||||
|
||||
func checkAndSetChroot(newroot string) error {
|
||||
if newroot == "" {
|
||||
return nil
|
||||
}
|
||||
if !util.IsRunningAsRoot() {
|
||||
return errors.New("Can only chroot when run as root")
|
||||
}
|
||||
if err := syscall.Chroot(newroot); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
func checkAndSetChroot(newroot string) error {
|
||||
return errors.New("chroot is only implimented in linux/darwin")
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/replicatedhq/troubleshoot/cmd/internal/util"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/k8sutil"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/logger"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
func RootCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "collect [url]",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Short: "Run a collector",
|
||||
Long: `Run a collector and output the results.`,
|
||||
SilenceUsage: true,
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
v := viper.GetViper()
|
||||
v.BindPFlags(cmd.Flags())
|
||||
|
||||
logger.SetupLogger(v)
|
||||
|
||||
if err := util.StartProfiling(); err != nil {
|
||||
klog.Errorf("Failed to start profiling: %v", err)
|
||||
}
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
v := viper.GetViper()
|
||||
|
||||
if err := checkAndSetChroot(v.GetString("chroot")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return runCollect(v, args[0])
|
||||
},
|
||||
PostRun: func(cmd *cobra.Command, args []string) {
|
||||
if err := util.StopProfiling(); err != nil {
|
||||
klog.Errorf("Failed to stop profiling: %v", err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
cobra.OnInitialize(initConfig)
|
||||
|
||||
cmd.AddCommand(util.VersionCmd())
|
||||
|
||||
cmd.Flags().StringSlice("redactors", []string{}, "names of the additional redactors to use")
|
||||
cmd.Flags().Bool("redact", true, "enable/disable default redactions")
|
||||
cmd.Flags().String("format", "json", "output format, one of json or raw.")
|
||||
cmd.Flags().String("collector-image", "", "the full name of the collector image to use")
|
||||
cmd.Flags().String("collector-pull-policy", "", "the pull policy of the collector image")
|
||||
cmd.Flags().String("selector", "", "selector (label query) to filter remote collection nodes on.")
|
||||
cmd.Flags().Bool("collect-without-permissions", false, "always generate a support bundle, even if it some require additional permissions")
|
||||
cmd.Flags().Bool("debug", false, "enable debug logging")
|
||||
cmd.Flags().String("chroot", "", "Chroot to path")
|
||||
|
||||
// hidden in favor of the `insecure-skip-tls-verify` flag
|
||||
cmd.Flags().Bool("allow-insecure-connections", false, "when set, do not verify TLS certs when retrieving spec and reporting results")
|
||||
cmd.Flags().MarkHidden("allow-insecure-connections")
|
||||
|
||||
viper.BindPFlags(cmd.Flags())
|
||||
|
||||
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
|
||||
|
||||
k8sutil.AddFlags(cmd.Flags())
|
||||
|
||||
// Initialize klog flags
|
||||
logger.InitKlogFlags(cmd)
|
||||
|
||||
// CPU and memory profiling flags
|
||||
util.AddProfilingFlags(cmd)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func InitAndExecute() {
|
||||
if err := RootCmd().Execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func initConfig() {
|
||||
viper.SetEnvPrefix("TROUBLESHOOT")
|
||||
viper.AutomaticEnv()
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/replicatedhq/troubleshoot/internal/util"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/client/troubleshootclientset/scheme"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/collect"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/docrewrite"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/k8sutil"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/specs"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/supportbundle"
|
||||
"github.com/spf13/viper"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultTimeout = 30 * time.Second
|
||||
)
|
||||
|
||||
func runCollect(v *viper.Viper, arg string) error {
|
||||
go func() {
|
||||
signalChan := make(chan os.Signal, 1)
|
||||
signal.Notify(signalChan, os.Interrupt)
|
||||
<-signalChan
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
var collectorContent []byte
|
||||
var err error
|
||||
if strings.HasPrefix(arg, "secret/") {
|
||||
// format secret/namespace-name/secret-name
|
||||
pathParts := strings.Split(arg, "/")
|
||||
if len(pathParts) != 3 {
|
||||
return errors.Errorf("path %s must have 3 components", arg)
|
||||
}
|
||||
|
||||
spec, err := specs.LoadFromSecret(pathParts[1], pathParts[2], "collect-spec")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to get spec from secret")
|
||||
}
|
||||
|
||||
collectorContent = spec
|
||||
} else if arg == "-" {
|
||||
b, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
collectorContent = b
|
||||
} else if _, err = os.Stat(arg); err == nil {
|
||||
b, err := os.ReadFile(arg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
collectorContent = b
|
||||
} else {
|
||||
if !util.IsURL(arg) {
|
||||
return fmt.Errorf("%s is not a URL and was not found", arg)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", arg, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("User-Agent", "Replicated_Collect/v1beta2")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
collectorContent = body
|
||||
}
|
||||
|
||||
collectorContent, err = docrewrite.ConvertToV1Beta2(collectorContent)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to convert to v1beta2")
|
||||
}
|
||||
|
||||
multidocs := strings.Split(string(collectorContent), "\n---\n")
|
||||
|
||||
decode := scheme.Codecs.UniversalDeserializer().Decode
|
||||
|
||||
redactors, err := supportbundle.GetRedactorsFromURIs(v.GetStringSlice("redactors"))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to get redactors")
|
||||
}
|
||||
|
||||
additionalRedactors := &troubleshootv1beta2.Redactor{
|
||||
Spec: troubleshootv1beta2.RedactorSpec{
|
||||
Redactors: redactors,
|
||||
},
|
||||
}
|
||||
|
||||
for i, additionalDoc := range multidocs {
|
||||
if i == 0 {
|
||||
continue
|
||||
}
|
||||
additionalDoc, err := docrewrite.ConvertToV1Beta2([]byte(additionalDoc))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to convert to v1beta2")
|
||||
}
|
||||
obj, _, err := decode(additionalDoc, nil, nil)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to parse additional doc %d", i)
|
||||
}
|
||||
multidocRedactors, ok := obj.(*troubleshootv1beta2.Redactor)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
additionalRedactors.Spec.Redactors = append(additionalRedactors.Spec.Redactors, multidocRedactors.Spec.Redactors...)
|
||||
}
|
||||
|
||||
// make sure we don't block any senders
|
||||
progressCh := make(chan interface{})
|
||||
defer close(progressCh)
|
||||
go func() {
|
||||
for range progressCh {
|
||||
}
|
||||
}()
|
||||
|
||||
restConfig, err := k8sutil.GetRESTConfig()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to convert kube flags to rest config")
|
||||
}
|
||||
|
||||
labelSelector, err := labels.Parse(v.GetString("selector"))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to parse selector")
|
||||
}
|
||||
|
||||
namespace := v.GetString("namespace")
|
||||
if namespace == "" {
|
||||
namespace = "default"
|
||||
}
|
||||
|
||||
timeout := v.GetDuration("request-timeout")
|
||||
if timeout == 0 {
|
||||
timeout = defaultTimeout
|
||||
}
|
||||
|
||||
createOpts := collect.CollectorRunOpts{
|
||||
CollectWithoutPermissions: v.GetBool("collect-without-permissions"),
|
||||
KubernetesRestConfig: restConfig,
|
||||
Image: v.GetString("collector-image"),
|
||||
PullPolicy: v.GetString("collector-pullpolicy"),
|
||||
LabelSelector: labelSelector.String(),
|
||||
Namespace: namespace,
|
||||
Timeout: timeout,
|
||||
ProgressChan: progressCh,
|
||||
}
|
||||
|
||||
// we only support HostCollector or RemoteCollector kinds.
|
||||
hostCollector, err := collect.ParseHostCollectorFromDoc([]byte(multidocs[0]))
|
||||
if err == nil {
|
||||
results, err := collect.CollectHost(hostCollector, additionalRedactors, createOpts)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to collect from host")
|
||||
}
|
||||
return showHostStdoutResults(v.GetString("format"), hostCollector.Name, results)
|
||||
}
|
||||
|
||||
remoteCollector, err := collect.ParseRemoteCollectorFromDoc([]byte(multidocs[0]))
|
||||
if err == nil {
|
||||
results, err := collect.CollectRemote(remoteCollector, additionalRedactors, createOpts)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to collect from remote host(s)")
|
||||
}
|
||||
return showRemoteStdoutResults(v.GetString("format"), remoteCollector.Name, results)
|
||||
}
|
||||
|
||||
return errors.New("failed to parse hostCollector or remoteCollector")
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/replicatedhq/troubleshoot/pkg/collect"
|
||||
)
|
||||
|
||||
const (
|
||||
// FormatJSON is intended for CLI output.
|
||||
FormatJSON = "json"
|
||||
|
||||
// FormatRaw is intended for consumption by a remote collector. Output is a
|
||||
// string of quoted JSON.
|
||||
FormatRaw = "raw"
|
||||
)
|
||||
|
||||
func showHostStdoutResults(format string, collectName string, results *collect.HostCollectResult) error {
|
||||
switch format {
|
||||
case FormatJSON:
|
||||
return showHostStdoutResultsJSON(collectName, results.AllCollectedData)
|
||||
case FormatRaw:
|
||||
return showHostStdoutResultsRaw(collectName, results.AllCollectedData)
|
||||
default:
|
||||
return errors.Errorf("unknown output format: %q", format)
|
||||
}
|
||||
}
|
||||
|
||||
func showRemoteStdoutResults(format string, collectName string, results *collect.RemoteCollectResult) error {
|
||||
switch format {
|
||||
case FormatJSON:
|
||||
return showRemoteStdoutResultsJSON(collectName, results.AllCollectedData)
|
||||
case FormatRaw:
|
||||
return errors.Errorf("raw format not supported for remote collectors")
|
||||
default:
|
||||
return errors.Errorf("unknown output format: %q", format)
|
||||
}
|
||||
}
|
||||
|
||||
func showHostStdoutResultsJSON(collectName string, results map[string][]byte) error {
|
||||
output := make(map[string]interface{})
|
||||
for file, collectorResult := range results {
|
||||
var collectedItems map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(collectorResult), &collectedItems); err != nil {
|
||||
return errors.Wrap(err, "failed to marshal collector results")
|
||||
}
|
||||
output[file] = collectedItems
|
||||
}
|
||||
|
||||
formatted, err := json.MarshalIndent(output, "", " ")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to convert output to json")
|
||||
}
|
||||
|
||||
fmt.Print(string(formatted))
|
||||
return nil
|
||||
}
|
||||
|
||||
// showHostStdoutResultsRaw outputs the collector output as a string of quoted json.
|
||||
func showHostStdoutResultsRaw(collectName string, results map[string][]byte) error {
|
||||
strData := map[string]string{}
|
||||
for k, v := range results {
|
||||
strData[k] = string(v)
|
||||
}
|
||||
formatted, err := json.MarshalIndent(strData, "", " ")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to convert output to json")
|
||||
}
|
||||
fmt.Print(string(formatted))
|
||||
return nil
|
||||
}
|
||||
|
||||
func showRemoteStdoutResultsJSON(collectName string, results map[string][]byte) error {
|
||||
type CollectorResult map[string]interface{}
|
||||
type NodeResult map[string]CollectorResult
|
||||
|
||||
var output = make(map[string]NodeResult)
|
||||
|
||||
for node, result := range results {
|
||||
var nodeResult map[string]string
|
||||
if err := json.Unmarshal(result, &nodeResult); err != nil {
|
||||
return errors.Wrap(err, "failed to marshal node results")
|
||||
}
|
||||
nr := make(NodeResult)
|
||||
for file, collectorResult := range nodeResult {
|
||||
var collectedItems map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(collectorResult), &collectedItems); err != nil {
|
||||
return errors.Wrap(err, "failed to marshal collector results")
|
||||
}
|
||||
nr[file] = collectedItems
|
||||
}
|
||||
output[node] = nr
|
||||
}
|
||||
|
||||
formatted, err := json.MarshalIndent(output, "", " ")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to convert output to json")
|
||||
}
|
||||
fmt.Print(string(formatted))
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/replicatedhq/troubleshoot/cmd/collect/cli"
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cli.InitAndExecute()
|
||||
}
|
||||
+25
-2
@@ -51,6 +51,29 @@ builds:
|
||||
- -installsuffix=netgo
|
||||
binary: support-bundle
|
||||
|
||||
- id: collect
|
||||
main: ./cmd/collect/main.go
|
||||
env: [CGO_ENABLED=0]
|
||||
goos: [linux, darwin, windows]
|
||||
goarch: [amd64, arm, arm64]
|
||||
ignore:
|
||||
- goos: windows
|
||||
goarch: arm
|
||||
ldflags:
|
||||
- -s -w
|
||||
- -X github.com/replicatedhq/troubleshoot/pkg/version.version={{ .Version }}
|
||||
- -X github.com/replicatedhq/troubleshoot/pkg/version.gitSHA={{ .Commit }}
|
||||
- -X github.com/replicatedhq/troubleshoot/pkg/version.buildTime={{ .Date }}
|
||||
- -extldflags "-static"
|
||||
flags:
|
||||
- -tags=netgo
|
||||
- -tags=containers_image_ostree_stub
|
||||
- -tags=exclude_graphdriver_devicemapper
|
||||
- -tags=exclude_graphdriver_btrfs
|
||||
- -tags=containers_image_openpgp
|
||||
- -installsuffix=netgo
|
||||
binary: collect
|
||||
|
||||
archives:
|
||||
- id: preflight
|
||||
ids: [preflight]
|
||||
@@ -135,7 +158,7 @@ dockers:
|
||||
ids:
|
||||
- support-bundle
|
||||
- preflight
|
||||
skip_push: true
|
||||
- collect
|
||||
- dockerfile: ./deploy/Dockerfile.troubleshoot
|
||||
image_templates:
|
||||
- "replicated/preflight:latest"
|
||||
@@ -145,7 +168,7 @@ dockers:
|
||||
ids:
|
||||
- support-bundle
|
||||
- preflight
|
||||
skip_push: true
|
||||
- collect
|
||||
|
||||
universal_binaries:
|
||||
- id: preflight-universal
|
||||
|
||||
@@ -7,6 +7,7 @@ RUN apt-get -qq update \
|
||||
|
||||
COPY support-bundle /troubleshoot/support-bundle
|
||||
COPY preflight /troubleshoot/preflight
|
||||
COPY collect /troubleshoot/collect
|
||||
|
||||
ENV PATH="/troubleshoot:${PATH}"
|
||||
|
||||
|
||||
@@ -448,12 +448,12 @@ func runRemoteHostCollectors(ctx context.Context, hostCollectors []*troubleshoot
|
||||
return err
|
||||
}
|
||||
|
||||
stdout, _, err := getExecOutputs(ctx, opts.KubernetesRestConfig, clientset, pod, specJSON)
|
||||
stdout, stderr, err := getExecOutputs(ctx, opts.KubernetesRestConfig, clientset, pod, specJSON)
|
||||
if err != nil {
|
||||
// span.SetStatus(codes.Error, err.Error())
|
||||
msg := fmt.Sprintf("[%s] Error: %v", collector.Title(), err)
|
||||
opts.CollectorProgressCallback(opts.ProgressChan, msg)
|
||||
return errors.Wrap(err, "failed to run remote host collector")
|
||||
return errors.Wrapf(err, "failed to run remote host collector: %s", string(stderr))
|
||||
}
|
||||
|
||||
result := map[string]string{}
|
||||
|
||||
Reference in New Issue
Block a user