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,6 +87,14 @@ type Config struct {
|
||||
// "tag_name" field (GitHub release JSON is the canonical
|
||||
// shape). WORKER_RELEASE_URL sets this.
|
||||
ReleaseURL string
|
||||
|
||||
// ComposeEnabled turns the Docker Compose discovery + management
|
||||
// subsystem on. Defaults to true (WORKER_COMPOSE_ENABLED=false to
|
||||
// disable): the refresher no-ops when the Docker daemon is absent,
|
||||
// so leaving it on is safe on non-Docker hosts. When enabled, the
|
||||
// /compose page lists every Compose project on the host and the
|
||||
// /web/api/compose/* endpoints drive up/down/stop/restart/pull.
|
||||
ComposeEnabled bool
|
||||
}
|
||||
|
||||
// ValidateBasicAuth enforces that WORKER_LOGIN and WORKER_PASSWORD
|
||||
@@ -177,9 +185,23 @@ func ConfigFromEnv(env map[string]string, defaultDataDir string) (Config, error)
|
||||
cfg.StorePath = v
|
||||
}
|
||||
cfg.ReleaseURL = strings.TrimSpace(env[envReleaseURL])
|
||||
// Compose discovery defaults to on; only an explicit false disables it.
|
||||
cfg.ComposeEnabled = !parseBoolFalseDefault(env[envComposeEnabled])
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// parseBoolFalseDefault returns true for any value that is not an
|
||||
// explicit falsy literal. Used by ComposeEnabled so that the default
|
||||
// (unset env var) keeps the feature on, unlike parseBool where the
|
||||
// default is false.
|
||||
func parseBoolFalseDefault(v string) bool {
|
||||
switch strings.ToLower(strings.TrimSpace(v)) {
|
||||
case "false", "0", "no", "off", "":
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// parseBool returns true for the strings "true", "1", "yes" (any
|
||||
// case, trimmed). Anything else is false. Used for opt-in feature
|
||||
// flags wired through env vars without dragging in a config-package
|
||||
@@ -201,6 +223,7 @@ func ConfigFromEnvOrDefault() Config {
|
||||
envWorkerHost, envWorkerPort, envWorkerURL, envWorkerLogin, envWorkerPassword,
|
||||
"RSMON_WEBAPP_DATA_DIR", "RSMON_WEBAPP_STORE_PATH",
|
||||
"WORKER_CLUSTER_ENABLED",
|
||||
envComposeEnabled,
|
||||
envReleaseURL,
|
||||
} {
|
||||
if v := os.Getenv(k); v != "" {
|
||||
@@ -336,6 +359,7 @@ type Server struct {
|
||||
templates *Templates
|
||||
logBuffer *LogBuffer
|
||||
inventory *Inventory
|
||||
compose *ComposeRefresher
|
||||
metrics *Metrics
|
||||
cluster ClusterView
|
||||
pruneStop chan struct{}
|
||||
@@ -402,6 +426,7 @@ func New(cfg Config, deps *Deps) (*Server, error) { //nolint:gocritic // Config
|
||||
templates: tmpl,
|
||||
logBuffer: NewLogBuffer(5000),
|
||||
inventory: NewInventory(store, deps.Logger),
|
||||
compose: NewComposeRefresher(cfg.ComposeEnabled, deps.Logger),
|
||||
metrics: NewMetrics(),
|
||||
cluster: deps.Cluster,
|
||||
pruneStop: make(chan struct{}),
|
||||
@@ -460,6 +485,10 @@ func (s *Server) SetCluster(c ClusterView) { s.cluster = c }
|
||||
// Cluster returns the attached cluster subsystem (or nil).
|
||||
func (s *Server) Cluster() ClusterView { return s.cluster }
|
||||
|
||||
// Compose returns the Compose discovery/management refresher so tests
|
||||
// and the cmd binary can drive it (e.g. inject a fixture discovery).
|
||||
func (s *Server) Compose() *ComposeRefresher { return s.compose }
|
||||
|
||||
// Close shuts down the HTTP listener, the prune goroutine, and the
|
||||
// embedded store. Safe to call multiple times.
|
||||
func (s *Server) Close(ctx context.Context) error {
|
||||
@@ -472,6 +501,9 @@ func (s *Server) Close(ctx context.Context) error {
|
||||
close(s.pruneStop)
|
||||
}
|
||||
s.pruneWG.Wait()
|
||||
if s.compose != nil {
|
||||
s.compose.Stop()
|
||||
}
|
||||
if s.httpServer != nil {
|
||||
shutdownCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
@@ -493,6 +525,12 @@ func (s *Server) Start(ctx context.Context) error {
|
||||
s.pruneWG.Add(1)
|
||||
go s.pruneLoop(ctx)
|
||||
|
||||
// Start the Compose discovery loop alongside the listener. It no-ops
|
||||
// when disabled or when Docker is absent, so it is always safe to
|
||||
// start. The first refresh runs immediately so /compose has data on
|
||||
// the first request.
|
||||
s.compose.Start(ctx)
|
||||
|
||||
// Run ListenAndServe in a goroutine so we can race it against ctx.
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
|
||||
Ссылка в новой задаче
Block a user