Skip to content

Commit 4c37c34

Browse files
committed
Add configuration files and tests!
1 parent a10c4d5 commit 4c37c34

33 files changed

+1157
-583
lines changed

.vscode/settings.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,16 @@
2020
"derphttp",
2121
"derpmap",
2222
"devel",
23+
"devtunnel",
2324
"dflags",
2425
"drpc",
2526
"drpcconn",
2627
"drpcmux",
2728
"drpcserver",
2829
"Dsts",
30+
"embeddedpostgres",
2931
"enablements",
32+
"errgroup",
3033
"eventsourcemock",
3134
"fatih",
3235
"Formik",
@@ -80,6 +83,7 @@
8083
"parameterscopeid",
8184
"pqtype",
8285
"prometheusmetrics",
86+
"promhttp",
8387
"promptui",
8488
"protobuf",
8589
"provisionerd",

cli/agent.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,8 +194,9 @@ func workspaceAgent() *cobra.Command {
194194
Logger: logger,
195195
EnvironmentVariables: map[string]string{
196196
// Override the "CODER_AGENT_TOKEN" variable in all
197-
// shells so "gitssh" works!
197+
// shells so "gitssh" and "gitaskpass" works!
198198
"CODER_AGENT_TOKEN": client.SessionToken,
199+
"GIT_ASKPASS": executablePath,
199200
},
200201
CoordinatorDialer: client.ListenWorkspaceAgentTailnet,
201202
StatsReporter: client.AgentReportStats,

cli/config/file.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ func (r Root) PostgresPort() File {
4242
return File(filepath.Join(r.PostgresPath(), "port"))
4343
}
4444

45+
func (r Root) ServerConfig() File {
46+
return File(filepath.Join(string(r), "server.yaml"))
47+
}
48+
4549
// File provides convenience methods for interacting with *os.File.
4650
type File string
4751

cli/config/server.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package config
2+
3+
import (
4+
"errors"
5+
"net/url"
6+
"os"
7+
8+
"github.com/spf13/cobra"
9+
"gopkg.in/yaml.v3"
10+
11+
"github.com/coder/coder/cli/cliui"
12+
"github.com/coder/coder/coderd/gitauth"
13+
14+
_ "embed"
15+
)
16+
17+
//go:embed server.yaml
18+
var defaultServer string
19+
20+
// Server represents a parsed server configuration.
21+
type Server struct {
22+
GitAuth []*gitauth.Config
23+
}
24+
25+
// ParseServer creates or consumes a server config by path.
26+
// If one does not exist, it will create one. If it fails to create,
27+
// a warning will appear but the server will not fail to start.
28+
// This is to prevent blocking execution on readonly file-systems
29+
// that didn't provide a default config.
30+
func ParseServer(cmd *cobra.Command, accessURL *url.URL, path string) (*Server, error) {
31+
_, err := os.Stat(path)
32+
if errors.Is(err, os.ErrNotExist) {
33+
err = os.WriteFile(path, []byte(defaultServer), 0600)
34+
if err != nil {
35+
cmd.Printf("%s Unable to write the default config file: %s", cliui.Styles.Warn.Render("Warning:"), err)
36+
}
37+
}
38+
data, err := os.ReadFile(path)
39+
if err != nil {
40+
data = []byte(defaultServer)
41+
}
42+
var server struct {
43+
GitAuth []*gitauth.YAML `yaml:"gitauth"`
44+
}
45+
err = yaml.Unmarshal(data, &server)
46+
if err != nil {
47+
return nil, err
48+
}
49+
configs, err := gitauth.ConvertYAML(server.GitAuth, accessURL)
50+
if err != nil {
51+
return nil, err
52+
}
53+
return &Server{
54+
GitAuth: configs,
55+
}, nil
56+
}

cli/config/server.yaml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Coder Server Configuration
2+
3+
# Automatically authenticate HTTP(s) Git requests.
4+
gitauth:
5+
# Supported: azure-devops, bitbucket, github, gitlab
6+
# - type: github
7+
# client_id: xxxxxx
8+
# client_secret: xxxxxx
9+
10+
# Multiple providers are an Enterprise feature.
11+
# Contact sales@coder.com for a license.
12+
#
13+
# If multiple providers are used, a unique "id"
14+
# must be provided for each one.
15+
# - id: example
16+
# type: azure-devops
17+
# client_id: xxxxxxx
18+
# client_secret: xxxxxxx
19+
# A custom regex can be used to match a specific
20+
# repository or organization to limit auth scope.
21+
# regex: github.com/coder
22+
# Custom authentication and token URLs should be
23+
# used for self-managed Git provider deployments.
24+
# auth_url: https://example.com/oauth/authorize
25+
# token_url: https://example.com/oauth/token

cli/config/server_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package config_test
2+
3+
import (
4+
"net/url"
5+
"os"
6+
"path/filepath"
7+
"testing"
8+
9+
"github.com/spf13/cobra"
10+
"github.com/stretchr/testify/require"
11+
12+
"github.com/coder/coder/cli/config"
13+
)
14+
15+
func TestServer(t *testing.T) {
16+
t.Parallel()
17+
t.Run("WritesDefault", func(t *testing.T) {
18+
t.Parallel()
19+
path := filepath.Join(t.TempDir(), "server.yaml")
20+
_, err := config.ParseServer(&cobra.Command{}, &url.URL{}, path)
21+
require.NoError(t, err)
22+
data, err := os.ReadFile(path)
23+
require.NoError(t, err)
24+
require.Greater(t, len(data), 0)
25+
})
26+
t.Run("Filled", func(t *testing.T) {
27+
t.Parallel()
28+
path := filepath.Join(t.TempDir(), "server.yaml")
29+
err := os.WriteFile(path, []byte(`
30+
gitauth:
31+
- type: github
32+
client_id: xxx
33+
client_secret: xxx
34+
`), 0600)
35+
require.NoError(t, err)
36+
cfg, err := config.ParseServer(&cobra.Command{}, &url.URL{}, path)
37+
require.NoError(t, err)
38+
require.Len(t, cfg.GitAuth, 1)
39+
})
40+
}

cli/deployment/flags.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ func Flags() *codersdk.DeploymentFlags {
5050
Hidden: true,
5151
Default: time.Minute,
5252
},
53+
ConfigPath: &codersdk.StringFlag{
54+
Name: "Configuration Path",
55+
Flag: "config",
56+
EnvVar: "CODER_SERVER_CONFIG",
57+
Description: "Path to the Coder configuration file.",
58+
},
5359
DerpServerEnable: &codersdk.BoolFlag{
5460
Name: "DERP Server Enabled",
5561
Flag: "derp-server-enable",

cli/gitaskpass.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
"os/signal"
6+
"time"
7+
8+
"github.com/spf13/cobra"
9+
"golang.org/x/xerrors"
10+
11+
"github.com/coder/coder/coderd/gitauth"
12+
"github.com/coder/retry"
13+
)
14+
15+
func gitAskpass() *cobra.Command {
16+
return &cobra.Command{
17+
Use: "gitaskpass",
18+
Hidden: true,
19+
Args: cobra.ExactArgs(1),
20+
RunE: func(cmd *cobra.Command, args []string) (err error) {
21+
ctx := cmd.Context()
22+
23+
ctx, stop := signal.NotifyContext(ctx, interruptSignals...)
24+
defer stop()
25+
26+
defer func() {
27+
if ctx.Err() != nil {
28+
err = ctx.Err()
29+
}
30+
}()
31+
32+
user, host, err := gitauth.ParseAskpass(args[0])
33+
if err != nil {
34+
return xerrors.Errorf("parse host: %w", err)
35+
}
36+
37+
client, err := createAgentClient(cmd)
38+
if err != nil {
39+
return xerrors.Errorf("create agent client: %w", err)
40+
}
41+
42+
token, err := client.WorkspaceAgentGitAuth(ctx, host, false)
43+
if err != nil {
44+
return xerrors.Errorf("get git token: %w", err)
45+
}
46+
if token.URL != "" {
47+
cmd.Printf("Visit the following URL to authenticate with Git:\n%s\n", token.URL)
48+
for r := retry.New(time.Second, 10*time.Second); r.Wait(ctx); {
49+
token, err = client.WorkspaceAgentGitAuth(ctx, host, true)
50+
if err != nil {
51+
continue
52+
}
53+
cmd.Printf("\nYou've been authenticated with Git!\n")
54+
break
55+
}
56+
}
57+
58+
if token.Password != "" {
59+
if user == "" {
60+
fmt.Fprintln(cmd.OutOrStdout(), token.Username)
61+
} else {
62+
fmt.Fprintln(cmd.OutOrStdout(), token.Password)
63+
}
64+
} else {
65+
fmt.Fprintln(cmd.OutOrStdout(), token.Username)
66+
}
67+
68+
return nil
69+
},
70+
}
71+
}

cli/gitaskpass_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
package cli_test

cli/root.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"github.com/coder/coder/cli/config"
2626
"github.com/coder/coder/cli/deployment"
2727
"github.com/coder/coder/coderd"
28+
"github.com/coder/coder/coderd/gitauth"
2829
"github.com/coder/coder/codersdk"
2930
)
3031

