odysseus-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +482 -0
- data/bin/odysseus +285 -0
- data/lib/odysseus/cli/cli.rb +1042 -0
- metadata +100 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 83fbeafc2b92417c763330d0917febf88b7f35a283449cd6be25c3a3b6f4de9f
|
|
4
|
+
data.tar.gz: aa37ee969336ceb718dde957b5ea8d762a6191ee4f5d629aa7c78e1c9a588721
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: bc38e11e61cdc48389853434fc7327c3c7ff00e3103cc1399bb4d2bfa943a827e09948794d4760f03b6644c6f07b8b5307d3f5ed00c9a2b2b8c49bbbd710ce93
|
|
7
|
+
data.tar.gz: 40e5da0f9800cb2188ebff426b1990bc8aa0061d13a79db4b27a599338912a287bb1369729b8c88fa89de70fd589c19ae2b4cd7d47a7b9ca5cc90907088dbad3
|
data/README.md
ADDED
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
# Odysseus CLI
|
|
2
|
+
|
|
3
|
+
Command-line interface for deploying Docker containers with zero-downtime using Caddy as a reverse proxy.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
gem install odysseus-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or add to your Gemfile:
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
gem 'odysseus-cli'
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
1. Create a `deploy.yml` in your project:
|
|
20
|
+
|
|
21
|
+
```yaml
|
|
22
|
+
service: myapp
|
|
23
|
+
image: myregistry/myapp
|
|
24
|
+
|
|
25
|
+
servers:
|
|
26
|
+
web:
|
|
27
|
+
hosts:
|
|
28
|
+
- server1.example.com
|
|
29
|
+
jobs:
|
|
30
|
+
hosts:
|
|
31
|
+
- server1.example.com
|
|
32
|
+
cmd: bundle exec good_job
|
|
33
|
+
|
|
34
|
+
proxy:
|
|
35
|
+
hosts:
|
|
36
|
+
- myapp.example.com
|
|
37
|
+
app_port: 3000
|
|
38
|
+
ssl: true
|
|
39
|
+
ssl_email: admin@example.com
|
|
40
|
+
healthcheck:
|
|
41
|
+
path: /health
|
|
42
|
+
interval: 10
|
|
43
|
+
timeout: 5
|
|
44
|
+
|
|
45
|
+
env:
|
|
46
|
+
clear:
|
|
47
|
+
RAILS_ENV: production
|
|
48
|
+
secret:
|
|
49
|
+
- DATABASE_URL
|
|
50
|
+
- RAILS_MASTER_KEY
|
|
51
|
+
|
|
52
|
+
ssh:
|
|
53
|
+
user: root
|
|
54
|
+
keys:
|
|
55
|
+
- ~/.ssh/id_ed25519
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
2. Build and deploy:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
# Build, distribute, and deploy in one command
|
|
62
|
+
odysseus deploy --image v1.0.0 --build
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The `--build` flag automatically chooses how to distribute the image:
|
|
66
|
+
- **Without `registry` config** → uses [pussh](https://github.com/psviderski/unregistry) to transfer images directly via SSH
|
|
67
|
+
- **With `registry` config** → pushes to registry, hosts pull from there
|
|
68
|
+
|
|
69
|
+
## Commands
|
|
70
|
+
|
|
71
|
+
### deploy
|
|
72
|
+
|
|
73
|
+
Deploy all roles to their configured hosts.
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
odysseus deploy [options]
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Options:
|
|
80
|
+
- `--config FILE` - Path to deploy.yml (default: deploy.yml)
|
|
81
|
+
- `--image TAG` - Docker image tag (default: latest)
|
|
82
|
+
- `--build` - Build and distribute image before deploying
|
|
83
|
+
- `--dry-run` - Show what would be deployed without doing it
|
|
84
|
+
- `-v, --verbose` - Show SSH commands being executed
|
|
85
|
+
|
|
86
|
+
The `--build` flag automatically chooses the distribution method based on your config:
|
|
87
|
+
- **No `registry` config** → uses pussh (direct SSH transfer to each host)
|
|
88
|
+
- **Has `registry` config** → pushes to registry (hosts pull from there)
|
|
89
|
+
|
|
90
|
+
Examples:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
# Deploy existing image
|
|
94
|
+
odysseus deploy --image v1.0.0
|
|
95
|
+
|
|
96
|
+
# Build, distribute, and deploy in one command
|
|
97
|
+
odysseus deploy --image v1.0.0 --build
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### build
|
|
101
|
+
|
|
102
|
+
Build Docker image locally or on a remote build host.
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
odysseus build [options]
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Options:
|
|
109
|
+
- `--config FILE` - Path to deploy.yml (default: deploy.yml)
|
|
110
|
+
- `--image TAG` - Docker image tag (default: latest)
|
|
111
|
+
- `--push` - Push image to registry after build
|
|
112
|
+
- `--context PATH` - Build context path (default: . relative to deploy.yml)
|
|
113
|
+
- `-v, --verbose` - Show build commands being executed
|
|
114
|
+
|
|
115
|
+
Examples:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
# Build locally
|
|
119
|
+
odysseus build --image v1.0.0
|
|
120
|
+
|
|
121
|
+
# Build and push to registry
|
|
122
|
+
odysseus build --image v1.0.0 --push
|
|
123
|
+
|
|
124
|
+
# Build with custom context path
|
|
125
|
+
odysseus build --image v1.0.0 --context ./app
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### pussh
|
|
129
|
+
|
|
130
|
+
Push Docker image directly to hosts via SSH (no registry needed). Uses [docker-pussh/unregistry](https://github.com/psviderski/unregistry) to transfer images.
|
|
131
|
+
|
|
132
|
+
> **Note:** When no `registry` is configured, `odysseus deploy --build` automatically uses pussh. This command is useful for manually pushing images without deploying.
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
odysseus pussh [options]
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Options:
|
|
139
|
+
- `--config FILE` - Path to deploy.yml (default: deploy.yml)
|
|
140
|
+
- `--image TAG` - Docker image tag (default: latest)
|
|
141
|
+
- `--build` - Build image before pushing
|
|
142
|
+
- `-v, --verbose` - Show commands being executed
|
|
143
|
+
|
|
144
|
+
Examples:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
# Push existing local image to all hosts
|
|
148
|
+
odysseus pussh --image v1.0.0
|
|
149
|
+
|
|
150
|
+
# Build and push in one step
|
|
151
|
+
odysseus pussh --image v1.0.0 --build
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**Prerequisites:** Install docker-pussh on your local machine:
|
|
155
|
+
```bash
|
|
156
|
+
# macOS/Linux
|
|
157
|
+
curl -fsSL https://github.com/psviderski/unregistry/releases/latest/download/docker-pussh-$(uname -s)-$(uname -m) \
|
|
158
|
+
-o ~/.docker/cli-plugins/docker-pussh && chmod +x ~/.docker/cli-plugins/docker-pussh
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### status
|
|
162
|
+
|
|
163
|
+
Show service status on a server.
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
odysseus status <server> [--config FILE]
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### containers
|
|
170
|
+
|
|
171
|
+
List containers for the service on a server.
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
odysseus containers <server> [--config FILE] [--service NAME]
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### logs
|
|
178
|
+
|
|
179
|
+
Show logs for a service.
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
odysseus logs <server> [options]
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Options:
|
|
186
|
+
- `--role ROLE` - Role to show logs for: web, jobs, etc (default: web)
|
|
187
|
+
- `-f, --follow` - Follow log output
|
|
188
|
+
- `-n, --lines N` - Number of lines to show (default: 100)
|
|
189
|
+
- `--since TIME` - Show logs since timestamp (e.g., '10m', '2h')
|
|
190
|
+
|
|
191
|
+
### cleanup
|
|
192
|
+
|
|
193
|
+
Clean up old containers and optionally prune images.
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
odysseus cleanup <server> [--prune-images]
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### validate
|
|
200
|
+
|
|
201
|
+
Validate your deploy.yml configuration.
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
odysseus validate [--config FILE]
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### accessory
|
|
208
|
+
|
|
209
|
+
Manage accessories (databases, Redis, etc).
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
# These commands use hosts from accessory config (no server argument needed)
|
|
213
|
+
odysseus accessory boot --name db
|
|
214
|
+
odysseus accessory boot-all
|
|
215
|
+
odysseus accessory remove --name db
|
|
216
|
+
odysseus accessory restart --name db
|
|
217
|
+
odysseus accessory upgrade --name db
|
|
218
|
+
odysseus accessory status
|
|
219
|
+
|
|
220
|
+
# These commands require a server argument
|
|
221
|
+
odysseus accessory logs <server> --name db [-f] [-n 100]
|
|
222
|
+
odysseus accessory exec <server> --name db --command "psql -U postgres"
|
|
223
|
+
odysseus accessory shell <server> --name db
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Accessory commands like `boot`, `remove`, `restart`, `upgrade`, and `status` read the target hosts from the accessory's `hosts` configuration in deploy.yml, similar to how `deploy` works. Only `logs`, `exec`, and `shell` require a server argument since they operate on a specific host.
|
|
227
|
+
|
|
228
|
+
### app
|
|
229
|
+
|
|
230
|
+
Run commands in app containers.
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
odysseus app shell <server>
|
|
234
|
+
odysseus app exec <server> --command "rails db:migrate"
|
|
235
|
+
odysseus app console <server> [--cmd "rails c"]
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### secrets
|
|
239
|
+
|
|
240
|
+
Manage encrypted secrets files.
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
# Generate a new master key
|
|
244
|
+
odysseus secrets generate-key
|
|
245
|
+
|
|
246
|
+
# Encrypt a plaintext secrets file
|
|
247
|
+
odysseus secrets encrypt --input secrets.yml --file secrets.yml.enc
|
|
248
|
+
|
|
249
|
+
# Decrypt and display secrets (values are masked)
|
|
250
|
+
odysseus secrets decrypt --file secrets.yml.enc
|
|
251
|
+
|
|
252
|
+
# Edit encrypted secrets using $EDITOR
|
|
253
|
+
odysseus secrets edit --file secrets.yml.enc
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
The master key should be set as `ODYSSEUS_MASTER_KEY` environment variable.
|
|
257
|
+
|
|
258
|
+
## Configuration Reference
|
|
259
|
+
|
|
260
|
+
### service
|
|
261
|
+
|
|
262
|
+
The name of your service. Used for container naming and Caddy routing.
|
|
263
|
+
|
|
264
|
+
### image
|
|
265
|
+
|
|
266
|
+
The Docker image name (without tag). Tags are specified at deploy time.
|
|
267
|
+
|
|
268
|
+
### servers
|
|
269
|
+
|
|
270
|
+
Define roles and their target hosts:
|
|
271
|
+
|
|
272
|
+
```yaml
|
|
273
|
+
servers:
|
|
274
|
+
web:
|
|
275
|
+
hosts:
|
|
276
|
+
- web1.example.com
|
|
277
|
+
- web2.example.com
|
|
278
|
+
options:
|
|
279
|
+
memory: 4g
|
|
280
|
+
cpus: 2
|
|
281
|
+
jobs:
|
|
282
|
+
hosts:
|
|
283
|
+
- worker1.example.com
|
|
284
|
+
cmd: bundle exec good_job
|
|
285
|
+
options:
|
|
286
|
+
memory: 2g
|
|
287
|
+
cpus: 1.5
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
Available options:
|
|
291
|
+
- `memory` - Hard memory limit (e.g., `4g`, `512m`)
|
|
292
|
+
- `memory_reservation` - Soft memory limit
|
|
293
|
+
- `cpus` - CPU limit (e.g., `2` for 2 cores, `1.5` for 1.5 cores)
|
|
294
|
+
- `cpu_shares` - Relative CPU weight (default: 1024)
|
|
295
|
+
|
|
296
|
+
#### Dynamic hosts with AWS Auto Scaling Groups
|
|
297
|
+
|
|
298
|
+
Instead of a static `hosts` list, you can configure Odysseus to resolve hosts dynamically from an AWS Auto Scaling Group:
|
|
299
|
+
|
|
300
|
+
```yaml
|
|
301
|
+
servers:
|
|
302
|
+
web:
|
|
303
|
+
aws:
|
|
304
|
+
asg: my-web-asg # ASG name (required)
|
|
305
|
+
region: us-east-1 # AWS region (required)
|
|
306
|
+
use_private_ip: false # Use private IPs instead of public (default: false)
|
|
307
|
+
state: InService # Instance lifecycle state filter (default: InService)
|
|
308
|
+
options:
|
|
309
|
+
memory: 4g
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
**AWS credentials** are loaded from the standard AWS credential chain:
|
|
313
|
+
- Environment variables (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`)
|
|
314
|
+
- Shared credentials file (`~/.aws/credentials`)
|
|
315
|
+
- IAM instance profile (when running on EC2)
|
|
316
|
+
|
|
317
|
+
**Prerequisites:** Install the AWS SDK gems:
|
|
318
|
+
```bash
|
|
319
|
+
gem install aws-sdk-autoscaling aws-sdk-ec2
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
Or add to your Gemfile:
|
|
323
|
+
```ruby
|
|
324
|
+
gem 'aws-sdk-autoscaling'
|
|
325
|
+
gem 'aws-sdk-ec2'
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
**SSH configuration** (bastions, ProxyJump, etc.) is your responsibility. Odysseus only needs the hostnames/IPs and relies on your local SSH config.
|
|
329
|
+
|
|
330
|
+
### proxy
|
|
331
|
+
|
|
332
|
+
Caddy reverse proxy configuration:
|
|
333
|
+
|
|
334
|
+
```yaml
|
|
335
|
+
proxy:
|
|
336
|
+
hosts:
|
|
337
|
+
- myapp.example.com
|
|
338
|
+
- www.myapp.example.com
|
|
339
|
+
app_port: 3000
|
|
340
|
+
ssl: true
|
|
341
|
+
ssl_email: admin@example.com
|
|
342
|
+
healthcheck:
|
|
343
|
+
path: /health
|
|
344
|
+
interval: 10
|
|
345
|
+
timeout: 5
|
|
346
|
+
expect_status: 200 # Optional: expected HTTP status (default: 2xx)
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### env
|
|
350
|
+
|
|
351
|
+
Environment variables:
|
|
352
|
+
|
|
353
|
+
```yaml
|
|
354
|
+
env:
|
|
355
|
+
clear:
|
|
356
|
+
RAILS_ENV: production
|
|
357
|
+
secret:
|
|
358
|
+
- DATABASE_URL
|
|
359
|
+
- RAILS_MASTER_KEY
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
- `clear` - Plaintext values stored in deploy.yml
|
|
363
|
+
- `secret` - Keys to load from encrypted secrets file or server environment
|
|
364
|
+
|
|
365
|
+
### secrets_file
|
|
366
|
+
|
|
367
|
+
Path to an encrypted secrets file (relative to deploy.yml or absolute):
|
|
368
|
+
|
|
369
|
+
```yaml
|
|
370
|
+
secrets_file: secrets.yml.enc
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
Create the encrypted file using `odysseus secrets encrypt`. The secrets file should be YAML format:
|
|
374
|
+
|
|
375
|
+
```yaml
|
|
376
|
+
# secrets.yml (before encryption)
|
|
377
|
+
DATABASE_URL: postgres://user:pass@db/myapp
|
|
378
|
+
RAILS_MASTER_KEY: abc123def456
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
During deploy, secrets listed in `env.secret` are loaded from the encrypted file. If a key is not found in the secrets file, it falls back to the server's environment variables.
|
|
382
|
+
|
|
383
|
+
### accessories
|
|
384
|
+
|
|
385
|
+
Long-running services like databases:
|
|
386
|
+
|
|
387
|
+
```yaml
|
|
388
|
+
accessories:
|
|
389
|
+
db:
|
|
390
|
+
image: postgres:16
|
|
391
|
+
hosts:
|
|
392
|
+
- db.example.com
|
|
393
|
+
volumes:
|
|
394
|
+
- /var/lib/odysseus/myapp/postgres:/var/lib/postgresql/data
|
|
395
|
+
env:
|
|
396
|
+
clear:
|
|
397
|
+
POSTGRES_USER: myapp
|
|
398
|
+
POSTGRES_DB: myapp_production
|
|
399
|
+
healthcheck:
|
|
400
|
+
cmd: pg_isready -U myapp
|
|
401
|
+
interval: 10
|
|
402
|
+
timeout: 5
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
Each accessory must define `hosts` - the servers where it should run.
|
|
406
|
+
|
|
407
|
+
### ssh
|
|
408
|
+
|
|
409
|
+
SSH connection settings:
|
|
410
|
+
|
|
411
|
+
```yaml
|
|
412
|
+
ssh:
|
|
413
|
+
user: root
|
|
414
|
+
keys:
|
|
415
|
+
- ~/.ssh/id_ed25519
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
### builder
|
|
419
|
+
|
|
420
|
+
Configuration for building Docker images:
|
|
421
|
+
|
|
422
|
+
```yaml
|
|
423
|
+
builder:
|
|
424
|
+
strategy: local # 'local' or 'remote'
|
|
425
|
+
host: build-server # Required if strategy is 'remote'
|
|
426
|
+
dockerfile: Dockerfile # Dockerfile name (default: Dockerfile)
|
|
427
|
+
context: . # Build context path (default: .)
|
|
428
|
+
arch: amd64 # Target architecture
|
|
429
|
+
build_args: # Build arguments
|
|
430
|
+
RUBY_VERSION: "3.2"
|
|
431
|
+
NODE_VERSION: "18"
|
|
432
|
+
cache: true # Use Docker build cache (default: true)
|
|
433
|
+
push: false # Auto-push after build (default: false)
|
|
434
|
+
multiarch: false # Multi-platform builds with buildx
|
|
435
|
+
platforms: # Platforms for multi-arch builds
|
|
436
|
+
- linux/amd64
|
|
437
|
+
- linux/arm64
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
Build strategies:
|
|
441
|
+
- `local` - Build on the local machine (default)
|
|
442
|
+
- `remote` - Build on a remote host via SSH (useful for CI or dedicated build servers)
|
|
443
|
+
|
|
444
|
+
### registry
|
|
445
|
+
|
|
446
|
+
Docker registry configuration. When present, `odysseus deploy --build` will push images to the registry instead of using pussh:
|
|
447
|
+
|
|
448
|
+
```yaml
|
|
449
|
+
registry:
|
|
450
|
+
server: docker.io # Registry server (required to enable registry mode)
|
|
451
|
+
username: myuser # Registry username
|
|
452
|
+
password: mypassword # Registry password (consider using secrets)
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
**Image distribution modes:**
|
|
456
|
+
|
|
457
|
+
| Config | `deploy --build` behavior |
|
|
458
|
+
|--------|---------------------------|
|
|
459
|
+
| No `registry` | Build locally → pussh to each host via SSH |
|
|
460
|
+
| Has `registry` | Build locally → push to registry → hosts pull |
|
|
461
|
+
|
|
462
|
+
For better security, you can store registry credentials in your encrypted secrets file and reference them.
|
|
463
|
+
|
|
464
|
+
## Server Requirements
|
|
465
|
+
|
|
466
|
+
Your target servers only need **Docker** installed. Odysseus automatically deploys and manages Caddy as a container (`odysseus-caddy`) - no manual Caddy installation required.
|
|
467
|
+
|
|
468
|
+
## How It Works
|
|
469
|
+
|
|
470
|
+
1. **Ensure Caddy** starts the Caddy container if not running
|
|
471
|
+
2. **Deploy** starts a new container with the specified image tag
|
|
472
|
+
3. **Health check** waits for the container to become healthy
|
|
473
|
+
4. **Caddy update** adds the new container to the upstream pool
|
|
474
|
+
5. **Drain** removes old containers from Caddy and waits for connections to close
|
|
475
|
+
6. **Cleanup** stops old containers and removes all but the 2 most recent
|
|
476
|
+
7. **Stale upstream cleanup** removes any Caddy routes pointing to stopped containers
|
|
477
|
+
|
|
478
|
+
This ensures zero-downtime deployments with automatic rollback if health checks fail.
|
|
479
|
+
|
|
480
|
+
## License
|
|
481
|
+
|
|
482
|
+
MIT
|