1
1
package agent
2
2
3
3
import (
4
+ "bufio"
4
5
"bytes"
5
6
"context"
6
7
"encoding/json"
@@ -22,46 +23,68 @@ type dockerCLIContainerLister struct{}
22
23
23
24
var _ ContainerLister = & dockerCLIContainerLister {}
24
25
25
- func (* dockerCLIContainerLister ) List (ctx context.Context ) ([] codersdk.WorkspaceAgentContainer , error ) {
26
- var buf bytes.Buffer
26
+ func (* dockerCLIContainerLister ) List (ctx context.Context ) (codersdk.WorkspaceAgentListContainersResponse , error ) {
27
+ var stdoutBuf , stderrBuf bytes.Buffer
27
28
// List all container IDs, one per line, with no truncation
28
29
cmd := exec .CommandContext (ctx , "docker" , "ps" , "--all" , "--quiet" , "--no-trunc" )
29
- cmd .Stdout = & buf
30
+ cmd .Stdout = & stdoutBuf
31
+ cmd .Stderr = & stderrBuf
30
32
if err := cmd .Run (); err != nil {
31
- return nil , xerrors .Errorf ("run docker ps: %w" , err )
33
+ // TODO(Cian): detect specific errors:
34
+ // - docker not installed
35
+ // - docker not running
36
+ // - no permissions to talk to docker
37
+ return codersdk.WorkspaceAgentListContainersResponse {}, xerrors .Errorf ("run docker ps: %w: %q" , err , strings .TrimSpace (stderrBuf .String ()))
32
38
}
33
39
34
40
ids := make ([]string , 0 )
35
- for _ , line := range strings .Split (buf .String (), "\n " ) {
36
- tmp := strings .TrimSpace (line )
41
+ scanner := bufio .NewScanner (& stdoutBuf )
42
+ for scanner .Scan () {
43
+ tmp := strings .TrimSpace (scanner .Text ())
37
44
if tmp == "" {
38
45
continue
39
46
}
40
47
ids = append (ids , tmp )
41
48
}
49
+ if err := scanner .Err (); err != nil {
50
+ return codersdk.WorkspaceAgentListContainersResponse {}, xerrors .Errorf ("scan docker ps output: %w" , err )
51
+ }
42
52
43
53
// now we can get the detailed information for each container
44
54
// Run `docker inspect` on each container ID
45
- buf .Reset ()
46
- execArgs := []string {"inspect" }
47
- execArgs = append (execArgs , ids ... )
48
- cmd = exec .CommandContext (ctx , "docker" , execArgs ... )
49
- cmd .Stdout = & buf
55
+ stdoutBuf .Reset ()
56
+ stderrBuf .Reset ()
57
+ // nolint: gosec // We are not executing user input, these IDs come from
58
+ // `docker ps`.
59
+ cmd = exec .CommandContext (ctx , "docker" , append ([]string {"inspect" }, ids ... )... )
60
+ cmd .Stdout = & stdoutBuf
61
+ cmd .Stderr = & stderrBuf
50
62
if err := cmd .Run (); err != nil {
51
- return nil , xerrors .Errorf ("run docker inspect: %w" , err )
63
+ return codersdk. WorkspaceAgentListContainersResponse {} , xerrors .Errorf ("run docker inspect: %w: %s " , err , strings . TrimSpace ( stderrBuf . String ()) )
52
64
}
53
65
54
- ins := make ([]dockerInspect , 0 )
55
- if err := json .NewDecoder (& buf ).Decode (& ins ); err != nil {
56
- return nil , xerrors .Errorf ("decode docker inspect output: %w" , err )
66
+ // NOTE: There is an unavoidable potential race condition where a
67
+ // container is removed between `docker ps` and `docker inspect`.
68
+ // In this case, stderr will contain an error message but stdout
69
+ // will still contain valid JSON. We will just end up missing
70
+ // information about the removed container. We could potentially
71
+ // log this error, but I'm not sure it's worth it.
72
+ ins := make ([]dockerInspect , 0 , len (ids ))
73
+ if err := json .NewDecoder (& stdoutBuf ).Decode (& ins ); err != nil {
74
+ // However, if we just get invalid JSON, we should absolutely return an error.
75
+ return codersdk.WorkspaceAgentListContainersResponse {}, xerrors .Errorf ("decode docker inspect output: %w" , err )
57
76
}
58
77
59
- out := make ([]codersdk.WorkspaceAgentContainer , 0 )
60
- for _ , in := range ins {
61
- out = append (out , convertDockerInspect (in ))
78
+ res := codersdk.WorkspaceAgentListContainersResponse {
79
+ Containers : make ([]codersdk.WorkspaceAgentDevcontainer , len (ins )),
80
+ }
81
+ for idx , in := range ins {
82
+ out , warns := convertDockerInspect (in )
83
+ res .Warnings = append (res .Warnings , warns ... )
84
+ res .Containers [idx ] = out
62
85
}
63
86
64
- return out , nil
87
+ return res , nil
65
88
}
66
89
67
90
// To avoid a direct dependency on the Docker API, we use the docker CLI
@@ -104,44 +127,47 @@ func (dis dockerInspectState) String() string {
104
127
return sb .String ()
105
128
}
106
129
107
- func convertDockerInspect (in dockerInspect ) codersdk.WorkspaceAgentContainer {
108
- out := codersdk.WorkspaceAgentContainer {
130
+ func convertDockerInspect (in dockerInspect ) (codersdk.WorkspaceAgentDevcontainer , []string ) {
131
+ var warns []string
132
+ out := codersdk.WorkspaceAgentDevcontainer {
109
133
CreatedAt : in .Created ,
110
134
// Remove the leading slash from the container name
111
135
FriendlyName : strings .TrimPrefix (in .Name , "/" ),
112
136
ID : in .ID ,
113
137
Image : in .Config .Image ,
114
138
Labels : in .Config .Labels ,
115
- Ports : make ([]codersdk.WorkspaceAgentListeningPort , 0 ),
139
+ Ports : make ([]codersdk.WorkspaceAgentListeningPort , 0 , len ( in . Config . ExposedPorts ) ),
116
140
Running : in .State .Running ,
117
141
Status : in .State .String (),
118
- Volumes : make (map [string ]string ),
142
+ Volumes : make (map [string ]string , len ( in . Config . Volumes ) ),
119
143
}
120
144
121
145
// sort the keys for deterministic output
122
146
portKeys := maps .Keys (in .Config .ExposedPorts )
123
147
sort .Strings (portKeys )
124
148
for _ , p := range portKeys {
125
- port , network , err := convertDockerPort (p )
126
- if err != nil {
127
- // ignore invalid ports
128
- continue
149
+ if port , network , err := convertDockerPort (p ); err != nil {
150
+ warns = append (warns , err .Error ())
151
+ } else {
152
+ out .Ports = append (out .Ports , codersdk.WorkspaceAgentListeningPort {
153
+ Network : network ,
154
+ Port : port ,
155
+ })
129
156
}
130
- out .Ports = append (out .Ports , codersdk.WorkspaceAgentListeningPort {
131
- Network : network ,
132
- Port : port ,
133
- })
134
157
}
135
158
136
159
// sort the keys for deterministic output
137
160
volKeys := maps .Keys (in .Config .Volumes )
138
161
sort .Strings (volKeys )
139
162
for _ , k := range volKeys {
140
- v0 , v1 := convertDockerVolume (k )
141
- out .Volumes [v0 ] = v1
163
+ if v0 , v1 , err := convertDockerVolume (k ); err != nil {
164
+ warns = append (warns , err .Error ())
165
+ } else {
166
+ out .Volumes [v0 ] = v1
167
+ }
142
168
}
143
169
144
- return out
170
+ return out , warns
145
171
}
146
172
147
173
// convertDockerPort converts a Docker port string to a port number and network
@@ -151,21 +177,21 @@ func convertDockerInspect(in dockerInspect) codersdk.WorkspaceAgentContainer {
151
177
func convertDockerPort (in string ) (uint16 , string , error ) {
152
178
parts := strings .Split (in , "/" )
153
179
switch len (parts ) {
154
- case 0 :
155
- return 0 , "" , xerrors .Errorf ("invalid port format: %s" , in )
156
180
case 1 :
157
181
// assume it's a TCP port
158
182
p , err := strconv .Atoi (parts [0 ])
159
183
if err != nil {
160
184
return 0 , "" , xerrors .Errorf ("invalid port format: %s" , in )
161
185
}
162
186
return uint16 (p ), "tcp" , nil
163
- default :
187
+ case 2 :
164
188
p , err := strconv .Atoi (parts [0 ])
165
189
if err != nil {
166
190
return 0 , "" , xerrors .Errorf ("invalid port format: %s" , in )
167
191
}
168
192
return uint16 (p ), parts [1 ], nil
193
+ default :
194
+ return 0 , "" , xerrors .Errorf ("invalid port format: %s" , in )
169
195
}
170
196
}
171
197
@@ -175,14 +201,14 @@ func convertDockerPort(in string) (uint16, string, error) {
175
201
// example: "/host/path=/container/path" -> "/host/path", "/container/path"
176
202
//
177
203
// "/container/path" -> "/container/path", "/container/path"
178
- func convertDockerVolume (in string ) (hostPath , containerPath string ) {
204
+ func convertDockerVolume (in string ) (hostPath , containerPath string , err error ) {
179
205
parts := strings .Split (in , "=" )
180
206
switch len (parts ) {
181
- case 0 :
182
- return in , in
183
207
case 1 :
184
- return parts [0 ], parts [0 ]
208
+ return parts [0 ], parts [0 ], nil
209
+ case 2 :
210
+ return parts [0 ], parts [1 ], nil
185
211
default :
186
- return parts [ 0 ], parts [ 1 ]
212
+ return "" , "" , xerrors . Errorf ( "invalid volume format: %s" , in )
187
213
}
188
214
}
0 commit comments