diff --git a/cmd/collect/cli/clickhouse.go b/cmd/collect/cli/clickhouse.go new file mode 100644 index 00000000..e866804b --- /dev/null +++ b/cmd/collect/cli/clickhouse.go @@ -0,0 +1,88 @@ +package cli + +import ( + troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" + "github.com/replicatedhq/troubleshoot/pkg/collect" + "github.com/spf13/cobra" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" +) + +// ClickhouseCmd runs the clickhouse collector against a single database and +// prints the native result JSON ({"isConnected":..,"version":..,"error":..}). +func ClickhouseCmd() *cobra.Command { + var ( + uri string + skipVerify bool + caCert string + clientCert string + clientKey string + secretName string + secretNamespace string + ) + + cmd := &cobra.Command{ + Use: "clickhouse", + Short: "Run the clickhouse collector against a database", + Long: `Run the clickhouse collector: connect to a ClickHouse server, run "SELECT version()", +and print the collector result as JSON. + +--uri is a clickhouse-go DSN: + clickhouse://user:password@host:9000/dbname + +TLS material may be provided inline / by file path (--tls-cacert, --tls-client-cert, +--tls-client-key) or sourced from a Secret (--tls-secret-name), which requires +cluster access. + +Example: + collect clickhouse --uri "clickhouse://default:@ch.default.svc:9000/default"`, + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + db := &troubleshootv1beta2.Database{ + CollectorMeta: troubleshootv1beta2.CollectorMeta{CollectorName: "clickhouse"}, + URI: uri, + } + + var ( + client kubernetes.Interface + cfg *rest.Config + ) + if skipVerify || caCert != "" || clientCert != "" || clientKey != "" || secretName != "" { + tls := &troubleshootv1beta2.TLSParams{ + SkipVerify: skipVerify, + CACert: caCert, + ClientCert: clientCert, + ClientKey: clientKey, + } + if secretName != "" { + tls.Secret = &troubleshootv1beta2.TLSSecret{Name: secretName, Namespace: secretNamespace} + var err error + client, cfg, err = k8sClientForCollectors() + if err != nil { + return err + } + } + db.TLS = tls + } + + c := &collect.CollectClickhouse{Collector: db, ClientConfig: cfg, Client: client, Context: cmd.Context()} + res, err := c.Collect(nil) + if err != nil { + return err + } + return printCollectorResult(res) + }, + } + + f := cmd.Flags() + f.StringVar(&uri, "uri", "", "ClickHouse connection DSN, e.g. clickhouse://user:pass@host:9000/db (required)") + cmd.MarkFlagRequired("uri") + f.BoolVar(&skipVerify, "tls-skip-verify", false, "skip TLS certificate verification") + f.StringVar(&caCert, "tls-cacert", "", "CA certificate (PEM contents or a file path)") + f.StringVar(&clientCert, "tls-client-cert", "", "client certificate (PEM contents or a file path)") + f.StringVar(&clientKey, "tls-client-key", "", "client key (PEM contents or a file path)") + f.StringVar(&secretName, "tls-secret-name", "", "name of a Secret holding TLS material (requires cluster access)") + f.StringVar(&secretNamespace, "tls-secret-namespace", "default", "namespace of the TLS Secret") + + return cmd +} diff --git a/cmd/collect/cli/collectors_common.go b/cmd/collect/cli/collectors_common.go new file mode 100644 index 00000000..9b5da461 --- /dev/null +++ b/cmd/collect/cli/collectors_common.go @@ -0,0 +1,47 @@ +// Package cli implements the `collect` command and its subcommands. +// +// The per-collector subcommands (http, postgres, mysql, mssql, redis) run a +// single collector and print its native result JSON to stdout. This lets a +// collector run inside a Pod using the troubleshoot image — e.g. as a runPod +// collector — so the check executes from within the cluster instead of from +// wherever the CLI happens to be invoked. +package cli + +import ( + "fmt" + + "github.com/replicatedhq/troubleshoot/pkg/collect" + "github.com/replicatedhq/troubleshoot/pkg/constants" + "github.com/replicatedhq/troubleshoot/pkg/k8sutil" + "github.com/replicatedhq/troubleshoot/pkg/version" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" +) + +// printCollectorResult writes the native collector result JSON to stdout. +// The single-collector subcommands run with an empty BundlePath, so the result +// is held in memory (map value = the JSON bytes) rather than written to disk. +func printCollectorResult(res collect.CollectorResult) error { + for _, b := range res { + fmt.Println(string(b)) + } + return nil +} + +// k8sClientForCollectors builds a Kubernetes client from the ambient kubeconfig +// or in-cluster config. Only the collectors that resolve TLS material from a +// Secret need this; plain connections and inline/file TLS do not. +func k8sClientForCollectors() (kubernetes.Interface, *rest.Config, error) { + cfg, err := k8sutil.GetRESTConfig() + if err != nil { + return nil, nil, err + } + cfg.QPS = constants.DEFAULT_CLIENT_QPS + cfg.Burst = constants.DEFAULT_CLIENT_BURST + cfg.UserAgent = fmt.Sprintf("%s/%s", constants.DEFAULT_CLIENT_USER_AGENT, version.Version()) + client, err := kubernetes.NewForConfig(cfg) + if err != nil { + return nil, nil, err + } + return client, cfg, nil +} diff --git a/cmd/collect/cli/http.go b/cmd/collect/cli/http.go new file mode 100644 index 00000000..42ce07a9 --- /dev/null +++ b/cmd/collect/cli/http.go @@ -0,0 +1,79 @@ +package cli + +import ( + "fmt" + "strings" + + troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" + "github.com/replicatedhq/troubleshoot/pkg/collect" + "github.com/spf13/cobra" +) + +// HTTPCmd runs the http collector against a single endpoint and prints the +// native result JSON ({"response":{"status":...}} or {"error":{...}}). +func HTTPCmd() *cobra.Command { + var ( + method string + url string + headers map[string]string + body string + timeout string + proxy string + insecure bool + caCert string + ) + + cmd := &cobra.Command{ + Use: "http", + Short: "Run the http collector against an endpoint", + Long: `Run the http collector: issue an HTTP request and print the collector result as JSON. + +The result contains the response status, body and headers, or an error object. + +Examples: + collect http --url http://myapp.default.svc:8080/healthz + collect http --method POST --url https://api.internal/ping \ + --header 'Content-Type=application/json' --body '{"ping":true}'`, + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + var tls *troubleshootv1beta2.TLSParams + if caCert != "" { + tls = &troubleshootv1beta2.TLSParams{CACert: caCert} + } + + spec := &troubleshootv1beta2.HTTP{ + CollectorMeta: troubleshootv1beta2.CollectorMeta{CollectorName: "http"}, + } + switch strings.ToUpper(method) { + case "GET": + spec.Get = &troubleshootv1beta2.Get{URL: url, Headers: headers, Timeout: timeout, Proxy: proxy, InsecureSkipVerify: insecure, TLS: tls} + case "POST": + spec.Post = &troubleshootv1beta2.Post{URL: url, Headers: headers, Body: body, Timeout: timeout, Proxy: proxy, InsecureSkipVerify: insecure, TLS: tls} + case "PUT": + spec.Put = &troubleshootv1beta2.Put{URL: url, Headers: headers, Body: body, Timeout: timeout, Proxy: proxy, InsecureSkipVerify: insecure, TLS: tls} + default: + return fmt.Errorf("unsupported --method %q (use GET, POST, or PUT)", method) + } + + c := &collect.CollectHTTP{Collector: spec} + res, err := c.Collect(nil) + if err != nil { + return err + } + return printCollectorResult(res) + }, + } + + f := cmd.Flags() + f.StringVar(&url, "url", "", "request URL (required)") + cmd.MarkFlagRequired("url") + f.StringVar(&method, "method", "GET", "HTTP method: GET, POST, or PUT") + f.StringToStringVar(&headers, "header", nil, "request header as key=value (repeatable)") + f.StringVar(&body, "body", "", "request body (POST/PUT only)") + f.StringVar(&timeout, "timeout", "", "request timeout, e.g. 15s (empty = no timeout)") + f.StringVar(&proxy, "proxy", "", "proxy URL to use for the request") + f.BoolVar(&insecure, "insecure-skip-verify", false, "do not verify the server's TLS certificate") + f.StringVar(&caCert, "tls-cacert", "", "CA certificate to trust (PEM contents or a file path)") + + return cmd +} diff --git a/cmd/collect/cli/mssql.go b/cmd/collect/cli/mssql.go new file mode 100644 index 00000000..49bd5633 --- /dev/null +++ b/cmd/collect/cli/mssql.go @@ -0,0 +1,49 @@ +package cli + +import ( + troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" + "github.com/replicatedhq/troubleshoot/pkg/collect" + "github.com/spf13/cobra" +) + +// MssqlCmd runs the mssql collector against a single database and prints the +// native result JSON ({"isConnected":..,"version":..,"error":..}). +// +// Unlike the other database collectors, the mssql collector does not honor a +// separate TLS field — TLS is configured through the connection URI query +// parameters (e.g. encrypt=true) — so this subcommand only takes --uri. +func MssqlCmd() *cobra.Command { + var uri string + + cmd := &cobra.Command{ + Use: "mssql", + Short: "Run the mssql collector against a database", + Long: `Run the mssql collector: connect to a Microsoft SQL Server, run "select @@VERSION", +and print the collector result as JSON. + +--uri is a go-mssqldb connection URL; TLS options are passed as query parameters: + sqlserver://user:password@host:1433?database=app&encrypt=true + +Example: + collect mssql --uri "sqlserver://sa:pass@mssql.default.svc:1433?encrypt=true"`, + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + db := &troubleshootv1beta2.Database{ + CollectorMeta: troubleshootv1beta2.CollectorMeta{CollectorName: "mssql"}, + URI: uri, + } + + c := &collect.CollectMssql{Collector: db, Context: cmd.Context()} + res, err := c.Collect(nil) + if err != nil { + return err + } + return printCollectorResult(res) + }, + } + + cmd.Flags().StringVar(&uri, "uri", "", "SQL Server connection URI, e.g. sqlserver://user:pass@host:1433 (required)") + cmd.MarkFlagRequired("uri") + + return cmd +} diff --git a/cmd/collect/cli/mysql.go b/cmd/collect/cli/mysql.go new file mode 100644 index 00000000..aeb7bb44 --- /dev/null +++ b/cmd/collect/cli/mysql.go @@ -0,0 +1,90 @@ +package cli + +import ( + troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" + "github.com/replicatedhq/troubleshoot/pkg/collect" + "github.com/spf13/cobra" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" +) + +// MysqlCmd runs the mysql collector against a single database and prints the +// native result JSON ({"isConnected":..,"version":..,"variables":..,"error":..}). +func MysqlCmd() *cobra.Command { + var ( + uri string + parameters []string + skipVerify bool + caCert string + clientCert string + clientKey string + secretName string + secretNamespace string + ) + + cmd := &cobra.Command{ + Use: "mysql", + Short: "Run the mysql collector against a database", + Long: `Run the mysql collector: connect to a MySQL server, run "select version()", +optionally collect server variables, and print the collector result as JSON. + +--uri is a go-sql-driver DSN (note: not a URL): + user:password@tcp(host:3306)/dbname + +--parameters names server variables to collect via "SHOW VARIABLES"; the matching +values are returned under "variables" in the result. This flag is unique to mysql. + +Example: + collect mysql --uri "root:pass@tcp(mysql.default.svc:3306)/" --parameters max_connections,version`, + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + db := &troubleshootv1beta2.Database{ + CollectorMeta: troubleshootv1beta2.CollectorMeta{CollectorName: "mysql"}, + URI: uri, + Parameters: parameters, + } + + var ( + client kubernetes.Interface + cfg *rest.Config + ) + if skipVerify || caCert != "" || clientCert != "" || clientKey != "" || secretName != "" { + tls := &troubleshootv1beta2.TLSParams{ + SkipVerify: skipVerify, + CACert: caCert, + ClientCert: clientCert, + ClientKey: clientKey, + } + if secretName != "" { + tls.Secret = &troubleshootv1beta2.TLSSecret{Name: secretName, Namespace: secretNamespace} + var err error + client, cfg, err = k8sClientForCollectors() + if err != nil { + return err + } + } + db.TLS = tls + } + + c := &collect.CollectMysql{Collector: db, ClientConfig: cfg, Client: client, Context: cmd.Context()} + res, err := c.Collect(nil) + if err != nil { + return err + } + return printCollectorResult(res) + }, + } + + f := cmd.Flags() + f.StringVar(&uri, "uri", "", "MySQL connection DSN, e.g. user:pass@tcp(host:3306)/db (required)") + cmd.MarkFlagRequired("uri") + f.StringSliceVar(¶meters, "parameters", nil, "server variables to collect via SHOW VARIABLES (comma-separated or repeatable)") + f.BoolVar(&skipVerify, "tls-skip-verify", false, "skip TLS certificate verification") + f.StringVar(&caCert, "tls-cacert", "", "CA certificate (PEM contents or a file path)") + f.StringVar(&clientCert, "tls-client-cert", "", "client certificate (PEM contents or a file path)") + f.StringVar(&clientKey, "tls-client-key", "", "client key (PEM contents or a file path)") + f.StringVar(&secretName, "tls-secret-name", "", "name of a Secret holding TLS material (requires cluster access)") + f.StringVar(&secretNamespace, "tls-secret-namespace", "default", "namespace of the TLS Secret") + + return cmd +} diff --git a/cmd/collect/cli/postgres.go b/cmd/collect/cli/postgres.go new file mode 100644 index 00000000..297699d5 --- /dev/null +++ b/cmd/collect/cli/postgres.go @@ -0,0 +1,88 @@ +package cli + +import ( + troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" + "github.com/replicatedhq/troubleshoot/pkg/collect" + "github.com/spf13/cobra" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" +) + +// PostgresCmd runs the postgres collector against a single database and prints +// the native result JSON ({"isConnected":..,"version":..,"error":..}). +func PostgresCmd() *cobra.Command { + var ( + uri string + skipVerify bool + caCert string + clientCert string + clientKey string + secretName string + secretNamespace string + ) + + cmd := &cobra.Command{ + Use: "postgres", + Short: "Run the postgres collector against a database", + Long: `Run the postgres collector: connect to a PostgreSQL server, run "select version()", +and print the collector result as JSON. + +--uri is a libpq/pgx connection URI: + postgres://user:password@host:5432/dbname?sslmode=disable + +TLS material may be provided inline / by file path (--tls-cacert, --tls-client-cert, +--tls-client-key) or sourced from a Secret (--tls-secret-name), which requires +cluster access. + +Example: + collect postgres --uri "postgres://user:pass@pg.default.svc:5432/app?sslmode=disable"`, + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + db := &troubleshootv1beta2.Database{ + CollectorMeta: troubleshootv1beta2.CollectorMeta{CollectorName: "postgres"}, + URI: uri, + } + + var ( + client kubernetes.Interface + cfg *rest.Config + ) + if skipVerify || caCert != "" || clientCert != "" || clientKey != "" || secretName != "" { + tls := &troubleshootv1beta2.TLSParams{ + SkipVerify: skipVerify, + CACert: caCert, + ClientCert: clientCert, + ClientKey: clientKey, + } + if secretName != "" { + tls.Secret = &troubleshootv1beta2.TLSSecret{Name: secretName, Namespace: secretNamespace} + var err error + client, cfg, err = k8sClientForCollectors() + if err != nil { + return err + } + } + db.TLS = tls + } + + c := &collect.CollectPostgres{Collector: db, ClientConfig: cfg, Client: client, Context: cmd.Context()} + res, err := c.Collect(nil) + if err != nil { + return err + } + return printCollectorResult(res) + }, + } + + f := cmd.Flags() + f.StringVar(&uri, "uri", "", "PostgreSQL connection URI (required)") + cmd.MarkFlagRequired("uri") + f.BoolVar(&skipVerify, "tls-skip-verify", false, "skip TLS certificate verification") + f.StringVar(&caCert, "tls-cacert", "", "CA certificate (PEM contents or a file path)") + f.StringVar(&clientCert, "tls-client-cert", "", "client certificate (PEM contents or a file path)") + f.StringVar(&clientKey, "tls-client-key", "", "client key (PEM contents or a file path)") + f.StringVar(&secretName, "tls-secret-name", "", "name of a Secret holding TLS material (requires cluster access)") + f.StringVar(&secretNamespace, "tls-secret-namespace", "default", "namespace of the TLS Secret") + + return cmd +} diff --git a/cmd/collect/cli/redis.go b/cmd/collect/cli/redis.go new file mode 100644 index 00000000..b21ed2b1 --- /dev/null +++ b/cmd/collect/cli/redis.go @@ -0,0 +1,89 @@ +package cli + +import ( + troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" + "github.com/replicatedhq/troubleshoot/pkg/collect" + "github.com/spf13/cobra" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" +) + +// RedisCmd runs the redis collector against a single instance and prints the +// native result JSON ({"isConnected":..,"version":..,"error":..}). +func RedisCmd() *cobra.Command { + var ( + uri string + skipVerify bool + caCert string + clientCert string + clientKey string + secretName string + secretNamespace string + ) + + cmd := &cobra.Command{ + Use: "redis", + Short: "Run the redis collector against an instance", + Long: `Run the redis collector: connect to a Redis server, read "INFO server" (server +version), and print the collector result as JSON. + +--uri is a go-redis connection URL: + redis://host:6379 (plaintext) + rediss://user:pass@host:6379 (TLS) + +TLS material may be provided inline / by file path (--tls-cacert, --tls-client-cert, +--tls-client-key) or sourced from a Secret (--tls-secret-name), which requires +cluster access. + +Example: + collect redis --uri "redis://redis.default.svc:6379"`, + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + db := &troubleshootv1beta2.Database{ + CollectorMeta: troubleshootv1beta2.CollectorMeta{CollectorName: "redis"}, + URI: uri, + } + + var ( + client kubernetes.Interface + cfg *rest.Config + ) + if skipVerify || caCert != "" || clientCert != "" || clientKey != "" || secretName != "" { + tls := &troubleshootv1beta2.TLSParams{ + SkipVerify: skipVerify, + CACert: caCert, + ClientCert: clientCert, + ClientKey: clientKey, + } + if secretName != "" { + tls.Secret = &troubleshootv1beta2.TLSSecret{Name: secretName, Namespace: secretNamespace} + var err error + client, cfg, err = k8sClientForCollectors() + if err != nil { + return err + } + } + db.TLS = tls + } + + c := &collect.CollectRedis{Collector: db, ClientConfig: cfg, Client: client, Context: cmd.Context()} + res, err := c.Collect(nil) + if err != nil { + return err + } + return printCollectorResult(res) + }, + } + + f := cmd.Flags() + f.StringVar(&uri, "uri", "", "Redis connection URL, e.g. redis://host:6379 (required)") + cmd.MarkFlagRequired("uri") + f.BoolVar(&skipVerify, "tls-skip-verify", false, "skip TLS certificate verification") + f.StringVar(&caCert, "tls-cacert", "", "CA certificate (PEM contents or a file path)") + f.StringVar(&clientCert, "tls-client-cert", "", "client certificate (PEM contents or a file path)") + f.StringVar(&clientKey, "tls-client-key", "", "client key (PEM contents or a file path)") + f.StringVar(&secretName, "tls-secret-name", "", "name of a Secret holding TLS material (requires cluster access)") + f.StringVar(&secretNamespace, "tls-secret-namespace", "default", "namespace of the TLS Secret") + + return cmd +} diff --git a/cmd/collect/cli/root.go b/cmd/collect/cli/root.go index 86fdc5c6..de929682 100644 --- a/cmd/collect/cli/root.go +++ b/cmd/collect/cli/root.go @@ -19,7 +19,9 @@ func RootCmd() *cobra.Command { Short: "Run a collector", Long: `Run a collector and output the results.`, SilenceUsage: true, - PreRun: func(cmd *cobra.Command, args []string) { + // PersistentPreRun/PersistentPostRun (rather than PreRun/PostRun) so this + // setup also runs for the per-collector subcommands, not just `collect [url]`. + PersistentPreRun: func(cmd *cobra.Command, args []string) { v := viper.GetViper() v.BindPFlags(cmd.Flags()) @@ -38,7 +40,7 @@ func RootCmd() *cobra.Command { return runCollect(v, args[0]) }, - PostRun: func(cmd *cobra.Command, args []string) { + PersistentPostRun: func(cmd *cobra.Command, args []string) { if err := util.StopProfiling(); err != nil { klog.Errorf("Failed to stop profiling: %v", err) } @@ -49,6 +51,17 @@ func RootCmd() *cobra.Command { cmd.AddCommand(util.VersionCmd()) + // Per-collector subcommands: run a single collector and print its native + // result JSON. Each has its own flags and help. These let a collector run + // inside a Pod via the troubleshoot image (e.g. as a runPod collector), so + // the check executes from within the cluster rather than wherever the CLI runs. + cmd.AddCommand(HTTPCmd()) + cmd.AddCommand(PostgresCmd()) + cmd.AddCommand(MysqlCmd()) + cmd.AddCommand(MssqlCmd()) + cmd.AddCommand(RedisCmd()) + cmd.AddCommand(ClickhouseCmd()) + cmd.Flags().StringSlice("redactors", []string{}, "names of the additional redactors to use") cmd.Flags().Bool("redact", true, "enable/disable default redactions") cmd.Flags().String("format", "json", "output format, one of json or raw.") @@ -56,7 +69,7 @@ func RootCmd() *cobra.Command { cmd.Flags().String("collector-pull-policy", "", "the pull policy of the collector image") cmd.Flags().String("selector", "", "selector (label query) to filter remote collection nodes on.") cmd.Flags().Bool("collect-without-permissions", false, "always generate a support bundle, even if it some require additional permissions") - cmd.Flags().Bool("debug", false, "enable debug logging") + cmd.PersistentFlags().Bool("debug", false, "enable debug logging") cmd.Flags().String("chroot", "", "Chroot to path") // hidden in favor of the `insecure-skip-tls-verify` flag @@ -67,7 +80,7 @@ func RootCmd() *cobra.Command { viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) - k8sutil.AddFlags(cmd.Flags()) + k8sutil.AddFlags(cmd.PersistentFlags()) // Initialize klog flags logger.InitKlogFlags(cmd)