6
6
"context"
7
7
"encoding/json"
8
8
"fmt"
9
+ "os/user"
10
+ "slices"
9
11
"sort"
10
12
"strconv"
11
13
"strings"
@@ -31,6 +33,172 @@ func NewDocker(execer agentexec.Execer) Lister {
31
33
}
32
34
}
33
35
36
+ // ContainerEnvInfoer is an implementation of agentssh.EnvInfoer that returns
37
+ // information about a container.
38
+ type ContainerEnvInfoer struct {
39
+ container string
40
+ user * user.User
41
+ userShell string
42
+ env []string
43
+ }
44
+
45
+ // EnvInfo returns information about the environment of a container.
46
+ func EnvInfo (ctx context.Context , execer agentexec.Execer , container , containerUser string ) (* ContainerEnvInfoer , error ) {
47
+ var dei ContainerEnvInfoer
48
+ dei .container = container
49
+
50
+ var stdoutBuf , stderrBuf bytes.Buffer
51
+ if containerUser == "" {
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
+ 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 ()))
60
+ }
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 ()))
64
+ }
65
+ containerUser = out
66
+ stdoutBuf .Reset ()
67
+ stderrBuf .Reset ()
68
+ }
69
+ // Now that we know the username, get the required info from the container.
70
+ // We can't assume the presence of `getent` so we'll just have to sniff /etc/passwd.
71
+ 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 ()))
77
+ }
78
+
79
+ scanner := bufio .NewScanner (& stdoutBuf )
80
+ var foundLine string
81
+ for scanner .Scan () {
82
+ line := strings .TrimSpace (scanner .Text ())
83
+ if len (line ) == 0 {
84
+ continue
85
+ }
86
+ if ! strings .HasPrefix (line , containerUser + ":" ) {
87
+ continue
88
+ }
89
+ foundLine = line
90
+ }
91
+ if err := scanner .Err (); err != nil {
92
+ return nil , xerrors .Errorf ("get container user: scan /etc/passwd: %w" , err )
93
+ }
94
+ if foundLine == "" {
95
+ return nil , xerrors .Errorf ("get container user: no matching entry for %q found in /etc/passwd" , containerUser )
96
+ }
97
+
98
+ // Parse the output of /etc/passwd. It looks like this:
99
+ // postgres:x:999:999::/var/lib/postgresql:/bin/bash
100
+ passwdFields := strings .Split (foundLine , ":" )
101
+ if len (passwdFields ) < 7 {
102
+ return nil , xerrors .Errorf ("get container user: invalid line in /etc/passwd: %q" , foundLine )
103
+ }
104
+
105
+ // The fourth entry in /etc/passwd contains GECOS information, which is a
106
+ // comma-separated list of fields. The first field is the user's full name.
107
+ gecos := strings .Split (passwdFields [4 ], "," )
108
+ fullName := ""
109
+ if len (gecos ) > 1 {
110
+ fullName = gecos [0 ]
111
+ }
112
+
113
+ dei .user = & user.User {
114
+ Gid : passwdFields [3 ],
115
+ HomeDir : passwdFields [5 ],
116
+ Name : fullName ,
117
+ Uid : passwdFields [2 ],
118
+ Username : containerUser ,
119
+ }
120
+ dei .userShell = passwdFields [6 ]
121
+
122
+ // 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 )
137
+ }
138
+ if err := scanner .Err (); err != nil {
139
+ return nil , xerrors .Errorf ("get container environment: scan env output: %w" , err )
140
+ }
141
+
142
+ return & dei , nil
143
+ }
144
+
145
+ func (dei * ContainerEnvInfoer ) CurrentUser () (* user.User , error ) {
146
+ // Clone the user so that the caller can't modify it
147
+ u := & user.User {
148
+ Gid : dei .user .Gid ,
149
+ HomeDir : dei .user .HomeDir ,
150
+ Name : dei .user .Name ,
151
+ Uid : dei .user .Uid ,
152
+ Username : dei .user .Username ,
153
+ }
154
+ return u , nil
155
+ }
156
+
157
+ func (dei * ContainerEnvInfoer ) Environ () []string {
158
+ // Return a clone of the environment so that the caller can't modify it
159
+ return slices .Clone (dei .env )
160
+ }
161
+
162
+ func (dei * ContainerEnvInfoer ) UserHomeDir () (string , error ) {
163
+ return dei .user .HomeDir , nil
164
+ }
165
+
166
+ func (dei * ContainerEnvInfoer ) UserShell (string ) (string , error ) {
167
+ return dei .userShell , nil
168
+ }
169
+
170
+ func (dei * ContainerEnvInfoer ) ModifyCommand (cmd string , args ... string ) (string , []string ) {
171
+ return WrapDockerExecPTY (dei .container , dei .user .Username )(cmd , args ... )
172
+ }
173
+
174
+ // WrapFn is a function that wraps a command and its arguments with another command and arguments.
175
+ type WrapFn func (cmd string , args ... string ) (string , []string )
176
+
177
+ // WrapDockerExec returns a WrapFn that wraps the given command and arguments
178
+ // with a docker exec command that runs as the given user in the given container.
179
+ func WrapDockerExec (containerName , userName string ) WrapFn {
180
+ return func (cmd string , args ... string ) (string , []string ) {
181
+ dockerArgs := []string {"exec" , "--interactive" }
182
+ if userName != "" {
183
+ dockerArgs = append (dockerArgs , "--user" , userName )
184
+ }
185
+ dockerArgs = append (dockerArgs , containerName , cmd )
186
+ return "docker" , append (dockerArgs , args ... )
187
+ }
188
+ }
189
+
190
+ // WrapDockerExecPTY is similar to WrapDockerExec but also allocates a PTY.
191
+ func WrapDockerExecPTY (containerName , userName string ) WrapFn {
192
+ return func (cmd string , args ... string ) (string , []string ) {
193
+ dockerArgs := []string {"exec" , "--interactive" , "--tty" }
194
+ if userName != "" {
195
+ dockerArgs = append (dockerArgs , "--user" , userName )
196
+ }
197
+ dockerArgs = append (dockerArgs , containerName , cmd )
198
+ return "docker" , append (dockerArgs , args ... )
199
+ }
200
+ }
201
+
34
202
func (dcl * DockerCLILister ) List (ctx context.Context ) (codersdk.WorkspaceAgentListContainersResponse , error ) {
35
203
var stdoutBuf , stderrBuf bytes.Buffer
36
204
// List all container IDs, one per line, with no truncation
0 commit comments