Skip to content

Commit 7215126

Browse files
committed
Allow enabling gitlab-shell "discover"-feature
This adds the possibility to enable features for GitLab shell. The first feature being recognized is "Discover": It's the command that is executed when running `ssh git@gitlab.example.com` and is called without a command. The gitlab key id or username is already parsed from the command line arguments. Currently we only support communicating with GitLab-rails using unix sockets. So features will not be enabled if the GitLab-url is using a different protocol. The url for this read from the config yaml. Pending ruby-specs have been added for the gitlab-shell command. Refactor to have separate command packages
1 parent 0cbbe1e commit 7215126

File tree

11 files changed

+507
-69
lines changed

11 files changed

+507
-69
lines changed

go/cmd/gitlab-shell/main.go

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import (
44
"fmt"
55
"os"
66
"path/filepath"
7-
"syscall"
87

8+
"gitlab.com/gitlab-org/gitlab-shell/go/internal/command"
9+
"gitlab.com/gitlab-org/gitlab-shell/go/internal/command/fallback"
910
"gitlab.com/gitlab-org/gitlab-shell/go/internal/config"
1011
)
1112

@@ -19,20 +20,12 @@ func init() {
1920
rootDir = filepath.Dir(binDir)
2021
}
2122

22-
func migrate(*config.Config) (int, bool) {
23-
// TODO: Dispatch appropriate requests to Go handlers and return
24-
// <exitstatus, true> depending on how they fare
25-
return 0, false
26-
}
27-
2823
// rubyExec will never return. It either replaces the current process with a
2924
// Ruby interpreter, or outputs an error and kills the process.
3025
func execRuby() {
31-
rubyCmd := filepath.Join(binDir, "gitlab-shell-ruby")
32-
33-
execErr := syscall.Exec(rubyCmd, os.Args, os.Environ())
34-
if execErr != nil {
35-
fmt.Fprintf(os.Stderr, "Failed to exec(%q): %v\n", rubyCmd, execErr)
26+
cmd := &fallback.Command{}
27+
if err := cmd.Execute(); err != nil {
28+
fmt.Fprintf(os.Stderr, "Failed to exec: %v\n", err)
3629
os.Exit(1)
3730
}
3831
}
@@ -46,11 +39,19 @@ func main() {
4639
execRuby()
4740
}
4841

49-
// Try to handle the command with the Go implementation
50-
if exitCode, done := migrate(config); done {
51-
os.Exit(exitCode)
42+
cmd, err := command.New(os.Args, config)
43+
if err != nil {
44+
// Failed to build the command, fall back to ruby.
45+
// For now this could happen if `SSH_CONNECTION` is not set on
46+
// the environment
47+
fmt.Fprintf(os.Stderr, "Failed to build command: %v\n", err)
48+
execRuby()
5249
}
5350

54-
// Since a migration has not handled the command, fall back to Ruby to do so
55-
execRuby()
51+
// The command will write to STDOUT on execution or replace the current
52+
// process in case of the `fallback.Command`
53+
if err = cmd.Execute(); err != nil {
54+
fmt.Fprintf(os.Stderr, "%s\n", err)
55+
os.Exit(1)
56+
}
5657
}

go/internal/command/command.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package command
2+
3+
import (
4+
"gitlab.com/gitlab-org/gitlab-shell/go/internal/command/commandargs"
5+
"gitlab.com/gitlab-org/gitlab-shell/go/internal/command/discover"
6+
"gitlab.com/gitlab-org/gitlab-shell/go/internal/command/fallback"
7+
"gitlab.com/gitlab-org/gitlab-shell/go/internal/config"
8+
)
9+
10+
type Command interface {
11+
Execute() error
12+
}
13+
14+
func New(arguments []string, config *config.Config) (Command, error) {
15+
args, err := commandargs.Parse(arguments)
16+
17+
if err != nil {
18+
return nil, err
19+
}
20+
21+
if config.FeatureEnabled(string(args.CommandType)) {
22+
return buildCommand(args, config), nil
23+
}
24+
25+
return &fallback.Command{}, nil
26+
}
27+
28+
func buildCommand(args *commandargs.CommandArgs, config *config.Config) Command {
29+
switch args.CommandType {
30+
case commandargs.Discover:
31+
return &discover.Command{Config: config, Args: args}
32+
}
33+
34+
return nil
35+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package command
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"gitlab.com/gitlab-org/gitlab-shell/go/internal/command/discover"
8+
"gitlab.com/gitlab-org/gitlab-shell/go/internal/command/fallback"
9+
"gitlab.com/gitlab-org/gitlab-shell/go/internal/config"
10+
"gitlab.com/gitlab-org/gitlab-shell/go/internal/testhelper"
11+
)
12+
13+
func TestNew(t *testing.T) {
14+
testCases := []struct {
15+
desc string
16+
arguments []string
17+
config *config.Config
18+
environment map[string]string
19+
expectedType interface{}
20+
}{
21+
{
22+
desc: "it returns a Discover command if the feature is enabled",
23+
arguments: []string{},
24+
config: &config.Config{
25+
GitlabUrl: "http+unix://gitlab.socket",
26+
Migration: config.MigrationConfig{Enabled: true, Features: []string{"discover"}},
27+
},
28+
environment: map[string]string{
29+
"SSH_CONNECTION": "1",
30+
"SSH_ORIGINAL_COMMAND": "",
31+
},
32+
expectedType: &discover.Command{},
33+
},
34+
{
35+
desc: "it returns a Fallback command no feature is enabled",
36+
arguments: []string{},
37+
config: &config.Config{
38+
GitlabUrl: "http+unix://gitlab.socket",
39+
Migration: config.MigrationConfig{Enabled: false},
40+
},
41+
environment: map[string]string{
42+
"SSH_CONNECTION": "1",
43+
"SSH_ORIGINAL_COMMAND": "",
44+
},
45+
expectedType: &fallback.Command{},
46+
},
47+
}
48+
49+
for _, tc := range testCases {
50+
t.Run(tc.desc, func(t *testing.T) {
51+
restoreEnv := testhelper.TempEnv(tc.environment)
52+
defer restoreEnv()
53+
54+
command, err := New(tc.arguments, tc.config)
55+
56+
assert.NoError(t, err)
57+
assert.IsType(t, tc.expectedType, command)
58+
})
59+
}
60+
}
61+
62+
func TestFailingNew(t *testing.T) {
63+
t.Run("It returns an error when SSH_CONNECTION is not set", func(t *testing.T) {
64+
restoreEnv := testhelper.TempEnv(map[string]string{})
65+
defer restoreEnv()
66+
67+
_, err := New([]string{}, &config.Config{})
68+
69+
assert.Error(t, err, "Only ssh allowed")
70+
})
71+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package commandargs
2+
3+
import (
4+
"errors"
5+
"os"
6+
"regexp"
7+
)
8+
9+
type CommandType string
10+
11+
const (
12+
Discover CommandType = "discover"
13+
)
14+
15+
var (
16+
whoKeyRegex = regexp.MustCompile(`\bkey-(?P<keyid>\d+)\b`)
17+
whoUsernameRegex = regexp.MustCompile(`\busername-(?P<username>\S+)\b`)
18+
)
19+
20+
type CommandArgs struct {
21+
GitlabUsername string
22+
GitlabKeyId string
23+
SshCommand string
24+
CommandType CommandType
25+
}
26+
27+
func Parse(arguments []string) (*CommandArgs, error) {
28+
if sshConnection := os.Getenv("SSH_CONNECTION"); sshConnection == "" {
29+
return nil, errors.New("Only ssh allowed")
30+
}
31+
32+
info := &CommandArgs{}
33+
34+
info.parseWho(arguments)
35+
info.parseCommand(os.Getenv("SSH_ORIGINAL_COMMAND"))
36+
37+
return info, nil
38+
}
39+
40+
func (info *CommandArgs) parseWho(arguments []string) {
41+
for _, argument := range arguments {
42+
if keyId := tryParseKeyId(argument); keyId != "" {
43+
info.GitlabKeyId = keyId
44+
break
45+
}
46+
47+
if username := tryParseUsername(argument); username != "" {
48+
info.GitlabUsername = username
49+
break
50+
}
51+
}
52+
}
53+
54+
func tryParseKeyId(argument string) string {
55+
matchInfo := whoKeyRegex.FindStringSubmatch(argument)
56+
if len(matchInfo) == 2 {
57+
// The first element is the full matched string
58+
// The second element is the named `keyid`
59+
return matchInfo[1]
60+
}
61+
62+
return ""
63+
}
64+
65+
func tryParseUsername(argument string) string {
66+
matchInfo := whoUsernameRegex.FindStringSubmatch(argument)
67+
if len(matchInfo) == 2 {
68+
// The first element is the full matched string
69+
// The second element is the named `username`
70+
return matchInfo[1]
71+
}
72+
73+
return ""
74+
}
75+
76+
func (c *CommandArgs) parseCommand(commandString string) {
77+
c.SshCommand = commandString
78+
79+
if commandString == "" {
80+
c.CommandType = Discover
81+
}
82+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package commandargs
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"gitlab.com/gitlab-org/gitlab-shell/go/internal/testhelper"
8+
)
9+
10+
func TestParseSuccess(t *testing.T) {
11+
testCases := []struct {
12+
desc string
13+
arguments []string
14+
environment map[string]string
15+
expectedArgs *CommandArgs
16+
}{
17+
// Setting the used env variables for every case to ensure we're
18+
// not using anything set in the original env.
19+
{
20+
desc: "It sets discover as the command when the command string was empty",
21+
environment: map[string]string{
22+
"SSH_CONNECTION": "1",
23+
"SSH_ORIGINAL_COMMAND": "",
24+
},
25+
expectedArgs: &CommandArgs{CommandType: Discover},
26+
},
27+
{
28+
desc: "It passes on the original ssh command from the environment",
29+
environment: map[string]string{
30+
"SSH_CONNECTION": "1",
31+
"SSH_ORIGINAL_COMMAND": "hello world",
32+
},
33+
expectedArgs: &CommandArgs{SshCommand: "hello world"},
34+
}, {
35+
desc: "It finds the key id in any passed arguments",
36+
environment: map[string]string{
37+
"SSH_CONNECTION": "1",
38+
"SSH_ORIGINAL_COMMAND": "",
39+
},
40+
arguments: []string{"hello", "key-123"},
41+
expectedArgs: &CommandArgs{CommandType: Discover, GitlabKeyId: "123"},
42+
}, {
43+
desc: "It finds the username in any passed arguments",
44+
environment: map[string]string{
45+
"SSH_CONNECTION": "1",
46+
"SSH_ORIGINAL_COMMAND": "",
47+
},
48+
arguments: []string{"hello", "username-jane-doe"},
49+
expectedArgs: &CommandArgs{CommandType: Discover, GitlabUsername: "jane-doe"},
50+
},
51+
}
52+
53+
for _, tc := range testCases {
54+
t.Run(tc.desc, func(t *testing.T) {
55+
restoreEnv := testhelper.TempEnv(tc.environment)
56+
defer restoreEnv()
57+
58+
result, err := Parse(tc.arguments)
59+
60+
assert.NoError(t, err)
61+
assert.Equal(t, tc.expectedArgs, result)
62+
})
63+
}
64+
}
65+
66+
func TestParseFailure(t *testing.T) {
67+
t.Run("It fails if SSH connection is not set", func(t *testing.T) {
68+
_, err := Parse([]string{})
69+
70+
assert.Error(t, err, "Only ssh allowed")
71+
})
72+
73+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package discover
2+
3+
import (
4+
"fmt"
5+
6+
"gitlab.com/gitlab-org/gitlab-shell/go/internal/command/commandargs"
7+
"gitlab.com/gitlab-org/gitlab-shell/go/internal/config"
8+
)
9+
10+
type Command struct {
11+
Config *config.Config
12+
Args *commandargs.CommandArgs
13+
}
14+
15+
func (c *Command) Execute() error {
16+
return fmt.Errorf("No feature is implemented yet")
17+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package fallback
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"syscall"
7+
)
8+
9+
type Command struct{}
10+
11+
var (
12+
binDir = filepath.Dir(os.Args[0])
13+
)
14+
15+
func (c *Command) Execute() error {
16+
rubyCmd := filepath.Join(binDir, "gitlab-shell-ruby")
17+
execErr := syscall.Exec(rubyCmd, os.Args, os.Environ())
18+
return execErr
19+
}

0 commit comments

Comments
 (0)