refactor(workload): extract Instance entirely; Container is canonical
Build / build (push) Successful in 10m41s

End-to-end extraction of the Instance concept. After this commit:

  * internal/store/instances.go — DELETED
  * internal/store/models.go — Instance struct gone, ProxyRoute moved here
  * containers table is the single source of truth for project/stack/site
    container state. instances table is dropped via DROP TABLE migration
    (idempotent; re-runnable on every boot).
  * Legacy tinyforge.project / tinyforge.stage / tinyforge.instance-id
    Docker labels are no longer emitted; only tinyforge.workload.{id,kind},
    tinyforge.role, and tinyforge.managed are stamped on new containers.

Backend rewrites:
  - internal/deployer:        executeDeploy + blueGreenDeploy + rollback +
                              promote use store.Container natively. New
                              removeContainer() replaces removeInstance().
                              enforceMaxInstances reads via
                              ListContainersByStageID.
  - internal/reconciler:      legacy tinyforge.instance-id dispatch removed;
                              upsertByWorkloadLabel now finds existing rows
                              by docker container ID first and falls back to
                              the deterministic workloadID:role key.
  - internal/stale/scanner:   Scan + new FindStaleContainers walk the
                              containers table; emit StaleContainer JSON.
  - internal/stats/collector: ListContainers replaces ListAllInstances.
  - internal/webhook/handler: workload-secret lookup tried first; falls back
                              to project / static_site secret column.
  - internal/api: instances.go, stale.go, stats.go, stats_history.go,
                  projects.go, settings.go, docker.go, dns.go all read /
                  write through Container.

Docker layer:
  - ManagedContainer exposes WorkloadID/Kind/Role from the canonical labels.
  - ListContainers filters by tinyforge.managed=true.
  - Network creation uses LabelManaged instead of LabelProject.

Frontend:
  - Instance type is now a Container alias; .status → .state,
    .last_alive_at → .last_seen_at.
  - InstanceCard takes stageId as a prop (no longer derived from Instance).
  - StaleContainer JSON shape rewritten: { container, workload_name, role,
    days_stale }. StaleContainerCard + /containers/stale page updated.
  - ProjectCard / homepage / SystemHealthCard filter by .state.

The migration loop now tolerates "no such table" alongside "duplicate
column" / "already exists" so obsolete ALTER TABLE entries targeting the
dropped instances table no-op cleanly on first boot.

