diff --git a/dev/sg/config.go b/dev/sg/config.go index 6befedbf425..21cdabc0cb4 100644 --- a/dev/sg/config.go +++ b/dev/sg/config.go @@ -35,6 +35,11 @@ func ParseConfigFile(name string) (*Config, error) { conf.Tests[name] = cmd } + for name, check := range conf.Checks { + check.Name = name + conf.Checks[name] = check + } + return &conf, nil } @@ -107,11 +112,18 @@ func equal(a, b []string) bool { return true } +type Check struct { + Name string `yaml:"-"` + Cmd string `yaml:"cmd"` + FailMessage string `yaml:"failMessage"` +} + type Config struct { Env map[string]string `yaml:"env"` Commands map[string]Command `yaml:"commands"` Commandsets map[string][]string `yaml:"commandsets"` Tests map[string]Command `yaml:"tests"` + Checks map[string]Check `yaml:"checks"` } // Merges merges the top-level entries of two Config objects, with the receiver diff --git a/dev/sg/main.go b/dev/sg/main.go index 9d780a42b50..9884b87bf1e 100644 --- a/dev/sg/main.go +++ b/dev/sg/main.go @@ -22,7 +22,7 @@ var ( ShortHelp: "Run the given command.", FlagSet: runFlagSet, Exec: runExec, - UsageFunc: runUsage, + UsageFunc: printRunUsage, } runSetFlagSet = flag.NewFlagSet("sg run-set", flag.ExitOnError) @@ -32,7 +32,7 @@ var ( ShortHelp: "Run the given command set.", FlagSet: runSetFlagSet, Exec: runSetExec, - UsageFunc: runSetUsage, + UsageFunc: printRunSetUsage, } startFlagSet = flag.NewFlagSet("sg start", flag.ExitOnError) @@ -42,7 +42,7 @@ var ( ShortHelp: "Runs the commandset with the name 'start'.", FlagSet: startFlagSet, Exec: startExec, - UsageFunc: startUsage, + UsageFunc: printStartUsage, } testFlagSet = flag.NewFlagSet("sg test", flag.ExitOnError) @@ -52,7 +52,17 @@ var ( ShortHelp: "Run the given test suite.", FlagSet: testFlagSet, Exec: testExec, - UsageFunc: testUsage, + UsageFunc: printTestUsage, + } + + doctorFlagSet = flag.NewFlagSet("sg doctor", flag.ExitOnError) + doctorCommand = &ffcli.Command{ + Name: "doctor", + ShortUsage: "sg doctor", + ShortHelp: "Run the checks defined in the config file to make sure your system is healthy.", + FlagSet: doctorFlagSet, + Exec: doctorExec, + UsageFunc: printDoctorUsage, } ) @@ -67,9 +77,8 @@ var ( overwriteConfigFlag = rootFlagSet.String("overwrite", defaultConfigOverwriteFile, "configuration overwrites file that is gitignored and can be used to, for example, add credentials") rootCommand = &ffcli.Command{ - ShortUsage: "sg [flags] ", - FlagSet: rootFlagSet, - Subcommands: []*ffcli.Command{runCommand, runSetCommand, startCommand, testCommand}, + ShortUsage: "sg [flags] ", + FlagSet: rootFlagSet, Exec: func(ctx context.Context, args []string) error { return flag.ErrHelp }, @@ -91,6 +100,7 @@ var ( return out.String() }, + Subcommands: []*ffcli.Command{runCommand, runSetCommand, startCommand, testCommand, doctorCommand}, } ) @@ -227,7 +237,11 @@ func runExec(ctx context.Context, args []string) error { return run(ctx, cmd) } -func runUsage(c *ffcli.Command) string { +func doctorExec(ctx context.Context, args []string) error { + return runChecks(ctx, conf.Checks) +} + +func printRunUsage(c *ffcli.Command) string { var out strings.Builder fmt.Fprintf(&out, "USAGE\n") @@ -249,7 +263,7 @@ func runUsage(c *ffcli.Command) string { return out.String() } -func testUsage(c *ffcli.Command) string { +func printTestUsage(c *ffcli.Command) string { var out strings.Builder fmt.Fprintf(&out, "USAGE\n") @@ -271,7 +285,7 @@ func testUsage(c *ffcli.Command) string { return out.String() } -func runSetUsage(c *ffcli.Command) string { +func printRunSetUsage(c *ffcli.Command) string { var out strings.Builder fmt.Fprintf(&out, "USAGE\n") @@ -292,7 +306,7 @@ func runSetUsage(c *ffcli.Command) string { return out.String() } -func startUsage(c *ffcli.Command) string { +func printStartUsage(c *ffcli.Command) string { var out strings.Builder fmt.Fprintf(&out, "USAGE\n") @@ -301,6 +315,15 @@ func startUsage(c *ffcli.Command) string { return out.String() } +func printDoctorUsage(c *ffcli.Command) string { + var out strings.Builder + + fmt.Fprintf(&out, "USAGE\n") + fmt.Fprintf(&out, " sg doctor\n") + + return out.String() +} + func fileExists(path string) (bool, error) { _, err := os.Stat(path) if err != nil { diff --git a/dev/sg/run.go b/dev/sg/run.go index 0daba87a1cf..4e521d4ec75 100644 --- a/dev/sg/run.go +++ b/dev/sg/run.go @@ -382,3 +382,41 @@ func runTest(ctx context.Context, cmd Command, args []string) error { return c.Run() } + +func runChecks(ctx context.Context, checks map[string]Check) error { + root, err := root.RepositoryRoot() + if err != nil { + return err + } + + for _, check := range checks { + commandCtx, cancel := context.WithCancel(ctx) + defer cancel() + + c := exec.CommandContext(commandCtx, "bash", "-c", check.Cmd) + c.Dir = root + c.Env = makeEnv(conf.Env) + + p := out.Pending(output.Linef(output.EmojiLightbulb, output.StylePending, "Running check %q...", check.Name)) + + if cmdOut, err := c.CombinedOutput(); err != nil { + p.Complete(output.Linef(output.EmojiFailure, output.StyleWarning, "Check %q failed: %s", check.Name, err)) + + out.WriteLine(output.Linef("", output.StyleWarning, "%s", check.FailMessage)) + if len(cmdOut) != 0 { + out.WriteLine(output.Linef("", output.StyleWarning, "Check produced the following output:")) + separator := strings.Repeat("-", 80) + line := output.Linef( + "", output.StyleWarning, + "%s\n%s%s%s%s%s", + separator, output.StyleReset, cmdOut, output.StyleWarning, separator, output.StyleReset, + ) + out.WriteLine(line) + } + } else { + p.Complete(output.Linef(output.EmojiSuccess, output.StyleSuccess, "Check %q success!", check.Name)) + } + } + + return nil +} diff --git a/lib/output/_examples/main.go b/lib/output/_examples/main.go index 05025882620..c1ca4c41f73 100644 --- a/lib/output/_examples/main.go +++ b/lib/output/_examples/main.go @@ -6,7 +6,7 @@ import ( "sync" "time" - "github.com/sourcegraph/src-cli/internal/output" + "github.com/sourcegraph/sourcegraph/lib/output" ) var ( diff --git a/sg.config.yaml b/sg.config.yaml index ecd919d5f6a..af7050ccfb0 100644 --- a/sg.config.yaml +++ b/sg.config.yaml @@ -315,6 +315,19 @@ commands: GOGC: 50 PATH: .bin:$PATH +checks: + docker: + cmd: docker -v + failMessage: "Failed to run 'docker -v'. Please make sure Docker is running." + + redis: + cmd: echo "PING" | nc localhost 6379 + failMessage: "Failed to connect to Redis on port 6379. Please make sure Redis is running." + + postgres: + cmd: psql -c 'SELECT 1;' + failMessage: "Failed to connect to Postgres database. Make sure environment variables are setup correctly so that psql can connect." + commandsets: default: - frontend