kamal 2.8.2 → 2.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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/lib/kamal/cli/accessory.rb +14 -7
  3. data/lib/kamal/cli/app/boot.rb +1 -1
  4. data/lib/kamal/cli/app.rb +74 -115
  5. data/lib/kamal/cli/healthcheck/poller.rb +1 -1
  6. data/lib/kamal/cli/main.rb +2 -1
  7. data/lib/kamal/cli/proxy.rb +42 -35
  8. data/lib/kamal/cli/secrets.rb +2 -1
  9. data/lib/kamal/cli/server.rb +2 -1
  10. data/lib/kamal/cli/templates/secrets +1 -0
  11. data/lib/kamal/commander.rb +3 -2
  12. data/lib/kamal/commands/app/execution.rb +7 -1
  13. data/lib/kamal/commands/app.rb +1 -1
  14. data/lib/kamal/commands/proxy.rb +21 -2
  15. data/lib/kamal/configuration/accessory.rb +63 -26
  16. data/lib/kamal/configuration/boot.rb +4 -0
  17. data/lib/kamal/configuration/builder.rb +10 -3
  18. data/lib/kamal/configuration/docs/accessory.yml +37 -5
  19. data/lib/kamal/configuration/docs/boot.yml +12 -10
  20. data/lib/kamal/configuration/docs/configuration.yml +10 -1
  21. data/lib/kamal/configuration/docs/proxy.yml +24 -0
  22. data/lib/kamal/configuration/docs/ssh.yml +7 -4
  23. data/lib/kamal/configuration/docs/sshkit.yml +8 -0
  24. data/lib/kamal/configuration/env.rb +7 -3
  25. data/lib/kamal/configuration/proxy/boot.rb +4 -9
  26. data/lib/kamal/configuration/proxy/run.rb +143 -0
  27. data/lib/kamal/configuration/proxy.rb +2 -2
  28. data/lib/kamal/configuration/role.rb +15 -3
  29. data/lib/kamal/configuration/ssh.rb +18 -3
  30. data/lib/kamal/configuration/sshkit.rb +4 -0
  31. data/lib/kamal/configuration/validator/proxy.rb +20 -0
  32. data/lib/kamal/configuration/validator.rb +34 -1
  33. data/lib/kamal/configuration/volume.rb +11 -4
  34. data/lib/kamal/configuration.rb +32 -1
  35. data/lib/kamal/secrets/adapters/passbolt.rb +1 -2
  36. data/lib/kamal/secrets/adapters/test.rb +3 -1
  37. data/lib/kamal/secrets/dotenv/inline_command_substitution.rb +15 -1
  38. data/lib/kamal/secrets.rb +17 -6
  39. data/lib/kamal/sshkit_with_ext.rb +127 -10
  40. data/lib/kamal/utils.rb +3 -3
  41. data/lib/kamal/version.rb +1 -1
  42. metadata +2 -1
@@ -3,6 +3,7 @@ require "sshkit/dsl"
3
3
  require "net/scp"
4
4
  require "active_support/core_ext/hash/deep_merge"
5
5
  require "json"
6
+ require "resolv"
6
7
  require "concurrent/atomic/semaphore"
7
8
 
8
9
  class SSHKit::Backend::Abstract
@@ -18,8 +19,11 @@ class SSHKit::Backend::Abstract
18
19
  JSON.pretty_generate(JSON.parse(capture(*args, **kwargs)))
19
20
  end
20
21
 
21
- def puts_by_host(host, output, type: "App")
22
- puts "#{type} Host: #{host}\n#{output}\n\n"
22
+ def puts_by_host(host, output, type: "App", quiet: false)
23
+ unless quiet
24
+ puts "#{type} Host: #{host}"
25
+ end
26
+ puts "#{output}\n\n"
23
27
  end
24
28
 
25
29
  # Our execution pattern is for the CLI execute args lists returned
@@ -58,10 +62,50 @@ class SSHKit::Backend::Abstract
58
62
  end
59
63
 
60
64
  class SSHKit::Backend::Netssh::Configuration
61
- attr_accessor :max_concurrent_starts
65
+ attr_accessor :max_concurrent_starts, :dns_retries
62
66
  end
63
67
 
64
68
  class SSHKit::Backend::Netssh
69
+ module DnsRetriable
70
+ DNS_RETRY_BASE = 0.1
71
+ DNS_RETRY_MAX = 2.0
72
+ DNS_RETRY_JITTER = 0.1
73
+ DNS_ERROR_MESSAGE = /getaddrinfo|Temporary failure in name resolution|Name or service not known|nodename nor servname provided|No address associated|failed to look up|resolve/i
74
+
75
+ def with_dns_retry(hostname, retries: config.dns_retries, base: DNS_RETRY_BASE, max_sleep: DNS_RETRY_MAX, jitter: DNS_RETRY_JITTER)
76
+ attempts = 0
77
+ begin
78
+ attempts += 1
79
+ yield
80
+ rescue => error
81
+ raise unless retryable_dns_error?(error) && attempts <= retries
82
+
83
+ delay = dns_retry_sleep(attempts, base: base, jitter: jitter, max_sleep: max_sleep)
84
+ SSHKit.config.output.warn("Retrying DNS for #{hostname} (attempt #{attempts}/#{retries}) in #{format("%0.2f", delay)}s: #{error.message}")
85
+ sleep delay
86
+ retry
87
+ end
88
+ end
89
+
90
+ private
91
+ def retryable_dns_error?(error)
92
+ case error
93
+ when Resolv::ResolvError, Resolv::ResolvTimeout
94
+ true
95
+ when SocketError
96
+ error.message =~ DNS_ERROR_MESSAGE
97
+ else
98
+ error.cause && retryable_dns_error?(error.cause)
99
+ end
100
+ end
101
+
102
+ def dns_retry_sleep(attempt, base:, jitter:, max_sleep:)
103
+ sleep_for = [ base * (2 ** (attempt - 1)), max_sleep ].min
104
+ sleep_for += Kernel.rand * jitter
105
+ sleep_for
106
+ end
107
+ end
108
+
65
109
  module LimitConcurrentStartsClass
66
110
  attr_reader :start_semaphore
67
111
 
@@ -76,14 +120,31 @@ class SSHKit::Backend::Netssh
76
120
 
77
121
  class << self
78
122
  prepend LimitConcurrentStartsClass
123
+ prepend DnsRetriable
79
124
  end
80
125
 
126
+ module ConnectSsh
127
+ private
128
+ def connect_ssh(...)
129
+ Net::SSH.start(...)
130
+ end
131
+ end
132
+ include ConnectSsh
133
+
134
+ module DnsRetriableConnection
135
+ private
136
+ def connect_ssh(...)
137
+ self.class.with_dns_retry(host.hostname) { super }
138
+ end
139
+ end
140
+ prepend DnsRetriableConnection
141
+
81
142
  module LimitConcurrentStartsInstance
82
143
  private
83
144
  def with_ssh(&block)
84
145
  host.ssh_options = self.class.config.ssh_options.merge(host.ssh_options || {})
85
146
  self.class.pool.with(
86
- method(:start_with_concurrency_limit),
147
+ method(:connect_ssh),
87
148
  String(host.hostname),
88
149
  host.username,
89
150
  host.netssh_options,
@@ -91,17 +152,18 @@ class SSHKit::Backend::Netssh
91
152
  )
92
153
  end
93
154
 
94
- def start_with_concurrency_limit(*args)
155
+ def connect_ssh(...)
156
+ with_concurrency_limit { super }
157
+ end
158
+
159
+ def with_concurrency_limit(&block)
95
160
  if self.class.start_semaphore
96
- self.class.start_semaphore.acquire do
97
- Net::SSH.start(*args)
98
- end
161
+ self.class.start_semaphore.acquire(&block)
99
162
  else
100
- Net::SSH.start(*args)
163
+ yield
101
164
  end
102
165
  end
103
166
  end
104
-
105
167
  prepend LimitConcurrentStartsInstance
106
168
  end
107
169
 
@@ -148,3 +210,58 @@ module NetSshForwardingNoPuts
148
210
  end
149
211
 
150
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.8.2"
2
+ VERSION = "2.10.0"
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.8.2
4
+ version: 2.10.0
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