Некоторые проверки не удались
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.
131 строка
4.4 KiB
Go
131 строка
4.4 KiB
Go
package compose
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os/exec"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// dockerBin is the Docker CLI binary name. Overridable in tests via
|
|
// SetDockerBin so the management/discovery code never hard-codes a path.
|
|
var dockerBin = "docker"
|
|
|
|
// SetDockerBin overrides the Docker CLI binary used by discovery and
|
|
// management. Pass an empty value to restore the default ("docker").
|
|
// Tests use it to point at a stub binary; production leaves it alone.
|
|
func SetDockerBin(name string) {
|
|
if name == "" {
|
|
dockerBin = "docker"
|
|
return
|
|
}
|
|
dockerBin = name
|
|
}
|
|
|
|
// commandTimeout is the deadline applied to every Docker CLI call so a
|
|
// hung dockerd cannot wedge the inventory refresh loop or a management
|
|
// request. 30s is well above `docker ps` / `docker inspect` on a busy
|
|
// host but short enough that the operator notices.
|
|
const commandTimeout = 30 * time.Second
|
|
|
|
// Available reports whether the Docker CLI can reach the daemon. Used
|
|
// by the refresh loop to no-op (instead of erroring) on hosts without
|
|
// Docker, and by the UI to show a "Docker not available" banner.
|
|
func Available(ctx context.Context) bool {
|
|
c, cancel := context.WithTimeout(ctx, commandTimeout)
|
|
defer cancel()
|
|
if err := exec.CommandContext(c, dockerBin, "version").Run(); err != nil {
|
|
return false
|
|
}
|
|
// `docker version` succeeds against the client even when the daemon
|
|
// is down on some builds; `docker ps` requires the daemon, so it is
|
|
// the authoritative liveness probe.
|
|
pc, pcancel := context.WithTimeout(ctx, commandTimeout)
|
|
defer pcancel()
|
|
return exec.CommandContext(pc, dockerBin, "ps", "--format", "{{.ID}}").Run() == nil
|
|
}
|
|
|
|
// composeLSItem is one row of `docker compose ls --all --format json`.
|
|
type composeLSItem struct {
|
|
Name string `json:"Name"`
|
|
Status string `json:"Status"`
|
|
ConfigFiles string `json:"ConfigFiles"`
|
|
}
|
|
|
|
// composeLS runs `docker compose ls --all --format json` and returns
|
|
// the projects keyed by name. Unlike `docker ps --format json` (NDJSON),
|
|
// `docker compose ls` emits a single JSON array, so the whole stdout is
|
|
// unmarshalled at once. Non-fatal: returns an empty map on any error.
|
|
func composeLS(ctx context.Context) map[string]composeLSItem {
|
|
out, err := runDocker(ctx, "compose", "ls", "--all", "--format", "json")
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
trimmed := strings.TrimSpace(out)
|
|
if trimmed == "" {
|
|
return nil
|
|
}
|
|
var items []composeLSItem
|
|
if err := json.Unmarshal([]byte(trimmed), &items); err != nil {
|
|
return nil
|
|
}
|
|
m := make(map[string]composeLSItem, len(items))
|
|
for _, it := range items {
|
|
m[it.Name] = it
|
|
}
|
|
return m
|
|
}
|
|
|
|
// runDocker executes the Docker CLI with the given args and returns
|
|
// stdout. The context deadline is enforced by Go (CommandContext); a
|
|
// non-zero exit yields an error that carries the daemon's stderr so the
|
|
// caller can surface the daemon's diagnostic instead of a bare exit code.
|
|
func runDocker(ctx context.Context, args ...string) (string, error) {
|
|
c, cancel := context.WithTimeout(ctx, commandTimeout)
|
|
defer cancel()
|
|
cmd := exec.CommandContext(c, dockerBin, args...)
|
|
out, err := cmd.Output()
|
|
if err != nil {
|
|
if ee, ok := err.(*exec.ExitError); ok && len(ee.Stderr) > 0 {
|
|
return "", fmt.Errorf("%s: %s", strings.Join(args, " "), strings.TrimSpace(string(ee.Stderr)))
|
|
}
|
|
return "", err
|
|
}
|
|
return string(out), nil
|
|
}
|
|
|
|
// dockerCombined runs the Docker CLI with a working directory and
|
|
// returns combined stdout+stderr. Used by management operations, which
|
|
// must run inside the project's working directory so Compose resolves
|
|
// the compose file and `.env` exactly as the operator would. Unlike
|
|
// runDocker, stderr is merged into the returned string because compose
|
|
// progress output goes to stderr and is useful to the operator.
|
|
func dockerCombined(ctx context.Context, dir string, args ...string) (string, error) {
|
|
c, cancel := context.WithTimeout(ctx, commandTimeout)
|
|
defer cancel()
|
|
cmd := exec.CommandContext(c, dockerBin, args...)
|
|
cmd.Dir = dir
|
|
out, err := cmd.CombinedOutput()
|
|
return string(out), err
|
|
}
|
|
|
|
// parseLabels parses the comma-separated `key=value` label string that
|
|
// `docker ps --format json` emits in the Labels field. Tolerates empty
|
|
// input and values containing '=' (only the first '=' splits).
|
|
func parseLabels(labels string) map[string]string {
|
|
out := make(map[string]string)
|
|
if labels == "" {
|
|
return out
|
|
}
|
|
for _, pair := range strings.Split(labels, ",") {
|
|
k, v, ok := strings.Cut(pair, "=")
|
|
if !ok {
|
|
continue
|
|
}
|
|
out[strings.TrimSpace(k)] = strings.TrimSpace(v)
|
|
}
|
|
return out
|
|
}
|