From c7b84ad1e53ec154becaf42d01abc02035fdbf3b Mon Sep 17 00:00:00 2001 From: Diamon Wiggins <38189728+diamonwiggins@users.noreply.github.com> Date: Mon, 3 Oct 2022 13:53:05 -0400 Subject: [PATCH] Refactor in-clusters collectors to use struct per collector (#670) refactor in-clusters collectors to use struct per collector --- cmd/preflight/cli/upload_results.go | 4 +- cmd/troubleshoot/cli/run.go | 2 +- pkg/collect/ceph.go | 43 +++- pkg/collect/cluster_info.go | 20 +- pkg/collect/cluster_resources.go | 31 ++- pkg/collect/collectd.go | 48 +++- pkg/collect/collector.go | 370 +++++----------------------- pkg/collect/collector_test.go | 40 ++- pkg/collect/configmap.go | 39 ++- pkg/collect/configmap_test.go | 4 +- pkg/collect/copy.go | 45 +++- pkg/collect/copy_from_host.go | 58 +++-- pkg/collect/data.go | 27 +- pkg/collect/exec.go | 51 ++-- pkg/collect/host_http.go | 2 +- pkg/collect/http.go | 45 +++- pkg/collect/logs.go | 47 +++- pkg/collect/longhorn.go | 32 ++- pkg/collect/mysql.go | 29 ++- pkg/collect/postgres.go | 27 +- pkg/collect/rbac.go | 64 +++++ pkg/collect/redis.go | 27 +- pkg/collect/registry.go | 55 +++-- pkg/collect/run.go | 267 +++----------------- pkg/collect/run_pod.go | 233 ++++++++++++++++++ pkg/collect/secret.go | 39 ++- pkg/collect/secret_test.go | 4 +- pkg/collect/sysctl.go | 58 +++-- pkg/preflight/collect.go | 106 ++++---- pkg/supportbundle/collect.go | 110 ++++----- 30 files changed, 1100 insertions(+), 827 deletions(-) diff --git a/cmd/preflight/cli/upload_results.go b/cmd/preflight/cli/upload_results.go index dcf5793a..8720dbb9 100644 --- a/cmd/preflight/cli/upload_results.go +++ b/cmd/preflight/cli/upload_results.go @@ -32,10 +32,10 @@ func uploadResults(uri string, analyzeResults []*analyzerunner.AnalyzeResult) er return upload(uri, uploadPreflightResults) } -func uploadErrors(uri string, collectors collect.Collectors) error { +func uploadErrors(uri string, collectors []collect.Collector) error { errors := []*preflight.UploadPreflightError{} for _, collector := range collectors { - for _, e := range collector.RBACErrors { + for _, e := range collector.GetRBACErrors() { errors = append(errors, &preflight.UploadPreflightError{ Error: e.Error(), }) diff --git a/cmd/troubleshoot/cli/run.go b/cmd/troubleshoot/cli/run.go index a483e0bb..5dd0e14f 100644 --- a/cmd/troubleshoot/cli/run.go +++ b/cmd/troubleshoot/cli/run.go @@ -295,7 +295,7 @@ the %s Admin Console to begin analysis.` return nil } - fmt.Printf("%s\n", response.ArchivePath) + fmt.Printf("\n%s\n", response.ArchivePath) return nil } diff --git a/pkg/collect/ceph.go b/pkg/collect/ceph.go index 6f823d4a..18027bda 100644 --- a/pkg/collect/ceph.go +++ b/pkg/collect/ceph.go @@ -11,6 +11,7 @@ import ( troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" corev1 "k8s.io/api/core/v1" "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" ) const ( @@ -105,23 +106,41 @@ var CephCommands = []CephCommand{ }, } -func Ceph(c *Collector, cephCollector *troubleshootv1beta2.Ceph) (CollectorResult, error) { +type CollectCeph struct { + Collector *troubleshootv1beta2.Ceph + BundlePath string + Namespace string + ClientConfig *rest.Config + Client kubernetes.Interface + ctx context.Context + RBACErrors +} + +func (c *CollectCeph) Title() string { + return collectorTitleOrDefault(c.Collector.CollectorMeta, "Cluster Info") +} + +func (c *CollectCeph) IsExcluded() (bool, error) { + return isExcluded(c.Collector.Exclude) +} + +func (c *CollectCeph) Collect(progressChan chan<- interface{}) (CollectorResult, error) { ctx := context.TODO() - if cephCollector.Namespace == "" { - cephCollector.Namespace = DefaultCephNamespace + if c.Namespace == "" { + c.Namespace = DefaultCephNamespace } - pod, err := findRookCephToolsPod(ctx, c, cephCollector.Namespace) + pod, err := findRookCephToolsPod(ctx, c, c.Namespace) if err != nil { return nil, err } output := NewResult() for _, command := range CephCommands { - err := cephCommandExec(ctx, c, cephCollector, pod, command, output) + err := cephCommandExec(ctx, progressChan, c, c.Collector, pod, command, output) if err != nil { - pathPrefix := GetCephCollectorFilepath(cephCollector.CollectorName, cephCollector.Namespace) + pathPrefix := GetCephCollectorFilepath(c.Collector.CollectorName, c.Namespace) dstFileName := path.Join(pathPrefix, fmt.Sprintf("%s.%s-error", command.ID, command.Format)) output.SaveResult(c.BundlePath, dstFileName, strings.NewReader(err.Error())) } @@ -130,20 +149,24 @@ func Ceph(c *Collector, cephCollector *troubleshootv1beta2.Ceph) (CollectorResul return output, nil } -func cephCommandExec(ctx context.Context, c *Collector, cephCollector *troubleshootv1beta2.Ceph, pod *corev1.Pod, command CephCommand, output CollectorResult) error { +func cephCommandExec(ctx context.Context, progressChan chan<- interface{}, c *CollectCeph, cephCollector *troubleshootv1beta2.Ceph, pod *corev1.Pod, command CephCommand, output CollectorResult) error { timeout := cephCollector.Timeout if timeout == "" { timeout = command.DefaultTimeout } - execCollector := &troubleshootv1beta2.Exec{ + execSpec := &troubleshootv1beta2.Exec{ Selector: labelsToSelector(pod.Labels), Namespace: pod.Namespace, Command: command.Command, Args: command.Args, Timeout: timeout, } - results, err := Exec(c, execCollector) + + rbacErrors := c.GetRBACErrors() + execCollector := &CollectExec{execSpec, c.BundlePath, c.Namespace, c.ClientConfig, c.Client, c.ctx, rbacErrors} + + results, err := execCollector.Collect(progressChan) if err != nil { return errors.Wrap(err, "failed to exec command") } @@ -171,7 +194,7 @@ func cephCommandExec(ctx context.Context, c *Collector, cephCollector *troublesh return nil } -func findRookCephToolsPod(ctx context.Context, c *Collector, namespace string) (*corev1.Pod, error) { +func findRookCephToolsPod(ctx context.Context, c *CollectCeph, namespace string) (*corev1.Pod, error) { client, err := kubernetes.NewForConfig(c.ClientConfig) if err != nil { return nil, errors.Wrap(err, "failed to create kubernetes client") diff --git a/pkg/collect/cluster_info.go b/pkg/collect/cluster_info.go index 66b8680d..96b2c58d 100644 --- a/pkg/collect/cluster_info.go +++ b/pkg/collect/cluster_info.go @@ -6,8 +6,10 @@ import ( "path/filepath" "github.com/pkg/errors" + troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" "k8s.io/apimachinery/pkg/version" "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" ) type ClusterVersion struct { @@ -15,7 +17,23 @@ type ClusterVersion struct { String string `json:"string"` } -func ClusterInfo(c *Collector) (CollectorResult, error) { +type CollectClusterInfo struct { + Collector *troubleshootv1beta2.ClusterInfo + BundlePath string + Namespace string + ClientConfig *rest.Config + RBACErrors +} + +func (c *CollectClusterInfo) Title() string { + return collectorTitleOrDefault(c.Collector.CollectorMeta, "Cluster Info") +} + +func (c *CollectClusterInfo) IsExcluded() (bool, error) { + return isExcluded(c.Collector.Exclude) +} + +func (c *CollectClusterInfo) Collect(progressChan chan<- interface{}) (CollectorResult, error) { client, err := kubernetes.NewForConfig(c.ClientConfig) if err != nil { return nil, errors.Wrap(err, "Failed to create kubernetes clientset") diff --git a/pkg/collect/cluster_resources.go b/pkg/collect/cluster_resources.go index 15ae77b5..59c82947 100644 --- a/pkg/collect/cluster_resources.go +++ b/pkg/collect/cluster_resources.go @@ -34,7 +34,28 @@ import ( "github.com/replicatedhq/troubleshoot/pkg/k8sutil/discovery" ) -func ClusterResources(c *Collector, clusterResourcesCollector *troubleshootv1beta2.ClusterResources) (CollectorResult, error) { +type CollectClusterResources struct { + Collector *troubleshootv1beta2.ClusterResources + BundlePath string + Namespace string + ClientConfig *rest.Config + RBACErrors +} + +func (c *CollectClusterResources) Title() string { + return collectorTitleOrDefault(c.Collector.CollectorMeta, "Cluster Resources") +} + +func (c *CollectClusterResources) IsExcluded() (bool, error) { + return isExcluded(c.Collector.Exclude) +} + +func (c *CollectClusterResources) Merge(allCollectors []Collector) ([]Collector, error) { + result := append(allCollectors, c) + return result, nil +} + +func (c *CollectClusterResources) Collect(progressChan chan<- interface{}) (CollectorResult, error) { client, err := kubernetes.NewForConfig(c.ClientConfig) if err != nil { return nil, err @@ -51,9 +72,9 @@ func ClusterResources(c *Collector, clusterResourcesCollector *troubleshootv1bet // namespaces nsListedFromCluster := false var namespaceNames []string - if len(clusterResourcesCollector.Namespaces) > 0 { - namespaces, namespaceErrors := getNamespaces(ctx, client, clusterResourcesCollector.Namespaces) - namespaceNames = clusterResourcesCollector.Namespaces + if len(c.Collector.Namespaces) > 0 { + namespaces, namespaceErrors := getNamespaces(ctx, client, c.Collector.Namespaces) + namespaceNames = c.Collector.Namespaces output.SaveResult(c.BundlePath, "cluster-resources/namespaces.json", bytes.NewBuffer(namespaces)) output.SaveResult(c.BundlePath, "cluster-resources/namespaces-errors.json", marshalErrors(namespaceErrors)) } else if c.Namespace != "" { @@ -82,7 +103,7 @@ func ClusterResources(c *Collector, clusterResourcesCollector *troubleshootv1bet } output.SaveResult(c.BundlePath, "cluster-resources/auth-cani-list-errors.json", marshalErrors(reviewStatusErrors)) - if nsListedFromCluster && !clusterResourcesCollector.IgnoreRBAC { + if nsListedFromCluster && !c.Collector.IgnoreRBAC { filteredNamespaces := []string{} for _, ns := range namespaceNames { status := reviewStatuses[ns] diff --git a/pkg/collect/collectd.go b/pkg/collect/collectd.go index 752356c1..35f61bb9 100644 --- a/pkg/collect/collectd.go +++ b/pkg/collect/collectd.go @@ -5,19 +5,41 @@ import ( troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" "k8s.io/client-go/kubernetes" - restclient "k8s.io/client-go/rest" + "k8s.io/client-go/rest" ) -func Collectd(ctx context.Context, c *Collector, collector *troubleshootv1beta2.Collectd, namespace string, clientConfig *restclient.Config, client kubernetes.Interface) (CollectorResult, error) { - copyFromHost := &troubleshootv1beta2.CopyFromHost{ - CollectorMeta: collector.CollectorMeta, - Name: "collectd/rrd", - Namespace: collector.Namespace, - Image: collector.Image, - ImagePullPolicy: collector.ImagePullPolicy, - ImagePullSecret: collector.ImagePullSecret, - Timeout: collector.Timeout, - HostPath: collector.HostPath, - } - return CopyFromHost(ctx, c, copyFromHost, namespace, clientConfig, client) +type CollectCollectd struct { + Collector *troubleshootv1beta2.Collectd + BundlePath string + Namespace string + ClientConfig *rest.Config + Client kubernetes.Interface + ctx context.Context + RBACErrors +} + +func (c *CollectCollectd) Title() string { + return collectorTitleOrDefault(c.Collector.CollectorMeta, "CollectD") +} + +func (c *CollectCollectd) IsExcluded() (bool, error) { + return isExcluded(c.Collector.Exclude) +} + +func (c *CollectCollectd) Collect(progressChan chan<- interface{}) (CollectorResult, error) { + copyFromHost := &troubleshootv1beta2.CopyFromHost{ + CollectorMeta: c.Collector.CollectorMeta, + Name: "collectd/rrd", + Namespace: c.Collector.Namespace, + Image: c.Collector.Image, + ImagePullPolicy: c.Collector.ImagePullPolicy, + ImagePullSecret: c.Collector.ImagePullSecret, + Timeout: c.Collector.Timeout, + HostPath: c.Collector.HostPath, + } + + rbacErrors := c.GetRBACErrors() + copyFromHostCollector := &CollectCopyFromHost{copyFromHost, c.BundlePath, c.Namespace, c.ClientConfig, c.Client, c.ctx, rbacErrors} + + return copyFromHostCollector.Collect(progressChan) } diff --git a/pkg/collect/collector.go b/pkg/collect/collector.go index f887a704..c7140dc5 100644 --- a/pkg/collect/collector.go +++ b/pkg/collect/collector.go @@ -2,29 +2,31 @@ package collect import ( "context" - "runtime" "strconv" + "time" "github.com/pkg/errors" troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" - "github.com/replicatedhq/troubleshoot/pkg/k8sutil" "github.com/replicatedhq/troubleshoot/pkg/multitype" - authorizationv1 "k8s.io/api/authorization/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" ) -type Collector struct { - Collect *troubleshootv1beta2.Collect - Redact bool - RBACErrors []error - ClientConfig *rest.Config - Namespace string - BundlePath string +type Collector interface { + Title() string + IsExcluded() (bool, error) + GetRBACErrors() []error + HasRBACErrors() bool + CheckRBAC(ctx context.Context, c Collector, collector *troubleshootv1beta2.Collect, clientConfig *rest.Config, namespace string) error + Collect(progressChan chan<- interface{}) (CollectorResult, error) } -type Collectors []*Collector +type MergeableCollector interface { + Collector + Merge(allCollectors []Collector) ([]Collector, error) +} + +//type Collectors []*Collector func isExcluded(excludeVal *multitype.BoolOrString) (bool, error) { if excludeVal == nil { @@ -47,305 +49,61 @@ func isExcluded(excludeVal *multitype.BoolOrString) (bool, error) { return parsed, nil } -// checks if a given collector has a spec with 'exclude' that evaluates to true. -func (c *Collector) IsExcluded() bool { - if c.Collect.ClusterInfo != nil { - isExcludedResult, err := isExcluded(c.Collect.ClusterInfo.Exclude) - if err != nil { - return true - } - if isExcludedResult { - return true - } - } else if c.Collect.ClusterResources != nil { - isExcludedResult, err := isExcluded(c.Collect.ClusterResources.Exclude) - if err != nil { - return true - } - if isExcludedResult { - return true - } - } else if c.Collect.Secret != nil { - isExcludedResult, err := isExcluded(c.Collect.Secret.Exclude) - if err != nil { - return true - } - if isExcludedResult { - return true - } - } else if c.Collect.ConfigMap != nil { - isExcludedResult, err := isExcluded(c.Collect.ConfigMap.Exclude) - if err != nil { - return true - } - if isExcludedResult { - return true - } - } else if c.Collect.Logs != nil { - isExcludedResult, err := isExcluded(c.Collect.Logs.Exclude) - if err != nil { - return true - } - if isExcludedResult { - return true - } - } else if c.Collect.Run != nil { - isExcludedResult, err := isExcluded(c.Collect.Run.Exclude) - if err != nil { - return true - } - if isExcludedResult { - return true - } - } else if c.Collect.RunPod != nil { - isExcludedResult, err := isExcluded(c.Collect.RunPod.Exclude) - if err != nil { - return true - } - if isExcludedResult { - return true - } - } else if c.Collect.Exec != nil { - isExcludedResult, err := isExcluded(c.Collect.Exec.Exclude) - if err != nil { - return true - } - if isExcludedResult { - return true - } - } else if c.Collect.Data != nil { - isExcludedResult, err := isExcluded(c.Collect.Data.Exclude) - if err != nil { - return true - } - if isExcludedResult { - return true - } - } else if c.Collect.Copy != nil { - isExcludedResult, err := isExcluded(c.Collect.Copy.Exclude) - if err != nil { - return true - } - if isExcludedResult { - return true - } - } else if c.Collect.CopyFromHost != nil { - isExcludedResult, err := isExcluded(c.Collect.CopyFromHost.Exclude) - if err != nil { - return true - } - if isExcludedResult { - return true - } - } else if c.Collect.HTTP != nil { - isExcludedResult, err := isExcluded(c.Collect.HTTP.Exclude) - if err != nil { - return true - } - if isExcludedResult { - return true - } - } else if c.Collect.Postgres != nil { - isExcludedResult, err := isExcluded(c.Collect.Postgres.Exclude) - if err != nil { - return true - } - if isExcludedResult { - return true - } - } else if c.Collect.Mysql != nil { - isExcludedResult, err := isExcluded(c.Collect.Mysql.Exclude) - if err != nil { - return true - } - if isExcludedResult { - return true - } - } else if c.Collect.Redis != nil { - isExcludedResult, err := isExcluded(c.Collect.Redis.Exclude) - if err != nil { - return true - } - if isExcludedResult { - return true - } - } else if c.Collect.Collectd != nil { - // TODO: see if redaction breaks these - isExcludedResult, err := isExcluded(c.Collect.Collectd.Exclude) - if err != nil { - return true - } - if isExcludedResult { - return true - } - } else if c.Collect.Ceph != nil { - isExcludedResult, err := isExcluded(c.Collect.Ceph.Exclude) - if err != nil { - return true - } - if isExcludedResult { - return true - } - } else if c.Collect.Longhorn != nil { - isExcludedResult, err := isExcluded(c.Collect.Longhorn.Exclude) - if err != nil { - return true - } - if isExcludedResult { - return true - } - } else if c.Collect.Sysctl != nil { - isExcludedResult, err := isExcluded(c.Collect.Sysctl.Exclude) - if err != nil { - return true - } - if isExcludedResult { - return true - } - } - - return false -} - -func (c *Collector) RunCollectorSync(clientConfig *rest.Config, client kubernetes.Interface, globalRedactors []*troubleshootv1beta2.Redact) (result CollectorResult, err error) { - defer func() { - if r := recover(); r != nil { - _, file, line, _ := runtime.Caller(4) - err = errors.Errorf("recovered from panic at \"%s:%d\": %v", file, line, r) - } - }() - - if c.IsExcluded() { - return - } +func GetCollector(collector *troubleshootv1beta2.Collect, bundlePath string, namespace string, clientConfig *rest.Config, client kubernetes.Interface, sinceTime *time.Time) (interface{}, bool) { ctx := context.TODO() - if c.Collect.ClusterInfo != nil { - result, err = ClusterInfo(c) - } else if c.Collect.ClusterResources != nil { - result, err = ClusterResources(c, c.Collect.ClusterResources) - } else if c.Collect.Secret != nil { - result, err = Secret(ctx, c, c.Collect.Secret, client) - } else if c.Collect.ConfigMap != nil { - result, err = ConfigMap(ctx, c, c.Collect.ConfigMap, client) - } else if c.Collect.Logs != nil { - result, err = Logs(c, c.Collect.Logs) - } else if c.Collect.Run != nil { - result, err = Run(c, c.Collect.Run) - } else if c.Collect.RunPod != nil { - result, err = RunPod(c, c.Collect.RunPod) - } else if c.Collect.Exec != nil { - result, err = Exec(c, c.Collect.Exec) - } else if c.Collect.Data != nil { - result, err = Data(c, c.Collect.Data) - } else if c.Collect.Copy != nil { - result, err = Copy(c, c.Collect.Copy) - } else if c.Collect.CopyFromHost != nil { - namespace := c.Collect.CopyFromHost.Namespace - if namespace == "" && c.Namespace == "" { - kubeconfig := k8sutil.GetKubeconfig() - namespace, _, _ = kubeconfig.Namespace() - } else if namespace == "" { - namespace = c.Namespace - } - result, err = CopyFromHost(ctx, c, c.Collect.CopyFromHost, namespace, clientConfig, client) - } else if c.Collect.HTTP != nil { - result, err = HTTP(c, c.Collect.HTTP) - } else if c.Collect.Postgres != nil { - result, err = Postgres(c, c.Collect.Postgres) - } else if c.Collect.Mysql != nil { - result, err = Mysql(c, c.Collect.Mysql) - } else if c.Collect.Redis != nil { - result, err = Redis(c, c.Collect.Redis) - } else if c.Collect.Collectd != nil { - // TODO: see if redaction breaks these - namespace := c.Collect.Collectd.Namespace - if namespace == "" && c.Namespace == "" { - kubeconfig := k8sutil.GetKubeconfig() - namespace, _, _ = kubeconfig.Namespace() - } else if namespace == "" { - namespace = c.Namespace - } - result, err = Collectd(ctx, c, c.Collect.Collectd, namespace, clientConfig, client) - } else if c.Collect.Ceph != nil { - result, err = Ceph(c, c.Collect.Ceph) - } else if c.Collect.Longhorn != nil { - result, err = Longhorn(c, c.Collect.Longhorn) - } else if c.Collect.RegistryImages != nil { - result, err = Registry(c, c.Collect.RegistryImages) - } else if c.Collect.Sysctl != nil { - if c.Collect.Sysctl.Namespace == "" { - c.Collect.Sysctl.Namespace = c.Namespace - } - if c.Collect.Sysctl.Namespace == "" { - kubeconfig := k8sutil.GetKubeconfig() - namespace, _, _ := kubeconfig.Namespace() - c.Collect.Sysctl.Namespace = namespace - } - result, err = Sysctl(ctx, c, client, c.Collect.Sysctl) - } else { - err = errors.New("no spec found to run") - return - } - if err != nil { - return - } + var RBACErrors []error - if c.Redact { - err = RedactResult(c.BundlePath, result, globalRedactors) - err = errors.Wrap(err, "failed to redact") + switch { + case collector.ClusterInfo != nil: + return &CollectClusterInfo{collector.ClusterInfo, bundlePath, namespace, clientConfig, RBACErrors}, true + case collector.ClusterResources != nil: + return &CollectClusterResources{collector.ClusterResources, bundlePath, namespace, clientConfig, RBACErrors}, true + case collector.Secret != nil: + return &CollectSecret{collector.Secret, bundlePath, namespace, clientConfig, client, ctx, RBACErrors}, true + case collector.ConfigMap != nil: + return &CollectConfigMap{collector.ConfigMap, bundlePath, namespace, clientConfig, client, ctx, RBACErrors}, true + case collector.Logs != nil: + return &CollectLogs{collector.Logs, bundlePath, namespace, clientConfig, client, ctx, sinceTime, RBACErrors}, true + case collector.Run != nil: + return &CollectRun{collector.Run, bundlePath, namespace, clientConfig, client, ctx, RBACErrors}, true + case collector.RunPod != nil: + return &CollectRunPod{collector.RunPod, bundlePath, namespace, clientConfig, client, ctx, RBACErrors}, true + case collector.Exec != nil: + return &CollectExec{collector.Exec, bundlePath, namespace, clientConfig, client, ctx, RBACErrors}, true + case collector.Data != nil: + return &CollectData{collector.Data, bundlePath, namespace, clientConfig, client, ctx, RBACErrors}, true + case collector.Copy != nil: + return &CollectCopy{collector.Copy, bundlePath, namespace, clientConfig, client, ctx, RBACErrors}, true + case collector.CopyFromHost != nil: + return &CollectCopyFromHost{collector.CopyFromHost, bundlePath, namespace, clientConfig, client, ctx, RBACErrors}, true + case collector.HTTP != nil: + return &CollectHTTP{collector.HTTP, bundlePath, namespace, clientConfig, client, RBACErrors}, true + case collector.Postgres != nil: + return &CollectPostgres{collector.Postgres, bundlePath, namespace, clientConfig, client, ctx, RBACErrors}, true + case collector.Mysql != nil: + return &CollectMysql{collector.Mysql, bundlePath, namespace, clientConfig, client, ctx, RBACErrors}, true + case collector.Redis != nil: + return &CollectRedis{collector.Redis, bundlePath, namespace, clientConfig, client, ctx, RBACErrors}, true + case collector.Collectd != nil: + return &CollectCollectd{collector.Collectd, bundlePath, namespace, clientConfig, client, ctx, RBACErrors}, true + case collector.Ceph != nil: + return &CollectCeph{collector.Ceph, bundlePath, namespace, clientConfig, client, ctx, RBACErrors}, true + case collector.Longhorn != nil: + return &CollectLonghorn{collector.Longhorn, bundlePath, namespace, clientConfig, client, ctx, RBACErrors}, true + case collector.RegistryImages != nil: + return &CollectRegistry{collector.RegistryImages, bundlePath, namespace, clientConfig, client, ctx, RBACErrors}, true + case collector.Sysctl != nil: + return &CollectSysctl{collector.Sysctl, bundlePath, namespace, clientConfig, client, ctx, RBACErrors}, true + default: + return nil, false } - - return } -func (c *Collector) GetDisplayName() string { - return c.Collect.GetName() -} - -func (c *Collector) CheckRBAC(ctx context.Context) error { - if c.IsExcluded() { - return nil // excluded collectors require no permissions - } - - client, err := kubernetes.NewForConfig(c.ClientConfig) - if err != nil { - return errors.Wrap(err, "failed to create client from config") - } - - forbidden := make([]error, 0) - - specs := c.Collect.AccessReviewSpecs(c.Namespace) - for _, spec := range specs { - sar := &authorizationv1.SelfSubjectAccessReview{ - Spec: spec, - } - - resp, err := client.AuthorizationV1().SelfSubjectAccessReviews().Create(ctx, sar, metav1.CreateOptions{}) - if err != nil { - return errors.Wrap(err, "failed to run subject review") - } - - if !resp.Status.Allowed { // all other fields of Status are empty... - forbidden = append(forbidden, RBACError{ - DisplayName: c.GetDisplayName(), - Namespace: spec.ResourceAttributes.Namespace, - Resource: spec.ResourceAttributes.Resource, - Verb: spec.ResourceAttributes.Verb, - }) - } - } - c.RBACErrors = forbidden - - return nil -} - -func (cs Collectors) CheckRBAC(ctx context.Context) error { - for _, c := range cs { - if err := c.CheckRBAC(ctx); err != nil { - return errors.Wrap(err, "failed to check RBAC") - } - } - return nil +func collectorTitleOrDefault(meta troubleshootv1beta2.CollectorMeta, defaultTitle string) string { + if meta.CollectorName != "" { + return meta.CollectorName + } + return defaultTitle } diff --git a/pkg/collect/collector_test.go b/pkg/collect/collector_test.go index a6580d45..64d3a764 100644 --- a/pkg/collect/collector_test.go +++ b/pkg/collect/collector_test.go @@ -273,16 +273,26 @@ pwd=somethinggoeshere;`, for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { req := require.New(t) - c := &Collector{ - Collect: tt.Collect, - Redact: true, + + var result CollectorResult + + collector, _ := GetCollector(tt.Collect, "", "", nil, nil, nil) + regCollector, _ := collector.(Collector) + + if excluded, err := regCollector.IsExcluded(); !excluded { + req.NoError(err) + + result, err = regCollector.Collect(nil) + req.NoError(err) + + err = RedactResult("", result, tt.Redactors) + + req.NoError(err) } - got, err := c.RunCollectorSync(nil, nil, tt.Redactors) - req.NoError(err) // convert to string to make differences easier to see toString := map[string]string{} - for k, v := range got { + for k, v := range result { toString[k] = string(v) } req.EqualValues(tt.want, toString) @@ -332,16 +342,22 @@ pwd=somethinggoeshere;`, for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { req := require.New(t) - c := &Collector{ - Collect: tt.Collect, - Redact: false, + + var result CollectorResult + + collector, _ := GetCollector(tt.Collect, "", "", nil, nil, nil) + regCollector, _ := collector.(Collector) + + if excluded, err := regCollector.IsExcluded(); !excluded { + req.NoError(err) + + result, err = regCollector.Collect(nil) + req.NoError(err) } - got, err := c.RunCollectorSync(nil, nil, tt.Redactors) - req.NoError(err) // convert to string to make differences easier to see toString := map[string]string{} - for k, v := range got { + for k, v := range result { toString[k] = string(v) } req.EqualValues(tt.want, toString) diff --git a/pkg/collect/configmap.go b/pkg/collect/configmap.go index 70f5f14f..3e052616 100644 --- a/pkg/collect/configmap.go +++ b/pkg/collect/configmap.go @@ -14,6 +14,7 @@ import ( kuberneteserrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" ) type ConfigMapOutput struct { @@ -26,28 +27,46 @@ type ConfigMapOutput struct { Data map[string]string `json:"data,omitonempty"` } -func ConfigMap(ctx context.Context, c *Collector, configMapCollector *troubleshootv1beta2.ConfigMap, client kubernetes.Interface) (CollectorResult, error) { +type CollectConfigMap struct { + Collector *troubleshootv1beta2.ConfigMap + BundlePath string + Namespace string + ClientConfig *rest.Config + Client kubernetes.Interface + ctx context.Context + RBACErrors +} + +func (c *CollectConfigMap) Title() string { + return collectorTitleOrDefault(c.Collector.CollectorMeta, "ConfigMap") +} + +func (c *CollectConfigMap) IsExcluded() (bool, error) { + return isExcluded(c.Collector.Exclude) +} + +func (c *CollectConfigMap) Collect(progressChan chan<- interface{}) (CollectorResult, error) { output := NewResult() configMaps := []corev1.ConfigMap{} - if configMapCollector.Name != "" { - configMap, err := client.CoreV1().ConfigMaps(configMapCollector.Namespace).Get(ctx, configMapCollector.Name, metav1.GetOptions{}) + if c.Collector.Name != "" { + configMap, err := c.Client.CoreV1().ConfigMaps(c.Collector.Namespace).Get(c.ctx, c.Collector.Name, metav1.GetOptions{}) if err != nil { if kuberneteserrors.IsNotFound(err) { - filePath, encoded, err := configMapToOutput(configMapCollector, nil, configMapCollector.Name) + filePath, encoded, err := configMapToOutput(c.Collector, nil, c.Collector.Name) if err != nil { - return output, errors.Wrapf(err, "collect secret %s", configMapCollector.Name) + return output, errors.Wrapf(err, "collect secret %s", c.Collector.Name) } output.SaveResult(c.BundlePath, filePath, bytes.NewBuffer(encoded)) } - output.SaveResult(c.BundlePath, GetConfigMapErrorsFileName(configMapCollector), marshalErrors([]string{err.Error()})) + output.SaveResult(c.BundlePath, GetConfigMapErrorsFileName(c.Collector), marshalErrors([]string{err.Error()})) return output, nil } configMaps = append(configMaps, *configMap) - } else if len(configMapCollector.Selector) > 0 { - cms, err := listConfigMapsForSelector(ctx, client, configMapCollector.Namespace, configMapCollector.Selector) + } else if len(c.Collector.Selector) > 0 { + cms, err := listConfigMapsForSelector(c.ctx, c.Client, c.Collector.Namespace, c.Collector.Selector) if err != nil { - output.SaveResult(c.BundlePath, GetConfigMapErrorsFileName(configMapCollector), marshalErrors([]string{err.Error()})) + output.SaveResult(c.BundlePath, GetConfigMapErrorsFileName(c.Collector), marshalErrors([]string{err.Error()})) return output, nil } configMaps = append(configMaps, cms...) @@ -56,7 +75,7 @@ func ConfigMap(ctx context.Context, c *Collector, configMapCollector *troublesho } for _, configMap := range configMaps { - filePath, encoded, err := configMapToOutput(configMapCollector, &configMap, configMap.Name) + filePath, encoded, err := configMapToOutput(c.Collector, &configMap, configMap.Name) if err != nil { return output, errors.Wrapf(err, "collect configMap %s", configMap.Name) } diff --git a/pkg/collect/configmap_test.go b/pkg/collect/configmap_test.go index 8678eb2a..0df7e240 100644 --- a/pkg/collect/configmap_test.go +++ b/pkg/collect/configmap_test.go @@ -328,8 +328,8 @@ func TestConfigMap(t *testing.T) { _, err := client.CoreV1().ConfigMaps(configMap.Namespace).Create(ctx, &configMap, metav1.CreateOptions{}) require.NoError(t, err) } - c := &Collector{} - got, err := ConfigMap(ctx, c, tt.configMapCollector, client) + configMapCollector := &CollectConfigMap{tt.configMapCollector, "", "", nil, client, ctx, nil} + got, err := configMapCollector.Collect(nil) if tt.wantErr { assert.Error(t, err) } else { diff --git a/pkg/collect/copy.go b/pkg/collect/copy.go index 71aa76bd..db270a47 100644 --- a/pkg/collect/copy.go +++ b/pkg/collect/copy.go @@ -15,12 +15,31 @@ import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" restclient "k8s.io/client-go/rest" "k8s.io/client-go/tools/remotecommand" ) +type CollectCopy struct { + Collector *troubleshootv1beta2.Copy + BundlePath string + Namespace string + ClientConfig *rest.Config + Client kubernetes.Interface + ctx context.Context + RBACErrors +} + +func (c *CollectCopy) Title() string { + return collectorTitleOrDefault(c.Collector.CollectorMeta, "Copy") +} + +func (c *CollectCopy) IsExcluded() (bool, error) { + return isExcluded(c.Collector.Exclude) +} + // Copy function gets a file or folder from a container specified in the specs. -func Copy(c *Collector, copyCollector *troubleshootv1beta2.Copy) (CollectorResult, error) { +func (c *CollectCopy) Collect(progressChan chan<- interface{}) (CollectorResult, error) { client, err := kubernetes.NewForConfig(c.ClientConfig) if err != nil { return nil, err @@ -30,40 +49,40 @@ func Copy(c *Collector, copyCollector *troubleshootv1beta2.Copy) (CollectorResul ctx := context.Background() - pods, podsErrors := listPodsInSelectors(ctx, client, copyCollector.Namespace, copyCollector.Selector) + pods, podsErrors := listPodsInSelectors(ctx, client, c.Collector.Namespace, c.Collector.Selector) if len(podsErrors) > 0 { - output.SaveResult(c.BundlePath, getCopyErrosFileName(copyCollector), marshalErrors(podsErrors)) + output.SaveResult(c.BundlePath, getCopyErrosFileName(c.Collector), marshalErrors(podsErrors)) } if len(pods) > 0 { for _, pod := range pods { containerName := pod.Spec.Containers[0].Name - if copyCollector.ContainerName != "" { - containerName = copyCollector.ContainerName + if c.Collector.ContainerName != "" { + containerName = c.Collector.ContainerName } - subPath := filepath.Join(copyCollector.Name, pod.Namespace, pod.Name, copyCollector.ContainerName) + subPath := filepath.Join(c.Collector.Name, pod.Namespace, pod.Name, c.Collector.ContainerName) - copyCollector.ExtractArchive = true // TODO: existing regression. this flag is always ignored and this matches current behaviour + c.Collector.ExtractArchive = true // TODO: existing regression. this flag is always ignored and this matches current behaviour copyErrors := map[string]string{} - dstPath := filepath.Join(c.BundlePath, subPath, filepath.Dir(copyCollector.ContainerPath)) - files, stderr, err := copyFilesFromPod(ctx, dstPath, c.ClientConfig, client, pod.Name, containerName, pod.Namespace, copyCollector.ContainerPath, copyCollector.ExtractArchive) + dstPath := filepath.Join(c.BundlePath, subPath, filepath.Dir(c.Collector.ContainerPath)) + files, stderr, err := copyFilesFromPod(ctx, dstPath, c.ClientConfig, client, pod.Name, containerName, pod.Namespace, c.Collector.ContainerPath, c.Collector.ExtractArchive) if err != nil { - copyErrors[filepath.Join(copyCollector.ContainerPath, "error")] = err.Error() + copyErrors[filepath.Join(c.Collector.ContainerPath, "error")] = err.Error() if len(stderr) > 0 { - copyErrors[filepath.Join(copyCollector.ContainerPath, "stderr")] = string(stderr) + copyErrors[filepath.Join(c.Collector.ContainerPath, "stderr")] = string(stderr) } - key := filepath.Join(subPath, copyCollector.ContainerPath+"-errors.json") + key := filepath.Join(subPath, c.Collector.ContainerPath+"-errors.json") output.SaveResult(c.BundlePath, key, marshalErrors(copyErrors)) continue } for k, v := range files { - output[filepath.Join(subPath, filepath.Dir(copyCollector.ContainerPath), k)] = v + output[filepath.Join(subPath, filepath.Dir(c.Collector.ContainerPath), k)] = v } } } diff --git a/pkg/collect/copy_from_host.go b/pkg/collect/copy_from_host.go index 47d9c85d..3139215a 100644 --- a/pkg/collect/copy_from_host.go +++ b/pkg/collect/copy_from_host.go @@ -11,6 +11,7 @@ import ( "github.com/pkg/errors" troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" + "github.com/replicatedhq/troubleshoot/pkg/k8sutil" "github.com/replicatedhq/troubleshoot/pkg/logger" "github.com/segmentio/ksuid" appsv1 "k8s.io/api/apps/v1" @@ -20,19 +21,40 @@ import ( "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" restclient "k8s.io/client-go/rest" "k8s.io/client-go/tools/remotecommand" ) -// CopyFromHost is a function that copies a file or directory from a host or hosts to include in the bundle. -func CopyFromHost(ctx context.Context, c *Collector, collector *troubleshootv1beta2.CopyFromHost, namespace string, clientConfig *restclient.Config, client kubernetes.Interface) (CollectorResult, error) { +type CollectCopyFromHost struct { + Collector *troubleshootv1beta2.CopyFromHost + BundlePath string + Namespace string + ClientConfig *rest.Config + Client kubernetes.Interface + ctx context.Context + RBACErrors +} + +func (c *CollectCopyFromHost) Title() string { + return collectorTitleOrDefault(c.Collector.CollectorMeta, "Copy from Host") +} + +func (c *CollectCopyFromHost) IsExcluded() (bool, error) { + return isExcluded(c.Collector.Exclude) +} + +// copies a file or directory from a host or hosts to include in the bundle. +func (c *CollectCopyFromHost) Collect(progressChan chan<- interface{}) (CollectorResult, error) { + var namespace string + labels := map[string]string{ "app.kubernetes.io/managed-by": "troubleshoot.sh", "troubleshoot.sh/collector": "copyfromhost", "troubleshoot.sh/copyfromhost-id": ksuid.New().String(), } - hostPath := filepath.Clean(collector.HostPath) // strip trailing slash + hostPath := filepath.Clean(c.Collector.HostPath) // strip trailing slash hostDir := filepath.Dir(hostPath) fileName := filepath.Base(hostPath) @@ -41,18 +63,24 @@ func CopyFromHost(ctx context.Context, c *Collector, collector *troubleshootv1be fileName = "." } - _, cleanup, err := copyFromHostCreateDaemonSet(ctx, client, collector, hostDir, namespace, "troubleshoot-copyfromhost-", labels) + namespace = c.Namespace + if namespace == "" { + kubeconfig := k8sutil.GetKubeconfig() + namespace, _, _ = kubeconfig.Namespace() + } + + _, cleanup, err := copyFromHostCreateDaemonSet(c.ctx, c.Client, c.Collector, hostDir, namespace, "troubleshoot-copyfromhost-", labels) defer cleanup() if err != nil { return nil, errors.Wrap(err, "create daemonset") } - childCtx, cancel := context.WithCancel(ctx) + childCtx, cancel := context.WithCancel(c.ctx) defer cancel() timeoutCtx := context.Background() - if collector.Timeout != "" { - timeout, err := time.ParseDuration(collector.Timeout) + if c.Collector.Timeout != "" { + timeout, err := time.ParseDuration(c.Collector.Timeout) if err != nil { return nil, errors.Wrap(err, "parse timeout") } @@ -67,12 +95,12 @@ func CopyFromHost(ctx context.Context, c *Collector, collector *troubleshootv1be resultCh := make(chan CollectorResult, 1) go func() { var outputFilename string - if collector.Name != "" { - outputFilename = collector.Name + if c.Collector.Name != "" { + outputFilename = c.Collector.Name } else { outputFilename = hostPath } - b, err := copyFromHostGetFilesFromPods(childCtx, c, collector, clientConfig, client, fileName, outputFilename, labels, namespace) + b, err := copyFromHostGetFilesFromPods(childCtx, c.BundlePath, c.Collector, c.ClientConfig, c.Client, fileName, outputFilename, labels, namespace) if err != nil { errCh <- err } else { @@ -220,7 +248,7 @@ func copyFromHostCreateDaemonSet(ctx context.Context, client kubernetes.Interfac return createdDS.Name, cleanup, nil } -func copyFromHostGetFilesFromPods(ctx context.Context, c *Collector, collector *troubleshootv1beta2.CopyFromHost, clientConfig *restclient.Config, client kubernetes.Interface, fileName string, outputFilename string, labelSelector map[string]string, namespace string) (CollectorResult, error) { +func copyFromHostGetFilesFromPods(ctx context.Context, bundlePath string, collector *troubleshootv1beta2.CopyFromHost, clientConfig *restclient.Config, client kubernetes.Interface, fileName string, outputFilename string, labelSelector map[string]string, namespace string) (CollectorResult, error) { opts := metav1.ListOptions{ LabelSelector: labels.SelectorFromSet(labelSelector).String(), } @@ -233,16 +261,16 @@ func copyFromHostGetFilesFromPods(ctx context.Context, c *Collector, collector * output := NewResult() for _, pod := range pods.Items { outputNodeFilename := filepath.Join(outputFilename, pod.Spec.NodeName) - files, stderr, err := copyFilesFromHost(ctx, filepath.Join(c.BundlePath, outputNodeFilename), clientConfig, client, pod.Name, "collector", namespace, filepath.Join("/host", fileName), collector.ExtractArchive) + files, stderr, err := copyFilesFromHost(ctx, filepath.Join(bundlePath, outputNodeFilename), clientConfig, client, pod.Name, "collector", namespace, filepath.Join("/host", fileName), collector.ExtractArchive) if err != nil { - output.SaveResult(c.BundlePath, filepath.Join(outputNodeFilename, "error.txt"), bytes.NewBuffer([]byte(err.Error()))) + output.SaveResult(bundlePath, filepath.Join(outputNodeFilename, "error.txt"), bytes.NewBuffer([]byte(err.Error()))) if len(stderr) > 0 { - output.SaveResult(c.BundlePath, filepath.Join(outputNodeFilename, "stderr.txt"), bytes.NewBuffer(stderr)) + output.SaveResult(bundlePath, filepath.Join(outputNodeFilename, "stderr.txt"), bytes.NewBuffer(stderr)) } } for k, v := range files { - relPath, err := filepath.Rel(c.BundlePath, filepath.Join(c.BundlePath, filepath.Join(outputNodeFilename, k))) + relPath, err := filepath.Rel(bundlePath, filepath.Join(bundlePath, filepath.Join(outputNodeFilename, k))) if err != nil { return nil, errors.Wrap(err, "relative path") } diff --git a/pkg/collect/data.go b/pkg/collect/data.go index a43b5b40..5dc9e596 100644 --- a/pkg/collect/data.go +++ b/pkg/collect/data.go @@ -2,16 +2,37 @@ package collect import ( "bytes" + "context" "path/filepath" troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" ) -func Data(c *Collector, dataCollector *troubleshootv1beta2.Data) (CollectorResult, error) { - bundlePath := filepath.Join(dataCollector.Name, dataCollector.CollectorName) +type CollectData struct { + Collector *troubleshootv1beta2.Data + BundlePath string + Namespace string + ClientConfig *rest.Config + Client kubernetes.Interface + ctx context.Context + RBACErrors +} + +func (c *CollectData) Title() string { + return collectorTitleOrDefault(c.Collector.CollectorMeta, "Data") +} + +func (c *CollectData) IsExcluded() (bool, error) { + return isExcluded(c.Collector.Exclude) +} + +func (c *CollectData) Collect(progressChan chan<- interface{}) (CollectorResult, error) { + bundlePath := filepath.Join(c.Collector.Name, c.Collector.CollectorName) output := NewResult() - output.SaveResult(c.BundlePath, bundlePath, bytes.NewBuffer([]byte(dataCollector.Data))) + output.SaveResult(c.BundlePath, bundlePath, bytes.NewBuffer([]byte(c.Collector.Data))) return output, nil } diff --git a/pkg/collect/exec.go b/pkg/collect/exec.go index bec55bbd..e3fcfef7 100644 --- a/pkg/collect/exec.go +++ b/pkg/collect/exec.go @@ -3,24 +3,43 @@ package collect import ( "bytes" "context" - "errors" "fmt" "path/filepath" "time" + "github.com/pkg/errors" troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" "k8s.io/client-go/tools/remotecommand" ) -func Exec(c *Collector, execCollector *troubleshootv1beta2.Exec) (CollectorResult, error) { - if execCollector.Timeout == "" { - return execWithoutTimeout(c, execCollector) +type CollectExec struct { + Collector *troubleshootv1beta2.Exec + BundlePath string + Namespace string + ClientConfig *rest.Config + Client kubernetes.Interface + ctx context.Context + RBACErrors +} + +func (c *CollectExec) Title() string { + return collectorTitleOrDefault(c.Collector.CollectorMeta, "Exec") +} + +func (c *CollectExec) IsExcluded() (bool, error) { + return isExcluded(c.Collector.Exclude) +} + +func (c *CollectExec) Collect(progressChan chan<- interface{}) (CollectorResult, error) { + if c.Collector.Timeout == "" { + return execWithoutTimeout(c.ClientConfig, c.BundlePath, c.Collector) } - timeout, err := time.ParseDuration(execCollector.Timeout) + timeout, err := time.ParseDuration(c.Collector.Timeout) if err != nil { return nil, err } @@ -29,7 +48,7 @@ func Exec(c *Collector, execCollector *troubleshootv1beta2.Exec) (CollectorResul resultCh := make(chan CollectorResult, 1) go func() { - b, err := execWithoutTimeout(c, execCollector) + b, err := execWithoutTimeout(c.ClientConfig, c.BundlePath, c.Collector) if err != nil { errCh <- err } else { @@ -47,8 +66,8 @@ func Exec(c *Collector, execCollector *troubleshootv1beta2.Exec) (CollectorResul } } -func execWithoutTimeout(c *Collector, execCollector *troubleshootv1beta2.Exec) (CollectorResult, error) { - client, err := kubernetes.NewForConfig(c.ClientConfig) +func execWithoutTimeout(clientConfig *rest.Config, bundlePath string, execCollector *troubleshootv1beta2.Exec) (CollectorResult, error) { + client, err := kubernetes.NewForConfig(clientConfig) if err != nil { return nil, err } @@ -59,23 +78,23 @@ func execWithoutTimeout(c *Collector, execCollector *troubleshootv1beta2.Exec) ( pods, podsErrors := listPodsInSelectors(ctx, client, execCollector.Namespace, execCollector.Selector) if len(podsErrors) > 0 { - output.SaveResult(c.BundlePath, getExecErrosFileName(execCollector), marshalErrors(podsErrors)) + output.SaveResult(bundlePath, getExecErrosFileName(execCollector), marshalErrors(podsErrors)) } if len(pods) > 0 { for _, pod := range pods { - stdout, stderr, execErrors := getExecOutputs(c, client, pod, execCollector) + stdout, stderr, execErrors := getExecOutputs(clientConfig, client, pod, execCollector) - bundlePath := filepath.Join(execCollector.Name, pod.Namespace, pod.Name) + path := filepath.Join(execCollector.Name, pod.Namespace, pod.Name) if len(stdout) > 0 { - output.SaveResult(c.BundlePath, filepath.Join(bundlePath, execCollector.CollectorName+"-stdout.txt"), bytes.NewBuffer(stdout)) + output.SaveResult(bundlePath, filepath.Join(path, execCollector.CollectorName+"-stdout.txt"), bytes.NewBuffer(stdout)) } if len(stderr) > 0 { - output.SaveResult(c.BundlePath, filepath.Join(bundlePath, execCollector.CollectorName+"-stderr.txt"), bytes.NewBuffer(stderr)) + output.SaveResult(bundlePath, filepath.Join(path, execCollector.CollectorName+"-stderr.txt"), bytes.NewBuffer(stderr)) } if len(execErrors) > 0 { - output.SaveResult(c.BundlePath, filepath.Join(bundlePath, execCollector.CollectorName+"-errors.json"), marshalErrors(execErrors)) + output.SaveResult(bundlePath, filepath.Join(path, execCollector.CollectorName+"-errors.json"), marshalErrors(execErrors)) continue } } @@ -84,7 +103,7 @@ func execWithoutTimeout(c *Collector, execCollector *troubleshootv1beta2.Exec) ( return output, nil } -func getExecOutputs(c *Collector, client *kubernetes.Clientset, pod corev1.Pod, execCollector *troubleshootv1beta2.Exec) ([]byte, []byte, []string) { +func getExecOutputs(clientConfig *rest.Config, client *kubernetes.Clientset, pod corev1.Pod, execCollector *troubleshootv1beta2.Exec) ([]byte, []byte, []string) { container := pod.Spec.Containers[0].Name if execCollector.ContainerName != "" { container = execCollector.ContainerName @@ -106,7 +125,7 @@ func getExecOutputs(c *Collector, client *kubernetes.Clientset, pod corev1.Pod, TTY: false, }, parameterCodec) - exec, err := remotecommand.NewSPDYExecutor(c.ClientConfig, "POST", req.URL()) + exec, err := remotecommand.NewSPDYExecutor(clientConfig, "POST", req.URL()) if err != nil { return nil, nil, []string{err.Error()} } diff --git a/pkg/collect/host_http.go b/pkg/collect/host_http.go index b25761a5..80a82b80 100644 --- a/pkg/collect/host_http.go +++ b/pkg/collect/host_http.go @@ -38,7 +38,7 @@ func (c *CollectHostHTTP) Collect(progressChan chan<- interface{}) (map[string][ return nil, errors.New("no supported http request type") } - responseOutput, err := responseToOutput(response, err, false) + responseOutput, err := responseToOutput(response, err) if err != nil { return nil, err } diff --git a/pkg/collect/http.go b/pkg/collect/http.go index 656482de..72c49015 100644 --- a/pkg/collect/http.go +++ b/pkg/collect/http.go @@ -4,13 +4,15 @@ import ( "bytes" "crypto/tls" "encoding/json" - "errors" "io/ioutil" "net/http" "path/filepath" "strings" + "github.com/pkg/errors" troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" ) type HTTPResponse struct { @@ -33,32 +35,49 @@ var ( } ) -func HTTP(c *Collector, httpCollector *troubleshootv1beta2.HTTP) (CollectorResult, error) { +type CollectHTTP struct { + Collector *troubleshootv1beta2.HTTP + BundlePath string + Namespace string + ClientConfig *rest.Config + Client kubernetes.Interface + RBACErrors +} + +func (c *CollectHTTP) Title() string { + return collectorTitleOrDefault(c.Collector.CollectorMeta, "HTTP") +} + +func (c *CollectHTTP) IsExcluded() (bool, error) { + return isExcluded(c.Collector.Exclude) +} + +func (c *CollectHTTP) Collect(progressChan chan<- interface{}) (CollectorResult, error) { var response *http.Response var err error - if httpCollector.Get != nil { - response, err = doGet(httpCollector.Get) - } else if httpCollector.Post != nil { - response, err = doPost(httpCollector.Post) - } else if httpCollector.Put != nil { - response, err = doPut(httpCollector.Put) + if c.Collector.Get != nil { + response, err = doGet(c.Collector.Get) + } else if c.Collector.Post != nil { + response, err = doPost(c.Collector.Post) + } else if c.Collector.Put != nil { + response, err = doPut(c.Collector.Put) } else { return nil, errors.New("no supported http request type") } - o, err := responseToOutput(response, err, c.Redact) + o, err := responseToOutput(response, err) if err != nil { return nil, err } fileName := "result.json" - if httpCollector.CollectorName != "" { - fileName = httpCollector.CollectorName + ".json" + if c.Collector.CollectorName != "" { + fileName = c.Collector.CollectorName + ".json" } output := NewResult() - output.SaveResult(c.BundlePath, filepath.Join(httpCollector.Name, fileName), bytes.NewBuffer(o)) + output.SaveResult(c.BundlePath, filepath.Join(c.Collector.Name, fileName), bytes.NewBuffer(o)) return output, nil } @@ -117,7 +136,7 @@ func doPut(put *troubleshootv1beta2.Put) (*http.Response, error) { return httpClient.Do(req) } -func responseToOutput(response *http.Response, err error, doRedact bool) ([]byte, error) { +func responseToOutput(response *http.Response, err error) ([]byte, error) { output := make(map[string]interface{}) if err != nil { output["error"] = HTTPError{ diff --git a/pkg/collect/logs.go b/pkg/collect/logs.go index 62c26147..03c50d40 100644 --- a/pkg/collect/logs.go +++ b/pkg/collect/logs.go @@ -13,9 +13,29 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" ) -func Logs(c *Collector, logsCollector *troubleshootv1beta2.Logs) (CollectorResult, error) { +type CollectLogs struct { + Collector *troubleshootv1beta2.Logs + BundlePath string + Namespace string + ClientConfig *rest.Config + Client kubernetes.Interface + ctx context.Context + SinceTime *time.Time + RBACErrors +} + +func (c *CollectLogs) Title() string { + return collectorTitleOrDefault(c.Collector.CollectorMeta, "Logs") +} + +func (c *CollectLogs) IsExcluded() (bool, error) { + return isExcluded(c.Collector.Exclude) +} + +func (c *CollectLogs) Collect(progressChan chan<- interface{}) (CollectorResult, error) { client, err := kubernetes.NewForConfig(c.ClientConfig) if err != nil { return nil, err @@ -25,14 +45,21 @@ func Logs(c *Collector, logsCollector *troubleshootv1beta2.Logs) (CollectorResul ctx := context.Background() - pods, podsErrors := listPodsInSelectors(ctx, client, logsCollector.Namespace, logsCollector.Selector) + if c.SinceTime != nil { + if c.Collector.Limits == nil { + c.Collector.Limits = new(troubleshootv1beta2.LogLimits) + } + c.Collector.Limits.SinceTime = metav1.NewTime(*c.SinceTime) + } + + pods, podsErrors := listPodsInSelectors(ctx, client, c.Collector.Namespace, c.Collector.Selector) if len(podsErrors) > 0 { - output.SaveResult(c.BundlePath, getLogsErrorsFileName(logsCollector), marshalErrors(podsErrors)) + output.SaveResult(c.BundlePath, getLogsErrorsFileName(c.Collector), marshalErrors(podsErrors)) } if len(pods) > 0 { for _, pod := range pods { - if len(logsCollector.ContainerNames) == 0 { + if len(c.Collector.ContainerNames) == 0 { // make a list of all the containers in the pod, so that we can get logs from all of them containerNames := []string{} for _, container := range pod.Spec.Containers { @@ -46,11 +73,11 @@ func Logs(c *Collector, logsCollector *troubleshootv1beta2.Logs) (CollectorResul if len(containerNames) == 1 { containerName = "" // if there was only one container, use the old behavior of not including the container name in the path } - podLogs, err := savePodLogs(ctx, c.BundlePath, client, pod, logsCollector.Name, containerName, logsCollector.Limits, false) + podLogs, err := savePodLogs(ctx, c.BundlePath, client, pod, c.Collector.Name, containerName, c.Collector.Limits, false) if err != nil { - key := fmt.Sprintf("%s/%s-errors.json", logsCollector.Name, pod.Name) + key := fmt.Sprintf("%s/%s-errors.json", c.Collector.Name, pod.Name) if containerName != "" { - key = fmt.Sprintf("%s/%s/%s-errors.json", logsCollector.Name, pod.Name, containerName) + key = fmt.Sprintf("%s/%s/%s-errors.json", c.Collector.Name, pod.Name, containerName) } err := output.SaveResult(c.BundlePath, key, marshalErrors([]string{err.Error()})) if err != nil { @@ -63,10 +90,10 @@ func Logs(c *Collector, logsCollector *troubleshootv1beta2.Logs) (CollectorResul } } } else { - for _, container := range logsCollector.ContainerNames { - containerLogs, err := savePodLogs(ctx, c.BundlePath, client, pod, logsCollector.Name, container, logsCollector.Limits, false) + for _, container := range c.Collector.ContainerNames { + containerLogs, err := savePodLogs(ctx, c.BundlePath, client, pod, c.Collector.Name, container, c.Collector.Limits, false) if err != nil { - key := fmt.Sprintf("%s/%s/%s-errors.json", logsCollector.Name, pod.Name, container) + key := fmt.Sprintf("%s/%s/%s-errors.json", c.Collector.Name, pod.Name, container) err := output.SaveResult(c.BundlePath, key, marshalErrors([]string{err.Error()})) if err != nil { return nil, err diff --git a/pkg/collect/longhorn.go b/pkg/collect/longhorn.go index b492e1d2..8a025bc1 100644 --- a/pkg/collect/longhorn.go +++ b/pkg/collect/longhorn.go @@ -29,12 +29,30 @@ const ( var checksumRX = regexp.MustCompile(`(\S+)\s+(\S+)`) -func Longhorn(c *Collector, longhornCollector *troubleshootv1beta2.Longhorn) (CollectorResult, error) { +type CollectLonghorn struct { + Collector *troubleshootv1beta2.Longhorn + BundlePath string + Namespace string + ClientConfig *rest.Config + Client kubernetes.Interface + ctx context.Context + RBACErrors +} + +func (c *CollectLonghorn) Title() string { + return collectorTitleOrDefault(c.Collector.CollectorMeta, "Longhorn") +} + +func (c *CollectLonghorn) IsExcluded() (bool, error) { + return isExcluded(c.Collector.Exclude) +} + +func (c *CollectLonghorn) Collect(progressChan chan<- interface{}) (CollectorResult, error) { ctx := context.TODO() ns := DefaultLonghornNamespace - if longhornCollector.Namespace != "" { - ns = longhornCollector.Namespace + if c.Collector.Namespace != "" { + ns = c.Collector.Namespace } client, err := longhornv1beta1.NewForConfig(c.ClientConfig) @@ -197,11 +215,15 @@ func Longhorn(c *Collector, longhornCollector *troubleshootv1beta2.Longhorn) (Co output.SaveResult(c.BundlePath, settingsKey, bytes.NewBuffer(settingsB)) // logs of all pods in namespace - logsCollector := &troubleshootv1beta2.Logs{ + logsCollectorSpec := &troubleshootv1beta2.Logs{ Selector: []string{""}, Namespace: ns, } - logs, err := Logs(c, logsCollector) + + rbacErrors := c.GetRBACErrors() + logsCollector := &CollectLogs{logsCollectorSpec, c.BundlePath, c.Namespace, c.ClientConfig, c.Client, c.ctx, nil, rbacErrors} + + logs, err := logsCollector.Collect(progressChan) if err != nil { return nil, errors.Wrap(err, "collect longhorn logs") } diff --git a/pkg/collect/mysql.go b/pkg/collect/mysql.go index b5febfc6..719372b3 100644 --- a/pkg/collect/mysql.go +++ b/pkg/collect/mysql.go @@ -2,6 +2,7 @@ package collect import ( "bytes" + "context" "database/sql" "encoding/json" "fmt" @@ -9,12 +10,32 @@ import ( _ "github.com/go-sql-driver/mysql" "github.com/pkg/errors" troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" ) -func Mysql(c *Collector, databaseCollector *troubleshootv1beta2.Database) (CollectorResult, error) { +type CollectMysql struct { + Collector *troubleshootv1beta2.Database + BundlePath string + Namespace string + ClientConfig *rest.Config + Client kubernetes.Interface + ctx context.Context + RBACErrors +} + +func (c *CollectMysql) Title() string { + return collectorTitleOrDefault(c.Collector.CollectorMeta, "Mysql") +} + +func (c *CollectMysql) IsExcluded() (bool, error) { + return isExcluded(c.Collector.Exclude) +} + +func (c *CollectMysql) Collect(progressChan chan<- interface{}) (CollectorResult, error) { databaseConnection := DatabaseConnection{} - db, err := sql.Open("mysql", databaseCollector.URI) + db, err := sql.Open("mysql", c.Collector.URI) if err != nil { databaseConnection.Error = err.Error() } else { @@ -30,7 +51,7 @@ func Mysql(c *Collector, databaseCollector *troubleshootv1beta2.Database) (Colle databaseConnection.Version = version } - requestedParameters := databaseCollector.Parameters + requestedParameters := c.Collector.Parameters if len(requestedParameters) > 0 { rows, err := db.Query("SHOW VARIABLES") @@ -68,7 +89,7 @@ func Mysql(c *Collector, databaseCollector *troubleshootv1beta2.Database) (Colle return nil, errors.Wrap(err, "failed to marshal database connection") } - collectorName := databaseCollector.CollectorName + collectorName := c.Collector.CollectorName if collectorName == "" { collectorName = "mysql" } diff --git a/pkg/collect/postgres.go b/pkg/collect/postgres.go index 21e94e54..3c7ca244 100644 --- a/pkg/collect/postgres.go +++ b/pkg/collect/postgres.go @@ -2,6 +2,7 @@ package collect import ( "bytes" + "context" "database/sql" "encoding/json" "fmt" @@ -10,12 +11,32 @@ import ( _ "github.com/lib/pq" "github.com/pkg/errors" troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" ) -func Postgres(c *Collector, databaseCollector *troubleshootv1beta2.Database) (CollectorResult, error) { +type CollectPostgres struct { + Collector *troubleshootv1beta2.Database + BundlePath string + Namespace string + ClientConfig *rest.Config + Client kubernetes.Interface + ctx context.Context + RBACErrors +} + +func (c *CollectPostgres) Title() string { + return collectorTitleOrDefault(c.Collector.CollectorMeta, "Postgres") +} + +func (c *CollectPostgres) IsExcluded() (bool, error) { + return isExcluded(c.Collector.Exclude) +} + +func (c *CollectPostgres) Collect(progressChan chan<- interface{}) (CollectorResult, error) { databaseConnection := DatabaseConnection{} - db, err := sql.Open("postgres", databaseCollector.URI) + db, err := sql.Open("postgres", c.Collector.URI) if err != nil { databaseConnection.Error = err.Error() } else { @@ -42,7 +63,7 @@ func Postgres(c *Collector, databaseCollector *troubleshootv1beta2.Database) (Co return nil, errors.Wrap(err, "failed to marshal database connection") } - collectorName := databaseCollector.CollectorName + collectorName := c.Collector.CollectorName if collectorName == "" { collectorName = "postgres" } diff --git a/pkg/collect/rbac.go b/pkg/collect/rbac.go index 9d9af736..dfca93fd 100644 --- a/pkg/collect/rbac.go +++ b/pkg/collect/rbac.go @@ -1,11 +1,43 @@ package collect import ( + "context" "fmt" "github.com/pkg/errors" + troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" + authorizationv1 "k8s.io/api/authorization/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" ) +type RBACErrors []error + +func (e RBACErrors) GetRBACErrors() []error { + return e +} + +func (e RBACErrors) HasRBACErrors() bool { + return len(e) > 0 +} + +func (e *RBACErrors) CheckRBAC(ctx context.Context, c Collector, collector *troubleshootv1beta2.Collect, clientConfig *rest.Config, namespace string) error { + exclude, err := c.IsExcluded() + if err != nil || exclude { + return nil + } + + rbacErrors, err := checkRBAC(ctx, clientConfig, namespace, c.Title(), collector) + if err != nil { + return err + } + + *e = rbacErrors + + return nil +} + type RBACError struct { DisplayName string Namespace string @@ -24,3 +56,35 @@ func IsRBACError(err error) bool { _, ok := errors.Cause(err).(RBACError) return ok } + +func checkRBAC(ctx context.Context, clientConfig *rest.Config, namespace string, title string, collector *troubleshootv1beta2.Collect) ([]error, error) { + client, err := kubernetes.NewForConfig(clientConfig) + if err != nil { + return nil, errors.Wrap(err, "failed to create client from config") + } + + forbidden := make([]error, 0) + + specs := collector.AccessReviewSpecs(namespace) + for _, spec := range specs { + sar := &authorizationv1.SelfSubjectAccessReview{ + Spec: spec, + } + + resp, err := client.AuthorizationV1().SelfSubjectAccessReviews().Create(ctx, sar, metav1.CreateOptions{}) + if err != nil { + return nil, errors.Wrap(err, "failed to run subject review") + } + + if !resp.Status.Allowed { // all other fields of Status are empty... + forbidden = append(forbidden, RBACError{ + DisplayName: title, + Namespace: spec.ResourceAttributes.Namespace, + Resource: spec.ResourceAttributes.Resource, + Verb: spec.ResourceAttributes.Verb, + }) + } + } + + return forbidden, nil +} diff --git a/pkg/collect/redis.go b/pkg/collect/redis.go index 12d9ed3a..7824b63b 100644 --- a/pkg/collect/redis.go +++ b/pkg/collect/redis.go @@ -2,6 +2,7 @@ package collect import ( "bytes" + "context" "encoding/json" "fmt" "strings" @@ -9,12 +10,32 @@ import ( "github.com/go-redis/redis/v7" "github.com/pkg/errors" troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" ) -func Redis(c *Collector, databaseCollector *troubleshootv1beta2.Database) (CollectorResult, error) { +type CollectRedis struct { + Collector *troubleshootv1beta2.Database + BundlePath string + Namespace string + ClientConfig *rest.Config + Client kubernetes.Interface + ctx context.Context + RBACErrors +} + +func (c *CollectRedis) Title() string { + return collectorTitleOrDefault(c.Collector.CollectorMeta, "Cluster Info") +} + +func (c *CollectRedis) IsExcluded() (bool, error) { + return isExcluded(c.Collector.Exclude) +} + +func (c *CollectRedis) Collect(progressChan chan<- interface{}) (CollectorResult, error) { databaseConnection := DatabaseConnection{} - opt, err := redis.ParseURL(databaseCollector.URI) + opt, err := redis.ParseURL(c.Collector.URI) if err != nil { databaseConnection.Error = err.Error() } else { @@ -45,7 +66,7 @@ func Redis(c *Collector, databaseCollector *troubleshootv1beta2.Database) (Colle return nil, errors.Wrap(err, "failed to marshal database connection") } - collectorName := databaseCollector.CollectorName + collectorName := c.Collector.CollectorName if collectorName == "" { collectorName = "redis" } diff --git a/pkg/collect/registry.go b/pkg/collect/registry.go index 88a8d57e..d486186e 100644 --- a/pkg/collect/registry.go +++ b/pkg/collect/registry.go @@ -20,6 +20,7 @@ import ( troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" ) type RegistryImage struct { @@ -36,13 +37,31 @@ type registryAuthConfig struct { password string } -func Registry(c *Collector, registryCollector *troubleshootv1beta2.RegistryImages) (CollectorResult, error) { +type CollectRegistry struct { + Collector *troubleshootv1beta2.RegistryImages + BundlePath string + Namespace string + ClientConfig *rest.Config + Client kubernetes.Interface + ctx context.Context + RBACErrors +} + +func (c *CollectRegistry) Title() string { + return collectorTitleOrDefault(c.Collector.CollectorMeta, "Registry Images") +} + +func (c *CollectRegistry) IsExcluded() (bool, error) { + return isExcluded(c.Collector.Exclude) +} + +func (c *CollectRegistry) Collect(progressChan chan<- interface{}) (CollectorResult, error) { registryInfo := RegistryInfo{ Images: map[string]RegistryImage{}, } - for _, image := range registryCollector.Images { - exists, err := imageExists(c, registryCollector, image) + for _, image := range c.Collector.Images { + exists, err := imageExists(c.Namespace, c.ClientConfig, c.Collector, image) if err != nil { registryInfo.Images[image] = RegistryImage{ Error: err.Error(), @@ -59,7 +78,7 @@ func Registry(c *Collector, registryCollector *troubleshootv1beta2.RegistryImage return nil, errors.Wrap(err, "failed to marshal database connection") } - collectorName := registryCollector.CollectorName + collectorName := c.Collector.CollectorName if collectorName == "" { collectorName = "images" } @@ -70,13 +89,13 @@ func Registry(c *Collector, registryCollector *troubleshootv1beta2.RegistryImage return output, nil } -func imageExists(c *Collector, registryCollector *troubleshootv1beta2.RegistryImages, image string) (bool, error) { +func imageExists(namespace string, clientConfig *rest.Config, registryCollector *troubleshootv1beta2.RegistryImages, image string) (bool, error) { imageRef, err := alltransports.ParseImageName(fmt.Sprintf("docker://%s", image)) if err != nil { return false, errors.Wrapf(err, "failed to parse image name %s", image) } - authConfig, err := getImageAuthConfig(c, registryCollector, imageRef) + authConfig, err := getImageAuthConfig(namespace, clientConfig, registryCollector, imageRef) if err != nil { return false, errors.Wrap(err, "failed to get auth config") } @@ -123,13 +142,13 @@ func imageExists(c *Collector, registryCollector *troubleshootv1beta2.RegistryIm return false, errors.Wrap(lastErr, "failed to retry") } -func getImageAuthConfig(c *Collector, registryCollector *troubleshootv1beta2.RegistryImages, imageRef types.ImageReference) (*registryAuthConfig, error) { +func getImageAuthConfig(namespace string, clientConfig *rest.Config, registryCollector *troubleshootv1beta2.RegistryImages, imageRef types.ImageReference) (*registryAuthConfig, error) { if registryCollector.ImagePullSecrets == nil { return nil, nil } if registryCollector.ImagePullSecrets.Data != nil { - config, err := getImageAuthConfigFromData(c, imageRef, registryCollector.ImagePullSecrets) + config, err := getImageAuthConfigFromData(imageRef, registryCollector.ImagePullSecrets) if err != nil { return nil, errors.Wrap(err, "failed to get auth from data") } @@ -137,14 +156,14 @@ func getImageAuthConfig(c *Collector, registryCollector *troubleshootv1beta2.Reg } if registryCollector.ImagePullSecrets.Name != "" { - namespace := registryCollector.Namespace - if namespace == "" { - namespace = c.Namespace + collectorNamespace := registryCollector.Namespace + if collectorNamespace == "" { + collectorNamespace = namespace } - if namespace == "" { - namespace = "default" + if collectorNamespace == "" { + collectorNamespace = "default" } - config, err := getImageAuthConfigFromSecret(c, imageRef, registryCollector.ImagePullSecrets, namespace) + config, err := getImageAuthConfigFromSecret(clientConfig, imageRef, registryCollector.ImagePullSecrets, collectorNamespace) if err != nil { return nil, errors.Wrap(err, "failed to get auth from secret") } @@ -154,7 +173,7 @@ func getImageAuthConfig(c *Collector, registryCollector *troubleshootv1beta2.Reg return nil, errors.New("image pull secret spec is not valid") } -func getImageAuthConfigFromData(c *Collector, imageRef types.ImageReference, pullSecrets *v1beta2.ImagePullSecrets) (*registryAuthConfig, error) { +func getImageAuthConfigFromData(imageRef types.ImageReference, pullSecrets *v1beta2.ImagePullSecrets) (*registryAuthConfig, error) { if pullSecrets.SecretType != "kubernetes.io/dockerconfigjson" { return nil, errors.Errorf("secret type is not supported: %s", pullSecrets.SecretType) } @@ -197,10 +216,10 @@ func getImageAuthConfigFromData(c *Collector, imageRef types.ImageReference, pul return &authConfig, nil } -func getImageAuthConfigFromSecret(c *Collector, imageRef types.ImageReference, pullSecrets *v1beta2.ImagePullSecrets, namespace string) (*registryAuthConfig, error) { +func getImageAuthConfigFromSecret(clientConfig *rest.Config, imageRef types.ImageReference, pullSecrets *v1beta2.ImagePullSecrets, namespace string) (*registryAuthConfig, error) { ctx := context.Background() - client, err := kubernetes.NewForConfig(c.ClientConfig) + client, err := kubernetes.NewForConfig(clientConfig) if err != nil { return nil, errors.Wrap(err, "failed to create client from config") } @@ -218,7 +237,7 @@ func getImageAuthConfigFromSecret(c *Collector, imageRef types.ImageReference, p }, } - config, err := getImageAuthConfigFromData(c, imageRef, foundSecrets) + config, err := getImageAuthConfigFromData(imageRef, foundSecrets) if err != nil { return nil, errors.Wrap(err, "failed to get auth from secret data") } diff --git a/pkg/collect/run.go b/pkg/collect/run.go index 45a2d23e..119906bb 100644 --- a/pkg/collect/run.go +++ b/pkg/collect/run.go @@ -1,264 +1,73 @@ package collect import ( - "bytes" "context" - "encoding/base64" - "encoding/json" - "time" - "github.com/pkg/errors" troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" - "github.com/replicatedhq/troubleshoot/pkg/logger" corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" ) -func Run(c *Collector, runCollector *troubleshootv1beta2.Run) (CollectorResult, error) { +type CollectRun struct { + Collector *troubleshootv1beta2.Run + BundlePath string + Namespace string + ClientConfig *rest.Config + Client kubernetes.Interface + ctx context.Context + RBACErrors +} + +func (c *CollectRun) Title() string { + return collectorTitleOrDefault(c.Collector.CollectorMeta, "Run") +} + +func (c *CollectRun) IsExcluded() (bool, error) { + return isExcluded(c.Collector.Exclude) +} + +func (c *CollectRun) Collect(progressChan chan<- interface{}) (CollectorResult, error) { pullPolicy := corev1.PullIfNotPresent - if runCollector.ImagePullPolicy != "" { - pullPolicy = corev1.PullPolicy(runCollector.ImagePullPolicy) + if c.Collector.ImagePullPolicy != "" { + pullPolicy = corev1.PullPolicy(c.Collector.ImagePullPolicy) } namespace := "default" - if runCollector.Namespace != "" { - namespace = runCollector.Namespace + if c.Collector.Namespace != "" { + namespace = c.Collector.Namespace } serviceAccountName := "default" - if runCollector.ServiceAccountName != "" { - serviceAccountName = runCollector.ServiceAccountName + if c.Collector.ServiceAccountName != "" { + serviceAccountName = c.Collector.ServiceAccountName } - runPodCollector := &troubleshootv1beta2.RunPod{ + runPodSpec := &troubleshootv1beta2.RunPod{ CollectorMeta: troubleshootv1beta2.CollectorMeta{ - CollectorName: runCollector.CollectorName, + CollectorName: c.Collector.CollectorName, }, - Name: runCollector.Name, + Name: c.Collector.Name, Namespace: namespace, - Timeout: runCollector.Timeout, - ImagePullSecret: runCollector.ImagePullSecret, + Timeout: c.Collector.Timeout, + ImagePullSecret: c.Collector.ImagePullSecret, PodSpec: corev1.PodSpec{ RestartPolicy: corev1.RestartPolicyNever, ServiceAccountName: serviceAccountName, Containers: []corev1.Container{ { - Image: runCollector.Image, + Image: c.Collector.Image, ImagePullPolicy: pullPolicy, Name: "collector", - Command: runCollector.Command, - Args: runCollector.Args, + Command: c.Collector.Command, + Args: c.Collector.Args, }, }, }, } - return RunPod(c, runPodCollector) -} - -func RunPod(c *Collector, runPodCollector *troubleshootv1beta2.RunPod) (CollectorResult, error) { - ctx := context.Background() - - client, err := kubernetes.NewForConfig(c.ClientConfig) - if err != nil { - return nil, errors.Wrap(err, "failed to create client from config") - } - - pod, err := runPodWithSpec(ctx, client, runPodCollector) - if err != nil { - return nil, errors.Wrap(err, "failed to run pod") - } - defer func() { - if err := client.CoreV1().Pods(pod.Namespace).Delete(context.Background(), pod.Name, metav1.DeleteOptions{}); err != nil { - logger.Printf("Failed to delete pod %s: %v", pod.Name, err) - } - }() - - if runPodCollector.ImagePullSecret != nil && runPodCollector.ImagePullSecret.Data != nil { - defer func() { - for _, k := range pod.Spec.ImagePullSecrets { - if err := client.CoreV1().Secrets(pod.Namespace).Delete(context.Background(), k.Name, metav1.DeleteOptions{}); err != nil { - logger.Printf("Failed to delete secret %s: %v", k.Name, err) - } - } - }() - } - if runPodCollector.Timeout == "" { - return runWithoutTimeout(ctx, c, pod, runPodCollector) - } - - timeout, err := time.ParseDuration(runPodCollector.Timeout) - if err != nil { - return nil, errors.Wrap(err, "failed to parse timeout") - } - - errCh := make(chan error, 1) - resultCh := make(chan CollectorResult, 1) - - timeoutCtx, cancel := context.WithTimeout(ctx, timeout) - defer cancel() - - go func() { - b, err := runWithoutTimeout(timeoutCtx, c, pod, runPodCollector) - if err != nil { - errCh <- err - } else { - resultCh <- b - } - }() - - select { - case <-time.After(timeout): - return nil, errors.New("timeout") - case result := <-resultCh: - return result, nil - case err := <-errCh: - return nil, err - } -} - -func runPodWithSpec(ctx context.Context, client *kubernetes.Clientset, runPodCollector *troubleshootv1beta2.RunPod) (*corev1.Pod, error) { - podLabels := make(map[string]string) - podLabels["troubleshoot-role"] = "run-collector" - - namespace := "default" - if runPodCollector.Namespace != "" { - namespace = runPodCollector.Namespace - } - - podName := "run-pod" - if runPodCollector.CollectorName != "" { - podName = runPodCollector.CollectorName - } else if runPodCollector.Name != "" { - podName = runPodCollector.Name - } - - pod := corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Name: podName, - Namespace: namespace, - Labels: podLabels, - }, - TypeMeta: metav1.TypeMeta{ - APIVersion: "v1", - Kind: "Pod", - }, - Spec: runPodCollector.PodSpec, - } - - if runPodCollector.ImagePullSecret != nil && runPodCollector.ImagePullSecret.Data != nil { - secretName, err := createSecret(ctx, client, pod.Namespace, runPodCollector.ImagePullSecret) - if err != nil { - return nil, errors.Wrap(err, "failed to create secret") - } - pod.Spec.ImagePullSecrets = append(pod.Spec.ImagePullSecrets, corev1.LocalObjectReference{Name: secretName}) - } - - created, err := client.CoreV1().Pods(namespace).Create(ctx, &pod, metav1.CreateOptions{}) - if err != nil { - return nil, errors.Wrap(err, "failed to create pod") - } - - return created, nil -} - -func runWithoutTimeout(ctx context.Context, c *Collector, pod *corev1.Pod, runPodCollector *troubleshootv1beta2.RunPod) (CollectorResult, error) { - client, err := kubernetes.NewForConfig(c.ClientConfig) - if err != nil { - return nil, errors.Wrap(err, "failed create client from config") - } - - for { - status, err := client.CoreV1().Pods(pod.Namespace).Get(ctx, pod.Name, metav1.GetOptions{}) - if err != nil { - return nil, errors.Wrap(err, "failed to get pod") - } - if status.Status.Phase == corev1.PodRunning || - status.Status.Phase == corev1.PodFailed || - status.Status.Phase == corev1.PodSucceeded { - break - } - if status.Status.Phase == corev1.PodPending { - for _, v := range status.Status.ContainerStatuses { - if v.State.Waiting != nil && v.State.Waiting.Reason == "ImagePullBackOff" { - return nil, errors.Errorf("run pod aborted after getting pod status 'ImagePullBackOff'") - } - } - } - time.Sleep(time.Second * 1) - } - - output := NewResult() - - collectorName := runPodCollector.Name - - limits := troubleshootv1beta2.LogLimits{ - MaxLines: 10000, - } - podLogs, err := savePodLogs(ctx, c.BundlePath, client, *pod, collectorName, "", &limits, true) - if err != nil { - return nil, errors.Wrap(err, "failed to get pod logs") - } - - for k, v := range podLogs { - output[k] = v - } - - return output, nil -} - -func createSecret(ctx context.Context, client kubernetes.Interface, namespace string, imagePullSecret *troubleshootv1beta2.ImagePullSecrets) (string, error) { - if imagePullSecret.Data == nil { - return "", nil - } - - var out bytes.Buffer - data := make(map[string][]byte) - if imagePullSecret.SecretType != "kubernetes.io/dockerconfigjson" { - return "", errors.Errorf("ImagePullSecret must be of type: kubernetes.io/dockerconfigjson") - } - - // Check if required field in data exists - v, found := imagePullSecret.Data[".dockerconfigjson"] - if !found { - return "", errors.Errorf("Secret type kubernetes.io/dockerconfigjson requires argument \".dockerconfigjson\"") - } - if len(imagePullSecret.Data) > 1 { - return "", errors.Errorf("Secret type kubernetes.io/dockerconfigjson accepts only one argument \".dockerconfigjson\"") - } - // K8s client accepts only Json formated files as data, provided data must be decoded and indented - parsedConfig, err := base64.StdEncoding.DecodeString(v) - if err != nil { - return "", errors.Wrap(err, "Unable to decode data.") - } - err = json.Indent(&out, parsedConfig, "", "\t") - if err != nil { - return "", errors.Wrap(err, "Unable to parse encoded data.") - } - data[".dockerconfigjson"] = out.Bytes() - - secret := corev1.Secret{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "v1", - Kind: "Secret", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: imagePullSecret.Name, - GenerateName: "troubleshoot", - Namespace: namespace, - Labels: map[string]string{ - "app.kubernetes.io/managed-by": "troubleshoot.sh", - }, - }, - Data: data, - Type: corev1.SecretType(imagePullSecret.SecretType), - } - - created, err := client.CoreV1().Secrets(namespace).Create(ctx, &secret, metav1.CreateOptions{}) - if err != nil { - return "", errors.Wrap(err, "failed to create secret") - } - - return created.Name, nil + rbacErrors := c.GetRBACErrors() + runPodCollector := &CollectRunPod{runPodSpec, c.BundlePath, c.Namespace, c.ClientConfig, c.Client, c.ctx, rbacErrors} + + return runPodCollector.Collect(progressChan) } diff --git a/pkg/collect/run_pod.go b/pkg/collect/run_pod.go index aeb566ff..bafa0857 100644 --- a/pkg/collect/run_pod.go +++ b/pkg/collect/run_pod.go @@ -1,20 +1,253 @@ package collect import ( + "bytes" "context" + "encoding/base64" + "encoding/json" "io/ioutil" "sync" + "time" "github.com/pkg/errors" + troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" "github.com/replicatedhq/troubleshoot/pkg/k8sutil" "github.com/replicatedhq/troubleshoot/pkg/logger" corev1 "k8s.io/api/core/v1" + "k8s.io/client-go/kubernetes" v1 "k8s.io/client-go/kubernetes/typed/core/v1" + "k8s.io/client-go/rest" kuberneteserrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +type CollectRunPod struct { + Collector *troubleshootv1beta2.RunPod + BundlePath string + Namespace string + ClientConfig *rest.Config + Client kubernetes.Interface + ctx context.Context + RBACErrors +} + +func (c *CollectRunPod) Title() string { + return collectorTitleOrDefault(c.Collector.CollectorMeta, "Run Pod") +} + +func (c *CollectRunPod) IsExcluded() (bool, error) { + return isExcluded(c.Collector.Exclude) +} + +func (c *CollectRunPod) Collect(progressChan chan<- interface{}) (CollectorResult, error) { + ctx := context.Background() + + client, err := kubernetes.NewForConfig(c.ClientConfig) + if err != nil { + return nil, errors.Wrap(err, "failed to create client from config") + } + + pod, err := runPodWithSpec(ctx, client, c.Collector) + if err != nil { + return nil, errors.Wrap(err, "failed to run pod") + } + defer func() { + if err := client.CoreV1().Pods(pod.Namespace).Delete(context.Background(), pod.Name, metav1.DeleteOptions{}); err != nil { + logger.Printf("Failed to delete pod %s: %v", pod.Name, err) + } + }() + + if c.Collector.ImagePullSecret != nil && c.Collector.ImagePullSecret.Data != nil { + defer func() { + for _, k := range pod.Spec.ImagePullSecrets { + if err := client.CoreV1().Secrets(pod.Namespace).Delete(context.Background(), k.Name, metav1.DeleteOptions{}); err != nil { + logger.Printf("Failed to delete secret %s: %v", k.Name, err) + } + } + }() + } + if c.Collector.Timeout == "" { + return runWithoutTimeout(ctx, c.BundlePath, c.ClientConfig, pod, c.Collector) + } + + timeout, err := time.ParseDuration(c.Collector.Timeout) + if err != nil { + return nil, errors.Wrap(err, "failed to parse timeout") + } + + errCh := make(chan error, 1) + resultCh := make(chan CollectorResult, 1) + + timeoutCtx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + go func() { + b, err := runWithoutTimeout(timeoutCtx, c.BundlePath, c.ClientConfig, pod, c.Collector) + if err != nil { + errCh <- err + } else { + resultCh <- b + } + }() + + select { + case <-time.After(timeout): + return nil, errors.New("timeout") + case result := <-resultCh: + return result, nil + case err := <-errCh: + return nil, err + } +} + +func runPodWithSpec(ctx context.Context, client *kubernetes.Clientset, runPodCollector *troubleshootv1beta2.RunPod) (*corev1.Pod, error) { + podLabels := make(map[string]string) + podLabels["troubleshoot-role"] = "run-collector" + + namespace := "default" + if runPodCollector.Namespace != "" { + namespace = runPodCollector.Namespace + } + + podName := "run-pod" + if runPodCollector.CollectorName != "" { + podName = runPodCollector.CollectorName + } else if runPodCollector.Name != "" { + podName = runPodCollector.Name + } + + pod := corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: podName, + Namespace: namespace, + Labels: podLabels, + }, + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Pod", + }, + Spec: runPodCollector.PodSpec, + } + + if runPodCollector.ImagePullSecret != nil && runPodCollector.ImagePullSecret.Data != nil { + secretName, err := createSecret(ctx, client, pod.Namespace, runPodCollector.ImagePullSecret) + if err != nil { + return nil, errors.Wrap(err, "failed to create secret") + } + pod.Spec.ImagePullSecrets = append(pod.Spec.ImagePullSecrets, corev1.LocalObjectReference{Name: secretName}) + } + + created, err := client.CoreV1().Pods(namespace).Create(ctx, &pod, metav1.CreateOptions{}) + if err != nil { + return nil, errors.Wrap(err, "failed to create pod") + } + + return created, nil +} + +func runWithoutTimeout(ctx context.Context, bundlePath string, clientConfig *rest.Config, pod *corev1.Pod, runPodCollector *troubleshootv1beta2.RunPod) (CollectorResult, error) { + client, err := kubernetes.NewForConfig(clientConfig) + if err != nil { + return nil, errors.Wrap(err, "failed create client from config") + } + + for { + status, err := client.CoreV1().Pods(pod.Namespace).Get(ctx, pod.Name, metav1.GetOptions{}) + if err != nil { + return nil, errors.Wrap(err, "failed to get pod") + } + if status.Status.Phase == corev1.PodRunning || + status.Status.Phase == corev1.PodFailed || + status.Status.Phase == corev1.PodSucceeded { + break + } + if status.Status.Phase == corev1.PodPending { + for _, v := range status.Status.ContainerStatuses { + if v.State.Waiting != nil && v.State.Waiting.Reason == "ImagePullBackOff" { + return nil, errors.Errorf("run pod aborted after getting pod status 'ImagePullBackOff'") + } + } + } + time.Sleep(time.Second * 1) + } + + output := NewResult() + + collectorName := runPodCollector.Name + + limits := troubleshootv1beta2.LogLimits{ + MaxLines: 10000, + } + podLogs, err := savePodLogs(ctx, bundlePath, client, *pod, collectorName, "", &limits, true) + if err != nil { + return nil, errors.Wrap(err, "failed to get pod logs") + } + + for k, v := range podLogs { + output[k] = v + } + + return output, nil +} + +func createSecret(ctx context.Context, client kubernetes.Interface, namespace string, imagePullSecret *troubleshootv1beta2.ImagePullSecrets) (string, error) { + if imagePullSecret.Data == nil { + return "", nil + } + + var out bytes.Buffer + data := make(map[string][]byte) + if imagePullSecret.SecretType != "kubernetes.io/dockerconfigjson" { + return "", errors.Errorf("ImagePullSecret must be of type: kubernetes.io/dockerconfigjson") + } + + // Check if required field in data exists + v, found := imagePullSecret.Data[".dockerconfigjson"] + if !found { + return "", errors.Errorf("Secret type kubernetes.io/dockerconfigjson requires argument \".dockerconfigjson\"") + } + if len(imagePullSecret.Data) > 1 { + return "", errors.Errorf("Secret type kubernetes.io/dockerconfigjson accepts only one argument \".dockerconfigjson\"") + } + // K8s client accepts only Json formated files as data, provided data must be decoded and indented + parsedConfig, err := base64.StdEncoding.DecodeString(v) + if err != nil { + return "", errors.Wrap(err, "Unable to decode data.") + } + err = json.Indent(&out, parsedConfig, "", "\t") + if err != nil { + return "", errors.Wrap(err, "Unable to parse encoded data.") + } + data[".dockerconfigjson"] = out.Bytes() + + secret := corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Secret", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: imagePullSecret.Name, + GenerateName: "troubleshoot", + Namespace: namespace, + Labels: map[string]string{ + "app.kubernetes.io/managed-by": "troubleshoot.sh", + }, + }, + Data: data, + Type: corev1.SecretType(imagePullSecret.SecretType), + } + + created, err := client.CoreV1().Secrets(namespace).Create(ctx, &secret, metav1.CreateOptions{}) + if err != nil { + return "", errors.Wrap(err, "failed to create secret") + } + + return created.Name, nil +} + +// RunPodOptions and RunPodReadyNodes currently only used for the Sysctl collector +// TODO: refactor sysctl collector and runPod collector to share the same code type RunPodOptions struct { Image string ImagePullPolicy string diff --git a/pkg/collect/secret.go b/pkg/collect/secret.go index 816101f7..cb650b71 100644 --- a/pkg/collect/secret.go +++ b/pkg/collect/secret.go @@ -14,6 +14,7 @@ import ( kuberneteserrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" ) type SecretOutput struct { @@ -25,28 +26,46 @@ type SecretOutput struct { Value string `json:"value,omitempty"` } -func Secret(ctx context.Context, c *Collector, secretCollector *troubleshootv1beta2.Secret, client kubernetes.Interface) (CollectorResult, error) { +type CollectSecret struct { + Collector *troubleshootv1beta2.Secret + BundlePath string + Namespace string + ClientConfig *rest.Config + Client kubernetes.Interface + ctx context.Context + RBACErrors +} + +func (c *CollectSecret) Title() string { + return collectorTitleOrDefault(c.Collector.CollectorMeta, "Secret") +} + +func (c *CollectSecret) IsExcluded() (bool, error) { + return isExcluded(c.Collector.Exclude) +} + +func (c *CollectSecret) Collect(progressChan chan<- interface{}) (CollectorResult, error) { output := NewResult() secrets := []corev1.Secret{} - if secretCollector.Name != "" { - secret, err := client.CoreV1().Secrets(secretCollector.Namespace).Get(ctx, secretCollector.Name, metav1.GetOptions{}) + if c.Collector.Name != "" { + secret, err := c.Client.CoreV1().Secrets(c.Collector.Namespace).Get(c.ctx, c.Collector.Name, metav1.GetOptions{}) if err != nil { if kuberneteserrors.IsNotFound(err) { - filePath, encoded, err := secretToOutput(secretCollector, nil, secretCollector.Name) + filePath, encoded, err := secretToOutput(c.Collector, nil, c.Collector.Name) if err != nil { - return output, errors.Wrapf(err, "collect secret %s", secretCollector.Name) + return output, errors.Wrapf(err, "collect secret %s", c.Collector.Name) } output.SaveResult(c.BundlePath, filePath, bytes.NewBuffer(encoded)) } - output.SaveResult(c.BundlePath, GetSecretErrorsFileName(secretCollector), marshalErrors([]string{err.Error()})) + output.SaveResult(c.BundlePath, GetSecretErrorsFileName(c.Collector), marshalErrors([]string{err.Error()})) return output, nil } secrets = append(secrets, *secret) - } else if len(secretCollector.Selector) > 0 { - ss, err := listSecretsForSelector(ctx, client, secretCollector.Namespace, secretCollector.Selector) + } else if len(c.Collector.Selector) > 0 { + ss, err := listSecretsForSelector(c.ctx, c.Client, c.Collector.Namespace, c.Collector.Selector) if err != nil { - output.SaveResult(c.BundlePath, GetSecretErrorsFileName(secretCollector), marshalErrors([]string{err.Error()})) + output.SaveResult(c.BundlePath, GetSecretErrorsFileName(c.Collector), marshalErrors([]string{err.Error()})) return output, nil } secrets = append(secrets, ss...) @@ -55,7 +74,7 @@ func Secret(ctx context.Context, c *Collector, secretCollector *troubleshootv1be } for _, secret := range secrets { - filePath, encoded, err := secretToOutput(secretCollector, &secret, secret.Name) + filePath, encoded, err := secretToOutput(c.Collector, &secret, secret.Name) if err != nil { return output, errors.Wrapf(err, "collect secret %s", secret.Name) } diff --git a/pkg/collect/secret_test.go b/pkg/collect/secret_test.go index 71e5c246..6d400580 100644 --- a/pkg/collect/secret_test.go +++ b/pkg/collect/secret_test.go @@ -223,8 +223,8 @@ func TestSecret(t *testing.T) { _, err := client.CoreV1().Secrets(secret.Namespace).Create(ctx, &secret, metav1.CreateOptions{}) require.NoError(t, err) } - c := &Collector{} - got, err := Secret(ctx, c, tt.secretCollector, client) + secretCollector := &CollectSecret{tt.secretCollector, "", "", nil, client, ctx, nil} + got, err := secretCollector.Collect(nil) if tt.wantErr { assert.Error(t, err) } else { diff --git a/pkg/collect/sysctl.go b/pkg/collect/sysctl.go index c8b62882..9a5d3257 100644 --- a/pkg/collect/sysctl.go +++ b/pkg/collect/sysctl.go @@ -8,31 +8,59 @@ import ( "github.com/pkg/errors" troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" + "github.com/replicatedhq/troubleshoot/pkg/k8sutil" "github.com/replicatedhq/troubleshoot/pkg/logger" kuberneteserrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" ) -func Sysctl(ctx context.Context, c *Collector, client kubernetes.Interface, collector *troubleshootv1beta2.Sysctl) (CollectorResult, error) { +type CollectSysctl struct { + Collector *troubleshootv1beta2.Sysctl + BundlePath string + Namespace string + ClientConfig *rest.Config + Client kubernetes.Interface + ctx context.Context + RBACErrors +} - if collector.Timeout != "" { - timeout, err := time.ParseDuration(collector.Timeout) +func (c *CollectSysctl) Title() string { + return collectorTitleOrDefault(c.Collector.CollectorMeta, "Sysctl") +} + +func (c *CollectSysctl) IsExcluded() (bool, error) { + return isExcluded(c.Collector.Exclude) +} + +func (c *CollectSysctl) Collect(progressChan chan<- interface{}) (CollectorResult, error) { + if c.Collector.Timeout != "" { + timeout, err := time.ParseDuration(c.Collector.Timeout) if err != nil { return nil, errors.Wrap(err, "parse timeout") } if timeout == 0 { timeout = time.Minute } - childCtx, cancel := context.WithTimeout(ctx, timeout) + childCtx, cancel := context.WithTimeout(c.ctx, timeout) defer cancel() - ctx = childCtx + c.ctx = childCtx + } + + if c.Collector.Namespace == "" { + c.Collector.Namespace = c.Namespace + } + if c.Collector.Namespace == "" { + kubeconfig := k8sutil.GetKubeconfig() + namespace, _, _ := kubeconfig.Namespace() + c.Collector.Namespace = namespace } runPodOptions := RunPodOptions{ - Image: collector.Image, - ImagePullPolicy: collector.ImagePullPolicy, - Namespace: collector.Namespace, + Image: c.Collector.Image, + ImagePullPolicy: c.Collector.ImagePullPolicy, + Namespace: c.Collector.Namespace, HostNetwork: true, } @@ -42,18 +70,18 @@ find /proc/sys/net/bridge -type f | while read f; do v=$(cat $f 2>/dev/null); ec ` runPodOptions.Command = []string{"sh", "-c", command} - if collector.ImagePullSecret != nil { - runPodOptions.ImagePullSecretName = collector.ImagePullSecret.Name + if c.Collector.ImagePullSecret != nil { + runPodOptions.ImagePullSecretName = c.Collector.ImagePullSecret.Name - if collector.ImagePullSecret.Data != nil { - secretName, err := createSecret(ctx, client, collector.Namespace, collector.ImagePullSecret) + if c.Collector.ImagePullSecret.Data != nil { + secretName, err := createSecret(c.ctx, c.Client, c.Collector.Namespace, c.Collector.ImagePullSecret) if err != nil { return nil, errors.Wrap(err, "create image pull secret") } defer func() { - err := client.CoreV1().Secrets(collector.Namespace).Delete(context.Background(), collector.ImagePullSecret.Name, metav1.DeleteOptions{}) + err := c.Client.CoreV1().Secrets(c.Collector.Namespace).Delete(context.Background(), c.Collector.ImagePullSecret.Name, metav1.DeleteOptions{}) if err != nil && !kuberneteserrors.IsNotFound(err) { - logger.Printf("Failed to delete secret %s: %v", collector.ImagePullSecret.Name, err) + logger.Printf("Failed to delete secret %s: %v", c.Collector.ImagePullSecret.Name, err) } }() @@ -61,7 +89,7 @@ find /proc/sys/net/bridge -type f | while read f; do v=$(cat $f 2>/dev/null); ec } } - results, err := RunPodsReadyNodes(ctx, client.CoreV1(), runPodOptions) + results, err := RunPodsReadyNodes(c.ctx, c.Client.CoreV1(), runPodOptions) if err != nil { return nil, err } diff --git a/pkg/preflight/collect.go b/pkg/preflight/collect.go index 27b31bcc..c7af543e 100644 --- a/pkg/preflight/collect.go +++ b/pkg/preflight/collect.go @@ -48,7 +48,7 @@ type CollectResult interface { type ClusterCollectResult struct { AllCollectedData map[string][]byte - Collectors collect.Collectors + Collectors []collect.Collector RemoteCollectors collect.RemoteCollectors isRBACAllowed bool Spec *troubleshootv1beta2.Preflight @@ -126,110 +126,117 @@ func Collect(opts CollectOpts, p *troubleshootv1beta2.Preflight) (CollectResult, collectSpecs = ensureCollectorInList(collectSpecs, troubleshootv1beta2.Collect{ClusterInfo: &troubleshootv1beta2.ClusterInfo{}}) collectSpecs = ensureCollectorInList(collectSpecs, troubleshootv1beta2.Collect{ClusterResources: &troubleshootv1beta2.ClusterResources{}}) + var allCollectors []collect.Collector + allCollectedData := make(map[string][]byte) - var collectors collect.Collectors - for _, desiredCollector := range collectSpecs { - collector := collect.Collector{ - Redact: true, - Collect: desiredCollector, - ClientConfig: opts.KubernetesRestConfig, - Namespace: opts.Namespace, - } - collectors = append(collectors, &collector) - } - - collectResult := ClusterCollectResult{ - Collectors: collectors, - Spec: p, - } - k8sClient, err := kubernetes.NewForConfig(opts.KubernetesRestConfig) if err != nil { - return collectResult, errors.Wrap(err, "failed to instantiate Kubernetes client") + return nil, errors.Wrap(err, "failed to instantiate Kubernetes client") } - if err := collectors.CheckRBAC(context.Background()); err != nil { - return collectResult, errors.Wrap(err, "failed to check RBAC for collectors") + for _, desiredCollector := range collectSpecs { + if collectorInterface, ok := collect.GetCollector(desiredCollector, "", opts.Namespace, opts.KubernetesRestConfig, k8sClient, nil); ok { + if collector, ok := collectorInterface.(collect.Collector); ok { + err := collector.CheckRBAC(context.Background(), collector, desiredCollector, opts.KubernetesRestConfig, opts.Namespace) + if err != nil { + return nil, errors.Wrap(err, "failed to check RBAC for collectors") + } + + if mergeCollector, ok := collectorInterface.(collect.MergeableCollector); ok { + allCollectors, err = mergeCollector.Merge(allCollectors) + if err != nil { + msg := fmt.Sprintf("failed to merge collector: %s: %s", collector.Title(), err) + opts.ProgressChan <- msg + } + } else { + allCollectors = append(allCollectors, collector) + } + } + } + } + + collectResult := ClusterCollectResult{ + Collectors: allCollectors, + Spec: p, } foundForbidden := false - for _, c := range collectors { - for _, e := range c.RBACErrors { + for _, c := range allCollectors { + for _, e := range c.GetRBACErrors() { foundForbidden = true opts.ProgressChan <- e } } if foundForbidden && !opts.IgnorePermissionErrors { - collectResult.isRBACAllowed = false - return collectResult, errors.New("insufficient permissions to run all collectors") + return nil, errors.New("insufficient permissions to run all collectors") } // generate a map of all collectors for atomic status messages collectorList := map[string]CollectorStatus{} - for _, collector := range collectors { - collectorList[collector.GetDisplayName()] = CollectorStatus{ + for _, collector := range allCollectors { + collectorList[collector.Title()] = CollectorStatus{ Status: "pending", } } - // Run preflights collectors synchronously - for i, collector := range collectors { - if len(collector.RBACErrors) > 0 { - // don't skip clusterResources collector due to RBAC issues - if collector.Collect.ClusterResources == nil { - collectorList[collector.GetDisplayName()] = CollectorStatus{ - Status: "skipped", - } - collectResult.isRBACAllowed = false // not failing, but going to report this - opts.ProgressChan <- fmt.Sprintf("skipping collector %s with insufficient RBAC permissions", collector.GetDisplayName()) + for i, collector := range allCollectors { + isExcluded, _ := collector.IsExcluded() + if isExcluded { + continue + } + + // skip collectors with RBAC errors unless its the ClusterResources collector + if collector.HasRBACErrors() { + if _, ok := collector.(*collect.CollectClusterResources); !ok { + opts.ProgressChan <- fmt.Sprintf("skipping collector %s with insufficient RBAC permissions", collector.Title()) opts.ProgressChan <- CollectProgress{ - CurrentName: collector.GetDisplayName(), + CurrentName: collector.Title(), CurrentStatus: "skipped", CompletedCount: i + 1, - TotalCount: len(collectors), + TotalCount: len(allCollectors), Collectors: collectorList, } continue } } - collectorList[collector.GetDisplayName()] = CollectorStatus{ + collectorList[collector.Title()] = CollectorStatus{ Status: "running", } opts.ProgressChan <- CollectProgress{ - CurrentName: collector.GetDisplayName(), + CurrentName: collector.Title(), CurrentStatus: "running", CompletedCount: i, - TotalCount: len(collectors), + TotalCount: len(allCollectors), Collectors: collectorList, } - result, err := collector.RunCollectorSync(opts.KubernetesRestConfig, k8sClient, nil) + result, err := collector.Collect(opts.ProgressChan) if err != nil { - collectorList[collector.GetDisplayName()] = CollectorStatus{ + collectorList[collector.Title()] = CollectorStatus{ Status: "failed", } - opts.ProgressChan <- errors.Errorf("failed to run collector %s: %v\n", collector.GetDisplayName(), err) + opts.ProgressChan <- errors.Errorf("failed to run collector: %s: %v", collector.Title(), err) opts.ProgressChan <- CollectProgress{ - CurrentName: collector.GetDisplayName(), + CurrentName: collector.Title(), CurrentStatus: "failed", CompletedCount: i + 1, - TotalCount: len(collectors), + TotalCount: len(allCollectors), Collectors: collectorList, } continue } - collectorList[collector.GetDisplayName()] = CollectorStatus{ + collectorList[collector.Title()] = CollectorStatus{ Status: "completed", } opts.ProgressChan <- CollectProgress{ - CurrentName: collector.GetDisplayName(), + CurrentName: collector.Title(), CurrentStatus: "completed", CompletedCount: i + 1, - TotalCount: len(collectors), + TotalCount: len(allCollectors), Collectors: collectorList, } @@ -239,6 +246,7 @@ func Collect(opts CollectOpts, p *troubleshootv1beta2.Preflight) (CollectResult, } collectResult.AllCollectedData = allCollectedData + return collectResult, nil } diff --git a/pkg/supportbundle/collect.go b/pkg/supportbundle/collect.go index 36251f8c..a3469518 100644 --- a/pkg/supportbundle/collect.go +++ b/pkg/supportbundle/collect.go @@ -7,7 +7,6 @@ import ( "fmt" "io" "os" - "time" "github.com/pkg/errors" analyze "github.com/replicatedhq/troubleshoot/pkg/analyze" @@ -16,7 +15,6 @@ import ( "github.com/replicatedhq/troubleshoot/pkg/convert" "github.com/replicatedhq/troubleshoot/pkg/version" "gopkg.in/yaml.v2" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" ) @@ -70,38 +68,45 @@ func runHostCollectors(hostCollectors []*troubleshootv1beta2.HostCollect, additi return collectResult, nil } -// TODO (dan): This is VERY similar to the Preflight collect package and should be refactored. func runCollectors(collectors []*troubleshootv1beta2.Collect, additionalRedactors *troubleshootv1beta2.Redactor, bundlePath string, opts SupportBundleCreateOpts) (collect.CollectorResult, error) { - collectSpecs := make([]*troubleshootv1beta2.Collect, 0) collectSpecs = append(collectSpecs, collectors...) collectSpecs = ensureCollectorInList(collectSpecs, troubleshootv1beta2.Collect{ClusterInfo: &troubleshootv1beta2.ClusterInfo{}}) collectSpecs = ensureCollectorInList(collectSpecs, troubleshootv1beta2.Collect{ClusterResources: &troubleshootv1beta2.ClusterResources{}}) - var cleanedCollectors collect.Collectors - for _, desiredCollector := range collectSpecs { - collector := collect.Collector{ - Redact: opts.Redact, - Collect: desiredCollector, - ClientConfig: opts.KubernetesRestConfig, - Namespace: opts.Namespace, - BundlePath: bundlePath, - } - cleanedCollectors = append(cleanedCollectors, &collector) - } + var allCollectors []collect.Collector + + allCollectedData := make(map[string][]byte) k8sClient, err := kubernetes.NewForConfig(opts.KubernetesRestConfig) if err != nil { return nil, errors.Wrap(err, "failed to instantiate Kubernetes client") } - if err := cleanedCollectors.CheckRBAC(context.Background()); err != nil { - return nil, errors.Wrap(err, "failed to check RBAC for collectors") + 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(context.Background(), collector, desiredCollector, opts.KubernetesRestConfig, opts.Namespace) + if err != nil { + return nil, errors.Wrap(err, "failed to check RBAC for collectors") + } + + if mergeCollector, ok := collectorInterface.(collect.MergeableCollector); ok { + allCollectors, err = mergeCollector.Merge(allCollectors) + if err != nil { + msg := fmt.Sprintf("failed to merge collector: %s: %s", collector.Title(), err) + opts.CollectorProgressCallback(opts.ProgressChan, msg) + } + } else { + allCollectors = append(allCollectors, collector) + } + } + } } foundForbidden := false - for _, c := range cleanedCollectors { - for _, e := range c.RBACErrors { + for _, c := range allCollectors { + for _, e := range c.GetRBACErrors() { foundForbidden = true opts.ProgressChan <- e } @@ -111,42 +116,47 @@ func runCollectors(collectors []*troubleshootv1beta2.Collect, additionalRedactor return nil, errors.New("insufficient permissions to run all collectors") } - globalRedactors := []*troubleshootv1beta2.Redact{} - if additionalRedactors != nil { - globalRedactors = additionalRedactors.Spec.Redactors - } + for _, collector := range allCollectors { + isExcluded, _ := collector.IsExcluded() + if isExcluded { + continue + } - if opts.SinceTime != nil { - applyLogSinceTime(*opts.SinceTime, &cleanedCollectors) - } - - result := collect.NewResult() - - // Run preflights collectors synchronously - for _, collector := range cleanedCollectors { - if len(collector.RBACErrors) > 0 { - // don't skip clusterResources collector due to RBAC issues - if collector.Collect.ClusterResources == nil { - msg := fmt.Sprintf("skipping collector %s with insufficient RBAC permissions", collector.GetDisplayName()) + // skip collectors with RBAC errors unless its the ClusterResources collector + if collector.HasRBACErrors() { + if _, ok := collector.(*collect.CollectClusterResources); !ok { + msg := fmt.Sprintf("skipping collector %s with insufficient RBAC permissions", collector.Title()) opts.CollectorProgressCallback(opts.ProgressChan, msg) continue } } - opts.CollectorProgressCallback(opts.ProgressChan, collector.GetDisplayName()) - - files, err := collector.RunCollectorSync(opts.KubernetesRestConfig, k8sClient, globalRedactors) + opts.ProgressChan <- fmt.Sprintf("[%s] Running collector...", collector.Title()) + result, err := collector.Collect(opts.ProgressChan) if err != nil { - opts.ProgressChan <- fmt.Errorf("failed to run collector %q: %v", collector.GetDisplayName(), err) - continue + opts.ProgressChan <- errors.Errorf("failed to run collector: %s: %v", collector.Title(), err) } - - for k, v := range files { - result[k] = v + for k, v := range result { + allCollectedData[k] = v } } - return result, nil + collectResult := allCollectedData + + globalRedactors := []*troubleshootv1beta2.Redact{} + if additionalRedactors != nil { + globalRedactors = additionalRedactors.Spec.Redactors + } + + if opts.Redact { + err := collect.RedactResult(bundlePath, collectResult, globalRedactors) + if err != nil { + err = errors.Wrap(err, "failed to redact") + return collectResult, err + } + } + + return collectResult, nil } func findFileName(basename, extension string) (string, error) { @@ -207,15 +217,3 @@ func getAnalysisFile(analyzeResults []*analyze.AnalyzeResult) (io.Reader, error) return bytes.NewBuffer(analysis), nil } - -func applyLogSinceTime(sinceTime time.Time, collectors *collect.Collectors) { - - for _, collector := range *collectors { - if collector.Collect.Logs != nil { - if collector.Collect.Logs.Limits == nil { - collector.Collect.Logs.Limits = new(troubleshootv1beta2.LogLimits) - } - collector.Collect.Logs.Limits.SinceTime = metav1.NewTime(sinceTime) - } - } -}