kitsune-kit 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 02a64110c8bffb9342d3cd0550360d95d871a4c1179f33899e34787abe59a080
4
- data.tar.gz: 2bf327e34d000c12ba3883573a088493f353f5265ca2b7aa4693b645ba9f3ee9
3
+ metadata.gz: '08f2d244f1d6869964e8722fe07a91e1fd84c8786d56faf3a6d92e5ffedeca03'
4
+ data.tar.gz: f8f5090aa7cb0f7bf3782c35564339dbf5891b675a96e538d99ab57a295334b9
5
5
  SHA512:
6
- metadata.gz: 23e94dae8106856bd149f8e66d47ee61c47d1ddb5ccbb3311ec8f214d0874b4cdf64b40777d7f6dd04b393be684044785e6135f23f78618237ba5e6927e317cb
7
- data.tar.gz: 59f168aa081fc7047a0e49a1107c288cdd4f2c04e0da45be46e514c89e181ee023e1f860ceb2511017e1f9885e9c2a773a6008959a9c3c78ff15e49bf75f8750
6
+ metadata.gz: 8a09c75539d4ec1cd52bd18aaac184a26491ea770044ef4e0a39ee9dd8057851c3a60944fb51b64173f23c24f36f4f79450d804b89e1ab211861089ef2b853a1
7
+ data.tar.gz: fceed8d60ab1d391ac09e9346ea383722e2407870a913a548eb73e782c29d704b19590859309be147a9f5ff794edd5e45a4a0bba007fdea5b7ae699eb09586a3
data/CHANGELOG.md CHANGED
@@ -5,6 +5,31 @@ and [Semantic Versioning](https://semver.org/); minor releases may change public
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.6.0] - 2026-08-14
9
+
10
+ ### Added
11
+
12
+ - Generated, overlay and fully custom Docker Compose modes for managed PostgreSQL and Redis.
13
+ - Local `compose show`, `validate`, `diff` and safe `eject` service commands.
14
+ - Project-bound file validation, inline-secret detection and explicit review metadata for unsafe Compose options.
15
+ - Multi-file upload, fingerprinting, state tracking, recovery and rollback for Compose customizations.
16
+ - Unit coverage for each customization mode, security boundary, ejection and remote multi-file execution.
17
+
18
+ ### Changed
19
+
20
+ - Initialized projects now declare the generated Compose mode explicitly.
21
+ - Service state records the Compose mode, ordered file set and content fingerprint.
22
+ - Kitsune Kit 0.6.0 replaces the fixed service blueprints directly; no legacy blueprint compatibility layer remains.
23
+
24
+ ### Fixed
25
+
26
+ - Keep local Compose inspection, validation, diff and ejection independent from provider and service credentials.
27
+
28
+ ### Security
29
+
30
+ - Customizations reject host namespace/device access, elevated capabilities, unmanaged ports, bind mounts, remote
31
+ builds, unmanaged env files and inline secrets unless the documented explicit unsafe escape applies.
32
+
8
33
  ## [0.5.0] - 2026-08-14
9
34
 
10
35
  ### Added
