Некоторые проверки не удались
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.
125 строки
4.5 KiB
Go
125 строки
4.5 KiB
Go
package compose
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// Action is a management operation the operator console can run against
|
|
// a project. The string values are the suffix used in the management
|
|
// API path (/compose/{project}/{action}) and the audit log target.
|
|
type Action string
|
|
|
|
const (
|
|
ActionUp Action = "up"
|
|
ActionDown Action = "down"
|
|
ActionStop Action = "stop"
|
|
ActionRestart Action = "restart"
|
|
ActionPull Action = "pull"
|
|
ActionLogs Action = "logs"
|
|
)
|
|
|
|
// projectActions is the allowlist of actions a project-level POST
|
|
// accepts. It maps the action to the compose subcommand(s). The map is
|
|
// the single source of truth so the route handler and the executor
|
|
// agree on what is permitted.
|
|
var projectActions = map[Action][]string{
|
|
ActionUp: {"up", "-d", "--remove-orphans"},
|
|
ActionDown: {"down", "--remove-orphans"},
|
|
ActionStop: {"stop"},
|
|
ActionRestart: {"restart"},
|
|
ActionPull: {"pull"},
|
|
}
|
|
|
|
// serviceActions is the allowlist for service-level operations
|
|
// (start/stop/restart a single service inside a project).
|
|
var serviceActions = map[Action][]string{
|
|
ActionUp: {"up", "-d", "--no-deps"},
|
|
ActionStop: {"stop"},
|
|
ActionRestart: {"restart"},
|
|
}
|
|
|
|
// ValidProjectAction reports whether name is an accepted project action.
|
|
func ValidProjectAction(name string) (Action, bool) {
|
|
a := Action(name)
|
|
_, ok := projectActions[a]
|
|
if !ok && a == ActionLogs {
|
|
return a, true
|
|
}
|
|
return a, ok
|
|
}
|
|
|
|
// ValidServiceAction reports whether name is an accepted service action.
|
|
func ValidServiceAction(name string) (Action, bool) {
|
|
a := Action(name)
|
|
_, ok := serviceActions[a]
|
|
return a, ok
|
|
}
|
|
|
|
// ManagementResult is returned by every management operation. Output is
|
|
// the combined compose stdout/stderr; OK is false when the command
|
|
// exited non-zero. The handler serializes it as JSON for the API path
|
|
// and renders Output in a <pre> block on the HTML path.
|
|
type ManagementResult struct {
|
|
OK bool `json:"ok"`
|
|
Action string `json:"action"`
|
|
Output string `json:"output"`
|
|
}
|
|
|
|
// ManageProject runs an action against a whole project. refs supplies
|
|
// the working directory (so Compose auto-discovers the compose file and
|
|
// .env) and the project name (pinned with -p). A missing working dir is
|
|
// a hard error: without it Compose cannot locate the project files.
|
|
func ManageProject(ctx context.Context, refs ProjectRefs, action Action) (ManagementResult, error) {
|
|
args, ok := projectActions[action]
|
|
if !ok {
|
|
return ManagementResult{}, fmt.Errorf("compose: unknown project action %q", action)
|
|
}
|
|
return runCompose(ctx, refs, args...)
|
|
}
|
|
|
|
// ManageService runs an action against a single service in a project.
|
|
// --no-deps on `up` ensures starting one service does not implicitly
|
|
// recreate its dependencies (matching dockge's service-start behavior).
|
|
func ManageService(ctx context.Context, refs ProjectRefs, service string, action Action) (ManagementResult, error) {
|
|
args, ok := serviceActions[action]
|
|
if !ok {
|
|
return ManagementResult{}, fmt.Errorf("compose: unknown service action %q", action)
|
|
}
|
|
if service == "" {
|
|
return ManagementResult{}, fmt.Errorf("compose: service is required")
|
|
}
|
|
full := append(append([]string{}, args...), service)
|
|
return runCompose(ctx, refs, full...)
|
|
}
|
|
|
|
// Logs returns the recent log output for a project as a single string.
|
|
// It runs `docker compose logs --no-color --tail <n>` (no -f) so the
|
|
// operator console gets a finite snapshot; streaming tails are a later
|
|
// enhancement (dockge uses a PTY + websocket, out of scope here).
|
|
func Logs(ctx context.Context, refs ProjectRefs, tail int) (ManagementResult, error) {
|
|
if tail <= 0 {
|
|
tail = 200
|
|
}
|
|
return runCompose(ctx, refs, "logs", "--no-color", "--tail", fmt.Sprintf("%d", tail))
|
|
}
|
|
|
|
// runCompose executes `docker compose -p <name> <args...>` with the
|
|
// working directory set to refs.WorkingDir. The working dir makes
|
|
// Compose resolve the compose file and `.env` exactly as the operator
|
|
// would from the shell; -p pins the project name so the command cannot
|
|
// accidentally target a different project that shares the directory.
|
|
func runCompose(ctx context.Context, refs ProjectRefs, args ...string) (ManagementResult, error) {
|
|
if refs.WorkingDir == "" {
|
|
return ManagementResult{OK: false, Output: "compose project working directory is unknown; cannot manage"}, fmt.Errorf("compose: empty working dir")
|
|
}
|
|
full := append([]string{"compose", "-p", refs.Name}, args...)
|
|
out, err := dockerCombined(ctx, refs.WorkingDir, full...)
|
|
res := ManagementResult{OK: err == nil, Action: strings.Join(args, " "), Output: out}
|
|
if err != nil {
|
|
return res, err
|
|
}
|
|
return res, nil
|
|
}
|