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.
Files changed (5) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +482 -0
  3. data/bin/odysseus +285 -0
  4. data/lib/odysseus/cli/cli.rb +1042 -0
  5. 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