kamal 2.9.0 → 2.10.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.
@@ -77,6 +77,7 @@ class Kamal::Configuration
77
77
  ensure_one_host_for_ssl_roles
78
78
  ensure_unique_hosts_for_ssl_roles
79
79
  ensure_local_registry_remote_builder_has_ssh_url
80
+ ensure_no_conflicting_proxy_runs
80
81
  end
81
82
 
82
83
  def version=(version)
@@ -122,6 +123,14 @@ class Kamal::Configuration
122
123
  (roles + accessories).flat_map(&:hosts).uniq
123
124
  end
124
125
 
126
+ def host_roles(host)
127
+ roles.select { |role| role.hosts.include?(host) }
128
+ end
129
+
130
+ def host_accessories(host)
131
+ accessories.select { |accessory| accessory.hosts.include?(host) }
132
+ end
133
+
125
134
  def app_hosts
126
135
  roles.flat_map(&:hosts).uniq
127
136
  end
@@ -165,6 +174,11 @@ class Kamal::Configuration
165
174
  name
166
175
  end
167
176
 
177
+ def proxy_run(host)
178
+ # We validate that all the config are identical for a host
179
+ proxy_runs(host.to_s).first
180
+ end
181
+
168
182
  def repository
169
183
  [ registry.server, image ].compact.join("/")
170
184
  end
@@ -378,6 +392,19 @@ class Kamal::Configuration
378
392
  true
379
393
  end
380
394
 
395
+ def ensure_no_conflicting_proxy_runs
396
+ all_hosts.each do |host|
397
+ run_configs = proxy_runs(host)
398
+ if run_configs.uniq.size > 1
399
+ raise Kamal::ConfigurationError, "Conflicting proxy run configurations for host #{host}"
400
+ end
401
+ end
402
+ end
403
+
404
+ def proxy_runs(host)
405
+ (host_roles(host) + host_accessories(host)).map(&:proxy).compact.map(&:run).compact
406
+ end
407
+
381
408
  def role_names
382
409
  raw_config.servers.is_a?(Array) ? [ "web" ] : raw_config.servers.keys.sort
383
410
  end
@@ -5,7 +5,9 @@ class Kamal::Secrets::Adapters::Test < Kamal::Secrets::Adapters::Base
5
5
  end
6
6
 
7
7
  def fetch_secrets(secrets, from:, account:, session:)
8
- prefixed_secrets(secrets, from: from).to_h { |secret| [ secret, secret.reverse ] }
8
+ prefixed_secrets(secrets, from: from).to_h do |secret|
9
+ [ secret, secret.gsub("LPAREN", "(").gsub("RPAREN", ")").reverse ]
10
+ end
9
11
  end
10
12
 
11
13
  def check_dependencies!
@@ -1,4 +1,18 @@
1
1
  class Kamal::Secrets::Dotenv::InlineCommandSubstitution
2
+ # Unlike dotenv, this regex does not match escaped
3
+ # parentheses when looking for command substitutions.
4
+ INTERPOLATED_SHELL_COMMAND = /
5
+ (?<backslash>\\)? # is it escaped with a backslash?
6
+ \$ # literal $
7
+ (?<cmd> # collect command content for eval
8
+ \( # require opening paren
9
+ (?:\\.|[^()\\]|\g<cmd>)+ # allow any number of non-parens or escaped
10
+ # parens (by nesting the <cmd> expression
11
+ # recursively)
12
+ \) # require closing paren
13
+ )
14
+ /x
15
+
2
16
  class << self
3
17
  def install!
4
18
  ::Dotenv::Parser.substitutions.map! { |sub| sub == ::Dotenv::Substitutions::Command ? self : sub }
@@ -6,7 +20,7 @@ class Kamal::Secrets::Dotenv::InlineCommandSubstitution
6
20
 
7
21
  def call(value, env, overwrite: false)
8
22
  # Process interpolated shell commands
9
- value.gsub(Dotenv::Substitutions::Command.singleton_class::INTERPOLATED_SHELL_COMMAND) do |*|
23
+ value.gsub(INTERPOLATED_SHELL_COMMAND) do |*|
10
24
  # Eliminate opening and closing parentheses
11
25
  command = $LAST_MATCH_INFO[:cmd][1..-2]
12
26
 
data/lib/kamal/secrets.rb CHANGED
@@ -10,10 +10,7 @@ class Kamal::Secrets
10
10
  end
11
11
 
12
12
  def [](key)
13
- # Fetching secrets may ask the user for input, so ensure only one thread does that
14
- @mutex.synchronize do
15
- secrets.fetch(key)
16
- end
13
+ synchronized_fetch(key)
17
14
  rescue KeyError
18
15
  if secrets_files.present?
19
16
  raise Kamal::ConfigurationError, "Secret '#{key}' not found in #{secrets_files.join(", ")}"
