diff options
| author | Claude <noreply@anthropic.com> | 2026-08-07 19:02:40 -0500 |
|---|---|---|
| committer | Claude <noreply@anthropic.com> | 2026-08-07 19:02:40 -0500 |
| commit | acde3827f60bc90738429fa490a36a1856a93283 (patch) | |
| tree | ae4b345b30a1308170ffe3e4c00cc1a8360c796b /cmd | |
| parent | d1e1c226d9a63432c652d373c039a3ae4430f3a2 (diff) | |
Add per-app flock locking around deploy, config change, rollback, new-app
Nothing previously serialized these against each other, so concurrent
invocations for the same app (a manual config change racing an
in-flight deploy, a double git push, a rollback mid-deploy) could
interleave snapshot/apply/restart/rollback sequences against the same
dataset and jail. The lock file lives under a new LocksDir, outside
any per-app ZFS dataset, since those datasets get rolled back on a
failed health check or a manual rollback.
Diffstat (limited to 'cmd')
| -rw-r--r-- | cmd/skiff/main.go | 28 |
1 files changed, 27 insertions, 1 deletions
diff --git a/cmd/skiff/main.go b/cmd/skiff/main.go index 1c48681..7c9d505 100644 --- a/cmd/skiff/main.go +++ b/cmd/skiff/main.go @@ -16,6 +16,7 @@ import ( "skiff/internal/envfile" "skiff/internal/jailmgr" "skiff/internal/layout" + "skiff/internal/lock" "skiff/internal/sh" "skiff/internal/tmpl" "skiff/internal/zfsutil" @@ -65,7 +66,14 @@ func cmdDeploy(args []string) error { return nil } - return deploy.Run(context.Background(), os.Stdout, layout.Default(), *app, *newSHA) + cfg := layout.Default() + unlock, err := lock.Acquire(cfg.LockPath(*app)) + if err != nil { + return fmt.Errorf("acquire app lock: %w", err) + } + defer unlock.Release() + + return deploy.Run(context.Background(), os.Stdout, cfg, *app, *newSHA) } func cmdNewApp(args []string) error { @@ -89,6 +97,12 @@ func cmdNewApp(args []string) error { out := os.Stdout cfg := layout.Default() + unlock, err := lock.Acquire(cfg.LockPath(*name)) + if err != nil { + return fmt.Errorf("acquire app lock: %w", err) + } + defer unlock.Release() + fmt.Fprintf(out, "==> cloning %s@%s -> %s\n", cfg.BaseDataset, *baseSnapshot, cfg.AppDataset(*name)) if err := zfsutil.Clone(ctx, out, cfg.BaseDataset, *baseSnapshot, cfg.AppDataset(*name), cfg.JailRoot(*name)); err != nil { return fmt.Errorf("clone base dataset: %w", err) @@ -161,6 +175,12 @@ func cmdRollback(args []string) error { return fmt.Errorf("--to is required (use --list to see snapshots)") } + unlock, err := lock.Acquire(cfg.LockPath(*app)) + if err != nil { + return fmt.Errorf("acquire app lock: %w", err) + } + defer unlock.Release() + if err := zfsutil.Rollback(ctx, os.Stdout, dataset, *to); err != nil { return fmt.Errorf("rollback: %w", err) } @@ -275,6 +295,12 @@ func applyConfigChange(app string, mutate func(map[string]string) error) error { return fmt.Errorf("app %q not found: %w", app, err) } + unlock, err := lock.Acquire(cfg.LockPath(app)) + if err != nil { + return fmt.Errorf("acquire app lock: %w", err) + } + defer unlock.Release() + kv, err := envfile.Load(cfg.EnvPath(app)) if err != nil { return fmt.Errorf("load config: %w", err) |
