wslc-wip 0.13.1 → 0.14.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 +4 -4
- data/README.md +50 -7
- data/lib/wip/cli.rb +27 -3
- data/lib/wip/command_builder.rb +10 -0
- data/lib/wip/compose_file.rb +217 -0
- data/lib/wip/config.rb +58 -14
- data/lib/wip/doctor.rb +25 -3
- data/lib/wip/initializer.rb +6 -49
- data/lib/wip/version.rb +1 -1
- data/lib/wip.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0c96178bbfe0be6dd82a52cc8cc7d3334688c97b981aaa7c35cfad5c0429c82b
|
|
4
|
+
data.tar.gz: f8574971724779ec668a41d9c1509aeab624222953dba34a609defd81b5c6589
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 938472e7df5f8f97e5c5160ad299a1a83a1f1026c6320a00183944e3386ca80b323104aa0a101d27ec7694f752de7b502d963e38970d32e2b4040d0388acc1bf
|
|
7
|
+
data.tar.gz: d33a1ff1ff31dce4d8d6d6c9114ce3a0fa2da8ac29a2f89757114ab351d0b1de8a331071f50ebb2456443b38ebb4b315c88c635efd12d5982eb879d267c48434
|
data/README.md
CHANGED
|
@@ -295,11 +295,53 @@ found, its version, and which compose file `wip` resolved.
|
|
|
295
295
|
- `commands:` entries with `type: run`/`type: build` aren't supported in compose mode — use your
|
|
296
296
|
compose tool's own `build`/`up --build` directly; compose owns builds for its own services.
|
|
297
297
|
|
|
298
|
+
### Compose mode (native)
|
|
299
|
+
|
|
300
|
+
Don't want to install a third-party compose-for-`wslc` tool at all? `mode: compose-native` parses
|
|
301
|
+
`compose.yml` itself and drives `wslc` directly, the same way `mode: container` +`dependencies:`
|
|
302
|
+
already does — no external binary, and `wip run` gets a real ephemeral `wslc run --rm` instead of
|
|
303
|
+
the `exec` fallback above:
|
|
304
|
+
|
|
305
|
+
```yaml
|
|
306
|
+
version: 1
|
|
307
|
+
mode: compose-native
|
|
308
|
+
|
|
309
|
+
compose:
|
|
310
|
+
service: app # required: which compose service wip run/exec/NAME target
|
|
311
|
+
file: compose.yml # optional; auto-detected next to wip.yml otherwise
|
|
312
|
+
project: myapp # optional; also names wip's own project network (defaults to the wip.yml
|
|
313
|
+
# directory's name) so services can reach each other by name
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
There's no `compose.command` here (no external binary to name), and no top-level `container:` —
|
|
317
|
+
`compose.service` already names it.
|
|
318
|
+
|
|
319
|
+
This is explicitly a stopgap for as long as `wslc` itself has no native Compose support (tracked
|
|
320
|
+
upstream in [microsoft/WSL#40948](https://github.com/microsoft/WSL/issues/40948)) and third-party
|
|
321
|
+
compose-for-`wslc` tools stay incomplete. It only understands a minimal subset of the Compose spec.
|
|
322
|
+
Within `services.<name>:`, anything outside that subset is a load-time `ConfigError` naming the
|
|
323
|
+
offending key, rather than silently ignored — but everything *outside* `services:` at the document's
|
|
324
|
+
top level (`networks:`, `volumes:`, `configs:`, `secrets:`, ...) is the one exception: it's read by
|
|
325
|
+
real Compose tools, not by `wip`, so `wip` silently ignores it rather than rejecting an otherwise
|
|
326
|
+
valid compose.yml over sections it doesn't need to look at:
|
|
327
|
+
|
|
328
|
+
- Per service: `image`, `build` (string or `{context:, dockerfile:}`; resolved relative to
|
|
329
|
+
`compose.yml`, not wherever `wip` is invoked from), `command` (shell or exec form), `environment`
|
|
330
|
+
(mapping or `KEY=VALUE` array — a mapping value must not be null; host environment pass-through
|
|
331
|
+
isn't supported), `ports`/`volumes` (short syntax only — `"host:container"` strings, not
|
|
332
|
+
long-syntax mappings), `working_dir`, `depends_on` (ordering only — a `condition:` other than
|
|
333
|
+
`service_started` is rejected, since there's no health-check support).
|
|
334
|
+
- `wip logs` takes at most one `SERVICE` (defaulting to `compose.service`) — `wslc logs`, like
|
|
335
|
+
`docker logs`, follows a single container, unlike a real compose tool's multi-service view.
|
|
336
|
+
- `sync:` behaves exactly like `mode: container`'s (falls back to the primary service's own image,
|
|
337
|
+
defaults to `sync.mode: exec`) — none of the external bridge's `sync.image`/`sync.build`
|
|
338
|
+
requirement applies, since wip itself boots every container here.
|
|
339
|
+
|
|
298
340
|
## Commands
|
|
299
341
|
|
|
300
342
|
| Command | Description |
|
|
301
343
|
|---|---|
|
|
302
|
-
| `wip init [--force]` | Write a starter `wip.yml`: `mode: compose` if a `compose.yml`/`docker-compose.yml` is found next to it, `mode: container` otherwise. Refuses to overwrite an existing `wip.yml` unless `--force` |
|
|
344
|
+
| `wip init [--force]` | Write a starter `wip.yml`: `mode: compose-native` if a `compose.yml`/`docker-compose.yml` is found next to it, `mode: container` otherwise. Refuses to overwrite an existing `wip.yml` unless `--force` |
|
|
303
345
|
| `wip version` | wip's version, plus WSLC's if it can be detected |
|
|
304
346
|
| `wip doctor` | Diagnose WSL2, interop, WSLC, config, architecture, and Git |
|
|
305
347
|
| `wip config` | Print the effective configuration (secrets masked) |
|
|
@@ -307,9 +349,9 @@ found, its version, and which compose file `wip` resolved.
|
|
|
307
349
|
| `wip up [-d] [--no-sync]` | Start the primary `dependencies:` entry (`container:` names which one) and its sidecars (creating any that are missing, on `network:` if set). `-d` runs the main container in the background; with `sync:` configured, the source is mirrored into the volume first unless `--no-sync` |
|
|
308
350
|
| `wip down` | Stop and remove the primary container and its sidecar `dependencies:` |
|
|
309
351
|
| `wip exec [--no-interactive] COMMAND...` | Run a command in the existing container |
|
|
310
|
-
| `wip run [--no-interactive] COMMAND...` | Run a command in a new `--rm` container (
|
|
352
|
+
| `wip run [--no-interactive] COMMAND...` | Run a command in a new `--rm` container (mode: compose `exec`s into `compose.service` instead — see "Compose mode" above) |
|
|
311
353
|
| `wip shell` | Open the configured shell, falling back to `bash` then `sh` |
|
|
312
|
-
| `wip logs [-f] [SERVICE...]` | Follow compose service logs (compose mode
|
|
354
|
+
| `wip logs [-f] [SERVICE...]` | Follow compose service logs (compose modes only; mode: compose-native takes at most one `SERVICE`) |
|
|
313
355
|
| `wip sync [-w] [--interval N]` | Mirror the source into the sync volume once, or keep re-syncing with `--watch` (needs `sync:`) |
|
|
314
356
|
| `wip NAME ARGS...` | Run `commands.NAME`, appending any extra arguments |
|
|
315
357
|
|
|
@@ -428,10 +470,11 @@ checklist.
|
|
|
428
470
|
|
|
429
471
|
## Not in the initial release
|
|
430
472
|
|
|
431
|
-
Full Compose compatibility isn't reimplemented in `wip` itself
|
|
432
|
-
a
|
|
433
|
-
|
|
434
|
-
|
|
473
|
+
Full Compose compatibility isn't reimplemented in `wip` itself — `mode: compose-native` only
|
|
474
|
+
covers a minimal subset (see [Compose mode (native)](#compose-mode-native)), and full parity is
|
|
475
|
+
otherwise available by delegating to a third-party compose-for-`wslc` tool (see
|
|
476
|
+
[Compose mode](#compose-mode)). A resident/daemon process, a GUI, PowerShell-specific tuning,
|
|
477
|
+
direct registry API/manifest parsing, self-update, and plugins are all unimplemented.
|
|
435
478
|
|
|
436
479
|
## Roadmap
|
|
437
480
|
|
data/lib/wip/cli.rb
CHANGED
|
@@ -48,7 +48,7 @@ module Wip
|
|
|
48
48
|
|
|
49
49
|
initializer = Initializer.new(dir: path.dirname)
|
|
50
50
|
path.write(initializer.call)
|
|
51
|
-
warn "wip: wrote #{path} (mode: #{initializer.compose? ? 'compose' : 'container'})"
|
|
51
|
+
warn "wip: wrote #{path} (mode: #{initializer.compose? ? 'compose-native' : 'container'})"
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
desc 'doctor', 'Diagnose the development environment'
|
|
@@ -82,6 +82,7 @@ module Wip
|
|
|
82
82
|
return execute(compose_bridge.up(detach: options[:detach]), interactive: tty?(!options[:detach]))
|
|
83
83
|
end
|
|
84
84
|
|
|
85
|
+
ensure_compose_images
|
|
85
86
|
ensure_network
|
|
86
87
|
sidecar_names.each { |name| ensure_dependency(name) }
|
|
87
88
|
sync_before_boot if options[:sync]
|
|
@@ -155,9 +156,22 @@ module Wip
|
|
|
155
156
|
desc 'logs [SERVICE...]', 'Follow logs from compose services (compose mode only)'
|
|
156
157
|
option :follow, type: :boolean, default: true, aliases: '-f'
|
|
157
158
|
def logs(*services)
|
|
158
|
-
|
|
159
|
+
unless load_config.compose? || load_config.compose_native?
|
|
160
|
+
raise ConfigError, '`wip logs` is only available in compose mode'
|
|
161
|
+
end
|
|
162
|
+
if load_config.compose?
|
|
163
|
+
return execute(compose_bridge.logs(services: services, follow: options[:follow]),
|
|
164
|
+
interactive: true)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# wslc has no compose-style multi-service log aggregation (see CommandBuilder#logs) —
|
|
168
|
+
# exactly one SERVICE is required, defaulting to compose.service when none is given.
|
|
169
|
+
if services.size > 1
|
|
170
|
+
raise ConfigError, '`wip logs` under mode: compose-native takes at most one SERVICE ' \
|
|
171
|
+
'(wslc logs, unlike a real compose tool, only follows one container at a time)'
|
|
172
|
+
end
|
|
159
173
|
|
|
160
|
-
execute(
|
|
174
|
+
execute(builder.logs(services.first || load_config.compose_service, follow: options[:follow]), interactive: true)
|
|
161
175
|
end
|
|
162
176
|
|
|
163
177
|
desc 'dispatch COMMAND [ARGS...]', 'Run a command defined in wip.yml'
|
|
@@ -272,6 +286,16 @@ module Wip
|
|
|
272
286
|
load_config.sync || raise(ConfigError, '`wip sync` needs a sync: block in wip.yml')
|
|
273
287
|
end
|
|
274
288
|
|
|
289
|
+
# Builds every compose-native service with a build: (instead of image:) once per
|
|
290
|
+
# `wip up` invocation, mirroring ensure_sync_image's "build once, not every tick"
|
|
291
|
+
# approach. A no-op for mode: container/compose and for build:-less services.
|
|
292
|
+
def ensure_compose_images
|
|
293
|
+
load_config.compose_build_specs.each_value do |spec|
|
|
294
|
+
extra = spec['dockerfile'] ? ['-f', spec['dockerfile']] : []
|
|
295
|
+
execute(builder.build(settings: { 'context' => spec['context'], 'tag' => spec['tag'] }, extra: extra))
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
275
299
|
# Builds sync.build's image once per invocation (not per --watch tick, which
|
|
276
300
|
# would pay Docker build-cache-lookup overhead on every mirror for no reason).
|
|
277
301
|
# A no-op unless sync.build is configured.
|
data/lib/wip/command_builder.rb
CHANGED
|
@@ -88,6 +88,16 @@ module Wip
|
|
|
88
88
|
[@wslc, 'stop', required_container]
|
|
89
89
|
end
|
|
90
90
|
|
|
91
|
+
# Only reachable under mode: compose-native — mode: compose delegates `wip logs`
|
|
92
|
+
# to the external compose command's own `logs` (ComposeBridge#logs) instead.
|
|
93
|
+
# Single container only, mirroring `wslc`/docker's own `logs`: there's no
|
|
94
|
+
# multi-service log aggregation the way a real compose tool provides.
|
|
95
|
+
def logs(name, follow: true)
|
|
96
|
+
command = [@wslc, 'logs']
|
|
97
|
+
command << '-f' if follow
|
|
98
|
+
command.push(name.to_s)
|
|
99
|
+
end
|
|
100
|
+
|
|
91
101
|
def remove
|
|
92
102
|
[@wslc, 'remove', '-f', required_container]
|
|
93
103
|
end
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
require 'pathname'
|
|
5
|
+
require 'shellwords'
|
|
6
|
+
|
|
7
|
+
module Wip
|
|
8
|
+
# Parses compose.yml/docker-compose.yml into the shape mode: compose-native
|
|
9
|
+
# needs to drive `wslc` directly, in place of an external compose-for-wslc
|
|
10
|
+
# binary (see ComposeBridge/mode: compose).
|
|
11
|
+
#
|
|
12
|
+
# This exists only because `wslc` has no native Compose support yet
|
|
13
|
+
# (tracked upstream in microsoft/WSL#40948). Delete this file — and its
|
|
14
|
+
# hooks in config.rb, cli.rb, doctor.rb and initializer.rb (see README
|
|
15
|
+
# "Compose mode (native)") — once wslc ships that support, or a
|
|
16
|
+
# compose-for-wslc tool reliably supports `run`.
|
|
17
|
+
class ComposeFile
|
|
18
|
+
Service = Struct.new(:image, :build, :command, :env, :ports, :volumes, :workdir, :depends_on, keyword_init: true)
|
|
19
|
+
|
|
20
|
+
SERVICE_KEYS = %w[image build command environment ports volumes working_dir depends_on].freeze
|
|
21
|
+
BUILD_KEYS = %w[context dockerfile].freeze
|
|
22
|
+
SUPPORTED_CONDITIONS = %w[service_started].freeze
|
|
23
|
+
|
|
24
|
+
def self.load(path)
|
|
25
|
+
raw = YAML.safe_load_file(path, aliases: true)
|
|
26
|
+
raise ConfigError, "#{path}: services: must be a mapping" unless raw.is_a?(Hash) && raw['services'].is_a?(Hash)
|
|
27
|
+
|
|
28
|
+
new(raw['services'], path: path)
|
|
29
|
+
rescue Psych::Exception => e
|
|
30
|
+
raise ConfigError, "Could not parse #{path}: #{e.message}"
|
|
31
|
+
rescue Errno::ENOENT
|
|
32
|
+
raise ConfigError, "Compose file not found: #{path}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def initialize(services, path:)
|
|
36
|
+
@path = path
|
|
37
|
+
@services = services.to_h { |name, entry| [name.to_s, build_service(name.to_s, entry)] }
|
|
38
|
+
validate_depends_on!
|
|
39
|
+
@order = topological_order
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def service_names_in_dependency_order = @order.dup
|
|
43
|
+
|
|
44
|
+
# name => {context:, dockerfile:, tag:} for every service with a build:.
|
|
45
|
+
def build_specs
|
|
46
|
+
@order.filter_map do |name|
|
|
47
|
+
build = @services.fetch(name).build
|
|
48
|
+
next unless build
|
|
49
|
+
|
|
50
|
+
[name, build.merge('tag' => image_tag(name))]
|
|
51
|
+
end.to_h
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Shaped like Config::DEPENDENCY_DEFAULTS expects: image/command/env/ports/volumes/workdir,
|
|
55
|
+
# in dependency order so callers iterating sidecars start them before their dependents.
|
|
56
|
+
def to_dependencies_hash
|
|
57
|
+
@order.to_h do |name|
|
|
58
|
+
service = @services.fetch(name)
|
|
59
|
+
[name, { 'image' => service.build ? image_tag(name) : service.image, 'command' => service.command,
|
|
60
|
+
'env' => service.env, 'ports' => service.ports, 'volumes' => service.volumes,
|
|
61
|
+
'workdir' => service.workdir }]
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def image_tag(name) = "wip-compose-#{name}:latest"
|
|
68
|
+
|
|
69
|
+
def build_service(name, entry)
|
|
70
|
+
raise ConfigError, "#{@path}: services.#{name} must be a mapping" unless entry.is_a?(Hash)
|
|
71
|
+
|
|
72
|
+
unknown = entry.keys.map(&:to_s) - SERVICE_KEYS
|
|
73
|
+
raise ConfigError, "#{@path}: services.#{name} has unsupported key(s): #{unknown.join(', ')}" if unknown.any?
|
|
74
|
+
|
|
75
|
+
image, build = image_or_build(name, entry)
|
|
76
|
+
Service.new(image: image, build: build, command: normalize_command(entry['command']),
|
|
77
|
+
env: normalize_env(name, entry['environment']),
|
|
78
|
+
ports: normalize_list(name, entry['ports'], 'ports'),
|
|
79
|
+
volumes: normalize_list(name, entry['volumes'], 'volumes'),
|
|
80
|
+
workdir: presence(entry['working_dir']), depends_on: normalize_depends_on(name, entry['depends_on']))
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def image_or_build(name, entry)
|
|
84
|
+
image = presence(entry['image'])
|
|
85
|
+
build = normalize_build(name, entry['build'])
|
|
86
|
+
raise ConfigError, "#{@path}: services.#{name} must set image or build" unless image || build
|
|
87
|
+
raise ConfigError, "#{@path}: services.#{name} must not set both image and build" if image && build
|
|
88
|
+
|
|
89
|
+
[image, build]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Compose allows both shell form ("bin/rails s") and exec form (["bin/rails", "s"]);
|
|
93
|
+
# CommandBuilder re-splits whatever's here with Shellwords, so join exec form back into
|
|
94
|
+
# one string instead of letting Array#to_s (via presence) turn it into a literal
|
|
95
|
+
# "[\"bin/rails\", \"s\"]" that Shellwords would then split wrong.
|
|
96
|
+
def normalize_command(value)
|
|
97
|
+
return presence(value) unless value.is_a?(Array)
|
|
98
|
+
|
|
99
|
+
presence(Shellwords.join(value.map(&:to_s)))
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def normalize_build(name, value)
|
|
103
|
+
return nil unless value
|
|
104
|
+
|
|
105
|
+
case value
|
|
106
|
+
when String then { 'context' => resolve_context(value) }
|
|
107
|
+
when Hash then normalize_build_mapping(name, value)
|
|
108
|
+
else raise ConfigError, "#{@path}: services.#{name}.build must be a string or mapping"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def normalize_build_mapping(name, value)
|
|
113
|
+
unknown = value.keys.map(&:to_s) - BUILD_KEYS
|
|
114
|
+
if unknown.any?
|
|
115
|
+
raise ConfigError,
|
|
116
|
+
"#{@path}: services.#{name}.build has unsupported key(s): #{unknown.join(', ')}"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
context = resolve_context(presence(value['context']) || '.')
|
|
120
|
+
dockerfile = presence(value['dockerfile'])
|
|
121
|
+
{ 'context' => context, 'dockerfile' => dockerfile && Pathname(context).join(dockerfile).to_s }.compact
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# build.context is relative to compose.yml's own directory (Compose's own rule),
|
|
125
|
+
# not wherever `wip` happens to be invoked from.
|
|
126
|
+
def resolve_context(raw) = Pathname(@path).expand_path.dirname.join(raw).to_s
|
|
127
|
+
|
|
128
|
+
def normalize_env(name, value)
|
|
129
|
+
return {} unless value
|
|
130
|
+
|
|
131
|
+
case value
|
|
132
|
+
when Hash then normalize_env_mapping(name, value)
|
|
133
|
+
when Array then normalize_env_array(name, value)
|
|
134
|
+
else raise ConfigError, "#{@path}: services.#{name}.environment must be a mapping or an array of KEY=VALUE"
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def normalize_env_mapping(name, value)
|
|
139
|
+
value.to_h do |key, val|
|
|
140
|
+
if val.nil?
|
|
141
|
+
raise ConfigError, "#{@path}: services.#{name}.environment.#{key} must have a value " \
|
|
142
|
+
'(host environment pass-through is not supported)'
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
[key.to_s, val.to_s]
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def normalize_env_array(name, value)
|
|
150
|
+
value.to_h do |item|
|
|
151
|
+
key, val = item.to_s.split('=', 2)
|
|
152
|
+
raise ConfigError, "#{@path}: services.#{name}.environment entries must be KEY=VALUE" unless val
|
|
153
|
+
|
|
154
|
+
[key, val]
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def normalize_list(name, value, key)
|
|
159
|
+
return [] unless value
|
|
160
|
+
raise ConfigError, "#{@path}: services.#{name}.#{key} must be an array" unless value.is_a?(Array)
|
|
161
|
+
if value.any?(Hash)
|
|
162
|
+
raise ConfigError, "#{@path}: services.#{name}.#{key} only supports short syntax (\"host:container\"), " \
|
|
163
|
+
'not long-syntax mappings'
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
value.map(&:to_s)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def normalize_depends_on(name, value)
|
|
170
|
+
return [] unless value
|
|
171
|
+
|
|
172
|
+
case value
|
|
173
|
+
when Array then value.map(&:to_s)
|
|
174
|
+
when Hash then value.map { |dep, opts| depends_on_entry(name, dep, opts) }
|
|
175
|
+
else raise ConfigError, "#{@path}: services.#{name}.depends_on must be an array or a mapping"
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def depends_on_entry(name, dep, opts)
|
|
180
|
+
condition = opts.is_a?(Hash) ? opts['condition'] : nil
|
|
181
|
+
if condition && !SUPPORTED_CONDITIONS.include?(condition)
|
|
182
|
+
raise ConfigError, "#{@path}: services.#{name}.depends_on.#{dep}: condition '#{condition}' is not " \
|
|
183
|
+
"supported (only #{SUPPORTED_CONDITIONS.join(', ')} — no health checks)"
|
|
184
|
+
end
|
|
185
|
+
dep.to_s
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def validate_depends_on!
|
|
189
|
+
@services.each do |name, service|
|
|
190
|
+
service.depends_on.each do |dep|
|
|
191
|
+
raise ConfigError, "#{@path}: services.#{name} depends_on unknown service '#{dep}'" unless @services.key?(dep)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def topological_order
|
|
197
|
+
visited = {}
|
|
198
|
+
visiting = {}
|
|
199
|
+
order = []
|
|
200
|
+
@services.each_key { |name| visit(name, visited, visiting, order) }
|
|
201
|
+
order
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def visit(name, visited, visiting, order)
|
|
205
|
+
return if visited[name]
|
|
206
|
+
raise ConfigError, "#{@path}: services.#{name} is part of a depends_on cycle" if visiting[name]
|
|
207
|
+
|
|
208
|
+
visiting[name] = true
|
|
209
|
+
@services.fetch(name).depends_on.each { |dep| visit(dep, visited, visiting, order) }
|
|
210
|
+
visiting.delete(name)
|
|
211
|
+
visited[name] = true
|
|
212
|
+
order << name
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def presence(value) = value.to_s.empty? ? nil : value.to_s
|
|
216
|
+
end
|
|
217
|
+
end
|
data/lib/wip/config.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'pathname'
|
|
4
|
+
|
|
3
5
|
module Wip
|
|
4
6
|
# Validated, defaulted access to a parsed wip.yml document.
|
|
5
7
|
class Config
|
|
@@ -11,7 +13,10 @@ module Wip
|
|
|
11
13
|
# Which orchestration path `up`/`down`/`sync`/etc. take. Explicit rather than
|
|
12
14
|
# inferred from a `compose:` block's presence, so a config reader doesn't have
|
|
13
15
|
# to know that rule to predict which mode wip runs in.
|
|
14
|
-
|
|
16
|
+
# compose bridges to an external compose-for-wslc binary (ComposeBridge);
|
|
17
|
+
# compose-native parses compose.yml itself and drives wslc directly (ComposeFile) —
|
|
18
|
+
# a stopgap for as long as those external tools stay incomplete (missing `run`, etc.).
|
|
19
|
+
MODES = %w[container compose compose-native].freeze
|
|
15
20
|
attr_reader :path
|
|
16
21
|
|
|
17
22
|
def initialize(raw, path = nil)
|
|
@@ -22,22 +27,37 @@ module Wip
|
|
|
22
27
|
|
|
23
28
|
def wslc_command = @raw.dig('wslc', 'command') || 'auto'
|
|
24
29
|
def commands = @raw['commands'] || {}
|
|
25
|
-
|
|
30
|
+
# Raw dependencies: block as written in wip.yml. Under compose-native mode this stays
|
|
31
|
+
# empty by construction (validate_compose! forbids combining the two) — #dependencies
|
|
32
|
+
# below is what callers actually want, since it's synthesized from compose.yml there.
|
|
33
|
+
def raw_dependencies = @raw['dependencies'] || {}
|
|
34
|
+
def dependencies = compose_native? ? parsed_compose_file.to_dependencies_hash : raw_dependencies
|
|
26
35
|
# Which dependencies: entry `up`/`down`/`exec`/`run`/`build`/`commands:` target
|
|
27
36
|
# by default — the one container wip itself considers "the app." Everything
|
|
28
37
|
# else in dependencies: is a sidecar wip only starts and stops. No default:
|
|
29
38
|
# guessing a name here (the old default was "app") either matches by luck or
|
|
30
39
|
# fails in a way that doesn't point at the real problem (a differently-named
|
|
31
40
|
# entry), so a project with any dependencies: must say which one explicitly.
|
|
32
|
-
|
|
33
|
-
|
|
41
|
+
# Under compose-native mode, container: is compose.service — compose.yml already
|
|
42
|
+
# names the service, so there's no separate container: key to set in wip.yml.
|
|
43
|
+
def container = compose_native? ? compose_service : presence(@raw['container'])
|
|
44
|
+
# Under compose-native mode, wip creates its own project network (compose.project,
|
|
45
|
+
# or the wip.yml directory's name) so services can reach each other by name — the
|
|
46
|
+
# same guarantee real Compose's per-project network gives.
|
|
47
|
+
def network = compose_native? ? (compose_project || default_compose_network) : @raw['network']
|
|
34
48
|
def mode = @raw['mode'] || 'container'
|
|
35
49
|
def compose = @raw['compose']
|
|
36
50
|
def compose? = mode == 'compose'
|
|
51
|
+
def compose_native? = mode == 'compose-native'
|
|
52
|
+
def compose_mode? = compose? || compose_native?
|
|
37
53
|
def compose_service = compose && compose['service']
|
|
38
54
|
def compose_file = compose && compose['file']
|
|
39
55
|
def compose_project = compose && compose['project']
|
|
40
56
|
def compose_command = compose && compose['command']
|
|
57
|
+
# name => {context:, dockerfile:, tag:} for compose-native services with a build:
|
|
58
|
+
# instead of an image: — empty otherwise. Consumed by `wip up` to build each one
|
|
59
|
+
# before starting it (see CommandBuilder#build).
|
|
60
|
+
def compose_build_specs = compose_native? ? parsed_compose_file.build_specs : {}
|
|
41
61
|
def sync? = !!sync
|
|
42
62
|
|
|
43
63
|
def sync
|
|
@@ -92,8 +112,10 @@ module Wip
|
|
|
92
112
|
|
|
93
113
|
def validate_mode!
|
|
94
114
|
raise ConfigError, "mode must be one of #{MODES.join(', ')}" unless MODES.include?(mode)
|
|
95
|
-
raise ConfigError,
|
|
96
|
-
|
|
115
|
+
raise ConfigError, "mode: #{mode} requires a compose: block" if compose_mode? && !@raw.key?('compose')
|
|
116
|
+
return unless @raw.key?('compose') && !compose_mode?
|
|
117
|
+
|
|
118
|
+
raise ConfigError, 'a compose: block requires mode: compose or compose-native'
|
|
97
119
|
end
|
|
98
120
|
|
|
99
121
|
# Forces SyncSettings to build (and so run its own validation, including
|
|
@@ -109,25 +131,41 @@ module Wip
|
|
|
109
131
|
end
|
|
110
132
|
|
|
111
133
|
def validate_dependencies!
|
|
112
|
-
raise ConfigError, 'dependencies must be a mapping' unless
|
|
113
|
-
# Under compose mode, populated dependencies: is already an error on its
|
|
114
|
-
# (validate_compose!, below) — this only needs to fire for the mode: container
|
|
134
|
+
raise ConfigError, 'dependencies must be a mapping' unless raw_dependencies.is_a?(Hash)
|
|
135
|
+
# Under either compose mode, populated dependencies: is already an error on its
|
|
136
|
+
# own (validate_compose!, below) — this only needs to fire for the mode: container
|
|
115
137
|
# case, where dependencies: having entries but no container: is otherwise valid.
|
|
116
|
-
if
|
|
138
|
+
if raw_dependencies.any? && !container && !compose_mode?
|
|
117
139
|
raise ConfigError,
|
|
118
140
|
'container: must be set when dependencies: has entries'
|
|
119
141
|
end
|
|
120
142
|
|
|
121
|
-
|
|
143
|
+
raw_dependencies.each { |name, entry| validate_dependency!(name, entry) }
|
|
122
144
|
end
|
|
123
145
|
|
|
124
146
|
def validate_compose!
|
|
125
147
|
return unless @raw.key?('compose')
|
|
126
148
|
raise ConfigError, 'compose must be a mapping' unless compose.is_a?(Hash)
|
|
127
149
|
raise ConfigError, 'compose.service must not be empty' if compose_service.to_s.empty?
|
|
128
|
-
raise ConfigError, 'compose
|
|
129
|
-
raise ConfigError, 'compose is mutually exclusive with
|
|
130
|
-
|
|
150
|
+
raise ConfigError, 'compose is mutually exclusive with dependencies' if raw_dependencies.any?
|
|
151
|
+
raise ConfigError, 'compose is mutually exclusive with network' if @raw['network']
|
|
152
|
+
|
|
153
|
+
validate_compose_command!
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# compose.command has opposite requiredness depending on which compose mode this
|
|
157
|
+
# is: mode: compose has no default (every external compose-for-wslc implementation
|
|
158
|
+
# must be named explicitly), while mode: compose-native drives wslc directly and so
|
|
159
|
+
# has no external binary to name at all.
|
|
160
|
+
def validate_compose_command!
|
|
161
|
+
if compose_native?
|
|
162
|
+
if compose_command
|
|
163
|
+
raise ConfigError, 'compose.command is not used under mode: compose-native (wip drives wslc ' \
|
|
164
|
+
'directly — there’s no external compose binary to name)'
|
|
165
|
+
end
|
|
166
|
+
elsif compose_command.to_s.empty?
|
|
167
|
+
raise ConfigError, 'compose.command must not be empty'
|
|
168
|
+
end
|
|
131
169
|
end
|
|
132
170
|
|
|
133
171
|
def validate_command!(name, entry)
|
|
@@ -162,5 +200,11 @@ module Wip
|
|
|
162
200
|
end
|
|
163
201
|
|
|
164
202
|
def presence(value) = value.to_s.empty? ? nil : value.to_s
|
|
203
|
+
|
|
204
|
+
def parsed_compose_file
|
|
205
|
+
@parsed_compose_file ||= ComposeFile.load(ComposeBridge.file_path(self))
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def default_compose_network = path && Pathname(path).dirname.basename.to_s
|
|
165
209
|
end
|
|
166
210
|
end
|
data/lib/wip/doctor.rb
CHANGED
|
@@ -51,9 +51,15 @@ module Wip
|
|
|
51
51
|
|
|
52
52
|
# dependencies.<container> having an empty image is already a load-time
|
|
53
53
|
# ConfigError (validate_dependency!), so the only way to reach here with a
|
|
54
|
-
# broken primary container is `container:` naming an entry that isn't defined
|
|
54
|
+
# broken primary container is `container:` naming an entry that isn't defined
|
|
55
|
+
# — or, under compose-native, compose.service naming a service compose.yml
|
|
56
|
+
# doesn't actually define.
|
|
55
57
|
def container_result(config)
|
|
56
58
|
return [:ok, 'Loaded wip.yml'] if config.compose? || config.primary
|
|
59
|
+
if config.compose_native?
|
|
60
|
+
return [:fail,
|
|
61
|
+
"compose.service '#{config.container}' has no matching service in compose.yml"]
|
|
62
|
+
end
|
|
57
63
|
|
|
58
64
|
[:fail, "No dependencies.#{config.container} entry"]
|
|
59
65
|
end
|
|
@@ -63,6 +69,7 @@ module Wip
|
|
|
63
69
|
|
|
64
70
|
check_wslc(config, results)
|
|
65
71
|
check_compose(config, results) if config.compose?
|
|
72
|
+
check_compose_native(config, results) if config.compose_native?
|
|
66
73
|
check_sync(config, results) if config.sync?
|
|
67
74
|
end
|
|
68
75
|
|
|
@@ -103,10 +110,25 @@ module Wip
|
|
|
103
110
|
nil
|
|
104
111
|
end
|
|
105
112
|
|
|
113
|
+
# Returns whether the file was found, so check_compose_native can skip
|
|
114
|
+
# attempting to parse a file that's already been reported missing.
|
|
106
115
|
def check_compose_file(config, results)
|
|
107
116
|
path = ComposeBridge.file_path(config)
|
|
108
|
-
|
|
109
|
-
|
|
117
|
+
found = path.file?
|
|
118
|
+
results << result(found ? :ok : :fail, "Found compose file #{path}", "Compose file not found: #{path}")
|
|
119
|
+
found
|
|
120
|
+
rescue ConfigError => e
|
|
121
|
+
results << Result.new(:fail, e.message)
|
|
122
|
+
false
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# mode: compose-native has no external binary to check (check_wslc, above, already
|
|
126
|
+
# covers the one binary it drives) — just that compose.yml exists and parses.
|
|
127
|
+
def check_compose_native(config, results)
|
|
128
|
+
return unless check_compose_file(config, results)
|
|
129
|
+
|
|
130
|
+
ComposeFile.load(ComposeBridge.file_path(config))
|
|
131
|
+
results << Result.new(:ok, 'Parsed compose file')
|
|
110
132
|
rescue ConfigError => e
|
|
111
133
|
results << Result.new(:fail, e.message)
|
|
112
134
|
end
|
data/lib/wip/initializer.rb
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'yaml'
|
|
4
|
-
|
|
5
3
|
module Wip
|
|
6
4
|
# Builds a starter wip.yml. Detects an existing compose file next to the
|
|
7
5
|
# target (the same filenames ComposeBridge auto-detects) to decide between
|
|
8
|
-
# mode: compose and mode: container, since that's the one decision
|
|
9
|
-
# init` can't leave to a placeholder.
|
|
6
|
+
# mode: compose-native and mode: container, since that's the one decision
|
|
7
|
+
# `wip init` can't leave to a placeholder.
|
|
10
8
|
class Initializer
|
|
11
9
|
CONTAINER_TEMPLATE = <<~YAML
|
|
12
10
|
version: 1
|
|
@@ -40,56 +38,15 @@ module Wip
|
|
|
40
38
|
def compose_template
|
|
41
39
|
<<~YAML
|
|
42
40
|
version: 1
|
|
43
|
-
mode: compose
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
mode: compose-native # wip parses #{compose_file} itself and drives wslc directly — no external
|
|
42
|
+
# compose-for-wslc binary needed. Prefer a real compose tool instead? Use
|
|
43
|
+
# mode: compose (see README "Compose mode") and set compose.command.
|
|
46
44
|
|
|
47
45
|
compose:
|
|
48
46
|
service: app # TODO: which service in #{compose_file} wip run/exec/NAME target
|
|
49
|
-
command: wslc-compose # TODO: the compose-for-wslc binary/path you have installed
|
|
50
47
|
|
|
51
|
-
|
|
48
|
+
sync: {} # optional; mirrors the source into a named volume instead of bind-mounting it live
|
|
52
49
|
YAML
|
|
53
50
|
end
|
|
54
|
-
|
|
55
|
-
# sync: needs sync.image or sync.build (mode: compose has no dependencies: entry to borrow an
|
|
56
|
-
# image from) and a named volume the compose service mounts, matching sync.volume ("app-src" by
|
|
57
|
-
# default). Checked against the detected compose file so the hint doesn't repeat what's set up.
|
|
58
|
-
def sync_hint
|
|
59
|
-
return sync_hint_configured if compose_volume_mounted?
|
|
60
|
-
|
|
61
|
-
sync_hint_todo
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def sync_hint_configured
|
|
65
|
-
<<~COMMENT.chomp
|
|
66
|
-
# #{compose_file} already mounts a volume matching sync.volume's default (app-src).
|
|
67
|
-
# sync: # add sync.build or sync.image too (one's required under mode: compose) — see README
|
|
68
|
-
COMMENT
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def sync_hint_todo
|
|
72
|
-
<<~COMMENT.chomp
|
|
73
|
-
# sync: # optional; mirrors the source into a named volume instead of bind-mounting it live
|
|
74
|
-
# build: # required under mode: compose (no dependencies: entry to borrow an image from)
|
|
75
|
-
# dockerfile: |
|
|
76
|
-
# FROM alpine:latest
|
|
77
|
-
# RUN apk add --no-cache rsync
|
|
78
|
-
# # the service above must also mount a volume named "app-src" (sync.volume's default) at the
|
|
79
|
-
# # path your app expects — add to #{compose_file}:
|
|
80
|
-
# # volumes:
|
|
81
|
-
# # - app-src:/app
|
|
82
|
-
# # and, alongside services:
|
|
83
|
-
# # volumes:
|
|
84
|
-
# # app-src:
|
|
85
|
-
COMMENT
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def compose_volume_mounted?
|
|
89
|
-
services = YAML.safe_load_file(File.join(@dir, compose_file), aliases: true)&.fetch('services', nil) || {}
|
|
90
|
-
services.values.any? { |service| Array(service['volumes']).any? { |v| v.to_s.start_with?('app-src:') } }
|
|
91
|
-
rescue StandardError
|
|
92
|
-
false
|
|
93
|
-
end
|
|
94
51
|
end
|
|
95
52
|
end
|
data/lib/wip/version.rb
CHANGED
data/lib/wip.rb
CHANGED
|
@@ -13,6 +13,7 @@ require_relative 'wip/command_resolver'
|
|
|
13
13
|
require_relative 'wip/error_interpreter'
|
|
14
14
|
require_relative 'wip/command_builder'
|
|
15
15
|
require_relative 'wip/compose_bridge'
|
|
16
|
+
require_relative 'wip/compose_file'
|
|
16
17
|
require_relative 'wip/initializer'
|
|
17
18
|
require_relative 'wip/command_display'
|
|
18
19
|
require_relative 'wip/command_runner'
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: wslc-wip
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.14.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Wip contributors
|
|
@@ -40,6 +40,7 @@ files:
|
|
|
40
40
|
- lib/wip/command_resolver.rb
|
|
41
41
|
- lib/wip/command_runner.rb
|
|
42
42
|
- lib/wip/compose_bridge.rb
|
|
43
|
+
- lib/wip/compose_file.rb
|
|
43
44
|
- lib/wip/config.rb
|
|
44
45
|
- lib/wip/config_loader.rb
|
|
45
46
|
- lib/wip/debug_reporter.rb
|