@@ -30,6 +27,12 @@ class Kamal::Secrets
30
27
  @secrets_files ||= secrets_filenames.select { |f| File.exist?(f) }
31
28
  end
32
29
 
30
+ def key?(key)
31
+ synchronized_fetch(key).present?
32
+ rescue KeyError
33
+ false
34
+ end
35
+
33
36
  private
34
37
  def secrets
35
38
  @secrets ||= secrets_files.inject({}) do |secrets, secrets_file|
@@ -40,4 +43,11 @@ class Kamal::Secrets
40
43
  def secrets_filenames
41
44
  [ "#{@secrets_path}-common", "#{@secrets_path}#{(".#{@destination}" if @destination)}" ]
42
45
  end
46
+
47
+ def synchronized_fetch(key)
48
+ # Fetching secrets may ask the user for input, so ensure only one thread does that
49
+ @mutex.synchronize do
50
+ secrets.fetch(key)
51
+ end
52
+ end
43
53
  end
@@ -210,3 +210,58 @@ module NetSshForwardingNoPuts
210
210
  end
211
211
 
212
212
  Net::SSH::Service::Forward.prepend NetSshForwardingNoPuts
213
+
214
+ module SSHKitDslRoles
215
+ # Execute on hosts grouped by role.
216
+ #
217
+ # Unlike `on()` which deduplicates hosts, this allows the same host to have
218
+ # multiple concurrent connections when it appears in multiple roles.
219
+ #
220
+ # Options:
221
+ # hosts: The hosts to run on (required)
222
+ # parallel: When true, each role runs in its own thread with separate
223
+ # connections. When false, hosts run in parallel but roles on each
224
+ # host run sequentially (default: true)
225
+ #
226
+ # Example:
227
+ # on_roles(roles) do |host, role|
228
+ # # deploy role to host
229
+ # end
230
+ def on_roles(roles, hosts:, parallel: true, &block)
231
+ if parallel
232
+ threads = roles.filter_map do |role|
233
+ if (role_hosts = role.hosts & hosts).any?
234
+ Thread.new do
235
+ on(role_hosts) { |host| instance_exec(host, role, &block) }
236
+ rescue StandardError => e
237
+ raise SSHKit::Runner::ExecuteError.new(e), "Exception while executing on #{role}: #{e.message}"
238
+ end
239
+ end
240
+ end
241
+
242
+ exceptions = []
243
+ threads.each do |t|
244
+ begin
245
+ t.join
246
+ rescue SSHKit::Runner::ExecuteError => e
247
+ exceptions << e
248
+ end
249
+ end
250
+
251
+ if exceptions.one?
252
+ raise exceptions.first
253
+ elsif exceptions.many?
254
+ raise exceptions.first, [ "Exceptions on #{exceptions.count} roles:", exceptions.map(&:message) ].join("\n")
255
+ end
256
+ else
257
+ # Host-first iteration: hosts run in parallel, roles on each host run sequentially
258
+ on(hosts) do |host|
259
+ roles.each do |role|
260
+ instance_exec(host, role, &block) if role.hosts.include?(host.to_s)
261
+ end
262
+ end
263
+ end
264
+ end
265
+ end
266
+
267
+ SSHKit::DSL.prepend SSHKitDslRoles
data/lib/kamal/utils.rb CHANGED
@@ -21,11 +21,11 @@ module Kamal::Utils
21
21
  end
22
22
 
23
23
  # Returns a list of shell-dashed option arguments. If the value is true, it's treated like a value-less option.
24
- def optionize(args, with: nil)
24
+ def optionize(args, with: nil, escape: true)
25
25
  options = if with
26
- flatten_args(args).collect { |(key, value)| value == true ? "--#{key}" : "--#{key}#{with}#{escape_shell_value(value)}" }
26
+ flatten_args(args).collect { |(key, value)| value == true ? "--#{key}" : "--#{key}#{with}#{escape ? escape_shell_value(value) : value}" }
27
27
  else
28
- flatten_args(args).collect { |(key, value)| [ "--#{key}", value == true ? nil : escape_shell_value(value) ] }
28
+ flatten_args(args).collect { |(key, value)| [ "--#{key}", value == true ? nil : escape ? escape_shell_value(value) : value ] }
29
29
  end
30
30
 
31
31
  options.flatten.compact
data/lib/kamal/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Kamal
2
- VERSION = "2.9.0"
2
+ VERSION = "2.10.1"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kamal
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.9.0
4
+ version: 2.10.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Heinemeier Hansson
@@ -299,6 +299,7 @@ files:
299
299
  - lib/kamal/configuration/logging.rb
300
300
  - lib/kamal/configuration/proxy.rb
301
301
  - lib/kamal/configuration/proxy/boot.rb
302
+ - lib/kamal/configuration/proxy/run.rb
302
303
  - lib/kamal/configuration/registry.rb
303
304
  - lib/kamal/configuration/role.rb
304
305
  - lib/kamal/configuration/servers.rb