Skip to content

Commit 034fc86

Browse files
committed
feat golden images
1 parent b714d3d commit 034fc86

12 files changed

Lines changed: 422 additions & 5 deletions

File tree

docs/.vitepress/config.mts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ export default defineConfig({
5656
{ text: "Overview", link: "/operations/" },
5757
{ text: "Reconfiguration", link: "/operations/reconfiguration" },
5858
{ text: "Clustering", link: "/operations/clustering" },
59-
{ text: "Backups and Restore", link: "/operations/backups-and-restore" }
59+
{ text: "Backups and Restore", link: "/operations/backups-and-restore" },
60+
{ text: "Golden Images", link: "/operations/golden-images" }
6061
]
6162
},
6263
{

docs/getting-started/templating.md

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,28 @@ trm launch ubuntu:24.04 customer-portal \
241241

242242
This creates one isolated container, prepares it with Ansible, starts two Compose stacks, and publishes two authenticated HTTPS routes.
243243

244-
## 8. Use Raw Cloud-Init When You Need Full Control
244+
## 8. Save a Reusable Golden Image
245+
246+
When a templated launch produces a container you want to reuse, turn it into a
247+
golden image:
248+
249+
```bash
250+
trm image create customer-portal golden-customer-portal
251+
```
252+
253+
Then launch more containers from that prepared state:
254+
255+
```bash
256+
trm image launch golden-customer-portal customer-portal-02 --profile dev
257+
```
258+
259+
Golden images are useful when provisioning is expensive or when you want a
260+
known-good starting point before a risky experiment. Terrarium strips published
261+
route config from the image source, so new containers do not accidentally
262+
inherit the old container's public hostname. See [Golden Images](../operations/golden-images.md)
263+
for the full workflow.
264+
265+
## 9. Use Raw Cloud-Init When You Need Full Control
245266

246267
If you already have a complete cloud-init file, pass it directly:
247268

@@ -260,7 +281,7 @@ trm launch ubuntu:24.04 raw-01 \
260281
--proxy https://raw.example.com:8080
261282
```
262283

263-
## 9. Debugging a Launch
284+
## 10. Debugging a Launch
264285

265286
Cloud-init runs after LXD reports that the container was created. If the instance exists but your app is not ready yet, open a shell:
266287

docs/operations/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ This section covers all the things you do *after* Terrarium is installed. Whethe
77
- **[Reconfiguration](reconfiguration.md):** How to safely change your domains, emails, and login methods without breaking anything.
88
- **[Clustering](clustering.md):** How to link multiple Terrarium servers together into a highly available swarm.
99
- **[Backups and Restore](backups-and-restore.md):** How to use your built-in time machine and off-site S3 exports.
10+
- **[Golden Images](golden-images.md):** How to save a configured container as a reusable launch template.
1011

1112
### The `terrariumctl` Command
1213

@@ -17,6 +18,7 @@ The most common commands you'll use day-to-day are:
1718
- `terrariumctl set emails`
1819
- `terrariumctl set idp` (To change between local ZITADEL and external OIDC logins)
1920
- `terrariumctl set s3` (To configure off-site backups)
21+
- `terrariumctl image create` (To publish reusable golden images)
2022
- `terrariumctl proxy sync` (To manually update your network routing rules)
2123

22-
*Want to see everything it can do? Check out the full [terrariumctl Reference](../reference/terrariumctl.md).*
24+
*Want to see everything it can do? Check out the full [terrariumctl Reference](../reference/terrariumctl.md).*

docs/operations/golden-images.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Golden Images
2+
3+
Backups are for recovery. Golden images are for reuse.
4+
5+
Terrarium's normal backup flow gives you a time machine for existing containers:
6+
you can roll back a broken app, restore a snapshot as a separate container, or
7+
export restore points to S3. A golden image is different. It is a named template
8+
you can launch again and again.
9+
10+
## Create a Golden Image
11+
12+
Start with a container that is already configured the way you want:
13+
14+
```bash
15+
trm image create web-01 golden-web
16+
```
17+
18+
Terrarium creates a temporary snapshot, copies it to a temporary instance,
19+
removes published-route proxy config from that copy, publishes the image, and
20+
cleans up the temporary resources.
21+
22+
If you already made the snapshot you want to preserve, publish that snapshot:
23+
24+
```bash
25+
lxc snapshot web-01 known-good
26+
trm image create web-01 golden-web --snapshot known-good
27+
```
28+
29+
If the image alias already exists and you want to replace it:
30+
31+
```bash
32+
trm image create web-01 golden-web --snapshot known-good --reuse
33+
```
34+
35+
## Launch From a Golden Image
36+
37+
Use the alias as the image name:
38+
39+
```bash
40+
trm image launch golden-web web-02 --profile dev
41+
```
42+
43+
You can still set basic launch-time resources:
44+
45+
```bash
46+
trm image launch golden-web web-03 --profile dev --disk 40G --memory 4G --cpu 2
47+
```
48+
49+
And you can publish the new container with a fresh route:
50+
51+
```bash
52+
trm image launch golden-web web-04 --proxy https://web-04.example.com:8080
53+
```
54+
55+
## Manage Images
56+
57+
List images:
58+
59+
```bash
60+
trm image list
61+
```
62+
63+
Delete an image alias when you no longer need it:
64+
65+
```bash
66+
trm image delete golden-web
67+
```
68+
69+
## When to Use This
70+
71+
Use golden images for:
72+
73+
- base development containers with your tools already installed
74+
- app templates you launch repeatedly
75+
- known-good agent environments before a large experiment
76+
77+
Use backups instead when you need to recover an existing container's history or
78+
export data off the server.

docs/operations/index.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ This section covers all the things you do *after* Terrarium is installed. Whethe
77
- **[Reconfiguration](reconfiguration.md):** How to safely change your domains, emails, and login methods without breaking anything.
88
- **[Clustering](clustering.md):** How to link multiple Terrarium servers together into a highly available swarm.
99
- **[Backups and Restore](backups-and-restore.md):** How to use your built-in time machine and off-site S3 exports.
10+
- **[Golden Images](golden-images.md):** How to save a configured container as a reusable launch template.
1011

1112
### The `terrariumctl` Command
1213

@@ -17,6 +18,7 @@ The most common commands you'll use day-to-day are:
1718
- `terrariumctl set emails`
1819
- `terrariumctl set idp` (To change between local ZITADEL and external OIDC logins)
1920
- `terrariumctl set s3` (To configure off-site backups)
21+
- `terrariumctl image create` (To publish reusable golden images)
2022
- `terrariumctl proxy sync` (To manually update your network routing rules)
2123

22-
*Want to see everything it can do? Check out the full [terrariumctl Reference](../reference/terrariumctl.md).*
24+
*Want to see everything it can do? Check out the full [terrariumctl Reference](../reference/terrariumctl.md).*

docs/reference/terrariumctl.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
| `terrariumctl install` | optional flags, see below | interactive mode | Installs or bootstraps Terrarium on the current host, including preflight verification for external OIDC and S3 when enabled. |
1010
| `terrariumctl status` | none | n/a | Shows Terrarium service status, management endpoints, IDP mode, admin group, and the oauth2-proxy state. |
1111
| `terrariumctl launch` | required: image and instance name; optional provisioning flags | normal LXD launch with optional generated cloud-init | Launches an LXD container, optionally applying resource limits, generated Ansible/Docker Compose provisioning, and a Terrarium `user.proxy` label. |
12+
| `terrariumctl image create` | required: instance and image alias; optional: `--snapshot`, `--live`, `--reuse` | creates a temporary snapshot and sanitized image | Publishes a reusable golden LXD image from a container or snapshot without carrying published-route proxy config into the image. |
13+
| `terrariumctl image launch` | required: image alias and new instance name; optional launch flags | same as `terrariumctl launch` | Launches a new container from a Terrarium golden image. |
14+
| `terrariumctl image list/delete` | optional image alias for delete | n/a | Lists or removes local LXD images. |
1215
| `terrariumctl exec` | required: instance name; optional command after `--`, `--root`, `--user` | `terrarium` login shell | Opens a shell or runs a command inside a container as the non-root `terrarium` user by default. |
1316
| `terrariumctl backup list` | none | n/a | Lists local ZFS snapshots and, when enabled, S3 manifests. |
1417
| `terrariumctl backup export` | none | n/a | Uploads the current incremental ZFS backup chain to configured S3 storage. |
@@ -100,6 +103,59 @@ trm launch ubuntu:24.04 app-01 \
100103
--proxy https://admin.example.com:3000@auth:admins
101104
```
102105

106+
## image
107+
108+
`trm image` creates and manages named golden images. Use it when a container is
109+
configured exactly how you want and you want to launch more containers from that
110+
state later.
111+
112+
Create an image from the current instance state:
113+
114+
```bash
115+
trm image create web-01 golden-web
116+
```
117+
118+
By default, Terrarium creates a temporary LXD snapshot, copies it to a temporary
119+
instance, removes `user.proxy` and LXD proxy devices from that copy, publishes
120+
the image, and then removes the temporary resources. This keeps the image from
121+
accidentally inheriting the source container's public route.
122+
123+
Create an image from an existing snapshot:
124+
125+
```bash
126+
trm image create web-01 golden-web --snapshot known-good
127+
```
128+
129+
Use `--live` when you explicitly want to publish the current instance state
130+
without creating a temporary snapshot first:
131+
132+
```bash
133+
trm image create web-01 golden-web --live
134+
```
135+
136+
If the alias already exists and you intentionally want to replace it, add
137+
`--reuse`:
138+
139+
```bash
140+
trm image create web-01 golden-web --snapshot known-good --reuse
141+
```
142+
143+
Launch from a golden image:
144+
145+
```bash
146+
trm image launch golden-web web-02 --profile dev
147+
```
148+
149+
`image launch` accepts the same basic launch flags as `trm launch`: `--profile`,
150+
`--disk`, `--memory`, `--cpu`, and `--proxy`.
151+
152+
List or remove images:
153+
154+
```bash
155+
trm image list
156+
trm image delete golden-web
157+
```
158+
103159
## install
104160

105161
| Flag | Argument | Required | Default | Meaning |

scripts/ctl/completion.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ describe("terrariumctl completion", () => {
4444
expect(script).toContain("update");
4545
expect(script).toContain("update) COMPREPLY");
4646
expect(script).toContain("launch) COMPREPLY");
47+
expect(script).toContain("image) COMPREPLY");
48+
expect(script).toContain("create list launch delete");
49+
expect(script).toContain("--snapshot --live --reuse --profile --disk --memory --cpu --proxy");
4750
expect(script).toContain("--profile --disk --memory --cpu --requirements --playbook --role --docker-compose --cloud-init --proxy");
4851
expect(script).toContain("--ref --skip-reconfigure --non-interactive");
4952
expect(script).toContain("--skip-reconfigure");
@@ -66,6 +69,7 @@ describe("terrariumctl completion", () => {
6669
expect(zsh).toContain("#compdef terrariumctl trm");
6770
expect(zsh).toContain("update) opts=(--ref --skip-reconfigure --non-interactive)");
6871
expect(zsh).toContain("launch) opts=(--profile --disk --memory --cpu --requirements --playbook --role --docker-compose --cloud-init --proxy)");
72+
expect(zsh).toContain("image) actions=(create list launch delete); opts=(--snapshot --live --reuse --profile --disk --memory --cpu --proxy)");
6973
expect(zsh).toContain("compadd local oidc");
7074
expect(zsh).toContain("compadd provider");
7175
expect(fish).toContain("complete -c terrariumctl");

scripts/ctl/completion.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const commands = [
2424
"reconfigure",
2525
"update",
2626
"launch",
27+
"image",
2728
"exec",
2829
"config",
2930
"cluster",
@@ -36,6 +37,7 @@ const commands = [
3637

3738
const actions: Record<string, string[]> = {
3839
backup: ["list", "export", "restore"],
40+
image: ["create", "list", "launch", "delete"],
3941
config: ["import", "export"],
4042
cluster: ["status", "init", "invite", "token", "join", "evacuate", "restore", "move", "remove", "ovn"],
4143
proxy: ["sync"],
@@ -99,6 +101,7 @@ const optionGroups: Record<string, string[]> = {
99101
"--cloud-init",
100102
"--proxy"
101103
],
104+
image: ["--snapshot", "--live", "--reuse", "--profile", "--disk", "--memory", "--cpu", "--proxy"],
102105
exec: ["--root", "--user"],
103106
cluster: [
104107
"--member",
@@ -353,6 +356,7 @@ _terrariumctl_complete() {
353356
backup) COMPREPLY=( $(compgen -W "${words(optionGroups.backup)} --help" -- "\${cur}") ) ;;
354357
update) COMPREPLY=( $(compgen -W "${words(optionGroups.update)} --help" -- "\${cur}") ) ;;
355358
launch) COMPREPLY=( $(compgen -W "${words(optionGroups.launch)} --help" -- "\${cur}") ) ;;
359+
image) COMPREPLY=( $(compgen -W "${words(optionGroups.image)} --help" -- "\${cur}") ) ;;
356360
exec) COMPREPLY=( $(compgen -W "${words(optionGroups.exec)} --help" -- "\${cur}") ) ;;
357361
cluster) COMPREPLY=( $(compgen -W "${words(optionGroups.cluster)} --help" -- "\${cur}") ) ;;
358362
mount) COMPREPLY=( $(compgen -W "${words(optionGroups.mount)} --help" -- "\${cur}") ) ;;
@@ -370,6 +374,7 @@ _terrariumctl_complete() {
370374
2)
371375
case "\${command}" in
372376
backup) COMPREPLY=( $(compgen -W "${words(actions.backup)}" -- "\${cur}") ) ;;
377+
image) COMPREPLY=( $(compgen -W "${words(actions.image)}" -- "\${cur}") ) ;;
373378
config) COMPREPLY=( $(compgen -W "${words(actions.config)}" -- "\${cur}") ) ;;
374379
cluster) COMPREPLY=( $(compgen -W "${words(actions.cluster)}" -- "\${cur}") ) ;;
375380
proxy) COMPREPLY=( $(compgen -W "${words(actions.proxy)}" -- "\${cur}") ) ;;
@@ -414,6 +419,7 @@ _terrariumctl() {
414419
415420
case "$words[2]" in
416421
backup) actions=(${words(actions.backup)}); opts=(${words(optionGroups.backup)}) ;;
422+
image) actions=(${words(actions.image)}); opts=(${words(optionGroups.image)}) ;;
417423
config) actions=(${words(actions.config)}) ;;
418424
cluster) actions=(${words(actions.cluster)}); opts=(${words(optionGroups.cluster)}) ;;
419425
proxy) actions=(${words(actions.proxy)}) ;;

