package compose import ( "context" "encoding/json" "fmt" "strings" ) // inspectData mirrors the subset of `docker inspect ` output that // discovery consumes. Env is intentionally absent: the worker must not // collect container environment (which carries secrets). Only Config // labels, runtime State (for the host PID), Mounts, and published Ports // are read. type inspectData struct { ID string `json:"Id"` State inspectState `json:"State"` Config inspectConfig `json:"Config"` Mounts []inspectMount `json:"Mounts"` NetworkSettings inspectNetworks `json:"NetworkSettings"` } type inspectState struct { Status string `json:"Status"` Running bool `json:"Running"` Pid int `json:"Pid"` ExitCode int `json:"ExitCode"` } type inspectConfig struct { Labels map[string]string `json:"Labels"` } type inspectMount struct { Type string `json:"Type"` Source string `json:"Source"` Destination string `json:"Destination"` } type inspectNetworks struct { Ports portMap `json:"Ports"` } // portMap mirrors the Docker inspect "Ports" object: the key is the // container port ("80/tcp") and the value is the list of host bindings // (nil when the port is exposed but not published). type portMap map[string][]portBinding type portBinding struct { HostIP string `json:"HostIp"` HostPort string `json:"HostPort"` } // inspectContainer runs `docker inspect ` and decodes the first // element. A missing container (race with `docker rm`) is reported as // an error so the caller can skip it without aborting the whole scan. func inspectContainer(ctx context.Context, id string) (*inspectData, error) { out, err := runDocker(ctx, "inspect", id) if err != nil { return nil, err } var data []inspectData if err := json.Unmarshal([]byte(strings.TrimSpace(out)), &data); err != nil { return nil, err } if len(data) == 0 { return nil, fmt.Errorf("no inspect data for %s", id) } return &data[0], nil } // extractPorts flattens the inspect Ports map into a slice of host // bindings. Container ports that are exposed but not published (nil // binding list) are skipped because they have no host-side footprint. func extractPorts(data *inspectData) []PortBinding { var ports []PortBinding for _, bindings := range data.NetworkSettings.Ports { for _, b := range bindings { ports = append(ports, PortBinding{HostIP: b.HostIP, HostPort: b.HostPort}) } } return ports } // extractMounts copies the inspect Mounts into the wire Mount shape, // dropping Docker-internal fields (mode, propagation, rw) the operator // console does not render. func extractMounts(data *inspectData) []Mount { mounts := make([]Mount, 0, len(data.Mounts)) for _, m := range data.Mounts { mounts = append(mounts, Mount{Source: m.Source, Destination: m.Destination, Type: m.Type}) } return mounts }