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.
- splent_feature_autodeploy-0.1.0/MANIFEST.in +10 -0
- splent_feature_autodeploy-0.1.0/PKG-INFO +265 -0
- splent_feature_autodeploy-0.1.0/README.md +259 -0
- splent_feature_autodeploy-0.1.0/pyproject.toml +112 -0
- splent_feature_autodeploy-0.1.0/setup.cfg +4 -0
- splent_feature_autodeploy-0.1.0/src/splent_feature_autodeploy.egg-info/PKG-INFO +265 -0
- splent_feature_autodeploy-0.1.0/src/splent_feature_autodeploy.egg-info/SOURCES.txt +20 -0
- splent_feature_autodeploy-0.1.0/src/splent_feature_autodeploy.egg-info/dependency_links.txt +1 -0
- splent_feature_autodeploy-0.1.0/src/splent_feature_autodeploy.egg-info/top_level.txt +1 -0
- splent_feature_autodeploy-0.1.0/src/splent_io/splent_feature_autodeploy/__init__.py +14 -0
- splent_feature_autodeploy-0.1.0/src/splent_io/splent_feature_autodeploy/commands.py +163 -0
- splent_feature_autodeploy-0.1.0/src/splent_io/splent_feature_autodeploy/config.py +33 -0
- splent_feature_autodeploy-0.1.0/src/splent_io/splent_feature_autodeploy/forms.py +6 -0
- splent_feature_autodeploy-0.1.0/src/splent_io/splent_feature_autodeploy/hooks.py +29 -0
- splent_feature_autodeploy-0.1.0/src/splent_io/splent_feature_autodeploy/presenters.py +198 -0
- splent_feature_autodeploy-0.1.0/src/splent_io/splent_feature_autodeploy/routes.py +125 -0
- splent_feature_autodeploy-0.1.0/src/splent_io/splent_feature_autodeploy/services.py +168 -0
- splent_feature_autodeploy-0.1.0/src/splent_io/splent_feature_autodeploy/templates/autodeploy/index.html +220 -0
- splent_feature_autodeploy-0.1.0/src/splent_io/splent_feature_autodeploy/templates/hooks/sidebar_deployments.html +5 -0
- splent_feature_autodeploy-0.1.0/src/splent_io/splent_feature_autodeploy/translations/es/LC_MESSAGES/messages.mo +0 -0
- splent_feature_autodeploy-0.1.0/src/splent_io/splent_feature_autodeploy/translations/es/LC_MESSAGES/messages.po +489 -0
- 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>"
|