Files
coder/cli/gitssh_test.go
T
Mathias Fredriksson d06b21df45 test(cli): increase timeout in TestGitSSH to reduce flakes (#21725)
The test occasionally times out at 15s on Windows CI runners.
Investigation of CI logs shows the HTTP request to the agent's
gitsshkey endpoint never appears in server logs, suggesting it
hangs before the request completes (possibly in connection setup,
middleware, or database queries). Increase to 60s to reduce flake
rate.

Fixes coder/internal#770
2026-01-28 14:01:07 +02:00

243 lines
6.5 KiB
Go

package cli_test
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"encoding/pem"
"fmt"
"net"
"os"
"path/filepath"
"strings"
"sync/atomic"
"testing"
"github.com/gliderlabs/ssh"
"github.com/stretchr/testify/require"
gossh "golang.org/x/crypto/ssh"
"github.com/coder/coder/v2/agent"
"github.com/coder/coder/v2/agent/agenttest"
"github.com/coder/coder/v2/cli/clitest"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbfake"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/agentsdk"
"github.com/coder/coder/v2/pty/ptytest"
"github.com/coder/coder/v2/testutil"
)
func prepareTestGitSSH(ctx context.Context, t *testing.T) (*agentsdk.Client, string, gossh.PublicKey) {
t.Helper()
client, db := coderdtest.NewWithDatabase(t, nil)
user := coderdtest.CreateFirstUser(t, client)
ctx, cancel := context.WithCancel(ctx)
defer t.Cleanup(cancel) // Defer so that cancel is the first cleanup.
// get user public key
keypair, err := client.GitSSHKey(ctx, codersdk.Me)
require.NoError(t, err)
//nolint:dogsled
pubkey, _, _, _, err := gossh.ParseAuthorizedKey([]byte(keypair.PublicKey))
require.NoError(t, err)
// setup template
r := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
OrganizationID: user.OrganizationID,
OwnerID: user.UserID,
}).WithAgent().Do()
// start workspace agent
agentClient := agentsdk.New(client.URL, agentsdk.WithFixedToken(r.AgentToken))
_ = agenttest.New(t, client.URL, r.AgentToken, func(o *agent.Options) {
o.Client = agentClient
})
_ = coderdtest.AwaitWorkspaceAgents(t, client, r.Workspace.ID)
return agentClient, r.AgentToken, pubkey
}
func serveSSHForGitSSH(t *testing.T, handler func(ssh.Session), pubkeys ...gossh.PublicKey) *net.TCPAddr {
t.Helper()
// start ssh server
l, err := net.Listen("tcp", "localhost:0")
require.NoError(t, err)
t.Cleanup(func() { _ = l.Close() })
serveOpts := []ssh.Option{
ssh.PublicKeyAuth(func(ctx ssh.Context, key ssh.PublicKey) bool {
for _, pubkey := range pubkeys {
if ssh.KeysEqual(pubkey, key) {
return true
}
}
return false
}),
}
errC := make(chan error, 1)
go func() {
// as long as we get a successful session we don't care if the server errors
errC <- ssh.Serve(l, handler, serveOpts...)
}()
t.Cleanup(func() {
_ = l.Close() // Ensure server shutdown.
<-errC
})
// start ssh session
addr, ok := l.Addr().(*net.TCPAddr)
require.True(t, ok)
return addr
}
func writePrivateKeyToFile(t *testing.T, name string, key *ecdsa.PrivateKey) {
t.Helper()
b, err := x509.MarshalPKCS8PrivateKey(key)
require.NoError(t, err)
b = pem.EncodeToMemory(&pem.Block{
Type: "PRIVATE KEY",
Bytes: b,
})
err = os.WriteFile(name, b, 0o600)
require.NoError(t, err)
}
func TestGitSSH(t *testing.T) {
t.Parallel()
t.Run("Dial", func(t *testing.T) {
t.Parallel()
setupCtx := testutil.Context(t, testutil.WaitLong)
client, token, pubkey := prepareTestGitSSH(setupCtx, t)
var inc int64
errC := make(chan error, 1)
addr := serveSSHForGitSSH(t, func(s ssh.Session) {
atomic.AddInt64(&inc, 1)
t.Log("got authenticated session")
select {
case errC <- s.Exit(0):
default:
t.Error("error channel is full")
}
}, pubkey)
// set to agent config dir
inv, _ := clitest.New(t,
"gitssh",
"--agent-url", client.SDK.URL.String(),
"--agent-token", token,
"--",
fmt.Sprintf("-p%d", addr.Port),
"-o", "StrictHostKeyChecking=no",
"-o", "IdentitiesOnly=yes",
"127.0.0.1",
)
// This occasionally times out at 15s on Windows CI runners. Use a
// longer timeout to reduce flakes.
ctx := testutil.Context(t, testutil.WaitSuperLong)
err := inv.WithContext(ctx).Run()
require.NoError(t, err)
require.EqualValues(t, 1, inc)
err = <-errC
require.NoError(t, err, "error in agent execute")
})
t.Run("Local SSH Keys", func(t *testing.T) {
t.Parallel()
home := t.TempDir()
sshdir := filepath.Join(home, ".ssh")
err := os.MkdirAll(sshdir, 0o700)
require.NoError(t, err)
idFile := filepath.Join(sshdir, "id_ed25519")
privkey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
localPubkey, err := gossh.NewPublicKey(&privkey.PublicKey)
require.NoError(t, err)
writePrivateKeyToFile(t, idFile, privkey)
setupCtx := testutil.Context(t, testutil.WaitLong)
client, token, coderPubkey := prepareTestGitSSH(setupCtx, t)
authkey := make(chan gossh.PublicKey, 1)
addr := serveSSHForGitSSH(t, func(s ssh.Session) {
t.Logf("authenticated with: %s", gossh.MarshalAuthorizedKey(s.PublicKey()))
select {
case authkey <- s.PublicKey():
default:
t.Error("authkey channel is full")
}
}, localPubkey, coderPubkey)
// Create a new config which sets an identity file.
config := filepath.Join(sshdir, "config")
knownHosts := filepath.Join(sshdir, "known_hosts")
err = os.WriteFile(config, []byte(strings.Join([]string{
"Host mytest",
" HostName 127.0.0.1",
fmt.Sprintf(" Port %d", addr.Port),
" StrictHostKeyChecking no",
" UserKnownHostsFile=" + knownHosts,
" IdentitiesOnly yes",
" IdentityFile=" + idFile,
}, "\n")), 0o600)
require.NoError(t, err)
pty := ptytest.New(t)
cmdArgs := []string{
"gitssh",
"--agent-url", client.SDK.URL.String(),
"--agent-token", token,
"--",
"-F", config,
"mytest",
}
// Test authentication via local private key.
inv, _ := clitest.New(t, cmdArgs...)
inv.Stdout = pty.Output()
inv.Stderr = pty.Output()
// This occasionally times out at 15s on Windows CI runners. Use a
// longer timeout to reduce flakes.
ctx := testutil.Context(t, testutil.WaitSuperLong)
err = inv.WithContext(ctx).Run()
require.NoError(t, err)
select {
case key := <-authkey:
require.Equal(t, localPubkey, key)
case <-ctx.Done():
t.Fatal("timeout waiting for auth")
}
// Delete the local private key.
err = os.Remove(idFile)
require.NoError(t, err)
// With the local file deleted, the coder key should be used.
inv, _ = clitest.New(t, cmdArgs...)
inv.Stdout = pty.Output()
inv.Stderr = pty.Output()
// This occasionally times out at 15s on Windows CI runners. Use a
// longer timeout to reduce flakes.
ctx = testutil.Context(t, testutil.WaitSuperLong) // Reset context for second cmd test.
err = inv.WithContext(ctx).Run()
require.NoError(t, err)
select {
case key := <-authkey:
require.Equal(t, coderPubkey, key)
case <-ctx.Done():
t.Fatal("timeout waiting for auth")
}
})
}