wslc-wip 0.3.1 → 0.4.1

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: d9b186a9b511381675ce6b5c7073292bc3ba8401287807c2f088d722f7cc1d7c
4
- data.tar.gz: 84a5b9b0ed0c9a3ada3ffe06eb793c12192d36d76b22197c9310776ddbe28c18
3
+ metadata.gz: 5808f27cf9f20fab582caacc33a35f4727d1b74764b8e541fe9f10a82ec8ceaa
4
+ data.tar.gz: 79c031022523b9ea21dfc79d7b160bcc46ab71cebe9f98d89050758ba581d986
5
5
  SHA512:
6
- metadata.gz: b46b8daa014806f40f361a5a8fb696cfbfa66f7e0652be23de5d328aaa13401248d640126767c39f1d87ca0329e2428e4a3abe7e19fb5d94e139209e43421646
7
- data.tar.gz: ed2eb9f43b08a29f8ac48d16e2820f7aa088bd8d8eb2e8db5f27c22b145f4e26e082f6b0efda81db9e04d395dc7a90a96a9b211b5d723ebd859fd0605899f28d
6
+ metadata.gz: 4b64f0d01f0f54476b5acacc7094dc6c54f2ea5ed480272644b20839a9a1d3217d9c32748935eb20d75f5ee3672012d6c8799dfba8192b1fa656e16a7c65380f
7
+ data.tar.gz: 5ab533ffee65252d885c20862c32450f3d4b7c5c4a8019cf2314e5075b9c3f5ea49efd551f08cf73c170663b90cebe420533be7041e5d20c6eff1988e6b3a758
data/README.md CHANGED
@@ -50,6 +50,7 @@ defaults:
50
50
  workdir: /app
51
51
  interactive: false
52
52
  remove: true
53
+ network: app-tier # optional; shared with `dependencies` so containers can resolve each other by name
53
54
  env:
54
55
  RAILS_ENV: development
55
56
  PORT: "3000"
@@ -60,6 +61,15 @@ defaults:
60
61
  up:
61
62
  command: server # command `wip up` passes to the image when it creates the container
62
63
  # (omit to use the image's default CMD)
64
+ dependencies:
65
+ redis:
66
+ image: redis:latest
67
+ development.mysql:
68
+ image: mysql:8.0
69
+ command: --default-authentication-plugin=mysql_native_password
70
+ env:
71
+ MYSQL_ROOT_PASSWORD: password
72
+ MYSQL_DATABASE: development
63
73
  commands:
64
74
  rails:
65
75
  type: exec
@@ -91,6 +101,16 @@ commands:
91
101
  credential, or auth. Keep real secrets out of the config file and in your runtime environment
92
102
  instead.
93
103
 
