diff options
author | Pavlo Strokov <pstrokov@gitlab.com> | 2023-02-06 19:07:27 +0300 |
---|---|---|
committer | Pavlo Strokov <pstrokov@gitlab.com> | 2023-02-13 14:39:43 +0300 |
commit | 4a527b5dbe7865f5573fe19897a41464d1a25c25 (patch) | |
tree | 4e4f26d73f18410ac8d5ba6697928d541501e979 | |
parent | e4c21598faf9af715c8a38c2bb3b8e132495e564 (diff) |
Gitaly: validate-configuration sub-commandps-config-validation
In order to improve developers experience in using Gitaly
the new 'validate-configuration' is added. The purpose of
it to validate provided configuration before starting
the service. The output lists all the problems of the
configuration in JSON format into STDOUT. The structure
of object includes 'key' which is a path to the field
where the problem detected and the 'message' with an
explanation of the problem.
Changelog: added
Part of: https://gitlab.com/gitlab-org/gitaly/-/issues/4650
-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 { |