Repository: replicatedhq/troubleshoot
Pull request #99 (2019-12-18 23:53:14)

Adding a text analyzer
This text analyzer will consume the collector name and file name along with a given regex pattern to check if a matching pattern exists in a given file. Based on the regex match it will return success or failure.


Patch:
diff --git a/.github/workflows/preflight.yaml b/.github/workflows/preflight.yaml
deleted file mode 100644
index 1e3d2841..00000000
--- a/.github/workflows/preflight.yaml
+++ /dev/null
@@ -1,35 +0,0 @@
-name: "Run preflights on a cluster"
-on: [push]
-
-jobs:
-  compile-preflight:
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/setup-go@v1
-        with:
-          go-version: '1.12.14'
-      - name: setup env
-        run: |
-          echo "::set-env name=GOPATH::$(go env GOPATH)"
-          echo "::add-path::$(go env GOPATH)/bin"
-        shell: bash
-      - uses: actions/checkout@master
-      - run: make preflight
-      - uses: actions/upload-artifact@v1
-        with:
-          name: preflight
-          path: bin/preflight
-
-  run-preflights:
-    runs-on: ubuntu-latest
-    needs: compile-preflight
-    steps:
-      - name: Download preflight binary
-        uses: actions/download-artifact@v1
-        with:
-          name: preflight
-          path: bin/
-      - uses: engineerd/setup-kind@v0.2.0
-      - run: chmod +x bin/preflight
-      - run: bin/preflight --interactive=false --format=json https://preflight.replicated.com
-
diff --git a/Makefile b/Makefile
index d13a8cfd..b7b3246d 100644
--- a/Makefile
+++ b/Makefile
@@ -4,57 +4,35 @@ IMG ?= controller:latest
 export GO111MODULE=on
 export GOPROXY=https://proxy.golang.org
 
-SHELL := /bin/bash -o pipefail
-VERSION_PACKAGE = github.com/replicatedhq/troubleshoot/pkg/version
-VERSION ?=`git describe --tags --dirty`
-DATE=`date -u +"%Y-%m-%dT%H:%M:%SZ"`
-
-GIT_TREE = $(shell git rev-parse --is-inside-work-tree 2>/dev/null)
-ifneq "$(GIT_TREE)" ""
-define GIT_UPDATE_INDEX_CMD
-git update-index --assume-unchanged
-endef
-define GIT_SHA
-`git rev-parse HEAD`
-endef
-else
-define GIT_UPDATE_INDEX_CMD
-echo "Not a git repo, skipping git update-index"
-endef
-define GIT_SHA
-""
-endef
-endif
-
-define LDFLAGS
--ldflags "\
-	-X ${VERSION_PACKAGE}.version=${VERSION} \
-	-X ${VERSION_PACKAGE}.gitSHA=${GIT_SHA} \
-	-X ${VERSION_PACKAGE}.buildTime=${DATE} \
-"
-endef
-
-all: test
+all: test manager
 
 .PHONY: ffi
 ffi: fmt vet
-	go build ${LDFLAGS} -o bin/troubleshoot.so -buildmode=c-shared ffi/main.go
+	go build -o bin/troubleshoot.so -buildmode=c-shared ffi/main.go
 
 # Run tests
 test: generate fmt vet manifests
 	go test ./pkg/... ./cmd/... -coverprofile cover.out
 
+.PHONY: manager
+manager: generate fmt vet
+	go build -o bin/manager github.com/replicatedhq/troubleshoot/cmd/manager
+
 .PHONY: support-bundle
 support-bundle: generate fmt vet
-	go build ${LDFLAGS} -o bin/support-bundle github.com/replicatedhq/troubleshoot/cmd/troubleshoot
+	go build -o bin/support-bundle github.com/replicatedhq/troubleshoot/cmd/troubleshoot
 
 .PHONY: preflight
 preflight: generate fmt vet
-	go build ${LDFLAGS} -o bin/preflight github.com/replicatedhq/troubleshoot/cmd/preflight
+	go build -o bin/preflight github.com/replicatedhq/troubleshoot/cmd/preflight
 
 .PHONY: analyze
 analyze: generate fmt vet
-	go build ${LDFLAGS} -o bin/analyze github.com/replicatedhq/troubleshoot/cmd/analyze
+	go build -o bin/analyze github.com/replicatedhq/troubleshoot/cmd/analyze
+
+.PHONY: run
+run: generate fmt vet
+	TROUBLESHOOT_EXTERNAL_MANAGER=1 go run ./cmd/manager/main.go
 
 .PHONY: install
 install: manifests
@@ -101,24 +79,27 @@ else
 CLIENT_GEN=$(shell which client-gen)
 endif
 
-.PHONY: release
-release: export GITHUB_TOKEN = $(shell echo ${GITHUB_TOKEN_TROUBLESHOOT})
-release:
-	curl -sL https://git.io/goreleaser | bash -s -- --rm-dist --config deploy/.goreleaser.yml
-
 .PHONY: snapshot-release
 snapshot-release:
 	curl -sL https://git.io/goreleaser | bash -s -- --rm-dist --snapshot --config deploy/.goreleaser.snapshot.yml
 	docker push replicated/troubleshoot:alpha
 	docker push replicated/preflight:alpha
+	docker push replicated/troubleshoot-manager:alpha
+
+.PHONY: release
+release: export GITHUB_TOKEN = $(shell echo ${GITHUB_TOKEN_TROUBLESHOOT})
+release:
+	curl -sL https://git.io/goreleaser | bash -s -- --rm-dist --config deploy/.goreleaser.yml
 
 .PHONY: local-release
 local-release:
 	curl -sL https://git.io/goreleaser | bash -s -- --rm-dist --snapshot --config deploy/.goreleaser.local.yml
 	docker tag replicated/troubleshoot:alpha localhost:32000/troubleshoot:alpha
 	docker tag replicated/preflight:alpha localhost:32000/preflight:alpha
+	docker tag replicated/troubleshoot-manager:alpha localhost:32000/troubleshoot-manager:alpha
 	docker push localhost:32000/troubleshoot:alpha
 	docker push localhost:32000/preflight:alpha
+	docker push localhost:32000/troubleshoot-manager:alpha
 
 .PHONY: run-preflight
 run-preflight: preflight
