10000 fix(agent/agentcontainers): improve testing of convertDockerInspect, return correct host port by johnstcn · Pull Request #16887 · coder/coder · GitHub
[go: up one dir, main page]

Skip to content

fix(agent/agentcontainers): improve testing of convertDockerInspect, return correct host port #16887

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Mar 18, 2025
Merged
Next Next commit
chore(agent/agentcontainers): refactor runDockerInspect and convertDo…
…ckerInspect
  • Loading branch information
johnstcn committed Mar 13, 2025
commit 80ac9a3a5295d10de1be6a5f42be8a63611c31e0
130 changes: 69 additions & 61 deletions agent/agentcontainers/containers_dockercli.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,23 +162,28 @@ func (dei *DockerEnvInfoer) ModifyCommand(cmd string, args ...string) (string, [
// devcontainerEnv is a helper function that inspects the container labels to
// find the required environment variables for running a command in the container.
func devcontainerEnv(ctx context.Context, execer agentexec.Execer, container string) ([]string, error) {
ins, stderr, err := runDockerInspect(ctx, execer, container)
stdout, stderr, err := runDockerInspect(ctx, execer, container)
if err != nil {
return nil, xerrors.Errorf("inspect container: %w: %q", err, stderr)
}

ins, _, err := convertDockerInspect(stdout)
if err != nil {
return nil, xerrors.Errorf("inspect container: %w", err)
}

if len(ins) != 1 {
return nil, xerrors.Errorf("inspect container: expected 1 container, got %d", len(ins))
}

in := ins[0]
if in.Config.Labels == nil {
if in.Labels == nil {
return nil, nil
}

// We want to look for the devcontainer metadata, which is in the
// value of the label `devcontainer.metadata`.
rawMeta, ok := in.Config.Labels["devcontainer.metadata"]
rawMeta, ok := in.Labels["devcontainer.metadata"]
if !ok {
return nil, nil
}
Expand Down Expand Up @@ -279,42 +284,36 @@ func (dcl *DockerCLILister) List(ctx context.Context) (codersdk.WorkspaceAgentLi
return codersdk.WorkspaceAgentListContainersResponse{}, xerrors.Errorf("run docker inspect: %w", err)
}

for _, in := range ins {
out, warns := convertDockerInspect(in)
res.Warnings = append(res.Warnings, warns...)
res.Containers = append(res.Containers, out)
}

if dockerPsStderr != "" {
res.Warnings = append(res.Warnings, dockerPsStderr)
}
if dockerInspectStderr != "" {
res.Warnings = append(res.Warnings, dockerInspectStderr)
}

outs, warns, err := convertDockerInspect(ins)
if err != nil {
return codersdk.WorkspaceAgentListContainersResponse{}, xerrors.Errorf("convert docker inspect output: %w", err)
}
res.Warnings = append(res.Warnings, warns...)
res.Containers = append(res.Containers, outs...)

return res, nil
}

// runDockerInspect is a helper function that runs `docker inspect` on the given
// container IDs and returns the parsed output.
// The stderr output is also returned for logging purposes.
func runDockerInspect(ctx context.Context, execer agentexec.Execer, ids ...string) ([]dockerInspect, string, error) {
func runDockerInspect(ctx context.Context, execer agentexec.Execer, ids ...string) (stdout, stderr string, err error) {
var stdoutBuf, stderrBuf bytes.Buffer
cmd := execer.CommandContext(ctx, "docker", append([]string{"inspect"}, ids...)...)
cmd.Stdout = &stdoutBuf
cmd.Stderr = &stderrBuf
err := cmd.Run()
stderr := strings.TrimSpace(stderrBuf.String())
err = cmd.Run()
stdout = strings.TrimSpace(stdoutBuf.String())
stderr = strings.TrimSpace(stderrBuf.String())
if err != nil {
return nil, stderr, err
return stdout, stderr, err
}

var ins []dockerInspect
if err := json.NewDecoder(&stdoutBuf).Decode(&ins); err != nil {
return nil, stderr, xerrors.Errorf("decode docker inspect output: %w", err)
}

return ins, stderr, nil
return stdoutBuf.String(), stderr, nil
}

// To avoid a direct dependency on the Docker API, we use the docker CLI
Expand Down Expand Up @@ -367,50 +366,59 @@ func (dis dockerInspectState) String() string {
return sb.String()
}

func convertDockerInspect(in dockerInspect) (codersdk.WorkspaceAgentDevcontainer, []string) {
func convertDockerInspect(raw string) ([]codersdk.WorkspaceAgentDevcontainer, []string, error) {
var warns []string
out := codersdk.WorkspaceAgentDevcontainer{
CreatedAt: in.Created,
// Remove the leading slash from the container name
FriendlyName: strings.TrimPrefix(in.Name, "/"),
ID: in.ID,
Image: in.Config.Image,
Labels: in.Config.Labels,
Ports: make([]codersdk.WorkspaceAgentListeningPort, 0),
Running: in.State.Running,
Status: in.State.String(),
Volumes: make(map[string]string, len(in.Mounts)),
}

if in.HostConfig.PortBindings == nil {
in.HostConfig.PortBindings = make(map[string]any)
}
portKeys := maps.Keys(in.HostConfig.PortBindings)
// Sort the ports for deterministic output.
sort.Strings(portKeys)
for _, p := range portKeys {
if port, network, err := convertDockerPort(p); err != nil {
warns = append(warns, err.Error())
} else {
out.Ports = append(out.Ports, codersdk.WorkspaceAgentListeningPort{
Network: network,
Port: port,
})
}
var ins []dockerInspect
if err := json.NewDecoder(strings.NewReader(raw)).Decode(&ins); err != nil {
return nil, nil, xerrors.Errorf("decode docker inspect output: %w", err)
}
outs := make([]codersdk.WorkspaceAgentDevcontainer, 0, len(ins))

if in.Mounts == nil {
in.Mounts = []dockerInspectMount{}
}
// Sort the mounts for deterministic output.
sort.Slice(in.Mounts, func(i, j int) bool {
return in.Mounts[i].Source < in.Mounts[j].Source
})
for _, k := range in.Mounts {
out.Volumes[k.Source] = k.Destination
for _, in := range ins {
out := codersdk.WorkspaceAgentDevcontainer{
CreatedAt: in.Created,
// Remove the leading slash from the container name
FriendlyName: strings.TrimPrefix(in.Name, "/"),
ID: in.ID,
Image: in.Config.Image,
Labels: in.Config.Labels,
Ports: make([]codersdk.WorkspaceAgentListeningPort, 0),
Running: in.State.Running,
Status: in.State.String(),
Volumes: make(map[string]string, len(in.Mounts)),
}

if in.HostConfig.PortBindings == nil {
in.HostConfig.PortBindings = make(map[string]any)
}
portKeys := maps.Keys(in.HostConfig.PortBindings)
// Sort the ports for deterministic output.
sort.Strings(portKeys)
for _, p := range portKeys {
if port, network, err := convertDockerPort(p); err != nil {
warns = append(warns, err.Error())
} else {
out.Ports = append(out.Ports, codersdk.WorkspaceAgentListeningPort{
Network: network,
Port: port,
})
}
}

if in.Mounts == nil {
in.Mounts = []dockerInspectMount{}
}
// Sort the mounts for deterministic output.
sort.Slice(in.Mounts, func(i, j int) bool {
return in.Mounts[i].Source < in.Mounts[j].Source
})
for _, k := range in.Mounts {
out.Volumes[k.Source] = k.Destination
}
outs = append(outs, out)
}

return out, warns
return outs, warns, nil
}

// convertDockerPort converts a Docker port string to a port number and network
Expand Down
0