feat(workload): wire stack + static-site into containers index

Stack manager now upserts a Container row per compose service
after every deploy (deterministic ID = workloadID + service so
re-deploys update in place). Stop/Start bulk-flip the state
field. Compose containers don't yet carry the new tinyforge.*
labels — the reconciler will join via com.docker.compose.project
when it lands.

Static site manager passes WorkloadID/Kind to ContainerConfig
so the new labels are stamped, and upserts a single Container
row per site (deterministic ID = workloadID + ":site"). Stop/
Start flip state. Delete cascades through the store layer.

Now every Tinyforge-managed container — project, stack service,
or static site — has a row in the containers index, ready for
the reconciler + global view in the next batches.
This commit is contained in:
2026-05-09 13:41:03 +03:00
parent abb1da903f
commit b6f20599d7
2 changed files with 131 additions and 2 deletions
+71
View File
@@ -128,6 +128,7 @@ func (m *Manager) Deploy(ctx context.Context, stackID, revisionID string) error
_ = m.store.UpdateStackRevisionStatus(rev.ID, "success", deploy.ID)
_ = m.store.SetStackCurrentRevision(st.ID, rev.ID)
m.setStatus(st, "running", "")
m.syncContainerRows(ctx, st, yamlPath)
return nil
}
@@ -217,6 +218,7 @@ func (m *Manager) Stop(ctx context.Context, stackID string) error {
return err
}
m.setStatus(st, "stopped", "")
m.markStackContainersState(stackID, "stopped")
return nil
}
@@ -230,6 +232,7 @@ func (m *Manager) Start(ctx context.Context, stackID string) error {
return err
}
m.setStatus(st, "running", "")
m.markStackContainersState(stackID, "running")
return nil
}
@@ -280,6 +283,74 @@ func (m *Manager) Logs(ctx context.Context, stackID, service string, tail int) (
// --- internals ---
// syncContainerRows upserts one Container row per compose service for this
// stack so the global container index stays in sync after every deploy. The
// Docker container ID is left empty here — the reconciler resolves it from
// `docker ps` via the `com.docker.compose.project` label. Best-effort: a
// failure here is logged but does not affect deploy outcome.
func (m *Manager) syncContainerRows(ctx context.Context, st store.Stack, yamlPath string) {
w, err := m.store.GetWorkloadByRef(store.WorkloadKindStack, st.ID)
if err != nil {
slog.Warn("stack: resolve workload", "stack", st.ID, "error", err)
return
}
psCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
services, err := m.compose.Ps(psCtx, st.ComposeProjectName, yamlPath)
if err != nil {
slog.Warn("stack: compose ps for container sync", "stack", st.ID, "error", err)
return
}
for _, svc := range services {
state := svc.State
if state == "" {
state = svc.Status
}
m.upsertStackContainer(w.ID, svc, state)
}
}
// upsertStackContainer writes a Container row for one compose service. The
// row ID is deterministic — `<workloadID>:<service>` — so re-deploys update
// the same row instead of accumulating rows.
func (m *Manager) upsertStackContainer(workloadID string, svc Service, state string) {
role := svc.Service
if role == "" {
role = svc.Name
}
if err := m.store.UpsertContainer(store.Container{
ID: workloadID + ":" + role,
WorkloadID: workloadID,
WorkloadKind: string(store.WorkloadKindStack),
Role: role,
ContainerID: "", // reconciler fills in from docker ps
Host: "local",
State: state,
LastSeenAt: store.Now(),
}); err != nil {
slog.Warn("stack: upsert container row", "workload_id", workloadID, "service", role, "error", err)
}
}
// markStackContainersState bulk-updates the state of every container row for
// this stack (used by Stop/Start which don't go through compose ps).
func (m *Manager) markStackContainersState(stackID, state string) {
w, err := m.store.GetWorkloadByRef(store.WorkloadKindStack, stackID)
if err != nil {
return
}
rows, err := m.store.ListContainersByWorkload(w.ID)
if err != nil {
slog.Warn("stack: list containers for state update", "workload_id", w.ID, "error", err)
return
}
for _, r := range rows {
if err := m.store.UpdateContainerState(r.ID, state); err != nil {
slog.Warn("stack: update container state", "container_row", r.ID, "error", err)
}
}
}
func (m *Manager) setStatus(st store.Stack, status, errMsg string) {
_ = m.store.UpdateStackStatus(st.ID, status, errMsg)
if m.eventBus != nil {