splent-feature-autodeploy 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (22) hide show
  1. splent_feature_autodeploy-0.1.0/MANIFEST.in +10 -0
  2. splent_feature_autodeploy-0.1.0/PKG-INFO +265 -0
  3. splent_feature_autodeploy-0.1.0/README.md +259 -0
  4. splent_feature_autodeploy-0.1.0/pyproject.toml +112 -0
  5. splent_feature_autodeploy-0.1.0/setup.cfg +4 -0
  6. splent_feature_autodeploy-0.1.0/src/splent_feature_autodeploy.egg-info/PKG-INFO +265 -0
  7. splent_feature_autodeploy-0.1.0/src/splent_feature_autodeploy.egg-info/SOURCES.txt +20 -0
  8. splent_feature_autodeploy-0.1.0/src/splent_feature_autodeploy.egg-info/dependency_links.txt +1 -0
  9. splent_feature_autodeploy-0.1.0/src/splent_feature_autodeploy.egg-info/top_level.txt +1 -0
  10. splent_feature_autodeploy-0.1.0/src/splent_io/splent_feature_autodeploy/__init__.py +14 -0
  11. splent_feature_autodeploy-0.1.0/src/splent_io/splent_feature_autodeploy/commands.py +163 -0
  12. splent_feature_autodeploy-0.1.0/src/splent_io/splent_feature_autodeploy/config.py +33 -0
  13. splent_feature_autodeploy-0.1.0/src/splent_io/splent_feature_autodeploy/forms.py +6 -0
  14. splent_feature_autodeploy-0.1.0/src/splent_io/splent_feature_autodeploy/hooks.py +29 -0
  15. splent_feature_autodeploy-0.1.0/src/splent_io/splent_feature_autodeploy/presenters.py +198 -0
  16. splent_feature_autodeploy-0.1.0/src/splent_io/splent_feature_autodeploy/routes.py +125 -0
  17. splent_feature_autodeploy-0.1.0/src/splent_io/splent_feature_autodeploy/services.py +168 -0
  18. splent_feature_autodeploy-0.1.0/src/splent_io/splent_feature_autodeploy/templates/autodeploy/index.html +220 -0
  19. splent_feature_autodeploy-0.1.0/src/splent_io/splent_feature_autodeploy/templates/hooks/sidebar_deployments.html +5 -0
  20. splent_feature_autodeploy-0.1.0/src/splent_io/splent_feature_autodeploy/translations/es/LC_MESSAGES/messages.mo +0 -0
  21. splent_feature_autodeploy-0.1.0/src/splent_io/splent_feature_autodeploy/translations/es/LC_MESSAGES/messages.po +489 -0
  22. splent_feature_autodeploy-0.1.0/src/splent_io/splent_feature_autodeploy/translations/messages.pot +425 -0
