feat(worker): add Docker Compose discovery and management
Некоторые проверки не удались
CI / test (push) Failing after 6s
Docker / Build and publish worker image (push) Failing after 8s
Некоторые проверки не удались
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.
Этот коммит содержится в:
87
internal/compose/traefik.go
Обычный файл
87
internal/compose/traefik.go
Обычный файл
@@ -0,0 +1,87 @@
|
||||
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)
|
||||
}
|
||||
Ссылка в новой задаче
Block a user