From 80ac9a3a5295d10de1be6a5f42be8a63611c31e0 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Tue, 11 Mar 2025 19:59:07 +0000 Subject: [PATCH 01/12] chore(agent/agentcontainers): refactor runDockerInspect and convertDockerInspect --- agent/agentcontainers/containers_dockercli.go | 130 ++++++++++-------- 1 file changed, 69 insertions(+), 61 deletions(-) diff --git a/agent/agentcontainers/containers_dockercli.go b/agent/agentcontainers/containers_dockercli.go index 4d4bd68ee0f10..1f1e0e041b8d2 100644 --- a/agent/agentcontainers/containers_dockercli.go +++ b/agent/agentcontainers/containers_dockercli.go @@ -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 } @@ -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 @@ -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 From 0ecceb01e2a2743ee02f322750c71159cf94975a Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Tue, 11 Mar 2025 20:32:13 +0000 Subject: [PATCH 02/12] chore(agent/agentcontainers): add dedicated test for convertDockerInspect --- .../containers_internal_test.go | 212 ++++++++++++++++ .../testdata/container_binds.json | 221 +++++++++++++++++ .../testdata/container_differentport.json | 222 +++++++++++++++++ .../testdata/container_labels.json | 204 ++++++++++++++++ .../testdata/container_sameport.json | 222 +++++++++++++++++ .../testdata/container_simple.json | 201 +++++++++++++++ .../testdata/container_volume.json | 214 ++++++++++++++++ .../testdata/devcontainer_appport.json | 230 ++++++++++++++++++ .../testdata/devcontainer_forwardport.json | 209 ++++++++++++++++ .../testdata/devcontainer_simple.json | 209 ++++++++++++++++ 10 files changed, 2144 insertions(+) create mode 100644 agent/agentcontainers/testdata/container_binds.json create mode 100644 agent/agentcontainers/testdata/container_differentport.json create mode 100644 agent/agentcontainers/testdata/container_labels.json create mode 100644 agent/agentcontainers/testdata/container_sameport.json create mode 100644 agent/agentcontainers/testdata/container_simple.json create mode 100644 agent/agentcontainers/testdata/container_volume.json create mode 100644 agent/agentcontainers/testdata/devcontainer_appport.json create mode 100644 agent/agentcontainers/testdata/devcontainer_forwardport.json create mode 100644 agent/agentcontainers/testdata/devcontainer_simple.json diff --git a/agent/agentcontainers/containers_internal_test.go b/agent/agentcontainers/containers_internal_test.go index fc3928229f2f5..ac1bc69ae3b01 100644 --- a/agent/agentcontainers/containers_internal_test.go +++ b/agent/agentcontainers/containers_internal_test.go @@ -3,6 +3,7 @@ package agentcontainers import ( "fmt" "os" + "path/filepath" "slices" "strconv" "strings" @@ -11,6 +12,7 @@ import ( "go.uber.org/mock/gomock" + "github.com/google/go-cmp/cmp" "github.com/google/uuid" "github.com/ory/dockertest/v3" "github.com/ory/dockertest/v3/docker" @@ -412,6 +414,216 @@ func TestConvertDockerVolume(t *testing.T) { } } +// TestConvertDockerInspect tests the convertDockerInspect function using +// fixtures from ./testdata. +func TestConvertDockerInspect(t *testing.T) { + t.Parallel() + + for _, tt := range []struct { + name string + expect []codersdk.WorkspaceAgentDevcontainer + expectWarns []string + expectError string + }{ + { + name: "container_simple", + expect: []codersdk.WorkspaceAgentDevcontainer{ + { + CreatedAt: time.Date(2025, 3, 11, 17, 55, 58, 91280203, time.UTC), + ID: "6b539b8c60f5230b8b0fde2502cd2332d31c0d526a3e6eb6eef1cc39439b3286", + FriendlyName: "eloquent_kowalevski", + Image: "debian:bookworm", + Labels: map[string]string{}, + Running: true, + Status: "running", + Ports: []codersdk.WorkspaceAgentListeningPort{}, + Volumes: map[string]string{}, + }, + }, + }, + { + name: "container_labels", + expect: []codersdk.WorkspaceAgentDevcontainer{ + { + CreatedAt: time.Date(2025, 3, 11, 20, 3, 28, 71706536, time.UTC), + ID: "bd8818e670230fc6f36145b21cf8d6d35580355662aa4d9fe5ae1b188a4c905f", + FriendlyName: "fervent_bardeen", + Image: "debian:bookworm", + Labels: map[string]string{"baz": "zap", "foo": "bar"}, + Running: true, + Status: "running", + Ports: []codersdk.WorkspaceAgentListeningPort{}, + Volumes: map[string]string{}, + }, + }, + }, + { + name: "container_binds", + expect: []codersdk.WorkspaceAgentDevcontainer{ + { + CreatedAt: time.Date(2025, 3, 11, 17, 58, 43, 522505027, time.UTC), + ID: "fdc75ebefdc0243c0fce959e7685931691ac7aede278664a0e2c23af8a1e8d6a", + FriendlyName: "silly_beaver", + Image: "debian:bookworm", + Labels: map[string]string{}, + Running: true, + Status: "running", + Ports: []codersdk.WorkspaceAgentListeningPort{}, + Volumes: map[string]string{ + "/tmp/test/a": "/var/coder/a", + "/tmp/test/b": "/var/coder/b", + }, + }, + }, + }, + { + name: "container_sameport", + expect: []codersdk.WorkspaceAgentDevcontainer{ + { + CreatedAt: time.Date(2025, 3, 11, 17, 56, 34, 842164541, time.UTC), + ID: "4eac5ce199d27b2329d0ff0ce1a6fc595612ced48eba3669aadb6c57ebef3fa2", + FriendlyName: "modest_varahamihira", + Image: "debian:bookworm", + Labels: map[string]string{}, + Running: true, + Status: "running", + Ports: []codersdk.WorkspaceAgentListeningPort{ + { + Network: "tcp", + Port: 12345, + }, + }, + Volumes: map[string]string{}, + }, + }, + }, + { + name: "container_differentport", + expect: []codersdk.WorkspaceAgentDevcontainer{ + { + CreatedAt: time.Date(2025, 3, 11, 17, 57, 8, 862545133, time.UTC), + ID: "3090de8b72b1224758a94a11b827c82ba2b09c45524f1263dc4a2d83e19625ea", + FriendlyName: "boring_ellis", + Image: "debian:bookworm", + Labels: map[string]string{}, + Running: true, + Status: "running", + Ports: []codersdk.WorkspaceAgentListeningPort{ + { + Network: "tcp", + Port: 23456, + }, + }, + Volumes: map[string]string{}, + }, + }, + }, + { + name: "container_volume", + expect: []codersdk.WorkspaceAgentDevcontainer{ + { + CreatedAt: time.Date(2025, 3, 11, 17, 59, 42, 39484134, time.UTC), + ID: "b3688d98c007f53402a55e46d803f2f3ba9181d8e3f71a2eb19b392cf0377b4e", + FriendlyName: "upbeat_carver", + Image: "debian:bookworm", + Labels: map[string]string{}, + Running: true, + Status: "running", + Ports: []codersdk.WorkspaceAgentListeningPort{}, + Volumes: map[string]string{ + "/var/lib/docker/volumes/testvol/_data": "/testvol", + }, + }, + }, + }, + { + name: "devcontainer_simple", + expect: []codersdk.WorkspaceAgentDevcontainer{ + { + CreatedAt: time.Date(2025, 3, 11, 17, 1, 5, 751972661, time.UTC), + ID: "0b2a9fcf5727d9562943ce47d445019f4520e37a2aa7c6d9346d01af4f4f9aed", + FriendlyName: "optimistic_hopper", + Image: "debian:bookworm", + Labels: map[string]string{ + "devcontainer.config_file": "/home/coder/src/coder/coder/agent/agentcontainers/testdata/devcontainer_simple.json", + "devcontainer.metadata": "[]", + }, + Running: true, + Status: "running", + Ports: []codersdk.WorkspaceAgentListeningPort{}, + Volumes: map[string]string{}, + }, + }, + }, + { + name: "devcontainer_forwardport", + expect: []codersdk.WorkspaceAgentDevcontainer{ + { + CreatedAt: time.Date(2025, 3, 11, 17, 3, 55, 22053072, time.UTC), + ID: "4a16af2293fb75dc827a6949a3905dd57ea28cc008823218ce24fab1cb66c067", + FriendlyName: "serene_khayyam", + Image: "debian:bookworm", + Labels: map[string]string{ + "devcontainer.config_file": "/home/coder/src/coder/coder/agent/agentcontainers/testdata/devcontainer_forwardport.json", + "devcontainer.metadata": "[]", + }, + Running: true, + Status: "running", + Ports: []codersdk.WorkspaceAgentListeningPort{}, + Volumes: map[string]string{}, + }, + }, + }, + { + name: "devcontainer_appport", + expect: []codersdk.WorkspaceAgentDevcontainer{ + { + CreatedAt: time.Date(2025, 3, 11, 17, 2, 42, 613747761, time.UTC), + ID: "52d23691f4b954d083f117358ea763e20f69af584e1c08f479c5752629ee0be3", + FriendlyName: "suspicious_margulis", + Image: "debian:bookworm", + Labels: map[string]string{ + "devcontainer.config_file": "/home/coder/src/coder/coder/agent/agentcontainers/testdata/devcontainer_appport.json", + "devcontainer.metadata": "[]", + }, + Running: true, + Status: "running", + Ports: []codersdk.WorkspaceAgentListeningPort{ + { + Network: "tcp", + Port: 8080, + }, + }, + Volumes: map[string]string{}, + }, + }, + }, + } { + // nolint:paralleltest // variable recapture no longer required + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + bs, err := os.ReadFile(filepath.Join("testdata", tt.name+".json")) + require.NoError(t, err, "failed to read testdata file") + actual, warns, err := convertDockerInspect(string(bs)) + if len(tt.expectWarns) > 0 { + assert.Len(t, warns, len(tt.expectWarns), "expected warnings") + for _, warn := range tt.expectWarns { + assert.Contains(t, warns, warn) + } + } + if tt.expectError != "" { + assert.Empty(t, actual, "expected no data") + assert.ErrorContains(t, err, tt.expectError) + return + } + require.NoError(t, err, "expected no error") + if diff := cmp.Diff(tt.expect, actual); diff != "" { + t.Errorf("unexpected diff (-want +got):\n%s", diff) + } + }) + } +} + // TestDockerEnvInfoer tests the ability of EnvInfo to extract information from // running containers. Containers are deleted after the test is complete. // As this test creates containers, it is skipped by default. diff --git a/agent/agentcontainers/testdata/container_binds.json b/agent/agentcontainers/testdata/container_binds.json new file mode 100644 index 0000000000000..69dc7ea321466 --- /dev/null +++ b/agent/agentcontainers/testdata/container_binds.json @@ -0,0 +1,221 @@ +[ + { + "Id": "fdc75ebefdc0243c0fce959e7685931691ac7aede278664a0e2c23af8a1e8d6a", + "Created": "2025-03-11T17:58:43.522505027Z", + "Path": "sleep", + "Args": [ + "infinity" + ], + "State": { + "Status": "running", + "Running": true, + "Paused": false, + "Restarting": false, + "OOMKilled": false, + "Dead": false, + "Pid": 644296, + "ExitCode": 0, + "Error": "", + "StartedAt": "2025-03-11T17:58:43.569966691Z", + "FinishedAt": "0001-01-01T00:00:00Z" + }, + "Image": "sha256:d4ccddb816ba27eaae22ef3d56175d53f47998e2acb99df1ae0e5b426b28a076", + "ResolvConfPath": "/var/lib/docker/containers/fdc75ebefdc0243c0fce959e7685931691ac7aede278664a0e2c23af8a1e8d6a/resolv.conf", + "HostnamePath": "/var/lib/docker/containers/fdc75ebefdc0243c0fce959e7685931691ac7aede278664a0e2c23af8a1e8d6a/hostname", + "HostsPath": "/var/lib/docker/containers/fdc75ebefdc0243c0fce959e7685931691ac7aede278664a0e2c23af8a1e8d6a/hosts", + "LogPath": "/var/lib/docker/containers/fdc75ebefdc0243c0fce959e7685931691ac7aede278664a0e2c23af8a1e8d6a/fdc75ebefdc0243c0fce959e7685931691ac7aede278664a0e2c23af8a1e8d6a-json.log", + "Name": "/silly_beaver", + "RestartCount": 0, + "Driver": "overlay2", + "Platform": "linux", + "MountLabel": "", + "ProcessLabel": "", + "AppArmorProfile": "", + "ExecIDs": null, + "HostConfig": { + "Binds": [ + "/tmp/test/a:/var/coder/a:ro", + "/tmp/test/b:/var/coder/b" + ], + "ContainerIDFile": "", + "LogConfig": { + "Type": "json-file", + "Config": {} + }, + "NetworkMode": "bridge", + "PortBindings": {}, + "RestartPolicy": { + "Name": "no", + "MaximumRetryCount": 0 + }, + "AutoRemove": false, + "VolumeDriver": "", + "VolumesFrom": null, + "ConsoleSize": [ + 108, + 176 + ], + "CapAdd": null, + "CapDrop": null, + "CgroupnsMode": "private", + "Dns": [], + "DnsOptions": [], + "DnsSearch": [], + "ExtraHosts": null, + "GroupAdd": null, + "IpcMode": "private", + "Cgroup": "", + "Links": null, + "OomScoreAdj": 10, + "PidMode": "", + "Privileged": false, + "PublishAllPorts": false, + "ReadonlyRootfs": false, + "SecurityOpt": null, + "UTSMode": "", + "UsernsMode": "", + "ShmSize": 67108864, + "Runtime": "runc", + "Isolation": "", + "CpuShares": 0, + "Memory": 0, + "NanoCpus": 0, + "CgroupParent": "", + "BlkioWeight": 0, + "BlkioWeightDevice": [], + "BlkioDeviceReadBps": [], + "BlkioDeviceWriteBps": [], + "BlkioDeviceReadIOps": [], + "BlkioDeviceWriteIOps": [], + "CpuPeriod": 0, + "CpuQuota": 0, + "CpuRealtimePeriod": 0, + "CpuRealtimeRuntime": 0, + "CpusetCpus": "", + "CpusetMems": "", + "Devices": [], + "DeviceCgroupRules": null, + "DeviceRequests": null, + "MemoryReservation": 0, + "MemorySwap": 0, + "MemorySwappiness": null, + "OomKillDisable": null, + "PidsLimit": null, + "Ulimits": [], + "CpuCount": 0, + "CpuPercent": 0, + "IOMaximumIOps": 0, + "IOMaximumBandwidth": 0, + "MaskedPaths": [ + "/proc/asound", + "/proc/acpi", + "/proc/kcore", + "/proc/keys", + "/proc/latency_stats", + "/proc/timer_list", + "/proc/timer_stats", + "/proc/sched_debug", + "/proc/scsi", + "/sys/firmware", + "/sys/devices/virtual/powercap" + ], + "ReadonlyPaths": [ + "/proc/bus", + "/proc/fs", + "/proc/irq", + "/proc/sys", + "/proc/sysrq-trigger" + ] + }, + "GraphDriver": { + "Data": { + "ID": "fdc75ebefdc0243c0fce959e7685931691ac7aede278664a0e2c23af8a1e8d6a", + "LowerDir": "/var/lib/docker/overlay2/c1519be93f8e138757310f6ed8c3946a524cdae2580ad8579913d19c3fe9ffd2-init/diff:/var/lib/docker/overlay2/4b4c37dfbdc0dc01b68d4fb1ddb86109398a2d73555439b874dbd23b87cd5c4b/diff", + "MergedDir": "/var/lib/docker/overlay2/c1519be93f8e138757310f6ed8c3946a524cdae2580ad8579913d19c3fe9ffd2/merged", + "UpperDir": "/var/lib/docker/overlay2/c1519be93f8e138757310f6ed8c3946a524cdae2580ad8579913d19c3fe9ffd2/diff", + "WorkDir": "/var/lib/docker/overlay2/c1519be93f8e138757310f6ed8c3946a524cdae2580ad8579913d19c3fe9ffd2/work" + }, + "Name": "overlay2" + }, + "Mounts": [ + { + "Type": "bind", + "Source": "/tmp/test/a", + "Destination": "/var/coder/a", + "Mode": "ro", + "RW": false, + "Propagation": "rprivate" + }, + { + "Type": "bind", + "Source": "/tmp/test/b", + "Destination": "/var/coder/b", + "Mode": "", + "RW": true, + "Propagation": "rprivate" + } + ], + "Config": { + "Hostname": "fdc75ebefdc0", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "Cmd": [ + "sleep", + "infinity" + ], + "Image": "debian:bookworm", + "Volumes": null, + "WorkingDir": "", + "Entrypoint": [], + "OnBuild": null, + "Labels": {} + }, + "NetworkSettings": { + "Bridge": "", + "SandboxID": "46f98b32002740b63709e3ebf87c78efe652adfaa8753b85d79b814f26d88107", + "SandboxKey": "/var/run/docker/netns/46f98b320027", + "Ports": {}, + "HairpinMode": false, + "LinkLocalIPv6Address": "", + "LinkLocalIPv6PrefixLen": 0, + "SecondaryIPAddresses": null, + "SecondaryIPv6Addresses": null, + "EndpointID": "356e429f15e354dd23250c7a3516aecf1a2afe9d58ea1dc2e97e33a75ac346a8", + "Gateway": "172.17.0.1", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "IPAddress": "172.17.0.2", + "IPPrefixLen": 16, + "IPv6Gateway": "", + "MacAddress": "22:2c:26:d9:da:83", + "Networks": { + "bridge": { + "IPAMConfig": null, + "Links": null, + "Aliases": null, + "MacAddress": "22:2c:26:d9:da:83", + "DriverOpts": null, + "GwPriority": 0, + "NetworkID": "c4dd768ab4945e41ad23fe3907c960dac811141592a861cc40038df7086a1ce1", + "EndpointID": "356e429f15e354dd23250c7a3516aecf1a2afe9d58ea1dc2e97e33a75ac346a8", + "Gateway": "172.17.0.1", + "IPAddress": "172.17.0.2", + "IPPrefixLen": 16, + "IPv6Gateway": "", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "DNSNames": null + } + } + } + } +] diff --git a/agent/agentcontainers/testdata/container_differentport.json b/agent/agentcontainers/testdata/container_differentport.json new file mode 100644 index 0000000000000..7c54d6f942be9 --- /dev/null +++ b/agent/agentcontainers/testdata/container_differentport.json @@ -0,0 +1,222 @@ +[ + { + "Id": "3090de8b72b1224758a94a11b827c82ba2b09c45524f1263dc4a2d83e19625ea", + "Created": "2025-03-11T17:57:08.862545133Z", + "Path": "sleep", + "Args": [ + "infinity" + ], + "State": { + "Status": "running", + "Running": true, + "Paused": false, + "Restarting": false, + "OOMKilled": false, + "Dead": false, + "Pid": 640137, + "ExitCode": 0, + "Error": "", + "StartedAt": "2025-03-11T17:57:08.909898821Z", + "FinishedAt": "0001-01-01T00:00:00Z" + }, + "Image": "sha256:d4ccddb816ba27eaae22ef3d56175d53f47998e2acb99df1ae0e5b426b28a076", + "ResolvConfPath": "/var/lib/docker/containers/3090de8b72b1224758a94a11b827c82ba2b09c45524f1263dc4a2d83e19625ea/resolv.conf", + "HostnamePath": "/var/lib/docker/containers/3090de8b72b1224758a94a11b827c82ba2b09c45524f1263dc4a2d83e19625ea/hostname", + "HostsPath": "/var/lib/docker/containers/3090de8b72b1224758a94a11b827c82ba2b09c45524f1263dc4a2d83e19625ea/hosts", + "LogPath": "/var/lib/docker/containers/3090de8b72b1224758a94a11b827c82ba2b09c45524f1263dc4a2d83e19625ea/3090de8b72b1224758a94a11b827c82ba2b09c45524f1263dc4a2d83e19625ea-json.log", + "Name": "/boring_ellis", + "RestartCount": 0, + "Driver": "overlay2", + "Platform": "linux", + "MountLabel": "", + "ProcessLabel": "", + "AppArmorProfile": "", + "ExecIDs": null, + "HostConfig": { + "Binds": null, + "ContainerIDFile": "", + "LogConfig": { + "Type": "json-file", + "Config": {} + }, + "NetworkMode": "bridge", + "PortBindings": { + "23456/tcp": [ + { + "HostIp": "", + "HostPort": "12345" + } + ] + }, + "RestartPolicy": { + "Name": "no", + "MaximumRetryCount": 0 + }, + "AutoRemove": false, + "VolumeDriver": "", + "VolumesFrom": null, + "ConsoleSize": [ + 108, + 176 + ], + "CapAdd": null, + "CapDrop": null, + "CgroupnsMode": "private", + "Dns": [], + "DnsOptions": [], + "DnsSearch": [], + "ExtraHosts": null, + "GroupAdd": null, + "IpcMode": "private", + "Cgroup": "", + "Links": null, + "OomScoreAdj": 10, + "PidMode": "", + "Privileged": false, + "PublishAllPorts": false, + "ReadonlyRootfs": false, + "SecurityOpt": null, + "UTSMode": "", + "UsernsMode": "", + "ShmSize": 67108864, + "Runtime": "runc", + "Isolation": "", + "CpuShares": 0, + "Memory": 0, + "NanoCpus": 0, + "CgroupParent": "", + "BlkioWeight": 0, + "BlkioWeightDevice": [], + "BlkioDeviceReadBps": [], + "BlkioDeviceWriteBps": [], + "BlkioDeviceReadIOps": [], + "BlkioDeviceWriteIOps": [], + "CpuPeriod": 0, + "CpuQuota": 0, + "CpuRealtimePeriod": 0, + "CpuRealtimeRuntime": 0, + "CpusetCpus": "", + "CpusetMems": "", + "Devices": [], + "DeviceCgroupRules": null, + "DeviceRequests": null, + "MemoryReservation": 0, + "MemorySwap": 0, + "MemorySwappiness": null, + "OomKillDisable": null, + "PidsLimit": null, + "Ulimits": [], + "CpuCount": 0, + "CpuPercent": 0, + "IOMaximumIOps": 0, + "IOMaximumBandwidth": 0, + "MaskedPaths": [ + "/proc/asound", + "/proc/acpi", + "/proc/kcore", + "/proc/keys", + "/proc/latency_stats", + "/proc/timer_list", + "/proc/timer_stats", + "/proc/sched_debug", + "/proc/scsi", + "/sys/firmware", + "/sys/devices/virtual/powercap" + ], + "ReadonlyPaths": [ + "/proc/bus", + "/proc/fs", + "/proc/irq", + "/proc/sys", + "/proc/sysrq-trigger" + ] + }, + "GraphDriver": { + "Data": { + "ID": "3090de8b72b1224758a94a11b827c82ba2b09c45524f1263dc4a2d83e19625ea", + "LowerDir": "/var/lib/docker/overlay2/e9f2dda207bde1f4b277f973457107d62cff259881901adcd9bcccfea9a92231-init/diff:/var/lib/docker/overlay2/4b4c37dfbdc0dc01b68d4fb1ddb86109398a2d73555439b874dbd23b87cd5c4b/diff", + "MergedDir": "/var/lib/docker/overlay2/e9f2dda207bde1f4b277f973457107d62cff259881901adcd9bcccfea9a92231/merged", + "UpperDir": "/var/lib/docker/overlay2/e9f2dda207bde1f4b277f973457107d62cff259881901adcd9bcccfea9a92231/diff", + "WorkDir": "/var/lib/docker/overlay2/e9f2dda207bde1f4b277f973457107d62cff259881901adcd9bcccfea9a92231/work" + }, + "Name": "overlay2" + }, + "Mounts": [], + "Config": { + "Hostname": "3090de8b72b1", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "ExposedPorts": { + "23456/tcp": {} + }, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "Cmd": [ + "sleep", + "infinity" + ], + "Image": "debian:bookworm", + "Volumes": null, + "WorkingDir": "", + "Entrypoint": [], + "OnBuild": null, + "Labels": {} + }, + "NetworkSettings": { + "Bridge": "", + "SandboxID": "ebcd8b749b4c719f90d80605c352b7aa508e4c61d9dcd2919654f18f17eb2840", + "SandboxKey": "/var/run/docker/netns/ebcd8b749b4c", + "Ports": { + "23456/tcp": [ + { + "HostIp": "0.0.0.0", + "HostPort": "12345" + }, + { + "HostIp": "::", + "HostPort": "12345" + } + ] + }, + "HairpinMode": false, + "LinkLocalIPv6Address": "", + "LinkLocalIPv6PrefixLen": 0, + "SecondaryIPAddresses": null, + "SecondaryIPv6Addresses": null, + "EndpointID": "465824b3cc6bdd2b307e9c614815fd458b1baac113dee889c3620f0cac3183fa", + "Gateway": "172.17.0.1", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "IPAddress": "172.17.0.2", + "IPPrefixLen": 16, + "IPv6Gateway": "", + "MacAddress": "52:b6:f6:7b:4b:5b", + "Networks": { + "bridge": { + "IPAMConfig": null, + "Links": null, + "Aliases": null, + "MacAddress": "52:b6:f6:7b:4b:5b", + "DriverOpts": null, + "GwPriority": 0, + "NetworkID": "c4dd768ab4945e41ad23fe3907c960dac811141592a861cc40038df7086a1ce1", + "EndpointID": "465824b3cc6bdd2b307e9c614815fd458b1baac113dee889c3620f0cac3183fa", + "Gateway": "172.17.0.1", + "IPAddress": "172.17.0.2", + "IPPrefixLen": 16, + "IPv6Gateway": "", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "DNSNames": null + } + } + } + } +] diff --git a/agent/agentcontainers/testdata/container_labels.json b/agent/agentcontainers/testdata/container_labels.json new file mode 100644 index 0000000000000..03cac564f59ad --- /dev/null +++ b/agent/agentcontainers/testdata/container_labels.json @@ -0,0 +1,204 @@ +[ + { + "Id": "bd8818e670230fc6f36145b21cf8d6d35580355662aa4d9fe5ae1b188a4c905f", + "Created": "2025-03-11T20:03:28.071706536Z", + "Path": "sleep", + "Args": [ + "infinity" + ], + "State": { + "Status": "running", + "Running": true, + "Paused": false, + "Restarting": false, + "OOMKilled": false, + "Dead": false, + "Pid": 913862, + "ExitCode": 0, + "Error": "", + "StartedAt": "2025-03-11T20:03:28.123599065Z", + "FinishedAt": "0001-01-01T00:00:00Z" + }, + "Image": "sha256:d4ccddb816ba27eaae22ef3d56175d53f47998e2acb99df1ae0e5b426b28a076", + "ResolvConfPath": "/var/lib/docker/containers/bd8818e670230fc6f36145b21cf8d6d35580355662aa4d9fe5ae1b188a4c905f/resolv.conf", + "HostnamePath": "/var/lib/docker/containers/bd8818e670230fc6f36145b21cf8d6d35580355662aa4d9fe5ae1b188a4c905f/hostname", + "HostsPath": "/var/lib/docker/containers/bd8818e670230fc6f36145b21cf8d6d35580355662aa4d9fe5ae1b188a4c905f/hosts", + "LogPath": "/var/lib/docker/containers/bd8818e670230fc6f36145b21cf8d6d35580355662aa4d9fe5ae1b188a4c905f/bd8818e670230fc6f36145b21cf8d6d35580355662aa4d9fe5ae1b188a4c905f-json.log", + "Name": "/fervent_bardeen", + "RestartCount": 0, + "Driver": "overlay2", + "Platform": "linux", + "MountLabel": "", + "ProcessLabel": "", + "AppArmorProfile": "", + "ExecIDs": null, + "HostConfig": { + "Binds": null, + "ContainerIDFile": "", + "LogConfig": { + "Type": "json-file", + "Config": {} + }, + "NetworkMode": "bridge", + "PortBindings": {}, + "RestartPolicy": { + "Name": "no", + "MaximumRetryCount": 0 + }, + "AutoRemove": false, + "VolumeDriver": "", + "VolumesFrom": null, + "ConsoleSize": [ + 108, + 176 + ], + "CapAdd": null, + "CapDrop": null, + "CgroupnsMode": "private", + "Dns": [], + "DnsOptions": [], + "DnsSearch": [], + "ExtraHosts": null, + "GroupAdd": null, + "IpcMode": "private", + "Cgroup": "", + "Links": null, + "OomScoreAdj": 10, + "PidMode": "", + "Privileged": false, + "PublishAllPorts": false, + "ReadonlyRootfs": false, + "SecurityOpt": null, + "UTSMode": "", + "UsernsMode": "", + "ShmSize": 67108864, + "Runtime": "runc", + "Isolation": "", + "CpuShares": 0, + "Memory": 0, + "NanoCpus": 0, + "CgroupParent": "", + "BlkioWeight": 0, + "BlkioWeightDevice": [], + "BlkioDeviceReadBps": [], + "BlkioDeviceWriteBps": [], + "BlkioDeviceReadIOps": [], + "BlkioDeviceWriteIOps": [], + "CpuPeriod": 0, + "CpuQuota": 0, + "CpuRealtimePeriod": 0, + "CpuRealtimeRuntime": 0, + "CpusetCpus": "", + "CpusetMems": "", + "Devices": [], + "DeviceCgroupRules": null, + "DeviceRequests": null, + "MemoryReservation": 0, + "MemorySwap": 0, + "MemorySwappiness": null, + "OomKillDisable": null, + "PidsLimit": null, + "Ulimits": [], + "CpuCount": 0, + "CpuPercent": 0, + "IOMaximumIOps": 0, + "IOMaximumBandwidth": 0, + "MaskedPaths": [ + "/proc/asound", + "/proc/acpi", + "/proc/kcore", + "/proc/keys", + "/proc/latency_stats", + "/proc/timer_list", + "/proc/timer_stats", + "/proc/sched_debug", + "/proc/scsi", + "/sys/firmware", + "/sys/devices/virtual/powercap" + ], + "ReadonlyPaths": [ + "/proc/bus", + "/proc/fs", + "/proc/irq", + "/proc/sys", + "/proc/sysrq-trigger" + ] + }, + "GraphDriver": { + "Data": { + "ID": "bd8818e670230fc6f36145b21cf8d6d35580355662aa4d9fe5ae1b188a4c905f", + "LowerDir": "/var/lib/docker/overlay2/55fc45976c381040c7d261c198333e6331889c51afe1500e2e7837c189a1b794-init/diff:/var/lib/docker/overlay2/4b4c37dfbdc0dc01b68d4fb1ddb86109398a2d73555439b874dbd23b87cd5c4b/diff", + "MergedDir": "/var/lib/docker/overlay2/55fc45976c381040c7d261c198333e6331889c51afe1500e2e7837c189a1b794/merged", + "UpperDir": "/var/lib/docker/overlay2/55fc45976c381040c7d261c198333e6331889c51afe1500e2e7837c189a1b794/diff", + "WorkDir": "/var/lib/docker/overlay2/55fc45976c381040c7d261c198333e6331889c51afe1500e2e7837c189a1b794/work" + }, + "Name": "overlay2" + }, + "Mounts": [], + "Config": { + "Hostname": "bd8818e67023", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "Cmd": [ + "sleep", + "infinity" + ], + "Image": "debian:bookworm", + "Volumes": null, + "WorkingDir": "", + "Entrypoint": [], + "OnBuild": null, + "Labels": { + "baz": "zap", + "foo": "bar" + } + }, + "NetworkSettings": { + "Bridge": "", + "SandboxID": "24faa8b9aaa58c651deca0d85a3f7bcc6c3e5e1a24b6369211f736d6e82f8ab0", + "SandboxKey": "/var/run/docker/netns/24faa8b9aaa5", + "Ports": {}, + "HairpinMode": false, + "LinkLocalIPv6Address": "", + "LinkLocalIPv6PrefixLen": 0, + "SecondaryIPAddresses": null, + "SecondaryIPv6Addresses": null, + "EndpointID": "c686f97d772d75c8ceed9285e06c1f671b71d4775d5513f93f26358c0f0b4671", + "Gateway": "172.17.0.1", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "IPAddress": "172.17.0.2", + "IPPrefixLen": 16, + "IPv6Gateway": "", + "MacAddress": "96:88:4e:3b:11:44", + "Networks": { + "bridge": { + "IPAMConfig": null, + "Links": null, + "Aliases": null, + "MacAddress": "96:88:4e:3b:11:44", + "DriverOpts": null, + "GwPriority": 0, + "NetworkID": "c4dd768ab4945e41ad23fe3907c960dac811141592a861cc40038df7086a1ce1", + "EndpointID": "c686f97d772d75c8ceed9285e06c1f671b71d4775d5513f93f26358c0f0b4671", + "Gateway": "172.17.0.1", + "IPAddress": "172.17.0.2", + "IPPrefixLen": 16, + "IPv6Gateway": "", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "DNSNames": null + } + } + } + } +] diff --git a/agent/agentcontainers/testdata/container_sameport.json b/agent/agentcontainers/testdata/container_sameport.json new file mode 100644 index 0000000000000..c7f2f84d4b397 --- /dev/null +++ b/agent/agentcontainers/testdata/container_sameport.json @@ -0,0 +1,222 @@ +[ + { + "Id": "4eac5ce199d27b2329d0ff0ce1a6fc595612ced48eba3669aadb6c57ebef3fa2", + "Created": "2025-03-11T17:56:34.842164541Z", + "Path": "sleep", + "Args": [ + "infinity" + ], + "State": { + "Status": "running", + "Running": true, + "Paused": false, + "Restarting": false, + "OOMKilled": false, + "Dead": false, + "Pid": 638449, + "ExitCode": 0, + "Error": "", + "StartedAt": "2025-03-11T17:56:34.894488648Z", + "FinishedAt": "0001-01-01T00:00:00Z" + }, + "Image": "sha256:d4ccddb816ba27eaae22ef3d56175d53f47998e2acb99df1ae0e5b426b28a076", + "ResolvConfPath": "/var/lib/docker/containers/4eac5ce199d27b2329d0ff0ce1a6fc595612ced48eba3669aadb6c57ebef3fa2/resolv.conf", + "HostnamePath": "/var/lib/docker/containers/4eac5ce199d27b2329d0ff0ce1a6fc595612ced48eba3669aadb6c57ebef3fa2/hostname", + "HostsPath": "/var/lib/docker/containers/4eac5ce199d27b2329d0ff0ce1a6fc595612ced48eba3669aadb6c57ebef3fa2/hosts", + "LogPath": "/var/lib/docker/containers/4eac5ce199d27b2329d0ff0ce1a6fc595612ced48eba3669aadb6c57ebef3fa2/4eac5ce199d27b2329d0ff0ce1a6fc595612ced48eba3669aadb6c57ebef3fa2-json.log", + "Name": "/modest_varahamihira", + "RestartCount": 0, + "Driver": "overlay2", + "Platform": "linux", + "MountLabel": "", + "ProcessLabel": "", + "AppArmorProfile": "", + "ExecIDs": null, + "HostConfig": { + "Binds": null, + "ContainerIDFile": "", + "LogConfig": { + "Type": "json-file", + "Config": {} + }, + "NetworkMode": "bridge", + "PortBindings": { + "12345/tcp": [ + { + "HostIp": "", + "HostPort": "12345" + } + ] + }, + "RestartPolicy": { + "Name": "no", + "MaximumRetryCount": 0 + }, + "AutoRemove": false, + "VolumeDriver": "", + "VolumesFrom": null, + "ConsoleSize": [ + 108, + 176 + ], + "CapAdd": null, + "CapDrop": null, + "CgroupnsMode": "private", + "Dns": [], + "DnsOptions": [], + "DnsSearch": [], + "ExtraHosts": null, + "GroupAdd": null, + "IpcMode": "private", + "Cgroup": "", + "Links": null, + "OomScoreAdj": 10, + "PidMode": "", + "Privileged": false, + "PublishAllPorts": false, + "ReadonlyRootfs": false, + "SecurityOpt": null, + "UTSMode": "", + "UsernsMode": "", + "ShmSize": 67108864, + "Runtime": "runc", + "Isolation": "", + "CpuShares": 0, + "Memory": 0, + "NanoCpus": 0, + "CgroupParent": "", + "BlkioWeight": 0, + "BlkioWeightDevice": [], + "BlkioDeviceReadBps": [], + "BlkioDeviceWriteBps": [], + "BlkioDeviceReadIOps": [], + "BlkioDeviceWriteIOps": [], + "CpuPeriod": 0, + "CpuQuota": 0, + "CpuRealtimePeriod": 0, + "CpuRealtimeRuntime": 0, + "CpusetCpus": "", + "CpusetMems": "", + "Devices": [], + "DeviceCgroupRules": null, + "DeviceRequests": null, + "MemoryReservation": 0, + "MemorySwap": 0, + "MemorySwappiness": null, + "OomKillDisable": null, + "PidsLimit": null, + "Ulimits": [], + "CpuCount": 0, + "CpuPercent": 0, + "IOMaximumIOps": 0, + "IOMaximumBandwidth": 0, + "MaskedPaths": [ + "/proc/asound", + "/proc/acpi", + "/proc/kcore", + "/proc/keys", + "/proc/latency_stats", + "/proc/timer_list", + "/proc/timer_stats", + "/proc/sched_debug", + "/proc/scsi", + "/sys/firmware", + "/sys/devices/virtual/powercap" + ], + "ReadonlyPaths": [ + "/proc/bus", + "/proc/fs", + "/proc/irq", + "/proc/sys", + "/proc/sysrq-trigger" + ] + }, + "GraphDriver": { + "Data": { + "ID": "4eac5ce199d27b2329d0ff0ce1a6fc595612ced48eba3669aadb6c57ebef3fa2", + "LowerDir": "/var/lib/docker/overlay2/35deac62dd3f610275aaf145d091aaa487f73a3c99de5a90df8ab871c969bc0b-init/diff:/var/lib/docker/overlay2/4b4c37dfbdc0dc01b68d4fb1ddb86109398a2d73555439b874dbd23b87cd5c4b/diff", + "MergedDir": "/var/lib/docker/overlay2/35deac62dd3f610275aaf145d091aaa487f73a3c99de5a90df8ab871c969bc0b/merged", + "UpperDir": "/var/lib/docker/overlay2/35deac62dd3f610275aaf145d091aaa487f73a3c99de5a90df8ab871c969bc0b/diff", + "WorkDir": "/var/lib/docker/overlay2/35deac62dd3f610275aaf145d091aaa487f73a3c99de5a90df8ab871c969bc0b/work" + }, + "Name": "overlay2" + }, + "Mounts": [], + "Config": { + "Hostname": "4eac5ce199d2", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "ExposedPorts": { + "12345/tcp": {} + }, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "Cmd": [ + "sleep", + "infinity" + ], + "Image": "debian:bookworm", + "Volumes": null, + "WorkingDir": "", + "Entrypoint": [], + "OnBuild": null, + "Labels": {} + }, + "NetworkSettings": { + "Bridge": "", + "SandboxID": "5e966e97ba02013054e0ef15ef87f8629f359ad882fad4c57b33c768ad9b90dc", + "SandboxKey": "/var/run/docker/netns/5e966e97ba02", + "Ports": { + "12345/tcp": [ + { + "HostIp": "0.0.0.0", + "HostPort": "12345" + }, + { + "HostIp": "::", + "HostPort": "12345" + } + ] + }, + "HairpinMode": false, + "LinkLocalIPv6Address": "", + "LinkLocalIPv6PrefixLen": 0, + "SecondaryIPAddresses": null, + "SecondaryIPv6Addresses": null, + "EndpointID": "f9e1896fc0ef48f3ea9aff3b4e98bc4291ba246412178331345f7b0745cccba9", + "Gateway": "172.17.0.1", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "IPAddress": "172.17.0.2", + "IPPrefixLen": 16, + "IPv6Gateway": "", + "MacAddress": "be:a6:89:39:7e:b0", + "Networks": { + "bridge": { + "IPAMConfig": null, + "Links": null, + "Aliases": null, + "MacAddress": "be:a6:89:39:7e:b0", + "DriverOpts": null, + "GwPriority": 0, + "NetworkID": "c4dd768ab4945e41ad23fe3907c960dac811141592a861cc40038df7086a1ce1", + "EndpointID": "f9e1896fc0ef48f3ea9aff3b4e98bc4291ba246412178331345f7b0745cccba9", + "Gateway": "172.17.0.1", + "IPAddress": "172.17.0.2", + "IPPrefixLen": 16, + "IPv6Gateway": "", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "DNSNames": null + } + } + } + } +] diff --git a/agent/agentcontainers/testdata/container_simple.json b/agent/agentcontainers/testdata/container_simple.json new file mode 100644 index 0000000000000..39c735aca5dc5 --- /dev/null +++ b/agent/agentcontainers/testdata/container_simple.json @@ -0,0 +1,201 @@ +[ + { + "Id": "6b539b8c60f5230b8b0fde2502cd2332d31c0d526a3e6eb6eef1cc39439b3286", + "Created": "2025-03-11T17:55:58.091280203Z", + "Path": "sleep", + "Args": [ + "infinity" + ], + "State": { + "Status": "running", + "Running": true, + "Paused": false, + "Restarting": false, + "OOMKilled": false, + "Dead": false, + "Pid": 636855, + "ExitCode": 0, + "Error": "", + "StartedAt": "2025-03-11T17:55:58.142417459Z", + "FinishedAt": "0001-01-01T00:00:00Z" + }, + "Image": "sha256:d4ccddb816ba27eaae22ef3d56175d53f47998e2acb99df1ae0e5b426b28a076", + "ResolvConfPath": "/var/lib/docker/containers/6b539b8c60f5230b8b0fde2502cd2332d31c0d526a3e6eb6eef1cc39439b3286/resolv.conf", + "HostnamePath": "/var/lib/docker/containers/6b539b8c60f5230b8b0fde2502cd2332d31c0d526a3e6eb6eef1cc39439b3286/hostname", + "HostsPath": "/var/lib/docker/containers/6b539b8c60f5230b8b0fde2502cd2332d31c0d526a3e6eb6eef1cc39439b3286/hosts", + "LogPath": "/var/lib/docker/containers/6b539b8c60f5230b8b0fde2502cd2332d31c0d526a3e6eb6eef1cc39439b3286/6b539b8c60f5230b8b0fde2502cd2332d31c0d526a3e6eb6eef1cc39439b3286-json.log", + "Name": "/eloquent_kowalevski", + "RestartCount": 0, + "Driver": "overlay2", + "Platform": "linux", + "MountLabel": "", + "ProcessLabel": "", + "AppArmorProfile": "", + "ExecIDs": null, + "HostConfig": { + "Binds": null, + "ContainerIDFile": "", + "LogConfig": { + "Type": "json-file", + "Config": {} + }, + "NetworkMode": "bridge", + "PortBindings": {}, + "RestartPolicy": { + "Name": "no", + "MaximumRetryCount": 0 + }, + "AutoRemove": false, + "VolumeDriver": "", + "VolumesFrom": null, + "ConsoleSize": [ + 108, + 176 + ], + "CapAdd": null, + "CapDrop": null, + "CgroupnsMode": "private", + "Dns": [], + "DnsOptions": [], + "DnsSearch": [], + "ExtraHosts": null, + "GroupAdd": null, + "IpcMode": "private", + "Cgroup": "", + "Links": null, + "OomScoreAdj": 10, + "PidMode": "", + "Privileged": false, + "PublishAllPorts": false, + "ReadonlyRootfs": false, + "SecurityOpt": null, + "UTSMode": "", + "UsernsMode": "", + "ShmSize": 67108864, + "Runtime": "runc", + "Isolation": "", + "CpuShares": 0, + "Memory": 0, + "NanoCpus": 0, + "CgroupParent": "", + "BlkioWeight": 0, + "BlkioWeightDevice": [], + "BlkioDeviceReadBps": [], + "BlkioDeviceWriteBps": [], + "BlkioDeviceReadIOps": [], + "BlkioDeviceWriteIOps": [], + "CpuPeriod": 0, + "CpuQuota": 0, + "CpuRealtimePeriod": 0, + "CpuRealtimeRuntime": 0, + "CpusetCpus": "", + "CpusetMems": "", + "Devices": [], + "DeviceCgroupRules": null, + "DeviceRequests": null, + "MemoryReservation": 0, + "MemorySwap": 0, + "MemorySwappiness": null, + "OomKillDisable": null, + "PidsLimit": null, + "Ulimits": [], + "CpuCount": 0, + "CpuPercent": 0, + "IOMaximumIOps": 0, + "IOMaximumBandwidth": 0, + "MaskedPaths": [ + "/proc/asound", + "/proc/acpi", + "/proc/kcore", + "/proc/keys", + "/proc/latency_stats", + "/proc/timer_list", + "/proc/timer_stats", + "/proc/sched_debug", + "/proc/scsi", + "/sys/firmware", + "/sys/devices/virtual/powercap" + ], + "ReadonlyPaths": [ + "/proc/bus", + "/proc/fs", + "/proc/irq", + "/proc/sys", + "/proc/sysrq-trigger" + ] + }, + "GraphDriver": { + "Data": { + "ID": "6b539b8c60f5230b8b0fde2502cd2332d31c0d526a3e6eb6eef1cc39439b3286", + "LowerDir": "/var/lib/docker/overlay2/4093560d7757c088e24060e5ff6f32807d8e733008c42b8af7057fe4fe6f56ba-init/diff:/var/lib/docker/overlay2/4b4c37dfbdc0dc01b68d4fb1ddb86109398a2d73555439b874dbd23b87cd5c4b/diff", + "MergedDir": "/var/lib/docker/overlay2/4093560d7757c088e24060e5ff6f32807d8e733008c42b8af7057fe4fe6f56ba/merged", + "UpperDir": "/var/lib/docker/overlay2/4093560d7757c088e24060e5ff6f32807d8e733008c42b8af7057fe4fe6f56ba/diff", + "WorkDir": "/var/lib/docker/overlay2/4093560d7757c088e24060e5ff6f32807d8e733008c42b8af7057fe4fe6f56ba/work" + }, + "Name": "overlay2" + }, + "Mounts": [], + "Config": { + "Hostname": "6b539b8c60f5", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "Cmd": [ + "sleep", + "infinity" + ], + "Image": "debian:bookworm", + "Volumes": null, + "WorkingDir": "", + "Entrypoint": [], + "OnBuild": null, + "Labels": {} + }, + "NetworkSettings": { + "Bridge": "", + "SandboxID": "08f2f3218a6d63ae149ab77672659d96b88bca350e85889240579ecb427e8011", + "SandboxKey": "/var/run/docker/netns/08f2f3218a6d", + "Ports": {}, + "HairpinMode": false, + "LinkLocalIPv6Address": "", + "LinkLocalIPv6PrefixLen": 0, + "SecondaryIPAddresses": null, + "SecondaryIPv6Addresses": null, + "EndpointID": "f83bd20711df6d6ff7e2f44f4b5799636cd94596ae25ffe507a70f424073532c", + "Gateway": "172.17.0.1", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "IPAddress": "172.17.0.2", + "IPPrefixLen": 16, + "IPv6Gateway": "", + "MacAddress": "f6:84:26:7a:10:5b", + "Networks": { + "bridge": { + "IPAMConfig": null, + "Links": null, + "Aliases": null, + "MacAddress": "f6:84:26:7a:10:5b", + "DriverOpts": null, + "GwPriority": 0, + "NetworkID": "c4dd768ab4945e41ad23fe3907c960dac811141592a861cc40038df7086a1ce1", + "EndpointID": "f83bd20711df6d6ff7e2f44f4b5799636cd94596ae25ffe507a70f424073532c", + "Gateway": "172.17.0.1", + "IPAddress": "172.17.0.2", + "IPPrefixLen": 16, + "IPv6Gateway": "", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "DNSNames": null + } + } + } + } +] diff --git a/agent/agentcontainers/testdata/container_volume.json b/agent/agentcontainers/testdata/container_volume.json new file mode 100644 index 0000000000000..1e826198e5d75 --- /dev/null +++ b/agent/agentcontainers/testdata/container_volume.json @@ -0,0 +1,214 @@ +[ + { + "Id": "b3688d98c007f53402a55e46d803f2f3ba9181d8e3f71a2eb19b392cf0377b4e", + "Created": "2025-03-11T17:59:42.039484134Z", + "Path": "sleep", + "Args": [ + "infinity" + ], + "State": { + "Status": "running", + "Running": true, + "Paused": false, + "Restarting": false, + "OOMKilled": false, + "Dead": false, + "Pid": 646777, + "ExitCode": 0, + "Error": "", + "StartedAt": "2025-03-11T17:59:42.081315917Z", + "FinishedAt": "0001-01-01T00:00:00Z" + }, + "Image": "sha256:d4ccddb816ba27eaae22ef3d56175d53f47998e2acb99df1ae0e5b426b28a076", + "ResolvConfPath": "/var/lib/docker/containers/b3688d98c007f53402a55e46d803f2f3ba9181d8e3f71a2eb19b392cf0377b4e/resolv.conf", + "HostnamePath": "/var/lib/docker/containers/b3688d98c007f53402a55e46d803f2f3ba9181d8e3f71a2eb19b392cf0377b4e/hostname", + "HostsPath": "/var/lib/docker/containers/b3688d98c007f53402a55e46d803f2f3ba9181d8e3f71a2eb19b392cf0377b4e/hosts", + "LogPath": "/var/lib/docker/containers/b3688d98c007f53402a55e46d803f2f3ba9181d8e3f71a2eb19b392cf0377b4e/b3688d98c007f53402a55e46d803f2f3ba9181d8e3f71a2eb19b392cf0377b4e-json.log", + "Name": "/upbeat_carver", + "RestartCount": 0, + "Driver": "overlay2", + "Platform": "linux", + "MountLabel": "", + "ProcessLabel": "", + "AppArmorProfile": "", + "ExecIDs": null, + "HostConfig": { + "Binds": [ + "testvol:/testvol" + ], + "ContainerIDFile": "", + "LogConfig": { + "Type": "json-file", + "Config": {} + }, + "NetworkMode": "bridge", + "PortBindings": {}, + "RestartPolicy": { + "Name": "no", + "MaximumRetryCount": 0 + }, + "AutoRemove": false, + "VolumeDriver": "", + "VolumesFrom": null, + "ConsoleSize": [ + 108, + 176 + ], + "CapAdd": null, + "CapDrop": null, + "CgroupnsMode": "private", + "Dns": [], + "DnsOptions": [], + "DnsSearch": [], + "ExtraHosts": null, + "GroupAdd": null, + "IpcMode": "private", + "Cgroup": "", + "Links": null, + "OomScoreAdj": 10, + "PidMode": "", + "Privileged": false, + "PublishAllPorts": false, + "ReadonlyRootfs": false, + "SecurityOpt": null, + "UTSMode": "", + "UsernsMode": "", + "ShmSize": 67108864, + "Runtime": "runc", + "Isolation": "", + "CpuShares": 0, + "Memory": 0, + "NanoCpus": 0, + "CgroupParent": "", + "BlkioWeight": 0, + "BlkioWeightDevice": [], + "BlkioDeviceReadBps": [], + "BlkioDeviceWriteBps": [], + "BlkioDeviceReadIOps": [], + "BlkioDeviceWriteIOps": [], + "CpuPeriod": 0, + "CpuQuota": 0, + "CpuRealtimePeriod": 0, + "CpuRealtimeRuntime": 0, + "CpusetCpus": "", + "CpusetMems": "", + "Devices": [], + "DeviceCgroupRules": null, + "DeviceRequests": null, + "MemoryReservation": 0, + "MemorySwap": 0, + "MemorySwappiness": null, + "OomKillDisable": null, + "PidsLimit": null, + "Ulimits": [], + "CpuCount": 0, + "CpuPercent": 0, + "IOMaximumIOps": 0, + "IOMaximumBandwidth": 0, + "MaskedPaths": [ + "/proc/asound", + "/proc/acpi", + "/proc/kcore", + "/proc/keys", + "/proc/latency_stats", + "/proc/timer_list", + "/proc/timer_stats", + "/proc/sched_debug", + "/proc/scsi", + "/sys/firmware", + "/sys/devices/virtual/powercap" + ], + "ReadonlyPaths": [ + "/proc/bus", + "/proc/fs", + "/proc/irq", + "/proc/sys", + "/proc/sysrq-trigger" + ] + }, + "GraphDriver": { + "Data": { + "ID": "b3688d98c007f53402a55e46d803f2f3ba9181d8e3f71a2eb19b392cf0377b4e", + "LowerDir": "/var/lib/docker/overlay2/d71790d2558bf17d7535451094e332780638a4e92697c021176f3447fc4c50f4-init/diff:/var/lib/docker/overlay2/4b4c37dfbdc0dc01b68d4fb1ddb86109398a2d73555439b874dbd23b87cd5c4b/diff", + "MergedDir": "/var/lib/docker/overlay2/d71790d2558bf17d7535451094e332780638a4e92697c021176f3447fc4c50f4/merged", + "UpperDir": "/var/lib/docker/overlay2/d71790d2558bf17d7535451094e332780638a4e92697c021176f3447fc4c50f4/diff", + "WorkDir": "/var/lib/docker/overlay2/d71790d2558bf17d7535451094e332780638a4e92697c021176f3447fc4c50f4/work" + }, + "Name": "overlay2" + }, + "Mounts": [ + { + "Type": "volume", + "Name": "testvol", + "Source": "/var/lib/docker/volumes/testvol/_data", + "Destination": "/testvol", + "Driver": "local", + "Mode": "z", + "RW": true, + "Propagation": "" + } + ], + "Config": { + "Hostname": "b3688d98c007", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "Cmd": [ + "sleep", + "infinity" + ], + "Image": "debian:bookworm", + "Volumes": null, + "WorkingDir": "", + "Entrypoint": [], + "OnBuild": null, + "Labels": {} + }, + "NetworkSettings": { + "Bridge": "", + "SandboxID": "e617ea865af5690d06c25df1c9a0154b98b4da6bbb9e0afae3b80ad29902538a", + "SandboxKey": "/var/run/docker/netns/e617ea865af5", + "Ports": {}, + "HairpinMode": false, + "LinkLocalIPv6Address": "", + "LinkLocalIPv6PrefixLen": 0, + "SecondaryIPAddresses": null, + "SecondaryIPv6Addresses": null, + "EndpointID": "1a7bb5bbe4af0674476c95c5d1c913348bc82a5f01fd1c1b394afc44d1cf5a49", + "Gateway": "172.17.0.1", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "IPAddress": "172.17.0.2", + "IPPrefixLen": 16, + "IPv6Gateway": "", + "MacAddress": "4a:d8:a5:47:1c:54", + "Networks": { + "bridge": { + "IPAMConfig": null, + "Links": null, + "Aliases": null, + "MacAddress": "4a:d8:a5:47:1c:54", + "DriverOpts": null, + "GwPriority": 0, + "NetworkID": "c4dd768ab4945e41ad23fe3907c960dac811141592a861cc40038df7086a1ce1", + "EndpointID": "1a7bb5bbe4af0674476c95c5d1c913348bc82a5f01fd1c1b394afc44d1cf5a49", + "Gateway": "172.17.0.1", + "IPAddress": "172.17.0.2", + "IPPrefixLen": 16, + "IPv6Gateway": "", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "DNSNames": null + } + } + } + } +] diff --git a/agent/agentcontainers/testdata/devcontainer_appport.json b/agent/agentcontainers/testdata/devcontainer_appport.json new file mode 100644 index 0000000000000..5d7c505c3e1cb --- /dev/null +++ b/agent/agentcontainers/testdata/devcontainer_appport.json @@ -0,0 +1,230 @@ +[ + { + "Id": "52d23691f4b954d083f117358ea763e20f69af584e1c08f479c5752629ee0be3", + "Created": "2025-03-11T17:02:42.613747761Z", + "Path": "/bin/sh", + "Args": [ + "-c", + "echo Container started\ntrap \"exit 0\" 15\n\nexec \"$@\"\nwhile sleep 1 & wait $!; do :; done", + "-" + ], + "State": { + "Status": "running", + "Running": true, + "Paused": false, + "Restarting": false, + "OOMKilled": false, + "Dead": false, + "Pid": 526198, + "ExitCode": 0, + "Error": "", + "StartedAt": "2025-03-11T17:02:42.658905789Z", + "FinishedAt": "0001-01-01T00:00:00Z" + }, + "Image": "sha256:d4ccddb816ba27eaae22ef3d56175d53f47998e2acb99df1ae0e5b426b28a076", + "ResolvConfPath": "/var/lib/docker/containers/52d23691f4b954d083f117358ea763e20f69af584e1c08f479c5752629ee0be3/resolv.conf", + "HostnamePath": "/var/lib/docker/containers/52d23691f4b954d083f117358ea763e20f69af584e1c08f479c5752629ee0be3/hostname", + "HostsPath": "/var/lib/docker/containers/52d23691f4b954d083f117358ea763e20f69af584e1c08f479c5752629ee0be3/hosts", + "LogPath": "/var/lib/docker/containers/52d23691f4b954d083f117358ea763e20f69af584e1c08f479c5752629ee0be3/52d23691f4b954d083f117358ea763e20f69af584e1c08f479c5752629ee0be3-json.log", + "Name": "/suspicious_margulis", + "RestartCount": 0, + "Driver": "overlay2", + "Platform": "linux", + "MountLabel": "", + "ProcessLabel": "", + "AppArmorProfile": "", + "ExecIDs": null, + "HostConfig": { + "Binds": null, + "ContainerIDFile": "", + "LogConfig": { + "Type": "json-file", + "Config": {} + }, + "NetworkMode": "bridge", + "PortBindings": { + "8080/tcp": [ + { + "HostIp": "", + "HostPort": "" + } + ] + }, + "RestartPolicy": { + "Name": "no", + "MaximumRetryCount": 0 + }, + "AutoRemove": false, + "VolumeDriver": "", + "VolumesFrom": null, + "ConsoleSize": [ + 108, + 176 + ], + "CapAdd": null, + "CapDrop": null, + "CgroupnsMode": "private", + "Dns": [], + "DnsOptions": [], + "DnsSearch": [], + "ExtraHosts": null, + "GroupAdd": null, + "IpcMode": "private", + "Cgroup": "", + "Links": null, + "OomScoreAdj": 10, + "PidMode": "", + "Privileged": false, + "PublishAllPorts": false, + "ReadonlyRootfs": false, + "SecurityOpt": null, + "UTSMode": "", + "UsernsMode": "", + "ShmSize": 67108864, + "Runtime": "runc", + "Isolation": "", + "CpuShares": 0, + "Memory": 0, + "NanoCpus": 0, + "CgroupParent": "", + "BlkioWeight": 0, + "BlkioWeightDevice": [], + "BlkioDeviceReadBps": [], + "BlkioDeviceWriteBps": [], + "BlkioDeviceReadIOps": [], + "BlkioDeviceWriteIOps": [], + "CpuPeriod": 0, + "CpuQuota": 0, + "CpuRealtimePeriod": 0, + "CpuRealtimeRuntime": 0, + "CpusetCpus": "", + "CpusetMems": "", + "Devices": [], + "DeviceCgroupRules": null, + "DeviceRequests": null, + "MemoryReservation": 0, + "MemorySwap": 0, + "MemorySwappiness": null, + "OomKillDisable": null, + "PidsLimit": null, + "Ulimits": [], + "CpuCount": 0, + "CpuPercent": 0, + "IOMaximumIOps": 0, + "IOMaximumBandwidth": 0, + "MaskedPaths": [ + "/proc/asound", + "/proc/acpi", + "/proc/kcore", + "/proc/keys", + "/proc/latency_stats", + "/proc/timer_list", + "/proc/timer_stats", + "/proc/sched_debug", + "/proc/scsi", + "/sys/firmware", + "/sys/devices/virtual/powercap" + ], + "ReadonlyPaths": [ + "/proc/bus", + "/proc/fs", + "/proc/irq", + "/proc/sys", + "/proc/sysrq-trigger" + ] + }, + "GraphDriver": { + "Data": { + "ID": "52d23691f4b954d083f117358ea763e20f69af584e1c08f479c5752629ee0be3", + "LowerDir": "/var/lib/docker/overlay2/e204eab11c98b3cacc18d5a0e7290c0c286a96d918c31e5c2fed4124132eec4f-init/diff:/var/lib/docker/overlay2/4b4c37dfbdc0dc01b68d4fb1ddb86109398a2d73555439b874dbd23b87cd5c4b/diff", + "MergedDir": "/var/lib/docker/overlay2/e204eab11c98b3cacc18d5a0e7290c0c286a96d918c31e5c2fed4124132eec4f/merged", + "UpperDir": "/var/lib/docker/overlay2/e204eab11c98b3cacc18d5a0e7290c0c286a96d918c31e5c2fed4124132eec4f/diff", + "WorkDir": "/var/lib/docker/overlay2/e204eab11c98b3cacc18d5a0e7290c0c286a96d918c31e5c2fed4124132eec4f/work" + }, + "Name": "overlay2" + }, + "Mounts": [], + "Config": { + "Hostname": "52d23691f4b9", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "ExposedPorts": { + "8080/tcp": {} + }, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "Cmd": [ + "-c", + "echo Container started\ntrap \"exit 0\" 15\n\nexec \"$@\"\nwhile sleep 1 & wait $!; do :; done", + "-" + ], + "Image": "debian:bookworm", + "Volumes": null, + "WorkingDir": "", + "Entrypoint": [ + "/bin/sh" + ], + "OnBuild": null, + "Labels": { + "devcontainer.config_file": "/home/coder/src/coder/coder/agent/agentcontainers/testdata/devcontainer_appport.json", + "devcontainer.metadata": "[]" + } + }, + "NetworkSettings": { + "Bridge": "", + "SandboxID": "e4fa65f769e331c72e27f43af2d65073efca638fd413b7c57f763ee9ebf69020", + "SandboxKey": "/var/run/docker/netns/e4fa65f769e3", + "Ports": { + "8080/tcp": [ + { + "HostIp": "0.0.0.0", + "HostPort": "32768" + }, + { + "HostIp": "::", + "HostPort": "32768" + } + ] + }, + "HairpinMode": false, + "LinkLocalIPv6Address": "", + "LinkLocalIPv6PrefixLen": 0, + "SecondaryIPAddresses": null, + "SecondaryIPv6Addresses": null, + "EndpointID": "14531bbbb26052456a4509e6d23753de45096ca8355ac11684c631d1656248ad", + "Gateway": "172.17.0.1", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "IPAddress": "172.17.0.2", + "IPPrefixLen": 16, + "IPv6Gateway": "", + "MacAddress": "36:88:48:04:4e:b4", + "Networks": { + "bridge": { + "IPAMConfig": null, + "Links": null, + "Aliases": null, + "MacAddress": "36:88:48:04:4e:b4", + "DriverOpts": null, + "GwPriority": 0, + "NetworkID": "c4dd768ab4945e41ad23fe3907c960dac811141592a861cc40038df7086a1ce1", + "EndpointID": "14531bbbb26052456a4509e6d23753de45096ca8355ac11684c631d1656248ad", + "Gateway": "172.17.0.1", + "IPAddress": "172.17.0.2", + "IPPrefixLen": 16, + "IPv6Gateway": "", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "DNSNames": null + } + } + } + } +] diff --git a/agent/agentcontainers/testdata/devcontainer_forwardport.json b/agent/agentcontainers/testdata/devcontainer_forwardport.json new file mode 100644 index 0000000000000..cedaca8fdfe30 --- /dev/null +++ b/agent/agentcontainers/testdata/devcontainer_forwardport.json @@ -0,0 +1,209 @@ +[ + { + "Id": "4a16af2293fb75dc827a6949a3905dd57ea28cc008823218ce24fab1cb66c067", + "Created": "2025-03-11T17:03:55.022053072Z", + "Path": "/bin/sh", + "Args": [ + "-c", + "echo Container started\ntrap \"exit 0\" 15\n\nexec \"$@\"\nwhile sleep 1 & wait $!; do :; done", + "-" + ], + "State": { + "Status": "running", + "Running": true, + "Paused": false, + "Restarting": false, + "OOMKilled": false, + "Dead": false, + "Pid": 529591, + "ExitCode": 0, + "Error": "", + "StartedAt": "2025-03-11T17:03:55.064323762Z", + "FinishedAt": "0001-01-01T00:00:00Z" + }, + "Image": "sha256:d4ccddb816ba27eaae22ef3d56175d53f47998e2acb99df1ae0e5b426b28a076", + "ResolvConfPath": "/var/lib/docker/containers/4a16af2293fb75dc827a6949a3905dd57ea28cc008823218ce24fab1cb66c067/resolv.conf", + "HostnamePath": "/var/lib/docker/containers/4a16af2293fb75dc827a6949a3905dd57ea28cc008823218ce24fab1cb66c067/hostname", + "HostsPath": "/var/lib/docker/containers/4a16af2293fb75dc827a6949a3905dd57ea28cc008823218ce24fab1cb66c067/hosts", + "LogPath": "/var/lib/docker/containers/4a16af2293fb75dc827a6949a3905dd57ea28cc008823218ce24fab1cb66c067/4a16af2293fb75dc827a6949a3905dd57ea28cc008823218ce24fab1cb66c067-json.log", + "Name": "/serene_khayyam", + "RestartCount": 0, + "Driver": "overlay2", + "Platform": "linux", + "MountLabel": "", + "ProcessLabel": "", + "AppArmorProfile": "", + "ExecIDs": null, + "HostConfig": { + "Binds": null, + "ContainerIDFile": "", + "LogConfig": { + "Type": "json-file", + "Config": {} + }, + "NetworkMode": "bridge", + "PortBindings": {}, + "RestartPolicy": { + "Name": "no", + "MaximumRetryCount": 0 + }, + "AutoRemove": false, + "VolumeDriver": "", + "VolumesFrom": null, + "ConsoleSize": [ + 108, + 176 + ], + "CapAdd": null, + "CapDrop": null, + "CgroupnsMode": "private", + "Dns": [], + "DnsOptions": [], + "DnsSearch": [], + "ExtraHosts": null, + "GroupAdd": null, + "IpcMode": "private", + "Cgroup": "", + "Links": null, + "OomScoreAdj": 10, + "PidMode": "", + "Privileged": false, + "PublishAllPorts": false, + "ReadonlyRootfs": false, + "SecurityOpt": null, + "UTSMode": "", + "UsernsMode": "", + "ShmSize": 67108864, + "Runtime": "runc", + "Isolation": "", + "CpuShares": 0, + "Memory": 0, + "NanoCpus": 0, + "CgroupParent": "", + "BlkioWeight": 0, + "BlkioWeightDevice": [], + "BlkioDeviceReadBps": [], + "BlkioDeviceWriteBps": [], + "BlkioDeviceReadIOps": [], + "BlkioDeviceWriteIOps": [], + "CpuPeriod": 0, + "CpuQuota": 0, + "CpuRealtimePeriod": 0, + "CpuRealtimeRuntime": 0, + "CpusetCpus": "", + "CpusetMems": "", + "Devices": [], + "DeviceCgroupRules": null, + "DeviceRequests": null, + "MemoryReservation": 0, + "MemorySwap": 0, + "MemorySwappiness": null, + "OomKillDisable": null, + "PidsLimit": null, + "Ulimits": [], + "CpuCount": 0, + "CpuPercent": 0, + "IOMaximumIOps": 0, + "IOMaximumBandwidth": 0, + "MaskedPaths": [ + "/proc/asound", + "/proc/acpi", + "/proc/kcore", + "/proc/keys", + "/proc/latency_stats", + "/proc/timer_list", + "/proc/timer_stats", + "/proc/sched_debug", + "/proc/scsi", + "/sys/firmware", + "/sys/devices/virtual/powercap" + ], + "ReadonlyPaths": [ + "/proc/bus", + "/proc/fs", + "/proc/irq", + "/proc/sys", + "/proc/sysrq-trigger" + ] + }, + "GraphDriver": { + "Data": { + "ID": "4a16af2293fb75dc827a6949a3905dd57ea28cc008823218ce24fab1cb66c067", + "LowerDir": "/var/lib/docker/overlay2/1974a49367024c771135c80dd6b62ba46cdebfa866e67a5408426c88a30bac3e-init/diff:/var/lib/docker/overlay2/4b4c37dfbdc0dc01b68d4fb1ddb86109398a2d73555439b874dbd23b87cd5c4b/diff", + "MergedDir": "/var/lib/docker/overlay2/1974a49367024c771135c80dd6b62ba46cdebfa866e67a5408426c88a30bac3e/merged", + "UpperDir": "/var/lib/docker/overlay2/1974a49367024c771135c80dd6b62ba46cdebfa866e67a5408426c88a30bac3e/diff", + "WorkDir": "/var/lib/docker/overlay2/1974a49367024c771135c80dd6b62ba46cdebfa866e67a5408426c88a30bac3e/work" + }, + "Name": "overlay2" + }, + "Mounts": [], + "Config": { + "Hostname": "4a16af2293fb", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "Cmd": [ + "-c", + "echo Container started\ntrap \"exit 0\" 15\n\nexec \"$@\"\nwhile sleep 1 & wait $!; do :; done", + "-" + ], + "Image": "debian:bookworm", + "Volumes": null, + "WorkingDir": "", + "Entrypoint": [ + "/bin/sh" + ], + "OnBuild": null, + "Labels": { + "devcontainer.config_file": "/home/coder/src/coder/coder/agent/agentcontainers/testdata/devcontainer_forwardport.json", + "devcontainer.metadata": "[]" + } + }, + "NetworkSettings": { + "Bridge": "", + "SandboxID": "e1c3bddb359d16c45d6d132561b83205af7809b01ed5cb985a8cb1b416b2ddd5", + "SandboxKey": "/var/run/docker/netns/e1c3bddb359d", + "Ports": {}, + "HairpinMode": false, + "LinkLocalIPv6Address": "", + "LinkLocalIPv6PrefixLen": 0, + "SecondaryIPAddresses": null, + "SecondaryIPv6Addresses": null, + "EndpointID": "2899f34f5f8b928619952dc32566d82bc121b033453f72e5de4a743feabc423b", + "Gateway": "172.17.0.1", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "IPAddress": "172.17.0.2", + "IPPrefixLen": 16, + "IPv6Gateway": "", + "MacAddress": "3e:94:61:83:1f:58", + "Networks": { + "bridge": { + "IPAMConfig": null, + "Links": null, + "Aliases": null, + "MacAddress": "3e:94:61:83:1f:58", + "DriverOpts": null, + "GwPriority": 0, + "NetworkID": "c4dd768ab4945e41ad23fe3907c960dac811141592a861cc40038df7086a1ce1", + "EndpointID": "2899f34f5f8b928619952dc32566d82bc121b033453f72e5de4a743feabc423b", + "Gateway": "172.17.0.1", + "IPAddress": "172.17.0.2", + "IPPrefixLen": 16, + "IPv6Gateway": "", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "DNSNames": null + } + } + } + } +] diff --git a/agent/agentcontainers/testdata/devcontainer_simple.json b/agent/agentcontainers/testdata/devcontainer_simple.json new file mode 100644 index 0000000000000..62d8c693d84fb --- /dev/null +++ b/agent/agentcontainers/testdata/devcontainer_simple.json @@ -0,0 +1,209 @@ +[ + { + "Id": "0b2a9fcf5727d9562943ce47d445019f4520e37a2aa7c6d9346d01af4f4f9aed", + "Created": "2025-03-11T17:01:05.751972661Z", + "Path": "/bin/sh", + "Args": [ + "-c", + "echo Container started\ntrap \"exit 0\" 15\n\nexec \"$@\"\nwhile sleep 1 & wait $!; do :; done", + "-" + ], + "State": { + "Status": "running", + "Running": true, + "Paused": false, + "Restarting": false, + "OOMKilled": false, + "Dead": false, + "Pid": 521929, + "ExitCode": 0, + "Error": "", + "StartedAt": "2025-03-11T17:01:06.002539252Z", + "FinishedAt": "0001-01-01T00:00:00Z" + }, + "Image": "sha256:d4ccddb816ba27eaae22ef3d56175d53f47998e2acb99df1ae0e5b426b28a076", + "ResolvConfPath": "/var/lib/docker/containers/0b2a9fcf5727d9562943ce47d445019f4520e37a2aa7c6d9346d01af4f4f9aed/resolv.conf", + "HostnamePath": "/var/lib/docker/containers/0b2a9fcf5727d9562943ce47d445019f4520e37a2aa7c6d9346d01af4f4f9aed/hostname", + "HostsPath": "/var/lib/docker/containers/0b2a9fcf5727d9562943ce47d445019f4520e37a2aa7c6d9346d01af4f4f9aed/hosts", + "LogPath": "/var/lib/docker/containers/0b2a9fcf5727d9562943ce47d445019f4520e37a2aa7c6d9346d01af4f4f9aed/0b2a9fcf5727d9562943ce47d445019f4520e37a2aa7c6d9346d01af4f4f9aed-json.log", + "Name": "/optimistic_hopper", + "RestartCount": 0, + "Driver": "overlay2", + "Platform": "linux", + "MountLabel": "", + "ProcessLabel": "", + "AppArmorProfile": "", + "ExecIDs": null, + "HostConfig": { + "Binds": null, + "ContainerIDFile": "", + "LogConfig": { + "Type": "json-file", + "Config": {} + }, + "NetworkMode": "bridge", + "PortBindings": {}, + "RestartPolicy": { + "Name": "no", + "MaximumRetryCount": 0 + }, + "AutoRemove": false, + "VolumeDriver": "", + "VolumesFrom": null, + "ConsoleSize": [ + 108, + 176 + ], + "CapAdd": null, + "CapDrop": null, + "CgroupnsMode": "private", + "Dns": [], + "DnsOptions": [], + "DnsSearch": [], + "ExtraHosts": null, + "GroupAdd": null, + "IpcMode": "private", + "Cgroup": "", + "Links": null, + "OomScoreAdj": 10, + "PidMode": "", + "Privileged": false, + "PublishAllPorts": false, + "ReadonlyRootfs": false, + "SecurityOpt": null, + "UTSMode": "", + "UsernsMode": "", + "ShmSize": 67108864, + "Runtime": "runc", + "Isolation": "", + "CpuShares": 0, + "Memory": 0, + "NanoCpus": 0, + "CgroupParent": "", + "BlkioWeight": 0, + "BlkioWeightDevice": [], + "BlkioDeviceReadBps": [], + "BlkioDeviceWriteBps": [], + "BlkioDeviceReadIOps": [], + "BlkioDeviceWriteIOps": [], + "CpuPeriod": 0, + "CpuQuota": 0, + "CpuRealtimePeriod": 0, + "CpuRealtimeRuntime": 0, + "CpusetCpus": "", + "CpusetMems": "", + "Devices": [], + "DeviceCgroupRules": null, + "DeviceRequests": null, + "MemoryReservation": 0, + "MemorySwap": 0, + "MemorySwappiness": null, + "OomKillDisable": null, + "PidsLimit": null, + "Ulimits": [], + "CpuCount": 0, + "CpuPercent": 0, + "IOMaximumIOps": 0, + "IOMaximumBandwidth": 0, + "MaskedPaths": [ + "/proc/asound", + "/proc/acpi", + "/proc/kcore", + "/proc/keys", + "/proc/latency_stats", + "/proc/timer_list", + "/proc/timer_stats", + "/proc/sched_debug", + "/proc/scsi", + "/sys/firmware", + "/sys/devices/virtual/powercap" + ], + "ReadonlyPaths": [ + "/proc/bus", + "/proc/fs", + "/proc/irq", + "/proc/sys", + "/proc/sysrq-trigger" + ] + }, + "GraphDriver": { + "Data": { + "ID": "0b2a9fcf5727d9562943ce47d445019f4520e37a2aa7c6d9346d01af4f4f9aed", + "LowerDir": "/var/lib/docker/overlay2/b698fd9f03f25014d4936cdc64ed258342fe685f0dfd8813ed6928dd6de75219-init/diff:/var/lib/docker/overlay2/4b4c37dfbdc0dc01b68d4fb1ddb86109398a2d73555439b874dbd23b87cd5c4b/diff", + "MergedDir": "/var/lib/docker/overlay2/b698fd9f03f25014d4936cdc64ed258342fe685f0dfd8813ed6928dd6de75219/merged", + "UpperDir": "/var/lib/docker/overlay2/b698fd9f03f25014d4936cdc64ed258342fe685f0dfd8813ed6928dd6de75219/diff", + "WorkDir": "/var/lib/docker/overlay2/b698fd9f03f25014d4936cdc64ed258342fe685f0dfd8813ed6928dd6de75219/work" + }, + "Name": "overlay2" + }, + "Mounts": [], + "Config": { + "Hostname": "0b2a9fcf5727", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "Cmd": [ + "-c", + "echo Container started\ntrap \"exit 0\" 15\n\nexec \"$@\"\nwhile sleep 1 & wait $!; do :; done", + "-" + ], + "Image": "debian:bookworm", + "Volumes": null, + "WorkingDir": "", + "Entrypoint": [ + "/bin/sh" + ], + "OnBuild": null, + "Labels": { + "devcontainer.config_file": "/home/coder/src/coder/coder/agent/agentcontainers/testdata/devcontainer_simple.json", + "devcontainer.metadata": "[]" + } + }, + "NetworkSettings": { + "Bridge": "", + "SandboxID": "25a29a57c1330e0d0d2342af6e3291ffd3e812aca1a6e3f6a1630e74b73d0fc6", + "SandboxKey": "/var/run/docker/netns/25a29a57c133", + "Ports": {}, + "HairpinMode": false, + "LinkLocalIPv6Address": "", + "LinkLocalIPv6PrefixLen": 0, + "SecondaryIPAddresses": null, + "SecondaryIPv6Addresses": null, + "EndpointID": "5c5ebda526d8fca90e841886ea81b77d7cc97fed56980c2aa89d275b84af7df2", + "Gateway": "172.17.0.1", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "IPAddress": "172.17.0.2", + "IPPrefixLen": 16, + "IPv6Gateway": "", + "MacAddress": "32:b6:d9:ab:c3:61", + "Networks": { + "bridge": { + "IPAMConfig": null, + "Links": null, + "Aliases": null, + "MacAddress": "32:b6:d9:ab:c3:61", + "DriverOpts": null, + "GwPriority": 0, + "NetworkID": "c4dd768ab4945e41ad23fe3907c960dac811141592a861cc40038df7086a1ce1", + "EndpointID": "5c5ebda526d8fca90e841886ea81b77d7cc97fed56980c2aa89d275b84af7df2", + "Gateway": "172.17.0.1", + "IPAddress": "172.17.0.2", + "IPPrefixLen": 16, + "IPv6Gateway": "", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "DNSNames": null + } + } + } + } +] From 55998d07564f1d2c2d1b832fe4964483812d0868 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Tue, 11 Mar 2025 21:02:35 +0000 Subject: [PATCH 03/12] fix(agent/agentcontainers): fix incorrectly parsed port --- agent/agentcontainers/containers_dockercli.go | 60 +++++++++++++------ .../containers_internal_test.go | 27 +++++---- 2 files changed, 57 insertions(+), 30 deletions(-) diff --git a/agent/agentcontainers/containers_dockercli.go b/agent/agentcontainers/containers_dockercli.go index 1f1e0e041b8d2..5bdc0a5d99569 100644 --- a/agent/agentcontainers/containers_dockercli.go +++ b/agent/agentcontainers/containers_dockercli.go @@ -319,13 +319,13 @@ func runDockerInspect(ctx context.Context, execer agentexec.Execer, ids ...strin // To avoid a direct dependency on the Docker API, we use the docker CLI // to fetch information about containers. type dockerInspect struct { - ID string `json:"Id"` - Created time.Time `json:"Created"` - Config dockerInspectConfig `json:"Config"` - HostConfig dockerInspectHostConfig `json:"HostConfig"` - Name string `json:"Name"` - Mounts []dockerInspectMount `json:"Mounts"` - State dockerInspectState `json:"State"` + ID string `json:"Id"` + Created time.Time `json:"Created"` + Config dockerInspectConfig `json:"Config"` + Name string `json:"Name"` + Mounts []dockerInspectMount `json:"Mounts"` + State dockerInspectState `json:"State"` + NetworkSettings dockerInspectNetworkSettings `json:"NetworkSettings"` } type dockerInspectConfig struct { @@ -333,8 +333,9 @@ type dockerInspectConfig struct { Labels map[string]string `json:"Labels"` } -type dockerInspectHostConfig struct { - PortBindings map[string]any `json:"PortBindings"` +type dockerInspectPort struct { + HostIP string `json:"HostIp"` + HostPort string `json:"HostPort"` } type dockerInspectMount struct { @@ -349,6 +350,10 @@ type dockerInspectState struct { Error string `json:"Error"` } +type dockerInspectNetworkSettings struct { + Ports map[string][]dockerInspectPort `json:"Ports"` +} + func (dis dockerInspectState) String() string { if dis.Running { return "running" @@ -388,20 +393,41 @@ func convertDockerInspect(raw string) ([]codersdk.WorkspaceAgentDevcontainer, [] Volumes: make(map[string]string, len(in.Mounts)), } - if in.HostConfig.PortBindings == nil { - in.HostConfig.PortBindings = make(map[string]any) + if in.NetworkSettings.Ports == nil { + in.NetworkSettings.Ports = make(map[string][]dockerInspectPort) } - portKeys := maps.Keys(in.HostConfig.PortBindings) + portKeys := maps.Keys(in.NetworkSettings.Ports) // 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 { + // Each port binding may have multiple entries mapped to the same interface. + // Keep track of the ports we've already seen. + seen := make(map[int]struct{}, len(in.NetworkSettings.Ports)) + for _, pk := range portKeys { + for _, p := range in.NetworkSettings.Ports[pk] { + _, network, err := convertDockerPort(pk) + if err != nil { + warns = append(warns, fmt.Sprintf("convert docker port: %s", err.Error())) + // Default network to "tcp" if we can't parse it. + network = "tcp" + } + hp, err := strconv.Atoi(p.HostPort) + if err != nil { + warns = append(warns, fmt.Sprintf("convert docker port: %s", err.Error())) + continue + } + if hp > 65535 || hp < 0 { // invalid port + warns = append(warns, fmt.Sprintf("convert docker port: invalid host port %d", hp)) + continue + } + if _, ok := seen[hp]; ok { + // We've already seen this port, so skip it. + continue + } out.Ports = append(out.Ports, codersdk.WorkspaceAgentListeningPort{ Network: network, - Port: port, + Port: uint16(hp), }) + seen[hp] = struct{}{} } } diff --git a/agent/agentcontainers/containers_internal_test.go b/agent/agentcontainers/containers_internal_test.go index ac1bc69ae3b01..8909e8cff77c0 100644 --- a/agent/agentcontainers/containers_internal_test.go +++ b/agent/agentcontainers/containers_internal_test.go @@ -357,7 +357,7 @@ func TestConvertDockerPort(t *testing.T) { expectError: "invalid port", }, } { - tc := tc // not needed anymore but makes the linter happy + //nolint: paralleltest // variable recapture no longer required t.Run(tc.name, func(t *testing.T) { t.Parallel() actualPort, actualNetwork, actualErr := convertDockerPort(tc.in) @@ -511,7 +511,7 @@ func TestConvertDockerInspect(t *testing.T) { Ports: []codersdk.WorkspaceAgentListeningPort{ { Network: "tcp", - Port: 23456, + Port: 12345, }, }, Volumes: map[string]string{}, @@ -548,10 +548,10 @@ func TestConvertDockerInspect(t *testing.T) { "devcontainer.config_file": "/home/coder/src/coder/coder/agent/agentcontainers/testdata/devcontainer_simple.json", "devcontainer.metadata": "[]", }, - Running: true, - Status: "running", - Ports: []codersdk.WorkspaceAgentListeningPort{}, - Volumes: map[string]string{}, + Running: true, + Status: "running", + Ports: []codersdk.WorkspaceAgentListeningPort{}, + Volumes: map[string]string{}, }, }, }, @@ -567,10 +567,10 @@ func TestConvertDockerInspect(t *testing.T) { "devcontainer.config_file": "/home/coder/src/coder/coder/agent/agentcontainers/testdata/devcontainer_forwardport.json", "devcontainer.metadata": "[]", }, - Running: true, - Status: "running", - Ports: []codersdk.WorkspaceAgentListeningPort{}, - Volumes: map[string]string{}, + Running: true, + Status: "running", + Ports: []codersdk.WorkspaceAgentListeningPort{}, + Volumes: map[string]string{}, }, }, }, @@ -586,12 +586,13 @@ func TestConvertDockerInspect(t *testing.T) { "devcontainer.config_file": "/home/coder/src/coder/coder/agent/agentcontainers/testdata/devcontainer_appport.json", "devcontainer.metadata": "[]", }, - Running: true, - Status: "running", + Running: true, + Status: "running", Ports: []codersdk.WorkspaceAgentListeningPort{ { Network: "tcp", - Port: 8080, + // Container port 8080 is mapped to 32768 on the host. + Port: 32768, }, }, Volumes: map[string]string{}, From fb78d335047225208ee8deccd61cf89c514e70fe Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 12 Mar 2025 10:35:11 +0000 Subject: [PATCH 04/12] nolint paralleltest --- agent/agentcontainers/containers_internal_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/agent/agentcontainers/containers_internal_test.go b/agent/agentcontainers/containers_internal_test.go index 8909e8cff77c0..a53ade10b9518 100644 --- a/agent/agentcontainers/containers_internal_test.go +++ b/agent/agentcontainers/containers_internal_test.go @@ -311,6 +311,7 @@ func TestContainersHandler(t *testing.T) { func TestConvertDockerPort(t *testing.T) { t.Parallel() + //nolint:paralleltest // variable recapture no longer required for _, tc := range []struct { name string in string @@ -419,6 +420,7 @@ func TestConvertDockerVolume(t *testing.T) { func TestConvertDockerInspect(t *testing.T) { t.Parallel() + //nolint:paralleltest // variable recapture no longer required for _, tt := range []struct { name string expect []codersdk.WorkspaceAgentDevcontainer From a7d1ea45e5716ecddaef809624ca7fbc333138a9 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 12 Mar 2025 16:11:02 +0000 Subject: [PATCH 05/12] chore: adjust testdata structure --- agent/agentcontainers/containers_internal_test.go | 2 +- .../docker_inspect.json} | 0 .../docker_inspect.json} | 0 .../docker_inspect.json} | 0 .../docker_inspect.json} | 0 .../docker_inspect.json} | 0 .../docker_inspect.json} | 0 .../docker_inspect.json} | 0 .../docker_inspect.json} | 0 .../docker_inspect.json} | 0 10 files changed, 1 insertion(+), 1 deletion(-) rename agent/agentcontainers/testdata/{container_binds.json => container_binds/docker_inspect.json} (100%) rename agent/agentcontainers/testdata/{container_differentport.json => container_differentport/docker_inspect.json} (100%) rename agent/agentcontainers/testdata/{container_labels.json => container_labels/docker_inspect.json} (100%) rename agent/agentcontainers/testdata/{container_sameport.json => container_sameport/docker_inspect.json} (100%) rename agent/agentcontainers/testdata/{container_simple.json => container_simple/docker_inspect.json} (100%) rename agent/agentcontainers/testdata/{container_volume.json => container_volume/docker_inspect.json} (100%) rename agent/agentcontainers/testdata/{devcontainer_appport.json => devcontainer_appport/docker_inspect.json} (100%) rename agent/agentcontainers/testdata/{devcontainer_forwardport.json => devcontainer_forwardport/docker_inspect.json} (100%) rename agent/agentcontainers/testdata/{devcontainer_simple.json => devcontainer_simple/docker_inspect.json} (100%) diff --git a/agent/agentcontainers/containers_internal_test.go b/agent/agentcontainers/containers_internal_test.go index a53ade10b9518..5d4c010718a55 100644 --- a/agent/agentcontainers/containers_internal_test.go +++ b/agent/agentcontainers/containers_internal_test.go @@ -605,7 +605,7 @@ func TestConvertDockerInspect(t *testing.T) { // nolint:paralleltest // variable recapture no longer required t.Run(tt.name, func(t *testing.T) { t.Parallel() - bs, err := os.ReadFile(filepath.Join("testdata", tt.name+".json")) + bs, err := os.ReadFile(filepath.Join("testdata", tt.name, "docker_inspect.json")) require.NoError(t, err, "failed to read testdata file") actual, warns, err := convertDockerInspect(string(bs)) if len(tt.expectWarns) > 0 { diff --git a/agent/agentcontainers/testdata/container_binds.json b/agent/agentcontainers/testdata/container_binds/docker_inspect.json similarity index 100% rename from agent/agentcontainers/testdata/container_binds.json rename to agent/agentcontainers/testdata/container_binds/docker_inspect.json diff --git a/agent/agentcontainers/testdata/container_differentport.json b/agent/agentcontainers/testdata/container_differentport/docker_inspect.json similarity index 100% rename from agent/agentcontainers/testdata/container_differentport.json rename to agent/agentcontainers/testdata/container_differentport/docker_inspect.json diff --git a/agent/agentcontainers/testdata/container_labels.json b/agent/agentcontainers/testdata/container_labels/docker_inspect.json similarity index 100% rename from agent/agentcontainers/testdata/container_labels.json rename to agent/agentcontainers/testdata/container_labels/docker_inspect.json diff --git a/agent/agentcontainers/testdata/container_sameport.json b/agent/agentcontainers/testdata/container_sameport/docker_inspect.json similarity index 100% rename from agent/agentcontainers/testdata/container_sameport.json rename to agent/agentcontainers/testdata/container_sameport/docker_inspect.json diff --git a/agent/agentcontainers/testdata/container_simple.json b/agent/agentcontainers/testdata/container_simple/docker_inspect.json similarity index 100% rename from agent/agentcontainers/testdata/container_simple.json rename to agent/agentcontainers/testdata/container_simple/docker_inspect.json diff --git a/agent/agentcontainers/testdata/container_volume.json b/agent/agentcontainers/testdata/container_volume/docker_inspect.json similarity index 100% rename from agent/agentcontainers/testdata/container_volume.json rename to agent/agentcontainers/testdata/container_volume/docker_inspect.json diff --git a/agent/agentcontainers/testdata/devcontainer_appport.json b/agent/agentcontainers/testdata/devcontainer_appport/docker_inspect.json similarity index 100% rename from agent/agentcontainers/testdata/devcontainer_appport.json rename to agent/agentcontainers/testdata/devcontainer_appport/docker_inspect.json diff --git a/agent/agentcontainers/testdata/devcontainer_forwardport.json b/agent/agentcontainers/testdata/devcontainer_forwardport/docker_inspect.json similarity index 100% rename from agent/agentcontainers/testdata/devcontainer_forwardport.json rename to agent/agentcontainers/testdata/devcontainer_forwardport/docker_inspect.json diff --git a/agent/agentcontainers/testdata/devcontainer_simple.json b/agent/agentcontainers/testdata/devcontainer_simple/docker_inspect.json similarity index 100% rename from agent/agentcontainers/testdata/devcontainer_simple.json rename to agent/agentcontainers/testdata/devcontainer_simple/docker_inspect.json From 393f6e98860f46fab16a02c9b7ffde3011994ea3 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 12 Mar 2025 16:16:46 +0000 Subject: [PATCH 06/12] use a []byte instead of a string --- agent/agentcontainers/containers_dockercli.go | 20 +++++++++---------- .../containers_internal_test.go | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/agent/agentcontainers/containers_dockercli.go b/agent/agentcontainers/containers_dockercli.go index 5bdc0a5d99569..6b5741297dbff 100644 --- a/agent/agentcontainers/containers_dockercli.go +++ b/agent/agentcontainers/containers_dockercli.go @@ -279,16 +279,16 @@ func (dcl *DockerCLILister) List(ctx context.Context) (codersdk.WorkspaceAgentLi // will still contain valid JSON. We will just end up missing // information about the removed container. We could potentially // log this error, but I'm not sure it's worth it. - ins, dockerInspectStderr, err := runDockerInspect(ctx, dcl.execer, ids...) + dockerInspectStdout, dockerInspectStderr, err := runDockerInspect(ctx, dcl.execer, ids...) if err != nil { return codersdk.WorkspaceAgentListContainersResponse{}, xerrors.Errorf("run docker inspect: %w", err) } - if dockerInspectStderr != "" { - res.Warnings = append(res.Warnings, dockerInspectStderr) + if len(dockerInspectStderr) > 0 { + res.Warnings = append(res.Warnings, string(dockerInspectStderr)) } - outs, warns, err := convertDockerInspect(ins) + outs, warns, err := convertDockerInspect(dockerInspectStdout) if err != nil { return codersdk.WorkspaceAgentListContainersResponse{}, xerrors.Errorf("convert docker inspect output: %w", err) } @@ -301,19 +301,19 @@ func (dcl *DockerCLILister) List(ctx context.Context) (codersdk.WorkspaceAgentLi // 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) (stdout, stderr string, err error) { +func runDockerInspect(ctx context.Context, execer agentexec.Execer, ids ...string) (stdout, stderr []byte, 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() - stdout = strings.TrimSpace(stdoutBuf.String()) - stderr = strings.TrimSpace(stderrBuf.String()) + stdout = bytes.TrimSpace(stdoutBuf.Bytes()) + stderr = bytes.TrimSpace(stderrBuf.Bytes()) if err != nil { return stdout, stderr, err } - return stdoutBuf.String(), stderr, nil + return stdout, stderr, nil } // To avoid a direct dependency on the Docker API, we use the docker CLI @@ -371,10 +371,10 @@ func (dis dockerInspectState) String() string { return sb.String() } -func convertDockerInspect(raw string) ([]codersdk.WorkspaceAgentDevcontainer, []string, error) { +func convertDockerInspect(raw []byte) ([]codersdk.WorkspaceAgentDevcontainer, []string, error) { var warns []string var ins []dockerInspect - if err := json.NewDecoder(strings.NewReader(raw)).Decode(&ins); err != nil { + if err := json.NewDecoder(bytes.NewReader(raw)).Decode(&ins); err != nil { return nil, nil, xerrors.Errorf("decode docker inspect output: %w", err) } outs := make([]codersdk.WorkspaceAgentDevcontainer, 0, len(ins)) diff --git a/agent/agentcontainers/containers_internal_test.go b/agent/agentcontainers/containers_internal_test.go index 5d4c010718a55..c42583d56d9dc 100644 --- a/agent/agentcontainers/containers_internal_test.go +++ b/agent/agentcontainers/containers_internal_test.go @@ -607,7 +607,7 @@ func TestConvertDockerInspect(t *testing.T) { t.Parallel() bs, err := os.ReadFile(filepath.Join("testdata", tt.name, "docker_inspect.json")) require.NoError(t, err, "failed to read testdata file") - actual, warns, err := convertDockerInspect(string(bs)) + actual, warns, err := convertDockerInspect(bs) if len(tt.expectWarns) > 0 { assert.Len(t, warns, len(tt.expectWarns), "expected warnings") for _, warn := range tt.expectWarns { From 95b156ec142cfcf1cd474e68346a33b85ca0eafe Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 13 Mar 2025 17:00:18 +0000 Subject: [PATCH 07/12] fix(agent/agentcontainers): create new WorkspaceAgentDevcontainerPort type, return container and host port --- agent/agentcontainers/containers_dockercli.go | 68 ++++++++++---- .../containers_internal_test.go | 89 +++++++++++++++---- .../docker_inspect.json | 51 +++++++++++ coderd/apidoc/docs.go | 23 ++++- coderd/apidoc/swagger.json | 23 ++++- codersdk/workspaceagents.go | 15 +++- docs/reference/api/agents.md | 5 +- docs/reference/api/schemas.md | 56 ++++++++---- site/src/api/typesGenerated.ts | 10 ++- 9 files changed, 283 insertions(+), 57 deletions(-) create mode 100644 agent/agentcontainers/testdata/container_sameportdiffip/docker_inspect.json diff --git a/agent/agentcontainers/containers_dockercli.go b/agent/agentcontainers/containers_dockercli.go index 6b5741297dbff..fe52bac52366f 100644 --- a/agent/agentcontainers/containers_dockercli.go +++ b/agent/agentcontainers/containers_dockercli.go @@ -6,6 +6,7 @@ import ( "context" "encoding/json" "fmt" + "net" "os/user" "slices" "sort" @@ -379,6 +380,19 @@ func convertDockerInspect(raw []byte) ([]codersdk.WorkspaceAgentDevcontainer, [] } outs := make([]codersdk.WorkspaceAgentDevcontainer, 0, len(ins)) + // Say you have two containers: + // - Container A with Host IP 127.0.0.1:8000 mapped to container port 8001 + // - Container B with Host IP [::1]:8000 mapped to container port 8001 + // A request to localhost:8000 may be routed to either container. + // We don't know which one for sure, so we need to surface this to the user. + // Keep track of all host ports we see. If we see the same host port + // mapped to multiple containers on different host IPs, we need to + // warn the user about this. + // Note that we only do this for loopback or unspecified IPs. + // We'll assume that the user knows what they're doing if they bind to + // a specific IP address. + hostPortContainers := make(map[int][]string) + for _, in := range ins { out := codersdk.WorkspaceAgentDevcontainer{ CreatedAt: in.Created, @@ -387,7 +401,7 @@ func convertDockerInspect(raw []byte) ([]codersdk.WorkspaceAgentDevcontainer, [] ID: in.ID, Image: in.Config.Image, Labels: in.Config.Labels, - Ports: make([]codersdk.WorkspaceAgentListeningPort, 0), + Ports: make([]codersdk.WorkspaceAgentDevcontainerPort, 0), Running: in.State.Running, Status: in.State.String(), Volumes: make(map[string]string, len(in.Mounts)), @@ -399,12 +413,12 @@ func convertDockerInspect(raw []byte) ([]codersdk.WorkspaceAgentDevcontainer, [] portKeys := maps.Keys(in.NetworkSettings.Ports) // Sort the ports for deterministic output. sort.Strings(portKeys) - // Each port binding may have multiple entries mapped to the same interface. - // Keep track of the ports we've already seen. - seen := make(map[int]struct{}, len(in.NetworkSettings.Ports)) + // If we see the same port bound to both ipv4 and ipv6 loopback or unspecified + // interfaces to the same container port, there is no point in adding it multiple times. + loopbackHostPortContainerPorts := make(map[int]uint16, 0) for _, pk := range portKeys { for _, p := range in.NetworkSettings.Ports[pk] { - _, network, err := convertDockerPort(pk) + cp, network, err := convertDockerPort(pk) if err != nil { warns = append(warns, fmt.Sprintf("convert docker port: %s", err.Error())) // Default network to "tcp" if we can't parse it. @@ -412,22 +426,30 @@ func convertDockerInspect(raw []byte) ([]codersdk.WorkspaceAgentDevcontainer, [] } hp, err := strconv.Atoi(p.HostPort) if err != nil { - warns = append(warns, fmt.Sprintf("convert docker port: %s", err.Error())) + warns = append(warns, fmt.Sprintf("convert docker host port: %s", err.Error())) continue } - if hp > 65535 || hp < 0 { // invalid port - warns = append(warns, fmt.Sprintf("convert docker port: invalid host port %d", hp)) + if hp > 65535 || hp < 1 { // invalid port + warns = append(warns, fmt.Sprintf("convert docker host port: invalid host port %d", hp)) continue } - if _, ok := seen[hp]; ok { - // We've already seen this port, so skip it. - continue + + // Deduplicate host ports for loopback and unspecified IPs. + if isLoopbackOrUnspecified(p.HostIP) { + if found, ok := loopbackHostPortContainerPorts[hp]; ok && found == cp { + // We've already seen this port, so skip it. + continue + } + loopbackHostPortContainerPorts[hp] = cp + // Also keep track of the host port and the container ID. + hostPortContainers[hp] = append(hostPortContainers[hp], in.ID) } - out.Ports = append(out.Ports, codersdk.WorkspaceAgentListeningPort{ - Network: network, - Port: uint16(hp), + out.Ports = append(out.Ports, codersdk.WorkspaceAgentDevcontainerPort{ + Network: network, + Port: cp, + HostPort: uint16(hp), + HostIP: p.HostIP, }) - seen[hp] = struct{}{} } } @@ -444,6 +466,13 @@ func convertDockerInspect(raw []byte) ([]codersdk.WorkspaceAgentDevcontainer, [] outs = append(outs, out) } + // Check if any host ports are mapped to multiple containers. + for hp, ids := range hostPortContainers { + if len(ids) > 1 { + warns = append(warns, fmt.Sprintf("host port %d is mapped to multiple containers on different interfaces: %s", hp, strings.Join(ids, ", "))) + } + } + return outs, warns, nil } @@ -471,3 +500,12 @@ func convertDockerPort(in string) (uint16, string, error) { return 0, "", xerrors.Errorf("invalid port format: %s", in) } } + +// convenience function to check if an IP address is loopback or unspecified +func isLoopbackOrUnspecified(ips string) bool { + nip := net.ParseIP(ips) + if nip == nil { + return false // technically correct, I suppose + } + return nip.IsLoopback() || nip.IsUnspecified() +} diff --git a/agent/agentcontainers/containers_internal_test.go b/agent/agentcontainers/containers_internal_test.go index c42583d56d9dc..8807c89bbfd8c 100644 --- a/agent/agentcontainers/containers_internal_test.go +++ b/agent/agentcontainers/containers_internal_test.go @@ -2,6 +2,7 @@ package agentcontainers import ( "fmt" + "math/rand" "os" "path/filepath" "slices" @@ -438,7 +439,7 @@ func TestConvertDockerInspect(t *testing.T) { Labels: map[string]string{}, Running: true, Status: "running", - Ports: []codersdk.WorkspaceAgentListeningPort{}, + Ports: []codersdk.WorkspaceAgentDevcontainerPort{}, Volumes: map[string]string{}, }, }, @@ -454,7 +455,7 @@ func TestConvertDockerInspect(t *testing.T) { Labels: map[string]string{"baz": "zap", "foo": "bar"}, Running: true, Status: "running", - Ports: []codersdk.WorkspaceAgentListeningPort{}, + Ports: []codersdk.WorkspaceAgentDevcontainerPort{}, Volumes: map[string]string{}, }, }, @@ -470,7 +471,7 @@ func TestConvertDockerInspect(t *testing.T) { Labels: map[string]string{}, Running: true, Status: "running", - Ports: []codersdk.WorkspaceAgentListeningPort{}, + Ports: []codersdk.WorkspaceAgentDevcontainerPort{}, Volumes: map[string]string{ "/tmp/test/a": "/var/coder/a", "/tmp/test/b": "/var/coder/b", @@ -489,10 +490,12 @@ func TestConvertDockerInspect(t *testing.T) { Labels: map[string]string{}, Running: true, Status: "running", - Ports: []codersdk.WorkspaceAgentListeningPort{ + Ports: []codersdk.WorkspaceAgentDevcontainerPort{ { - Network: "tcp", - Port: 12345, + Network: "tcp", + Port: 12345, + HostPort: 12345, + HostIP: "0.0.0.0", }, }, Volumes: map[string]string{}, @@ -510,16 +513,60 @@ func TestConvertDockerInspect(t *testing.T) { Labels: map[string]string{}, Running: true, Status: "running", - Ports: []codersdk.WorkspaceAgentListeningPort{ + Ports: []codersdk.WorkspaceAgentDevcontainerPort{ { - Network: "tcp", - Port: 12345, + Network: "tcp", + Port: 23456, + HostPort: 12345, + HostIP: "0.0.0.0", }, }, Volumes: map[string]string{}, }, }, }, + { + name: "container_sameportdiffip", + expect: []codersdk.WorkspaceAgentDevcontainer{ + { + CreatedAt: time.Date(2025, 3, 11, 17, 56, 34, 842164541, time.UTC), + ID: "a", + FriendlyName: "a", + Image: "debian:bookworm", + Labels: map[string]string{}, + Running: true, + Status: "running", + Ports: []codersdk.WorkspaceAgentDevcontainerPort{ + { + Network: "tcp", + Port: 8001, + HostPort: 8000, + HostIP: "0.0.0.0", + }, + }, + Volumes: map[string]string{}, + }, + { + CreatedAt: time.Date(2025, 3, 11, 17, 56, 34, 842164541, time.UTC), + ID: "b", + FriendlyName: "b", + Image: "debian:bookworm", + Labels: map[string]string{}, + Running: true, + Status: "running", + Ports: []codersdk.WorkspaceAgentDevcontainerPort{ + { + Network: "tcp", + Port: 8001, + HostPort: 8000, + HostIP: "::", + }, + }, + Volumes: map[string]string{}, + }, + }, + expectWarns: []string{"host port 8000 is mapped to multiple containers on different interfaces: a, b"}, + }, { name: "container_volume", expect: []codersdk.WorkspaceAgentDevcontainer{ @@ -531,7 +578,7 @@ func TestConvertDockerInspect(t *testing.T) { Labels: map[string]string{}, Running: true, Status: "running", - Ports: []codersdk.WorkspaceAgentListeningPort{}, + Ports: []codersdk.WorkspaceAgentDevcontainerPort{}, Volumes: map[string]string{ "/var/lib/docker/volumes/testvol/_data": "/testvol", }, @@ -552,7 +599,7 @@ func TestConvertDockerInspect(t *testing.T) { }, Running: true, Status: "running", - Ports: []codersdk.WorkspaceAgentListeningPort{}, + Ports: []codersdk.WorkspaceAgentDevcontainerPort{}, Volumes: map[string]string{}, }, }, @@ -571,7 +618,7 @@ func TestConvertDockerInspect(t *testing.T) { }, Running: true, Status: "running", - Ports: []codersdk.WorkspaceAgentListeningPort{}, + Ports: []codersdk.WorkspaceAgentDevcontainerPort{}, Volumes: map[string]string{}, }, }, @@ -590,11 +637,12 @@ func TestConvertDockerInspect(t *testing.T) { }, Running: true, Status: "running", - Ports: []codersdk.WorkspaceAgentListeningPort{ + Ports: []codersdk.WorkspaceAgentDevcontainerPort{ { - Network: "tcp", - // Container port 8080 is mapped to 32768 on the host. - Port: 32768, + Network: "tcp", + Port: 8080, + HostPort: 32768, + HostIP: "0.0.0.0", }, }, Volumes: map[string]string{}, @@ -771,10 +819,13 @@ func fakeContainer(t *testing.T, mut ...func(*codersdk.WorkspaceAgentDevcontaine testutil.GetRandomName(t): testutil.GetRandomName(t), }, Running: true, - Ports: []codersdk.WorkspaceAgentListeningPort{ + Ports: []codersdk.WorkspaceAgentDevcontainerPort{ { - Network: "tcp", - Port: testutil.RandomPortNoListen(t), + Network: "tcp", + Port: testutil.RandomPortNoListen(t), + HostPort: testutil.RandomPortNoListen(t), + //nolint:gosec // this is a test + HostIP: []string{"127.0.0.1", "[::1]", "localhost", "0.0.0.0", "[::]", testutil.GetRandomName(t)}[rand.Intn(6)], }, }, Status: testutil.MustRandString(t, 10), diff --git a/agent/agentcontainers/testdata/container_sameportdiffip/docker_inspect.json b/agent/agentcontainers/testdata/container_sameportdiffip/docker_inspect.json new file mode 100644 index 0000000000000..f50e6fa12ec3f --- /dev/null +++ b/agent/agentcontainers/testdata/container_sameportdiffip/docker_inspect.json @@ -0,0 +1,51 @@ +[ + { + "Id": "a", + "Created": "2025-03-11T17:56:34.842164541Z", + "State": { + "Running": true, + "ExitCode": 0, + "Error": "" + }, + "Name": "/a", + "Mounts": [], + "Config": { + "Image": "debian:bookworm", + "Labels": {} + }, + "NetworkSettings": { + "Ports": { + "8001/tcp": [ + { + "HostIp": "0.0.0.0", + "HostPort": "8000" + } + ] + } + } + }, + { + "Id": "b", + "Created": "2025-03-11T17:56:34.842164541Z", + "State": { + "Running": true, + "ExitCode": 0, + "Error": "" + }, + "Name": "/b", + "Config": { + "Image": "debian:bookworm", + "Labels": {} + }, + "NetworkSettings": { + "Ports": { + "8001/tcp": [ + { + "HostIp": "::", + "HostPort": "8000" + } + ] + } + } + } +] diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 0fd3d1165ed8e..a646917e74ece 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -16005,7 +16005,7 @@ const docTemplate = `{ "description": "Ports includes ports exposed by the container.", "type": "array", "items": { - "$ref": "#/definitions/codersdk.WorkspaceAgentListeningPort" + "$ref": "#/definitions/codersdk.WorkspaceAgentDevcontainerPort" } }, "running": { @@ -16025,6 +16025,27 @@ const docTemplate = `{ } } }, + "codersdk.WorkspaceAgentDevcontainerPort": { + "type": "object", + "properties": { + "host_ip": { + "description": "HostIP is the IP address of the host interface to which the port is\nbound. Note that this can be an IPv4 or IPv6 address.", + "type": "string" + }, + "host_port": { + "description": "HostPort is the port number *outside* the container.", + "type": "integer" + }, + "network": { + "description": "Network is the network protocol used by the port (tcp, udp, etc).", + "type": "string" + }, + "port": { + "description": "Port is the port number *inside* the container.", + "type": "integer" + } + } + }, "codersdk.WorkspaceAgentHealth": { "type": "object", "properties": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 21546acb32ab3..4a2f77b813cb1 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -14590,7 +14590,7 @@ "description": "Ports includes ports exposed by the container.", "type": "array", "items": { - "$ref": "#/definitions/codersdk.WorkspaceAgentListeningPort" + "$ref": "#/definitions/codersdk.WorkspaceAgentDevcontainerPort" } }, "running": { @@ -14610,6 +14610,27 @@ } } }, + "codersdk.WorkspaceAgentDevcontainerPort": { + "type": "object", + "properties": { + "host_ip": { + "description": "HostIP is the IP address of the host interface to which the port is\nbound. Note that this can be an IPv4 or IPv6 address.", + "type": "string" + }, + "host_port": { + "description": "HostPort is the port number *outside* the container.", + "type": "integer" + }, + "network": { + "description": "Network is the network protocol used by the port (tcp, udp, etc).", + "type": "string" + }, + "port": { + "description": "Port is the port number *inside* the container.", + "type": "integer" + } + } + }, "codersdk.WorkspaceAgentHealth": { "type": "object", "properties": { diff --git a/codersdk/workspaceagents.go b/codersdk/workspaceagents.go index 8e2209fa8072b..2e481c20602b4 100644 --- a/codersdk/workspaceagents.go +++ b/codersdk/workspaceagents.go @@ -410,7 +410,7 @@ type WorkspaceAgentDevcontainer struct { // Running is true if the container is currently running. Running bool `json:"running"` // Ports includes ports exposed by the container. - Ports []WorkspaceAgentListeningPort `json:"ports"` + Ports []WorkspaceAgentDevcontainerPort `json:"ports"` // Status is the current status of the container. This is somewhat // implementation-dependent, but should generally be a human-readable // string. @@ -420,6 +420,19 @@ type WorkspaceAgentDevcontainer struct { Volumes map[string]string `json:"volumes"` } +// WorkspaceAgentDevcontainerPort describes a port as exposed by a container. +type WorkspaceAgentDevcontainerPort struct { + // Port is the port number *inside* the container. + Port uint16 `json:"port"` + // Network is the network protocol used by the port (tcp, udp, etc). + Network string `json:"network"` + // HostIP is the IP address of the host interface to which the port is + // bound. Note that this can be an IPv4 or IPv6 address. + HostIP string `json:"host_ip,omitempty"` + // HostPort is the port number *outside* the container. + HostPort uint16 `json:"host_port,omitempty"` +} + // WorkspaceAgentListContainersResponse is the response to the list containers // request. type WorkspaceAgentListContainersResponse struct { diff --git a/docs/reference/api/agents.md b/docs/reference/api/agents.md index 38e30c35e18cd..ec996e9f57d7d 100644 --- a/docs/reference/api/agents.md +++ b/docs/reference/api/agents.md @@ -676,9 +676,10 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/{workspaceagent}/con "name": "string", "ports": [ { + "host_ip": "string", + "host_port": 0, "network": "string", - "port": 0, - "process_name": "string" + "port": 0 } ], "running": true, diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 42ef8a7ade184..059628e343945 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -7732,9 +7732,10 @@ If the schedule is empty, the user will be updated to use the default schedule.| "name": "string", "ports": [ { + "host_ip": "string", + "host_port": 0, "network": "string", - "port": 0, - "process_name": "string" + "port": 0 } ], "running": true, @@ -7748,19 +7749,39 @@ If the schedule is empty, the user will be updated to use the default schedule.| ### Properties -| Name | Type | Required | Restrictions | Description | -|--------------------|---------------------------------------------------------------------------------------|----------|--------------|--------------------------------------------------------------------------------------------------------------------------------------------| -| `created_at` | string | false | | Created at is the time the container was created. | -| `id` | string | false | | ID is the unique identifier of the container. | -| `image` | string | false | | Image is the name of the container image. | -| `labels` | object | false | | Labels is a map of key-value pairs of container labels. | -| » `[any property]` | string | false | | | -| `name` | string | false | | Name is the human-readable name of the container. | -| `ports` | array of [codersdk.WorkspaceAgentListeningPort](#codersdkworkspaceagentlisteningport) | false | | Ports includes ports exposed by the container. | -| `running` | boolean | false | | Running is true if the container is currently running. | -| `status` | string | false | | Status is the current status of the container. This is somewhat implementation-dependent, but should generally be a human-readable string. | -| `volumes` | object | false | | Volumes is a map of "things" mounted into the container. Again, this is somewhat implementation-dependent. | -| » `[any property]` | string | false | | | +| Name | Type | Required | Restrictions | Description | +|--------------------|---------------------------------------------------------------------------------------------|----------|--------------|--------------------------------------------------------------------------------------------------------------------------------------------| +| `created_at` | string | false | | Created at is the time the container was created. | +| `id` | string | false | | ID is the unique identifier of the container. | +| `image` | string | false | | Image is the name of the container image. | +| `labels` | object | false | | Labels is a map of key-value pairs of container labels. | +| » `[any property]` | string | false | | | +| `name` | string | false | | Name is the human-readable name of the container. | +| `ports` | array of [codersdk.WorkspaceAgentDevcontainerPort](#codersdkworkspaceagentdevcontainerport) | false | | Ports includes ports exposed by the container. | +| `running` | boolean | false | | Running is true if the container is currently running. | +| `status` | string | false | | Status is the current status of the container. This is somewhat implementation-dependent, but should generally be a human-readable string. | +| `volumes` | object | false | | Volumes is a map of "things" mounted into the container. Again, this is somewhat implementation-dependent. | +| » `[any property]` | string | false | | | + +## codersdk.WorkspaceAgentDevcontainerPort + +```json +{ + "host_ip": "string", + "host_port": 0, + "network": "string", + "port": 0 +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +|-------------|---------|----------|--------------|----------------------------------------------------------------------------------------------------------------------------| +| `host_ip` | string | false | | Host ip is the IP address of the host interface to which the port is bound. Note that this can be an IPv4 or IPv6 address. | +| `host_port` | integer | false | | Host port is the port number *outside* the container. | +| `network` | string | false | | Network is the network protocol used by the port (tcp, udp, etc). | +| `port` | integer | false | | Port is the port number *inside* the container. | ## codersdk.WorkspaceAgentHealth @@ -7816,9 +7837,10 @@ If the schedule is empty, the user will be updated to use the default schedule.| "name": "string", "ports": [ { + "host_ip": "string", + "host_port": 0, "network": "string", - "port": 0, - "process_name": "string" + "port": 0 } ], "running": true, diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 6fdfb5ea9d9a1..bce6fc31a2508 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -3015,11 +3015,19 @@ export interface WorkspaceAgentDevcontainer { readonly image: string; readonly labels: Record; readonly running: boolean; - readonly ports: readonly WorkspaceAgentListeningPort[]; + readonly ports: readonly WorkspaceAgentDevcontainerPort[]; readonly status: string; readonly volumes: Record; } +// From codersdk/workspaceagents.go +export interface WorkspaceAgentDevcontainerPort { + readonly port: number; + readonly network: string; + readonly host_ip?: string; + readonly host_port?: number; +} + // From codersdk/workspaceagents.go export interface WorkspaceAgentHealth { readonly healthy: boolean; From f8f30003a1e2e0f379cf123b9d9eaabb4eb23578 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 13 Mar 2025 17:01:01 +0000 Subject: [PATCH 08/12] fix(site): correct container port link --- .../resources/AgentDevcontainerCard.tsx | 56 ++++++++++++------- 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/site/src/modules/resources/AgentDevcontainerCard.tsx b/site/src/modules/resources/AgentDevcontainerCard.tsx index fc58c21f95bcb..b8cebc9db4d95 100644 --- a/site/src/modules/resources/AgentDevcontainerCard.tsx +++ b/site/src/modules/resources/AgentDevcontainerCard.tsx @@ -1,11 +1,16 @@ import Link from "@mui/material/Link"; -import type { Workspace, WorkspaceAgentDevcontainer } from "api/typesGenerated"; +import type { + Workspace, + WorkspaceAgentDevcontainer, + WorkspaceAgentDevcontainerPort, +} from "api/typesGenerated"; import { ExternalLinkIcon } from "lucide-react"; import type { FC } from "react"; import { portForwardURL } from "utils/portForward"; import { AgentButton } from "./AgentButton"; import { AgentDevcontainerSSHButton } from "./SSHButton/SSHButton"; import { TerminalLink } from "./TerminalLink/TerminalLink"; +import Tooltip, { type TooltipProps } from "@mui/material/Tooltip"; type AgentDevcontainerCardProps = { container: WorkspaceAgentDevcontainer; @@ -47,25 +52,38 @@ export const AgentDevcontainerCard: FC = ({ /> {wildcardHostname !== "" && container.ports.map((port) => { + let portLabel = `${port.port}/${port.network.toUpperCase()}`; + let hasHostBind = + port.host_port !== undefined && + port.host_port !== null && + port.host_ip !== undefined && + port.host_ip !== null; + let helperText = hasHostBind + ? `${port.host_ip}:${port.host_port}` + : "Not bound to host"; return ( - } - href={portForwardURL( - wildcardHostname, - port.port, - agentName, - workspace.name, - workspace.owner_name, - location.protocol === "https" ? "https" : "http", - )} - > - {port.process_name || - `${port.port}/${port.network.toUpperCase()}`} - + + + } + disabled={!hasHostBind} + href={portForwardURL( + wildcardHostname, + port.host_port!, + agentName, + workspace.name, + workspace.owner_name, + location.protocol === "https" ? "https" : "http", + )} + > + {portLabel} + + + ); })} From 999469f6e90dc89a3c30661845ca5f5e33846a77 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 13 Mar 2025 17:01:30 +0000 Subject: [PATCH 09/12] chore(site): add stories for AgentDevcontainerCard --- .../AgentDevcontainerCard.stories.tsx | 32 ++++++++++++++ site/src/testHelpers/entities.ts | 43 +++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 site/src/modules/resources/AgentDevcontainerCard.stories.tsx diff --git a/site/src/modules/resources/AgentDevcontainerCard.stories.tsx b/site/src/modules/resources/AgentDevcontainerCard.stories.tsx new file mode 100644 index 0000000000000..95462be3cc6c7 --- /dev/null +++ b/site/src/modules/resources/AgentDevcontainerCard.stories.tsx @@ -0,0 +1,32 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { AgentDevcontainerCard } from "./AgentDevcontainerCard"; +import { + MockWorkspace, + MockWorkspaceAgentDevcontainer, + MockWorkspaceAgentDevcontainerPorts, +} from "testHelpers/entities"; + +const meta: Meta = { + title: "modules/resources/AgentDevcontainerCard", + component: AgentDevcontainerCard, + args: { + container: MockWorkspaceAgentDevcontainer, + workspace: MockWorkspace, + wildcardHostname: "*.wildcard.hostname", + agentName: "dev", + }, +}; + +export default meta; +type Story = StoryObj; + +export const NoPorts: Story = {}; + +export const WithPorts: Story = { + args: { + container: { + ...MockWorkspaceAgentDevcontainer, + ports: MockWorkspaceAgentDevcontainerPorts, + }, + }, +}; diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index ef18611caeb8a..55c5509025b33 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -4272,3 +4272,46 @@ function mockTwoDaysAgo() { date.setDate(date.getDate() - 2); return date.toISOString(); } + +export const MockWorkspaceAgentDevcontainerPorts: TypesGen.WorkspaceAgentDevcontainerPort[] = [ + { + port: 1000, + network: "tcp", + host_port: 1000, + host_ip: "0.0.0.0" + }, + { + port: 2001, + network: "tcp", + host_port: 2000, + host_ip: "::1" + }, + { + port: 8888, + network: "tcp", + } +] + +export const MockWorkspaceAgentDevcontainer : TypesGen.WorkspaceAgentDevcontainer = { + created_at: "2024-01-04T15:53:03.21563Z", + id: "abcd1234", + name: "container-1", + image: "ubuntu:latest", + labels: { + "foo": "bar" + }, + ports: [], + running: true, + status: "running", + volumes: { + "/mnt/volume1": "/volume1", + } +} + +export const MockWorkspaceAgentListContainersResponse: TypesGen.WorkspaceAgentListContainersResponse = +{ + containers: [ + MockWorkspaceAgentDevcontainer, + ], + "warnings": ["This is a warning"], +} From 8338af35d999f236b2faa108cd2a46de3fd464f0 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 13 Mar 2025 17:05:49 +0000 Subject: [PATCH 10/12] make fmt lint --- coderd/workspaceagents_test.go | 8 +- .../AgentDevcontainerCard.stories.tsx | 2 +- .../resources/AgentDevcontainerCard.tsx | 8 +- site/src/testHelpers/entities.ts | 78 +++++++++---------- 4 files changed, 49 insertions(+), 47 deletions(-) diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index 69bba9d8baabd..5b03cf5270b91 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -1173,10 +1173,12 @@ func TestWorkspaceAgentContainers(t *testing.T) { Labels: testLabels, Running: true, Status: "running", - Ports: []codersdk.WorkspaceAgentListeningPort{ + Ports: []codersdk.WorkspaceAgentDevcontainerPort{ { - Network: "tcp", - Port: 80, + Network: "tcp", + Port: 80, + HostIP: "0.0.0.0", + HostPort: 8000, }, }, Volumes: map[string]string{ diff --git a/site/src/modules/resources/AgentDevcontainerCard.stories.tsx b/site/src/modules/resources/AgentDevcontainerCard.stories.tsx index 95462be3cc6c7..fed618a428669 100644 --- a/site/src/modules/resources/AgentDevcontainerCard.stories.tsx +++ b/site/src/modules/resources/AgentDevcontainerCard.stories.tsx @@ -1,10 +1,10 @@ import type { Meta, StoryObj } from "@storybook/react"; -import { AgentDevcontainerCard } from "./AgentDevcontainerCard"; import { MockWorkspace, MockWorkspaceAgentDevcontainer, MockWorkspaceAgentDevcontainerPorts, } from "testHelpers/entities"; +import { AgentDevcontainerCard } from "./AgentDevcontainerCard"; const meta: Meta = { title: "modules/resources/AgentDevcontainerCard", diff --git a/site/src/modules/resources/AgentDevcontainerCard.tsx b/site/src/modules/resources/AgentDevcontainerCard.tsx index b8cebc9db4d95..5f01ebb44fbbd 100644 --- a/site/src/modules/resources/AgentDevcontainerCard.tsx +++ b/site/src/modules/resources/AgentDevcontainerCard.tsx @@ -1,4 +1,5 @@ import Link from "@mui/material/Link"; +import Tooltip, { type TooltipProps } from "@mui/material/Tooltip"; import type { Workspace, WorkspaceAgentDevcontainer, @@ -10,7 +11,6 @@ import { portForwardURL } from "utils/portForward"; import { AgentButton } from "./AgentButton"; import { AgentDevcontainerSSHButton } from "./SSHButton/SSHButton"; import { TerminalLink } from "./TerminalLink/TerminalLink"; -import Tooltip, { type TooltipProps } from "@mui/material/Tooltip"; type AgentDevcontainerCardProps = { container: WorkspaceAgentDevcontainer; @@ -52,13 +52,13 @@ export const AgentDevcontainerCard: FC = ({ /> {wildcardHostname !== "" && container.ports.map((port) => { - let portLabel = `${port.port}/${port.network.toUpperCase()}`; - let hasHostBind = + const portLabel = `${port.port}/${port.network.toUpperCase()}`; + const hasHostBind = port.host_port !== undefined && port.host_port !== null && port.host_ip !== undefined && port.host_ip !== null; - let helperText = hasHostBind + const helperText = hasHostBind ? `${port.host_ip}:${port.host_port}` : "Not bound to host"; return ( diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 55c5509025b33..cd12234e0f5ca 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -4273,45 +4273,45 @@ function mockTwoDaysAgo() { return date.toISOString(); } -export const MockWorkspaceAgentDevcontainerPorts: TypesGen.WorkspaceAgentDevcontainerPort[] = [ - { - port: 1000, - network: "tcp", - host_port: 1000, - host_ip: "0.0.0.0" - }, - { - port: 2001, - network: "tcp", - host_port: 2000, - host_ip: "::1" - }, +export const MockWorkspaceAgentDevcontainerPorts: TypesGen.WorkspaceAgentDevcontainerPort[] = + [ + { + port: 1000, + network: "tcp", + host_port: 1000, + host_ip: "0.0.0.0", + }, + { + port: 2001, + network: "tcp", + host_port: 2000, + host_ip: "::1", + }, + { + port: 8888, + network: "tcp", + }, + ]; + +export const MockWorkspaceAgentDevcontainer: TypesGen.WorkspaceAgentDevcontainer = { - port: 8888, - network: "tcp", - } -] - -export const MockWorkspaceAgentDevcontainer : TypesGen.WorkspaceAgentDevcontainer = { - created_at: "2024-01-04T15:53:03.21563Z", - id: "abcd1234", - name: "container-1", - image: "ubuntu:latest", - labels: { - "foo": "bar" - }, - ports: [], - running: true, - status: "running", - volumes: { - "/mnt/volume1": "/volume1", - } -} + created_at: "2024-01-04T15:53:03.21563Z", + id: "abcd1234", + name: "container-1", + image: "ubuntu:latest", + labels: { + foo: "bar", + }, + ports: [], + running: true, + status: "running", + volumes: { + "/mnt/volume1": "/volume1", + }, + }; export const MockWorkspaceAgentListContainersResponse: TypesGen.WorkspaceAgentListContainersResponse = -{ - containers: [ - MockWorkspaceAgentDevcontainer, - ], - "warnings": ["This is a warning"], -} + { + containers: [MockWorkspaceAgentDevcontainer], + warnings: ["This is a warning"], + }; From 2f0180e58331e09a1ca0929fa69e968ca5a51cad Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Tue, 18 Mar 2025 13:29:44 +0000 Subject: [PATCH 11/12] rm extraneous null check --- site/src/modules/resources/AgentDevcontainerCard.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/site/src/modules/resources/AgentDevcontainerCard.tsx b/site/src/modules/resources/AgentDevcontainerCard.tsx index 5f01ebb44fbbd..07a9be1ed55e1 100644 --- a/site/src/modules/resources/AgentDevcontainerCard.tsx +++ b/site/src/modules/resources/AgentDevcontainerCard.tsx @@ -54,10 +54,7 @@ export const AgentDevcontainerCard: FC = ({ container.ports.map((port) => { const portLabel = `${port.port}/${port.network.toUpperCase()}`; const hasHostBind = - port.host_port !== undefined && - port.host_port !== null && - port.host_ip !== undefined && - port.host_ip !== null; + port.host_port !== undefined && port.host_ip !== undefined; const helperText = hasHostBind ? `${port.host_ip}:${port.host_port}` : "Not bound to host"; From 1ae601554607d1e60614f0446e7b283ddfc44ea2 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Tue, 18 Mar 2025 13:51:13 +0000 Subject: [PATCH 12/12] address PR comment --- .../resources/AgentDevcontainerCard.tsx | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/site/src/modules/resources/AgentDevcontainerCard.tsx b/site/src/modules/resources/AgentDevcontainerCard.tsx index 07a9be1ed55e1..759a316e4a7ce 100644 --- a/site/src/modules/resources/AgentDevcontainerCard.tsx +++ b/site/src/modules/resources/AgentDevcontainerCard.tsx @@ -1,10 +1,6 @@ import Link from "@mui/material/Link"; import Tooltip, { type TooltipProps } from "@mui/material/Tooltip"; -import type { - Workspace, - WorkspaceAgentDevcontainer, - WorkspaceAgentDevcontainerPort, -} from "api/typesGenerated"; +import type { Workspace, WorkspaceAgentDevcontainer } from "api/typesGenerated"; import { ExternalLinkIcon } from "lucide-react"; import type { FC } from "react"; import { portForwardURL } from "utils/portForward"; @@ -58,6 +54,16 @@ export const AgentDevcontainerCard: FC = ({ const helperText = hasHostBind ? `${port.host_ip}:${port.host_port}` : "Not bound to host"; + const linkDest = hasHostBind + ? portForwardURL( + wildcardHostname, + port.host_port!, + agentName, + workspace.name, + workspace.owner_name, + location.protocol === "https" ? "https" : "http", + ) + : ""; return ( @@ -68,14 +74,7 @@ export const AgentDevcontainerCard: FC = ({ underline="none" startIcon={} disabled={!hasHostBind} - href={portForwardURL( - wildcardHostname, - port.host_port!, - agentName, - workspace.name, - workspace.owner_name, - location.protocol === "https" ? "https" : "http", - )} + href={linkDest} > {portLabel}