diff --git a/cmd/preflight/cli/root.go b/cmd/preflight/cli/root.go index 555d3b83..5d2004d8 100644 --- a/cmd/preflight/cli/root.go +++ b/cmd/preflight/cli/root.go @@ -4,7 +4,6 @@ import ( "os" "strings" - troubleshootv1beta1 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta1" "github.com/spf13/cobra" "github.com/spf13/viper" "k8s.io/cli-runtime/pkg/genericclioptions" @@ -64,16 +63,3 @@ func initConfig() { viper.SetEnvPrefix("PREFLIGHT") viper.AutomaticEnv() } - -func ensureCollectorInList(list []*troubleshootv1beta1.Collect, collector troubleshootv1beta1.Collect) []*troubleshootv1beta1.Collect { - for _, inList := range list { - if collector.ClusterResources != nil && inList.ClusterResources != nil { - return list - } - if collector.ClusterInfo != nil && inList.ClusterInfo != nil { - return list - } - } - - return append(list, &collector) -} diff --git a/cmd/preflight/cli/run.go b/cmd/preflight/cli/run.go index 16121c99..1cac5ca6 100644 --- a/cmd/preflight/cli/run.go +++ b/cmd/preflight/cli/run.go @@ -1,23 +1,18 @@ package cli import ( - "encoding/base64" - "encoding/json" "fmt" "io/ioutil" "net/http" "os" - "path/filepath" - "strings" "time" cursor "github.com/ahmetalpbalkan/go-cursor" "github.com/fatih/color" "github.com/pkg/errors" - analyzerunner "github.com/replicatedhq/troubleshoot/pkg/analyze" troubleshootv1beta1 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta1" troubleshootclientsetscheme "github.com/replicatedhq/troubleshoot/pkg/client/troubleshootclientset/scheme" - "github.com/replicatedhq/troubleshoot/pkg/collect" + "github.com/replicatedhq/troubleshoot/pkg/preflight" "github.com/spf13/viper" spin "github.com/tj/go-spin" "k8s.io/client-go/kubernetes/scheme" @@ -66,7 +61,7 @@ func runPreflights(v *viper.Viper, arg string) error { return errors.Wrapf(err, "failed to parse %s", arg) } - preflight := obj.(*troubleshootv1beta1.Preflight) + preflightSpec := obj.(*troubleshootv1beta1.Preflight) s := spin.New() finishedCh := make(chan bool, 1) @@ -98,48 +93,34 @@ func runPreflights(v *viper.Viper, arg string) error { close(finishedCh) }() - allCollectedData, err := runCollectors(v, *preflight, progressChan) + restConfig, err := KubernetesConfigFlags.ToRESTConfig() if err != nil { + return errors.Wrap(err, "failed to convert kube flags to rest config") + } + + collectOpts := preflight.CollectOpts{ + Namespace: v.GetString("namespace"), + IgnorePermissionErrors: v.GetBool("collect-without-permissions"), + ProgressChan: progressChan, + KubernetesRestConfig: restConfig, + } + + collectResults, err := preflight.Collect(collectOpts, preflightSpec) + if err != nil { + if !collectResults.IsRBACAllowed { + if preflightSpec.Spec.UploadResultsTo != "" { + err := uploadErrors(preflightSpec.Spec.UploadResultsTo, collectResults.Collectors) + if err != nil { + progressChan <- err + } + } + } return err } - getCollectedFileContents := func(fileName string) ([]byte, error) { - contents, ok := allCollectedData[fileName] - if !ok { - return nil, fmt.Errorf("file %s was not collected", fileName) - } - - return contents, nil - } - getChildCollectedFileContents := func(prefix string) (map[string][]byte, error) { - matching := make(map[string][]byte) - for k, v := range allCollectedData { - if strings.HasPrefix(k, prefix) { - matching[k] = v - } - } - - return matching, nil - } - - analyzeResults := []*analyzerunner.AnalyzeResult{} - for _, analyzer := range preflight.Spec.Analyzers { - analyzeResult, err := analyzerunner.Analyze(analyzer, getCollectedFileContents, getChildCollectedFileContents) - if err != nil { - analyzeResult = &analyzerunner.AnalyzeResult{ - IsFail: true, - Title: "Analyzer Failed", - Message: err.Error(), - } - } - - if analyzeResult != nil { - analyzeResults = append(analyzeResults, analyzeResult) - } - } - - if preflight.Spec.UploadResultsTo != "" { - err := uploadResults(preflight.Spec.UploadResultsTo, analyzeResults) + analyzeResults := collectResults.Analyze() + if preflightSpec.Spec.UploadResultsTo != "" { + err := uploadResults(preflightSpec.Spec.UploadResultsTo, analyzeResults) if err != nil { progressChan <- err } @@ -151,117 +132,8 @@ func runPreflights(v *viper.Viper, arg string) error { if len(analyzeResults) == 0 { return errors.New("no data has been collected") } - return showInteractiveResults(preflight.Name, analyzeResults) + return showInteractiveResults(preflightSpec.Name, analyzeResults) } - return showStdoutResults(v.GetString("format"), preflight.Name, analyzeResults) -} - -func runCollectors(v *viper.Viper, preflight troubleshootv1beta1.Preflight, progressChan chan interface{}) (map[string][]byte, error) { - collectSpecs := make([]*troubleshootv1beta1.Collect, 0, 0) - collectSpecs = append(collectSpecs, preflight.Spec.Collectors...) - collectSpecs = ensureCollectorInList(collectSpecs, troubleshootv1beta1.Collect{ClusterInfo: &troubleshootv1beta1.ClusterInfo{}}) - collectSpecs = ensureCollectorInList(collectSpecs, troubleshootv1beta1.Collect{ClusterResources: &troubleshootv1beta1.ClusterResources{}}) - - allCollectedData := make(map[string][]byte) - - config, err := KubernetesConfigFlags.ToRESTConfig() - if err != nil { - return nil, errors.Wrap(err, "failed to convert kube flags to rest config") - } - - var collectors collect.Collectors - for _, desiredCollector := range collectSpecs { - collector := collect.Collector{ - Redact: true, - Collect: desiredCollector, - ClientConfig: config, - Namespace: v.GetString("namespace"), - } - collectors = append(collectors, &collector) - } - - if err := collectors.CheckRBAC(); err != nil { - return nil, errors.Wrap(err, "failed to check RBAC for collectors") - } - - foundForbidden := false - for _, c := range collectors { - for _, e := range c.RBACErrors { - foundForbidden = true - progressChan <- e - } - } - - if foundForbidden && !v.GetBool("collect-without-permissions") { - if preflight.Spec.UploadResultsTo != "" { - err := uploadErrors(preflight.Spec.UploadResultsTo, collectors) - if err != nil { - progressChan <- err - } - } - return nil, errors.New("insufficient permissions to run all collectors") - } - - // Run preflights collectors synchronously - for _, collector := range collectors { - if len(collector.RBACErrors) > 0 { - // don't skip clusterResources collector due to RBAC issues - if collector.Collect.ClusterResources == nil { - progressChan <- fmt.Sprintf("skipping collector %s with insufficient RBAC permissions", collector.GetDisplayName()) - continue - } - } - - result, err := collector.RunCollectorSync() - if err != nil { - progressChan <- errors.Errorf("failed to run collector %s: %v\n", collector.GetDisplayName(), err) - continue - } - - if result != nil { - output, err := parseCollectorOutput(string(result)) - if err != nil { - progressChan <- errors.Errorf("failed to parse collector output %s: %v\n", collector.GetDisplayName(), err) - continue - } - for k, v := range output { - allCollectedData[k] = v - } - } - } - - return allCollectedData, nil -} - -func parseCollectorOutput(output string) (map[string][]byte, error) { - input := make(map[string]interface{}) - files := make(map[string][]byte) - if err := json.Unmarshal([]byte(output), &input); err != nil { - return nil, err - } - - for filename, maybeContents := range input { - fileDir, fileName := filepath.Split(filename) - - switch maybeContents.(type) { - case string: - decoded, err := base64.StdEncoding.DecodeString(maybeContents.(string)) - if err != nil { - return nil, err - } - files[filepath.Join(fileDir, fileName)] = decoded - - case map[string]interface{}: - for k, v := range maybeContents.(map[string]interface{}) { - decoded, err := base64.StdEncoding.DecodeString(v.(string)) - if err != nil { - return nil, err - } - files[filepath.Join(fileDir, fileName, k)] = decoded - } - } - } - - return files, nil + return showStdoutResults(v.GetString("format"), preflightSpec.Name, analyzeResults) } diff --git a/cmd/preflight/cli/upload_results.go b/cmd/preflight/cli/upload_results.go index 7168c7cf..1eacdd6d 100644 --- a/cmd/preflight/cli/upload_results.go +++ b/cmd/preflight/cli/upload_results.go @@ -8,33 +8,15 @@ import ( "github.com/pkg/errors" analyzerunner "github.com/replicatedhq/troubleshoot/pkg/analyze" "github.com/replicatedhq/troubleshoot/pkg/collect" + "github.com/replicatedhq/troubleshoot/pkg/preflight" ) -type UploadPreflightResult struct { - IsFail bool `json:"isFail,omitempty"` - IsWarn bool `json:"isWarn,omitempty"` - IsPass bool `json:"isPass,omitempty"` - - Title string `json:"title"` - Message string `json:"message"` - URI string `json:"uri,omitempty"` -} - -type UploadPreflightError struct { - Error string `json:"error"` -} - -type UploadPreflightResults struct { - Results []*UploadPreflightResult `json:"results,omitempty"` - Errors []*UploadPreflightError `json:"errors,omitempty"` -} - func uploadResults(uri string, analyzeResults []*analyzerunner.AnalyzeResult) error { - uploadPreflightResults := &UploadPreflightResults{ - Results: []*UploadPreflightResult{}, + uploadPreflightResults := &preflight.UploadPreflightResults{ + Results: []*preflight.UploadPreflightResult{}, } for _, analyzeResult := range analyzeResults { - uploadPreflightResult := &UploadPreflightResult{ + uploadPreflightResult := &preflight.UploadPreflightResult{ IsFail: analyzeResult.IsFail, IsWarn: analyzeResult.IsWarn, IsPass: analyzeResult.IsPass, @@ -50,23 +32,23 @@ func uploadResults(uri string, analyzeResults []*analyzerunner.AnalyzeResult) er } func uploadErrors(uri string, collectors collect.Collectors) error { - errors := []*UploadPreflightError{} + errors := []*preflight.UploadPreflightError{} for _, collector := range collectors { for _, e := range collector.RBACErrors { - errors = append(errors, &UploadPreflightError{ + errors = append(errors, &preflight.UploadPreflightError{ Error: e.Error(), }) } } - results := &UploadPreflightResults{ + results := &preflight.UploadPreflightResults{ Errors: errors, } return upload(uri, results) } -func upload(uri string, payload *UploadPreflightResults) error { +func upload(uri string, payload *preflight.UploadPreflightResults) error { b, err := json.Marshal(payload) if err != nil { return errors.Wrap(err, "failed to marshal payload") diff --git a/pkg/preflight/analyze.go b/pkg/preflight/analyze.go new file mode 100644 index 00000000..1c5c1e17 --- /dev/null +++ b/pkg/preflight/analyze.go @@ -0,0 +1,48 @@ +package preflight + +import ( + "fmt" + "strings" + + analyze "github.com/replicatedhq/troubleshoot/pkg/analyze" +) + +// Analyze runs the analyze phase of preflight checks +func (c CollectResult) Analyze() []*analyze.AnalyzeResult { + getCollectedFileContents := func(fileName string) ([]byte, error) { + contents, ok := c.AllCollectedData[fileName] + if !ok { + return nil, fmt.Errorf("file %s was not collected", fileName) + } + + return contents, nil + } + getChildCollectedFileContents := func(prefix string) (map[string][]byte, error) { + matching := make(map[string][]byte) + for k, v := range c.AllCollectedData { + if strings.HasPrefix(k, prefix) { + matching[k] = v + } + } + + return matching, nil + } + + analyzeResults := []*analyze.AnalyzeResult{} + for _, analyzer := range c.Spec.Spec.Analyzers { + analyzeResult, err := analyze.Analyze(analyzer, getCollectedFileContents, getChildCollectedFileContents) + if err != nil { + analyzeResult = &analyze.AnalyzeResult{ + IsFail: true, + Title: "Analyzer Failed", + Message: err.Error(), + } + } + + if analyzeResult != nil { + analyzeResults = append(analyzeResults, analyzeResult) + } + } + + return analyzeResults +} diff --git a/pkg/preflight/collect.go b/pkg/preflight/collect.go new file mode 100644 index 00000000..14e0786b --- /dev/null +++ b/pkg/preflight/collect.go @@ -0,0 +1,147 @@ +package preflight + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "path/filepath" + + "github.com/pkg/errors" + troubleshootv1beta1 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta1" + "github.com/replicatedhq/troubleshoot/pkg/collect" + "k8s.io/client-go/rest" +) + +type CollectOpts struct { + Namespace string + IgnorePermissionErrors bool + KubernetesRestConfig *rest.Config + ProgressChan chan interface{} +} + +type CollectResult struct { + AllCollectedData map[string][]byte + Collectors collect.Collectors + IsRBACAllowed bool + Spec *troubleshootv1beta1.Preflight +} + +// Collect runs the collection phase of preflight checks +func Collect(opts CollectOpts, p *troubleshootv1beta1.Preflight) (CollectResult, error) { + collectSpecs := make([]*troubleshootv1beta1.Collect, 0, 0) + collectSpecs = append(collectSpecs, p.Spec.Collectors...) + collectSpecs = ensureCollectorInList(collectSpecs, troubleshootv1beta1.Collect{ClusterInfo: &troubleshootv1beta1.ClusterInfo{}}) + collectSpecs = ensureCollectorInList(collectSpecs, troubleshootv1beta1.Collect{ClusterResources: &troubleshootv1beta1.ClusterResources{}}) + + 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 := CollectResult{ + Collectors: collectors, + Spec: p, + } + + if err := collectors.CheckRBAC(); err != nil { + return collectResult, errors.Wrap(err, "failed to check RBAC for collectors") + } + + foundForbidden := false + for _, c := range collectors { + for _, e := range c.RBACErrors { + foundForbidden = true + opts.ProgressChan <- e + } + } + + if foundForbidden && !opts.IgnorePermissionErrors { + collectResult.IsRBACAllowed = false + return collectResult, errors.New("insufficient permissions to run all collectors") + } + + // Run preflights collectors synchronously + for _, collector := range collectors { + if len(collector.RBACErrors) > 0 { + // don't skip clusterResources collector due to RBAC issues + if collector.Collect.ClusterResources == nil { + collectResult.IsRBACAllowed = false // not failing, but going to report this + opts.ProgressChan <- fmt.Sprintf("skipping collector %s with insufficient RBAC permissions", collector.GetDisplayName()) + continue + } + } + + result, err := collector.RunCollectorSync() + if err != nil { + opts.ProgressChan <- errors.Errorf("failed to run collector %s: %v\n", collector.GetDisplayName(), err) + continue + } + + if result != nil { + output, err := parseCollectorOutput(string(result)) + if err != nil { + opts.ProgressChan <- errors.Errorf("failed to parse collector output %s: %v\n", collector.GetDisplayName(), err) + continue + } + for k, v := range output { + allCollectedData[k] = v + } + } + } + + collectResult.AllCollectedData = allCollectedData + return collectResult, nil +} + +func parseCollectorOutput(output string) (map[string][]byte, error) { + input := make(map[string]interface{}) + files := make(map[string][]byte) + if err := json.Unmarshal([]byte(output), &input); err != nil { + return nil, err + } + + for filename, maybeContents := range input { + fileDir, fileName := filepath.Split(filename) + + switch maybeContents.(type) { + case string: + decoded, err := base64.StdEncoding.DecodeString(maybeContents.(string)) + if err != nil { + return nil, err + } + files[filepath.Join(fileDir, fileName)] = decoded + + case map[string]interface{}: + for k, v := range maybeContents.(map[string]interface{}) { + decoded, err := base64.StdEncoding.DecodeString(v.(string)) + if err != nil { + return nil, err + } + files[filepath.Join(fileDir, fileName, k)] = decoded + } + } + } + + return files, nil +} + +func ensureCollectorInList(list []*troubleshootv1beta1.Collect, collector troubleshootv1beta1.Collect) []*troubleshootv1beta1.Collect { + for _, inList := range list { + if collector.ClusterResources != nil && inList.ClusterResources != nil { + return list + } + if collector.ClusterInfo != nil && inList.ClusterInfo != nil { + return list + } + } + + return append(list, &collector) +} diff --git a/pkg/preflight/server.go b/pkg/preflight/server.go deleted file mode 100644 index 68166793..00000000 --- a/pkg/preflight/server.go +++ /dev/null @@ -1,132 +0,0 @@ -package preflight - -import ( - "context" - "fmt" - "time" - - corev1 "k8s.io/api/core/v1" - kuberneteserrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - runtime "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/intstr" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" -) - -type PreflightServerOptions struct { - ImageName string - PullPolicy string - - Name string - Namespace string - - OwnerReference metav1.Object -} - -func CreatePreflightServer(client client.Client, scheme *runtime.Scheme, options PreflightServerOptions) (*corev1.Pod, *corev1.Service, error) { - name := fmt.Sprintf("%s-%s", options.Name, "preflight") - namespacedName := types.NamespacedName{ - Name: name, - Namespace: options.Namespace, - } - - found := &corev1.Pod{} - err := client.Get(context.Background(), namespacedName, found) - if err == nil || !kuberneteserrors.IsNotFound(err) { - return nil, nil, err - } - - imageName := "replicated/troubleshoot:latest" - imagePullPolicy := corev1.PullAlways - - if options.ImageName != "" { - imageName = options.ImageName - } - if options.PullPolicy != "" { - imagePullPolicy = corev1.PullPolicy(options.PullPolicy) - } - - podLabels := make(map[string]string) - podLabels["preflight"] = options.Name - podLabels["troubleshoot-role"] = "preflight" - - pod := corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: options.Namespace, - Labels: podLabels, - }, - TypeMeta: metav1.TypeMeta{ - APIVersion: "v1", - Kind: "Pod", - }, - Spec: corev1.PodSpec{ - RestartPolicy: corev1.RestartPolicyNever, - Containers: []corev1.Container{ - { - Image: imageName, - ImagePullPolicy: imagePullPolicy, - Name: "preflight", - Command: []string{"preflight"}, - Args: []string{"server"}, - Ports: []corev1.ContainerPort{ - { - Name: "http", - ContainerPort: 8000, - }, - }, - }, - }, - }, - } - - if scheme != nil { - if err := controllerutil.SetControllerReference(options.OwnerReference, &pod, scheme); err != nil { - return nil, nil, err - } - } - - if err := client.Create(context.Background(), &pod); err != nil { - return nil, nil, err - } - - service := corev1.Service{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: options.Namespace, - }, - TypeMeta: metav1.TypeMeta{ - APIVersion: "v1", - Kind: "Service", - }, - Spec: corev1.ServiceSpec{ - Selector: podLabels, - Type: corev1.ServiceTypeClusterIP, - Ports: []corev1.ServicePort{ - { - Name: "http", - Port: 8000, - TargetPort: intstr.FromInt(8000), - }, - }, - }, - } - - if scheme != nil { - if err := controllerutil.SetControllerReference(options.OwnerReference, &service, scheme); err != nil { - return nil, nil, err - } - } - - if err := client.Create(context.Background(), &service); err != nil { - return nil, nil, err - } - - // wait for the server to be ready - // TODO - time.Sleep(time.Second * 5) - - return &pod, &service, nil -} diff --git a/pkg/preflight/types.go b/pkg/preflight/types.go new file mode 100644 index 00000000..f4b3a9e7 --- /dev/null +++ b/pkg/preflight/types.go @@ -0,0 +1,20 @@ +package preflight + +type UploadPreflightResult struct { + IsFail bool `json:"isFail,omitempty"` + IsWarn bool `json:"isWarn,omitempty"` + IsPass bool `json:"isPass,omitempty"` + + Title string `json:"title"` + Message string `json:"message"` + URI string `json:"uri,omitempty"` +} + +type UploadPreflightError struct { + Error string `json:"error"` +} + +type UploadPreflightResults struct { + Results []*UploadPreflightResult `json:"results,omitempty"` + Errors []*UploadPreflightError `json:"errors,omitempty"` +}