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:
@@ -0,0 +1,114 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Source is the contract for one deployable shape (image, compose, static,
|
||||
// ...). Implementations are stateless: every method receives Deps so the
|
||||
// same value can serve concurrent deploys safely.
|
||||
//
|
||||
// A Source owns the full lifecycle of its containers — it is expected to
|
||||
// reconcile rows in the containers index, register/deregister proxy
|
||||
// routes via Deps.Proxy, and manage DNS via Deps.DNS. The deployer
|
||||
// pipeline only chooses the right Source and feeds it a DeploymentIntent.
|
||||
type Source interface {
|
||||
// Kind is the registration key (e.g. "image", "compose", "static").
|
||||
Kind() string
|
||||
|
||||
// Validate type-checks a raw config blob before it is persisted.
|
||||
// Return a user-friendly error — the message is shown in the UI.
|
||||
Validate(cfg json.RawMessage) error
|
||||
|
||||
// Deploy executes one deployment of w using intent. Whether this is a
|
||||
// fresh start, an update, or a no-op is the Source's call: e.g. an
|
||||
// image source short-circuits if the requested tag already runs.
|
||||
Deploy(ctx context.Context, deps Deps, w Workload, intent DeploymentIntent) error
|
||||
|
||||
// Teardown removes everything Deploy created (containers, proxy
|
||||
// routes, DNS, source-specific state). Idempotent.
|
||||
Teardown(ctx context.Context, deps Deps, w Workload) error
|
||||
|
||||
// Reconcile brings the containers index in sync with reality. Called
|
||||
// by the periodic reconciler — must be cheap when nothing changed.
|
||||
Reconcile(ctx context.Context, deps Deps, w Workload) error
|
||||
}
|
||||
|
||||
var (
|
||||
sourcesMu sync.RWMutex
|
||||
sources = map[string]Source{}
|
||||
)
|
||||
|
||||
// RegisterSource installs s under s.Kind(). Panics on duplicate
|
||||
// registration: that always indicates a bug in init() ordering, not a
|
||||
// recoverable runtime condition.
|
||||
func RegisterSource(s Source) {
|
||||
sourcesMu.Lock()
|
||||
defer sourcesMu.Unlock()
|
||||
k := s.Kind()
|
||||
if _, dup := sources[k]; dup {
|
||||
panic(fmt.Sprintf("plugin: source %q already registered", k))
|
||||
}
|
||||
sources[k] = s
|
||||
}
|
||||
|
||||
// GetSource returns the Source registered for kind, or an error mentioning
|
||||
// the kind that was missing — useful when a workload row references a
|
||||
// kind whose package was not blank-imported.
|
||||
func GetSource(kind string) (Source, error) {
|
||||
sourcesMu.RLock()
|
||||
defer sourcesMu.RUnlock()
|
||||
s, ok := sources[kind]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("plugin: no source registered for kind %q", kind)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Schemaer is the optional interface a Source or Trigger may implement
|
||||
// to surface a sample config blob. The /api/hooks/kinds/{kind}/schema
|
||||
// endpoint uses this so frontends can render kind-aware forms without
|
||||
// hardcoding samples per call-site. Plugins that don't implement it
|
||||
// produce an empty object on the wire.
|
||||
type Schemaer interface {
|
||||
SchemaSample() any
|
||||
}
|
||||
|
||||
// SchemaSampleFor returns the typed sample value declared by the plugin
|
||||
// registered under kind, or nil if no sample is published.
|
||||
func SchemaSampleFor(kind string) (any, bool) {
|
||||
sourcesMu.RLock()
|
||||
if s, ok := sources[kind]; ok {
|
||||
sourcesMu.RUnlock()
|
||||
if sm, ok := s.(Schemaer); ok {
|
||||
return sm.SchemaSample(), true
|
||||
}
|
||||
return nil, true
|
||||
}
|
||||
sourcesMu.RUnlock()
|
||||
triggersMu.RLock()
|
||||
defer triggersMu.RUnlock()
|
||||
if t, ok := triggers[kind]; ok {
|
||||
if sm, ok := t.(Schemaer); ok {
|
||||
return sm.SchemaSample(), true
|
||||
}
|
||||
return nil, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// SourceKinds returns all registered source kinds, sorted for stable
|
||||
// listing in /api/workloads/source-kinds.
|
||||
func SourceKinds() []string {
|
||||
sourcesMu.RLock()
|
||||
defer sourcesMu.RUnlock()
|
||||
out := make([]string, 0, len(sources))
|
||||
for k := range sources {
|
||||
out = append(out, k)
|
||||
}
|
||||
sortStrings(out)
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user