mirror of
https://github.com/coder/coder.git
synced 2025-07-08 11:39:50 +00:00
test: Fix GPG test so it does not inherit parent parallelism (#5820)
* test: Fix GPG test so it does not inherit parent parallelism Running a subtest in a parent with `t.Parallel()` and using `t.Setenv` is not allowed in Go 1.20, so we move it to a separate test function. * Fix shadowed import
This commit is contained in:
committed by
GitHub
parent
73afdd7c09
commit
6a245ab1cc
294
cli/ssh_test.go
294
cli/ssh_test.go
@ -293,20 +293,21 @@ func TestSSH(t *testing.T) {
|
|||||||
pty.WriteLine("exit")
|
pty.WriteLine("exit")
|
||||||
<-cmdDone
|
<-cmdDone
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
//nolint:paralleltest // This test uses t.Setenv.
|
//nolint:paralleltest // This test uses t.Setenv, parent test MUST NOT be parallel.
|
||||||
t.Run("ForwardGPG", func(t *testing.T) {
|
func TestSSH_ForwardGPG(t *testing.T) {
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
// While GPG forwarding from a Windows client works, we currently do
|
// While GPG forwarding from a Windows client works, we currently do
|
||||||
// not support forwarding to a Windows workspace. Our tests use the
|
// not support forwarding to a Windows workspace. Our tests use the
|
||||||
// same platform for the "client" and "workspace" as they run in the
|
// same platform for the "client" and "workspace" as they run in the
|
||||||
// same process.
|
// same process.
|
||||||
t.Skip("Test not supported on windows")
|
t.Skip("Test not supported on windows")
|
||||||
}
|
}
|
||||||
|
|
||||||
// This key is for dean@coder.com.
|
// This key is for dean@coder.com.
|
||||||
const randPublicKeyFingerprint = "7BDFBA0CC7F5A96537C806C427BC6335EB5117F1"
|
const randPublicKeyFingerprint = "7BDFBA0CC7F5A96537C806C427BC6335EB5117F1"
|
||||||
const randPublicKey = `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
const randPublicKey = `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
|
|
||||||
mQINBF6SWkEBEADB8sAhBaT36VQ6HEhAmtKexLldu1HUdXNw16rdF+1wiBzSFfJN
|
mQINBF6SWkEBEADB8sAhBaT36VQ6HEhAmtKexLldu1HUdXNw16rdF+1wiBzSFfJN
|
||||||
aPeX4Y9iFIZgC2wU0wOjJ04BpioyOLtJngbThI5WpeoQ/1yQZOpnDaCMPPLp+uJ+
|
aPeX4Y9iFIZgC2wU0wOjJ04BpioyOLtJngbThI5WpeoQ/1yQZOpnDaCMPPLp+uJ+
|
||||||
@ -359,40 +360,40 @@ p7KeSZdlk47pMBGOfnvEmoQ=
|
|||||||
=OxHv
|
=OxHv
|
||||||
-----END PGP PUBLIC KEY BLOCK-----`
|
-----END PGP PUBLIC KEY BLOCK-----`
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
gpgPath, err := exec.LookPath("gpg")
|
gpgPath, err := exec.LookPath("gpg")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Skip("gpg not found")
|
t.Skip("gpg not found")
|
||||||
}
|
}
|
||||||
gpgConfPath, err := exec.LookPath("gpgconf")
|
gpgConfPath, err := exec.LookPath("gpgconf")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Skip("gpgconf not found")
|
t.Skip("gpgconf not found")
|
||||||
}
|
}
|
||||||
gpgAgentPath, err := exec.LookPath("gpg-agent")
|
gpgAgentPath, err := exec.LookPath("gpg-agent")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Skip("gpg-agent not found")
|
t.Skip("gpg-agent not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup GPG home directory on the "client".
|
// Setup GPG home directory on the "client".
|
||||||
gnupgHomeClient := tempDirUnixSocket(t)
|
gnupgHomeClient := tempDirUnixSocket(t)
|
||||||
t.Setenv("GNUPGHOME", gnupgHomeClient)
|
t.Setenv("GNUPGHOME", gnupgHomeClient)
|
||||||
|
|
||||||
// Get the agent extra socket path.
|
// Get the agent extra socket path.
|
||||||
var (
|
var (
|
||||||
stdout = bytes.NewBuffer(nil)
|
stdout = bytes.NewBuffer(nil)
|
||||||
stderr = bytes.NewBuffer(nil)
|
stderr = bytes.NewBuffer(nil)
|
||||||
)
|
)
|
||||||
c := exec.CommandContext(ctx, gpgConfPath, "--list-dir", "agent-extra-socket")
|
c := exec.CommandContext(ctx, gpgConfPath, "--list-dir", "agent-extra-socket")
|
||||||
c.Stdout = stdout
|
c.Stdout = stdout
|
||||||
c.Stderr = stderr
|
c.Stderr = stderr
|
||||||
err = c.Run()
|
err = c.Run()
|
||||||
require.NoError(t, err, "get extra socket path failed: %s", stderr.String())
|
require.NoError(t, err, "get extra socket path failed: %s", stderr.String())
|
||||||
extraSocketPath := strings.TrimSpace(stdout.String())
|
extraSocketPath := strings.TrimSpace(stdout.String())
|
||||||
|
|
||||||
// Generate private key non-interactively.
|
// Generate private key non-interactively.
|
||||||
genKeyScript := `
|
genKeyScript := `
|
||||||
Key-Type: 1
|
Key-Type: 1
|
||||||
Key-Length: 2048
|
Key-Length: 2048
|
||||||
Subkey-Type: 1
|
Subkey-Type: 1
|
||||||
@ -402,119 +403,118 @@ Name-Email: test@coder.com
|
|||||||
Expire-Date: 0
|
Expire-Date: 0
|
||||||
%no-protection
|
%no-protection
|
||||||
`
|
`
|
||||||
c = exec.CommandContext(ctx, gpgPath, "--batch", "--gen-key")
|
c = exec.CommandContext(ctx, gpgPath, "--batch", "--gen-key")
|
||||||
c.Stdin = strings.NewReader(genKeyScript)
|
c.Stdin = strings.NewReader(genKeyScript)
|
||||||
out, err := c.CombinedOutput()
|
out, err := c.CombinedOutput()
|
||||||
require.NoError(t, err, "generate key failed: %s", out)
|
require.NoError(t, err, "generate key failed: %s", out)
|
||||||
|
|
||||||
// Import a random public key.
|
// Import a random public key.
|
||||||
stdin := strings.NewReader(randPublicKey + "\n")
|
stdin := strings.NewReader(randPublicKey + "\n")
|
||||||
c = exec.CommandContext(ctx, gpgPath, "--import", "-")
|
c = exec.CommandContext(ctx, gpgPath, "--import", "-")
|
||||||
c.Stdin = stdin
|
c.Stdin = stdin
|
||||||
out, err = c.CombinedOutput()
|
out, err = c.CombinedOutput()
|
||||||
require.NoError(t, err, "import key failed: %s", out)
|
require.NoError(t, err, "import key failed: %s", out)
|
||||||
|
|
||||||
// Set ultimate trust on imported key.
|
// Set ultimate trust on imported key.
|
||||||
stdin = strings.NewReader(randPublicKeyFingerprint + ":6:\n")
|
stdin = strings.NewReader(randPublicKeyFingerprint + ":6:\n")
|
||||||
c = exec.CommandContext(ctx, gpgPath, "--import-ownertrust")
|
c = exec.CommandContext(ctx, gpgPath, "--import-ownertrust")
|
||||||
c.Stdin = stdin
|
c.Stdin = stdin
|
||||||
out, err = c.CombinedOutput()
|
out, err = c.CombinedOutput()
|
||||||
require.NoError(t, err, "import ownertrust failed: %s", out)
|
require.NoError(t, err, "import ownertrust failed: %s", out)
|
||||||
|
|
||||||
// Start the GPG agent.
|
// Start the GPG agent.
|
||||||
agentCmd := exec.CommandContext(ctx, gpgAgentPath, "--no-detach", "--extra-socket", extraSocketPath)
|
agentCmd := exec.CommandContext(ctx, gpgAgentPath, "--no-detach", "--extra-socket", extraSocketPath)
|
||||||
agentCmd.Env = append(agentCmd.Env, "GNUPGHOME="+gnupgHomeClient)
|
agentCmd.Env = append(agentCmd.Env, "GNUPGHOME="+gnupgHomeClient)
|
||||||
agentPTY, agentProc, err := pty.Start(agentCmd, pty.WithPTYOption(pty.WithGPGTTY()))
|
agentPTY, agentProc, err := pty.Start(agentCmd, pty.WithPTYOption(pty.WithGPGTTY()))
|
||||||
require.NoError(t, err, "launch agent failed")
|
require.NoError(t, err, "launch agent failed")
|
||||||
defer func() {
|
defer func() {
|
||||||
_ = agentProc.Kill()
|
_ = agentProc.Kill()
|
||||||
_ = agentPTY.Close()
|
_ = agentPTY.Close()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Get the agent socket path in the "workspace".
|
// Get the agent socket path in the "workspace".
|
||||||
gnupgHomeWorkspace := tempDirUnixSocket(t)
|
gnupgHomeWorkspace := tempDirUnixSocket(t)
|
||||||
|
|
||||||
stdout = bytes.NewBuffer(nil)
|
stdout = bytes.NewBuffer(nil)
|
||||||
stderr = bytes.NewBuffer(nil)
|
stderr = bytes.NewBuffer(nil)
|
||||||
c = exec.CommandContext(ctx, gpgConfPath, "--list-dir", "agent-socket")
|
c = exec.CommandContext(ctx, gpgConfPath, "--list-dir", "agent-socket")
|
||||||
c.Env = append(c.Env, "GNUPGHOME="+gnupgHomeWorkspace)
|
c.Env = append(c.Env, "GNUPGHOME="+gnupgHomeWorkspace)
|
||||||
c.Stdout = stdout
|
c.Stdout = stdout
|
||||||
c.Stderr = stderr
|
c.Stderr = stderr
|
||||||
err = c.Run()
|
err = c.Run()
|
||||||
require.NoError(t, err, "get agent socket path in workspace failed: %s", stderr.String())
|
require.NoError(t, err, "get agent socket path in workspace failed: %s", stderr.String())
|
||||||
workspaceAgentSocketPath := strings.TrimSpace(stdout.String())
|
workspaceAgentSocketPath := strings.TrimSpace(stdout.String())
|
||||||
require.NotEqual(t, extraSocketPath, workspaceAgentSocketPath, "socket path should be different")
|
require.NotEqual(t, extraSocketPath, workspaceAgentSocketPath, "socket path should be different")
|
||||||
|
|
||||||
client, workspace, agentToken := setupWorkspaceForAgent(t, nil)
|
client, workspace, agentToken := setupWorkspaceForAgent(t, nil)
|
||||||
|
|
||||||
agentClient := codersdk.New(client.URL)
|
agentClient := codersdk.New(client.URL)
|
||||||
agentClient.SetSessionToken(agentToken)
|
agentClient.SetSessionToken(agentToken)
|
||||||
agentCloser := agent.New(agent.Options{
|
agentCloser := agent.New(agent.Options{
|
||||||
Client: agentClient,
|
Client: agentClient,
|
||||||
EnvironmentVariables: map[string]string{
|
EnvironmentVariables: map[string]string{
|
||||||
"GNUPGHOME": gnupgHomeWorkspace,
|
"GNUPGHOME": gnupgHomeWorkspace,
|
||||||
},
|
},
|
||||||
Logger: slogtest.Make(t, nil).Named("agent"),
|
Logger: slogtest.Make(t, nil).Named("agent"),
|
||||||
})
|
|
||||||
defer agentCloser.Close()
|
|
||||||
|
|
||||||
cmd, root := clitest.New(t,
|
|
||||||
"ssh",
|
|
||||||
workspace.Name,
|
|
||||||
"--forward-gpg",
|
|
||||||
)
|
|
||||||
clitest.SetupConfig(t, client, root)
|
|
||||||
pty := ptytest.New(t)
|
|
||||||
cmd.SetIn(pty.Input())
|
|
||||||
cmd.SetOut(pty.Output())
|
|
||||||
cmd.SetErr(pty.Output())
|
|
||||||
cmdDone := tGo(t, func() {
|
|
||||||
err := cmd.ExecuteContext(ctx)
|
|
||||||
assert.NoError(t, err, "ssh command failed")
|
|
||||||
})
|
|
||||||
// Prevent the test from hanging if the asserts below kill the test
|
|
||||||
// early. This will cause the command to exit with an error, which will
|
|
||||||
// let the t.Cleanup'd `<-done` inside of `tGo` exit and not hang.
|
|
||||||
// Without this, the test will hang forever on failure, preventing the
|
|
||||||
// real error from being printed.
|
|
||||||
t.Cleanup(cancel)
|
|
||||||
|
|
||||||
// Wait for the prompt or any output really to indicate the command has
|
|
||||||
// started and accepting input on stdin.
|
|
||||||
_ = pty.Peek(ctx, 1)
|
|
||||||
|
|
||||||
pty.WriteLine("echo hello 'world'")
|
|
||||||
pty.ExpectMatch("hello world")
|
|
||||||
|
|
||||||
// Check the GNUPGHOME was correctly inherited via shell.
|
|
||||||
pty.WriteLine("env && echo env-''-command-done")
|
|
||||||
match := pty.ExpectMatch("env--command-done")
|
|
||||||
require.Contains(t, match, "GNUPGHOME="+gnupgHomeWorkspace, match)
|
|
||||||
|
|
||||||
// Get the agent extra socket path in the "workspace" via shell.
|
|
||||||
pty.WriteLine("gpgconf --list-dir agent-socket && echo gpgconf-''-agentsocket-command-done")
|
|
||||||
pty.ExpectMatch(workspaceAgentSocketPath)
|
|
||||||
pty.ExpectMatch("gpgconf--agentsocket-command-done")
|
|
||||||
|
|
||||||
// List the keys in the "workspace".
|
|
||||||
pty.WriteLine("gpg --list-keys && echo gpg-''-listkeys-command-done")
|
|
||||||
listKeysOutput := pty.ExpectMatch("gpg--listkeys-command-done")
|
|
||||||
require.Contains(t, listKeysOutput, "[ultimate] Coder Test <test@coder.com>")
|
|
||||||
require.Contains(t, listKeysOutput, "[ultimate] Dean Sheather (work key) <dean@coder.com>")
|
|
||||||
|
|
||||||
// Try to sign something. This demonstrates that the forwarding is
|
|
||||||
// working as expected, since the workspace doesn't have access to the
|
|
||||||
// private key directly and must use the forwarded agent.
|
|
||||||
pty.WriteLine("echo 'hello world' | gpg --clearsign && echo gpg-''-sign-command-done")
|
|
||||||
pty.ExpectMatch("BEGIN PGP SIGNED MESSAGE")
|
|
||||||
pty.ExpectMatch("Hash:")
|
|
||||||
pty.ExpectMatch("hello world")
|
|
||||||
pty.ExpectMatch("gpg--sign-command-done")
|
|
||||||
|
|
||||||
// And we're done.
|
|
||||||
pty.WriteLine("exit")
|
|
||||||
<-cmdDone
|
|
||||||
})
|
})
|
||||||
|
defer agentCloser.Close()
|
||||||
|
|
||||||
|
cmd, root := clitest.New(t,
|
||||||
|
"ssh",
|
||||||
|
workspace.Name,
|
||||||
|
"--forward-gpg",
|
||||||
|
)
|
||||||
|
clitest.SetupConfig(t, client, root)
|
||||||
|
tpty := ptytest.New(t)
|
||||||
|
cmd.SetIn(tpty.Input())
|
||||||
|
cmd.SetOut(tpty.Output())
|
||||||
|
cmd.SetErr(tpty.Output())
|
||||||
|
cmdDone := tGo(t, func() {
|
||||||
|
err := cmd.ExecuteContext(ctx)
|
||||||
|
assert.NoError(t, err, "ssh command failed")
|
||||||
|
})
|
||||||
|
// Prevent the test from hanging if the asserts below kill the test
|
||||||
|
// early. This will cause the command to exit with an error, which will
|
||||||
|
// let the t.Cleanup'd `<-done` inside of `tGo` exit and not hang.
|
||||||
|
// Without this, the test will hang forever on failure, preventing the
|
||||||
|
// real error from being printed.
|
||||||
|
t.Cleanup(cancel)
|
||||||
|
|
||||||
|
// Wait for the prompt or any output really to indicate the command has
|
||||||
|
// started and accepting input on stdin.
|
||||||
|
_ = tpty.Peek(ctx, 1)
|
||||||
|
|
||||||
|
tpty.WriteLine("echo hello 'world'")
|
||||||
|
tpty.ExpectMatch("hello world")
|
||||||
|
|
||||||
|
// Check the GNUPGHOME was correctly inherited via shell.
|
||||||
|
tpty.WriteLine("env && echo env-''-command-done")
|
||||||
|
match := tpty.ExpectMatch("env--command-done")
|
||||||
|
require.Contains(t, match, "GNUPGHOME="+gnupgHomeWorkspace, match)
|
||||||
|
|
||||||
|
// Get the agent extra socket path in the "workspace" via shell.
|
||||||
|
tpty.WriteLine("gpgconf --list-dir agent-socket && echo gpgconf-''-agentsocket-command-done")
|
||||||
|
tpty.ExpectMatch(workspaceAgentSocketPath)
|
||||||
|
tpty.ExpectMatch("gpgconf--agentsocket-command-done")
|
||||||
|
|
||||||
|
// List the keys in the "workspace".
|
||||||
|
tpty.WriteLine("gpg --list-keys && echo gpg-''-listkeys-command-done")
|
||||||
|
listKeysOutput := tpty.ExpectMatch("gpg--listkeys-command-done")
|
||||||
|
require.Contains(t, listKeysOutput, "[ultimate] Coder Test <test@coder.com>")
|
||||||
|
require.Contains(t, listKeysOutput, "[ultimate] Dean Sheather (work key) <dean@coder.com>")
|
||||||
|
|
||||||
|
// Try to sign something. This demonstrates that the forwarding is
|
||||||
|
// working as expected, since the workspace doesn't have access to the
|
||||||
|
// private key directly and must use the forwarded agent.
|
||||||
|
tpty.WriteLine("echo 'hello world' | gpg --clearsign && echo gpg-''-sign-command-done")
|
||||||
|
tpty.ExpectMatch("BEGIN PGP SIGNED MESSAGE")
|
||||||
|
tpty.ExpectMatch("Hash:")
|
||||||
|
tpty.ExpectMatch("hello world")
|
||||||
|
tpty.ExpectMatch("gpg--sign-command-done")
|
||||||
|
|
||||||
|
// And we're done.
|
||||||
|
tpty.WriteLine("exit")
|
||||||
|
<-cmdDone
|
||||||
}
|
}
|
||||||
|
|
||||||
// tGoContext runs fn in a goroutine passing a context that will be
|
// tGoContext runs fn in a goroutine passing a context that will be
|
||||||
|
Reference in New Issue
Block a user