feat!: merge secrets/registry_auth into unified auth config, add attachments CRUD CLI
BREAKING CHANGE: Config restructure merges [secrets] and [registry_auth] into [auth] Auth config changes: - [secrets].backend -> [auth].secrets_backend - [registry_auth].* -> [auth].* - Secret paths: gordon/registry/* -> gordon/auth/* - auth.enabled defaults to true for security New attachments CLI: - gordon attachments list [domain-or-group] - gordon attachments add <target> <image> - gordon attachments remove <target> <image> - gordon attach alias for gordon attachments - Works locally and remotely via --remote flag New files: - docs/config/auth.md (replaces registry-auth.md) - internal/adapters/in/cli/attachments.go - internal/adapters/dto/admin_attachments.go Updated documentation across docs/ and wiki/ to reflect new config structure.
- commit
- 50b1179947459f77f5b4a54b27d96d8ad42defa7
- author
- brice · · 2026-01-17 15:25 UTC
- parent
- bdeaa00
- signature
- unsigned
22 files · +1262 -192
M docs/cli/commands.md +67 -6
| @@ -13,6 +13,7 @@ Gordon provides a command-line interface for server management, deployment, and | |||
| 13 | 13 | | `gordon version` | Print version information | | |
| 14 | 14 | | `gordon auth` | Manage registry authentication | | |
| 15 | 15 | | `gordon routes` | Manage routes (local or remote) | | |
| 16 | + | | `gordon attachments` | Manage attachments (local or remote) | | |
| 16 | 17 | | `gordon secrets` | Manage secrets (local or remote) | | |
| 17 | 18 | | `gordon remotes` | Manage saved remote Gordon instances | | |
| 18 | 19 | ||
| @@ -368,7 +369,7 @@ Manage registry authentication tokens and passwords. | |||
| 368 | 369 | |------------|-------------| | |
| 369 | 370 | | `token generate` | Generate a new JWT token | | |
| 370 | 371 | | `token list` | List all stored tokens | | |
| 371 | - | | `token revoke` | Revoke a token by ID | | |
| 372 | + | | `token revoke` | Revoke a token by ID (use `--all` to revoke all tokens) | | |
| 372 | 373 | | `password hash` | Generate bcrypt password hash | | |
| 373 | 374 | | `internal` | Show internal registry credentials | | |
| 374 | 375 | ||
| @@ -494,9 +495,9 @@ Bcrypt hash (store in your secrets backend): | |||
| 494 | 495 | $2a$10$N9qo8uLOickgx2ZMRZoMye... | |
| 495 | 496 | ||
| 496 | 497 | Then reference the path in your config: | |
| 497 | - | [registry_auth] | |
| 498 | + | [auth] | |
| 498 | 499 | type = "password" | |
| 499 | - | password_hash = "gordon/registry/password_hash" | |
| 500 | + | password_hash = "gordon/auth/password_hash" | |
| 500 | 501 | ``` | |
| 501 | 502 | ||
| 502 | 503 | ### gordon auth internal | |
| @@ -595,15 +596,16 @@ gordon auth password hash | |||
| 595 | 596 | # Enter: mypassword | |
| 596 | 597 | ||
| 597 | 598 | # Store in pass | |
| 598 | - | pass insert gordon/registry/password_hash | |
| 599 | + | pass insert gordon/auth/password_hash | |
| 599 | 600 | # Paste: $2a$10$... | |
| 600 | 601 | ||
| 601 | 602 | # Configure gordon.toml | |
| 602 | - | [registry_auth] | |
| 603 | + | [auth] | |
| 603 | 604 | enabled = true | |
| 604 | 605 | type = "password" | |
| 606 | + | secrets_backend = "pass" | |
| 605 | 607 | username = "deploy" | |
| 606 | - | password_hash = "gordon/registry/password_hash" | |
| 608 | + | password_hash = "gordon/auth/password_hash" | |
| 607 | 609 | ``` | |
| 608 | 610 | ||
| 609 | 611 | --- | |
| @@ -659,6 +661,65 @@ gordon routes deploy myapp.example.com | |||
| 659 | 661 | ||
| 660 | 662 | --- | |
| 661 | 663 | ||
| 664 | + | ## gordon attachments | |
| 665 | + | ||
| 666 | + | Manage attachments on local or remote Gordon instances. | |
| 667 | + | ||
| 668 | + | ### Subcommands | |
| 669 | + | ||
| 670 | + | | Subcommand | Description | | |
| 671 | + | |------------|-------------| | |
| 672 | + | | `list` | List all attachments or attachments for a specific target | | |
| 673 | + | | `add` | Add an attachment to a domain or network group | | |
| 674 | + | | `remove` | Remove an attachment from a domain or network group | | |
| 675 | + | ||
| 676 | + | ### gordon attachments list | |
| 677 | + | ||
| 678 | + | List all configured attachments. | |
| 679 | + | ||
| 680 | + | ```bash | |
| 681 | + | # List all attachments | |
| 682 | + | gordon attachments list | |
| 683 | + | ||
| 684 | + | # List attachments for a specific domain or group | |
| 685 | + | gordon attachments list app.example.com | |
| 686 | + | gordon attachments list backend | |
| 687 | + | ||
| 688 | + | # Remote mode | |
| 689 | + | gordon attachments list --remote https://gordon.mydomain.com --token $TOKEN | |
| 690 | + | ``` | |
| 691 | + | ||
| 692 | + | ### gordon attachments add | |
| 693 | + | ||
| 694 | + | Add an attachment to a domain or network group. | |
| 695 | + | ||
| 696 | + | ```bash | |
| 697 | + | gordon attachments add <domain-or-group> <image> | |
| 698 | + | gordon attachments add app.example.com postgres:15 | |
| 699 | + | gordon attachments add backend redis:7-alpine | |
| 700 | + | ``` | |
| 701 | + | ||
| 702 | + | ### gordon attachments remove | |
| 703 | + | ||
| 704 | + | Remove an attachment. | |
| 705 | + | ||
| 706 | + | ```bash | |
| 707 | + | gordon attachments remove <domain-or-group> <image> | |
| 708 | + | gordon attachments remove app.example.com postgres:15 | |
| 709 | + | ``` | |
| 710 | + | ||
| 711 | + | ### Alias | |
| 712 | + | ||
| 713 | + | `gordon attach` is an alias for `gordon attachments`: | |
| 714 | + | ||
| 715 | + | ```bash | |
| 716 | + | gordon attach list | |
| 717 | + | gordon attach add app.example.com postgres:15 | |
| 718 | + | gordon attach remove app.example.com postgres:15 | |
| 719 | + | ``` | |
| 720 | + | ||
| 721 | + | --- | |
| 722 | + | ||
| 662 | 723 | ## gordon secrets | |
| 663 | 724 | ||
| 664 | 725 | Manage secrets on local or remote Gordon instances. | |
| @@ -13,6 +13,7 @@ Gordon provides a command-line interface for server management, deployment, and | |||||
| 13 | | `gordon version` | Print version information | | 13 | | `gordon version` | Print version information | | ||
| 14 | | `gordon auth` | Manage registry authentication | | 14 | | `gordon auth` | Manage registry authentication | | ||
| 15 | | `gordon routes` | Manage routes (local or remote) | | 15 | | `gordon routes` | Manage routes (local or remote) | | ||
| 16 | + | | `gordon attachments` | Manage attachments (local or remote) | | |||
| 16 | | `gordon secrets` | Manage secrets (local or remote) | | 17 | | `gordon secrets` | Manage secrets (local or remote) | | ||
| 17 | | `gordon remotes` | Manage saved remote Gordon instances | | 18 | | `gordon remotes` | Manage saved remote Gordon instances | | ||
| 18 | 19 | ||||
| @@ -368,7 +369,7 @@ Manage registry authentication tokens and passwords. | |||||
| 368 | |------------|-------------| | 369 | |------------|-------------| | ||
| 369 | | `token generate` | Generate a new JWT token | | 370 | | `token generate` | Generate a new JWT token | | ||
| 370 | | `token list` | List all stored tokens | | 371 | | `token list` | List all stored tokens | | ||
| 371 | - | | `token revoke` | Revoke a token by ID | | 372 | + | | `token revoke` | Revoke a token by ID (use `--all` to revoke all tokens) | |
| 372 | | `password hash` | Generate bcrypt password hash | | 373 | | `password hash` | Generate bcrypt password hash | | ||
| 373 | | `internal` | Show internal registry credentials | | 374 | | `internal` | Show internal registry credentials | | ||
| 374 | 375 | ||||
| @@ -494,9 +495,9 @@ Bcrypt hash (store in your secrets backend): | |||||
| 494 | $2a$10$N9qo8uLOickgx2ZMRZoMye... | 495 | $2a$10$N9qo8uLOickgx2ZMRZoMye... | ||
| 495 | 496 | ||||
| 496 | Then reference the path in your config: | 497 | Then reference the path in your config: | ||
| 497 | - | [registry_auth] | 498 | + | [auth] |
| 498 | type = "password" | 499 | type = "password" | ||
| 499 | - | password_hash = "gordon/registry/password_hash" | 500 | + | password_hash = "gordon/auth/password_hash" |
| 500 | ``` | 501 | ``` | ||
| 501 | 502 | ||||
| 502 | ### gordon auth internal | 503 | ### gordon auth internal | ||
| @@ -595,15 +596,16 @@ gordon auth password hash | |||||
| 595 | # Enter: mypassword | 596 | # Enter: mypassword | ||
| 596 | 597 | ||||
| 597 | # Store in pass | 598 | # Store in pass | ||
| 598 | - | pass insert gordon/registry/password_hash | 599 | + | pass insert gordon/auth/password_hash |
| 599 | # Paste: $2a$10$... | 600 | # Paste: $2a$10$... | ||
| 600 | 601 | ||||
| 601 | # Configure gordon.toml | 602 | # Configure gordon.toml | ||
| 602 | - | [registry_auth] | 603 | + | [auth] |
| 603 | enabled = true | 604 | enabled = true | ||
| 604 | type = "password" | 605 | type = "password" | ||
| 606 | + | secrets_backend = "pass" | |||
| 605 | username = "deploy" | 607 | username = "deploy" | ||
| 606 | - | password_hash = "gordon/registry/password_hash" | 608 | + | password_hash = "gordon/auth/password_hash" |
| 607 | ``` | 609 | ``` | ||
| 608 | 610 | ||||
| 609 | --- | 611 | --- | ||
| @@ -659,6 +661,65 @@ gordon routes deploy myapp.example.com | |||||
| 659 | 661 | ||||
| 660 | --- | 662 | --- | ||
| 661 | 663 | ||||
| 664 | + | ## gordon attachments | |||
| 665 | + | ||||
| 666 | + | Manage attachments on local or remote Gordon instances. | |||
| 667 | + | ||||
| 668 | + | ### Subcommands | |||
| 669 | + | ||||
| 670 | + | | Subcommand | Description | | |||
| 671 | + | |------------|-------------| | |||
| 672 | + | | `list` | List all attachments or attachments for a specific target | | |||
| 673 | + | | `add` | Add an attachment to a domain or network group | | |||
| 674 | + | | `remove` | Remove an attachment from a domain or network group | | |||
| 675 | + | ||||
| 676 | + | ### gordon attachments list | |||
| 677 | + | ||||
| 678 | + | List all configured attachments. | |||
| 679 | + | ||||
| 680 | + | ```bash | |||
| 681 | + | # List all attachments | |||
| 682 | + | gordon attachments list | |||
| 683 | + | ||||
| 684 | + | # List attachments for a specific domain or group | |||
| 685 | + | gordon attachments list app.example.com | |||
| 686 | + | gordon attachments list backend | |||
| 687 | + | ||||
| 688 | + | # Remote mode | |||
| 689 | + | gordon attachments list --remote https://gordon.mydomain.com --token $TOKEN | |||
| 690 | + | ``` | |||
| 691 | + | ||||
| 692 | + | ### gordon attachments add | |||
| 693 | + | ||||
| 694 | + | Add an attachment to a domain or network group. | |||
| 695 | + | ||||
| 696 | + | ```bash | |||
| 697 | + | gordon attachments add <domain-or-group> <image> | |||
| 698 | + | gordon attachments add app.example.com postgres:15 | |||
| 699 | + | gordon attachments add backend redis:7-alpine | |||
| 700 | + | ``` | |||
| 701 | + | ||||
| 702 | + | ### gordon attachments remove | |||
| 703 | + | ||||
| 704 | + | Remove an attachment. | |||
| 705 | + | ||||
| 706 | + | ```bash | |||
| 707 | + | gordon attachments remove <domain-or-group> <image> | |||
| 708 | + | gordon attachments remove app.example.com postgres:15 | |||
| 709 | + | ``` | |||
| 710 | + | ||||
| 711 | + | ### Alias | |||
| 712 | + | ||||
| 713 | + | `gordon attach` is an alias for `gordon attachments`: | |||
| 714 | + | ||||
| 715 | + | ```bash | |||
| 716 | + | gordon attach list | |||
| 717 | + | gordon attach add app.example.com postgres:15 | |||
| 718 | + | gordon attach remove app.example.com postgres:15 | |||
| 719 | + | ``` | |||
| 720 | + | ||||
| 721 | + | --- | |||
| 722 | + | ||||
| 662 | ## gordon secrets | 723 | ## gordon secrets | ||
| 663 | 724 | ||||
| 664 | Manage secrets on local or remote Gordon instances. | 725 | Manage secrets on local or remote Gordon instances. | ||
M docs/config/attachments.md +55 -0
| @@ -204,8 +204,63 @@ Gordon adds labels to attachment containers: | |||
| 204 | 204 | | `gordon.attachment` | `true` | | |
| 205 | 205 | | `gordon.attached-to` | Domain or group name | | |
| 206 | 206 | ||
| 207 | + | ## CLI Management | |
| 208 | + | ||
| 209 | + | Manage attachments via the CLI without editing configuration files. | |
| 210 | + | ||
| 211 | + | ### List Attachments | |
| 212 | + | ||
| 213 | + | ```bash | |
| 214 | + | # List all attachments | |
| 215 | + | gordon attachments list | |
| 216 | + | ||
| 217 | + | # List attachments for a specific domain or network group | |
| 218 | + | gordon attachments list app.mydomain.com | |
| 219 | + | gordon attachments list backend | |
| 220 | + | ||
| 221 | + | # Remote mode | |
| 222 | + | gordon attachments list --remote https://gordon.mydomain.com --token $TOKEN | |
| 223 | + | ``` | |
| 224 | + | ||
| 225 | + | ### Add Attachments | |
| 226 | + | ||
| 227 | + | ```bash | |
| 228 | + | # Add attachment to a domain | |
| 229 | + | gordon attachments add app.mydomain.com postgres:15 | |
| 230 | + | ||
| 231 | + | # Add attachment to a network group | |
| 232 | + | gordon attachments add backend redis:7-alpine | |
| 233 | + | ||
| 234 | + | # Remote mode | |
| 235 | + | gordon attachments add app.mydomain.com postgres:15 --remote https://gordon.mydomain.com --token $TOKEN | |
| 236 | + | ``` | |
| 237 | + | ||
| 238 | + | ### Remove Attachments | |
| 239 | + | ||
| 240 | + | ```bash | |
| 241 | + | # Remove attachment from a domain | |
| 242 | + | gordon attachments remove app.mydomain.com postgres:15 | |
| 243 | + | ||
| 244 | + | # Remove from network group | |
| 245 | + | gordon attachments remove backend redis:7-alpine | |
| 246 | + | ||
| 247 | + | # Remote mode | |
| 248 | + | gordon attachments remove app.mydomain.com postgres:15 --remote https://gordon.mydomain.com --token $TOKEN | |
| 249 | + | ``` | |
| 250 | + | ||
| 251 | + | ### Alias | |
| 252 | + | ||
| 253 | + | The `gordon attach` command is an alias for `gordon attachments`: | |
| 254 | + | ||
| 255 | + | ```bash | |
| 256 | + | gordon attach list | |
| 257 | + | gordon attach add app.mydomain.com postgres:15 | |
| 258 | + | gordon attach remove app.mydomain.com postgres:15 | |
| 259 | + | ``` | |
| 260 | + | ||
| 207 | 261 | ## Related | |
| 208 | 262 | ||
| 209 | 263 | - [Network Isolation](./network-isolation.md) | |
| 210 | 264 | - [Network Groups](./network-groups.md) | |
| 211 | 265 | - [Routes](./routes.md) | |
| 266 | + | - [CLI Commands](/docs/cli/commands.md) | |
| @@ -204,8 +204,63 @@ Gordon adds labels to attachment containers: | |||||
| 204 | | `gordon.attachment` | `true` | | 204 | | `gordon.attachment` | `true` | | ||
| 205 | | `gordon.attached-to` | Domain or group name | | 205 | | `gordon.attached-to` | Domain or group name | | ||
| 206 | 206 | ||||
| 207 | + | ## CLI Management | |||
| 208 | + | ||||
| 209 | + | Manage attachments via the CLI without editing configuration files. | |||
| 210 | + | ||||
| 211 | + | ### List Attachments | |||
| 212 | + | ||||
| 213 | + | ```bash | |||
| 214 | + | # List all attachments | |||
| 215 | + | gordon attachments list | |||
| 216 | + | ||||
| 217 | + | # List attachments for a specific domain or network group | |||
| 218 | + | gordon attachments list app.mydomain.com | |||
| 219 | + | gordon attachments list backend | |||
| 220 | + | ||||
| 221 | + | # Remote mode | |||
| 222 | + | gordon attachments list --remote https://gordon.mydomain.com --token $TOKEN | |||
| 223 | + | ``` | |||
| 224 | + | ||||
| 225 | + | ### Add Attachments | |||
| 226 | + | ||||
| 227 | + | ```bash | |||
| 228 | + | # Add attachment to a domain | |||
| 229 | + | gordon attachments add app.mydomain.com postgres:15 | |||
| 230 | + | ||||
| 231 | + | # Add attachment to a network group | |||
| 232 | + | gordon attachments add backend redis:7-alpine | |||
| 233 | + | ||||
| 234 | + | # Remote mode | |||
| 235 | + | gordon attachments add app.mydomain.com postgres:15 --remote https://gordon.mydomain.com --token $TOKEN | |||
| 236 | + | ``` | |||
| 237 | + | ||||
| 238 | + | ### Remove Attachments | |||
| 239 | + | ||||
| 240 | + | ```bash | |||
| 241 | + | # Remove attachment from a domain | |||
| 242 | + | gordon attachments remove app.mydomain.com postgres:15 | |||
| 243 | + | ||||
| 244 | + | # Remove from network group | |||
| 245 | + | gordon attachments remove backend redis:7-alpine | |||
| 246 | + | ||||
| 247 | + | # Remote mode | |||
| 248 | + | gordon attachments remove app.mydomain.com postgres:15 --remote https://gordon.mydomain.com --token $TOKEN | |||
| 249 | + | ``` | |||
| 250 | + | ||||
| 251 | + | ### Alias | |||
| 252 | + | ||||
| 253 | + | The `gordon attach` command is an alias for `gordon attachments`: | |||
| 254 | + | ||||
| 255 | + | ```bash | |||
| 256 | + | gordon attach list | |||
| 257 | + | gordon attach add app.mydomain.com postgres:15 | |||
| 258 | + | gordon attach remove app.mydomain.com postgres:15 | |||
| 259 | + | ``` | |||
| 260 | + | ||||
| 207 | ## Related | 261 | ## Related | ||
| 208 | 262 | ||||
| 209 | - [Network Isolation](./network-isolation.md) | 263 | - [Network Isolation](./network-isolation.md) | ||
| 210 | - [Network Groups](./network-groups.md) | 264 | - [Network Groups](./network-groups.md) | ||
| 211 | - [Routes](./routes.md) | 265 | - [Routes](./routes.md) | ||
| 266 | + | - [CLI Commands](/docs/cli/commands.md) | |||
R docs/config/registry-auth.md → docs/config/auth.md +96 -56 renamed, 56% similar
| @@ -1,33 +1,43 @@ | |||
| 1 | - | # Registry Authentication | |
| 1 | + | # Authentication | |
| 2 | 2 | ||
| 3 | - | Secure your container registry with password or token-based authentication. | |
| 3 | + | Configure authentication for the Gordon registry and Admin API. | |
| 4 | 4 | ||
| 5 | 5 | ## Configuration | |
| 6 | 6 | ||
| 7 | 7 | ```toml | |
| 8 | - | [registry_auth] | |
| 8 | + | [auth] | |
| 9 | 9 | enabled = true | |
| 10 | - | type = "token" # "password" or "token" | |
| 11 | - | ||
| 12 | - | # For token auth: | |
| 13 | - | token_secret = "gordon/registry/token_secret" # Path in secrets backend | |
| 14 | - | token_expiry = "720h" # Duration or 0 for never | |
| 15 | - | ||
| 16 | - | # For password auth: | |
| 17 | - | # username = "deploy" | |
| 18 | - | # password_hash = "gordon/registry/password_hash" # Path in secrets backend | |
| 10 | + | type = "token" | |
| 11 | + | secrets_backend = "pass" | |
| 12 | + | token_secret = "gordon/auth/token_secret" | |
| 13 | + | token_expiry = "30d" | |
| 19 | 14 | ``` | |
| 20 | 15 | ||
| 16 | + | > **Note:** Authentication is enabled by default for security. For local development without auth, explicitly set `enabled = false`. | |
| 17 | + | ||
| 21 | 18 | ## Options | |
| 22 | 19 | ||
| 23 | 20 | | Option | Type | Default | Description | | |
| 24 | 21 | |--------|------|---------|-------------| | |
| 25 | - | | `enabled` | bool | `false` | Enable registry authentication | | |
| 22 | + | | `enabled` | bool | `true` | Enable authentication | | |
| 26 | 23 | | `type` | string | `"password"` | Auth type: `"password"` or `"token"` | | |
| 24 | + | | `secrets_backend` | string | `"unsafe"` | Secrets backend: `"pass"`, `"sops"`, or `"unsafe"` | | |
| 27 | 25 | | `username` | string | - | Username for password auth | | |
| 28 | 26 | | `password_hash` | string | - | Path to bcrypt hash in secrets backend | | |
| 29 | 27 | | `token_secret` | string | - | Path to JWT signing secret in secrets backend | | |
| 30 | - | | `token_expiry` | string | `"30d"` | Token validity duration (0 = never expires). Supports: d (days), w (weeks), M (months), y (years) | | |
| 28 | + | | `token_expiry` | string | `"30d"` | Token validity duration (0 = never expires) | | |
| 29 | + | ||
| 30 | + | ## Secrets Backend | |
| 31 | + | ||
| 32 | + | The `secrets_backend` option determines how Gordon retrieves sensitive values like `token_secret` and `password_hash`: | |
| 33 | + | ||
| 34 | + | | Backend | Description | | |
| 35 | + | |---------|-------------| | |
| 36 | + | | `pass` | Unix password manager (GPG-encrypted) | | |
| 37 | + | | `sops` | Mozilla SOPS encrypted files | | |
| 38 | + | | `unsafe` | Plain text files (development only) | | |
| 39 | + | ||
| 40 | + | See [Secret Providers](./secrets.md) for detailed configuration of each backend. | |
| 31 | 41 | ||
| 32 | 42 | ## Authentication Types | |
| 33 | 43 | ||
| @@ -36,20 +46,18 @@ token_expiry = "720h" # Duration or 0 for never | |||
| 36 | 46 | JWT-based authentication, ideal for CI/CD pipelines: | |
| 37 | 47 | ||
| 38 | 48 | ```toml | |
| 39 | - | [secrets] | |
| 40 | - | backend = "pass" | |
| 41 | - | ||
| 42 | - | [registry_auth] | |
| 49 | + | [auth] | |
| 43 | 50 | enabled = true | |
| 44 | 51 | type = "token" | |
| 45 | - | token_secret = "gordon/registry/token_secret" | |
| 46 | - | token_expiry = "30d" # 30 days, or "0" for never. Also: 1y, 2w, 6M | |
| 52 | + | secrets_backend = "pass" | |
| 53 | + | token_secret = "gordon/auth/token_secret" | |
| 54 | + | token_expiry = "30d" | |
| 47 | 55 | ``` | |
| 48 | 56 | ||
| 49 | 57 | **Setup:** | |
| 50 | 58 | ```bash | |
| 51 | 59 | # Create token secret (random 32+ characters) | |
| 52 | - | pass insert gordon/registry/token_secret | |
| 60 | + | pass insert gordon/auth/token_secret | |
| 53 | 61 | ||
| 54 | 62 | # Generate a token | |
| 55 | 63 | gordon auth token generate --subject ci-bot --scopes push,pull --expiry 0 | |
| @@ -66,8 +74,11 @@ docker login -u ci-bot -p <token> registry.mydomain.com | |||
| 66 | 74 | # List all tokens | |
| 67 | 75 | gordon auth token list | |
| 68 | 76 | ||
| 69 | - | # Revoke a token | |
| 77 | + | # Revoke a specific token | |
| 70 | 78 | gordon auth token revoke <token-id> | |
| 79 | + | ||
| 80 | + | # Revoke all tokens | |
| 81 | + | gordon auth token revoke --all | |
| 71 | 82 | ``` | |
| 72 | 83 | ||
| 73 | 84 | ### Password Authentication | |
| @@ -75,14 +86,12 @@ gordon auth token revoke <token-id> | |||
| 75 | 86 | Simple username/password authentication: | |
| 76 | 87 | ||
| 77 | 88 | ```toml | |
| 78 | - | [secrets] | |
| 79 | - | backend = "pass" | |
| 80 | - | ||
| 81 | - | [registry_auth] | |
| 89 | + | [auth] | |
| 82 | 90 | enabled = true | |
| 83 | 91 | type = "password" | |
| 92 | + | secrets_backend = "pass" | |
| 84 | 93 | username = "deploy" | |
| 85 | - | password_hash = "gordon/registry/password_hash" | |
| 94 | + | password_hash = "gordon/auth/password_hash" | |
| 86 | 95 | ``` | |
| 87 | 96 | ||
| 88 | 97 | **Setup:** | |
| @@ -92,7 +101,7 @@ gordon auth password hash | |||
| 92 | 101 | # Enter password when prompted | |
| 93 | 102 | ||
| 94 | 103 | # Store hash in secrets backend | |
| 95 | - | pass insert gordon/registry/password_hash | |
| 104 | + | pass insert gordon/auth/password_hash | |
| 96 | 105 | # Paste the bcrypt hash | |
| 97 | 106 | ``` | |
| 98 | 107 | ||
| @@ -105,27 +114,51 @@ docker login -u deploy -p <password> registry.mydomain.com | |||
| 105 | 114 | ||
| 106 | 115 | Tokens can have different permission levels: | |
| 107 | 116 | ||
| 117 | + | ### Registry Scopes | |
| 118 | + | ||
| 108 | 119 | | Scope | Permission | | |
| 109 | 120 | |-------|------------| | |
| 110 | 121 | | `push` | Push images to registry | | |
| 111 | 122 | | `pull` | Pull images from registry | | |
| 112 | 123 | | `push,pull` | Both push and pull (default) | | |
| 113 | 124 | ||
| 125 | + | ### Admin Scopes | |
| 126 | + | ||
| 127 | + | For remote CLI access via the Admin API: | |
| 128 | + | ||
| 129 | + | | Scope | Permission | | |
| 130 | + | |-------|------------| | |
| 131 | + | | `admin:*:*` | Full admin access | | |
| 132 | + | | `admin:routes:read` | Read-only routes access | | |
| 133 | + | | `admin:routes:write` | Routes write access | | |
| 134 | + | | `admin:config:read` | Read-only config access | | |
| 135 | + | | `admin:config:write` | Config write access | | |
| 136 | + | | `admin:status:read` | Read-only status/health | | |
| 137 | + | | `admin:secrets:read` | List secret keys | | |
| 138 | + | | `admin:secrets:write` | Set/delete secrets | | |
| 139 | + | ||
| 140 | + | **Examples:** | |
| 114 | 141 | ```bash | |
| 115 | 142 | # Pull-only token (for read-only access) | |
| 116 | 143 | gordon auth token generate --subject reader --scopes pull --expiry 30d | |
| 117 | 144 | ||
| 118 | 145 | # Push-only token (for CI builds) | |
| 119 | 146 | gordon auth token generate --subject builder --scopes push --expiry 0 | |
| 147 | + | ||
| 148 | + | # Full admin access (for remote CLI) | |
| 149 | + | gordon auth token generate --subject admin --scopes "push,pull,admin:*:*" --expiry 0 | |
| 120 | 150 | ``` | |
| 121 | 151 | ||
| 122 | 152 | ## Token Expiry | |
| 123 | 153 | ||
| 124 | 154 | Control how long tokens remain valid. Supports human-friendly units: | |
| 125 | - | - `d` - days (24 hours) | |
| 126 | - | - `w` - weeks (7 days) | |
| 127 | - | - `M` - months (30 days) | |
| 128 | - | - `y` - years (365 days) | |
| 155 | + | ||
| 156 | + | | Unit | Description | | |
| 157 | + | |------|-------------| | |
| 158 | + | | `d` | days (24 hours) | | |
| 159 | + | | `w` | weeks (7 days) | | |
| 160 | + | | `M` | months (30 days) | | |
| 161 | + | | `y` | years (365 days) | | |
| 129 | 162 | ||
| 130 | 163 | Compound durations work too: `1y6M`, `2w3d` | |
| 131 | 164 | ||
| @@ -138,39 +171,40 @@ gordon auth token generate --subject deploy --expiry 1y | |||
| 138 | 171 | ||
| 139 | 172 | # Expires in 30 days | |
| 140 | 173 | gordon auth token generate --subject temp --expiry 30d | |
| 141 | - | ||
| 142 | - | # Expires in 2 weeks | |
| 143 | - | gordon auth token generate --subject short --expiry 2w | |
| 144 | 174 | ``` | |
| 145 | 175 | ||
| 146 | 176 | ## Instance-Specific Tokens | |
| 147 | 177 | ||
| 148 | - | Tokens are signed with the `token_secret`, which is unique to each Gordon instance. This means: | |
| 178 | + | Tokens are signed with the `token_secret`, which is unique to each Gordon instance: | |
| 149 | 179 | ||
| 150 | 180 | - Tokens from one Gordon instance won't work on another | |
| 151 | 181 | - Changing the `token_secret` invalidates all existing tokens | |
| 152 | 182 | - Each environment (dev, staging, prod) should have different secrets | |
| 153 | 183 | ||
| 184 | + | ## Development Setup | |
| 185 | + | ||
| 186 | + | For local development without authentication: | |
| 187 | + | ||
| 188 | + | ```toml | |
| 189 | + | [auth] | |
| 190 | + | enabled = false | |
| 191 | + | ``` | |
| 192 | + | ||
| 154 | 193 | ## Examples | |
| 155 | 194 | ||
| 156 | 195 | ### CI/CD Setup | |
| 157 | 196 | ||
| 158 | 197 | ```toml | |
| 159 | - | [secrets] | |
| 160 | - | backend = "pass" | |
| 161 | - | ||
| 162 | - | [registry_auth] | |
| 198 | + | [auth] | |
| 163 | 199 | enabled = true | |
| 164 | 200 | type = "token" | |
| 165 | - | token_secret = "gordon/registry/token_secret" | |
| 201 | + | secrets_backend = "pass" | |
| 202 | + | token_secret = "gordon/auth/token_secret" | |
| 166 | 203 | ``` | |
| 167 | 204 | ||
| 168 | 205 | ```bash | |
| 169 | 206 | # Generate CI token | |
| 170 | 207 | gordon auth token generate --subject github-actions --scopes push,pull --expiry 0 | |
| 171 | - | ||
| 172 | - | # Token output: | |
| 173 | - | # eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... | |
| 174 | 208 | ``` | |
| 175 | 209 | ||
| 176 | 210 | In GitHub Actions: | |
| @@ -179,32 +213,38 @@ In GitHub Actions: | |||
| 179 | 213 | run: docker login -u github-actions -p ${{ secrets.GORDON_TOKEN }} ${{ secrets.GORDON_REGISTRY }} | |
| 180 | 214 | ``` | |
| 181 | 215 | ||
| 182 | - | ### Development Setup | |
| 216 | + | ### Production with Password | |
| 183 | 217 | ||
| 184 | 218 | ```toml | |
| 185 | - | [registry_auth] | |
| 186 | - | enabled = false # Disable for local development | |
| 219 | + | [auth] | |
| 220 | + | enabled = true | |
| 221 | + | type = "password" | |
| 222 | + | secrets_backend = "pass" | |
| 223 | + | username = "deploy" | |
| 224 | + | password_hash = "gordon/auth/password_hash" | |
| 187 | 225 | ``` | |
| 188 | 226 | ||
| 189 | - | ### Production with Password | |
| 227 | + | ### Enterprise with SOPS | |
| 190 | 228 | ||
| 191 | 229 | ```toml | |
| 192 | - | [secrets] | |
| 193 | - | backend = "pass" | |
| 194 | - | ||
| 195 | - | [registry_auth] | |
| 230 | + | [auth] | |
| 196 | 231 | enabled = true | |
| 197 | - | type = "password" | |
| 198 | - | username = "deploy" | |
| 199 | - | password_hash = "gordon/registry/password_hash" | |
| 232 | + | type = "token" | |
| 233 | + | secrets_backend = "sops" | |
| 234 | + | token_secret = "gordon/auth/token_secret" | |
| 200 | 235 | ``` | |
| 201 | 236 | ||
| 202 | 237 | ## Internal Registry Auth | |
| 203 | 238 | ||
| 204 | 239 | Gordon generates internal credentials automatically when auth is enabled. These are used for loopback pulls when deploying containers and are regenerated on each restart. | |
| 205 | 240 | ||
| 241 | + | To view internal credentials (for troubleshooting): | |
| 242 | + | ```bash | |
| 243 | + | gordon auth internal | |
| 244 | + | ``` | |
| 245 | + | ||
| 206 | 246 | ## Related | |
| 207 | 247 | ||
| 208 | - | - [Secrets Configuration](./secrets.md) | |
| 248 | + | - [Secret Providers](./secrets.md) | |
| 209 | 249 | - [CLI Commands](../cli/commands.md) | |
| 210 | 250 | - [GitHub Actions Deployment](../deployment/github-actions.md) | |
| @@ -1,33 +1,43 @@ | |||||
| 1 | - | # Registry Authentication | 1 | + | # Authentication |
| 2 | 2 | ||||
| 3 | - | Secure your container registry with password or token-based authentication. | 3 | + | Configure authentication for the Gordon registry and Admin API. |
| 4 | 4 | ||||
| 5 | ## Configuration | 5 | ## Configuration | ||
| 6 | 6 | ||||
| 7 | ```toml | 7 | ```toml | ||
| 8 | - | [registry_auth] | 8 | + | [auth] |
| 9 | enabled = true | 9 | enabled = true | ||
| 10 | - | type = "token" # "password" or "token" | 10 | + | type = "token" |
| 11 | - | 11 | + | secrets_backend = "pass" | |
| 12 | - | # For token auth: | 12 | + | token_secret = "gordon/auth/token_secret" |
| 13 | - | token_secret = "gordon/registry/token_secret" # Path in secrets backend | 13 | + | token_expiry = "30d" |
| 14 | - | token_expiry = "720h" # Duration or 0 for never | |||
| 15 | - | ||||
| 16 | - | # For password auth: | |||
| 17 | - | # username = "deploy" | |||
| 18 | - | # password_hash = "gordon/registry/password_hash" # Path in secrets backend | |||
| 19 | ``` | 14 | ``` | ||
| 20 | 15 | ||||
| 16 | + | > **Note:** Authentication is enabled by default for security. For local development without auth, explicitly set `enabled = false`. | |||
| 17 | + | ||||
| 21 | ## Options | 18 | ## Options | ||
| 22 | 19 | ||||
| 23 | | Option | Type | Default | Description | | 20 | | Option | Type | Default | Description | | ||
| 24 | |--------|------|---------|-------------| | 21 | |--------|------|---------|-------------| | ||
| 25 | - | | `enabled` | bool | `false` | Enable registry authentication | | 22 | + | | `enabled` | bool | `true` | Enable authentication | |
| 26 | | `type` | string | `"password"` | Auth type: `"password"` or `"token"` | | 23 | | `type` | string | `"password"` | Auth type: `"password"` or `"token"` | | ||
| 24 | + | | `secrets_backend` | string | `"unsafe"` | Secrets backend: `"pass"`, `"sops"`, or `"unsafe"` | | |||
| 27 | | `username` | string | - | Username for password auth | | 25 | | `username` | string | - | Username for password auth | | ||
| 28 | | `password_hash` | string | - | Path to bcrypt hash in secrets backend | | 26 | | `password_hash` | string | - | Path to bcrypt hash in secrets backend | | ||
| 29 | | `token_secret` | string | - | Path to JWT signing secret in secrets backend | | 27 | | `token_secret` | string | - | Path to JWT signing secret in secrets backend | | ||
| 30 | - | | `token_expiry` | string | `"30d"` | Token validity duration (0 = never expires). Supports: d (days), w (weeks), M (months), y (years) | | 28 | + | | `token_expiry` | string | `"30d"` | Token validity duration (0 = never expires) | |
| 29 | + | ||||
| 30 | + | ## Secrets Backend | |||
| 31 | + | ||||
| 32 | + | The `secrets_backend` option determines how Gordon retrieves sensitive values like `token_secret` and `password_hash`: | |||
| 33 | + | ||||
| 34 | + | | Backend | Description | | |||
| 35 | + | |---------|-------------| | |||
| 36 | + | | `pass` | Unix password manager (GPG-encrypted) | | |||
| 37 | + | | `sops` | Mozilla SOPS encrypted files | | |||
| 38 | + | | `unsafe` | Plain text files (development only) | | |||
| 39 | + | ||||
| 40 | + | See [Secret Providers](./secrets.md) for detailed configuration of each backend. | |||
| 31 | 41 | ||||
| 32 | ## Authentication Types | 42 | ## Authentication Types | ||
| 33 | 43 | ||||
| @@ -36,20 +46,18 @@ token_expiry = "720h" # Duration or 0 for never | |||||
| 36 | JWT-based authentication, ideal for CI/CD pipelines: | 46 | JWT-based authentication, ideal for CI/CD pipelines: | ||
| 37 | 47 | ||||
| 38 | ```toml | 48 | ```toml | ||
| 39 | - | [secrets] | 49 | + | [auth] |
| 40 | - | backend = "pass" | |||
| 41 | - | ||||
| 42 | - | [registry_auth] | |||
| 43 | enabled = true | 50 | enabled = true | ||
| 44 | type = "token" | 51 | type = "token" | ||
| 45 | - | token_secret = "gordon/registry/token_secret" | 52 | + | secrets_backend = "pass" |
| 46 | - | token_expiry = "30d" # 30 days, or "0" for never. Also: 1y, 2w, 6M | 53 | + | token_secret = "gordon/auth/token_secret" |
| 54 | + | token_expiry = "30d" | |||
| 47 | ``` | 55 | ``` | ||
| 48 | 56 | ||||
| 49 | **Setup:** | 57 | **Setup:** | ||
| 50 | ```bash | 58 | ```bash | ||
| 51 | # Create token secret (random 32+ characters) | 59 | # Create token secret (random 32+ characters) | ||
| 52 | - | pass insert gordon/registry/token_secret | 60 | + | pass insert gordon/auth/token_secret |
| 53 | 61 | ||||
| 54 | # Generate a token | 62 | # Generate a token | ||
| 55 | gordon auth token generate --subject ci-bot --scopes push,pull --expiry 0 | 63 | gordon auth token generate --subject ci-bot --scopes push,pull --expiry 0 | ||
| @@ -66,8 +74,11 @@ docker login -u ci-bot -p <token> registry.mydomain.com | |||||
| 66 | # List all tokens | 74 | # List all tokens | ||
| 67 | gordon auth token list | 75 | gordon auth token list | ||
| 68 | 76 | ||||
| 69 | - | # Revoke a token | 77 | + | # Revoke a specific token |
| 70 | gordon auth token revoke <token-id> | 78 | gordon auth token revoke <token-id> | ||
| 79 | + | ||||
| 80 | + | # Revoke all tokens | |||
| 81 | + | gordon auth token revoke --all | |||
| 71 | ``` | 82 | ``` | ||
| 72 | 83 | ||||
| 73 | ### Password Authentication | 84 | ### Password Authentication | ||
| @@ -75,14 +86,12 @@ gordon auth token revoke <token-id> | |||||
| 75 | Simple username/password authentication: | 86 | Simple username/password authentication: | ||
| 76 | 87 | ||||
| 77 | ```toml | 88 | ```toml | ||
| 78 | - | [secrets] | 89 | + | [auth] |
| 79 | - | backend = "pass" | |||
| 80 | - | ||||
| 81 | - | [registry_auth] | |||
| 82 | enabled = true | 90 | enabled = true | ||
| 83 | type = "password" | 91 | type = "password" | ||
| 92 | + | secrets_backend = "pass" | |||
| 84 | username = "deploy" | 93 | username = "deploy" | ||
| 85 | - | password_hash = "gordon/registry/password_hash" | 94 | + | password_hash = "gordon/auth/password_hash" |
| 86 | ``` | 95 | ``` | ||
| 87 | 96 | ||||
| 88 | **Setup:** | 97 | **Setup:** | ||
| @@ -92,7 +101,7 @@ gordon auth password hash | |||||
| 92 | # Enter password when prompted | 101 | # Enter password when prompted | ||
| 93 | 102 | ||||
| 94 | # Store hash in secrets backend | 103 | # Store hash in secrets backend | ||
| 95 | - | pass insert gordon/registry/password_hash | 104 | + | pass insert gordon/auth/password_hash |
| 96 | # Paste the bcrypt hash | 105 | # Paste the bcrypt hash | ||
| 97 | ``` | 106 | ``` | ||
| 98 | 107 | ||||
| @@ -105,27 +114,51 @@ docker login -u deploy -p <password> registry.mydomain.com | |||||
| 105 | 114 | ||||
| 106 | Tokens can have different permission levels: | 115 | Tokens can have different permission levels: | ||
| 107 | 116 | ||||
| 117 | + | ### Registry Scopes | |||
| 118 | + | ||||
| 108 | | Scope | Permission | | 119 | | Scope | Permission | | ||
| 109 | |-------|------------| | 120 | |-------|------------| | ||
| 110 | | `push` | Push images to registry | | 121 | | `push` | Push images to registry | | ||
| 111 | | `pull` | Pull images from registry | | 122 | | `pull` | Pull images from registry | | ||
| 112 | | `push,pull` | Both push and pull (default) | | 123 | | `push,pull` | Both push and pull (default) | | ||
| 113 | 124 | ||||
| 125 | + | ### Admin Scopes | |||
| 126 | + | ||||
| 127 | + | For remote CLI access via the Admin API: | |||
| 128 | + | ||||
| 129 | + | | Scope | Permission | | |||
| 130 | + | |-------|------------| | |||
| 131 | + | | `admin:*:*` | Full admin access | | |||
| 132 | + | | `admin:routes:read` | Read-only routes access | | |||
| 133 | + | | `admin:routes:write` | Routes write access | | |||
| 134 | + | | `admin:config:read` | Read-only config access | | |||
| 135 | + | | `admin:config:write` | Config write access | | |||
| 136 | + | | `admin:status:read` | Read-only status/health | | |||
| 137 | + | | `admin:secrets:read` | List secret keys | | |||
| 138 | + | | `admin:secrets:write` | Set/delete secrets | | |||
| 139 | + | ||||
| 140 | + | **Examples:** | |||
| 114 | ```bash | 141 | ```bash | ||
| 115 | # Pull-only token (for read-only access) | 142 | # Pull-only token (for read-only access) | ||
| 116 | gordon auth token generate --subject reader --scopes pull --expiry 30d | 143 | gordon auth token generate --subject reader --scopes pull --expiry 30d | ||
| 117 | 144 | ||||
| 118 | # Push-only token (for CI builds) | 145 | # Push-only token (for CI builds) | ||
| 119 | gordon auth token generate --subject builder --scopes push --expiry 0 | 146 | gordon auth token generate --subject builder --scopes push --expiry 0 | ||
| 147 | + | ||||
| 148 | + | # Full admin access (for remote CLI) | |||
| 149 | + | gordon auth token generate --subject admin --scopes "push,pull,admin:*:*" --expiry 0 | |||
| 120 | ``` | 150 | ``` | ||
| 121 | 151 | ||||
| 122 | ## Token Expiry | 152 | ## Token Expiry | ||
| 123 | 153 | ||||
| 124 | Control how long tokens remain valid. Supports human-friendly units: | 154 | Control how long tokens remain valid. Supports human-friendly units: | ||
| 125 | - | - `d` - days (24 hours) | 155 | + | |
| 126 | - | - `w` - weeks (7 days) | 156 | + | | Unit | Description | |
| 127 | - | - `M` - months (30 days) | 157 | + | |------|-------------| |
| 128 | - | - `y` - years (365 days) | 158 | + | | `d` | days (24 hours) | |
| 159 | + | | `w` | weeks (7 days) | | |||
| 160 | + | | `M` | months (30 days) | | |||
| 161 | + | | `y` | years (365 days) | | |||
| 129 | 162 | ||||
| 130 | Compound durations work too: `1y6M`, `2w3d` | 163 | Compound durations work too: `1y6M`, `2w3d` | ||
| 131 | 164 | ||||
| @@ -138,39 +171,40 @@ gordon auth token generate --subject deploy --expiry 1y | |||||
| 138 | 171 | ||||
| 139 | # Expires in 30 days | 172 | # Expires in 30 days | ||
| 140 | gordon auth token generate --subject temp --expiry 30d | 173 | gordon auth token generate --subject temp --expiry 30d | ||
| 141 | - | ||||
| 142 | - | # Expires in 2 weeks | |||
| 143 | - | gordon auth token generate --subject short --expiry 2w | |||
| 144 | ``` | 174 | ``` | ||
| 145 | 175 | ||||
| 146 | ## Instance-Specific Tokens | 176 | ## Instance-Specific Tokens | ||
| 147 | 177 | ||||
| 148 | - | Tokens are signed with the `token_secret`, which is unique to each Gordon instance. This means: | 178 | + | Tokens are signed with the `token_secret`, which is unique to each Gordon instance: |
| 149 | 179 | ||||
| 150 | - Tokens from one Gordon instance won't work on another | 180 | - Tokens from one Gordon instance won't work on another | ||
| 151 | - Changing the `token_secret` invalidates all existing tokens | 181 | - Changing the `token_secret` invalidates all existing tokens | ||
| 152 | - Each environment (dev, staging, prod) should have different secrets | 182 | - Each environment (dev, staging, prod) should have different secrets | ||
| 153 | 183 | ||||
| 184 | + | ## Development Setup | |||
| 185 | + | ||||
| 186 | + | For local development without authentication: | |||
| 187 | + | ||||
| 188 | + | ```toml | |||
| 189 | + | [auth] | |||
| 190 | + | enabled = false | |||
| 191 | + | ``` | |||
| 192 | + | ||||
| 154 | ## Examples | 193 | ## Examples | ||
| 155 | 194 | ||||
| 156 | ### CI/CD Setup | 195 | ### CI/CD Setup | ||
| 157 | 196 | ||||
| 158 | ```toml | 197 | ```toml | ||
| 159 | - | [secrets] | 198 | + | [auth] |
| 160 | - | backend = "pass" | |||
| 161 | - | ||||
| 162 | - | [registry_auth] | |||
| 163 | enabled = true | 199 | enabled = true | ||
| 164 | type = "token" | 200 | type = "token" | ||
| 165 | - | token_secret = "gordon/registry/token_secret" | 201 | + | secrets_backend = "pass" |
| 202 | + | token_secret = "gordon/auth/token_secret" | |||
| 166 | ``` | 203 | ``` | ||
| 167 | 204 | ||||
| 168 | ```bash | 205 | ```bash | ||
| 169 | # Generate CI token | 206 | # Generate CI token | ||
| 170 | gordon auth token generate --subject github-actions --scopes push,pull --expiry 0 | 207 | gordon auth token generate --subject github-actions --scopes push,pull --expiry 0 | ||
| 171 | - | ||||
| 172 | - | # Token output: | |||
| 173 | - | # eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... | |||
| 174 | ``` | 208 | ``` | ||
| 175 | 209 | ||||
| 176 | In GitHub Actions: | 210 | In GitHub Actions: | ||
| @@ -179,32 +213,38 @@ In GitHub Actions: | |||||
| 179 | run: docker login -u github-actions -p ${{ secrets.GORDON_TOKEN }} ${{ secrets.GORDON_REGISTRY }} | 213 | run: docker login -u github-actions -p ${{ secrets.GORDON_TOKEN }} ${{ secrets.GORDON_REGISTRY }} | ||
| 180 | ``` | 214 | ``` | ||
| 181 | 215 | ||||
| 182 | - | ### Development Setup | 216 | + | ### Production with Password |
| 183 | 217 | ||||
| 184 | ```toml | 218 | ```toml | ||
| 185 | - | [registry_auth] | 219 | + | [auth] |
| 186 | - | enabled = false # Disable for local development | 220 | + | enabled = true |
| 221 | + | type = "password" | |||
| 222 | + | secrets_backend = "pass" | |||
| 223 | + | username = "deploy" | |||
| 224 | + | password_hash = "gordon/auth/password_hash" | |||
| 187 | ``` | 225 | ``` | ||
| 188 | 226 | ||||
| 189 | - | ### Production with Password | 227 | + | ### Enterprise with SOPS |
| 190 | 228 | ||||
| 191 | ```toml | 229 | ```toml | ||
| 192 | - | [secrets] | 230 | + | [auth] |
| 193 | - | backend = "pass" | |||
| 194 | - | ||||
| 195 | - | [registry_auth] | |||
| 196 | enabled = true | 231 | enabled = true | ||
| 197 | - | type = "password" | 232 | + | type = "token" |
| 198 | - | username = "deploy" | 233 | + | secrets_backend = "sops" |
| 199 | - | password_hash = "gordon/registry/password_hash" | 234 | + | token_secret = "gordon/auth/token_secret" |
| 200 | ``` | 235 | ``` | ||
| 201 | 236 | ||||
| 202 | ## Internal Registry Auth | 237 | ## Internal Registry Auth | ||
| 203 | 238 | ||||
| 204 | Gordon generates internal credentials automatically when auth is enabled. These are used for loopback pulls when deploying containers and are regenerated on each restart. | 239 | Gordon generates internal credentials automatically when auth is enabled. These are used for loopback pulls when deploying containers and are regenerated on each restart. | ||
| 205 | 240 | ||||
| 241 | + | To view internal credentials (for troubleshooting): | |||
| 242 | + | ```bash | |||
| 243 | + | gordon auth internal | |||
| 244 | + | ``` | |||
| 245 | + | ||||
| 206 | ## Related | 246 | ## Related | ||
| 207 | 247 | ||||
| 208 | - | - [Secrets Configuration](./secrets.md) | 248 | + | - [Secret Providers](./secrets.md) |
| 209 | - [CLI Commands](../cli/commands.md) | 249 | - [CLI Commands](../cli/commands.md) | ||
| 210 | - [GitHub Actions Deployment](../deployment/github-actions.md) | 250 | - [GitHub Actions Deployment](../deployment/github-actions.md) | ||
M docs/config/index.md +13 -18
| @@ -33,19 +33,16 @@ registry_port = 5000 # Registry port (default: 5000) | |||
| 33 | 33 | gordon_domain = "gordon.mydomain.com" # Required: Gordon domain (registry + API) | |
| 34 | 34 | # data_dir = "~/.gordon" # Default for user installations | |
| 35 | 35 | ||
| 36 | - | # Secrets backend | |
| 37 | - | [secrets] | |
| 38 | - | backend = "pass" # "pass", "sops", or "unsafe" | |
| 39 | - | ||
| 40 | - | # Registry authentication | |
| 41 | - | [registry_auth] | |
| 42 | - | enabled = true | |
| 36 | + | # Authentication (includes secrets backend) | |
| 37 | + | [auth] | |
| 38 | + | enabled = true # Enable registry authentication (default: true) | |
| 43 | 39 | type = "token" # "password" or "token" | |
| 40 | + | secrets_backend = "pass" # "pass", "sops", or "unsafe" | |
| 44 | 41 | # Password auth: | |
| 45 | 42 | # username = "deploy" | |
| 46 | - | # password_hash = "gordon/registry/password_hash" | |
| 43 | + | # password_hash = "gordon/auth/password_hash" | |
| 47 | 44 | # Token auth: | |
| 48 | - | token_secret = "gordon/registry/token_secret" | |
| 45 | + | token_secret = "gordon/auth/token_secret" | |
| 49 | 46 | token_expiry = "30d" # Duration (1y, 30d, 2w) or 0 for never | |
| 50 | 47 | ||
| 51 | 48 | # Deploy behavior | |
| @@ -111,8 +108,7 @@ enabled = false # Auto-create routes from image names | |||
| 111 | 108 | | Section | Description | Documentation | | |
| 112 | 109 | |---------|-------------|---------------| | |
| 113 | 110 | | `[server]` | Core server settings | [Server](./server.md) | | |
| 114 | - | | `[secrets]` | Secrets backend configuration | [Secrets](./secrets.md) | | |
| 115 | - | | `[registry_auth]` | Registry authentication | [Registry Auth](./registry-auth.md) | | |
| 111 | + | | `[auth]` | Authentication and secrets backend | [Auth](./auth.md) | | |
| 116 | 112 | | `[deploy]` | Deployment behavior | [Deploy](./deploy.md) | | |
| 117 | 113 | | `[logging]` | Logging configuration | [Logging](./logging.md) | | |
| 118 | 114 | | `[env]` | Environment variable settings | [Environment](./env.md) | | |
| @@ -131,10 +127,10 @@ enabled = false # Auto-create routes from image names | |||
| 131 | 127 | | `server.port` | `80` | | |
| 132 | 128 | | `server.registry_port` | `5000` | | |
| 133 | 129 | | `server.data_dir` | `~/.gordon` | | |
| 134 | - | | `secrets.backend` | `"unsafe"` | | |
| 135 | - | | `registry_auth.enabled` | `false` | | |
| 136 | - | | `registry_auth.type` | `"password"` | | |
| 137 | - | | `registry_auth.token_expiry` | `"30d"` | | |
| 130 | + | | `auth.enabled` | `true` | | |
| 131 | + | | `auth.type` | `"password"` | | |
| 132 | + | | `auth.secrets_backend` | `"unsafe"` | | |
| 133 | + | | `auth.token_expiry` | `"30d"` | | |
| 138 | 134 | | `deploy.pull_policy` | `"if-tag-changed"` | | |
| 139 | 135 | | `logging.level` | `"info"` | | |
| 140 | 136 | | `logging.format` | `"console"` | | |
| @@ -162,8 +158,7 @@ The following settings require a restart to take effect: | |||
| 162 | 158 | - `server.port` | |
| 163 | 159 | - `server.registry_port` | |
| 164 | 160 | - `server.data_dir` | |
| 165 | - | - `secrets.backend` | |
| 166 | - | - `registry_auth` settings | |
| 161 | + | - `auth` settings | |
| 167 | 162 | - `deploy.pull_policy` | |
| 168 | 163 | ||
| 169 | 164 | ## Environment Variable Override | |
| @@ -182,4 +177,4 @@ Pattern: `GORDON_SECTION_KEY` (uppercase, underscores instead of dots) | |||
| 182 | 177 | - [Server Configuration](./server.md) | |
| 183 | 178 | - [Routes Configuration](./routes.md) | |
| 184 | 179 | - [External Routes](./external-routes.md) | |
| 185 | - | - [Registry Authentication](./registry-auth.md) | |
| 180 | + | - [Authentication](./auth.md) | |
| @@ -33,19 +33,16 @@ registry_port = 5000 # Registry port (default: 5000) | |||||
| 33 | gordon_domain = "gordon.mydomain.com" # Required: Gordon domain (registry + API) | 33 | gordon_domain = "gordon.mydomain.com" # Required: Gordon domain (registry + API) | ||
| 34 | # data_dir = "~/.gordon" # Default for user installations | 34 | # data_dir = "~/.gordon" # Default for user installations | ||
| 35 | 35 | ||||
| 36 | - | # Secrets backend | 36 | + | # Authentication (includes secrets backend) |
| 37 | - | [secrets] | 37 | + | [auth] |
| 38 | - | backend = "pass" # "pass", "sops", or "unsafe" | 38 | + | enabled = true # Enable registry authentication (default: true) |
| 39 | - | ||||
| 40 | - | # Registry authentication | |||
| 41 | - | [registry_auth] | |||
| 42 | - | enabled = true | |||
| 43 | type = "token" # "password" or "token" | 39 | type = "token" # "password" or "token" | ||
| 40 | + | secrets_backend = "pass" # "pass", "sops", or "unsafe" | |||
| 44 | # Password auth: | 41 | # Password auth: | ||
| 45 | # username = "deploy" | 42 | # username = "deploy" | ||
| 46 | - | # password_hash = "gordon/registry/password_hash" | 43 | + | # password_hash = "gordon/auth/password_hash" |
| 47 | # Token auth: | 44 | # Token auth: | ||
| 48 | - | token_secret = "gordon/registry/token_secret" | 45 | + | token_secret = "gordon/auth/token_secret" |
| 49 | token_expiry = "30d" # Duration (1y, 30d, 2w) or 0 for never | 46 | token_expiry = "30d" # Duration (1y, 30d, 2w) or 0 for never | ||
| 50 | 47 | ||||
| 51 | # Deploy behavior | 48 | # Deploy behavior | ||
| @@ -111,8 +108,7 @@ enabled = false # Auto-create routes from image names | |||||
| 111 | | Section | Description | Documentation | | 108 | | Section | Description | Documentation | | ||
| 112 | |---------|-------------|---------------| | 109 | |---------|-------------|---------------| | ||
| 113 | | `[server]` | Core server settings | [Server](./server.md) | | 110 | | `[server]` | Core server settings | [Server](./server.md) | | ||
| 114 | - | | `[secrets]` | Secrets backend configuration | [Secrets](./secrets.md) | | 111 | + | | `[auth]` | Authentication and secrets backend | [Auth](./auth.md) | |
| 115 | - | | `[registry_auth]` | Registry authentication | [Registry Auth](./registry-auth.md) | | |||
| 116 | | `[deploy]` | Deployment behavior | [Deploy](./deploy.md) | | 112 | | `[deploy]` | Deployment behavior | [Deploy](./deploy.md) | | ||
| 117 | | `[logging]` | Logging configuration | [Logging](./logging.md) | | 113 | | `[logging]` | Logging configuration | [Logging](./logging.md) | | ||
| 118 | | `[env]` | Environment variable settings | [Environment](./env.md) | | 114 | | `[env]` | Environment variable settings | [Environment](./env.md) | | ||
| @@ -131,10 +127,10 @@ enabled = false # Auto-create routes from image names | |||||
| 131 | | `server.port` | `80` | | 127 | | `server.port` | `80` | | ||
| 132 | | `server.registry_port` | `5000` | | 128 | | `server.registry_port` | `5000` | | ||
| 133 | | `server.data_dir` | `~/.gordon` | | 129 | | `server.data_dir` | `~/.gordon` | | ||
| 134 | - | | `secrets.backend` | `"unsafe"` | | 130 | + | | `auth.enabled` | `true` | |
| 135 | - | | `registry_auth.enabled` | `false` | | 131 | + | | `auth.type` | `"password"` | |
| 136 | - | | `registry_auth.type` | `"password"` | | 132 | + | | `auth.secrets_backend` | `"unsafe"` | |
| 137 | - | | `registry_auth.token_expiry` | `"30d"` | | 133 | + | | `auth.token_expiry` | `"30d"` | |
| 138 | | `deploy.pull_policy` | `"if-tag-changed"` | | 134 | | `deploy.pull_policy` | `"if-tag-changed"` | | ||
| 139 | | `logging.level` | `"info"` | | 135 | | `logging.level` | `"info"` | | ||
| 140 | | `logging.format` | `"console"` | | 136 | | `logging.format` | `"console"` | | ||
| @@ -162,8 +158,7 @@ The following settings require a restart to take effect: | |||||
| 162 | - `server.port` | 158 | - `server.port` | ||
| 163 | - `server.registry_port` | 159 | - `server.registry_port` | ||
| 164 | - `server.data_dir` | 160 | - `server.data_dir` | ||
| 165 | - | - `secrets.backend` | 161 | + | - `auth` settings |
| 166 | - | - `registry_auth` settings | |||
| 167 | - `deploy.pull_policy` | 162 | - `deploy.pull_policy` | ||
| 168 | 163 | ||||
| 169 | ## Environment Variable Override | 164 | ## Environment Variable Override | ||
| @@ -182,4 +177,4 @@ Pattern: `GORDON_SECTION_KEY` (uppercase, underscores instead of dots) | |||||
| 182 | - [Server Configuration](./server.md) | 177 | - [Server Configuration](./server.md) | ||
| 183 | - [Routes Configuration](./routes.md) | 178 | - [Routes Configuration](./routes.md) | ||
| 184 | - [External Routes](./external-routes.md) | 179 | - [External Routes](./external-routes.md) | ||
| 185 | - | - [Registry Authentication](./registry-auth.md) | 180 | + | - [Authentication](./auth.md) |
M docs/config/secrets.md +29 -33
| @@ -4,16 +4,18 @@ Configure how Gordon stores and retrieves sensitive data. | |||
| 4 | 4 | ||
| 5 | 5 | ## Configuration | |
| 6 | 6 | ||
| 7 | + | The secrets backend is configured within the `[auth]` section: | |
| 8 | + | ||
| 7 | 9 | ```toml | |
| 8 | - | [secrets] | |
| 9 | - | backend = "pass" # "pass", "sops", or "unsafe" | |
| 10 | + | [auth] | |
| 11 | + | secrets_backend = "pass" # "pass", "sops", or "unsafe" | |
| 10 | 12 | ``` | |
| 11 | 13 | ||
| 12 | 14 | ## Options | |
| 13 | 15 | ||
| 14 | 16 | | Option | Type | Default | Description | | |
| 15 | 17 | |--------|------|---------|-------------| | |
| 16 | - | | `backend` | string | `"unsafe"` | Secrets storage backend | | |
| 18 | + | | `secrets_backend` | string | `"unsafe"` | Secrets storage backend | | |
| 17 | 19 | ||
| 18 | 20 | ## Backends | |
| 19 | 21 | ||
| @@ -22,8 +24,8 @@ backend = "pass" # "pass", "sops", or "unsafe" | |||
| 22 | 24 | Uses the Unix password manager (`pass`) for secure secret storage: | |
| 23 | 25 | ||
| 24 | 26 | ```toml | |
| 25 | - | [secrets] | |
| 26 | - | backend = "pass" | |
| 27 | + | [auth] | |
| 28 | + | secrets_backend = "pass" | |
| 27 | 29 | ``` | |
| 28 | 30 | ||
| 29 | 31 | **Setup:** | |
| @@ -35,15 +37,15 @@ sudo apt install pass | |||
| 35 | 37 | pass init your-gpg-key-id | |
| 36 | 38 | ||
| 37 | 39 | # Store secrets | |
| 38 | - | pass insert gordon/registry/password_hash | |
| 39 | - | pass insert gordon/registry/token_secret | |
| 40 | + | pass insert gordon/auth/password_hash | |
| 41 | + | pass insert gordon/auth/token_secret | |
| 40 | 42 | ``` | |
| 41 | 43 | ||
| 42 | 44 | **Usage in config:** | |
| 43 | 45 | ```toml | |
| 44 | - | [registry_auth] | |
| 45 | - | password_hash = "gordon/registry/password_hash" # Path in pass store | |
| 46 | - | token_secret = "gordon/registry/token_secret" | |
| 46 | + | [auth] | |
| 47 | + | password_hash = "gordon/auth/password_hash" # Path in pass store | |
| 48 | + | token_secret = "gordon/auth/token_secret" | |
| 47 | 49 | ``` | |
| 48 | 50 | ||
| 49 | 51 | **Benefits:** | |
| @@ -57,8 +59,8 @@ token_secret = "gordon/registry/token_secret" | |||
| 57 | 59 | Uses Mozilla SOPS for encrypted file-based secrets: | |
| 58 | 60 | ||
| 59 | 61 | ```toml | |
| 60 | - | [secrets] | |
| 61 | - | backend = "sops" | |
| 62 | + | [auth] | |
| 63 | + | secrets_backend = "sops" | |
| 62 | 64 | ``` | |
| 63 | 65 | ||
| 64 | 66 | **Setup:** | |
| @@ -88,15 +90,15 @@ DB_PASSWORD=${sops:secrets.yaml:database.password} | |||
| 88 | 90 | Stores secrets as plain text files: | |
| 89 | 91 | ||
| 90 | 92 | ```toml | |
| 91 | - | [secrets] | |
| 92 | - | backend = "unsafe" | |
| 93 | + | [auth] | |
| 94 | + | secrets_backend = "unsafe" | |
| 93 | 95 | ``` | |
| 94 | 96 | ||
| 95 | 97 | **Storage location:** | |
| 96 | 98 | ``` | |
| 97 | 99 | {data_dir}/secrets/ | |
| 98 | 100 | ├── gordon/ | |
| 99 | - | │ └── registry/ | |
| 101 | + | │ └── auth/ | |
| 100 | 102 | │ ├── password_hash | |
| 101 | 103 | │ └── token_secret | |
| 102 | 104 | ``` | |
| @@ -104,8 +106,8 @@ backend = "unsafe" | |||
| 104 | 106 | **Usage:** | |
| 105 | 107 | ```bash | |
| 106 | 108 | # Create secret | |
| 107 | - | mkdir -p ~/.gordon/secrets/gordon/registry | |
| 108 | - | echo "your-bcrypt-hash" > ~/.gordon/secrets/gordon/registry/password_hash | |
| 109 | + | mkdir -p ~/.gordon/secrets/gordon/auth | |
| 110 | + | echo "your-bcrypt-hash" > ~/.gordon/secrets/gordon/auth/password_hash | |
| 109 | 111 | ``` | |
| 110 | 112 | ||
| 111 | 113 | > **Warning:** Only use for local development. Secrets are stored in plain text. | |
| @@ -135,18 +137,16 @@ API_SECRET=${sops:production.yaml:api.secret.key} | |||
| 135 | 137 | ### Production with Pass | |
| 136 | 138 | ||
| 137 | 139 | ```toml | |
| 138 | - | [secrets] | |
| 139 | - | backend = "pass" | |
| 140 | - | ||
| 141 | - | [registry_auth] | |
| 140 | + | [auth] | |
| 142 | 141 | enabled = true | |
| 143 | 142 | type = "token" | |
| 144 | - | token_secret = "gordon/registry/token_secret" | |
| 143 | + | secrets_backend = "pass" | |
| 144 | + | token_secret = "gordon/auth/token_secret" | |
| 145 | 145 | ``` | |
| 146 | 146 | ||
| 147 | 147 | ```bash | |
| 148 | 148 | # Setup | |
| 149 | - | pass insert gordon/registry/token_secret | |
| 149 | + | pass insert gordon/auth/token_secret | |
| 150 | 150 | # Enter a random 32+ character string | |
| 151 | 151 | ||
| 152 | 152 | # Generate tokens | |
| @@ -156,23 +156,19 @@ gordon auth token generate --subject deploy --expiry 0 | |||
| 156 | 156 | ### Development with Unsafe | |
| 157 | 157 | ||
| 158 | 158 | ```toml | |
| 159 | - | [secrets] | |
| 160 | - | backend = "unsafe" | |
| 161 | - | ||
| 162 | - | [registry_auth] | |
| 159 | + | [auth] | |
| 163 | 160 | enabled = false | |
| 161 | + | secrets_backend = "unsafe" | |
| 164 | 162 | ``` | |
| 165 | 163 | ||
| 166 | 164 | ### Enterprise with SOPS | |
| 167 | 165 | ||
| 168 | 166 | ```toml | |
| 169 | - | [secrets] | |
| 170 | - | backend = "sops" | |
| 171 | - | ||
| 172 | - | [registry_auth] | |
| 167 | + | [auth] | |
| 173 | 168 | enabled = true | |
| 174 | 169 | type = "token" | |
| 175 | - | token_secret = "gordon/registry/token_secret" | |
| 170 | + | secrets_backend = "sops" | |
| 171 | + | token_secret = "gordon/auth/token_secret" | |
| 176 | 172 | ``` | |
| 177 | 173 | ||
| 178 | 174 | Environment file: | |
| @@ -194,6 +190,6 @@ JWT_SECRET=${sops:secrets.yaml:jwt.secret} | |||
| 194 | 190 | ||
| 195 | 191 | ## Related | |
| 196 | 192 | ||
| 197 | - | - [Registry Authentication](./registry-auth.md) | |
| 193 | + | - [Authentication](./auth.md) | |
| 198 | 194 | - [Environment Variables](./env.md) | |
| 199 | 195 | - [Configuration Overview](./index.md) | |
| @@ -4,16 +4,18 @@ Configure how Gordon stores and retrieves sensitive data. | |||||
| 4 | 4 | ||||
| 5 | ## Configuration | 5 | ## Configuration | ||
| 6 | 6 | ||||
| 7 | + | The secrets backend is configured within the `[auth]` section: | |||
| 8 | + | ||||
| 7 | ```toml | 9 | ```toml | ||
| 8 | - | [secrets] | 10 | + | [auth] |
| 9 | - | backend = "pass" # "pass", "sops", or "unsafe" | 11 | + | secrets_backend = "pass" # "pass", "sops", or "unsafe" |
| 10 | ``` | 12 | ``` | ||
| 11 | 13 | ||||
| 12 | ## Options | 14 | ## Options | ||
| 13 | 15 | ||||
| 14 | | Option | Type | Default | Description | | 16 | | Option | Type | Default | Description | | ||
| 15 | |--------|------|---------|-------------| | 17 | |--------|------|---------|-------------| | ||
| 16 | - | | `backend` | string | `"unsafe"` | Secrets storage backend | | 18 | + | | `secrets_backend` | string | `"unsafe"` | Secrets storage backend | |
| 17 | 19 | ||||
| 18 | ## Backends | 20 | ## Backends | ||
| 19 | 21 | ||||
| @@ -22,8 +24,8 @@ backend = "pass" # "pass", "sops", or "unsafe" | |||||
| 22 | Uses the Unix password manager (`pass`) for secure secret storage: | 24 | Uses the Unix password manager (`pass`) for secure secret storage: | ||
| 23 | 25 | ||||
| 24 | ```toml | 26 | ```toml | ||
| 25 | - | [secrets] | 27 | + | [auth] |
| 26 | - | backend = "pass" | 28 | + | secrets_backend = "pass" |
| 27 | ``` | 29 | ``` | ||
| 28 | 30 | ||||
| 29 | **Setup:** | 31 | **Setup:** | ||
| @@ -35,15 +37,15 @@ sudo apt install pass | |||||
| 35 | pass init your-gpg-key-id | 37 | pass init your-gpg-key-id | ||
| 36 | 38 | ||||
| 37 | # Store secrets | 39 | # Store secrets | ||
| 38 | - | pass insert gordon/registry/password_hash | 40 | + | pass insert gordon/auth/password_hash |
| 39 | - | pass insert gordon/registry/token_secret | 41 | + | pass insert gordon/auth/token_secret |
| 40 | ``` | 42 | ``` | ||
| 41 | 43 | ||||
| 42 | **Usage in config:** | 44 | **Usage in config:** | ||
| 43 | ```toml | 45 | ```toml | ||
| 44 | - | [registry_auth] | 46 | + | [auth] |
| 45 | - | password_hash = "gordon/registry/password_hash" # Path in pass store | 47 | + | password_hash = "gordon/auth/password_hash" # Path in pass store |
| 46 | - | token_secret = "gordon/registry/token_secret" | 48 | + | token_secret = "gordon/auth/token_secret" |
| 47 | ``` | 49 | ``` | ||
| 48 | 50 | ||||
| 49 | **Benefits:** | 51 | **Benefits:** | ||
| @@ -57,8 +59,8 @@ token_secret = "gordon/registry/token_secret" | |||||
| 57 | Uses Mozilla SOPS for encrypted file-based secrets: | 59 | Uses Mozilla SOPS for encrypted file-based secrets: | ||
| 58 | 60 | ||||
| 59 | ```toml | 61 | ```toml | ||
| 60 | - | [secrets] | 62 | + | [auth] |
| 61 | - | backend = "sops" | 63 | + | secrets_backend = "sops" |
| 62 | ``` | 64 | ``` | ||
| 63 | 65 | ||||
| 64 | **Setup:** | 66 | **Setup:** | ||
| @@ -88,15 +90,15 @@ DB_PASSWORD=${sops:secrets.yaml:database.password} | |||||
| 88 | Stores secrets as plain text files: | 90 | Stores secrets as plain text files: | ||
| 89 | 91 | ||||
| 90 | ```toml | 92 | ```toml | ||
| 91 | - | [secrets] | 93 | + | [auth] |
| 92 | - | backend = "unsafe" | 94 | + | secrets_backend = "unsafe" |
| 93 | ``` | 95 | ``` | ||
| 94 | 96 | ||||
| 95 | **Storage location:** | 97 | **Storage location:** | ||
| 96 | ``` | 98 | ``` | ||
| 97 | {data_dir}/secrets/ | 99 | {data_dir}/secrets/ | ||
| 98 | ├── gordon/ | 100 | ├── gordon/ | ||
| 99 | - | │ └── registry/ | 101 | + | │ └── auth/ |
| 100 | │ ├── password_hash | 102 | │ ├── password_hash | ||
| 101 | │ └── token_secret | 103 | │ └── token_secret | ||
| 102 | ``` | 104 | ``` | ||
| @@ -104,8 +106,8 @@ backend = "unsafe" | |||||
| 104 | **Usage:** | 106 | **Usage:** | ||
| 105 | ```bash | 107 | ```bash | ||
| 106 | # Create secret | 108 | # Create secret | ||
| 107 | - | mkdir -p ~/.gordon/secrets/gordon/registry | 109 | + | mkdir -p ~/.gordon/secrets/gordon/auth |
| 108 | - | echo "your-bcrypt-hash" > ~/.gordon/secrets/gordon/registry/password_hash | 110 | + | echo "your-bcrypt-hash" > ~/.gordon/secrets/gordon/auth/password_hash |
| 109 | ``` | 111 | ``` | ||
| 110 | 112 | ||||
| 111 | > **Warning:** Only use for local development. Secrets are stored in plain text. | 113 | > **Warning:** Only use for local development. Secrets are stored in plain text. | ||
| @@ -135,18 +137,16 @@ API_SECRET=${sops:production.yaml:api.secret.key} | |||||
| 135 | ### Production with Pass | 137 | ### Production with Pass | ||
| 136 | 138 | ||||
| 137 | ```toml | 139 | ```toml | ||
| 138 | - | [secrets] | 140 | + | [auth] |
| 139 | - | backend = "pass" | |||
| 140 | - | ||||
| 141 | - | [registry_auth] | |||
| 142 | enabled = true | 141 | enabled = true | ||
| 143 | type = "token" | 142 | type = "token" | ||
| 144 | - | token_secret = "gordon/registry/token_secret" | 143 | + | secrets_backend = "pass" |
| 144 | + | token_secret = "gordon/auth/token_secret" | |||
| 145 | ``` | 145 | ``` | ||
| 146 | 146 | ||||
| 147 | ```bash | 147 | ```bash | ||
| 148 | # Setup | 148 | # Setup | ||
| 149 | - | pass insert gordon/registry/token_secret | 149 | + | pass insert gordon/auth/token_secret |
| 150 | # Enter a random 32+ character string | 150 | # Enter a random 32+ character string | ||
| 151 | 151 | ||||
| 152 | # Generate tokens | 152 | # Generate tokens | ||
| @@ -156,23 +156,19 @@ gordon auth token generate --subject deploy --expiry 0 | |||||
| 156 | ### Development with Unsafe | 156 | ### Development with Unsafe | ||
| 157 | 157 | ||||
| 158 | ```toml | 158 | ```toml | ||
| 159 | - | [secrets] | 159 | + | [auth] |
| 160 | - | backend = "unsafe" | |||
| 161 | - | ||||
| 162 | - | [registry_auth] | |||
| 163 | enabled = false | 160 | enabled = false | ||
| 161 | + | secrets_backend = "unsafe" | |||
| 164 | ``` | 162 | ``` | ||
| 165 | 163 | ||||
| 166 | ### Enterprise with SOPS | 164 | ### Enterprise with SOPS | ||
| 167 | 165 | ||||
| 168 | ```toml | 166 | ```toml | ||
| 169 | - | [secrets] | 167 | + | [auth] |
| 170 | - | backend = "sops" | |||
| 171 | - | ||||
| 172 | - | [registry_auth] | |||
| 173 | enabled = true | 168 | enabled = true | ||
| 174 | type = "token" | 169 | type = "token" | ||
| 175 | - | token_secret = "gordon/registry/token_secret" | 170 | + | secrets_backend = "sops" |
| 171 | + | token_secret = "gordon/auth/token_secret" | |||
| 176 | ``` | 172 | ``` | ||
| 177 | 173 | ||||
| 178 | Environment file: | 174 | Environment file: | ||
| @@ -194,6 +190,6 @@ JWT_SECRET=${sops:secrets.yaml:jwt.secret} | |||||
| 194 | 190 | ||||
| 195 | ## Related | 191 | ## Related | ||
| 196 | 192 | ||||
| 197 | - | - [Registry Authentication](./registry-auth.md) | 193 | + | - [Authentication](./auth.md) |
| 198 | - [Environment Variables](./env.md) | 194 | - [Environment Variables](./env.md) | ||
| 199 | - [Configuration Overview](./index.md) | 195 | - [Configuration Overview](./index.md) | ||
A internal/adapters/dto/admin_attachments.go +23 -0
| @@ -0,0 +1,23 @@ | |||
| 1 | + | // Package dto provides shared data transfer objects for API responses. | |
| 2 | + | package dto | |
| 3 | + | ||
| 4 | + | // AttachmentsConfigResponse represents all configured attachments. | |
| 5 | + | type AttachmentsConfigResponse struct { | |
| 6 | + | Attachments map[string][]string `json:"attachments"` | |
| 7 | + | } | |
| 8 | + | ||
| 9 | + | // AttachmentConfigResponse represents attachments for a specific target. | |
| 10 | + | type AttachmentConfigResponse struct { | |
| 11 | + | Target string `json:"target"` | |
| 12 | + | Images []string `json:"images"` | |
| 13 | + | } | |
| 14 | + | ||
| 15 | + | // AttachmentAddRequest represents a request to add an attachment. | |
| 16 | + | type AttachmentAddRequest struct { | |
| 17 | + | Image string `json:"image"` | |
| 18 | + | } | |
| 19 | + | ||
| 20 | + | // AttachmentStatusResponse represents attachment operation result. | |
| 21 | + | type AttachmentStatusResponse struct { | |
| 22 | + | Status string `json:"status"` | |
| 23 | + | } | |
| @@ -0,0 +1,23 @@ | |||||
| 1 | + | // Package dto provides shared data transfer objects for API responses. | |||
| 2 | + | package dto | |||
| 3 | + | ||||
| 4 | + | // AttachmentsConfigResponse represents all configured attachments. | |||
| 5 | + | type AttachmentsConfigResponse struct { | |||
| 6 | + | Attachments map[string][]string `json:"attachments"` | |||
| 7 | + | } | |||
| 8 | + | ||||
| 9 | + | // AttachmentConfigResponse represents attachments for a specific target. | |||
| 10 | + | type AttachmentConfigResponse struct { | |||
| 11 | + | Target string `json:"target"` | |||
| 12 | + | Images []string `json:"images"` | |||
| 13 | + | } | |||
| 14 | + | ||||
| 15 | + | // AttachmentAddRequest represents a request to add an attachment. | |||
| 16 | + | type AttachmentAddRequest struct { | |||
| 17 | + | Image string `json:"image"` | |||
| 18 | + | } | |||
| 19 | + | ||||
| 20 | + | // AttachmentStatusResponse represents attachment operation result. | |||
| 21 | + | type AttachmentStatusResponse struct { | |||
| 22 | + | Status string `json:"status"` | |||
| 23 | + | } | |||
A internal/adapters/in/cli/attachments.go +272 -0
| @@ -0,0 +1,272 @@ | |||
| 1 | + | package cli | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "fmt" | |
| 6 | + | "sort" | |
| 7 | + | ||
| 8 | + | "gordon/internal/adapters/in/cli/remote" | |
| 9 | + | "gordon/internal/adapters/in/cli/ui/components" | |
| 10 | + | "gordon/internal/adapters/in/cli/ui/styles" | |
| 11 | + | ||
| 12 | + | "github.com/spf13/cobra" | |
| 13 | + | ) | |
| 14 | + | ||
| 15 | + | // newAttachmentsCmd creates the attachments command group. | |
| 16 | + | func newAttachmentsCmd() *cobra.Command { | |
| 17 | + | cmd := &cobra.Command{ | |
| 18 | + | Use: "attachments", | |
| 19 | + | Aliases: []string{"attach"}, | |
| 20 | + | Short: "Manage container attachments", | |
| 21 | + | Long: `Manage container attachments (databases, caches, queues) in the configuration. | |
| 22 | + | ||
| 23 | + | Attachments are service dependencies that run alongside your application containers. | |
| 24 | + | They are defined per-domain or per-network-group in the configuration. | |
| 25 | + | ||
| 26 | + | When targeting a remote Gordon instance (via --remote flag or GORDON_REMOTE env var), | |
| 27 | + | these commands operate on the remote server. Otherwise, they require access to | |
| 28 | + | the local Gordon configuration.`, | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | cmd.AddCommand(newAttachmentsListCmd()) | |
| 32 | + | cmd.AddCommand(newAttachmentsAddCmd()) | |
| 33 | + | cmd.AddCommand(newAttachmentsRemoveCmd()) | |
| 34 | + | ||
| 35 | + | return cmd | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | // newAttachmentsListCmd creates the attachments list command. | |
| 39 | + | func newAttachmentsListCmd() *cobra.Command { | |
| 40 | + | return &cobra.Command{ | |
| 41 | + | Use: "list [domain-or-group]", | |
| 42 | + | Short: "List configured attachments", | |
| 43 | + | Long: `List all configured attachments, or attachments for a specific domain/group. | |
| 44 | + | ||
| 45 | + | Examples: | |
| 46 | + | gordon attachments list # List all attachments | |
| 47 | + | gordon attachments list app.example.com # List attachments for domain | |
| 48 | + | gordon attachments list backend # List attachments for network group`, | |
| 49 | + | Args: cobra.MaximumNArgs(1), | |
| 50 | + | RunE: func(cmd *cobra.Command, args []string) error { | |
| 51 | + | ctx := context.Background() | |
| 52 | + | target := "" | |
| 53 | + | if len(args) > 0 { | |
| 54 | + | target = args[0] | |
| 55 | + | } | |
| 56 | + | ||
| 57 | + | client, isRemote := GetRemoteClient() | |
| 58 | + | if isRemote { | |
| 59 | + | return runAttachmentsListRemote(ctx, client, target) | |
| 60 | + | } | |
| 61 | + | return runAttachmentsListLocal(ctx, configPath, target) | |
| 62 | + | }, | |
| 63 | + | } | |
| 64 | + | } | |
| 65 | + | ||
| 66 | + | // runAttachmentsListRemote lists attachments from a remote Gordon instance. | |
| 67 | + | func runAttachmentsListRemote(ctx context.Context, client *remote.Client, target string) error { | |
| 68 | + | if target != "" { | |
| 69 | + | // List for specific target | |
| 70 | + | images, err := client.GetAttachmentsConfig(ctx, target) | |
| 71 | + | if err != nil { | |
| 72 | + | return fmt.Errorf("failed to get attachments: %w", err) | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | if len(images) == 0 { | |
| 76 | + | fmt.Printf("No attachments configured for '%s'\n", target) | |
| 77 | + | return nil | |
| 78 | + | } | |
| 79 | + | ||
| 80 | + | fmt.Println(styles.Theme.Title.Render(fmt.Sprintf("Attachments for %s", target))) | |
| 81 | + | fmt.Println() | |
| 82 | + | for _, img := range images { | |
| 83 | + | fmt.Printf(" %s\n", img) | |
| 84 | + | } | |
| 85 | + | return nil | |
| 86 | + | } | |
| 87 | + | ||
| 88 | + | // List all attachments | |
| 89 | + | attachments, err := client.GetAllAttachmentsConfig(ctx) | |
| 90 | + | if err != nil { | |
| 91 | + | return fmt.Errorf("failed to list attachments: %w", err) | |
| 92 | + | } | |
| 93 | + | ||
| 94 | + | if len(attachments) == 0 { | |
| 95 | + | fmt.Println(styles.Theme.Muted.Render("No attachments configured")) | |
| 96 | + | return nil | |
| 97 | + | } | |
| 98 | + | ||
| 99 | + | // Sort targets for consistent output | |
| 100 | + | targets := make([]string, 0, len(attachments)) | |
| 101 | + | for t := range attachments { | |
| 102 | + | targets = append(targets, t) | |
| 103 | + | } | |
| 104 | + | sort.Strings(targets) | |
| 105 | + | ||
| 106 | + | fmt.Println(styles.Theme.Title.Render("Attachments")) | |
| 107 | + | fmt.Println() | |
| 108 | + | ||
| 109 | + | for _, t := range targets { | |
| 110 | + | images := attachments[t] | |
| 111 | + | fmt.Printf("%s\n", styles.Theme.Bold.Render(t)) | |
| 112 | + | for _, img := range images { | |
| 113 | + | fmt.Printf(" %s\n", img) | |
| 114 | + | } | |
| 115 | + | } | |
| 116 | + | ||
| 117 | + | return nil | |
| 118 | + | } | |
| 119 | + | ||
| 120 | + | // runAttachmentsListLocal lists attachments from local configuration. | |
| 121 | + | func runAttachmentsListLocal(ctx context.Context, cfgPath string, target string) error { | |
| 122 | + | local, err := GetLocalServices(cfgPath) | |
| 123 | + | if err != nil { | |
| 124 | + | return fmt.Errorf("failed to initialize local services: %w", err) | |
| 125 | + | } | |
| 126 | + | ||
| 127 | + | if target != "" { | |
| 128 | + | // List for specific target | |
| 129 | + | images, err := local.GetConfigService().GetAttachmentsFor(ctx, target) | |
| 130 | + | if err != nil { | |
| 131 | + | return fmt.Errorf("failed to get attachments: %w", err) | |
| 132 | + | } | |
| 133 | + | ||
| 134 | + | if len(images) == 0 { | |
| 135 | + | fmt.Printf("No attachments configured for '%s'\n", target) | |
| 136 | + | return nil | |
| 137 | + | } | |
| 138 | + | ||
| 139 | + | fmt.Println(styles.Theme.Title.Render(fmt.Sprintf("Attachments for %s (local)", target))) | |
| 140 | + | fmt.Println() | |
| 141 | + | for _, img := range images { | |
| 142 | + | fmt.Printf(" %s\n", img) | |
| 143 | + | } | |
| 144 | + | return nil | |
| 145 | + | } | |
| 146 | + | ||
| 147 | + | // List all attachments | |
| 148 | + | attachments := local.GetConfigService().GetAllAttachments(ctx) | |
| 149 | + | ||
| 150 | + | if len(attachments) == 0 { | |
| 151 | + | fmt.Println(styles.Theme.Muted.Render("No attachments configured")) | |
| 152 | + | return nil | |
| 153 | + | } | |
| 154 | + | ||
| 155 | + | // Sort targets for consistent output | |
| 156 | + | targets := make([]string, 0, len(attachments)) | |
| 157 | + | for t := range attachments { | |
| 158 | + | targets = append(targets, t) | |
| 159 | + | } | |
| 160 | + | sort.Strings(targets) | |
| 161 | + | ||
| 162 | + | fmt.Println(styles.Theme.Title.Render("Attachments (local)")) | |
| 163 | + | fmt.Println() | |
| 164 | + | ||
| 165 | + | for _, t := range targets { | |
| 166 | + | images := attachments[t] | |
| 167 | + | fmt.Printf("%s\n", styles.Theme.Bold.Render(t)) | |
| 168 | + | for _, img := range images { | |
| 169 | + | fmt.Printf(" %s\n", img) | |
| 170 | + | } | |
| 171 | + | } | |
| 172 | + | ||
| 173 | + | return nil | |
| 174 | + | } | |
| 175 | + | ||
| 176 | + | // newAttachmentsAddCmd creates the attachments add command. | |
| 177 | + | func newAttachmentsAddCmd() *cobra.Command { | |
| 178 | + | cmd := &cobra.Command{ | |
| 179 | + | Use: "add <domain-or-group> <image>", | |
| 180 | + | Short: "Add an attachment", | |
| 181 | + | Long: `Add a container attachment to a domain or network group. | |
| 182 | + | ||
| 183 | + | Examples: | |
| 184 | + | gordon attachments add app.example.com postgres:15 | |
| 185 | + | gordon attachments add backend redis:7-alpine | |
| 186 | + | gordon --remote https://gordon.mydomain.com attachments add api.mydomain.com memcached:latest`, | |
| 187 | + | Args: cobra.ExactArgs(2), | |
| 188 | + | RunE: func(cmd *cobra.Command, args []string) error { | |
| 189 | + | ctx := context.Background() | |
| 190 | + | target := args[0] | |
| 191 | + | image := args[1] | |
| 192 | + | ||
| 193 | + | client, isRemote := GetRemoteClient() | |
| 194 | + | if isRemote { | |
| 195 | + | if err := client.AddAttachment(ctx, target, image); err != nil { | |
| 196 | + | return fmt.Errorf("failed to add attachment: %w", err) | |
| 197 | + | } | |
| 198 | + | } else { | |
| 199 | + | local, err := GetLocalServices(configPath) | |
| 200 | + | if err != nil { | |
| 201 | + | return fmt.Errorf("failed to initialize local services: %w", err) | |
| 202 | + | } | |
| 203 | + | if err := local.GetConfigService().AddAttachment(ctx, target, image); err != nil { | |
| 204 | + | return fmt.Errorf("failed to add attachment: %w", err) | |
| 205 | + | } | |
| 206 | + | } | |
| 207 | + | ||
| 208 | + | fmt.Println(styles.RenderSuccess(fmt.Sprintf("Attachment added: %s -> %s", target, image))) | |
| 209 | + | return nil | |
| 210 | + | }, | |
| 211 | + | } | |
| 212 | + | ||
| 213 | + | return cmd | |
| 214 | + | } | |
| 215 | + | ||
| 216 | + | // newAttachmentsRemoveCmd creates the attachments remove command. | |
| 217 | + | func newAttachmentsRemoveCmd() *cobra.Command { | |
| 218 | + | var force bool | |
| 219 | + | ||
| 220 | + | cmd := &cobra.Command{ | |
| 221 | + | Use: "remove <domain-or-group> <image>", | |
| 222 | + | Short: "Remove an attachment", | |
| 223 | + | Long: `Remove a container attachment from a domain or network group. | |
| 224 | + | ||
| 225 | + | Examples: | |
| 226 | + | gordon attachments remove app.example.com postgres:15 | |
| 227 | + | gordon attachments remove backend redis:7-alpine --force`, | |
| 228 | + | Args: cobra.ExactArgs(2), | |
| 229 | + | RunE: func(cmd *cobra.Command, args []string) error { | |
| 230 | + | ctx := context.Background() | |
| 231 | + | target := args[0] | |
| 232 | + | image := args[1] | |
| 233 | + | ||
| 234 | + | // Confirm unless --force | |
| 235 | + | if !force { | |
| 236 | + | confirmed, err := components.RunConfirm( | |
| 237 | + | fmt.Sprintf("Remove attachment '%s' from '%s'?", image, target), | |
| 238 | + | components.WithDescription("This will remove the attachment from the configuration. The container will be stopped on next reload."), | |
| 239 | + | ) | |
| 240 | + | if err != nil { | |
| 241 | + | return err | |
| 242 | + | } | |
| 243 | + | if !confirmed { | |
| 244 | + | fmt.Println(styles.Theme.Muted.Render("Cancelled")) | |
| 245 | + | return nil | |
| 246 | + | } | |
| 247 | + | } | |
| 248 | + | ||
| 249 | + | client, isRemote := GetRemoteClient() | |
| 250 | + | if isRemote { | |
| 251 | + | if err := client.RemoveAttachment(ctx, target, image); err != nil { | |
| 252 | + | return fmt.Errorf("failed to remove attachment: %w", err) | |
| 253 | + | } | |
| 254 | + | } else { | |
| 255 | + | local, err := GetLocalServices(configPath) | |
| 256 | + | if err != nil { | |
| 257 | + | return fmt.Errorf("failed to initialize local services: %w", err) | |
| 258 | + | } | |
| 259 | + | if err := local.GetConfigService().RemoveAttachment(ctx, target, image); err != nil { | |
| 260 | + | return fmt.Errorf("failed to remove attachment: %w", err) | |
| 261 | + | } | |
| 262 | + | } | |
| 263 | + | ||
| 264 | + | fmt.Println(styles.RenderSuccess(fmt.Sprintf("Attachment removed: %s -> %s", target, image))) | |
| 265 | + | return nil | |
| 266 | + | }, | |
| 267 | + | } | |
| 268 | + | ||
| 269 | + | cmd.Flags().BoolVarP(&force, "force", "f", false, "Skip confirmation") | |
| 270 | + | ||
| 271 | + | return cmd | |
| 272 | + | } | |
| @@ -0,0 +1,272 @@ | |||||
| 1 | + | package cli | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "fmt" | |||
| 6 | + | "sort" | |||
| 7 | + | ||||
| 8 | + | "gordon/internal/adapters/in/cli/remote" | |||
| 9 | + | "gordon/internal/adapters/in/cli/ui/components" | |||
| 10 | + | "gordon/internal/adapters/in/cli/ui/styles" | |||
| 11 | + | ||||
| 12 | + | "github.com/spf13/cobra" | |||
| 13 | + | ) | |||
| 14 | + | ||||
| 15 | + | // newAttachmentsCmd creates the attachments command group. | |||
| 16 | + | func newAttachmentsCmd() *cobra.Command { | |||
| 17 | + | cmd := &cobra.Command{ | |||
| 18 | + | Use: "attachments", | |||
| 19 | + | Aliases: []string{"attach"}, | |||
| 20 | + | Short: "Manage container attachments", | |||
| 21 | + | Long: `Manage container attachments (databases, caches, queues) in the configuration. | |||
| 22 | + | ||||
| 23 | + | Attachments are service dependencies that run alongside your application containers. | |||
| 24 | + | They are defined per-domain or per-network-group in the configuration. | |||
| 25 | + | ||||
| 26 | + | When targeting a remote Gordon instance (via --remote flag or GORDON_REMOTE env var), | |||
| 27 | + | these commands operate on the remote server. Otherwise, they require access to | |||
| 28 | + | the local Gordon configuration.`, | |||
| 29 | + | } | |||
| 30 | + | ||||
| 31 | + | cmd.AddCommand(newAttachmentsListCmd()) | |||
| 32 | + | cmd.AddCommand(newAttachmentsAddCmd()) | |||
| 33 | + | cmd.AddCommand(newAttachmentsRemoveCmd()) | |||
| 34 | + | ||||
| 35 | + | return cmd | |||
| 36 | + | } | |||
| 37 | + | ||||
| 38 | + | // newAttachmentsListCmd creates the attachments list command. | |||
| 39 | + | func newAttachmentsListCmd() *cobra.Command { | |||
| 40 | + | return &cobra.Command{ | |||
| 41 | + | Use: "list [domain-or-group]", | |||
| 42 | + | Short: "List configured attachments", | |||
| 43 | + | Long: `List all configured attachments, or attachments for a specific domain/group. | |||
| 44 | + | ||||
| 45 | + | Examples: | |||
| 46 | + | gordon attachments list # List all attachments | |||
| 47 | + | gordon attachments list app.example.com # List attachments for domain | |||
| 48 | + | gordon attachments list backend # List attachments for network group`, | |||
| 49 | + | Args: cobra.MaximumNArgs(1), | |||
| 50 | + | RunE: func(cmd *cobra.Command, args []string) error { | |||
| 51 | + | ctx := context.Background() | |||
| 52 | + | target := "" | |||
| 53 | + | if len(args) > 0 { | |||
| 54 | + | target = args[0] | |||
| 55 | + | } | |||
| 56 | + | ||||
| 57 | + | client, isRemote := GetRemoteClient() | |||
| 58 | + | if isRemote { | |||
| 59 | + | return runAttachmentsListRemote(ctx, client, target) | |||
| 60 | + | } | |||
| 61 | + | return runAttachmentsListLocal(ctx, configPath, target) | |||
| 62 | + | }, | |||
| 63 | + | } | |||
| 64 | + | } | |||
| 65 | + | ||||
| 66 | + | // runAttachmentsListRemote lists attachments from a remote Gordon instance. | |||
| 67 | + | func runAttachmentsListRemote(ctx context.Context, client *remote.Client, target string) error { | |||
| 68 | + | if target != "" { | |||
| 69 | + | // List for specific target | |||
| 70 | + | images, err := client.GetAttachmentsConfig(ctx, target) | |||
| 71 | + | if err != nil { | |||
| 72 | + | return fmt.Errorf("failed to get attachments: %w", err) | |||
| 73 | + | } | |||
| 74 | + | ||||
| 75 | + | if len(images) == 0 { | |||
| 76 | + | fmt.Printf("No attachments configured for '%s'\n", target) | |||
| 77 | + | return nil | |||
| 78 | + | } | |||
| 79 | + | ||||
| 80 | + | fmt.Println(styles.Theme.Title.Render(fmt.Sprintf("Attachments for %s", target))) | |||
| 81 | + | fmt.Println() | |||
| 82 | + | for _, img := range images { | |||
| 83 | + | fmt.Printf(" %s\n", img) | |||
| 84 | + | } | |||
| 85 | + | return nil | |||
| 86 | + | } | |||
| 87 | + | ||||
| 88 | + | // List all attachments | |||
| 89 | + | attachments, err := client.GetAllAttachmentsConfig(ctx) | |||
| 90 | + | if err != nil { | |||
| 91 | + | return fmt.Errorf("failed to list attachments: %w", err) | |||
| 92 | + | } | |||
| 93 | + | ||||
| 94 | + | if len(attachments) == 0 { | |||
| 95 | + | fmt.Println(styles.Theme.Muted.Render("No attachments configured")) | |||
| 96 | + | return nil | |||
| 97 | + | } | |||
| 98 | + | ||||
| 99 | + | // Sort targets for consistent output | |||
| 100 | + | targets := make([]string, 0, len(attachments)) | |||
| 101 | + | for t := range attachments { | |||
| 102 | + | targets = append(targets, t) | |||
| 103 | + | } | |||
| 104 | + | sort.Strings(targets) | |||
| 105 | + | ||||
| 106 | + | fmt.Println(styles.Theme.Title.Render("Attachments")) | |||
| 107 | + | fmt.Println() | |||
| 108 | + | ||||
| 109 | + | for _, t := range targets { | |||
| 110 | + | images := attachments[t] | |||
| 111 | + | fmt.Printf("%s\n", styles.Theme.Bold.Render(t)) | |||
| 112 | + | for _, img := range images { | |||
| 113 | + | fmt.Printf(" %s\n", img) | |||
| 114 | + | } | |||
| 115 | + | } | |||
| 116 | + | ||||
| 117 | + | return nil | |||
| 118 | + | } | |||
| 119 | + | ||||
| 120 | + | // runAttachmentsListLocal lists attachments from local configuration. | |||
| 121 | + | func runAttachmentsListLocal(ctx context.Context, cfgPath string, target string) error { | |||
| 122 | + | local, err := GetLocalServices(cfgPath) | |||
| 123 | + | if err != nil { | |||
| 124 | + | return fmt.Errorf("failed to initialize local services: %w", err) | |||
| 125 | + | } | |||
| 126 | + | ||||
| 127 | + | if target != "" { | |||
| 128 | + | // List for specific target | |||
| 129 | + | images, err := local.GetConfigService().GetAttachmentsFor(ctx, target) | |||
| 130 | + | if err != nil { | |||
| 131 | + | return fmt.Errorf("failed to get attachments: %w", err) | |||
| 132 | + | } | |||
| 133 | + | ||||
| 134 | + | if len(images) == 0 { | |||
| 135 | + | fmt.Printf("No attachments configured for '%s'\n", target) | |||
| 136 | + | return nil | |||
| 137 | + | } | |||
| 138 | + | ||||
| 139 | + | fmt.Println(styles.Theme.Title.Render(fmt.Sprintf("Attachments for %s (local)", target))) | |||
| 140 | + | fmt.Println() | |||
| 141 | + | for _, img := range images { | |||
| 142 | + | fmt.Printf(" %s\n", img) | |||
| 143 | + | } | |||
| 144 | + | return nil | |||
| 145 | + | } | |||
| 146 | + | ||||
| 147 | + | // List all attachments | |||
| 148 | + | attachments := local.GetConfigService().GetAllAttachments(ctx) | |||
| 149 | + | ||||
| 150 | + | if len(attachments) == 0 { | |||
| 151 | + | fmt.Println(styles.Theme.Muted.Render("No attachments configured")) | |||
| 152 | + | return nil | |||
| 153 | + | } | |||
| 154 | + | ||||
| 155 | + | // Sort targets for consistent output | |||
| 156 | + | targets := make([]string, 0, len(attachments)) | |||
| 157 | + | for t := range attachments { | |||
| 158 | + | targets = append(targets, t) | |||
| 159 | + | } | |||
| 160 | + | sort.Strings(targets) | |||
| 161 | + | ||||
| 162 | + | fmt.Println(styles.Theme.Title.Render("Attachments (local)")) | |||
| 163 | + | fmt.Println() | |||
| 164 | + | ||||
| 165 | + | for _, t := range targets { | |||
| 166 | + | images := attachments[t] | |||
| 167 | + | fmt.Printf("%s\n", styles.Theme.Bold.Render(t)) | |||
| 168 | + | for _, img := range images { | |||
| 169 | + | fmt.Printf(" %s\n", img) | |||
| 170 | + | } | |||
| 171 | + | } | |||
| 172 | + | ||||
| 173 | + | return nil | |||
| 174 | + | } | |||
| 175 | + | ||||
| 176 | + | // newAttachmentsAddCmd creates the attachments add command. | |||
| 177 | + | func newAttachmentsAddCmd() *cobra.Command { | |||
| 178 | + | cmd := &cobra.Command{ | |||
| 179 | + | Use: "add <domain-or-group> <image>", | |||
| 180 | + | Short: "Add an attachment", | |||
| 181 | + | Long: `Add a container attachment to a domain or network group. | |||
| 182 | + | ||||
| 183 | + | Examples: | |||
| 184 | + | gordon attachments add app.example.com postgres:15 | |||
| 185 | + | gordon attachments add backend redis:7-alpine | |||
| 186 | + | gordon --remote https://gordon.mydomain.com attachments add api.mydomain.com memcached:latest`, | |||
| 187 | + | Args: cobra.ExactArgs(2), | |||
| 188 | + | RunE: func(cmd *cobra.Command, args []string) error { | |||
| 189 | + | ctx := context.Background() | |||
| 190 | + | target := args[0] | |||
| 191 | + | image := args[1] | |||
| 192 | + | ||||
| 193 | + | client, isRemote := GetRemoteClient() | |||
| 194 | + | if isRemote { | |||
| 195 | + | if err := client.AddAttachment(ctx, target, image); err != nil { | |||
| 196 | + | return fmt.Errorf("failed to add attachment: %w", err) | |||
| 197 | + | } | |||
| 198 | + | } else { | |||
| 199 | + | local, err := GetLocalServices(configPath) | |||
| 200 | + | if err != nil { | |||
| 201 | + | return fmt.Errorf("failed to initialize local services: %w", err) | |||
| 202 | + | } | |||
| 203 | + | if err := local.GetConfigService().AddAttachment(ctx, target, image); err != nil { | |||
| 204 | + | return fmt.Errorf("failed to add attachment: %w", err) | |||
| 205 | + | } | |||
| 206 | + | } | |||
| 207 | + | ||||
| 208 | + | fmt.Println(styles.RenderSuccess(fmt.Sprintf("Attachment added: %s -> %s", target, image))) | |||
| 209 | + | return nil | |||
| 210 | + | }, | |||
| 211 | + | } | |||
| 212 | + | ||||
| 213 | + | return cmd | |||
| 214 | + | } | |||
| 215 | + | ||||
| 216 | + | // newAttachmentsRemoveCmd creates the attachments remove command. | |||
| 217 | + | func newAttachmentsRemoveCmd() *cobra.Command { | |||
| 218 | + | var force bool | |||
| 219 | + | ||||
| 220 | + | cmd := &cobra.Command{ | |||
| 221 | + | Use: "remove <domain-or-group> <image>", | |||
| 222 | + | Short: "Remove an attachment", | |||
| 223 | + | Long: `Remove a container attachment from a domain or network group. | |||
| 224 | + | ||||
| 225 | + | Examples: | |||
| 226 | + | gordon attachments remove app.example.com postgres:15 | |||
| 227 | + | gordon attachments remove backend redis:7-alpine --force`, | |||
| 228 | + | Args: cobra.ExactArgs(2), | |||
| 229 | + | RunE: func(cmd *cobra.Command, args []string) error { | |||
| 230 | + | ctx := context.Background() | |||
| 231 | + | target := args[0] | |||
| 232 | + | image := args[1] | |||
| 233 | + | ||||
| 234 | + | // Confirm unless --force | |||
| 235 | + | if !force { | |||
| 236 | + | confirmed, err := components.RunConfirm( | |||
| 237 | + | fmt.Sprintf("Remove attachment '%s' from '%s'?", image, target), | |||
| 238 | + | components.WithDescription("This will remove the attachment from the configuration. The container will be stopped on next reload."), | |||
| 239 | + | ) | |||
| 240 | + | if err != nil { | |||
| 241 | + | return err | |||
| 242 | + | } | |||
| 243 | + | if !confirmed { | |||
| 244 | + | fmt.Println(styles.Theme.Muted.Render("Cancelled")) | |||
| 245 | + | return nil | |||
| 246 | + | } | |||
| 247 | + | } | |||
| 248 | + | ||||
| 249 | + | client, isRemote := GetRemoteClient() | |||
| 250 | + | if isRemote { | |||
| 251 | + | if err := client.RemoveAttachment(ctx, target, image); err != nil { | |||
| 252 | + | return fmt.Errorf("failed to remove attachment: %w", err) | |||
| 253 | + | } | |||
| 254 | + | } else { | |||
| 255 | + | local, err := GetLocalServices(configPath) | |||
| 256 | + | if err != nil { | |||
| 257 | + | return fmt.Errorf("failed to initialize local services: %w", err) | |||
| 258 | + | } | |||
| 259 | + | if err := local.GetConfigService().RemoveAttachment(ctx, target, image); err != nil { | |||
| 260 | + | return fmt.Errorf("failed to remove attachment: %w", err) | |||
| 261 | + | } | |||
| 262 | + | } | |||
| 263 | + | ||||
| 264 | + | fmt.Println(styles.RenderSuccess(fmt.Sprintf("Attachment removed: %s -> %s", target, image))) | |||
| 265 | + | return nil | |||
| 266 | + | }, | |||
| 267 | + | } | |||
| 268 | + | ||||
| 269 | + | cmd.Flags().BoolVarP(&force, "force", "f", false, "Skip confirmation") | |||
| 270 | + | ||||
| 271 | + | return cmd | |||
| 272 | + | } | |||
M internal/adapters/in/cli/auth.go +7 -7
| @@ -224,9 +224,9 @@ func newPasswordHashCmd() *cobra.Command { | |||
| 224 | 224 | ||
| 225 | 225 | The hash can be stored in your secrets backend and referenced in the config: | |
| 226 | 226 | ||
| 227 | - | [registry_auth] | |
| 227 | + | [auth] | |
| 228 | 228 | type = "password" | |
| 229 | - | password_hash = "gordon/registry/password_hash"`, | |
| 229 | + | password_hash = "gordon/auth/password_hash"`, | |
| 230 | 230 | RunE: func(cmd *cobra.Command, args []string) error { | |
| 231 | 231 | return runPasswordHash() | |
| 232 | 232 | }, | |
| @@ -410,9 +410,9 @@ func runPasswordHash() error { | |||
| 410 | 410 | fmt.Println(hash) | |
| 411 | 411 | fmt.Println() | |
| 412 | 412 | fmt.Println("Then reference the path in your config:") | |
| 413 | - | fmt.Println(" [registry_auth]") | |
| 413 | + | fmt.Println(" [auth]") | |
| 414 | 414 | fmt.Println(" type = \"password\"") | |
| 415 | - | fmt.Println(" password_hash = \"gordon/registry/password_hash\"") | |
| 415 | + | fmt.Println(" password_hash = \"gordon/auth/password_hash\"") | |
| 416 | 416 | ||
| 417 | 417 | return nil | |
| 418 | 418 | } | |
| @@ -430,7 +430,7 @@ func loadAuthConfig(configPath string) (*cliConfig, error) { | |||
| 430 | 430 | ||
| 431 | 431 | // Set defaults | |
| 432 | 432 | v.SetDefault("server.data_dir", app.DefaultDataDir()) | |
| 433 | - | v.SetDefault("secrets.backend", "unsafe") | |
| 433 | + | v.SetDefault("auth.secrets_backend", "unsafe") | |
| 434 | 434 | ||
| 435 | 435 | app.ConfigureViper(v, configPath) | |
| 436 | 436 | ||
| @@ -446,7 +446,7 @@ func loadAuthConfig(configPath string) (*cliConfig, error) { | |||
| 446 | 446 | } | |
| 447 | 447 | ||
| 448 | 448 | // Determine backend | |
| 449 | - | switch v.GetString("secrets.backend") { | |
| 449 | + | switch v.GetString("auth.secrets_backend") { | |
| 450 | 450 | case "pass": | |
| 451 | 451 | cfg.Backend = domain.SecretsBackendPass | |
| 452 | 452 | case "sops": | |
| @@ -456,7 +456,7 @@ func loadAuthConfig(configPath string) (*cliConfig, error) { | |||
| 456 | 456 | } | |
| 457 | 457 | ||
| 458 | 458 | // Load token secret from secrets backend | |
| 459 | - | tokenSecretPath := v.GetString("registry_auth.token_secret") | |
| 459 | + | tokenSecretPath := v.GetString("auth.token_secret") | |
| 460 | 460 | if tokenSecretPath != "" { | |
| 461 | 461 | // Load actual secret from the configured backend | |
| 462 | 462 | ctx := context.Background() | |
| @@ -224,9 +224,9 @@ func newPasswordHashCmd() *cobra.Command { | |||||
| 224 | 224 | ||||
| 225 | The hash can be stored in your secrets backend and referenced in the config: | 225 | The hash can be stored in your secrets backend and referenced in the config: | ||
| 226 | 226 | ||||
| 227 | - | [registry_auth] | 227 | + | [auth] |
| 228 | type = "password" | 228 | type = "password" | ||
| 229 | - | password_hash = "gordon/registry/password_hash"`, | 229 | + | password_hash = "gordon/auth/password_hash"`, |
| 230 | RunE: func(cmd *cobra.Command, args []string) error { | 230 | RunE: func(cmd *cobra.Command, args []string) error { | ||
| 231 | return runPasswordHash() | 231 | return runPasswordHash() | ||
| 232 | }, | 232 | }, | ||
| @@ -410,9 +410,9 @@ func runPasswordHash() error { | |||||
| 410 | fmt.Println(hash) | 410 | fmt.Println(hash) | ||
| 411 | fmt.Println() | 411 | fmt.Println() | ||
| 412 | fmt.Println("Then reference the path in your config:") | 412 | fmt.Println("Then reference the path in your config:") | ||
| 413 | - | fmt.Println(" [registry_auth]") | 413 | + | fmt.Println(" [auth]") |
| 414 | fmt.Println(" type = \"password\"") | 414 | fmt.Println(" type = \"password\"") | ||
| 415 | - | fmt.Println(" password_hash = \"gordon/registry/password_hash\"") | 415 | + | fmt.Println(" password_hash = \"gordon/auth/password_hash\"") |
| 416 | 416 | ||||
| 417 | return nil | 417 | return nil | ||
| 418 | } | 418 | } | ||
| @@ -430,7 +430,7 @@ func loadAuthConfig(configPath string) (*cliConfig, error) { | |||||
| 430 | 430 | ||||
| 431 | // Set defaults | 431 | // Set defaults | ||
| 432 | v.SetDefault("server.data_dir", app.DefaultDataDir()) | 432 | v.SetDefault("server.data_dir", app.DefaultDataDir()) | ||
| 433 | - | v.SetDefault("secrets.backend", "unsafe") | 433 | + | v.SetDefault("auth.secrets_backend", "unsafe") |
| 434 | 434 | ||||
| 435 | app.ConfigureViper(v, configPath) | 435 | app.ConfigureViper(v, configPath) | ||
| 436 | 436 | ||||
| @@ -446,7 +446,7 @@ func loadAuthConfig(configPath string) (*cliConfig, error) { | |||||
| 446 | } | 446 | } | ||
| 447 | 447 | ||||
| 448 | // Determine backend | 448 | // Determine backend | ||
| 449 | - | switch v.GetString("secrets.backend") { | 449 | + | switch v.GetString("auth.secrets_backend") { |
| 450 | case "pass": | 450 | case "pass": | ||
| 451 | cfg.Backend = domain.SecretsBackendPass | 451 | cfg.Backend = domain.SecretsBackendPass | ||
| 452 | case "sops": | 452 | case "sops": | ||
| @@ -456,7 +456,7 @@ func loadAuthConfig(configPath string) (*cliConfig, error) { | |||||
| 456 | } | 456 | } | ||
| 457 | 457 | ||||
| 458 | // Load token secret from secrets backend | 458 | // Load token secret from secrets backend | ||
| 459 | - | tokenSecretPath := v.GetString("registry_auth.token_secret") | 459 | + | tokenSecretPath := v.GetString("auth.token_secret") |
| 460 | if tokenSecretPath != "" { | 460 | if tokenSecretPath != "" { | ||
| 461 | // Load actual secret from the configured backend | 461 | // Load actual secret from the configured backend | ||
| 462 | ctx := context.Background() | 462 | ctx := context.Background() | ||
M internal/adapters/in/cli/remote/client.go +75 -0
| @@ -458,6 +458,81 @@ func (c *Client) StreamContainerLogs(ctx context.Context, logDomain string, line | |||
| 458 | 458 | return c.streamLogs(ctx, path) | |
| 459 | 459 | } | |
| 460 | 460 | ||
| 461 | + | // Attachments Config API | |
| 462 | + | ||
| 463 | + | // GetAllAttachmentsConfig returns all configured attachments. | |
| 464 | + | func (c *Client) GetAllAttachmentsConfig(ctx context.Context) (map[string][]string, error) { | |
| 465 | + | resp, err := c.request(ctx, http.MethodGet, "/attachments", nil) | |
| 466 | + | if err != nil { | |
| 467 | + | return nil, err | |
| 468 | + | } | |
| 469 | + | ||
| 470 | + | var result struct { | |
| 471 | + | Attachments map[string][]string `json:"attachments"` | |
| 472 | + | } | |
| 473 | + | if err := parseResponse(resp, &result); err != nil { | |
| 474 | + | return nil, err | |
| 475 | + | } | |
| 476 | + | ||
| 477 | + | return result.Attachments, nil | |
| 478 | + | } | |
| 479 | + | ||
| 480 | + | // GetAttachmentsConfig returns attachments for a specific domain/group from config. | |
| 481 | + | func (c *Client) GetAttachmentsConfig(ctx context.Context, domainOrGroup string) ([]string, error) { | |
| 482 | + | if domainOrGroup == "" { | |
| 483 | + | return nil, fmt.Errorf("domain or group is required") | |
| 484 | + | } | |
| 485 | + | path := "/attachments/" + url.PathEscape(domainOrGroup) | |
| 486 | + | resp, err := c.request(ctx, http.MethodGet, path, nil) | |
| 487 | + | if err != nil { | |
| 488 | + | return nil, err | |
| 489 | + | } | |
| 490 | + | ||
| 491 | + | var result struct { | |
| 492 | + | Target string `json:"target"` | |
| 493 | + | Images []string `json:"images"` | |
| 494 | + | } | |
| 495 | + | if err := parseResponse(resp, &result); err != nil { | |
| 496 | + | return nil, err | |
| 497 | + | } | |
| 498 | + | ||
| 499 | + | return result.Images, nil | |
| 500 | + | } | |
| 501 | + | ||
| 502 | + | // AddAttachment adds an attachment to a domain/group. | |
| 503 | + | func (c *Client) AddAttachment(ctx context.Context, domainOrGroup, image string) error { | |
| 504 | + | if domainOrGroup == "" { | |
| 505 | + | return fmt.Errorf("domain or group is required") | |
| 506 | + | } | |
| 507 | + | if image == "" { | |
| 508 | + | return fmt.Errorf("image is required") | |
| 509 | + | } | |
| 510 | + | path := "/attachments/" + url.PathEscape(domainOrGroup) | |
| 511 | + | resp, err := c.request(ctx, http.MethodPost, path, struct { | |
| 512 | + | Image string `json:"image"` | |
| 513 | + | }{Image: image}) | |
| 514 | + | if err != nil { | |
| 515 | + | return err | |
| 516 | + | } | |
| 517 | + | return parseResponse(resp, nil) | |
| 518 | + | } | |
| 519 | + | ||
| 520 | + | // RemoveAttachment removes an attachment from a domain/group. | |
| 521 | + | func (c *Client) RemoveAttachment(ctx context.Context, domainOrGroup, image string) error { | |
| 522 | + | if domainOrGroup == "" { | |
| 523 | + | return fmt.Errorf("domain or group is required") | |
| 524 | + | } | |
| 525 | + | if image == "" { | |
| 526 | + | return fmt.Errorf("image is required") | |
| 527 | + | } | |
| 528 | + | path := "/attachments/" + url.PathEscape(domainOrGroup) + "/" + url.PathEscape(image) | |
| 529 | + | resp, err := c.request(ctx, http.MethodDelete, path, nil) | |
| 530 | + | if err != nil { | |
| 531 | + | return err | |
| 532 | + | } | |
| 533 | + | return parseResponse(resp, nil) | |
| 534 | + | } | |
| 535 | + | ||
| 461 | 536 | // streamLogs handles SSE streaming for log endpoints. | |
| 462 | 537 | func (c *Client) streamLogs(ctx context.Context, path string) (<-chan string, error) { | |
| 463 | 538 | url := c.baseURL + "/admin" + path | |
| @@ -458,6 +458,81 @@ func (c *Client) StreamContainerLogs(ctx context.Context, logDomain string, line | |||||
| 458 | return c.streamLogs(ctx, path) | 458 | return c.streamLogs(ctx, path) | ||
| 459 | } | 459 | } | ||
| 460 | 460 | ||||
| 461 | + | // Attachments Config API | |||
| 462 | + | ||||
| 463 | + | // GetAllAttachmentsConfig returns all configured attachments. | |||
| 464 | + | func (c *Client) GetAllAttachmentsConfig(ctx context.Context) (map[string][]string, error) { | |||
| 465 | + | resp, err := c.request(ctx, http.MethodGet, "/attachments", nil) | |||
| 466 | + | if err != nil { | |||
| 467 | + | return nil, err | |||
| 468 | + | } | |||
| 469 | + | ||||
| 470 | + | var result struct { | |||
| 471 | + | Attachments map[string][]string `json:"attachments"` | |||
| 472 | + | } | |||
| 473 | + | if err := parseResponse(resp, &result); err != nil { | |||
| 474 | + | return nil, err | |||
| 475 | + | } | |||
| 476 | + | ||||
| 477 | + | return result.Attachments, nil | |||
| 478 | + | } | |||
| 479 | + | ||||
| 480 | + | // GetAttachmentsConfig returns attachments for a specific domain/group from config. | |||
| 481 | + | func (c *Client) GetAttachmentsConfig(ctx context.Context, domainOrGroup string) ([]string, error) { | |||
| 482 | + | if domainOrGroup == "" { | |||
| 483 | + | return nil, fmt.Errorf("domain or group is required") | |||
| 484 | + | } | |||
| 485 | + | path := "/attachments/" + url.PathEscape(domainOrGroup) | |||
| 486 | + | resp, err := c.request(ctx, http.MethodGet, path, nil) | |||
| 487 | + | if err != nil { | |||
| 488 | + | return nil, err | |||
| 489 | + | } | |||
| 490 | + | ||||
| 491 | + | var result struct { | |||
| 492 | + | Target string `json:"target"` | |||
| 493 | + | Images []string `json:"images"` | |||
| 494 | + | } | |||
| 495 | + | if err := parseResponse(resp, &result); err != nil { | |||
| 496 | + | return nil, err | |||
| 497 | + | } | |||
| 498 | + | ||||
| 499 | + | return result.Images, nil | |||
| 500 | + | } | |||
| 501 | + | ||||
| 502 | + | // AddAttachment adds an attachment to a domain/group. | |||
| 503 | + | func (c *Client) AddAttachment(ctx context.Context, domainOrGroup, image string) error { | |||
| 504 | + | if domainOrGroup == "" { | |||
| 505 | + | return fmt.Errorf("domain or group is required") | |||
| 506 | + | } | |||
| 507 | + | if image == "" { | |||
| 508 | + | return fmt.Errorf("image is required") | |||
| 509 | + | } | |||
| 510 | + | path := "/attachments/" + url.PathEscape(domainOrGroup) | |||
| 511 | + | resp, err := c.request(ctx, http.MethodPost, path, struct { | |||
| 512 | + | Image string `json:"image"` | |||
| 513 | + | }{Image: image}) | |||
| 514 | + | if err != nil { | |||
| 515 | + | return err | |||
| 516 | + | } | |||
| 517 | + | return parseResponse(resp, nil) | |||
| 518 | + | } | |||
| 519 | + | ||||
| 520 | + | // RemoveAttachment removes an attachment from a domain/group. | |||
| 521 | + | func (c *Client) RemoveAttachment(ctx context.Context, domainOrGroup, image string) error { | |||
| 522 | + | if domainOrGroup == "" { | |||
| 523 | + | return fmt.Errorf("domain or group is required") | |||
| 524 | + | } | |||
| 525 | + | if image == "" { | |||
| 526 | + | return fmt.Errorf("image is required") | |||
| 527 | + | } | |||
| 528 | + | path := "/attachments/" + url.PathEscape(domainOrGroup) + "/" + url.PathEscape(image) | |||
| 529 | + | resp, err := c.request(ctx, http.MethodDelete, path, nil) | |||
| 530 | + | if err != nil { | |||
| 531 | + | return err | |||
| 532 | + | } | |||
| 533 | + | return parseResponse(resp, nil) | |||
| 534 | + | } | |||
| 535 | + | ||||
| 461 | // streamLogs handles SSE streaming for log endpoints. | 536 | // streamLogs handles SSE streaming for log endpoints. | ||
| 462 | func (c *Client) streamLogs(ctx context.Context, path string) (<-chan string, error) { | 537 | func (c *Client) streamLogs(ctx context.Context, path string) (<-chan string, error) { | ||
| 463 | url := c.baseURL + "/admin" + path | 538 | url := c.baseURL + "/admin" + path | ||
M internal/adapters/in/cli/root.go +1 -0
| @@ -53,6 +53,7 @@ GORDON_REMOTE environment variable.`, | |||
| 53 | 53 | ||
| 54 | 54 | // Remote management commands | |
| 55 | 55 | rootCmd.AddCommand(newRoutesCmd()) | |
| 56 | + | rootCmd.AddCommand(newAttachmentsCmd()) | |
| 56 | 57 | rootCmd.AddCommand(newSecretsCmd()) | |
| 57 | 58 | rootCmd.AddCommand(newRemotesCmd()) | |
| 58 | 59 | rootCmd.AddCommand(newStatusCmd()) | |
| @@ -53,6 +53,7 @@ GORDON_REMOTE environment variable.`, | |||||
| 53 | 53 | ||||
| 54 | // Remote management commands | 54 | // Remote management commands | ||
| 55 | rootCmd.AddCommand(newRoutesCmd()) | 55 | rootCmd.AddCommand(newRoutesCmd()) | ||
| 56 | + | rootCmd.AddCommand(newAttachmentsCmd()) | |||
| 56 | rootCmd.AddCommand(newSecretsCmd()) | 57 | rootCmd.AddCommand(newSecretsCmd()) | ||
| 57 | rootCmd.AddCommand(newRemotesCmd()) | 58 | rootCmd.AddCommand(newRemotesCmd()) | ||
| 58 | rootCmd.AddCommand(newStatusCmd()) | 59 | rootCmd.AddCommand(newStatusCmd()) | ||
M internal/adapters/in/http/admin/handler.go +141 -0
| @@ -123,6 +123,8 @@ func (h *Handler) handleAdminRoutes(w http.ResponseWriter, r *http.Request) { | |||
| 123 | 123 | ||
| 124 | 124 | // Route to appropriate handler | |
| 125 | 125 | switch { | |
| 126 | + | case path == "/attachments" || strings.HasPrefix(path, "/attachments/"): | |
| 127 | + | h.handleAttachmentsConfig(w, r, path) | |
| 126 | 128 | case path == "/routes" || strings.HasPrefix(path, "/routes/"): | |
| 127 | 129 | h.handleRoutes(w, r, path) | |
| 128 | 130 | case path == "/networks": | |
| @@ -857,3 +859,142 @@ func (h *Handler) streamContainerLogs(w http.ResponseWriter, r *http.Request, lo | |||
| 857 | 859 | } | |
| 858 | 860 | } | |
| 859 | 861 | } | |
| 862 | + | ||
| 863 | + | // handleAttachmentsConfig handles /admin/attachments endpoints for config-level attachments. | |
| 864 | + | func (h *Handler) handleAttachmentsConfig(w http.ResponseWriter, r *http.Request, path string) { | |
| 865 | + | // Parse target (domain or group) from path | |
| 866 | + | target := strings.TrimPrefix(path, "/attachments/") | |
| 867 | + | if target == "/attachments" { | |
| 868 | + | target = "" | |
| 869 | + | } | |
| 870 | + | ||
| 871 | + | switch r.Method { | |
| 872 | + | case http.MethodGet: | |
| 873 | + | h.handleAttachmentsConfigGet(w, r, target) | |
| 874 | + | case http.MethodPost: | |
| 875 | + | h.handleAttachmentsConfigPost(w, r, target) | |
| 876 | + | case http.MethodDelete: | |
| 877 | + | h.handleAttachmentsConfigDelete(w, r, target) | |
| 878 | + | default: | |
| 879 | + | h.sendError(w, http.StatusMethodNotAllowed, "method not allowed") | |
| 880 | + | } | |
| 881 | + | } | |
| 882 | + | ||
| 883 | + | func (h *Handler) handleAttachmentsConfigGet(w http.ResponseWriter, r *http.Request, target string) { | |
| 884 | + | ctx := r.Context() | |
| 885 | + | ||
| 886 | + | // Check read permission | |
| 887 | + | if !HasAccess(ctx, domain.AdminResourceConfig, domain.AdminActionRead) { | |
| 888 | + | h.sendError(w, http.StatusForbidden, "insufficient permissions for config:read") | |
| 889 | + | return | |
| 890 | + | } | |
| 891 | + | ||
| 892 | + | if target == "" { | |
| 893 | + | // List all attachments | |
| 894 | + | attachments := h.configSvc.GetAllAttachments(ctx) | |
| 895 | + | h.sendJSON(w, http.StatusOK, dto.AttachmentsConfigResponse{Attachments: attachments}) | |
| 896 | + | return | |
| 897 | + | } | |
| 898 | + | ||
| 899 | + | // List attachments for specific target | |
| 900 | + | images, err := h.configSvc.GetAttachmentsFor(ctx, target) | |
| 901 | + | if err != nil { | |
| 902 | + | if errors.Is(err, domain.ErrAttachmentNotFound) { | |
| 903 | + | h.sendError(w, http.StatusNotFound, "no attachments found for target") | |
| 904 | + | return | |
| 905 | + | } | |
| 906 | + | h.sendError(w, http.StatusInternalServerError, "failed to get attachments") | |
| 907 | + | return | |
| 908 | + | } | |
| 909 | + | ||
| 910 | + | h.sendJSON(w, http.StatusOK, dto.AttachmentConfigResponse{Target: target, Images: images}) | |
| 911 | + | } | |
| 912 | + | ||
| 913 | + | func (h *Handler) handleAttachmentsConfigPost(w http.ResponseWriter, r *http.Request, target string) { | |
| 914 | + | ctx := r.Context() | |
| 915 | + | log := zerowrap.FromCtx(ctx) | |
| 916 | + | ||
| 917 | + | // Check write permission | |
| 918 | + | if !HasAccess(ctx, domain.AdminResourceConfig, domain.AdminActionWrite) { | |
| 919 | + | h.sendError(w, http.StatusForbidden, "insufficient permissions for config:write") | |
| 920 | + | return | |
| 921 | + | } | |
| 922 | + | ||
| 923 | + | if target == "" { | |
| 924 | + | h.sendError(w, http.StatusBadRequest, "target (domain or group) required in path") | |
| 925 | + | return | |
| 926 | + | } | |
| 927 | + | ||
| 928 | + | // Limit request body size | |
| 929 | + | r.Body = http.MaxBytesReader(w, r.Body, maxAdminRequestSize) | |
| 930 | + | ||
| 931 | + | var req dto.AttachmentAddRequest | |
| 932 | + | if err := json.NewDecoder(r.Body).Decode(&req); err != nil { | |
| 933 | + | log.Warn().Err(err).Msg("invalid attachment JSON") | |
| 934 | + | h.sendError(w, http.StatusBadRequest, "invalid JSON") | |
| 935 | + | return | |
| 936 | + | } | |
| 937 | + | ||
| 938 | + | if err := h.configSvc.AddAttachment(ctx, target, req.Image); err != nil { | |
| 939 | + | log.Error().Err(err).Str("target", target).Str("image", req.Image).Msg("failed to add attachment") | |
| 940 | + | switch { | |
| 941 | + | case errors.Is(err, domain.ErrAttachmentExists): | |
| 942 | + | h.sendError(w, http.StatusConflict, "attachment already exists") | |
| 943 | + | case errors.Is(err, domain.ErrAttachmentImageEmpty): | |
| 944 | + | h.sendError(w, http.StatusBadRequest, err.Error()) | |
| 945 | + | case errors.Is(err, domain.ErrAttachmentTargetEmpty): | |
| 946 | + | h.sendError(w, http.StatusBadRequest, err.Error()) | |
| 947 | + | default: | |
| 948 | + | h.sendError(w, http.StatusInternalServerError, "failed to add attachment") | |
| 949 | + | } | |
| 950 | + | return | |
| 951 | + | } | |
| 952 | + | ||
| 953 | + | log.Info().Str("target", target).Str("image", req.Image).Msg("attachment added") | |
| 954 | + | h.sendJSON(w, http.StatusCreated, dto.AttachmentStatusResponse{Status: "added"}) | |
| 955 | + | } | |
| 956 | + | ||
| 957 | + | func (h *Handler) handleAttachmentsConfigDelete(w http.ResponseWriter, r *http.Request, target string) { | |
| 958 | + | ctx := r.Context() | |
| 959 | + | log := zerowrap.FromCtx(ctx) | |
| 960 | + | ||
| 961 | + | // Check write permission | |
| 962 | + | if !HasAccess(ctx, domain.AdminResourceConfig, domain.AdminActionWrite) { | |
| 963 | + | h.sendError(w, http.StatusForbidden, "insufficient permissions for config:write") | |
| 964 | + | return | |
| 965 | + | } | |
| 966 | + | ||
| 967 | + | if target == "" { | |
| 968 | + | h.sendError(w, http.StatusBadRequest, "target (domain or group) required in path") | |
| 969 | + | return | |
| 970 | + | } | |
| 971 | + | ||
| 972 | + | // Parse image from path: /attachments/{target}/{image} | |
| 973 | + | // target at this point contains "{domain}/{image}" or just "{domain}" | |
| 974 | + | parts := strings.SplitN(target, "/", 2) | |
| 975 | + | if len(parts) != 2 { | |
| 976 | + | h.sendError(w, http.StatusBadRequest, "image required in path: /attachments/{target}/{image}") | |
| 977 | + | return | |
| 978 | + | } | |
| 979 | + | ||
| 980 | + | domainOrGroup := parts[0] | |
| 981 | + | image := parts[1] | |
| 982 | + | ||
| 983 | + | if err := h.configSvc.RemoveAttachment(ctx, domainOrGroup, image); err != nil { | |
| 984 | + | log.Error().Err(err).Str("target", domainOrGroup).Str("image", image).Msg("failed to remove attachment") | |
| 985 | + | switch { | |
| 986 | + | case errors.Is(err, domain.ErrAttachmentNotFound): | |
| 987 | + | h.sendError(w, http.StatusNotFound, "attachment not found") | |
| 988 | + | case errors.Is(err, domain.ErrAttachmentImageEmpty): | |
| 989 | + | h.sendError(w, http.StatusBadRequest, err.Error()) | |
| 990 | + | case errors.Is(err, domain.ErrAttachmentTargetEmpty): | |
| 991 | + | h.sendError(w, http.StatusBadRequest, err.Error()) | |
| 992 | + | default: | |
| 993 | + | h.sendError(w, http.StatusInternalServerError, "failed to remove attachment") | |
| 994 | + | } | |
| 995 | + | return | |
| 996 | + | } | |
| 997 | + | ||
| 998 | + | log.Info().Str("target", domainOrGroup).Str("image", image).Msg("attachment removed") | |
| 999 | + | h.sendJSON(w, http.StatusOK, dto.AttachmentStatusResponse{Status: "removed"}) | |
| 1000 | + | } | |
| @@ -123,6 +123,8 @@ func (h *Handler) handleAdminRoutes(w http.ResponseWriter, r *http.Request) { | |||||
| 123 | 123 | ||||
| 124 | // Route to appropriate handler | 124 | // Route to appropriate handler | ||
| 125 | switch { | 125 | switch { | ||
| 126 | + | case path == "/attachments" || strings.HasPrefix(path, "/attachments/"): | |||
| 127 | + | h.handleAttachmentsConfig(w, r, path) | |||
| 126 | case path == "/routes" || strings.HasPrefix(path, "/routes/"): | 128 | case path == "/routes" || strings.HasPrefix(path, "/routes/"): | ||
| 127 | h.handleRoutes(w, r, path) | 129 | h.handleRoutes(w, r, path) | ||
| 128 | case path == "/networks": | 130 | case path == "/networks": | ||
| @@ -857,3 +859,142 @@ func (h *Handler) streamContainerLogs(w http.ResponseWriter, r *http.Request, lo | |||||
| 857 | } | 859 | } | ||
| 858 | } | 860 | } | ||
| 859 | } | 861 | } | ||
| 862 | + | ||||
| 863 | + | // handleAttachmentsConfig handles /admin/attachments endpoints for config-level attachments. | |||
| 864 | + | func (h *Handler) handleAttachmentsConfig(w http.ResponseWriter, r *http.Request, path string) { | |||
| 865 | + | // Parse target (domain or group) from path | |||
| 866 | + | target := strings.TrimPrefix(path, "/attachments/") | |||
| 867 | + | if target == "/attachments" { | |||
| 868 | + | target = "" | |||
| 869 | + | } | |||
| 870 | + | ||||
| 871 | + | switch r.Method { | |||
| 872 | + | case http.MethodGet: | |||
| 873 | + | h.handleAttachmentsConfigGet(w, r, target) | |||
| 874 | + | case http.MethodPost: | |||
| 875 | + | h.handleAttachmentsConfigPost(w, r, target) | |||
| 876 | + | case http.MethodDelete: | |||
| 877 | + | h.handleAttachmentsConfigDelete(w, r, target) | |||
| 878 | + | default: | |||
| 879 | + | h.sendError(w, http.StatusMethodNotAllowed, "method not allowed") | |||
| 880 | + | } | |||
| 881 | + | } | |||
| 882 | + | ||||
| 883 | + | func (h *Handler) handleAttachmentsConfigGet(w http.ResponseWriter, r *http.Request, target string) { | |||
| 884 | + | ctx := r.Context() | |||
| 885 | + | ||||
| 886 | + | // Check read permission | |||
| 887 | + | if !HasAccess(ctx, domain.AdminResourceConfig, domain.AdminActionRead) { | |||
| 888 | + | h.sendError(w, http.StatusForbidden, "insufficient permissions for config:read") | |||
| 889 | + | return | |||
| 890 | + | } | |||
| 891 | + | ||||
| 892 | + | if target == "" { | |||
| 893 | + | // List all attachments | |||
| 894 | + | attachments := h.configSvc.GetAllAttachments(ctx) | |||
| 895 | + | h.sendJSON(w, http.StatusOK, dto.AttachmentsConfigResponse{Attachments: attachments}) | |||
| 896 | + | return | |||
| 897 | + | } | |||
| 898 | + | ||||
| 899 | + | // List attachments for specific target | |||
| 900 | + | images, err := h.configSvc.GetAttachmentsFor(ctx, target) | |||
| 901 | + | if err != nil { | |||
| 902 | + | if errors.Is(err, domain.ErrAttachmentNotFound) { | |||
| 903 | + | h.sendError(w, http.StatusNotFound, "no attachments found for target") | |||
| 904 | + | return | |||
| 905 | + | } | |||
| 906 | + | h.sendError(w, http.StatusInternalServerError, "failed to get attachments") | |||
| 907 | + | return | |||
| 908 | + | } | |||
| 909 | + | ||||
| 910 | + | h.sendJSON(w, http.StatusOK, dto.AttachmentConfigResponse{Target: target, Images: images}) | |||
| 911 | + | } | |||
| 912 | + | ||||
| 913 | + | func (h *Handler) handleAttachmentsConfigPost(w http.ResponseWriter, r *http.Request, target string) { | |||
| 914 | + | ctx := r.Context() | |||
| 915 | + | log := zerowrap.FromCtx(ctx) | |||
| 916 | + | ||||
| 917 | + | // Check write permission | |||
| 918 | + | if !HasAccess(ctx, domain.AdminResourceConfig, domain.AdminActionWrite) { | |||
| 919 | + | h.sendError(w, http.StatusForbidden, "insufficient permissions for config:write") | |||
| 920 | + | return | |||
| 921 | + | } | |||
| 922 | + | ||||
| 923 | + | if target == "" { | |||
| 924 | + | h.sendError(w, http.StatusBadRequest, "target (domain or group) required in path") | |||
| 925 | + | return | |||
| 926 | + | } | |||
| 927 | + | ||||
| 928 | + | // Limit request body size | |||
| 929 | + | r.Body = http.MaxBytesReader(w, r.Body, maxAdminRequestSize) | |||
| 930 | + | ||||
| 931 | + | var req dto.AttachmentAddRequest | |||
| 932 | + | if err := json.NewDecoder(r.Body).Decode(&req); err != nil { | |||
| 933 | + | log.Warn().Err(err).Msg("invalid attachment JSON") | |||
| 934 | + | h.sendError(w, http.StatusBadRequest, "invalid JSON") | |||
| 935 | + | return | |||
| 936 | + | } | |||
| 937 | + | ||||
| 938 | + | if err := h.configSvc.AddAttachment(ctx, target, req.Image); err != nil { | |||
| 939 | + | log.Error().Err(err).Str("target", target).Str("image", req.Image).Msg("failed to add attachment") | |||
| 940 | + | switch { | |||
| 941 | + | case errors.Is(err, domain.ErrAttachmentExists): | |||
| 942 | + | h.sendError(w, http.StatusConflict, "attachment already exists") | |||
| 943 | + | case errors.Is(err, domain.ErrAttachmentImageEmpty): | |||
| 944 | + | h.sendError(w, http.StatusBadRequest, err.Error()) | |||
| 945 | + | case errors.Is(err, domain.ErrAttachmentTargetEmpty): | |||
| 946 | + | h.sendError(w, http.StatusBadRequest, err.Error()) | |||
| 947 | + | default: | |||
| 948 | + | h.sendError(w, http.StatusInternalServerError, "failed to add attachment") | |||
| 949 | + | } | |||
| 950 | + | return | |||
| 951 | + | } | |||
| 952 | + | ||||
| 953 | + | log.Info().Str("target", target).Str("image", req.Image).Msg("attachment added") | |||
| 954 | + | h.sendJSON(w, http.StatusCreated, dto.AttachmentStatusResponse{Status: "added"}) | |||
| 955 | + | } | |||
| 956 | + | ||||
| 957 | + | func (h *Handler) handleAttachmentsConfigDelete(w http.ResponseWriter, r *http.Request, target string) { | |||
| 958 | + | ctx := r.Context() | |||
| 959 | + | log := zerowrap.FromCtx(ctx) | |||
| 960 | + | ||||
| 961 | + | // Check write permission | |||
| 962 | + | if !HasAccess(ctx, domain.AdminResourceConfig, domain.AdminActionWrite) { | |||
| 963 | + | h.sendError(w, http.StatusForbidden, "insufficient permissions for config:write") | |||
| 964 | + | return | |||
| 965 | + | } | |||
| 966 | + | ||||
| 967 | + | if target == "" { | |||
| 968 | + | h.sendError(w, http.StatusBadRequest, "target (domain or group) required in path") | |||
| 969 | + | return | |||
| 970 | + | } | |||
| 971 | + | ||||
| 972 | + | // Parse image from path: /attachments/{target}/{image} | |||
| 973 | + | // target at this point contains "{domain}/{image}" or just "{domain}" | |||
| 974 | + | parts := strings.SplitN(target, "/", 2) | |||
| 975 | + | if len(parts) != 2 { | |||
| 976 | + | h.sendError(w, http.StatusBadRequest, "image required in path: /attachments/{target}/{image}") | |||
| 977 | + | return | |||
| 978 | + | } | |||
| 979 | + | ||||
| 980 | + | domainOrGroup := parts[0] | |||
| 981 | + | image := parts[1] | |||
| 982 | + | ||||
| 983 | + | if err := h.configSvc.RemoveAttachment(ctx, domainOrGroup, image); err != nil { | |||
| 984 | + | log.Error().Err(err).Str("target", domainOrGroup).Str("image", image).Msg("failed to remove attachment") | |||
| 985 | + | switch { | |||
| 986 | + | case errors.Is(err, domain.ErrAttachmentNotFound): | |||
| 987 | + | h.sendError(w, http.StatusNotFound, "attachment not found") | |||
| 988 | + | case errors.Is(err, domain.ErrAttachmentImageEmpty): | |||
| 989 | + | h.sendError(w, http.StatusBadRequest, err.Error()) | |||
| 990 | + | case errors.Is(err, domain.ErrAttachmentTargetEmpty): | |||
| 991 | + | h.sendError(w, http.StatusBadRequest, err.Error()) | |||
| 992 | + | default: | |||
| 993 | + | h.sendError(w, http.StatusInternalServerError, "failed to remove attachment") | |||
| 994 | + | } | |||
| 995 | + | return | |||
| 996 | + | } | |||
| 997 | + | ||||
| 998 | + | log.Info().Str("target", domainOrGroup).Str("image", image).Msg("attachment removed") | |||
| 999 | + | h.sendJSON(w, http.StatusOK, dto.AttachmentStatusResponse{Status: "removed"}) | |||
| 1000 | + | } | |||
M internal/app/run.go +31 -34
| @@ -90,19 +90,16 @@ type Config struct { | |||
| 90 | 90 | Dir string `mapstructure:"dir"` | |
| 91 | 91 | } `mapstructure:"env"` | |
| 92 | 92 | ||
| 93 | - | Secrets struct { | |
| 94 | - | Backend string `mapstructure:"backend"` // "pass", "sops", or "unsafe" | |
| 95 | - | } `mapstructure:"secrets"` | |
| 96 | - | ||
| 97 | - | RegistryAuth struct { | |
| 98 | - | Enabled bool `mapstructure:"enabled"` | |
| 99 | - | Type string `mapstructure:"type"` // "password" or "token" | |
| 100 | - | Username string `mapstructure:"username"` | |
| 101 | - | Password string `mapstructure:"password"` // deprecated: use password_hash | |
| 102 | - | PasswordHash string `mapstructure:"password_hash"` // path in secrets backend | |
| 103 | - | TokenSecret string `mapstructure:"token_secret"` // path in secrets backend | |
| 104 | - | TokenExpiry string `mapstructure:"token_expiry"` // e.g., "720h" | |
| 105 | - | } `mapstructure:"registry_auth"` | |
| 93 | + | Auth struct { | |
| 94 | + | Enabled bool `mapstructure:"enabled"` | |
| 95 | + | Type string `mapstructure:"type"` // "password" or "token" | |
| 96 | + | SecretsBackend string `mapstructure:"secrets_backend"` // "pass", "sops", or "unsafe" | |
| 97 | + | Username string `mapstructure:"username"` | |
| 98 | + | Password string `mapstructure:"password"` // deprecated: use password_hash | |
| 99 | + | PasswordHash string `mapstructure:"password_hash"` // path in secrets backend | |
| 100 | + | TokenSecret string `mapstructure:"token_secret"` // path in secrets backend | |
| 101 | + | TokenExpiry string `mapstructure:"token_expiry"` // e.g., "720h", "30d" | |
| 102 | + | } `mapstructure:"auth"` | |
| 106 | 103 | } | |
| 107 | 104 | ||
| 108 | 105 | // services holds all the services used by the application. | |
| @@ -264,7 +261,7 @@ func createServices(ctx context.Context, v *viper.Viper, cfg Config, log zerowra | |||
| 264 | 261 | } | |
| 265 | 262 | ||
| 266 | 263 | // Generate internal registry credentials for loopback-only pulls | |
| 267 | - | if cfg.RegistryAuth.Enabled { | |
| 264 | + | if cfg.Auth.Enabled { | |
| 268 | 265 | svc.internalRegUser, svc.internalRegPass, err = generateInternalRegistryAuth() | |
| 269 | 266 | if err != nil { | |
| 270 | 267 | return nil, log.WrapErr(err, "failed to generate internal registry credentials") | |
| @@ -517,12 +514,12 @@ func GetInternalCredentials() (*InternalCredentials, error) { | |||
| 517 | 514 | ||
| 518 | 515 | // createAuthService creates the authentication service and token store. | |
| 519 | 516 | func createAuthService(ctx context.Context, cfg Config, log zerowrap.Logger) (out.TokenStore, *auth.Service, error) { | |
| 520 | - | if !cfg.RegistryAuth.Enabled { | |
| 517 | + | if !cfg.Auth.Enabled { | |
| 521 | 518 | return nil, nil, nil | |
| 522 | 519 | } | |
| 523 | 520 | ||
| 524 | - | authType := resolveAuthType(cfg.RegistryAuth.Type) | |
| 525 | - | backend := resolveSecretsBackend(cfg.Secrets.Backend) | |
| 521 | + | authType := resolveAuthType(cfg.Auth.Type) | |
| 522 | + | backend := resolveSecretsBackend(cfg.Auth.SecretsBackend) | |
| 526 | 523 | dataDir := resolveDataDir(cfg.Server.DataDir) | |
| 527 | 524 | ||
| 528 | 525 | store, err := createTokenStore(authType, backend, dataDir, log) | |
| @@ -586,9 +583,9 @@ func createTokenStore(authType domain.AuthType, backend domain.SecretsBackend, d | |||
| 586 | 583 | ||
| 587 | 584 | func buildAuthConfig(ctx context.Context, cfg Config, authType domain.AuthType, backend domain.SecretsBackend, dataDir string, log zerowrap.Logger) (auth.Config, error) { | |
| 588 | 585 | authConfig := auth.Config{ | |
| 589 | - | Enabled: cfg.RegistryAuth.Enabled, | |
| 586 | + | Enabled: cfg.Auth.Enabled, | |
| 590 | 587 | AuthType: authType, | |
| 591 | - | Username: cfg.RegistryAuth.Username, | |
| 588 | + | Username: cfg.Auth.Username, | |
| 592 | 589 | } | |
| 593 | 590 | ||
| 594 | 591 | switch authType { | |
| @@ -611,17 +608,17 @@ func buildAuthConfig(ctx context.Context, cfg Config, authType domain.AuthType, | |||
| 611 | 608 | } | |
| 612 | 609 | ||
| 613 | 610 | func loadPasswordHash(ctx context.Context, cfg Config, backend domain.SecretsBackend, dataDir string, log zerowrap.Logger) (string, error) { | |
| 614 | - | if cfg.RegistryAuth.PasswordHash != "" { | |
| 615 | - | hash, err := loadSecret(ctx, backend, cfg.RegistryAuth.PasswordHash, dataDir, log) | |
| 611 | + | if cfg.Auth.PasswordHash != "" { | |
| 612 | + | hash, err := loadSecret(ctx, backend, cfg.Auth.PasswordHash, dataDir, log) | |
| 616 | 613 | if err != nil { | |
| 617 | 614 | return "", log.WrapErr(err, "failed to load password hash") | |
| 618 | 615 | } | |
| 619 | 616 | return hash, nil | |
| 620 | 617 | } | |
| 621 | 618 | ||
| 622 | - | if cfg.RegistryAuth.Password != "" { | |
| 619 | + | if cfg.Auth.Password != "" { | |
| 623 | 620 | log.Warn().Msg("using plain password in config is deprecated, use password_hash with a secrets backend") | |
| 624 | - | return cfg.RegistryAuth.Password, nil | |
| 621 | + | return cfg.Auth.Password, nil | |
| 625 | 622 | } | |
| 626 | 623 | ||
| 627 | 624 | return "", nil | |
| @@ -633,7 +630,7 @@ func loadTokenConfig(ctx context.Context, cfg Config, backend domain.SecretsBack | |||
| 633 | 630 | return nil, 0, err | |
| 634 | 631 | } | |
| 635 | 632 | ||
| 636 | - | expiry, err := parseTokenExpiry(cfg.RegistryAuth.TokenExpiry) | |
| 633 | + | expiry, err := parseTokenExpiry(cfg.Auth.TokenExpiry) | |
| 637 | 634 | if err != nil { | |
| 638 | 635 | return nil, 0, err | |
| 639 | 636 | } | |
| @@ -642,11 +639,11 @@ func loadTokenConfig(ctx context.Context, cfg Config, backend domain.SecretsBack | |||
| 642 | 639 | } | |
| 643 | 640 | ||
| 644 | 641 | func loadTokenSecret(ctx context.Context, cfg Config, backend domain.SecretsBackend, dataDir string, log zerowrap.Logger) ([]byte, error) { | |
| 645 | - | if cfg.RegistryAuth.TokenSecret == "" { | |
| 642 | + | if cfg.Auth.TokenSecret == "" { | |
| 646 | 643 | return nil, fmt.Errorf("token_secret is required for token authentication") | |
| 647 | 644 | } | |
| 648 | 645 | ||
| 649 | - | secret, err := loadSecret(ctx, backend, cfg.RegistryAuth.TokenSecret, dataDir, log) | |
| 646 | + | secret, err := loadSecret(ctx, backend, cfg.Auth.TokenSecret, dataDir, log) | |
| 650 | 647 | if err != nil { | |
| 651 | 648 | return nil, log.WrapErr(err, "failed to load token secret") | |
| 652 | 649 | } | |
| @@ -692,11 +689,11 @@ func loadSecret(ctx context.Context, backend domain.SecretsBackend, path, dataDi | |||
| 692 | 689 | // createContainerService creates the container service with configuration. | |
| 693 | 690 | func createContainerService(v *viper.Viper, cfg Config, svc *services) *container.Service { | |
| 694 | 691 | containerConfig := container.Config{ | |
| 695 | - | RegistryAuthEnabled: cfg.RegistryAuth.Enabled, | |
| 692 | + | RegistryAuthEnabled: cfg.Auth.Enabled, | |
| 696 | 693 | RegistryDomain: cfg.Server.RegistryDomain, | |
| 697 | 694 | RegistryPort: cfg.Server.RegistryPort, | |
| 698 | - | RegistryUsername: cfg.RegistryAuth.Username, | |
| 699 | - | RegistryPassword: cfg.RegistryAuth.Password, | |
| 695 | + | RegistryUsername: cfg.Auth.Username, | |
| 696 | + | RegistryPassword: cfg.Auth.Password, | |
| 700 | 697 | InternalRegistryUsername: svc.internalRegUser, | |
| 701 | 698 | InternalRegistryPassword: svc.internalRegPass, | |
| 702 | 699 | PullPolicy: v.GetString("deploy.pull_policy"), | |
| @@ -803,7 +800,7 @@ func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger) (http.Ha | |||
| 803 | 800 | middleware.RequestLogger(log), | |
| 804 | 801 | } | |
| 805 | 802 | ||
| 806 | - | if cfg.RegistryAuth.Enabled && svc.authSvc != nil { | |
| 803 | + | if cfg.Auth.Enabled && svc.authSvc != nil { | |
| 807 | 804 | internalAuth := middleware.InternalRegistryAuth{ | |
| 808 | 805 | Username: svc.internalRegUser, | |
| 809 | 806 | Password: svc.internalRegPass, | |
| @@ -1143,10 +1140,10 @@ func loadConfig(v *viper.Viper, configPath string) error { | |||
| 1143 | 1140 | v.SetDefault("logging.container_logs.max_backups", 3) | |
| 1144 | 1141 | v.SetDefault("logging.container_logs.max_age", 28) | |
| 1145 | 1142 | v.SetDefault("env.dir", "") // defaults to {data_dir}/env when empty | |
| 1146 | - | v.SetDefault("secrets.backend", "unsafe") | |
| 1147 | - | v.SetDefault("registry_auth.enabled", false) | |
| 1148 | - | v.SetDefault("registry_auth.type", "password") | |
| 1149 | - | v.SetDefault("registry_auth.token_expiry", "720h") | |
| 1143 | + | v.SetDefault("auth.enabled", true) | |
| 1144 | + | v.SetDefault("auth.type", "password") | |
| 1145 | + | v.SetDefault("auth.secrets_backend", "unsafe") | |
| 1146 | + | v.SetDefault("auth.token_expiry", "720h") | |
| 1150 | 1147 | v.SetDefault("auto_route.enabled", false) | |
| 1151 | 1148 | v.SetDefault("network_isolation.enabled", false) | |
| 1152 | 1149 | v.SetDefault("network_isolation.network_prefix", "gordon") | |
| @@ -90,19 +90,16 @@ type Config struct { | |||||
| 90 | Dir string `mapstructure:"dir"` | 90 | Dir string `mapstructure:"dir"` | ||
| 91 | } `mapstructure:"env"` | 91 | } `mapstructure:"env"` | ||
| 92 | 92 | ||||
| 93 | - | Secrets struct { | 93 | + | Auth struct { |
| 94 | - | Backend string `mapstructure:"backend"` // "pass", "sops", or "unsafe" | 94 | + | Enabled bool `mapstructure:"enabled"` |
| 95 | - | } `mapstructure:"secrets"` | 95 | + | Type string `mapstructure:"type"` // "password" or "token" |
| 96 | - | 96 | + | SecretsBackend string `mapstructure:"secrets_backend"` // "pass", "sops", or "unsafe" | |
| 97 | - | RegistryAuth struct { | 97 | + | Username string `mapstructure:"username"` |
| 98 | - | Enabled bool `mapstructure:"enabled"` | 98 | + | Password string `mapstructure:"password"` // deprecated: use password_hash |
| 99 | - | Type string `mapstructure:"type"` // "password" or "token" | 99 | + | PasswordHash string `mapstructure:"password_hash"` // path in secrets backend |
| 100 | - | Username string `mapstructure:"username"` | 100 | + | TokenSecret string `mapstructure:"token_secret"` // path in secrets backend |
| 101 | - | Password string `mapstructure:"password"` // deprecated: use password_hash | 101 | + | TokenExpiry string `mapstructure:"token_expiry"` // e.g., "720h", "30d" |
| 102 | - | PasswordHash string `mapstructure:"password_hash"` // path in secrets backend | 102 | + | } `mapstructure:"auth"` |
| 103 | - | TokenSecret string `mapstructure:"token_secret"` // path in secrets backend | |||
| 104 | - | TokenExpiry string `mapstructure:"token_expiry"` // e.g., "720h" | |||
| 105 | - | } `mapstructure:"registry_auth"` | |||
| 106 | } | 103 | } | ||
| 107 | 104 | ||||
| 108 | // services holds all the services used by the application. | 105 | // services holds all the services used by the application. | ||
| @@ -264,7 +261,7 @@ func createServices(ctx context.Context, v *viper.Viper, cfg Config, log zerowra | |||||
| 264 | } | 261 | } | ||
| 265 | 262 | ||||
| 266 | // Generate internal registry credentials for loopback-only pulls | 263 | // Generate internal registry credentials for loopback-only pulls | ||
| 267 | - | if cfg.RegistryAuth.Enabled { | 264 | + | if cfg.Auth.Enabled { |
| 268 | svc.internalRegUser, svc.internalRegPass, err = generateInternalRegistryAuth() | 265 | svc.internalRegUser, svc.internalRegPass, err = generateInternalRegistryAuth() | ||
| 269 | if err != nil { | 266 | if err != nil { | ||
| 270 | return nil, log.WrapErr(err, "failed to generate internal registry credentials") | 267 | return nil, log.WrapErr(err, "failed to generate internal registry credentials") | ||
| @@ -517,12 +514,12 @@ func GetInternalCredentials() (*InternalCredentials, error) { | |||||
| 517 | 514 | ||||
| 518 | // createAuthService creates the authentication service and token store. | 515 | // createAuthService creates the authentication service and token store. | ||
| 519 | func createAuthService(ctx context.Context, cfg Config, log zerowrap.Logger) (out.TokenStore, *auth.Service, error) { | 516 | func createAuthService(ctx context.Context, cfg Config, log zerowrap.Logger) (out.TokenStore, *auth.Service, error) { | ||
| 520 | - | if !cfg.RegistryAuth.Enabled { | 517 | + | if !cfg.Auth.Enabled { |
| 521 | return nil, nil, nil | 518 | return nil, nil, nil | ||
| 522 | } | 519 | } | ||
| 523 | 520 | ||||
| 524 | - | authType := resolveAuthType(cfg.RegistryAuth.Type) | 521 | + | authType := resolveAuthType(cfg.Auth.Type) |
| 525 | - | backend := resolveSecretsBackend(cfg.Secrets.Backend) | 522 | + | backend := resolveSecretsBackend(cfg.Auth.SecretsBackend) |
| 526 | dataDir := resolveDataDir(cfg.Server.DataDir) | 523 | dataDir := resolveDataDir(cfg.Server.DataDir) | ||
| 527 | 524 | ||||
| 528 | store, err := createTokenStore(authType, backend, dataDir, log) | 525 | store, err := createTokenStore(authType, backend, dataDir, log) | ||
| @@ -586,9 +583,9 @@ func createTokenStore(authType domain.AuthType, backend domain.SecretsBackend, d | |||||
| 586 | 583 | ||||
| 587 | func buildAuthConfig(ctx context.Context, cfg Config, authType domain.AuthType, backend domain.SecretsBackend, dataDir string, log zerowrap.Logger) (auth.Config, error) { | 584 | func buildAuthConfig(ctx context.Context, cfg Config, authType domain.AuthType, backend domain.SecretsBackend, dataDir string, log zerowrap.Logger) (auth.Config, error) { | ||
| 588 | authConfig := auth.Config{ | 585 | authConfig := auth.Config{ | ||
| 589 | - | Enabled: cfg.RegistryAuth.Enabled, | 586 | + | Enabled: cfg.Auth.Enabled, |
| 590 | AuthType: authType, | 587 | AuthType: authType, | ||
| 591 | - | Username: cfg.RegistryAuth.Username, | 588 | + | Username: cfg.Auth.Username, |
| 592 | } | 589 | } | ||
| 593 | 590 | ||||
| 594 | switch authType { | 591 | switch authType { | ||
| @@ -611,17 +608,17 @@ func buildAuthConfig(ctx context.Context, cfg Config, authType domain.AuthType, | |||||
| 611 | } | 608 | } | ||
| 612 | 609 | ||||
| 613 | func loadPasswordHash(ctx context.Context, cfg Config, backend domain.SecretsBackend, dataDir string, log zerowrap.Logger) (string, error) { | 610 | func loadPasswordHash(ctx context.Context, cfg Config, backend domain.SecretsBackend, dataDir string, log zerowrap.Logger) (string, error) { | ||
| 614 | - | if cfg.RegistryAuth.PasswordHash != "" { | 611 | + | if cfg.Auth.PasswordHash != "" { |
| 615 | - | hash, err := loadSecret(ctx, backend, cfg.RegistryAuth.PasswordHash, dataDir, log) | 612 | + | hash, err := loadSecret(ctx, backend, cfg.Auth.PasswordHash, dataDir, log) |
| 616 | if err != nil { | 613 | if err != nil { | ||
| 617 | return "", log.WrapErr(err, "failed to load password hash") | 614 | return "", log.WrapErr(err, "failed to load password hash") | ||
| 618 | } | 615 | } | ||
| 619 | return hash, nil | 616 | return hash, nil | ||
| 620 | } | 617 | } | ||
| 621 | 618 | ||||
| 622 | - | if cfg.RegistryAuth.Password != "" { | 619 | + | if cfg.Auth.Password != "" { |
| 623 | log.Warn().Msg("using plain password in config is deprecated, use password_hash with a secrets backend") | 620 | log.Warn().Msg("using plain password in config is deprecated, use password_hash with a secrets backend") | ||
| 624 | - | return cfg.RegistryAuth.Password, nil | 621 | + | return cfg.Auth.Password, nil |
| 625 | } | 622 | } | ||
| 626 | 623 | ||||
| 627 | return "", nil | 624 | return "", nil | ||
| @@ -633,7 +630,7 @@ func loadTokenConfig(ctx context.Context, cfg Config, backend domain.SecretsBack | |||||
| 633 | return nil, 0, err | 630 | return nil, 0, err | ||
| 634 | } | 631 | } | ||
| 635 | 632 | ||||
| 636 | - | expiry, err := parseTokenExpiry(cfg.RegistryAuth.TokenExpiry) | 633 | + | expiry, err := parseTokenExpiry(cfg.Auth.TokenExpiry) |
| 637 | if err != nil { | 634 | if err != nil { | ||
| 638 | return nil, 0, err | 635 | return nil, 0, err | ||
| 639 | } | 636 | } | ||
| @@ -642,11 +639,11 @@ func loadTokenConfig(ctx context.Context, cfg Config, backend domain.SecretsBack | |||||
| 642 | } | 639 | } | ||
| 643 | 640 | ||||
| 644 | func loadTokenSecret(ctx context.Context, cfg Config, backend domain.SecretsBackend, dataDir string, log zerowrap.Logger) ([]byte, error) { | 641 | func loadTokenSecret(ctx context.Context, cfg Config, backend domain.SecretsBackend, dataDir string, log zerowrap.Logger) ([]byte, error) { | ||
| 645 | - | if cfg.RegistryAuth.TokenSecret == "" { | 642 | + | if cfg.Auth.TokenSecret == "" { |
| 646 | return nil, fmt.Errorf("token_secret is required for token authentication") | 643 | return nil, fmt.Errorf("token_secret is required for token authentication") | ||
| 647 | } | 644 | } | ||
| 648 | 645 | ||||
| 649 | - | secret, err := loadSecret(ctx, backend, cfg.RegistryAuth.TokenSecret, dataDir, log) | 646 | + | secret, err := loadSecret(ctx, backend, cfg.Auth.TokenSecret, dataDir, log) |
| 650 | if err != nil { | 647 | if err != nil { | ||
| 651 | return nil, log.WrapErr(err, "failed to load token secret") | 648 | return nil, log.WrapErr(err, "failed to load token secret") | ||
| 652 | } | 649 | } | ||
| @@ -692,11 +689,11 @@ func loadSecret(ctx context.Context, backend domain.SecretsBackend, path, dataDi | |||||
| 692 | // createContainerService creates the container service with configuration. | 689 | // createContainerService creates the container service with configuration. | ||
| 693 | func createContainerService(v *viper.Viper, cfg Config, svc *services) *container.Service { | 690 | func createContainerService(v *viper.Viper, cfg Config, svc *services) *container.Service { | ||
| 694 | containerConfig := container.Config{ | 691 | containerConfig := container.Config{ | ||
| 695 | - | RegistryAuthEnabled: cfg.RegistryAuth.Enabled, | 692 | + | RegistryAuthEnabled: cfg.Auth.Enabled, |
| 696 | RegistryDomain: cfg.Server.RegistryDomain, | 693 | RegistryDomain: cfg.Server.RegistryDomain, | ||
| 697 | RegistryPort: cfg.Server.RegistryPort, | 694 | RegistryPort: cfg.Server.RegistryPort, | ||
| 698 | - | RegistryUsername: cfg.RegistryAuth.Username, | 695 | + | RegistryUsername: cfg.Auth.Username, |
| 699 | - | RegistryPassword: cfg.RegistryAuth.Password, | 696 | + | RegistryPassword: cfg.Auth.Password, |
| 700 | InternalRegistryUsername: svc.internalRegUser, | 697 | InternalRegistryUsername: svc.internalRegUser, | ||
| 701 | InternalRegistryPassword: svc.internalRegPass, | 698 | InternalRegistryPassword: svc.internalRegPass, | ||
| 702 | PullPolicy: v.GetString("deploy.pull_policy"), | 699 | PullPolicy: v.GetString("deploy.pull_policy"), | ||
| @@ -803,7 +800,7 @@ func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger) (http.Ha | |||||
| 803 | middleware.RequestLogger(log), | 800 | middleware.RequestLogger(log), | ||
| 804 | } | 801 | } | ||
| 805 | 802 | ||||
| 806 | - | if cfg.RegistryAuth.Enabled && svc.authSvc != nil { | 803 | + | if cfg.Auth.Enabled && svc.authSvc != nil { |
| 807 | internalAuth := middleware.InternalRegistryAuth{ | 804 | internalAuth := middleware.InternalRegistryAuth{ | ||
| 808 | Username: svc.internalRegUser, | 805 | Username: svc.internalRegUser, | ||
| 809 | Password: svc.internalRegPass, | 806 | Password: svc.internalRegPass, | ||
| @@ -1143,10 +1140,10 @@ func loadConfig(v *viper.Viper, configPath string) error { | |||||
| 1143 | v.SetDefault("logging.container_logs.max_backups", 3) | 1140 | v.SetDefault("logging.container_logs.max_backups", 3) | ||
| 1144 | v.SetDefault("logging.container_logs.max_age", 28) | 1141 | v.SetDefault("logging.container_logs.max_age", 28) | ||
| 1145 | v.SetDefault("env.dir", "") // defaults to {data_dir}/env when empty | 1142 | v.SetDefault("env.dir", "") // defaults to {data_dir}/env when empty | ||
| 1146 | - | v.SetDefault("secrets.backend", "unsafe") | 1143 | + | v.SetDefault("auth.enabled", true) |
| 1147 | - | v.SetDefault("registry_auth.enabled", false) | 1144 | + | v.SetDefault("auth.type", "password") |
| 1148 | - | v.SetDefault("registry_auth.type", "password") | 1145 | + | v.SetDefault("auth.secrets_backend", "unsafe") |
| 1149 | - | v.SetDefault("registry_auth.token_expiry", "720h") | 1146 | + | v.SetDefault("auth.token_expiry", "720h") |
| 1150 | v.SetDefault("auto_route.enabled", false) | 1147 | v.SetDefault("auto_route.enabled", false) | ||
| 1151 | v.SetDefault("network_isolation.enabled", false) | 1148 | v.SetDefault("network_isolation.enabled", false) | ||
| 1152 | v.SetDefault("network_isolation.network_prefix", "gordon") | 1149 | v.SetDefault("network_isolation.network_prefix", "gordon") | ||
M internal/boundaries/in/config.go +12 -0
| @@ -56,4 +56,16 @@ type ConfigService interface { | |||
| 56 | 56 | ||
| 57 | 57 | // GetExternalRoutes returns all configured external routes. | |
| 58 | 58 | GetExternalRoutes() map[string]string | |
| 59 | + | ||
| 60 | + | // GetAllAttachments returns all configured attachments. | |
| 61 | + | GetAllAttachments(ctx context.Context) map[string][]string | |
| 62 | + | ||
| 63 | + | // GetAttachmentsFor returns attachments for a specific domain or network group. | |
| 64 | + | GetAttachmentsFor(ctx context.Context, domainOrGroup string) ([]string, error) | |
| 65 | + | ||
| 66 | + | // AddAttachment adds an image to a domain/group's attachments. | |
| 67 | + | AddAttachment(ctx context.Context, domainOrGroup, image string) error | |
| 68 | + | ||
| 69 | + | // RemoveAttachment removes an image from a domain/group's attachments. | |
| 70 | + | RemoveAttachment(ctx context.Context, domainOrGroup, image string) error | |
| 59 | 71 | } | |
| @@ -56,4 +56,16 @@ type ConfigService interface { | |||||
| 56 | 56 | ||||
| 57 | // GetExternalRoutes returns all configured external routes. | 57 | // GetExternalRoutes returns all configured external routes. | ||
| 58 | GetExternalRoutes() map[string]string | 58 | GetExternalRoutes() map[string]string | ||
| 59 | + | ||||
| 60 | + | // GetAllAttachments returns all configured attachments. | |||
| 61 | + | GetAllAttachments(ctx context.Context) map[string][]string | |||
| 62 | + | ||||
| 63 | + | // GetAttachmentsFor returns attachments for a specific domain or network group. | |||
| 64 | + | GetAttachmentsFor(ctx context.Context, domainOrGroup string) ([]string, error) | |||
| 65 | + | ||||
| 66 | + | // AddAttachment adds an image to a domain/group's attachments. | |||
| 67 | + | AddAttachment(ctx context.Context, domainOrGroup, image string) error | |||
| 68 | + | ||||
| 69 | + | // RemoveAttachment removes an image from a domain/group's attachments. | |||
| 70 | + | RemoveAttachment(ctx context.Context, domainOrGroup, image string) error | |||
| 59 | } | 71 | } | ||
M internal/boundaries/in/mocks/mock_config_service.go +247 -0
| @@ -38,6 +38,69 @@ func (_m *MockConfigService) EXPECT() *MockConfigService_Expecter { | |||
| 38 | 38 | return &MockConfigService_Expecter{mock: &_m.Mock} | |
| 39 | 39 | } | |
| 40 | 40 | ||
| 41 | + | // AddAttachment provides a mock function for the type MockConfigService | |
| 42 | + | func (_mock *MockConfigService) AddAttachment(ctx context.Context, domainOrGroup string, image string) error { | |
| 43 | + | ret := _mock.Called(ctx, domainOrGroup, image) | |
| 44 | + | ||
| 45 | + | if len(ret) == 0 { | |
| 46 | + | panic("no return value specified for AddAttachment") | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | var r0 error | |
| 50 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) error); ok { | |
| 51 | + | r0 = returnFunc(ctx, domainOrGroup, image) | |
| 52 | + | } else { | |
| 53 | + | r0 = ret.Error(0) | |
| 54 | + | } | |
| 55 | + | return r0 | |
| 56 | + | } | |
| 57 | + | ||
| 58 | + | // MockConfigService_AddAttachment_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddAttachment' | |
| 59 | + | type MockConfigService_AddAttachment_Call struct { | |
| 60 | + | *mock.Call | |
| 61 | + | } | |
| 62 | + | ||
| 63 | + | // AddAttachment is a helper method to define mock.On call | |
| 64 | + | // - ctx context.Context | |
| 65 | + | // - domainOrGroup string | |
| 66 | + | // - image string | |
| 67 | + | func (_e *MockConfigService_Expecter) AddAttachment(ctx interface{}, domainOrGroup interface{}, image interface{}) *MockConfigService_AddAttachment_Call { | |
| 68 | + | return &MockConfigService_AddAttachment_Call{Call: _e.mock.On("AddAttachment", ctx, domainOrGroup, image)} | |
| 69 | + | } | |
| 70 | + | ||
| 71 | + | func (_c *MockConfigService_AddAttachment_Call) Run(run func(ctx context.Context, domainOrGroup string, image string)) *MockConfigService_AddAttachment_Call { | |
| 72 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 73 | + | var arg0 context.Context | |
| 74 | + | if args[0] != nil { | |
| 75 | + | arg0 = args[0].(context.Context) | |
| 76 | + | } | |
| 77 | + | var arg1 string | |
| 78 | + | if args[1] != nil { | |
| 79 | + | arg1 = args[1].(string) | |
| 80 | + | } | |
| 81 | + | var arg2 string | |
| 82 | + | if args[2] != nil { | |
| 83 | + | arg2 = args[2].(string) | |
| 84 | + | } | |
| 85 | + | run( | |
| 86 | + | arg0, | |
| 87 | + | arg1, | |
| 88 | + | arg2, | |
| 89 | + | ) | |
| 90 | + | }) | |
| 91 | + | return _c | |
| 92 | + | } | |
| 93 | + | ||
| 94 | + | func (_c *MockConfigService_AddAttachment_Call) Return(err error) *MockConfigService_AddAttachment_Call { | |
| 95 | + | _c.Call.Return(err) | |
| 96 | + | return _c | |
| 97 | + | } | |
| 98 | + | ||
| 99 | + | func (_c *MockConfigService_AddAttachment_Call) RunAndReturn(run func(ctx context.Context, domainOrGroup string, image string) error) *MockConfigService_AddAttachment_Call { | |
| 100 | + | _c.Call.Return(run) | |
| 101 | + | return _c | |
| 102 | + | } | |
| 103 | + | ||
| 41 | 104 | // AddRoute provides a mock function for the type MockConfigService | |
| 42 | 105 | func (_mock *MockConfigService) AddRoute(ctx context.Context, route domain.Route) error { | |
| 43 | 106 | ret := _mock.Called(ctx, route) | |
| @@ -95,6 +158,127 @@ func (_c *MockConfigService_AddRoute_Call) RunAndReturn(run func(ctx context.Con | |||
| 95 | 158 | return _c | |
| 96 | 159 | } | |
| 97 | 160 | ||
| 161 | + | // GetAllAttachments provides a mock function for the type MockConfigService | |
| 162 | + | func (_mock *MockConfigService) GetAllAttachments(ctx context.Context) map[string][]string { | |
| 163 | + | ret := _mock.Called(ctx) | |
| 164 | + | ||
| 165 | + | if len(ret) == 0 { | |
| 166 | + | panic("no return value specified for GetAllAttachments") | |
| 167 | + | } | |
| 168 | + | ||
| 169 | + | var r0 map[string][]string | |
| 170 | + | if returnFunc, ok := ret.Get(0).(func(context.Context) map[string][]string); ok { | |
| 171 | + | r0 = returnFunc(ctx) | |
| 172 | + | } else { | |
| 173 | + | if ret.Get(0) != nil { | |
| 174 | + | r0 = ret.Get(0).(map[string][]string) | |
| 175 | + | } | |
| 176 | + | } | |
| 177 | + | return r0 | |
| 178 | + | } | |
| 179 | + | ||
| 180 | + | // MockConfigService_GetAllAttachments_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAllAttachments' | |
| 181 | + | type MockConfigService_GetAllAttachments_Call struct { | |
| 182 | + | *mock.Call | |
| 183 | + | } | |
| 184 | + | ||
| 185 | + | // GetAllAttachments is a helper method to define mock.On call | |
| 186 | + | // - ctx context.Context | |
| 187 | + | func (_e *MockConfigService_Expecter) GetAllAttachments(ctx interface{}) *MockConfigService_GetAllAttachments_Call { | |
| 188 | + | return &MockConfigService_GetAllAttachments_Call{Call: _e.mock.On("GetAllAttachments", ctx)} | |
| 189 | + | } | |
| 190 | + | ||
| 191 | + | func (_c *MockConfigService_GetAllAttachments_Call) Run(run func(ctx context.Context)) *MockConfigService_GetAllAttachments_Call { | |
| 192 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 193 | + | var arg0 context.Context | |
| 194 | + | if args[0] != nil { | |
| 195 | + | arg0 = args[0].(context.Context) | |
| 196 | + | } | |
| 197 | + | run( | |
| 198 | + | arg0, | |
| 199 | + | ) | |
| 200 | + | }) | |
| 201 | + | return _c | |
| 202 | + | } | |
| 203 | + | ||
| 204 | + | func (_c *MockConfigService_GetAllAttachments_Call) Return(stringToStrings map[string][]string) *MockConfigService_GetAllAttachments_Call { | |
| 205 | + | _c.Call.Return(stringToStrings) | |
| 206 | + | return _c | |
| 207 | + | } | |
| 208 | + | ||
| 209 | + | func (_c *MockConfigService_GetAllAttachments_Call) RunAndReturn(run func(ctx context.Context) map[string][]string) *MockConfigService_GetAllAttachments_Call { | |
| 210 | + | _c.Call.Return(run) | |
| 211 | + | return _c | |
| 212 | + | } | |
| 213 | + | ||
| 214 | + | // GetAttachmentsFor provides a mock function for the type MockConfigService | |
| 215 | + | func (_mock *MockConfigService) GetAttachmentsFor(ctx context.Context, domainOrGroup string) ([]string, error) { | |
| 216 | + | ret := _mock.Called(ctx, domainOrGroup) | |
| 217 | + | ||
| 218 | + | if len(ret) == 0 { | |
| 219 | + | panic("no return value specified for GetAttachmentsFor") | |
| 220 | + | } | |
| 221 | + | ||
| 222 | + | var r0 []string | |
| 223 | + | var r1 error | |
| 224 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) ([]string, error)); ok { | |
| 225 | + | return returnFunc(ctx, domainOrGroup) | |
| 226 | + | } | |
| 227 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) []string); ok { | |
| 228 | + | r0 = returnFunc(ctx, domainOrGroup) | |
| 229 | + | } else { | |
| 230 | + | if ret.Get(0) != nil { | |
| 231 | + | r0 = ret.Get(0).([]string) | |
| 232 | + | } | |
| 233 | + | } | |
| 234 | + | if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok { | |
| 235 | + | r1 = returnFunc(ctx, domainOrGroup) | |
| 236 | + | } else { | |
| 237 | + | r1 = ret.Error(1) | |
| 238 | + | } | |
| 239 | + | return r0, r1 | |
| 240 | + | } | |
| 241 | + | ||
| 242 | + | // MockConfigService_GetAttachmentsFor_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAttachmentsFor' | |
| 243 | + | type MockConfigService_GetAttachmentsFor_Call struct { | |
| 244 | + | *mock.Call | |
| 245 | + | } | |
| 246 | + | ||
| 247 | + | // GetAttachmentsFor is a helper method to define mock.On call | |
| 248 | + | // - ctx context.Context | |
| 249 | + | // - domainOrGroup string | |
| 250 | + | func (_e *MockConfigService_Expecter) GetAttachmentsFor(ctx interface{}, domainOrGroup interface{}) *MockConfigService_GetAttachmentsFor_Call { | |
| 251 | + | return &MockConfigService_GetAttachmentsFor_Call{Call: _e.mock.On("GetAttachmentsFor", ctx, domainOrGroup)} | |
| 252 | + | } | |
| 253 | + | ||
| 254 | + | func (_c *MockConfigService_GetAttachmentsFor_Call) Run(run func(ctx context.Context, domainOrGroup string)) *MockConfigService_GetAttachmentsFor_Call { | |
| 255 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 256 | + | var arg0 context.Context | |
| 257 | + | if args[0] != nil { | |
| 258 | + | arg0 = args[0].(context.Context) | |
| 259 | + | } | |
| 260 | + | var arg1 string | |
| 261 | + | if args[1] != nil { | |
| 262 | + | arg1 = args[1].(string) | |
| 263 | + | } | |
| 264 | + | run( | |
| 265 | + | arg0, | |
| 266 | + | arg1, | |
| 267 | + | ) | |
| 268 | + | }) | |
| 269 | + | return _c | |
| 270 | + | } | |
| 271 | + | ||
| 272 | + | func (_c *MockConfigService_GetAttachmentsFor_Call) Return(strings []string, err error) *MockConfigService_GetAttachmentsFor_Call { | |
| 273 | + | _c.Call.Return(strings, err) | |
| 274 | + | return _c | |
| 275 | + | } | |
| 276 | + | ||
| 277 | + | func (_c *MockConfigService_GetAttachmentsFor_Call) RunAndReturn(run func(ctx context.Context, domainOrGroup string) ([]string, error)) *MockConfigService_GetAttachmentsFor_Call { | |
| 278 | + | _c.Call.Return(run) | |
| 279 | + | return _c | |
| 280 | + | } | |
| 281 | + | ||
| 98 | 282 | // GetDataDir provides a mock function for the type MockConfigService | |
| 99 | 283 | func (_mock *MockConfigService) GetDataDir() string { | |
| 100 | 284 | ret := _mock.Called() | |
| @@ -621,6 +805,69 @@ func (_c *MockConfigService_Load_Call) RunAndReturn(run func(ctx context.Context | |||
| 621 | 805 | return _c | |
| 622 | 806 | } | |
| 623 | 807 | ||
| 808 | + | // RemoveAttachment provides a mock function for the type MockConfigService | |
| 809 | + | func (_mock *MockConfigService) RemoveAttachment(ctx context.Context, domainOrGroup string, image string) error { | |
| 810 | + | ret := _mock.Called(ctx, domainOrGroup, image) | |
| 811 | + | ||
| 812 | + | if len(ret) == 0 { | |
| 813 | + | panic("no return value specified for RemoveAttachment") | |
| 814 | + | } | |
| 815 | + | ||
| 816 | + | var r0 error | |
| 817 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) error); ok { | |
| 818 | + | r0 = returnFunc(ctx, domainOrGroup, image) | |
| 819 | + | } else { | |
| 820 | + | r0 = ret.Error(0) | |
| 821 | + | } | |
| 822 | + | return r0 | |
| 823 | + | } | |
| 824 | + | ||
| 825 | + | // MockConfigService_RemoveAttachment_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoveAttachment' | |
| 826 | + | type MockConfigService_RemoveAttachment_Call struct { | |
| 827 | + | *mock.Call | |
| 828 | + | } | |
| 829 | + | ||
| 830 | + | // RemoveAttachment is a helper method to define mock.On call | |
| 831 | + | // - ctx context.Context | |
| 832 | + | // - domainOrGroup string | |
| 833 | + | // - image string | |
| 834 | + | func (_e *MockConfigService_Expecter) RemoveAttachment(ctx interface{}, domainOrGroup interface{}, image interface{}) *MockConfigService_RemoveAttachment_Call { | |
| 835 | + | return &MockConfigService_RemoveAttachment_Call{Call: _e.mock.On("RemoveAttachment", ctx, domainOrGroup, image)} | |
| 836 | + | } | |
| 837 | + | ||
| 838 | + | func (_c *MockConfigService_RemoveAttachment_Call) Run(run func(ctx context.Context, domainOrGroup string, image string)) *MockConfigService_RemoveAttachment_Call { | |
| 839 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 840 | + | var arg0 context.Context | |
| 841 | + | if args[0] != nil { | |
| 842 | + | arg0 = args[0].(context.Context) | |
| 843 | + | } | |
| 844 | + | var arg1 string | |
| 845 | + | if args[1] != nil { | |
| 846 | + | arg1 = args[1].(string) | |
| 847 | + | } | |
| 848 | + | var arg2 string | |
| 849 | + | if args[2] != nil { | |
| 850 | + | arg2 = args[2].(string) | |
| 851 | + | } | |
| 852 | + | run( | |
| 853 | + | arg0, | |
| 854 | + | arg1, | |
| 855 | + | arg2, | |
| 856 | + | ) | |
| 857 | + | }) | |
| 858 | + | return _c | |
| 859 | + | } | |
| 860 | + | ||
| 861 | + | func (_c *MockConfigService_RemoveAttachment_Call) Return(err error) *MockConfigService_RemoveAttachment_Call { | |
| 862 | + | _c.Call.Return(err) | |
| 863 | + | return _c | |
| 864 | + | } | |
| 865 | + | ||
| 866 | + | func (_c *MockConfigService_RemoveAttachment_Call) RunAndReturn(run func(ctx context.Context, domainOrGroup string, image string) error) *MockConfigService_RemoveAttachment_Call { | |
| 867 | + | _c.Call.Return(run) | |
| 868 | + | return _c | |
| 869 | + | } | |
| 870 | + | ||
| 624 | 871 | // RemoveRoute provides a mock function for the type MockConfigService | |
| 625 | 872 | func (_mock *MockConfigService) RemoveRoute(ctx context.Context, domain1 string) error { | |
| 626 | 873 | ret := _mock.Called(ctx, domain1) | |
| @@ -38,6 +38,69 @@ func (_m *MockConfigService) EXPECT() *MockConfigService_Expecter { | |||||
| 38 | return &MockConfigService_Expecter{mock: &_m.Mock} | 38 | return &MockConfigService_Expecter{mock: &_m.Mock} | ||
| 39 | } | 39 | } | ||
| 40 | 40 | ||||
| 41 | + | // AddAttachment provides a mock function for the type MockConfigService | |||
| 42 | + | func (_mock *MockConfigService) AddAttachment(ctx context.Context, domainOrGroup string, image string) error { | |||
| 43 | + | ret := _mock.Called(ctx, domainOrGroup, image) | |||
| 44 | + | ||||
| 45 | + | if len(ret) == 0 { | |||
| 46 | + | panic("no return value specified for AddAttachment") | |||
| 47 | + | } | |||
| 48 | + | ||||
| 49 | + | var r0 error | |||
| 50 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) error); ok { | |||
| 51 | + | r0 = returnFunc(ctx, domainOrGroup, image) | |||
| 52 | + | } else { | |||
| 53 | + | r0 = ret.Error(0) | |||
| 54 | + | } | |||
| 55 | + | return r0 | |||
| 56 | + | } | |||
| 57 | + | ||||
| 58 | + | // MockConfigService_AddAttachment_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddAttachment' | |||
| 59 | + | type MockConfigService_AddAttachment_Call struct { | |||
| 60 | + | *mock.Call | |||
| 61 | + | } | |||
| 62 | + | ||||
| 63 | + | // AddAttachment is a helper method to define mock.On call | |||
| 64 | + | // - ctx context.Context | |||
| 65 | + | // - domainOrGroup string | |||
| 66 | + | // - image string | |||
| 67 | + | func (_e *MockConfigService_Expecter) AddAttachment(ctx interface{}, domainOrGroup interface{}, image interface{}) *MockConfigService_AddAttachment_Call { | |||
| 68 | + | return &MockConfigService_AddAttachment_Call{Call: _e.mock.On("AddAttachment", ctx, domainOrGroup, image)} | |||
| 69 | + | } | |||
| 70 | + | ||||
| 71 | + | func (_c *MockConfigService_AddAttachment_Call) Run(run func(ctx context.Context, domainOrGroup string, image string)) *MockConfigService_AddAttachment_Call { | |||
| 72 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 73 | + | var arg0 context.Context | |||
| 74 | + | if args[0] != nil { | |||
| 75 | + | arg0 = args[0].(context.Context) | |||
| 76 | + | } | |||
| 77 | + | var arg1 string | |||
| 78 | + | if args[1] != nil { | |||
| 79 | + | arg1 = args[1].(string) | |||
| 80 | + | } | |||
| 81 | + | var arg2 string | |||
| 82 | + | if args[2] != nil { | |||
| 83 | + | arg2 = args[2].(string) | |||
| 84 | + | } | |||
| 85 | + | run( | |||
| 86 | + | arg0, | |||
| 87 | + | arg1, | |||
| 88 | + | arg2, | |||
| 89 | + | ) | |||
| 90 | + | }) | |||
| 91 | + | return _c | |||
| 92 | + | } | |||
| 93 | + | ||||
| 94 | + | func (_c *MockConfigService_AddAttachment_Call) Return(err error) *MockConfigService_AddAttachment_Call { | |||
| 95 | + | _c.Call.Return(err) | |||
| 96 | + | return _c | |||
| 97 | + | } | |||
| 98 | + | ||||
| 99 | + | func (_c *MockConfigService_AddAttachment_Call) RunAndReturn(run func(ctx context.Context, domainOrGroup string, image string) error) *MockConfigService_AddAttachment_Call { | |||
| 100 | + | _c.Call.Return(run) | |||
| 101 | + | return _c | |||
| 102 | + | } | |||
| 103 | + | ||||
| 41 | // AddRoute provides a mock function for the type MockConfigService | 104 | // AddRoute provides a mock function for the type MockConfigService | ||
| 42 | func (_mock *MockConfigService) AddRoute(ctx context.Context, route domain.Route) error { | 105 | func (_mock *MockConfigService) AddRoute(ctx context.Context, route domain.Route) error { | ||
| 43 | ret := _mock.Called(ctx, route) | 106 | ret := _mock.Called(ctx, route) | ||
| @@ -95,6 +158,127 @@ func (_c *MockConfigService_AddRoute_Call) RunAndReturn(run func(ctx context.Con | |||||
| 95 | return _c | 158 | return _c | ||
| 96 | } | 159 | } | ||
| 97 | 160 | ||||
| 161 | + | // GetAllAttachments provides a mock function for the type MockConfigService | |||
| 162 | + | func (_mock *MockConfigService) GetAllAttachments(ctx context.Context) map[string][]string { | |||
| 163 | + | ret := _mock.Called(ctx) | |||
| 164 | + | ||||
| 165 | + | if len(ret) == 0 { | |||
| 166 | + | panic("no return value specified for GetAllAttachments") | |||
| 167 | + | } | |||
| 168 | + | ||||
| 169 | + | var r0 map[string][]string | |||
| 170 | + | if returnFunc, ok := ret.Get(0).(func(context.Context) map[string][]string); ok { | |||
| 171 | + | r0 = returnFunc(ctx) | |||
| 172 | + | } else { | |||
| 173 | + | if ret.Get(0) != nil { | |||
| 174 | + | r0 = ret.Get(0).(map[string][]string) | |||
| 175 | + | } | |||
| 176 | + | } | |||
| 177 | + | return r0 | |||
| 178 | + | } | |||
| 179 | + | ||||
| 180 | + | // MockConfigService_GetAllAttachments_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAllAttachments' | |||
| 181 | + | type MockConfigService_GetAllAttachments_Call struct { | |||
| 182 | + | *mock.Call | |||
| 183 | + | } | |||
| 184 | + | ||||
| 185 | + | // GetAllAttachments is a helper method to define mock.On call | |||
| 186 | + | // - ctx context.Context | |||
| 187 | + | func (_e *MockConfigService_Expecter) GetAllAttachments(ctx interface{}) *MockConfigService_GetAllAttachments_Call { | |||
| 188 | + | return &MockConfigService_GetAllAttachments_Call{Call: _e.mock.On("GetAllAttachments", ctx)} | |||
| 189 | + | } | |||
| 190 | + | ||||
| 191 | + | func (_c *MockConfigService_GetAllAttachments_Call) Run(run func(ctx context.Context)) *MockConfigService_GetAllAttachments_Call { | |||
| 192 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 193 | + | var arg0 context.Context | |||
| 194 | + | if args[0] != nil { | |||
| 195 | + | arg0 = args[0].(context.Context) | |||
| 196 | + | } | |||
| 197 | + | run( | |||
| 198 | + | arg0, | |||
| 199 | + | ) | |||
| 200 | + | }) | |||
| 201 | + | return _c | |||
| 202 | + | } | |||
| 203 | + | ||||
| 204 | + | func (_c *MockConfigService_GetAllAttachments_Call) Return(stringToStrings map[string][]string) *MockConfigService_GetAllAttachments_Call { | |||
| 205 | + | _c.Call.Return(stringToStrings) | |||
| 206 | + | return _c | |||
| 207 | + | } | |||
| 208 | + | ||||
| 209 | + | func (_c *MockConfigService_GetAllAttachments_Call) RunAndReturn(run func(ctx context.Context) map[string][]string) *MockConfigService_GetAllAttachments_Call { | |||
| 210 | + | _c.Call.Return(run) | |||
| 211 | + | return _c | |||
| 212 | + | } | |||
| 213 | + | ||||
| 214 | + | // GetAttachmentsFor provides a mock function for the type MockConfigService | |||
| 215 | + | func (_mock *MockConfigService) GetAttachmentsFor(ctx context.Context, domainOrGroup string) ([]string, error) { | |||
| 216 | + | ret := _mock.Called(ctx, domainOrGroup) | |||
| 217 | + | ||||
| 218 | + | if len(ret) == 0 { | |||
| 219 | + | panic("no return value specified for GetAttachmentsFor") | |||
| 220 | + | } | |||
| 221 | + | ||||
| 222 | + | var r0 []string | |||
| 223 | + | var r1 error | |||
| 224 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) ([]string, error)); ok { | |||
| 225 | + | return returnFunc(ctx, domainOrGroup) | |||
| 226 | + | } | |||
| 227 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string) []string); ok { | |||
| 228 | + | r0 = returnFunc(ctx, domainOrGroup) | |||
| 229 | + | } else { | |||
| 230 | + | if ret.Get(0) != nil { | |||
| 231 | + | r0 = ret.Get(0).([]string) | |||
| 232 | + | } | |||
| 233 | + | } | |||
| 234 | + | if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok { | |||
| 235 | + | r1 = returnFunc(ctx, domainOrGroup) | |||
| 236 | + | } else { | |||
| 237 | + | r1 = ret.Error(1) | |||
| 238 | + | } | |||
| 239 | + | return r0, r1 | |||
| 240 | + | } | |||
| 241 | + | ||||
| 242 | + | // MockConfigService_GetAttachmentsFor_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAttachmentsFor' | |||
| 243 | + | type MockConfigService_GetAttachmentsFor_Call struct { | |||
| 244 | + | *mock.Call | |||
| 245 | + | } | |||
| 246 | + | ||||
| 247 | + | // GetAttachmentsFor is a helper method to define mock.On call | |||
| 248 | + | // - ctx context.Context | |||
| 249 | + | // - domainOrGroup string | |||
| 250 | + | func (_e *MockConfigService_Expecter) GetAttachmentsFor(ctx interface{}, domainOrGroup interface{}) *MockConfigService_GetAttachmentsFor_Call { | |||
| 251 | + | return &MockConfigService_GetAttachmentsFor_Call{Call: _e.mock.On("GetAttachmentsFor", ctx, domainOrGroup)} | |||
| 252 | + | } | |||
| 253 | + | ||||
| 254 | + | func (_c *MockConfigService_GetAttachmentsFor_Call) Run(run func(ctx context.Context, domainOrGroup string)) *MockConfigService_GetAttachmentsFor_Call { | |||
| 255 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 256 | + | var arg0 context.Context | |||
| 257 | + | if args[0] != nil { | |||
| 258 | + | arg0 = args[0].(context.Context) | |||
| 259 | + | } | |||
| 260 | + | var arg1 string | |||
| 261 | + | if args[1] != nil { | |||
| 262 | + | arg1 = args[1].(string) | |||
| 263 | + | } | |||
| 264 | + | run( | |||
| 265 | + | arg0, | |||
| 266 | + | arg1, | |||
| 267 | + | ) | |||
| 268 | + | }) | |||
| 269 | + | return _c | |||
| 270 | + | } | |||
| 271 | + | ||||
| 272 | + | func (_c *MockConfigService_GetAttachmentsFor_Call) Return(strings []string, err error) *MockConfigService_GetAttachmentsFor_Call { | |||
| 273 | + | _c.Call.Return(strings, err) | |||
| 274 | + | return _c | |||
| 275 | + | } | |||
| 276 | + | ||||
| 277 | + | func (_c *MockConfigService_GetAttachmentsFor_Call) RunAndReturn(run func(ctx context.Context, domainOrGroup string) ([]string, error)) *MockConfigService_GetAttachmentsFor_Call { | |||
| 278 | + | _c.Call.Return(run) | |||
| 279 | + | return _c | |||
| 280 | + | } | |||
| 281 | + | ||||
| 98 | // GetDataDir provides a mock function for the type MockConfigService | 282 | // GetDataDir provides a mock function for the type MockConfigService | ||
| 99 | func (_mock *MockConfigService) GetDataDir() string { | 283 | func (_mock *MockConfigService) GetDataDir() string { | ||
| 100 | ret := _mock.Called() | 284 | ret := _mock.Called() | ||
| @@ -621,6 +805,69 @@ func (_c *MockConfigService_Load_Call) RunAndReturn(run func(ctx context.Context | |||||
| 621 | return _c | 805 | return _c | ||
| 622 | } | 806 | } | ||
| 623 | 807 | ||||
| 808 | + | // RemoveAttachment provides a mock function for the type MockConfigService | |||
| 809 | + | func (_mock *MockConfigService) RemoveAttachment(ctx context.Context, domainOrGroup string, image string) error { | |||
| 810 | + | ret := _mock.Called(ctx, domainOrGroup, image) | |||
| 811 | + | ||||
| 812 | + | if len(ret) == 0 { | |||
| 813 | + | panic("no return value specified for RemoveAttachment") | |||
| 814 | + | } | |||
| 815 | + | ||||
| 816 | + | var r0 error | |||
| 817 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) error); ok { | |||
| 818 | + | r0 = returnFunc(ctx, domainOrGroup, image) | |||
| 819 | + | } else { | |||
| 820 | + | r0 = ret.Error(0) | |||
| 821 | + | } | |||
| 822 | + | return r0 | |||
| 823 | + | } | |||
| 824 | + | ||||
| 825 | + | // MockConfigService_RemoveAttachment_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoveAttachment' | |||
| 826 | + | type MockConfigService_RemoveAttachment_Call struct { | |||
| 827 | + | *mock.Call | |||
| 828 | + | } | |||
| 829 | + | ||||
| 830 | + | // RemoveAttachment is a helper method to define mock.On call | |||
| 831 | + | // - ctx context.Context | |||
| 832 | + | // - domainOrGroup string | |||
| 833 | + | // - image string | |||
| 834 | + | func (_e *MockConfigService_Expecter) RemoveAttachment(ctx interface{}, domainOrGroup interface{}, image interface{}) *MockConfigService_RemoveAttachment_Call { | |||
| 835 | + | return &MockConfigService_RemoveAttachment_Call{Call: _e.mock.On("RemoveAttachment", ctx, domainOrGroup, image)} | |||
| 836 | + | } | |||
| 837 | + | ||||
| 838 | + | func (_c *MockConfigService_RemoveAttachment_Call) Run(run func(ctx context.Context, domainOrGroup string, image string)) *MockConfigService_RemoveAttachment_Call { | |||
| 839 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 840 | + | var arg0 context.Context | |||
| 841 | + | if args[0] != nil { | |||
| 842 | + | arg0 = args[0].(context.Context) | |||
| 843 | + | } | |||
| 844 | + | var arg1 string | |||
| 845 | + | if args[1] != nil { | |||
| 846 | + | arg1 = args[1].(string) | |||
| 847 | + | } | |||
| 848 | + | var arg2 string | |||
| 849 | + | if args[2] != nil { | |||
| 850 | + | arg2 = args[2].(string) | |||
| 851 | + | } | |||
| 852 | + | run( | |||
| 853 | + | arg0, | |||
| 854 | + | arg1, | |||
| 855 | + | arg2, | |||
| 856 | + | ) | |||
| 857 | + | }) | |||
| 858 | + | return _c | |||
| 859 | + | } | |||
| 860 | + | ||||
| 861 | + | func (_c *MockConfigService_RemoveAttachment_Call) Return(err error) *MockConfigService_RemoveAttachment_Call { | |||
| 862 | + | _c.Call.Return(err) | |||
| 863 | + | return _c | |||
| 864 | + | } | |||
| 865 | + | ||||
| 866 | + | func (_c *MockConfigService_RemoveAttachment_Call) RunAndReturn(run func(ctx context.Context, domainOrGroup string, image string) error) *MockConfigService_RemoveAttachment_Call { | |||
| 867 | + | _c.Call.Return(run) | |||
| 868 | + | return _c | |||
| 869 | + | } | |||
| 870 | + | ||||
| 624 | // RemoveRoute provides a mock function for the type MockConfigService | 871 | // RemoveRoute provides a mock function for the type MockConfigService | ||
| 625 | func (_mock *MockConfigService) RemoveRoute(ctx context.Context, domain1 string) error { | 872 | func (_mock *MockConfigService) RemoveRoute(ctx context.Context, domain1 string) error { | ||
| 626 | ret := _mock.Called(ctx, domain1) | 873 | ret := _mock.Called(ctx, domain1) | ||
M internal/domain/errors.go +6 -0
| @@ -62,4 +62,10 @@ var ( | |||
| 62 | 62 | ErrInvalidPath = errors.New("invalid path format") | |
| 63 | 63 | ErrInsufficientScope = errors.New("insufficient scope for operation") | |
| 64 | 64 | ErrInvalidScope = errors.New("invalid scope format") | |
| 65 | + | ||
| 66 | + | // Attachment errors | |
| 67 | + | ErrAttachmentNotFound = errors.New("attachment not found") | |
| 68 | + | ErrAttachmentExists = errors.New("attachment already exists") | |
| 69 | + | ErrAttachmentImageEmpty = errors.New("attachment image cannot be empty") | |
| 70 | + | ErrAttachmentTargetEmpty = errors.New("attachment target (domain or group) cannot be empty") | |
| 65 | 71 | ) | |
| @@ -62,4 +62,10 @@ var ( | |||||
| 62 | ErrInvalidPath = errors.New("invalid path format") | 62 | ErrInvalidPath = errors.New("invalid path format") | ||
| 63 | ErrInsufficientScope = errors.New("insufficient scope for operation") | 63 | ErrInsufficientScope = errors.New("insufficient scope for operation") | ||
| 64 | ErrInvalidScope = errors.New("invalid scope format") | 64 | ErrInvalidScope = errors.New("invalid scope format") | ||
| 65 | + | ||||
| 66 | + | // Attachment errors | |||
| 67 | + | ErrAttachmentNotFound = errors.New("attachment not found") | |||
| 68 | + | ErrAttachmentExists = errors.New("attachment already exists") | |||
| 69 | + | ErrAttachmentImageEmpty = errors.New("attachment image cannot be empty") | |||
| 70 | + | ErrAttachmentTargetEmpty = errors.New("attachment target (domain or group) cannot be empty") | |||
| 65 | ) | 71 | ) | ||
M internal/testutils/helpers.go +3 -2
| @@ -44,8 +44,9 @@ registry_port = 5000 | |||
| 44 | 44 | registry_domain = "registry.example.com" | |
| 45 | 45 | runtime = "docker" | |
| 46 | 46 | ||
| 47 | - | [registry_auth] | |
| 47 | + | [auth] | |
| 48 | 48 | enabled = true | |
| 49 | + | secrets_backend = "unsafe" | |
| 49 | 50 | username = "admin" | |
| 50 | 51 | password = "password123" | |
| 51 | 52 | ||
| @@ -72,7 +73,7 @@ level = "info"` | |||
| 72 | 73 | registry_domain = "registry.example.com" | |
| 73 | 74 | runtime = "docker" | |
| 74 | 75 | ||
| 75 | - | [registry_auth] | |
| 76 | + | [auth] | |
| 76 | 77 | enabled = false` | |
| 77 | 78 | case "invalid.toml": | |
| 78 | 79 | return `[server | |
| @@ -44,8 +44,9 @@ registry_port = 5000 | |||||
| 44 | registry_domain = "registry.example.com" | 44 | registry_domain = "registry.example.com" | ||
| 45 | runtime = "docker" | 45 | runtime = "docker" | ||
| 46 | 46 | ||||
| 47 | - | [registry_auth] | 47 | + | [auth] |
| 48 | enabled = true | 48 | enabled = true | ||
| 49 | + | secrets_backend = "unsafe" | |||
| 49 | username = "admin" | 50 | username = "admin" | ||
| 50 | password = "password123" | 51 | password = "password123" | ||
| 51 | 52 | ||||
| @@ -72,7 +73,7 @@ level = "info"` | |||||
| 72 | registry_domain = "registry.example.com" | 73 | registry_domain = "registry.example.com" | ||
| 73 | runtime = "docker" | 74 | runtime = "docker" | ||
| 74 | 75 | ||||
| 75 | - | [registry_auth] | 76 | + | [auth] |
| 76 | enabled = false` | 77 | enabled = false` | ||
| 77 | case "invalid.toml": | 78 | case "invalid.toml": | ||
| 78 | return `[server | 79 | return `[server | ||
M internal/usecase/config/service.go +156 -4
| @@ -101,9 +101,9 @@ func (s *Service) loadConfigValues() Config { | |||
| 101 | 101 | AutoRouteEnabled: s.viper.GetBool("auto_route.enabled"), | |
| 102 | 102 | NetworkIsolation: s.viper.GetBool("network_isolation.enabled"), | |
| 103 | 103 | NetworkPrefix: s.viper.GetString("network_isolation.network_prefix"), | |
| 104 | - | RegistryAuthEnabled: s.viper.GetBool("registry_auth.enabled"), | |
| 105 | - | RegistryAuthUsername: s.viper.GetString("registry_auth.username"), | |
| 106 | - | RegistryAuthPassword: s.viper.GetString("registry_auth.password"), | |
| 104 | + | RegistryAuthEnabled: s.viper.GetBool("auth.enabled"), | |
| 105 | + | RegistryAuthUsername: s.viper.GetString("auth.username"), | |
| 106 | + | RegistryAuthPassword: s.viper.GetString("auth.password"), | |
| 107 | 107 | VolumeAutoCreate: s.viper.GetBool("volumes.auto_create"), | |
| 108 | 108 | VolumePrefix: s.viper.GetString("volumes.prefix"), | |
| 109 | 109 | VolumePreserve: s.viper.GetBool("volumes.preserve"), | |
| @@ -333,8 +333,9 @@ func (s *Service) Save(ctx context.Context) error { | |||
| 333 | 333 | s.mu.Lock() | |
| 334 | 334 | defer s.mu.Unlock() | |
| 335 | 335 | ||
| 336 | - | // Update viper with current routes | |
| 336 | + | // Update viper with current config | |
| 337 | 337 | s.viper.Set("routes", s.config.Routes) | |
| 338 | + | s.viper.Set("attachments", s.config.Attachments) | |
| 338 | 339 | ||
| 339 | 340 | // Record save time to debounce file watcher events | |
| 340 | 341 | atomic.StoreInt64(&s.lastSaveTime, time.Now().UnixNano()) | |
| @@ -475,6 +476,7 @@ func (s *Service) GetNetworkGroups() map[string][]string { | |||
| 475 | 476 | } | |
| 476 | 477 | ||
| 477 | 478 | // GetAttachments returns attachment configuration. | |
| 479 | + | // Deprecated: Use GetAllAttachments instead. | |
| 478 | 480 | func (s *Service) GetAttachments() map[string][]string { | |
| 479 | 481 | s.mu.RLock() | |
| 480 | 482 | defer s.mu.RUnlock() | |
| @@ -486,6 +488,156 @@ func (s *Service) GetAttachments() map[string][]string { | |||
| 486 | 488 | return result | |
| 487 | 489 | } | |
| 488 | 490 | ||
| 491 | + | // GetAllAttachments returns all configured attachments. | |
| 492 | + | func (s *Service) GetAllAttachments(_ context.Context) map[string][]string { | |
| 493 | + | s.mu.RLock() | |
| 494 | + | defer s.mu.RUnlock() | |
| 495 | + | ||
| 496 | + | result := make(map[string][]string) | |
| 497 | + | for k, v := range s.config.Attachments { | |
| 498 | + | result[k] = append([]string{}, v...) | |
| 499 | + | } | |
| 500 | + | return result | |
| 501 | + | } | |
| 502 | + | ||
| 503 | + | // GetAttachmentsFor returns attachments for a specific domain or network group. | |
| 504 | + | func (s *Service) GetAttachmentsFor(_ context.Context, domainOrGroup string) ([]string, error) { | |
| 505 | + | s.mu.RLock() | |
| 506 | + | defer s.mu.RUnlock() | |
| 507 | + | ||
| 508 | + | images, exists := s.config.Attachments[domainOrGroup] | |
| 509 | + | if !exists { | |
| 510 | + | return nil, domain.ErrAttachmentNotFound | |
| 511 | + | } | |
| 512 | + | ||
| 513 | + | return append([]string{}, images...), nil | |
| 514 | + | } | |
| 515 | + | ||
| 516 | + | // AddAttachment adds an image to a domain/group's attachments. | |
| 517 | + | func (s *Service) AddAttachment(ctx context.Context, domainOrGroup, image string) error { | |
| 518 | + | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | |
| 519 | + | zerowrap.FieldLayer: "usecase", | |
| 520 | + | zerowrap.FieldUseCase: "AddAttachment", | |
| 521 | + | "target": domainOrGroup, | |
| 522 | + | "image": image, | |
| 523 | + | }) | |
| 524 | + | log := zerowrap.FromCtx(ctx) | |
| 525 | + | ||
| 526 | + | // Validate input | |
| 527 | + | if domainOrGroup == "" { | |
| 528 | + | return domain.ErrAttachmentTargetEmpty | |
| 529 | + | } | |
| 530 | + | if image == "" { | |
| 531 | + | return domain.ErrAttachmentImageEmpty | |
| 532 | + | } | |
| 533 | + | ||
| 534 | + | s.mu.Lock() | |
| 535 | + | ||
| 536 | + | // Initialize map if needed | |
| 537 | + | if s.config.Attachments == nil { | |
| 538 | + | s.config.Attachments = make(map[string][]string) | |
| 539 | + | } | |
| 540 | + | ||
| 541 | + | // Check if already exists | |
| 542 | + | existing := s.config.Attachments[domainOrGroup] | |
| 543 | + | for _, img := range existing { | |
| 544 | + | if img == image { | |
| 545 | + | s.mu.Unlock() | |
| 546 | + | return domain.ErrAttachmentExists | |
| 547 | + | } | |
| 548 | + | } | |
| 549 | + | ||
| 550 | + | // Store previous value for rollback | |
| 551 | + | previousImages := append([]string{}, existing...) | |
| 552 | + | hadKey := len(existing) > 0 | |
| 553 | + | ||
| 554 | + | // Add the image | |
| 555 | + | s.config.Attachments[domainOrGroup] = append(existing, image) | |
| 556 | + | s.mu.Unlock() | |
| 557 | + | ||
| 558 | + | // Persist to disk - rollback on failure | |
| 559 | + | if err := s.Save(ctx); err != nil { | |
| 560 | + | log.Warn().Err(err).Msg("failed to persist attachment to disk, rolling back") | |
| 561 | + | s.mu.Lock() | |
| 562 | + | if hadKey { | |
| 563 | + | s.config.Attachments[domainOrGroup] = previousImages | |
| 564 | + | } else { | |
| 565 | + | delete(s.config.Attachments, domainOrGroup) | |
| 566 | + | } | |
| 567 | + | s.mu.Unlock() | |
| 568 | + | return err | |
| 569 | + | } | |
| 570 | + | ||
| 571 | + | log.Info().Msg("attachment added to configuration") | |
| 572 | + | return nil | |
| 573 | + | } | |
| 574 | + | ||
| 575 | + | // RemoveAttachment removes an image from a domain/group's attachments. | |
| 576 | + | func (s *Service) RemoveAttachment(ctx context.Context, domainOrGroup, image string) error { | |
| 577 | + | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | |
| 578 | + | zerowrap.FieldLayer: "usecase", | |
| 579 | + | zerowrap.FieldUseCase: "RemoveAttachment", | |
| 580 | + | "target": domainOrGroup, | |
| 581 | + | "image": image, | |
| 582 | + | }) | |
| 583 | + | log := zerowrap.FromCtx(ctx) | |
| 584 | + | ||
| 585 | + | // Validate input | |
| 586 | + | if domainOrGroup == "" { | |
| 587 | + | return domain.ErrAttachmentTargetEmpty | |
| 588 | + | } | |
| 589 | + | if image == "" { | |
| 590 | + | return domain.ErrAttachmentImageEmpty | |
| 591 | + | } | |
| 592 | + | ||
| 593 | + | s.mu.Lock() | |
| 594 | + | ||
| 595 | + | existing, exists := s.config.Attachments[domainOrGroup] | |
| 596 | + | if !exists { | |
| 597 | + | s.mu.Unlock() | |
| 598 | + | return domain.ErrAttachmentNotFound | |
| 599 | + | } | |
| 600 | + | ||
| 601 | + | // Find and remove the image | |
| 602 | + | found := false | |
| 603 | + | newImages := make([]string, 0, len(existing)) | |
| 604 | + | for _, img := range existing { | |
| 605 | + | if img == image { | |
| 606 | + | found = true | |
| 607 | + | } else { | |
| 608 | + | newImages = append(newImages, img) | |
| 609 | + | } | |
| 610 | + | } | |
| 611 | + | ||
| 612 | + | if !found { | |
| 613 | + | s.mu.Unlock() | |
| 614 | + | return domain.ErrAttachmentNotFound | |
| 615 | + | } | |
| 616 | + | ||
| 617 | + | // Store previous value for rollback | |
| 618 | + | previousImages := existing | |
| 619 | + | ||
| 620 | + | // Update or remove the key | |
| 621 | + | if len(newImages) == 0 { | |
| 622 | + | delete(s.config.Attachments, domainOrGroup) | |
| 623 | + | } else { | |
| 624 | + | s.config.Attachments[domainOrGroup] = newImages | |
| 625 | + | } | |
| 626 | + | s.mu.Unlock() | |
| 627 | + | ||
| 628 | + | // Persist to disk - rollback on failure | |
| 629 | + | if err := s.Save(ctx); err != nil { | |
| 630 | + | log.Warn().Err(err).Msg("failed to persist attachment removal to disk, rolling back") | |
| 631 | + | s.mu.Lock() | |
| 632 | + | s.config.Attachments[domainOrGroup] = previousImages | |
| 633 | + | s.mu.Unlock() | |
| 634 | + | return err | |
| 635 | + | } | |
| 636 | + | ||
| 637 | + | log.Info().Msg("attachment removed from configuration") | |
| 638 | + | return nil | |
| 639 | + | } | |
| 640 | + | ||
| 489 | 641 | // GetExternalRoutes returns all configured external routes. | |
| 490 | 642 | func (s *Service) GetExternalRoutes() map[string]string { | |
| 491 | 643 | s.mu.RLock() | |
| @@ -101,9 +101,9 @@ func (s *Service) loadConfigValues() Config { | |||||
| 101 | AutoRouteEnabled: s.viper.GetBool("auto_route.enabled"), | 101 | AutoRouteEnabled: s.viper.GetBool("auto_route.enabled"), | ||
| 102 | NetworkIsolation: s.viper.GetBool("network_isolation.enabled"), | 102 | NetworkIsolation: s.viper.GetBool("network_isolation.enabled"), | ||
| 103 | NetworkPrefix: s.viper.GetString("network_isolation.network_prefix"), | 103 | NetworkPrefix: s.viper.GetString("network_isolation.network_prefix"), | ||
| 104 | - | RegistryAuthEnabled: s.viper.GetBool("registry_auth.enabled"), | 104 | + | RegistryAuthEnabled: s.viper.GetBool("auth.enabled"), |
| 105 | - | RegistryAuthUsername: s.viper.GetString("registry_auth.username"), | 105 | + | RegistryAuthUsername: s.viper.GetString("auth.username"), |
| 106 | - | RegistryAuthPassword: s.viper.GetString("registry_auth.password"), | 106 | + | RegistryAuthPassword: s.viper.GetString("auth.password"), |
| 107 | VolumeAutoCreate: s.viper.GetBool("volumes.auto_create"), | 107 | VolumeAutoCreate: s.viper.GetBool("volumes.auto_create"), | ||
| 108 | VolumePrefix: s.viper.GetString("volumes.prefix"), | 108 | VolumePrefix: s.viper.GetString("volumes.prefix"), | ||
| 109 | VolumePreserve: s.viper.GetBool("volumes.preserve"), | 109 | VolumePreserve: s.viper.GetBool("volumes.preserve"), | ||
| @@ -333,8 +333,9 @@ func (s *Service) Save(ctx context.Context) error { | |||||
| 333 | s.mu.Lock() | 333 | s.mu.Lock() | ||
| 334 | defer s.mu.Unlock() | 334 | defer s.mu.Unlock() | ||
| 335 | 335 | ||||
| 336 | - | // Update viper with current routes | 336 | + | // Update viper with current config |
| 337 | s.viper.Set("routes", s.config.Routes) | 337 | s.viper.Set("routes", s.config.Routes) | ||
| 338 | + | s.viper.Set("attachments", s.config.Attachments) | |||
| 338 | 339 | ||||
| 339 | // Record save time to debounce file watcher events | 340 | // Record save time to debounce file watcher events | ||
| 340 | atomic.StoreInt64(&s.lastSaveTime, time.Now().UnixNano()) | 341 | atomic.StoreInt64(&s.lastSaveTime, time.Now().UnixNano()) | ||
| @@ -475,6 +476,7 @@ func (s *Service) GetNetworkGroups() map[string][]string { | |||||
| 475 | } | 476 | } | ||
| 476 | 477 | ||||
| 477 | // GetAttachments returns attachment configuration. | 478 | // GetAttachments returns attachment configuration. | ||
| 479 | + | // Deprecated: Use GetAllAttachments instead. | |||
| 478 | func (s *Service) GetAttachments() map[string][]string { | 480 | func (s *Service) GetAttachments() map[string][]string { | ||
| 479 | s.mu.RLock() | 481 | s.mu.RLock() | ||
| 480 | defer s.mu.RUnlock() | 482 | defer s.mu.RUnlock() | ||
| @@ -486,6 +488,156 @@ func (s *Service) GetAttachments() map[string][]string { | |||||
| 486 | return result | 488 | return result | ||
| 487 | } | 489 | } | ||
| 488 | 490 | ||||
| 491 | + | // GetAllAttachments returns all configured attachments. | |||
| 492 | + | func (s *Service) GetAllAttachments(_ context.Context) map[string][]string { | |||
| 493 | + | s.mu.RLock() | |||
| 494 | + | defer s.mu.RUnlock() | |||
| 495 | + | ||||
| 496 | + | result := make(map[string][]string) | |||
| 497 | + | for k, v := range s.config.Attachments { | |||
| 498 | + | result[k] = append([]string{}, v...) | |||
| 499 | + | } | |||
| 500 | + | return result | |||
| 501 | + | } | |||
| 502 | + | ||||
| 503 | + | // GetAttachmentsFor returns attachments for a specific domain or network group. | |||
| 504 | + | func (s *Service) GetAttachmentsFor(_ context.Context, domainOrGroup string) ([]string, error) { | |||
| 505 | + | s.mu.RLock() | |||
| 506 | + | defer s.mu.RUnlock() | |||
| 507 | + | ||||
| 508 | + | images, exists := s.config.Attachments[domainOrGroup] | |||
| 509 | + | if !exists { | |||
| 510 | + | return nil, domain.ErrAttachmentNotFound | |||
| 511 | + | } | |||
| 512 | + | ||||
| 513 | + | return append([]string{}, images...), nil | |||
| 514 | + | } | |||
| 515 | + | ||||
| 516 | + | // AddAttachment adds an image to a domain/group's attachments. | |||
| 517 | + | func (s *Service) AddAttachment(ctx context.Context, domainOrGroup, image string) error { | |||
| 518 | + | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | |||
| 519 | + | zerowrap.FieldLayer: "usecase", | |||
| 520 | + | zerowrap.FieldUseCase: "AddAttachment", | |||
| 521 | + | "target": domainOrGroup, | |||
| 522 | + | "image": image, | |||
| 523 | + | }) | |||
| 524 | + | log := zerowrap.FromCtx(ctx) | |||
| 525 | + | ||||
| 526 | + | // Validate input | |||
| 527 | + | if domainOrGroup == "" { | |||
| 528 | + | return domain.ErrAttachmentTargetEmpty | |||
| 529 | + | } | |||
| 530 | + | if image == "" { | |||
| 531 | + | return domain.ErrAttachmentImageEmpty | |||
| 532 | + | } | |||
| 533 | + | ||||
| 534 | + | s.mu.Lock() | |||
| 535 | + | ||||
| 536 | + | // Initialize map if needed | |||
| 537 | + | if s.config.Attachments == nil { | |||
| 538 | + | s.config.Attachments = make(map[string][]string) | |||
| 539 | + | } | |||
| 540 | + | ||||
| 541 | + | // Check if already exists | |||
| 542 | + | existing := s.config.Attachments[domainOrGroup] | |||
| 543 | + | for _, img := range existing { | |||
| 544 | + | if img == image { | |||
| 545 | + | s.mu.Unlock() | |||
| 546 | + | return domain.ErrAttachmentExists | |||
| 547 | + | } | |||
| 548 | + | } | |||
| 549 | + | ||||
| 550 | + | // Store previous value for rollback | |||
| 551 | + | previousImages := append([]string{}, existing...) | |||
| 552 | + | hadKey := len(existing) > 0 | |||
| 553 | + | ||||
| 554 | + | // Add the image | |||
| 555 | + | s.config.Attachments[domainOrGroup] = append(existing, image) | |||
| 556 | + | s.mu.Unlock() | |||
| 557 | + | ||||
| 558 | + | // Persist to disk - rollback on failure | |||
| 559 | + | if err := s.Save(ctx); err != nil { | |||
| 560 | + | log.Warn().Err(err).Msg("failed to persist attachment to disk, rolling back") | |||
| 561 | + | s.mu.Lock() | |||
| 562 | + | if hadKey { | |||
| 563 | + | s.config.Attachments[domainOrGroup] = previousImages | |||
| 564 | + | } else { | |||
| 565 | + | delete(s.config.Attachments, domainOrGroup) | |||
| 566 | + | } | |||
| 567 | + | s.mu.Unlock() | |||
| 568 | + | return err | |||
| 569 | + | } | |||
| 570 | + | ||||
| 571 | + | log.Info().Msg("attachment added to configuration") | |||
| 572 | + | return nil | |||
| 573 | + | } | |||
| 574 | + | ||||
| 575 | + | // RemoveAttachment removes an image from a domain/group's attachments. | |||
| 576 | + | func (s *Service) RemoveAttachment(ctx context.Context, domainOrGroup, image string) error { | |||
| 577 | + | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | |||
| 578 | + | zerowrap.FieldLayer: "usecase", | |||
| 579 | + | zerowrap.FieldUseCase: "RemoveAttachment", | |||
| 580 | + | "target": domainOrGroup, | |||
| 581 | + | "image": image, | |||
| 582 | + | }) | |||
| 583 | + | log := zerowrap.FromCtx(ctx) | |||
| 584 | + | ||||
| 585 | + | // Validate input | |||
| 586 | + | if domainOrGroup == "" { | |||
| 587 | + | return domain.ErrAttachmentTargetEmpty | |||
| 588 | + | } | |||
| 589 | + | if image == "" { | |||
| 590 | + | return domain.ErrAttachmentImageEmpty | |||
| 591 | + | } | |||
| 592 | + | ||||
| 593 | + | s.mu.Lock() | |||
| 594 | + | ||||
| 595 | + | existing, exists := s.config.Attachments[domainOrGroup] | |||
| 596 | + | if !exists { | |||
| 597 | + | s.mu.Unlock() | |||
| 598 | + | return domain.ErrAttachmentNotFound | |||
| 599 | + | } | |||
| 600 | + | ||||
| 601 | + | // Find and remove the image | |||
| 602 | + | found := false | |||
| 603 | + | newImages := make([]string, 0, len(existing)) | |||
| 604 | + | for _, img := range existing { | |||
| 605 | + | if img == image { | |||
| 606 | + | found = true | |||
| 607 | + | } else { | |||
| 608 | + | newImages = append(newImages, img) | |||
| 609 | + | } | |||
| 610 | + | } | |||
| 611 | + | ||||
| 612 | + | if !found { | |||
| 613 | + | s.mu.Unlock() | |||
| 614 | + | return domain.ErrAttachmentNotFound | |||
| 615 | + | } | |||
| 616 | + | ||||
| 617 | + | // Store previous value for rollback | |||
| 618 | + | previousImages := existing | |||
| 619 | + | ||||
| 620 | + | // Update or remove the key | |||
| 621 | + | if len(newImages) == 0 { | |||
| 622 | + | delete(s.config.Attachments, domainOrGroup) | |||
| 623 | + | } else { | |||
| 624 | + | s.config.Attachments[domainOrGroup] = newImages | |||
| 625 | + | } | |||
| 626 | + | s.mu.Unlock() | |||
| 627 | + | ||||
| 628 | + | // Persist to disk - rollback on failure | |||
| 629 | + | if err := s.Save(ctx); err != nil { | |||
| 630 | + | log.Warn().Err(err).Msg("failed to persist attachment removal to disk, rolling back") | |||
| 631 | + | s.mu.Lock() | |||
| 632 | + | s.config.Attachments[domainOrGroup] = previousImages | |||
| 633 | + | s.mu.Unlock() | |||
| 634 | + | return err | |||
| 635 | + | } | |||
| 636 | + | ||||
| 637 | + | log.Info().Msg("attachment removed from configuration") | |||
| 638 | + | return nil | |||
| 639 | + | } | |||
| 640 | + | ||||
| 489 | // GetExternalRoutes returns all configured external routes. | 641 | // GetExternalRoutes returns all configured external routes. | ||
| 490 | func (s *Service) GetExternalRoutes() map[string]string { | 642 | func (s *Service) GetExternalRoutes() map[string]string { | ||
| 491 | s.mu.RLock() | 643 | s.mu.RLock() | ||
M internal/usecase/config/service_test.go +3 -3
| @@ -28,9 +28,9 @@ func TestService_Load(t *testing.T) { | |||
| 28 | 28 | v.Set("auto_route.enabled", true) | |
| 29 | 29 | v.Set("network_isolation.enabled", true) | |
| 30 | 30 | v.Set("network_isolation.network_prefix", "gordon") | |
| 31 | - | v.Set("registry_auth.enabled", true) | |
| 32 | - | v.Set("registry_auth.username", "admin") | |
| 33 | - | v.Set("registry_auth.password", "secret") | |
| 31 | + | v.Set("auth.enabled", true) | |
| 32 | + | v.Set("auth.username", "admin") | |
| 33 | + | v.Set("auth.password", "secret") | |
| 34 | 34 | v.Set("volumes.auto_create", true) | |
| 35 | 35 | v.Set("volumes.prefix", "gordon") | |
| 36 | 36 | v.Set("volumes.preserve", false) | |
| @@ -28,9 +28,9 @@ func TestService_Load(t *testing.T) { | |||||
| 28 | v.Set("auto_route.enabled", true) | 28 | v.Set("auto_route.enabled", true) | ||
| 29 | v.Set("network_isolation.enabled", true) | 29 | v.Set("network_isolation.enabled", true) | ||
| 30 | v.Set("network_isolation.network_prefix", "gordon") | 30 | v.Set("network_isolation.network_prefix", "gordon") | ||
| 31 | - | v.Set("registry_auth.enabled", true) | 31 | + | v.Set("auth.enabled", true) |
| 32 | - | v.Set("registry_auth.username", "admin") | 32 | + | v.Set("auth.username", "admin") |
| 33 | - | v.Set("registry_auth.password", "secret") | 33 | + | v.Set("auth.password", "secret") |
| 34 | v.Set("volumes.auto_create", true) | 34 | v.Set("volumes.auto_create", true) | ||
| 35 | v.Set("volumes.prefix", "gordon") | 35 | v.Set("volumes.prefix", "gordon") | ||
| 36 | v.Set("volumes.preserve", false) | 36 | v.Set("volumes.preserve", false) | ||
M wiki/examples/production.md +5 -8
| @@ -19,15 +19,12 @@ port = 8080 # Cloudflare forwards 443 → 8080 | |||
| 19 | 19 | registry_port = 5000 | |
| 20 | 20 | gordon_domain = "gordon.company.com" | |
| 21 | 21 | ||
| 22 | - | # Use pass for secrets (recommended) | |
| 23 | - | [secrets] | |
| 24 | - | backend = "pass" | |
| 25 | - | ||
| 26 | - | # Token authentication for CI/CD | |
| 27 | - | [registry_auth] | |
| 22 | + | # Authentication with pass backend (recommended) | |
| 23 | + | [auth] | |
| 28 | 24 | enabled = true | |
| 29 | 25 | type = "token" | |
| 30 | - | token_secret = "gordon/registry/token_secret" | |
| 26 | + | secrets_backend = "pass" | |
| 27 | + | token_secret = "gordon/auth/token_secret" | |
| 31 | 28 | ||
| 32 | 29 | # File-based logging with rotation | |
| 33 | 30 | [logging] | |
| @@ -96,7 +93,7 @@ pass init your-gpg-key-id | |||
| 96 | 93 | ||
| 97 | 94 | ```bash | |
| 98 | 95 | # Generate random secret | |
| 99 | - | openssl rand -base64 32 | pass insert -m gordon/registry/token_secret | |
| 96 | + | openssl rand -base64 32 | pass insert -m gordon/auth/token_secret | |
| 100 | 97 | ``` | |
| 101 | 98 | ||
| 102 | 99 | ### 3. Generate CI Token | |
| @@ -19,15 +19,12 @@ port = 8080 # Cloudflare forwards 443 → 8080 | |||||
| 19 | registry_port = 5000 | 19 | registry_port = 5000 | ||
| 20 | gordon_domain = "gordon.company.com" | 20 | gordon_domain = "gordon.company.com" | ||
| 21 | 21 | ||||
| 22 | - | # Use pass for secrets (recommended) | 22 | + | # Authentication with pass backend (recommended) |
| 23 | - | [secrets] | 23 | + | [auth] |
| 24 | - | backend = "pass" | |||
| 25 | - | ||||
| 26 | - | # Token authentication for CI/CD | |||
| 27 | - | [registry_auth] | |||
| 28 | enabled = true | 24 | enabled = true | ||
| 29 | type = "token" | 25 | type = "token" | ||
| 30 | - | token_secret = "gordon/registry/token_secret" | 26 | + | secrets_backend = "pass" |
| 27 | + | token_secret = "gordon/auth/token_secret" | |||
| 31 | 28 | ||||
| 32 | # File-based logging with rotation | 29 | # File-based logging with rotation | ||
| 33 | [logging] | 30 | [logging] | ||
| @@ -96,7 +93,7 @@ pass init your-gpg-key-id | |||||
| 96 | 93 | ||||
| 97 | ```bash | 94 | ```bash | ||
| 98 | # Generate random secret | 95 | # Generate random secret | ||
| 99 | - | openssl rand -base64 32 | pass insert -m gordon/registry/token_secret | 96 | + | openssl rand -base64 32 | pass insert -m gordon/auth/token_secret |
| 100 | ``` | 97 | ``` | ||
| 101 | 98 | ||||
| 102 | ### 3. Generate CI Token | 99 | ### 3. Generate CI Token | ||
M wiki/guides/remote-cli.md +5 -2
| @@ -206,6 +206,9 @@ These commands work with remote targeting: | |||
| 206 | 206 | | `gordon routes add <domain> <image>` | Add a route | | |
| 207 | 207 | | `gordon routes remove <domain>` | Remove a route | | |
| 208 | 208 | | `gordon routes deploy <domain>` | Deploy/redeploy a route | | |
| 209 | + | | `gordon attachments list [target]` | List all or targeted attachments | | |
| 210 | + | | `gordon attachments add <target> <image>` | Add an attachment | | |
| 211 | + | | `gordon attachments remove <target> <image>` | Remove an attachment | | |
| 209 | 212 | | `gordon secrets list <domain>` | List secrets for a domain | | |
| 210 | 213 | | `gordon secrets set <domain> KEY=value` | Set secrets | | |
| 211 | 214 | | `gordon secrets remove <domain> <key>` | Remove a secret | | |
| @@ -433,5 +436,5 @@ See [Remote Logs Requirement](#remote-logs-requirement) for details. | |||
| 433 | 436 | ## Related | |
| 434 | 437 | ||
| 435 | 438 | - [CLI Commands](/docs/cli/commands.md) | |
| 436 | - | - [Authentication](/docs/config/registry-auth.md) | |
| 437 | - | - [Admin API](/docs/config/admin.md) | |
| 439 | + | - [Authentication](/docs/config/auth.md) | |
| 440 | + | - [Attachments](/docs/config/attachments.md) | |
| @@ -206,6 +206,9 @@ These commands work with remote targeting: | |||||
| 206 | | `gordon routes add <domain> <image>` | Add a route | | 206 | | `gordon routes add <domain> <image>` | Add a route | | ||
| 207 | | `gordon routes remove <domain>` | Remove a route | | 207 | | `gordon routes remove <domain>` | Remove a route | | ||
| 208 | | `gordon routes deploy <domain>` | Deploy/redeploy a route | | 208 | | `gordon routes deploy <domain>` | Deploy/redeploy a route | | ||
| 209 | + | | `gordon attachments list [target]` | List all or targeted attachments | | |||
| 210 | + | | `gordon attachments add <target> <image>` | Add an attachment | | |||
| 211 | + | | `gordon attachments remove <target> <image>` | Remove an attachment | | |||
| 209 | | `gordon secrets list <domain>` | List secrets for a domain | | 212 | | `gordon secrets list <domain>` | List secrets for a domain | | ||
| 210 | | `gordon secrets set <domain> KEY=value` | Set secrets | | 213 | | `gordon secrets set <domain> KEY=value` | Set secrets | | ||
| 211 | | `gordon secrets remove <domain> <key>` | Remove a secret | | 214 | | `gordon secrets remove <domain> <key>` | Remove a secret | | ||
| @@ -433,5 +436,5 @@ See [Remote Logs Requirement](#remote-logs-requirement) for details. | |||||
| 433 | ## Related | 436 | ## Related | ||
| 434 | 437 | ||||
| 435 | - [CLI Commands](/docs/cli/commands.md) | 438 | - [CLI Commands](/docs/cli/commands.md) | ||
| 436 | - | - [Authentication](/docs/config/registry-auth.md) | 439 | + | - [Authentication](/docs/config/auth.md) |
| 437 | - | - [Admin API](/docs/config/admin.md) | 440 | + | - [Attachments](/docs/config/attachments.md) |
M wiki/guides/secrets-pass.md +8 -10
| @@ -86,10 +86,10 @@ This creates `~/.password-store/`. | |||
| 86 | 86 | ```bash | |
| 87 | 87 | # Registry password hash (bcrypt) | |
| 88 | 88 | # Generate with: htpasswd -nbB admin yourpassword | cut -d: -f2 | |
| 89 | - | pass insert gordon/registry/password_hash | |
| 89 | + | pass insert gordon/auth/password_hash | |
| 90 | 90 | ||
| 91 | 91 | # Token secret (random 32+ chars for JWT signing) | |
| 92 | - | pass generate gordon/registry/token_secret 32 | |
| 92 | + | pass generate gordon/auth/token_secret 32 | |
| 93 | 93 | ``` | |
| 94 | 94 | ||
| 95 | 95 | ## Gordon Configuration | |
| @@ -97,15 +97,13 @@ pass generate gordon/registry/token_secret 32 | |||
| 97 | 97 | ### Enable Pass Backend | |
| 98 | 98 | ||
| 99 | 99 | ```toml | |
| 100 | - | [secrets] | |
| 101 | - | backend = "pass" | |
| 102 | - | ||
| 103 | - | [registry_auth] | |
| 100 | + | [auth] | |
| 104 | 101 | enabled = true | |
| 105 | 102 | type = "token" | |
| 103 | + | secrets_backend = "pass" | |
| 106 | 104 | username = "admin" | |
| 107 | - | password_hash = "gordon/registry/password_hash" | |
| 108 | - | token_secret = "gordon/registry/token_secret" | |
| 105 | + | password_hash = "gordon/auth/password_hash" | |
| 106 | + | token_secret = "gordon/auth/token_secret" | |
| 109 | 107 | ``` | |
| 110 | 108 | ||
| 111 | 109 | ### Using Secrets in Environment Files | |
| @@ -126,7 +124,7 @@ Recommended directory structure in pass: | |||
| 126 | 124 | ``` | |
| 127 | 125 | ~/.password-store/ | |
| 128 | 126 | ├── gordon/ | |
| 129 | - | │ └── registry/ | |
| 127 | + | │ └── auth/ | |
| 130 | 128 | │ ├── password_hash.gpg | |
| 131 | 129 | │ └── token_secret.gpg | |
| 132 | 130 | ├── myapp/ | |
| @@ -286,6 +284,6 @@ chmod 700 ~/.password-store | |||
| 286 | 284 | ## Related | |
| 287 | 285 | ||
| 288 | 286 | - [Secrets Configuration](/docs/config/secrets.md) | |
| 289 | - | - [Registry Authentication](/docs/config/registry-auth.md) | |
| 287 | + | - [Authentication](/docs/config/auth.md) | |
| 290 | 288 | - [Running in Containers](/wiki/guides/running-in-container.md) | |
| 291 | 289 | - [Using SOPS for Secrets](./secrets-sops.md) | |
| @@ -86,10 +86,10 @@ This creates `~/.password-store/`. | |||||
| 86 | ```bash | 86 | ```bash | ||
| 87 | # Registry password hash (bcrypt) | 87 | # Registry password hash (bcrypt) | ||
| 88 | # Generate with: htpasswd -nbB admin yourpassword | cut -d: -f2 | 88 | # Generate with: htpasswd -nbB admin yourpassword | cut -d: -f2 | ||
| 89 | - | pass insert gordon/registry/password_hash | 89 | + | pass insert gordon/auth/password_hash |
| 90 | 90 | ||||
| 91 | # Token secret (random 32+ chars for JWT signing) | 91 | # Token secret (random 32+ chars for JWT signing) | ||
| 92 | - | pass generate gordon/registry/token_secret 32 | 92 | + | pass generate gordon/auth/token_secret 32 |
| 93 | ``` | 93 | ``` | ||
| 94 | 94 | ||||
| 95 | ## Gordon Configuration | 95 | ## Gordon Configuration | ||
| @@ -97,15 +97,13 @@ pass generate gordon/registry/token_secret 32 | |||||
| 97 | ### Enable Pass Backend | 97 | ### Enable Pass Backend | ||
| 98 | 98 | ||||
| 99 | ```toml | 99 | ```toml | ||
| 100 | - | [secrets] | 100 | + | [auth] |
| 101 | - | backend = "pass" | |||
| 102 | - | ||||
| 103 | - | [registry_auth] | |||
| 104 | enabled = true | 101 | enabled = true | ||
| 105 | type = "token" | 102 | type = "token" | ||
| 103 | + | secrets_backend = "pass" | |||
| 106 | username = "admin" | 104 | username = "admin" | ||
| 107 | - | password_hash = "gordon/registry/password_hash" | 105 | + | password_hash = "gordon/auth/password_hash" |
| 108 | - | token_secret = "gordon/registry/token_secret" | 106 | + | token_secret = "gordon/auth/token_secret" |
| 109 | ``` | 107 | ``` | ||
| 110 | 108 | ||||
| 111 | ### Using Secrets in Environment Files | 109 | ### Using Secrets in Environment Files | ||
| @@ -126,7 +124,7 @@ Recommended directory structure in pass: | |||||
| 126 | ``` | 124 | ``` | ||
| 127 | ~/.password-store/ | 125 | ~/.password-store/ | ||
| 128 | ├── gordon/ | 126 | ├── gordon/ | ||
| 129 | - | │ └── registry/ | 127 | + | │ └── auth/ |
| 130 | │ ├── password_hash.gpg | 128 | │ ├── password_hash.gpg | ||
| 131 | │ └── token_secret.gpg | 129 | │ └── token_secret.gpg | ||
| 132 | ├── myapp/ | 130 | ├── myapp/ | ||
| @@ -286,6 +284,6 @@ chmod 700 ~/.password-store | |||||
| 286 | ## Related | 284 | ## Related | ||
| 287 | 285 | ||||
| 288 | - [Secrets Configuration](/docs/config/secrets.md) | 286 | - [Secrets Configuration](/docs/config/secrets.md) | ||
| 289 | - | - [Registry Authentication](/docs/config/registry-auth.md) | 287 | + | - [Authentication](/docs/config/auth.md) |
| 290 | - [Running in Containers](/wiki/guides/running-in-container.md) | 288 | - [Running in Containers](/wiki/guides/running-in-container.md) | ||
| 291 | - [Using SOPS for Secrets](./secrets-sops.md) | 289 | - [Using SOPS for Secrets](./secrets-sops.md) | ||
M wiki/guides/secrets-sops.md +7 -9
| @@ -136,7 +136,7 @@ sops edit secrets.yaml | |||
| 136 | 136 | This opens your editor. Add secrets in YAML format: | |
| 137 | 137 | ||
| 138 | 138 | ```yaml | |
| 139 | - | registry: | |
| 139 | + | auth: | |
| 140 | 140 | password_hash: "$2y$10$..." | |
| 141 | 141 | token_secret: "your-random-32-char-string-here" | |
| 142 | 142 | database: | |
| @@ -156,7 +156,7 @@ cat secrets.yaml | |||
| 156 | 156 | Output shows encrypted values: | |
| 157 | 157 | ||
| 158 | 158 | ```yaml | |
| 159 | - | registry: | |
| 159 | + | auth: | |
| 160 | 160 | password_hash: ENC[AES256_GCM,data:...,iv:...,tag:...] | |
| 161 | 161 | token_secret: ENC[AES256_GCM,data:...,iv:...,tag:...] | |
| 162 | 162 | database: | |
| @@ -195,15 +195,13 @@ sops edit secrets.yaml | |||
| 195 | 195 | ### Enable SOPS Backend | |
| 196 | 196 | ||
| 197 | 197 | ```toml | |
| 198 | - | [secrets] | |
| 199 | - | backend = "sops" | |
| 200 | - | ||
| 201 | - | [registry_auth] | |
| 198 | + | [auth] | |
| 202 | 199 | enabled = true | |
| 203 | 200 | type = "token" | |
| 201 | + | secrets_backend = "sops" | |
| 204 | 202 | username = "admin" | |
| 205 | - | password_hash = "secrets.yaml:registry.password_hash" | |
| 206 | - | token_secret = "secrets.yaml:registry.token_secret" | |
| 203 | + | password_hash = "secrets.yaml:auth.password_hash" | |
| 204 | + | token_secret = "secrets.yaml:auth.token_secret" | |
| 207 | 205 | ``` | |
| 208 | 206 | ||
| 209 | 207 | The path format is `file:key.path` where: | |
| @@ -424,6 +422,6 @@ ls -la ~/.config/sops/age/keys.txt | |||
| 424 | 422 | ## Related | |
| 425 | 423 | ||
| 426 | 424 | - [Secrets Configuration](/docs/config/secrets.md) | |
| 427 | - | - [Registry Authentication](/docs/config/registry-auth.md) | |
| 425 | + | - [Authentication](/docs/config/auth.md) | |
| 428 | 426 | - [Running in Containers](/wiki/guides/running-in-container.md) | |
| 429 | 427 | - [Using Pass for Secrets](./secrets-pass.md) | |
| @@ -136,7 +136,7 @@ sops edit secrets.yaml | |||||
| 136 | This opens your editor. Add secrets in YAML format: | 136 | This opens your editor. Add secrets in YAML format: | ||
| 137 | 137 | ||||
| 138 | ```yaml | 138 | ```yaml | ||
| 139 | - | registry: | 139 | + | auth: |
| 140 | password_hash: "$2y$10$..." | 140 | password_hash: "$2y$10$..." | ||
| 141 | token_secret: "your-random-32-char-string-here" | 141 | token_secret: "your-random-32-char-string-here" | ||
| 142 | database: | 142 | database: | ||
| @@ -156,7 +156,7 @@ cat secrets.yaml | |||||
| 156 | Output shows encrypted values: | 156 | Output shows encrypted values: | ||
| 157 | 157 | ||||
| 158 | ```yaml | 158 | ```yaml | ||
| 159 | - | registry: | 159 | + | auth: |
| 160 | password_hash: ENC[AES256_GCM,data:...,iv:...,tag:...] | 160 | password_hash: ENC[AES256_GCM,data:...,iv:...,tag:...] | ||
| 161 | token_secret: ENC[AES256_GCM,data:...,iv:...,tag:...] | 161 | token_secret: ENC[AES256_GCM,data:...,iv:...,tag:...] | ||
| 162 | database: | 162 | database: | ||
| @@ -195,15 +195,13 @@ sops edit secrets.yaml | |||||
| 195 | ### Enable SOPS Backend | 195 | ### Enable SOPS Backend | ||
| 196 | 196 | ||||
| 197 | ```toml | 197 | ```toml | ||
| 198 | - | [secrets] | 198 | + | [auth] |
| 199 | - | backend = "sops" | |||
| 200 | - | ||||
| 201 | - | [registry_auth] | |||
| 202 | enabled = true | 199 | enabled = true | ||
| 203 | type = "token" | 200 | type = "token" | ||
| 201 | + | secrets_backend = "sops" | |||
| 204 | username = "admin" | 202 | username = "admin" | ||
| 205 | - | password_hash = "secrets.yaml:registry.password_hash" | 203 | + | password_hash = "secrets.yaml:auth.password_hash" |
| 206 | - | token_secret = "secrets.yaml:registry.token_secret" | 204 | + | token_secret = "secrets.yaml:auth.token_secret" |
| 207 | ``` | 205 | ``` | ||
| 208 | 206 | ||||
| 209 | The path format is `file:key.path` where: | 207 | The path format is `file:key.path` where: | ||
| @@ -424,6 +422,6 @@ ls -la ~/.config/sops/age/keys.txt | |||||
| 424 | ## Related | 422 | ## Related | ||
| 425 | 423 | ||||
| 426 | - [Secrets Configuration](/docs/config/secrets.md) | 424 | - [Secrets Configuration](/docs/config/secrets.md) | ||
| 427 | - | - [Registry Authentication](/docs/config/registry-auth.md) | 425 | + | - [Authentication](/docs/config/auth.md) |
| 428 | - [Running in Containers](/wiki/guides/running-in-container.md) | 426 | - [Running in Containers](/wiki/guides/running-in-container.md) | ||
| 429 | - [Using Pass for Secrets](./secrets-pass.md) | 427 | - [Using Pass for Secrets](./secrets-pass.md) | ||