splent-feature-autodeploy 0.2.0__tar.gz → 0.3.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.
Files changed (25) hide show
  1. {splent_feature_autodeploy-0.2.0/src/splent_feature_autodeploy.egg-info → splent_feature_autodeploy-0.3.0}/PKG-INFO +107 -43
  2. {splent_feature_autodeploy-0.2.0 → splent_feature_autodeploy-0.3.0}/README.md +106 -42
  3. {splent_feature_autodeploy-0.2.0 → splent_feature_autodeploy-0.3.0}/pyproject.toml +27 -5
  4. {splent_feature_autodeploy-0.2.0 → splent_feature_autodeploy-0.3.0/src/splent_feature_autodeploy.egg-info}/PKG-INFO +107 -43
  5. {splent_feature_autodeploy-0.2.0 → splent_feature_autodeploy-0.3.0}/src/splent_io/splent_feature_autodeploy/commands.py +7 -0
  6. {splent_feature_autodeploy-0.2.0 → splent_feature_autodeploy-0.3.0}/COPYING +0 -0
  7. {splent_feature_autodeploy-0.2.0 → splent_feature_autodeploy-0.3.0}/COPYING.LESSER +0 -0
  8. {splent_feature_autodeploy-0.2.0 → splent_feature_autodeploy-0.3.0}/MANIFEST.in +0 -0
  9. {splent_feature_autodeploy-0.2.0 → splent_feature_autodeploy-0.3.0}/setup.cfg +0 -0
  10. {splent_feature_autodeploy-0.2.0 → splent_feature_autodeploy-0.3.0}/src/splent_feature_autodeploy.egg-info/SOURCES.txt +0 -0
  11. {splent_feature_autodeploy-0.2.0 → splent_feature_autodeploy-0.3.0}/src/splent_feature_autodeploy.egg-info/dependency_links.txt +0 -0
  12. {splent_feature_autodeploy-0.2.0 → splent_feature_autodeploy-0.3.0}/src/splent_feature_autodeploy.egg-info/requires.txt +0 -0
  13. {splent_feature_autodeploy-0.2.0 → splent_feature_autodeploy-0.3.0}/src/splent_feature_autodeploy.egg-info/top_level.txt +0 -0
  14. {splent_feature_autodeploy-0.2.0 → splent_feature_autodeploy-0.3.0}/src/splent_io/splent_feature_autodeploy/__init__.py +0 -0
  15. {splent_feature_autodeploy-0.2.0 → splent_feature_autodeploy-0.3.0}/src/splent_io/splent_feature_autodeploy/config.py +0 -0
  16. {splent_feature_autodeploy-0.2.0 → splent_feature_autodeploy-0.3.0}/src/splent_io/splent_feature_autodeploy/forms.py +0 -0
  17. {splent_feature_autodeploy-0.2.0 → splent_feature_autodeploy-0.3.0}/src/splent_io/splent_feature_autodeploy/hooks.py +0 -0
  18. {splent_feature_autodeploy-0.2.0 → splent_feature_autodeploy-0.3.0}/src/splent_io/splent_feature_autodeploy/presenters.py +0 -0
  19. {splent_feature_autodeploy-0.2.0 → splent_feature_autodeploy-0.3.0}/src/splent_io/splent_feature_autodeploy/routes.py +0 -0
  20. {splent_feature_autodeploy-0.2.0 → splent_feature_autodeploy-0.3.0}/src/splent_io/splent_feature_autodeploy/services.py +0 -0
  21. {splent_feature_autodeploy-0.2.0 → splent_feature_autodeploy-0.3.0}/src/splent_io/splent_feature_autodeploy/templates/autodeploy/index.html +0 -0
  22. {splent_feature_autodeploy-0.2.0 → splent_feature_autodeploy-0.3.0}/src/splent_io/splent_feature_autodeploy/templates/hooks/sidebar_deployments.html +0 -0
  23. {splent_feature_autodeploy-0.2.0 → splent_feature_autodeploy-0.3.0}/src/splent_io/splent_feature_autodeploy/translations/es/LC_MESSAGES/messages.mo +0 -0
  24. {splent_feature_autodeploy-0.2.0 → splent_feature_autodeploy-0.3.0}/src/splent_io/splent_feature_autodeploy/translations/es/LC_MESSAGES/messages.po +0 -0
  25. {splent_feature_autodeploy-0.2.0 → splent_feature_autodeploy-0.3.0}/src/splent_io/splent_feature_autodeploy/translations/messages.pot +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: splent_feature_autodeploy
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  License-Expression: LGPL-3.0-or-later
5
5
  Requires-Python: >=3.13
6
6
  Description-Content-Type: text/markdown
@@ -12,12 +12,14 @@ Dynamic: license-file
12
12
  # splent_feature_autodeploy
13
13
 
14
14
  Release-gated continuous delivery for a SPLENT product. A product that
15
- selects this feature runs a small agent next to its web container in
16
- production. The agent follows a registry image channel, for example
17
- `git.splent.io/splent-io/<product>:stable`, and when the channel points to a
18
- new image it redeploys the product. The database is backed up first, the
19
- new version has to pass its health check, and an automatic rollback of both the container and the database when
20
- the new version does not come up.
15
+ selects this feature runs a small agent in a container of its own next to
16
+ its web container in production. The agent follows a registry image
17
+ channel, for example `git.splent.io/splent-io/<product>:stable`, and when
18
+ the channel points to a new image it redeploys the product on that image's
19
+ digest. It backs up first (off the host too, when the product has
20
+ `splent_feature_backup`), migrates once, waits for the new version's
21
+ `/readyz`, and rolls the container back, and the database when the deploy
22
+ changed it, when the new version is not ready.
21
23
 
22
24
  The channel only moves when someone runs `splent product:release`, so nothing
23
25
  reaches production that nobody released. The agent never builds anything and
@@ -29,41 +31,92 @@ Every `AUTODEPLOY_INTERVAL` seconds, and whenever an administrator asks,
29
31
  the agent goes through these steps.