Tests: store + deployer + reconciler + webhook + staticsite + notify all
still pass. Frontend svelte-check: zero errors.
This commit is contained in:
2026-05-09 14:43:12 +03:00
parent d516462750
commit d8ab22876f
32 changed files with 649 additions and 957 deletions
+3 -10
View File
@@ -9,17 +9,10 @@ import (
// Labels applied to all containers managed by Tinyforge.
//
// Workload-shaped labels (LabelWorkloadID, LabelWorkloadKind, LabelRole,
// LabelManaged) are the canonical set going forward and what the reconciler
// queries by. The legacy project/stage/instance-id labels are still emitted
// alongside them for back-compat with anything that selects on them
// (operator runbooks, monitoring scrape rules, ad-hoc shell debugging) — they
// will be removed once the migration soaks.
// The legacy tinyforge.project / tinyforge.stage / tinyforge.instance-id
// labels were removed in the workload refactor — the deployer now stamps
// only the workload-shaped labels below at create time.
const (
LabelProject = "tinyforge.project"
LabelStage = "tinyforge.stage"
LabelInstanceID = "tinyforge.instance-id"
LabelManaged = "tinyforge.managed" // present on every Tinyforge-managed container
LabelWorkloadID = "tinyforge.workload.id" // workload row primary key
LabelWorkloadKind = "tinyforge.workload.kind" // 'project' | 'stack' | 'site'
+27 -44
View File
@@ -39,15 +39,6 @@ type ContainerConfig struct {
// Tinyforge management labels are added automatically via Project, Stage, and InstanceID.
Labels map[string]string
// Project is the Tinyforge project name (used for labelling).
Project string
// Stage is the Tinyforge stage name (used for labelling).
Stage string
// InstanceID is the Tinyforge instance ID (used for labelling).
InstanceID string
// WorkloadID is the unifying primitive's row ID (Workload.ID). Future
// reconciler / global views key off this label, so it must be set on
// every Tinyforge-managed container (project, stack, site).
@@ -106,12 +97,7 @@ func (c *Client) CreateContainer(ctx context.Context, cfg ContainerConfig) (stri
for k, v := range cfg.Labels {
labels[k] = v
}
// Legacy labels (kept for back-compat with operator runbooks /
// monitoring scrape rules; will be removed after the workload soak).
labels[LabelProject] = cfg.Project
labels[LabelStage] = cfg.Stage
labels[LabelInstanceID] = cfg.InstanceID
// Workload-shaped labels — canonical going forward.
// Workload-shaped labels — the canonical Tinyforge label set.
labels[LabelManaged] = "true"
if cfg.WorkloadID != "" {
labels[LabelWorkloadID] = cfg.WorkloadID
@@ -225,26 +211,27 @@ func (c *Client) RestartContainer(ctx context.Context, containerID string, timeo
}
// ManagedContainer holds summary information about a container managed by Tinyforge.
// WorkloadID/Kind/Role are pulled from the canonical Tinyforge labels.
type ManagedContainer struct {
ID string
Name string
Image string
Status string
State string
Project string
Stage string
InstanceID string
Ports []uint16
ID string
Name string
Image string
Status string
State string
WorkloadID string
WorkloadKind string
Role string
Ports []uint16
}
// ListContainers returns all containers matching the given label filters.
// Pass nil or an empty map to list all Tinyforge managed containers.
// Label filters are key=value pairs applied as Docker label filters.
// ListContainers returns all Tinyforge-managed containers (label
// tinyforge.managed=true), optionally narrowed by additional label filters.
// Returns the workload labels so callers can dispatch / display without an
// extra inspect call.
func (c *Client) ListContainers(ctx context.Context, labelFilters map[string]string) ([]ManagedContainer, error) {
filterArgs := make(client.Filters)
// Always filter by the Tinyforge project label to only return managed containers.
filterArgs.Add("label", LabelProject)
filterArgs.Add("label", LabelManaged+"=true")
for k, v := range labelFilters {
if v != "" {
@@ -278,15 +265,15 @@ func (c *Client) ListContainers(ctx context.Context, labelFilters map[string]str
}
result = append(result, ManagedContainer{
ID: ctr.ID,
Name: name,
Image: ctr.Image,
Status: ctr.Status,
State: string(ctr.State),
Project: ctr.Labels[LabelProject],
Stage: ctr.Labels[LabelStage],
InstanceID: ctr.Labels[LabelInstanceID],
Ports: ports,
ID: ctr.ID,
Name: name,
Image: ctr.Image,
Status: ctr.Status,
State: string(ctr.State),
WorkloadID: ctr.Labels[LabelWorkloadID],
WorkloadKind: ctr.Labels[LabelWorkloadKind],
Role: ctr.Labels[LabelRole],
Ports: ports,
})
}
@@ -308,9 +295,8 @@ type ReconcileItem struct {
// ListAllForReconciler returns every container the daemon knows about whose
// labels mark it as Tinyforge-managed by ANY of the supported schemes:
// - tinyforge.managed (canonical, new)
// - tinyforge.project / tinyforge.instance-id (legacy project)
// - tinyforge.static-site (legacy site)
// - tinyforge.managed (canonical — every project, stack, site we own)
// - tinyforge.static-site (sites that predate the workload labels)
// - com.docker.compose.project starting with "tinyforge-" (stacks)
//
// The Docker API does not support OR'd label filters, so we list everything
@@ -361,9 +347,6 @@ func isTinyforgeManaged(labels map[string]string) bool {
if labels[LabelManaged] == "true" {
return true
}
if labels[LabelProject] != "" || labels[LabelInstanceID] != "" {
return true
}
if _, ok := labels["tinyforge.static-site"]; ok {
return true
}
+1 -1
View File
@@ -32,7 +32,7 @@ func (c *Client) EnsureNetwork(ctx context.Context, networkName string) (string,
resp, err := c.api.NetworkCreate(ctx, networkName, client.NetworkCreateOptions{
Driver: "bridge",
Labels: map[string]string{
LabelProject: "tinyforge",
LabelManaged: "true",
},
})
if err != nil {