@studiometa/trafic-agent 0.1.49 → 0.1.50
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 +156 -13
- package/dist/cli.js +134 -14
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/{server-DZAo8Clg.js → server-B_iFvS6v.js} +366 -28
- package/dist/server-B_iFvS6v.js.map +1 -0
- package/dist/server.d.ts +21 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +2 -2
- package/dist/setup/agent.d.ts +11 -1
- package/dist/setup/agent.d.ts.map +1 -1
- package/dist/setup/migrations/0020__backup_dir.d.ts +23 -0
- package/dist/setup/migrations/0020__backup_dir.d.ts.map +1 -0
- package/dist/setup/migrations/index.d.ts.map +1 -1
- package/dist/{setup-DWgb6dAD.js → setup-BbXL1ZFY.js} +826 -209
- package/dist/setup-BbXL1ZFY.js.map +1 -0
- package/dist/tasks/access-logs.d.ts +8 -0
- package/dist/tasks/access-logs.d.ts.map +1 -0
- package/dist/tasks/backup.d.ts +48 -0
- package/dist/tasks/backup.d.ts.map +1 -0
- package/dist/tasks/destroy-project.d.ts +71 -0
- package/dist/tasks/destroy-project.d.ts.map +1 -0
- package/dist/tasks/destroy-scheduled.d.ts +41 -0
- package/dist/tasks/destroy-scheduled.d.ts.map +1 -0
- package/dist/tasks/running-limit.d.ts +54 -0
- package/dist/tasks/running-limit.d.ts.map +1 -0
- package/dist/tasks/stop-idle.d.ts +12 -2
- package/dist/tasks/stop-idle.d.ts.map +1 -1
- package/dist/types.d.ts +41 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/config.d.ts +6 -0
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/db.d.ts +59 -1
- package/dist/utils/db.d.ts.map +1 -1
- package/dist/utils/ddev.d.ts +10 -3
- package/dist/utils/ddev.d.ts.map +1 -1
- package/dist/utils/paths.d.ts +3 -0
- package/dist/utils/paths.d.ts.map +1 -0
- package/package.json +1 -1
- package/dist/server-DZAo8Clg.js.map +0 -1
- package/dist/setup-DWgb6dAD.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @studiometa/trafic-agent
|
|
2
2
|
|
|
3
|
-
Server agent for DDEV preview environments — auth, scale-to-zero,
|
|
3
|
+
Server agent for DDEV preview environments — auth, scale-to-zero, auto-start, scheduled removals and backups.
|
|
4
4
|
|
|
5
5
|
Part of [Trafic](https://github.com/studiometa/trafic), a tool for managing DDEV preview environments on Linux servers.
|
|
6
6
|
|
|
@@ -10,6 +10,9 @@ Part of [Trafic](https://github.com/studiometa/trafic), a tool for managing DDEV
|
|
|
10
10
|
- **Scale-to-zero** — Automatically stop idle DDEV projects to save RAM
|
|
11
11
|
- **Auto-start** — Show a waiting page and restart stopped projects on request
|
|
12
12
|
- **Per-project config** — Override auth and idle timeout per project
|
|
13
|
+
- **Backups on removal** — Dump the database and archive the upload directories before a project is removed
|
|
14
|
+
- **Scheduled removal** — Remove a project after a delay; a new deploy cancels it
|
|
15
|
+
- **Running limit** — `max_running_projects` stops the least recently used projects
|
|
13
16
|
|
|
14
17
|
## Installation
|
|
15
18
|
|
|
@@ -19,24 +22,47 @@ npm install -g @studiometa/trafic-agent
|
|
|
19
22
|
|
|
20
23
|
## Commands
|
|
21
24
|
|
|
22
|
-
### `trafic-agent
|
|
25
|
+
### `trafic-agent start`
|
|
23
26
|
|
|
24
|
-
Start the agent server.
|
|
27
|
+
Start the agent server. Setup installs a systemd service that runs this command.
|
|
25
28
|
|
|
26
29
|
```bash
|
|
27
|
-
trafic-agent
|
|
30
|
+
trafic-agent start
|
|
28
31
|
# or with custom config
|
|
29
|
-
trafic-agent
|
|
32
|
+
trafic-agent start --config /etc/trafic/config.toml
|
|
33
|
+
# or on another port than the one in the config
|
|
34
|
+
trafic-agent start --port 9877
|
|
30
35
|
```
|
|
31
36
|
|
|
37
|
+
| Option | Description | Default |
|
|
38
|
+
|--------|-------------|---------|
|
|
39
|
+
| `-c`, `--config` | Path to the config file | first of `/etc/trafic/config.toml`, `./config.toml`, `./trafic.toml` |
|
|
40
|
+
| `-p`, `--port` | Port of the agent HTTP server. Replaces `port` from the config | `port` from the config |
|
|
41
|
+
|
|
32
42
|
### `trafic-agent setup`
|
|
33
43
|
|
|
34
|
-
|
|
44
|
+
Server setup, run as root. It installs Docker, DDEV and Node.js, configures DDEV and Traefik for forward auth, writes `/etc/trafic/config.toml`, installs the agent as a systemd service and hardens the server. You configure the wildcard DNS yourself. Setup takes its options from flags only: it asks no questions.
|
|
35
45
|
|
|
36
46
|
```bash
|
|
37
|
-
trafic-agent setup --tld
|
|
47
|
+
sudo trafic-agent setup --tld=previews.example.com
|
|
38
48
|
```
|
|
39
49
|
|
|
50
|
+
`trafic setup` runs this command over SSH. See the [CLI README](../trafic-cli/README.md#trafic-setup).
|
|
51
|
+
|
|
52
|
+
| Option | Description | Default |
|
|
53
|
+
|--------|-------------|---------|
|
|
54
|
+
| `--tld` | TLD for DDEV projects. Required on the first run; a re-run reuses `tld` from the config | `tld` from the config |
|
|
55
|
+
| `--email` | Email for Let's Encrypt certificates. Without it, projects use a mkcert certificate | - |
|
|
56
|
+
| `--dns-provider` | DNS-01 provider (lego name, e.g. `cloudflare`), for one wildcard `*.<tld>` certificate. Requires `--email`. A re-run reuses `[tls] dns_provider` from the config, and then `--email` is required again. See [TLS](#tls) | `[tls] dns_provider` from the config |
|
|
57
|
+
| `--dns-env` | Credential for that provider as `KEY=VALUE`, repeatable | `[tls.dns_env]` from the config |
|
|
58
|
+
| `--ssh-users` | SSH users to allow, comma-separated | `ddev` |
|
|
59
|
+
| `--trusted-proxy-hops` | Proxies in front of the agent (`2` behind a CDN). Written to a new config only; ignored when the config exists | `1` |
|
|
60
|
+
| `--no-hardening` | Skip server hardening | `false` |
|
|
61
|
+
| `--no-root-ssh` | Disable root SSH login (recovery via rescue mode only) | `false` |
|
|
62
|
+
| `--no-docker` | Skip Docker installation | `false` |
|
|
63
|
+
| `--no-ddev` | Skip DDEV installation | `false` |
|
|
64
|
+
| `--dry-run` | Show what would be done without making changes | `false` |
|
|
65
|
+
|
|
40
66
|
**Requirements:**
|
|
41
67
|
- Ubuntu 24.04 LTS
|
|
42
68
|
- Root access (for initial setup)
|
|
@@ -44,6 +70,90 @@ trafic-agent setup --tld previews.example.com
|
|
|
44
70
|
|
|
45
71
|
**Automatic security updates.** Setup installs `unattended-upgrades` and writes `/etc/apt/apt.conf.d/50unattended-upgrades`, allowing Ubuntu's release, security and ESM pockets plus Docker Engine (`origin=Docker`, from `download.docker.com`). Docker security releases are therefore applied daily with the rest. Note that a Docker Engine update restarts the daemon and stops running preview containers; the waiting page starts a project again on its next request. DDEV is deliberately excluded — it is updated by `trafic-agent upgrade` instead. The server never reboots on its own (`Unattended-Upgrade::Automatic-Reboot "false"`).
|
|
46
72
|
|
|
73
|
+
### `trafic-agent audit`
|
|
74
|
+
|
|
75
|
+
Check the security state of the server: SSH, firewall, Fail2ban, automatic updates, the agent service, Docker, disk usage, the permissions of the config, the owner of the projects directory and, with a `[tls]` section, the wildcard certificate and the permissions of its credentials. The command exits with code 1 when a check fails. Run it as root: `ufw status` and the files under `/var/lib/docker` need root.
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
sudo trafic-agent audit
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### `trafic-agent version` / `trafic-agent help`
|
|
82
|
+
|
|
83
|
+
`trafic-agent version` (also `-v`, `--version`) prints the version. `trafic-agent help` (also `-h`, `--help`) prints the help with every option.
|
|
84
|
+
|
|
85
|
+
### `trafic-agent skill`
|
|
86
|
+
|
|
87
|
+
Print the [`trafic-agent` agent skill](../../skills/trafic-agent/SKILL.md) of this version to stdout. A coding agent reads it to operate the server. To install the skill from the repository instead, run `npx skills add studiometa/trafic` or `gh skill install studiometa/trafic`.
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
trafic-agent skill
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### `trafic-agent destroy`
|
|
94
|
+
|
|
95
|
+
Remove a project from the server. Run it as the `ddev` user, the user the service runs as. The command refuses to run as root: DDEV does not run as root, and root could leave files in the database directory that the service cannot write to.
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
trafic-agent destroy preview-42--my-app
|
|
99
|
+
# or with custom config
|
|
100
|
+
trafic-agent destroy preview-42--my-app --config /etc/trafic/config.toml
|
|
101
|
+
# back up the project first
|
|
102
|
+
trafic-agent destroy preview-42--my-app --backup
|
|
103
|
+
# give the backup up to one hour
|
|
104
|
+
trafic-agent destroy preview-42--my-app --backup --timeout 1h
|
|
105
|
+
# remove the project in 48 hours, after a backup
|
|
106
|
+
trafic-agent destroy preview-42--my-app --delay 48h --backup
|
|
107
|
+
# cancel that removal
|
|
108
|
+
trafic-agent destroy preview-42--my-app --cancel
|
|
109
|
+
# list the scheduled removals
|
|
110
|
+
trafic-agent destroy --list
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Steps:
|
|
114
|
+
1. **Backup** (with `--backup` only) — see [Backups](#backups). If the backup fails, the command removes nothing and exits with an error
|
|
115
|
+
2. **DDEV** — runs `ddev delete -Oy` in the project directory (containers and volumes, no snapshot). A failure is reported as a warning and does not stop the removal
|
|
116
|
+
3. **Directory** — removes the project directory
|
|
117
|
+
4. **Database** — deletes the project's rows from the agent database (`projects`, `access_logs` and `scheduled_destroys`)
|
|
118
|
+
|
|
119
|
+
The project directory is the `approot` from DDEV's `project_list.yaml`, or `<projects_dir>/<name>` when DDEV no longer knows the project. When the directory does not exist, the command prints a warning, does not run DDEV and only deletes the project's rows from the agent database, so a retry after a failed run cleans them up. The command refuses a directory that is not inside `projects_dir`, also after it follows symbolic links, and refuses a project directory that is a symbolic link. It also refuses an empty name, `.`, `..` or a name with a `/`.
|
|
120
|
+
|
|
121
|
+
The command checks the config as `start` does, and exits with an error before it changes anything when the config is not valid (for example a relative `[backup] dir`).
|
|
122
|
+
|
|
123
|
+
`trafic destroy` runs this command over SSH.
|
|
124
|
+
|
|
125
|
+
### Scheduled removals
|
|
126
|
+
|
|
127
|
+
`trafic-agent destroy <name> --delay <duration>` removes nothing. It records the removal date (now plus the delay) in the agent database and exits. The delay uses hours, minutes and seconds, in this order and without spaces: `48h`, `1h30m`, `90m`. The command refuses any other delay, for example `2d`, `1h30` or `48h 30m`. With `--backup`, the agent backs up the project before the scheduled removal, and keeps `--timeout` as the time limit of that backup. A new `--delay` for the same project replaces the date, the backup choice and the timeout, unless the removal has started.
|
|
128
|
+
|
|
129
|
+
The agent service checks the scheduled removals every `idle_check_interval` (default `5m`). It removes each project whose date is past with the same steps as `trafic-agent destroy`. When a removal fails (for example, the backup fails), the agent logs the error, keeps the removal scheduled and tries again on the next check. A removal that fails after its backup (for example, the service stops during `ddev delete`) does not back up again on the next check: the backup is already made. A failure on one project does not stop the removal of the others.
|
|
130
|
+
|
|
131
|
+
The service makes the backup of a scheduled removal itself, inside its systemd sandbox (`ProtectSystem=strict`). The sandbox lets it write to `/var/backups/trafic`, `/home/ddev` and `/var/lib/trafic` only. When `[backup] dir` is another directory, each scheduled backup fails with a `Cannot write to the backup directory` error that names the sandbox, and the project is never removed. Keep `[backup] dir` inside one of these directories.
|
|
132
|
+
|
|
133
|
+
`trafic deploy` cancels the scheduled removal of the project it deploys. To cancel it by hand, run `trafic-agent destroy <name> --cancel`. A cancel stops a removal that is due but not started, or that is still in its backup. After the backup, the agent marks the removal as started, just before `ddev delete`. From then on, `--cancel` fails with `the removal of <name> is in progress` and the project is removed, so a `trafic deploy` fails at its first step instead of deploying a project that is about to go. `trafic-agent destroy --list` shows the scheduled removals, soonest first, and marks a started removal as `in progress`.
|
|
134
|
+
|
|
135
|
+
The date is kept in the agent database, not in `.ddev/config.trafic.yaml`: the agent finds it even when DDEV no longer knows the project, and a deploy cancels it with one command.
|
|
136
|
+
|
|
137
|
+
### Backups
|
|
138
|
+
|
|
139
|
+
`trafic-agent destroy --backup` writes a backup to `<backup.dir>/<project>/<timestamp>/`, for example `/var/backups/trafic/preview-42--my-app/2026-10-14T07-40-24Z/`:
|
|
140
|
+
|
|
141
|
+
- `db.sql.gz` — the `ddev export-db` dump. Skipped when the project has no database (`omit_containers: [db]`)
|
|
142
|
+
- `files.tar.gz` — the upload directories, with paths relative to the project directory. Skipped when the project has none
|
|
143
|
+
|
|
144
|
+
The upload directories are the ones DDEV uses: `upload_dirs` from the project's `.ddev/config.yaml`, or the default of the project type (for example `wp-content/uploads` for WordPress). The agent reads them from `DDEV_FILES_DIRS` in the web container. Both steps need the project to run, so the agent starts a stopped project and stops it again after the backup. While it removes a project, the idle sweep and [`max_running_projects`](#running-project-limit) do not stop it, and the waiting page does not start it again (it shows an error page): the project has the status `destroying` in the agent database. When the removal is killed (for example, the CI job is cancelled or the SSH connection drops), the agent sets the status back to `running` once the `trafic-agent destroy` process is gone, at the next idle sweep or request. The idle sweep then stops the project when it is idle.
|
|
145
|
+
|
|
146
|
+
The backup has one time limit for the dump and the archive together: 40 minutes by default. `--timeout` sets it, as a duration such as `90s`, `10m` or `1h` (a bare number is minutes). The start and the stop of the project are not part of it. A backup that takes longer fails, and nothing is removed. `trafic destroy --backup` passes its own `--timeout` (default `10m`). A file that changes while the agent archives it (the project runs during the backup) does not fail the backup: the archive has the file as `tar` read it, and the agent prints a warning.
|
|
147
|
+
|
|
148
|
+
The agent service removes backups older than `retain_days` once an hour. It can only write to `/var/backups/trafic`, `/home/ddev` and `/var/lib/trafic`: if you set `dir` to another path, remove old backups yourself, and do not use `--delay` with `--backup` (see [Scheduled removals](#scheduled-removals)). `dir` must not be `projects_dir`, be inside it or contain it (so not `/home/ddev` itself, with the default `projects_dir`), because the removal of a project would remove its backup: the agent refuses such a config. Setup creates `/var/backups/trafic`, owned by `ddev` with mode `700`.
|
|
149
|
+
|
|
150
|
+
To restore a backup into a project:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
ddev import-db --file=/var/backups/trafic/<project>/<timestamp>/db.sql.gz
|
|
154
|
+
tar -xzf /var/backups/trafic/<project>/<timestamp>/files.tar.gz -C <project directory>
|
|
155
|
+
```
|
|
156
|
+
|
|
47
157
|
### `trafic-agent upgrade` / `trafic-agent update`
|
|
48
158
|
|
|
49
159
|
Upgrade the server to the latest version of `trafic-agent` in one command. `update` is an alias for `upgrade`.
|
|
@@ -81,7 +191,9 @@ Migration state is stored in `/etc/trafic/.migrations.json` and updated after ea
|
|
|
81
191
|
|
|
82
192
|
## Configuration
|
|
83
193
|
|
|
84
|
-
|
|
194
|
+
Setup writes `/etc/trafic/config.toml`. Edit it, then run `sudo systemctl restart trafic-agent`. See [examples/config.toml](../../examples/config.toml) for every key.
|
|
195
|
+
|
|
196
|
+
Without `--config`, the agent reads the first file it finds of `/etc/trafic/config.toml`, `./config.toml` and `./trafic.toml`. When there is none, it uses the defaults.
|
|
85
197
|
|
|
86
198
|
```toml
|
|
87
199
|
# Required: TLD for DDEV projects
|
|
@@ -90,12 +202,26 @@ tld = "previews.example.com"
|
|
|
90
202
|
# Agent HTTP server port (default: 9876)
|
|
91
203
|
port = 9876
|
|
92
204
|
|
|
93
|
-
#
|
|
205
|
+
# Must match trafic deploy --projects-dir (default: /home/ddev/www)
|
|
206
|
+
projects_dir = "/home/ddev/www"
|
|
207
|
+
|
|
208
|
+
# Scale-to-zero: stop idle projects after this duration (default: 30m; setup writes 4h)
|
|
94
209
|
idle_timeout = "4h"
|
|
95
210
|
|
|
211
|
+
# Idle, running-limit and scheduled-removal sweeps (default: 5m)
|
|
212
|
+
idle_check_interval = "5m"
|
|
213
|
+
|
|
214
|
+
# Most projects running at the same time (default: 0, no limit)
|
|
215
|
+
max_running_projects = 8
|
|
216
|
+
|
|
217
|
+
# Backups made by `trafic-agent destroy --backup`
|
|
218
|
+
[backup]
|
|
219
|
+
dir = "/var/backups/trafic" # default
|
|
220
|
+
retain_days = 14 # default; older backups are removed
|
|
221
|
+
|
|
96
222
|
# Authentication
|
|
97
223
|
[auth]
|
|
98
|
-
default_policy = "basic" # allow, deny, basic, or token
|
|
224
|
+
default_policy = "basic" # allow, deny, basic, or token; basic also accepts the tokens
|
|
99
225
|
|
|
100
226
|
# Proxies in front of the agent (default: 1 = ddev-router/Traefik alone).
|
|
101
227
|
# Raise by one for each extra proxy: a CDN or load balancer ahead of
|
|
@@ -109,10 +235,10 @@ allowed_ips = ["192.168.1.0/24", "10.0.0.0/8"]
|
|
|
109
235
|
# Bearer tokens for CI/API access
|
|
110
236
|
tokens = ["your-ci-token"]
|
|
111
237
|
|
|
112
|
-
# Basic auth credentials
|
|
238
|
+
# Basic auth credentials, as "user:password" or { username, password } tables
|
|
113
239
|
basic_auth = ["user:password"]
|
|
114
240
|
|
|
115
|
-
# Per-hostname rules
|
|
241
|
+
# Per-hostname rules (first match wins, after the global allowed_ips)
|
|
116
242
|
[[auth.rules]]
|
|
117
243
|
match = "*.public.*"
|
|
118
244
|
policy = "allow"
|
|
@@ -120,6 +246,7 @@ policy = "allow"
|
|
|
120
246
|
[[auth.rules]]
|
|
121
247
|
match = "admin.*"
|
|
122
248
|
policy = "basic"
|
|
249
|
+
allowed_ips = ["203.0.113.0/24"] # optional: these IPs skip basic auth
|
|
123
250
|
|
|
124
251
|
# Optional: one wildcard certificate instead of one per preview.
|
|
125
252
|
# See the TLS section below.
|
|
@@ -142,6 +269,20 @@ auth_policy: allow # allow, deny, basic, or token
|
|
|
142
269
|
idle_timeout: never
|
|
143
270
|
```
|
|
144
271
|
|
|
272
|
+
- `idle_timeout` replaces the global `idle_timeout` for this project, shorter or longer. The idle sweep reads it at each check.
|
|
273
|
+
- `auth_policy` replaces only `default_policy`. The global `allowed_ips` and a matching `[[auth.rules]]` still apply first.
|
|
274
|
+
- The agent reads `auth_policy` when it starts and when DDEV's `project_list.yaml` changes. After you change it, restart the agent: `sudo systemctl restart trafic-agent`.
|
|
275
|
+
|
|
276
|
+
## Running project limit
|
|
277
|
+
|
|
278
|
+
`max_running_projects` limits how many projects run at the same time, so that a burst of traffic on many previews does not use all the memory of the server. Unset or `0` means no limit.
|
|
279
|
+
|
|
280
|
+
- Before the waiting page starts a stopped project, the agent counts the running projects: the projects that `ddev list` shows up, and the projects the agent is starting. The project to start is one of them. Over the limit, it stops the running projects with the oldest access first. When many previews start at the same time, each start frees its own slot.
|
|
281
|
+
- `trafic deploy` runs `ddev start` without the agent. The idle sweep (every `idle_check_interval`) stops the projects with the oldest access until the server is back under the limit. A project that a deploy started counts as accessed when the sweep first sees it, so it is not the first one stopped.
|
|
282
|
+
- Projects with `idle_timeout: never` are never stopped by this rule. When no other project can be stopped, the agent starts the project anyway and logs a warning.
|
|
283
|
+
- A project that `trafic-agent destroy` is removing (also from a scheduled removal) is never stopped by this rule or by the idle sweep: its backup starts the project, and a stop would fail the backup. It still counts as running.
|
|
284
|
+
- A start that the agent service did not finish (the service restarted during `ddev start`) does not keep its slot: when the service starts again, it sets the project back to `running`, and the idle sweep checks it against DDEV.
|
|
285
|
+
|
|
145
286
|
## How it works
|
|
146
287
|
|
|
147
288
|
```
|
|
@@ -158,6 +299,7 @@ Traefik (DDEV router)
|
|
|
158
299
|
└─► 502 error → errors middleware → trafic-agent
|
|
159
300
|
│
|
|
160
301
|
├─► known project → Waiting page + auto-start
|
|
302
|
+
├─► project being removed → Error page (503)
|
|
161
303
|
└─► unknown → Error page
|
|
162
304
|
```
|
|
163
305
|
|
|
@@ -167,6 +309,7 @@ The agent:
|
|
|
167
309
|
3. Tracks project activity for scale-to-zero
|
|
168
310
|
4. Starts stopped projects when requested
|
|
169
311
|
5. Shows a waiting page while projects start
|
|
312
|
+
6. Removes projects whose [scheduled removal](#scheduled-removals) is due
|
|
170
313
|
|
|
171
314
|
## Endpoints
|
|
172
315
|
|
|
@@ -248,7 +391,7 @@ router container and starts one running project. DDEV regenerates the static
|
|
|
248
391
|
config on any project start, but reads `router-compose.*.yaml` only when it
|
|
249
392
|
recreates the router, so the removal is what gets the credentials into the
|
|
250
393
|
container. The router is back within seconds. With no project running, the
|
|
251
|
-
next deploy applies it. `trafic-agent audit` reports whether the certificate
|
|
394
|
+
next deploy applies it. `sudo trafic-agent audit` reports whether the certificate
|
|
252
395
|
was issued.
|
|
253
396
|
|
|
254
397
|
The first order takes about a minute after the router starts: lego waits for
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { i as runPendingMigrations, n as setup, r as listMigrations, s as
|
|
2
|
+
import { $ as info, A as cancelScheduledDestroy, Ct as validateConfig, M as closeDb, Q as execSilent, St as parseTimeout, Z as exec, at as warn, bt as loadConfig, et as isRoot, it as success, l as startServer, rt as step, z as getScheduledDestroys } from "./server-B_iFvS6v.js";
|
|
3
|
+
import { _ as startIdleScheduler, d as destroyProject, f as loadDestroyConfig, g as startBackupSweepScheduler, i as runPendingMigrations, l as scheduleProjectDestroy, n as setup, p as openDestroyDb, r as listMigrations, s as startAccessLogSweepScheduler, t as audit, u as startScheduledDestroyScheduler } from "./setup-BbXL1ZFY.js";
|
|
4
4
|
import { readFileSync } from "node:fs";
|
|
5
5
|
import { execSync, spawnSync } from "node:child_process";
|
|
6
6
|
import { parseArgs } from "node:util";
|
|
@@ -113,7 +113,7 @@ function upgradeDdev(dryRun) {
|
|
|
113
113
|
/** The real collaborators, used unless a caller passes their own. */
|
|
114
114
|
function nodeUpgradeIo() {
|
|
115
115
|
return {
|
|
116
|
-
currentVersion: "0.1.
|
|
116
|
+
currentVersion: "0.1.50",
|
|
117
117
|
alreadyReExeced: process.env["TRAFIC_UPGRADE_REEXEC"] === "1",
|
|
118
118
|
isRoot,
|
|
119
119
|
fetchLatestVersion,
|
|
@@ -192,6 +192,9 @@ function upgradeDdevStep(dryRun, io) {
|
|
|
192
192
|
else success("DDEV up to date");
|
|
193
193
|
}
|
|
194
194
|
//#endregion
|
|
195
|
+
//#region ../../skills/trafic-agent/SKILL.md?raw
|
|
196
|
+
var SKILL_default = "---\nname: trafic-agent\ndescription: Operate a Trafic preview server with trafic-agent (@studiometa/trafic-agent), the service that runs next to DDEV and Traefik. Covers setup flags, the trafic-agent systemd service and its logs, upgrade and migrations, audit, destroy (--backup, --timeout, --delay, --cancel, --list), the keys of /etc/trafic/config.toml (auth, idle timeout, max_running_projects, [backup], [tls]) and the per-project .ddev/config.trafic.yaml. Use when you install, configure, upgrade, debug or clean up a server that hosts DDEV previews deployed with trafic, or when you edit its config.toml.\nlicense: MIT\n---\n\n# trafic-agent\n\n`trafic-agent` runs on the server that hosts the DDEV previews. It does these jobs:\n\n- Forward authentication for Traefik: IP allowlist, bearer tokens, basic auth, rules per hostname.\n- Scale-to-zero: it stops idle projects, and starts a stopped project on its next request behind a waiting page.\n- A limit on the number of running projects (`max_running_projects`).\n- Project removal, with an optional backup, now or at a scheduled date.\n\nThis file can be older or newer than the agent on the server. `trafic-agent skill` prints the copy of this skill that matches the installed version: run it on the server and prefer its content when it differs.\n\n## Users and files\n\n| Item | Value |\n|---|---|\n| Service | `trafic-agent.service`, runs `trafic-agent start --config /etc/trafic/config.toml` as user `ddev` |\n| Config | `/etc/trafic/config.toml`, mode `640`, owner `root:ddev`. Edit it as root |\n| Database | `/var/lib/trafic/db.sqlite` (projects, access logs, scheduled removals), owned by `ddev` |\n| Migration state | `/etc/trafic/.migrations.json` |\n| Projects | `/home/ddev/www/<project>` (`projects_dir`) |\n| Backups | `/var/backups/trafic/<project>/<timestamp>/` (`[backup] dir`), owner `ddev`, mode `700` |\n| HTTP port | `9876` (`port`), open to the Docker bridge only |\n\nWhich user runs which command:\n\n- As root (or with `sudo`): `setup`, `upgrade`, `audit`, edits of `config.toml`, `systemctl`. The `ddev` user has no general sudo.\n- As `ddev`: `destroy`. It refuses to run as root, because DDEV does not run as root and root could leave files in the database directory that the service cannot write.\n- As any user: `upgrade --list`, `version`, `help`, `skill`.\n\n## Service and logs\n\n```bash\nsudo systemctl status trafic-agent\nsudo systemctl restart trafic-agent # after each edit of config.toml\njournalctl -u trafic-agent -f # follow the logs\njournalctl -u trafic-agent -n 200 --no-pager\ncurl -s http://127.0.0.1:9876/__health__ # agent health and version\n```\n\nThe unit has a systemd sandbox (`ProtectSystem=strict`). The service can write only to `/var/lib/trafic`, `/home/ddev` and `/var/backups/trafic`. When the config is not valid, `start` prints `Configuration errors:` with one line per error, exits with code 1, and systemd restarts it every 5 seconds. Read the journal, fix the config, restart.\n\n## `trafic-agent setup`\n\nRun as root on a fresh Ubuntu 24.04 server, or remotely with `trafic setup` from the CLI. It installs Docker, DDEV and Node.js, configures DDEV and Traefik for forward auth, writes `/etc/trafic/config.toml` (only when it does not exist), creates the systemd service and hardens the server (SSH, UFW, Fail2ban, unattended-upgrades, `DOCKER-USER` firewall rules). It asks no questions. A second run is safe: each step is skipped when the server is already in the target state.\n\n```bash\nsudo trafic-agent setup --tld=previews.example.com --email=admin@example.com\n```\n\n| Flag | Use |\n|---|---|\n| `--tld` | Domain of the previews. Required on the first run. A second run reads `tld` from the config |\n| `--email` | Let's Encrypt email. Without it, projects use a mkcert certificate |\n| `--dns-provider <lego name>` | One wildcard `*.<tld>` certificate through DNS-01. Requires `--email`, also on a second run that reads it from the config |\n| `--dns-env KEY=VALUE` | Credential of the DNS provider, repeatable |\n| `--ssh-users <list>` | Users that SSH allows, comma-separated. Default `ddev` |\n| `--trusted-proxy-hops <n>` | `2` when a CDN or load balancer is in front of Traefik. Written to a new config only |\n| `--no-hardening`, `--no-root-ssh`, `--no-docker`, `--no-ddev` | Skip or change these steps. With `--no-root-ssh`, only the rescue mode of the provider can recover root access |\n| `--dry-run` | Show what setup would do |\n\nSetup does not create the wildcard DNS record (`*.<tld>` to the server IP) and does not add SSH keys. Add CI keys to `/home/ddev/.ssh/authorized_keys`.\n\nA second run applies the flags you give it, not the flags of the first run. Pass the same `--email`, `--ssh-users`, `--no-root-ssh` and `--no-*` flags again: without `--email`, it turns Let's Encrypt off and deletes the issued certificates; without `--ssh-users`, SSH allows only `ddev`, root and the user who ran `sudo`. It reads only `tld` and `[tls]` from the config.\n\nWhen the config exists, setup does not change it. With a new `--dns-provider`, it prints the `[tls]` section to add by hand.\n\n## `trafic-agent upgrade`\n\n`update` is an alias.\n\n```bash\ntrafic-agent upgrade --list # migrations and their status, no change\nsudo trafic-agent upgrade --dry-run\nsudo trafic-agent upgrade\n```\n\nSteps: install the latest `@studiometa/trafic-agent` from npm when it is newer, run the pending migrations, upgrade the `ddev` apt package (a failure is only a warning), restart `trafic-agent`.\n\n- Upgrade the agent **before** you raise the CLI version in CI. `trafic deploy` and `trafic destroy` call `trafic-agent destroy` on the server, and an older agent does not know the new options.\n- Migrations fix the state of existing servers. They run once per server, in order, and the state file records each one after it succeeds. A fresh `setup` marks all of them as applied.\n- DDEV is upgraded only here, never by unattended-upgrades, so that a new major version does not break previews without an operator.\n\n## `trafic-agent audit`\n\n```bash\nsudo trafic-agent audit\n```\n\nRun it as root. `ufw status` and the files under `/var/lib/docker` need root, so as another user the firewall and wildcard certificate checks fail. It checks SSH, the firewall, Fail2ban, automatic updates, the agent service, Docker, disk usage, the permissions of the config, the owner of the projects directory and, with a `[tls]` section, the wildcard certificate and the permissions of its credentials. It exits with code 1 when a check fails, and each failed check prints a fix.\n\n## `trafic-agent destroy`\n\nRun it as `ddev`. `trafic destroy` from the CLI runs the same command over SSH.\n\n```bash\ntrafic-agent destroy preview-42--my-app\ntrafic-agent destroy preview-42--my-app --backup\ntrafic-agent destroy preview-42--my-app --backup --timeout 1h\ntrafic-agent destroy preview-42--my-app --delay 48h --backup\ntrafic-agent destroy preview-42--my-app --cancel\ntrafic-agent destroy --list\n```\n\nSteps: backup (with `--backup`; if it fails, nothing is removed), `ddev delete -Oy` (a failure is only a warning), removal of the project directory, removal of the project's rows in the agent database.\n\n| Flag | Use |\n|---|---|\n| `-c`, `--config` | Config file. Default: first of `/etc/trafic/config.toml`, `./config.toml`, `./trafic.toml` |\n| `--backup` | Dump the database (`db.sql.gz`) and archive the upload directories (`files.tar.gz`) to `<backup dir>/<project>/<timestamp>/` first. The agent starts a stopped project for the backup and stops it again |\n| `--timeout <duration>` | Time limit of the dump and the archive together. Default `40m`. `10m`, `90s`, `1h`; a bare number is minutes |\n| `--delay <duration>` | Remove nothing now. Record a removal at now plus the delay. Hours, minutes and seconds, in that order, no spaces: `48h`, `1h30m`, `90m` |\n| `--cancel` | Cancel the scheduled removal of the project |\n| `--list` | List the scheduled removals, soonest first. `in progress` marks a removal that has started |\n\nSafety rules of the command:\n\n- It refuses a project directory outside `projects_dir`, also after it follows symbolic links, and a project directory that is a symbolic link. It refuses an empty name, `.`, `..` and names with `/`.\n- When the directory does not exist, it only deletes the database rows. So you can run it again after a failed removal.\n- It checks the config as `start` does, and stops before any change when the config is not valid.\n\nScheduled removals:\n\n- The service checks them every `idle_check_interval`, and removes each project whose date is past with the same steps.\n- A failed scheduled removal is logged in the journal and tried again at the next check. A removal that fails after its backup does not back up again.\n- `trafic deploy` cancels the scheduled removal of the project it deploys. After the backup, the agent marks the removal as started, just before `ddev delete`. From then on, `--cancel` and a deploy fail with `the removal of <project> is in progress`.\n- The service makes the backup of a scheduled removal inside its sandbox. If `[backup] dir` is outside `/var/backups/trafic`, `/home/ddev` and `/var/lib/trafic`, each scheduled backup fails with `Cannot write to the backup directory` and the project is never removed.\n\nRestore a backup:\n\n```bash\ncd <project directory>\nddev import-db --file=/var/backups/trafic/<project>/<timestamp>/db.sql.gz\ntar -xzf /var/backups/trafic/<project>/<timestamp>/files.tar.gz -C <project directory>\n```\n\n## `config.toml`\n\nEdit `/etc/trafic/config.toml` as root, then `sudo systemctl restart trafic-agent`. Values not in the file take their default.\n\n| Key | Default | Use |\n|---|---|---|\n| `tld` | none, required | Domain of the previews |\n| `port` | `9876` | Agent HTTP port |\n| `db_path` | `/var/lib/trafic/db.sqlite` | Agent database |\n| `project_list_path` | `/home/ddev/.ddev/project_list.yaml` | DDEV project list, watched for changes |\n| `projects_dir` | `/home/ddev/www` | Must be the same as `trafic deploy --projects-dir` (default `~/www`). `destroy` never removes a directory outside it |\n| `idle_timeout` | `30m` (setup writes `4h`) | Stop a project after this time without requests |\n| `idle_check_interval` | `5m` | Interval of the idle sweep, the running limit and the scheduled removals |\n| `max_running_projects` | `0` (no limit) | Most projects that run at the same time. See below |\n| `[backup] dir` | `/var/backups/trafic` | Absolute path. Must not be `projects_dir`, be inside it or contain it (so not `/home/ddev`) |\n| `[backup] retain_days` | `14` | The service removes older backups once an hour. Whole number, 1 or more |\n| `[auth] default_policy` | `basic` | `allow`, `deny`, `basic` or `token`. `basic` also accepts the tokens |\n| `[auth] trusted_proxy_hops` | `1` | Proxies in front of the agent. `2` with a CDN. Set it before `allowed_ips` |\n| `[auth] allowed_ips` | `[]` | IPs or CIDR ranges that skip authentication |\n| `[auth] tokens` | `[]` | Bearer tokens, sent as `Authorization: Bearer <token>` |\n| `[auth] basic_auth` | `[]` | `\"user:password\"` strings, or `{ username, password }` tables |\n| `[[auth.rules]]` | none | `match` (hostname glob), `policy`, optional `tokens` and `allowed_ips`. First match wins, after the global `allowed_ips` |\n| `[tls] dns_provider`, `ca_server`, `[tls.dns_env]` | none | Wildcard certificate through DNS-01. A restart does not apply a change: migration `0016__wildcard_dns_challenge` applies a first `[tls]` on a server set up before it (`sudo trafic-agent upgrade`); otherwise run `sudo trafic-agent setup` again with the flags of the first run |\n\nExample:\n\n```toml\ntld = \"previews.example.com\"\nprojects_dir = \"/home/ddev/www\"\nidle_timeout = \"4h\"\nidle_check_interval = \"5m\"\nmax_running_projects = 8\n\n[backup]\ndir = \"/var/backups/trafic\"\nretain_days = 14\n\n[auth]\ndefault_policy = \"basic\"\ntrusted_proxy_hops = 1\nallowed_ips = [\"203.0.113.0/24\"]\ntokens = [\"ci-token\"]\nbasic_auth = [\"preview:change-me\"]\n\n[[auth.rules]]\nmatch = \"*.public.*\"\npolicy = \"allow\"\n```\n\nValidation errors that stop `start` and `destroy`: `tld is required`, `port must be between 1 and 65535`, `max_running_projects must be a whole number, 0 or more`, `auth.default_policy must be ...`, `backup.dir must be an absolute path`, `backup.dir must not be projects_dir, be inside it or contain it`, `backup.retain_days must be a whole number of 1 or more`, and errors on `tls.dns_env` keys.\n\n## `max_running_projects`\n\n- Before the waiting page starts a stopped project, the agent counts the running projects and the projects it is starting. Over the limit, it stops the projects with the oldest access first.\n- `trafic deploy` starts projects without the agent. The idle sweep then stops the projects with the oldest access until the server is under the limit.\n- Projects with `idle_timeout: never` are never stopped by this rule. If no other project can stop, the agent starts the project anyway and logs a warning.\n- A project that `destroy` is removing is never stopped by this rule or by the idle sweep.\n\n## Per-project config: `.ddev/config.trafic.yaml`\n\nCommitted in the project repository. Flat `key: value` lines only.\n\n```yaml\nauth_policy: allow # allow, deny, basic or token\nidle_timeout: never # or a duration such as 2h\n```\n\n- `idle_timeout` replaces the global value for this project, shorter or longer. The idle sweep reads it at each check.\n- `auth_policy` replaces only `default_policy`. The global `allowed_ips` and a matching `[[auth.rules]]` still apply first. The agent reads it when it starts and when DDEV's `project_list.yaml` changes; after a change, restart the service.\n\n## Troubleshooting\n\n| Symptom | Check and fix |\n|---|---|\n| Service restarts in a loop | `journalctl -u trafic-agent -n 50`. Fix the `Configuration errors:` lines, then restart |\n| Preview asks for a password | `default_policy = \"basic\"`. Add the client IP to `allowed_ips`, a rule, or `auth_policy: allow` in the project |\n| `allowed_ips` does not match | `trusted_proxy_hops` is wrong. Use `2` behind a CDN |\n| Preview stays on the waiting page | `ddev list`, then `ddev start` in the project directory to see the DDEV error. Check `max_running_projects` and the journal |\n| Project stops too soon | Raise `idle_timeout`, or set `idle_timeout: never` in the project |\n| `cannot read ... (EACCES): connect as the ddev user` | Run `destroy` as `ddev`, not as another user |\n| `Refusing to remove ...: it is not inside the projects directory` | Set `projects_dir` to the directory that `trafic deploy` uses |\n| `The backup timed out ...` | Run again with a larger `--timeout`, for example `1h` |\n| `Cannot write to the backup directory` | Put `[backup] dir` in `/var/backups/trafic`, `/home/ddev` (not `projects_dir`) or `/var/lib/trafic`, owned by `ddev` |\n| Certificate warning on previews | `sudo trafic-agent audit`. Without `--email` at setup, projects use mkcert. Per-host certificates are limited to 50 new ones per domain per week; use `[tls]` for one wildcard |\n| Ports 8025, 8026, 8142, 8143 reachable | The `DOCKER-USER` rules are missing: `sudo iptables -S DOCKER-USER`, `sudo systemctl status trafic-docker-firewall` |\n\n## CI side\n\nFor the CI jobs that call this server (`trafic deploy`, `trafic destroy`), use the `trafic-cli` skill, or run `trafic skill` with the CLI version that CI uses.\n";
|
|
197
|
+
//#endregion
|
|
195
198
|
//#region src/cli.ts
|
|
196
199
|
var HELP = `
|
|
197
200
|
trafic-agent — Server agent for DDEV preview environments
|
|
@@ -202,14 +205,18 @@ Usage:
|
|
|
202
205
|
Commands:
|
|
203
206
|
start Start the agent server
|
|
204
207
|
setup Setup a new server (Docker, DDEV, hardening)
|
|
205
|
-
upgrade
|
|
208
|
+
upgrade Install the latest agent, run migrations, upgrade DDEV, restart the service
|
|
206
209
|
update Alias for upgrade
|
|
207
210
|
audit Run security audit checks
|
|
208
|
-
|
|
209
|
-
|
|
211
|
+
destroy <name> Remove a project: DDEV project, directory, database records,
|
|
212
|
+
now or after a delay
|
|
213
|
+
skill Print the agent skill (SKILL.md) of this version
|
|
214
|
+
version Show version (also -v, --version)
|
|
215
|
+
help Show this help (also -h, --help)
|
|
210
216
|
|
|
211
217
|
Start options:
|
|
212
|
-
-c, --config <path> Path to config file (default: /etc/trafic/config.toml
|
|
218
|
+
-c, --config <path> Path to config file (default: first of /etc/trafic/config.toml,
|
|
219
|
+
./config.toml, ./trafic.toml)
|
|
213
220
|
-p, --port <port> Override port from config
|
|
214
221
|
|
|
215
222
|
Setup options:
|
|
@@ -227,6 +234,20 @@ Setup options:
|
|
|
227
234
|
Written to the generated config; ignored if one exists
|
|
228
235
|
--dry-run Show what would be done without making changes
|
|
229
236
|
|
|
237
|
+
Destroy options:
|
|
238
|
+
-c, --config <path> Path to config file (default: first of /etc/trafic/config.toml,
|
|
239
|
+
./config.toml, ./trafic.toml)
|
|
240
|
+
--backup Back up the database and upload directories first, to
|
|
241
|
+
[backup] dir. If the backup fails, nothing is removed
|
|
242
|
+
--timeout <duration> Time limit of the backup, the database dump and the files
|
|
243
|
+
archive together: 10m, 90s, 1h; a bare number is minutes
|
|
244
|
+
(default: 40m)
|
|
245
|
+
--delay <duration> Remove nothing now: schedule the removal after this delay
|
|
246
|
+
(hours, minutes, seconds only: 48h, 1h30m). The agent
|
|
247
|
+
removes the project when it is due
|
|
248
|
+
--cancel Cancel the scheduled removal of the project
|
|
249
|
+
--list List the scheduled removals
|
|
250
|
+
|
|
230
251
|
Upgrade/update options:
|
|
231
252
|
--dry-run Show what would be done without making changes
|
|
232
253
|
--list List all migrations and their status (no install or restart)
|
|
@@ -247,30 +268,42 @@ Examples:
|
|
|
247
268
|
|
|
248
269
|
sudo trafic-agent setup --tld=previews.example.com --no-hardening --dry-run
|
|
249
270
|
|
|
250
|
-
# Upgrade: install latest
|
|
271
|
+
# Upgrade: install the latest agent, run migrations, upgrade DDEV, restart the service
|
|
251
272
|
sudo trafic-agent upgrade
|
|
252
273
|
sudo trafic-agent upgrade --dry-run
|
|
253
274
|
trafic-agent upgrade --list
|
|
254
275
|
sudo trafic-agent update # alias for upgrade
|
|
255
276
|
|
|
256
277
|
# Run security audit
|
|
257
|
-
trafic-agent audit
|
|
278
|
+
sudo trafic-agent audit
|
|
279
|
+
|
|
280
|
+
# Remove a project (as the ddev user, never as root)
|
|
281
|
+
trafic-agent destroy preview-42--my-app
|
|
282
|
+
trafic-agent destroy preview-42--my-app --backup
|
|
283
|
+
trafic-agent destroy preview-42--my-app --backup --timeout 1h
|
|
284
|
+
trafic-agent destroy preview-42--my-app --delay 48h --backup
|
|
285
|
+
trafic-agent destroy preview-42--my-app --cancel
|
|
286
|
+
trafic-agent destroy --list
|
|
258
287
|
`;
|
|
259
288
|
function printHelp() {
|
|
260
289
|
console.log(HELP.trim());
|
|
261
290
|
}
|
|
262
291
|
function printVersion() {
|
|
263
|
-
console.log("0.1.
|
|
292
|
+
console.log("0.1.50");
|
|
264
293
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
if (values.port) config.port = parseInt(values.port, 10);
|
|
294
|
+
/** Print the configuration errors and exit with code 1, if there are any. */
|
|
295
|
+
function exitOnConfigErrors(config) {
|
|
268
296
|
const errors = validateConfig(config);
|
|
269
297
|
if (errors.length > 0) {
|
|
270
298
|
console.error("Configuration errors:");
|
|
271
299
|
for (const error of errors) console.error(` - ${error}`);
|
|
272
300
|
process.exit(1);
|
|
273
301
|
}
|
|
302
|
+
}
|
|
303
|
+
async function runStart(values) {
|
|
304
|
+
const config = loadConfig(values.config);
|
|
305
|
+
if (values.port) config.port = parseInt(values.port, 10);
|
|
306
|
+
exitOnConfigErrors(config);
|
|
274
307
|
const shutdown = () => {
|
|
275
308
|
console.log("\nShutting down...");
|
|
276
309
|
closeDb();
|
|
@@ -280,6 +313,75 @@ async function runStart(values) {
|
|
|
280
313
|
process.on("SIGTERM", shutdown);
|
|
281
314
|
startServer(config);
|
|
282
315
|
startIdleScheduler(config);
|
|
316
|
+
startBackupSweepScheduler(config);
|
|
317
|
+
startAccessLogSweepScheduler();
|
|
318
|
+
startScheduledDestroyScheduler(config);
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Remove a project. Runs as the ddev user, next to the running service: both
|
|
322
|
+
* open the same database, so the config is read the same way `start` reads it.
|
|
323
|
+
*/
|
|
324
|
+
async function runDestroy(values, name) {
|
|
325
|
+
if (isRoot()) {
|
|
326
|
+
console.error("Error: run trafic-agent destroy as the ddev user, not root");
|
|
327
|
+
process.exit(1);
|
|
328
|
+
}
|
|
329
|
+
if (!name && !values.list) {
|
|
330
|
+
console.error("Error: a project name is required");
|
|
331
|
+
console.error("Example: trafic-agent destroy preview-42--my-app");
|
|
332
|
+
process.exit(1);
|
|
333
|
+
}
|
|
334
|
+
const timeout = values.timeout;
|
|
335
|
+
const timeoutMs = timeout === void 0 ? void 0 : parseTimeout(timeout);
|
|
336
|
+
if (timeout !== void 0 && timeoutMs === void 0) {
|
|
337
|
+
console.error(`Error: invalid --timeout "${timeout}". Expected a duration such as 10m, 90s or 1h`);
|
|
338
|
+
process.exit(1);
|
|
339
|
+
}
|
|
340
|
+
const backup = values.backup;
|
|
341
|
+
const delay = values.delay;
|
|
342
|
+
try {
|
|
343
|
+
const config = loadDestroyConfig(values.config);
|
|
344
|
+
exitOnConfigErrors(config);
|
|
345
|
+
openDestroyDb(config.dbPath);
|
|
346
|
+
if (values.list) printScheduledDestroys();
|
|
347
|
+
else if (values.cancel) cancelDestroy(name);
|
|
348
|
+
else if (delay !== void 0) {
|
|
349
|
+
const destroyAt = scheduleProjectDestroy(name, delay, config, {
|
|
350
|
+
backup,
|
|
351
|
+
timeoutMs
|
|
352
|
+
});
|
|
353
|
+
console.log(`Scheduled the removal of ${name} at ${destroyAt.toISOString()}${backup ? ", after a backup" : ""}`);
|
|
354
|
+
} else await destroyProject(name, config, {
|
|
355
|
+
backup,
|
|
356
|
+
timeoutMs
|
|
357
|
+
});
|
|
358
|
+
} catch (error) {
|
|
359
|
+
console.error(`Error: ${error.message}`);
|
|
360
|
+
process.exitCode = 1;
|
|
361
|
+
} finally {
|
|
362
|
+
closeDb();
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Cancel the scheduled removal of a project. Fails when the sweep already
|
|
367
|
+
* removes the project, so a deploy that cancels first stops there.
|
|
368
|
+
*/
|
|
369
|
+
function cancelDestroy(name) {
|
|
370
|
+
const result = cancelScheduledDestroy(name);
|
|
371
|
+
if (result === "removing") throw new Error(`the removal of ${name} is in progress, it is too late to cancel it`);
|
|
372
|
+
console.log(result === "cancelled" ? `Cancelled the scheduled removal of ${name}` : `No removal scheduled for ${name}`);
|
|
373
|
+
}
|
|
374
|
+
/** Print the scheduled removals, soonest first. */
|
|
375
|
+
function printScheduledDestroys() {
|
|
376
|
+
const scheduled = getScheduledDestroys();
|
|
377
|
+
if (scheduled.length === 0) {
|
|
378
|
+
console.log("No removal scheduled");
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
for (const { name, destroyAt, backup, removing } of scheduled) {
|
|
382
|
+
const notes = [backup && "backup", removing && "in progress"].filter(Boolean);
|
|
383
|
+
console.log(`${new Date(destroyAt).toISOString()} ${name}${notes.length ? ` (${notes.join(", ")})` : ""}`);
|
|
384
|
+
}
|
|
283
385
|
}
|
|
284
386
|
function handleUpgrade(values) {
|
|
285
387
|
const dryRun = values["dry-run"];
|
|
@@ -374,6 +476,10 @@ async function main() {
|
|
|
374
476
|
type: "boolean",
|
|
375
477
|
short: "h"
|
|
376
478
|
},
|
|
479
|
+
version: {
|
|
480
|
+
type: "boolean",
|
|
481
|
+
short: "v"
|
|
482
|
+
},
|
|
377
483
|
config: {
|
|
378
484
|
type: "string",
|
|
379
485
|
short: "c"
|
|
@@ -396,7 +502,11 @@ async function main() {
|
|
|
396
502
|
},
|
|
397
503
|
"trusted-proxy-hops": { type: "string" },
|
|
398
504
|
"dry-run": { type: "boolean" },
|
|
399
|
-
list: { type: "boolean" }
|
|
505
|
+
list: { type: "boolean" },
|
|
506
|
+
backup: { type: "boolean" },
|
|
507
|
+
timeout: { type: "string" },
|
|
508
|
+
delay: { type: "string" },
|
|
509
|
+
cancel: { type: "boolean" }
|
|
400
510
|
}
|
|
401
511
|
});
|
|
402
512
|
const command = positionals[0] ?? "help";
|
|
@@ -404,6 +514,10 @@ async function main() {
|
|
|
404
514
|
printHelp();
|
|
405
515
|
process.exit(0);
|
|
406
516
|
}
|
|
517
|
+
if (values.version) {
|
|
518
|
+
printVersion();
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
407
521
|
switch (command) {
|
|
408
522
|
case "help":
|
|
409
523
|
printHelp();
|
|
@@ -411,6 +525,9 @@ async function main() {
|
|
|
411
525
|
case "version":
|
|
412
526
|
printVersion();
|
|
413
527
|
break;
|
|
528
|
+
case "skill":
|
|
529
|
+
process.stdout.write(SKILL_default);
|
|
530
|
+
break;
|
|
414
531
|
case "start":
|
|
415
532
|
await runStart(values);
|
|
416
533
|
break;
|
|
@@ -424,6 +541,9 @@ async function main() {
|
|
|
424
541
|
case "audit":
|
|
425
542
|
await audit();
|
|
426
543
|
break;
|
|
544
|
+
case "destroy":
|
|
545
|
+
await runDestroy(values, positionals[1]);
|
|
546
|
+
break;
|
|
427
547
|
default:
|
|
428
548
|
console.error(`Unknown command: ${command}`);
|
|
429
549
|
printHelp();
|