Refactor in-clusters collectors to use struct per collector (#670)

refactor in-clusters collectors to use struct per collector
This commit is contained in:
Diamon Wiggins
2022-10-03 13:53:05 -04:00
committed by GitHub
parent 44ae409081
commit c7b84ad1e5
30 changed files with 1100 additions and 827 deletions

View File

@@ -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(),
})

View File

@@ -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
}

View File

@@ -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")

View File

@@ -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")

View File

@@ -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]

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -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 {

View File

@@ -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
}
}
}

View File

@@ -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")
}

View File

@@ -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
}

View File

@@ -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()}
}

View File

@@ -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
}

View File

@@ -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{

View File

@@ -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

View File

@@ -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")
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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
}

View File

@@ -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"
}

View File

@@ -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")
}

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)
}
}
}