@@ -42,51 +42,56 @@ type ContainerEnvInfoer struct {
42
42
env []string
43
43
}
44
44
45
+ // Helper function to run a command and return its stdout and stderr.
46
+ // We want to differentiate stdout and stderr instead of using CombinedOutput.
47
+ // We also want to differentiate between a command running successfully with
48
+ // output to stderr and a non-zero exit code.
49
+ func run (ctx context.Context , execer agentexec.Execer , cmd string , args ... string ) (stdout , stderr string , err error ) {
50
+ var stdoutBuf , stderrBuf bytes.Buffer
51
+ execCmd := execer .CommandContext (ctx , cmd , args ... )
52
+ execCmd .Stdout = & stdoutBuf
53
+ execCmd .Stderr = & stderrBuf
54
+ err = execCmd .Run ()
55
+ stdout = strings .TrimSpace (stdoutBuf .String ())
56
+ stderr = strings .TrimSpace (stderrBuf .String ())
57
+ return stdout , stderr , err
58
+ }
59
+
45
60
// EnvInfo returns information about the environment of a container.
46
61
func EnvInfo (ctx context.Context , execer agentexec.Execer , container , containerUser string ) (* ContainerEnvInfoer , error ) {
47
- var dei ContainerEnvInfoer
48
- dei .container = container
62
+ var cei ContainerEnvInfoer
63
+ cei .container = container
49
64
50
- var stdoutBuf , stderrBuf bytes.Buffer
51
65
if containerUser == "" {
52
66
// Get the "default" user of the container if no user is specified.
53
67
// TODO: handle different container runtimes.
54
68
cmd , args := WrapDockerExec (container , "" )("whoami" )
55
- execCmd := execer .CommandContext (ctx , cmd , args ... )
56
- execCmd .Stdout = & stdoutBuf
57
- execCmd .Stderr = & stderrBuf
58
- if err := execCmd .Run (); err != nil {
59
- return nil , xerrors .Errorf ("get container user: run whoami: %w: stderr: %q" , err , strings .TrimSpace (stderrBuf .String ()))
69
+ stdout , stderr , err := run (ctx , execer , cmd , args ... )
70
+ if err != nil {
71
+ return nil , xerrors .Errorf ("get container user: run whoami: %w: %s" , err , stderr )
60
72
}
61
- out := strings .TrimSpace (stdoutBuf .String ())
62
- if len (out ) == 0 {
63
- return nil , xerrors .Errorf ("get container user: run whoami: empty output: stderr: %q" , strings .TrimSpace (stderrBuf .String ()))
73
+ if len (stdout ) == 0 {
74
+ return nil , xerrors .Errorf ("get container user: run whoami: empty output" )
64
75
}
65
- containerUser = out
66
- stdoutBuf .Reset ()
67
- stderrBuf .Reset ()
76
+ containerUser = stdout
68
77
}
69
78
// Now that we know the username, get the required info from the container.
70
79
// We can't assume the presence of `getent` so we'll just have to sniff /etc/passwd.
71
80
cmd , args := WrapDockerExec (container , containerUser )("cat" , "/etc/passwd" )
72
- execCmd := execer .CommandContext (ctx , cmd , args ... )
73
- execCmd .Stdout = & stdoutBuf
74
- execCmd .Stderr = & stderrBuf
75
- if err := execCmd .Run (); err != nil {
76
- return nil , xerrors .Errorf ("get container user: read /etc/passwd: %w stderr: %q" , err , strings .TrimSpace (stderrBuf .String ()))
81
+ stdout , stderr , err := run (ctx , execer , cmd , args ... )
82
+ if err != nil {
83
+ return nil , xerrors .Errorf ("get container user: read /etc/passwd: %w: %q" , err , stderr )
77
84
}
78
85
79
- scanner := bufio .NewScanner (& stdoutBuf )
86
+ scanner := bufio .NewScanner (strings . NewReader ( stdout ) )
80
87
var foundLine string
81
88
for scanner .Scan () {
82
89
line := strings .TrimSpace (scanner .Text ())
83
- if len (line ) == 0 {
84
- continue
85
- }
86
90
if ! strings .HasPrefix (line , containerUser + ":" ) {
87
91
continue
88
92
}
89
93
foundLine = line
94
+ break
90
95
}
91
96
if err := scanner .Err (); err != nil {
92
97
return nil , xerrors .Errorf ("get container user: scan /etc/passwd: %w" , err )
@@ -110,36 +115,40 @@ func EnvInfo(ctx context.Context, execer agentexec.Execer, container, containerU
110
115
fullName = gecos [0 ]
111
116
}
112
117
113
- dei .user = & user.User {
118
+ cei .user = & user.User {
114
119
Gid : passwdFields [3 ],
115
120
HomeDir : passwdFields [5 ],
116
121
Name : fullName ,
117
122
Uid : passwdFields [2 ],
118
123
Username : containerUser ,
119
124
}
120
- dei .userShell = passwdFields [6 ]
125
+ cei .userShell = passwdFields [6 ]
121
126
122
127
// Finally, get the environment of the container.
123
- stdoutBuf .Reset ()
124
- stderrBuf .Reset ()
125
- cmd , args = WrapDockerExec (container , containerUser )("env" )
126
- execCmd = execer .CommandContext (ctx , cmd , args ... )
127
- execCmd .Stdout = & stdoutBuf
128
- execCmd .Stderr = & stderrBuf
129
- if err := execCmd .Run (); err != nil {
130
- return nil , xerrors .Errorf ("get container environment: run env: %w stderr: %q" , err , strings .TrimSpace (stderrBuf .String ()))
131
- }
132
-
133
- scanner = bufio .NewScanner (& stdoutBuf )
134
- for scanner .Scan () {
135
- line := strings .TrimSpace (scanner .Text ())
136
- dei .env = append (dei .env , line )
128
+ // TODO(cian): for devcontainers we will need to also take into account both
129
+ // remoteEnv and userEnvProbe.
130
+ // ref: https://code.visualstudio.com/docs/devcontainers/attach-container
131
+ cmd , args = WrapDockerExec (container , containerUser )("env" , "-0" )
132
+ stdout , stderr , err = run (ctx , execer , cmd , args ... )
133
+ if err != nil {
134
+ return nil , xerrors .Errorf ("get container environment: run env -0: %w: %q" , err , stderr )
135
+ }
136
+
137
+ // Parse the output of `env -0` which is a null-terminated list of environment
138
+ // variables. This is important as environment variables can contain newlines.
139
+ envs := strings .Split (stdout , "\x00 " )
140
+ for _ , env := range envs {
141
+ env = strings .TrimSpace (env )
142
+ if env == "" {
143
+ continue
144
+ }
145
+ cei .env = append (cei .env , env )
137
146
}
138
147
if err := scanner .Err (); err != nil {
139
148
return nil , xerrors .Errorf ("get container environment: scan env output: %w" , err )
140
149
}
141
150
142
- return & dei , nil
151
+ return & cei , nil
143
152
}
144
153
145
154
func (dei * ContainerEnvInfoer ) CurrentUser () (* user.User , error ) {
0 commit comments