shiplord 0.1.0__tar.gz
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.
- shiplord-0.1.0/PKG-INFO +312 -0
- shiplord-0.1.0/README.md +304 -0
- shiplord-0.1.0/pyproject.toml +17 -0
- shiplord-0.1.0/setup.cfg +4 -0
- shiplord-0.1.0/src/shiplord/__init__.py +3 -0
- shiplord-0.1.0/src/shiplord/admin.py +140 -0
- shiplord-0.1.0/src/shiplord/authorization.py +82 -0
- shiplord-0.1.0/src/shiplord/cli.py +552 -0
- shiplord-0.1.0/src/shiplord/client.py +271 -0
- shiplord-0.1.0/src/shiplord/commands.py +347 -0
- shiplord-0.1.0/src/shiplord/configure.py +168 -0
- shiplord-0.1.0/src/shiplord/deploy.py +394 -0
- shiplord-0.1.0/src/shiplord/detect.py +122 -0
- shiplord-0.1.0/src/shiplord/dispatch.py +64 -0
- shiplord-0.1.0/src/shiplord/environment.py +73 -0
- shiplord-0.1.0/src/shiplord/framing.py +90 -0
- shiplord-0.1.0/src/shiplord/github.py +548 -0
- shiplord-0.1.0/src/shiplord/host.py +90 -0
- shiplord-0.1.0/src/shiplord/installer.py +256 -0
- shiplord-0.1.0/src/shiplord/jobs.py +188 -0
- shiplord-0.1.0/src/shiplord/keys.py +183 -0
- shiplord-0.1.0/src/shiplord/layout.py +90 -0
- shiplord-0.1.0/src/shiplord/manifest.py +182 -0
- shiplord-0.1.0/src/shiplord/native.py +278 -0
- shiplord-0.1.0/src/shiplord/payload.py +102 -0
- shiplord-0.1.0/src/shiplord/privileged_host.py +248 -0
- shiplord-0.1.0/src/shiplord/probes.py +171 -0
- shiplord-0.1.0/src/shiplord/protocol.py +74 -0
- shiplord-0.1.0/src/shiplord/provision.py +434 -0
- shiplord-0.1.0/src/shiplord/registry.py +151 -0
- shiplord-0.1.0/src/shiplord/release_state.py +401 -0
- shiplord-0.1.0/src/shiplord/retire.py +38 -0
- shiplord-0.1.0/src/shiplord/routing.py +507 -0
- shiplord-0.1.0/src/shiplord/runtime.py +522 -0
- shiplord-0.1.0/src/shiplord/serverdoctor.py +89 -0
- shiplord-0.1.0/src/shiplord/servers.py +244 -0
- shiplord-0.1.0/src/shiplord/services.py +85 -0
- shiplord-0.1.0/src/shiplord/shutdown.py +79 -0
- shiplord-0.1.0/src/shiplord/storage.py +65 -0
- shiplord-0.1.0/src/shiplord/system.py +109 -0
- shiplord-0.1.0/src/shiplord/tailscale.py +127 -0
- shiplord-0.1.0/src/shiplord/template.py +195 -0
- shiplord-0.1.0/src/shiplord/transport.py +127 -0
- shiplord-0.1.0/src/shiplord/worker.py +82 -0
- shiplord-0.1.0/src/shiplord/workflow.py +196 -0
- shiplord-0.1.0/src/shiplord.egg-info/PKG-INFO +312 -0
- shiplord-0.1.0/src/shiplord.egg-info/SOURCES.txt +94 -0
- shiplord-0.1.0/src/shiplord.egg-info/dependency_links.txt +1 -0
- shiplord-0.1.0/src/shiplord.egg-info/entry_points.txt +2 -0
- shiplord-0.1.0/src/shiplord.egg-info/requires.txt +1 -0
- shiplord-0.1.0/src/shiplord.egg-info/top_level.txt +1 -0
- shiplord-0.1.0/tests/test_authorization.py +186 -0
- shiplord-0.1.0/tests/test_caddy_integration.py +199 -0
- shiplord-0.1.0/tests/test_cli.py +377 -0
- shiplord-0.1.0/tests/test_client.py +268 -0
- shiplord-0.1.0/tests/test_commands.py +366 -0
- shiplord-0.1.0/tests/test_configure.py +166 -0
- shiplord-0.1.0/tests/test_deploy.py +503 -0
- shiplord-0.1.0/tests/test_deploy_integration.py +539 -0
- shiplord-0.1.0/tests/test_detect.py +86 -0
- shiplord-0.1.0/tests/test_dispatch.py +73 -0
- shiplord-0.1.0/tests/test_e2e.py +506 -0
- shiplord-0.1.0/tests/test_e2e_systemd.py +351 -0
- shiplord-0.1.0/tests/test_environment.py +80 -0
- shiplord-0.1.0/tests/test_framing.py +92 -0
- shiplord-0.1.0/tests/test_github.py +622 -0
- shiplord-0.1.0/tests/test_helper_actions.py +364 -0
- shiplord-0.1.0/tests/test_host.py +84 -0
- shiplord-0.1.0/tests/test_host_integration.py +206 -0
- shiplord-0.1.0/tests/test_installer.py +213 -0
- shiplord-0.1.0/tests/test_jobs.py +190 -0
- shiplord-0.1.0/tests/test_keys.py +158 -0
- shiplord-0.1.0/tests/test_native.py +366 -0
- shiplord-0.1.0/tests/test_native_commands.py +138 -0
- shiplord-0.1.0/tests/test_native_helper.py +163 -0
- shiplord-0.1.0/tests/test_payload.py +109 -0
- shiplord-0.1.0/tests/test_probes.py +238 -0
- shiplord-0.1.0/tests/test_protocol.py +23 -0
- shiplord-0.1.0/tests/test_provision.py +645 -0
- shiplord-0.1.0/tests/test_registry.py +199 -0
- shiplord-0.1.0/tests/test_release_state.py +317 -0
- shiplord-0.1.0/tests/test_retire.py +52 -0
- shiplord-0.1.0/tests/test_routing.py +584 -0
- shiplord-0.1.0/tests/test_runtime.py +520 -0
- shiplord-0.1.0/tests/test_serverdoctor.py +153 -0
- shiplord-0.1.0/tests/test_servers.py +210 -0
- shiplord-0.1.0/tests/test_shutdown.py +482 -0
- shiplord-0.1.0/tests/test_ssh_integration.py +236 -0
- shiplord-0.1.0/tests/test_ssh_options.py +103 -0
- shiplord-0.1.0/tests/test_storage.py +209 -0
- shiplord-0.1.0/tests/test_sudo_integration.py +146 -0
- shiplord-0.1.0/tests/test_system.py +77 -0
- shiplord-0.1.0/tests/test_tailscale.py +93 -0
- shiplord-0.1.0/tests/test_template.py +191 -0
- shiplord-0.1.0/tests/test_transport.py +261 -0
- shiplord-0.1.0/tests/test_workflow.py +197 -0
shiplord-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: shiplord
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Deployment tooling for applications on a server
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: typer<1,>=0.12
|
|
8
|
+
|
|
9
|
+
# Shiplord
|
|
10
|
+
|
|
11
|
+
Shiplord ships tested releases of an application to your own Linux server over SSH. A coding agent or a
|
|
12
|
+
developer creates an application from the pinned template, provisions it once, and then deploys already-built
|
|
13
|
+
artifacts with one command, from a terminal or from GitHub Actions. Nothing is built on the server, no
|
|
14
|
+
deployment daemon runs, and every release is verified, journaled, and reversible.
|
|
15
|
+
|
|
16
|
+
The design and acceptance criteria are in [ship-cli-plan.md](ship-cli-plan.md). The executable is `shiplord`
|
|
17
|
+
and the application manifest is `ship.toml`.
|
|
18
|
+
|
|
19
|
+
## Status
|
|
20
|
+
|
|
21
|
+
Built and verified (see [docs/evidence.md](docs/evidence.md) for what ran, where, and what did not):
|
|
22
|
+
|
|
23
|
+
- Application creation from the pinned template, manifest validation, server registration.
|
|
24
|
+
- Server bootstrap, application provisioning, SSH keys, runtime environment, port allocation.
|
|
25
|
+
- Docker and native-binary deployment, readiness and routing checks, rollback, recovery, status, logs, plan.
|
|
26
|
+
- Caddy route transactions with validate-before-publish and restore on failure.
|
|
27
|
+
|
|
28
|
+
Also run on a real Hetzner server (Ubuntu, x86-64, behind Cloudflare): native deploys, rollback, a failed release
|
|
29
|
+
restored automatically, Let's Encrypt HTTPS, and a release deployed by GitHub Actions through Tailscale OIDC. Not
|
|
30
|
+
verified: the plan's controlled install on *your* production server (§12E). See [docs/evidence.md](docs/evidence.md).
|
|
31
|
+
|
|
32
|
+
## Install the CLI
|
|
33
|
+
|
|
34
|
+
Python 3.11 or later and OpenSSH are required on the machine you work from.
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
python -m pip install .
|
|
38
|
+
shiplord --help
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## How it fits together
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
your machine ──ssh──▶ ship-<app>@server ──forced command──▶ dispatcher ──sudo──▶ helper (root, one app)
|
|
45
|
+
│ starts a detached job
|
|
46
|
+
administrator ──ssh + sudo──▶ shiplord-admin (bootstrap, provision, keys, env) ▼
|
|
47
|
+
worker: journal → runtime → ready → route → commit
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
- **Administrator operations** (`server bootstrap`, `provision`, `env`) use your own SSH login and your own sudo
|
|
51
|
+
(you are asked for the sudo password if one is needed; it is sent on stdin, never in a command line).
|
|
52
|
+
- **Application operations** (`deploy`, `status`, `logs`, `rollback`, `recover`, `plan`) use a per-application
|
|
53
|
+
account whose key can run exactly one forced command. The root helper binds every request to that account's
|
|
54
|
+
registration, so an application's deployment account can operate only that application. (The running
|
|
55
|
+
application itself is only confined as described under *Security model*.)
|
|
56
|
+
- A deployment is a **job** that outlives your SSH session. A transient systemd unit runs it; you can disconnect
|
|
57
|
+
and read the result later.
|
|
58
|
+
|
|
59
|
+
## Quick start
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
# once per machine: register a server you can already SSH into with a trusted host key
|
|
63
|
+
shiplord server add production --ssh appuser@server.example.com
|
|
64
|
+
|
|
65
|
+
# once per server: installs the helper, wires Caddy and sshd (re-running changes nothing)
|
|
66
|
+
shiplord server bootstrap production
|
|
67
|
+
|
|
68
|
+
# once per application
|
|
69
|
+
shiplord init orders --template google-adk
|
|
70
|
+
cd orders
|
|
71
|
+
shiplord configure --server production --api-domain orders-api.example.com \
|
|
72
|
+
--frontend https://orders.example.com # or --no-frontend
|
|
73
|
+
shiplord provision # registers your SSH key for this application
|
|
74
|
+
shiplord env set --from-file .env.production # runtime secrets; values are never printed
|
|
75
|
+
shiplord registry login --username bot --token-file t # only for a private image: a read-only pull login
|
|
76
|
+
|
|
77
|
+
# every release
|
|
78
|
+
shiplord plan --image ghcr.io/owner/orders@sha256:<digest> --commit <sha> # read-only
|
|
79
|
+
shiplord deploy --image ghcr.io/owner/orders@sha256:<digest> --commit <sha>
|
|
80
|
+
shiplord status
|
|
81
|
+
shiplord logs --tail 100
|
|
82
|
+
shiplord rollback
|
|
83
|
+
shiplord recover
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Create the DNS record for `--api-domain` at your DNS provider; Shiplord checks DNS and reports it but never
|
|
87
|
+
changes it.
|
|
88
|
+
|
|
89
|
+
For a native Linux binary, configure with `--runtime native` and deploy with
|
|
90
|
+
`shiplord deploy --binary ./dist/orders --commit <sha>`. Build it for the server's architecture (a Linux ELF;
|
|
91
|
+
x86-64 and arm64 are supported), for example in CI. The binary is uploaded over the same SSH connection,
|
|
92
|
+
verified by size, SHA-256 and ELF header on the server, and run as an unprivileged account under a confined
|
|
93
|
+
systemd unit.
|
|
94
|
+
|
|
95
|
+
## Existing Docker applications
|
|
96
|
+
|
|
97
|
+
Use the generic profile to deploy an existing image without Google ADK settings. In a terminal, run
|
|
98
|
+
`shiplord configure` with no options in the repository. It lists your registered servers and asks only for what is
|
|
99
|
+
missing. When the repository has a `Dockerfile` (or `Containerfile`), it suggests `generic` and the port from the
|
|
100
|
+
final stage's `EXPOSE`. You confirm a summary before anything is written. Without a terminal (agents, CI, pipes)
|
|
101
|
+
nothing is asked, and every value comes from options:
|
|
102
|
+
|
|
103
|
+
```sh
|
|
104
|
+
shiplord configure --name handwriting --server production \
|
|
105
|
+
--api-domain handwriting.example.com --no-frontend --template generic \
|
|
106
|
+
--container-port 3000 --health-path /health --memory 512
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
This creates `template = "generic"` and `runtime = "docker"`. Without those options the defaults remain 8080,
|
|
110
|
+
`/ready`, and 512 MiB, and the repository is not inspected. Shiplord picks the server-side port and routes the
|
|
111
|
+
hostname to it, so `--container-port` is only the port your application listens on inside its container. The
|
|
112
|
+
image must start its own HTTP service, listen on `0.0.0.0` at `http.container_port` (Shiplord also sets `PORT` to
|
|
113
|
+
that value), and return HTTP 200 at `http.readiness_path`.
|
|
114
|
+
An image can serve both its website and API at the configured hostname.
|
|
115
|
+
|
|
116
|
+
Both profiles receive `HOST`, `PORT`, and `LOG_LEVEL`. Only `google-adk` receives Shiplord's `AGENT_DIR`,
|
|
117
|
+
`AGENT_NAME`, `RELOAD_AGENTS`, `SERVE_WEB_INTERFACE`, and `ALLOW_ORIGINS` settings. Generic images keep
|
|
118
|
+
their own defaults for those five variables and must implement their own CORS behavior when needed.
|
|
119
|
+
All managed variable names remain reserved in `shiplord env set`, including ADK names, because environment
|
|
120
|
+
files belong to the application and can survive a profile change.
|
|
121
|
+
|
|
122
|
+
`configure` defaults to `google-adk` for compatibility. `init` still scaffolds only `google-adk`, and native
|
|
123
|
+
binaries still require that profile. `github setup` generates the ADK template's workflow and refuses generic
|
|
124
|
+
applications before creating keys or changing GitHub; use a custom workflow that builds your image, runs
|
|
125
|
+
your application's checks, and invokes `shiplord deploy` by digest.
|
|
126
|
+
|
|
127
|
+
The CLI requires the helper capability `docker_generic_v1` before submitting a generic plan or deploy.
|
|
128
|
+
Upgrade with `shiplord server bootstrap` when it is missing. Do not downgrade to a helper that cannot read
|
|
129
|
+
generic release records. Changing the profile replaces the container even with the same image, commit,
|
|
130
|
+
and sequence; rollback and journal recovery restore the recorded profile. Containers without a profile
|
|
131
|
+
label are interpreted as legacy `google-adk` containers.
|
|
132
|
+
|
|
133
|
+
## Persistent storage (Docker)
|
|
134
|
+
|
|
135
|
+
Each Docker application has one named volume, `shiplord-<application>-artifacts`, mounted at `/app/src/.adk` by
|
|
136
|
+
default. To mount it elsewhere, add a table to `ship.toml`:
|
|
137
|
+
|
|
138
|
+
```toml
|
|
139
|
+
[storage]
|
|
140
|
+
container_path = "/data"
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
The path must be a canonical absolute directory path (letters, digits, `.`, `_`, `-`; no `..`, trailing or repeated
|
|
144
|
+
slashes), and may not be, or be inside, `/proc`, `/sys`, `/dev`, `/run`, `/etc`, `/bin`, `/sbin`, `/usr`, `/lib`,
|
|
145
|
+
`/lib64`, `/boot`, `/root`, `/var/run` or `/var/lock`. Native applications reject `[storage]`.
|
|
146
|
+
|
|
147
|
+
- Changing the path mounts the **same** volume at the new location. No files move and nothing is migrated:
|
|
148
|
+
relative file names, including SQLite databases, stay where they were. `shiplord plan` shows the old and new path.
|
|
149
|
+
- A mount can hide files the image keeps at that path; check the destination against your image.
|
|
150
|
+
- Rollback restores the path the earlier release recorded. It does not roll back data, permissions or schema.
|
|
151
|
+
- Shiplord refuses a volume that is not a plain local volume labelled for the application. It creates the volume on
|
|
152
|
+
the first deploy (never during `plan`) and never removes it.
|
|
153
|
+
- Using `[storage]` needs a helper that advertises `docker_storage_v1`; a newer CLI refuses to submit it to an
|
|
154
|
+
older helper (re-run `shiplord server bootstrap`). A helper must not be downgraded after deploying records that
|
|
155
|
+
use `[storage]`.
|
|
156
|
+
- `owner_uid` / `owner_gid` are validated but **not yet supported**: the helper rejects them, because establishing
|
|
157
|
+
ownership safely needs a pinned initializer image that has not been built and reviewed.
|
|
158
|
+
- An image can establish the mount directory's ownership during its build. Verify a fresh volume is writable
|
|
159
|
+
by the image's application user with real Docker before deployment. Shiplord does not repair existing
|
|
160
|
+
volume permissions, and changing the mount path does not migrate data from another application.
|
|
161
|
+
|
|
162
|
+
## Graceful replacement (Docker)
|
|
163
|
+
|
|
164
|
+
Replacing a Docker release now signals the old container, waits, and only then removes it and starts the new one:
|
|
165
|
+
|
|
166
|
+
```toml
|
|
167
|
+
[deploy]
|
|
168
|
+
shutdown_timeout_seconds = 120 # 1-300; default 10
|
|
169
|
+
shutdown_timeout_action = "fail" # "force" (default) or "fail"
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
- The container's stop signal (image `STOPSIGNAL`, default SIGTERM) is sent after its restart policy is turned off.
|
|
173
|
+
Recovery may resend it if a worker stops before recording that Docker accepted it. SIGKILL/SIGSTOP are refused as
|
|
174
|
+
graceful signals.
|
|
175
|
+
- At the deadline, `force` sends an explicit SIGKILL and reports `forced: true`; `fail` kills and removes nothing, starts no
|
|
176
|
+
candidate, keeps the journal and exits 6 (`intervention_required`). Run `shiplord recover` once the process has exited;
|
|
177
|
+
recovery will not clear the journal while it is still draining. Daemon or inspection errors never trigger force.
|
|
178
|
+
- The **outgoing** container's recorded policy governs its stop (containers carry it as labels; older containers are 10 s /
|
|
179
|
+
`force`). `shiplord plan` shows both. Neither policy guarantees an application's jobs complete.
|
|
180
|
+
- Two live containers for one application are refused before anything is signalled.
|
|
181
|
+
- Retirement removes the route first; if the stop then fails, the route stays gone, registration and data are kept.
|
|
182
|
+
- An explicit policy is refused if its worst-case time (lock wait, pull, both stops, both readiness waits) exceeds the
|
|
183
|
+
3600 s job limit. Needs helper capability `docker_shutdown_v1`; do not downgrade a helper after using it.
|
|
184
|
+
- The shutdown journal persists the original deadline. Recovery uses the time remaining rather than starting a new
|
|
185
|
+
timeout period.
|
|
186
|
+
|
|
187
|
+
## Deploying from GitHub Actions
|
|
188
|
+
|
|
189
|
+
```sh
|
|
190
|
+
shiplord github setup --repo owner/orders --tooling-ref <commit of Shiplord pushed to GitHub>
|
|
191
|
+
git add .github/workflows/shiplord-release.yml ship.toml && git commit # merge to the deployment branch
|
|
192
|
+
shiplord github check-ci # joins Tailscale and logs in with the CI key; builds and deploys nothing
|
|
193
|
+
shiplord github status --check-server
|
|
194
|
+
shiplord github rotate-key
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
`github setup` uses your authenticated `gh`. It creates a dedicated Ed25519 CI key, registers it on the server,
|
|
198
|
+
**proves it authenticates over SSH against the server's pinned host keys** (read over your already-trusted
|
|
199
|
+
connection, never from `ssh-keyscan`), and only then stores it as the `SHIPLORD_SSH_PRIVATE_KEY` secret (through
|
|
200
|
+
stdin) and sets the `SHIPLORD_*` repository variables. It then writes `.github/workflows/shiplord-release.yml`:
|
|
201
|
+
the template's quality checks, an image build, and `shiplord deploy` by digest from a runner joined to your tailnet.
|
|
202
|
+
It is resumable, keeps the private key only until GitHub has it, and never uploads your personal key.
|
|
203
|
+
|
|
204
|
+
Two things stay with you because they need authority Shiplord does not have: the **Tailscale trust credential** and
|
|
205
|
+
**DNS**. For Tailscale, `github setup` guides you instead: it reads the OIDC subject GitHub will actually present (repositories
|
|
206
|
+
with immutable subjects use `repo:Owner@<id>/name@<id>`, not the plain names), prints the policy line and the
|
|
207
|
+
credential settings with every value filled in, asks for the client ID and audience (not secrets), checks they belong
|
|
208
|
+
together, and saves them so the next application's setup does not ask again. Use the owner-wide subject it suggests,
|
|
209
|
+
`repo:Owner@<id>/*:ref:refs/heads/main`, to do this once for every repository. `--tailscale-client-id` and
|
|
210
|
+
`--tailscale-audience` skip the prompt. `github check-ci` then proves the whole path with a check-only run. The generated workflow pins every action to a commit, never cancels a running
|
|
211
|
+
production deploy, and gives `id-token: write` only to the jobs that need it. `rotate-key` adds and proves a new key,
|
|
212
|
+
updates GitHub, and only then revokes the old one.
|
|
213
|
+
|
|
214
|
+
CI runs `shiplord` in pinned mode: `SHIPLORD_SSH_TARGET`, `SHIPLORD_SSH_IDENTITY`, `SHIPLORD_SSH_KNOWN_HOSTS_FILE`
|
|
215
|
+
(and optionally `SHIPLORD_SSH_PORT`) replace the server alias, and SSH ignores `~/.ssh/config` entirely.
|
|
216
|
+
|
|
217
|
+
## Commands
|
|
218
|
+
|
|
219
|
+
| Command | Purpose |
|
|
220
|
+
| --- | --- |
|
|
221
|
+
| `server add <alias> --ssh user@host` | Register a trusted SSH login. Never accepts or updates host keys. |
|
|
222
|
+
| `server list` | Show the registered aliases and their SSH targets. Local only; contacts nothing. |
|
|
223
|
+
| `server bootstrap <alias>` | Install or update the helper on the server. Idempotent. |
|
|
224
|
+
| `init`, `configure` | Create a project from the pinned template; create `ship.toml` (asks for missing values in a terminal). |
|
|
225
|
+
| `provision` | Create the application's account, state, port, and key access on the server. |
|
|
226
|
+
| `env set --from-file F` / `env keys` / `env clear` | Manage runtime environment. `set` refuses an empty file; `keys` lists names only. |
|
|
227
|
+
| `plan [--image I \| --binary B] --commit C` | Local preview, or with an artifact, what the server would do. Changes nothing. |
|
|
228
|
+
| `deploy --image I \| --binary B --commit C` | Deploy a built artifact. `--request-id` makes retries safe. |
|
|
229
|
+
| `status`, `logs --tail N` | Release state, running state, unfinished work; bounded logs. |
|
|
230
|
+
| `rollback`, `recover` | Restore the previous release; reconcile an interrupted deployment. |
|
|
231
|
+
| `registry login --username U --token-file F \| --token-stdin` / `logout` / `status` | A per-application read-only pull login for private images. There is no `--token` option. |
|
|
232
|
+
| `github setup --repo R` / `check-ci` / `status [--check-server]` / `rotate-key` | CI key, secret and variables, guided Tailscale setup, release workflow; a check-only CI run. |
|
|
233
|
+
| `doctor --local \| --server <alias> [--deep] \| (in an app directory)` | Prerequisites; helper probe (`--deep`: Caddy, sshd, Docker, disk, each application); access, helper, DNS. |
|
|
234
|
+
|
|
235
|
+
Every command that reports a result accepts `--json`. No output contains private keys, secret values, or complete
|
|
236
|
+
environments.
|
|
237
|
+
|
|
238
|
+
### Exit codes
|
|
239
|
+
|
|
240
|
+
| Code | Meaning |
|
|
241
|
+
| --- | --- |
|
|
242
|
+
| 0 | Succeeded. A deployment is verified ready. |
|
|
243
|
+
| 1 | The operation failed; read the result. Also: stale release, recovery required. |
|
|
244
|
+
| 2 | Invalid arguments, configuration, or request. |
|
|
245
|
+
| 3 | A prerequisite is missing: SSH trust, credentials, permissions, sudo. |
|
|
246
|
+
| 4 | Incompatible CLI and helper protocol versions. |
|
|
247
|
+
| 5 | The deployment failed and the previous release was restored. Still a failure. |
|
|
248
|
+
| 6 | Recovery failed or manual intervention is required; an interrupted job is also reported as 6. |
|
|
249
|
+
|
|
250
|
+
## What a deployment does
|
|
251
|
+
|
|
252
|
+
1. The helper checks the request against the application's registration (hostname, runtime, image repository)
|
|
253
|
+
and refuses anything that would change them.
|
|
254
|
+
2. **Prepare, without touching the running app:** pull the image by digest (platform and revision label
|
|
255
|
+
verified) or verify and install the uploaded binary. A failure here changes nothing.
|
|
256
|
+
3. **Journal** the transaction, replace the runtime (a brief restart window), poll the readiness path.
|
|
257
|
+
4. Apply the Caddy route only if it changed, verify the active configuration routes the hostname, then commit.
|
|
258
|
+
5. On any failure, restore the last committed release, verify it, and only then clear the journal. The command
|
|
259
|
+
still exits nonzero. A first release that fails leaves no runtime and no route.
|
|
260
|
+
6. A candidate that failed leaves nothing behind; the image or binary of a release that leaves the
|
|
261
|
+
two-release window is removed.
|
|
262
|
+
|
|
263
|
+
Releases are ordered by the commit's committer timestamp (`--sequence` overrides). An honest client's queued older
|
|
264
|
+
release cannot replace a newer one (the server refuses it); the check relies on the sequence the caller supplies, and
|
|
265
|
+
`--sequence`/`allow_older` exist, so it protects against mistakes, not against a malicious account. Use `rollback` to go
|
|
266
|
+
back deliberately. Database and persistent data are never
|
|
267
|
+
rolled back.
|
|
268
|
+
|
|
269
|
+
If a worker is killed mid-deployment the journal remains, further deployments are refused with
|
|
270
|
+
`recovery_required`, and `shiplord recover` restores the last committed release.
|
|
271
|
+
|
|
272
|
+
## Security model
|
|
273
|
+
|
|
274
|
+
- Per-application deployment accounts; authorized keys are root-owned and carry `command=...,restrict`.
|
|
275
|
+
- The dispatcher never evaluates the client's command line; sudo permits one fixed helper with no arguments.
|
|
276
|
+
- Application ports bind to loopback only; Caddy is the only public entry point.
|
|
277
|
+
- Runtime environment files are root-only; secrets never appear in argv, logs, results, or generated files.
|
|
278
|
+
- Host keys are pinned (`StrictHostKeyChecking=yes`); a changed key fails closed.
|
|
279
|
+
- Containers run with a memory limit, bounded logs, `no-new-privileges` and all capabilities dropped; native
|
|
280
|
+
services run as a separate unprivileged account with `ProtectSystem=strict` and no capabilities.
|
|
281
|
+
|
|
282
|
+
### Limits of the confinement
|
|
283
|
+
|
|
284
|
+
- A deployment account may request at most the application's registered memory ceiling (default 4096 MiB;
|
|
285
|
+
`provision --max-memory-mb`), and only the current and previous releases are kept (`retain_successful_releases` must be 2).
|
|
286
|
+
- Native services may bind only their own port (`SocketBindAllow`), but **can still connect to other loopback services**
|
|
287
|
+
on the host, including Caddy's admin API on `127.0.0.1:2019`. Docker applications share Docker's default bridge.
|
|
288
|
+
Do not run an untrusted application on a host that holds anything else you care about; for stronger isolation move
|
|
289
|
+
Caddy's admin endpoint to a unix socket.
|
|
290
|
+
- A deployment account cannot be used to run arbitrary code as root, but anyone holding its key can deploy whatever
|
|
291
|
+
that application's registration allows.
|
|
292
|
+
|
|
293
|
+
## Removing an application
|
|
294
|
+
|
|
295
|
+
`shiplord deprovision --yes` removes the account, keys, registration and state of an application that has no release
|
|
296
|
+
deployed. `--retire` first removes the route, stops the runtime and discards its releases. Application data (the data
|
|
297
|
+
directory, runtime account and Docker volume) is **never** deleted; remove it by hand when you are sure.
|
|
298
|
+
|
|
299
|
+
## Development
|
|
300
|
+
|
|
301
|
+
```sh
|
|
302
|
+
python -m pip install -e .
|
|
303
|
+
python -m unittest discover -s tests -v # unit tests; real-host suites are skipped
|
|
304
|
+
tests/linux/run.sh all # real sshd, sudo, Caddy, Docker, systemd in containers
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
`tests/linux/run.sh <unit|caddy|host|docker|e2e|systemd>` runs one suite; see the script header. These suites
|
|
308
|
+
change `/etc` and system accounts, so they refuse to run outside a disposable container or CI runner.
|
|
309
|
+
|
|
310
|
+
The server-side helper code is stdlib-only and is installed from the same source as the CLI. See
|
|
311
|
+
[docs/protocol.md](docs/protocol.md) for the wire protocol and [docs/operations.md](docs/operations.md) for the
|
|
312
|
+
host layout and troubleshooting.
|
shiplord-0.1.0/README.md
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
# Shiplord
|
|
2
|
+
|
|
3
|
+
Shiplord ships tested releases of an application to your own Linux server over SSH. A coding agent or a
|
|
4
|
+
developer creates an application from the pinned template, provisions it once, and then deploys already-built
|
|
5
|
+
artifacts with one command, from a terminal or from GitHub Actions. Nothing is built on the server, no
|
|
6
|
+
deployment daemon runs, and every release is verified, journaled, and reversible.
|
|
7
|
+
|
|
8
|
+
The design and acceptance criteria are in [ship-cli-plan.md](ship-cli-plan.md). The executable is `shiplord`
|
|
9
|
+
and the application manifest is `ship.toml`.
|
|
10
|
+
|
|
11
|
+
## Status
|
|
12
|
+
|
|
13
|
+
Built and verified (see [docs/evidence.md](docs/evidence.md) for what ran, where, and what did not):
|
|
14
|
+
|
|
15
|
+
- Application creation from the pinned template, manifest validation, server registration.
|
|
16
|
+
- Server bootstrap, application provisioning, SSH keys, runtime environment, port allocation.
|
|
17
|
+
- Docker and native-binary deployment, readiness and routing checks, rollback, recovery, status, logs, plan.
|
|
18
|
+
- Caddy route transactions with validate-before-publish and restore on failure.
|
|
19
|
+
|
|
20
|
+
Also run on a real Hetzner server (Ubuntu, x86-64, behind Cloudflare): native deploys, rollback, a failed release
|
|
21
|
+
restored automatically, Let's Encrypt HTTPS, and a release deployed by GitHub Actions through Tailscale OIDC. Not
|
|
22
|
+
verified: the plan's controlled install on *your* production server (§12E). See [docs/evidence.md](docs/evidence.md).
|
|
23
|
+
|
|
24
|
+
## Install the CLI
|
|
25
|
+
|
|
26
|
+
Python 3.11 or later and OpenSSH are required on the machine you work from.
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
python -m pip install .
|
|
30
|
+
shiplord --help
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## How it fits together
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
your machine ──ssh──▶ ship-<app>@server ──forced command──▶ dispatcher ──sudo──▶ helper (root, one app)
|
|
37
|
+
│ starts a detached job
|
|
38
|
+
administrator ──ssh + sudo──▶ shiplord-admin (bootstrap, provision, keys, env) ▼
|
|
39
|
+
worker: journal → runtime → ready → route → commit
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
- **Administrator operations** (`server bootstrap`, `provision`, `env`) use your own SSH login and your own sudo
|
|
43
|
+
(you are asked for the sudo password if one is needed; it is sent on stdin, never in a command line).
|
|
44
|
+
- **Application operations** (`deploy`, `status`, `logs`, `rollback`, `recover`, `plan`) use a per-application
|
|
45
|
+
account whose key can run exactly one forced command. The root helper binds every request to that account's
|
|
46
|
+
registration, so an application's deployment account can operate only that application. (The running
|
|
47
|
+
application itself is only confined as described under *Security model*.)
|
|
48
|
+
- A deployment is a **job** that outlives your SSH session. A transient systemd unit runs it; you can disconnect
|
|
49
|
+
and read the result later.
|
|
50
|
+
|
|
51
|
+
## Quick start
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
# once per machine: register a server you can already SSH into with a trusted host key
|
|
55
|
+
shiplord server add production --ssh appuser@server.example.com
|
|
56
|
+
|
|
57
|
+
# once per server: installs the helper, wires Caddy and sshd (re-running changes nothing)
|
|
58
|
+
shiplord server bootstrap production
|
|
59
|
+
|
|
60
|
+
# once per application
|
|
61
|
+
shiplord init orders --template google-adk
|
|
62
|
+
cd orders
|
|
63
|
+
shiplord configure --server production --api-domain orders-api.example.com \
|
|
64
|
+
--frontend https://orders.example.com # or --no-frontend
|
|
65
|
+
shiplord provision # registers your SSH key for this application
|
|
66
|
+
shiplord env set --from-file .env.production # runtime secrets; values are never printed
|
|
67
|
+
shiplord registry login --username bot --token-file t # only for a private image: a read-only pull login
|
|
68
|
+
|
|
69
|
+
# every release
|
|
70
|
+
shiplord plan --image ghcr.io/owner/orders@sha256:<digest> --commit <sha> # read-only
|
|
71
|
+
shiplord deploy --image ghcr.io/owner/orders@sha256:<digest> --commit <sha>
|
|
72
|
+
shiplord status
|
|
73
|
+
shiplord logs --tail 100
|
|
74
|
+
shiplord rollback
|
|
75
|
+
shiplord recover
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Create the DNS record for `--api-domain` at your DNS provider; Shiplord checks DNS and reports it but never
|
|
79
|
+
changes it.
|
|
80
|
+
|
|
81
|
+
For a native Linux binary, configure with `--runtime native` and deploy with
|
|
82
|
+
`shiplord deploy --binary ./dist/orders --commit <sha>`. Build it for the server's architecture (a Linux ELF;
|
|
83
|
+
x86-64 and arm64 are supported), for example in CI. The binary is uploaded over the same SSH connection,
|
|
84
|
+
verified by size, SHA-256 and ELF header on the server, and run as an unprivileged account under a confined
|
|
85
|
+
systemd unit.
|
|
86
|
+
|
|
87
|
+
## Existing Docker applications
|
|
88
|
+
|
|
89
|
+
Use the generic profile to deploy an existing image without Google ADK settings. In a terminal, run
|
|
90
|
+
`shiplord configure` with no options in the repository. It lists your registered servers and asks only for what is
|
|
91
|
+
missing. When the repository has a `Dockerfile` (or `Containerfile`), it suggests `generic` and the port from the
|
|
92
|
+
final stage's `EXPOSE`. You confirm a summary before anything is written. Without a terminal (agents, CI, pipes)
|
|
93
|
+
nothing is asked, and every value comes from options:
|
|
94
|
+
|
|
95
|
+
```sh
|
|
96
|
+
shiplord configure --name handwriting --server production \
|
|
97
|
+
--api-domain handwriting.example.com --no-frontend --template generic \
|
|
98
|
+
--container-port 3000 --health-path /health --memory 512
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
This creates `template = "generic"` and `runtime = "docker"`. Without those options the defaults remain 8080,
|
|
102
|
+
`/ready`, and 512 MiB, and the repository is not inspected. Shiplord picks the server-side port and routes the
|
|
103
|
+
hostname to it, so `--container-port` is only the port your application listens on inside its container. The
|
|
104
|
+
image must start its own HTTP service, listen on `0.0.0.0` at `http.container_port` (Shiplord also sets `PORT` to
|
|
105
|
+
that value), and return HTTP 200 at `http.readiness_path`.
|
|
106
|
+
An image can serve both its website and API at the configured hostname.
|
|
107
|
+
|
|
108
|
+
Both profiles receive `HOST`, `PORT`, and `LOG_LEVEL`. Only `google-adk` receives Shiplord's `AGENT_DIR`,
|
|
109
|
+
`AGENT_NAME`, `RELOAD_AGENTS`, `SERVE_WEB_INTERFACE`, and `ALLOW_ORIGINS` settings. Generic images keep
|
|
110
|
+
their own defaults for those five variables and must implement their own CORS behavior when needed.
|
|
111
|
+
All managed variable names remain reserved in `shiplord env set`, including ADK names, because environment
|
|
112
|
+
files belong to the application and can survive a profile change.
|
|
113
|
+
|
|
114
|
+
`configure` defaults to `google-adk` for compatibility. `init` still scaffolds only `google-adk`, and native
|
|
115
|
+
binaries still require that profile. `github setup` generates the ADK template's workflow and refuses generic
|
|
116
|
+
applications before creating keys or changing GitHub; use a custom workflow that builds your image, runs
|
|
117
|
+
your application's checks, and invokes `shiplord deploy` by digest.
|
|
118
|
+
|
|
119
|
+
The CLI requires the helper capability `docker_generic_v1` before submitting a generic plan or deploy.
|
|
120
|
+
Upgrade with `shiplord server bootstrap` when it is missing. Do not downgrade to a helper that cannot read
|
|
121
|
+
generic release records. Changing the profile replaces the container even with the same image, commit,
|
|
122
|
+
and sequence; rollback and journal recovery restore the recorded profile. Containers without a profile
|
|
123
|
+
label are interpreted as legacy `google-adk` containers.
|
|
124
|
+
|
|
125
|
+
## Persistent storage (Docker)
|
|
126
|
+
|
|
127
|
+
Each Docker application has one named volume, `shiplord-<application>-artifacts`, mounted at `/app/src/.adk` by
|
|
128
|
+
default. To mount it elsewhere, add a table to `ship.toml`:
|
|
129
|
+
|
|
130
|
+
```toml
|
|
131
|
+
[storage]
|
|
132
|
+
container_path = "/data"
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
The path must be a canonical absolute directory path (letters, digits, `.`, `_`, `-`; no `..`, trailing or repeated
|
|
136
|
+
slashes), and may not be, or be inside, `/proc`, `/sys`, `/dev`, `/run`, `/etc`, `/bin`, `/sbin`, `/usr`, `/lib`,
|
|
137
|
+
`/lib64`, `/boot`, `/root`, `/var/run` or `/var/lock`. Native applications reject `[storage]`.
|
|
138
|
+
|
|
139
|
+
- Changing the path mounts the **same** volume at the new location. No files move and nothing is migrated:
|
|
140
|
+
relative file names, including SQLite databases, stay where they were. `shiplord plan` shows the old and new path.
|
|
141
|
+
- A mount can hide files the image keeps at that path; check the destination against your image.
|
|
142
|
+
- Rollback restores the path the earlier release recorded. It does not roll back data, permissions or schema.
|
|
143
|
+
- Shiplord refuses a volume that is not a plain local volume labelled for the application. It creates the volume on
|
|
144
|
+
the first deploy (never during `plan`) and never removes it.
|
|
145
|
+
- Using `[storage]` needs a helper that advertises `docker_storage_v1`; a newer CLI refuses to submit it to an
|
|
146
|
+
older helper (re-run `shiplord server bootstrap`). A helper must not be downgraded after deploying records that
|
|
147
|
+
use `[storage]`.
|
|
148
|
+
- `owner_uid` / `owner_gid` are validated but **not yet supported**: the helper rejects them, because establishing
|
|
149
|
+
ownership safely needs a pinned initializer image that has not been built and reviewed.
|
|
150
|
+
- An image can establish the mount directory's ownership during its build. Verify a fresh volume is writable
|
|
151
|
+
by the image's application user with real Docker before deployment. Shiplord does not repair existing
|
|
152
|
+
volume permissions, and changing the mount path does not migrate data from another application.
|
|
153
|
+
|
|
154
|
+
## Graceful replacement (Docker)
|
|
155
|
+
|
|
156
|
+
Replacing a Docker release now signals the old container, waits, and only then removes it and starts the new one:
|
|
157
|
+
|
|
158
|
+
```toml
|
|
159
|
+
[deploy]
|
|
160
|
+
shutdown_timeout_seconds = 120 # 1-300; default 10
|
|
161
|
+
shutdown_timeout_action = "fail" # "force" (default) or "fail"
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
- The container's stop signal (image `STOPSIGNAL`, default SIGTERM) is sent after its restart policy is turned off.
|
|
165
|
+
Recovery may resend it if a worker stops before recording that Docker accepted it. SIGKILL/SIGSTOP are refused as
|
|
166
|
+
graceful signals.
|
|
167
|
+
- At the deadline, `force` sends an explicit SIGKILL and reports `forced: true`; `fail` kills and removes nothing, starts no
|
|
168
|
+
candidate, keeps the journal and exits 6 (`intervention_required`). Run `shiplord recover` once the process has exited;
|
|
169
|
+
recovery will not clear the journal while it is still draining. Daemon or inspection errors never trigger force.
|
|
170
|
+
- The **outgoing** container's recorded policy governs its stop (containers carry it as labels; older containers are 10 s /
|
|
171
|
+
`force`). `shiplord plan` shows both. Neither policy guarantees an application's jobs complete.
|
|
172
|
+
- Two live containers for one application are refused before anything is signalled.
|
|
173
|
+
- Retirement removes the route first; if the stop then fails, the route stays gone, registration and data are kept.
|
|
174
|
+
- An explicit policy is refused if its worst-case time (lock wait, pull, both stops, both readiness waits) exceeds the
|
|
175
|
+
3600 s job limit. Needs helper capability `docker_shutdown_v1`; do not downgrade a helper after using it.
|
|
176
|
+
- The shutdown journal persists the original deadline. Recovery uses the time remaining rather than starting a new
|
|
177
|
+
timeout period.
|
|
178
|
+
|
|
179
|
+
## Deploying from GitHub Actions
|
|
180
|
+
|
|
181
|
+
```sh
|
|
182
|
+
shiplord github setup --repo owner/orders --tooling-ref <commit of Shiplord pushed to GitHub>
|
|
183
|
+
git add .github/workflows/shiplord-release.yml ship.toml && git commit # merge to the deployment branch
|
|
184
|
+
shiplord github check-ci # joins Tailscale and logs in with the CI key; builds and deploys nothing
|
|
185
|
+
shiplord github status --check-server
|
|
186
|
+
shiplord github rotate-key
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
`github setup` uses your authenticated `gh`. It creates a dedicated Ed25519 CI key, registers it on the server,
|
|
190
|
+
**proves it authenticates over SSH against the server's pinned host keys** (read over your already-trusted
|
|
191
|
+
connection, never from `ssh-keyscan`), and only then stores it as the `SHIPLORD_SSH_PRIVATE_KEY` secret (through
|
|
192
|
+
stdin) and sets the `SHIPLORD_*` repository variables. It then writes `.github/workflows/shiplord-release.yml`:
|
|
193
|
+
the template's quality checks, an image build, and `shiplord deploy` by digest from a runner joined to your tailnet.
|
|
194
|
+
It is resumable, keeps the private key only until GitHub has it, and never uploads your personal key.
|
|
195
|
+
|
|
196
|
+
Two things stay with you because they need authority Shiplord does not have: the **Tailscale trust credential** and
|
|
197
|
+
**DNS**. For Tailscale, `github setup` guides you instead: it reads the OIDC subject GitHub will actually present (repositories
|
|
198
|
+
with immutable subjects use `repo:Owner@<id>/name@<id>`, not the plain names), prints the policy line and the
|
|
199
|
+
credential settings with every value filled in, asks for the client ID and audience (not secrets), checks they belong
|
|
200
|
+
together, and saves them so the next application's setup does not ask again. Use the owner-wide subject it suggests,
|
|
201
|
+
`repo:Owner@<id>/*:ref:refs/heads/main`, to do this once for every repository. `--tailscale-client-id` and
|
|
202
|
+
`--tailscale-audience` skip the prompt. `github check-ci` then proves the whole path with a check-only run. The generated workflow pins every action to a commit, never cancels a running
|
|
203
|
+
production deploy, and gives `id-token: write` only to the jobs that need it. `rotate-key` adds and proves a new key,
|
|
204
|
+
updates GitHub, and only then revokes the old one.
|
|
205
|
+
|
|
206
|
+
CI runs `shiplord` in pinned mode: `SHIPLORD_SSH_TARGET`, `SHIPLORD_SSH_IDENTITY`, `SHIPLORD_SSH_KNOWN_HOSTS_FILE`
|
|
207
|
+
(and optionally `SHIPLORD_SSH_PORT`) replace the server alias, and SSH ignores `~/.ssh/config` entirely.
|
|
208
|
+
|
|
209
|
+
## Commands
|
|
210
|
+
|
|
211
|
+
| Command | Purpose |
|
|
212
|
+
| --- | --- |
|
|
213
|
+
| `server add <alias> --ssh user@host` | Register a trusted SSH login. Never accepts or updates host keys. |
|
|
214
|
+
| `server list` | Show the registered aliases and their SSH targets. Local only; contacts nothing. |
|
|
215
|
+
| `server bootstrap <alias>` | Install or update the helper on the server. Idempotent. |
|
|
216
|
+
| `init`, `configure` | Create a project from the pinned template; create `ship.toml` (asks for missing values in a terminal). |
|
|
217
|
+
| `provision` | Create the application's account, state, port, and key access on the server. |
|
|
218
|
+
| `env set --from-file F` / `env keys` / `env clear` | Manage runtime environment. `set` refuses an empty file; `keys` lists names only. |
|
|
219
|
+
| `plan [--image I \| --binary B] --commit C` | Local preview, or with an artifact, what the server would do. Changes nothing. |
|
|
220
|
+
| `deploy --image I \| --binary B --commit C` | Deploy a built artifact. `--request-id` makes retries safe. |
|
|
221
|
+
| `status`, `logs --tail N` | Release state, running state, unfinished work; bounded logs. |
|
|
222
|
+
| `rollback`, `recover` | Restore the previous release; reconcile an interrupted deployment. |
|
|
223
|
+
| `registry login --username U --token-file F \| --token-stdin` / `logout` / `status` | A per-application read-only pull login for private images. There is no `--token` option. |
|
|
224
|
+
| `github setup --repo R` / `check-ci` / `status [--check-server]` / `rotate-key` | CI key, secret and variables, guided Tailscale setup, release workflow; a check-only CI run. |
|
|
225
|
+
| `doctor --local \| --server <alias> [--deep] \| (in an app directory)` | Prerequisites; helper probe (`--deep`: Caddy, sshd, Docker, disk, each application); access, helper, DNS. |
|
|
226
|
+
|
|
227
|
+
Every command that reports a result accepts `--json`. No output contains private keys, secret values, or complete
|
|
228
|
+
environments.
|
|
229
|
+
|
|
230
|
+
### Exit codes
|
|
231
|
+
|
|
232
|
+
| Code | Meaning |
|
|
233
|
+
| --- | --- |
|
|
234
|
+
| 0 | Succeeded. A deployment is verified ready. |
|
|
235
|
+
| 1 | The operation failed; read the result. Also: stale release, recovery required. |
|
|
236
|
+
| 2 | Invalid arguments, configuration, or request. |
|
|
237
|
+
| 3 | A prerequisite is missing: SSH trust, credentials, permissions, sudo. |
|
|
238
|
+
| 4 | Incompatible CLI and helper protocol versions. |
|
|
239
|
+
| 5 | The deployment failed and the previous release was restored. Still a failure. |
|
|
240
|
+
| 6 | Recovery failed or manual intervention is required; an interrupted job is also reported as 6. |
|
|
241
|
+
|
|
242
|
+
## What a deployment does
|
|
243
|
+
|
|
244
|
+
1. The helper checks the request against the application's registration (hostname, runtime, image repository)
|
|
245
|
+
and refuses anything that would change them.
|
|
246
|
+
2. **Prepare, without touching the running app:** pull the image by digest (platform and revision label
|
|
247
|
+
verified) or verify and install the uploaded binary. A failure here changes nothing.
|
|
248
|
+
3. **Journal** the transaction, replace the runtime (a brief restart window), poll the readiness path.
|
|
249
|
+
4. Apply the Caddy route only if it changed, verify the active configuration routes the hostname, then commit.
|
|
250
|
+
5. On any failure, restore the last committed release, verify it, and only then clear the journal. The command
|
|
251
|
+
still exits nonzero. A first release that fails leaves no runtime and no route.
|
|
252
|
+
6. A candidate that failed leaves nothing behind; the image or binary of a release that leaves the
|
|
253
|
+
two-release window is removed.
|
|
254
|
+
|
|
255
|
+
Releases are ordered by the commit's committer timestamp (`--sequence` overrides). An honest client's queued older
|
|
256
|
+
release cannot replace a newer one (the server refuses it); the check relies on the sequence the caller supplies, and
|
|
257
|
+
`--sequence`/`allow_older` exist, so it protects against mistakes, not against a malicious account. Use `rollback` to go
|
|
258
|
+
back deliberately. Database and persistent data are never
|
|
259
|
+
rolled back.
|
|
260
|
+
|
|
261
|
+
If a worker is killed mid-deployment the journal remains, further deployments are refused with
|
|
262
|
+
`recovery_required`, and `shiplord recover` restores the last committed release.
|
|
263
|
+
|
|
264
|
+
## Security model
|
|
265
|
+
|
|
266
|
+
- Per-application deployment accounts; authorized keys are root-owned and carry `command=...,restrict`.
|
|
267
|
+
- The dispatcher never evaluates the client's command line; sudo permits one fixed helper with no arguments.
|
|
268
|
+
- Application ports bind to loopback only; Caddy is the only public entry point.
|
|
269
|
+
- Runtime environment files are root-only; secrets never appear in argv, logs, results, or generated files.
|
|
270
|
+
- Host keys are pinned (`StrictHostKeyChecking=yes`); a changed key fails closed.
|
|
271
|
+
- Containers run with a memory limit, bounded logs, `no-new-privileges` and all capabilities dropped; native
|
|
272
|
+
services run as a separate unprivileged account with `ProtectSystem=strict` and no capabilities.
|
|
273
|
+
|
|
274
|
+
### Limits of the confinement
|
|
275
|
+
|
|
276
|
+
- A deployment account may request at most the application's registered memory ceiling (default 4096 MiB;
|
|
277
|
+
`provision --max-memory-mb`), and only the current and previous releases are kept (`retain_successful_releases` must be 2).
|
|
278
|
+
- Native services may bind only their own port (`SocketBindAllow`), but **can still connect to other loopback services**
|
|
279
|
+
on the host, including Caddy's admin API on `127.0.0.1:2019`. Docker applications share Docker's default bridge.
|
|
280
|
+
Do not run an untrusted application on a host that holds anything else you care about; for stronger isolation move
|
|
281
|
+
Caddy's admin endpoint to a unix socket.
|
|
282
|
+
- A deployment account cannot be used to run arbitrary code as root, but anyone holding its key can deploy whatever
|
|
283
|
+
that application's registration allows.
|
|
284
|
+
|
|
285
|
+
## Removing an application
|
|
286
|
+
|
|
287
|
+
`shiplord deprovision --yes` removes the account, keys, registration and state of an application that has no release
|
|
288
|
+
deployed. `--retire` first removes the route, stops the runtime and discards its releases. Application data (the data
|
|
289
|
+
directory, runtime account and Docker volume) is **never** deleted; remove it by hand when you are sure.
|
|
290
|
+
|
|
291
|
+
## Development
|
|
292
|
+
|
|
293
|
+
```sh
|
|
294
|
+
python -m pip install -e .
|
|
295
|
+
python -m unittest discover -s tests -v # unit tests; real-host suites are skipped
|
|
296
|
+
tests/linux/run.sh all # real sshd, sudo, Caddy, Docker, systemd in containers
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
`tests/linux/run.sh <unit|caddy|host|docker|e2e|systemd>` runs one suite; see the script header. These suites
|
|
300
|
+
change `/etc` and system accounts, so they refuse to run outside a disposable container or CI runner.
|
|
301
|
+
|
|
302
|
+
The server-side helper code is stdlib-only and is installed from the same source as the CLI. See
|
|
303
|
+
[docs/protocol.md](docs/protocol.md) for the wire protocol and [docs/operations.md](docs/operations.md) for the
|
|
304
|
+
host layout and troubleshooting.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "shiplord"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Deployment tooling for applications on a server"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
dependencies = ["typer>=0.12,<1"]
|
|
12
|
+
|
|
13
|
+
[project.scripts]
|
|
14
|
+
shiplord = "shiplord.cli:main"
|
|
15
|
+
|
|
16
|
+
[tool.setuptools.packages.find]
|
|
17
|
+
where = ["src"]
|
shiplord-0.1.0/setup.cfg
ADDED