diff --git a/cmd/collector/cli/root.go b/cmd/collector/cli/root.go
new file mode 100644
index 00000000..a4b7df6c
--- /dev/null
+++ b/cmd/collector/cli/root.go
@@ -0,0 +1,39 @@
+package cli
+
+import (
+	"fmt"
+	"os"
+	"strings"
+
+	"github.com/spf13/cobra"
+	"github.com/spf13/viper"
+)
+
+func RootCmd() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:          "collector",
+		Short:        "Run the cluster-side server for bundle collection",
+		Long:         ``,
+		SilenceUsage: true,
+	}
+
+	cobra.OnInitialize(initConfig)
+
+	cmd.AddCommand(Run())
+	cmd.AddCommand(Server())
+
+	viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
+	return cmd
+}
+
+func InitAndExecute() {
+	if err := RootCmd().Execute(); err != nil {
+		fmt.Println(err)
+		os.Exit(1)
+	}
+}
+
+func initConfig() {
+	viper.SetEnvPrefix("TROUBLESHOOT")
+	viper.AutomaticEnv()
+}
diff --git a/cmd/collector/cli/run.go b/cmd/collector/cli/run.go
new file mode 100644
index 00000000..1deb144b
--- /dev/null
+++ b/cmd/collector/cli/run.go
@@ -0,0 +1,58 @@
+package cli
+
+import (
+	"fmt"
+	"io/ioutil"
+
+	"github.com/replicatedhq/troubleshoot/pkg/collect"
+	"github.com/spf13/cobra"
+	"github.com/spf13/viper"
+)
+
+func Run() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "run",
+		Short: "run a single collector",
+		Long:  `...`,
+		PreRun: func(cmd *cobra.Command, args []string) {
+			viper.BindPFlag("collector", cmd.Flags().Lookup("collector"))
+			viper.BindPFlag("redact", cmd.Flags().Lookup("redact"))
+		},
+		RunE: func(cmd *cobra.Command, args []string) error {
+			v := viper.GetViper()
+
+			specContents, err := ioutil.ReadFile(v.GetString("collector"))
+			if err != nil {
+				return err
+			}
+
+			c, err := collect.ParseSpec(string(specContents))
+			if err != nil {
+				return err
+			}
+
+			collector := collect.Collector{
+				Collect:   c,
+				Redact:    v.GetBool("redact"),
+				Namespace: v.GetString("namespace"),
+			}
+			b, err := collector.RunCollectorSync()
+			if err != nil {
+				return err
+			}
+
+			fmt.Printf("%s", b)
+
+			return nil
+		},
+	}
+
+	cmd.Flags().String("collector", "", "path to a single collector spec to collect")
+	cmd.Flags().Bool("redact", true, "enable/disable default redactions")
+
+	cmd.MarkFlagRequired("collector")
+
+	viper.BindPFlags(cmd.Flags())
+
+	return cmd
+}
diff --git a/cmd/collector/cli/server.go b/cmd/collector/cli/server.go
new file mode 100644
index 00000000..09037078
--- /dev/null
+++ b/cmd/collector/cli/server.go
@@ -0,0 +1,43 @@
+package cli
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"os/signal"
+
+	"github.com/replicatedhq/troubleshoot/pkg/server"
+	"github.com/spf13/cobra"
+	"github.com/spf13/viper"
+)
+
+func Server() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:    "server",
+		Short:  "run the http server",
+		Hidden: true,
+		Long:   `...`,
+		PreRun: func(cmd *cobra.Command, args []string) {
+			viper.BindPFlag("port", cmd.Flags().Lookup("port"))
+		},
+		RunE: func(cmd *cobra.Command, args []string) error {
+			v := viper.GetViper()
+
+			server.ServeCollector(context.Background(), fmt.Sprintf(":%d", v.GetInt("port")))
+
+			c := make(chan os.Signal, 1)
+			signal.Notify(c, os.Interrupt)
+
+			select {
+			case <-c:
+				return nil
+			}
+		},
+	}
+
+	cmd.Flags().Int("port", 8000, "port to listen on")
+
+	viper.BindPFlags(cmd.Flags())
+
+	return cmd
+}
diff --git a/cmd/collector/main.go b/cmd/collector/main.go
new file mode 100644
index 00000000..f53cfd8a
--- /dev/null
+++ b/cmd/collector/main.go
@@ -0,0 +1,7 @@
+package main
+
+import "github.com/replicatedhq/troubleshoot/cmd/collector/cli"
+
+func main() {
+	cli.InitAndExecute()
+}
diff --git a/cmd/manager/main.go b/cmd/manager/main.go
new file mode 100644
index 00000000..b792dd5a
--- /dev/null
+++ b/cmd/manager/main.go
@@ -0,0 +1,84 @@
+/*
+Copyright 2019 Replicated, Inc..
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package main
+
+import (
+	"flag"
+	"os"
+
+	"github.com/replicatedhq/troubleshoot/pkg/apis"
+	"github.com/replicatedhq/troubleshoot/pkg/controller"
+	"github.com/replicatedhq/troubleshoot/pkg/webhook"
+	_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
+	"sigs.k8s.io/controller-runtime/pkg/client/config"
+	"sigs.k8s.io/controller-runtime/pkg/manager"
+	logf "sigs.k8s.io/controller-runtime/pkg/runtime/log"
+	"sigs.k8s.io/controller-runtime/pkg/runtime/signals"
+)
+
+func main() {
+	var metricsAddr string
+	flag.StringVar(&metricsAddr, "metrics-addr", ":8088", "The address the metric endpoint binds to.")
+	flag.Parse()
+	logf.SetLogger(logf.ZapLogger(false))
+	log := logf.Log.WithName("entrypoint")
+
+	// Get a config to talk to the apiserver
+	log.Info("setting up client for manager")
+	cfg, err := config.GetConfig()
+	if err != nil {
+		log.Error(err, "unable to set up client config")
+		os.Exit(1)
+	}
+
+	// Create a new Cmd to provide shared dependencies and start components
+	log.Info("setting up manager")
+	mgr, err := manager.New(cfg, manager.Options{MetricsBindAddress: metricsAddr})
+	if err != nil {
+		log.Error(err, "unable to set up overall controller manager")
+		os.Exit(1)
+	}
+
+	log.Info("Registering Components.")
+
+	// Setup Scheme for all resources
+	log.Info("setting up scheme")
+	if err := apis.AddToScheme(mgr.GetScheme()); err != nil {
+		log.Error(err, "unable add APIs to scheme")
+		os.Exit(1)
+	}
+
+	// Setup all Controllers
+	log.Info("Setting up controller")
+	if err := controller.AddToManager(mgr); err != nil {
+		log.Error(err, "unable to register controllers to the manager")
+		os.Exit(1)
+	}
+
+	log.Info("setting up webhooks")
+	if err := webhook.AddToManager(mgr); err != nil {
+		log.Error(err, "unable to register webhooks to the manager")
+		os.Exit(1)
+	}
+
+	// Start the Cmd
+	log.Info("Starting the Cmd.")
+	if err := mgr.Start(signals.SetupSignalHandler()); err != nil {
+		log.Error(err, "unable to run the manager")
+		os.Exit(1)
+	}
+}
diff --git a/cmd/preflight/cli/root.go b/cmd/preflight/cli/root.go
index e33843e2..caf1a5e2 100644
--- a/cmd/preflight/cli/root.go
+++ b/cmd/preflight/cli/root.go
@@ -18,7 +18,6 @@ var (
 func RootCmd() *cobra.Command {
 	cmd := &cobra.Command{
 		Use:   "preflight [url]",
-		Args:  cobra.MinimumNArgs(1),
 		Short: "Run and retrieve preflight checks in a cluster",
 		Long: `A preflight check is a set of validations that can and should be run to ensure
 that a cluster meets the requirements to run an application.`,
@@ -27,14 +26,17 @@ that a cluster meets the requirements to run an application.`,
 		},
 		RunE: func(cmd *cobra.Command, args []string) error {
 			v := viper.GetViper()
-			return runPreflights(v, args[0])
+
+			if len(args) == 0 {
+				return runPreflightsCRD(v)
+			}
+
+			return runPreflightsNoCRD(v, args[0])
 		},
 	}
 
 	cobra.OnInitialize(initConfig)
 
-	cmd.AddCommand(VersionCmd())
-
 	cmd.Flags().Bool("interactive", true, "interactive preflights")
 	cmd.Flags().String("format", "human", "output format, one of human, json, yaml. only used when interactive is set to false")
 	cmd.Flags().String("preflight", "", "name of the preflight to use")
diff --git a/cmd/preflight/cli/run_crd.go b/cmd/preflight/cli/run_crd.go
new file mode 100644
index 00000000..bbf3a7f4
--- /dev/null
+++ b/cmd/preflight/cli/run_crd.go
@@ -0,0 +1,107 @@
+package cli
+
+import (
+	"fmt"
+	"time"
+
+	"github.com/pkg/errors"
+	troubleshootv1beta1 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta1"
+	"github.com/replicatedhq/troubleshoot/pkg/k8sutil"
+	"github.com/spf13/viper"
+	kuberneteserrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+func runPreflightsCRD(v *viper.Viper) error {
+	troubleshootClient, err := createTroubleshootK8sClient(KubernetesConfigFlags)
+	if err != nil {
+		return err
+	}
+
+	preflightName := v.GetString("preflight")
+	if preflightName == "" {
+		preflights, err := troubleshootClient.Preflights(v.GetString("namespace")).List(metav1.ListOptions{})
+		if err != nil {
+			return err
+		}
+
+		if len(preflights.Items) == 1 {
+			preflightName = preflights.Items[0].Name
+		}
+	}
+
+	if preflightName == "" {
+		return errors.New("unable to preflight, try using the --preflight flags")
+	}
+
+	// generate a unique name
+	now := time.Now()
+	suffix := fmt.Sprintf("%d", now.Unix())
+
+	preflightJobName := fmt.Sprintf("%s-job-%s", preflightName, suffix[len(suffix)-4:])
+	preflightJob := troubleshootv1beta1.PreflightJob{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      preflightJobName,
+			Namespace: v.GetString("namespace"),
+		},
+		TypeMeta: metav1.TypeMeta{
+			APIVersion: "v1",
+			Kind:       "preflightjob.troubleshoot.replicated.com",
+		},
+		Spec: troubleshootv1beta1.PreflightJobSpec{
+			Preflight: troubleshootv1beta1.PreflightRef{
+				Name:      preflightName,
+				Namespace: v.GetString("namespace"),
+			},
+			Image:                    v.GetString("image"),
+			ImagePullPolicy:          v.GetString("pullpolicy"),
+			CollectorImage:           v.GetString("collector-image"),
+			CollectorImagePullPolicy: v.GetString("collector-pullpolicy"),
+		},
+	}
+	if _, err := troubleshootClient.PreflightJobs(v.GetString("namespace")).Create(&preflightJob); err != nil {
+		return err
+	}
+
+	// Poll the status of the Custom Resource for it to include a callback
+	var found *troubleshootv1beta1.PreflightJob
+	start := time.Now()
+	for {
+		current, err := troubleshootClient.PreflightJobs(v.GetString("namespace")).Get(preflightJobName, metav1.GetOptions{})
+		if err != nil && kuberneteserrors.IsNotFound(err) {
+			continue
+		} else if err != nil {
+			return err
+		}
+
+		if current.Status.IsServerReady {
+			found = current
+			break
+		}
+
+		if time.Now().Sub(start) > time.Duration(time.Second*10) {
+			return errors.New("preflightjob failed to start")
+		}
+
+		time.Sleep(time.Millisecond * 200)
+	}
+
+	config, err := KubernetesConfigFlags.ToRESTConfig()
+	if err != nil {
+		return errors.Wrap(err, "failed to convert kube flags to rest config")
+	}
+
+	stopChan, err := k8sutil.PortForward(config, 8000, 8000, found.Status.ServerPodNamespace, found.Status.ServerPodName)
+	if err != nil {
+		return err
+	}
+
+	if err := receivePreflightResults(found.Namespace, found.Name); err != nil {
+		return err
+	}
+
+	// Write
+
+	close(stopChan)
+	return nil
+}
diff --git a/cmd/preflight/cli/run.go b/cmd/preflight/cli/run_nocrd.go
similarity index 91%
rename from cmd/preflight/cli/run.go
rename to cmd/preflight/cli/run_nocrd.go
index a5f9f89a..395cf554 100644
--- a/cmd/preflight/cli/run.go
+++ b/cmd/preflight/cli/run_nocrd.go
@@ -22,7 +22,7 @@ import (
 	"gopkg.in/yaml.v2"
 )
 
-func runPreflights(v *viper.Viper, arg string) error {
+func runPreflightsNoCRD(v *viper.Viper, arg string) error {
 	fmt.Print(cursor.Hide())
 	defer fmt.Print(cursor.Show())
 
@@ -112,9 +112,7 @@ func runPreflights(v *viper.Viper, arg string) error {
 			continue
 		}
 
-		if analyzeResult != nil {
-			analyzeResults = append(analyzeResults, analyzeResult)
-		}
+		analyzeResults = append(analyzeResults, analyzeResult)
 	}
 
 	finishedCh <- true
@@ -126,7 +124,7 @@ func runPreflights(v *viper.Viper, arg string) error {
 		return showInteractiveResults(preflight.Name, analyzeResults)
 	}
 
-	return showStdoutResults(v.GetString("format"), preflight.Name, analyzeResults)
+	return showStdoutResults(preflight.Name, analyzeResults)
 }
 
 func runCollectors(v *viper.Viper, preflight troubleshootv1beta1.Preflight) (map[string][]byte, error) {
@@ -158,14 +156,12 @@ func runCollectors(v *viper.Viper, preflight troubleshootv1beta1.Preflight) (map
 			return nil, errors.Wrap(err, "failed to run collector")
 		}
 
-		if result != nil {
-			output, err := parseCollectorOutput(string(result))
-			if err != nil {
-				return nil, errors.Wrap(err, "failed to parse collector output")
-			}
-			for k, v := range output {
-				allCollectedData[k] = v
-			}
+		output, err := parseCollectorOutput(string(result))
+		if err != nil {
+			return nil, errors.Wrap(err, "failed to parse collector output")
+		}
+		for k, v := range output {
+			allCollectedData[k] = v
 		}
 	}
 
diff --git a/cmd/preflight/cli/stdout_results.go b/cmd/preflight/cli/stdout_results.go
index 028c3a83..c0b552da 100644
--- a/cmd/preflight/cli/stdout_results.go
+++ b/cmd/preflight/cli/stdout_results.go
@@ -1,30 +1,20 @@
 package cli
 
 import (
-	"encoding/json"
 	"fmt"
 
-	"github.com/pkg/errors"
 	analyzerunner "github.com/replicatedhq/troubleshoot/pkg/analyze"
 )
 
-func showStdoutResults(format string, preflightName string, analyzeResults []*analyzerunner.AnalyzeResult) error {
-	if format == "human" {
-		return showStdoutResultsHuman(preflightName, analyzeResults)
-	} else if format == "json" {
-		return showStdoutResultsJSON(preflightName, analyzeResults)
-	}
-
-	return errors.Errorf("unknown output format: %q", format)
-}
+var failed = false
 
-func showStdoutResultsHuman(preflightName string, analyzeResults []*analyzerunner.AnalyzeResult) error {
-	var failed bool
+func showStdoutResults(preflightName string, analyzeResults []*analyzerunner.AnalyzeResult) error {
+	fmt.Printf("\n=== TEST   %s\n", preflightName)
+	for _, analyzeResult := range analyzeResults {
+		fmt.Printf("=== RUN:   %s\n", analyzeResult.Title)
+	}
 	for _, analyzeResult := range analyzeResults {
-		testResultfailed := outputResult(analyzeResult)
-		if testResultfailed {
-			failed = true
-		}
+		outputResult(analyzeResult)
 	}
 	if failed {
 		fmt.Printf("--- FAIL   %s\n", preflightName)
@@ -36,51 +26,7 @@ func showStdoutResultsHuman(preflightName string, analyzeResults []*analyzerunne
 	return nil
 }
 
-func showStdoutResultsJSON(preflightName string, analyzeResults []*analyzerunner.AnalyzeResult) error {
-	type ResultOutput struct {
-		Title   string `json:"title"`
-		Message string `json:"message"`
-		URI     string `json:"uri,omitempty"`
-	}
-	type Output struct {
-		Pass []ResultOutput `json:"pass,omitempty"`
-		Warn []ResultOutput `json:"warn,omitempty"`
-		Fail []ResultOutput `json:"fail,omitempty"`
-	}
-
-	output := Output{
-		Pass: []ResultOutput{},
-		Warn: []ResultOutput{},
-		Fail: []ResultOutput{},
-	}
-
-	for _, analyzeResult := range analyzeResults {
-		resultOutput := ResultOutput{
-			Title:   analyzeResult.Title,
-			Message: analyzeResult.Message,
-			URI:     analyzeResult.URI,
-		}
-
-		if analyzeResult.IsPass {
-			output.Pass = append(output.Pass, resultOutput)
-		} else if analyzeResult.IsWarn {
-			output.Warn = append(output.Warn, resultOutput)
-		} else if analyzeResult.IsFail {
-			output.Fail = append(output.Fail, resultOutput)
-		}
-	}
-
-	b, err := json.MarshalIndent(output, "", "  ")
-	if err != nil {
-		return errors.Wrap(err, "failed to marshal results")
-	}
-
-	fmt.Printf("%s\n", b)
-
-	return nil
-}
-
-func outputResult(analyzeResult *analyzerunner.AnalyzeResult) bool {
+func outputResult(analyzeResult *analyzerunner.AnalyzeResult) {
 	if analyzeResult.IsPass {
 		fmt.Printf("   --- PASS %s\n", analyzeResult.Title)
 		fmt.Printf("      --- %s\n", analyzeResult.Message)
@@ -88,9 +34,8 @@ func outputResult(analyzeResult *analyzerunner.AnalyzeResult) bool {
 		fmt.Printf("   --- WARN: %s\n", analyzeResult.Title)
 		fmt.Printf("      --- %s\n", analyzeResult.Message)
 	} else if analyzeResult.IsFail {
+		failed = true
 		fmt.Printf("   --- FAIL: %s\n", analyzeResult.Title)
 		fmt.Printf("      --- %s\n", analyzeResult.Message)
-		return true
 	}
-	return false
 }
diff --git a/cmd/preflight/cli/version.go b/cmd/preflight/cli/version.go
deleted file mode 100644
index 3793847a..00000000
--- a/cmd/preflight/cli/version.go
+++ /dev/null
@@ -1,23 +0,0 @@
-package cli
-
-import (
-	"fmt"
-
-	"github.com/spf13/cobra"
-
-	"github.com/replicatedhq/troubleshoot/pkg/version"
-)
-
-func VersionCmd() *cobra.Command {
-	cmd := &cobra.Command{
-		Use:   "version",
-		Short: "Print the current version and exit",
-		Long:  `Print the current version and exit`,
-		RunE: func(cmd *cobra.Command, args []string) error {
-			fmt.Printf("Replicated Preflight %s\n", version.Version())
-
-			return nil
-		},
-	}
-	return cmd
-}
diff --git a/cmd/troubleshoot/cli/root.go b/cmd/troubleshoot/cli/root.go
index 66a60878..fe7fae46 100644
--- a/cmd/troubleshoot/cli/root.go
+++ b/cmd/troubleshoot/cli/root.go
@@ -30,14 +30,18 @@ from a server that can be used to assist when troubleshooting a server.`,
 			v := viper.GetViper()
 
 			logger.SetQuiet(v.GetBool("quiet"))
-			return runTroubleshoot(v, args[0])
+
+			if len(args) == 0 {
+				return runTroubleshootCRD(v)
+			}
+
+			return runTroubleshootNoCRD(v, args[0])
 		},
 	}
 
 	cobra.OnInitialize(initConfig)
 
 	cmd.AddCommand(Analyze())
-	cmd.AddCommand(VersionCmd())
 
 	cmd.Flags().String("collectors", "", "name of the collectors to use")
 	cmd.Flags().String("image", "", "the full name of the collector image to use")
diff --git a/cmd/troubleshoot/cli/run_crd.go b/cmd/troubleshoot/cli/run_crd.go
new file mode 100644
index 00000000..d628137e
--- /dev/null
+++ b/cmd/troubleshoot/cli/run_crd.go
@@ -0,0 +1,106 @@
+package cli
+
+import (
+	"fmt"
+	"time"
+
+	"github.com/pkg/errors"
+	troubleshootv1beta1 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta1"
+	"github.com/replicatedhq/troubleshoot/pkg/k8sutil"
+	"github.com/spf13/viper"
+	kuberneteserrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+func runTroubleshootCRD(v *viper.Viper) error {
+	troubleshootClient, err := createTroubleshootK8sClient(KubernetesConfigFlags)
+	if err != nil {
+		return err
+	}
+
+	collectorName := v.GetString("collectors")
+	if collectorName == "" {
+		collectors, err := troubleshootClient.Collectors(v.GetString("namespace")).List(metav1.ListOptions{})
+		if err != nil {
+			return err
+		}
+
+		if len(collectors.Items) == 1 {
+			collectorName = collectors.Items[0].Name
+		}
+	}
+
+	if collectorName == "" {
+		return errors.New("unknown collectors, try using the --collectors flags")
+	}
+
+	// generate a unique name
+	now := time.Now()
+	suffix := fmt.Sprintf("%d", now.Unix())
+
+	collectorJobName := fmt.Sprintf("%s-job-%s", collectorName, suffix[len(suffix)-4:])
+	collectorJob := troubleshootv1beta1.CollectorJob{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      collectorJobName,
+			Namespace: v.GetString("namespace"),
+		},
+		TypeMeta: metav1.TypeMeta{
+			APIVersion: "v1",
+			Kind:       "collectorjob.troubleshoot.replicated.com",
+		},
+		Spec: troubleshootv1beta1.CollectorJobSpec{
+			Collector: troubleshootv1beta1.CollectorRef{
+				Name:      collectorName,
+				Namespace: v.GetString("namespace"),
+			},
+			Image:           v.GetString("image"),
+			ImagePullPolicy: v.GetString("pullpolicy"),
+			Redact:          v.GetBool("redact"),
+		},
+	}
+	if _, err := troubleshootClient.CollectorJobs(v.GetString("namespace")).Create(&collectorJob); err != nil {
+		return err
+	}
+
+	// Poll the status of the Custom Resource for it to include a callback
+	var found *troubleshootv1beta1.CollectorJob
+	start := time.Now()
+	for {
+		current, err := troubleshootClient.CollectorJobs(v.GetString("namespace")).Get(collectorJobName, metav1.GetOptions{})
+		if err != nil && kuberneteserrors.IsNotFound(err) {
+			continue
+		} else if err != nil {
+			return err
+		}
+
+		if current.Status.IsServerReady {
+			found = current
+			break
+		}
+
+		if time.Now().Sub(start) > time.Duration(time.Second*10) {
+			return errors.New("collectorjob failed to start")
+		}
+
+		time.Sleep(time.Millisecond * 200)
+	}
+
+	config, err := KubernetesConfigFlags.ToRESTConfig()
+	if err != nil {
+		return errors.Wrap(err, "failed to convert kube flags to rest config")
+	}
+
+	stopChan, err := k8sutil.PortForward(config, 8000, 8000, found.Status.ServerPodNamespace, found.Status.ServerPodName)
+	if err != nil {
+		return err
+	}
+
+	if err := receiveSupportBundle(found.Namespace, found.Name); err != nil {
+		return err
+	}
+
+	// Write
+
+	close(stopChan)
+	return nil
+}
diff --git a/cmd/troubleshoot/cli/run.go b/cmd/troubleshoot/cli/run_nocrd.go
similarity index 96%
rename from cmd/troubleshoot/cli/run.go
rename to cmd/troubleshoot/cli/run_nocrd.go
index 59a26aca..be472de0 100644
--- a/cmd/troubleshoot/cli/run.go
+++ b/cmd/troubleshoot/cli/run_nocrd.go
@@ -22,7 +22,7 @@ import (
 	"gopkg.in/yaml.v2"
 )
 
-func runTroubleshoot(v *viper.Viper, arg string) error {
+func runTroubleshootNoCRD(v *viper.Viper, arg string) error {
 	fmt.Print(cursor.Hide())
 	defer fmt.Print(cursor.Show())
 
@@ -179,12 +179,10 @@ func runCollectors(v *viper.Viper, collector troubleshootv1beta1.Collector, prog
 			continue
 		}
 
-		if result != nil {
-			err = parseAndSaveCollectorOutput(string(result), bundlePath)
-			if err != nil {
-				progressChan <- fmt.Errorf("failed to parse collector spec %q: %v", collector.GetDisplayName(), err)
-				continue
-			}
+		err = parseAndSaveCollectorOutput(string(result), bundlePath)
+		if err != nil {
+			progressChan <- fmt.Errorf("failed to parse collector spec %q: %v", collector.GetDisplayName(), err)
+			continue
 		}
 	}
 
diff --git a/cmd/troubleshoot/cli/version.go b/cmd/troubleshoot/cli/version.go
index 8e793230..3a029755 100644
--- a/cmd/troubleshoot/cli/version.go
+++ b/cmd/troubleshoot/cli/version.go
@@ -1,31 +1,12 @@
 package cli
 
 import (
-	"fmt"
-
-	"io/ioutil"
-	"path/filepath"
-
 	troubleshootv1beta1 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta1"
-	"github.com/replicatedhq/troubleshoot/pkg/version"
-	"github.com/spf13/cobra"
 	"gopkg.in/yaml.v2"
+	"io/ioutil"
+	"path/filepath"
 )
 
-func VersionCmd() *cobra.Command {
-	cmd := &cobra.Command{
-		Use:   "version",
-		Short: "Print the current version and exit",
-		Long:  `Print the current version and exit`,
-		RunE: func(cmd *cobra.Command, args []string) error {
-			fmt.Printf("Replicated Preflight %s\n", version.Version())
-
-			return nil
-		},
-	}
-	return cmd
-}
-
 const VersionFilename = "version.yaml"
 
 func writeVersionFile(path string) error {
diff --git a/config/crds/troubleshoot.replicated.com_analyzers.yaml b/config/crds/troubleshoot.replicated.com_analyzers.yaml
index b1311088..552de142 100644
--- a/config/crds/troubleshoot.replicated.com_analyzers.yaml
+++ b/config/crds/troubleshoot.replicated.com_analyzers.yaml
@@ -398,8 +398,6 @@ spec:
                       properties:
                         checkName:
                           type: string
-                        exclude:
-                          type: boolean
                         outcomes:
                           items:
                             properties:
@@ -439,8 +437,6 @@ spec:
                       properties:
                         checkName:
                           type: string
-                        exclude:
-                          type: boolean
                         outcomes:
                           items:
                             properties:
@@ -482,8 +478,6 @@ spec:
                           type: string
                         customResourceDefinitionName:
                           type: string
-                        exclude:
-                          type: boolean
                         outcomes:
                           items:
                             properties:
@@ -524,8 +518,6 @@ spec:
                       properties:
                         checkName:
                           type: string
-                        exclude:
-                          type: boolean
                         name:
                           type: string
                         namespace:
@@ -571,8 +563,6 @@ spec:
                       properties:
                         checkName:
                           type: string
-                        exclude:
-                          type: boolean
                         outcomes:
                           items:
                             properties:
@@ -612,8 +602,6 @@ spec:
                       properties:
                         checkName:
                           type: string
-                        exclude:
-                          type: boolean
                         outcomes:
                           items:
                             properties:
@@ -656,8 +644,6 @@ spec:
                       properties:
                         checkName:
                           type: string
-                        exclude:
-                          type: boolean
                         ingressName:
                           type: string
                         namespace:
@@ -703,8 +689,6 @@ spec:
                       properties:
                         checkName:
                           type: string
-                        exclude:
-                          type: boolean
                         key:
                           type: string
                         namespace:
@@ -752,8 +736,6 @@ spec:
                       properties:
                         checkName:
                           type: string
-                        exclude:
-                          type: boolean
                         name:
                           type: string
                         namespace:
@@ -799,8 +781,6 @@ spec:
                       properties:
                         checkName:
                           type: string
-                        exclude:
-                          type: boolean
                         outcomes:
                           items:
                             properties:
diff --git a/config/crds/troubleshoot.replicated.com_collectors.yaml b/config/crds/troubleshoot.replicated.com_collectors.yaml
index fe1037e2..317ed448 100644
--- a/config/crds/troubleshoot.replicated.com_collectors.yaml
+++ b/config/crds/troubleshoot.replicated.com_collectors.yaml
@@ -420,22 +420,8 @@ spec:
                 items:
                   properties:
                     clusterInfo:
-                      properties:
-                        collectorName:
-                          type: string
-                        when:
-                          type: boolean
-                      required:
-                      - when
                       type: object
                     clusterResources:
-                      properties:
-                        collectorName:
-                          type: string
-                        when:
-                          type: boolean
-                      required:
-                      - when
                       type: object
                     copy:
                       properties:
@@ -453,38 +439,10 @@ spec:
                           items:
                             type: string
                           type: array
-                        when:
-                          type: boolean
                       required:
                       - selector
                       - namespace
                       - containerPath
-                      - when
-                      type: object
-                    data:
-                      properties:
-                        collectorName:
-                          type: string
-                        data:
-                          type: string
-                        name:
-                          type: string
-                        when:
-                          type: boolean
-                      required:
-                      - data
-                      - when
-                      type: object
-                    data:
-                      properties:
-                        collectorName:
-                          type: string
-                        data:
-                          type: string
-                        name:
-                          type: string
-                      required:
-                      - data
                       type: object
                     exec:
                       properties:
@@ -510,12 +468,9 @@ spec:
                           type: array
                         timeout:
                           type: string
-                        when:
-                          type: boolean
                       required:
                       - selector
                       - namespace
-                      - when
                       type: object
                     http:
                       properties:
@@ -566,10 +521,6 @@ spec:
                           required:
                           - url
                           type: object
-                        when:
-                          type: boolean
-                      required:
-                      - when
                       type: object
                     logs:
                       properties:
@@ -595,11 +546,8 @@ spec:
                           items:
                             type: string
                           type: array
-                        when:
-                          type: boolean
                       required:
                       - selector
-                      - when
                       type: object
                     run:
                       properties:
@@ -623,12 +571,9 @@ spec:
                           type: string
                         timeout:
                           type: string
-                        when:
-                          type: boolean
                       required:
                       - namespace
                       - image
-                      - when
                       type: object
                     secret:
                       properties:
@@ -642,11 +587,8 @@ spec:
                           type: string
                         namespace:
                           type: string
-                        when:
-                          type: boolean
                       required:
                       - name
-                      - when
                       type: object
                   type: object
                 type: array
diff --git a/config/crds/troubleshoot.replicated.com_preflights.yaml b/config/crds/troubleshoot.replicated.com_preflights.yaml
index 64f937ac..2146cf2c 100644
--- a/config/crds/troubleshoot.replicated.com_preflights.yaml
+++ b/config/crds/troubleshoot.replicated.com_preflights.yaml
@@ -398,8 +398,6 @@ spec:
                       properties:
                         checkName:
                           type: string
-                        exclude:
-                          type: boolean
                         outcomes:
                           items:
                             properties:
@@ -439,8 +437,6 @@ spec:
                       properties:
                         checkName:
                           type: string
-                        exclude:
-                          type: boolean
                         outcomes:
                           items:
                             properties:
@@ -482,8 +478,6 @@ spec:
                           type: string
                         customResourceDefinitionName:
                           type: string
-                        exclude:
-                          type: boolean
                         outcomes:
                           items:
                             properties:
@@ -524,8 +518,6 @@ spec:
                       properties:
                         checkName:
                           type: string
-                        exclude:
-                          type: boolean
                         name:
                           type: string
                         namespace:
@@ -571,8 +563,6 @@ spec:
                       properties:
                         checkName:
                           type: string
-                        exclude:
-                          type: boolean
                         outcomes:
                           items:
                             properties:
@@ -612,8 +602,6 @@ spec:
                       properties:
                         checkName:
                           type: string
-                        exclude:
-                          type: boolean
                         outcomes:
                           items:
                             properties:
@@ -656,8 +644,6 @@ spec:
                       properties:
                         checkName:
                           type: string
-                        exclude:
-                          type: boolean
                         ingressName:
                           type: string
                         namespace:
@@ -703,8 +689,6 @@ spec:
                       properties:
                         checkName:
                           type: string
-                        exclude:
-                          type: boolean
                         key:
                           type: string
                         namespace:
@@ -752,8 +736,6 @@ spec:
                       properties:
                         checkName:
                           type: string
-                        exclude:
-                          type: boolean
                         name:
                           type: string
                         namespace:
@@ -799,8 +781,6 @@ spec:
                       properties:
                         checkName:
                           type: string
-                        exclude:
-                          type: boolean
                         outcomes:
                           items:
                             properties:
@@ -845,22 +825,8 @@ spec:
                 items:
                   properties:
                     clusterInfo:
-                      properties:
-                        collectorName:
-                          type: string
-                        when:
-                          type: boolean
-                      required:
-                      - when
                       type: object
                     clusterResources:
-                      properties:
-                        collectorName:
-                          type: string
-                        when:
-                          type: boolean
-                      required:
-                      - when
                       type: object
                     copy:
                       properties:
@@ -878,38 +844,10 @@ spec:
                           items:
                             type: string
                           type: array
-                        when:
-                          type: boolean
                       required:
                       - selector
                       - namespace
                       - containerPath
-                      - when
-                      type: object
-                    data:
-                      properties:
-                        collectorName:
-                          type: string
-                        data:
-                          type: string
-                        name:
-                          type: string
-                        when:
-                          type: boolean
-                      required:
-                      - data
-                      - when
-                      type: object
-                    data:
-                      properties:
-                        collectorName:
-                          type: string
-                        data:
-                          type: string
-                        name:
-                          type: string
-                      required:
-                      - data
                       type: object
                     exec:
                       properties:
@@ -935,12 +873,9 @@ spec:
                           type: array
                         timeout:
                           type: string
-                        when:
-                          type: boolean
                       required:
                       - selector
                       - namespace
-                      - when
                       type: object
                     http:
                       properties:
@@ -991,10 +926,6 @@ spec:
                           required:
                           - url
                           type: object
-                        when:
-                          type: boolean
-                      required:
-                      - when
                       type: object
                     logs:
                       properties:
@@ -1020,11 +951,8 @@ spec:
                           items:
                             type: string
                           type: array
-                        when:
-                          type: boolean
                       required:
                       - selector
-                      - when
                       type: object
                     run:
                       properties:
@@ -1048,12 +976,9 @@ spec:
                           type: string
                         timeout:
                           type: string
-                        when:
-                          type: boolean
                       required:
                       - namespace
                       - image
-                      - when
                       type: object
                     secret:
                       properties:
@@ -1067,11 +992,8 @@ spec:
                           type: string
                         namespace:
                           type: string
-                        when:
-                          type: boolean
                       required:
                       - name
-                      - when
                       type: object
                   type: object
                 type: array
diff --git a/config/crds/zz_generated.deepcopy.go b/config/crds/zz_generated.deepcopy.go
index d75c49ae..8dbd570b 100644
--- a/config/crds/zz_generated.deepcopy.go
+++ b/config/crds/zz_generated.deepcopy.go
@@ -332,7 +332,6 @@ func (in *AnalyzerStatus) DeepCopy() *AnalyzerStatus {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ClusterInfo) DeepCopyInto(out *ClusterInfo) {
 	*out = *in
-	out.CollectorMeta = in.CollectorMeta
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterInfo.
@@ -348,7 +347,6 @@ func (in *ClusterInfo) DeepCopy() *ClusterInfo {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ClusterResources) DeepCopyInto(out *ClusterResources) {
 	*out = *in
-	out.CollectorMeta = in.CollectorMeta
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterResources.
@@ -421,11 +419,6 @@ func (in *Collect) DeepCopyInto(out *Collect) {
 		*out = new(Exec)
 		(*in).DeepCopyInto(*out)
 	}
-	if in.Data != nil {
-		in, out := &in.Data, &out.Data
-		*out = new(Data)
-		**out = **in
-	}
 	if in.Copy != nil {
 		in, out := &in.Copy, &out.Copy
 		*out = new(Copy)
@@ -769,22 +762,6 @@ func (in *CustomResourceDefinition) DeepCopy() *CustomResourceDefinition {
 	return out
 }
 
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *Data) DeepCopyInto(out *Data) {
-	*out = *in
-	out.CollectorMeta = in.CollectorMeta
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Data.
-func (in *Data) DeepCopy() *Data {
-	if in == nil {
-		return nil
-	}
-	out := new(Data)
-	in.DeepCopyInto(out)
-	return out
-}
-
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *DeploymentStatus) DeepCopyInto(out *DeploymentStatus) {
 	*out = *in
diff --git a/deploy/.goreleaser.local.yml b/deploy/.goreleaser.local.yml
index db2e7262..b2297829 100644
--- a/deploy/.goreleaser.local.yml
+++ b/deploy/.goreleaser.local.yml
@@ -4,6 +4,22 @@ release:
     owner: replicatedhq
     name: troubleshoot
 builds:
+  - id: collector
+    goos:
+      - linux
+    goarch:
+      - amd64
+    env:
+      - CGO_ENABLED=0
+    main: cmd/collector/main.go
+    ldflags: -s -w
+      -X github.com/replicatedhq/troubleshoot/pkg/version.version={{.Version}}
+      -X github.com/replicatedhq/troubleshoot/pkg/version.gitSHA={{.Commit}}
+      -X github.com/replicatedhq/troubleshoot/pkg/version.buildTime={{.Date}}
+      -extldflags "-static"
+    flags: -tags netgo -installsuffix netgo
+    binary: collector
+    hooks: {}
   - id: preflight
     goos:
       - linux
@@ -36,6 +52,22 @@ builds:
     flags: -tags netgo -installsuffix netgo
     binary: support-bundle
     hooks: {}
+  - id: manager
+    goos:
+      - linux
+    goarch:
+      - amd64
+    env:
+      - CGO_ENABLED=0
+    main: cmd/manager/main.go
+    ldflags: -s -w
+      -X github.com/replicatedhq/troubleshoot/pkg/version.version={{.Version}}
+      -X github.com/replicatedhq/troubleshoot/pkg/version.gitSHA={{.Commit}}
+      -X github.com/replicatedhq/troubleshoot/pkg/version.buildTime={{.Date}}
+      -extldflags "-static"
+    flags: -tags netgo -installsuffix netgo
+    binary: manager
+    hooks: {}
 archives:
   - id: tar
     format: tar.gz
@@ -54,13 +86,20 @@ dockers:
     image_templates:
       - "replicated/troubleshoot:alpha"
     binaries:
+      - collector
       - support-bundle
       - preflight
   - dockerfile: ./deploy/Dockerfile.troubleshoot
     image_templates:
       - "replicated/preflight:alpha"
     binaries:
+      - collector
       - support-bundle
       - preflight
+  - dockerfile: ./deploy/Dockerfile.manager
+    image_templates:
+      - "replicated/troubleshoot-manager:alpha"
+    binaries:
+      - manager
 snapshot:
   name_template: SNAPSHOT-{{ .Commit }}
diff --git a/deploy/.goreleaser.snapshot.yml b/deploy/.goreleaser.snapshot.yml
index 551dcdb0..33a5cbc0 100644
--- a/deploy/.goreleaser.snapshot.yml
+++ b/deploy/.goreleaser.snapshot.yml
@@ -16,6 +16,22 @@ builds:
     flags: -buildmode=c-shared
     binary: troubleshoot.so
     hooks: {}
+  - id: collector
+    goos:
+      - linux
+    goarch:
+      - amd64
+    env:
+      - CGO_ENABLED=0
+    main: cmd/collector/main.go
+    ldflags: -s -w
+      -X github.com/replicatedhq/troubleshoot/pkg/version.version={{.Version}}
+      -X github.com/replicatedhq/troubleshoot/pkg/version.gitSHA={{.Commit}}
+      -X github.com/replicatedhq/troubleshoot/pkg/version.buildTime={{.Date}}
+      -extldflags "-static"
+    flags: -tags netgo -installsuffix netgo
+    binary: collector
+    hooks: {}
   - id: preflight
     goos:
       - linux
@@ -52,6 +68,22 @@ builds:
     flags: -tags netgo -installsuffix netgo
     binary: support-bundle
     hooks: {}
+  - id: manager
+    goos:
+      - linux
+    goarch:
+      - amd64
+    env:
+      - CGO_ENABLED=0
+    main: cmd/manager/main.go
+    ldflags: -s -w
+      -X github.com/replicatedhq/troubleshoot/pkg/version.version={{.Version}}
+      -X github.com/replicatedhq/troubleshoot/pkg/version.gitSHA={{.Commit}}
+      -X github.com/replicatedhq/troubleshoot/pkg/version.buildTime={{.Date}}
+      -extldflags "-static"
+    flags: -tags netgo -installsuffix netgo
+    binary: manager
+    hooks: {}
 archives:
   - id: so
     builds:
@@ -74,13 +106,20 @@ dockers:
     image_templates:
       - "replicated/troubleshoot:alpha"
     binaries:
+      - collector
       - support-bundle
       - preflight
   - dockerfile: ./deploy/Dockerfile.troubleshoot
     image_templates:
       - "replicated/preflight:alpha"
     binaries:
+      - collector
       - support-bundle
       - preflight
+  - dockerfile: ./deploy/Dockerfile.manager
+    image_templates:
+      - "replicated/troubleshoot-manager:alpha"
+    binaries:
+      - manager
 snapshot:
   name_template: SNAPSHOT-{{ .Commit }}
diff --git a/deploy/.goreleaser.yml b/deploy/.goreleaser.yml
index e6e5f720..5199292c 100644
--- a/deploy/.goreleaser.yml
+++ b/deploy/.goreleaser.yml
@@ -16,6 +16,22 @@ builds:
     flags: -buildmode=c-shared
     binary: troubleshoot.so
     hooks: {}
+  - id: collector
+    goos:
+      - linux
+    goarch:
+      - amd64
+    env:
+      - CGO_ENABLED=0
+    main: cmd/collector/main.go
+    ldflags: -s -w
+      -X github.com/replicatedhq/troubleshoot/pkg/version.version={{.Version}}
+      -X github.com/replicatedhq/troubleshoot/pkg/version.gitSHA={{.Commit}}
+      -X github.com/replicatedhq/troubleshoot/pkg/version.buildTime={{.Date}}
+      -extldflags "-static"
+    flags: -tags netgo -installsuffix netgo
+    binary: collector
+    hooks: {}
   - id: preflight
     goos:
       - linux
@@ -52,6 +68,22 @@ builds:
     flags: -tags netgo -installsuffix netgo
     binary: support-bundle
     hooks: {}
+  - id: manager
+    goos:
+      - linux
+    goarch:
+      - amd64
+    env:
+      - CGO_ENABLED=0
+    main: cmd/manager/main.go
+    ldflags: -s -w
+      -X github.com/replicatedhq/troubleshoot/pkg/version.version={{.Version}}
+      -X github.com/replicatedhq/troubleshoot/pkg/version.gitSHA={{.Commit}}
+      -X github.com/replicatedhq/troubleshoot/pkg/version.buildTime={{.Date}}
+      -extldflags "-static"
+    flags: -tags netgo -installsuffix netgo
+    binary: manager
+    hooks: {}
 archives:
   - id: so
     builds:
@@ -100,6 +132,7 @@ dockers:
       - "replicated/troubleshoot:{{ .Major }}.{{ .Minor }}"
       - "replicated/troubleshoot:{{ .Major }}.{{ .Minor }}.{{ .Patch }}"
     binaries:
+      - collector
       - support-bundle
       - preflight
   - dockerfile: ./deploy/Dockerfile.troubleshoot
@@ -109,5 +142,14 @@ dockers:
       - "replicated/preflight:{{ .Major }}.{{ .Minor }}"
       - "replicated/preflight:{{ .Major }}.{{ .Minor }}.{{ .Patch }}"
     binaries:
+      - collector
       - support-bundle
       - preflight
+  - dockerfile: ./deploy/Dockerfile.manager
+    image_templates:
+      - "replicated/troubleshoot-manager:latest"
+      - "replicated/troubleshoot-manager:{{ .Major }}"
+      - "replicated/troubleshoot-manager:{{ .Major }}.{{ .Minor }}"
+      - "replicated/troubleshoot-manager:{{ .Major }}.{{ .Minor }}.{{ .Patch }}"
+    binaries:
+      - manager
diff --git a/deploy/Dockerfile.manager b/deploy/Dockerfile.manager
new file mode 100644
index 00000000..bdcce9d9
--- /dev/null
+++ b/deploy/Dockerfile.manager
@@ -0,0 +1,4 @@
+FROM ubuntu:latest
+WORKDIR /
+COPY manager /manager
+ENTRYPOINT ["/manager"]
diff --git a/deploy/Dockerfile.troubleshoot b/deploy/Dockerfile.troubleshoot
index d8e689b7..797eb43d 100644
--- a/deploy/Dockerfile.troubleshoot
+++ b/deploy/Dockerfile.troubleshoot
@@ -6,6 +6,7 @@ RUN apt-get -qq update \
     ca-certificates
 
 COPY support-bundle /troubleshoot/support-bundle
+COPY collector /troubleshoot/collector
 COPY preflight /troubleshoot/preflight
 
 ENV PATH="/troubleshoot:${PATH}"
diff --git a/pkg/analyze/analyzer.go b/pkg/analyze/analyzer.go
index bb71c660..3ded296e 100644
--- a/pkg/analyze/analyzer.go
+++ b/pkg/analyze/analyzer.go
@@ -20,65 +20,38 @@ type getChildCollectedFileContents func(string) (map[string][]byte, error)
 
 func Analyze(analyzer *troubleshootv1beta1.Analyze, getFile getCollectedFileContents, findFiles getChildCollectedFileContents) (*AnalyzeResult, error) {
 	if analyzer.ClusterVersion != nil {
-		if analyzer.ClusterVersion.Exclude {
-			return nil, nil
-		}
 		return analyzeClusterVersion(analyzer.ClusterVersion, getFile)
 	}
 	if analyzer.StorageClass != nil {
-		if analyzer.StorageClass.Exclude {
-			return nil, nil
-		}
 		return analyzeStorageClass(analyzer.StorageClass, getFile)
 	}
 	if analyzer.CustomResourceDefinition != nil {
-		if analyzer.CustomResourceDefinition.Exclude {
-			return nil, nil
-		}
 		return analyzeCustomResourceDefinition(analyzer.CustomResourceDefinition, getFile)
 	}
 	if analyzer.Ingress != nil {
-		if analyzer.Ingress.Exclude {
-			return nil, nil
-		}
 		return analyzeIngress(analyzer.Ingress, getFile)
 	}
 	if analyzer.Secret != nil {
-		if analyzer.Secret.Exclude {
-			return nil, nil
-		}
 		return analyzeSecret(analyzer.Secret, getFile)
 	}
 	if analyzer.ImagePullSecret != nil {
-		if analyzer.ImagePullSecret.Exclude {
-			return nil, nil
-		}
 		return analyzeImagePullSecret(analyzer.ImagePullSecret, findFiles)
 	}
 	if analyzer.DeploymentStatus != nil {
-		if analyzer.DeploymentStatus.Exclude {
-			return nil, nil
-		}
 		return analyzeDeploymentStatus(analyzer.DeploymentStatus, getFile)
 	}
 	if analyzer.StatefulsetStatus != nil {
-		if analyzer.StatefulsetStatus.Exclude {
-			return nil, nil
-		}
 		return analyzeStatefulsetStatus(analyzer.StatefulsetStatus, getFile)
 	}
 	if analyzer.ContainerRuntime != nil {
-		if analyzer.ContainerRuntime.Exclude {
-			return nil, nil
-		}
 		return analyzeContainerRuntime(analyzer.ContainerRuntime, getFile)
 	}
 	if analyzer.Distribution != nil {
-		if analyzer.Distribution.Exclude {
-			return nil, nil
-		}
 		return analyzeDistribution(analyzer.Distribution, getFile)
 	}
+	if analyzer.TextAnalyze != nil {
+		return analyzeTextAnalyze(analyzer.TextAnalyze, getFile)
+	}
 
 	return nil, errors.New("invalid analyzer")
 }
diff --git a/pkg/analyze/download.go b/pkg/analyze/download.go
index 2c40617e..1ad2a2b5 100644
--- a/pkg/analyze/download.go
+++ b/pkg/analyze/download.go
@@ -62,9 +62,7 @@ func DownloadAndAnalyze(bundleURL string, analyzersSpec string) ([]*AnalyzeResul
 			continue
 		}
 
-		if analyzeResult != nil {
-			analyzeResults = append(analyzeResults, analyzeResult)
-		}
+		analyzeResults = append(analyzeResults, analyzeResult)
 	}
 
 	return analyzeResults, nil
diff --git a/pkg/analyze/text_analyze.go b/pkg/analyze/text_analyze.go
new file mode 100644
index 00000000..4ae4f44f
--- /dev/null
+++ b/pkg/analyze/text_analyze.go
@@ -0,0 +1,47 @@
+package analyzer
+
+import (
+	"path"
+	"regexp"
+
+	"github.com/pkg/errors"
+	troubleshootv1beta1 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta1"
+)
+
+func analyzeTextAnalyze(analyzer *troubleshootv1beta1.TextAnalyze, getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) {
+	fullPath := path.Join(analyzer.CollectorName, analyzer.FileName)
+	collected, err := getCollectedFileContents(fullPath)
+	if err != nil {
+		return nil, errors.Wrapf(err, "failed to read collected file name: %s", fullPath)
+	}
+
+	re, err := regexp.Compile(analyzer.RegexPattern)
+	if err != nil {
+		return nil, errors.Wrapf(err, "failed to compile regex: %s", analyzer.RegexPattern)
+	}
+
+	var failOutcome *troubleshootv1beta1.Outcome
+	var passOutcome *troubleshootv1beta1.Outcome
+	for _, outcome := range analyzer.Outcomes {
+		if outcome.Fail != nil {
+			failOutcome = outcome
+		} else if outcome.Pass != nil {
+			passOutcome = outcome
+		}
+	}
+
+	if re.MatchString(string(collected)) {
+		return &AnalyzeResult{
+			Title:   analyzer.CheckName,
+			IsPass:  true,
+			Message: passOutcome.Pass.Message,
+			URI:     passOutcome.Pass.URI,
+		}, nil
+	}
+	return &AnalyzeResult{
+		Title:   analyzer.CheckName,
+		IsFail:  true,
+		Message: failOutcome.Fail.Message,
+		URI:     failOutcome.Fail.URI,
+	}, nil
+}
diff --git a/pkg/apis/troubleshoot/v1beta1/analyzer_shared.go b/pkg/apis/troubleshoot/v1beta1/analyzer_shared.go
index 29b6b955..35182a72 100644
--- a/pkg/apis/troubleshoot/v1beta1/analyzer_shared.go
+++ b/pkg/apis/troubleshoot/v1beta1/analyzer_shared.go
@@ -74,9 +74,16 @@ type Distribution struct {
 	Outcomes    []*Outcome `json:"outcomes" yaml:"outcomes"`
 }
 
+type TextAnalyze struct {
+	AnalyzeMeta   `json:",inline" yaml:",inline"`
+	CollectorName string     `json:"collectorName,omitempty" yaml:"collectorName,omitempty"`
+	FileName      string     `json:"fileName,omitempty" yaml:"fileName,omitempty"`
+	RegexPattern  string     `json:"regex,omitempty" yaml:"regex,omitempty"`
+	Outcomes      []*Outcome `json:"outcomes" yaml:"outcomes"`
+}
+
 type AnalyzeMeta struct {
 	CheckName string `json:"checkName,omitempty" yaml:"checkName,omitempty"`
-	Exclude   bool   `json:"exclude,omitempty" yaml:"exclude,omitempty"`
 }
 
 type Analyze struct {
@@ -90,4 +97,5 @@ type Analyze struct {
 	StatefulsetStatus        *StatefulsetStatus        `json:"statefulsetStatus,omitempty" yaml:"statefulsetStatus,omitempty"`
 	ContainerRuntime         *ContainerRuntime         `json:"containerRuntime,omitempty" yaml:"containerRuntime,omitempty"`
 	Distribution             *Distribution             `json:"distribution,omitempty" yaml:"distribution,omitempty"`
+	TextAnalyze              *TextAnalyze              `json:"textAnalyze,omitempty" yaml:"textAnalyze,omitempty"`
 }
diff --git a/pkg/apis/troubleshoot/v1beta1/collector_shared.go b/pkg/apis/troubleshoot/v1beta1/collector_shared.go
index e437a056..e75d7c1a 100644
--- a/pkg/apis/troubleshoot/v1beta1/collector_shared.go
+++ b/pkg/apis/troubleshoot/v1beta1/collector_shared.go
@@ -2,20 +2,17 @@ package v1beta1
 
 type CollectorMeta struct {
 	CollectorName string `json:"collectorName,omitempty" yaml:"collectorName,omitempty"`
-	Exclude       bool   `json:"when,omitmempty" yaml:"when,omitempty"`
 }
 
 type ClusterInfo struct {
-	CollectorMeta `json:",inline" yaml:",inline"`
 }
 
 type ClusterResources struct {
-	CollectorMeta `json:",inline" yaml:",inline"`
 }
 
 type Secret struct {
 	CollectorMeta `json:",inline" yaml:",inline"`
-	SecretName    string `json:"name" yaml:"name"`
+	Name          string `json:"name" yaml:"name"`
 	Namespace     string `json:"namespace,omitempty" yaml:"namespace,omitempty"`
 	Key           string `json:"key,omitempty" yaml:"key,omitempty"`
 	IncludeValue  bool   `json:"includeValue,omitempty" yaml:"includeValue,omitempty"`
diff --git a/pkg/apis/troubleshoot/v1beta1/zz_generated.deepcopy.go b/pkg/apis/troubleshoot/v1beta1/zz_generated.deepcopy.go
index 6d6455ce..8dac22e9 100644
--- a/pkg/apis/troubleshoot/v1beta1/zz_generated.deepcopy.go
+++ b/pkg/apis/troubleshoot/v1beta1/zz_generated.deepcopy.go
@@ -102,6 +102,11 @@ func (in *Analyze) DeepCopyInto(out *Analyze) {
 		*out = new(Distribution)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.TextAnalyze != nil {
+		in, out := &in.TextAnalyze, &out.TextAnalyze
+		*out = new(TextAnalyze)
+		(*in).DeepCopyInto(*out)
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Analyze.
@@ -348,7 +353,6 @@ func (in *AnalyzerStatus) DeepCopy() *AnalyzerStatus {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ClusterInfo) DeepCopyInto(out *ClusterInfo) {
 	*out = *in
-	out.CollectorMeta = in.CollectorMeta
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterInfo.
@@ -364,7 +368,6 @@ func (in *ClusterInfo) DeepCopy() *ClusterInfo {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ClusterResources) DeepCopyInto(out *ClusterResources) {
 	*out = *in
-	out.CollectorMeta = in.CollectorMeta
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterResources.
@@ -1499,3 +1502,30 @@ func (in *SupportBundleVersion) DeepCopy() *SupportBundleVersion {
 	in.DeepCopyInto(out)
 	return out
 }
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *TextAnalyze) DeepCopyInto(out *TextAnalyze) {
+	*out = *in
+	out.AnalyzeMeta = in.AnalyzeMeta
+	if in.Outcomes != nil {
+		in, out := &in.Outcomes, &out.Outcomes
+		*out = make([]*Outcome, len(*in))
+		for i := range *in {
+			if (*in)[i] != nil {
+				in, out := &(*in)[i], &(*out)[i]
+				*out = new(Outcome)
+				(*in).DeepCopyInto(*out)
+			}
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TextAnalyze.
+func (in *TextAnalyze) DeepCopy() *TextAnalyze {
+	if in == nil {
+		return nil
+	}
+	out := new(TextAnalyze)
+	in.DeepCopyInto(out)
+	return out
+}
diff --git a/pkg/collect/cluster_resources.go b/pkg/collect/cluster_resources.go
index c42816c5..dbd04681 100644
--- a/pkg/collect/cluster_resources.go
+++ b/pkg/collect/cluster_resources.go
@@ -7,9 +7,7 @@ import (
 	"strings"
 
 	"github.com/replicatedhq/troubleshoot/pkg/redact"
-	authorizationv1 "k8s.io/api/authorization/v1"
 	corev1 "k8s.io/api/core/v1"
-	rbacv1 "k8s.io/api/rbac/v1"
 	apiextensionsv1beta1clientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/client-go/kubernetes"
@@ -34,10 +32,6 @@ type ClusterResourcesOutput struct {
 	ImagePullSecretsErrors          []byte            `json:"cluster-resources/image-pull-secrets-errors.json,omitempty"`
 	Nodes                           []byte            `json:"cluster-resources/nodes.json,omitempty"`
 	NodesErrors                     []byte            `json:"cluster-resources/nodes-errors.json,omitempty"`
-
-	// TODO these should be considered for relocation to an rbac or auth package.  cluster resources might not be the right place
-	AuthCanI       map[string][]byte `json:"cluster-resources/auth-cani-list,omitempty"`
-	AuthCanIErrors []byte            `json:"cluster-resources/auth-cani-list-errors.json,omitempty"`
 }
 
 func ClusterResources(ctx *Context) ([]byte, error) {
@@ -137,14 +131,6 @@ func ClusterResources(ctx *Context) ([]byte, error) {
 		return nil, err
 	}
 
-	// auth cani
-	authCanI, authCanIErrors := authCanI(client, namespaceNames)
-	clusterResourcesOutput.AuthCanI = authCanI
-	clusterResourcesOutput.AuthCanIErrors, err = marshalNonNil(authCanIErrors)
-	if err != nil {
-		return nil, err
-	}
-
 	if ctx.Redact {
 		clusterResourcesOutput, err = clusterResourcesOutput.Redact()
 		if err != nil {
@@ -374,63 +360,6 @@ func nodes(client *kubernetes.Clientset) ([]byte, []string) {
 	return b, nil
 }
 
-func authCanI(client *kubernetes.Clientset, namespaces []string) (map[string][]byte, map[string]string) {
-	// https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/auth/cani.go
-
-	authListByNamespace := make(map[string][]byte)
-	errorsByNamespace := make(map[string]string)
-
-	for _, namespace := range namespaces {
-		sar := &authorizationv1.SelfSubjectRulesReview{
-			Spec: authorizationv1.SelfSubjectRulesReviewSpec{
-				Namespace: namespace,
-			},
-		}
-		response, err := client.AuthorizationV1().SelfSubjectRulesReviews().Create(sar)
-		if err != nil {
-			errorsByNamespace[namespace] = err.Error()
-			continue
-		}
-
-		rules := []rbacv1.PolicyRule{}
-		for _, rule := range convertToPolicyRule(response.Status) {
-			rules = append(rules, rule)
-		}
-
-		b, err := json.MarshalIndent(rules, "", "  ")
-		if err != nil {
-			errorsByNamespace[namespace] = err.Error()
-			continue
-		}
-
-		authListByNamespace[namespace+".json"] = b
-	}
-
-	return authListByNamespace, errorsByNamespace
-}
-
-// not exprted from: https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/auth/cani.go#L339
-func convertToPolicyRule(status authorizationv1.SubjectRulesReviewStatus) []rbacv1.PolicyRule {
-	ret := []rbacv1.PolicyRule{}
-	for _, resource := range status.ResourceRules {
-		ret = append(ret, rbacv1.PolicyRule{
-			Verbs:         resource.Verbs,
-			APIGroups:     resource.APIGroups,
-			Resources:     resource.Resources,
-			ResourceNames: resource.ResourceNames,
-		})
-	}
-
-	for _, nonResource := range status.NonResourceRules {
-		ret = append(ret, rbacv1.PolicyRule{
-			Verbs:           nonResource.Verbs,
-			NonResourceURLs: nonResource.NonResourceURLs,
-		})
-	}
-
-	return ret
-}
-
 func (c *ClusterResourcesOutput) Redact() (*ClusterResourcesOutput, error) {
 	namespaces, err := redact.Redact(c.Namespaces)
 	if err != nil {
@@ -464,7 +393,6 @@ func (c *ClusterResourcesOutput) Redact() (*ClusterResourcesOutput, error) {
 	if err != nil {
 		return nil, err
 	}
-
 	return &ClusterResourcesOutput{
 		Namespaces:                      namespaces,
 		NamespacesErrors:                c.NamespacesErrors,
@@ -484,7 +412,5 @@ func (c *ClusterResourcesOutput) Redact() (*ClusterResourcesOutput, error) {
 		CustomResourceDefinitionsErrors: c.CustomResourceDefinitionsErrors,
 		ImagePullSecrets:                c.ImagePullSecrets,
 		ImagePullSecretsErrors:          c.ImagePullSecretsErrors,
-		AuthCanI:                        c.AuthCanI,
-		AuthCanIErrors:                  c.AuthCanIErrors,
 	}, nil
 }
diff --git a/pkg/collect/collector.go b/pkg/collect/collector.go
index 1f9f2ed0..d214683b 100644
--- a/pkg/collect/collector.go
+++ b/pkg/collect/collector.go
@@ -25,57 +25,30 @@ type Context struct {
 
 func (c *Collector) RunCollectorSync() ([]byte, error) {
 	if c.Collect.ClusterInfo != nil {
-		if c.Collect.ClusterInfo.Exclude {
-			return nil, nil
-		}
 		return ClusterInfo(c.GetContext())
 	}
 	if c.Collect.ClusterResources != nil {
-		if c.Collect.ClusterResources.Exclude {
-			return nil, nil
-		}
 		return ClusterResources(c.GetContext())
 	}
 	if c.Collect.Secret != nil {
-		if c.Collect.Secret.Exclude {
-			return nil, nil
-		}
 		return Secret(c.GetContext(), c.Collect.Secret)
 	}
 	if c.Collect.Logs != nil {
-		if c.Collect.Logs.Exclude {
-			return nil, nil
-		}
 		return Logs(c.GetContext(), c.Collect.Logs)
 	}
 	if c.Collect.Run != nil {
-		if c.Collect.Run.Exclude {
-			return nil, nil
-		}
 		return Run(c.GetContext(), c.Collect.Run)
 	}
 	if c.Collect.Exec != nil {
-		if c.Collect.Exec.Exclude {
-			return nil, nil
-		}
 		return Exec(c.GetContext(), c.Collect.Exec)
 	}
 	if c.Collect.Data != nil {
-		if c.Collect.Data.Exclude {
-			return nil, nil
-		}
 		return Data(c.GetContext(), c.Collect.Data)
 	}
 	if c.Collect.Copy != nil {
-		if c.Collect.Copy.Exclude {
-			return nil, nil
-		}
 		return Copy(c.GetContext(), c.Collect.Copy)
 	}
 	if c.Collect.HTTP != nil {
-		if c.Collect.HTTP.Exclude {
-			return nil, nil
-		}
 		return HTTP(c.GetContext(), c.Collect.HTTP)
 	}
 
diff --git a/pkg/collect/secret.go b/pkg/collect/secret.go
index 64db4291..61513d16 100644
--- a/pkg/collect/secret.go
+++ b/pkg/collect/secret.go
@@ -60,11 +60,11 @@ func Secret(ctx *Context, secretCollector *troubleshootv1beta1.Secret) ([]byte,
 }
 
 func secret(client *kubernetes.Clientset, secretCollector *troubleshootv1beta1.Secret) (*FoundSecret, []byte, error) {
-	found, err := client.CoreV1().Secrets(secretCollector.Namespace).Get(secretCollector.SecretName, metav1.GetOptions{})
+	found, err := client.CoreV1().Secrets(secretCollector.Namespace).Get(secretCollector.Name, metav1.GetOptions{})
 	if err != nil {
 		missingSecret := FoundSecret{
 			Namespace:    secretCollector.Namespace,
-			Name:         secretCollector.SecretName,
+			Name:         secretCollector.Name,
 			SecretExists: false,
 		}
 
diff --git a/pkg/collect/util.go b/pkg/collect/util.go
index 6702aa75..3b8aa8c6 100644
--- a/pkg/collect/util.go
+++ b/pkg/collect/util.go
@@ -22,7 +22,7 @@ func DeterministicIDForCollector(collector *troubleshootv1beta1.Collect) string
 	}
 
 	if collector.Secret != nil {
-		unsafeID = fmt.Sprintf("secret-%s-%s", collector.Secret.Namespace, collector.Secret.SecretName)
+		unsafeID = fmt.Sprintf("secret-%s-%s", collector.Secret.Namespace, collector.Secret.Name)
 	}
 
 	if collector.Logs != nil {
diff --git a/pkg/version/build.go b/pkg/version/build.go
deleted file mode 100644
index a79e72b1..00000000
--- a/pkg/version/build.go
+++ /dev/null
@@ -1,7 +0,0 @@
-package version
-
-// NOTE: these variables are injected at build time
-
-var (
-	version, gitSHA, buildTime string
-)
diff --git a/pkg/version/run.go b/pkg/version/run.go
deleted file mode 100644
index b6a51474..00000000
--- a/pkg/version/run.go
+++ /dev/null
@@ -1,12 +0,0 @@
-package version
-
-import (
-	"time"
-)
-
-var RunAt time.Time
-
-func init() {
-	RunAt = time.Now().UTC()
-	initBuild()
-}
diff --git a/pkg/version/version.go b/pkg/version/version.go
deleted file mode 100644
index 1936f6fc..00000000
--- a/pkg/version/version.go
+++ /dev/null
@@ -1,72 +0,0 @@
-package version
-
-import (
-	"runtime"
-	"time"
-)
-
-var (
-	build Build
-)
-
-// Build holds details about this build of the binary
-type Build struct {
-	Version      string     `json:"version,omitempty"`
-	GitSHA       string     `json:"git,omitempty"`
-	BuildTime    time.Time  `json:"buildTime,omitempty"`
-	TimeFallback string     `json:"buildTimeFallback,omitempty"`
-	GoInfo       GoInfo     `json:"go,omitempty"`
-	RunAt        *time.Time `json:"runAt,omitempty"`
-}
-
-type GoInfo struct {
-	Version  string `json:"version,omitempty"`
-	Compiler string `json:"compiler,omitempty"`
-	OS       string `json:"os,omitempty"`
-	Arch     string `json:"arch,omitempty"`
-}
-
-// initBuild sets up the version info from build args
-func initBuild() {
-	build.Version = version
-	if len(gitSHA) >= 7 {
-		build.GitSHA = gitSHA[:7]
-	}
-	var err error
-	build.BuildTime, err = time.Parse(time.RFC3339, buildTime)
-	if err != nil {
-		build.TimeFallback = buildTime
-	}
-
-	build.GoInfo = getGoInfo()
-	build.RunAt = &RunAt
-}
-
-// GetBuild gets the build
-func GetBuild() Build {
-	return build
-}
-
-// Version gets the version
-func Version() string {
-	return build.Version
-}
-
-// GitSHA gets the gitsha
-func GitSHA() string {
-	return build.GitSHA
-}
-
-// BuildTime gets the build time
-func BuildTime() time.Time {
-	return build.BuildTime
-}
-
-func getGoInfo() GoInfo {
-	return GoInfo{
-		Version:  runtime.Version(),
-		Compiler: runtime.Compiler,
-		OS:       runtime.GOOS,
-		Arch:     runtime.GOARCH,
-	}
-}


Test patch:
diff --git a/pkg/analyze/text_analyze_test.go b/pkg/analyze/text_analyze_test.go
new file mode 100644
index 00000000..425f857a
--- /dev/null
+++ b/pkg/analyze/text_analyze_test.go
@@ -0,0 +1,212 @@
+package analyzer
+
+import (
+	"fmt"
+	"testing"
+
+	troubleshootv1beta1 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta1"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func Test_textAnalyze(t *testing.T) {
+	tests := []struct {
+		name         string
+		analyzer     troubleshootv1beta1.TextAnalyze
+		expectResult AnalyzeResult
+		files        map[string][]byte
+	}{
+		{
+			name: "success case 1",
+			analyzer: troubleshootv1beta1.TextAnalyze{
+				Outcomes: []*troubleshootv1beta1.Outcome{
+					{
+						Pass: &troubleshootv1beta1.SingleOutcome{
+							Message: "pass",
+						},
+					},
+					{
+						Fail: &troubleshootv1beta1.SingleOutcome{
+							Message: "fail",
+						},
+					},
+				},
+				CollectorName: "text-collector-1",
+				FileName:      "cfile-1.txt",
+				RegexPattern:  "succeeded",
+			},
+			expectResult: AnalyzeResult{
+				IsPass:  true,
+				IsWarn:  false,
+				IsFail:  false,
+				Message: "pass",
+			},
+			files: map[string][]byte{
+				"text-collector-1/cfile-1.txt": []byte("Yes it all succeeded"),
+			},
+		},
+		{
+			name: "failure case 1",
+			analyzer: troubleshootv1beta1.TextAnalyze{
+				Outcomes: []*troubleshootv1beta1.Outcome{
+					{
+						Pass: &troubleshootv1beta1.SingleOutcome{
+							Message: "success",
+						},
+					},
+					{
+						Fail: &troubleshootv1beta1.SingleOutcome{
+							Message: "fail",
+						},
+					},
+				},
+				CollectorName: "text-collector-2",
+				FileName:      "cfile-2.txt",
+				RegexPattern:  "succeeded",
+			},
+			expectResult: AnalyzeResult{
+				IsPass:  false,
+				IsWarn:  false,
+				IsFail:  true,
+				Message: "fail",
+			},
+			files: map[string][]byte{
+				"text-collector-2/cfile-2.txt": []byte(""),
+			},
+		},
+		{
+			name: "success case 2",
+			analyzer: troubleshootv1beta1.TextAnalyze{
+				Outcomes: []*troubleshootv1beta1.Outcome{
+					{
+						Pass: &troubleshootv1beta1.SingleOutcome{
+							Message: "success",
+						},
+					},
+					{
+						Fail: &troubleshootv1beta1.SingleOutcome{
+							Message: "fail",
+						},
+					},
+				},
+				CollectorName: "text-collector-3",
+				FileName:      "cfile-3.txt",
+				RegexPattern:  "",
+			},
+			expectResult: AnalyzeResult{
+				IsPass:  true,
+				IsWarn:  false,
+				IsFail:  false,
+				Message: "success",
+			},
+			files: map[string][]byte{
+				"text-collector-3/cfile-3.txt": []byte("Connection to service succeeded"),
+			},
+		},
+		{
+			name: "success case 3",
+			analyzer: troubleshootv1beta1.TextAnalyze{
+				Outcomes: []*troubleshootv1beta1.Outcome{
+					{
+						Pass: &troubleshootv1beta1.SingleOutcome{
+							Message: "success",
+						},
+					},
+					{
+						Fail: &troubleshootv1beta1.SingleOutcome{
+							Message: "fail",
+						},
+					},
+				},
+				CollectorName: "text-collector-5",
+				FileName:      "cfile-5.txt",
+				RegexPattern:  "([a-zA-Z0-9\\-_:*\\s])*succe([a-zA-Z0-9\\-_:*\\s!])*",
+			},
+			expectResult: AnalyzeResult{
+				IsPass:  true,
+				IsWarn:  false,
+				IsFail:  false,
+				Message: "success",
+			},
+			files: map[string][]byte{
+				"text-collector-5/cfile-5.txt": []byte("Connection to service succeeded!"),
+			},
+		},
+		{
+			name: "failure case 3",
+			analyzer: troubleshootv1beta1.TextAnalyze{
+				Outcomes: []*troubleshootv1beta1.Outcome{
+					{
+						Pass: &troubleshootv1beta1.SingleOutcome{
+							Message: "success",
+						},
+					},
+					{
+						Fail: &troubleshootv1beta1.SingleOutcome{
+							Message: "fail",
+						},
+					},
+				},
+				CollectorName: "text-collector-4",
+				FileName:      "cfile-4.txt",
+				RegexPattern:  "succeeded",
+			},
+			expectResult: AnalyzeResult{
+				IsPass:  false,
+				IsWarn:  false,
+				IsFail:  true,
+				Message: "fail",
+			},
+			files: map[string][]byte{
+				"text-collector-4/cfile-4.txt": []byte("A different message"),
+			},
+		},
+		{
+			name: "failure case 4",
+			analyzer: troubleshootv1beta1.TextAnalyze{
+				Outcomes: []*troubleshootv1beta1.Outcome{
+					{
+						Pass: &troubleshootv1beta1.SingleOutcome{
+							Message: "success",
+						},
+					},
+					{
+						Fail: &troubleshootv1beta1.SingleOutcome{
+							Message: "fail",
+						},
+					},
+				},
+				CollectorName: "text-collector-6",
+				FileName:      "cfile-6.txt",
+				RegexPattern:  "([a-zA-Z0-9\\-_:*\\s])*succe([a-zA-Z0-9\\-_:*\\s!])*",
+			},
+			expectResult: AnalyzeResult{
+				IsPass:  false,
+				IsWarn:  false,
+				IsFail:  true,
+				Message: "fail",
+			},
+			files: map[string][]byte{
+				"text-collector-6/cfile-6.txt": []byte("A different message"),
+			},
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			req := require.New(t)
+
+			getFiles := func(n string) ([]byte, error) {
+				val, ok := test.files[n]
+				if !ok {
+					return nil, fmt.Errorf("File not found: %s", n)
+				}
+				return val, nil
+			}
+
+			actual, err := analyzeTextAnalyze(&test.analyzer, getFiles)
+			req.NoError(err)
+			assert.Equal(t, &test.expectResult, actual)
+		})
+	}
+}
diff --git a/pkg/collect/util_test.go b/pkg/collect/util_test.go
index ea7efd8a..ba22776a 100644
--- a/pkg/collect/util_test.go
+++ b/pkg/collect/util_test.go
@@ -52,8 +52,8 @@ func Test_DeterministicIDForCollector(t *testing.T) {
 			name: "secret",
 			collector: &troubleshootv1beta1.Collect{
 				Secret: &troubleshootv1beta1.Secret{
-					SecretName: "secret-agent-woman",
-					Namespace:  "top-secret",
+					Name:      "secret-agent-woman",
+					Namespace: "top-secret",
 				},
 			},
 			expect: "secret-top-secret-secret-agent-woman",