Skip to content

Commit eb3f862

Browse files
authored
Merge pull request #39 from stuartleeks/sl/ssh-auth-sock
Enable SSH key forwarding for 'devcontainer exec'
2 parents 8750c68 + 2ab8363 commit eb3f862

File tree

5 files changed

+164
-93
lines changed

5 files changed

+164
-93
lines changed

.devcontainer/Dockerfile

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,14 @@ ENV DEBIAN_FRONTEND=noninteractive
1010

1111
# Configure apt, install packages and tools
1212
RUN apt-get update \
13-
&& apt-get -y install --no-install-recommends apt-utils dialog nano \
13+
&& apt-get -y install --no-install-recommends apt-utils dialog nano sudo bsdmainutils \
1414
#
1515
# Verify git, process tools, lsb-release (common in install instructions for CLIs) installed
1616
&& apt-get -y install git iproute2 procps lsb-release \
1717
# Install Release Tools
1818
#
1919
# --> RPM used by goreleaser
20-
&& apt install -y rpm \
21-
# Clean up
22-
&& apt-get autoremove -y \
23-
&& apt-get clean -y \
24-
&& rm -rf /var/lib/apt/lists/*
20+
&& apt install -y rpm
2521

2622
# This Dockerfile adds a non-root user with sudo access. Use the "remoteUser"
2723
# property in devcontainer.json to use it. On Linux, the container user's GID/UIDs

.devcontainer/devcontainer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@
4848

4949
// Add the IDs of extensions you want installed when the container is created.
5050
"extensions": [
51-
"golang.go"
51+
"golang.go",
52+
"stuartleeks.vscode-go-by-example"
5253
]
5354

5455
// Use 'forwardPorts' to make a list of ports inside the container available locally.

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,11 @@ You can use this with Windows Terminal profiles:
9292
},
9393
```
9494

95-
By default, `devcontainer exec` will set the working directory to be the mount path for the dev container. This can be overridden using `--work-dir`.
95+
96+
There are some other benefits of `devcontainer exec` compared to `docker exec`:
97+
- it sets the working directory to be the mount path for the dev container. This can be overridden using `--work-dir`.
98+
- it checks whether you have [configured a user in the dev container](https://code.visualstudio.com/docs/remote/containers-advanced#_adding-a-nonroot-user-to-your-dev-container) and uses this user for the `docker exec`.
99+
- it checks whether you have set up an SSH agent. If you have and VS Code detects it then VS Code will [forward key requests from the container](https://code.visualstudio.com/docs/remote/containers#_using-ssh-keys). In this scenario, `devcontainer exec` configures the exec session to also forward key requests. This enables operations against git remotes secured with SSH keys to succeed.
96100

97101
### Working with devcontainer templates
98102

cmd/devcontainer/devcontainer.go

Lines changed: 5 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,11 @@ package main
33
import (
44
"fmt"
55
"os"
6-
"os/exec"
7-
"path"
8-
"path/filepath"
96
"sort"
10-
"strings"
117
"text/tabwriter"
128

139
"github.com/spf13/cobra"
1410
"github.com/stuartleeks/devcontainer-cli/internal/pkg/devcontainers"
15-
"github.com/stuartleeks/devcontainer-cli/internal/pkg/wsl"
1611
)
1712

1813
func createListCommand() *cobra.Command {
@@ -103,19 +98,13 @@ func createExecCommand() *cobra.Command {
10398
return cmd.Usage()
10499
}
105100

106-
containerID := ""
101+
containerIDOrName := ""
107102
devcontainerList, err := devcontainers.ListDevcontainers()
108103
if err != nil {
109104
return err
110105
}
111106
if argDevcontainerName != "" {
112-
devcontainerName := argDevcontainerName
113-
for _, devcontainer := range devcontainerList {
114-
if devcontainer.ContainerName == devcontainerName || devcontainer.DevcontainerName == devcontainerName {
115-
containerID = devcontainer.ContainerID
116-
break
117-
}
118-
}
107+
containerIDOrName = argDevcontainerName
119108
} else if argPromptForDevcontainer {
120109
// prompt user
121110
fmt.Println("Specify the devcontainer to use:")
@@ -127,85 +116,16 @@ func createExecCommand() *cobra.Command {
127116
if selection < 0 || selection >= len(devcontainerList) {
128117
return fmt.Errorf("Invalid option")
129118
}
130-
containerID = devcontainerList[selection].ContainerID
119+
containerIDOrName = devcontainerList[selection].ContainerID
131120
} else {
132121
devcontainerPath := argDevcontainerPath
133-
if devcontainerPath == "" {
134-
devcontainerPath = "."
135-
}
136-
absPath, err := filepath.Abs(devcontainerPath)
137-
if err != nil {
138-
return fmt.Errorf("Error handling path %q: %s", devcontainerPath, err)
139-
}
140-
141-
windowsPath := absPath
142-
if wsl.IsWsl() {
143-
var err error
144-
windowsPath, err = wsl.ConvertWslPathToWindowsPath(windowsPath)
145-
if err != nil {
146-
return err
147-
}
148-
}
149-
for _, devcontainer := range devcontainerList {
150-
if devcontainer.LocalFolderPath == windowsPath {
151-
containerID = devcontainer.ContainerID
152-
break
153-
}
154-
}
155-
}
156-
157-
if containerID == "" {
158-
fmt.Println("Failed to find a matching (running) dev container")
159-
return cmd.Usage()
160-
}
161-
162-
localPath, err := devcontainers.GetLocalFolderFromDevContainer(containerID)
163-
if err != nil {
164-
return err
165-
}
166-
167-
mountPath := argWorkDir
168-
if mountPath == "" {
169-
mountPath, err = devcontainers.GetWorkspaceMountPath(localPath)
122+
containerIDOrName, err = devcontainers.GetContainerIDForPath(devcontainerPath)
170123
if err != nil {
171124
return err
172125
}
173126
}
174127

175-
wslPath := localPath
176-
if strings.HasPrefix(wslPath, "\\\\wsl$") && wsl.IsWsl() {
177-
wslPath, err = wsl.ConvertWindowsPathToWslPath(wslPath)
178-
if err != nil {
179-
return fmt.Errorf("error converting path: %s", err)
180-
}
181-
}
182-
183-
devcontainerJSONPath := path.Join(wslPath, ".devcontainer/devcontainer.json")
184-
userName, err := devcontainers.GetDevContainerUserName(devcontainerJSONPath)
185-
if err != nil {
186-
return err
187-
}
188-
189-
dockerArgs := []string{"exec", "-it", "--workdir", mountPath}
190-
if userName != "" {
191-
dockerArgs = append(dockerArgs, "--user", userName)
192-
}
193-
dockerArgs = append(dockerArgs, containerID)
194-
dockerArgs = append(dockerArgs, args...)
195-
196-
dockerCmd := exec.Command("docker", dockerArgs...)
197-
dockerCmd.Stdin = os.Stdin
198-
dockerCmd.Stdout = os.Stdout
199-
200-
err = dockerCmd.Start()
201-
if err != nil {
202-
return fmt.Errorf("Exec: start error: %s", err)
203-
}
204-
err = dockerCmd.Wait()
205-
if err != nil {
206-
return fmt.Errorf("Exec: wait error: %s", err)
207-
}
208-
return nil
128+
return devcontainers.ExecInDevContainer(containerIDOrName, argWorkDir, args)
209129
},
210130
Args: cobra.ArbitraryArgs,
211131
DisableFlagsInUseLine: true,

internal/pkg/devcontainers/dockerutils.go

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,13 @@ import (
44
"bufio"
55
"bytes"
66
"fmt"
7+
"os"
78
"os/exec"
9+
"path"
10+
"path/filepath"
811
"strings"
12+
13+
"github.com/stuartleeks/devcontainer-cli/internal/pkg/wsl"
914
)
1015

1116
// Devcontainer names are a derived property.
@@ -85,3 +90,148 @@ func GetLocalFolderFromDevContainer(containerIDOrName string) (string, error) {
8590

8691
return strings.TrimSpace(string(output)), nil
8792
}
93+
94+
// GetContainerIDForPath returns the ID of the running container that matches the path
95+
func GetContainerIDForPath(devcontainerPath string) (string, error) {
96+
if devcontainerPath == "" {
97+
devcontainerPath = "."
98+
}
99+
absPath, err := filepath.Abs(devcontainerPath)
100+
if err != nil {
101+
return "", fmt.Errorf("Error handling path %q: %s", devcontainerPath, err)
102+
}
103+
104+
windowsPath := absPath
105+
if wsl.IsWsl() {
106+
var err error
107+
windowsPath, err = wsl.ConvertWslPathToWindowsPath(windowsPath)
108+
if err != nil {
109+
return "", err
110+
}
111+
}
112+
113+
devcontainerList, err := ListDevcontainers()
114+
if err != nil {
115+
return "", fmt.Errorf("Error getting container list: %s", err)
116+
}
117+
118+
for _, devcontainer := range devcontainerList {
119+
if devcontainer.LocalFolderPath == windowsPath {
120+
containerID := devcontainer.ContainerID
121+
return containerID, nil
122+
}
123+
}
124+
return "", fmt.Errorf("Could not find running container for path %q", devcontainerPath)
125+
}
126+
127+
func ExecInDevContainer(containerIDOrName string, workDir string, args []string) error {
128+
129+
containerID := ""
130+
devcontainerList, err := ListDevcontainers()
131+
if err != nil {
132+
return err
133+
}
134+
135+
for _, devcontainer := range devcontainerList {
136+
if devcontainer.ContainerName == containerIDOrName ||
137+
devcontainer.DevcontainerName == containerIDOrName ||
138+
devcontainer.ContainerID == containerIDOrName {
139+
containerID = devcontainer.ContainerID
140+
break
141+
}
142+
}
143+
144+
if containerID == "" {
145+
return fmt.Errorf("Failed to find a matching (running) dev container for %q", containerIDOrName)
146+
}
147+
148+
localPath, err := GetLocalFolderFromDevContainer(containerID)
149+
if err != nil {
150+
return err
151+
}
152+
153+
if workDir == "" {
154+
workDir, err = GetWorkspaceMountPath(localPath)
155+
if err != nil {
156+
return err
157+
}
158+
}
159+
160+
wslPath := localPath
161+
if strings.HasPrefix(wslPath, "\\\\wsl$") && wsl.IsWsl() {
162+
wslPath, err = wsl.ConvertWindowsPathToWslPath(wslPath)
163+
if err != nil {
164+
return fmt.Errorf("error converting path: %s", err)
165+
}
166+
}
167+
168+
devcontainerJSONPath := path.Join(wslPath, ".devcontainer/devcontainer.json")
169+
userName, err := GetDevContainerUserName(devcontainerJSONPath)
170+
if err != nil {
171+
return err
172+
}
173+
174+
sshAuthSockValue, err := getSshAuthSockValue(containerID)
175+
if err != nil {
176+
// output error and continue without SSH_AUTH_SOCK value
177+
sshAuthSockValue = ""
178+
fmt.Printf("Warning: Failed to get SSH_AUTH_SOCK value: %s\n", err)
179+
fmt.Println("Continuing without setting SSH_AUTH_SOCK...")
180+
}
181+
182+
dockerArgs := []string{"exec", "-it", "--workdir", workDir}
183+
if userName != "" {
184+
dockerArgs = append(dockerArgs, "--user", userName)
185+
}
186+
if sshAuthSockValue != "" {
187+
dockerArgs = append(dockerArgs, "--env", "SSH_AUTH_SOCK="+sshAuthSockValue)
188+
}
189+
dockerArgs = append(dockerArgs, containerID)
190+
dockerArgs = append(dockerArgs, args...)
191+
192+
dockerCmd := exec.Command("docker", dockerArgs...)
193+
dockerCmd.Stdin = os.Stdin
194+
dockerCmd.Stdout = os.Stdout
195+
196+
err = dockerCmd.Start()
197+
if err != nil {
198+
return fmt.Errorf("Exec: start error: %s", err)
199+
}
200+
err = dockerCmd.Wait()
201+
if err != nil {
202+
return fmt.Errorf("Exec: wait error: %s", err)
203+
}
204+
return nil
205+
}
206+
207+
// getSshAuthSockValue returns the value to use for the SSH_AUTH_SOCK env var when exec'ing into the container, or empty string if no value is found
208+
func getSshAuthSockValue(containerID string) (string, error) {
209+
210+
// If the host has SSH_AUTH_SOCK set then VS Code spins up forwarding for key requests
211+
// inside the dev container to the SSH agent on the host.
212+
213+
hostSshAuthSockValue := os.Getenv("SSH_AUTH_SOCK")
214+
if hostSshAuthSockValue == "" {
215+
// Nothing to see, move along
216+
return "", nil
217+
}
218+
219+
// Host has SSH_AUTH_SOCK set, so expecting the dev container to have forwarding set up
220+
// Find the latest /tmp/vscode-ssh-auth-<...>.sock
221+
222+
dockerArgs := []string{"exec", containerID, "bash", "-c", "ls -t -d -1 \"${TMPDIR:-/tmp}\"/vscode-ssh-auth-*"}
223+
224+
dockerCmd := exec.Command("docker", dockerArgs...)
225+
buf, err := dockerCmd.CombinedOutput()
226+
if err != nil {
227+
errMessage := string(buf)
228+
return "", fmt.Errorf("Docker exec error: %s (%s)", err, strings.TrimSpace(errMessage))
229+
}
230+
231+
output := string(buf)
232+
lines := strings.Split(output, "\n")
233+
if len(lines) <= 0 {
234+
return "", nil
235+
}
236+
return strings.TrimSpace(lines[0]), nil
237+
}

0 commit comments

Comments
 (0)