@@ -0,0 +1,10 @@
1
+ # Jinja templates (feature views + hook fragments)
2
+ recursive-include src/splent_io/splent_feature_autodeploy/templates *.html
3
+
4
+ # Translations. The .mo is what gettext reads at runtime; the .po travels
5
+ # with it so the next translator has something to edit.
6
+ recursive-include src/splent_io/splent_feature_autodeploy/translations *.mo *.po *.pot
7
+
8
+ # docker/ (the agent's Dockerfile, compose file and code) is not packaged:
9
+ # product:build reads it from the feature's checkout at the pinned release
10
+ # in .splent_cache, like every feature that ships a container.
@@ -0,0 +1,265 @@
1
+ Metadata-Version: 2.4
2
+ Name: splent_feature_autodeploy
3
+ Version: 0.1.0
4
+ Requires-Python: >=3.13
5
+ Description-Content-Type: text/markdown
6
+
7
+ # splent_feature_autodeploy
8
+
9
+ Release-gated continuous delivery for a SPLENT product. A product that
10
+ selects this feature runs a small agent next to its web container in
11
+ production. The agent follows a registry image channel, for example
12
+ `git.splent.io/splent-io/<product>:stable`, and when the channel points to a
13
+ new image it redeploys the product. The database is backed up first, the
14
+ new version has to pass its health check, and an automatic rollback of both the container and the database when
15
+ the new version does not come up.
16
+
17
+ The channel only moves when someone runs `splent product:release`, so nothing
18
+ reaches production that nobody released. The agent never builds anything and
19
+ never follows a branch.
20
+
21
+ ## How a check goes
22
+
23
+ Every `AUTODEPLOY_INTERVAL` seconds, and whenever an administrator asks,
24
+ the agent goes through these steps.
25
+
26
+ 1. Pull the channel image. If its id is the one the web container runs,
27
+ there is nothing to do.
28
+ 2. Skip an image that failed before, until the channel points elsewhere.
29
+ Honour the pause flag and the update window (`AUTODEPLOY_WINDOW`). A
30
+ check somebody asks for (Check now) deploys outside the window too.
31
+ 3. Back up the database. `mariadb-dump --single-transaction --routines
32
+ --triggers` runs as root inside the database container, with the
33
+ credentials that container already holds, and the gzipped dump is kept
34
+ only if `mariadb-dump` exited cleanly and the dump ends with its
35
+ `Dump completed` trailer. No complete backup, no deploy.
36
+ 4. Record what the web container runs (`splent version --json`).
37
+ 5. Recreate the web container on the new image with everything it was
38
+ configured with, the way Watchtower does it. That is its name, its
39
+ environment (the container's own values on top of the new image's), its
40
+ labels, mounts and binds, port bindings, restart policy, healthcheck, and
41
+ its networks with their aliases. Cmd, Entrypoint and WorkingDir follow the new image when the old
42
+ container only inherited them, and are kept when it chose them (the
43
+ compose file's `command:`). The old container is renamed
44
+ `<name>-previous` and stopped, not removed.
45
+ 6. Wait for the healthcheck, up to `AUTODEPLOY_HEALTH_TIMEOUT` seconds. The
46
+ entrypoint runs every feature's migrations before gunicorn starts, so this
47
+ is generous.
48
+ 7. Once it is healthy, remove the previous container and write the history
49
+ entry (from and to image, product version, the features whose version
50
+ changed, duration).
51
+ 8. If it turns unhealthy, exits, restarts or runs out of time, keep the new
52
+ container's last 80 log lines, remove it, drop and recreate the database and import the
53
+ backup (so tables the new migrations created do not linger), put the
54
+ previous container back under its name, wait for it to be healthy, and
55
+ refuse that image from then on. The history entry says `rolled_back` and
56
+ carries the log lines.
57
+
58
+ Every step is written to `/data/state.json` before it is taken. An agent
59
+ restarted in the middle of a deploy (a reboot, a redeploy of the agent
60
+ itself) finishes it if the new container turns healthy, and rolls it back
61
+ otherwise.
62
+
63
+ | State | Meaning |
64
+ |----------------------|--------------------------------------------------------------|
65
+ | `idle` | The web container runs the image the channel points to |
66
+ | `waiting_for_window` | A new image is ready and waits for the update window |
67
+ | `deploying` | Backup, recreate or health wait in progress |
68
+ | `rolling_back` | The new image failed and the previous version is coming back |
69
+ | `paused` | An administrator paused automatic deploys |
70
+ | `held` | The only image available failed before; waiting for the next release |
71
+ | `error` | Something needs a person, the detail says what |
72
+ | `not_configured` | A required setting is missing, the detail lists it |
73
+
74
+ ## Enable it in a product
75
+
76
+ 1. Add the feature to the product line as an optional feature, from a
77
+ detached workspace.
78
+
79
+ ```bash
80
+ splent spl:add-feature <spl> splent_feature_autodeploy
81
+ ```
82
+
83
+ In the UVL model it reads as below, with the constraint that keeps the
84
+ admin panel it lives in.
85
+
86
+ ```
87
+ optional
88
+ autodeploy {org 'splent-io', package 'splent_feature_autodeploy'}
89
+ constraints
90
+ autodeploy => admin
91
+ ```
92
+
93
+ 2. Attach it to the product and release the product, so the channel exists.
94
+
95
+ ```bash
96
+ splent product:select <product>
97
+ splent feature:attach splent-io/splent_feature_autodeploy v0.1.0
98
+ splent product:release
99
+ ```
100
+
101
+ A product release pushes `<registry>/<org>/<product>:<version>` and moves
102
+ `:stable` to it.
103
+
104
+ 3. On the server, set `SPLENT_IMAGE` in `docker/.env.deploy` to the channel
105
+ the server follows, then build and deploy. The web service of the
106
+ product's `docker-compose.prod.yml` must take its image from that
107
+ variable, as in `image: ${SPLENT_IMAGE:-<product>:latest}`.
108
+
109
+ ```bash
110
+ SPLENT_IMAGE=git.splent.io/splent-io/<product>:stable
111
+ ```
112
+
113
+ ```bash
114
+ splent product:build --no-image
115
+ splent product:deploy
116
+ ```
117
+
118
+ `product:build` adds the agent's service to `docker-compose.deploy.yml`
119
+ and copies `docker/` to `docker/features/splent_feature_autodeploy/`.
120
+ `product:deploy` offers a random value for `AUTODEPLOY_SECRET`, builds the
121
+ agent's image and starts it.
122
+
123
+ 4. For a private registry set `AUTODEPLOY_REGISTRY_USER` and
124
+ `AUTODEPLOY_REGISTRY_TOKEN` (a token that can read packages).
125
+
126
+ From then on, `splent product:release` is the whole deploy.
127
+
128
+ ## Configuration
129
+
130
+ Declared in `[tool.splent.config]`, so they appear in `.env.deploy.example`
131
+ with the product's names filled in.
132
+
133
+ | Variable | Default | Meaning |
134
+ |------------------------------|-------------------------------------|---------|
135
+ | `AUTODEPLOY_IMAGE` | empty | Image to follow. Empty means `SPLENT_IMAGE` |
136
+ | `AUTODEPLOY_INTERVAL` | `300` | Seconds between checks (at least 30) |
137
+ | `AUTODEPLOY_WINDOW` | empty | UTC span for deploys, like `02:00-05:00` or `22:00-04:00`. Empty means any time |
138
+ | `AUTODEPLOY_HEALTH_TIMEOUT` | `600` | Seconds the new container gets to turn healthy |
139
+ | `AUTODEPLOY_KEEP_BACKUPS` | `5` | Backups kept, newest first |
140
+ | `AUTODEPLOY_WEB_CONTAINER` | `<product>_web_deploy` | The container that is replaced |
141
+ | `AUTODEPLOY_DB_CONTAINER` | `<product>_db_deploy` | The MariaDB container that is backed up |
142
+ | `AUTODEPLOY_AGENT_CONTAINER` | `<product>_autodeploy` | The agent's container name |
143
+ | `AUTODEPLOY_URL` | `http://<product>_autodeploy:8080` | Where the web side finds the agent |
144
+ | `AUTODEPLOY_REGISTRY_USER` | empty | Registry account, for a private channel |
145
+ | `AUTODEPLOY_REGISTRY_TOKEN` | empty | Registry token, for a private channel |
146
+ | `AUTODEPLOY_SECRET` | `<SET>` | Shared by the web container and the agent |
147
+
148
+ ## The Deployments page
149
+
150
+ `/admin/deployments`, in the admin panel's sidebar, for administrators only
151
+ (the admin feature's `ADMIN_ROLES`). It shows the channel, the running image
152
+ and product version, what the agent is doing and why, the last check, the
153
+ update window, whether updates are paused, and the latest deploys with the
154
+ features each one moved. A rolled back deploy shows its log lines in a
155
+ collapsible block, and a banner says so until the next deploy. Check now,
156
+ Pause and Resume are CSRF protected forms.
157
+
158
+ When the agent is missing (development), stopped or refuses the secret the
159
+ page explains what is going on. The site itself is never affected.
160
+
161
+ ## Commands
162
+
163
+ ```bash
164
+ splent feature:autodeploy status [--json]
165
+ splent feature:autodeploy history [--limit N] [--json]
166
+ splent feature:autodeploy check
167
+ splent feature:autodeploy pause
168
+ splent feature:autodeploy resume
169
+ ```
170
+
171
+ They talk to the agent's API, so they need the product network. On a
172
+ production server run them in the web container.
173
+
174
+ ```bash
175
+ docker exec <product>_web_deploy splent feature:autodeploy status
176
+ ```
177
+
178
+ ## The agent's API
179
+
180
+ Port 8080 on the product network, never published on the host. Every request
181
+ carries `Authorization: Bearer <AUTODEPLOY_SECRET>`, compared in constant
182
+ time; anything else is a 401, whatever the path.
183
+
184
+ | Method | Path | Does |
185
+ |--------|--------------------|------|
186
+ | GET | `/status` | State, configuration, running versions, backups |
187
+ | GET | `/history?limit=N` | The newest N deploys, newest first (1 to 200) |
188
+ | POST | `/check` | Look for a new image now |
189
+ | POST | `/pause` | Stop deploying; a deploy in progress finishes |
190
+ | POST | `/resume` | Deploy again, and check right away |
191
+
192
+ There is no rollback endpoint on purpose. A rollback restores the backup taken
193
+ before the deploy, so run later by hand it would throw away everything written
194
+ since.
195
+
196
+ ## Security
197
+
198
+ - The agent mounts `/var/run/docker.sock`. Whoever controls that socket is
199
+ root on the host, so the agent is as sensitive as the host itself. It runs
200
+ only in production, from the image `product:deploy` builds out of this
201
+ feature's `docker/` directory, and does only what this README describes.
202
+ - It publishes no port. Only containers on the product network reach the API,
203
+ and every call needs the shared secret. The secret never appears in the
204
+ status, the history or the logs.
205
+ - The web side calls the agent without any proxy, so an `HTTP_PROXY` in the
206
+ environment never sees the secret.
207
+ - The database credentials stay in the database container. The dump and the
208
+ restore read them from that container's environment and pass them through
209
+ `MYSQL_PWD`, so they are not on any command line and are never printed.
210
+ - Registry credentials are only sent to the registry, with the pull.
211
+
212
+ ## Backups
213
+
214
+ One per deploy, in the agent's `autodeploy_data` volume under
215
+ `/data/backups/`, named `<UTC time>-<short id of the image being replaced>.sql.gz`,
216
+ so each file says which code its schema belongs to. The newest
217
+ `AUTODEPLOY_KEEP_BACKUPS` are kept. They live on the same host as the
218
+ database, so they protect against a bad release, not against losing the
219
+ server. Copy them elsewhere if you need that.
220
+
221
+ ## Restoring a backup by hand
222
+
223
+ Restoring a backup loses everything written after it was taken. Pause the
224
+ agent first, so it does not deploy in the middle.
225
+
226
+ ```bash
227
+ docker exec <product>_web_deploy splent feature:autodeploy pause
228
+ docker cp <product>_autodeploy:/data/backups/<file>.sql.gz .
229
+ docker stop <product>_web_deploy
230
+ docker exec <product>_db_deploy bash -c 'MYSQL_PWD="$MARIADB_ROOT_PASSWORD" mariadb -uroot -e "DROP DATABASE IF EXISTS \`$MARIADB_DATABASE\`; CREATE DATABASE \`$MARIADB_DATABASE\`"'
231
+ gunzip -c <file>.sql.gz | docker exec -i <product>_db_deploy bash -c 'MYSQL_PWD="$MARIADB_ROOT_PASSWORD" mariadb -uroot "$MARIADB_DATABASE"'
232
+ ```
233
+
234
+ Then run a web container on the image named in the file name, so code and
235
+ schema match, and resume the agent.
236
+
237
+ To let the agent try an image it refused, pause it, delete the image id from
238
+ `bad_images` in `/data/state.json` in the agent's volume, and resume.
239
+
240
+ ## What the agent does not do
241
+
242
+ - It replaces the web container only. A release that adds a service (a
243
+ feature that ships its own container), or needs a new setting without a
244
+ default in `.env.deploy`, has to go through `splent product:deploy`; pause
245
+ the agent while you do it. Left to the agent such a release fails its
246
+ health check and is rolled back, which is safe but not what you want.
247
+ - It does not update itself. A new version of this feature reaches the
248
+ server with `splent product:deploy`, which rebuilds the agent's image.
249
+ - It replaces one web container, not a set of replicas.
250
+
251
+ ## Develop
252
+
253
+ The agent is plain Python 3.13 with docker-py, in `docker/autodeploy_agent/`.
254
+ It gets the Docker client handed in, so its tests run against a fake Engine.
255
+
256
+ ```bash
257
+ splent feature:test splent_feature_autodeploy
258
+ ```
259
+
260
+ From a bare checkout, with a Python that has the framework, Flask-Babel,
261
+ Flask-Login and Flask-WTF installed, the same tests run without a product.
262
+
263
+ ```bash
264
+ python -m pytest src/splent_io/splent_feature_autodeploy/tests
265
+ ```
@@ -0,0 +1,259 @@
1
+ # splent_feature_autodeploy
2
+
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.
10
+
11
+ The channel only moves when someone runs `splent product:release`, so nothing
12
+ reaches production that nobody released. The agent never builds anything and
13
+ never follows a branch.
14
+
15
+ ## How a check goes
16
+
17
+ Every `AUTODEPLOY_INTERVAL` seconds, and whenever an administrator asks,
18
+ the agent goes through these steps.
19
+
20
+ 1. Pull the channel image. If its id is the one the web container runs,
21
+ there is nothing to do.
22
+ 2. Skip an image that failed before, until the channel points elsewhere.
23
+ Honour the pause flag and the update window (`AUTODEPLOY_WINDOW`). A
24
+ 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.
51
+
52
+ Every step is written to `/data/state.json` before it is taken. An agent
53
+ 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.
56
+
57
+ | State | Meaning |
58
+ |----------------------|--------------------------------------------------------------|
59
+ | `idle` | The web container runs the image the channel points to |
60
+ | `waiting_for_window` | A new image is ready and waits for the update window |
61
+ | `deploying` | Backup, recreate or health wait in progress |
62
+ | `rolling_back` | The new image failed and the previous version is coming back |
63
+ | `paused` | An administrator paused automatic deploys |
64
+ | `held` | The only image available failed before; waiting for the next release |
65
+ | `error` | Something needs a person, the detail says what |
66
+ | `not_configured` | A required setting is missing, the detail lists it |
67
+
68
+ ## Enable it in a product
69
+
70
+ 1. Add the feature to the product line as an optional feature, from a
71
+ detached workspace.
72
+
73
+ ```bash
74
+ splent spl:add-feature <spl> splent_feature_autodeploy
75
+ ```
76
+
77
+ In the UVL model it reads as below, with the constraint that keeps the
78
+ admin panel it lives in.
79
+
80
+ ```
81
+ optional
82
+ autodeploy {org 'splent-io', package 'splent_feature_autodeploy'}
83
+ constraints
84
+ autodeploy => admin
85
+ ```
86
+
87
+ 2. Attach it to the product and release the product, so the channel exists.
88
+
89
+ ```bash
90
+ splent product:select <product>
91
+ splent feature:attach splent-io/splent_feature_autodeploy v0.1.0
92
+ splent product:release
93
+ ```
94
+
95
+ A product release pushes `<registry>/<org>/<product>:<version>` and moves
96
+ `:stable` to it.
97
+
98
+ 3. On the server, set `SPLENT_IMAGE` in `docker/.env.deploy` to the channel
99
+ the server follows, then build and deploy. The web service of the
100
+ product's `docker-compose.prod.yml` must take its image from that
101
+ variable, as in `image: ${SPLENT_IMAGE:-<product>:latest}`.
102
+
103
+ ```bash
104
+ SPLENT_IMAGE=git.splent.io/splent-io/<product>:stable
105
+ ```
106
+
107
+ ```bash
108
+ splent product:build --no-image
109
+ splent product:deploy
110
+ ```
111
+
112
+ `product:build` adds the agent's service to `docker-compose.deploy.yml`
113
+ and copies `docker/` to `docker/features/splent_feature_autodeploy/`.
114
+ `product:deploy` offers a random value for `AUTODEPLOY_SECRET`, builds the
115
+ agent's image and starts it.
116
+
117
+ 4. For a private registry set `AUTODEPLOY_REGISTRY_USER` and
118
+ `AUTODEPLOY_REGISTRY_TOKEN` (a token that can read packages).
119
+
120
+ From then on, `splent product:release` is the whole deploy.
121
+
122
+ ## Configuration
123
+
124
+ Declared in `[tool.splent.config]`, so they appear in `.env.deploy.example`
125
+ with the product's names filled in.
126
+
127
+ | Variable | Default | Meaning |
128
+ |------------------------------|-------------------------------------|---------|
129
+ | `AUTODEPLOY_IMAGE` | empty | Image to follow. Empty means `SPLENT_IMAGE` |
130
+ | `AUTODEPLOY_INTERVAL` | `300` | Seconds between checks (at least 30) |
131
+ | `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 |
134
+ | `AUTODEPLOY_WEB_CONTAINER` | `<product>_web_deploy` | The container that is replaced |
135
+ | `AUTODEPLOY_DB_CONTAINER` | `<product>_db_deploy` | The MariaDB container that is backed up |
136
+ | `AUTODEPLOY_AGENT_CONTAINER` | `<product>_autodeploy` | The agent's container name |
137
+ | `AUTODEPLOY_URL` | `http://<product>_autodeploy:8080` | Where the web side finds the agent |
138
+ | `AUTODEPLOY_REGISTRY_USER` | empty | Registry account, for a private channel |
139
+ | `AUTODEPLOY_REGISTRY_TOKEN` | empty | Registry token, for a private channel |
140
+ | `AUTODEPLOY_SECRET` | `<SET>` | Shared by the web container and the agent |
141
+
142
+ ## The Deployments page
143
+
144
+ `/admin/deployments`, in the admin panel's sidebar, for administrators only
145
+ (the admin feature's `ADMIN_ROLES`). It shows the channel, the running image
146
+ and product version, what the agent is doing and why, the last check, the
147
+ update window, whether updates are paused, and the latest deploys with the
148
+ features each one moved. A rolled back deploy shows its log lines in a
149
+ collapsible block, and a banner says so until the next deploy. Check now,
150
+ Pause and Resume are CSRF protected forms.
151
+
152
+ When the agent is missing (development), stopped or refuses the secret the
153
+ page explains what is going on. The site itself is never affected.
154
+
155
+ ## Commands
156
+
157
+ ```bash
158
+ splent feature:autodeploy status [--json]
159
+ splent feature:autodeploy history [--limit N] [--json]
160
+ splent feature:autodeploy check
161
+ splent feature:autodeploy pause
162
+ splent feature:autodeploy resume
163
+ ```
164
+
165
+ They talk to the agent's API, so they need the product network. On a
166
+ production server run them in the web container.
167
+
168
+ ```bash
169
+ docker exec <product>_web_deploy splent feature:autodeploy status
170
+ ```
171
+
172
+ ## The agent's API
173
+
174
+ Port 8080 on the product network, never published on the host. Every request
175
+ carries `Authorization: Bearer <AUTODEPLOY_SECRET>`, compared in constant
176
+ time; anything else is a 401, whatever the path.
177
+
178
+ | Method | Path | Does |
179
+ |--------|--------------------|------|
180
+ | GET | `/status` | State, configuration, running versions, backups |
181
+ | GET | `/history?limit=N` | The newest N deploys, newest first (1 to 200) |
182
+ | POST | `/check` | Look for a new image now |
183
+ | POST | `/pause` | Stop deploying; a deploy in progress finishes |
184
+ | POST | `/resume` | Deploy again, and check right away |
185
+
186
+ There is no rollback endpoint on purpose. A rollback restores the backup taken
187
+ before the deploy, so run later by hand it would throw away everything written
188
+ since.
189
+
190
+ ## Security
191
+
192
+ - The agent mounts `/var/run/docker.sock`. Whoever controls that socket is
193
+ 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.
196
+ - It publishes no port. Only containers on the product network reach the API,
197
+ and every call needs the shared secret. The secret never appears in the
198
+ status, the history or the logs.
199
+ - The web side calls the agent without any proxy, so an `HTTP_PROXY` in the
200
+ environment never sees the secret.
201
+ - The database credentials stay in the database container. The dump and the
202
+ restore read them from that container's environment and pass them through
203
+ `MYSQL_PWD`, so they are not on any command line and are never printed.
204
+ - Registry credentials are only sent to the registry, with the pull.
205
+
206
+ ## Backups
207
+
208
+ One per deploy, in the agent's `autodeploy_data` volume under
209
+ `/data/backups/`, named `<UTC time>-<short id of the image being replaced>.sql.gz`,
210
+ so each file says which code its schema belongs to. The newest
211
+ `AUTODEPLOY_KEEP_BACKUPS` are kept. They live on the same host as the
212
+ database, so they protect against a bad release, not against losing the
213
+ server. Copy them elsewhere if you need that.
214
+
215
+ ## Restoring a backup by hand
216
+
217
+ Restoring a backup loses everything written after it was taken. Pause the
218
+ agent first, so it does not deploy in the middle.
219
+
220
+ ```bash
221
+ docker exec <product>_web_deploy splent feature:autodeploy pause
222
+ docker cp <product>_autodeploy:/data/backups/<file>.sql.gz .
223
+ docker stop <product>_web_deploy
224
+ docker exec <product>_db_deploy bash -c 'MYSQL_PWD="$MARIADB_ROOT_PASSWORD" mariadb -uroot -e "DROP DATABASE IF EXISTS \`$MARIADB_DATABASE\`; CREATE DATABASE \`$MARIADB_DATABASE\`"'
225
+ gunzip -c <file>.sql.gz | docker exec -i <product>_db_deploy bash -c 'MYSQL_PWD="$MARIADB_ROOT_PASSWORD" mariadb -uroot "$MARIADB_DATABASE"'
226
+ ```
227
+
228
+ Then run a web container on the image named in the file name, so code and
229
+ schema match, and resume the agent.
230
+
231
+ To let the agent try an image it refused, pause it, delete the image id from
232
+ `bad_images` in `/data/state.json` in the agent's volume, and resume.
233
+
234
+ ## What the agent does not do
235
+
236
+ - It replaces the web container only. A release that adds a service (a
237
+ feature that ships its own container), or needs a new setting without a
238
+ default in `.env.deploy`, has to go through `splent product:deploy`; pause
239
+ 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.
241
+ - It does not update itself. A new version of this feature reaches the
242
+ server with `splent product:deploy`, which rebuilds the agent's image.
243
+ - It replaces one web container, not a set of replicas.
244
+
245
+ ## Develop
246
+
247
+ The agent is plain Python 3.13 with docker-py, in `docker/autodeploy_agent/`.
248
+ It gets the Docker client handed in, so its tests run against a fake Engine.
249
+
250
+ ```bash
251
+ splent feature:test splent_feature_autodeploy
252
+ ```
253
+
254
+ From a bare checkout, with a Python that has the framework, Flask-Babel,
255
+ Flask-Login and Flask-WTF installed, the same tests run without a product.
256
+
257
+ ```bash
258
+ python -m pytest src/splent_io/splent_feature_autodeploy/tests
259
+ ```
@@ -0,0 +1,112 @@
1
+ [build-system]
2
+ requires = ["setuptools>=80.3.1", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "splent_feature_autodeploy"
7
+ version = "0.1.0"
8
+ readme = "README.md"
9
+ requires-python = ">=3.13"
10
+
11
+ # The web side talks to the agent with the standard library. The agent's own
12
+ # dependency (docker-py) is installed in its image by docker/Dockerfile and
13
+ # is not needed here.
14
+ dependencies = [
15
+
16
+ ]
17
+
18
+ [tool.setuptools]
19
+ package-dir = { "" = "src" }
20
+ include-package-data = true
21
+
22
+ [tool.setuptools.packages.find]
23
+ where = ["src"]
24
+ exclude = ["*.tests", "*.tests.*"]
25
+
26
+ [tool.splent]
27
+ cli_version = "1.28.2"
28
+
29
+ # ── Feature Contract (auto-generated) ────────────────────────────────────────
30
+ # Do not edit manually — re-run `splent feature:contract --write` to refresh.
31
+ # Preserved across refreshes: description, category, tags, env,
32
+ # requires.features_manual (dependencies invisible to static analysis)
33
+ # and provides.routes_manual (routes built from configuration at
34
+ # registration time, which have no literal path to find in source).
35
+ [tool.splent.contract]
36
+ description = "Release-gated continuous delivery: follows a registry image channel and redeploys the product safely, with backup, health check and rollback"
37
+ archetype = "light"
38
+ category = "operations"
39
+ tags = ["deploy", "docker", "release", "backup", "admin"]
40
+
41
+ [tool.splent.contract.provides]
42
+ routes = ["/admin/deployments", "/admin/deployments/check", "/admin/deployments/pause", "/admin/deployments/resume"]
43
+ blueprints = ["autodeploy_bp"]
44
+ models = []
45
+ commands = ["check", "history", "pause", "resume", "status"]
46
+ hooks = ["layout.authenticated_sidebar"]
47
+ services = ["AutodeployService"]
48
+ signals = []
49
+ translations = ["es"]
50
+ docker = ["docker-compose.prod.yml"]
51
+
52
+ [tool.splent.contract.requires]
53
+ features = ["admin", "auth"]
54
+ features_manual = ["auth"]
55
+ env_vars = ["AUTODEPLOY_IMAGE", "AUTODEPLOY_INTERVAL", "AUTODEPLOY_SECRET", "AUTODEPLOY_URL", "AUTODEPLOY_WINDOW"]
56
+ signals = []
57
+
58
+ [tool.splent.contract.extensible]
59
+ services = ["AutodeployService"]
60
+ templates = ["autodeploy/index.html", "hooks/sidebar_deployments.html"]
61
+ models = []
62
+ hooks = ["layout.authenticated_sidebar"]
63
+ routes = true
64
+
65
+ [tool.splent.contract.docker]
66
+ services = ["splent_feature_autodeploy"]
67
+ ports = []
68
+ volumes = ["autodeploy_data"]
69
+ networks = ["splent_network"]
70
+ build = true
71
+ healthcheck = true
72
+
73
+ [tool.splent.contract.docker.depends_on]
74
+ services = []
75
+
76
+ # ── Configuration ─────────────────────────────────────────────────────────────
77
+ # What this feature reads from the environment, and what it falls back to.
78
+ # product:build writes these into .env.deploy.example, resolving
79
+ # __PRODUCT___web to <product>_web_deploy and __PRODUCT__ to the product name,
80
+ # and product:deploy hands .env.deploy to Compose, so the agent's compose file
81
+ # interpolates them.
82
+ [tool.splent.config]
83
+ # The registry image to follow. Empty means SPLENT_IMAGE, the image the web
84
+ # service runs, which is the usual choice: the server follows the channel it
85
+ # was deployed from, git.splent.io/splent-io/<product>:stable for example,
86
+ # and that tag only moves when someone runs splent product:release.
87
+ AUTODEPLOY_IMAGE = ""
88
+ # Seconds between two looks at the registry. A pull of an image already
89
+ # present costs one manifest request.
90
+ AUTODEPLOY_INTERVAL = 300
91
+ # Daily UTC span in which a deploy may start, like 02:00-05:00. It may wrap
92
+ # past midnight (22:00-04:00). Empty means any time.
93
+ AUTODEPLOY_WINDOW = ""
94
+ # Seconds the new web container gets to report healthy. Its entrypoint runs
95
+ # every feature's migrations before gunicorn starts, so this is generous.
96
+ AUTODEPLOY_HEALTH_TIMEOUT = 600
97
+ # Database backups kept in the agent's volume, one per deploy, newest first.
98
+ AUTODEPLOY_KEEP_BACKUPS = 5
99
+ # The containers the agent works on, as the production compose names them.
100
+ AUTODEPLOY_WEB_CONTAINER = "__PRODUCT___web"
101
+ AUTODEPLOY_DB_CONTAINER = "__PRODUCT___db_deploy"
102
+ # The agent's own container, and where the web side finds its API on the
103
+ # product network. No port is published on the host.
104
+ AUTODEPLOY_AGENT_CONTAINER = "__PRODUCT___autodeploy"
105
+ AUTODEPLOY_URL = "http://__PRODUCT___autodeploy:8080"
106
+ # Credentials for a private registry. Empty for a public channel. A token
107
+ # with read access to packages is enough.
108
+ AUTODEPLOY_REGISTRY_USER = ""
109
+ AUTODEPLOY_REGISTRY_TOKEN = ""
110
+ # Shared by the web container and the agent; every API call carries it.
111
+ # product:deploy offers a random value for it.
112
+ AUTODEPLOY_SECRET = "<SET>"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+