From 6d76acad6efb54423c8242f239c7c321b88c790f Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Fri, 3 May 2024 16:17:03 +0000 Subject: [PATCH 1/4] feat: fetch SSH key from Coder if required --- git.go | 46 +++++++++++++++++++++++++++++++++++++--- git_test.go | 60 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 3 deletions(-) diff --git a/git.go b/git.go index 692434bd..438907c3 100644 --- a/git.go +++ b/git.go @@ -2,6 +2,7 @@ package envbuilder import ( "context" + "crypto/md5" "errors" "fmt" "io" @@ -9,8 +10,10 @@ import ( "net/url" "os" "strings" + "time" "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/codersdk/agentsdk" "github.com/go-git/go-billy/v5" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" @@ -22,7 +25,6 @@ import ( gitssh "github.com/go-git/go-git/v5/plumbing/transport/ssh" "github.com/go-git/go-git/v5/storage/filesystem" "github.com/skeema/knownhosts" - "golang.org/x/crypto/ssh" gossh "golang.org/x/crypto/ssh" ) @@ -207,14 +209,29 @@ func SetupRepoAuth(options *Options) transport.AuthMethod { // Assume SSH auth for all other formats. options.Logger(codersdk.LogLevelInfo, "#1: 🔑 Using SSH authentication!") - var signer ssh.Signer + var signer gossh.Signer if options.GitSSHPrivateKeyPath != "" { s, err := ReadPrivateKey(options.GitSSHPrivateKeyPath) if err != nil { options.Logger(codersdk.LogLevelError, "#1: ❌ Failed to read private key from %s: %s", options.GitSSHPrivateKeyPath, err.Error()) } else { - options.Logger(codersdk.LogLevelInfo, "#1: 🔑 Using %s key!", s.PublicKey().Type()) signer = s + options.Logger(codersdk.LogLevelInfo, "#1: 🔑 Using %s key %s!", s.PublicKey().Type(), keyFingerprint(signer)[:8]) + } + } + + // If we have no signer but we have a Coder URL and agent token, try to fetch + // an SSH key from Coder! + if signer == nil && options.CoderAgentURL != nil && options.CoderAgentToken != "" { + options.Logger(codersdk.LogLevelInfo, "#1: 🔑 Fetching key from %s!", options.CoderAgentURL.String()) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + s, err := FetchCoderSSHKey(ctx, options.CoderAgentURL, options.CoderAgentToken) + if err == nil { + signer = s + options.Logger(codersdk.LogLevelInfo, "#1: 🔑 Fetched %s key %s !", signer.PublicKey().Type(), keyFingerprint(signer)[:8]) + } else { + options.Logger(codersdk.LogLevelInfo, "#1: ❌ Failed to fetch SSH key: %w", options.CoderAgentURL.String(), err) } } @@ -251,3 +268,26 @@ func SetupRepoAuth(options *Options) transport.AuthMethod { } return auth } + +// FetchCoderSSHKey fetches the user's Git SSH key from Coder using the supplied +// Coder URL and agent token. +func FetchCoderSSHKey(ctx context.Context, coderURL url.URL, agentToken string) (gossh.Signer, error) { + client := agentsdk.New(&coderURL) + client.SetSessionToken(agentToken) + key, err := client.GitSSHKey(ctx) + if err != nil { + return nil, fmt.Errorf("get coder ssh key: %w", err) + } + signer, err := gossh.ParsePrivateKey([]byte(key.PrivateKey)) + if err != nil { + return nil, fmt.Errorf("parse coder ssh key: %w", err) + } + return signer, nil +} + +// keyFingerprint returns the md5 checksum of the public key of signer. +func keyFingerprint(s gossh.Signer) string { + h := md5.New() + h.Write(s.PublicKey().Marshal()) + return fmt.Sprintf("%x", h.Sum(nil)) +} diff --git a/git_test.go b/git_test.go index 5a575723..644aaae9 100644 --- a/git_test.go +++ b/git_test.go @@ -3,8 +3,10 @@ package envbuilder_test import ( "context" "crypto/ed25519" + "encoding/json" "fmt" "io" + "net/http" "net/http/httptest" "net/url" "os" @@ -13,6 +15,7 @@ import ( "testing" "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/codersdk/agentsdk" "github.com/coder/envbuilder" "github.com/coder/envbuilder/testutil/gittest" "github.com/go-git/go-billy/v5" @@ -20,6 +23,8 @@ import ( "github.com/go-git/go-billy/v5/osfs" githttp "github.com/go-git/go-git/v5/plumbing/transport/http" gitssh "github.com/go-git/go-git/v5/plumbing/transport/ssh" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" gossh "golang.org/x/crypto/ssh" ) @@ -382,6 +387,60 @@ func TestSetupRepoAuth(t *testing.T) { auth := envbuilder.SetupRepoAuth(opts) require.Nil(t, auth) // TODO: actually test SSH_AUTH_SOCK }) + + t.Run("SSH/Coder", func(t *testing.T) { + token := uuid.NewString() + actualSigner, err := gossh.ParsePrivateKey([]byte(testKey)) + require.NoError(t, err) + handler := func(w http.ResponseWriter, r *http.Request) { + hdr := r.Header.Get("Coder-Session-Token") + if !assert.Equal(t, hdr, token) { + w.WriteHeader(http.StatusForbidden) + return + } + switch r.URL.Path { + case "/api/v2/workspaceagents/me/gitsshkey": + _ = json.NewEncoder(w).Encode(&agentsdk.GitSSHKey{ + PublicKey: string(actualSigner.PublicKey().Marshal()), + PrivateKey: string(testKey), + }) + default: + assert.Fail(t, "unknown path: %q", r.URL.Path) + } + } + srv := httptest.NewServer(http.HandlerFunc(handler)) + u, err := url.Parse(srv.URL) + require.NoError(t, err) + opts := &envbuilder.Options{ + CoderAgentURL: u, + CoderAgentToken: token, + GitURL: "ssh://git@host.tld:repo/path", + Logger: testLog(t), + } + auth := envbuilder.SetupRepoAuth(opts) + pk, ok := auth.(*gitssh.PublicKeys) + require.True(t, ok) + require.NotNil(t, pk.Signer) + require.Equal(t, actualSigner, pk.Signer) + }) + + t.Run("SSH/CoderForbidden", func(t *testing.T) { + token := uuid.NewString() + handler := func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusForbidden) + } + srv := httptest.NewServer(http.HandlerFunc(handler)) + u, err := url.Parse(srv.URL) + require.NoError(t, err) + opts := &envbuilder.Options{ + CoderAgentURL: u, + CoderAgentToken: token, + GitURL: "ssh://git@host.tld:repo/path", + Logger: testLog(t), + } + auth := envbuilder.SetupRepoAuth(opts) + require.Nil(t, auth) + }) } func mustRead(t *testing.T, fs billy.Filesystem, path string) string { @@ -405,6 +464,7 @@ func randKeygen(t *testing.T) gossh.Signer { func testLog(t *testing.T) envbuilder.LoggerFunc { return func(_ codersdk.LogLevel, format string, args ...interface{}) { + t.Helper() t.Logf(format, args...) } } From efd51b60b3238aeb434e0aaccf260f153e076a17 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Fri, 3 May 2024 16:21:59 +0000 Subject: [PATCH 2/4] fix up after rebase --- git.go | 14 +++++++++----- git_test.go | 12 +++++------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/git.go b/git.go index 438907c3..03e3792e 100644 --- a/git.go +++ b/git.go @@ -222,8 +222,8 @@ func SetupRepoAuth(options *Options) transport.AuthMethod { // If we have no signer but we have a Coder URL and agent token, try to fetch // an SSH key from Coder! - if signer == nil && options.CoderAgentURL != nil && options.CoderAgentToken != "" { - options.Logger(codersdk.LogLevelInfo, "#1: 🔑 Fetching key from %s!", options.CoderAgentURL.String()) + if signer == nil && options.CoderAgentURL != "" && options.CoderAgentToken != "" { + options.Logger(codersdk.LogLevelInfo, "#1: 🔑 Fetching key from %s!", options.CoderAgentURL) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() s, err := FetchCoderSSHKey(ctx, options.CoderAgentURL, options.CoderAgentToken) @@ -231,7 +231,7 @@ func SetupRepoAuth(options *Options) transport.AuthMethod { signer = s options.Logger(codersdk.LogLevelInfo, "#1: 🔑 Fetched %s key %s !", signer.PublicKey().Type(), keyFingerprint(signer)[:8]) } else { - options.Logger(codersdk.LogLevelInfo, "#1: ❌ Failed to fetch SSH key: %w", options.CoderAgentURL.String(), err) + options.Logger(codersdk.LogLevelInfo, "#1: ❌ Failed to fetch SSH key: %w", options.CoderAgentURL, err) } } @@ -271,8 +271,12 @@ func SetupRepoAuth(options *Options) transport.AuthMethod { // FetchCoderSSHKey fetches the user's Git SSH key from Coder using the supplied // Coder URL and agent token. -func FetchCoderSSHKey(ctx context.Context, coderURL url.URL, agentToken string) (gossh.Signer, error) { - client := agentsdk.New(&coderURL) +func FetchCoderSSHKey(ctx context.Context, coderURL string, agentToken string) (gossh.Signer, error) { + u, err := url.Parse(coderURL) + if err != nil { + return nil, fmt.Errorf("invalid Coder URL: %w", err) + } + client := agentsdk.New(u) client.SetSessionToken(agentToken) key, err := client.GitSSHKey(ctx) if err != nil { diff --git a/git_test.go b/git_test.go index 644aaae9..150c513d 100644 --- a/git_test.go +++ b/git_test.go @@ -393,7 +393,7 @@ func TestSetupRepoAuth(t *testing.T) { actualSigner, err := gossh.ParsePrivateKey([]byte(testKey)) require.NoError(t, err) handler := func(w http.ResponseWriter, r *http.Request) { - hdr := r.Header.Get("Coder-Session-Token") + hdr := r.Header.Get(codersdk.SessionTokenHeader) if !assert.Equal(t, hdr, token) { w.WriteHeader(http.StatusForbidden) return @@ -409,10 +409,8 @@ func TestSetupRepoAuth(t *testing.T) { } } srv := httptest.NewServer(http.HandlerFunc(handler)) - u, err := url.Parse(srv.URL) - require.NoError(t, err) opts := &envbuilder.Options{ - CoderAgentURL: u, + CoderAgentURL: srv.URL, CoderAgentToken: token, GitURL: "ssh://git@host.tld:repo/path", Logger: testLog(t), @@ -427,13 +425,13 @@ func TestSetupRepoAuth(t *testing.T) { t.Run("SSH/CoderForbidden", func(t *testing.T) { token := uuid.NewString() handler := func(w http.ResponseWriter, r *http.Request) { + hdr := r.Header.Get(codersdk.SessionTokenHeader) + assert.Equal(t, hdr, token) w.WriteHeader(http.StatusForbidden) } srv := httptest.NewServer(http.HandlerFunc(handler)) - u, err := url.Parse(srv.URL) - require.NoError(t, err) opts := &envbuilder.Options{ - CoderAgentURL: u, + CoderAgentURL: srv.URL, CoderAgentToken: token, GitURL: "ssh://git@host.tld:repo/path", Logger: testLog(t), From b1ba2802ea6ef11afa963014b00be4833b7e50d4 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Fri, 3 May 2024 16:30:57 +0000 Subject: [PATCH 3/4] update README --- README.md | 18 ++++++++++++++++++ git.go | 7 +++++++ 2 files changed, 25 insertions(+) diff --git a/README.md b/README.md index 0b230cb8..056ac40b 100644 --- a/README.md +++ b/README.md @@ -185,6 +185,24 @@ envbuilder will assume SSH authentication. You have the following options: ghcr.io/coder/envbuilder ``` +1. Fetch the SSH key from Coder: as long as `CODER_AGENT_URL` and + `CODER_AGENT_TOKEN` are set, then envbuilder will attempt to fetch the + corresponding Git SSH key directly from Coder. Example: + + ```terraform + resource "docker_container" "workspace" { + count = data.coder_workspace.me.start_count + image = "ghcr.io/coder/envbuilder:version" + name = + "coder-${data.coder_workspace.me.owner}-${lower(data.coder_workspace.me.name)}" + + env = [ + "CODER_AGENT_TOKEN=${coder_agent.dev.token}", + "CODER_AGENT_URL=${data.coder_workspace.me.access_url}", + ... + ] + ``` + 1. Agent-based authentication: set `SSH_AUTH_SOCK` and mount in your agent socket, for example: ```bash diff --git a/git.go b/git.go index 03e3792e..1367266a 100644 --- a/git.go +++ b/git.go @@ -176,6 +176,13 @@ func LogHostKeyCallback(log LoggerFunc) gossh.HostKeyCallback { // If SSH_PRIVATE_KEY_PATH is set, an SSH private key will be read from // that path and the SSH auth method will be configured with that key. // +// If no SSH_PRIVATE_KEY_PATH is set, but CODER_AGENT_URL and CODER_AGENT_TOKEN +// are both specified, envbuilder will attempt to fetch the corresponding +// Git SSH key for the user. +// +// Otherwise, SSH authentication will fall back to SSH_AUTH_SOCK, in which +// case SSH_AUTH_SOCK must be set to the path of a listening SSH agent socket. +// // If SSH_KNOWN_HOSTS is not set, the SSH auth method will be configured // to accept and log all host keys. Otherwise, host key checking will be // performed as usual. From 19eed21d1424aa2f8e4b7ac716f47eb123111c17 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Fri, 3 May 2024 17:56:12 +0000 Subject: [PATCH 4/4] retry fetching agent ssh key --- envbuilder.go | 2 +- git.go | 43 +++++++++++++++++++++++++++++---- git_test.go | 66 ++++++++++++++++++++++++++++++++++++++------------- 3 files changed, 90 insertions(+), 21 deletions(-) diff --git a/envbuilder.go b/envbuilder.go index eb1fafea..eec447c1 100644 --- a/envbuilder.go +++ b/envbuilder.go @@ -194,7 +194,7 @@ func Run(ctx context.Context, options Options) error { CABundle: caBundle, } - cloneOpts.RepoAuth = SetupRepoAuth(&options) + cloneOpts.RepoAuth = SetupRepoAuth(ctx, &options) if options.GitHTTPProxyURL != "" { cloneOpts.ProxyOptions = transport.ProxyOptions{ URL: options.GitHTTPProxyURL, diff --git a/git.go b/git.go index 1367266a..054351a6 100644 --- a/git.go +++ b/git.go @@ -7,11 +7,13 @@ import ( "fmt" "io" "net" + "net/http" "net/url" "os" "strings" "time" + "github.com/cenkalti/backoff/v4" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/agentsdk" "github.com/go-git/go-billy/v5" @@ -186,7 +188,7 @@ func LogHostKeyCallback(log LoggerFunc) gossh.HostKeyCallback { // If SSH_KNOWN_HOSTS is not set, the SSH auth method will be configured // to accept and log all host keys. Otherwise, host key checking will be // performed as usual. -func SetupRepoAuth(options *Options) transport.AuthMethod { +func SetupRepoAuth(ctx context.Context, options *Options) transport.AuthMethod { if options.GitURL == "" { options.Logger(codersdk.LogLevelInfo, "#1: ❔ No Git URL supplied!") return nil @@ -231,14 +233,14 @@ func SetupRepoAuth(options *Options) transport.AuthMethod { // an SSH key from Coder! if signer == nil && options.CoderAgentURL != "" && options.CoderAgentToken != "" { options.Logger(codersdk.LogLevelInfo, "#1: 🔑 Fetching key from %s!", options.CoderAgentURL) - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + fetchCtx, cancel := context.WithCancel(ctx) defer cancel() - s, err := FetchCoderSSHKey(ctx, options.CoderAgentURL, options.CoderAgentToken) + s, err := FetchCoderSSHKeyRetry(fetchCtx, options.Logger, options.CoderAgentURL, options.CoderAgentToken) if err == nil { signer = s options.Logger(codersdk.LogLevelInfo, "#1: 🔑 Fetched %s key %s !", signer.PublicKey().Type(), keyFingerprint(signer)[:8]) } else { - options.Logger(codersdk.LogLevelInfo, "#1: ❌ Failed to fetch SSH key: %w", options.CoderAgentURL, err) + options.Logger(codersdk.LogLevelInfo, "#1: ❌ Failed to fetch SSH key from %s: %w", options.CoderAgentURL, err) } } @@ -276,6 +278,39 @@ func SetupRepoAuth(options *Options) transport.AuthMethod { return auth } +// FetchCoderSSHKeyRetry wraps FetchCoderSSHKey in backoff.Retry. +// Retries are attempted if Coder responds with a 401 Unauthorized. +// This indicates that the workspace build has not yet completed. +// It will retry for up to 1 minute with exponential backoff. +// Any other error is considered a permanent failure. +func FetchCoderSSHKeyRetry(ctx context.Context, log LoggerFunc, coderURL, agentToken string) (gossh.Signer, error) { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + signerChan := make(chan gossh.Signer, 1) + eb := backoff.NewExponentialBackOff() + eb.MaxElapsedTime = 0 + eb.MaxInterval = time.Minute + bkoff := backoff.WithContext(eb, ctx) + err := backoff.Retry(func() error { + s, err := FetchCoderSSHKey(ctx, coderURL, agentToken) + if err != nil { + var sdkErr *codersdk.Error + if errors.As(err, &sdkErr) && sdkErr.StatusCode() == http.StatusUnauthorized { + // Retry, as this may just mean that the workspace build has not yet + // completed. + log(codersdk.LogLevelInfo, "#1: 🕐 Backing off as the workspace build has not yet completed...") + return err + } + close(signerChan) + return backoff.Permanent(err) + } + signerChan <- s + return nil + }, bkoff) + return <-signerChan, err +} + // FetchCoderSSHKey fetches the user's Git SSH key from Coder using the supplied // Coder URL and agent token. func FetchCoderSSHKey(ctx context.Context, coderURL string, agentToken string) (gossh.Signer, error) { diff --git a/git_test.go b/git_test.go index 150c513d..2addc25a 100644 --- a/git_test.go +++ b/git_test.go @@ -12,6 +12,7 @@ import ( "os" "path/filepath" "regexp" + "sync/atomic" "testing" "github.com/coder/coder/v2/codersdk" @@ -268,11 +269,12 @@ func TestCloneRepoSSH(t *testing.T) { // nolint:paralleltest // t.Setenv for SSH_AUTH_SOCK func TestSetupRepoAuth(t *testing.T) { t.Setenv("SSH_AUTH_SOCK", "") + ctx := context.Background() t.Run("Empty", func(t *testing.T) { opts := &envbuilder.Options{ Logger: testLog(t), } - auth := envbuilder.SetupRepoAuth(opts) + auth := envbuilder.SetupRepoAuth(ctx, opts) require.Nil(t, auth) }) @@ -281,7 +283,7 @@ func TestSetupRepoAuth(t *testing.T) { GitURL: "http://host.tld/repo", Logger: testLog(t), } - auth := envbuilder.SetupRepoAuth(opts) + auth := envbuilder.SetupRepoAuth(ctx, opts) require.Nil(t, auth) }) @@ -292,7 +294,7 @@ func TestSetupRepoAuth(t *testing.T) { GitPassword: "pass", Logger: testLog(t), } - auth := envbuilder.SetupRepoAuth(opts) + auth := envbuilder.SetupRepoAuth(ctx, opts) ba, ok := auth.(*githttp.BasicAuth) require.True(t, ok) require.Equal(t, opts.GitUsername, ba.Username) @@ -306,7 +308,7 @@ func TestSetupRepoAuth(t *testing.T) { GitPassword: "pass", Logger: testLog(t), } - auth := envbuilder.SetupRepoAuth(opts) + auth := envbuilder.SetupRepoAuth(ctx, opts) ba, ok := auth.(*githttp.BasicAuth) require.True(t, ok) require.Equal(t, opts.GitUsername, ba.Username) @@ -320,7 +322,7 @@ func TestSetupRepoAuth(t *testing.T) { GitSSHPrivateKeyPath: kPath, Logger: testLog(t), } - auth := envbuilder.SetupRepoAuth(opts) + auth := envbuilder.SetupRepoAuth(ctx, opts) _, ok := auth.(*gitssh.PublicKeys) require.True(t, ok) }) @@ -332,7 +334,7 @@ func TestSetupRepoAuth(t *testing.T) { GitSSHPrivateKeyPath: kPath, Logger: testLog(t), } - auth := envbuilder.SetupRepoAuth(opts) + auth := envbuilder.SetupRepoAuth(ctx, opts) _, ok := auth.(*gitssh.PublicKeys) require.True(t, ok) }) @@ -345,7 +347,7 @@ func TestSetupRepoAuth(t *testing.T) { GitSSHPrivateKeyPath: kPath, Logger: testLog(t), } - auth := envbuilder.SetupRepoAuth(opts) + auth := envbuilder.SetupRepoAuth(ctx, opts) _, ok := auth.(*gitssh.PublicKeys) require.True(t, ok) }) @@ -358,7 +360,7 @@ func TestSetupRepoAuth(t *testing.T) { GitUsername: "user", Logger: testLog(t), } - auth := envbuilder.SetupRepoAuth(opts) + auth := envbuilder.SetupRepoAuth(ctx, opts) _, ok := auth.(*gitssh.PublicKeys) require.True(t, ok) }) @@ -370,7 +372,7 @@ func TestSetupRepoAuth(t *testing.T) { GitSSHPrivateKeyPath: kPath, Logger: testLog(t), } - auth := envbuilder.SetupRepoAuth(opts) + auth := envbuilder.SetupRepoAuth(ctx, opts) pk, ok := auth.(*gitssh.PublicKeys) require.True(t, ok) require.NotNil(t, pk.Signer) @@ -384,7 +386,7 @@ func TestSetupRepoAuth(t *testing.T) { GitURL: "ssh://git@host.tld:repo/path", Logger: testLog(t), } - auth := envbuilder.SetupRepoAuth(opts) + auth := envbuilder.SetupRepoAuth(ctx, opts) require.Nil(t, auth) // TODO: actually test SSH_AUTH_SOCK }) @@ -415,19 +417,51 @@ func TestSetupRepoAuth(t *testing.T) { GitURL: "ssh://git@host.tld:repo/path", Logger: testLog(t), } - auth := envbuilder.SetupRepoAuth(opts) + auth := envbuilder.SetupRepoAuth(ctx, opts) pk, ok := auth.(*gitssh.PublicKeys) require.True(t, ok) require.NotNil(t, pk.Signer) require.Equal(t, actualSigner, pk.Signer) }) - t.Run("SSH/CoderForbidden", func(t *testing.T) { + t.Run("SSH/CoderRetry", func(t *testing.T) { token := uuid.NewString() + actualSigner, err := gossh.ParsePrivateKey([]byte(testKey)) + require.NoError(t, err) + var count atomic.Int64 + // Return 401 initially, but eventually 200. handler := func(w http.ResponseWriter, r *http.Request) { - hdr := r.Header.Get(codersdk.SessionTokenHeader) - assert.Equal(t, hdr, token) - w.WriteHeader(http.StatusForbidden) + c := count.Add(1) + if c < 3 { + hdr := r.Header.Get(codersdk.SessionTokenHeader) + assert.Equal(t, hdr, token) + w.WriteHeader(http.StatusUnauthorized) + return + } + _ = json.NewEncoder(w).Encode(&agentsdk.GitSSHKey{ + PublicKey: string(actualSigner.PublicKey().Marshal()), + PrivateKey: string(testKey), + }) + } + srv := httptest.NewServer(http.HandlerFunc(handler)) + opts := &envbuilder.Options{ + CoderAgentURL: srv.URL, + CoderAgentToken: token, + GitURL: "ssh://git@host.tld:repo/path", + Logger: testLog(t), + } + auth := envbuilder.SetupRepoAuth(ctx, opts) + pk, ok := auth.(*gitssh.PublicKeys) + require.True(t, ok) + require.NotNil(t, pk.Signer) + require.Equal(t, actualSigner, pk.Signer) + }) + + t.Run("SSH/NotCoder", func(t *testing.T) { + token := uuid.NewString() + handler := func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusTeapot) + _, _ = w.Write([]byte("I'm a teapot!")) } srv := httptest.NewServer(http.HandlerFunc(handler)) opts := &envbuilder.Options{ @@ -436,7 +470,7 @@ func TestSetupRepoAuth(t *testing.T) { GitURL: "ssh://git@host.tld:repo/path", Logger: testLog(t), } - auth := envbuilder.SetupRepoAuth(opts) + auth := envbuilder.SetupRepoAuth(ctx, opts) require.Nil(t, auth) }) }