30
32
 
31
33
  1. Pull the channel image. If its id is the one the web container runs,
32
- there is nothing to do.
34
+ there is nothing to do. Otherwise the image is named from now on by its
35
+ digest (`<registry>/<org>/<product>@sha256:...`), which no push can
36
+ move: that is what the new container runs and what the history records.
37
+ An image the registry knows no digest of runs by its id, just as fixed.
33
38
  2. Skip an image that failed before, until the channel points elsewhere.
34
39
  Honour the pause flag and the update window (`AUTODEPLOY_WINDOW`). A
35
40
  check somebody asks for (Check now) deploys outside the window too.
36
- 3. Back up the database. `mariadb-dump --single-transaction --routines
37
- --triggers` runs as root inside the database container, with the
38
- credentials that container already holds, and the gzipped dump is kept
39
- only if `mariadb-dump` exited cleanly and the dump ends with its
40
- `Dump completed` trailer. No complete backup, no deploy.
41
- 4. Record what the web container runs (`splent version --json`).
42
- 5. Recreate the web container on the new image with everything it was
43
- configured with, the way Watchtower does it. That is its name, its
44
- environment (the container's own values on top of the new image's), its
45
- labels, mounts and binds, port bindings, restart policy, healthcheck, and
46
- its networks with their aliases. Cmd, Entrypoint and WorkingDir follow the new image when the old
47
- container only inherited them, and are kept when it chose them (the
48
- compose file's `command:`). The old container is renamed
49
- `<name>-previous` and stopped, not removed.
50
- 6. Wait for the healthcheck, up to `AUTODEPLOY_HEALTH_TIMEOUT` seconds. The
51
- entrypoint runs every feature's migrations before gunicorn starts, so this
52
- is generous.
53
- 7. Once it is healthy, remove the previous container and write the history
54
- entry (from and to image, product version, the features whose version
55
- changed, duration).
56
- 8. If it turns unhealthy, exits, restarts or runs out of time, keep the new
57
- container's last 80 log lines, remove it, drop and recreate the database and import the
58
- backup (so tables the new migrations created do not linger), put the
59
- previous container back under its name, wait for it to be healthy, and
60
- refuse that image from then on. The history entry says `rolled_back` and
61
- carries the log lines.
41
+ 3. Back up.
42
+ - When the product has `splent_feature_backup`, run its off-host backup
43
+ in the running web container, as the app's user:
44
+ `splent feature:backup run --tag pre-deploy-<short id of the image
45
+ being replaced>` (`AUTODEPLOY_BACKUP_COMMAND`). It is encrypted to keys
46
+ the server does not hold, and it is the copy that survives the host.
47
+ - Then the agent's own dump, the one a rollback restores:
48
+ `mariadb-dump --single-transaction --routines --triggers` runs as root
49
+ inside the database container, with the credentials that container
50
+ already holds, and the gzipped dump is kept only if `mariadb-dump`
51
+ exited cleanly and the dump ends with its `Dump completed` trailer.
52
+
53
+ No complete backup, no deploy.
54
+ 4. Record what the web container runs (`splent version --json`), and a
55
+ fingerprint of the schema: every table's columns and every migration
56
+ revision (`alembic_*`, `splent_migrations`).
57
+ 5. Create the new web container on the image's digest with everything the
58
+ old one was configured with, the way Watchtower does it. That is its
59
+ name, its environment (the container's own values on top of the new
60
+ image's), its labels, mounts and binds, port bindings, restart policy,
61
+ healthcheck, and its networks with their aliases. Cmd, Entrypoint and
62
+ WorkingDir follow the new image when the old container only inherited
63
+ them, and are kept when it chose them (the compose file's `command:`).
64
+ The old container is renamed `<name>-previous` and stopped, not removed.
65
+ 6. Migrate, once: a one-off container of the new image, `<name>-migrate`,
66
+ with the web container's environment, mounts and networks, runs
67
+ `AUTODEPLOY_MIGRATE_COMMAND` (`splent db:upgrade`) as the app's user
68
+ while neither version serves. It answers to no alias, so nginx never
69
+ finds it, publishes no port and is removed once it exits. The new
70
+ container's entrypoint upgrades again as it starts and finds nothing
71
+ left to do.
72
+ 7. Start the new container and wait, up to `AUTODEPLOY_HEALTH_TIMEOUT`
73
+ seconds, for `/readyz` to answer 200 (`AUTODEPLOY_READY_PATH`, asked by
74
+ the container's name on `AUTODEPLOY_WEB_PORT`), while Docker's
75
+ healthcheck does not say unhealthy. `/readyz` is the framework's
76
+ (1.22.0 and later): 200 when the database and the services the product
77
+ declares answer. An image without it answers 404, and the healthcheck
78
+ decides for it.
79
+ 8. Once it is ready, remove the previous container and write the history
80
+ entry (from and to image, the digest, product version, the features
81
+ whose version changed, the off-host backup, duration).
82
+ 9. If the migration fails, or the new container turns unhealthy, exits,
83
+ restarts or is not ready in time, keep its last 80 log lines (the
84
+ migration's, when that is what failed), remove it, put the database
85
+ back when the rule below says so, put the previous container back under
86
+ its name, wait for it to be healthy, and refuse that image from then on.
87
+ The history entry says `rolled_back`, carries the log lines and says
88
+ whether the database went back and why.
89
+
90
+ ### When the database goes back
91
+
92
+ A rollback restores the agent's pre-deploy dump (dropping the database
93
+ first, so tables the new migrations created do not linger) when:
94
+
95
+ - the migration failed, or the agent died during it. MariaDB commits DDL as
96
+ it goes, so a failed migration can leave half of its changes behind
97
+ without recording them;
98
+ - the migration finished, and the schema fingerprint differs from the one
99
+ taken before the deploy. The previous version cannot run on the new
100
+ schema, and a down-migration is not something to trust with production
101
+ data;
102
+ - the fingerprint cannot be read, which counts as changed;
103
+ - `AUTODEPLOY_MIGRATE_COMMAND=off`: the new container migrated as it
104
+ started, and the agent cannot tell a failed migration from anything
105
+ else.
106
+
107
+ It does not when the deploy stopped before anything ran against the
108
+ database (a failed backup, a container that could not be created), and when
109
+ the migration finished without changing the schema: the previous version
110
+ comes back on the same database, and nothing written while the new one
111
+ tried to start is lost. The dump is taken right before the old version
112
+ stops, so a restore loses only what the new version wrote during its
113
+ attempt.
62
114
 
63
115
  Every step is written to `/data/state.json` before it is taken. An agent
64
116
  restarted in the middle of a deploy (a reboot, a redeploy of the agent
65
- itself) finishes it if the new container turns healthy, and rolls it back
66
- otherwise.
117
+ itself) finishes it if the new container turns ready, and rolls it back
118
+ otherwise; one restarted during the migration rolls back with the
119
+ database.
67
120
 
68
121
  | State | Meaning |
69
122
  |----------------------|--------------------------------------------------------------|
@@ -137,11 +190,16 @@ with the product's names filled in.
137
190
 
138
191
  | Variable | Default | Meaning |
139
192
  |------------------------------|-------------------------------------|---------|
140
- | `AUTODEPLOY_IMAGE` | empty | Image to follow. Empty means `SPLENT_IMAGE` |
193
+ | `AUTODEPLOY_IMAGE` | empty | Image to follow. Empty means `SPLENT_IMAGE`. A channel (`:stable`) or a version tag; `:latest`, or no tag at all, is refused |
141
194
  | `AUTODEPLOY_INTERVAL` | `300` | Seconds between checks (at least 30) |
142
195
  | `AUTODEPLOY_WINDOW` | empty | UTC span for deploys, like `02:00-05:00` or `22:00-04:00`. Empty means any time |
143
- | `AUTODEPLOY_HEALTH_TIMEOUT` | `600` | Seconds the new container gets to turn healthy |
144
- | `AUTODEPLOY_KEEP_BACKUPS` | `5` | Backups kept, newest first |
196
+ | `AUTODEPLOY_HEALTH_TIMEOUT` | `600` | Seconds the migrations get, and then the new container to turn ready |
197
+ | `AUTODEPLOY_MIGRATE_COMMAND` | `splent db:upgrade` | What the one-off migration container runs. `off` leaves the migrations to the new container's start |
198
+ | `AUTODEPLOY_READY_PATH` | `/readyz` | The readiness probe. `off` waits for Docker's healthcheck alone |
199
+ | `AUTODEPLOY_WEB_PORT` | `5000` | The port the web container answers on, inside the product network |
200
+ | `AUTODEPLOY_BACKUP_COMMAND` | `auto` | The off-host backup before a deploy. `auto` is `splent_feature_backup`'s when the product has it, `off` none, anything else the command to run in the web container |
201
+ | `AUTODEPLOY_APP_USER` | `splent` | The user the migration and the backup command run as |
202
+ | `AUTODEPLOY_KEEP_BACKUPS` | `5` | Rollback dumps kept, newest first |
145
203
  | `AUTODEPLOY_WEB_CONTAINER` | `<product>_web_deploy` | The container that is replaced |
146
204
  | `AUTODEPLOY_DB_CONTAINER` | `<product>_db_deploy` | The MariaDB container that is backed up |
147
205
  | `AUTODEPLOY_AGENT_CONTAINER` | `<product>_autodeploy` | The agent's container name |
@@ -202,8 +260,10 @@ since.
202
260
 
203
261
  - The agent mounts `/var/run/docker.sock`. Whoever controls that socket is
204
262
  root on the host, so the agent is as sensitive as the host itself. It runs
205
- only in production, from the image `product:deploy` builds out of this
206
- feature's `docker/` directory, and does only what this README describes.
263
+ only in production, in a container of its own, from the image
264
+ `product:deploy` builds out of this feature's `docker/` directory, and does
265
+ only what this README describes. No container of the app mounts the
266
+ socket: not the web container, not the migration job it creates.
207
267
  - It publishes no port. Only containers on the product network reach the API,
208
268
  and every call needs the shared secret. The secret never appears in the
209
269
  status, the history or the logs.
@@ -221,7 +281,8 @@ One per deploy, in the agent's `autodeploy_data` volume under
221
281
  so each file says which code its schema belongs to. The newest
222
282
  `AUTODEPLOY_KEEP_BACKUPS` are kept. They live on the same host as the
223
283
  database, so they protect against a bad release, not against losing the
224
- server. Copy them elsewhere if you need that.
284
+ server: that is what `splent_feature_backup`'s off-host backup, taken just
285
+ before, is for. Without that feature, copy them elsewhere if you need it.
225
286
 
226
287
  ## Restoring a backup by hand
227
288
 
@@ -248,7 +309,10 @@ To let the agent try an image it refused, pause it, delete the image id from
248
309
  feature that ships its own container), or needs a new setting without a
249
310
  default in `.env.deploy`, has to go through `splent product:deploy`; pause
250
311
  the agent while you do it. Left to the agent such a release fails its
251
- health check and is rolled back, which is safe but not what you want.
312
+ readiness check and is rolled back, which is safe but not what you want.
313
+ - It takes no database lock (`GET_LOCK`) around the migration: one agent
314
+ replaces one web container, which is stopped while the job runs, so the
315
+ job is the only migrator. Several replicas would need that lock.
252
316
  - It does not update itself. A new version of this feature reaches the
253
317
  server with `splent product:deploy`, which rebuilds the agent's image.
254
318
  - It replaces one web container, not a set of replicas.
@@ -1,12 +1,14 @@
1
1
  # splent_feature_autodeploy
2
2
 
3
3
  Release-gated continuous delivery for a SPLENT product. A product that
4
- selects this feature runs a small agent next to its web container in
5
- production. The agent follows a registry image channel, for example
6
- `git.splent.io/splent-io/<product>:stable`, and when the channel points to a
7
- new image it redeploys the product. The database is backed up first, the
8
- new version has to pass its health check, and an automatic rollback of both the container and the database when
9
- the new version does not come up.
4
+ selects this feature runs a small agent in a container of its own next to
5
+ its web container in production. The agent follows a registry image
6
+ channel, for example `git.splent.io/splent-io/<product>:stable`, and when
7
+ the channel points to a new image it redeploys the product on that image's
8
+ digest. It backs up first (off the host too, when the product has
9
+ `splent_feature_backup`), migrates once, waits for the new version's
10
+ `/readyz`, and rolls the container back, and the database when the deploy
11
+ changed it, when the new version is not ready.
10
12
 
11
13
  The channel only moves when someone runs `splent product:release`, so nothing
12
14
  reaches production that nobody released. The agent never builds anything and
@@ -18,41 +20,92 @@ Every `AUTODEPLOY_INTERVAL` seconds, and whenever an administrator asks,
18
20
  the agent goes through these steps.
19
21
 
20
22
  1. Pull the channel image. If its id is the one the web container runs,
21
- there is nothing to do.
23
+ there is nothing to do. Otherwise the image is named from now on by its
24
+ digest (`<registry>/<org>/<product>@sha256:...`), which no push can
25
+ move: that is what the new container runs and what the history records.
26
+ An image the registry knows no digest of runs by its id, just as fixed.
22
27
  2. Skip an image that failed before, until the channel points elsewhere.
23
28
  Honour the pause flag and the update window (`AUTODEPLOY_WINDOW`). A
24
29
  check somebody asks for (Check now) deploys outside the window too.
25
- 3. Back up the database. `mariadb-dump --single-transaction --routines
26
- --triggers` runs as root inside the database container, with the
27
- credentials that container already holds, and the gzipped dump is kept
28
- only if `mariadb-dump` exited cleanly and the dump ends with its
29
- `Dump completed` trailer. No complete backup, no deploy.
30
- 4. Record what the web container runs (`splent version --json`).
31
- 5. Recreate the web container on the new image with everything it was
32
- configured with, the way Watchtower does it. That is its name, its
33
- environment (the container's own values on top of the new image's), its
34
- labels, mounts and binds, port bindings, restart policy, healthcheck, and
35
- its networks with their aliases. Cmd, Entrypoint and WorkingDir follow the new image when the old
36
- container only inherited them, and are kept when it chose them (the
37
- compose file's `command:`). The old container is renamed
38
- `<name>-previous` and stopped, not removed.
39
- 6. Wait for the healthcheck, up to `AUTODEPLOY_HEALTH_TIMEOUT` seconds. The
40
- entrypoint runs every feature's migrations before gunicorn starts, so this
41
- is generous.
42
- 7. Once it is healthy, remove the previous container and write the history
43
- entry (from and to image, product version, the features whose version
44
- changed, duration).
45
- 8. If it turns unhealthy, exits, restarts or runs out of time, keep the new
46
- container's last 80 log lines, remove it, drop and recreate the database and import the
47
- backup (so tables the new migrations created do not linger), put the
48
- previous container back under its name, wait for it to be healthy, and
49
- refuse that image from then on. The history entry says `rolled_back` and
50
- carries the log lines.
30
+ 3. Back up.
31
+ - When the product has `splent_feature_backup`, run its off-host backup
32
+ in the running web container, as the app's user:
33
+ `splent feature:backup run --tag pre-deploy-<short id of the image
34
+ being replaced>` (`AUTODEPLOY_BACKUP_COMMAND`). It is encrypted to keys
35
+ the server does not hold, and it is the copy that survives the host.
36
+ - Then the agent's own dump, the one a rollback restores:
37
+ `mariadb-dump --single-transaction --routines --triggers` runs as root
38
+ inside the database container, with the credentials that container
39
+ already holds, and the gzipped dump is kept only if `mariadb-dump`
40
+ exited cleanly and the dump ends with its `Dump completed` trailer.
41
+
42
+ No complete backup, no deploy.
43
+ 4. Record what the web container runs (`splent version --json`), and a
44
+ fingerprint of the schema: every table's columns and every migration
45
+ revision (`alembic_*`, `splent_migrations`).
46
+ 5. Create the new web container on the image's digest with everything the
47
+ old one was configured with, the way Watchtower does it. That is its
48
+ name, its environment (the container's own values on top of the new
49
+ image's), its labels, mounts and binds, port bindings, restart policy,
50
+ healthcheck, and its networks with their aliases. Cmd, Entrypoint and
51
+ WorkingDir follow the new image when the old container only inherited
52
+ them, and are kept when it chose them (the compose file's `command:`).
53
+ The old container is renamed `<name>-previous` and stopped, not removed.
54
+ 6. Migrate, once: a one-off container of the new image, `<name>-migrate`,
55
+ with the web container's environment, mounts and networks, runs
56
+ `AUTODEPLOY_MIGRATE_COMMAND` (`splent db:upgrade`) as the app's user
57
+ while neither version serves. It answers to no alias, so nginx never
58
+ finds it, publishes no port and is removed once it exits. The new
59
+ container's entrypoint upgrades again as it starts and finds nothing
60
+ left to do.
61
+ 7. Start the new container and wait, up to `AUTODEPLOY_HEALTH_TIMEOUT`
62
+ seconds, for `/readyz` to answer 200 (`AUTODEPLOY_READY_PATH`, asked by
63
+ the container's name on `AUTODEPLOY_WEB_PORT`), while Docker's
64
+ healthcheck does not say unhealthy. `/readyz` is the framework's
65
+ (1.22.0 and later): 200 when the database and the services the product
66
+ declares answer. An image without it answers 404, and the healthcheck
67
+ decides for it.
68
+ 8. Once it is ready, remove the previous container and write the history
69
+ entry (from and to image, the digest, product version, the features
70
+ whose version changed, the off-host backup, duration).
71
+ 9. If the migration fails, or the new container turns unhealthy, exits,
72
+ restarts or is not ready in time, keep its last 80 log lines (the
73
+ migration's, when that is what failed), remove it, put the database
74
+ back when the rule below says so, put the previous container back under
75
+ its name, wait for it to be healthy, and refuse that image from then on.
76
+ The history entry says `rolled_back`, carries the log lines and says
77
+ whether the database went back and why.
78
+
79
+ ### When the database goes back
80
+
81
+ A rollback restores the agent's pre-deploy dump (dropping the database
82
+ first, so tables the new migrations created do not linger) when:
83
+
84
+ - the migration failed, or the agent died during it. MariaDB commits DDL as
85
+ it goes, so a failed migration can leave half of its changes behind
86
+ without recording them;
87
+ - the migration finished, and the schema fingerprint differs from the one
88
+ taken before the deploy. The previous version cannot run on the new
89
+ schema, and a down-migration is not something to trust with production
90
+ data;
91
+ - the fingerprint cannot be read, which counts as changed;
92
+ - `AUTODEPLOY_MIGRATE_COMMAND=off`: the new container migrated as it
93
+ started, and the agent cannot tell a failed migration from anything
94
+ else.
95
+
96
+ It does not when the deploy stopped before anything ran against the
97
+ database (a failed backup, a container that could not be created), and when
98
+ the migration finished without changing the schema: the previous version
99
+ comes back on the same database, and nothing written while the new one
100
+ tried to start is lost. The dump is taken right before the old version
101
+ stops, so a restore loses only what the new version wrote during its
102
+ attempt.
51
103
 
52
104
  Every step is written to `/data/state.json` before it is taken. An agent
53
105
  restarted in the middle of a deploy (a reboot, a redeploy of the agent
54
- itself) finishes it if the new container turns healthy, and rolls it back
55
- otherwise.
106
+ itself) finishes it if the new container turns ready, and rolls it back
107
+ otherwise; one restarted during the migration rolls back with the
108
+ database.
56
109
 
57
110
  | State | Meaning |
58
111
  |----------------------|--------------------------------------------------------------|
@@ -126,11 +179,16 @@ with the product's names filled in.
126
179
 
127
180
  | Variable | Default | Meaning |
128
181
  |------------------------------|-------------------------------------|---------|
129
- | `AUTODEPLOY_IMAGE` | empty | Image to follow. Empty means `SPLENT_IMAGE` |
182
+ | `AUTODEPLOY_IMAGE` | empty | Image to follow. Empty means `SPLENT_IMAGE`. A channel (`:stable`) or a version tag; `:latest`, or no tag at all, is refused |
130
183
  | `AUTODEPLOY_INTERVAL` | `300` | Seconds between checks (at least 30) |
131
184
  | `AUTODEPLOY_WINDOW` | empty | UTC span for deploys, like `02:00-05:00` or `22:00-04:00`. Empty means any time |
132
- | `AUTODEPLOY_HEALTH_TIMEOUT` | `600` | Seconds the new container gets to turn healthy |
133
- | `AUTODEPLOY_KEEP_BACKUPS` | `5` | Backups kept, newest first |
185
+ | `AUTODEPLOY_HEALTH_TIMEOUT` | `600` | Seconds the migrations get, and then the new container to turn ready |
186
+ | `AUTODEPLOY_MIGRATE_COMMAND` | `splent db:upgrade` | What the one-off migration container runs. `off` leaves the migrations to the new container's start |
187
+ | `AUTODEPLOY_READY_PATH` | `/readyz` | The readiness probe. `off` waits for Docker's healthcheck alone |
188
+ | `AUTODEPLOY_WEB_PORT` | `5000` | The port the web container answers on, inside the product network |
189
+ | `AUTODEPLOY_BACKUP_COMMAND` | `auto` | The off-host backup before a deploy. `auto` is `splent_feature_backup`'s when the product has it, `off` none, anything else the command to run in the web container |
190
+ | `AUTODEPLOY_APP_USER` | `splent` | The user the migration and the backup command run as |
191
+ | `AUTODEPLOY_KEEP_BACKUPS` | `5` | Rollback dumps kept, newest first |
134
192
  | `AUTODEPLOY_WEB_CONTAINER` | `<product>_web_deploy` | The container that is replaced |
135
193
  | `AUTODEPLOY_DB_CONTAINER` | `<product>_db_deploy` | The MariaDB container that is backed up |
136
194
  | `AUTODEPLOY_AGENT_CONTAINER` | `<product>_autodeploy` | The agent's container name |
@@ -191,8 +249,10 @@ since.
191
249
 
192
250
  - The agent mounts `/var/run/docker.sock`. Whoever controls that socket is
193
251
  root on the host, so the agent is as sensitive as the host itself. It runs
194
- only in production, from the image `product:deploy` builds out of this
195
- feature's `docker/` directory, and does only what this README describes.
252
+ only in production, in a container of its own, from the image
253
+ `product:deploy` builds out of this feature's `docker/` directory, and does
254
+ only what this README describes. No container of the app mounts the
255
+ socket: not the web container, not the migration job it creates.
196
256
  - It publishes no port. Only containers on the product network reach the API,
197
257
  and every call needs the shared secret. The secret never appears in the
198
258
  status, the history or the logs.
@@ -210,7 +270,8 @@ One per deploy, in the agent's `autodeploy_data` volume under
210
270
  so each file says which code its schema belongs to. The newest
211
271
  `AUTODEPLOY_KEEP_BACKUPS` are kept. They live on the same host as the
212
272
  database, so they protect against a bad release, not against losing the
213
- server. Copy them elsewhere if you need that.
273
+ server: that is what `splent_feature_backup`'s off-host backup, taken just
274
+ before, is for. Without that feature, copy them elsewhere if you need it.
214
275
 
215
276
  ## Restoring a backup by hand
216
277
 
@@ -237,7 +298,10 @@ To let the agent try an image it refused, pause it, delete the image id from
237
298
  feature that ships its own container), or needs a new setting without a
238
299
  default in `.env.deploy`, has to go through `splent product:deploy`; pause
239
300
  the agent while you do it. Left to the agent such a release fails its
240
- health check and is rolled back, which is safe but not what you want.
301
+ readiness check and is rolled back, which is safe but not what you want.
302
+ - It takes no database lock (`GET_LOCK`) around the migration: one agent
303
+ replaces one web container, which is stopped while the job runs, so the
304
+ job is the only migrator. Several replicas would need that lock.
241
305
  - It does not update itself. A new version of this feature reaches the
242
306
  server with `splent product:deploy`, which rebuilds the agent's image.
243
307
  - It replaces one web container, not a set of replicas.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "splent_feature_autodeploy"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  readme = "README.md"
9
9
  requires-python = ">=3.13"
10
10
  license = "LGPL-3.0-or-later"
@@ -95,7 +95,9 @@ services = []
95
95
  # The registry image to follow. Empty means SPLENT_IMAGE, the image the web
96
96
  # service runs, which is the usual choice: the server follows the channel it
97
97
  # was deployed from, git.splent.io/splent-io/<product>:stable for example,
98
- # and that tag only moves when someone runs splent product:release.
98
+ # and that tag only moves when someone runs splent product:release. Never
99
+ # :latest, which the agent refuses; the container it creates runs the
100
+ # image's digest, whatever tag it followed.
99
101
  AUTODEPLOY_IMAGE = ""
100
102
  # Seconds between two looks at the registry. A pull of an image already
101
103
  # present costs one manifest request.
@@ -103,10 +105,30 @@ AUTODEPLOY_INTERVAL = 300
103
105
  # Daily UTC span in which a deploy may start, like 02:00-05:00. It may wrap
104
106
  # past midnight (22:00-04:00). Empty means any time.
105
107
  AUTODEPLOY_WINDOW = ""
106
- # Seconds the new web container gets to report healthy. Its entrypoint runs
107
- # every feature's migrations before gunicorn starts, so this is generous.
108
+ # Seconds the migrations get, and then the new web container to answer its
109
+ # readiness probe.
108
110
  AUTODEPLOY_HEALTH_TIMEOUT = 600
109
- # Database backups kept in the agent's volume, one per deploy, newest first.
111
+ # What a one-off container of the new image runs, once, between stopping the
112
+ # old version and starting the new one. off leaves the migrations to the new
113
+ # container's start, and then a failed deploy always restores the database.
114
+ AUTODEPLOY_MIGRATE_COMMAND = "splent db:upgrade"
115
+ # The new version is ready when this answers 200, asked by the web
116
+ # container's name on its AUTODEPLOY_WEB_PORT. off waits for Docker's
117
+ # healthcheck alone. An image whose framework has no /readyz (before 1.22.0)
118
+ # answers 404, and the healthcheck decides for it.
119
+ AUTODEPLOY_READY_PATH = "/readyz"
120
+ AUTODEPLOY_WEB_PORT = 5000
121
+ # The off-host backup taken in the web container before anything changes.
122
+ # auto runs splent_feature_backup's (splent feature:backup run --tag
123
+ # pre-deploy-<image>) when the product has that feature; off runs none; any
124
+ # other value is the command to run. It failing stops the deploy.
125
+ AUTODEPLOY_BACKUP_COMMAND = "auto"
126
+ # The user the migrations and the backup command run as: the one the
127
+ # production image runs the app as, so what they write belongs to the app.
128
+ AUTODEPLOY_APP_USER = "splent"
129
+ # Database dumps kept in the agent's volume, one per deploy, newest first.
130
+ # They are the rollback's, on this host; the off-host copy is the backup
131
+ # feature's.
110
132
  AUTODEPLOY_KEEP_BACKUPS = 5
111
133
  # The web container the agent replaces, as the production compose names it.
112
134
  AUTODEPLOY_WEB_CONTAINER = "__PRODUCT___web"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: splent_feature_autodeploy
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  License-Expression: LGPL-3.0-or-later
5
5
  Requires-Python: >=3.13
6
6
  Description-Content-Type: text/markdown
@@ -12,12 +12,14 @@ Dynamic: license-file
12
12
  # splent_feature_autodeploy
13
13
 
14
14
  Release-gated continuous delivery for a SPLENT product. A product that
15
- selects this feature runs a small agent next to its web container in
16
- production. The agent follows a registry image channel, for example
17
- `git.splent.io/splent-io/<product>:stable`, and when the channel points to a
18
- new image it redeploys the product. The database is backed up first, the
19
- new version has to pass its health check, and an automatic rollback of both the container and the database when
20
- the new version does not come up.
15
+ selects this feature runs a small agent in a container of its own next to
16
+ its web container in production. The agent follows a registry image
17
+ channel, for example `git.splent.io/splent-io/<product>:stable`, and when
18
+ the channel points to a new image it redeploys the product on that image's
19
+ digest. It backs up first (off the host too, when the product has
20
+ `splent_feature_backup`), migrates once, waits for the new version's
21
+ `/readyz`, and rolls the container back, and the database when the deploy
22
+ changed it, when the new version is not ready.
21
23
 
22
24
  The channel only moves when someone runs `splent product:release`, so nothing
23
25
  reaches production that nobody released. The agent never builds anything and
@@ -29,41 +31,92 @@ Every `AUTODEPLOY_INTERVAL` seconds, and whenever an administrator asks,
29
31
  the agent goes through these steps.
30
32
 
31
33
  1. Pull the channel image. If its id is the one the web container runs,
32
- there is nothing to do.
34
+ there is nothing to do. Otherwise the image is named from now on by its
35
+ digest (`<registry>/<org>/<product>@sha256:...`), which no push can
36
+ move: that is what the new container runs and what the history records.
37
+ An image the registry knows no digest of runs by its id, just as fixed.
33
38
  2. Skip an image that failed before, until the channel points elsewhere.
34
39
  Honour the pause flag and the update window (`AUTODEPLOY_WINDOW`). A
35
40
  check somebody asks for (Check now) deploys outside the window too.
36
- 3. Back up the database. `mariadb-dump --single-transaction --routines
37
- --triggers` runs as root inside the database container, with the
38
- credentials that container already holds, and the gzipped dump is kept
39
- only if `mariadb-dump` exited cleanly and the dump ends with its
40
- `Dump completed` trailer. No complete backup, no deploy.
41
- 4. Record what the web container runs (`splent version --json`).
42
- 5. Recreate the web container on the new image with everything it was
43
- configured with, the way Watchtower does it. That is its name, its
44
- environment (the container's own values on top of the new image's), its
45
- labels, mounts and binds, port bindings, restart policy, healthcheck, and
46
- its networks with their aliases. Cmd, Entrypoint and WorkingDir follow the new image when the old
47
- container only inherited them, and are kept when it chose them (the
48
- compose file's `command:`). The old container is renamed
49
- `<name>-previous` and stopped, not removed.
50
- 6. Wait for the healthcheck, up to `AUTODEPLOY_HEALTH_TIMEOUT` seconds. The
51
- entrypoint runs every feature's migrations before gunicorn starts, so this
52
- is generous.
53
- 7. Once it is healthy, remove the previous container and write the history
54
- entry (from and to image, product version, the features whose version
55
- changed, duration).
56
- 8. If it turns unhealthy, exits, restarts or runs out of time, keep the new
57
- container's last 80 log lines, remove it, drop and recreate the database and import the
58
- backup (so tables the new migrations created do not linger), put the
59
- previous container back under its name, wait for it to be healthy, and
60
- refuse that image from then on. The history entry says `rolled_back` and
61
- carries the log lines.
41
+ 3. Back up.
42
+ - When the product has `splent_feature_backup`, run its off-host backup
43
+ in the running web container, as the app's user:
44
+ `splent feature:backup run --tag pre-deploy-<short id of the image
45
+ being replaced>` (`AUTODEPLOY_BACKUP_COMMAND`). It is encrypted to keys
46
+ the server does not hold, and it is the copy that survives the host.
47
+ - Then the agent's own dump, the one a rollback restores:
48
+ `mariadb-dump --single-transaction --routines --triggers` runs as root
49
+ inside the database container, with the credentials that container
50
+ already holds, and the gzipped dump is kept only if `mariadb-dump`
51
+ exited cleanly and the dump ends with its `Dump completed` trailer.
52
+
53
+ No complete backup, no deploy.
54
+ 4. Record what the web container runs (`splent version --json`), and a
55
+ fingerprint of the schema: every table's columns and every migration
56
+ revision (`alembic_*`, `splent_migrations`).
57
+ 5. Create the new web container on the image's digest with everything the
58
+ old one was configured with, the way Watchtower does it. That is its
59
+ name, its environment (the container's own values on top of the new
60
+ image's), its labels, mounts and binds, port bindings, restart policy,
61
+ healthcheck, and its networks with their aliases. Cmd, Entrypoint and
62
+ WorkingDir follow the new image when the old container only inherited
63
+ them, and are kept when it chose them (the compose file's `command:`).
64
+ The old container is renamed `<name>-previous` and stopped, not removed.
65
+ 6. Migrate, once: a one-off container of the new image, `<name>-migrate`,
66
+ with the web container's environment, mounts and networks, runs
67
+ `AUTODEPLOY_MIGRATE_COMMAND` (`splent db:upgrade`) as the app's user
68
+ while neither version serves. It answers to no alias, so nginx never
69
+ finds it, publishes no port and is removed once it exits. The new
70
+ container's entrypoint upgrades again as it starts and finds nothing
71
+ left to do.
72
+ 7. Start the new container and wait, up to `AUTODEPLOY_HEALTH_TIMEOUT`
73
+ seconds, for `/readyz` to answer 200 (`AUTODEPLOY_READY_PATH`, asked by
74
+ the container's name on `AUTODEPLOY_WEB_PORT`), while Docker's
75
+ healthcheck does not say unhealthy. `/readyz` is the framework's
76
+ (1.22.0 and later): 200 when the database and the services the product
77
+ declares answer. An image without it answers 404, and the healthcheck
78
+ decides for it.
79
+ 8. Once it is ready, remove the previous container and write the history
80
+ entry (from and to image, the digest, product version, the features
81
+ whose version changed, the off-host backup, duration).
82
+ 9. If the migration fails, or the new container turns unhealthy, exits,
83
+ restarts or is not ready in time, keep its last 80 log lines (the
84
+ migration's, when that is what failed), remove it, put the database
85
+ back when the rule below says so, put the previous container back under
86
+ its name, wait for it to be healthy, and refuse that image from then on.
87
+ The history entry says `rolled_back`, carries the log lines and says
88
+ whether the database went back and why.
89
+
90
+ ### When the database goes back
91
+
92
+ A rollback restores the agent's pre-deploy dump (dropping the database
93
+ first, so tables the new migrations created do not linger) when:
94
+
95
+ - the migration failed, or the agent died during it. MariaDB commits DDL as
96
+ it goes, so a failed migration can leave half of its changes behind
97
+ without recording them;
98
+ - the migration finished, and the schema fingerprint differs from the one
99
+ taken before the deploy. The previous version cannot run on the new
100
+ schema, and a down-migration is not something to trust with production
101
+ data;
102
+ - the fingerprint cannot be read, which counts as changed;
103
+ - `AUTODEPLOY_MIGRATE_COMMAND=off`: the new container migrated as it
104
+ started, and the agent cannot tell a failed migration from anything
105
+ else.
106
+
107
+ It does not when the deploy stopped before anything ran against the
108
+ database (a failed backup, a container that could not be created), and when
109
+ the migration finished without changing the schema: the previous version
110
+ comes back on the same database, and nothing written while the new one
111
+ tried to start is lost. The dump is taken right before the old version
112
+ stops, so a restore loses only what the new version wrote during its
113
+ attempt.
62
114
 
63
115
  Every step is written to `/data/state.json` before it is taken. An agent
64
116
  restarted in the middle of a deploy (a reboot, a redeploy of the agent
65
- itself) finishes it if the new container turns healthy, and rolls it back
66
- otherwise.
117
+ itself) finishes it if the new container turns ready, and rolls it back
118
+ otherwise; one restarted during the migration rolls back with the
119
+ database.
67
120
 
68
121
  | State | Meaning |
69
122
  |----------------------|--------------------------------------------------------------|
@@ -137,11 +190,16 @@ with the product's names filled in.
137
190
 
138
191
  | Variable | Default | Meaning |
139
192
  |------------------------------|-------------------------------------|---------|
140
- | `AUTODEPLOY_IMAGE` | empty | Image to follow. Empty means `SPLENT_IMAGE` |
193
+ | `AUTODEPLOY_IMAGE` | empty | Image to follow. Empty means `SPLENT_IMAGE`. A channel (`:stable`) or a version tag; `:latest`, or no tag at all, is refused |
141
194
  | `AUTODEPLOY_INTERVAL` | `300` | Seconds between checks (at least 30) |
142
195
  | `AUTODEPLOY_WINDOW` | empty | UTC span for deploys, like `02:00-05:00` or `22:00-04:00`. Empty means any time |
143
- | `AUTODEPLOY_HEALTH_TIMEOUT` | `600` | Seconds the new container gets to turn healthy |
144
- | `AUTODEPLOY_KEEP_BACKUPS` | `5` | Backups kept, newest first |
196
+ | `AUTODEPLOY_HEALTH_TIMEOUT` | `600` | Seconds the migrations get, and then the new container to turn ready |
197
+ | `AUTODEPLOY_MIGRATE_COMMAND` | `splent db:upgrade` | What the one-off migration container runs. `off` leaves the migrations to the new container's start |
198
+ | `AUTODEPLOY_READY_PATH` | `/readyz` | The readiness probe. `off` waits for Docker's healthcheck alone |
199
+ | `AUTODEPLOY_WEB_PORT` | `5000` | The port the web container answers on, inside the product network |
200
+ | `AUTODEPLOY_BACKUP_COMMAND` | `auto` | The off-host backup before a deploy. `auto` is `splent_feature_backup`'s when the product has it, `off` none, anything else the command to run in the web container |
201
+ | `AUTODEPLOY_APP_USER` | `splent` | The user the migration and the backup command run as |
202
+ | `AUTODEPLOY_KEEP_BACKUPS` | `5` | Rollback dumps kept, newest first |
145
203
  | `AUTODEPLOY_WEB_CONTAINER` | `<product>_web_deploy` | The container that is replaced |
146
204
  | `AUTODEPLOY_DB_CONTAINER` | `<product>_db_deploy` | The MariaDB container that is backed up |
147
205
  | `AUTODEPLOY_AGENT_CONTAINER` | `<product>_autodeploy` | The agent's container name |
@@ -202,8 +260,10 @@ since.
202
260
 
203
261
  - The agent mounts `/var/run/docker.sock`. Whoever controls that socket is
204
262
  root on the host, so the agent is as sensitive as the host itself. It runs
205
- only in production, from the image `product:deploy` builds out of this
206
- feature's `docker/` directory, and does only what this README describes.
263
+ only in production, in a container of its own, from the image
264
+ `product:deploy` builds out of this feature's `docker/` directory, and does
265
+ only what this README describes. No container of the app mounts the
266
+ socket: not the web container, not the migration job it creates.
207
267
  - It publishes no port. Only containers on the product network reach the API,
208
268
  and every call needs the shared secret. The secret never appears in the
209
269
  status, the history or the logs.
@@ -221,7 +281,8 @@ One per deploy, in the agent's `autodeploy_data` volume under
221
281
  so each file says which code its schema belongs to. The newest
222
282
  `AUTODEPLOY_KEEP_BACKUPS` are kept. They live on the same host as the
223
283
  database, so they protect against a bad release, not against losing the
224
- server. Copy them elsewhere if you need that.
284
+ server: that is what `splent_feature_backup`'s off-host backup, taken just
285
+ before, is for. Without that feature, copy them elsewhere if you need it.
225
286
 
226
287
  ## Restoring a backup by hand
227
288
 
@@ -248,7 +309,10 @@ To let the agent try an image it refused, pause it, delete the image id from
248
309
  feature that ships its own container), or needs a new setting without a
249
310
  default in `.env.deploy`, has to go through `splent product:deploy`; pause
250
311
  the agent while you do it. Left to the agent such a release fails its
251
- health check and is rolled back, which is safe but not what you want.
312
+ readiness check and is rolled back, which is safe but not what you want.
313
+ - It takes no database lock (`GET_LOCK`) around the migration: one agent
314
+ replaces one web container, which is stopped while the job runs, so the
315
+ job is the only migrator. Several replicas would need that lock.
252
316
  - It does not update itself. A new version of this feature reaches the
253
317
  server with `splent product:deploy`, which rebuilds the agent's image.
254
318
  - It replaces one web container, not a set of replicas.
@@ -129,8 +129,15 @@ def history(limit, as_json):
129
129
  f" {change.get('name')} {change.get('from') or '(new)'} -> "
130
130
  f"{change.get('to') or '(removed)'}"
131
131
  )
132
+ if entry.get("to_ref"):
133
+ click.echo(f" image {entry['to_ref']}")
134
+ if entry.get("offsite_backup"):
135
+ click.echo(f" off-host backup {entry['offsite_backup']}")
132
136
  if entry.get("reason"):
133
137
  click.echo(f" reason {entry['reason']}")
138
+ if "database_restored" in entry:
139
+ state = "restored" if entry["database_restored"] else "kept"
140
+ click.echo(f" database {state}: {entry.get('database_decision')}")
134
141
  for problem in entry.get("problems") or []:
135
142
  click.secho(f" problem {problem}", fg="red")
136
143
  click.echo()