Skip to content

Commit 1dfb981

Browse files
committed
Merge branch 'main' into admin/presleyp/591
2 parents acc92f8 + 4c1ef38 commit 1dfb981

30 files changed

+873
-98
lines changed

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
site @coder/frontend
2+
site/src/xServices @presleyp

agent/agent.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,11 @@ func (a *agent) handleSSHSession(session ssh.Session) error {
230230

231231
cmd := exec.CommandContext(session.Context(), command, args...)
232232
cmd.Env = append(os.Environ(), session.Environ()...)
233+
executablePath, err := os.Executable()
234+
if err != nil {
235+
return xerrors.Errorf("getting os executable: %w", err)
236+
}
237+
cmd.Env = append(session.Environ(), fmt.Sprintf(`GIT_SSH_COMMAND="%s gitssh --"`, executablePath))
233238

234239
sshPty, windowSize, isPty := session.Pty()
235240
if isPty {

agent/agent_test.go

Lines changed: 35 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -29,24 +29,7 @@ func TestAgent(t *testing.T) {
2929
t.Parallel()
3030
t.Run("SessionExec", func(t *testing.T) {
3131
t.Parallel()
32-
api := setup(t)
33-
stream, err := api.NegotiateConnection(context.Background())
34-
require.NoError(t, err)
35-
conn, err := peerbroker.Dial(stream, []webrtc.ICEServer{}, &peer.ConnOptions{
36-
Logger: slogtest.Make(t, nil),
37-
})
38-
require.NoError(t, err)
39-
t.Cleanup(func() {
40-
_ = conn.Close()
41-
})
42-
client := agent.Conn{
43-
Negotiator: api,
44-
Conn: conn,
45-
}
46-
sshClient, err := client.SSHClient()
47-
require.NoError(t, err)
48-
session, err := sshClient.NewSession()
49-
require.NoError(t, err)
32+
session := setupSSH(t)
5033
command := "echo test"
5134
if runtime.GOOS == "windows" {
5235
command = "cmd.exe /c echo test"
@@ -56,33 +39,28 @@ func TestAgent(t *testing.T) {
5639
require.Equal(t, "test", strings.TrimSpace(string(output)))
5740
})
5841

59-
t.Run("SessionTTY", func(t *testing.T) {
42+
t.Run("GitSSH", func(t *testing.T) {
6043
t.Parallel()
61-
api := setup(t)
62-
stream, err := api.NegotiateConnection(context.Background())
63-
require.NoError(t, err)
64-
conn, err := peerbroker.Dial(stream, []webrtc.ICEServer{}, &peer.ConnOptions{
65-
Logger: slogtest.Make(t, nil),
66-
})
67-
require.NoError(t, err)
68-
t.Cleanup(func() {
69-
_ = conn.Close()
70-
})
71-
client := &agent.Conn{
72-
Negotiator: api,
73-
Conn: conn,
44+
session := setupSSH(t)
45+
command := "sh -c 'echo $GIT_SSH_COMMAND'"
46+
if runtime.GOOS == "windows" {
47+
command = "cmd.exe /c echo %GIT_SSH_COMMAND%"
7448
}
75-
sshClient, err := client.SSHClient()
76-
require.NoError(t, err)
77-
session, err := sshClient.NewSession()
49+
output, err := session.Output(command)
7850
require.NoError(t, err)
51+
require.Contains(t, string(output), "gitssh --")
52+
})
53+
54+
t.Run("SessionTTY", func(t *testing.T) {
55+
t.Parallel()
56+
session := setupSSH(t)
7957
prompt := "$"
8058
command := "bash"
8159
if runtime.GOOS == "windows" {
8260
command = "cmd.exe"
8361
prompt = ">"
8462
}
85-
err = session.RequestPty("xterm", 128, 128, ssh.TerminalModes{})
63+
err := session.RequestPty("xterm", 128, 128, ssh.TerminalModes{})
8664
require.NoError(t, err)
8765
ptty := ptytest.New(t)
8866
require.NoError(t, err)
@@ -100,7 +78,7 @@ func TestAgent(t *testing.T) {
10078
})
10179
}
10280

103-
func setup(t *testing.T) proto.DRPCPeerBrokerClient {
81+
func setupSSH(t *testing.T) *ssh.Session {
10482
client, server := provisionersdk.TransportPipe()
10583
closer := agent.New(func(ctx context.Context, opts *peer.ConnOptions) (*peerbroker.Listener, error) {
10684
return peerbroker.Listen(server, nil, opts)
@@ -112,5 +90,24 @@ func setup(t *testing.T) proto.DRPCPeerBrokerClient {
11290
_ = server.Close()
11391
_ = closer.Close()
11492
})
115-
return proto.NewDRPCPeerBrokerClient(provisionersdk.Conn(client))
93+
api := proto.NewDRPCPeerBrokerClient(provisionersdk.Conn(client))
94+
stream, err := api.NegotiateConnection(context.Background())
95+
require.NoError(t, err)
96+
conn, err := peerbroker.Dial(stream, []webrtc.ICEServer{}, &peer.ConnOptions{
97+
Logger: slogtest.Make(t, nil),
98+
})
99+
require.NoError(t, err)
100+
t.Cleanup(func() {
101+
_ = conn.Close()
102+
})
103+
agentClient := &agent.Conn{
104+
Negotiator: api,
105+
Conn: conn,
106+
}
107+
sshClient, err := agentClient.SSHClient()
108+
require.NoError(t, err)
109+
session, err := sshClient.NewSession()
110+
require.NoError(t, err)
111+
112+
return session
116113
}

cli/cliui/prompt.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/bgentry/speakeasy"
1515
"github.com/mattn/go-isatty"
1616
"github.com/spf13/cobra"
17+
"golang.org/x/xerrors"
1718
)
1819

1920
// PromptOptions supply a set of options to the prompt.
@@ -48,7 +49,7 @@ func Prompt(cmd *cobra.Command, opts PromptOptions) (string, error) {
4849
if opts.Secret && valid && isatty.IsTerminal(inFile.Fd()) {
4950
line, err = speakeasy.Ask("")
5051
} else {
51-
if runtime.GOOS == "darwin" && valid {
52+
if !opts.IsConfirm && runtime.GOOS == "darwin" && valid {
5253
var restore func()
5354
restore, err = removeLineLengthLimit(int(inFile.Fd()))
5455
if err != nil {
@@ -99,7 +100,7 @@ func Prompt(cmd *cobra.Command, opts PromptOptions) (string, error) {
99100
return "", err
100101
case line := <-lineCh:
101102
if opts.IsConfirm && line != "yes" && line != "y" {
102-
return line, Canceled
103+
return line, xerrors.Errorf("got %q: %w", line, Canceled)
103104
}
104105
if opts.Validate != nil {
105106
err := opts.Validate(line)

cli/config/file.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ func (r Root) Organization() File {
2121
return File(filepath.Join(string(r), "organization"))
2222
}
2323

24+
func (r Root) AgentSession() File {
25+
return File(filepath.Join(string(r), "agentsession"))
26+
}
27+
2428
// File provides convenience methods for interacting with *os.File.
2529
type File string
2630

cli/gitssh.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package cli
2+
3+
import (
4+
"os"
5+
"os/exec"
6+
7+
"github.com/spf13/cobra"
8+
"golang.org/x/xerrors"
9+
)
10+
11+
func gitssh() *cobra.Command {
12+
return &cobra.Command{
13+
Use: "gitssh",
14+
Hidden: true,
15+
Short: `Wraps the "ssh" command and uses the coder gitssh key for authentication`,
16+
RunE: func(cmd *cobra.Command, args []string) error {
17+
client, err := createClient(cmd)
18+
if err != nil {
19+
return xerrors.Errorf("create codersdk client: %w", err)
20+
}
21+
cfg := createConfig(cmd)
22+
session, err := cfg.AgentSession().Read()
23+
if err != nil {
24+
return xerrors.Errorf("read agent session from config: %w", err)
25+
}
26+
client.SessionToken = session
27+
28+
key, err := client.AgentGitSSHKey(cmd.Context())
29+
if err != nil {
30+
return xerrors.Errorf("get agent git ssh token: %w", err)
31+
}
32+
33+
privateKeyFile, err := os.CreateTemp("", "coder-gitsshkey-*")
34+
if err != nil {
35+
return xerrors.Errorf("create temp gitsshkey file: %w", err)
36+
}
37+
defer func() {
38+
_ = privateKeyFile.Close()
39+
_ = os.Remove(privateKeyFile.Name())
40+
}()
41+
_, err = privateKeyFile.WriteString(key.PrivateKey)
42+
if err != nil {
43+
return xerrors.Errorf("write to temp gitsshkey file: %w", err)
44+
}
45+
err = privateKeyFile.Close()
46+
if err != nil {
47+
return xerrors.Errorf("close temp gitsshkey file: %w", err)
48+
}
49+
50+
a := append([]string{"-i", privateKeyFile.Name()}, args...)
51+
c := exec.CommandContext(cmd.Context(), "ssh", a...)
52+
c.Stdout = cmd.OutOrStdout()
53+
c.Stdin = cmd.InOrStdin()
54+
err = c.Run()
55+
if err != nil {
56+
return xerrors.Errorf("run ssh command: %w", err)
57+
}
58+
59+
return nil
60+
},
61+
}
62+
}

cli/gitssh_test.go

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package cli_test
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net"
7+
"sync/atomic"
8+
"testing"
9+
"time"
10+
11+
"github.com/gliderlabs/ssh"
12+
"github.com/spf13/cobra"
13+
"github.com/stretchr/testify/require"
14+
gossh "golang.org/x/crypto/ssh"
15+
16+
"github.com/coder/coder/cli/clitest"
17+
"github.com/coder/coder/cli/config"
18+
"github.com/coder/coder/coderd/coderdtest"
19+
"github.com/coder/coder/codersdk"
20+
"github.com/coder/coder/provisioner/echo"
21+
"github.com/coder/coder/provisionersdk/proto"
22+
)
23+
24+
func TestGitSSH(t *testing.T) {
25+
t.Parallel()
26+
t.Run("Dial", func(t *testing.T) {
27+
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
28+
defer cancel()
29+
instanceID := "instanceidentifier"
30+
certificates, metadataClient := coderdtest.NewAWSInstanceIdentity(t, instanceID)
31+
client := coderdtest.New(t, &coderdtest.Options{
32+
AWSInstanceIdentity: certificates,
33+
})
34+
user := coderdtest.CreateFirstUser(t, client)
35+
36+
// get user public key
37+
keypair, err := client.GitSSHKey(ctx, codersdk.Me)
38+
require.NoError(t, err)
39+
publicKey, _, _, _, err := gossh.ParseAuthorizedKey([]byte(keypair.PublicKey))
40+
require.NoError(t, err)
41+
42+
// setup provisioner
43+
coderdtest.NewProvisionerDaemon(t, client)
44+
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
45+
Parse: echo.ParseComplete,
46+
Provision: []*proto.Provision_Response{{
47+
Type: &proto.Provision_Response_Complete{
48+
Complete: &proto.Provision_Complete{
49+
Resources: []*proto.Resource{{
50+
Name: "somename",
51+
Type: "someinstance",
52+
Agent: &proto.Agent{
53+
Auth: &proto.Agent_InstanceId{
54+
InstanceId: instanceID,
55+
},
56+
},
57+
}},
58+
},
59+
},
60+
}},
61+
})
62+
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
63+
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
64+
workspace := coderdtest.CreateWorkspace(t, client, codersdk.Me, template.ID)
65+
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
66+
67+
// start workspace agent
68+
cmd, root := clitest.New(t, "workspaces", "agent", "--auth", "aws-instance-identity", "--url", client.URL.String())
69+
agentClient := &*client
70+
clitest.SetupConfig(t, agentClient, root)
71+
ctx, cancelFunc := context.WithCancel(context.Background())
72+
defer cancelFunc()
73+
go func() {
74+
// A linting error occurs for weakly typing the context value here,
75+
// but it seems reasonable for a one-off test.
76+
// nolint
77+
ctx = context.WithValue(ctx, "aws-client", metadataClient)
78+
err := cmd.ExecuteContext(ctx)
79+
require.NoError(t, err)
80+
}()
81+
coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID)
82+
resources, err := client.WorkspaceResourcesByBuild(ctx, workspace.LatestBuild.ID)
83+
require.NoError(t, err)
84+
dialer, err := client.DialWorkspaceAgent(ctx, resources[0].ID, nil, nil)
85+
require.NoError(t, err)
86+
defer dialer.Close()
87+
_, err = dialer.Ping()
88+
require.NoError(t, err)
89+
90+
// start ssh server
91+
l, err := net.Listen("tcp", "localhost:0")
92+
require.NoError(t, err)
93+
defer l.Close()
94+
publicKeyOption := ssh.PublicKeyAuth(func(ctx ssh.Context, key ssh.PublicKey) bool {
95+
return ssh.KeysEqual(publicKey, key)
96+
})
97+
var inc int64
98+
go func() {
99+
// as long as we get a successful session we don't care if the server errors
100+
_ = ssh.Serve(l, func(s ssh.Session) {
101+
atomic.AddInt64(&inc, 1)
102+
t.Log("got authenticated sesion")
103+
err := s.Exit(0)
104+
require.NoError(t, err)
105+
}, publicKeyOption)
106+
}()
107+
108+
// start ssh session
109+
addr, ok := l.Addr().(*net.TCPAddr)
110+
require.True(t, ok)
111+
cfgDir := createConfig(cmd)
112+
// set to agent config dir
113+
cmd, root = clitest.New(t, "gitssh", "--global-config="+string(cfgDir), "--", fmt.Sprintf("-p%d", addr.Port), "-o", "StrictHostKeyChecking=no", "127.0.0.1")
114+
clitest.SetupConfig(t, agentClient, root)
115+
116+
err = cmd.ExecuteContext(ctx)
117+
require.NoError(t, err)
118+
require.EqualValues(t, 1, inc)
119+
})
120+
}
121+
122+
// createConfig consumes the global configuration flag to produce a config root.
123+
func createConfig(cmd *cobra.Command) config.Root {
124+
globalRoot, err := cmd.Flags().GetString("global-config")
125+
if err != nil {
126+
panic(err)
127+
}
128+
return config.Root(globalRoot)
129+
}

cli/publickey.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package cli
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
"golang.org/x/xerrors"
6+
7+
"github.com/coder/coder/codersdk"
8+
)
9+
10+
func publickey() *cobra.Command {
11+
return &cobra.Command{
12+
Use: "publickey",
13+
RunE: func(cmd *cobra.Command, args []string) error {
14+
client, err := createClient(cmd)
15+
if err != nil {
16+
return xerrors.Errorf("create codersdk client: %w", err)
17+
}
18+
19+
key, err := client.GitSSHKey(cmd.Context(), codersdk.Me)
20+
if err != nil {
21+
return xerrors.Errorf("create codersdk client: %w", err)
22+
}
23+
24+
cmd.Println(key.PublicKey)
25+
26+
return nil
27+
},
28+
}
29+
}

0 commit comments

Comments
 (0)