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 +4 -4
- data/README.md +77 -47
- data/lib/wip/cli.rb +10 -4
- data/lib/wip/command_builder.rb +24 -15
- data/lib/wip/config.rb +23 -14
- data/lib/wip/doctor.rb +10 -3
- data/lib/wip/initializer.rb +45 -4
- data/lib/wip/sync_settings.rb +16 -4
- data/lib/wip/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a32d5a9003a7197fd47fa4b81b7ca764b495ea7955ccc523cda29330f0e4e5b2
|
|
4
|
+
data.tar.gz: 9d47155b5a53497dc9e5ad16cbd48f402ee0ea9d3865cfd932f577d6a1f4a746
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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 #
|
|
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
|
-
|
|
52
|
-
|
|
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
|
|
119
|
-
command/dependency's own `env` — those always win on conflict. Pass `--env-file PATH`
|
|
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
|
-
|
|
132
|
-
and
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
(
|
|
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:
|
|
151
|
-
volume: app-src # named volume holding the mirror (default: "<
|
|
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 `
|
|
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
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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:`/`
|
|
212
|
-
|
|
213
|
-
|
|
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 `
|
|
248
|
-
| `wip down` | Stop and remove
|
|
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:` + `
|
|
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
|
-
`
|
|
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'] ||
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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}'"
|
data/lib/wip/command_builder.rb
CHANGED
|
@@ -13,14 +13,17 @@ module Wip
|
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def exec(arguments, settings: {}, interactive: true)
|
|
16
|
-
|
|
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 =
|
|
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 =
|
|
32
|
-
command = [@wslc, 'run', '--name',
|
|
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(
|
|
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',
|
|
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
|
-
|
|
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(
|
|
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',
|
|
71
|
+
[@wslc, 'exec', @config.container, *required_sync.mirror_command]
|
|
68
72
|
end
|
|
69
73
|
|
|
70
74
|
def down
|
|
71
|
-
[@wslc, 'stop',
|
|
75
|
+
[@wslc, 'stop', @config.container]
|
|
72
76
|
end
|
|
73
77
|
|
|
74
78
|
def remove
|
|
75
|
-
[@wslc, 'remove', '-f',
|
|
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 =
|
|
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 (
|
|
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
|
-
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 }, '
|
|
58
|
-
'
|
|
59
|
-
'
|
|
60
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
|
data/lib/wip/initializer.rb
CHANGED
|
@@ -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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
data/lib/wip/sync_settings.rb
CHANGED
|
@@ -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
|
-
# `
|
|
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