Skip to content

Commit 8b081ac

Browse files
committed
correctly detect ports and volumes
1 parent 1dbcf0f commit 8b081ac

File tree

2 files changed

+75
-46
lines changed

2 files changed

+75
-46
lines changed

agent/agentcontainers/containers_dockercli.go

Lines changed: 46 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ func (dcl *DockerCLILister) List(ctx context.Context) (codersdk.WorkspaceAgentLi
5858
return codersdk.WorkspaceAgentListContainersResponse{}, xerrors.Errorf("scan docker ps output: %w", err)
5959
}
6060

61+
dockerPsStderr := strings.TrimSpace(stderrBuf.String())
62+
6163
// now we can get the detailed information for each container
6264
// Run `docker inspect` on each container ID
6365
stdoutBuf.Reset()
@@ -71,6 +73,8 @@ func (dcl *DockerCLILister) List(ctx context.Context) (codersdk.WorkspaceAgentLi
7173
return codersdk.WorkspaceAgentListContainersResponse{}, xerrors.Errorf("run docker inspect: %w: %s", err, strings.TrimSpace(stderrBuf.String()))
7274
}
7375

76+
dockerInspectStderr := strings.TrimSpace(stderrBuf.String())
77+
7478
// NOTE: There is an unavoidable potential race condition where a
7579
// container is removed between `docker ps` and `docker inspect`.
7680
// In this case, stderr will contain an error message but stdout
@@ -92,24 +96,41 @@ func (dcl *DockerCLILister) List(ctx context.Context) (codersdk.WorkspaceAgentLi
9296
res.Containers[idx] = out
9397
}
9498

99+
if dockerPsStderr != "" {
100+
res.Warnings = append(res.Warnings, dockerPsStderr)
101+
}
102+
if dockerInspectStderr != "" {
103+
res.Warnings = append(res.Warnings, dockerInspectStderr)
104+
}
105+
95106
return res, nil
96107
}
97108

98109
// To avoid a direct dependency on the Docker API, we use the docker CLI
99110
// to fetch information about containers.
100111
type dockerInspect struct {
101-
ID string `json:"Id"`
102-
Created time.Time `json:"Created"`
103-
Name string `json:"Name"`
104-
Config dockerInspectConfig `json:"Config"`
105-
State dockerInspectState `json:"State"`
112+
ID string `json:"Id"`
113+
Created time.Time `json:"Created"`
114+
Config dockerInspectConfig `json:"Config"`
115+
HostConfig dockerInspectHostConfig `json:"HostConfig"`
116+
Name string `json:"Name"`
117+
Mounts []dockerInspectMount `json:"Mounts"`
118+
State dockerInspectState `json:"State"`
106119
}
107120

108121
type dockerInspectConfig struct {
109-
ExposedPorts map[string]struct{} `json:"ExposedPorts"`
110-
Image string `json:"Image"`
111-
Labels map[string]string `json:"Labels"`
112-
Volumes map[string]struct{} `json:"Volumes"`
122+
Image string `json:"Image"`
123+
Labels map[string]string `json:"Labels"`
124+
}
125+
126+
type dockerInspectHostConfig struct {
127+
PortBindings map[string]any `json:"PortBindings"`
128+
}
129+
130+
type dockerInspectMount struct {
131+
Source string `json:"Source"`
132+
Destination string `json:"Destination"`
133+
Type string `json:"Type"`
113134
}
114135

