odysseus-core 0.9.0 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5fad8198958c2e3991a174b1d7821199e30c4807a48fe7bd9011f689ca430ae6
4
- data.tar.gz: 9241a316d823c6397e3d293208bc186dcd0b6db88a3b1410e05599a43b99d686
3
+ metadata.gz: f6d1b931d263332fd0a431b4a0842949f52d78f2f366f8dbf72cb98f0ea95282
4
+ data.tar.gz: f3ea3a6477a88cbd38b1386f04e983bad5d7d782475f46322989568389e4fa5b
5
5
  SHA512:
6
- metadata.gz: a323a1f3f08eded7052792ac13b83514a6fa045a7bcfb8bf928d28eb99ef4321ca8ec9abb2f9cd08e752467147e9cca8465bb60a3cc83d036c312cf3749c6a91
7
- data.tar.gz: d800125c7690876145e8c7297d986d0a3ce1aba2856454df17567a209a7ae958eb79414afc0f4094390f82ad2205a61544cbc7ba31b583fd704040b15a0abcc4
6
+ metadata.gz: b89f611e20800ecfbdbd30fe571b6a9979877a59f52655a0c05fb3aeadc7567f6af4325711c92eb02698bbdd23c1746f6c1d150436e20dc45318a4f0fae0f1b4
7
+ data.tar.gz: 6db503709fb510a38980f0357a69f82f07b7b04e77b8b63c7286035f88425134039c06618164648876324a4a22f902341d4ca41fa4bb5a027a5addc07a4a7309
data/CHANGELOG.md CHANGED
@@ -7,8 +7,31 @@ gem artifacts, so they are summaries rather than contemporaneous notes.
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.10.0] - 2026-08-28
11
+
10
12
  ### Fixed
11
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
+
12
35
  - Commands echoed under `--debug`/`-v` no longer print secrets. `SSH#execute`
13
36
  echoed the command verbatim, and registry login is built as
14
37
  `echo '<password>' | docker login <server> -u <user> --password-stdin` --
@@ -10,6 +10,11 @@ module Odysseus
10
10
  CONTAINER_NAME = 'odysseus-caddy'.freeze
11
11
  CADDY_IMAGE = 'caddy:2-alpine'.freeze
12
12
 
13
+ # The image's own CMD. Repeated here because --resume has to be appended
14
+ # to it: passing only --resume would drop the Caddyfile fallback that
15
+ # creates srv0 on a host with no autosave yet.
16
+ CADDY_RUN_CMD = 'caddy run --config /etc/caddy/Caddyfile --adapter caddyfile'.freeze
17
+
13
18
  # @param ssh [Odysseus::Deployer::SSH] SSH connection to server
14
19
  # @param docker [Odysseus::Docker::Client] Docker client
15
20
  def initialize(ssh:, docker:)
@@ -34,6 +39,17 @@ module Odysseus
34
39
  # Nothing is lost: certificates live in the mounted volume, and routes
35
40
  # are re-added by the deploy that follows.
36
41
  #
42
+ # Started with --resume, so the routes added through the admin API by
43
+ # previous deploys come back with the container rather than having to be
44
+ # re-added by redeploying every service on the host.
45
+ #
46
+ # If that start fails the autosave is the only new thing that could have
47
+ # caused it -- CADDY_IMAGE is a moving tag, so a newer Caddy can meet an
48
+ # autosave it will not parse. Since WebDeploy aborts when Caddy does not
49
+ # come up, leaving that unhandled would fail every deploy on the host
50
+ # until someone deleted the file by hand. The retry gives up the routes,
51
+ # which is exactly the behaviour that existed before --resume.
52
+ #
37
53
  # @return [Boolean] true if caddy is running
38
54
  def ensure_running
39
55
  return true if running?
@@ -41,6 +57,9 @@ module Odysseus
41
57
  @docker.remove(CONTAINER_NAME) if @docker.container_exists?(CONTAINER_NAME)
42
58
 
43
59
  start_caddy
60
+ return true if running?
61
+
62
+ restart_without_autosave
44
63
  running?
45
64
  end
46
65
 
@@ -51,12 +70,18 @@ module Odysseus
51
70
  end
52
71
 
53
72
  # Start Caddy container
54
- def start_caddy
73
+ # @param resume [Boolean] reload the autosaved config (see #ensure_running)
74
+ def start_caddy(resume: true)
55
75
  # Create network if not exists (with label to protect from prune)
56
76
  @ssh.execute('docker network create --label odysseus.managed=true odysseus 2>/dev/null || true')
57
77
 
