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
+60 -2
View File
@@ -55,6 +55,56 @@ func (m *Manager) SetProxyProvider(provider proxy.Provider) {
m.proxyProvider = provider
}
// resolveSiteWorkloadID returns the workload ID paired with a static site.
// Boot-time backfill guarantees the row exists; on lookup miss this returns
// empty so the caller can decide (the deployer continues without the label).
func (m *Manager) resolveSiteWorkloadID(siteID string) string {
w, err := m.store.GetWorkloadByRef(store.WorkloadKindSite, siteID)
if err != nil {
slog.Warn("static site: resolve workload", "site_id", siteID, "error", err)
return ""
}
return w.ID
}
// upsertSiteContainer keeps the global container index in sync with the
// site's current container. Row ID is deterministic (workloadID + ":site")
// so re-deploys update in place. Best-effort.
func (m *Manager) upsertSiteContainer(site store.StaticSite, containerID, state string) {
workloadID := m.resolveSiteWorkloadID(site.ID)
if workloadID == "" {
return
}
if err := m.store.UpsertContainer(store.Container{
ID: workloadID + ":site",
WorkloadID: workloadID,
WorkloadKind: string(store.WorkloadKindSite),
Role: "",
ContainerID: containerID,
Host: "local",
State: state,
Subdomain: site.Domain,
ProxyRouteID: site.ProxyRouteID,
LastSeenAt: store.Now(),
}); err != nil {
slog.Warn("static site: upsert container row", "site_id", site.ID, "error", err)
}
}
// markSiteContainerState bulk-updates state for the site's container row.
// Used by Stop/Start which only flip state.
func (m *Manager) markSiteContainerState(siteID, state string) {
workloadID := m.resolveSiteWorkloadID(siteID)
if workloadID == "" {
return
}
rowID := workloadID + ":site"
if err := m.store.UpdateContainerState(rowID, state); err != nil {
// NotFound is fine — the site may have never deployed.
slog.Debug("static site: update container state", "row", rowID, "error", err)
}
}
// Deploy fetches content from Gitea and deploys a static site container.
// If force is true, skips the "no changes" check and always rebuilds/redeploys.
func (m *Manager) Deploy(ctx context.Context, siteID string, force bool) error {
@@ -233,8 +283,11 @@ func (m *Manager) Deploy(ctx context.Context, siteID string, force bool) error {
"tinyforge.static-site": site.ID,
"tinyforge.static-site-name": site.Name,
},
Project: "static-site",
Stage: site.Name,
Project: "static-site",
Stage: site.Name,
WorkloadID: m.resolveSiteWorkloadID(site.ID),
WorkloadKind: string(store.WorkloadKindSite),
Role: "",
})
if err != nil {
// Container might already exist — try to remove and recreate.
@@ -341,6 +394,9 @@ func (m *Manager) Deploy(ctx context.Context, siteID string, force bool) error {
if err := m.store.UpdateStaticSiteContainer(site.ID, containerID, proxyRouteID); err != nil {
slog.Error("static site: failed to update container info", "site", site.Name, "error", err)
}
site.ContainerID = containerID
site.ProxyRouteID = proxyRouteID
m.upsertSiteContainer(site, containerID, "running")
m.updateStatus(site.ID, "deployed", latestSHA, "")
m.publishEvent(site.ID, site.Name, "deployed")
@@ -406,6 +462,7 @@ func (m *Manager) Stop(ctx context.Context, siteID string) error {
if err := m.store.UpdateStaticSiteContainer(site.ID, site.ContainerID, ""); err != nil {
slog.Error("static site: failed to clear proxy route", "site", site.Name, "error", err)
}
m.markSiteContainerState(site.ID, "stopped")
m.updateStatus(site.ID, "stopped", site.LastCommitSHA, "")
m.publishEvent(site.ID, site.Name, "stopped")
@@ -468,6 +525,7 @@ func (m *Manager) Start(ctx context.Context, siteID string) error {
}
}
m.markSiteContainerState(site.ID, "running")
m.updateStatus(site.ID, "deployed", site.LastCommitSHA, "")
m.publishEvent(site.ID, site.Name, "deployed")