diff --git a/Makefile b/Makefile index abc59d186..b7b3246d3 100644 --- a/Makefile +++ b/Makefile @@ -22,14 +22,14 @@ manager: generate fmt vet support-bundle: generate fmt vet go build -o bin/support-bundle github.com/replicatedhq/troubleshoot/cmd/troubleshoot -.PHONY: collector -collector: generate fmt vet - go build -o bin/collector github.com/replicatedhq/troubleshoot/cmd/collector - .PHONY: preflight preflight: generate fmt vet go build -o bin/preflight github.com/replicatedhq/troubleshoot/cmd/preflight +.PHONY: analyze +analyze: generate fmt vet + 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 @@ -103,14 +103,12 @@ local-release: .PHONY: run-preflight run-preflight: preflight - ./bin/preflight \ - --image=localhost:32000/troubleshoot:alpha \ - --pullpolicy=Always \ - ./examples/preflight/sample-preflight.yaml + ./bin/preflight ./examples/preflight/sample-preflight.yaml .PHONY: run-troubleshoot run-troubleshoot: support-bundle - ./bin/support-bundle \ - --image=localhost:32000/troubleshoot:alpha \ - --pullpolicy=Always \ - ./examples/troubleshoot/sample-troubleshoot.yaml + ./bin/support-bundle ./examples/troubleshoot/sample-troubleshoot.yaml + +.PHONY: run-analyze +run-analyze: analyze + ./bin/analyze --analyzers ./examples/troubleshoot/sample-analyzers.yaml ./support-bundle.tar.gz diff --git a/cmd/analyze/cli/root.go b/cmd/analyze/cli/root.go new file mode 100644 index 000000000..0ae16ac4d --- /dev/null +++ b/cmd/analyze/cli/root.go @@ -0,0 +1,60 @@ +package cli + +import ( + "fmt" + "os" + "strings" + + "github.com/replicatedhq/troubleshoot/pkg/logger" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "k8s.io/cli-runtime/pkg/genericclioptions" +) + +var ( + KubernetesConfigFlags *genericclioptions.ConfigFlags +) + +func RootCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "analyze [url]", + Args: cobra.MinimumNArgs(1), + Short: "Analyze a support bundle", + Long: `Run a series of analyzers on a support bundle archive`, + PreRun: func(cmd *cobra.Command, args []string) { + viper.BindPFlags(cmd.Flags()) + }, + RunE: func(cmd *cobra.Command, args []string) error { + v := viper.GetViper() + + logger.SetQuiet(v.GetBool("quiet")) + + return runAnalyzers(v, args[0]) + }, + } + + cobra.OnInitialize(initConfig) + + cmd.Flags().String("analyzers", "", "filename or url of the analyzers to use") + + viper.BindPFlags(cmd.Flags()) + + viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) + + KubernetesConfigFlags = genericclioptions.NewConfigFlags(false) + KubernetesConfigFlags.AddFlags(cmd.Flags()) + + 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/analyze/cli/run.go b/cmd/analyze/cli/run.go new file mode 100644 index 000000000..a004b9360 --- /dev/null +++ b/cmd/analyze/cli/run.go @@ -0,0 +1,75 @@ +package cli + +import ( + "fmt" + "io/ioutil" + "net/http" + "net/url" + "os" + + "github.com/pkg/errors" + analyzer "github.com/replicatedhq/troubleshoot/pkg/analyze" + "github.com/spf13/viper" +) + +func runAnalyzers(v *viper.Viper, bundlePath string) error { + specPath := v.GetString("analyzers") + + specContent := "" + if !isURL(specPath) { + if _, err := os.Stat(specPath); os.IsNotExist(err) { + return fmt.Errorf("%s was not found", specPath) + } + + b, err := ioutil.ReadFile(specPath) + if err != nil { + return err + } + + specContent = string(b) + } else { + req, err := http.NewRequest("GET", specPath, nil) + if err != nil { + return err + } + req.Header.Set("User-Agent", "Replicated_Analyzer/v1beta1") + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + + specContent = string(body) + } + + analyzeResults, err := analyzer.DownloadAndAnalyze(specContent, bundlePath) + if err != nil { + return errors.Wrap(err, "failed to download and analyze bundle") + } + + for _, analyzeResult := range analyzeResults { + if analyzeResult.IsPass { + fmt.Printf("Pass: %s\n %s\n", analyzeResult.Title, analyzeResult.Message) + } else if analyzeResult.IsWarn { + fmt.Printf("Warn: %s\n %s\n", analyzeResult.Title, analyzeResult.Message) + } else if analyzeResult.IsFail { + fmt.Printf("Fail: %s\n %s\n", analyzeResult.Title, analyzeResult.Message) + } + } + + return nil +} + +func isURL(str string) bool { + parsed, err := url.ParseRequestURI(str) + if err != nil { + return false + } + + return parsed.Scheme != "" +} diff --git a/cmd/analyze/main.go b/cmd/analyze/main.go new file mode 100644 index 000000000..c2a6dedec --- /dev/null +++ b/cmd/analyze/main.go @@ -0,0 +1,10 @@ +package main + +import ( + "github.com/replicatedhq/troubleshoot/cmd/analyze/cli" + _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" +) + +func main() { + cli.InitAndExecute() +} diff --git a/cmd/preflight/cli/util.go b/cmd/preflight/cli/util.go index 8c90938ea..176f4e880 100644 --- a/cmd/preflight/cli/util.go +++ b/cmd/preflight/cli/util.go @@ -17,12 +17,12 @@ func homeDir() string { } func isURL(str string) bool { - _, err := url.ParseRequestURI(str) + parsed, err := url.ParseRequestURI(str) if err != nil { return false } - return true + return parsed.Scheme != "" } func createTroubleshootK8sClient(configFlags *genericclioptions.ConfigFlags) (*troubleshootclientv1beta1.TroubleshootV1beta1Client, error) { diff --git a/cmd/troubleshoot/cli/analyze.go b/cmd/troubleshoot/cli/analyze.go index 7a32a2429..4ce1e204b 100644 --- a/cmd/troubleshoot/cli/analyze.go +++ b/cmd/troubleshoot/cli/analyze.go @@ -1,7 +1,6 @@ package cli import ( - "context" "encoding/json" "fmt" @@ -28,7 +27,7 @@ func Analyze() *cobra.Command { logger.SetQuiet(v.GetBool("quiet")) - result, err := analyzer.DownloadAndAnalyze(context.TODO(), v.GetString("url")) + result, err := analyzer.DownloadAndAnalyze("", v.GetString("url")) if err != nil { return err } diff --git a/cmd/troubleshoot/cli/util.go b/cmd/troubleshoot/cli/util.go index d8f4cd1fa..f7aeee8b4 100644 --- a/cmd/troubleshoot/cli/util.go +++ b/cmd/troubleshoot/cli/util.go @@ -18,12 +18,12 @@ func homeDir() string { } func isURL(str string) bool { - _, err := url.ParseRequestURI(str) + parsed, err := url.ParseRequestURI(str) if err != nil { return false } - return true + return parsed.Scheme != "" } func createTroubleshootK8sClient(configFlags *genericclioptions.ConfigFlags) (*troubleshootclientv1beta1.TroubleshootV1beta1Client, error) { diff --git a/examples/troubleshoot/sample-analyzers.yaml b/examples/troubleshoot/sample-analyzers.yaml new file mode 100644 index 000000000..58a3453fc --- /dev/null +++ b/examples/troubleshoot/sample-analyzers.yaml @@ -0,0 +1,19 @@ +apiVersion: troubleshoot.replicated.com/v1beta1 +kind: Analyzer +metadata: + name: defaultAnalyzers +spec: + analyzers: + - clusterVersion: + outcomes: + - fail: + when: "< 1.13.0" + message: The application requires at Kubernetes 1.13.0 or later, and recommends 1.15.0. + uri: https://www.kubernetes.io + - warn: + when: "< 1.15.0" + message: Your cluster meets the minimum version of Kubernetes, but we recommend you update to 1.15.0 or later. + uri: https://kubernetes.io + - pass: + when: ">= 1.15.0" + message: Your cluster meets the recommended and required versions of Kubernetes. diff --git a/ffi/main.go b/ffi/main.go index 8d3d2e248..24d3f1047 100644 --- a/ffi/main.go +++ b/ffi/main.go @@ -15,10 +15,10 @@ import ( ) //export Analyze -func Analyze(bundleURL string, outputFormat string, compatibility string) *C.char { +func Analyze(bundleURL string, analyzers string, outputFormat string, compatibility string) *C.char { logger.SetQuiet(true) - result, err := analyzer.DownloadAndAnalyze(context.TODO(), bundleURL) + result, err := analyzer.DownloadAndAnalyze(bundleURL, analyzers) if err != nil { fmt.Printf("error downloading and analyzing: %s\n", err.Error()) return C.CString("") diff --git a/pkg/analyze/download.go b/pkg/analyze/download.go index 282d55afa..df1e24655 100644 --- a/pkg/analyze/download.go +++ b/pkg/analyze/download.go @@ -3,26 +3,24 @@ package analyzer import ( "archive/tar" "compress/gzip" - "context" - "fmt" "io" "io/ioutil" - "net/http" "os" "path/filepath" getter "github.com/hashicorp/go-getter" "github.com/pkg/errors" troubleshootv1beta1 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta1" + troubleshootscheme "github.com/replicatedhq/troubleshoot/pkg/client/troubleshootclientset/scheme" "github.com/replicatedhq/troubleshoot/pkg/logger" - "gopkg.in/yaml.v2" + "k8s.io/client-go/kubernetes/scheme" ) type fileContentProvider struct { rootDir string } -func DownloadAndAnalyze(ctx context.Context, bundleURL string) ([]*AnalyzeResult, error) { +func DownloadAndAnalyze(analyzersSpec string, bundleURL string) ([]*AnalyzeResult, error) { tmpDir, err := ioutil.TempDir("", "troubleshoot-k8s") if err != nil { return nil, errors.Wrap(err, "failed to create temp dir") @@ -38,9 +36,20 @@ func DownloadAndAnalyze(ctx context.Context, bundleURL string) ([]*AnalyzeResult return nil, errors.Wrap(err, "failed to read version.yaml") } - analyzers, err := getTroubleshootAnalyzers() - if err != nil { - return nil, errors.Wrap(err, "failed to get analyzers") + analyzers := []*troubleshootv1beta1.Analyze{} + + if analyzersSpec == "" { + defaultAnalyzers, err := getDefaultAnalyzers() + if err != nil { + return nil, errors.Wrap(err, "failed to get default analyzers") + } + analyzers = defaultAnalyzers + } else { + parsedAnalyzers, err := parseAnalyzers(analyzersSpec) + if err != nil { + return nil, errors.Wrap(err, "failed to parse analyzers") + } + analyzers = parsedAnalyzers } fcp := fileContentProvider{rootDir: tmpDir} @@ -138,29 +147,41 @@ func extractTroubleshootBundle(reader io.Reader, destDir string) error { return nil } -func getTroubleshootAnalyzers() ([]*troubleshootv1beta1.Analyze, error) { - specURL := `https://troubleshoot.replicated.com/` - resp, err := http.Get(specURL) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("could not download analyzer spec, status code: %v", resp.StatusCode) - } +func parseAnalyzers(spec string) ([]*troubleshootv1beta1.Analyze, error) { + troubleshootscheme.AddToScheme(scheme.Scheme) + decode := scheme.Codecs.UniversalDeserializer().Decode - spec, err := ioutil.ReadAll(resp.Body) + obj, _, err := decode([]byte(spec), nil, nil) if err != nil { - return nil, err + return nil, errors.Wrap(err, "failed to decode analyzers") } - preflight := troubleshootv1beta1.Preflight{} - if err := yaml.Unmarshal([]byte(spec), &preflight); err != nil { - return nil, err - } + analyzer := obj.(*troubleshootv1beta1.Analyzer) + return analyzer.Spec.Analyzers, nil +} - return preflight.Spec.Analyzers, nil +func getDefaultAnalyzers() ([]*troubleshootv1beta1.Analyze, error) { + spec := `apiVersion: troubleshoot.replicated.com/v1beta1 +kind: Analyzer +metadata: + name: defaultAnalyzers +spec: + analyzers: + - clusterVersion: + outcomes: + - fail: + when: "< 1.13.0" + message: The application requires at Kubernetes 1.13.0 or later, and recommends 1.15.0. + uri: https://www.kubernetes.io + - warn: + when: "< 1.15.0" + message: Your cluster meets the minimum version of Kubernetes, but we recommend you update to 1.15.0 or later. + uri: https://kubernetes.io + - pass: + when: ">= 1.15.0" + message: Your cluster meets the recommended and required versions of Kubernetes.` + + return parseAnalyzers(spec) } func (f fileContentProvider) getFileContents(fileName string) ([]byte, error) { diff --git a/pkg/apis/troubleshoot/v1beta1/analyzer_types.go b/pkg/apis/troubleshoot/v1beta1/analyzer_types.go index ca50647d8..e1a90aee7 100644 --- a/pkg/apis/troubleshoot/v1beta1/analyzer_types.go +++ b/pkg/apis/troubleshoot/v1beta1/analyzer_types.go @@ -20,19 +20,13 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! -// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. - // AnalyzerSpec defines the desired state of Analyzer type AnalyzerSpec struct { - // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster - // Important: Run "make" to regenerate code after modifying this file + Analyzers []*Analyze `json:"analyzers,omitempty"` } // AnalyzerStatus defines the observed state of Analyzer type AnalyzerStatus struct { - // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster - // Important: Run "make" to regenerate code after modifying this file } // +genclient diff --git a/pkg/apis/troubleshoot/v1beta1/zz_generated.deepcopy.go b/pkg/apis/troubleshoot/v1beta1/zz_generated.deepcopy.go index 32aff21d9..1ac4a6708 100644 --- a/pkg/apis/troubleshoot/v1beta1/zz_generated.deepcopy.go +++ b/pkg/apis/troubleshoot/v1beta1/zz_generated.deepcopy.go @@ -16,7 +16,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -// autogenerated by controller-gen object, do not modify manually +// Code generated by controller-gen. DO NOT EDIT. package v1beta1 @@ -141,7 +141,7 @@ func (in *Analyzer) DeepCopyInto(out *Analyzer) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) out.Status = in.Status } @@ -287,6 +287,17 @@ func (in *AnalyzerList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AnalyzerSpec) DeepCopyInto(out *AnalyzerSpec) { *out = *in + if in.Analyzers != nil { + in, out := &in.Analyzers, &out.Analyzers + *out = make([]*Analyze, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(Analyze) + (*in).DeepCopyInto(*out) + } + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AnalyzerSpec.