@rip-lang/stamp 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +217 -67
- package/directives/dataset.rip +1 -1
- package/directives/ensure.rip +29 -0
- package/directives/firewall.rip +11 -5
- package/directives/incus.rip +6 -3
- package/directives/pool.rip +8 -2
- package/directives/ssh.rip +8 -2
- package/package.json +24 -23
- package/src/cli.rip +4 -3
- package/src/engine.rip +12 -8
- package/src/parser.rip +1 -1
- package/stamps/basic +17 -0
- package/stamps/host +102 -0
- package/stamps/mac-install +22 -0
- package/stamps/mac-vm +95 -0
package/README.md
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
**Declarative host provisioning — no state file, no agent, no YAML.**
|
|
6
6
|
|
|
7
|
-
Stamp reads a Stampfile, resolves each directive to a handler, and
|
|
8
|
-
the declared state against reality
|
|
9
|
-
and
|
|
7
|
+
Stamp reads a Stampfile, resolves each directive to a handler, and
|
|
8
|
+
reconciles the declared state against reality. No state file to lose.
|
|
9
|
+
No agent to install. No YAML to wrestle. Just a blueprint and an engine.
|
|
10
10
|
|
|
11
11
|
```
|
|
12
12
|
stamp apply Hostfile
|
|
@@ -14,49 +14,64 @@ stamp apply Hostfile
|
|
|
14
14
|
|
|
15
15
|
---
|
|
16
16
|
|
|
17
|
+
## Why Stamp?
|
|
18
|
+
|
|
19
|
+
- **No state file.** Every handler queries the live system. The Stampfile
|
|
20
|
+
IS the source of truth. Running `stamp apply` twice is always safe.
|
|
21
|
+
- **Tiny directives.** Each handler is 30–50 lines of Rip. Three functions:
|
|
22
|
+
`check`, `apply`, `verify`. No imports, no boilerplate.
|
|
23
|
+
- **Injection-safe by default.** Shell commands use `$"..."` tagged templates —
|
|
24
|
+
interpolated values can never become shell code.
|
|
25
|
+
- **Pluggable.** Drop a `.rip` file in `directives/` and it just works.
|
|
26
|
+
Community handlers are npm packages with three exported functions.
|
|
27
|
+
- **Cross-platform.** Works on macOS (Homebrew, Multipass) and Linux
|
|
28
|
+
(apt-get, ZFS, Incus, systemd).
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
17
32
|
## Quick Start
|
|
18
33
|
|
|
19
34
|
```bash
|
|
20
|
-
#
|
|
21
|
-
stamp
|
|
35
|
+
stamp plan Hostfile # preview what would change
|
|
36
|
+
stamp apply Hostfile # make it so
|
|
37
|
+
stamp verify Hostfile # audit current state
|
|
38
|
+
```
|
|
22
39
|
|
|
23
|
-
|
|
24
|
-
|
|
40
|
+
All three commands are read-safe. `plan` and `verify` never modify
|
|
41
|
+
anything. `apply` only changes what doesn't match.
|
|
25
42
|
|
|
26
|
-
|
|
27
|
-
stamp verify Hostfile
|
|
28
|
-
```
|
|
43
|
+
---
|
|
29
44
|
|
|
30
45
|
## Stampfile Syntax
|
|
31
46
|
|
|
32
47
|
A Stampfile declares the desired state of a system. Each top-level entry
|
|
33
48
|
names a resource using a **directive**. Indented lines below it describe
|
|
34
|
-
properties.
|
|
49
|
+
properties. The file is read top to bottom — order IS the dependency order.
|
|
50
|
+
|
|
51
|
+
### Inline — one line, done
|
|
35
52
|
|
|
36
53
|
```
|
|
37
54
|
packages curl git jq zfsutils-linux
|
|
55
|
+
```
|
|
38
56
|
|
|
39
|
-
|
|
40
|
-
compression zstd
|
|
41
|
-
atime off
|
|
57
|
+
### Block — directive + properties
|
|
42
58
|
|
|
59
|
+
```
|
|
43
60
|
container web ubuntu/24.04
|
|
44
61
|
profile trusted
|
|
45
62
|
disk data /tank/web -> /data
|
|
46
63
|
start
|
|
47
|
-
|
|
48
|
-
firewall
|
|
49
|
-
default deny incoming
|
|
50
|
-
default allow outgoing
|
|
51
|
-
allow ssh
|
|
52
|
-
allow 443/tcp
|
|
53
64
|
```
|
|
54
65
|
|
|
55
|
-
###
|
|
66
|
+
### Expanded — property with its own sub-block
|
|
56
67
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
68
|
+
```
|
|
69
|
+
container web ubuntu/24.04
|
|
70
|
+
disk data
|
|
71
|
+
source /tank/web
|
|
72
|
+
path /data
|
|
73
|
+
readonly true
|
|
74
|
+
```
|
|
60
75
|
|
|
61
76
|
### Variables
|
|
62
77
|
|
|
@@ -65,11 +80,17 @@ set POOL tank
|
|
|
65
80
|
set DEVICE /dev/sdb
|
|
66
81
|
|
|
67
82
|
pool $POOL $DEVICE
|
|
83
|
+
compression zstd
|
|
84
|
+
atime off
|
|
85
|
+
mountpoint /tank
|
|
68
86
|
```
|
|
69
87
|
|
|
88
|
+
Variables are expanded before parsing. `$NAME` and `${NAME}` both work.
|
|
89
|
+
Undefined variables expand to the empty string.
|
|
90
|
+
|
|
70
91
|
### The `->` operator
|
|
71
92
|
|
|
72
|
-
Source-to-destination mapping:
|
|
93
|
+
Source-to-destination mapping for disks, mounts, and similar:
|
|
73
94
|
|
|
74
95
|
```
|
|
75
96
|
disk data /tank/web -> /data
|
|
@@ -88,78 +109,207 @@ datasets
|
|
|
88
109
|
mode 2775
|
|
89
110
|
```
|
|
90
111
|
|
|
91
|
-
|
|
112
|
+
This is syntactic sugar — `datasets` decomposes to individual `dataset`
|
|
113
|
+
calls before dispatching.
|
|
92
114
|
|
|
93
|
-
|
|
94
|
-
|-----------|---------|
|
|
95
|
-
| `brew` | Homebrew packages (macOS) |
|
|
96
|
-
| `packages` | System packages (apt-get) |
|
|
97
|
-
| `pool` | ZFS pool creation |
|
|
98
|
-
| `dataset` | ZFS dataset with ownership/permissions |
|
|
99
|
-
| `profile` | Incus profile configuration |
|
|
100
|
-
| `container` | Incus container management |
|
|
101
|
-
| `incus` | Incus daemon initialization |
|
|
102
|
-
| `multipass` | Multipass virtual machines |
|
|
103
|
-
| `user` | System user management |
|
|
104
|
-
| `group` | System group management |
|
|
105
|
-
| `firewall` | ufw firewall rules |
|
|
106
|
-
| `ssh` | SSH daemon configuration |
|
|
107
|
-
| `service` | systemd service management |
|
|
115
|
+
---
|
|
108
116
|
|
|
109
|
-
##
|
|
117
|
+
## Examples
|
|
110
118
|
|
|
111
|
-
|
|
119
|
+
### macOS: Create an Ubuntu VM with Multipass
|
|
112
120
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
- **verify(name, props)** — return `[{ status, message }]` results
|
|
121
|
+
```
|
|
122
|
+
brew multipass
|
|
116
123
|
|
|
117
|
-
|
|
124
|
+
multipass stamp 24.04
|
|
125
|
+
cpus 2
|
|
126
|
+
memory 4G
|
|
127
|
+
disk 20G
|
|
128
|
+
start
|
|
118
129
|
|
|
119
|
-
|
|
130
|
+
ensure unzip
|
|
131
|
+
check multipass exec stamp -- which unzip
|
|
132
|
+
apply multipass exec stamp -- sudo apt-get update -qq
|
|
133
|
+
apply multipass exec stamp -- sudo apt-get install -y -qq unzip
|
|
120
134
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
4. npm (`@stamp/<name>` or `stamp-<name>`)
|
|
125
|
-
5. Remote (via `use` directive)
|
|
135
|
+
ensure bun
|
|
136
|
+
check multipass exec stamp -- test -x /home/ubuntu/.bun/bin/bun
|
|
137
|
+
apply multipass exec stamp -- bash -lc "curl -fsSL https://bun.sh/install | bash"
|
|
126
138
|
|
|
127
|
-
|
|
139
|
+
ensure rip
|
|
140
|
+
check multipass exec stamp -- test -x /home/ubuntu/.bun/bin/rip
|
|
141
|
+
apply multipass exec stamp -- /home/ubuntu/.bun/bin/bun add -g rip-lang
|
|
142
|
+
```
|
|
128
143
|
|
|
129
|
-
|
|
130
|
-
|
|
144
|
+
One file installs Multipass, creates a VM, and bootstraps Bun + Rip
|
|
145
|
+
inside it. Run it again — everything shows `ok`, nothing changes.
|
|
146
|
+
|
|
147
|
+
### Linux: Provision an Incus + ZFS host
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
set POOL tank
|
|
151
|
+
set DEVICE /dev/disk/by-id/scsi-0Linode_Volume_tank
|
|
152
|
+
set MOUNT /tank
|
|
153
|
+
|
|
154
|
+
packages
|
|
155
|
+
zfsutils-linux
|
|
156
|
+
incus
|
|
157
|
+
openssh-server
|
|
158
|
+
fail2ban
|
|
159
|
+
ufw
|
|
160
|
+
|
|
161
|
+
pool $POOL $DEVICE
|
|
162
|
+
compression zstd
|
|
163
|
+
atime off
|
|
164
|
+
mountpoint $MOUNT
|
|
165
|
+
|
|
166
|
+
dataset $POOL/home
|
|
167
|
+
dataset $POOL/home/web
|
|
168
|
+
owner 1000:1000
|
|
131
169
|
|
|
170
|
+
incus
|
|
171
|
+
storage default zfs $POOL/incus/default
|
|
172
|
+
network incusbr0
|
|
173
|
+
|
|
174
|
+
profile trusted
|
|
175
|
+
security.privileged true
|
|
176
|
+
limits.memory 2GB
|
|
177
|
+
limits.cpu 2
|
|
178
|
+
|
|
179
|
+
container web ubuntu/24.04
|
|
180
|
+
profile trusted
|
|
181
|
+
disk home $MOUNT/home/web -> /home
|
|
182
|
+
user shreeve 1000:1000
|
|
183
|
+
start
|
|
184
|
+
|
|
185
|
+
firewall
|
|
186
|
+
default deny incoming
|
|
187
|
+
default allow outgoing
|
|
188
|
+
allow ssh
|
|
189
|
+
|
|
190
|
+
service fail2ban
|
|
191
|
+
|
|
192
|
+
ssh
|
|
193
|
+
password-auth no
|
|
194
|
+
permit-root-login prohibit-password
|
|
195
|
+
pubkey-auth yes
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
This replaces ~600 lines of bash scripts, preseed templates, and
|
|
199
|
+
numbered setup files with a single declarative file.
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Built-in Directives
|
|
204
|
+
|
|
205
|
+
| Directive | Purpose | Platform |
|
|
206
|
+
|-------------|----------------------------------------|----------|
|
|
207
|
+
| `brew` | Homebrew packages | macOS |
|
|
208
|
+
| `packages` | System packages (apt-get) | Linux |
|
|
209
|
+
| `ensure` | Guarded imperative commands | any |
|
|
210
|
+
| `pool` | ZFS pool creation | Linux |
|
|
211
|
+
| `dataset` | ZFS dataset with ownership/permissions | Linux |
|
|
212
|
+
| `profile` | Incus profile configuration | Linux |
|
|
213
|
+
| `container` | Incus container management | Linux |
|
|
214
|
+
| `incus` | Incus daemon initialization | Linux |
|
|
215
|
+
| `multipass` | Multipass virtual machines | macOS |
|
|
216
|
+
| `user` | System user management | Linux |
|
|
217
|
+
| `group` | System group management | Linux |
|
|
218
|
+
| `firewall` | ufw firewall rules | Linux |
|
|
219
|
+
| `ssh` | SSH daemon configuration | Linux |
|
|
220
|
+
| `service` | systemd service management | Linux |
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## Writing a Directive
|
|
225
|
+
|
|
226
|
+
A directive is a `.rip` file that exports three functions. That's it.
|
|
227
|
+
No imports needed — `sh`, `ok`, and `run` are available globally.
|
|
228
|
+
|
|
229
|
+
```coffee
|
|
132
230
|
export name = "mydirective"
|
|
133
231
|
export description = "What it does"
|
|
134
232
|
|
|
135
233
|
export check = (name, props) ->
|
|
136
|
-
return 'missing' unless ok "some-check"
|
|
234
|
+
return 'missing' unless ok $"some-check #{name}"
|
|
137
235
|
'ok'
|
|
138
236
|
|
|
139
237
|
export apply = (name, props) ->
|
|
140
|
-
sh "some-command"
|
|
238
|
+
sh $"some-command #{name}"
|
|
141
239
|
|
|
142
240
|
export verify = (name, props) ->
|
|
143
|
-
|
|
241
|
+
results = []
|
|
242
|
+
if ok $"some-check #{name}"
|
|
243
|
+
results.push { status: 'pass', message: "#{name} is good" }
|
|
244
|
+
else
|
|
245
|
+
results.push { status: 'fail', message: "#{name} is missing" }
|
|
246
|
+
results
|
|
144
247
|
```
|
|
145
248
|
|
|
249
|
+
### Shell helpers
|
|
250
|
+
|
|
251
|
+
| Function | Returns | Use case |
|
|
252
|
+
|--------------|----------------------------------|------------------------|
|
|
253
|
+
| `sh $"cmd"` | stdout string, throws on failure | Do the thing |
|
|
254
|
+
| `ok $"cmd"` | boolean | Does this exist? |
|
|
255
|
+
| `run $"cmd"` | `{ ok, stdout, stderr, code }` | Need the full picture |
|
|
256
|
+
| `sh [array]` | stdout string, throws on failure | Dynamic argument lists |
|
|
257
|
+
|
|
258
|
+
The `$"..."` syntax prevents shell injection — interpolated values are
|
|
259
|
+
passed as separate arguments, never interpreted by a shell.
|
|
260
|
+
|
|
261
|
+
### Handler contract
|
|
262
|
+
|
|
263
|
+
- **check** has no side effects. Returns `"ok"`, `"drift"`, or `"missing"`.
|
|
264
|
+
- **apply** is idempotent. Only called when check returns something other
|
|
265
|
+
than `"ok"`. The engine runs a post-apply re-check to confirm success.
|
|
266
|
+
- **verify** has no side effects. Returns `[{ status, message }]` results
|
|
267
|
+
where status is `"pass"`, `"warn"`, or `"fail"`.
|
|
268
|
+
|
|
269
|
+
### Plugin resolution
|
|
270
|
+
|
|
271
|
+
Handlers resolve in this order:
|
|
272
|
+
|
|
273
|
+
1. **Built-in** — `directives/` in the stamp package
|
|
274
|
+
2. **Local** — `./directives/` beside the Stampfile
|
|
275
|
+
3. **Installed** — `~/.stamp/directives/`
|
|
276
|
+
4. **npm** — `@stamp/<name>` or `stamp-<name>`
|
|
277
|
+
5. **Remote** — fetched via `use` directive in the Stampfile
|
|
278
|
+
|
|
279
|
+
The first match wins. Drop a file in `./directives/` beside your
|
|
280
|
+
Stampfile to override any built-in.
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
146
284
|
## CLI
|
|
147
285
|
|
|
148
286
|
```
|
|
149
|
-
stamp apply [file]
|
|
150
|
-
stamp verify [file]
|
|
151
|
-
stamp plan [file]
|
|
152
|
-
stamp list
|
|
153
|
-
stamp info <directive>
|
|
154
|
-
stamp version
|
|
155
|
-
stamp help
|
|
287
|
+
stamp apply [file] Reconcile system to match Stampfile
|
|
288
|
+
stamp verify [file] Check current state, report PASS/WARN/FAIL
|
|
289
|
+
stamp plan [file] Dry-run: show what apply would do
|
|
290
|
+
stamp list Show all available directives
|
|
291
|
+
stamp info <directive> Show a directive's syntax and properties
|
|
292
|
+
stamp version Print version
|
|
293
|
+
stamp help Show help
|
|
156
294
|
```
|
|
157
295
|
|
|
158
296
|
Default file search: `Stampfile`, `Hostfile`, `Containerfile`.
|
|
159
297
|
|
|
298
|
+
### Exit codes
|
|
299
|
+
|
|
300
|
+
| Code | Meaning |
|
|
301
|
+
|------|-----------------------------------------------------------------|
|
|
302
|
+
| 0 | Success (apply completed, verify had no FAILs, plan found nothing) |
|
|
303
|
+
| 1 | Failure (apply error, verify had FAILs, plan found changes) |
|
|
304
|
+
| 2 | Usage error (bad arguments, file not found) |
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
160
308
|
## Runtime
|
|
161
309
|
|
|
162
|
-
Bun + Rip. Zero dependencies beyond `rip-lang`.
|
|
310
|
+
Bun + Rip. Zero dependencies beyond `rip-lang`. The entire engine —
|
|
311
|
+
parser, handler resolution, execution loop, and shell helpers — is
|
|
312
|
+
524 lines of Rip.
|
|
163
313
|
|
|
164
314
|
## License
|
|
165
315
|
|
package/directives/dataset.rip
CHANGED
|
@@ -15,7 +15,7 @@ export check = (path, props) ->
|
|
|
15
15
|
'ok'
|
|
16
16
|
|
|
17
17
|
export apply = (path, props) ->
|
|
18
|
-
sh $"zfs create #{path}" unless ok $"zfs list #{path}"
|
|
18
|
+
sh $"zfs create -p #{path}" unless ok $"zfs list #{path}"
|
|
19
19
|
mountpoint = sh $"zfs get -H -o value mountpoint #{path}"
|
|
20
20
|
sh $"chown #{props.owner[0].args[0]} #{mountpoint}" if props.owner
|
|
21
21
|
sh $"chmod #{props.mode[0].args[0]} #{mountpoint}" if props.mode
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export name = "ensure"
|
|
2
|
+
export description = "Ensures a condition is met, running commands if not"
|
|
3
|
+
export positional = ["name"]
|
|
4
|
+
export properties =
|
|
5
|
+
check: { description: "Command to test — exit 0 means ok" }
|
|
6
|
+
apply: { description: "Commands to run if check fails", multi: true }
|
|
7
|
+
|
|
8
|
+
getCheck = (props) ->
|
|
9
|
+
props.check?[0]?.args
|
|
10
|
+
|
|
11
|
+
export check = (name, props) ->
|
|
12
|
+
return 'ok' unless args = getCheck(props)
|
|
13
|
+
if ok args then 'ok' else 'missing'
|
|
14
|
+
|
|
15
|
+
export apply = (name, props) ->
|
|
16
|
+
if props.apply
|
|
17
|
+
for entry in props.apply
|
|
18
|
+
sh entry.args
|
|
19
|
+
|
|
20
|
+
export verify = (name, props) ->
|
|
21
|
+
results = []
|
|
22
|
+
unless args = getCheck props
|
|
23
|
+
results.push { status: 'warn', message: "#{name} has no check command" }
|
|
24
|
+
return results
|
|
25
|
+
if ok args
|
|
26
|
+
results.push { status: 'pass', message: "#{name} is present" }
|
|
27
|
+
else
|
|
28
|
+
results.push { status: 'fail', message: "#{name} is missing" }
|
|
29
|
+
results
|
package/directives/firewall.rip
CHANGED
|
@@ -18,13 +18,14 @@ export check = (name, props) ->
|
|
|
18
18
|
status = sh $"ufw status verbose"
|
|
19
19
|
for entry in props.default
|
|
20
20
|
[action, direction] = entry.args
|
|
21
|
-
return 'drift' unless status.includes action
|
|
21
|
+
return 'drift' unless status.includes "#{action} (#{direction})"
|
|
22
22
|
|
|
23
23
|
if props.allow
|
|
24
|
-
status
|
|
24
|
+
status ??= sh $"ufw status verbose"
|
|
25
25
|
for entry in props.allow
|
|
26
26
|
rule = entry.args.join ' '
|
|
27
|
-
|
|
27
|
+
port = (run $"getent services #{rule}").stdout.split(/\s+/)?[1] or ''
|
|
28
|
+
return 'drift' unless status.toUpperCase().includes(rule.toUpperCase()) or status.includes(port)
|
|
28
29
|
|
|
29
30
|
'ok'
|
|
30
31
|
|
|
@@ -65,14 +66,19 @@ export verify = (name, props) ->
|
|
|
65
66
|
status = sh $"ufw status verbose"
|
|
66
67
|
for entry in props.default
|
|
67
68
|
[action, direction] = entry.args
|
|
68
|
-
if status.includes action
|
|
69
|
+
if status.includes "#{action} (#{direction})"
|
|
69
70
|
results.push { status: 'pass', message: "default #{action} #{direction}" }
|
|
70
71
|
else
|
|
71
72
|
results.push { status: 'fail', message: "default #{action} #{direction} not set" }
|
|
72
73
|
|
|
73
74
|
if props.allow
|
|
75
|
+
status ??= sh $"ufw status verbose"
|
|
74
76
|
for entry in props.allow
|
|
75
77
|
rule = entry.args.join ' '
|
|
76
|
-
|
|
78
|
+
port = (run $"getent services #{rule}").stdout.split(/\s+/)?[1] or ''
|
|
79
|
+
if status.toUpperCase().includes(rule.toUpperCase()) or status.includes(port)
|
|
80
|
+
results.push { status: 'pass', message: "allow #{rule}" }
|
|
81
|
+
else
|
|
82
|
+
results.push { status: 'fail', message: "allow #{rule} not found" }
|
|
77
83
|
|
|
78
84
|
results
|
package/directives/incus.rip
CHANGED
|
@@ -11,6 +11,7 @@ export check = (name, props) ->
|
|
|
11
11
|
if props.storage
|
|
12
12
|
poolName = props.storage[0].args[0]
|
|
13
13
|
return 'drift' unless ok $"incus storage show #{poolName}"
|
|
14
|
+
return 'drift' unless ok $"incus profile device get default root pool"
|
|
14
15
|
|
|
15
16
|
if props.network
|
|
16
17
|
bridge = props.network[0].args[0]
|
|
@@ -76,14 +77,16 @@ export apply = (name, props) ->
|
|
|
76
77
|
raise "incus admin init failed: #{out.stderr.toString().trim()}"
|
|
77
78
|
else
|
|
78
79
|
if props.storage
|
|
79
|
-
poolName = props.storage[0].args
|
|
80
|
+
[poolName, driver, source] = props.storage[0].args
|
|
80
81
|
unless ok $"incus storage show #{poolName}"
|
|
81
|
-
|
|
82
|
+
sh $"incus storage create #{poolName} #{driver} source=#{source}"
|
|
83
|
+
unless ok $"incus profile device get default root pool"
|
|
84
|
+
sh $"incus profile device add default root disk path=/ pool=#{poolName}"
|
|
82
85
|
|
|
83
86
|
if props.network
|
|
84
87
|
bridge = props.network[0].args[0]
|
|
85
88
|
unless ok $"incus network show #{bridge}"
|
|
86
|
-
|
|
89
|
+
sh $"incus network create #{bridge}"
|
|
87
90
|
|
|
88
91
|
export verify = (name, props) ->
|
|
89
92
|
results = []
|
package/directives/pool.rip
CHANGED
|
@@ -9,6 +9,12 @@ export properties =
|
|
|
9
9
|
|
|
10
10
|
export check = (name, props) ->
|
|
11
11
|
return 'missing' unless ok $"zpool list #{name}"
|
|
12
|
+
if want = props.compression?[0]?.args?[0]
|
|
13
|
+
return 'drift' if want isnt sh $"zfs get -H -o value compression #{name}"
|
|
14
|
+
if want = props.atime?[0]?.args?[0]
|
|
15
|
+
return 'drift' if want isnt sh $"zfs get -H -o value atime #{name}"
|
|
16
|
+
if want = props.mountpoint?[0]?.args?[0]
|
|
17
|
+
return 'drift' if want isnt sh $"zfs get -H -o value mountpoint #{name}"
|
|
12
18
|
'ok'
|
|
13
19
|
|
|
14
20
|
export apply = (name, props) ->
|
|
@@ -16,8 +22,8 @@ export apply = (name, props) ->
|
|
|
16
22
|
raise "pool '#{name}' requires a device argument" unless device
|
|
17
23
|
|
|
18
24
|
unless ok $"zpool list #{name}"
|
|
19
|
-
raise "
|
|
20
|
-
raise "Device #{device} has existing data. Run 'wipefs -a #{device}' first." if ok $"blkid #{device}"
|
|
25
|
+
raise "Device not found: #{device}" unless ok $"test -b #{device}" or ok $"test -f #{device}"
|
|
26
|
+
raise "Device #{device} has existing data. Run 'wipefs -a #{device}' first." if ok $"test -b #{device}" and ok $"blkid #{device}"
|
|
21
27
|
|
|
22
28
|
ashift = props.ashift?[0]?.args?[0] or '12'
|
|
23
29
|
sh $"zpool create -f -o ashift=#{ashift} #{name} #{device}"
|
package/directives/ssh.rip
CHANGED
|
@@ -15,6 +15,12 @@ OPTION_MAP =
|
|
|
15
15
|
'permit-root-login': 'PermitRootLogin'
|
|
16
16
|
'pubkey-auth': 'PubkeyAuthentication'
|
|
17
17
|
|
|
18
|
+
SYNONYMS =
|
|
19
|
+
'without-password': 'prohibit-password'
|
|
20
|
+
|
|
21
|
+
normalize = (val) ->
|
|
22
|
+
SYNONYMS[val] or val
|
|
23
|
+
|
|
18
24
|
export check = (name, props) ->
|
|
19
25
|
out = run $"sshd -T"
|
|
20
26
|
return 'missing' unless out.ok
|
|
@@ -24,7 +30,7 @@ export check = (name, props) ->
|
|
|
24
30
|
sshdKey = OPTION_MAP[key]?.toLowerCase()
|
|
25
31
|
continue unless sshdKey
|
|
26
32
|
match = out.stdout.match new RegExp "^#{sshdKey}\\s+(\\S+)", 'mi'
|
|
27
|
-
return 'drift' if match?[1] != want
|
|
33
|
+
return 'drift' if normalize(match?[1]) != want
|
|
28
34
|
|
|
29
35
|
'ok'
|
|
30
36
|
|
|
@@ -55,7 +61,7 @@ export verify = (name, props) ->
|
|
|
55
61
|
sshdKey = OPTION_MAP[key]?.toLowerCase()
|
|
56
62
|
continue unless sshdKey
|
|
57
63
|
match = out.stdout.match new RegExp "^#{sshdKey}\\s+(\\S+)", 'mi'
|
|
58
|
-
actual = match?[1]
|
|
64
|
+
actual = normalize match?[1]
|
|
59
65
|
if actual == want
|
|
60
66
|
results.push { status: 'pass', message: "#{key} is #{want}" }
|
|
61
67
|
else
|
package/package.json
CHANGED
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rip-lang/stamp",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.6",
|
|
4
|
+
"author": "Steve Shreeve <steve.shreeve@gmail.com>",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/shreeve/rip-lang.git",
|
|
8
|
+
"directory": "packages/stamp"
|
|
9
|
+
},
|
|
6
10
|
"main": "src/cli.rip",
|
|
7
11
|
"bin": {
|
|
8
12
|
"stamp": "./bin/stamp"
|
|
9
13
|
},
|
|
10
|
-
"
|
|
11
|
-
"
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/shreeve/rip-lang/issues"
|
|
12
16
|
},
|
|
17
|
+
"description": "Declarative host provisioning — no state file, no agent, no YAML",
|
|
18
|
+
"files": [
|
|
19
|
+
"bin/",
|
|
20
|
+
"src/",
|
|
21
|
+
"directives/",
|
|
22
|
+
"stamps/",
|
|
23
|
+
"README.md"
|
|
24
|
+
],
|
|
25
|
+
"homepage": "https://github.com/shreeve/rip-lang/tree/main/packages/stamp#readme",
|
|
13
26
|
"keywords": [
|
|
14
27
|
"provisioning",
|
|
15
28
|
"declarative",
|
|
@@ -22,24 +35,12 @@
|
|
|
22
35
|
"rip",
|
|
23
36
|
"rip-lang"
|
|
24
37
|
],
|
|
25
|
-
"repository": {
|
|
26
|
-
"type": "git",
|
|
27
|
-
"url": "git+https://github.com/shreeve/rip-lang.git",
|
|
28
|
-
"directory": "packages/stamp"
|
|
29
|
-
},
|
|
30
|
-
"homepage": "https://github.com/shreeve/rip-lang/tree/main/packages/stamp#readme",
|
|
31
|
-
"bugs": {
|
|
32
|
-
"url": "https://github.com/shreeve/rip-lang/issues"
|
|
33
|
-
},
|
|
34
|
-
"author": "Steve Shreeve <steve.shreeve@gmail.com>",
|
|
35
38
|
"license": "MIT",
|
|
36
|
-
"
|
|
37
|
-
"
|
|
39
|
+
"scripts": {
|
|
40
|
+
"test": "bun --preload ../../rip-loader.js test/runner.rip"
|
|
38
41
|
},
|
|
39
|
-
"
|
|
40
|
-
|
|
41
|
-
"
|
|
42
|
-
|
|
43
|
-
"README.md"
|
|
44
|
-
]
|
|
42
|
+
"type": "module",
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"rip-lang": ">=3.13.107"
|
|
45
|
+
}
|
|
45
46
|
}
|
package/src/cli.rip
CHANGED
|
@@ -3,7 +3,8 @@ import * as path from 'path'
|
|
|
3
3
|
import { parse } from './parser.rip'
|
|
4
4
|
import { execute } from './engine.rip'
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
pkg = JSON.parse fs.readFileSync path.join(import.meta.dir, '..', 'package.json'), 'utf-8'
|
|
7
|
+
VERSION = pkg.version
|
|
7
8
|
|
|
8
9
|
# ── Default file resolution ──────────────────────────────────────────────────
|
|
9
10
|
|
|
@@ -37,7 +38,7 @@ showVersion = ->
|
|
|
37
38
|
# ── Subcommands ──────────────────────────────────────────────────────────────
|
|
38
39
|
|
|
39
40
|
runMode = (mode, file) ->
|
|
40
|
-
stampfile = file or findStampfile
|
|
41
|
+
stampfile = file or findStampfile()
|
|
41
42
|
|
|
42
43
|
unless stampfile
|
|
43
44
|
warn "No Stampfile found. Provide a file or create Stampfile, Hostfile, or Containerfile."
|
|
@@ -63,7 +64,7 @@ runMode = (mode, file) ->
|
|
|
63
64
|
exit 1
|
|
64
65
|
|
|
65
66
|
listDirectives = ->
|
|
66
|
-
builtins = ['brew', 'packages', 'pool', 'dataset', 'profile', 'container', 'incus', 'multipass', 'user', 'group', 'firewall', 'ssh', 'service']
|
|
67
|
+
builtins = ['brew', 'packages', 'ensure', 'pool', 'dataset', 'profile', 'container', 'incus', 'multipass', 'user', 'group', 'firewall', 'ssh', 'service']
|
|
67
68
|
p "\nBuilt-in directives:\n"
|
|
68
69
|
for name in builtins
|
|
69
70
|
p " #{name}"
|
package/src/engine.rip
CHANGED
|
@@ -11,9 +11,11 @@ globalThis.run ??= run
|
|
|
11
11
|
|
|
12
12
|
STAMP_HOME = process.env.STAMP_HOME or path.join(process.env.HOME or '', '.stamp')
|
|
13
13
|
|
|
14
|
+
BUILTIN_DIR = path.join import.meta.dir, '..', 'directives'
|
|
15
|
+
|
|
14
16
|
resolveHandler = (type, stampDir) ->
|
|
15
17
|
candidates = [
|
|
16
|
-
path.join(
|
|
18
|
+
path.join(BUILTIN_DIR, "#{type}.rip") # Built-in
|
|
17
19
|
path.join(stampDir, 'directives', "#{type}.rip") # Local (beside Stampfile)
|
|
18
20
|
path.join(stampDir, 'directives', "#{type}.js")
|
|
19
21
|
path.join(STAMP_HOME, 'directives', "#{type}.rip") # Installed
|
|
@@ -80,18 +82,19 @@ export apply = (directives, handlers) ->
|
|
|
80
82
|
for dir in directives
|
|
81
83
|
handler = handlers[dir.type]
|
|
82
84
|
props = propsFor dir
|
|
85
|
+
label = dir.name or dir.args?.join(' ') or null
|
|
83
86
|
result = handler.check! dir.name, props
|
|
84
87
|
|
|
85
88
|
if result == 'ok'
|
|
86
|
-
log.ok dir.type,
|
|
89
|
+
log.ok dir.type, label
|
|
87
90
|
else
|
|
88
|
-
log.apply dir.type,
|
|
91
|
+
log.apply dir.type, label
|
|
89
92
|
handler.apply! dir.name, props
|
|
90
93
|
|
|
91
94
|
after = handler.check! dir.name, props
|
|
92
95
|
if after != 'ok'
|
|
93
|
-
log.fail dir.type,
|
|
94
|
-
|
|
96
|
+
log.fail dir.type, label, "still not ok after apply"
|
|
97
|
+
raise "Directive '#{dir.type}#{if label then ' ' + label else ''}' failed post-apply check"
|
|
95
98
|
applied++
|
|
96
99
|
|
|
97
100
|
p "\nApplied #{applied} of #{total} directives."
|
|
@@ -120,12 +123,13 @@ export plan = (directives, handlers) ->
|
|
|
120
123
|
for dir in directives
|
|
121
124
|
handler = handlers[dir.type]
|
|
122
125
|
props = propsFor dir
|
|
126
|
+
label = dir.name or dir.args?.join(' ') or null
|
|
123
127
|
result = handler.check! dir.name, props
|
|
124
128
|
|
|
125
129
|
switch result
|
|
126
|
-
when 'ok' then log.ok dir.type,
|
|
127
|
-
when 'drift' then log.update dir.type,
|
|
128
|
-
when 'missing' then log.create dir.type,
|
|
130
|
+
when 'ok' then log.ok dir.type, label
|
|
131
|
+
when 'drift' then log.update dir.type, label; changes++
|
|
132
|
+
when 'missing' then log.create dir.type, label; changes++
|
|
129
133
|
|
|
130
134
|
if changes > 0
|
|
131
135
|
p "\n#{changes} change#{if changes > 1 then 's' else ''} would be applied."
|
package/src/parser.rip
CHANGED
|
@@ -159,7 +159,7 @@ export parse = (source, env = {}) ->
|
|
|
159
159
|
singular = PLURALS[dir.type]
|
|
160
160
|
if singular
|
|
161
161
|
for own pname, props of dir.properties
|
|
162
|
-
sub = { type: singular, name: pname, args: [], properties: {}, line: dir.line }
|
|
162
|
+
sub = { type: singular, name: pname, args: props[0]?.args or [], properties: {}, line: dir.line }
|
|
163
163
|
for prop in props when prop.args.length or prop.source? or prop.sub?
|
|
164
164
|
for own k, v of (prop.sub ?? {})
|
|
165
165
|
sub.properties[k] = v
|
package/stamps/basic
ADDED
package/stamps/host
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Hostfile — test parsing only
|
|
2
|
+
|
|
3
|
+
set POOL tank
|
|
4
|
+
set DEVICE /dev/disk/by-id/scsi-0Linode_Volume_tank
|
|
5
|
+
set MOUNT /tank
|
|
6
|
+
set INCUS_DS incus/default
|
|
7
|
+
set SHARED shared/common
|
|
8
|
+
|
|
9
|
+
# ── system packages ──────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
packages
|
|
12
|
+
zfsutils-linux
|
|
13
|
+
incus
|
|
14
|
+
uidmap
|
|
15
|
+
acl
|
|
16
|
+
gettext-base
|
|
17
|
+
openssh-server
|
|
18
|
+
ripgrep
|
|
19
|
+
fail2ban
|
|
20
|
+
ufw
|
|
21
|
+
unattended-upgrades
|
|
22
|
+
|
|
23
|
+
# ── ZFS pool and datasets ───────────────────────────────────
|
|
24
|
+
|
|
25
|
+
pool $POOL $DEVICE
|
|
26
|
+
ashift 12
|
|
27
|
+
compression zstd
|
|
28
|
+
atime off
|
|
29
|
+
mountpoint $MOUNT
|
|
30
|
+
|
|
31
|
+
dataset $POOL/$INCUS_DS
|
|
32
|
+
|
|
33
|
+
dataset $POOL/home
|
|
34
|
+
dataset $POOL/home/foo
|
|
35
|
+
owner 1000:1000
|
|
36
|
+
dataset $POOL/home/bar
|
|
37
|
+
owner 1000:1000
|
|
38
|
+
|
|
39
|
+
dataset $POOL/$SHARED
|
|
40
|
+
owner 1001:1001
|
|
41
|
+
mode 2775
|
|
42
|
+
|
|
43
|
+
# ── host users and groups ───────────────────────────────────
|
|
44
|
+
|
|
45
|
+
user shreeve 1000:1000
|
|
46
|
+
groups trust
|
|
47
|
+
|
|
48
|
+
user trust 1001:1001
|
|
49
|
+
|
|
50
|
+
group jail 1002
|
|
51
|
+
|
|
52
|
+
# ── Incus initialization ────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
incus
|
|
55
|
+
storage default zfs $POOL/$INCUS_DS
|
|
56
|
+
network incusbr0
|
|
57
|
+
|
|
58
|
+
# ── Incus profiles ──────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
profile trusted
|
|
61
|
+
security.privileged true
|
|
62
|
+
limits.memory 2GB
|
|
63
|
+
limits.cpu 2
|
|
64
|
+
|
|
65
|
+
profile hardened
|
|
66
|
+
security.idmap.isolated true
|
|
67
|
+
security.nesting false
|
|
68
|
+
limits.memory 1GB
|
|
69
|
+
limits.cpu 1
|
|
70
|
+
limits.processes 512
|
|
71
|
+
|
|
72
|
+
# ── containers ──────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
container foo ubuntu/24.04
|
|
75
|
+
profile trusted
|
|
76
|
+
disk home $MOUNT/home/foo -> /home
|
|
77
|
+
disk shared $MOUNT/$SHARED -> /shared
|
|
78
|
+
user shreeve 1000:1000
|
|
79
|
+
user trust 1001:1001
|
|
80
|
+
start
|
|
81
|
+
|
|
82
|
+
container bar ubuntu/24.04
|
|
83
|
+
profile trusted
|
|
84
|
+
disk home $MOUNT/home/bar -> /home
|
|
85
|
+
disk shared $MOUNT/$SHARED -> /shared readonly
|
|
86
|
+
user shreeve 1000:1000
|
|
87
|
+
user trust 1001:1001
|
|
88
|
+
start
|
|
89
|
+
|
|
90
|
+
# ── security ────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
firewall
|
|
93
|
+
default deny incoming
|
|
94
|
+
default allow outgoing
|
|
95
|
+
allow ssh
|
|
96
|
+
|
|
97
|
+
service fail2ban
|
|
98
|
+
|
|
99
|
+
ssh
|
|
100
|
+
password-auth no
|
|
101
|
+
permit-root-login prohibit-password
|
|
102
|
+
pubkey-auth yes
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# VMfile — Ubuntu 24.04 VM on macOS via Multipass
|
|
2
|
+
|
|
3
|
+
brew multipass
|
|
4
|
+
|
|
5
|
+
multipass stamp 24.04
|
|
6
|
+
cpus 2
|
|
7
|
+
memory 4G
|
|
8
|
+
disk 20G
|
|
9
|
+
start
|
|
10
|
+
|
|
11
|
+
ensure unzip
|
|
12
|
+
check multipass exec stamp -- which unzip
|
|
13
|
+
apply multipass exec stamp -- sudo apt-get update -qq
|
|
14
|
+
apply multipass exec stamp -- sudo apt-get install -y -qq unzip
|
|
15
|
+
|
|
16
|
+
ensure bun
|
|
17
|
+
check multipass exec stamp -- test -x /home/ubuntu/.bun/bin/bun
|
|
18
|
+
apply multipass exec stamp -- bash -lc "curl -fsSL https://bun.sh/install | bash"
|
|
19
|
+
|
|
20
|
+
ensure rip
|
|
21
|
+
check multipass exec stamp -- test -x /home/ubuntu/.bun/bin/rip
|
|
22
|
+
apply multipass exec stamp -- /home/ubuntu/.bun/bin/bun add -g rip-lang
|
package/stamps/mac-vm
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# VMHostfile — Full Incus + ZFS host inside a Multipass VM
|
|
2
|
+
#
|
|
3
|
+
# Adapted from Hostfile for local VM testing:
|
|
4
|
+
# - File-backed ZFS pool (no block device)
|
|
5
|
+
# - UIDs offset to avoid conflict with ubuntu (1000)
|
|
6
|
+
|
|
7
|
+
set POOL tank
|
|
8
|
+
set MOUNT /tank
|
|
9
|
+
set INCUS_DS incus/default
|
|
10
|
+
set SHARED shared/common
|
|
11
|
+
|
|
12
|
+
# ── system packages ──────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
packages
|
|
15
|
+
zfsutils-linux
|
|
16
|
+
incus
|
|
17
|
+
uidmap
|
|
18
|
+
acl
|
|
19
|
+
gettext-base
|
|
20
|
+
openssh-server
|
|
21
|
+
ripgrep
|
|
22
|
+
fail2ban
|
|
23
|
+
ufw
|
|
24
|
+
unattended-upgrades
|
|
25
|
+
|
|
26
|
+
# ── ZFS pool (file-backed for VM testing) ────────────────────
|
|
27
|
+
|
|
28
|
+
ensure tank-image
|
|
29
|
+
check test -f /tmp/tank.img
|
|
30
|
+
apply truncate -s 2G /tmp/tank.img
|
|
31
|
+
|
|
32
|
+
pool $POOL /tmp/tank.img
|
|
33
|
+
compression zstd
|
|
34
|
+
atime off
|
|
35
|
+
mountpoint $MOUNT
|
|
36
|
+
|
|
37
|
+
# ── ZFS datasets ─────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
dataset $POOL/$INCUS_DS
|
|
40
|
+
|
|
41
|
+
dataset $POOL/home
|
|
42
|
+
dataset $POOL/home/foo
|
|
43
|
+
owner 1100:1100
|
|
44
|
+
dataset $POOL/home/bar
|
|
45
|
+
owner 1100:1100
|
|
46
|
+
|
|
47
|
+
dataset $POOL/$SHARED
|
|
48
|
+
owner 1101:1101
|
|
49
|
+
mode 2775
|
|
50
|
+
|
|
51
|
+
# ── host users and groups ────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
group trust 1101
|
|
54
|
+
group jail 1002
|
|
55
|
+
|
|
56
|
+
user shreeve 1100:1100
|
|
57
|
+
groups trust
|
|
58
|
+
|
|
59
|
+
user trust 1101:1101
|
|
60
|
+
|
|
61
|
+
# ── Incus initialization ─────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
incus
|
|
64
|
+
storage default zfs $POOL/$INCUS_DS
|
|
65
|
+
network incusbr0
|
|
66
|
+
|
|
67
|
+
# ── Incus profiles ───────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
profile trusted
|
|
70
|
+
security.privileged true
|
|
71
|
+
limits.memory 1GB
|
|
72
|
+
limits.cpu 1
|
|
73
|
+
|
|
74
|
+
# ── containers ───────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
container foo images:ubuntu/24.04
|
|
77
|
+
profile trusted
|
|
78
|
+
disk home $MOUNT/home/foo -> /home
|
|
79
|
+
disk shared $MOUNT/$SHARED -> /shared
|
|
80
|
+
user shreeve 1100:1100
|
|
81
|
+
start
|
|
82
|
+
|
|
83
|
+
# ── security ─────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
firewall
|
|
86
|
+
default deny incoming
|
|
87
|
+
default allow outgoing
|
|
88
|
+
allow ssh
|
|
89
|
+
|
|
90
|
+
service fail2ban
|
|
91
|
+
|
|
92
|
+
ssh
|
|
93
|
+
password-auth no
|
|
94
|
+
permit-root-login prohibit-password
|
|
95
|
+
pubkey-auth yes
|