diff options
-rw-r--r-- | cmd/gitaly/main.go | 18 | ||||
-rw-r--r-- | cmd/gitaly/validate.go | 73 | ||||
-rw-r--r-- | cmd/gitaly/validate_test.go | 106 | ||||
-rw-r--r-- | internal/gitaly/config/config.go | 2 |
4 files changed, 194 insertions, 5 deletions
diff --git a/cmd/gitaly/main.go b/cmd/gitaly/main.go index df49c8b44..4b8a0b3a7 100644 --- a/cmd/gitaly/main.go +++ b/cmd/gitaly/main.go @@ -76,13 +76,23 @@ func flagUsage() { fmt.Println(version.GetVersionString("Gitaly")) fmt.Printf("Usage: %v [command] [options] <configfile>\n", os.Args[0]) flag.PrintDefaults() - fmt.Printf("\nThe commands are:\n\n\tcheck\tchecks accessability of internal Rails API\n") + fmt.Printf(` +The commands are: + + check checks accessability of internal Rails API + validate-configuration validates provided configuration +`) } func main() { - // If invoked with subcommand check - if len(os.Args) > 1 && os.Args[1] == "check" { - execCheck() + // If invoked with subcommand + if len(os.Args) > 1 { + switch os.Args[1] { + case "check": + execCheck() + case "validate-configuration": + execValidateConfiguration() + } } flag.Usage = flagUsage diff --git a/cmd/gitaly/validate.go b/cmd/gitaly/validate.go new file mode 100644 index 000000000..4e5a3f5be --- /dev/null +++ b/cmd/gitaly/validate.go @@ -0,0 +1,73 @@ +package main + +import ( + "encoding/json" + "errors" + "flag" + "fmt" + "io" + "os" + + "github.com/pelletier/go-toml/v2" + "github.com/sirupsen/logrus" + "gitlab.com/gitlab-org/gitaly/v15/internal/gitaly/config" +) + +type validationOutputError struct { + Key []string `json:"key,omitempty"` + Message string `json:"message,omitempty"` +} + +type validationOutput struct { + Errors []validationOutputError `json:"errors,omitempty"` +} + +func execValidateConfiguration() { + logrus.SetLevel(logrus.ErrorLevel) + + command := flag.NewFlagSet("validate-configuration", flag.ExitOnError) + command.Usage = func() { + fmt.Fprintf(os.Stderr, "Usage: %v validate-configuration < <configfile>\n", os.Args[0]) + command.PrintDefaults() + } + + cfg, err := config.Load(os.Stdin) + if err != nil { + terr := &toml.DecodeError{} + if errors.As(err, &terr) { + row, column := terr.Position() + jsonEncoded(os.Stdout, os.Stderr, " ", validationOutput{Errors: []validationOutputError{{ + Key: terr.Key(), + Message: fmt.Sprintf("line %d column %d: %v", row, column, terr.Error()), + }}}) + os.Exit(1) + } + fmt.Fprintf(os.Stderr, "processing input data: %v\n", err) + os.Exit(1) + } + + if err := cfg.Validate(); err != nil { + var terr config.ValidationErrors + if errors.As(err, &terr) { + out := validationOutput{} + for _, err := range terr { + out.Errors = append(out.Errors, validationOutputError{ + Key: err.Key, + Message: err.Message, + }) + } + jsonEncoded(os.Stdout, os.Stderr, " ", out) + os.Exit(1) + } + } + + os.Exit(0) +} + +func jsonEncoded(outStream io.Writer, errStream io.Writer, indent string, val any) { + encoder := json.NewEncoder(outStream) + encoder.SetIndent("", indent) + if err := encoder.Encode(val); err != nil { + fmt.Fprintf(errStream, "writing results: %v\n", err) + } +} diff --git a/cmd/gitaly/validate_test.go b/cmd/gitaly/validate_test.go new file mode 100644 index 000000000..5725f4022 --- /dev/null +++ b/cmd/gitaly/validate_test.go @@ -0,0 +1,106 @@ +package main + +import ( + "bytes" + "io" + "os/exec" + "strings" + "testing" + + "github.com/pelletier/go-toml/v2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gitlab.com/gitlab-org/gitaly/v15/internal/command" + "gitlab.com/gitlab-org/gitaly/v15/internal/gitaly/config" + "gitlab.com/gitlab-org/gitaly/v15/internal/testhelper/testcfg" +) + +func TestValidateConfiguration(t *testing.T) { + t.Parallel() + cfg := testcfg.Build(t) + testcfg.BuildGitaly(t, cfg) + + for _, tc := range []struct { + name string + exitCode int + stdin func(t *testing.T) io.Reader + stderr string + stdout string + }{ + { + name: "ok", + exitCode: 0, + stdin: func(*testing.T) io.Reader { + t.Helper() + var stdin bytes.Buffer + require.NoError(t, toml.NewEncoder(&stdin).Encode(cfg)) + return &stdin + }, + }, + { + name: "bad toml format", + exitCode: 1, + stdin: func(*testing.T) io.Reader { + return strings.NewReader(`graceful_restart_timeout = "bad value"`) + }, + stdout: `{ + "errors": [ + { + "message": "line 1 column 28: toml: time: invalid duration \"bad value\"" + } + ] +} +`, + }, + { + name: "validation failures", + exitCode: 1, + stdin: func(t *testing.T) io.Reader { + cfg := cfg + cfg.Git.Config = []config.GitConfig{{Key: "bad"}} + cfg.Storages = []config.Storage{{Name: " ", Path: cfg.Storages[0].Path}} + var stdin bytes.Buffer + require.NoError(t, toml.NewEncoder(&stdin).Encode(cfg)) + return &stdin + }, + stdout: `{ + "errors": [ + { + "key": [ + "storage", + "name" + ], + "message": "empty value at declaration 1" + }, + { + "key": [ + "git", + "config" + ], + "message": "invalid configuration key 'bad': key must contain at least one section" + } + ] +} +`, + }, + } { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + cmd := exec.Command(cfg.BinaryPath("gitaly"), "validate-configuration") + var stderr, stdout bytes.Buffer + cmd.Stderr = &stderr + cmd.Stdout = &stdout + cmd.Stdin = tc.stdin(t) + + err := cmd.Run() + if tc.exitCode != 0 { + status, ok := command.ExitStatus(err) + require.Truef(t, ok, "%T: %v", err, err) + assert.Equal(t, tc.exitCode, status) + } + assert.Equal(t, tc.stderr, stderr.String()) + assert.Equal(t, tc.stdout, stdout.String()) + }) + } +} diff --git a/internal/gitaly/config/config.go b/internal/gitaly/config/config.go index f36e484f7..632b65c5e 100644 --- a/internal/gitaly/config/config.go +++ b/internal/gitaly/config/config.go @@ -300,7 +300,7 @@ func Load(file io.Reader) (Cfg, error) { } if err := toml.NewDecoder(file).Decode(&cfg); err != nil { - return Cfg{}, fmt.Errorf("load toml: %v", err) + return Cfg{}, fmt.Errorf("load toml: %w", err) } if err := cfg.setDefaults(); err != nil { |