234c3c711e
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
91 lines
3.2 KiB
Go
91 lines
3.2 KiB
Go
// 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.
|
|
//
|
|
// 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.
|
|
//
|
|
// 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 (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"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).
|
|
type Config struct {
|
|
Provider string `json:"provider"` // "gitea" | "github" | "gitlab"; "" = autodetect
|
|
BaseURL string `json:"base_url"` // e.g. https://git.example.com
|
|
RepoOwner string `json:"repo_owner"`
|
|
RepoName string `json:"repo_name"`
|
|
Branch string `json:"branch"`
|
|
FolderPath string `json:"folder_path"` // path within repo
|
|
AccessToken string `json:"access_token"` // encrypted; optional for public repos
|
|
Mode string `json:"mode"` // "static" | "deno"
|
|
RenderMarkdown bool `json:"render_markdown"`
|
|
StorageEnabled bool `json:"storage_enabled"`
|
|
StorageLimitMB int `json:"storage_limit_mb"`
|
|
}
|
|
|
|
type source struct{}
|
|
|
|
// 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" }
|
|
|
|
func (*source) SchemaSample() any {
|
|
return Config{
|
|
Provider: "gitea",
|
|
BaseURL: "https://git.example.com",
|
|
RepoOwner: "owner",
|
|
RepoName: "pages",
|
|
Branch: "main",
|
|
FolderPath: "",
|
|
Mode: "static",
|
|
}
|
|
}
|
|
|
|
func (*source) Validate(cfg json.RawMessage) error {
|
|
var c Config
|
|
if len(cfg) == 0 {
|
|
return fmt.Errorf("static source: config is required")
|
|
}
|
|
if err := json.Unmarshal(cfg, &c); err != nil {
|
|
return fmt.Errorf("static source: invalid json: %w", err)
|
|
}
|
|
if strings.TrimSpace(c.RepoOwner) == "" || strings.TrimSpace(c.RepoName) == "" {
|
|
return fmt.Errorf("static source: repo_owner and repo_name are required")
|
|
}
|
|
if c.Mode != "" && c.Mode != "static" && c.Mode != "deno" {
|
|
return fmt.Errorf("static source: mode must be \"static\" or \"deno\"")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (*source) Deploy(ctx context.Context, deps plugin.Deps, w plugin.Workload, intent plugin.DeploymentIntent) error {
|
|
return deploy(ctx, deps, w, intent)
|
|
}
|
|
|
|
func (*source) Teardown(ctx context.Context, deps plugin.Deps, w plugin.Workload) error {
|
|
return teardown(ctx, deps, w)
|
|
}
|
|
|
|
func (*source) Reconcile(ctx context.Context, deps plugin.Deps, w plugin.Workload) error {
|
|
return reconcile(ctx, deps, w)
|
|
}
|