wslc-wip 0.9.0 → 0.11.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: d16349112264f413d1492541731ec704d56616ccb4b05cef1f505d667a5d823d
4
- data.tar.gz: 05f7f5819345079aa82e062ee368a3255f93bdeaa62cf55fda2621c2bd36c4ed
3
+ metadata.gz: a32d5a9003a7197fd47fa4b81b7ca764b495ea7955ccc523cda29330f0e4e5b2
4
+ data.tar.gz: 9d47155b5a53497dc9e5ad16cbd48f402ee0ea9d3865cfd932f577d6a1f4a746
5
5
  SHA512:
6
- metadata.gz: cdbeea98b7472f9ec6e1ccec8883bf136dfcb76164c2ba2d2ddbd63fe52f22c20f72a994a2a85527b1d9e7a7511016fe1b54ab64e678aca009530616887435f1
7
- data.tar.gz: a6f524f41b20fe20af6262e3e22e2dafa924d37e31279ff21ed94268824587017ad94670084ae81aeb79513369a923d37011fd95bd9471a1e357551b90701769
6
+ metadata.gz: 167b4aa726f99b28e2ce78087c07e10c122382a82e61db43820f43f7a28ca3bb8cfc24ab4aabb27d2577018f586257aec4a8bd3c18ae13dfcabc70be53ce2108
7
+ data.tar.gz: c4c9c0a6766fa93fcf5b40365d22526b40ff7d9cf22615484f030d5043b4b851878e9e36cfc70e1402943296592ed69074b1b813ea372332e4a3a2002cc13bc9
data/README.md CHANGED
@@ -45,27 +45,28 @@ Put a `wip.yml` in your project root. Running from a subdirectory walks up to fi
45
45
 
46
46
  ```yaml
47
47
  version: 1
48
- mode: container # or compose; picks the orchestration path `up`/`down`/`sync`/etc. take (default: container)
48
+ mode: container # default. This example is container mode end-to-end see "Compose mode" below
49
+ # for mode: compose, which replaces container:/network:/dependencies: with a
50
+ # compose: block instead; the two don't mix within one wip.yml.
49
51
  wslc:
50
52
  command: auto # tries wslc.exe, wslc, then System32; an absolute path also works
51
- defaults:
52
- container: app
53
- image: slidict/slidict:development
54
- workdir: /app
55
- interactive: false
56
- remove: true
57
- network: app-tier # optional; shared with `dependencies` so containers can resolve each other by name
58
- env:
59
- RAILS_ENV: development
60
- PORT: "3000"
61
- ports:
62
- - "3000:3000"
63
- volumes:
64
- - ".:/app"
65
- up:
66
- command: server # command `wip up` passes to the image when it creates the container
67
- # (omit to use the image's default CMD)
53
+ container: app # optional; which dependencies: entry `up`/`exec`/`run`/`build`/`commands:` target (default: app)
54
+ network: app-tier # optional; shared by every dependencies: entry so containers can resolve each other by name
68
55
  dependencies:
56
+ app: # container: points here — the one container wip execs into and runs commands in
57
+ image: slidict/slidict:development
58
+ workdir: /app
59
+ interactive: false
60
+ remove: true
61
+ command: server # extra args appended when `wip up` creates the container
62
+ # (omit to use the image's default CMD)
63
+ env:
64
+ RAILS_ENV: development
65
+ PORT: "3000"
66
+ ports:
67
+ - "3000:3000"
68
+ volumes:
69
+ - ".:/app"
69
70
  redis:
70
71
  image: redis:latest
71
72
  development.mysql:
@@ -115,9 +116,9 @@ instead.
115
116
  Like `docker compose`, `wip` automatically loads a `.env` file next to `wip.yml` (one `KEY=VALUE`
116
117
  per line; `#` comments, blank lines, `export` prefixes, and quoted values are all supported) and
117
118
  passes its keys through as container environment variables on `build`, `up`, `run`, `exec`, and
118
- custom commands. `.env` only fills in keys that aren't already set by `defaults.env` or a
119
- command/dependency's own `env` — those always win on conflict. Pass `--env-file PATH` to load a
120
- different file instead.
119
+ custom commands. `.env` only fills in keys that aren't already set by the primary container's
120
+ `env` or a command/dependency's own `env` — those always win on conflict. Pass `--env-file PATH`
121
+ to load a different file instead.
121
122
 
