odysseus-core 0.3.2 → 0.10.0

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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +508 -1
  3. data/LICENSE.txt +25 -7
  4. data/README.md +12 -3
  5. data/lib/odysseus/builder/client.rb +15 -15
  6. data/lib/odysseus/caddy/client.rb +174 -45
  7. data/lib/odysseus/command_redaction.rb +38 -0
  8. data/lib/odysseus/config/parser.rb +43 -18
  9. data/lib/odysseus/core/deploy_versioning.rb +42 -0
  10. data/lib/odysseus/core/environment.rb +63 -0
  11. data/lib/odysseus/core/version.rb +1 -1
  12. data/lib/odysseus/core/volume_namespacer.rb +0 -0
  13. data/lib/odysseus/core.rb +2 -2
  14. data/lib/odysseus/deploy_log.rb +120 -0
  15. data/lib/odysseus/deploy_version.rb +9 -0
  16. data/lib/odysseus/deployer/dependency_manager.rb +121 -0
  17. data/lib/odysseus/deployer/executor.rb +213 -161
  18. data/lib/odysseus/deployer/retention_sweeper.rb +94 -0
  19. data/lib/odysseus/deployer/ssh.rb +92 -18
  20. data/lib/odysseus/docker/client.rb +240 -48
  21. data/lib/odysseus/docker/labels.rb +43 -0
  22. data/lib/odysseus/errors.rb +10 -0
  23. data/lib/odysseus/git.rb +70 -0
  24. data/lib/odysseus/host_paths.rb +93 -0
  25. data/lib/odysseus/host_providers/base.rb +1 -1
  26. data/lib/odysseus/host_providers/static.rb +1 -1
  27. data/lib/odysseus/host_providers.rb +3 -4
  28. data/lib/odysseus/host_verifier.rb +156 -0
  29. data/lib/odysseus/host_versions.rb +59 -0
  30. data/lib/odysseus/orchestrator/{accessory_deploy.rb → dependency_deploy.rb} +84 -88
  31. data/lib/odysseus/orchestrator/job_deploy.rb +31 -43
  32. data/lib/odysseus/orchestrator/web_deploy.rb +61 -55
  33. data/lib/odysseus/plugins.rb +72 -0
  34. data/lib/odysseus/retention_plan.rb +14 -0
  35. data/lib/odysseus/retention_planner.rb +58 -0
  36. data/lib/odysseus/rollback_plan.rb +20 -0
  37. data/lib/odysseus/rollback_planner.rb +107 -0
  38. data/lib/odysseus/sails.rb +0 -0
  39. data/lib/odysseus/secrets/encrypted_file.rb +5 -7
  40. data/lib/odysseus/secrets/loader.rb +1 -3
  41. data/lib/odysseus/setup/docker_apt.rb +178 -0
  42. data/lib/odysseus/setup/escalation.rb +73 -0
  43. data/lib/odysseus/setup/preparer.rb +372 -0
  44. data/lib/odysseus/setup/public_key.rb +127 -0
  45. data/lib/odysseus/validators/config.rb +38 -25
  46. data/lib/odysseus/version_resolver.rb +80 -0
  47. data/lib/odysseus.rb +0 -0
  48. data/sig/odysseus/core.rbs +0 -0
  49. metadata +48 -27
  50. data/Rakefile +0 -12
  51. data/lib/odysseus/version.rb +0 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e3fd4c3bfd44f78e7cdb0cbcf455d365b23e7ccd18fb9497a6f3cdd427f25b9f
4
- data.tar.gz: 2531a37231560cfe10c0c7a9f3d7e3feed5080ad932e7400953670e0dbe8029d
3
+ metadata.gz: f6d1b931d263332fd0a431b4a0842949f52d78f2f366f8dbf72cb98f0ea95282
4
+ data.tar.gz: f3ea3a6477a88cbd38b1386f04e983bad5d7d782475f46322989568389e4fa5b
5
5
  SHA512:
6
- metadata.gz: e7e90c0115224ab15e0d73d22504bb6f9262f9efdedbb6f0c8766a7e42008ebb3798ebab9eae3237fde1ef6221cecab997ce00181982fe7744fca857219093bc
7
- data.tar.gz: 18f19cbf04921ea7bc858f0911155e0e02156c22d54c58ce641e0d3fdd3283d5dd7bf80c7fdde887f82f93d5dfede7183045f5864b2e06335da337b940ad1507
6
+ metadata.gz: b89f611e20800ecfbdbd30fe571b6a9979877a59f52655a0c05fb3aeadc7567f6af4325711c92eb02698bbdd23c1746f6c1d150436e20dc45318a4f0fae0f1b4
7
+ data.tar.gz: 6db503709fb510a38980f0357a69f82f07b7b04e77b8b63c7286035f88425134039c06618164648876324a4a22f902341d4ca41fa4bb5a027a5addc07a4a7309
data/CHANGELOG.md CHANGED
@@ -1,5 +1,512 @@
1
+ # Changelog
2
+
3
+ All notable changes to odysseus-core are documented here.
4
+
5
+ Entries for 0.3.1 and earlier were reconstructed from git history and the built
6
+ gem artifacts, so they are summaries rather than contemporaneous notes.
7
+
1
8
  ## [Unreleased]
2
9
 
10
+ ## [0.10.0] - 2026-08-28
11
+
12
+ ### Fixed
13
+
14
+ - Caddy no longer loses every route when its container is replaced or the host
15
+ reboots. Routes are added through the admin API and live in Caddy's memory;
16
+ Caddy autosaves them to `$XDG_CONFIG_HOME/caddy/autosave.json`, which the
17
+ official image sets to `/config` -- and odysseus mounted only `/data`
18
+ (`XDG_DATA_HOME`), so certificates persisted and routes were discarded with
19
+ the container. On a host with nine services, all nine stayed unreachable until
20
+ nine separate deploys ran. `HostPaths#caddy_config_dir` is now mounted at
21
+ `/config` and Caddy starts with `--resume`.
22
+
23
+ If `--resume` fails to start the container, odysseus renames the autosave to
24
+ `autosave.json.rejected` and starts once without it. `CADDY_IMAGE` is the
25
+ moving `caddy:2-alpine` tag, so a newer Caddy can meet an autosave it will not
26
+ parse, and `WebDeploy` aborts when Caddy does not come up -- unhandled, one
27
+ bad autosave would have failed every deploy on the host. The rename runs
28
+ inside a container: Caddy creates `/config/caddy` as root with mode 0700, so a
29
+ non-root deploy user cannot move the file from the host.
30
+
31
+ A host that has never run Caddy still has no routes until each service
32
+ deploys; `--resume` falls back to the image's Caddyfile there, which is what
33
+ creates `srv0`.
34
+
35
+ - Commands echoed under `--debug`/`-v` no longer print secrets. `SSH#execute`
36
+ echoed the command verbatim, and registry login is built as
37
+ `echo '<password>' | docker login <server> -u <user> --password-stdin` --
38
+ `--password-stdin` keeps the password off the argv, and interpolating it into
39
+ `echo` put it straight back into the string being echoed. With no `-p` and no
40
+ `--password`, every pattern written to catch those missed it. New
41
+ `Odysseus::CommandRedaction` runs on the way to the terminal only; the command
42
+ executed is unchanged.
43
+
44
+ ## [0.9.0] - 2026-08-19
45
+
46
+ ### Fixed
47
+
48
+ - `Setup::Escalation`'s sudo probe no longer reports an unreachable host as a
49
+ sudo problem. It rescued `Odysseus::Error`, the shared ancestor of
50
+ `SSHCommandError` and `SSHConnectionError`, and the probe is the first
51
+ command setup sends — so a host whose name did not resolve was told to
52
+ configure passwordless sudo. It now catches only the error that means the
53
+ command ran and sudo refused.
54
+ - `SSHAuthenticationError` is a new class under `SSHError`, raised where
55
+ `SSHConnectionError` was raised for `Net::SSH::AuthenticationFailed`. "The
56
+ host refused this identity" and "the host was never reached" were the same
57
+ class, so no caller could advise on one without guessing about the other.
58
+ Nothing rescued `SSHConnectionError` specifically, so no other behaviour
59
+ changes.
60
+ - `SSH#with_connection` no longer describes a connection that never opened as
61
+ a mid-command drop. `IOError`, `Net::SSH::Disconnect`, `ECONNRESET` and
62
+ `EPIPE` arrive both while opening and while a command runs, and the rescue
63
+ wrapped both, so a host that accepted the connection and hung up before any
64
+ command existed — one still booting, or an sshd not yet up — was reported as
65
+ having dropped a command that had never been sent.
66
+ - `Docker::Client` escapes the healthcheck command instead of wrapping it in
67
+ hand-written single quotes. A cmd containing a quote, such as
68
+ `--execute='SELECT 1'`, closed the quote early and word-split into docker's
69
+ arguments, so the tail landed where the image name goes and docker reported
70
+ it could not find image `'1:latest'`. Volume specs are escaped for the same
71
+ reason. A container's `cmd` stays unescaped by design — it has to reach the
72
+ container as separate arguments.
73
+
74
+ ### Added
75
+ - `Odysseus::Setup::Escalation`, `Odysseus::Setup::PublicKey`,
76
+ `Odysseus::Setup::DockerApt` and `Odysseus::Setup::Preparer`, the classes
77
+ behind `odysseus setup` (odysseus-cli). `Escalation` gets root on the
78
+ target host via passwordless sudo, or none at all when connected as root,
79
+ and refuses up front — before anything is touched — if passwordless sudo
80
+ isn't available, since a password prompt can't be answered. `PublicKey`
81
+ resolves which key(s) to install, entirely on the local machine before
82
+ any host is touched: from `--key`, or from each `ssh.keys` entry's `.pub`
83
+ sibling, deriving one via `ssh-keygen -y` only when that sibling is empty
84
+ or missing; a key that doesn't resolve, or a sibling with content that
85
+ doesn't validate, refuses by name rather than silently substituting a
86
+ different key. `DockerApt` installs Docker from Docker's own official
87
+ apt repository, following Docker's published instructions for Ubuntu: it
88
+ installs `docker-ce docker-ce-cli containerd.io docker-buildx-plugin`
89
+ (Docker's instructions also install `docker-compose-plugin`, which is
90
+ deliberately left out — nothing in this codebase invokes `docker
91
+ compose`). The keyring and apt sources file are written whole on every
92
+ run, never appended, so an interrupted run leaves a stale file the next
93
+ run replaces rather than a corrupt one. apt runs non-interactively with a
94
+ 300-second bounded wait for the dpkg lock, long enough to outlast
95
+ cloud-init and unattended-upgrades on a minutes-old host; a timeout names
96
+ the process holding the lock. It repairs no apt or dpkg state it did not
97
+ create. The GPG key's fingerprint is deliberately not pinned, matching
98
+ Docker's own instructions, which trust TLS rather than pin it — pinning
99
+ would turn Docker's key rotation into an outage for everyone using this
100
+ command. `Preparer` runs the actual sequence against a host: installs
101
+ Docker via `DockerApt` if the host doesn't already have it, creates the
102
+ deploy user and its home, adds it to the `docker` group, installs the
103
+ resolved key(s), creates the state directory, and finishes by opening a
104
+ second connection as that new user to prove Docker and the state
105
+ directory both work before reporting success. A host whose Docker daemon
106
+ still does not answer after the install is failed, naming that the host
107
+ may need a reboot or that the daemon may have failed to start — success
108
+ is decided by the daemon answering after the install runs, not by apt
109
+ exiting zero.
110
+
111
+ ## [0.8.0] - 2026-08-17
112
+
113
+ ### Fixed
114
+ - `VersionResolver#resolve` now names the deployer for an explicitly-tagged
115
+ deploy (`odysseus deploy --image v1.2.3`), instead of recording `nil`. The
116
+ deployer comes from `git config user.email`, falling back to `$USER` — a
117
+ lookup that needs no commit and no clean working tree, so withholding it
118
+ alongside `ref` was never justified. Every explicitly-tagged deploy wrote a
119
+ host-side log line with no name on it, which emptied the "by whom" column
120
+ of `odysseus rollback --list` and left the host-side deploy history
121
+ unattributed. `ref` still stays `nil` for an explicit tag: an arbitrary tag
122
+ names no commit it honestly identifies, so there is nothing true to record
123
+ there.
124
+ - `Caddy::Client#start_caddy` now publishes the admin API on
125
+ `127.0.0.1:2019` instead of `2019:2019`, which bound it to every
126
+ interface. That API can rewrite the proxy configuration — routes,
127
+ upstreams, TLS — for every service on the host, so anyone who could reach
128
+ the port controlled the proxy. `Caddy::Client` only ever calls it via
129
+ `curl localhost` over SSH, so nothing needed the external exposure. The
130
+ in-container `CADDY_ADMIN` bind stays `0.0.0.0:2019`, which is what the
131
+ published port maps to — only where the port lands on the host changed.
132
+ **This does not close the exposure on a host where Caddy is already
133
+ running**: `ensure_running` returns early when the container is up, so
134
+ the new binding only takes effect the next time Caddy is recreated
135
+ (stopped and restarted, or removed). An already-running Caddy stays bound
136
+ to every interface until that happens.
137
+ - `JobDeploy` now ensures the `odysseus` Docker network exists before
138
+ starting a container, instead of assuming it. It ran every container with
139
+ `network: 'odysseus'` but never created that network itself, so a jobs-only
140
+ service deployed to a fresh host failed at `docker run`. The gap was
141
+ invisible on any host that had ever deployed a web role or a dependency
142
+ first: a web role's `ensure_caddy!` creates the network as a side effect of
143
+ starting Caddy, and `DependencyDeploy` has always had its own
144
+ `ensure_network!`. Found on a genuinely fresh host with the deploy user
145
+ ready but no prior deploy of either kind.
146
+ - `Caddy::Client#ensure_running` now recreates a stopped Caddy container
147
+ instead of trying to `docker run` a new one over it. Once Caddy had been
148
+ stopped, `--name odysseus-caddy` collided with the container Docker still
149
+ had by that name, and `docker run` refused every time after: every
150
+ subsequent deploy failed and the proxy stayed down until someone removed
151
+ the container by hand. Absent and already-running containers are
152
+ unaffected. The stopped container is removed rather than `docker start`ed
153
+ so it always picks up current configuration — notably the deploy-user
154
+ Caddy directory added earlier in this file — instead of resuming with
155
+ whatever it was created with.
156
+
157
+ ### Added
158
+ - `Odysseus::HostPaths`, which decides where odysseus keeps state on a host
159
+ from the connecting SSH user: `root` still gets `/var/lib/odysseus`, exactly
160
+ as before and with no extra SSH round trip; any other user gets
161
+ `$HOME/.odysseus`, resolved by asking the host once per connection.
162
+ - `Odysseus::HostVerifier`, a read-only diagnosis of one host, run as the
163
+ user the config names rather than root. `#verify` runs distro, docker
164
+ reachability, docker group membership, state-directory writability and
165
+ deploy-log location, and returns one `Result` per check with a `:ok`,
166
+ `:warn` or `:fail` status — it writes nothing to the host. `odysseus
167
+ doctor` (odysseus-cli) drives it over every host in a config.
168
+
169
+ ### Changed
170
+ - Env files (`Docker::Client`) and the deploy log (`DeployLog`) now follow
171
+ `HostPaths` instead of a fixed `/var/lib/odysseus` constant. Before this,
172
+ a non-root deploy could not work at all: `write_env_file` chmods its
173
+ directory before writing into it, and a non-owner cannot chmod a directory
174
+ root created, so the deploy died at the first container start. This is the
175
+ change that makes a non-root deploy possible, on a host you have already
176
+ configured for it.
177
+ - Caddy's certificate directory (`Caddy::Client`) now follows `HostPaths`
178
+ too, instead of staying fixed at `/var/lib/odysseus/caddy`. It was
179
+ deliberately left out of the earlier change in this release to avoid
180
+ moving issued Let's Encrypt certificates or re-issuing against rate
181
+ limits — but that risk only ever applied to root installs, which are the
182
+ only ones with certificates at the old path. Deriving the directory
183
+ protects them identically: root still resolves to
184
+ `/var/lib/odysseus/caddy`, byte-identical. Leaving it fixed meant a
185
+ non-root deploy user could never create it, so `ensure_running` failed at
186
+ `mkdir -p /var/lib/odysseus/caddy` on every non-root web deploy — found on
187
+ a real host. Root is unaffected.
188
+ - `rollback --list` keeps its history across the move: a host that deployed
189
+ as root and later switches to a deploy user still has its old log read as
190
+ a fallback when the new location is absent (or unreadable) rather than
191
+ empty — an empty file at the new location exits 0 and suppresses the
192
+ fallback. Reads fall back to the old location; appends only ever go to the
193
+ new one.
194
+ - `Executor#host_roles` is public API now, not a private helper. It answers
195
+ the same question the CLI needs answered — which hosts does this config
196
+ target, and which roles does each serve — so `odysseus doctor` can visit
197
+ every host once, however many roles it serves, without duplicating the
198
+ host-resolution logic this method already implements.
199
+ - Nothing changes for a root install.
200
+
201
+ ## [0.7.0] - 2026-08-16
202
+
203
+ A minor bump for two reasons: there is new public API, and a one-off container's
204
+ environment changes shape. Anything that ran `app exec` and relied on reading
205
+ `-e` flags out of the host's process list — or on a secret being *absent* from
206
+ one-off runs — sees different behaviour.
207
+
208
+ ### Fixed
209
+ - A one-off container — what `app exec`, `app shell` and `app console` run —
210
+ no longer has its environment inlined into the docker command as `-e
211
+ KEY=VALUE`. Every value now travels in the same 0600 env file a deployed
212
+ container's does, so a customer's database password is no longer visible in
213
+ `ps` on the deploy target, and a value containing a space or a shell
214
+ metacharacter arrives intact instead of splitting or being interpreted. The
215
+ file is named so it cannot collide with a running container's, and is
216
+ removed even when the command fails.
217
+ - The env file is removed over a fresh connection when the connection it was
218
+ written over has died in the meantime. Cleanup rescued `Odysseus::SSHError`
219
+ alone, and a connection that drops mid-session raises `IOError`,
220
+ `Net::SSH::Disconnect`, `Errno::EPIPE` or `Errno::ECONNRESET` — none of them
221
+ an `SSHError`. The cleanup's own failure therefore escaped the ensure and
222
+ replaced whatever the block was raising, so a file of secrets was left on the
223
+ host *and* an interactive session's exit status arrived as a backtrace. The
224
+ interactive paths are where this is likeliest: they hold the connection open,
225
+ idle and unpumped, for as long as the user's session lasts, which is what an
226
+ idle NAT timeout, sshd's `ClientAlive` limit or a Tailscale relay change need.
227
+ Any failure of the removal is now swallowed rather than raised — including on
228
+ the second attempt, after which the file is left behind, `0600` in a `0700`
229
+ directory.
230
+ - An upload that fails partway no longer orphans a partial env file. The path
231
+ was learned from the return value of the write, so a write that raised left
232
+ the caller with `nil` and its cleanup with nothing to remove — while scp had
233
+ already created the remote file and begun streaming secrets into it. The path
234
+ is settled before the write now, so the file removed is the file written,
235
+ whether or not the write finished.
236
+
237
+ ### Added
238
+ - `Odysseus::Core::Environment`, the environment a container starts with —
239
+ `env.clear` merged with each `env.secret` resolved from the encrypted
240
+ secrets file or the host's own environment. WebDeploy and JobDeploy each had
241
+ their own copy; one-off commands had neither, which is why `rails
242
+ db:migrate` on a one-off container started without a `DATABASE_URL`.
243
+ - `Docker::Client#with_env_file`, which writes an env file, yields its path
244
+ and removes it afterwards even on failure. `app shell` and `app console`
245
+ need an interactive TTY and so run docker themselves; this is how their
246
+ environment reaches the host as a file rather than as `-e` flags.
247
+
248
+ ## [0.6.0] - 2026-08-15
249
+
250
+ A minor bump: `plugins:` is a new key, and `odysseus validate` can now fail
251
+ where it used to pass — on a machine that does not have a named plugin gem
252
+ installed. That is the point of the change, but it is a behaviour change.
253
+
254
+ ### Added
255
+ - `plugins:` in deploy.yml, a list of gem names loaded before the config is
256
+ validated, so a sail can register its strategy in time for
257
+ `servers.<role>.deploy.strategy` to resolve. `sails:` is accepted as an
258
+ alias. Until now nothing ever loaded a sail: both registries raised "is the
259
+ gem loaded?" for every user, so `deploy.strategy` and the `aws:` host hook
260
+ were unreachable while both READMEs described them.
261
+ - `Odysseus::Core::DeployVersioning`, the shared container version identity —
262
+ `odysseus.version`, `odysseus.git_ref` and `odysseus.deployed_at` — included
263
+ by both built-in orchestrators and available to sails. `status`, `rollback`
264
+ and image retention all read these labels, so an orchestrator that invents
265
+ its own scheme is invisible to them.
266
+
267
+ ### Changed
268
+ - A deploy.yml carrying both `plugins:` and `sails:`, or both `dependencies:`
269
+ and `accessories:`, is now an error. Silently preferring one meant editing
270
+ the wrong key had no visible effect.
271
+ - `odysseus validate` loads plugins, so it now catches a plugin gem that is
272
+ not installed. It will fail on a machine without the gem, where it passed
273
+ before.
274
+ - A plugin that fails to load now reports the file that was actually missing,
275
+ and says so differently when the plugin itself was found. A sail whose own
276
+ dependency is absent — `odysseus-sail-aws-asg` without `aws-sdk-autoscaling`
277
+ — used to read as if the sail were not installed, advising an install that
278
+ could not fix it. The advice also names the Gemfile, which is what a
279
+ `bundle exec odysseus` run needs.
280
+ - Plugin errors quote the key the deploy.yml actually used. Writing `sails:`
281
+ and getting told about `plugins:` named a key that was not in the file.
282
+
283
+ ## [0.5.0] - 2026-08-13
284
+
285
+ A minor bump rather than a patch: odysseus now deletes images on your hosts.
286
+ Nothing did that before, and there is no undo, so the release number should
287
+ make you read this entry.
288
+
289
+ ### Added
290
+ - `retain_versions` in deploy.yml, default 5: how many distinct versions of a
291
+ service's image each host keeps. After a successful deploy, images beyond
292
+ that window are removed from the host. Until now SHA-tagged images
293
+ accumulated without limit — `cleanup --prune-images` only removes *dangling*
294
+ images, and a tagged image is never dangling.
295
+ - `RetentionPlanner`, `Docker::Client#remove_image` and
296
+ `Docker::Client#versions_in_use`.
297
+
298
+ ### Changed
299
+ - `deploy` prunes old images on each host once all of that host's roles are
300
+ deployed. `rollback` deliberately does not: deleting images during a
301
+ recovery is the wrong moment, and the version just rolled back from is the
302
+ most likely next thing wanted.
303
+
304
+ Three independent things must agree before an image is deleted: it must
305
+ fall outside the retain window, no container on the host may reference it
306
+ (stopped containers included, since a stopped container still references its
307
+ image and an operator may still need it), and docker must accept the
308
+ removal. Each removal is attempted on its own, so one
309
+ refusal is a logged skip rather than a failed deploy. A host with no
310
+ `deploys.log` is skipped entirely rather than pruned by image creation time,
311
+ which is build time and can be out of order. `latest` is never removed
312
+ automatically.
313
+
314
+ ## [0.4.4] - 2026-08-13
315
+
316
+ A rename, with the old names still working. Nothing on a host changes.
317
+
318
+ ### Changed
319
+ - `accessories:` in deploy.yml is now `dependencies:`. The old name implied
320
+ optional extras, when a database the app cannot boot without is not optional.
321
+ The old key is still accepted and parses identically, so existing deploy.yml
322
+ files keep working; it will be removed in a later release.
323
+ - Nothing on a host changes as a result. Container names and the
324
+ `odysseus.service` label are built from the service name plus the individual
325
+ dependency's name, so the top-level key never reaches a host and running
326
+ containers are adopted rather than orphaned. A spec runs the real parser over
327
+ a legacy fixture and asserts the resulting container name, so this cannot
328
+ regress silently.
329
+ - `Orchestrator::AccessoryDeploy` is `Orchestrator::DependencyDeploy`,
330
+ `Deployer::AccessoryManager` is `Deployer::DependencyManager`, and
331
+ `Executor`'s six accessory methods are now `deploy_dependency`,
332
+ `remove_dependency`, `restart_dependency`, `upgrade_dependency`,
333
+ `dependency_status` and `boot_dependencies`. No deprecated aliases: nothing
334
+ outside odysseus-cli consumes these, and the two gems ship in lockstep.
335
+ - The config error messages changed accordingly: `Dependency 'x' not found in
336
+ config` and `No hosts configured for dependency x`.
337
+
338
+ ### Internal
339
+ - `Config::Parser#parse_accessories` and its two helpers are now
340
+ `parse_dependencies`, `parse_dependency_healthcheck` and
341
+ `parse_dependency_proxy`. The parser gained its first coverage for this block
342
+ in the process.
343
+
344
+ ## [0.4.3] - 2026-08-13
345
+
346
+ Rollback. A previously deployed version can be put back on the whole fleet, and
347
+ the fleet refuses to move at all unless every host has the image.
348
+
349
+ ### Added
350
+ - `Executor#rollback_plan` and `#rollback_all`, which return every role on
351
+ every host to a previously deployed version by reusing the deploy path, so
352
+ health gating and proxy handling are shared with `deploy`. The target is
353
+ chosen from what the hosts report — running container labels, images present,
354
+ and each host's `deploys.log` — never from the local repository, which can
355
+ drift from what a host can actually run.
356
+ - A fleet pre-flight: the target image must be present on every host across all
357
+ roles before any host is touched. A half-rolled-back fleet is worse than a
358
+ refused command.
359
+ - `Executor#version_survey`, `HostVersions` and `RollbackPlanner`.
360
+ - `Docker::Client#image_tags`, listing the tags a host has for a repository.
361
+
362
+ ### Changed
363
+ - Deploys are recorded on the host by `Executor` rather than by each
364
+ orchestrator. A rollback now records `kind=rolled-back` with the version it
365
+ replaced, and a role deployed by a sail-provided strategy gets the same audit
366
+ trail instead of none.
367
+ - `VersionResolver#deployer` is public, so a rollback can name who ran it even
368
+ though its version came from a host rather than a commit.
369
+
370
+ ### Fixed
371
+ - `odysseus accessory status` never worked in any released version. The code
372
+ called `orchestrator.get_status(name:)`, but `Orchestrator::AccessoryDeploy`
373
+ defines `list_status` and no `get_status`, so the command always raised
374
+ `NoMethodError`. The bad call was introduced on 2025-12-27 in `343030b`,
375
+ three days before the `accessory status` subcommand itself shipped in
376
+ odysseus-cli 0.2.0 (2025-12-30) — so there was never a working version to
377
+ regress from. Fixed in `188a695`: `AccessoryManager#status_on` now calls
378
+ `list_status` and selects the requested accessory out of its results, and a
379
+ new spec exercises it through a verifying double, so a future rename of
380
+ `list_status` breaks the build instead of the command.
381
+
382
+ ### Internal
383
+ - Accessory lifecycle methods (`deploy`, `remove`, `restart`, `upgrade`,
384
+ `status`, `boot_all`) moved out of `Executor` into
385
+ `Deployer::AccessoryManager`, to keep both classes under their existing size
386
+ limits. The public API is unchanged, and `AccessoryManager` gets the first
387
+ spec coverage that code has ever had.
388
+
389
+ ## [0.4.2] - 2026-08-13
390
+
391
+ A running container can now be traced back to the commit it was built from. The
392
+ notable behaviour change is that the image tag no longer defaults to `latest`.
393
+
394
+ ### Added
395
+ - The image tag now defaults to the git commit being deployed, and containers
396
+ carry `odysseus.version`, `odysseus.deployed_at` and `odysseus.git_ref` labels,
397
+ so a running container can be traced to a commit. `odysseus.version` previously
398
+ held the deploy timestamp, which `odysseus.deployed_at` now carries.
399
+ - Each host records successful deploys in `/var/lib/odysseus/<service>/deploys.log`.
400
+
401
+ ### Changed
402
+ - `deploy`, `build` and `pussh` no longer default to the `latest` tag. Outside
403
+ a git repository, or with uncommitted changes, they stop and ask for
404
+ `--image`. The version resolves before the dry-run check, so `--dry-run`
405
+ also requires a resolvable version even though it makes no changes.
406
+
407
+ ## [0.4.1] - 2026-08-12
408
+
409
+ ### Fixed
410
+ - `Caddy::Client#enable_tls_for_hosts` updates an existing `tls` app in place
411
+ instead of trying to recreate it. Caddy's `PUT` creates and answers 409 if the
412
+ key is already there, so on any host whose Caddy already had a `tls` app this
413
+ call had always failed and the policy update had always been discarded — a
414
+ domain added to `proxy.hosts` after the first deploy never got a policy and so
415
+ never got an ACME account email. Certificates still arrived through Caddy's
416
+ implicit automation, which is why it went unnoticed. 0.4.0 did not break this;
417
+ it stopped hiding it.
418
+ - The new policy is merged onto the existing `tls` config rather than replacing
419
+ it, so sibling settings — explicit certificate loaders, `on_demand` limits —
420
+ survive a deploy.
421
+
422
+ ## [0.4.0] - 2026-08-12
423
+
424
+ Deploy reliability. `SSH#execute` and the Caddy client now raise where they used
425
+ to stay quiet, so this is a minor bump rather than a patch: a deploy that
426
+ previously reported success while failing will now stop and say so.
427
+
428
+ ### Fixed
429
+ - `Deployer::SSH#execute` reads the channel's exit status and raises
430
+ `SSHCommandError` on a non-zero exit. Previously every remote command appeared
431
+ to succeed, so a failed `docker stop`, `docker rm`, `mkdir` or network create
432
+ was invisible.
433
+ - `Deployer::SSH#execute` no longer merges stderr into its return value, so
434
+ callers parsing stdout are not handed warning text. This also repairs
435
+ `Docker::Client#health_status` for containers with no Health block.
436
+ - `Caddy::Client` raises `ProxyApiError` when a mutating admin API request
437
+ fails. `curl` exits 0 for HTTP errors, so a rejected route write was parsed as
438
+ an ordinary response and discarded — the deploy reported success while no
439
+ traffic was routed. Reads still return nil, as Caddy answers 500 for config
440
+ paths that do not exist yet.
441
+ - `Orchestrator::WebDeploy` aborts when Caddy fails to start. The result of
442
+ `ensure_running` was discarded and the guard clause that raises had become
443
+ dead code.
444
+ - A web container is always given a health command, defaulting to `GET /` on
445
+ `proxy.app_port`. Without a `healthcheck` block — the shape both READMEs
446
+ show — the container had no health command, its status stayed `none`, and every
447
+ deploy timed out after 60s and rolled back. A web role with no `app_port` now
448
+ fails fast with a config error.
449
+ - `Docker::Client#wait_healthy` honours the timeout it is passed instead of
450
+ capping every wait at 60s, so the 120s requested by `JobDeploy` and
451
+ `AccessoryDeploy` is respected.
452
+ - `Caddy::Client#add_upstream` reconciles a route's Host matcher, so editing
453
+ `proxy.hosts` takes effect instead of requiring the route to be removed by
454
+ hand.
455
+
456
+ ### Changed
457
+ - Container environment is passed through a `0600` env file under
458
+ `/var/lib/odysseus/env` and `--env-file`, instead of `-e KEY=VALUE` on the
459
+ command line. Secrets no longer appear in the host's process list, and values
460
+ containing spaces or shell metacharacters survive intact. A value containing a
461
+ newline is rejected rather than silently truncated.
462
+ - Licensed under MIT. The gemspec and LICENSE previously said LGPL-3.0-only
463
+ while the READMEs said MIT.
464
+
465
+ ### Removed
466
+ - The unused `Odysseus::VERSION` constant, stale at 0.1.0.
467
+ `Odysseus::Core::VERSION` is the single source of truth.
468
+
469
+ ### Internal
470
+ - `validators/config.rb` and `sails.rb` are covered by specs for the first time.
471
+ - `rake` runs RSpec and RuboCop clean. Remaining Metrics offences are recorded
472
+ in `.rubocop_todo.yml` rather than hidden in the main configuration.
473
+
474
+ ## [0.3.1] - 2026-04-05
475
+
476
+ ### Fixed
477
+ - Ruby 4.0 compatibility: declare the `logger` dependency explicitly.
478
+
479
+ ### Removed
480
+ - The AWS Auto Scaling Group host provider, extracted to
481
+ `odysseus-sail-aws-asg`. `HostProviders` keeps the registry and the `aws:`
482
+ config hook the sail consumes.
483
+
484
+ ## [0.3.0] - 2026-03-30
485
+
486
+ ### Added
487
+ - `Sails` plugin registry, with the rolling deploy strategy extracted to
488
+ `odysseus-sail-rolling`. `servers.<role>.deploy.strategy` resolves through it.
489
+ - Volume namespacing: named volumes are prefixed with the service name so
490
+ containers sharing a host no longer collide, reusing an existing
491
+ un-namespaced volume rather than losing data.
492
+ - `Docker::Client#ensure_network`, so accessories can boot before any service
493
+ deploy has created the network.
494
+ - Parsing for `servers.<role>.containers` and `servers.<role>.deploy`, including
495
+ timeouts and an HTTP health check with a success threshold.
496
+
497
+ ## [0.2.0] - 2025-12-30
498
+
499
+ ### Added
500
+ - Encrypted secrets files (AES-256-GCM) with `Secrets::EncryptedFile` and
501
+ `Secrets::Loader`.
502
+ - Builder with local and remote (build host) strategies, plus pussh
503
+ distribution over SSH for registry-less deploys.
504
+ - Host provider registry behind `HostProviders`, with the static provider as
505
+ the default.
506
+ - CPU and memory limits for containers.
507
+
3
508
  ## [0.1.0] - 2025-12-19
4
509
 
5
- - Initial release
510
+ - Initial release: config parsing, SSH and Docker clients, Caddy proxy
511
+ integration with automatic TLS, web and job orchestrators, and the accessory
512
+ lifecycle.
data/LICENSE.txt CHANGED
@@ -1,8 +1,26 @@
1
+ MIT License
2
+
1
3
  Copyright (c) Imfiny SAS / Wa Systems SAS
2
-
3
- Odysseus is an Open Source project licensed under the terms of
4
- the LGPLv3 license. Please see <http://www.gnu.org/licenses/lgpl-3.0.html>
5
- for license text.
6
-
7
- Odysseus Pro and Odysseus Enterprise have a commercial-friendly license.
8
-
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ ---
24
+
25
+ Odysseus Pro and Odysseus Enterprise are distributed under separate
26
+ commercial terms.
data/README.md CHANGED
@@ -19,6 +19,9 @@ gem 'odysseus-core'
19
19
  Odysseus Core provides the foundational components for Docker container deployment:
20
20
 
21
21
  - **Configuration parsing** - YAML-based deploy.yml configuration
22
+ - **Sail plugins** - gems named in `plugins:`/`sails:` are loaded before the
23
+ rest of deploy.yml is validated, so they can register a deploy strategy or
24
+ a host provider before anything checks whether one exists
22
25
  - **Docker client** - Container lifecycle management via SSH
