wslc-wip 0.8.0 → 0.10.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: afc914ac16e4359e48e682f5dab523827ad782251365abee7abd60b6eee89044
4
- data.tar.gz: dcc97ec1bace3262bc1c419162085de6a3f5e24f0dde8babb2532ef9bdd6446c
3
+ metadata.gz: f34ba1e90b936146999504b8db29e2b0376efd4b1bf8faa65a398e8ae7758a6c
4
+ data.tar.gz: 94945faa2bb0d04ed9cc5159ab21d9ad1811bf8bdbd8e1049b22e91acf282f24
5
5
  SHA512:
6
- metadata.gz: 526f9730c15eb6beabda31c3df3ee95cac2f8058c6da2dddf27100aadb48276bb68d66a3ede2b7383f49ecb6cd6fd45604b39ec1130058718cfbf439e7629f5a
7
- data.tar.gz: 87a5f6c40e948fabdc79eeaf3e81cb73fac3cdb0fdd306189a72a437f459de22deeb57fbd92167411c157485d66dd82f40734f72316f46cb65fe6aa74b111511
6
+ metadata.gz: e3420daa82aa13024e1c8d4331170be74e76249fe044c4f051a58e412bba33ea0b3481c0bccdab813868b719dba61c0f30c8af7becb4b37a0f6bb35eb9edfab7
7
+ data.tar.gz: 23b08e3581ff3ffc6967125a339c02d82197f0247070f57ff0cff6b62c7065558ad36c18524c682a870d6c093d5ff7485fb00962d350bc341c41bcd203442c13
data/README.md CHANGED
@@ -31,6 +31,7 @@ From source: `bundle install && bundle exec exe/wip version`.
31
31
  ```bash
32
32
  gem install wslc-wip
33
33
  cd my-project
34
+ wip init # writes a starter wip.yml; edit the TODOs, then:
34
35
  wip doctor
35
36
  wip build
36
37
  wip up -d
@@ -44,27 +45,28 @@ Put a `wip.yml` in your project root. Running from a subdirectory walks up to fi
44
45
 
45
46
  ```yaml
46
47
  version: 1
47
- 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.
48
51
  wslc:
49
52
  command: auto # tries wslc.exe, wslc, then System32; an absolute path also works
50
- defaults:
51
- container: app
52
- image: slidict/slidict:development
53
- workdir: /app
54
- interactive: false
55
- remove: true
56
- network: app-tier # optional; shared with `dependencies` so containers can resolve each other by name
57
- env:
58
- RAILS_ENV: development
59
- PORT: "3000"
60
- ports:
61
- - "3000:3000"
62
- volumes:
63
- - ".:/app"
64
- up:
65
- command: server # command `wip up` passes to the image when it creates the container
66
- # (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
67
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"
68
70
  redis:
69
71
  image: redis:latest
70
72
  development.mysql:
@@ -114,9 +116,9 @@ instead.
114
116
  Like `docker compose`, `wip` automatically loads a `.env` file next to `wip.yml` (one `KEY=VALUE`
115
117
  per line; `#` comments, blank lines, `export` prefixes, and quoted values are all supported) and
116
118
  passes its keys through as container environment variables on `build`, `up`, `run`, `exec`, and
117
- custom commands. `.env` only fills in keys that aren't already set by `defaults.env` or a
118
- command/dependency's own `env` — those always win on conflict. Pass `--env-file PATH` to load a
119
- 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.
120
122
 
121
123
  ### .dockerignore
122
124
 
@@ -127,13 +129,19 @@ directly with no copying.
127
129
 
128
130
  ### Dependency containers
129
131
 
