// Package layout defines Skiff's directory and dataset conventions in one // place. Nothing else in the codebase should hardcode a path. package layout import ( "fmt" "path/filepath" ) const ( DefaultHealthPort = 8080 DefaultHealthPath = "/healthz" HealthTimeoutSecs = 10 DefaultJailIface = "vtnet0" ) // Config is Skiff's host-wide configuration. Every field has a sane FreeBSD // default; override via flags on the skiff binary if a host differs. type Config struct { ZFSPool string // e.g. "zroot" BaseDataset string // template jail root, cloned per app ReposDir string // bare git repos live here JailsMount string // parent mountpoint for per-app datasets JailConfDir string // /etc/jail.conf.d NginxConfDir string // nginx include dir regenerated on every deploy LocksDir string // per-app flock files live here, outside any per-app dataset } func Default() Config { return Config{ ZFSPool: "zroot/skiff", BaseDataset: "zroot/skiff/base", ReposDir: "/skiff/repos", JailsMount: "/skiff/apps", JailConfDir: "/etc/jail.conf.d", NginxConfDir: "/usr/local/etc/nginx/skiff.d", LocksDir: "/skiff/locks", } } func (c Config) AppDataset(app string) string { return fmt.Sprintf("%s/apps/%s", c.ZFSPool, app) } func (c Config) JailRoot(app string) string { return filepath.Join(c.JailsMount, app) } func (c Config) RepoPath(app string) string { return filepath.Join(c.ReposDir, app+".git") } // LockPath is the per-app flock file that serializes deploy, config change, // and rollback against each other. It deliberately lives outside // AppDataset(app): that dataset gets `zfs rollback`ed on a failed health // check or a manual rollback, which would discard a lock file living inside // it along with everything else written since the snapshot. func (c Config) LockPath(app string) string { return filepath.Join(c.LocksDir, app+".lock") } func (c Config) JailConfPath(app string) string { return filepath.Join(c.JailConfDir, app+".conf") } func (c Config) NginxConfPath(app string) string { return filepath.Join(c.NginxConfDir, app+".conf") } func (c Config) PostReceiveHookPath(app string) string { return filepath.Join(c.RepoPath(app), "hooks", "post-receive") } // RCScriptPath, RCConfPath, and DMAConfPath are paths inside the jail's own // chroot, not the host's rc.d/rc.conf/dma.conf. func (c Config) RCScriptPath(app string) string { return filepath.Join(c.JailRoot(app), "usr", "local", "etc", "rc.d", app) } func (c Config) RCConfPath(app string) string { return filepath.Join(c.JailRoot(app), "etc", "rc.conf") } func (c Config) DMAConfPath(app string) string { return filepath.Join(c.JailRoot(app), "etc", "dma", "dma.conf") } // Everything below is Capistrano-style: each deploy gets its own immutable // releases/ checkout (built in place, inside the jail's own dataset), // `current` is a symlink to whichever release is live, and data.db/env are // persistent siblings that outlive any single release. func (c Config) AppDir(app string) string { return filepath.Join(c.JailRoot(app), "app") } func (c Config) ReleasesDir(app string) string { return filepath.Join(c.AppDir(app), "releases") } func (c Config) ReleaseDir(app, sha string) string { return filepath.Join(c.ReleasesDir(app), sha) } func (c Config) CurrentRelease(app string) string { return filepath.Join(c.AppDir(app), "current") } // ReleaseDirInJail is releases/ as seen from inside the jail's own // chroot, not the host. The `current` symlink must be written with this // form as its target: it's dereferenced by the jail's rc.d script (which // execs /app/current/app, per internal/tmpl/rc.d.tmpl's command_args), // where a host-absolute path like ReleaseDir's resolves relative to the // jail root instead and never exists. func (c Config) ReleaseDirInJail(sha string) string { return filepath.Join("/app", "releases", sha) } func (c Config) MetaPath(app string) string { return filepath.Join(c.AppDir(app), "meta.json") } func (c Config) EnvPath(app string) string { return filepath.Join(c.AppDir(app), "env") }