115136
type dockerInspectState struct {
@@ -144,14 +165,17 @@ func convertDockerInspect(in dockerInspect) (codersdk.WorkspaceAgentDevcontainer
144165
ID: in.ID,
145166
Image: in.Config.Image,
146167
Labels: in.Config.Labels,
147-
Ports: make([]codersdk.WorkspaceAgentListeningPort, 0, len(in.Config.ExposedPorts)),
168+
Ports: make([]codersdk.WorkspaceAgentListeningPort, 0),
148169
Running: in.State.Running,
149170
Status: in.State.String(),
150-
Volumes: make(map[string]string, len(in.Config.Volumes)),
171+
Volumes: make(map[string]string, len(in.Mounts)),
151172
}
152173

153-
// sort the keys for deterministic output
154-
portKeys := maps.Keys(in.Config.ExposedPorts)
174+
if in.HostConfig.PortBindings == nil {
175+
in.HostConfig.PortBindings = make(map[string]any)
176+
}
177+
portKeys := maps.Keys(in.HostConfig.PortBindings)
178+
// Sort the ports for deterministic output.
155179
sort.Strings(portKeys)
156180
for _, p := range portKeys {
157181
if port, network, err := convertDockerPort(p); err != nil {
@@ -164,15 +188,15 @@ func convertDockerInspect(in dockerInspect) (codersdk.WorkspaceAgentDevcontainer
164188
}
165189
}
166190

167-
// sort the keys for deterministic output
168-
volKeys := maps.Keys(in.Config.Volumes)
169-
sort.Strings(volKeys)
170-
for _, k := range volKeys {
171-
if v0, v1, err := convertDockerVolume(k); err != nil {
172-
warns = append(warns, err.Error())
173-
} else {
174-
out.Volumes[v0] = v1
175-
}
191+
if in.Mounts == nil {
192+
in.Mounts = []dockerInspectMount{}
193+
}
194+
// Sort the mounts for deterministic output.
195+
sort.Slice(in.Mounts, func(i, j int) bool {
196+
return in.Mounts[i].Source < in.Mounts[j].Source
197+
})
198+
for _, k := range in.Mounts {
199+
out.Volumes[k.Source] = k.Destination
176200
}
177201

178202
return out, warns
@@ -202,21 +226,3 @@ func convertDockerPort(in string) (uint16, string, error) {
202226
return 0, "", xerrors.Errorf("invalid port format: %s", in)
203227
}
204228
}
205-
206-
// convertDockerVolume converts a Docker volume string to a host path and
207-
// container path. If the host path is not specified, the container path is used
208-
// as the host path.
209-
// example: "/host/path=/container/path" -> "/host/path", "/container/path"
210-
//
211-
// "/container/path" -> "/container/path", "/container/path"
212-
func convertDockerVolume(in string) (hostPath, containerPath string, err error) {
213-
parts := strings.Split(in, "=")
214-
switch len(parts) {
215-
case 1:
216-
return parts[0], parts[0], nil
217-
case 2:
218-
return parts[0], parts[1], nil
219-
default:
220-
return "", "", xerrors.Errorf("invalid volume format: %s", in)
221-
}
222-
}

agent/agentcontainers/containers_internal_test.go

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package agentcontainers
22

33
import (
4+
"fmt"
45
"os/exec"
56
"runtime"
7+
"strconv"
68
"strings"
79
"testing"
810
"time"
@@ -39,18 +41,34 @@ func TestDockerCLIContainerLister(t *testing.T) {
3941
pool, err := dockertest.NewPool("")
4042
require.NoError(t, err, "Could not connect to docker")
4143
testLabelValue := uuid.New().String()
44+
// Create a temporary directory to validate that we surface mounts correctly.
45+
testTempDir := t.TempDir()
46+
// Pick a random port to expose for testing port bindings.
47+
testRandPort := testutil.RandomPortNoListen(t)
4248
ct, err := pool.RunWithOptions(&dockertest.RunOptions{
43-
Repository: "busybox",
44-
Tag: "latest",
45-
Cmd: []string{"sleep", "infnity"},
46-
Labels: map[string]string{"com.coder.test": testLabelValue},
49+
Repository: "busybox",
50+
Tag: "latest",
51+
Cmd: []string{"sleep", "infnity"},
52+
Labels: map[string]string{"com.coder.test": testLabelValue},
53+
Mounts: []string{testTempDir + ":" + testTempDir},
54+
ExposedPorts: []string{fmt.Sprintf("%d/tcp", testRandPort)},
55+
PortBindings: map[docker.Port][]docker.PortBinding{
56+
docker.Port(fmt.Sprintf("%d/tcp", testRandPort)): {
57+
{
58+
HostIP: "0.0.0.0",
59+
HostPort: strconv.FormatInt(int64(testRandPort), 10),
60+
},
61+
},
62+
},
4763
}, func(config *docker.HostConfig) {
4864
config.AutoRemove = true
4965
config.RestartPolicy = docker.RestartPolicy{Name: "no"}
5066
})
5167
require.NoError(t, err, "Could not start test docker container")
68+
t.Logf("Created container %q", ct.Container.Name)
5269
t.Cleanup(func() {
5370
assert.NoError(t, pool.Purge(ct), "Could not purge resource %q", ct.Container.Name)
71+
t.Logf("Purged container %q", ct.Container.Name)
5472
})
5573

5674
dcl := NewDocker(agentexec.DefaultExecer)
@@ -70,8 +88,13 @@ func TestDockerCLIContainerLister(t *testing.T) {
7088
assert.Equal(t, ct.Container.Config.Labels, foundContainer.Labels)
7189
assert.True(t, foundContainer.Running)
7290
assert.Equal(t, "running", foundContainer.Status)
73-
assert.Len(t, foundContainer.Ports, 0)
74-
assert.Len(t, foundContainer.Volumes, 0)
91+
if assert.Len(t, foundContainer.Ports, 1) {
92+
assert.Equal(t, testRandPort, foundContainer.Ports[0].Port)
93+
assert.Equal(t, "tcp", foundContainer.Ports[0].Network)
94+
}
95+
if assert.Len(t, foundContainer.Volumes, 1) {
96+
assert.Equal(t, testTempDir, foundContainer.Volumes[testTempDir])
97+
}
7598
break
7699
}
77100
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy