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

Commit304007b

Browse files
johnstcnmafredri
andauthored
feat(agent/agentcontainers): add ContainerEnvInfoer (#16623)
This PR adds an alternative implementation of EnvInfo(#16603) that reads information froma running container.---------Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
1 parentac88c9b commit304007b

File tree

3 files changed

+462
-27
lines changed

3 files changed

+462
-27
lines changed

‎agent/agentcontainers/containers.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,8 @@ type Lister interface {
144144
// NoopLister is a Lister interface that never returns any containers.
145145
typeNoopListerstruct{}
146146

147+
var_Lister=NoopLister{}
148+
147149
func (NoopLister)List(_ context.Context) (codersdk.WorkspaceAgentListContainersResponse,error) {
148150
return codersdk.WorkspaceAgentListContainersResponse{},nil
149151
}

‎agent/agentcontainers/containers_dockercli.go

Lines changed: 233 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import (
66
"context"
77
"encoding/json"
88
"fmt"
9+
"os"
10+
"os/user"
11+
"slices"
912
"sort"
1013
"strconv"
1114
"strings"
@@ -31,6 +34,210 @@ func NewDocker(execer agentexec.Execer) Lister {
3134
}
3235
}
3336

37+
// DockerEnvInfoer is an implementation of agentssh.EnvInfoer that returns
38+
// information about a container.
39+
typeDockerEnvInfoerstruct {
40+
containerstring
41+
user*user.User
42+
userShellstring
43+
env []string
44+
}
45+
46+
// EnvInfo returns information about the environment of a container.
47+
funcEnvInfo(ctx context.Context,execer agentexec.Execer,container,containerUserstring) (*DockerEnvInfoer,error) {
48+
vardeiDockerEnvInfoer
49+
dei.container=container
50+
51+
ifcontainerUser=="" {
52+
// Get the "default" user of the container if no user is specified.
53+
// TODO: handle different container runtimes.
54+
cmd,args:=wrapDockerExec(container,"","whoami")
55+
stdout,stderr,err:=run(ctx,execer,cmd,args...)
56+
iferr!=nil {
57+
returnnil,xerrors.Errorf("get container user: run whoami: %w: %s",err,stderr)
58+
}
59+
iflen(stdout)==0 {
60+
returnnil,xerrors.Errorf("get container user: run whoami: empty output")
61+
}
62+
containerUser=stdout
63+
}
64+
// Now that we know the username, get the required info from the container.
65+
// We can't assume the presence of `getent` so we'll just have to sniff /etc/passwd.
66+
cmd,args:=wrapDockerExec(container,containerUser,"cat","/etc/passwd")
67+
stdout,stderr,err:=run(ctx,execer,cmd,args...)
68+
iferr!=nil {
69+
returnnil,xerrors.Errorf("get container user: read /etc/passwd: %w: %q",err,stderr)
70+
}
71+
72+
scanner:=bufio.NewScanner(strings.NewReader(stdout))
73+
varfoundLinestring
74+
forscanner.Scan() {
75+
line:=strings.TrimSpace(scanner.Text())
76+
if!strings.HasPrefix(line,containerUser+":") {
77+
continue
78+
}
79+
foundLine=line
80+
break
81+
}
82+
iferr:=scanner.Err();err!=nil {
83+
returnnil,xerrors.Errorf("get container user: scan /etc/passwd: %w",err)
84+
}
85+
iffoundLine=="" {
86+
returnnil,xerrors.Errorf("get container user: no matching entry for %q found in /etc/passwd",containerUser)
87+
}
88+
89+
// Parse the output of /etc/passwd. It looks like this:
90+
// postgres:x:999:999::/var/lib/postgresql:/bin/bash
91+
passwdFields:=strings.Split(foundLine,":")
92+
iflen(passwdFields)!=7 {
93+
returnnil,xerrors.Errorf("get container user: invalid line in /etc/passwd: %q",foundLine)
94+
}
95+
96+
// The fifth entry in /etc/passwd contains GECOS information, which is a
97+
// comma-separated list of fields. The first field is the user's full name.
98+
gecos:=strings.Split(passwdFields[4],",")
99+
fullName:=""
100+
iflen(gecos)>1 {
101+
fullName=gecos[0]
102+
}
103+
104+
dei.user=&user.User{
105+
Gid:passwdFields[3],
106+
HomeDir:passwdFields[5],
107+
Name:fullName,
108+
Uid:passwdFields[2],
109+
Username:containerUser,
110+
}
111+
dei.userShell=passwdFields[6]
112+
113+
// We need to inspect the container labels for remoteEnv and append these to
114+
// the resulting docker exec command.
115+
// ref: https://code.visualstudio.com/docs/devcontainers/attach-container
116+
env,err:=devcontainerEnv(ctx,execer,container)
117+
iferr!=nil {// best effort.
118+
returnnil,xerrors.Errorf("read devcontainer remoteEnv: %w",err)
119+
}
120+
dei.env=env
121+
122+
return&dei,nil
123+
}
124+
125+
func (dei*DockerEnvInfoer)CurrentUser() (*user.User,error) {
126+
// Clone the user so that the caller can't modify it
127+
u:=*dei.user
128+
return&u,nil
129+
}
130+
131+
func (*DockerEnvInfoer)Environ() []string {
132+
// Return a clone of the environment so that the caller can't modify it
133+
returnos.Environ()
134+
}
135+
136+
func (*DockerEnvInfoer)UserHomeDir() (string,error) {
137+
// We default the working directory of the command to the user's home
138+
// directory. Since this came from inside the container, we cannot guarantee
139+
// that this exists on the host. Return the "real" home directory of the user
140+
// instead.
141+
returnos.UserHomeDir()
142+
}
143+
144+
func (dei*DockerEnvInfoer)UserShell(string) (string,error) {
145+
returndei.userShell,nil
146+
}
147+
148+
func (dei*DockerEnvInfoer)ModifyCommand(cmdstring,args...string) (string, []string) {
149+
// Wrap the command with `docker exec` and run it as the container user.
150+
// There is some additional munging here regarding the container user and environment.
151+
dockerArgs:= []string{
152+
"exec",
153+
// The assumption is that this command will be a shell command, so allocate a PTY.
154+
"--interactive",
155+
"--tty",
156+
// Run the command as the user in the container.
157+
"--user",
158+
dei.user.Username,
159+
// Set the working directory to the user's home directory as a sane default.
160+
"--workdir",
161+
dei.user.HomeDir,
162+
}
163+
164+
// Append the environment variables from the container.
165+
for_,e:=rangedei.env {
166+
dockerArgs=append(dockerArgs,"--env",e)
167+
}
168+
169+
// Append the container name and the command.
170+
dockerArgs=append(dockerArgs,dei.container,cmd)
171+
return"docker",append(dockerArgs,args...)
172+
}
173+
174+
// devcontainerEnv is a helper function that inspects the container labels to
175+
// find the required environment variables for running a command in the container.
176+
funcdevcontainerEnv(ctx context.Context,execer agentexec.Execer,containerstring) ([]string,error) {
177+
ins,stderr,err:=runDockerInspect(ctx,execer,container)
178+
iferr!=nil {
179+
returnnil,xerrors.Errorf("inspect container: %w: %q",err,stderr)
180+
}
181+
182+
iflen(ins)!=1 {
183+
returnnil,xerrors.Errorf("inspect container: expected 1 container, got %d",len(ins))
184+
}
185+
186+
in:=ins[0]
187+
ifin.Config.Labels==nil {
188+
returnnil,nil
189+
}
190+
191+
// We want to look for the devcontainer metadata, which is in the
192+
// value of the label `devcontainer.metadata`.
193+
rawMeta,ok:=in.Config.Labels["devcontainer.metadata"]
194+
if!ok {
195+
returnnil,nil
196+
}
197+
meta:=struct {
198+
RemoteEnvmap[string]string`json:"remoteEnv"`
199+
}{}
200+
iferr:=json.Unmarshal([]byte(rawMeta),&meta);err!=nil {
201+
returnnil,xerrors.Errorf("unmarshal devcontainer.metadata: %w",err)
202+
}
203+
204+
// The environment variables are stored in the `remoteEnv` key.
205+
env:=make([]string,0,len(meta.RemoteEnv))
206+
fork,v:=rangemeta.RemoteEnv {
207+
env=append(env,fmt.Sprintf("%s=%s",k,v))
208+
}
209+
slices.Sort(env)
210+
returnenv,nil
211+
}
212+
213+
// wrapDockerExec is a helper function that wraps the given command and arguments
214+
// with a docker exec command that runs as the given user in the given
215+
// container. This is used to fetch information about a container prior to
216+
// running the actual command.
217+
funcwrapDockerExec(containerName,userName,cmdstring,args...string) (string, []string) {
218+
dockerArgs:= []string{"exec","--interactive"}
219+
ifuserName!="" {
220+
dockerArgs=append(dockerArgs,"--user",userName)
221+
}
222+
dockerArgs=append(dockerArgs,containerName,cmd)
223+
return"docker",append(dockerArgs,args...)
224+
}
225+
226+
// Helper function to run a command and return its stdout and stderr.
227+
// We want to differentiate stdout and stderr instead of using CombinedOutput.
228+
// We also want to differentiate between a command running successfully with
229+
// output to stderr and a non-zero exit code.
230+
funcrun(ctx context.Context,execer agentexec.Execer,cmdstring,args...string) (stdout,stderrstring,errerror) {
231+
varstdoutBuf,stderrBuf strings.Builder
232+
execCmd:=execer.CommandContext(ctx,cmd,args...)
233+
execCmd.Stdout=&stdoutBuf
234+
execCmd.Stderr=&stderrBuf
235+
err=execCmd.Run()
236+
stdout=strings.TrimSpace(stdoutBuf.String())
237+
stderr=strings.TrimSpace(stderrBuf.String())
238+
returnstdout,stderr,err
239+
}
240+
34241
func (dcl*DockerCLILister)List(ctx context.Context) (codersdk.WorkspaceAgentListContainersResponse,error) {
35242
varstdoutBuf,stderrBuf bytes.Buffer
36243
// List all container IDs, one per line, with no truncation
@@ -66,30 +273,16 @@ func (dcl *DockerCLILister) List(ctx context.Context) (codersdk.WorkspaceAgentLi
66273
}
67274

68275
// now we can get the detailed information for each container
69-
// Run `docker inspect` on each container ID
70-
stdoutBuf.Reset()
71-
stderrBuf.Reset()
72-
// nolint: gosec // We are not executing user input, these IDs come from
73-
// `docker ps`.
74-
cmd=dcl.execer.CommandContext(ctx,"docker",append([]string{"inspect"},ids...)...)
75-
cmd.Stdout=&stdoutBuf
76-
cmd.Stderr=&stderrBuf
77-
iferr:=cmd.Run();err!=nil {
78-
return codersdk.WorkspaceAgentListContainersResponse{},xerrors.Errorf("run docker inspect: %w: %s",err,strings.TrimSpace(stderrBuf.String()))
79-
}
80-
81-
dockerInspectStderr:=strings.TrimSpace(stderrBuf.String())
82-
276+
// Run `docker inspect` on each container ID.
83277
// NOTE: There is an unavoidable potential race condition where a
84278
// container is removed between `docker ps` and `docker inspect`.
85279
// In this case, stderr will contain an error message but stdout
86280
// will still contain valid JSON. We will just end up missing
87281
// information about the removed container. We could potentially
88282
// log this error, but I'm not sure it's worth it.
89-
ins:=make([]dockerInspect,0,len(ids))
90-
iferr:=json.NewDecoder(&stdoutBuf).Decode(&ins);err!=nil {
91-
// However, if we just get invalid JSON, we should absolutely return an error.
92-
return codersdk.WorkspaceAgentListContainersResponse{},xerrors.Errorf("decode docker inspect output: %w",err)
283+
ins,dockerInspectStderr,err:=runDockerInspect(ctx,dcl.execer,ids...)
284+
iferr!=nil {
285+
return codersdk.WorkspaceAgentListContainersResponse{},xerrors.Errorf("run docker inspect: %w",err)
93286
}
94287

95288
res:= codersdk.WorkspaceAgentListContainersResponse{
@@ -111,6 +304,28 @@ func (dcl *DockerCLILister) List(ctx context.Context) (codersdk.WorkspaceAgentLi
111304
returnres,nil
112305
}
113306

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

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp