wslc-wip 0.7.4 → 0.9.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 +15 -4
- data/lib/wip/cli.rb +15 -5
- data/lib/wip/command_builder.rb +7 -9
- data/lib/wip/config.rb +20 -6
- data/lib/wip/initializer.rb +48 -0
- data/lib/wip/sync_settings.rb +19 -3
- data/lib/wip/version.rb +1 -1
- data/lib/wip.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d16349112264f413d1492541731ec704d56616ccb4b05cef1f505d667a5d823d
|
|
4
|
+
data.tar.gz: 05f7f5819345079aa82e062ee368a3255f93bdeaa62cf55fda2621c2bd36c4ed
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cdbeea98b7472f9ec6e1ccec8883bf136dfcb76164c2ba2d2ddbd63fe52f22c20f72a994a2a85527b1d9e7a7511016fe1b54ab64e678aca009530616887435f1
|
|
7
|
+
data.tar.gz: a6f524f41b20fe20af6262e3e22e2dafa924d37e31279ff21ed94268824587017ad94670084ae81aeb79513369a923d37011fd95bd9471a1e357551b90701769
|
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,6 +45,7 @@ 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
|
|
48
|
+
mode: container # or compose; picks the orchestration path `up`/`down`/`sync`/etc. take (default: container)
|
|
47
49
|
wslc:
|
|
48
50
|
command: auto # tries wslc.exe, wslc, then System32; an absolute path also works
|
|
49
51
|
defaults:
|
|
@@ -156,6 +158,8 @@ sync:
|
|
|
156
158
|
command: rsync # binary that does the mirroring (default: rsync)
|
|
157
159
|
options: [] # extra flags appended to the rsync invocation
|
|
158
160
|
interval: 2 # seconds between syncs for `wip sync --watch` (default: 2)
|
|
161
|
+
mode: exec # exec (mirror inside the running container) or run (a throwaway one);
|
|
162
|
+
# default: exec for `mode: container`, run for `mode: compose`
|
|
159
163
|
```
|
|
160
164
|
|
|
161
165
|
Everything below `sync:` is optional — `sync: {}` alone already works. With it in place:
|
|
@@ -166,8 +170,10 @@ Everything below `sync:` is optional — `sync: {}` alone already works. With it
|
|
|
166
170
|
`dependencies:` keep mounting whatever they declare.
|
|
167
171
|
- `wip up` mirrors the source into the volume before the container boots; `wip up --no-sync`
|
|
168
172
|
skips that step.
|
|
169
|
-
- `wip sync` mirrors on demand
|
|
170
|
-
|
|
173
|
+
- `wip sync` mirrors on demand: `sync.mode: exec` (the default under `mode: container`) execs
|
|
174
|
+
rsync inside the already-running container; `sync.mode: run` always uses a throwaway container
|
|
175
|
+
with the same mounts instead. Which one runs is fixed by config, not guessed at from whether a
|
|
176
|
+
container happens to be up.
|
|
171
177
|
- `wip sync --watch [--interval N]` keeps re-syncing until Ctrl-C, so host edits reach the
|
|
172
178
|
container with a short delay. Run it in a second terminal alongside `wip up -d`.
|
|
173
179
|
- `wip doctor` reports the resolved source, volume, and target, and fails if the source is missing.
|
|
@@ -181,8 +187,11 @@ image already has. And the mirror is one-way (host → volume): anything the app
|
|
|
181
187
|
`target` is removed by the next `--delete` pass unless you `exclude` it, give it its own volume,
|
|
182
188
|
or set `delete: false`.
|
|
183
189
|
|
|
184
|
-
`sync:`
|
|
185
|
-
|
|
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).
|
|
186
195
|
|
|
187
196
|
### Compose mode
|
|
188
197
|
|
|
@@ -191,6 +200,7 @@ If your project already has a real `compose.yml`, don't duplicate it in `depende
|
|
|
191
200
|
|
|
192
201
|
```yaml
|
|
193
202
|
version: 1
|
|
203
|
+
mode: compose # required to enable compose mode; a compose: block with no mode: compose is an error
|
|
194
204
|
compose:
|
|
195
205
|
service: app # required: which compose service wip run/exec/NAME target
|
|
196
206
|
command: wslc-compose # required: the compose-for-wslc binary/path you have installed
|
|
@@ -229,6 +239,7 @@ found, its version, and which compose file `wip` resolved.
|
|
|
229
239
|
|
|
230
240
|
| Command | Description |
|
|
231
241
|
|---|---|
|
|
242
|
+
| `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` |
|
|
232
243
|
| `wip version` | wip's version, plus WSLC's if it can be detected |
|
|
233
244
|
| `wip doctor` | Diagnose WSL2, interop, WSLC, config, architecture, and Git |
|
|
234
245
|
| `wip config` | Print the effective configuration (secrets masked) |
|
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
|
|
@@ -271,15 +282,14 @@ module Wip
|
|
|
271
282
|
"run it with `wip dispatch #{name}`"
|
|
272
283
|
end
|
|
273
284
|
|
|
274
|
-
#
|
|
275
|
-
#
|
|
285
|
+
# sync.mode picks the path explicitly (default: exec for mode: container,
|
|
286
|
+
# run for mode: compose) rather than probing whether a container happens
|
|
287
|
+
# to be running under a matching name.
|
|
276
288
|
def run_sync(exit_on_failure: true)
|
|
277
|
-
command =
|
|
289
|
+
command = sync_settings!.exec? ? builder.sync_exec : builder.sync_run
|
|
278
290
|
execute(command, exit_on_failure: exit_on_failure)
|
|
279
291
|
end
|
|
280
292
|
|
|
281
|
-
def container_running? = resource_exists?(builder.find_running)
|
|
282
|
-
|
|
283
293
|
def sync_before_boot
|
|
284
294
|
settings = load_config.sync
|
|
285
295
|
return unless settings
|
data/lib/wip/command_builder.rb
CHANGED
|
@@ -49,13 +49,9 @@ module Wip
|
|
|
49
49
|
[@wslc, 'list', '--all', '--filter', "name=#{container}", '--format', 'json']
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
# Mirrors into the volume from a throwaway container, for when the app
|
|
58
|
-
# container isn't running yet (or at all) — e.g. just before `up` boots it.
|
|
52
|
+
# Mirrors into the volume from a throwaway container. Used for sync.mode:
|
|
53
|
+
# 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).
|
|
59
55
|
def sync_run
|
|
60
56
|
sync = required_sync
|
|
61
57
|
command = [@wslc, 'run', '--rm']
|
|
@@ -63,8 +59,10 @@ module Wip
|
|
|
63
59
|
command.push(required(@config.defaults, 'image')).concat(sync.mirror_command)
|
|
64
60
|
end
|
|
65
61
|
|
|
66
|
-
# Mirrors from inside the running container
|
|
67
|
-
#
|
|
62
|
+
# Mirrors from inside the already-running container. Only valid for
|
|
63
|
+
# sync.mode: exec (mode: container's default), since only a container wip
|
|
64
|
+
# itself booted is guaranteed to have both the read-only source mount and
|
|
65
|
+
# the volume attached.
|
|
68
66
|
def sync_exec
|
|
69
67
|
[@wslc, 'exec', required(@config.defaults, 'container'), *required_sync.mirror_command]
|
|
70
68
|
end
|
data/lib/wip/config.rb
CHANGED
|
@@ -6,6 +6,10 @@ module Wip
|
|
|
6
6
|
DEFAULTS = { 'container' => 'app', 'workdir' => '/app', 'interactive' => false,
|
|
7
7
|
'remove' => true, 'env' => {}, 'ports' => [], 'volumes' => [] }.freeze
|
|
8
8
|
SECRET_PATTERN = /token|password|secret|credential|auth/i
|
|
9
|
+
# Which orchestration path `up`/`down`/`sync`/etc. take. Explicit rather than
|
|
10
|
+
# inferred from a `compose:` block's presence, so a config reader doesn't have
|
|
11
|
+
# to know that rule to predict which mode wip runs in.
|
|
12
|
+
MODES = %w[container compose].freeze
|
|
9
13
|
attr_reader :path
|
|
10
14
|
|
|
11
15
|
def initialize(raw, path = nil)
|
|
@@ -20,8 +24,9 @@ module Wip
|
|
|
20
24
|
def up_command = @raw.dig('up', 'command')
|
|
21
25
|
def dependencies = @raw['dependencies'] || {}
|
|
22
26
|
def network = defaults['network']
|
|
27
|
+
def mode = @raw['mode'] || 'container'
|
|
23
28
|
def compose = @raw['compose']
|
|
24
|
-
def compose? =
|
|
29
|
+
def compose? = mode == 'compose'
|
|
25
30
|
def compose_service = compose && compose['service']
|
|
26
31
|
def compose_file = compose && compose['file']
|
|
27
32
|
def compose_project = compose && compose['project']
|
|
@@ -50,8 +55,8 @@ module Wip
|
|
|
50
55
|
|
|
51
56
|
def to_h(redact: true)
|
|
52
57
|
value = { 'version' => 1, 'wslc' => { 'command' => wslc_command }, 'defaults' => defaults,
|
|
53
|
-
'up' => { 'command' => up_command }, '
|
|
54
|
-
'sync' => sync&.to_h,
|
|
58
|
+
'up' => { 'command' => up_command }, 'mode' => mode, 'dependencies' => dependencies,
|
|
59
|
+
'compose' => compose, 'sync' => sync&.to_h,
|
|
55
60
|
'commands' => commands.transform_values { |entry| defaults.merge('type' => 'exec').merge(entry) } }
|
|
56
61
|
redact ? redact_secrets(value) : value
|
|
57
62
|
end
|
|
@@ -62,6 +67,7 @@ module Wip
|
|
|
62
67
|
raise ConfigError, "Unsupported configuration version: #{@raw['version']}" unless (@raw['version'] || 1) == 1
|
|
63
68
|
raise ConfigError, 'up must be a mapping' if @raw.key?('up') && !@raw['up'].is_a?(Hash)
|
|
64
69
|
|
|
70
|
+
validate_mode!
|
|
65
71
|
validate_commands!
|
|
66
72
|
validate_dependencies!
|
|
67
73
|
validate_compose!
|
|
@@ -70,12 +76,20 @@ module Wip
|
|
|
70
76
|
|
|
71
77
|
def build_sync
|
|
72
78
|
SyncSettings.new(@raw['sync'], base: path && File.dirname(path.to_s),
|
|
73
|
-
workdir: defaults['workdir'], container: defaults['container']
|
|
79
|
+
workdir: defaults['workdir'], container: defaults['container'],
|
|
80
|
+
compose: compose?)
|
|
74
81
|
end
|
|
75
82
|
|
|
83
|
+
def validate_mode!
|
|
84
|
+
raise ConfigError, "mode must be one of #{MODES.join(', ')}" unless MODES.include?(mode)
|
|
85
|
+
raise ConfigError, 'mode: compose requires a compose: block' if compose? && !@raw.key?('compose')
|
|
86
|
+
raise ConfigError, 'a compose: block requires mode: compose' if @raw.key?('compose') && !compose?
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Forces SyncSettings to build (and so run its own validation, including
|
|
90
|
+
# sync.mode vs. mode) at load time instead of on first access.
|
|
76
91
|
def validate_sync!
|
|
77
|
-
|
|
78
|
-
raise ConfigError, 'sync is mutually exclusive with compose' if compose?
|
|
92
|
+
sync if @raw.key?('sync')
|
|
79
93
|
end
|
|
80
94
|
|
|
81
95
|
def validate_commands!
|
|
@@ -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
|
+
defaults:
|
|
14
|
+
container: app # TODO: name of the container wip creates
|
|
15
|
+
image: your/image:tag # TODO: image to run
|
|
16
|
+
workdir: /app
|
|
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
|
data/lib/wip/sync_settings.rb
CHANGED
|
@@ -26,19 +26,26 @@ module Wip
|
|
|
26
26
|
BASE_OPTIONS = %w[-r -l -t --whole-file].freeze
|
|
27
27
|
# Trailing mount options wslc/docker accept after the container path.
|
|
28
28
|
VOLUME_MODES = %w[ro rw z Z cached delegated consistent].freeze
|
|
29
|
+
# exec mirrors inside the already-running, wip-managed container (fast,
|
|
30
|
+
# but only correct when wip itself created that container with the sync
|
|
31
|
+
# mounts attached). run always mirrors from a disposable container, since
|
|
32
|
+
# compose owns its own services' mounts and never guarantees that shape.
|
|
33
|
+
SYNC_MODES = %w[exec run].freeze
|
|
29
34
|
|
|
30
|
-
attr_reader :target, :mount, :volume, :exclude, :binary, :extra_options, :interval
|
|
35
|
+
attr_reader :target, :mount, :volume, :exclude, :binary, :extra_options, :interval, :mode
|
|
31
36
|
|
|
32
|
-
def initialize(raw, base: nil, workdir: nil, container: nil)
|
|
37
|
+
def initialize(raw, base: nil, workdir: nil, container: nil, compose: false)
|
|
33
38
|
raise ConfigError, 'sync must be a mapping' unless raw.is_a?(Hash)
|
|
34
39
|
|
|
35
40
|
@base = base
|
|
36
41
|
assign_paths(raw, workdir: workdir, container: container)
|
|
37
42
|
assign_mirror(raw)
|
|
43
|
+
assign_mode(raw, compose: compose)
|
|
38
44
|
validate!
|
|
39
45
|
end
|
|
40
46
|
|
|
41
47
|
def delete? = !!@delete
|
|
48
|
+
def exec? = mode == 'exec'
|
|
42
49
|
|
|
43
50
|
# Expanded against the wip.yml directory so the mirror covers the same tree
|
|
44
51
|
# no matter which subdirectory wip was invoked from.
|
|
@@ -69,7 +76,7 @@ module Wip
|
|
|
69
76
|
def to_h
|
|
70
77
|
{ 'source' => source, 'target' => target, 'mount' => mount, 'volume' => volume,
|
|
71
78
|
'delete' => delete?, 'exclude' => exclude, 'command' => binary, 'options' => extra_options,
|
|
72
|
-
'interval' => interval }
|
|
79
|
+
'interval' => interval, 'mode' => mode }
|
|
73
80
|
end
|
|
74
81
|
|
|
75
82
|
private
|
|
@@ -89,6 +96,15 @@ module Wip
|
|
|
89
96
|
@interval = raw.key?('interval') ? raw['interval'] : DEFAULT_INTERVAL
|
|
90
97
|
end
|
|
91
98
|
|
|
99
|
+
def assign_mode(raw, compose:)
|
|
100
|
+
@mode = presence(raw['mode']) || (compose ? 'run' : 'exec')
|
|
101
|
+
raise ConfigError, "sync.mode must be one of #{SYNC_MODES.join(', ')}" unless SYNC_MODES.include?(@mode)
|
|
102
|
+
return unless compose && exec?
|
|
103
|
+
|
|
104
|
+
raise ConfigError, 'sync.mode: exec needs mode: container (compose owns its services’ mounts, ' \
|
|
105
|
+
'so it can’t guarantee the running container has the sync mounts attached)'
|
|
106
|
+
end
|
|
107
|
+
|
|
92
108
|
def validate!
|
|
93
109
|
raise ConfigError, 'sync.target must be an absolute path' unless @target.start_with?('/')
|
|
94
110
|
raise ConfigError, 'sync.mount must be an absolute path' unless @mount.start_with?('/')
|
data/lib/wip/version.rb
CHANGED
data/lib/wip.rb
CHANGED
|
@@ -13,6 +13,7 @@ require_relative 'wip/command_resolver'
|
|
|
13
13
|
require_relative 'wip/error_interpreter'
|
|
14
14
|
require_relative 'wip/command_builder'
|
|
15
15
|
require_relative 'wip/compose_bridge'
|
|
16
|
+
require_relative 'wip/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.
|
|
4
|
+
version: 0.9.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
|