23
26
  - **Caddy client** - Reverse proxy configuration and routing
24
27
  - **Deployer** - Zero-downtime deployment orchestration
@@ -48,10 +51,11 @@ executor.deploy_all(image_tag: 'v1.0.0')
48
51
 
49
52
  Parses deploy.yml configuration files with support for:
50
53
  - Server roles (web, jobs, workers)
54
+ - Sail plugins (`plugins:`, aliased `sails:`), loaded before the config below
55
+ it is validated
51
56
  - Proxy configuration (Caddy)
52
- - Accessories (databases, Redis, etc.)
57
+ - Dependencies (databases, Redis, etc.)
53
58
  - Environment variables and secrets
54
- - AWS Auto Scaling Group integration
55
59
 
56
60
  ### Odysseus::Docker::Client
57
61
 
@@ -75,6 +79,11 @@ Deployment orchestration:
75
79
  - Zero-downtime container replacement
76
80
  - Health check verification
77
81
  - Automatic rollback on failure
82
+ - Roll a service back to a previously deployed version, chosen from what the
83
+ hosts report rather than the local repository
84
+ - Prune a service's superseded image versions from each host after a
85
+ successful deploy, keeping the newest `retain_versions` and never a version
86
+ a container still references
78
87
 
79
88
  ### Odysseus::Secrets::EncryptedFile
80
89
 
@@ -85,4 +94,4 @@ Encrypted secrets management:
85
94
 
86
95
  ## License
87
96
 
88
- LGPL-3.0-only
97
+ MIT