scripts/ctl/image.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { describe, expect, test } from "bun:test";
2+
import { buildImageCreatePlan } from "./image";
3+
4+
const lxc = process.env.TERRARIUM_LXC_BIN ?? "/snap/bin/lxc";
5+
6+
describe("terrariumctl image", () => {
7+
test("creates a temporary snapshot-backed sanitized image plan by default", () => {
8+
expect(buildImageCreatePlan("web-01", "golden-web", {}, { now: 123, pid: 456 })).toEqual({
9+
instance: "web-01",
10+
alias: "golden-web",
11+
source: "web-01/terrarium-golden-123",
12+
tempInstance: "terrarium-image-golden-web-456-123",
13+
snapshotToCreate: "terrarium-golden-123",
14+
publishArgs: [lxc, "publish", "terrarium-image-golden-web-456-123", "--alias", "golden-web"]
15+
});
16+
});
17+
18+
test("can publish an existing snapshot or live instance", () => {
19+
expect(buildImageCreatePlan("web-01", "golden-web", { snapshot: "known-good", reuse: true }, { now: 123, pid: 456 })).toMatchObject({
20+
source: "web-01/known-good",
21+
publishArgs: [lxc, "publish", "terrarium-image-golden-web-456-123", "--alias", "golden-web", "--reuse"]
22+
});
23+
const livePlan = buildImageCreatePlan("web-01", "golden-web", { live: true }, { now: 123, pid: 456 });
24+
expect(livePlan).toMatchObject({ source: "web-01" });
25+
expect(livePlan).not.toHaveProperty("snapshotToCreate");
26+
});
27+
28+
test("rejects ambiguous or missing image create inputs", () => {
29+
expect(() => buildImageCreatePlan("", "golden-web")).toThrow("instance is required");
30+
expect(() => buildImageCreatePlan("web-01", "")).toThrow("image alias is required");
31+
expect(() => buildImageCreatePlan("web-01", "golden-web", { snapshot: "known-good", live: true })).toThrow("use either --snapshot or --live");
32+
});
33+
});

0 commit comments

Comments
 (0)