1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
|
// 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
AcmeWebrootDir string // static dir nginx serves ACME HTTP-01 challenge files from, shared by every app
LetsEncryptDir string // certbot's own state dir
}
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",
AcmeWebrootDir: "/skiff/acme-challenge",
LetsEncryptDir: "/usr/local/etc/letsencrypt",
}
}
// AppDataset is the OS layer, cloned from BaseDataset and mounted at
// JailRoot(app) - the jail's own root. Named "appbase" (not "apps") to
// read clearly alongside AppDataDataset now that the two are siblings.
func (c Config) AppDataset(app string) string {
return fmt.Sprintf("%s/appbase/%s", c.ZFSPool, app)
}
// AppDataDataset is the persistent data layer, mounted at AppDir(app):
// releases/, current, env, meta.json, and the app's own data.db. It is a
// separate dataset from AppDataset so `skiff rebuild` can destroy and
// reclone the OS layer without touching it.
func (c Config) AppDataDataset(app string) string {
return fmt.Sprintf("%s/appdata/%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")
}
// LetsEncryptLiveDir is where certbot writes the live cert/key for a
// certificate named certName (Skiff always names one after its app's
// primary domain).
func (c Config) LetsEncryptLiveDir(certName string) string {
return filepath.Join(c.LetsEncryptDir, "live", certName)
}
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/<sha> 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/<sha> 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")
}
|