diff --git a/go.mod b/go.mod index 435c0a1b..bc88c6d8 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( github.com/gizak/termui/v3 v3.1.0 github.com/go-openapi/spec v0.19.4 // indirect github.com/go-openapi/validate v0.19.5 // indirect + github.com/go-sql-driver/mysql v1.5.0 github.com/golang/snappy v0.0.1 // indirect github.com/google/gofuzz v1.1.0 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect diff --git a/go.sum b/go.sum index 9c33ea98..add86a69 100644 --- a/go.sum +++ b/go.sum @@ -212,6 +212,8 @@ github.com/go-openapi/validate v0.17.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+ github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4= github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA= github.com/go-openapi/validate v0.19.5/go.mod h1:8DJv2CVJQ6kGNpFW6eV9N3JviE1C85nY1c2z52x1Gk4= +github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gobuffalo/flect v0.1.5 h1:xpKq9ap8MbYfhuPCF0dBH854Gp9CxZjr/IocxELFflo= github.com/gobuffalo/flect v0.1.5/go.mod h1:W3K3X9ksuZfir8f/LrfVtWmCDQFfayuylOJ7sz/Fj80= diff --git a/pkg/analyze/analyzer.go b/pkg/analyze/analyzer.go index ff758676..403a61a9 100644 --- a/pkg/analyze/analyzer.go +++ b/pkg/analyze/analyzer.go @@ -157,15 +157,25 @@ func Analyze(analyzer *troubleshootv1beta1.Analyze, getFile getCollectedFileCont } return analyzeTextAnalyze(analyzer.TextAnalyze, getFile) } - if analyzer.PostgresAnalyze != nil { - isExcluded, err := isExcluded(analyzer.PostgresAnalyze.Exclude) + if analyzer.Postgres != nil { + isExcluded, err := isExcluded(analyzer.Postgres.Exclude) if err != nil { return nil, err } if isExcluded { return nil, nil } - return analyzePostgresAnalyze(analyzer.PostgresAnalyze, getFile) + return analyzePostgres(analyzer.Postgres, getFile) + } + if analyzer.Mysql != nil { + isExcluded, err := isExcluded(analyzer.Mysql.Exclude) + if err != nil { + return nil, err + } + if isExcluded { + return nil, nil + } + return analyzeMysql(analyzer.Mysql, getFile) } return nil, errors.New("invalid analyzer") diff --git a/pkg/analyze/database_shared.go b/pkg/analyze/database_shared.go new file mode 100644 index 00000000..f0b56f51 --- /dev/null +++ b/pkg/analyze/database_shared.go @@ -0,0 +1,52 @@ +package analyzer + +import ( + "fmt" + "strconv" + "strings" + + "github.com/blang/semver" + "github.com/pkg/errors" + "github.com/replicatedhq/troubleshoot/pkg/collect" +) + +func compareDatabaseConditionalToActual(conditional string, result *collect.DatabaseConnection) (bool, error) { + parts := strings.Split(strings.TrimSpace(conditional), " ") + + if len(parts) != 3 { + return false, errors.New("unable to parse conditional") + } + + switch parts[0] { + case "connected": + expected, err := strconv.ParseBool(parts[2]) + if err != nil { + return false, errors.Wrap(err, "failed to parse bool") + } + + switch parts[1] { + case "=", "==", "===": + return expected == result.IsConnected, nil + case "!=", "!==": + return expected != result.IsConnected, nil + + } + + return false, errors.New("unable to parse postgres connected analyzer") + + case "version": + expectedRange, err := semver.ParseRange(fmt.Sprintf("%s %s", parts[1], parts[2])) + if err != nil { + return false, errors.Wrap(err, "failed to parse semver range") + } + + actual, err := semver.Parse(result.Version) + if err != nil { + return false, errors.Wrap(err, "failed to parse actual psotgres version") + } + + return expectedRange(actual), nil + } + + return false, nil +} diff --git a/pkg/analyze/postgres_analyze_test.go b/pkg/analyze/database_shared_test.go similarity index 100% rename from pkg/analyze/postgres_analyze_test.go rename to pkg/analyze/database_shared_test.go diff --git a/pkg/analyze/mysql.go b/pkg/analyze/mysql.go new file mode 100644 index 00000000..ea9e0fb4 --- /dev/null +++ b/pkg/analyze/mysql.go @@ -0,0 +1,110 @@ +package analyzer + +import ( + "encoding/json" + "fmt" + "path" + + "github.com/pkg/errors" + troubleshootv1beta1 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta1" + "github.com/replicatedhq/troubleshoot/pkg/collect" +) + +func analyzeMysql(analyzer *troubleshootv1beta1.DatabaseAnalyze, getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) { + collectorName := analyzer.CollectorName + if collectorName == "" { + collectorName = "mysql" + } + + fullPath := path.Join("mysql", fmt.Sprintf("%s.json", collectorName)) + + collected, err := getCollectedFileContents(fullPath) + if err != nil { + return nil, errors.Wrapf(err, "failed to read collected file name: %s", fullPath) + } + + databaseConnection := collect.DatabaseConnection{} + if err := json.Unmarshal(collected, &databaseConnection); err != nil { + return nil, errors.Wrap(err, "failed to unmarshal databased connection result") + } + + checkName := analyzer.CheckName + if checkName == "" { + checkName = collectorName + } + + result := &AnalyzeResult{ + Title: checkName, + IconKey: "kubernetes_mysql_analyze", + IconURI: "https://troubleshoot.sh/images/analyzer-icons/mysql-analyze.svg", + } + + for _, outcome := range analyzer.Outcomes { + if outcome.Fail != nil { + if outcome.Fail.When == "" { + result.IsFail = true + result.Message = outcome.Fail.Message + result.URI = outcome.Fail.URI + + return result, nil + } + + isMatch, err := compareDatabaseConditionalToActual(outcome.Fail.When, &databaseConnection) + if err != nil { + return result, errors.Wrap(err, "failed to compare mysql database conditional") + } + + if isMatch { + result.IsFail = true + result.Message = outcome.Fail.Message + result.URI = outcome.Fail.URI + + return result, nil + } + } else if outcome.Warn != nil { + if outcome.Pass.When == "" { + result.IsWarn = true + result.Message = outcome.Warn.Message + result.URI = outcome.Warn.URI + + return result, nil + } + + isMatch, err := compareDatabaseConditionalToActual(outcome.Warn.When, &databaseConnection) + if err != nil { + return result, errors.Wrap(err, "failed to compare mysql database conditional") + } + + if isMatch { + result.IsWarn = true + result.Message = outcome.Warn.Message + result.URI = outcome.Warn.URI + + return result, nil + } + } else if outcome.Pass != nil { + if outcome.Pass.When == "" { + result.IsPass = true + result.Message = outcome.Pass.Message + result.URI = outcome.Pass.URI + + return result, nil + } + + isMatch, err := compareDatabaseConditionalToActual(outcome.Pass.When, &databaseConnection) + if err != nil { + return result, errors.Wrap(err, "failed to compare mysql database conditional") + } + + if isMatch { + result.IsPass = true + result.Message = outcome.Pass.Message + result.URI = outcome.Pass.URI + + return result, nil + } + } + } + + return result, nil +} diff --git a/pkg/analyze/postgres_analyze.go b/pkg/analyze/postgres.go similarity index 68% rename from pkg/analyze/postgres_analyze.go rename to pkg/analyze/postgres.go index cbd521c6..9205f1f5 100644 --- a/pkg/analyze/postgres_analyze.go +++ b/pkg/analyze/postgres.go @@ -4,16 +4,13 @@ import ( "encoding/json" "fmt" "path" - "strconv" - "strings" - "github.com/blang/semver" "github.com/pkg/errors" troubleshootv1beta1 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta1" "github.com/replicatedhq/troubleshoot/pkg/collect" ) -func analyzePostgresAnalyze(analyzer *troubleshootv1beta1.PostgresAnalyze, getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) { +func analyzePostgres(analyzer *troubleshootv1beta1.DatabaseAnalyze, getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) { collectorName := analyzer.CollectorName if collectorName == "" { collectorName = "postgres" @@ -111,44 +108,3 @@ func analyzePostgresAnalyze(analyzer *troubleshootv1beta1.PostgresAnalyze, getCo return result, nil } - -func compareDatabaseConditionalToActual(conditional string, result *collect.DatabaseConnection) (bool, error) { - parts := strings.Split(strings.TrimSpace(conditional), " ") - - if len(parts) != 3 { - return false, errors.New("unable to parse conditional") - } - - switch parts[0] { - case "connected": - expected, err := strconv.ParseBool(parts[2]) - if err != nil { - return false, errors.Wrap(err, "failed to parse bool") - } - - switch parts[1] { - case "=", "==", "===": - return expected == result.IsConnected, nil - case "!=", "!==": - return expected != result.IsConnected, nil - - } - - return false, errors.New("unable to parse postgres connected analyzer") - - case "version": - expectedRange, err := semver.ParseRange(fmt.Sprintf("%s %s", parts[1], parts[2])) - if err != nil { - return false, errors.Wrap(err, "failed to parse semver range") - } - - actual, err := semver.Parse(result.Version) - if err != nil { - return false, errors.Wrap(err, "failed to parse actual psotgres version") - } - - return expectedRange(actual), nil - } - - return false, nil -} diff --git a/pkg/apis/troubleshoot/v1beta1/analyzer_shared.go b/pkg/apis/troubleshoot/v1beta1/analyzer_shared.go index a1f0fb20..d2d6e5d3 100644 --- a/pkg/apis/troubleshoot/v1beta1/analyzer_shared.go +++ b/pkg/apis/troubleshoot/v1beta1/analyzer_shared.go @@ -103,7 +103,7 @@ type TextAnalyze struct { Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"` } -type PostgresAnalyze struct { +type DatabaseAnalyze struct { AnalyzeMeta `json:",inline" yaml:",inline"` Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"` CollectorName string `json:"collectorName" yaml:"collectorName"` @@ -128,5 +128,6 @@ type Analyze struct { Distribution *Distribution `json:"distribution,omitempty" yaml:"distribution,omitempty"` NodeResources *NodeResources `json:"nodeResources,omitempty" yaml:"nodeResources,omitempty"` TextAnalyze *TextAnalyze `json:"textAnalyze,omitempty" yaml:"textAnalyze,omitempty"` - PostgresAnalyze *PostgresAnalyze `json:"postgres,omitempty" yaml:"postgres,omitempty"` + Postgres *DatabaseAnalyze `json:"postgres,omitempty" yaml:"postgres,omitempty"` + Mysql *DatabaseAnalyze `json:"mysql,omitempty" yaml:"mysql,omitempty"` } diff --git a/pkg/apis/troubleshoot/v1beta1/collector_shared.go b/pkg/apis/troubleshoot/v1beta1/collector_shared.go index c78bc108..9e61b3bf 100644 --- a/pkg/apis/troubleshoot/v1beta1/collector_shared.go +++ b/pkg/apis/troubleshoot/v1beta1/collector_shared.go @@ -109,7 +109,7 @@ type Put struct { Body string `json:"body,omitempty" yaml:"body,omitempty"` } -type Postgres struct { +type Database struct { CollectorMeta `json:",inline" yaml:",inline"` URI string `json:"uri" yaml:"uri"` } @@ -124,7 +124,8 @@ type Collect struct { Data *Data `json:"data,omitempty" yaml:"data,omitempty"` Copy *Copy `json:"copy,omitempty" yaml:"copy,omitempty"` HTTP *HTTP `json:"http,omitempty" yaml:"http,omitempty"` - Postgres *Postgres `json:"postgres,omitempty" yaml:"postgres,omitempty"` + Postgres *Database `json:"postgres,omitempty" yaml:"postgres,omitempty"` + Mysql *Database `json:"mysql,omitempty" yaml:"mysql,omitempty"` } func (c *Collect) AccessReviewSpecs(overrideNS string) []authorizationv1.SelfSubjectAccessReviewSpec { diff --git a/pkg/apis/troubleshoot/v1beta1/zz_generated.deepcopy.go b/pkg/apis/troubleshoot/v1beta1/zz_generated.deepcopy.go index 9df6565c..8660d2c6 100644 --- a/pkg/apis/troubleshoot/v1beta1/zz_generated.deepcopy.go +++ b/pkg/apis/troubleshoot/v1beta1/zz_generated.deepcopy.go @@ -112,9 +112,14 @@ func (in *Analyze) DeepCopyInto(out *Analyze) { *out = new(TextAnalyze) (*in).DeepCopyInto(*out) } - if in.PostgresAnalyze != nil { - in, out := &in.PostgresAnalyze, &out.PostgresAnalyze - *out = new(PostgresAnalyze) + if in.Postgres != nil { + in, out := &in.Postgres, &out.Postgres + *out = new(DatabaseAnalyze) + (*in).DeepCopyInto(*out) + } + if in.Mysql != nil { + in, out := &in.Mysql, &out.Mysql + *out = new(DatabaseAnalyze) (*in).DeepCopyInto(*out) } } @@ -470,7 +475,12 @@ func (in *Collect) DeepCopyInto(out *Collect) { } if in.Postgres != nil { in, out := &in.Postgres, &out.Postgres - *out = new(Postgres) + *out = new(Database) + **out = **in + } + if in.Mysql != nil { + in, out := &in.Mysql, &out.Mysql + *out = new(Database) **out = **in } } @@ -823,6 +833,49 @@ func (in *Data) DeepCopy() *Data { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Database) DeepCopyInto(out *Database) { + *out = *in + out.CollectorMeta = in.CollectorMeta +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Database. +func (in *Database) DeepCopy() *Database { + if in == nil { + return nil + } + out := new(Database) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DatabaseAnalyze) DeepCopyInto(out *DatabaseAnalyze) { + *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 DatabaseAnalyze. +func (in *DatabaseAnalyze) DeepCopy() *DatabaseAnalyze { + if in == nil { + return nil + } + out := new(DatabaseAnalyze) + 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 @@ -1160,49 +1213,6 @@ func (in *Post) DeepCopy() *Post { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Postgres) DeepCopyInto(out *Postgres) { - *out = *in - out.CollectorMeta = in.CollectorMeta -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Postgres. -func (in *Postgres) DeepCopy() *Postgres { - if in == nil { - return nil - } - out := new(Postgres) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *PostgresAnalyze) DeepCopyInto(out *PostgresAnalyze) { - *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 PostgresAnalyze. -func (in *PostgresAnalyze) DeepCopy() *PostgresAnalyze { - if in == nil { - return nil - } - out := new(PostgresAnalyze) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Preflight) DeepCopyInto(out *Preflight) { *out = *in diff --git a/pkg/collect/collector.go b/pkg/collect/collector.go index 31c96b1a..627af341 100644 --- a/pkg/collect/collector.go +++ b/pkg/collect/collector.go @@ -141,6 +141,16 @@ func (c *Collector) RunCollectorSync() ([]byte, error) { } return Postgres(c.GetContext(), c.Collect.Postgres) } + if c.Collect.Mysql != nil { + isExcluded, err := isExcluded(c.Collect.Mysql.Exclude) + if err != nil { + return nil, err + } + if isExcluded { + return nil, nil + } + return Mysql(c.GetContext(), c.Collect.Mysql) + } return nil, errors.New("no spec found to run") } diff --git a/pkg/collect/database_shared.go b/pkg/collect/database_shared.go new file mode 100644 index 00000000..bb03e728 --- /dev/null +++ b/pkg/collect/database_shared.go @@ -0,0 +1,7 @@ +package collect + +type DatabaseConnection struct { + IsConnected bool `json:"isConnected"` + Error string `json:"error"` + Version string `json:"version"` +} diff --git a/pkg/collect/mysql.go b/pkg/collect/mysql.go new file mode 100644 index 00000000..7bf795d8 --- /dev/null +++ b/pkg/collect/mysql.go @@ -0,0 +1,53 @@ +package collect + +import ( + "database/sql" + "encoding/json" + "fmt" + + _ "github.com/go-sql-driver/mysql" + "github.com/pkg/errors" + troubleshootv1beta1 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta1" +) + +type MysqlOutput map[string][]byte + +func Mysql(ctx *Context, databaseCollector *troubleshootv1beta1.Database) ([]byte, error) { + databaseConnection := DatabaseConnection{} + + db, err := sql.Open("mysql", databaseCollector.URI) + if err != nil { + databaseConnection.Error = err.Error() + } else { + query := `select version()` + row := db.QueryRow(query) + version := "" + if err := row.Scan(&version); err != nil { + databaseConnection.Error = err.Error() + } else { + databaseConnection.IsConnected = true + databaseConnection.Version = version + } + } + + b, err := json.Marshal(databaseConnection) + if err != nil { + return nil, errors.Wrap(err, "failed to marshal database connection") + } + + collectorName := databaseCollector.CollectorName + if collectorName == "" { + collectorName = "mysql" + } + + mysqlOutput := map[string][]byte{ + fmt.Sprintf("mysql/%s.json", collectorName): b, + } + + bb, err := json.Marshal(mysqlOutput) + if err != nil { + return nil, errors.Wrap(err, "failed to marshal mysql output") + } + + return bb, nil +} diff --git a/pkg/collect/postgres.go b/pkg/collect/postgres.go index 5d4263c8..2156aced 100644 --- a/pkg/collect/postgres.go +++ b/pkg/collect/postgres.go @@ -12,16 +12,10 @@ import ( type PostgresOutput map[string][]byte -type DatabaseConnection struct { - IsConnected bool `json:"isConnected"` - Error string `json:"error"` - Version string `json:"version"` -} - -func Postgres(ctx *Context, postgresCollector *troubleshootv1beta1.Postgres) ([]byte, error) { +func Postgres(ctx *Context, databaseCollector *troubleshootv1beta1.Database) ([]byte, error) { databaseConnection := DatabaseConnection{} - db, err := sql.Open("postgres", postgresCollector.URI) + db, err := sql.Open("postgres", databaseCollector.URI) if err != nil { databaseConnection.Error = err.Error() } else { @@ -41,7 +35,7 @@ func Postgres(ctx *Context, postgresCollector *troubleshootv1beta1.Postgres) ([] return nil, errors.Wrap(err, "failed to marshal database connection") } - collectorName := postgresCollector.CollectorName + collectorName := databaseCollector.CollectorName if collectorName == "" { collectorName = "postgres" }