@@ -109,12 +110,31 @@ func AGPL() []*cobra.Command {
109110
}
110111

111112
func Root(subcommands []*cobra.Command) *cobra.Command {
113+
// The GIT_ASKPASS environment variable must point at
114+
// a binary with no arguments. To prevent writing
115+
// cross-platform scripts to invoke the Coder binary
116+
// with a `gitaskpass` subcommand, we override the entrypoint
117+
// to check if the command was invoked.
118+
isGitAskpass := false
119+
112120
cmd := &cobra.Command{
113121
Use: "coder",
114122
SilenceErrors: true,
115123
SilenceUsage: true,
116124
Long: `Coder — A tool for provisioning self-hosted development environments with Terraform.
117-
`,
125+
`, Args: func(cmd *cobra.Command, args []string) error {
126+
if gitauth.CheckCommand(args, os.Environ()) {
127+
isGitAskpass = true
128+
return nil
129+
}
130+
return cobra.NoArgs(cmd, args)
131+
},
132+
RunE: func(cmd *cobra.Command, args []string) error {
133+
if isGitAskpass {
134+
return gitAskpass().RunE(cmd, args)
135+
}
136+
return cmd.Help()
137+
},
118138
PersistentPreRun: func(cmd *cobra.Command, args []string) {
119139
if cliflag.IsSetBool(cmd, varNoVersionCheck) &&
120140
cliflag.IsSetBool(cmd, varNoFeatureWarning) {
@@ -134,6 +154,9 @@ func Root(subcommands []*cobra.Command) *cobra.Command {
134154
if cmd.Name() == "login" || cmd.Name() == "server" || cmd.Name() == "agent" || cmd.Name() == "gitssh" {
135155
return
136156
}
157+
if isGitAskpass {
158+
return
159+
}
137160

138161
client, err := CreateClient(cmd)
139162
// If we are unable to create a client, presumably the subcommand will fail as well

cli/server.go

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ func Server(dflags *codersdk.DeploymentFlags, newAPI func(context.Context, *code
9090
// be interrupted by additional signals. Note that we avoid
9191
// shadowing cancel() (from above) here because notifyStop()
9292
// restores default behavior for the signals. This protects
93-
// the shutdown sequence from abrubtly terminating things
93+
// the shutdown sequence from abruptly terminating things
9494
// like: database migrations, provisioner work, workspace
9595
// cleanup in dev-mode, etc.
9696
//
@@ -143,13 +143,13 @@ func Server(dflags *codersdk.DeploymentFlags, newAPI func(context.Context, *code
143143
}
144144
}
145145

146-
config := createConfig(cmd)
146+
cfg := createConfig(cmd)
147147
builtinPostgres := false
148148
// Only use built-in if PostgreSQL URL isn't specified!
149149
if !dflags.InMemoryDatabase.Value && dflags.PostgresURL.Value == "" {
150150
var closeFunc func() error
151-
cmd.Printf("Using built-in PostgreSQL (%s)\n", config.PostgresPath())
152-
dflags.PostgresURL.Value, closeFunc, err = startBuiltinPostgres(ctx, config, logger)
151+
cmd.Printf("Using built-in PostgreSQL (%s)\n", cfg.PostgresPath())
152+
dflags.PostgresURL.Value, closeFunc, err = startBuiltinPostgres(ctx, cfg, logger)
153153
if err != nil {
154154
return err
155155
}
@@ -311,6 +311,14 @@ func Server(dflags *codersdk.DeploymentFlags, newAPI func(context.Context, *code
311311
}
312312
}
313313

314+
if dflags.ConfigPath.Value == "" {
315+
dflags.ConfigPath.Value = string(cfg.ServerConfig())
316+
}
317+
serverConfig, err := config.ParseServer(cmd, accessURLParsed, dflags.ConfigPath.Value)
318+
if err != nil {
319+
return xerrors.Errorf("parse server config: %w", err)
320+
}
321+
314322
options := &coderd.Options{
315323
AccessURL: accessURLParsed,
316324
AppHostname: appHostname,
@@ -321,6 +329,7 @@ func Server(dflags *codersdk.DeploymentFlags, newAPI func(context.Context, *code
321329
Pubsub: database.NewPubsubInMemory(),
322330
CacheDir: dflags.CacheDir.Value,
323331
GoogleTokenValidator: googleTokenValidator,
332+
GitAuthConfigs: serverConfig.GitAuth,
324333
SecureAuthCookie: dflags.SecureAuthCookie.Value,
325334
SSHKeygenAlgorithm: sshKeygenAlgorithm,
326335
TracerProvider: tracerProvider,
@@ -602,7 +611,7 @@ func Server(dflags *codersdk.DeploymentFlags, newAPI func(context.Context, *code
602611
// This is helpful for tests, but can be silently ignored.
603612
// Coder may be ran as users that don't have permission to write in the homedir,
604613
// such as via the systemd service.
605-
_ = config.URL().Write(client.URL.String())
614+
_ = cfg.URL().Write(client.URL.String())
606615

607616
// Currently there is no way to ask the server to shut
608617
// itself down, so any exit signal will result in a non-zero

coderd/coderd.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
"github.com/coder/coder/coderd/audit"
3232
"github.com/coder/coder/coderd/awsidentity"
3333
"github.com/coder/coder/coderd/database"
34+
"github.com/coder/coder/coderd/gitauth"
3435
"github.com/coder/coder/coderd/gitsshkey"
3536
"github.com/coder/coder/coderd/httpapi"
3637
"github.com/coder/coder/coderd/httpmw"
@@ -83,7 +84,7 @@ type Options struct {
8384
Telemetry telemetry.Reporter
8485
TracerProvider trace.TracerProvider
8586
AutoImportTemplates []AutoImportTemplate
86-
GitAuthConfigs []*GitAuthConfig
87+
GitAuthConfigs []*gitauth.Config
8788

8889
// TLSCertificates is used to mesh DERP servers securely.
8990
TLSCertificates []tls.Certificate

0 commit comments

Comments
 (0)