diff options
Diffstat (limited to 'go/internal')
-rw-r--r-- | go/internal/command/command.go | 35 | ||||
-rw-r--r-- | go/internal/command/command_test.go | 71 | ||||
-rw-r--r-- | go/internal/command/commandargs/command_args.go | 82 | ||||
-rw-r--r-- | go/internal/command/commandargs/command_args_test.go | 73 | ||||
-rw-r--r-- | go/internal/command/discover/discover.go | 17 | ||||
-rw-r--r-- | go/internal/command/fallback/fallback.go | 19 | ||||
-rw-r--r-- | go/internal/config/config.go | 30 | ||||
-rw-r--r-- | go/internal/config/config_test.go | 63 | ||||
-rw-r--r-- | go/internal/testhelper/testhelper.go | 17 |
9 files changed, 407 insertions, 0 deletions
diff --git a/go/internal/command/command.go b/go/internal/command/command.go new file mode 100644 index 0000000..cb2acdc --- /dev/null +++ b/go/internal/command/command.go @@ -0,0 +1,35 @@ +package command + +import ( + "gitlab.com/gitlab-org/gitlab-shell/go/internal/command/commandargs" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/command/discover" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/command/fallback" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/config" +) + +type Command interface { + Execute() error +} + +func New(arguments []string, config *config.Config) (Command, error) { + args, err := commandargs.Parse(arguments) + + if err != nil { + return nil, err + } + + if config.FeatureEnabled(string(args.CommandType)) { + return buildCommand(args, config), nil + } + + return &fallback.Command{}, nil +} + +func buildCommand(args *commandargs.CommandArgs, config *config.Config) Command { + switch args.CommandType { + case commandargs.Discover: + return &discover.Command{Config: config, Args: args} + } + + return nil +} diff --git a/go/internal/command/command_test.go b/go/internal/command/command_test.go new file mode 100644 index 0000000..02fc0d0 --- /dev/null +++ b/go/internal/command/command_test.go @@ -0,0 +1,71 @@ +package command + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/command/discover" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/command/fallback" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/config" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/testhelper" +) + +func TestNew(t *testing.T) { + testCases := []struct { + desc string + arguments []string + config *config.Config + environment map[string]string + expectedType interface{} + }{ + { + desc: "it returns a Discover command if the feature is enabled", + arguments: []string{}, + config: &config.Config{ + GitlabUrl: "http+unix://gitlab.socket", + Migration: config.MigrationConfig{Enabled: true, Features: []string{"discover"}}, + }, + environment: map[string]string{ + "SSH_CONNECTION": "1", + "SSH_ORIGINAL_COMMAND": "", + }, + expectedType: &discover.Command{}, + }, + { + desc: "it returns a Fallback command no feature is enabled", + arguments: []string{}, + config: &config.Config{ + GitlabUrl: "http+unix://gitlab.socket", + Migration: config.MigrationConfig{Enabled: false}, + }, + environment: map[string]string{ + "SSH_CONNECTION": "1", + "SSH_ORIGINAL_COMMAND": "", + }, + expectedType: &fallback.Command{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + restoreEnv := testhelper.TempEnv(tc.environment) + defer restoreEnv() + + command, err := New(tc.arguments, tc.config) + + assert.NoError(t, err) + assert.IsType(t, tc.expectedType, command) + }) + } +} + +func TestFailingNew(t *testing.T) { + t.Run("It returns an error when SSH_CONNECTION is not set", func(t *testing.T) { + restoreEnv := testhelper.TempEnv(map[string]string{}) + defer restoreEnv() + + _, err := New([]string{}, &config.Config{}) + + assert.Error(t, err, "Only ssh allowed") + }) +} diff --git a/go/internal/command/commandargs/command_args.go b/go/internal/command/commandargs/command_args.go new file mode 100644 index 0000000..9e679d3 --- /dev/null +++ b/go/internal/command/commandargs/command_args.go @@ -0,0 +1,82 @@ +package commandargs + +import ( + "errors" + "os" + "regexp" +) + +type CommandType string + +const ( + Discover CommandType = "discover" +) + +var ( + whoKeyRegex = regexp.MustCompile(`\bkey-(?P<keyid>\d+)\b`) + whoUsernameRegex = regexp.MustCompile(`\busername-(?P<username>\S+)\b`) +) + +type CommandArgs struct { + GitlabUsername string + GitlabKeyId string + SshCommand string + CommandType CommandType +} + +func Parse(arguments []string) (*CommandArgs, error) { + if sshConnection := os.Getenv("SSH_CONNECTION"); sshConnection == "" { + return nil, errors.New("Only ssh allowed") + } + + info := &CommandArgs{} + + info.parseWho(arguments) + info.parseCommand(os.Getenv("SSH_ORIGINAL_COMMAND")) + + return info, nil +} + +func (c *CommandArgs) parseWho(arguments []string) { + for _, argument := range arguments { + if keyId := tryParseKeyId(argument); keyId != "" { + c.GitlabKeyId = keyId + break + } + + if username := tryParseUsername(argument); username != "" { + c.GitlabUsername = username + break + } + } +} + +func tryParseKeyId(argument string) string { + matchInfo := whoKeyRegex.FindStringSubmatch(argument) + if len(matchInfo) == 2 { + // The first element is the full matched string + // The second element is the named `keyid` + return matchInfo[1] + } + + return "" +} + +func tryParseUsername(argument string) string { + matchInfo := whoUsernameRegex.FindStringSubmatch(argument) + if len(matchInfo) == 2 { + // The first element is the full matched string + // The second element is the named `username` + return matchInfo[1] + } + + return "" +} + +func (c *CommandArgs) parseCommand(commandString string) { + c.SshCommand = commandString + + if commandString == "" { + c.CommandType = Discover + } +} diff --git a/go/internal/command/commandargs/command_args_test.go b/go/internal/command/commandargs/command_args_test.go new file mode 100644 index 0000000..10c46fe --- /dev/null +++ b/go/internal/command/commandargs/command_args_test.go @@ -0,0 +1,73 @@ +package commandargs + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/testhelper" +) + +func TestParseSuccess(t *testing.T) { + testCases := []struct { + desc string + arguments []string + environment map[string]string + expectedArgs *CommandArgs + }{ + // Setting the used env variables for every case to ensure we're + // not using anything set in the original env. + { + desc: "It sets discover as the command when the command string was empty", + environment: map[string]string{ + "SSH_CONNECTION": "1", + "SSH_ORIGINAL_COMMAND": "", + }, + expectedArgs: &CommandArgs{CommandType: Discover}, + }, + { + desc: "It passes on the original ssh command from the environment", + environment: map[string]string{ + "SSH_CONNECTION": "1", + "SSH_ORIGINAL_COMMAND": "hello world", + }, + expectedArgs: &CommandArgs{SshCommand: "hello world"}, + }, { + desc: "It finds the key id in any passed arguments", + environment: map[string]string{ + "SSH_CONNECTION": "1", + "SSH_ORIGINAL_COMMAND": "", + }, + arguments: []string{"hello", "key-123"}, + expectedArgs: &CommandArgs{CommandType: Discover, GitlabKeyId: "123"}, + }, { + desc: "It finds the username in any passed arguments", + environment: map[string]string{ + "SSH_CONNECTION": "1", + "SSH_ORIGINAL_COMMAND": "", + }, + arguments: []string{"hello", "username-jane-doe"}, + expectedArgs: &CommandArgs{CommandType: Discover, GitlabUsername: "jane-doe"}, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + restoreEnv := testhelper.TempEnv(tc.environment) + defer restoreEnv() + + result, err := Parse(tc.arguments) + + assert.NoError(t, err) + assert.Equal(t, tc.expectedArgs, result) + }) + } +} + +func TestParseFailure(t *testing.T) { + t.Run("It fails if SSH connection is not set", func(t *testing.T) { + _, err := Parse([]string{}) + + assert.Error(t, err, "Only ssh allowed") + }) + +} diff --git a/go/internal/command/discover/discover.go b/go/internal/command/discover/discover.go new file mode 100644 index 0000000..63a7a32 --- /dev/null +++ b/go/internal/command/discover/discover.go @@ -0,0 +1,17 @@ +package discover + +import ( + "fmt" + + "gitlab.com/gitlab-org/gitlab-shell/go/internal/command/commandargs" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/config" +) + +type Command struct { + Config *config.Config + Args *commandargs.CommandArgs +} + +func (c *Command) Execute() error { + return fmt.Errorf("No feature is implemented yet") +} diff --git a/go/internal/command/fallback/fallback.go b/go/internal/command/fallback/fallback.go new file mode 100644 index 0000000..a136657 --- /dev/null +++ b/go/internal/command/fallback/fallback.go @@ -0,0 +1,19 @@ +package fallback + +import ( + "os" + "path/filepath" + "syscall" +) + +type Command struct{} + +var ( + binDir = filepath.Dir(os.Args[0]) +) + +func (c *Command) Execute() error { + rubyCmd := filepath.Join(binDir, "gitlab-shell-ruby") + execErr := syscall.Exec(rubyCmd, os.Args, os.Environ()) + return execErr +} diff --git a/go/internal/config/config.go b/go/internal/config/config.go index 435cb29..e745a25 100644 --- a/go/internal/config/config.go +++ b/go/internal/config/config.go @@ -2,8 +2,10 @@ package config import ( "io/ioutil" + "net/url" "os" "path" + "strings" yaml "gopkg.in/yaml.v2" ) @@ -23,6 +25,7 @@ type Config struct { LogFile string `yaml:"log_file"` LogFormat string `yaml:"log_format"` Migration MigrationConfig `yaml:"migration"` + GitlabUrl string `yaml:"gitlab_url"` } func New() (*Config, error) { @@ -38,6 +41,24 @@ func NewFromDir(dir string) (*Config, error) { return newFromFile(path.Join(dir, configFile)) } +func (c *Config) FeatureEnabled(featureName string) bool { + if !c.Migration.Enabled { + return false + } + + if !strings.HasPrefix(c.GitlabUrl, "http+unix://") { + return false + } + + for _, enabledFeature := range c.Migration.Features { + if enabledFeature == featureName { + return true + } + } + + return false +} + func newFromFile(filename string) (*Config, error) { cfg := &Config{RootDir: path.Dir(filename)} @@ -71,5 +92,14 @@ func parseConfig(configBytes []byte, cfg *Config) error { cfg.LogFormat = "text" } + if cfg.GitlabUrl != "" { + unescapedUrl, err := url.PathUnescape(cfg.GitlabUrl) + if err != nil { + return err + } + + cfg.GitlabUrl = unescapedUrl + } + return nil } diff --git a/go/internal/config/config_test.go b/go/internal/config/config_test.go index 87a582f..6640f42 100644 --- a/go/internal/config/config_test.go +++ b/go/internal/config/config_test.go @@ -4,6 +4,8 @@ import ( "fmt" "strings" "testing" + + "github.com/stretchr/testify/assert" ) func TestParseConfig(t *testing.T) { @@ -12,6 +14,7 @@ func TestParseConfig(t *testing.T) { yaml string path string format string + gitlabUrl string migration MigrationConfig }{ {path: "/foo/bar/gitlab-shell.log", format: "text"}, @@ -24,6 +27,12 @@ func TestParseConfig(t *testing.T) { format: "text", migration: MigrationConfig{Enabled: true, Features: []string{"foo", "bar"}}, }, + { + yaml: "gitlab_url: http+unix://%2Fpath%2Fto%2Fgitlab%2Fgitlab.socket", + path: "/foo/bar/gitlab-shell.log", + format: "text", + gitlabUrl: "http+unix:///path/to/gitlab/gitlab.socket", + }, } for _, tc := range testCases { @@ -48,6 +57,60 @@ func TestParseConfig(t *testing.T) { if cfg.LogFormat != tc.format { t.Fatalf("expected %q, got %q", tc.format, cfg.LogFormat) } + + assert.Equal(t, tc.gitlabUrl, cfg.GitlabUrl) + }) + } +} + +func TestFeatureEnabled(t *testing.T) { + testCases := []struct { + desc string + config *Config + feature string + expectEnabled bool + }{ + { + desc: "When the protocol is supported and the feature enabled", + config: &Config{ + GitlabUrl: "http+unix://gitlab.socket", + Migration: MigrationConfig{Enabled: true, Features: []string{"discover"}}, + }, + feature: "discover", + expectEnabled: true, + }, + { + desc: "When the protocol is supported and the feature is not enabled", + config: &Config{ + GitlabUrl: "http+unix://gitlab.socket", + Migration: MigrationConfig{Enabled: true, Features: []string{}}, + }, + feature: "discover", + expectEnabled: false, + }, + { + desc: "When the protocol is supported and all features are disabled", + config: &Config{ + GitlabUrl: "http+unix://gitlab.socket", + Migration: MigrationConfig{Enabled: false, Features: []string{"discover"}}, + }, + feature: "discover", + expectEnabled: false, + }, + { + desc: "When the protocol is not supported", + config: &Config{ + GitlabUrl: "https://localhost:3000", + Migration: MigrationConfig{Enabled: true, Features: []string{"discover"}}, + }, + feature: "discover", + expectEnabled: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + assert.Equal(t, tc.expectEnabled, tc.config.FeatureEnabled(string(tc.feature))) }) } } diff --git a/go/internal/testhelper/testhelper.go b/go/internal/testhelper/testhelper.go new file mode 100644 index 0000000..5cdab89 --- /dev/null +++ b/go/internal/testhelper/testhelper.go @@ -0,0 +1,17 @@ +package testhelper + +import "os" + +func TempEnv(env map[string]string) func() { + var original = make(map[string]string) + for key, value := range env { + original[key] = os.Getenv(key) + os.Setenv(key, value) + } + + return func() { + for key, originalValue := range original { + os.Setenv(key, originalValue) + } + } +} |