122
123
  ### .dockerignore
123
124
 
@@ -128,13 +129,19 @@ directly with no copying.
128
129
 
129
130
  ### Dependency containers
130
131
 
131
- If your app needs sidecar services (a database, Redis, ...), declare them under `dependencies`
132
- and set `defaults.network`. `wip up` creates the network first (if it doesn't exist), then brings
133
- up each dependency by name before the main container — so `bin/rails c` (or anything else run
134
- inside the main container) can reach `development.mysql`/`redis`/etc. by their dependency name,
135
- the same way Compose's service names resolve. `wip down` tears the main container and all
136
- dependencies down (the network itself is left in place). Each dependency entry accepts `image`
137
- (required), `command`, `env`, `ports`, `volumes`, and `workdir` the same shape as `defaults`.
132
+ `dependencies:` holds every container uniformly the primary one `container:` points at (`app`
133
+ by default) and any sidecar services (a database, Redis, ...) alongside it. Each entry accepts
134
+ `image` (required), `command`, `env`, `ports`, `volumes`, and `workdir`; there's no separate,
135
+ differently-shaped block for "the one you exec into."
136
+
137
+ What sets the primary entry apart is operational, not structural: `wip up` brings up every other
138
+ entry by name first (creating `network:` beforehand if it doesn't exist and set), then boots or
139
+ starts the primary one — so `bin/rails c` (or anything else run inside it) can reach
140
+ `development.mysql`/`redis`/etc. by their dependency name, the same way Compose's service names
141
+ resolve. `wip down` tears the primary container and all sidecars down (the network itself is left
142
+ in place). Only the primary container is a target for `exec`/`run`/`build`/`commands:` — sidecars
143
+ are only ever started and stopped, matching Compose's own service-vs-you-exec-into-one-of-them
144
+ split.
138
145
 
139
146
  ### Source sync
140
147
 
@@ -147,8 +154,8 @@ mirrors one into the other with `rsync`.
147
154
  ```yaml
148
155
  sync:
149
156
  source: . # host path, relative to wip.yml (default: the wip.yml directory)
150
- target: /app # container path served by the volume (default: defaults.workdir, else /app)
151
- volume: app-src # named volume holding the mirror (default: "<defaults.container>-src")
157
+ target: /app # container path served by the volume (default: the primary container's workdir, else /app)
158
+ volume: app-src # named volume holding the mirror (default: "<container>-src")
152
159
  mount: /host-src # where the source is bind-mounted read-only (default: /host-src)
153
160
  exclude: # rsync --exclude patterns
154
161
  - .git
@@ -160,14 +167,17 @@ sync:
160
167
  interval: 2 # seconds between syncs for `wip sync --watch` (default: 2)
161
168
  mode: exec # exec (mirror inside the running container) or run (a throwaway one);
162
169
  # default: exec for `mode: container`, run for `mode: compose`
170
+ image: null # image for the mirror container; required under mode: compose (there's no
171
+ # dependencies: entry to borrow one from there), unused under mode: container
172
+ # unless set (falls back to the primary container's own image)
163
173
  ```
164
174
 
165
175
  Everything below `sync:` is optional — `sync: {}` alone already works. With it in place:
166
176
 
167
- - Any `defaults.volumes` entry mounting `target` (the usual `.:/app`) is replaced by
168
- `<source>:/host-src:ro` plus `app-src:/app`, so the running app only ever touches the volume.
169
- Other volumes (`bundle:/usr/local/bundle`, ...) are passed through untouched, and
170
- `dependencies:` keep mounting whatever they declare.
177
+ - Any `volumes` entry on the primary container mounting `target` (the usual `.:/app`) is replaced
178
+ by `<source>:/host-src:ro` plus `app-src:/app`, so the running app only ever touches the volume.
179
+ Other volumes (`bundle:/usr/local/bundle`, ...) are passed through untouched, and sidecar
180
+ `dependencies:` entries keep mounting whatever they declare.
171
181
  - `wip up` mirrors the source into the volume before the container boots; `wip up --no-sync`
172
182
  skips that step.
173
183
  - `wip sync` mirrors on demand: `sync.mode: exec` (the default under `mode: container`) execs
@@ -187,11 +197,31 @@ image already has. And the mirror is one-way (host → volume): anything the app
187
197
  `target` is removed by the next `--delete` pass unless you `exclude` it, give it its own volume,
188
198
  or set `delete: false`.
189
199
 
190
- `sync:` works alongside `mode: compose` too. Compose still owns the volume layout — a compose
191
- service must mount the same named volume `sync.volume` names — but wip's mirror runs as its own
192
- throwaway container (`sync.mode` defaults to `run` and can't be set to `exec` under `mode:
193
- compose`, since only a container wip itself booted is guaranteed to have the read-only source
194
- mount attached).
200
+ `sync:` works alongside `mode: compose` too, but two things change:
201
+
202
+ - Compose still owns the volume layout, so wip doesn't rewrite any mounts for you: the compose
203
+ service that runs your app must itself declare a named volume with the exact same name as
204
+ `sync.volume` (`<container>-src` by default) mounted at the path your app expects, e.g.:
205
+ ```yaml
206
+ # compose.yml
207
+ services:
208
+ app:
209
+ volumes:
210
+ - app-src:/app
211
+ volumes:
212
+ app-src:
213
+ ```
214
+ wip's mirror writes into that volume from a separate, disposable container; it never touches
215
+ the compose service directly.
216
+ - `sync.mode` defaults to `run` and can't be set to `exec` (only a container wip itself booted is
217
+ guaranteed to have the read-only source mount attached, which compose services never do), and
218
+ `sync.image` becomes required, since that disposable container needs an image from somewhere —
219
+ under `mode: container` it borrows the primary `dependencies:` entry's image, but compose mode
220
+ has no such entry to borrow from.
221
+
222
+ `wip up`'s pre-boot mirror (and the `--no-sync` flag that skips it) works the same way under
223
+ `mode: compose` as it does otherwise: the source is mirrored into the volume before
224
+ `compose up` starts the service that mounts it.
195
225
 
196
226
  ### Compose mode
197
227
 
@@ -208,9 +238,9 @@ compose:
208
238
  project: myapp # optional; omitted lets the compose tool pick its own default
209
239
  ```
210
240
 
211
- `compose:` is mutually exclusive with `dependencies:`/`defaults.network` — pick one orchestration
212
- path per project. In compose mode, `wip` becomes a thin bridge to an external compose-for-`wslc`
213
- CLI rather than reimplementing Compose itself.
241
+ `compose:` is mutually exclusive with `dependencies:`/`network` — pick one orchestration path per
242
+ project. In compose mode, `wip` becomes a thin bridge to an external compose-for-`wslc` CLI rather
243
+ than reimplementing Compose itself.
214
244
 
215
245
  `wslc` itself has no native Compose support yet (tracked upstream in
216
246
  [microsoft/WSL#40948](https://github.com/microsoft/WSL/issues/40948)), and until it does,
@@ -244,8 +274,8 @@ found, its version, and which compose file `wip` resolved.
244
274
  | `wip doctor` | Diagnose WSL2, interop, WSLC, config, architecture, and Git |
245
275
  | `wip config` | Print the effective configuration (secrets masked) |
246
276
  | `wip build -- --no-cache` | Build the image from the `build` definition |
247
- | `wip up [-d] [--no-sync]` | Start `defaults.container` and its `dependencies` (creating any that are missing, on `defaults.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` |
248
- | `wip down` | Stop and remove `defaults.container` and its `dependencies` |
277
+ | `wip up [-d] [--no-sync]` | Start the primary `dependencies:` entry (`container:`, default `app`) 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` |
278
+ | `wip down` | Stop and remove the primary container and its sidecar `dependencies:` |
249
279
  | `wip exec [--no-interactive] COMMAND...` | Run a command in the existing container |
250
280
  | `wip run [--no-interactive] COMMAND...` | Run a command in a new `--rm` container (compose mode: `exec`s into `compose.service` instead) |
251
281
  | `wip shell` | Open the configured shell, falling back to `bash` then `sh` |
@@ -377,7 +407,7 @@ plugins are all unimplemented.
377
407
 
378
408
  `wip` already covers most of what [`dip`](https://github.com/bibendi/dip) adds on top of Compose —
379
409
  named commands (`commands:`), `run`/`exec` hidden behind a single verb, `.env` passthrough, and
380
- sidecar services via `dependencies:` + `defaults.network`. Fuller Compose semantics
410
+ sidecar services via `dependencies:` + `network:`. Fuller Compose semantics
381
411
  (`depends_on` ordering/health checks, log aggregation, named volumes, profiles, scaling) are now
382
412
  handled by delegating to a separate compose-for-`wslc` tool in [compose mode](#compose-mode)
383
413
  rather than being reimplemented in `wip` itself. Which of those actually work is entirely up to
@@ -404,8 +434,8 @@ for `wip`, roughly in priority order:
404
434
  soon as they land.
405
435
 
406
436
  Each of these should stay additive to the existing `wip.yml` shape — no breaking changes to
407
- `defaults`, `commands`, `dependencies`, or `compose` are planned. A resident daemon and a GUI
408
- remain out of scope; see "Not in the initial release" above.
437
+ `container`, `network`, `commands`, `dependencies`, or `compose` are planned. A resident daemon
438
+ and a GUI remain out of scope; see "Not in the initial release" above.
409
439
 
410
440
  ## License
411
441
 
data/lib/wip/cli.rb CHANGED
@@ -66,7 +66,7 @@ module Wip
66
66
  def build(*extra)
67
67
  extra.shift if extra.first == '--'
68
68
  settings = load_config.command('build') || {}
69
- context = settings['context'] || load_config.defaults['context'] || '.'
69
+ context = settings['context'] || '.'
70
70
  BuildContext.new(context).stage do |staged_context|
71
71
  execute(builder.build(settings: settings.merge('context' => staged_context), extra: extra))
72
72
  end
@@ -77,11 +77,12 @@ module Wip
77
77
  option :sync, type: :boolean, default: true, desc: 'Mirror the source into the sync volume first (--no-sync skips)'
78
78
  def up
79
79
  if load_config.compose?
80
+ sync_before_boot if options[:sync]
80
81
  return execute(compose_bridge.up(detach: options[:detach]), interactive: tty?(!options[:detach]))
81
82
  end
82
83
 
83
84
  ensure_network
84
- load_config.dependencies.each_key { |name| ensure_dependency(name) }
85
+ sidecar_names.each { |name| ensure_dependency(name) }
85
86
  sync_before_boot if options[:sync]
86
87
  ensure_container
87
88
  end
@@ -111,7 +112,7 @@ module Wip
111
112
 
112
113
  execute(builder.down, exit_on_failure: false)
113
114
  execute(builder.remove, exit_on_failure: false)
114
- load_config.dependencies.each_key do |name|
115
+ sidecar_names.each do |name|
115
116
  execute(builder.dependency_down(name), exit_on_failure: false)
116
117
  execute(builder.dependency_remove(name), exit_on_failure: false)
117
118
  end
@@ -250,6 +251,11 @@ module Wip
250
251
  false
251
252
  end
252
253
 
254
+ # dependencies: holds every container uniformly, including the primary one
255
+ # `container:` points at; that one gets its own up/down/start/find via
256
+ # ensure_container, so it's excluded here to avoid double-starting it.
257
+ def sidecar_names = load_config.dependencies.keys - [load_config.container]
258
+
253
259
  def ensure_dependency(name)
254
260
  if resource_exists?(builder.dependency_find(name))
255
261
  warn "wip: starting existing dependency '#{name}'"
@@ -300,7 +306,7 @@ module Wip
300
306
  end
301
307
 
302
308
  def ensure_container
303
- container = load_config.defaults['container']
309
+ container = load_config.container
304
310
  interactive = tty?(!options[:detach])
305
311
  if resource_exists?(builder.find)
306
312
  warn "wip: starting existing container '#{container}'"
@@ -13,14 +13,17 @@ module Wip
13
13
  end
14
14
 
15
15
  def exec(arguments, settings: {}, interactive: true)
16
- values = @config.defaults.merge(settings)
16
+ # dependencies: entries don't carry their own name (it's the hash key), so the
17
+ # exec target defaults to @config.container; a commands: entry can still
18
+ # redirect it by setting its own `container:`.
19
+ values = primary_values.merge('container' => @config.container).merge(settings)
17
20
  command = [@wslc, 'exec']
18
21
  command << '-it' if tty?(interactive)
19
22
  command.concat(options(values, include_container: true, include_publish: false)).concat(arguments)
20
23
  end
21
24
 
22
25
  def run(arguments, settings: {}, interactive: true)
23
- values = @config.defaults.merge(settings)
26
+ values = primary_values.merge(settings)
24
27
  command = [@wslc, 'run']
25
28
  command << '--rm' if values['remove']
26
29
  command << '-it' if tty?(interactive)
@@ -28,35 +31,36 @@ module Wip
28
31
  end
29
32
 
30
33
  def up(detach: false)
31
- values = @config.defaults
32
- command = [@wslc, 'run', '--name', required(values, 'container')]
34
+ values = primary_values
35
+ command = [@wslc, 'run', '--name', @config.container]
33
36
  command.push('--network', @config.network) if @config.network
34
37
  command << '-d' if detach
35
38
  command << '-it' if !detach && tty?(true)
36
39
  command.concat(options(values)).push(required(values, 'image'))
37
- command.concat(Shellwords.split(@config.up_command.to_s)) if @config.up_command
40
+ command.concat(Shellwords.split(values['command'].to_s)) unless values['command'].to_s.empty?
38
41
  command
39
42
  end
40
43
 
41
44
  def start(detach: false)
42
- command = [@wslc, 'start', required(@config.defaults, 'container')]
45
+ command = [@wslc, 'start', @config.container]
43
46
  command.push('-a', '-i') unless detach
44
47
  command
45
48
  end
46
49
 
47
50
  def find
48
- container = required(@config.defaults, 'container')
49
- [@wslc, 'list', '--all', '--filter', "name=#{container}", '--format', 'json']
51
+ [@wslc, 'list', '--all', '--filter', "name=#{@config.container}", '--format', 'json']
50
52
  end
51
53
 
52
54
  # Mirrors into the volume from a throwaway container. Used for sync.mode:
53
55
  # run (compose's default, and mode: container's fallback for when the app
54
- # container isn't running yet — e.g. just before `up` boots it).
56
+ # container isn't running yet — e.g. just before `up` boots it). sync.image
57
+ # overrides the mirror container's image; compose mode requires it, since
58
+ # there's no dependencies: entry to fall back to (compose owns those).
55
59
  def sync_run
56
60
  sync = required_sync
57
61
  command = [@wslc, 'run', '--rm']
58
62
  sync.volume_specs.each { |spec| command.push('-v', spec) }
59
- command.push(required(@config.defaults, 'image')).concat(sync.mirror_command)
63
+ command.push(sync.image || required(primary_values, 'image')).concat(sync.mirror_command)
60
64
  end
61
65
 
62
66
  # Mirrors from inside the already-running container. Only valid for
@@ -64,15 +68,15 @@ module Wip
64
68
  # itself booted is guaranteed to have both the read-only source mount and
65
69
  # the volume attached.
66
70
  def sync_exec
67
- [@wslc, 'exec', required(@config.defaults, 'container'), *required_sync.mirror_command]
71
+ [@wslc, 'exec', @config.container, *required_sync.mirror_command]
68
72
  end
69
73
 
70
74
  def down
71
- [@wslc, 'stop', required(@config.defaults, 'container')]
75
+ [@wslc, 'stop', @config.container]
72
76
  end
73
77
 
74
78
  def remove
75
- [@wslc, 'remove', '-f', required(@config.defaults, 'container')]
79
+ [@wslc, 'remove', '-f', @config.container]
76
80
  end
77
81
 
78
82
  def network_create
@@ -110,7 +114,7 @@ module Wip
110
114
  end
111
115
 
112
116
  def build(settings:, extra: [])
113
- values = @config.defaults.merge(settings)
117
+ values = primary_values.merge(settings)
114
118
  context = values['context'] || '.'
115
119
  tag = values['tag'] || values['image']
116
120
  raise ConfigError, 'Build image/tag must not be empty' if tag.to_s.empty?
@@ -154,7 +158,7 @@ module Wip
154
158
  specs.reject { |spec| settings.replaces?(spec) } + settings.volume_specs
155
159
  end
156
160
 
157
- # .env supplies defaults; env set in wip.yml (defaults or per-command) wins on conflict.
161
+ # .env supplies defaults; env set in wip.yml (primary or per-command) wins on conflict.
158
162
  def merged_env(values) = @dotenv.merge(values.fetch('env', {}))
159
163
 
160
164
  def required(values, key)
@@ -178,5 +182,10 @@ module Wip
178
182
  def dependency_values(name)
179
183
  @config.dependency(name) || raise(ConfigError, "Unknown dependency: #{name}")
180
184
  end
185
+
186
+ def primary_values
187
+ @config.primary || raise(ConfigError, "No dependencies.#{@config.container} entry " \
188
+ '(set container: to name a different one)')
189
+ end
181
190
  end
182
191
  end
data/lib/wip/config.rb CHANGED
@@ -3,8 +3,10 @@
3
3
  module Wip
4
4
  # Validated, defaulted access to a parsed wip.yml document.
5
5
  class Config
6
- DEFAULTS = { 'container' => 'app', 'workdir' => '/app', 'interactive' => false,
7
- 'remove' => true, 'env' => {}, 'ports' => [], 'volumes' => [] }.freeze
6
+ # Applied to every dependencies: entry, primary container included there
7
+ # is no separate, differently-shaped bucket for "the one you exec into."
8
+ DEPENDENCY_DEFAULTS = { 'workdir' => nil, 'interactive' => false, 'remove' => true,
9
+ 'env' => {}, 'ports' => [], 'volumes' => [] }.freeze
8
10
  SECRET_PATTERN = /token|password|secret|credential|auth/i
9
11
  # Which orchestration path `up`/`down`/`sync`/etc. take. Explicit rather than
10
12
  # inferred from a `compose:` block's presence, so a config reader doesn't have
@@ -20,10 +22,12 @@ module Wip
20
22
 
21
23
  def wslc_command = @raw.dig('wslc', 'command') || 'auto'
22
24
  def commands = @raw['commands'] || {}
23
- def defaults = DEFAULTS.merge(@raw['defaults'] || {})
24
- def up_command = @raw.dig('up', 'command')
25
25
  def dependencies = @raw['dependencies'] || {}
26
- def network = defaults['network']
26
+ # Which dependencies: entry `up`/`down`/`exec`/`run`/`build`/`commands:` target
27
+ # by default — the one container wip itself considers "the app." Everything
28
+ # else in dependencies: is a sidecar wip only starts and stops.
29
+ def container = presence(@raw['container']) || 'app'
30
+ def network = @raw['network']
27
31
  def mode = @raw['mode'] || 'container'
28
32
  def compose = @raw['compose']
29
33
  def compose? = mode == 'compose'
@@ -39,25 +43,29 @@ module Wip
39
43
  @sync = @raw.key?('sync') ? build_sync : nil
40
44
  end
41
45
 
46
+ # The dependencies: entry `container` points at, or nil if it isn't defined.
47
+ def primary = dependency(container)
48
+
42
49
  def command(name)
43
50
  entry = commands[name.to_s]
44
51
  return unless entry
45
52
 
46
- defaults.merge('type' => 'exec').merge(entry)
53
+ (primary || {}).merge('type' => 'exec').merge(entry)
47
54
  end
48
55
 
49
56
  def dependency(name)
50
57
  entry = dependencies[name.to_s]
51
58
  return unless entry
52
59
 
53
- { 'workdir' => nil, 'env' => {}, 'ports' => [], 'volumes' => [] }.merge(entry)
60
+ DEPENDENCY_DEFAULTS.merge(entry)
54
61
  end
55
62
 
56
63
  def to_h(redact: true)
57
- value = { 'version' => 1, 'wslc' => { 'command' => wslc_command }, 'defaults' => defaults,
58
- 'up' => { 'command' => up_command }, 'mode' => mode, 'dependencies' => dependencies,
59
- 'compose' => compose, 'sync' => sync&.to_h,
60
- 'commands' => commands.transform_values { |entry| defaults.merge('type' => 'exec').merge(entry) } }
64
+ value = { 'version' => 1, 'wslc' => { 'command' => wslc_command }, 'mode' => mode, 'container' => container,
65
+ 'network' => network, 'dependencies' => dependencies, 'compose' => compose, 'sync' => sync&.to_h,
66
+ 'commands' => commands.transform_values do |entry|
67
+ (primary || {}).merge('type' => 'exec').merge(entry)
68
+ end }
61
69
  redact ? redact_secrets(value) : value
62
70
  end
63
71
 
@@ -65,7 +73,6 @@ module Wip
65
73
 
66
74
  def validate!
67
75
  raise ConfigError, "Unsupported configuration version: #{@raw['version']}" unless (@raw['version'] || 1) == 1
68
- raise ConfigError, 'up must be a mapping' if @raw.key?('up') && !@raw['up'].is_a?(Hash)
69
76
 
70
77
  validate_mode!
71
78
  validate_commands!
@@ -76,7 +83,7 @@ module Wip
76
83
 
77
84
  def build_sync
78
85
  SyncSettings.new(@raw['sync'], base: path && File.dirname(path.to_s),
79
- workdir: defaults['workdir'], container: defaults['container'],
86
+ workdir: primary && primary['workdir'], container: container,
80
87
  compose: compose?)
81
88
  end
82
89
 
@@ -110,7 +117,7 @@ module Wip
110
117
  raise ConfigError, 'compose.service must not be empty' if compose_service.to_s.empty?
111
118
  raise ConfigError, 'compose.command must not be empty' if compose_command.to_s.empty?
112
119
  raise ConfigError, 'compose is mutually exclusive with dependencies' if dependencies.any?
113
- raise ConfigError, 'compose is mutually exclusive with defaults.network' if network
120
+ raise ConfigError, 'compose is mutually exclusive with network' if network
114
121
  end
115
122
 
116
123
  def validate_command!(name, entry)
@@ -143,5 +150,7 @@ module Wip
143
150
 
144
151
  object.to_h { |key, value| [key, key.match?(SECRET_PATTERN) ? '[REDACTED]' : redact_secrets(value)] }
145
152
  end
153
+
154
+ def presence(value) = value.to_s.empty? ? nil : value.to_s
146
155
  end
147
156
  end
data/lib/wip/doctor.rb CHANGED
@@ -42,15 +42,22 @@ module Wip
42
42
 
43
43
  def load_config(results)
44
44
  config = @loader.load
45
- invalid = config.compose? ? [] : %w[container image].select { |key| config.defaults[key].to_s.empty? }
46
- results << Result.new(invalid.empty? ? :ok : :fail,
47
- invalid.empty? ? 'Loaded wip.yml' : "Empty defaults: #{invalid.join(', ')}")
45
+ results << Result.new(*container_result(config))
48
46
  config
49
47
  rescue ConfigError => e
50
48
  results << Result.new(:fail, e.message)
51
49
  nil
52
50
  end
53
51
 
52
+ # dependencies.<container> having an empty image is already a load-time
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.
55
+ def container_result(config)
56
+ return [:ok, 'Loaded wip.yml'] if config.compose? || config.primary
57
+
58
+ [:fail, "No dependencies.#{config.container} entry"]
59
+ end
60
+
54
61
  def check_config(config, results)
55
62
  return unless config
56
63
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'yaml'
4
+
3
5
  module Wip
4
6
  # Builds a starter wip.yml. Detects an existing compose file next to the
5
7
  # target (the same filenames ComposeBridge auto-detects) to decide between
@@ -10,10 +12,10 @@ module Wip
10
12
  version: 1
11
13
  mode: container
12
14
 
13
- defaults:
14
- container: app # TODO: name of the container wip creates
15
- image: your/image:tag # TODO: image to run
16
- workdir: /app
15
+ dependencies:
16
+ app: # TODO: this is the container wip creates, execs into, and runs commands in
17
+ image: your/image:tag # TODO: image to run
18
+ workdir: /app # TODO: adjust to match your image, or delete this line
17
19
 
18
20
  sync: {} # optional; mirrors the source into a named volume instead of bind-mounting it live
19
21
  YAML
@@ -42,7 +44,46 @@ module Wip
42
44
  compose:
43
45
  service: app # TODO: which service in #{compose_file} wip run/exec/NAME target
44
46
  command: wslc-compose # TODO: the compose-for-wslc binary/path you have installed
47
+
48
+ #{sync_hint}
45
49
  YAML
46
50
  end
51
+
52
+ # sync: needs sync.image (mode: compose has no dependencies: entry to borrow one from) and a
53
+ # named volume the compose service mounts, matching sync.volume ("app-src" by default). Checked
54
+ # against the detected compose file so the hint doesn't repeat what's already set up.
55
+ def sync_hint
56
+ return sync_hint_configured if compose_volume_mounted?
57
+
58
+ sync_hint_todo
59
+ end
60
+
61
+ def sync_hint_configured
62
+ <<~COMMENT.chomp
63
+ # #{compose_file} already mounts a volume matching sync.volume's default (app-src).
64
+ # sync: # add sync.image too (required under mode: compose) — see README
65
+ COMMENT
66
+ end
67
+
68
+ def sync_hint_todo
69
+ <<~COMMENT.chomp
70
+ # sync: # optional; mirrors the source into a named volume instead of bind-mounting it live
71
+ # image: your/image:tag # required under mode: compose (no dependencies: entry to borrow one from)
72
+ # # the service above must also mount a volume named "app-src" (sync.volume's default) at the
73
+ # # path your app expects — add to #{compose_file}:
74
+ # # volumes:
75
+ # # - app-src:/app
76
+ # # and, alongside services:
77
+ # # volumes:
78
+ # # app-src:
79
+ COMMENT
80
+ end
81
+
82
+ def compose_volume_mounted?
83
+ services = YAML.safe_load_file(File.join(@dir, compose_file), aliases: true)&.fetch('services', nil) || {}
84
+ services.values.any? { |service| Array(service['volumes']).any? { |v| v.to_s.start_with?('app-src:') } }
85
+ rescue StandardError
86
+ false
87
+ end
47
88
  end
48
89
  end
@@ -32,7 +32,7 @@ module Wip
32
32
  # compose owns its own services' mounts and never guarantees that shape.
33
33
  SYNC_MODES = %w[exec run].freeze
34
34
 
35
- attr_reader :target, :mount, :volume, :exclude, :binary, :extra_options, :interval, :mode
35
+ attr_reader :target, :mount, :volume, :exclude, :binary, :extra_options, :interval, :mode, :image
36
36
 
37
37
  def initialize(raw, base: nil, workdir: nil, container: nil, compose: false)
38
38
  raise ConfigError, 'sync must be a mapping' unless raw.is_a?(Hash)
@@ -57,8 +57,8 @@ module Wip
57
57
  # named volume where the app actually runs.
58
58
  def volume_specs = ["#{source}:#{mount}:ro", "#{volume}:#{target}"]
59
59
 
60
- # True for a configured volume that sync replaces, so `.:/app` in
61
- # `defaults.volumes` quietly becomes the read-only mount plus the volume.
60
+ # True for a configured volume that sync replaces, so `.:/app` in the
61
+ # primary container's `volumes` quietly becomes the read-only mount plus the volume.
62
62
  def replaces?(spec)
63
63
  [target.chomp('/'), mount.chomp('/')].include?(container_path(spec))
64
64
  end
@@ -76,7 +76,7 @@ module Wip
76
76
  def to_h
77
77
  { 'source' => source, 'target' => target, 'mount' => mount, 'volume' => volume,
78
78
  'delete' => delete?, 'exclude' => exclude, 'command' => binary, 'options' => extra_options,
79
- 'interval' => interval, 'mode' => mode }
79
+ 'interval' => interval, 'mode' => mode, 'image' => image }
80
80
  end
81
81
 
82
82
  private
@@ -98,13 +98,25 @@ module Wip
98
98
 
99
99
  def assign_mode(raw, compose:)
100
100
  @mode = presence(raw['mode']) || (compose ? 'run' : 'exec')
101
+ @image = presence(raw['image'])
101
102
  raise ConfigError, "sync.mode must be one of #{SYNC_MODES.join(', ')}" unless SYNC_MODES.include?(@mode)
103
+
104
+ validate_image!(compose)
102
105
  return unless compose && exec?
103
106
 
104
107
  raise ConfigError, 'sync.mode: exec needs mode: container (compose owns its services’ mounts, ' \
105
108
  'so it can’t guarantee the running container has the sync mounts attached)'
106
109
  end
107
110
 
111
+ # compose mode has no dependencies: entry to fall back to for the mirror
112
+ # container's image, so sync.image can't be left implicit there.
113
+ def validate_image!(compose)
114
+ return if !compose || @image
115
+
116
+ raise ConfigError, 'sync.image is required under mode: compose (there’s no dependencies: entry ' \
117
+ 'to borrow the mirror container’s image from)'
118
+ end
119
+
108
120
  def validate!
109
121
  raise ConfigError, 'sync.target must be an absolute path' unless @target.start_with?('/')
110
122
  raise ConfigError, 'sync.mount must be an absolute path' unless @mount.start_with?('/')
data/lib/wip/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Wip
4
- VERSION = '0.9.0'
4
+ VERSION = '0.11.0'
5
5
  end
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.9.0
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wip contributors