@@ -111,7 +136,8 @@ and [Semantic Versioning](https://semver.org/); minor releases may change public
111
136
  - First public preview.
112
137
  - Bootstrap commands for DigitalOcean, Docker and PostgreSQL.
113
138
 
114
- [Unreleased]: https://github.com/omarhrra/kitsune-kit/compare/v0.5.0...HEAD
139
+ [Unreleased]: https://github.com/omarhrra/kitsune-kit/compare/v0.6.0...HEAD
140
+ [0.6.0]: https://github.com/omarhrra/kitsune-kit/compare/v0.5.0...v0.6.0
115
141
  [0.5.0]: https://github.com/omarhrra/kitsune-kit/compare/v0.4.1...v0.5.0
116
142
  [0.4.1]: https://github.com/omarhrra/kitsune-kit/compare/v0.4.0...v0.4.1
117
143
  [0.4.0]: https://github.com/omarhrra/kitsune-kit/compare/v0.3.0...v0.4.0
data/README.md CHANGED
@@ -2,14 +2,14 @@
2
2
 
3
3
  Kitsune Kit prepares Ubuntu servers for Docker and Kamal deployments through a predictable, inspectable CLI.
4
4
 
5
- > Status: `0.5.0` (pre-1.0). DigitalOcean and Ubuntu 22.04/24.04 LTS are supported. The command, configuration and state schemas may still change before 1.0.
5
+ > Status: `0.6.0` (pre-1.0). DigitalOcean and Ubuntu 22.04/24.04 LTS are supported. The command, configuration and state schemas may still change before 1.0.
6
6
 
7
7
  ## What it does
8
8
 
9
9
  - Provisions an explicitly tagged DigitalOcean Droplet.
10
10
  - Configures a deploy user, verified SSH policy, UFW, swap and unattended security updates.
11
11
  - Installs Docker Engine and Docker Compose from Docker's official Ubuntu repository.
12
- - Optionally installs private PostgreSQL/Redis services or represents provider-managed external endpoints.
12
+ - Optionally installs private PostgreSQL/Redis services, with generated, overlay or fully custom Docker Compose.
13
13
  - Creates exact DNS records without adopting unrelated resources.
14
14
  - Shows a plan before changing infrastructure and records managed state for resume and rollback.
15
15
  - Offers both a conventional CLI and an optional full-screen TUI over the same workflows.
@@ -32,7 +32,7 @@ gem install kitsune-kit
32
32
  Or add it to a project:
33
33
 
34
34
  ```ruby
35
- gem "kitsune-kit", "~> 0.5.0"
35
+ gem "kitsune-kit", "~> 0.6.0"
36
36
  ```
37
37
 
38
38
  ## Safe quick start
@@ -72,6 +72,7 @@ See [Getting started](docs/getting-started.md) for the complete first-run proced
72
72
  | `kit rollback` | Restore managed configuration; preserve server and service data | Yes |
73
73
  | `kit server ACTION` | Show, create, configure, connect to or destroy the server | Depends |
74
74
  | `kit service TYPE ACTION` | Manage PostgreSQL or Redis | Depends |
75
+ | `kit service TYPE compose ACTION` | Show, validate, diff or eject Compose | Local |
75
76
  | `kit dns ACTION` | List, plan, apply or remove configured records | Depends |
76
77
  | `kit docker ACTION` | Inspect, install or uninstall Docker | Depends |
77
78
  | `kit env ACTION [NAME]` | List, read or select environments | Local |
@@ -118,6 +119,9 @@ kit service postgres install
118
119
 
119
120
  See [PostgreSQL](docs/services/postgres.md) and [Redis](docs/services/redis.md).
120
121
 
122
+ Compose remains generated and secure by default. Use an overlay for ordinary Docker options or eject a complete,
123
+ editable file when the generated model is no longer sufficient. See [Compose customization](docs/services/compose.md).
124
+
121
125
  ## CLI and optional TUI
122
126
 
123
127
  Kitsune Kit is fully usable without the TUI. Every infrastructure action available in the full-screen interface invokes the same domain workflow and has a conventional command equivalent. Scripts and CI should use subcommands, `--no-input`, and optionally `--format json`.
@@ -151,6 +155,7 @@ Docker-backed integration tests and credential-gated DigitalOcean E2E tests are
151
155
  - [Getting started](docs/getting-started.md)
152
156
  - [Configuration](docs/configuration.md)
153
157
  - [Command reference](docs/commands.md)
158
+ - [Compose customization](docs/services/compose.md)
154
159
  - [Security](docs/security.md)
155
160
  - [Manual security audit](docs/security-audit.md)
156
161
  - [Troubleshooting](docs/troubleshooting.md)
@@ -0,0 +1,30 @@
1
+ # ADR 0007: Managed Compose customization boundary
2
+
3
+ - Status: accepted
4
+ - Date: 2026-08-14
5
+
6
+ ## Context
7
+
8
+ The fixed PostgreSQL and Redis generators are safe and convenient, but cannot express every operational Docker
9
+ setting. Making arbitrary YAML fragments part of the core configuration would duplicate the Compose schema and
10
+ produce a weaker, less familiar interface.
11
+
12
+ ## Decision
13
+
14
+ Managed services support three explicit modes: deterministic generated output, a generated base plus one project
15
+ overlay, and one complete project-owned custom document. All modes pass through a single renderer, fingerprint,
16
+ remote validation, state and recovery path.
17
+
18
+ Customization files remain within the project and are subject to size, type, secret and host-boundary validation.
19
+ Security-sensitive Compose settings fail closed unless `allow_unsafe` records an explicit operator decision.
20
+ Inline secrets always fail. The `eject` command creates a complete custom starting point and a configuration
21
+ backup instead of preserving compatibility with the earlier fixed blueprint internals.
22
+
23
+ ## Consequences
24
+
25
+ - Users can express ordinary Compose settings without Kitsune Kit reimplementing the Compose schema.
26
+ - Generated mode remains the default and safest supported path.
27
+ - Custom mode transfers responsibility for image, health, network and volume semantics to the project.
28
+ - Kitsune Kit tracks and restores the ordered document set, but does not claim ownership of arbitrary host
29
+ resources introduced through the unsafe escape hatch.
30
+ - Docker's server-side `compose config` remains authoritative for Compose-version compatibility.
data/docs/architecture.md CHANGED
@@ -68,7 +68,10 @@ Safety-critical SSH/firewall operations execute inside one preserved authenticat
68
68
 
69
69
  ## Services
70
70
 
71
- `EnsureService` composes smaller collaborators for generated Compose YAML, file transaction/backups, firewall reconciliation, data backup and state transitions. Data lifecycle is deliberately separate from container/config lifecycle.
71
+ `EnsureService` composes smaller collaborators for generated, overlay or custom Compose YAML, file
72
+ transactions/backups, firewall reconciliation, data backup and state transitions. The ordered Compose file set is
73
+ fingerprinted and persisted so recovery can restart the prior definition. Data lifecycle is deliberately separate
74
+ from container/config lifecycle.
72
75
 
73
76
  An enabled service in `external` mode is configuration metadata, not an operation. It is visible through the
74
77
  CLI but excluded from VPS plans, Docker, firewall, doctor port checks and destructive lifecycle methods.
@@ -87,3 +90,4 @@ Detailed accepted decisions are in [architecture/decisions](architecture/decisio
87
90
  - supported platforms;
88
91
  - operation semantics;
89
92
  - optional TUI backend.
93
+ - managed Compose customization boundary.
data/docs/commands.md CHANGED
@@ -101,6 +101,10 @@ kit service postgres install
101
101
  kit service postgres backup
102
102
  kit service postgres remove --yes
103
103
  kit service postgres destroy-data --backup-before-destroy --confirm-destroy postgres@ENV
104
+ kit service postgres compose show
105
+ kit service postgres compose validate
106
+ kit service postgres compose diff
107
+ kit service postgres compose eject
104
108
  ```
105
109
 
106
110
  Replace `postgres` with `redis` for Redis.
@@ -112,11 +116,21 @@ Replace `postgres` with `redis` for Redis.
112
116
  configuration backups/files and state. In an interactive terminal it offers the backup and requires typing
113
117
  `TYPE@ENV`; automation can request it with `--backup-before-destroy` and must supply `--confirm-destroy`.
114
118
  The data archive is retained outside the service directory, but must be copied to independent storage.
119
+ - `compose show`: print the exact generated/custom documents that would be uploaded, without resolving secrets.
120
+ - `compose validate`: parse the configured YAML and run Kitsune Kit's local structure and security checks.
121
+ - `compose diff`: compare the desired Compose fingerprint with the last applied state. An older state without a
122
+ Compose fingerprint is reported as not installed/unknown rather than guessed.
123
+ - `compose eject`: write `.kitsune/compose/TYPE.yml`, back up `config.yml` as `config.yml.backup`, and switch the
124
+ service to `custom`. It refuses existing targets unless `--force` is explicit.
115
125
 
116
126
  When an enabled service has `mode: external`, `status` returns only its configured host/port and
117
127
  `managed: false`. All local lifecycle actions fail safely; Kitsune Kit never treats an external provider's data as
118
128
  a VPS Docker volume.
119
129
 
130
+ `install` and `apply` additionally run `docker compose config --quiet` on the server before starting containers.
131
+ For overlay mode every `--file` is passed in deterministic base-then-overlay order. See
132
+ [Compose customization](services/compose.md).
133
+
120
134
  ## DNS
121
135
 
122
136
  ```text
@@ -66,6 +66,10 @@ services:
66
66
  allowed_cidrs: []
67
67
  port: 5432
68
68
  password_env: POSTGRES_PASSWORD
69
+ compose:
70
+ mode: generated
71
+ file:
72
+ allow_unsafe: false
69
73
  redis:
70
74
  enabled: false
71
75
  mode: managed
@@ -76,6 +80,10 @@ services:
76
80
  allowed_cidrs: []
77
81
  port: 6379
78
82
  password_env: REDIS_PASSWORD
83
+ compose:
84
+ mode: generated
85
+ file:
86
+ allow_unsafe: false
79
87
 
80
88
  system:
81
89
  swap_size_gb: 2
@@ -135,6 +143,9 @@ SSH policy is ordered to keep a verified path open: create/verify the deploy use
135
143
  | `allowed_cidrs` | Required and non-empty when `publish` is true. |
136
144
  | `port` | Host port, 1–65535. Container ports remain 5432/6379. |
137
145
  | `password_env` | Environment-variable name containing the required secret. |
146
+ | `compose.mode` | `generated`, `overlay`, or `custom`; only valid for managed services. |
147
+ | `compose.file` | Project-local YAML file required by `overlay` and `custom`; empty for `generated`. |
148
+ | `compose.allow_unsafe` | Explicit opt-in for security-sensitive Compose options. Defaults to false. |
138
149
 
139
150
  Images, binds, ports and secret names reject newline/shell/YAML injection patterns before any SSH connection is opened.
140
151
 
@@ -155,6 +166,11 @@ status` reports the endpoint without its secret. Install, backup, remove and des
155
166
  availability, TLS, backups and destruction with the external provider. External services never add Docker,
156
167
  firewall or private-port checks to the VPS plan.
157
168
 
169
+ Compose files must be regular, non-symlink files inside the project root and no larger than 256 KiB. `overlay`
170
+ combines the generated base with the configured file; `custom` replaces the generated document and must define
171
+ `services.postgres` or `services.redis` respectively. Inline secrets are rejected. See
172
+ [Compose customization](services/compose.md) for examples, validation rules and commands.
173
+
158
174
  ### `system`
159
175
 
160
176
  | Field | Validation |
@@ -0,0 +1,164 @@
1
+ # Compose customization
2
+
3
+ Kitsune Kit can manage PostgreSQL and Redis with a generated Compose document, a user overlay, or a complete
4
+ custom document. All three modes use the same `plan`, `apply`, state, recovery, rollback and data-safety workflow.
5
+ The TUI is optional; these features are fully available through ordinary CLI commands.
6
+
7
+ ## Choosing a mode
8
+
9
+ | Mode | Kitsune Kit generates the base | Your file | Best use |
10
+ | --- | --- | --- | --- |
11
+ | `generated` | Yes | None | Secure defaults with image, port and password settings controlled by `config.yml`. |
12
+ | `overlay` | Yes | Compose override | Add labels, resource limits, logging, environment options or other supported overrides. |
13
+ | `custom` | No | Complete Compose document | Take direct ownership of the service definition while retaining Kitsune Kit lifecycle management. |
14
+
15
+ Start with `generated`. Prefer `overlay` while the base service, volume, network and health check still fit. Use
16
+ `custom` when you need to replace those assumptions. There is no legacy-blueprint compatibility layer.
17
+
18
+ ## Generated mode
19
+
20
+ ```yaml
21
+ services:
22
+ postgres:
23
+ compose:
24
+ mode: generated
25
+ file:
26
+ allow_unsafe: false
27
+ ```
28
+
29
+ The generated document is deterministic. Passwords appear only as `${POSTGRES_PASSWORD}` or
30
+ `${REDIS_PASSWORD}` references; their values are uploaded separately in a mode-`0600` `.env` file.
31
+
32
+ ## Overlay mode
33
+
34
+ Create a project file such as `.kitsune/compose/postgres.override.yml`:
35
+
36
+ ```yaml
37
+ services:
38
+ postgres:
39
+ environment:
40
+ LOG_STATEMENT: ddl
41
+ logging:
42
+ options:
43
+ max-size: 20m
44
+ max-file: "5"
45
+ deploy:
46
+ resources:
47
+ limits:
48
+ memory: 1G
49
+ ```
50
+
51
+ Reference it from configuration:
52
+
53
+ ```yaml
54
+ services:
55
+ postgres:
56
+ compose:
57
+ mode: overlay
58
+ file: .kitsune/compose/postgres.override.yml
59
+ allow_unsafe: false
60
+ ```
61
+
62
+ Kitsune Kit uploads `compose.yml` followed by `compose.override.yml` and supplies both to every Docker Compose
63
+ command in that order. Changes to either document change the plan fingerprint.
64
+
65
+ ## Custom mode and eject
66
+
67
+ Create a complete starting document automatically:
68
+
69
+ ```bash
70
+ kit service postgres compose eject
71
+ ```
72
+
73
+ This operation:
74
+
75
+ 1. writes `.kitsune/compose/postgres.yml` from the current generated definition;
76
+ 2. copies `.kitsune/config.yml` to `.kitsune/config.yml.backup`;
77
+ 3. changes the service to `compose.mode: custom` and references the new file.
78
+
79
+ Existing output or backup files are not overwritten. Review both and use `--force` only when replacement is
80
+ intentional. YAML comments in `config.yml` may be reformatted by ejection; the byte-for-byte backup is retained.
81
+
82
+ A custom PostgreSQL file must contain `services.postgres`; Redis must contain `services.redis`. Configuration
83
+ fields such as `image` no longer alter a custom document, but `password_env`, publication/firewall policy and
84
+ lifecycle state remain part of Kitsune Kit's safety model.
85
+
86
+ ## Inspecting and validating
87
+
88
+ ```bash
89
+ kit service postgres compose show
90
+ kit service postgres compose validate
91
+ kit service postgres compose diff
92
+ kit service postgres compose show --format json
93
+ ```
94
+
95
+ - `show` displays the exact ordered documents and their sources.
96
+ - `validate` parses YAML, validates the required service and applies local security policy.
97
+ - `diff` compares desired content with the last successfully applied Compose fingerprint.
98
+ - `plan` includes mode, ordered files, findings and the overall service fingerprint.
99
+ - `apply` uploads restricted files and runs `docker compose config --quiet` remotely before `up`.
100
+
101
+ Local validation does not replace Docker's own validation. A customization can pass local checks but fail because
102
+ an option is unsupported by the Docker Compose version on the VPS; `apply` then recovers the previous managed
103
+ files and attempts to restart the previous file set.
104
+
105
+ ## File boundary
106
+
107
+ Customization files must:
108
+
109
+ - be inside `--root` after path expansion and resolution;
110
+ - be regular files, not symlinks;
111
+ - be at most 256 KiB;
112
+ - contain a top-level YAML mapping;
113
+ - avoid YAML aliases.
114
+
115
+ These constraints keep the deployment input reviewable and prevent a project configuration from reading an
116
+ arbitrary local file.
117
+
118
+ ## Secrets
119
+
120
+ Never put a secret value in Compose YAML. Sensitive mapping keys and `KEY=value` environment-list entries are
121
+ rejected unless the value is a `${VARIABLE_NAME}` reference.
122
+
123
+ ```yaml
124
+ services:
125
+ postgres:
126
+ environment:
127
+ POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
128
+ ```
129
+
130
+ Export the variable before `doctor`, `plan`, `apply` or `install`. Compose display/JSON output never resolves its
131
+ value. The remote `.env`, Compose documents and backups use restricted permissions.
132
+
133
+ ## Unsafe options
134
+
135
+ The local policy flags options that cross Kitsune Kit's normal isolation boundary, including:
136
+
137
+ - privileged containers and host PID/IPC/network namespaces;
138
+ - `ALL`, `SYS_ADMIN`, `SYS_PTRACE` or `NET_ADMIN` capabilities;
139
+ - host devices, absolute bind mounts and the Docker socket;
140
+ - ports outside the exact configured bind/port/firewall model;
141
+ - remote `build` contexts and unmanaged `env_file` references.
142
+
143
+ By default, any finding stops `show`, `validate`, `plan` and `apply`. If the configuration is intentionally outside
144
+ the standard boundary, review every finding and set:
145
+
146
+ ```yaml
147
+ compose:
148
+ mode: custom
149
+ file: .kitsune/compose/postgres.yml
150
+ allow_unsafe: true
151
+ ```
152
+
153
+ This is an escape hatch, not a promise that Kitsune Kit manages or reverses the extra host resources. Findings
154
+ remain visible in metadata and plan details. Inline secrets are always rejected and cannot be enabled by
155
+ `allow_unsafe`.
156
+
157
+ ## Rollback behavior
158
+
159
+ State records the mode, ordered filenames and content fingerprint after a verified apply. Before an update,
160
+ Kitsune Kit backs up all currently tracked and desired Compose files plus `.env`. A failed apply removes the new
161
+ file set, restores the backup and starts the prior ordered file set. `kit rollback` uses the same recorded set.
162
+
163
+ Docker volumes remain governed by the existing service contract: `remove` preserves data and `destroy-data`
164
+ requires exact confirmation. Compose customization does not broaden deletion ownership.
@@ -205,17 +205,24 @@ module Kitsune
205
205
  end
206
206
  end
207
207
 
208
- desc "service TYPE ACTION", "Manage postgres or redis: status, install, backup, remove, destroy-data"
208
+ desc "service TYPE ACTION [SUBACTION]",
209
+ "Manage postgres or redis, including Compose show, validate, diff and eject"
209
210
  option :confirm_destroy, type: :string, desc: "Exact TYPE@ENV value required for data destruction"
210
211
  option :backup_before_destroy, type: :boolean, default: false,
211
212
  desc: "Create a restricted remote data backup before destroy-data"
212
- def service(type, action)
213
+ option :force, type: :boolean, default: false, desc: "Replace an existing ejected Compose file and backup"
214
+ def service(type, action, subaction = nil)
213
215
  execute do
214
- app, reporter = build_application
215
- operation = find_service_operation(app, type, action)
216
- result = dispatch_service_action(app, operation, type, action)
216
+ app, reporter = action == "compose" ? build_local_application : build_application
217
+ result = if action == "compose"
218
+ dispatch_service_compose(app, type, subaction)
219
+ else
220
+ operation = find_service_operation(app, type, action)
221
+ dispatch_service_action(app, operation, type, action)
222
+ end
217
223
  if json? && result.is_a?(Result)
218
- flush_json(reporter, result, "service.#{type}.#{action}", app.config.environment)
224
+ command = ["service", type, action, subaction].compact.join(".")
225
+ flush_json(reporter, result, command, app.config.environment)
219
226
  end
220
227
  0
221
228
  end
@@ -345,6 +352,21 @@ module Kitsune
345
352
  [app, reporter]
346
353
  end
347
354
 
355
+ def build_local_application
356
+ filter = @secret_filter = build_secret_filter
357
+ bus = Events::Bus.new
358
+ reporter = build_reporter(filter)
359
+ bus.subscribe(reporter)
360
+ subscribe_logger(bus, filter)
361
+ config = Configuration::Loader.new(
362
+ root: options[:root], env: ENV, config_path: options[:config], validate_secrets: false
363
+ ).load(environment: options[:env])
364
+ app = Application.new(config: config, provider: nil, state_store: StateStore.new(root: options[:root]),
365
+ event_bus: bus)
366
+ register_configured_secrets(filter, config)
367
+ [app, reporter]
368
+ end
369
+
348
370
  def build_tui
349
371
  unless $stdin.tty? && $stdout.tty?
350
372
  raise Errors::ConfigurationError.new(
@@ -475,6 +497,61 @@ module Kitsune
475
497
  end
476
498
  end
477
499
 
500
+ def dispatch_service_compose(app, type, action)
501
+ raise Errors::ConfigurationError, "unknown service type: #{type}" unless %w[postgres redis].include?(type)
502
+
503
+ service = app.config.services.public_send(type)
504
+ if service.mode == "external"
505
+ raise Errors::UnsafeOperationError, "Compose is not managed for an external #{type} service"
506
+ end
507
+
508
+ compose = ServiceCompose.new(config: app.config, type: type, service: service)
509
+ case action
510
+ when "show" then compose_show(compose)
511
+ when "validate" then compose_validate(type, compose)
512
+ when "diff" then compose_diff(app, type, compose)
513
+ when "eject" then compose_eject(app, type)
514
+ else
515
+ raise Errors::ConfigurationError,
516
+ "unknown Compose action: #{action || '(missing)'}; expected show, validate, diff or eject"
517
+ end
518
+ end
519
+
520
+ def compose_show(compose)
521
+ puts compose.display unless json?
522
+ Result.success(compose.metadata.merge(content: compose.display, fingerprint: compose.fingerprint))
523
+ end
524
+
525
+ def compose_validate(type, compose)
526
+ puts "#{type} Compose customization is valid (#{compose.mode})." unless json?
527
+ Result.success(compose.metadata.merge(valid: true, fingerprint: compose.fingerprint))
528
+ end
529
+
530
+ def compose_eject(app, type)
531
+ value = Workflows::EjectCompose.new(
532
+ root: options[:root], config: app.config, type: type, config_path: options[:config]
533
+ ).call(force: options[:force])
534
+ puts "Created #{value[:compose_file]} and updated #{value[:config_file]}." unless json?
535
+ Result.success(value)
536
+ end
537
+
538
+ def compose_diff(app, type, compose)
539
+ state = app.state_store.read(app.config.environment).dig("resources", "service.#{type}")
540
+ installed = state&.fetch("compose_fingerprint", nil)
541
+ changed = installed != compose.fingerprint
542
+ value = { changed: changed, desired_fingerprint: compose.fingerprint,
543
+ installed_fingerprint: installed, mode: compose.mode }
544
+ unless json?
545
+ label = if installed
546
+ changed ? "changed" : "unchanged"
547
+ else
548
+ "not installed or from an older state"
549
+ end
550
+ puts "#{type} Compose: #{label}"
551
+ end
552
+ Result.success(value)
553
+ end
554
+
478
555
  def destroy_service_data(app, operation, type)
479
556
  expected = "#{type}@#{app.config.environment}"
480
557
  state = app.state_store.read(app.config.environment).dig("resources", "service.#{type}")