Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit467a6e8

Browse files
committed
env from devcontainer metadata
1 parentd52d0c1 commit467a6e8

File tree

2 files changed

+137
-42
lines changed

2 files changed

+137
-42
lines changed

‎agent/agentcontainers/containers_dockercli.go‎

Lines changed: 79 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"fmt"
99
"os"
1010
"os/user"
11+
"slices"
1112
"sort"
1213
"strconv"
1314
"strings"
@@ -109,13 +110,14 @@ func EnvInfo(ctx context.Context, execer agentexec.Execer, container, containerU
109110
}
110111
cei.userShell=passwdFields[6]
111112

112-
// Finally, get the environment of the container.
113-
// TODO: For containers, we can't simply run `env` inside the container.
114113
// We need to inspect the container labels for remoteEnv and append these to
115114
// the resulting docker exec command.
116115
// ref: https://code.visualstudio.com/docs/devcontainers/attach-container
117-
// For now, we're simply returning the host environment.
118-
cei.env=make([]string,0)
116+
env,err:=devcontainerEnv(ctx,execer,container)
117+
iferr!=nil {// best effort.
118+
returnnil,xerrors.Errorf("read devcontainer remoteEnv: %w",err)
119+
}
120+
cei.env=env
119121

120122
return&cei,nil
121123
}
@@ -158,12 +160,57 @@ func (cei *ContainerEnvInfoer) ModifyCommand(cmd string, args ...string) (string
158160
// Set the working directory to the user's home directory as a sane default.
159161
"--workdir",
160162
cei.user.HomeDir,
161-
cei.container,
162-
cmd,
163163
}
164+
165+
// Append the environment variables from the container.
166+
for_,e:=rangecei.env {
167+
dockerArgs=append(dockerArgs,"--env",e)
168+
}
169+
170+
// Append the container name and the command.
171+
dockerArgs=append(dockerArgs,cei.container,cmd)
164172
return"docker",append(dockerArgs,args...)
165173
}
166174

175+
// devcontainerEnv is a helper function that inspects the container labels to
176+
// find the required environment variables for running a command in the container.
177+
funcdevcontainerEnv(ctx context.Context,execer agentexec.Execer,containerstring) ([]string,error) {
178+
ins,stderr,err:=runDockerInspect(ctx,execer,container)
179+
iferr!=nil {
180+
returnnil,xerrors.Errorf("inspect container: %w: %q",err,stderr)
181+
}
182+
183+
iflen(ins)!=1 {
184+
returnnil,xerrors.Errorf("inspect container: expected 1 container, got %d",len(ins))
185+
}
186+
187+
in:=ins[0]
188+
ifin.Config.Labels==nil {
189+
returnnil,nil
190+
}
191+
192+
// We want to look for the devcontainer metadata, which is in the
193+
// value of the label `devcontainer.metadata`.
194+
rawMeta,ok:=in.Config.Labels["devcontainer.metadata"]
195+
if!ok {
196+
returnnil,nil
197+
}
198+
meta:=struct {
199+
RemoteEnvmap[string]string`json:"remoteEnv"`
200+
}{}
201+
iferr:=json.Unmarshal([]byte(rawMeta),&meta);err!=nil {
202+
returnnil,xerrors.Errorf("unmarshal devcontainer.metadata: %w",err)
203+
}
204+
205+
// The environment variables are stored in the `remoteEnv` key.
206+
env:=make([]string,0,len(meta.RemoteEnv))
207+
fork,v:=rangemeta.RemoteEnv {
208+
env=append(env,fmt.Sprintf("%s=%s",k,v))
209+
}
210+
slices.Sort(env)
211+
returnenv,nil
212+
}
213+
167214
// wrapDockerExec is a helper function that wraps the given command and arguments
168215
// with a docker exec command that runs as the given user in the given
169216
// container. This is used to fetch information about a container prior to
@@ -227,30 +274,16 @@ func (dcl *DockerCLILister) List(ctx context.Context) (codersdk.WorkspaceAgentLi
227274
}
228275