104
+ ### Dependency containers
105
+
106
+ If your app needs sidecar services (a database, Redis, ...), declare them under `dependencies`
107
+ and set `defaults.network`. `wip up` creates the network first (if it doesn't exist), then brings
108
+ up each dependency by name before the main container — so `bin/rails c` (or anything else run
109
+ inside the main container) can reach `development.mysql`/`redis`/etc. by their dependency name,
110
+ the same way Compose's service names resolve. `wip down` tears the main container and all
111
+ dependencies down (the network itself is left in place). Each dependency entry accepts `image`
112
+ (required), `command`, `env`, `ports`, `volumes`, and `workdir` — the same shape as `defaults`.
113
+
94
114
  ## Commands
95
115
 
96
116
  | Command | Description |
@@ -99,8 +119,8 @@ instead.
99
119
  | `wip doctor` | Diagnose WSL2, interop, WSLC, config, architecture, and Git |
100
120
  | `wip config` | Print the effective configuration (secrets masked) |
101
121
  | `wip build -- --no-cache` | Build the image from the `build` definition |
102
- | `wip up [-d]` | Start `defaults.container` (creating it with `up.command` if missing). `-d` runs it in the background |
103
- | `wip down` | Stop and remove `defaults.container` |
122
+ | `wip up [-d]` | Start `defaults.container` and its `dependencies` (creating any that are missing, on `defaults.network` if set). `-d` runs the main container in the background |
123
+ | `wip down` | Stop and remove `defaults.container` and its `dependencies` |
104
124
  | `wip exec [--no-interactive] COMMAND...` | Run a command in the existing container |
105
125
  | `wip run [--no-interactive] COMMAND...` | Run a command in a new `--rm` container |
106
126
  | `wip shell` | Open the configured shell, falling back to `bash` then `sh` |
@@ -165,10 +185,10 @@ checklist.
165
185
 
166
186
  ## Not in the initial release
167
187
 
168
- Compose compatibility, a resident/daemon process, a GUI, PowerShell-specific tuning, direct
169
- registry API/manifest parsing, self-update, and plugins are all unimplemented. Likely future
170
- additions: a richer config schema, lifecycle hooks, multi-container support, platform selection,
171
- and more detailed diagnostics.
188
+ Full Compose compatibility (multiple networks, `depends_on` ordering/health checks, profiles), a
189
+ resident/daemon process, a GUI, PowerShell-specific tuning, direct registry API/manifest parsing,
190
+ self-update, and plugins are all unimplemented. Likely future additions: a richer config schema,
191
+ lifecycle hooks, platform selection, and more detailed diagnostics.
172
192
 
173
193
  ## License
174
194
 
data/lib/wip/cli.rb CHANGED
@@ -53,24 +53,22 @@ module Wip
53
53
  execute(builder.build(settings: load_config.command('build') || {}, extra: extra))
54
54
  end
55
55
 
56
- desc 'up', 'Start the configured container, creating it if necessary'
56
+ desc 'up', 'Start the configured container and its dependencies, creating them if necessary'
57
57
  option :detach, type: :boolean, default: false, aliases: '-d'
58
58
  def up
59
- container = load_config.defaults['container']
60
- interactive = builder.tty?(!options[:detach])
61
- if container_exists?
62
- warn "wip: starting existing container '#{container}'"
63
- execute(builder.start(detach: options[:detach]), interactive: interactive)
64
- else
65
- warn "wip: container '#{container}' not found, creating it"
66
- execute(builder.up(detach: options[:detach]), interactive: interactive)
67
- end
59
+ ensure_network
60
+ load_config.dependencies.each_key { |name| ensure_dependency(name) }
61
+ ensure_container
68
62
  end
69
63
 
70
- desc 'down', 'Stop and remove the configured container'
64
+ desc 'down', 'Stop and remove the configured container and its dependencies'
71
65
  def down
72
66
  execute(builder.down, exit_on_failure: false)
73
67
  execute(builder.remove, exit_on_failure: false)
68
+ load_config.dependencies.each_key do |name|
69
+ execute(builder.dependency_down(name), exit_on_failure: false)
70
+ execute(builder.dependency_remove(name), exit_on_failure: false)
71
+ end
74
72
  end
75
73
 
76
74
  desc 'exec COMMAND...', 'Execute a command in the running container'
@@ -126,12 +124,53 @@ module Wip
126
124
  code
127
125
  end
128
126
 
129
- def container_exists?
127
+ def resource_exists?(find_command)
130
128
  out = StringIO.new
131
- code = CommandRunner.new(stdout: out, stderr: StringIO.new).run(builder.find)
129
+ code = CommandRunner.new(stdout: out, stderr: StringIO.new).run(find_command)
132
130
  code.zero? && !JSON.parse(out.string).empty?
133
131
  rescue JSON::ParserError
134
132
  false
135
133
  end
134
+
135
+ def ensure_network
136
+ network = load_config.network
137
+ return unless network
138
+ return if network_exists?(network)
139
+
140
+ warn "wip: creating network '#{network}'"
141
+ execute(builder.network_create, exit_on_failure: false)
142
+ end
143
+
144
+ def network_exists?(network)
145
+ out = StringIO.new
146
+ code = CommandRunner.new(stdout: out, stderr: StringIO.new).run(builder.network_list)
147
+ return false unless code.zero?
148
+
149
+ JSON.parse(out.string).any? { |entry| entry['Name'] == network }
150
+ rescue JSON::ParserError
151
+ false
152
+ end
153
+
154
+ def ensure_dependency(name)
155
+ if resource_exists?(builder.dependency_find(name))
156
+ warn "wip: starting existing dependency '#{name}'"
157
+ execute(builder.dependency_start(name))
158
+ else
159
+ warn "wip: dependency '#{name}' not found, creating it"
160
+ execute(builder.dependency_up(name))
161
+ end
162
+ end
163
+
164
+ def ensure_container
165
+ container = load_config.defaults['container']
166
+ interactive = builder.tty?(!options[:detach])
167
+ if resource_exists?(builder.find)
168
+ warn "wip: starting existing container '#{container}'"
169
+ execute(builder.start(detach: options[:detach]), interactive: interactive)
170
+ else
171
+ warn "wip: container '#{container}' not found, creating it"
172
+ execute(builder.up(detach: options[:detach]), interactive: interactive)
173
+ end
174
+ end
136
175
  end
137
176
  end
@@ -29,6 +29,7 @@ module Wip
29
29
  def up(detach: false)
30
30
  values = @config.defaults
31
31
  command = [@wslc, 'run', '--name', required(values, 'container')]
32
+ command.push('--network', @config.network) if @config.network
32
33
  command << '-d' if detach
33
34
  command << '-it' if !detach && tty?(true)
34
35
  command.concat(options(values)).push(required(values, 'image'))
@@ -55,6 +56,40 @@ module Wip
55
56
  [@wslc, 'remove', '-f', required(@config.defaults, 'container')]
56
57
  end
57
58
 
59
+ def network_create
60
+ [@wslc, 'network', 'create', required_network]
61
+ end
62
+
63
+ def network_list
64
+ [@wslc, 'network', 'list', '--format', 'json']
65
+ end
66
+
67
+ def dependency_up(name, detach: true)
68
+ values = dependency_values(name)
69
+ command = [@wslc, 'run', '--name', name.to_s]
70
+ command.push('--network', @config.network) if @config.network
71
+ command << '-d' if detach
72
+ command.concat(options(values)).push(required(values, 'image'))
73
+ command.concat(Shellwords.split(values['command'].to_s)) unless values['command'].to_s.empty?
74
+ command
75
+ end
76
+
77
+ def dependency_start(name)
78
+ [@wslc, 'start', name.to_s]
79
+ end
80
+
81
+ def dependency_find(name)
82
+ [@wslc, 'list', '--all', '--filter', "name=#{name}", '--format', 'json']
83
+ end
84
+
85
+ def dependency_down(name)
86
+ [@wslc, 'stop', name.to_s]
87
+ end
88
+
89
+ def dependency_remove(name)
90
+ [@wslc, 'remove', '-f', name.to_s]
91
+ end
92
+
58
93
  def build(settings:, extra: [])
59
94
  values = @config.defaults.merge(settings)
60
95
  context = values['context'] || '.'
@@ -95,5 +130,16 @@ module Wip
95
130
 
96
131
  value
97
132
  end
133
+
134
+ def required_network
135
+ network = @config.network
136
+ raise ConfigError, 'Configured network must not be empty' if network.to_s.empty?
137
+
138
+ network
139
+ end
140
+
141
+ def dependency_values(name)
142
+ @config.dependency(name) || raise(ConfigError, "Unknown dependency: #{name}")
143
+ end
98
144
  end
99
145
  end
data/lib/wip/config.rb CHANGED
@@ -18,6 +18,8 @@ module Wip
18
18
  def commands = @raw['commands'] || {}
19
19
  def defaults = DEFAULTS.merge(@raw['defaults'] || {})
20
20
  def up_command = @raw.dig('up', 'command')
21
+ def dependencies = @raw['dependencies'] || {}
22
+ def network = defaults['network']
21
23
 
22
24
  def command(name)
23
25
  entry = commands[name.to_s]
@@ -26,9 +28,16 @@ module Wip
26
28
  defaults.merge('type' => 'exec').merge(entry)
27
29
  end
28
30
 
31
+ def dependency(name)
32
+ entry = dependencies[name.to_s]
33
+ return unless entry
34
+
35
+ { 'workdir' => nil, 'env' => {}, 'ports' => [], 'volumes' => [] }.merge(entry)
36
+ end
37
+
29
38
  def to_h(redact: true)
30
39
  value = { 'version' => 1, 'wslc' => { 'command' => wslc_command }, 'defaults' => defaults,
31
- 'up' => { 'command' => up_command },
40
+ 'up' => { 'command' => up_command }, 'dependencies' => dependencies,
32
41
  'commands' => commands.transform_values { |entry| defaults.merge('type' => 'exec').merge(entry) } }
33
42
  redact ? redact_secrets(value) : value
34
43
  end
@@ -37,12 +46,24 @@ module Wip
37
46
 
38
47
  def validate!
39
48
  raise ConfigError, "Unsupported configuration version: #{@raw['version']}" unless (@raw['version'] || 1) == 1
40
- raise ConfigError, 'commands must be a mapping' unless commands.is_a?(Hash)
41
49
  raise ConfigError, 'up must be a mapping' if @raw.key?('up') && !@raw['up'].is_a?(Hash)
42
50
 
51
+ validate_commands!
52
+ validate_dependencies!
53
+ end
54
+
55
+ def validate_commands!
56
+ raise ConfigError, 'commands must be a mapping' unless commands.is_a?(Hash)
57
+
43
58
  commands.each { |name, entry| validate_command!(name, entry) }
44
59
  end
45
60
 
61
+ def validate_dependencies!
62
+ raise ConfigError, 'dependencies must be a mapping' unless dependencies.is_a?(Hash)
63
+
64
+ dependencies.each { |name, entry| validate_dependency!(name, entry) }
65
+ end
66
+
46
67
  def validate_command!(name, entry)
47
68
  raise ConfigError, "commands.#{name} must be a mapping" unless entry.is_a?(Hash)
48
69
 
@@ -52,6 +73,13 @@ module Wip
52
73
  entry['env']&.transform_values!(&:to_s)
53
74
  end
54
75
 
76
+ def validate_dependency!(name, entry)
77
+ raise ConfigError, "dependencies.#{name} must be a mapping" unless entry.is_a?(Hash)
78
+ raise ConfigError, "dependencies.#{name} must set image" if entry['image'].to_s.empty?
79
+
80
+ entry['env']&.transform_values!(&:to_s)
81
+ end
82
+
55
83
  def stringify(object)
56
84
  case object
57
85
  when Hash then object.to_h { |key, value| [key.to_s, stringify(value)] }
data/lib/wip/doctor.rb CHANGED
@@ -15,9 +15,8 @@ module Wip
15
15
 
16
16
  def call
17
17
  results = []
18
- results << result(@environment.wsl2? ? :ok : :fail, 'Running on WSL2', 'Not running on WSL2')
19
- results << result(@environment.windows_interop? ? :ok : :fail, 'Windows executable interoperability is enabled',
20
- 'Windows executable interoperability is disabled')
18
+ results << result(@environment.wsl2? ? :ok : :fail, *wsl2_messages)
19
+ results << interop_result unless @environment.windows?
21
20
  results << Result.new(:ok, "Architecture: #{@environment.architecture}")
22
21
  config = load_config(results)
23
22
  command = resolve(config, results) if config
@@ -29,6 +28,17 @@ module Wip
29
28
 
30
29
  private
31
30
 
31
+ def wsl2_messages
32
+ return ['WSL2 is available', 'WSL2 is not available'] if @environment.windows?
33
+
34
+ ['Running on WSL2', 'Not running on WSL2']
35
+ end
36
+
37
+ def interop_result
38
+ result(@environment.windows_interop? ? :ok : :fail, 'Windows executable interoperability is enabled',
39
+ 'Windows executable interoperability is disabled')
40
+ end
41
+
32
42
  def load_config(results)
33
43
  config = @loader.load
34
44
  invalid = %w[container image].select { |key| config.defaults[key].to_s.empty? }
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'open3'
4
+
3
5
  module Wip
4
6
  # Detects WSL2, Windows interop, and architecture facts about the host.
5
7
  class Environment
@@ -8,7 +10,11 @@ module Wip
8
10
  @stdout = stdout
9
11
  end
10
12
 
13
+ def windows? = Gem.win_platform?
14
+
11
15
  def wsl2?
16
+ return wsl2_backend_available? if windows?
17
+
12
18
  File.read('/proc/version').match?(/microsoft.*WSL2/i)
13
19
  rescue Errno::ENOENT
14
20
  false
@@ -18,9 +24,20 @@ module Wip
18
24
  def interactive? = @stdin.tty? && @stdout.tty?
19
25
 
20
26
  def architecture
21
- machine = `uname -m`.strip
22
- { 'x86_64' => 'linux/amd64', 'aarch64' => 'linux/arm64', 'arm64' => 'linux/arm64' }.fetch(machine,
23
- "linux/#{machine}")
27
+ machine = RbConfig::CONFIG['host_cpu']
28
+ { 'x86_64' => 'linux/amd64', 'x64' => 'linux/amd64', 'aarch64' => 'linux/arm64', 'arm64' => 'linux/arm64' }
29
+ .fetch(machine, "linux/#{machine}")
30
+ end
31
+
32
+ private
33
+
34
+ # On native Windows there is no /proc/version to read, so ask Windows
35
+ # itself whether the WSL2 backend that WSLC depends on is installed.
36
+ def wsl2_backend_available?
37
+ _output, status = Open3.capture2e('wsl.exe', '--status')
38
+ status.success?
39
+ rescue Errno::ENOENT
40
+ false
24
41
  end
25
42
  end
26
43
  end
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.3.1'
4
+ VERSION = '0.4.1'
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.3.1
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wip contributors