8d6a527a2b
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>
134 lines
4.7 KiB
Go
134 lines
4.7 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
"github.com/alexei/tinyforge/internal/staticsite"
|
|
"github.com/alexei/tinyforge/internal/store"
|
|
"github.com/alexei/tinyforge/internal/workload/plugin"
|
|
"github.com/alexei/tinyforge/internal/workload/plugin/source/static"
|
|
)
|
|
|
|
// staticBackend is the bridge between the plugin static source and the
|
|
// existing staticsite.Manager. The Manager operates on store.StaticSite
|
|
// rows keyed by site ID; this adapter keeps a phantom static_sites row
|
|
// for every plugin-native static workload (row ID = workload ID) so the
|
|
// Manager's deploy pipeline runs unchanged.
|
|
//
|
|
// The phantom row carries no UI weight — the legacy /api/static_sites
|
|
// endpoints will still surface it during the cutover window, which is
|
|
// fine: it lets operators inspect state through the existing legacy UI
|
|
// until /apps grows the equivalent screens. When the legacy cutover
|
|
// finishes, we can rewrite the static source to operate against the
|
|
// containers table directly and drop this adapter.
|
|
type staticBackend struct {
|
|
store *store.Store
|
|
mgr *staticsite.Manager
|
|
}
|
|
|
|
func newStaticBackend(st *store.Store, mgr *staticsite.Manager) *staticBackend {
|
|
return &staticBackend{store: st, mgr: mgr}
|
|
}
|
|
|
|
func (b *staticBackend) Deploy(ctx context.Context, _ plugin.Deps, w plugin.Workload, _ plugin.DeploymentIntent) error {
|
|
cfg, err := plugin.SourceConfigOf[static.Config](w)
|
|
if err != nil {
|
|
return fmt.Errorf("static backend: decode config: %w", err)
|
|
}
|
|
site, err := b.syncPhantomSite(w, cfg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return b.mgr.Deploy(ctx, site.ID, true /* force */)
|
|
}
|
|
|
|
func (b *staticBackend) Teardown(ctx context.Context, _ plugin.Deps, w plugin.Workload) error {
|
|
// Stop best-effort (the row may not exist yet if Deploy never ran).
|
|
if _, err := b.store.GetStaticSiteByID(w.ID); err == nil {
|
|
if err := b.mgr.Stop(ctx, w.ID); err != nil {
|
|
// Log via the manager's own pipeline; we keep going so the
|
|
// phantom row is always dropped.
|
|
_ = err
|
|
}
|
|
_ = b.store.DeleteStaticSite(w.ID)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (b *staticBackend) Reconcile(_ context.Context, _ plugin.Deps, w plugin.Workload) error {
|
|
// The staticsite.HealthChecker already polls every site row; no
|
|
// per-tick work is needed here. Reconcile becomes a no-op until the
|
|
// inline port lands.
|
|
_ = w
|
|
return nil
|
|
}
|
|
|
|
// syncPhantomSite upserts a store.StaticSite keyed on the workload ID,
|
|
// translating the plugin Config into the legacy shape. It is also where
|
|
// we shape the "single public face" expectation of the legacy table into
|
|
// a single domain string.
|
|
func (b *staticBackend) syncPhantomSite(w plugin.Workload, cfg static.Config) (store.StaticSite, error) {
|
|
domain := ""
|
|
for _, f := range w.PublicFaces {
|
|
// Pick the first enabled face. The API validator already caps
|
|
// faces at one for v1, but iterate defensively.
|
|
if f.Subdomain != "" || f.Domain != "" {
|
|
d := f.Domain
|
|
sub := f.Subdomain
|
|
switch {
|
|
case sub != "" && d != "":
|
|
domain = sub + "." + d
|
|
case sub == "" && d != "":
|
|
domain = d
|
|
case sub != "" && d == "":
|
|
// Domain falls back to settings.domain inside the
|
|
// Manager. Leave empty — Manager handles it.
|
|
domain = sub
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
site := store.StaticSite{
|
|
ID: w.ID,
|
|
Name: w.Name,
|
|
Provider: cfg.Provider,
|
|
GiteaURL: cfg.BaseURL,
|
|
RepoOwner: cfg.RepoOwner,
|
|
RepoName: cfg.RepoName,
|
|
Branch: cfg.Branch,
|
|
FolderPath: cfg.FolderPath,
|
|
AccessToken: cfg.AccessToken,
|
|
Domain: domain,
|
|
Mode: cfg.Mode,
|
|
RenderMarkdown: cfg.RenderMarkdown,
|
|
SyncTrigger: "manual",
|
|
StorageEnabled: cfg.StorageEnabled,
|
|
StorageLimitMB: cfg.StorageLimitMB,
|
|
NotificationURL: w.NotificationURL,
|
|
NotificationSecret: w.NotificationSecret,
|
|
WebhookSecret: w.WebhookSecret,
|
|
WebhookSigningSecret: w.WebhookSigningSecret,
|
|
WebhookRequireSignature: w.WebhookRequireSignature,
|
|
}
|
|
if err := b.store.UpsertStaticSiteWithID(site); err != nil {
|
|
return store.StaticSite{}, fmt.Errorf("static backend: sync phantom site: %w", err)
|
|
}
|
|
return site, nil
|
|
}
|
|
|
|
// wireStaticBackend installs the adapter so the plugin static source
|
|
// becomes deployable. Called once from main() after the staticsite
|
|
// Manager is constructed. Safe to call multiple times only because
|
|
// static.SetBackend itself panics on the second call — keeping the
|
|
// invariant explicit.
|
|
func wireStaticBackend(st *store.Store, mgr *staticsite.Manager) {
|
|
static.SetBackend(newStaticBackend(st, mgr))
|
|
}
|
|
|
|
// Unused but kept so the json import is referenced if we ever need to
|
|
// inspect raw SourceConfig blobs here for debugging.
|
|
var _ = json.Marshal
|