djaploy 1.1.8__tar.gz → 1.2.2__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.
- {djaploy-1.1.8 → djaploy-1.2.2}/PKG-INFO +102 -1
- {djaploy-1.1.8 → djaploy-1.2.2}/README.md +101 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/apps/nginx/infra/djaploy_hooks.py +13 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/apps/systemd/infra/djaploy_hooks.py +36 -11
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/builtin_hooks.py +15 -2
- djaploy-1.2.2/djaploy/commands/activate.py +26 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/commands/deploy.py +9 -0
- djaploy-1.2.2/djaploy/commands/status.py +88 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/config.py +2 -2
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/deploy.py +24 -0
- djaploy-1.2.2/djaploy/infra/bluegreen.py +145 -0
- djaploy-1.2.2/djaploy/infra/djaploy_hooks.py +941 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/infra/templates.py +121 -2
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/infra/utils.py +145 -42
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/management/commands/djaploy.py +11 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy.egg-info/PKG-INFO +102 -1
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy.egg-info/SOURCES.txt +6 -1
- {djaploy-1.1.8 → djaploy-1.2.2}/pyproject.toml +1 -1
- djaploy-1.2.2/tests/test_bluegreen.py +486 -0
- djaploy-1.2.2/tests/test_versioning.py +329 -0
- djaploy-1.1.8/djaploy/infra/djaploy_hooks.py +0 -511
- {djaploy-1.1.8 → djaploy-1.2.2}/LICENSE +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/__init__.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/app.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/apps/__init__.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/apps/borg/__init__.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/apps/borg/infra/__init__.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/apps/borg/infra/djaploy_hooks.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/apps/janitor/__init__.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/apps/janitor/apps.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/apps/janitor/infra/__init__.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/apps/janitor/infra/commands/createjanitoruser.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/apps/janitor/infra/djaploy_hooks.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/apps/nginx/__init__.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/apps/nginx/apps.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/apps/nginx/infra/__init__.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/apps/rclone/__init__.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/apps/rclone/apps.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/apps/rclone/infra/__init__.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/apps/rclone/infra/djaploy_hooks.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/apps/sync_certs/__init__.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/apps/sync_certs/apps.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/apps/sync_certs/infra/__init__.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/apps/sync_certs/infra/djaploy_hooks.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/apps/systemd/__init__.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/apps/systemd/apps.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/apps/systemd/infra/__init__.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/apps/tailscale/__init__.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/apps/tailscale/apps.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/apps/tailscale/infra/__init__.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/apps/tailscale/infra/djaploy_hooks.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/apps/versioning/__init__.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/apps/versioning/apps.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/apps/versioning/infra/__init__.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/apps/versioning/infra/djaploy_hooks.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/artifact.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/bin/__init__.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/bin/django_pyinfra.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/bin/gunicornherder.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/certificates.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/changelog.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/commands/__init__.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/commands/_utils.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/commands/configure.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/commands/restore.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/commands/rollback.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/commands/sync_certs.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/discovery.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/hooks.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/infra/__init__.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/management/__init__.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/management/commands/__init__.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/management/commands/restore_backup.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/management/commands/sync_certs.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/management/commands/update_certs.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/management/commands/verify.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/management/utils.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/notifications.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/utils.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/version.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy/versioning.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy.egg-info/dependency_links.txt +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy.egg-info/entry_points.txt +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy.egg-info/requires.txt +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/djaploy.egg-info/top_level.txt +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/setup.cfg +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/tests/test_borg.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/tests/test_config.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/tests/test_deploy_scripts.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/tests/test_discovery.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/tests/test_gunicornherder.py +0 -0
- {djaploy-1.1.8 → djaploy-1.2.2}/tests/test_hooks.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: djaploy
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.2
|
|
4
4
|
Summary: Modular Django deployment system based on pyinfra
|
|
5
5
|
Author-email: Johanna Mae Dimayuga <johanna@techco.fi>
|
|
6
6
|
License: MIT License
|
|
@@ -236,6 +236,107 @@ Deployment flow:
|
|
|
236
236
|
7. Collects static files
|
|
237
237
|
8. Restarts services
|
|
238
238
|
|
|
239
|
+
## Deployment Strategies
|
|
240
|
+
|
|
241
|
+
djaploy supports three deployment strategies, configured via `deployment_strategy` on `HostConfig`.
|
|
242
|
+
|
|
243
|
+
### In-place (`"in_place"`)
|
|
244
|
+
|
|
245
|
+
The simplest strategy. Code is extracted directly into the app directory and services are restarted. Has brief downtime during restart.
|
|
246
|
+
|
|
247
|
+
### Zero-downtime (`"zero_downtime"`)
|
|
248
|
+
|
|
249
|
+
Uses a `releases/` directory with a `current` symlink. Each deploy creates a new immutable release, swaps the symlink atomically, and sends USR2 via gunicornherder to reload gunicorn. No downtime, but no pre-activation testing.
|
|
250
|
+
|
|
251
|
+
### Blue-green (`"bluegreen"`)
|
|
252
|
+
|
|
253
|
+
Two independent slots (blue and green), each running its own gunicorn process on a separate Unix socket. Traffic switching happens via nginx reload. Supports staging a release for testing before switching.
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
HostConfig(
|
|
257
|
+
"my-server",
|
|
258
|
+
ssh_hostname="192.168.1.100",
|
|
259
|
+
app_name="myapp",
|
|
260
|
+
app_user="myapp-api",
|
|
261
|
+
deployment_strategy="bluegreen",
|
|
262
|
+
# ...
|
|
263
|
+
)
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
#### Blue-green commands
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
# Deploy to inactive slot (does NOT switch traffic)
|
|
270
|
+
python manage.py djaploy deploy --env production --latest
|
|
271
|
+
|
|
272
|
+
# Activate: switch nginx to the staged slot (zero downtime)
|
|
273
|
+
python manage.py djaploy activate --env production
|
|
274
|
+
|
|
275
|
+
# Deploy + activate in one step
|
|
276
|
+
python manage.py djaploy deploy --env production --latest --activate
|
|
277
|
+
|
|
278
|
+
# Show both slots with release info, paths, service status
|
|
279
|
+
python manage.py djaploy status --env production
|
|
280
|
+
|
|
281
|
+
# Rollback: switch back to previous slot (instant)
|
|
282
|
+
python manage.py djaploy rollback --env production
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
#### Blue-green deployment flow
|
|
286
|
+
|
|
287
|
+
1. **Deploy** -- extracts artifact to inactive slot, installs dependencies, runs migrations, starts the slot's gunicorn service
|
|
288
|
+
2. **Test** -- the staged slot is running and reachable via its socket (e.g. `curl --unix-socket /run/myapp-green/myapp.sock http://localhost/health/`)
|
|
289
|
+
3. **Activate** -- rewrites nginx upstream to point to the new slot, reloads nginx
|
|
290
|
+
4. **Rollback** (if needed) -- switches nginx back to the previous slot, which is still running
|
|
291
|
+
|
|
292
|
+
### Server directory layout comparison
|
|
293
|
+
|
|
294
|
+
For `app_user="myapp-api"`, `app_name="myapp"`:
|
|
295
|
+
|
|
296
|
+
| Path | `in_place` | `zero_downtime` | `bluegreen` |
|
|
297
|
+
|------|-----------|-----------------|-------------|
|
|
298
|
+
| App code | `.../apps/myapp/` | `.../apps/myapp/current/` | `.../apps/myapp/slots/{blue\|green}/` |
|
|
299
|
+
| Virtualenv | Managed by Poetry | `.../shared/venv-{HASH}-py{ver}/` | `.../shared/venv-{HASH}-py{ver}/` |
|
|
300
|
+
| Static files | `.../apps/myapp/staticfiles/` | `.../apps/myapp/shared/staticfiles/` | `.../apps/myapp/shared/staticfiles/` |
|
|
301
|
+
| Media files | `.../apps/myapp/media/` | `.../apps/myapp/shared/media/` | `.../apps/myapp/shared/media/` |
|
|
302
|
+
| Database | via `db_dir` | via `db_dir` | via `db_dir` |
|
|
303
|
+
|
|
304
|
+
All paths are relative to `/home/{app_user}/`.
|
|
305
|
+
|
|
306
|
+
#### Shared directory (`zero_downtime` and `bluegreen`)
|
|
307
|
+
|
|
308
|
+
Both strategies use a `shared/` directory for resources that persist across deployments:
|
|
309
|
+
|
|
310
|
+
| Content | Purpose |
|
|
311
|
+
|---------|---------|
|
|
312
|
+
| `venv-{HASH}-py{version}/` | Virtualenvs keyed by `poetry.lock` hash. Reused when dependencies haven't changed. |
|
|
313
|
+
| `staticfiles/` | Output of `collectstatic`. Served by nginx. |
|
|
314
|
+
| `media/` | User-uploaded files. Served by nginx. |
|
|
315
|
+
| Custom paths via `shared_resources` | Project-specific shared directories (e.g. `bostad/public`). |
|
|
316
|
+
|
|
317
|
+
#### Systemd services comparison
|
|
318
|
+
|
|
319
|
+
| Strategy | Service name | Socket path | Process |
|
|
320
|
+
|----------|-------------|-------------|---------|
|
|
321
|
+
| `in_place` | `{app}.service` | `/run/{app}/{app}.sock` | `poetry run gunicorn` |
|
|
322
|
+
| `zero_downtime` | `{app}.service` | `/run/{app}/{app}.sock` | gunicornherder wrapping gunicorn |
|
|
323
|
+
| `bluegreen` | `{app}-blue.service`, `{app}-green.service` | `/run/{app}-blue/{app}.sock`, `/run/{app}-green/{app}.sock` | Plain gunicorn (`Type=notify`) |
|
|
324
|
+
|
|
325
|
+
Blue-green uses `Type=notify` -- gunicorn has native systemd-notify support, so systemd knows when the process is ready without needing gunicornherder.
|
|
326
|
+
|
|
327
|
+
#### Nginx configuration (bluegreen)
|
|
328
|
+
|
|
329
|
+
Blue-green deploys the nginx upstream as a separate include file so it can be rewritten during activation without touching the site config:
|
|
330
|
+
|
|
331
|
+
- Site config: `/etc/nginx/sites-available/{app_name}` (no inline upstream block)
|
|
332
|
+
- Upstream config: `/etc/nginx/sites-available/{app_name}-upstream.conf`
|
|
333
|
+
|
|
334
|
+
Activation rewrites the upstream file to point to the new slot's socket and reloads nginx.
|
|
335
|
+
|
|
336
|
+
#### State tracking (bluegreen)
|
|
337
|
+
|
|
338
|
+
Blue-green maintains a `state.json` file at `/home/{app_user}/apps/{app_name}/state.json` that tracks the active slot and deployment metadata (release name, commit, venv path, python interpreter) for each slot. This is printed during deploy, activate, and status commands.
|
|
339
|
+
|
|
239
340
|
### Certificate management
|
|
240
341
|
|
|
241
342
|
```bash
|
|
@@ -174,6 +174,107 @@ Deployment flow:
|
|
|
174
174
|
7. Collects static files
|
|
175
175
|
8. Restarts services
|
|
176
176
|
|
|
177
|
+
## Deployment Strategies
|
|
178
|
+
|
|
179
|
+
djaploy supports three deployment strategies, configured via `deployment_strategy` on `HostConfig`.
|
|
180
|
+
|
|
181
|
+
### In-place (`"in_place"`)
|
|
182
|
+
|
|
183
|
+
The simplest strategy. Code is extracted directly into the app directory and services are restarted. Has brief downtime during restart.
|
|
184
|
+
|
|
185
|
+
### Zero-downtime (`"zero_downtime"`)
|
|
186
|
+
|
|
187
|
+
Uses a `releases/` directory with a `current` symlink. Each deploy creates a new immutable release, swaps the symlink atomically, and sends USR2 via gunicornherder to reload gunicorn. No downtime, but no pre-activation testing.
|
|
188
|
+
|
|
189
|
+
### Blue-green (`"bluegreen"`)
|
|
190
|
+
|
|
191
|
+
Two independent slots (blue and green), each running its own gunicorn process on a separate Unix socket. Traffic switching happens via nginx reload. Supports staging a release for testing before switching.
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
HostConfig(
|
|
195
|
+
"my-server",
|
|
196
|
+
ssh_hostname="192.168.1.100",
|
|
197
|
+
app_name="myapp",
|
|
198
|
+
app_user="myapp-api",
|
|
199
|
+
deployment_strategy="bluegreen",
|
|
200
|
+
# ...
|
|
201
|
+
)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
#### Blue-green commands
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
# Deploy to inactive slot (does NOT switch traffic)
|
|
208
|
+
python manage.py djaploy deploy --env production --latest
|
|
209
|
+
|
|
210
|
+
# Activate: switch nginx to the staged slot (zero downtime)
|
|
211
|
+
python manage.py djaploy activate --env production
|
|
212
|
+
|
|
213
|
+
# Deploy + activate in one step
|
|
214
|
+
python manage.py djaploy deploy --env production --latest --activate
|
|
215
|
+
|
|
216
|
+
# Show both slots with release info, paths, service status
|
|
217
|
+
python manage.py djaploy status --env production
|
|
218
|
+
|
|
219
|
+
# Rollback: switch back to previous slot (instant)
|
|
220
|
+
python manage.py djaploy rollback --env production
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
#### Blue-green deployment flow
|
|
224
|
+
|
|
225
|
+
1. **Deploy** -- extracts artifact to inactive slot, installs dependencies, runs migrations, starts the slot's gunicorn service
|
|
226
|
+
2. **Test** -- the staged slot is running and reachable via its socket (e.g. `curl --unix-socket /run/myapp-green/myapp.sock http://localhost/health/`)
|
|
227
|
+
3. **Activate** -- rewrites nginx upstream to point to the new slot, reloads nginx
|
|
228
|
+
4. **Rollback** (if needed) -- switches nginx back to the previous slot, which is still running
|
|
229
|
+
|
|
230
|
+
### Server directory layout comparison
|
|
231
|
+
|
|
232
|
+
For `app_user="myapp-api"`, `app_name="myapp"`:
|
|
233
|
+
|
|
234
|
+
| Path | `in_place` | `zero_downtime` | `bluegreen` |
|
|
235
|
+
|------|-----------|-----------------|-------------|
|
|
236
|
+
| App code | `.../apps/myapp/` | `.../apps/myapp/current/` | `.../apps/myapp/slots/{blue\|green}/` |
|
|
237
|
+
| Virtualenv | Managed by Poetry | `.../shared/venv-{HASH}-py{ver}/` | `.../shared/venv-{HASH}-py{ver}/` |
|
|
238
|
+
| Static files | `.../apps/myapp/staticfiles/` | `.../apps/myapp/shared/staticfiles/` | `.../apps/myapp/shared/staticfiles/` |
|
|
239
|
+
| Media files | `.../apps/myapp/media/` | `.../apps/myapp/shared/media/` | `.../apps/myapp/shared/media/` |
|
|
240
|
+
| Database | via `db_dir` | via `db_dir` | via `db_dir` |
|
|
241
|
+
|
|
242
|
+
All paths are relative to `/home/{app_user}/`.
|
|
243
|
+
|
|
244
|
+
#### Shared directory (`zero_downtime` and `bluegreen`)
|
|
245
|
+
|
|
246
|
+
Both strategies use a `shared/` directory for resources that persist across deployments:
|
|
247
|
+
|
|
248
|
+
| Content | Purpose |
|
|
249
|
+
|---------|---------|
|
|
250
|
+
| `venv-{HASH}-py{version}/` | Virtualenvs keyed by `poetry.lock` hash. Reused when dependencies haven't changed. |
|
|
251
|
+
| `staticfiles/` | Output of `collectstatic`. Served by nginx. |
|
|
252
|
+
| `media/` | User-uploaded files. Served by nginx. |
|
|
253
|
+
| Custom paths via `shared_resources` | Project-specific shared directories (e.g. `bostad/public`). |
|
|
254
|
+
|
|
255
|
+
#### Systemd services comparison
|
|
256
|
+
|
|
257
|
+
| Strategy | Service name | Socket path | Process |
|
|
258
|
+
|----------|-------------|-------------|---------|
|
|
259
|
+
| `in_place` | `{app}.service` | `/run/{app}/{app}.sock` | `poetry run gunicorn` |
|
|
260
|
+
| `zero_downtime` | `{app}.service` | `/run/{app}/{app}.sock` | gunicornherder wrapping gunicorn |
|
|
261
|
+
| `bluegreen` | `{app}-blue.service`, `{app}-green.service` | `/run/{app}-blue/{app}.sock`, `/run/{app}-green/{app}.sock` | Plain gunicorn (`Type=notify`) |
|
|
262
|
+
|
|
263
|
+
Blue-green uses `Type=notify` -- gunicorn has native systemd-notify support, so systemd knows when the process is ready without needing gunicornherder.
|
|
264
|
+
|
|
265
|
+
#### Nginx configuration (bluegreen)
|
|
266
|
+
|
|
267
|
+
Blue-green deploys the nginx upstream as a separate include file so it can be rewritten during activation without touching the site config:
|
|
268
|
+
|
|
269
|
+
- Site config: `/etc/nginx/sites-available/{app_name}` (no inline upstream block)
|
|
270
|
+
- Upstream config: `/etc/nginx/sites-available/{app_name}-upstream.conf`
|
|
271
|
+
|
|
272
|
+
Activation rewrites the upstream file to point to the new slot's socket and reloads nginx.
|
|
273
|
+
|
|
274
|
+
#### State tracking (bluegreen)
|
|
275
|
+
|
|
276
|
+
Blue-green maintains a `state.json` file at `/home/{app_user}/apps/{app_name}/state.json` that tracks the active slot and deployment metadata (release name, commit, venv path, python interpreter) for each slot. This is printed during deploy, activate, and status commands.
|
|
277
|
+
|
|
177
278
|
### Certificate management
|
|
178
279
|
|
|
179
280
|
```bash
|
|
@@ -34,6 +34,7 @@ def configure_nginx(host_data):
|
|
|
34
34
|
def deploy_nginx(host_data, artifact_path):
|
|
35
35
|
"""Deploy NGINX configuration files and SSL certificates."""
|
|
36
36
|
from pyinfra.operations import server, files
|
|
37
|
+
from djaploy.infra.utils import is_bluegreen
|
|
37
38
|
|
|
38
39
|
server.shell(
|
|
39
40
|
name="Clear default NGINX sites",
|
|
@@ -80,6 +81,18 @@ def deploy_nginx(host_data, artifact_path):
|
|
|
80
81
|
_sudo=True,
|
|
81
82
|
)
|
|
82
83
|
|
|
84
|
+
# For bluegreen, also enable the upstream config
|
|
85
|
+
if is_bluegreen(host_data):
|
|
86
|
+
server.shell(
|
|
87
|
+
name=f"Enable {app_name} NGINX upstream config",
|
|
88
|
+
commands=[
|
|
89
|
+
f"test -f /etc/nginx/sites-available/{app_name}-upstream.conf && "
|
|
90
|
+
f"ln -fs /etc/nginx/sites-available/{app_name}-upstream.conf "
|
|
91
|
+
f"/etc/nginx/sites-enabled/{app_name}-upstream.conf || true",
|
|
92
|
+
],
|
|
93
|
+
_sudo=True,
|
|
94
|
+
)
|
|
95
|
+
|
|
83
96
|
|
|
84
97
|
@deploy_hook("deploy:start")
|
|
85
98
|
def reload_nginx(host_data, artifact_path):
|
|
@@ -21,13 +21,33 @@ def reload_systemd_daemon(host_data, artifact_path):
|
|
|
21
21
|
@deploy_hook("deploy:start")
|
|
22
22
|
def start_services(host_data, artifact_path):
|
|
23
23
|
"""Start or restart application services after deploy."""
|
|
24
|
+
from pyinfra import host
|
|
24
25
|
from pyinfra.operations import systemd
|
|
25
26
|
|
|
26
|
-
from djaploy.infra.utils import is_zero_downtime
|
|
27
|
-
|
|
27
|
+
from djaploy.infra.utils import is_zero_downtime, is_bluegreen, get_slot_service_name
|
|
28
|
+
|
|
29
|
+
if is_bluegreen(host_data):
|
|
30
|
+
# Start/restart only the target slot's service.
|
|
31
|
+
# The other slot may be empty (first deploy) so we must NOT
|
|
32
|
+
# try to start it — only enable the target slot's service.
|
|
33
|
+
target_slot = getattr(host.data, '_bluegreen_target_slot', None)
|
|
34
|
+
if not target_slot:
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
for service in getattr(host_data, "services", []):
|
|
38
|
+
app_name = getattr(host_data, 'app_name', service)
|
|
39
|
+
slot_service = get_slot_service_name(app_name, target_slot)
|
|
28
40
|
|
|
29
|
-
|
|
30
|
-
|
|
41
|
+
systemd.service(
|
|
42
|
+
name=f"Start and restart {slot_service} (target slot)",
|
|
43
|
+
service=slot_service,
|
|
44
|
+
running=True,
|
|
45
|
+
enabled=True,
|
|
46
|
+
restarted=True,
|
|
47
|
+
_sudo=True,
|
|
48
|
+
)
|
|
49
|
+
elif is_zero_downtime(host_data):
|
|
50
|
+
for service in getattr(host_data, "services", []):
|
|
31
51
|
systemd.service(
|
|
32
52
|
name=f"Start and enable {service}",
|
|
33
53
|
service=service,
|
|
@@ -41,7 +61,8 @@ def start_services(host_data, artifact_path):
|
|
|
41
61
|
reloaded=True,
|
|
42
62
|
_sudo=True,
|
|
43
63
|
)
|
|
44
|
-
|
|
64
|
+
else:
|
|
65
|
+
for service in getattr(host_data, "services", []):
|
|
45
66
|
systemd.service(
|
|
46
67
|
name=f"Restart and enable {service}",
|
|
47
68
|
service=service,
|
|
@@ -65,19 +86,23 @@ def start_services(host_data, artifact_path):
|
|
|
65
86
|
def reload_services_on_rollback(host_data, release):
|
|
66
87
|
"""Reload or restart services after a rollback."""
|
|
67
88
|
from pyinfra.operations import systemd
|
|
68
|
-
from djaploy.infra.utils import is_zero_downtime
|
|
69
|
-
|
|
70
|
-
zero_downtime = is_zero_downtime(host_data)
|
|
89
|
+
from djaploy.infra.utils import is_zero_downtime, is_bluegreen
|
|
71
90
|
|
|
72
|
-
|
|
73
|
-
|
|
91
|
+
if is_bluegreen(host_data):
|
|
92
|
+
# For bluegreen rollback, nginx switching is handled by the core
|
|
93
|
+
# rollback hook. We just need to ensure the target slot's service
|
|
94
|
+
# is running (it should already be from the previous deploy).
|
|
95
|
+
pass
|
|
96
|
+
elif is_zero_downtime(host_data):
|
|
97
|
+
for service in getattr(host_data, "services", []):
|
|
74
98
|
systemd.service(
|
|
75
99
|
name=f"Reload {service} after rollback",
|
|
76
100
|
service=service,
|
|
77
101
|
reloaded=True,
|
|
78
102
|
_sudo=True,
|
|
79
103
|
)
|
|
80
|
-
|
|
104
|
+
else:
|
|
105
|
+
for service in getattr(host_data, "services", []):
|
|
81
106
|
systemd.service(
|
|
82
107
|
name=f"Restart {service} after rollback",
|
|
83
108
|
service=service,
|
|
@@ -103,6 +103,19 @@ def _deploy_calculate_release_info(context):
|
|
|
103
103
|
if release_info:
|
|
104
104
|
context["pyinfra_data"]["version"] = release_info["new_version"]
|
|
105
105
|
context["pyinfra_data"]["commit"] = release_info["commit"]
|
|
106
|
+
elif "commit" not in context["pyinfra_data"]:
|
|
107
|
+
# Always pass commit hash even without versioning configured
|
|
108
|
+
try:
|
|
109
|
+
import subprocess
|
|
110
|
+
from django.conf import settings
|
|
111
|
+
commit = subprocess.check_output(
|
|
112
|
+
["git", "rev-parse", "--short", "HEAD"],
|
|
113
|
+
cwd=str(settings.GIT_DIR),
|
|
114
|
+
text=True,
|
|
115
|
+
).strip()
|
|
116
|
+
context["pyinfra_data"]["commit"] = commit
|
|
117
|
+
except Exception:
|
|
118
|
+
pass
|
|
106
119
|
|
|
107
120
|
|
|
108
121
|
# ── deploy:postcommand ───────────────────────────────────────────────
|
|
@@ -150,7 +163,7 @@ def _rollback_validate_strategy(context):
|
|
|
150
163
|
_, data = hosts[0]
|
|
151
164
|
strategy = (data.get("deployment_strategy") if isinstance(data, dict)
|
|
152
165
|
else getattr(data, "deployment_strategy", "zero_downtime"))
|
|
153
|
-
if strategy
|
|
166
|
+
if strategy not in ("zero_downtime", "bluegreen"):
|
|
154
167
|
raise ValueError(
|
|
155
|
-
"Rollback is only supported with deployment_strategy='zero_downtime'"
|
|
168
|
+
"Rollback is only supported with deployment_strategy='zero_downtime' or 'bluegreen'"
|
|
156
169
|
)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pyinfra command: activate a blue-green deployment slot.
|
|
3
|
+
|
|
4
|
+
Discovers and runs all @deploy_hook functions for the activate phase:
|
|
5
|
+
|
|
6
|
+
activate:pre — verify target slot is healthy
|
|
7
|
+
activate — switch nginx upstream, update state
|
|
8
|
+
activate:post — cleanup tasks
|
|
9
|
+
|
|
10
|
+
Usage (via djaploy management command):
|
|
11
|
+
manage.py djaploy activate --env production
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from pyinfra import host
|
|
15
|
+
from pyinfra.api import deploy as _deploy_decorator
|
|
16
|
+
|
|
17
|
+
from djaploy.hooks import discover_hooks, get_registry
|
|
18
|
+
|
|
19
|
+
discover_hooks()
|
|
20
|
+
registry = get_registry()
|
|
21
|
+
|
|
22
|
+
for phase in ("activate:pre", "activate", "activate:post"):
|
|
23
|
+
for hook in registry.get_remote_hooks(phase):
|
|
24
|
+
_deploy_decorator(hook.function.__name__)(hook.function)(
|
|
25
|
+
host.data
|
|
26
|
+
)
|
|
@@ -29,3 +29,12 @@ for phase in ("deploy:upload", "deploy:configure", "deploy:pre", "deploy:start")
|
|
|
29
29
|
_deploy_decorator(hook.function.__name__)(hook.function)(
|
|
30
30
|
host.data, artifact_path
|
|
31
31
|
)
|
|
32
|
+
|
|
33
|
+
# If --activate flag is set, run activation phases after deploy
|
|
34
|
+
_activate_val = getattr(host.data, "activate", None)
|
|
35
|
+
if _activate_val in (True, "true", "True"):
|
|
36
|
+
for phase in ("activate:pre", "activate", "activate:post"):
|
|
37
|
+
for hook in registry.get_remote_hooks(phase):
|
|
38
|
+
_deploy_decorator(hook.function.__name__)(hook.function)(
|
|
39
|
+
host.data
|
|
40
|
+
)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pyinfra command: show blue-green deployment status.
|
|
3
|
+
|
|
4
|
+
Reads and displays the blue-green state from each host.
|
|
5
|
+
|
|
6
|
+
Usage (via djaploy management command):
|
|
7
|
+
manage.py djaploy status --env production
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from pyinfra import host
|
|
11
|
+
from pyinfra.api import deploy as _deploy_decorator
|
|
12
|
+
from pyinfra.operations import python as python_op
|
|
13
|
+
|
|
14
|
+
from djaploy.hooks import discover_hooks, get_registry
|
|
15
|
+
from djaploy.infra.utils import is_bluegreen, get_app_path, get_slot_socket_path
|
|
16
|
+
from djaploy.infra.djaploy_hooks import _read_slot_info_from_remote
|
|
17
|
+
|
|
18
|
+
discover_hooks()
|
|
19
|
+
registry = get_registry()
|
|
20
|
+
|
|
21
|
+
if is_bluegreen(host.data):
|
|
22
|
+
app_name = getattr(host.data, 'app_name', 'unknown')
|
|
23
|
+
app_path = get_app_path(host.data)
|
|
24
|
+
state_file = f"{app_path}/state.json"
|
|
25
|
+
|
|
26
|
+
def _show_status(app_n, state_f):
|
|
27
|
+
blue_info = _read_slot_info_from_remote(host, "blue", state_f)
|
|
28
|
+
green_info = _read_slot_info_from_remote(host, "green", state_f)
|
|
29
|
+
|
|
30
|
+
# Read active slot
|
|
31
|
+
import json
|
|
32
|
+
result = host.run_shell_command(command=f"cat {state_f}")
|
|
33
|
+
active = "none"
|
|
34
|
+
if result[0] and len(result) > 1:
|
|
35
|
+
try:
|
|
36
|
+
lines = [l.line if hasattr(l, 'line') else str(l) for l in result[1]]
|
|
37
|
+
state_data = json.loads("\n".join(lines))
|
|
38
|
+
active = state_data.get("active_slot") or "none"
|
|
39
|
+
except (json.JSONDecodeError, KeyError):
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
print(f"\nBlue-Green Status for {app_n}")
|
|
43
|
+
print("=" * 40)
|
|
44
|
+
print(f"Active slot: {active}\n")
|
|
45
|
+
|
|
46
|
+
for slot, info in [("blue", blue_info), ("green", green_info)]:
|
|
47
|
+
tag = "ACTIVE" if active == slot else "inactive"
|
|
48
|
+
print(f"{slot.upper()} ({tag}):")
|
|
49
|
+
if info:
|
|
50
|
+
print(f" Release: {info.get('release', 'unknown')}")
|
|
51
|
+
print(f" Commit: {info.get('commit', 'unknown')}")
|
|
52
|
+
print(f" Deployed at: {info.get('deployed_at', 'unknown')}")
|
|
53
|
+
print(f" Python: {info.get('python_interpreter', 'unknown')}")
|
|
54
|
+
print(f" Venv: {info.get('venv_path', 'unknown')}")
|
|
55
|
+
print(f" Socket: {get_slot_socket_path(app_n, slot)}")
|
|
56
|
+
else:
|
|
57
|
+
print(" (empty)")
|
|
58
|
+
print()
|
|
59
|
+
|
|
60
|
+
# Show service status
|
|
61
|
+
for slot in ("blue", "green"):
|
|
62
|
+
svc = f"{app_n}-{slot}.service"
|
|
63
|
+
res = host.run_shell_command(command=f"systemctl is-active {svc} 2>/dev/null || echo inactive")
|
|
64
|
+
status_str = "unknown"
|
|
65
|
+
if res[0] and len(res) > 1:
|
|
66
|
+
try:
|
|
67
|
+
status_str = (list(res[1])[0].line if hasattr(list(res[1])[0], 'line') else str(list(res[1])[0])).strip()
|
|
68
|
+
except (IndexError, AttributeError):
|
|
69
|
+
pass
|
|
70
|
+
print(f"Service {svc}: {status_str}")
|
|
71
|
+
print()
|
|
72
|
+
|
|
73
|
+
python_op.call(
|
|
74
|
+
name="Show blue-green status",
|
|
75
|
+
function=_show_status,
|
|
76
|
+
app_n=app_name,
|
|
77
|
+
state_f=state_file,
|
|
78
|
+
)
|
|
79
|
+
else:
|
|
80
|
+
def _show_non_bluegreen():
|
|
81
|
+
strategy = getattr(host.data, "deployment_strategy", "zero_downtime")
|
|
82
|
+
print(f"\nDeployment strategy: {strategy}")
|
|
83
|
+
print("Not a blue-green deployment. Use deploy/rollback commands.\n")
|
|
84
|
+
|
|
85
|
+
python_op.call(
|
|
86
|
+
name="Show deployment status",
|
|
87
|
+
function=_show_non_bluegreen,
|
|
88
|
+
)
|
|
@@ -213,10 +213,10 @@ class HostConfig(tuple, metaclass=HostConfigMetaclass):
|
|
|
213
213
|
|
|
214
214
|
# Validate deployment_strategy
|
|
215
215
|
strategy = config.get("deployment_strategy", "zero_downtime")
|
|
216
|
-
if strategy not in ("in_place", "zero_downtime"):
|
|
216
|
+
if strategy not in ("in_place", "zero_downtime", "bluegreen"):
|
|
217
217
|
raise ValueError(
|
|
218
218
|
f"Invalid deployment_strategy: {strategy!r}. "
|
|
219
|
-
f"Must be 'in_place' or '
|
|
219
|
+
f"Must be 'in_place', 'zero_downtime', or 'bluegreen'"
|
|
220
220
|
)
|
|
221
221
|
|
|
222
222
|
# Expand SSH key path if provided
|
|
@@ -222,6 +222,30 @@ def rollback_project(inventory_file: str,
|
|
|
222
222
|
})
|
|
223
223
|
|
|
224
224
|
|
|
225
|
+
def activate_project(inventory_file: str, **kwargs):
|
|
226
|
+
"""Activate the staged blue-green slot."""
|
|
227
|
+
env_name = Path(inventory_file).stem
|
|
228
|
+
run_command({
|
|
229
|
+
"command": "activate",
|
|
230
|
+
"env": env_name,
|
|
231
|
+
"command_file": str(_get_command_file("activate")),
|
|
232
|
+
"inventory_file": inventory_file,
|
|
233
|
+
"pyinfra_data": _build_pyinfra_data(env_name),
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def bluegreen_status(inventory_file: str, **kwargs):
|
|
238
|
+
"""Show blue-green deployment status."""
|
|
239
|
+
env_name = Path(inventory_file).stem
|
|
240
|
+
run_command({
|
|
241
|
+
"command": "status",
|
|
242
|
+
"env": env_name,
|
|
243
|
+
"command_file": str(_get_command_file("status")),
|
|
244
|
+
"inventory_file": inventory_file,
|
|
245
|
+
"pyinfra_data": _build_pyinfra_data(env_name),
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
|
|
225
249
|
# ------------------------------------------------------------------
|
|
226
250
|
# Internal helpers (called by hooks in builtin_hooks.py)
|
|
227
251
|
# ------------------------------------------------------------------
|