58
- # Create data directory for certificates
78
+ # Create data directory for certificates, and the config directory
79
+ # holding the autosave. Two directories because the image sets
80
+ # XDG_DATA_HOME=/data and XDG_CONFIG_HOME=/config, and Caddy writes the
81
+ # autosave under the latter -- mounting only /data persisted the
82
+ # certificates and threw the routes away with the container.
59
83
  @ssh.execute("mkdir -p #{Shellwords.escape(host_paths.caddy_dir)}")
84
+ @ssh.execute("mkdir -p #{Shellwords.escape(host_paths.caddy_config_dir)}")
60
85
 
61
86
  # Run Caddy with admin API enabled and persistent storage for certs
62
87
  #
@@ -81,13 +106,17 @@ module Odysseus
81
106
  ports: ['80:80', '443:443', "127.0.0.1:#{ADMIN_API_PORT}:#{ADMIN_API_PORT}"],
82
107
  network: 'odysseus',
83
108
  restart: 'unless-stopped',
84
- volumes: ["#{Shellwords.escape(host_paths.caddy_dir)}:/data"],
109
+ volumes: [
110
+ "#{Shellwords.escape(host_paths.caddy_dir)}:/data",
111
+ "#{Shellwords.escape(host_paths.caddy_config_dir)}:/config"
112
+ ],
85
113
  env: {
86
114
  'CADDY_ADMIN' => "0.0.0.0:#{ADMIN_API_PORT}"
87
115
  },
88
116
  labels: {
89
117
  'odysseus.managed' => 'true'
90
- }
118
+ },
119
+ cmd: resume ? "#{CADDY_RUN_CMD} --resume" : CADDY_RUN_CMD
91
120
  }
92
121
  )
93
122
 
@@ -316,6 +345,29 @@ module Odysseus
316
345
 
317
346
  private
318
347
 
348
+ def restart_without_autosave
349
+ @docker.remove(CONTAINER_NAME) if @docker.container_exists?(CONTAINER_NAME)
350
+ discard_autosave
351
+ start_caddy(resume: false)
352
+ end
353
+
354
+ # Caddy creates /config/caddy as root with mode 0700, so a non-root deploy
355
+ # user cannot move the file from the host -- it has to be moved from
356
+ # inside a container. CADDY_IMAGE is guaranteed to be on the host: it is
357
+ # the image that just failed to start.
358
+ #
359
+ # Renamed rather than deleted, because if --resume was not the reason
360
+ # Caddy failed then this file is the only copy of the host's routes.
361
+ # Tolerates its own failure: a Caddy that died for some other reason may
362
+ # have no autosave at all, and that must not abort the retry.
363
+ def discard_autosave
364
+ mount = "#{Shellwords.escape(host_paths.caddy_config_dir)}:/config"
365
+ @ssh.execute(
366
+ "docker run --rm -v #{mount} #{CADDY_IMAGE} " \
367
+ 'mv /config/caddy/autosave.json /config/caddy/autosave.json.rejected 2>/dev/null || true'
368
+ )
369
+ end
370
+
319
371
  def host_paths
320
372
  @host_paths ||= Odysseus::HostPaths.new(@ssh)
321
373
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Odysseus
4
4
  module Core
5
- VERSION = '0.9.0'
5
+ VERSION = '0.10.0'
6
6
  end
7
7
  end
@@ -57,6 +57,20 @@ module Odysseus
57
57
  File.join(base, 'caddy')
58
58
  end
59
59
 
60
+ # Caddy's /config mount: the autosave of its running configuration.
61
+ #
62
+ # The official image sets XDG_CONFIG_HOME=/config and XDG_DATA_HOME=/data,
63
+ # and Caddy writes $XDG_CONFIG_HOME/caddy/autosave.json on every admin API
64
+ # change. Mounting only #caddy_dir therefore persisted certificates and
65
+ # discarded the routes, which is why a removed or restarted Caddy came back
66
+ # serving nothing until every service on the host redeployed. Kept separate
67
+ # from #caddy_dir rather than nested inside it because the container owns
68
+ # the layout of both, and /data already has a meaning to Caddy.
69
+ # @return [String]
70
+ def caddy_config_dir
71
+ File.join(base, 'caddy-config')
72
+ end
73
+
60
74
  # Where a root install wrote, whoever is connected now. Used to read the
61
75
  # deploy history of a host that has since moved to a deploy user.
62
76
  # @return [String]
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: odysseus-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Thomas
@@ -157,7 +157,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
157
157
  - !ruby/object:Gem::Version
158
158
  version: '0'
159
159
  requirements: []
160
- rubygems_version: 4.0.16
160
+ rubygems_version: 4.0.19
161
161
  specification_version: 4
162
162
  summary: Core library for Odysseus deployment tool
163
163
  test_files: []