229276
// now we can get the detailed information for each container
230-
// Run `docker inspect` on each container ID
231-
stdoutBuf.Reset()
232-
stderrBuf.Reset()
233-
// nolint: gosec // We are not executing user input, these IDs come from
234-
// `docker ps`.
235-
cmd=dcl.execer.CommandContext(ctx,"docker",append([]string{"inspect"},ids...)...)
236-
cmd.Stdout=&stdoutBuf
237-
cmd.Stderr=&stderrBuf
238-
iferr:=cmd.Run();err!=nil {
239-
return codersdk.WorkspaceAgentListContainersResponse{},xerrors.Errorf("run docker inspect: %w: %s",err,strings.TrimSpace(stderrBuf.String()))
240-
}
241-
242-
dockerInspectStderr:=strings.TrimSpace(stderrBuf.String())
243-
277+
// Run `docker inspect` on each container ID.
244278
// NOTE: There is an unavoidable potential race condition where a
245279
// container is removed between `docker ps` and `docker inspect`.
246280
// In this case, stderr will contain an error message but stdout
247281
// will still contain valid JSON. We will just end up missing
248282
// information about the removed container. We could potentially
249283
// log this error, but I'm not sure it's worth it.
250-
ins:=make([]dockerInspect,0,len(ids))
251-
iferr:=json.NewDecoder(&stdoutBuf).Decode(&ins);err!=nil {
252-
// However, if we just get invalid JSON, we should absolutely return an error.
253-
return codersdk.WorkspaceAgentListContainersResponse{},xerrors.Errorf("decode docker inspect output: %w",err)
284+
ins,dockerInspectStderr,err:=runDockerInspect(ctx,dcl.execer,ids...)
285+
iferr!=nil {
286+
return codersdk.WorkspaceAgentListContainersResponse{},xerrors.Errorf("run docker inspect: %w",err)
254287
}
255288

256289
res:= codersdk.WorkspaceAgentListContainersResponse{
@@ -272,6 +305,28 @@ func (dcl *DockerCLILister) List(ctx context.Context) (codersdk.WorkspaceAgentLi
272305
returnres,nil
273306
}
274307

308+
// runDockerInspect is a helper function that runs `docker inspect` on the given
309+
// container IDs and returns the parsed output.
310+
// The stderr output is also returned for logging purposes.
311+
funcrunDockerInspect(ctx context.Context,execer agentexec.Execer,ids...string) ([]dockerInspect,string,error) {
312+
varstdoutBuf,stderrBuf bytes.Buffer
313+
cmd:=execer.CommandContext(ctx,"docker",append([]string{"inspect"},ids...)...)
314+
cmd.Stdout=&stdoutBuf
315+
cmd.Stderr=&stderrBuf
316+
err:=cmd.Run()
317+
stderr:=strings.TrimSpace(stderrBuf.String())
318+
iferr!=nil {
319+
returnnil,stderr,err
320+
}
321+
322+
varins []dockerInspect
323+
iferr:=json.NewDecoder(&stdoutBuf).Decode(&ins);err!=nil {
324+
returnnil,stderr,xerrors.Errorf("decode docker inspect output: %w",err)
325+
}
326+
327+
returnins,stderr,nil
328+
}
329+
275330
// To avoid a direct dependency on the Docker API, we use the docker CLI
276331
// to fetch information about containers.
277332
typedockerInspectstruct {

‎agent/agentcontainers/containers_internal_test.go‎

Lines changed: 58 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package agentcontainers
33
import (
44
"fmt"
55
"os"
6+
"slices"
67
"strconv"
78
"strings"
89
"testing"
@@ -47,10 +48,13 @@ func TestIntegrationDocker(t *testing.T) {
4748
// Pick a random port to expose for testing port bindings.
4849
testRandPort:=testutil.RandomPortNoListen(t)
4950
ct,err:=pool.RunWithOptions(&dockertest.RunOptions{
50-
Repository:"busybox",
51-
Tag:"latest",
52-
Cmd: []string{"sleep","infnity"},
53-
Labels:map[string]string{"com.coder.test":testLabelValue},
51+
Repository:"busybox",
52+
Tag:"latest",
53+
Cmd: []string{"sleep","infnity"},
54+
Labels:map[string]string{
55+
"com.coder.test":testLabelValue,
56+
"devcontainer.metadata":`{"remoteEnv": {"FOO": "bar", "MULTILINE": "foo\nbar\nbaz"}}`,
57+
},
5458
Mounts: []string{testTempDir+":"+testTempDir},
5559
ExposedPorts: []string{fmt.Sprintf("%d/tcp",testRandPort)},
5660
PortBindings:map[docker.Port][]docker.PortBinding{
@@ -122,6 +126,12 @@ func TestIntegrationDocker(t *testing.T) {
122126
matchHostnameOuput:=func(linestring)bool {
123127
returnstrings.Contains(strings.TrimSpace(line),ct.Container.Config.Hostname)
124128
}
129+
matchEnvCmd:=func(linestring)bool {
130+
returnstrings.Contains(strings.TrimSpace(line),"env")
131+
}
132+
matchEnvOutput:=func(linestring)bool {
133+
returnstrings.Contains(line,"FOO=bar")||strings.Contains(line,"MULTILINE=foo")
134+
}
125135
require.NoError(t,tr.ReadUntil(ctx,matchPrompt),"failed to match prompt")
126136
t.Logf("Matched prompt")
127137
_,err=ptyCmd.InputWriter().Write([]byte("hostname\r\n"))
@@ -131,6 +141,13 @@ func TestIntegrationDocker(t *testing.T) {
131141
t.Logf("Matched hostname command")
132142
require.NoError(t,tr.ReadUntil(ctx,matchHostnameOuput),"failed to match hostname output")
133143
t.Logf("Matched hostname output")
144+
_,err=ptyCmd.InputWriter().Write([]byte("env\r\n"))
145+
require.NoError(t,err,"failed to write to pty")
146+
t.Logf("Wrote env command")
147+
require.NoError(t,tr.ReadUntil(ctx,matchEnvCmd),"failed to match env command")
148+
t.Logf("Matched env command")
149+
require.NoError(t,tr.ReadUntil(ctx,matchEnvOutput),"failed to match env output")
150+
t.Logf("Matched env output")
134151
break
135152
}
136153
}
@@ -403,49 +420,56 @@ func TestConvertDockerVolume(t *testing.T) {
403420
// CODER_TEST_USE_DOCKER=1 go test ./agent/agentcontainers -run TestDockerEnvInfoer
404421
funcTestDockerEnvInfoer(t*testing.T) {
405422
t.Parallel()
406-
ifctud,ok:=os.LookupEnv("CODER_TEST_USE_DOCKER");!ok||ctud!="1" {
407-
t.Skip("Set CODER_TEST_USE_DOCKER=1 to run this test")
408-
}
423+
//if ctud, ok := os.LookupEnv("CODER_TEST_USE_DOCKER"); !ok || ctud != "1" {
424+
//t.Skip("Set CODER_TEST_USE_DOCKER=1 to run this test")
425+
//}
409426

410427
pool,err:=dockertest.NewPool("")
411428
require.NoError(t,err,"Could not connect to docker")
412429
// nolint:paralleltest // variable recapture no longer required
413430
foridx,tt:=range []struct {
414431
imagestring
415-
env []string
432+
labelsmap[string]string
433+
expectedEnv []string
416434
containerUserstring
417435
expectedUsernamestring
418436
expectedUserShellstring
419437
}{
420438
{
421-
image:"busybox:latest",
422-
env: []string{"FOO=bar","MULTILINE=foo\nbar\nbaz"},
439+
image:"busybox:latest",
440+
labels:map[string]string{`devcontainer.metadata`:`{"remoteEnv": {"FOO": "bar", "MULTILINE": "foo\nbar\nbaz"}}`},
441+
442+
expectedEnv: []string{"FOO=bar","MULTILINE=foo\nbar\nbaz"},
423443
expectedUsername:"root",
424444
expectedUserShell:"/bin/sh",
425445
},
426446
{
427447
image:"busybox:latest",
428-
env: []string{"FOO=bar","MULTILINE=foo\nbar\nbaz"},
448+
labels:map[string]string{`devcontainer.metadata`:`{"remoteEnv": {"FOO": "bar", "MULTILINE": "foo\nbar\nbaz"}}`},
449+
expectedEnv: []string{"FOO=bar","MULTILINE=foo\nbar\nbaz"},
429450
containerUser:"root",
430451
expectedUsername:"root",
431452
expectedUserShell:"/bin/sh",
432453
},
433454
{
434455
image:"codercom/enterprise-minimal:ubuntu",
435-
env: []string{"FOO=bar","MULTILINE=foo\nbar\nbaz"},
456+
labels:map[string]string{`devcontainer.metadata`:`{"remoteEnv": {"FOO": "bar", "MULTILINE": "foo\nbar\nbaz"}}`},
457+
expectedEnv: []string{"FOO=bar","MULTILINE=foo\nbar\nbaz"},
436458
expectedUsername:"coder",
437459
expectedUserShell:"/bin/bash",
438460
},
439461
{
440462
image:"codercom/enterprise-minimal:ubuntu",
441-
env: []string{"FOO=bar","MULTILINE=foo\nbar\nbaz"},
463+
labels:map[string]string{`devcontainer.metadata`:`{"remoteEnv": {"FOO": "bar", "MULTILINE": "foo\nbar\nbaz"}}`},
464+
expectedEnv: []string{"FOO=bar","MULTILINE=foo\nbar\nbaz"},
442465
containerUser:"coder",
443466
expectedUsername:"coder",
444467
expectedUserShell:"/bin/bash",
445468
},
446469
{
447470
image:"codercom/enterprise-minimal:ubuntu",
448-
env: []string{"FOO=bar","MULTILINE=foo\nbar\nbaz"},
471+
labels:map[string]string{`devcontainer.metadata`:`{"remoteEnv": {"FOO": "bar", "MULTILINE": "foo\nbar\nbaz"}}`},
472+
expectedEnv: []string{"FOO=bar","MULTILINE=foo\nbar\nbaz"},
449473
containerUser:"root",
450474
expectedUsername:"root",
451475
expectedUserShell:"/bin/bash",
@@ -462,7 +486,7 @@ func TestDockerEnvInfoer(t *testing.T) {
462486
Repository:image,
463487
Tag:tag,
464488
Cmd: []string{"sleep","infinity"},
465-
Env:tt.env,
489+
Labels:tt.labels,
466490
},func(config*docker.HostConfig) {
467491
config.AutoRemove=true
468492
config.RestartPolicy= docker.RestartPolicy{Name:"no"}
@@ -490,9 +514,25 @@ func TestDockerEnvInfoer(t *testing.T) {
490514
require.NoError(t,err,"Expected no error from UserShell()")
491515
require.Equal(t,tt.expectedUserShell,sh,"Expected user shell to match")
492516

493-
// TODO: environment from devcontainer labels.
494-
// environ := dei.Environ()
495-
// require.Subset(t, environ, tt.env, "Expected environment to match")
517+
// We don't need to test the actual environment variables here.
518+
environ:=dei.Environ()
519+
require.NotEmpty(t,environ,"Expected environ to be non-empty")
520+
521+
// Test that the environment variables are present in modified command
522+
// output.
523+
envCmd,envArgs:=dei.ModifyCommand("env")
524+
for_,env:=rangett.expectedEnv {
525+
require.Subset(t,envArgs, []string{"--env",env})
526+
}
527+
// Run the command in the container and check the output
528+
// HACK: we remove the --tty argument because we're not running in a tty
529+
envArgs=slices.DeleteFunc(envArgs,func(sstring)bool {returns=="--tty" })
530+
stdout,stderr,err:=run(ctx,agentexec.DefaultExecer,envCmd,envArgs...)
531+
require.Empty(t,stderr,"Expected no stderr output")
532+
require.NoError(t,err,"Expected no error from running command")
533+
for_,env:=rangett.expectedEnv {
534+
require.Contains(t,stdout,env)
535+
}
496536
})
497537
}
498538
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp