Files
worker/internal/compose/inspect.go
root ff0d2f088f
Некоторые проверки не удались
CI / test (push) Failing after 6s
Docker / Build and publish worker image (push) Failing after 8s
feat(worker): add Docker Compose discovery and management
Add an internal/compose package that discovers Compose projects via
`docker compose ls` + `docker ps` labels (grouped by
com.docker.compose.project/service) and enriches each container with
`docker inspect` ports/mounts and Traefik router labels. Management
runs `docker compose` in each project's working directory for
up/down/stop/restart/pull plus per-service variants and log tails.

Wire it into the webapp: a 60s ComposeRefresher (constructed in New,
started in Start, stopped in Close), a /compose list + detail + logs
HTML surface, and /web/api/compose/* JSON endpoints (list, detail,
logs, project/service lifecycle). Browser lifecycle POSTs are
session+CSRF protected; the /web/api/* variants accept HTTP basic auth.
WORKER_COMPOSE_ENABLED defaults on (false to disable).

Tests cover discovery parsing/grouping/traefik/summary, the action
allowlists, and the full handler surface (list/detail/logs HTML+API,
CSRF enforcement, disabled/unknown-action rejection, audit writes,
success+failure exec paths) via a stub Docker binary.
2026-07-29 21:31:52 +03:00

95 строки
2.8 KiB
Go

package compose
import (
"context"
"encoding/json"
"fmt"
"strings"
)
// inspectData mirrors the subset of `docker inspect <id>` 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 <id>` 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
}