Files
worker/internal/compose/traefik.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

88 строки
2.6 KiB
Go

package compose
import (
"regexp"
"strings"
)
// Traefik label parsing. Compose projects fronted by Traefik encode
// their router rules as `traefik.http.routers.<name>.rule=Host(...)`.
// We surface hostnames/paths so the operator console can show which
// domains a project serves without reading raw labels.
const (
traefikLabelPrefix = "traefik.http.routers."
traefikRuleSuffix = ".rule"
)
var (
// Host(`example.com`) or Host("example.com")
hostRegex = regexp.MustCompile(`Host\s*\(\s*[` + "`" + `"]([^` + "`" + `"]+)[` + "`" + `"]\s*\)`)
// Path(`/api`) or Path("/api")
pathRegex = regexp.MustCompile(`Path\s*\(\s*[` + "`" + `"]([^` + "`" + `"]+)[` + "`" + `"]\s*\)`)
// PathPrefix(`/api`) or PathPrefix("/api")
pathPrefixRegex = regexp.MustCompile(`PathPrefix\s*\(\s*[` + "`" + `"]([^` + "`" + `"]+)[` + "`" + `"]\s*\)`)
)
// TraefikRoute is one decoded Traefik router derived from labels.
type TraefikRoute struct {
RouterName string `json:"router_name"`
Hostnames []string `json:"hostnames"`
Paths []string `json:"paths"`
PathPrefixes []string `json:"path_prefixes"`
Rule string `json:"rule"`
}
// ParseTraefikLabels scans a label map for `traefik.http.routers.<n>.rule`
// entries and returns one TraefikRoute per router, with the rule's
// Host/Path/PathPrefix operands extracted. Pure function: no I/O.
func ParseTraefikLabels(labels map[string]string) []TraefikRoute {
routes := map[string]*TraefikRoute{}
for key, value := range labels {
if !strings.HasPrefix(key, traefikLabelPrefix) {
continue
}
suffix := strings.TrimPrefix(key, traefikLabelPrefix)
idx := strings.Index(suffix, ".")
if idx == -1 {
continue
}
routerName := suffix[:idx]
property := suffix[idx:]
if property != traefikRuleSuffix {
continue
}
route := routes[routerName]
if route == nil {
route = &TraefikRoute{RouterName: routerName}
}
route.Rule = value
route.Hostnames = extractAll(hostRegex, value)
route.Paths = extractAll(pathRegex, value)
route.PathPrefixes = extractAll(pathPrefixRegex, value)
routes[routerName] = route
}
out := make([]TraefikRoute, 0, len(routes))
for _, r := range routes {
out = append(out, *r)
}
return out
}
func extractAll(re *regexp.Regexp, rule string) []string {
matches := re.FindAllStringSubmatch(rule, -1)
out := make([]string, 0, len(matches))
for _, m := range matches {
if len(m) > 1 && m[1] != "" {
out = append(out, m[1])
}
}
return out
}
// ExtractTraefikRoutes returns the Traefik routes declared by a
// container's labels.
func ExtractTraefikRoutes(c Container) []TraefikRoute {
return ParseTraefikLabels(c.Labels)
}