feat(static): inline static-source plugin; drop phantom-row adapter
Build / build (push) Successful in 10m43s
Build / build (push) Successful in 10m43s
Lift the static-site deploy pipeline from internal/staticsite/manager.go into internal/workload/plugin/source/static/ so plugin-native static workloads operate directly on plugin.Workload + the containers table + workload_env. The cmd/server/static_backend.go phantom-row adapter is gone; the legacy static_sites table is no longer touched on plugin deploys. Backend - new state.go: runtimeState (last_commit_sha, last_sync_at, last_error, status) persisted in containers.extra_json under the deterministic row id <workloadID>:site - per-workload sync.Mutex serializes saveState read-modify-write so parallel deploys for the same workload can't race container_id / proxy_route_id writes - extra_json round-trips through map[string]json.RawMessage so unknown keys survive — typed runtimeStateKeys are stripped before merge so clearing a typed field actually drops the key - new env.go reads workload_env (replaces static_site_secrets for plugin-native sites); decrypt-failure logs and skips one entry rather than failing the whole deploy - new build.go ports prepareDenoBuild + prepareStaticBuild + copyDir; copyDir uses filepath.WalkDir + Lstat to refuse symlinks and non-regular files - new deploy.go is the ~300-line core; intent.Reason gates force vs skip-if-no-changes; success-path saveState failure rolls back container + proxy route and writes "failed" state (no orphans) - new teardown.go combines Remove + Stop; idempotent on never-deployed workloads - new reconcile.go refreshes container state from Docker; flips runtimeState.Status to failed when the container is missing/crashed Hardening (from go-reviewer + security-reviewer subagent passes; 1 CRITICAL + 5 HIGH + 3 MEDIUM addressed before merge) - path-traversal defense in all 3 providers (gitea_content, github_provider, gitlab_provider): reject tree entries whose resolved local path escapes destDir - verifyDownloadInsideRoot walks the build dir post-download as a second line of defense - sanitizeError redacts the access token, collapses to one line, and clamps to 240 bytes before persisting to extra_json or fanning out to the notification webhook - container/image/volume names suffixed with workload-id short prefix (workload name is not UNIQUE in schema) - primaryDomain reads settings.Domain to complete a bare subdomain face into a full FQDN (matches legacy Manager behavior) - ctx-aware health-check sleep - json.Marshal for event metadata (was fmt.Sprintf JSON template) - strings.HasPrefix for failed-status detection (was brittle slice expression) Wire-up - cmd/server/main.go: removed wireStaticBackend(...) call; existing blank import on _ ".../source/static" drives init() registration - cmd/server/static_backend.go deleted Doc - WORKLOAD_REFACTOR_TODO: static port marked DONE; next focus is the hard legacy cutover (drop /api/projects, /api/stacks, /api/sites, /api/stages + their tables, internal/stack + internal/staticsite packages, frontend /projects /stacks /sites) Behavior notes for operators - plugin-native static workloads no longer write to static_sites; legacy /api/sites/* still serves original rows unchanged - legacy tinyforge.static-site / .static-site-name container labels dropped on plugin deploys; canonical tinyforge.workload.id / .kind cover ownership - container/image/volume names gained an 8-char ID suffix (e.g. dw-site-mysite-a1b2c3d4); legacy-deployed sites keep the old shape until redeployed through the plugin path
This commit is contained in:
@@ -1,16 +1,16 @@
|
||||
// Package static implements the "static" source: a git-folder-backed
|
||||
// deployable that can serve plain files or run a Deno backend. Builds an
|
||||
// image from the cloned folder and runs one container.
|
||||
// deployable that can serve plain files or run a Deno backend. Builds
|
||||
// an image from the cloned folder and runs one container.
|
||||
//
|
||||
// The full deploy pipeline lives in internal/staticsite (git providers,
|
||||
// markdown rendering, Dockerfile codegen, Deno scaffolding, image build,
|
||||
// proxy registration) and is wired in via a function variable so that
|
||||
// neither this package nor staticsite has to depend on the other.
|
||||
// The full deploy pipeline is implemented inline in this package
|
||||
// (deploy.go / teardown.go / reconcile.go). It operates directly on
|
||||
// plugin.Workload + the containers / workload_env tables — there is no
|
||||
// longer a synthetic static_sites row backing each workload.
|
||||
//
|
||||
// cmd/server/main.go (or any caller with access to both packages)
|
||||
// populates DeployFn / TeardownFn / ReconcileFn at startup; until then,
|
||||
// Source methods return an explicit error so misconfiguration surfaces
|
||||
// loudly instead of silently failing.
|
||||
// The legacy internal/staticsite package remains alive to serve the
|
||||
// /api/sites/* HTTP routes and the existing static_sites table; this
|
||||
// plugin does not depend on it for state, only for git-provider
|
||||
// helpers and Deno scaffolding generation.
|
||||
package static
|
||||
|
||||
import (
|
||||
@@ -18,15 +18,13 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/alexei/tinyforge/internal/workload/plugin"
|
||||
)
|
||||
|
||||
// Config is the per-workload source config blob. Mirrors the fields that
|
||||
// used to live on the static_sites table, less anything moved to Workload
|
||||
// (notification config, webhook secrets, public_face).
|
||||
// Config is the per-workload source config blob. Mirrors the fields
|
||||
// that used to live on the static_sites table, less anything moved to
|
||||
// Workload (notification config, webhook secrets, public_face).
|
||||
type Config struct {
|
||||
Provider string `json:"provider"` // "gitea" | "github" | "gitlab"; "" = autodetect
|
||||
BaseURL string `json:"base_url"` // e.g. https://git.example.com
|
||||
@@ -41,55 +39,12 @@ type Config struct {
|
||||
StorageLimitMB int `json:"storage_limit_mb"`
|
||||
}
|
||||
|
||||
// Backend captures the deploy lifecycle of a static site. main.go wires
|
||||
// an implementation that adapts internal/staticsite.Manager to this
|
||||
// interface; the plugin contract sees only this shape so it stays
|
||||
// independent of any specific manager type.
|
||||
type Backend interface {
|
||||
Deploy(ctx context.Context, deps plugin.Deps, w plugin.Workload, intent plugin.DeploymentIntent) error
|
||||
Teardown(ctx context.Context, deps plugin.Deps, w plugin.Workload) error
|
||||
Reconcile(ctx context.Context, deps plugin.Deps, w plugin.Workload) error
|
||||
}
|
||||
|
||||
var (
|
||||
backendMu sync.RWMutex
|
||||
backend Backend
|
||||
backendSet atomic.Bool
|
||||
)
|
||||
|
||||
// SetBackend wires the staticsite-package adapter into this Source AND
|
||||
// registers the source with the plugin registry. MUST be called exactly
|
||||
// once from cmd/server/main.go before any plugin invocation. Subsequent
|
||||
// calls panic — a swapped backend at runtime is a trust-boundary
|
||||
// inversion (a future plugin loaded via blank import could replace
|
||||
// deploy/teardown logic that handles git tokens).
|
||||
func SetBackend(b Backend) {
|
||||
if !backendSet.CompareAndSwap(false, true) {
|
||||
panic("static: backend already wired (SetBackend may be called once)")
|
||||
}
|
||||
backendMu.Lock()
|
||||
backend = b
|
||||
backendMu.Unlock()
|
||||
plugin.RegisterSource(&source{})
|
||||
}
|
||||
|
||||
func currentBackend() (Backend, error) {
|
||||
backendMu.RLock()
|
||||
defer backendMu.RUnlock()
|
||||
if backend == nil {
|
||||
return nil, fmt.Errorf("static source: backend not wired; call static.SetBackend from main.go")
|
||||
}
|
||||
return backend, nil
|
||||
}
|
||||
|
||||
type source struct{}
|
||||
|
||||
// Static source registers itself only after SetBackend is called from
|
||||
// main.go. Eager init() registration would advertise "static" via
|
||||
// /api/hooks/kinds before there is anything to dispatch to — frontends
|
||||
// would render it in pickers and operators would hit "backend not wired"
|
||||
// at deploy time. Lazy registration keeps the kind invisible until it's
|
||||
// actually usable.
|
||||
// Eager registration — the deploy pipeline lives entirely inside this
|
||||
// package now, so the kind is usable as soon as init() fires. No more
|
||||
// "backend not wired" failure mode at deploy time.
|
||||
func init() { plugin.RegisterSource(&source{}) }
|
||||
|
||||
func (*source) Kind() string { return "static" }
|
||||
|
||||
@@ -123,25 +78,13 @@ func (*source) Validate(cfg json.RawMessage) error {
|
||||
}
|
||||
|
||||
func (*source) Deploy(ctx context.Context, deps plugin.Deps, w plugin.Workload, intent plugin.DeploymentIntent) error {
|
||||
b, err := currentBackend()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return b.Deploy(ctx, deps, w, intent)
|
||||
return deploy(ctx, deps, w, intent)
|
||||
}
|
||||
|
||||
func (*source) Teardown(ctx context.Context, deps plugin.Deps, w plugin.Workload) error {
|
||||
b, err := currentBackend()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return b.Teardown(ctx, deps, w)
|
||||
return teardown(ctx, deps, w)
|
||||
}
|
||||
|
||||
func (*source) Reconcile(ctx context.Context, deps plugin.Deps, w plugin.Workload) error {
|
||||
b, err := currentBackend()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return b.Reconcile(ctx, deps, w)
|
||||
return reconcile(ctx, deps, w)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user