Некоторые проверки не удались
CI / test (push) Failing after 6s
Docker / Build and publish worker image (push) Failing after 8s
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.
95 строки
2.8 KiB
Go
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
|
|
}
|