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:
@@ -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.UpdateStackRevisionStatus(rev.ID, "success", deploy.ID)
|
||||||
_ = m.store.SetStackCurrentRevision(st.ID, rev.ID)
|
_ = m.store.SetStackCurrentRevision(st.ID, rev.ID)
|
||||||
m.setStatus(st, "running", "")
|
m.setStatus(st, "running", "")
|
||||||
|
m.syncContainerRows(ctx, st, yamlPath)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,6 +218,7 @@ func (m *Manager) Stop(ctx context.Context, stackID string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
m.setStatus(st, "stopped", "")
|
m.setStatus(st, "stopped", "")
|
||||||
|
m.markStackContainersState(stackID, "stopped")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,6 +232,7 @@ func (m *Manager) Start(ctx context.Context, stackID string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
m.setStatus(st, "running", "")
|
m.setStatus(st, "running", "")
|
||||||
|
m.markStackContainersState(stackID, "running")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,6 +283,74 @@ func (m *Manager) Logs(ctx context.Context, stackID, service string, tail int) (
|
|||||||
|
|
||||||
// --- internals ---
|
// --- 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) {
|
func (m *Manager) setStatus(st store.Stack, status, errMsg string) {
|
||||||
_ = m.store.UpdateStackStatus(st.ID, status, errMsg)
|
_ = m.store.UpdateStackStatus(st.ID, status, errMsg)
|
||||||
if m.eventBus != nil {
|
if m.eventBus != nil {
|
||||||
|
|||||||
@@ -55,6 +55,56 @@ func (m *Manager) SetProxyProvider(provider proxy.Provider) {
|
|||||||
m.proxyProvider = 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.
|
// Deploy fetches content from Gitea and deploys a static site container.
|
||||||
// If force is true, skips the "no changes" check and always rebuilds/redeploys.
|
// 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 {
|
func (m *Manager) Deploy(ctx context.Context, siteID string, force bool) error {
|
||||||
@@ -235,6 +285,9 @@ func (m *Manager) Deploy(ctx context.Context, siteID string, force bool) error {
|
|||||||
},
|
},
|
||||||
Project: "static-site",
|
Project: "static-site",
|
||||||
Stage: site.Name,
|
Stage: site.Name,
|
||||||
|
WorkloadID: m.resolveSiteWorkloadID(site.ID),
|
||||||
|
WorkloadKind: string(store.WorkloadKindSite),
|
||||||
|
Role: "",
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Container might already exist — try to remove and recreate.
|
// 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 {
|
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)
|
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.updateStatus(site.ID, "deployed", latestSHA, "")
|
||||||
m.publishEvent(site.ID, site.Name, "deployed")
|
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 {
|
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)
|
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.updateStatus(site.ID, "stopped", site.LastCommitSHA, "")
|
||||||
m.publishEvent(site.ID, site.Name, "stopped")
|
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.updateStatus(site.ID, "deployed", site.LastCommitSHA, "")
|
||||||
m.publishEvent(site.ID, site.Name, "deployed")
|
m.publishEvent(site.ID, site.Name, "deployed")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user