feat(apps): per-app deploy/activity timeline
Every deploy across all four source kinds now writes a workload-scoped
event via a shared plugin.EmitDeployEvent helper (replacing the inline
emit duplicated in static/dockerfile, standardizing static's metadata
key site_id->workload_id, and adding emission to image+compose which
were silent). New indexed event_log.workload_id column, EventLogFilter
.WorkloadID, and GET /api/workloads/{id}/events (id pinned from path).
Frontend: a forge "Activity" panel on /apps/[id] reusing EventLogEntry,
live SSE prepend filtered by workload_id, load-more pagination, an
All/Errors severity filter, and a shared toEventLogEntry mapper. en/ru
i18n parity.
Security: compose's failure status emits a generic reason instead of raw
`docker compose up` output, which can echo app secrets and egresses to
operator webhooks (NotificationURL + event-trigger actions); full detail
stays only in the returned error. Rune-safe 256-rune status cap.
Reviewed: go + typescript APPROVE; security HIGH fixed.
This commit is contained in:
@@ -84,7 +84,7 @@ func (*source) Validate(cfg json.RawMessage) error {
|
||||
// `docker compose -p <project> up -d`, then syncs one Container row per
|
||||
// service. The workload ID is the natural compose project name unless
|
||||
// the user supplied one explicitly.
|
||||
func (*source) Deploy(ctx context.Context, deps plugin.Deps, w plugin.Workload, intent plugin.DeploymentIntent) error {
|
||||
func (*source) Deploy(ctx context.Context, deps plugin.Deps, w plugin.Workload, intent plugin.DeploymentIntent) (err error) {
|
||||
cfg, err := plugin.SourceConfigOf[Config](w)
|
||||
if err != nil {
|
||||
return fmt.Errorf("compose source: decode config: %w", err)
|
||||
@@ -93,6 +93,29 @@ func (*source) Deploy(ctx context.Context, deps plugin.Deps, w plugin.Workload,
|
||||
return fmt.Errorf("compose source: workload %s has empty compose_yaml", w.ID)
|
||||
}
|
||||
|
||||
// compose.Deploy has no idempotency short-circuit (no "already up"
|
||||
// fast path that returns nil), so every call past config validation
|
||||
// is a real deploy. Arm the terminal audit emit here — after pure
|
||||
// config-validation errors above (kept quiet, mirroring the image
|
||||
// plugin) but before any real work — so all real failures and the
|
||||
// success are captured for the per-app timeline. err is the named
|
||||
// return.
|
||||
defer func() {
|
||||
if err != nil {
|
||||
// SECURITY: the compose.Up failure wraps raw `docker compose`
|
||||
// combined output (which can include the deployed app's own
|
||||
// stderr — potentially secrets). Deploy events are persisted
|
||||
// indefinitely AND egress to operator webhooks (the global
|
||||
// NotificationURL + event-trigger actions), so the emitted
|
||||
// status must NOT carry that output. The full detail still
|
||||
// reaches the server log + admin deploy result via the returned
|
||||
// err; the timeline records only a generic, secret-free reason.
|
||||
plugin.EmitDeployEvent(deps, w, "compose", "failed")
|
||||
} else {
|
||||
plugin.EmitDeployEvent(deps, w, "compose", "deployed")
|
||||
}
|
||||
}()
|
||||
|
||||
projectName := composeProjectName(cfg.ComposeProjectName, w)
|
||||
yamlPath, err := writeYAML(w.ID, cfg.ComposeYAML)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user