Files
troubleshoot/pkg/supportbundle/collect.go
Ethan MosbaughandClaude Sonnet 4.6 48c45d7b0a refactor: replace SPDY executor with WebSocket-first fallback executor (#2031)
* docs: add design spec for SPDY executor removal

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs: add implementation plan for SPDY executor removal

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(k8sutil): add NewFallbackExecutor helper

* fix(k8sutil): rename url param to u to avoid shadowing net/url

* refactor(collect): use fallback executor in exec collector

* refactor(collect): use fallback executor in copy collector

* refactor(collect): use fallback executor in copy_from_host collector

* refactor(collect): use fallback executor in sonobuoy_results collector

* refactor(collect): use fallback executor in etcd collector

* refactor(collect): use fallback executor in longhorn collector

* refactor(supportbundle): use fallback executor in collect

* chore(k8sutil): delete unused PortForward function

* fix(k8sutil): use GET for WebSocket executor and broaden fallback predicate

WebSocket upgrade requires GET per RFC 6455. Also add IsHTTPSProxyError to
the fallback predicate so HTTPS proxy environments fall back to SPDY correctly,
matching kubectl's implementation. Remove method param from NewFallbackExecutor
since the methods are now transport-specific and not caller-controlled.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: remove superpowers docs artifacts

* fix(collect): enable stdout capture in exec collector

* fix(collect): enable stdout capture in copy collectors

PodExecOptions.Stdout: false causes the WebSocket API server to discard
stdout entirely, silently breaking tar output for file copy operations.
With SPDY this mismatch was harmless; WebSocket strictly respects the field.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 06:21:42 -07:00

722 lines
23 KiB
Go

package supportbundle
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"reflect"
"strings"
"sync"
"time"
"github.com/pkg/errors"
analyze "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/redact"
"github.com/replicatedhq/troubleshoot/pkg/version"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"golang.org/x/sync/errgroup"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
kuberneteserrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/storage/names"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/remotecommand"
"k8s.io/klog/v2"
"k8s.io/utils/ptr"
)
const (
selectorLabelKey = "ds-selector-label"
defaultTimeout = 30
)
func runHostCollectors(ctx context.Context, hostCollectors []*troubleshootv1beta2.HostCollect, additionalRedactors *troubleshootv1beta2.Redactor, bundlePath string, opts SupportBundleCreateOpts) (collect.CollectorResult, error) {
var err error
var collectResult map[string][]byte
if opts.RunHostCollectorsInPod {
collectResult, err = runRemoteHostCollectors(ctx, hostCollectors, bundlePath, opts)
if err != nil {
return collectResult, err
}
} else {
collectResult = runLocalHostCollectors(ctx, hostCollectors, bundlePath, opts)
}
// redact result if any
globalRedactors := []*troubleshootv1beta2.Redact{}
if additionalRedactors != nil {
globalRedactors = additionalRedactors.Spec.Redactors
}
if opts.Redact {
// Enable tokenization if requested (safer than environment variables)
if opts.Tokenize {
redact.EnableTokenization()
defer redact.DisableTokenization() // Always cleanup, even on error
}
_, span := otel.Tracer(constants.LIB_TRACER_NAME).Start(ctx, "Host collectors")
span.SetAttributes(attribute.String("type", "Redactors"))
err := collect.RedactResult(bundlePath, collectResult, globalRedactors)
if err != nil {
err = errors.Wrap(err, "failed to redact host collector results")
span.SetStatus(codes.Error, err.Error())
return collectResult, err
}
span.End()
}
return collectResult, nil
}
func runCollectors(ctx context.Context, collectors []*troubleshootv1beta2.Collect, additionalRedactors *troubleshootv1beta2.Redactor, bundlePath string, opts SupportBundleCreateOpts) (collect.CollectorResult, error) {
var allCollectors []collect.Collector
var foundForbidden bool
collectSpecs := make([]*troubleshootv1beta2.Collect, 0)
collectSpecs = append(collectSpecs, collectors...)
collectSpecs = collect.EnsureCollectorInList(collectSpecs, troubleshootv1beta2.Collect{ClusterInfo: &troubleshootv1beta2.ClusterInfo{}})
collectSpecs = collect.EnsureCollectorInList(collectSpecs, troubleshootv1beta2.Collect{ClusterResources: &troubleshootv1beta2.ClusterResources{}})
collectSpecs = collect.DedupCollectors(collectSpecs)
collectSpecs = collect.EnsureClusterResourcesFirst(collectSpecs)
opts.KubernetesRestConfig.QPS = constants.DEFAULT_CLIENT_QPS
opts.KubernetesRestConfig.Burst = constants.DEFAULT_CLIENT_BURST
opts.KubernetesRestConfig.UserAgent = fmt.Sprintf("%s/%s", constants.DEFAULT_CLIENT_USER_AGENT, version.Version())
k8sClient, err := kubernetes.NewForConfig(opts.KubernetesRestConfig)
if err != nil {
return nil, errors.Wrap(err, "failed to instantiate Kubernetes client")
}
allCollectorsMap := make(map[reflect.Type][]collect.Collector)
collectorTypeOrder := make([]reflect.Type, 0) // Preserve order of collector types
allCollectedData := map[string][]byte{}
for _, desiredCollector := range collectSpecs {
if collectorInterface, ok := collect.GetCollector(desiredCollector, bundlePath, opts.Namespace, opts.KubernetesRestConfig, k8sClient, opts.SinceTime); ok {
if collector, ok := collectorInterface.(collect.Collector); ok {
err := collector.CheckRBAC(ctx, collector, desiredCollector, opts.KubernetesRestConfig, opts.Namespace)
if err != nil {
return nil, errors.Wrap(err, "failed to check RBAC for collectors")
}
collectorType := reflect.TypeOf(collector)
if _, exists := allCollectorsMap[collectorType]; !exists {
collectorTypeOrder = append(collectorTypeOrder, collectorType)
}
allCollectorsMap[collectorType] = append(allCollectorsMap[collectorType], collector)
}
}
}
// Iterate over collector types in the order they appeared in collectSpecs
for _, collectorType := range collectorTypeOrder {
collectors := allCollectorsMap[collectorType]
if mergeCollector, ok := collectors[0].(collect.MergeableCollector); ok {
mergedCollectors, err := mergeCollector.Merge(collectors)
if err != nil {
msg := fmt.Sprintf("failed to merge collector: %s: %s", mergeCollector.Title(), err)
opts.CollectorProgressCallback(opts.ProgressChan, msg)
}
allCollectors = append(allCollectors, mergedCollectors...)
} else {
allCollectors = append(allCollectors, collectors...)
}
foundForbidden = false
for _, collector := range collectors {
for _, e := range collector.GetRBACErrors() {
foundForbidden = true
opts.ProgressChan <- e
}
}
}
if foundForbidden && !opts.CollectWithoutPermissions {
return nil, collect.ErrInsufficientPermissionsToRun
}
// move Copy Collectors if any to the end of the execution list
allCollectors = collect.EnsureCopyLast(allCollectors)
var skippedCollectors []collect.SkippedCollector
for _, collector := range allCollectors {
_, span := otel.Tracer(constants.LIB_TRACER_NAME).Start(ctx, collector.Title())
span.SetAttributes(attribute.String("type", reflect.TypeOf(collector).String()))
isExcluded, _ := collector.IsExcluded()
if isExcluded {
msg := fmt.Sprintf("excluding %q collector", collector.Title())
opts.CollectorProgressCallback(opts.ProgressChan, msg)
span.SetAttributes(attribute.Bool(constants.EXCLUDED, true))
span.End()
skippedCollectors = append(skippedCollectors, collect.SkippedCollector{
Collector: collector.Title(),
Reason: "excluded",
Timestamp: time.Now().Format(time.RFC3339),
})
continue
}
// skip collectors with RBAC errors unless its the ClusterResources collector
if collector.HasRBACErrors() {
if _, ok := collector.(*collect.CollectClusterResources); !ok {
msg := fmt.Sprintf("skipping collector %q with insufficient RBAC permissions", collector.Title())
opts.CollectorProgressCallback(opts.ProgressChan, msg)
span.SetStatus(codes.Error, "skipping collector, insufficient RBAC permissions")
span.End()
rbacErrors := collector.GetRBACErrors()
errorMessages := make([]string, 0, len(rbacErrors))
for _, e := range rbacErrors {
errorMessages = append(errorMessages, e.Error())
}
skippedCollectors = append(skippedCollectors, collect.SkippedCollector{
Collector: collector.Title(),
Reason: "insufficient RBAC permissions",
Errors: errorMessages,
Timestamp: time.Now().Format(time.RFC3339),
})
continue
}
}
opts.CollectorProgressCallback(opts.ProgressChan, collector.Title())
result, err := collector.Collect(opts.ProgressChan)
if err != nil {
span.SetStatus(codes.Error, err.Error())
opts.ProgressChan <- errors.Errorf("failed to run collector: %s: %v", collector.Title(), err)
// Save collector error to bundle (write to disk)
errorInfo := map[string]string{
"collector": collector.Title(),
"error": err.Error(),
"timestamp": time.Now().Format(time.RFC3339),
}
if errorJSON, marshalErr := json.Marshal(errorInfo); marshalErr == nil {
errorPath := fmt.Sprintf("collector-errors/%s-error.json", collector.Title())
// Always store bytes in-memory for consistency with memory-only bundles
allCollectedData[errorPath] = errorJSON
// Also attempt to persist to disk best-effort
if writeErr := os.MkdirAll(filepath.Join(bundlePath, "collector-errors"), 0755); writeErr == nil {
_ = os.WriteFile(filepath.Join(bundlePath, errorPath), errorJSON, 0644)
}
}
}
for k, v := range result {
allCollectedData[k] = v
}
span.End()
}
// Write skipped collectors manifest to the bundle so users can see what was missed
collect.WriteSkippedCollectors(skippedCollectors, allCollectedData, bundlePath)
collectResult := allCollectedData
globalRedactors := []*troubleshootv1beta2.Redact{}
if additionalRedactors != nil {
globalRedactors = additionalRedactors.Spec.Redactors
}
if opts.Redact {
// Enable tokenization if requested (safer than environment variables)
if opts.Tokenize {
redact.EnableTokenization()
defer redact.DisableTokenization() // Always cleanup, even on error
}
// TODO: Should we record how long each redactor takes?
_, span := otel.Tracer(constants.LIB_TRACER_NAME).Start(ctx, "In-cluster collectors")
span.SetAttributes(attribute.String("type", "Redactors"))
err := collect.RedactResult(bundlePath, collectResult, globalRedactors)
if err != nil {
err := errors.Wrap(err, "failed to redact in cluster collector results")
span.SetStatus(codes.Error, err.Error())
span.End()
return collectResult, err
}
span.End()
}
return collectResult, nil
}
func findFileName(basename, extension string) (string, error) {
n := 1
name := basename
for {
filename := name + "." + extension
if _, err := os.Stat(filename); os.IsNotExist(err) {
return filename, nil
} else if err != nil {
return "", errors.Wrap(err, "check file exists")
}
name = fmt.Sprintf("%s (%d)", basename, n)
n = n + 1
}
}
func getAnalysisFile(analyzeResults []*analyze.AnalyzeResult) (io.Reader, error) {
data := convert.FromAnalyzerResult(analyzeResults)
analysis, err := json.MarshalIndent(data, "", " ")
if err != nil {
return nil, errors.Wrap(err, "failed to marshal analysis")
}
return bytes.NewBuffer(analysis), nil
}
func runLocalHostCollectors(ctx context.Context, hostCollectors []*troubleshootv1beta2.HostCollect, bundlePath string, opts SupportBundleCreateOpts) map[string][]byte {
collectSpecs := make([]*troubleshootv1beta2.HostCollect, 0)
collectSpecs = append(collectSpecs, hostCollectors...)
allCollectedData := make(map[string][]byte)
var collectors []collect.HostCollector
for _, desiredCollector := range collectSpecs {
collector, ok := collect.GetHostCollector(desiredCollector, bundlePath)
if ok {
collectors = append(collectors, collector)
}
}
for _, collector := range collectors {
// TODO: Add context to host collectors
_, span := otel.Tracer(constants.LIB_TRACER_NAME).Start(ctx, collector.Title())
span.SetAttributes(attribute.String("type", reflect.TypeOf(collector).String()))
isExcluded, _ := collector.IsExcluded()
if isExcluded {
opts.ProgressChan <- fmt.Sprintf("[%s] Excluding host collector", collector.Title())
span.SetAttributes(attribute.Bool(constants.EXCLUDED, true))
span.End()
continue
}
opts.ProgressChan <- fmt.Sprintf("[%s] Running host collector...", collector.Title())
result, err := collector.Collect(opts.ProgressChan)
if err != nil {
span.SetStatus(codes.Error, err.Error())
opts.ProgressChan <- errors.Errorf("failed to run host collector: %s: %v", collector.Title(), err)
// Save collector error to bundle (write to disk)
errorInfo := map[string]string{
"collector": collector.Title(),
"error": err.Error(),
"timestamp": time.Now().Format(time.RFC3339),
}
if errorJSON, marshalErr := json.Marshal(errorInfo); marshalErr == nil {
errorPath := fmt.Sprintf("host-collectors/errors/%s-error.json", collector.Title())
// Always store bytes in-memory for consistency with memory-only bundles
allCollectedData[errorPath] = errorJSON
// Also attempt to persist to disk best-effort
if mkErr := os.MkdirAll(filepath.Join(bundlePath, "host-collectors/errors"), 0755); mkErr == nil {
_ = os.WriteFile(filepath.Join(bundlePath, errorPath), errorJSON, 0644)
}
}
}
span.End()
for k, v := range result {
allCollectedData[k] = v
}
}
return allCollectedData
}
// getExecOutputs executes `collect -` with collector data passed to stdin and returns stdout, stderr and error
func getExecOutputs(
ctx context.Context, clientConfig *rest.Config, client *kubernetes.Clientset, pod corev1.Pod, collectorData []byte,
) ([]byte, []byte, error) {
container := pod.Spec.Containers[0].Name
req := client.CoreV1().RESTClient().Post().Resource("pods").Name(pod.Name).Namespace(pod.Namespace).SubResource("exec")
scheme := runtime.NewScheme()
if err := corev1.AddToScheme(scheme); err != nil {
return nil, nil, err
}
parameterCodec := runtime.NewParameterCodec(scheme)
req.VersionedParams(&corev1.PodExecOptions{
Command: []string{"/troubleshoot/collect", "-", "--chroot", "/host", "--format", "raw"},
Container: container,
Stdin: true,
Stdout: true,
Stderr: true,
TTY: false,
}, parameterCodec)
exec, err := k8sutil.NewFallbackExecutor(clientConfig, req.URL())
if err != nil {
return nil, nil, err
}
stdout := new(bytes.Buffer)
stderr := new(bytes.Buffer)
err = exec.StreamWithContext(ctx, remotecommand.StreamOptions{
Stdin: bytes.NewBuffer(collectorData),
Stdout: stdout,
Stderr: stderr,
Tty: false,
})
if err != nil {
return stdout.Bytes(), stderr.Bytes(), err
}
return stdout.Bytes(), stderr.Bytes(), nil
}
func runRemoteHostCollectors(ctx context.Context, hostCollectors []*troubleshootv1beta2.HostCollect, bundlePath string, opts SupportBundleCreateOpts) (map[string][]byte, error) {
output := collect.NewResult()
timeoutSec := getCollectTimeout(opts)
clientset, err := kubernetes.NewForConfig(opts.KubernetesRestConfig)
if err != nil {
return nil, err
}
// TODO: rbac check
// create remote pod for each node
labels := map[string]string{
"troubleshoot.sh/remote-collector": "true",
}
var mu sync.Mutex
nodeLogs := make(map[string]map[string][]byte)
ds, err := createHostCollectorDS(ctx, clientset, labels, "default")
if err != nil {
return nil, err
}
// wait for at least one pod to be scheduled
err = waitForDS(ctx, clientset, ds, timeoutSec)
if err != nil {
return nil, err
}
klog.V(2).Infof("Created Remote Host Collector Daemonset %s", ds.Name)
pods, err := clientset.CoreV1().Pods(ds.Namespace).List(ctx, metav1.ListOptions{
LabelSelector: selectorLabelKey + "=" + ds.Name,
// use the default logs collector timeout for now
TimeoutSeconds: ptr.To(int64(defaultTimeout)),
Limit: 0,
})
if err != nil {
return nil, err
}
var eg errgroup.Group
if err := saveNodeList(output, opts, bundlePath); err != nil {
return nil, err
}
for _, collectorSpec := range hostCollectors {
collector, ok := collect.GetHostCollector(collectorSpec, bundlePath)
if !ok {
opts.ProgressChan <- "Host collector not found"
continue
}
// Start a span for tracing
_, span := otel.Tracer(constants.LIB_TRACER_NAME).Start(ctx, collector.Title())
span.SetAttributes(attribute.String("type", "Collect"))
isExcluded, _ := collector.IsExcluded()
if isExcluded {
msg := fmt.Sprintf("[%s] Excluding host collector", collector.Title())
opts.CollectorProgressCallback(opts.ProgressChan, msg)
span.SetAttributes(attribute.Bool(constants.EXCLUDED, true))
span.End()
continue
}
// Send progress event: starting the collector
msg := fmt.Sprintf("[%s] Running host collector...", collector.Title())
opts.CollectorProgressCallback(opts.ProgressChan, msg)
// convert host collectors into a HostCollector spec
spec := createHostCollectorsSpec([]*troubleshootv1beta2.HostCollect{collectorSpec})
specJSON, err := json.Marshal(spec)
if err != nil {
return nil, err
}
klog.V(2).Infof("HostCollector spec: %s", specJSON)
for _, pod := range pods.Items {
eg.Go(func() error {
if err := waitForPodRunning(ctx, clientset, &pod, timeoutSec); err != nil {
return err
}
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.Wrapf(err, "failed to run remote host collector: %s", string(stderr))
}
result := map[string]string{}
if err := json.Unmarshal(stdout, &result); err != nil {
return err
}
// Send progress event: completed successfully
msg = fmt.Sprintf("[%s] Completed host collector", collector.Title())
opts.CollectorProgressCallback(opts.ProgressChan, msg)
// Aggregate the results
mu.Lock()
for file, data := range result {
if nodeLogs[pod.Spec.NodeName] == nil {
nodeLogs[pod.Spec.NodeName] = make(map[string][]byte)
}
nodeLogs[pod.Spec.NodeName][file] = []byte(data)
}
mu.Unlock()
return nil
})
}
err = eg.Wait()
if err != nil {
return nil, err
}
span.End()
}
klog.V(2).Infof("All remote host collectors completed")
defer func() {
// TODO:
// delete the config map
// delete the remote pods
// check if the daemonset still exists
if ds == nil || ds.Name == "" {
return
}
if err := clientset.AppsV1().DaemonSets(ds.Namespace).Delete(ctx, ds.Name, metav1.DeleteOptions{}); err != nil {
if kuberneteserrors.IsNotFound(err) {
klog.Errorf("Remote host collector daemonset %s not found", ds.Name)
} else {
klog.Errorf("Failed to delete remote host collector daemonset %s: %v", ds.Name, err)
}
return
}
}()
for node, logs := range nodeLogs {
for file, data := range logs {
// trim host-collectors/ prefix
file = strings.TrimPrefix(file, "host-collectors/")
err := output.SaveResult(bundlePath, fmt.Sprintf("host-collectors/%s/%s", node, file), bytes.NewBuffer(data))
if err != nil {
// TODO: error handling
return nil, err
}
}
}
return output, nil
}
func createHostCollectorsSpec(hostCollectors []*troubleshootv1beta2.HostCollect) *troubleshootv1beta2.HostCollector {
return &troubleshootv1beta2.HostCollector{
TypeMeta: metav1.TypeMeta{
APIVersion: "troubleshoot.sh/v1beta2",
Kind: "HostCollector",
},
ObjectMeta: metav1.ObjectMeta{
Name: "remoteHostCollector",
},
Spec: troubleshootv1beta2.HostCollectorSpec{
Collectors: hostCollectors,
},
}
}
func createHostCollectorDS(ctx context.Context, clientset kubernetes.Interface, labels map[string]string, ns string) (*appsv1.DaemonSet, error) {
dsName := names.SimpleNameGenerator.GenerateName("remote-host-collector" + "-")
imageName := "replicated/troubleshoot:latest"
imagePullPolicy := corev1.PullIfNotPresent
labels[selectorLabelKey] = dsName
podSpec := corev1.PodSpec{
HostNetwork: true,
HostPID: true,
HostIPC: true,
Containers: []corev1.Container{
{
Image: imageName,
ImagePullPolicy: imagePullPolicy,
Name: "remote-collector",
Command: []string{"tail", "-f", "/dev/null"},
SecurityContext: &corev1.SecurityContext{
Privileged: ptr.To(true),
},
VolumeMounts: []corev1.VolumeMount{
{
Name: "host-root",
MountPath: "/host",
},
},
},
},
Volumes: []corev1.Volume{
{
Name: "host-root",
VolumeSource: corev1.VolumeSource{
HostPath: &corev1.HostPathVolumeSource{
Path: "/",
},
},
},
},
}
ds := &appsv1.DaemonSet{
ObjectMeta: metav1.ObjectMeta{
Name: dsName,
Namespace: ns,
Labels: labels,
},
Spec: appsv1.DaemonSetSpec{
Selector: &metav1.LabelSelector{
MatchLabels: labels,
MatchExpressions: []metav1.LabelSelectorRequirement{
{
Key: selectorLabelKey,
Operator: "In",
Values: []string{dsName},
},
},
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: labels,
},
Spec: podSpec,
},
},
}
createdDS, err := clientset.AppsV1().DaemonSets(ns).Create(ctx, ds, metav1.CreateOptions{})
if err != nil {
return nil, errors.Wrap(err, "failed to create Remote Host Collector Pod")
}
return createdDS, nil
}
func waitForPodRunning(ctx context.Context, clientset kubernetes.Interface, pod *corev1.Pod, timeoutSec int) error {
timeoutCh := time.After(time.Duration(timeoutSec) * time.Second)
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-timeoutCh:
return fmt.Errorf("timed out waiting for pod %s to be running", pod.Name)
case <-ticker.C:
currentPod, err := clientset.CoreV1().Pods(pod.Namespace).Get(ctx, pod.Name, metav1.GetOptions{})
if err != nil {
return fmt.Errorf("failed to get pod %s: %w", pod.Name, err)
}
// Check container status
for _, containerStatus := range currentPod.Status.ContainerStatuses {
if containerStatus.Name == "remote-collector" {
if containerStatus.State.Running != nil && containerStatus.State.Terminated == nil && containerStatus.Ready {
return nil
}
}
}
}
}
}
func waitForDS(ctx context.Context, clientset kubernetes.Interface, ds *appsv1.DaemonSet, timeoutSec int) error {
timeoutCh := time.After(time.Duration(timeoutSec) * time.Second)
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-timeoutCh:
return fmt.Errorf("timed out waiting for DaemonSet %s to be ready", ds.Name)
case <-ticker.C:
currentDS, err := clientset.AppsV1().DaemonSets(ds.Namespace).Get(ctx, ds.Name, metav1.GetOptions{})
if err != nil {
return fmt.Errorf("failed to get DaemonSet %s: %w", ds.Name, err)
}
if currentDS.Status.NumberReady > 0 && currentDS.Status.DesiredNumberScheduled == currentDS.Status.NumberReady {
return nil
}
}
}
}
func saveNodeList(result collect.CollectorResult, opts SupportBundleCreateOpts, bundlePath string) error {
clientset, err := kubernetes.NewForConfig(opts.KubernetesRestConfig)
if err != nil {
return errors.Wrap(err, "failed to create kubernetes clientset to run host collectors in pod")
}
nodeList, err := getNodeList(clientset, opts)
if err != nil {
return errors.Wrap(err, "failed to get remote node list")
}
nodeListBytes, err := json.MarshalIndent(nodeList, "", " ")
if err != nil {
return errors.Wrap(err, "failed to marshal remote node list")
}
err = result.SaveResult(bundlePath, constants.NODE_LIST_FILE, bytes.NewBuffer(nodeListBytes))
if err != nil {
return errors.Wrap(err, "failed to write remote node list")
}
return nil
}
func getCollectTimeout(opts SupportBundleCreateOpts) int {
if opts.RemoteHostCollectTimeoutSeconds > 0 {
return opts.RemoteHostCollectTimeoutSeconds
}
return defaultTimeout
}