refactor(workload): plugin architecture wave + apps UI + volume scopes
Completes the workload-first refactor's plugin layer:
- internal/workload/plugin/ — Source/Trigger plugin contract,
registry, types (Workload, DeploymentIntent, InboundEvent,
PublicFace). Self-registering init() pattern + blank-import
in cmd/server/main.go.
- Source plugins: image (blue-green with multi-face proxy routing),
compose, static. Trigger plugins: registry, git, manual.
- internal/deployer/dispatch.go — DispatchPlugin/Teardown/Reconcile
seam routing the legacy deployer through plugins.
- internal/api/workload_*.go — REST surface: workloads, env,
volumes, chain (parent/children), promote-from. hooks.go
serves /api/hooks/kinds/{kind}/schema for the wizard.
- internal/store: workload_env (encrypt-at-rest secrets) and
workload_volumes tables, keyed on workload_id.
- cmd/server/static_backend.go — phantom-row adapter delegating
the static source plugin to the legacy staticsite.Manager
(deleted at hard cutover once the static inline port lands).
- web/src/routes/apps/ — /apps list + /apps/new wizard +
/apps/[id] detail with kind-aware compose / image / static
forms (Advanced JSON toggle), env panel, volumes panel,
webhook panel, chain panel, manual deploy.
Volume scope generalization (v2 resolver):
- internal/volume.ResolveWorkloadPath (workload-keyed, sits
next to legacy ResolvePath). Honors all VolumeScope values:
absolute, ephemeral, instance, stage, project, project_named,
named. internal/workload/plugin/source/image/image.go
computeMounts wires settings + imageTag through. Coverage in
internal/volume/resolver_test.go (portable Linux/Windows via
t.TempDir).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,7 @@ package reconciler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"strings"
|
||||
@@ -28,6 +29,7 @@ import (
|
||||
|
||||
"github.com/alexei/tinyforge/internal/docker"
|
||||
"github.com/alexei/tinyforge/internal/store"
|
||||
"github.com/alexei/tinyforge/internal/workload/plugin"
|
||||
)
|
||||
|
||||
// DockerLister is the subset of docker.Client the reconciler depends on.
|
||||
@@ -37,11 +39,19 @@ type DockerLister interface {
|
||||
ListAllForReconciler(ctx context.Context) ([]docker.ReconcileItem, error)
|
||||
}
|
||||
|
||||
// PluginReconciler is the optional dispatch surface for per-workload
|
||||
// Source.Reconcile calls. Nil-safe — when unset, the reconciler skips
|
||||
// the plugin pass and only refreshes the containers index from Docker.
|
||||
type PluginReconciler interface {
|
||||
DispatchReconcile(ctx context.Context, w plugin.Workload) error
|
||||
}
|
||||
|
||||
// Reconciler is the background worker that syncs the containers index.
|
||||
type Reconciler struct {
|
||||
store *store.Store
|
||||
docker DockerLister
|
||||
interval time.Duration
|
||||
plugins PluginReconciler // optional; nil disables the per-workload Source.Reconcile pass.
|
||||
|
||||
stop chan struct{}
|
||||
cancel context.CancelFunc // populated in Start; invoked by Stop so an in-flight tick is unblocked.
|
||||
@@ -66,6 +76,11 @@ func New(st *store.Store, dockerClient DockerLister, interval time.Duration) *Re
|
||||
}
|
||||
}
|
||||
|
||||
// SetPluginReconciler injects the per-workload Source.Reconcile dispatch.
|
||||
// Safe to call before or after Start; tick uses whatever's set at the
|
||||
// time.
|
||||
func (r *Reconciler) SetPluginReconciler(p PluginReconciler) { r.plugins = p }
|
||||
|
||||
// Start kicks off the background reconciliation loop. Runs one tick
|
||||
// immediately so startup populates the index without waiting for the first
|
||||
// timer fire. The provided context is wrapped with a child cancel func so
|
||||
@@ -115,9 +130,65 @@ func (r *Reconciler) ReconcileOnce(ctx context.Context) error {
|
||||
}
|
||||
|
||||
r.markMissingRows(seen)
|
||||
r.reconcilePluginWorkloads(ctx)
|
||||
return nil
|
||||
}
|
||||
|
||||
// reconcilePluginWorkloads iterates every workload row that opted into
|
||||
// the plugin pipeline (source_kind + trigger_kind both set) and asks the
|
||||
// dispatcher to invoke Source.Reconcile. Failures are logged per-workload
|
||||
// — one workload's broken state must not stop sweeping the rest.
|
||||
//
|
||||
// No-op when the plugin dispatcher hasn't been wired (boot-time race,
|
||||
// disabled deployments, tests).
|
||||
func (r *Reconciler) reconcilePluginWorkloads(ctx context.Context) {
|
||||
if r.plugins == nil {
|
||||
return
|
||||
}
|
||||
rows, err := r.store.ListWorkloads("")
|
||||
if err != nil {
|
||||
slog.Warn("reconciler: list workloads for plugin pass", "error", err)
|
||||
return
|
||||
}
|
||||
for _, w := range rows {
|
||||
if w.SourceKind == "" || w.TriggerKind == "" {
|
||||
continue
|
||||
}
|
||||
pw := toPluginWorkload(w)
|
||||
if err := r.plugins.DispatchReconcile(ctx, pw); err != nil {
|
||||
slog.Warn("reconciler: plugin reconcile failed",
|
||||
"workload", w.ID, "kind", w.SourceKind, "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// toPluginWorkload mirrors the api / webhook converters; kept local to
|
||||
// avoid an import dependency between those packages.
|
||||
func toPluginWorkload(w store.Workload) plugin.Workload {
|
||||
var faces []plugin.PublicFace
|
||||
if w.PublicFaces != "" {
|
||||
_ = json.Unmarshal([]byte(w.PublicFaces), &faces)
|
||||
}
|
||||
return plugin.Workload{
|
||||
ID: w.ID,
|
||||
Name: w.Name,
|
||||
GroupID: w.AppID,
|
||||
ParentWorkloadID: w.ParentWorkloadID,
|
||||
SourceKind: w.SourceKind,
|
||||
SourceConfig: json.RawMessage(w.SourceConfig),
|
||||
TriggerKind: w.TriggerKind,
|
||||
TriggerConfig: json.RawMessage(w.TriggerConfig),
|
||||
PublicFaces: faces,
|
||||
NotificationURL: w.NotificationURL,
|
||||
NotificationSecret: w.NotificationSecret,
|
||||
WebhookSecret: w.WebhookSecret,
|
||||
WebhookSigningSecret: w.WebhookSigningSecret,
|
||||
WebhookRequireSignature: w.WebhookRequireSignature,
|
||||
CreatedAt: w.CreatedAt,
|
||||
UpdatedAt: w.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Reconciler) loop(ctx context.Context) {
|
||||
defer r.wg.Done()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user