Compare commits

...
Author SHA1 Message Date
Ethan Mosbaugh 3e862f0ba2 add back collect 2026-03-11 10:20:21 -07:00
Ethan Mosbaugh 4651478368 fix(ci): release docker image 2026-03-11 06:50:38 -07:00
11 changed files with 485 additions and 5 deletions
+14 -1
View File
@@ -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
+21
View File
@@ -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
}
+21
View File
@@ -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
}
+9
View File
@@ -0,0 +1,9 @@
package cli
import (
"errors"
)
func checkAndSetChroot(newroot string) error {
return errors.New("chroot is only implimented in linux/darwin")
}
+90
View File
@@ -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()
}
+189
View File
@@ -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")
}
+103
View File
@@ -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
}
+10
View File
@@ -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
View File
@@ -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
+1
View File
@@ -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}"
+2 -2
View File
@@ -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{}