130
- If your app needs sidecar services (a database, Redis, ...), declare them under `dependencies`
131
- and set `defaults.network`. `wip up` creates the network first (if it doesn't exist), then brings
132
- up each dependency by name before the main container — so `bin/rails c` (or anything else run
133
- inside the main container) can reach `development.mysql`/`redis`/etc. by their dependency name,
134
- the same way Compose's service names resolve. `wip down` tears the main container and all
135
- dependencies down (the network itself is left in place). Each dependency entry accepts `image`
136
- (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.
137
145
 
138
146
  ### Source sync
139
147
 
@@ -146,8 +154,8 @@ mirrors one into the other with `rsync`.
146
154
  ```yaml
147
155
  sync:
148
156
  source: . # host path, relative to wip.yml (default: the wip.yml directory)
149
- target: /app # container path served by the volume (default: defaults.workdir, else /app)
150
- 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")
151
159
  mount: /host-src # where the source is bind-mounted read-only (default: /host-src)
152
160
  exclude: # rsync --exclude patterns
153
161
  - .git
@@ -159,14 +167,17 @@ sync:
159
167
  interval: 2 # seconds between syncs for `wip sync --watch` (default: 2)
160
168
  mode: exec # exec (mirror inside the running container) or run (a throwaway one);
161
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)
162
173
  ```
163
174
 
164
175
  Everything below `sync:` is optional — `sync: {}` alone already works. With it in place:
165
176
 
166
- - Any `defaults.volumes` entry mounting `target` (the usual `.:/app`) is replaced by
167
- `<source>:/host-src:ro` plus `app-src:/app`, so the running app only ever touches the volume.
168
- Other volumes (`bundle:/usr/local/bundle`, ...) are passed through untouched, and
169
- `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.
170
181
  - `wip up` mirrors the source into the volume before the container boots; `wip up --no-sync`
171
182
  skips that step.
172
183
  - `wip sync` mirrors on demand: `sync.mode: exec` (the default under `mode: container`) execs
@@ -186,11 +197,31 @@ image already has. And the mirror is one-way (host → volume): anything the app
186
197
  `target` is removed by the next `--delete` pass unless you `exclude` it, give it its own volume,
187
198
  or set `delete: false`.
188
199
 
189
- `sync:` works alongside `mode: compose` too. Compose still owns the volume layout — a compose
190
- service must mount the same named volume `sync.volume` names — but wip's mirror runs as its own
191
- throwaway container (`sync.mode` defaults to `run` and can't be set to `exec` under `mode:
192
- compose`, since only a container wip itself booted is guaranteed to have the read-only source
193
- 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.
194
225
 
195
226
  ### Compose mode
196
227
 
@@ -207,9 +238,9 @@ compose:
207
238
  project: myapp # optional; omitted lets the compose tool pick its own default
208
239
  ```
209
240
 
210
- `compose:` is mutually exclusive with `dependencies:`/`defaults.network` — pick one orchestration
211
- path per project. In compose mode, `wip` becomes a thin bridge to an external compose-for-`wslc`
212
- 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.
213
244
 
214
245
  `wslc` itself has no native Compose support yet (tracked upstream in
215
246
  [microsoft/WSL#40948](https://github.com/microsoft/WSL/issues/40948)), and until it does,
@@ -238,12 +269,13 @@ found, its version, and which compose file `wip` resolved.
238
269
 
239
270
  | Command | Description |
240
271
  |---|---|
272
+ | `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` |
241
273
  | `wip version` | wip's version, plus WSLC's if it can be detected |
242
274
  | `wip doctor` | Diagnose WSL2, interop, WSLC, config, architecture, and Git |
243
275
  | `wip config` | Print the effective configuration (secrets masked) |
244
276
  | `wip build -- --no-cache` | Build the image from the `build` definition |
245
- | `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` |
246
- | `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:` |
247
279
  | `wip exec [--no-interactive] COMMAND...` | Run a command in the existing container |
248
280
  | `wip run [--no-interactive] COMMAND...` | Run a command in a new `--rm` container (compose mode: `exec`s into `compose.service` instead) |
249
281
  | `wip shell` | Open the configured shell, falling back to `bash` then `sh` |
@@ -375,7 +407,7 @@ plugins are all unimplemented.
375
407
 
376
408
  `wip` already covers most of what [`dip`](https://github.com/bibendi/dip) adds on top of Compose —
377
409
  named commands (`commands:`), `run`/`exec` hidden behind a single verb, `.env` passthrough, and
378
- sidecar services via `dependencies:` + `defaults.network`. Fuller Compose semantics
410
+ sidecar services via `dependencies:` + `network:`. Fuller Compose semantics
379
411
  (`depends_on` ordering/health checks, log aggregation, named volumes, profiles, scaling) are now
380
412
  handled by delegating to a separate compose-for-`wslc` tool in [compose mode](#compose-mode)
381
413
  rather than being reimplemented in `wip` itself. Which of those actually work is entirely up to
@@ -402,8 +434,8 @@ for `wip`, roughly in priority order:
402
434
  soon as they land.
403
435
 
404
436
  Each of these should stay additive to the existing `wip.yml` shape — no breaking changes to
405
- `defaults`, `commands`, `dependencies`, or `compose` are planned. A resident daemon and a GUI
406
- 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.
407
439
 
408
440
  ## License
409
441
 
data/lib/wip/cli.rb CHANGED
@@ -39,6 +39,17 @@ module Wip
39
39
  nil
40
40
  end
41
41
 
42
+ desc 'init', 'Create a starter wip.yml (detects an existing compose.yml/docker-compose.yml)'
43
+ option :force, type: :boolean, default: false, desc: 'Overwrite an existing wip.yml'
44
+ def init
45
+ path = Pathname(options[:config] || ConfigLoader::FILENAME).expand_path
46
+ raise Error, "#{path} already exists (use --force to overwrite)" if path.file? && !options[:force]
47
+
48
+ initializer = Initializer.new(dir: path.dirname)
49
+ path.write(initializer.call)
50
+ warn "wip: wrote #{path} (mode: #{initializer.compose? ? 'compose' : 'container'})"
51
+ end
52
+
42
53
  desc 'doctor', 'Diagnose the development environment'
43
54
  def doctor
44
55
  results = Doctor.new(loader: loader).call
@@ -55,7 +66,7 @@ module Wip
55
66
  def build(*extra)
56
67
  extra.shift if extra.first == '--'
57
68
  settings = load_config.command('build') || {}
58
- context = settings['context'] || load_config.defaults['context'] || '.'
69
+ context = settings['context'] || '.'
59
70
  BuildContext.new(context).stage do |staged_context|
60
71
  execute(builder.build(settings: settings.merge('context' => staged_context), extra: extra))
61
72
  end
@@ -66,11 +77,12 @@ module Wip
66
77
  option :sync, type: :boolean, default: true, desc: 'Mirror the source into the sync volume first (--no-sync skips)'
67
78
  def up
68
79
  if load_config.compose?
80
+ sync_before_boot if options[:sync]
69
81
  return execute(compose_bridge.up(detach: options[:detach]), interactive: tty?(!options[:detach]))
70
82
  end
71
83
 
72
84
  ensure_network
73
- load_config.dependencies.each_key { |name| ensure_dependency(name) }
85
+ sidecar_names.each { |name| ensure_dependency(name) }
74
86
  sync_before_boot if options[:sync]
75
87
  ensure_container
76
88
  end
@@ -100,7 +112,7 @@ module Wip
100
112
 
101
113
  execute(builder.down, exit_on_failure: false)
102
114
  execute(builder.remove, exit_on_failure: false)
103
- load_config.dependencies.each_key do |name|
115
+ sidecar_names.each do |name|
104
116
  execute(builder.dependency_down(name), exit_on_failure: false)
105
117
  execute(builder.dependency_remove(name), exit_on_failure: false)
106
118
  end
@@ -239,6 +251,11 @@ module Wip
239
251
  false
240
252
  end
241
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
+
242
259
  def ensure_dependency(name)
243
260
  if resource_exists?(builder.dependency_find(name))
244
261
  warn "wip: starting existing dependency '#{name}'"
@@ -289,7 +306,7 @@ module Wip
289
306
  end
290
307
 
291
308
  def ensure_container
292
- container = load_config.defaults['container']
309
+ container = load_config.container
293
310
  interactive = tty?(!options[:detach])
294
311
  if resource_exists?(builder.find)
295
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
 
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wip
4
+ # Builds a starter wip.yml. Detects an existing compose file next to the
5
+ # target (the same filenames ComposeBridge auto-detects) to decide between
6
+ # mode: compose and mode: container, since that's the one decision `wip
7
+ # init` can't leave to a placeholder.
8
+ class Initializer
9
+ CONTAINER_TEMPLATE = <<~YAML
10
+ version: 1
11
+ mode: container
12
+
13
+ dependencies:
14
+ app: # TODO: this is the container wip creates, execs into, and runs commands in
15
+ image: your/image:tag # TODO: image to run
16
+ workdir: /app # TODO: adjust to match your image, or delete this line
17
+
18
+ sync: {} # optional; mirrors the source into a named volume instead of bind-mounting it live
19
+ YAML
20
+
21
+ def initialize(dir: Dir.pwd)
22
+ @dir = dir
23
+ end
24
+
25
+ def compose? = !!compose_file
26
+
27
+ def call
28
+ compose? ? compose_template : CONTAINER_TEMPLATE
29
+ end
30
+
31
+ private
32
+
33
+ def compose_file
34
+ @compose_file ||= ComposeBridge::FILENAMES.find { |name| File.file?(File.join(@dir, name)) }
35
+ end
36
+
37
+ def compose_template
38
+ <<~YAML
39
+ version: 1
40
+ mode: compose
41
+
42
+ compose:
43
+ service: app # TODO: which service in #{compose_file} wip run/exec/NAME target
44
+ command: wslc-compose # TODO: the compose-for-wslc binary/path you have installed
45
+ YAML
46
+ end
47
+ end
48
+ 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.8.0'
4
+ VERSION = '0.10.0'
5
5
  end
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/initializer'
16
17
  require_relative 'wip/command_display'
17
18
  require_relative 'wip/command_runner'
18
19
  require_relative 'wip/resource_monitor'
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.8.0
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wip contributors
@@ -49,6 +49,7 @@ files:
49
49
  - lib/wip/environment.rb
50
50
  - lib/wip/error_interpreter.rb
51
51
  - lib/wip/errors.rb
52
+ - lib/wip/initializer.rb
52
53
  - lib/wip/resource_monitor.rb
53
54
  - lib/wip/sync_settings.rb
54
55
  - lib/wip/version.rb