feat(workload): container index reconciler
Background worker that keeps the containers table in sync with docker ps. Runs one boot pass and ticks every 30s. Dispatch precedence per container: 1. tinyforge.workload.id label (canonical, new) 2. tinyforge.instance-id label (legacy project — joins via instances) 3. tinyforge.static-site label (legacy site) 4. com.docker.compose.project (stacks — joins via ComposeProjectName) Rows whose Docker container ID is no longer present are flipped to state='missing'. Placeholder rows (empty container_id, e.g. a deploy mid-flight) are left alone so a tick that races a deploy doesn't mark them as missing. DockerLister interface lets tests substitute a fake daemon — 6 unit tests cover the dispatch matrix, missing-sweep, and state normalization. Wired into cmd/server/main.go between docker.New and the existing startup chain. Boot pass populates the containers table from any pre-refactor running containers.
This commit is contained in:
@@ -293,6 +293,86 @@ func (c *Client) ListContainers(ctx context.Context, labelFilters map[string]str
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ReconcileItem is a fat container summary aimed at the reconciler — it
|
||||
// exposes the full label map so the caller can dispatch by workload labels,
|
||||
// legacy labels, or compose labels without re-inspecting.
|
||||
type ReconcileItem struct {
|
||||
ID string
|
||||
Name string
|
||||
Image string
|
||||
State string
|
||||
Status string
|
||||
Labels map[string]string
|
||||
Ports []uint16
|
||||
}
|
||||
|
||||
// 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)
|
||||
// - com.docker.compose.project starting with "tinyforge-" (stacks)
|
||||
//
|
||||
// The Docker API does not support OR'd label filters, so we list everything
|
||||
// and filter in-process. On a small/medium daemon this is cheap; the
|
||||
// reconciler runs on a 30s tick.
|
||||
func (c *Client) ListAllForReconciler(ctx context.Context) ([]ReconcileItem, error) {
|
||||
listResult, err := c.api.ContainerList(ctx, client.ContainerListOptions{All: true})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list containers: %w", err)
|
||||
}
|
||||
|
||||
out := make([]ReconcileItem, 0, len(listResult.Items))
|
||||
for _, ctr := range listResult.Items {
|
||||
labels := ctr.Labels
|
||||
if !isTinyforgeManaged(labels) {
|
||||
continue
|
||||
}
|
||||
|
||||
name := ""
|
||||
if len(ctr.Names) > 0 {
|
||||
name = strings.TrimPrefix(ctr.Names[0], "/")
|
||||
}
|
||||
var ports []uint16
|
||||
for _, p := range ctr.Ports {
|
||||
if p.PublicPort > 0 {
|
||||
ports = append(ports, p.PublicPort)
|
||||
}
|
||||
}
|
||||
out = append(out, ReconcileItem{
|
||||
ID: ctr.ID,
|
||||
Name: name,
|
||||
Image: ctr.Image,
|
||||
State: string(ctr.State),
|
||||
Status: ctr.Status,
|
||||
Labels: labels,
|
||||
Ports: ports,
|
||||
})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// isTinyforgeManaged returns true when a container's labels mark it as
|
||||
// belonging to Tinyforge under any of the supported labelling schemes.
|
||||
func isTinyforgeManaged(labels map[string]string) bool {
|
||||
if labels == nil {
|
||||
return false
|
||||
}
|
||||
if labels[LabelManaged] == "true" {
|
||||
return true
|
||||
}
|
||||
if labels[LabelProject] != "" || labels[LabelInstanceID] != "" {
|
||||
return true
|
||||
}
|
||||
if _, ok := labels["tinyforge.static-site"]; ok {
|
||||
return true
|
||||
}
|
||||
if cp, ok := labels["com.docker.compose.project"]; ok && strings.HasPrefix(cp, "tinyforge-") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ContainerLogs returns a log stream for a container.
|
||||
// If follow is true, the stream stays open for new log lines.
|
||||
// tail specifies the number of lines from the end to return (e.g., "200").
|
||||
|
||||
Reference in New Issue
Block a user