kamal 2.11.0 → 2.12.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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/lib/kamal/cli/accessory.rb +48 -39
  3. data/lib/kamal/cli/app.rb +57 -48
  4. data/lib/kamal/cli/base.rb +99 -11
  5. data/lib/kamal/cli/build.rb +9 -6
  6. data/lib/kamal/cli/lock.rb +5 -16
  7. data/lib/kamal/cli/main.rb +59 -53
  8. data/lib/kamal/cli/proxy.rb +9 -9
  9. data/lib/kamal/cli/prune.rb +3 -3
  10. data/lib/kamal/cli/server.rb +24 -15
  11. data/lib/kamal/cli/templates/sample_hooks/docker-setup.sample +1 -1
  12. data/lib/kamal/cli/templates/sample_hooks/post-app-boot.sample +1 -1
  13. data/lib/kamal/cli/templates/sample_hooks/post-deploy.sample +1 -1
  14. data/lib/kamal/cli/templates/sample_hooks/post-proxy-reboot.sample +1 -1
  15. data/lib/kamal/cli/templates/sample_hooks/pre-app-boot.sample +1 -1
  16. data/lib/kamal/cli/templates/sample_hooks/pre-build.sample +1 -1
  17. data/lib/kamal/cli/templates/sample_hooks/pre-proxy-reboot.sample +1 -1
  18. data/lib/kamal/cli/templates/secrets +4 -0
  19. data/lib/kamal/commander.rb +54 -1
  20. data/lib/kamal/commands/accessory.rb +2 -2
  21. data/lib/kamal/commands/app/logging.rb +1 -1
  22. data/lib/kamal/commands/app.rb +1 -1
  23. data/lib/kamal/commands/builder/clone.rb +2 -1
  24. data/lib/kamal/configuration/accessory.rb +13 -5
  25. data/lib/kamal/configuration/docs/configuration.yml +18 -3
  26. data/lib/kamal/configuration/docs/env.yml +6 -4
  27. data/lib/kamal/configuration/docs/output.yml +25 -0
  28. data/lib/kamal/configuration/docs/role.yml +1 -0
  29. data/lib/kamal/configuration/docs/ssh.yml +8 -0
  30. data/lib/kamal/configuration/output.rb +34 -0
  31. data/lib/kamal/configuration/proxy/run.rb +9 -0
  32. data/lib/kamal/configuration/role.rb +18 -6
  33. data/lib/kamal/configuration/ssh.rb +5 -1
  34. data/lib/kamal/configuration/validator.rb +14 -2
  35. data/lib/kamal/configuration.rb +6 -1
  36. data/lib/kamal/git.rb +1 -1
  37. data/lib/kamal/otel_shipper.rb +176 -0
  38. data/lib/kamal/output/base_logger.rb +29 -0
  39. data/lib/kamal/output/file_logger.rb +51 -0
  40. data/lib/kamal/output/formatter.rb +36 -0
  41. data/lib/kamal/output/otel_logger.rb +70 -0
  42. data/lib/kamal/secrets/adapters/aws_secrets_manager.rb +10 -2
  43. data/lib/kamal/secrets/adapters/passbolt.rb +1 -1
  44. data/lib/kamal/secrets.rb +1 -1
  45. data/lib/kamal/sshkit_with_ext.rb +9 -4
  46. data/lib/kamal/version.rb +1 -1
  47. metadata +9 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c8f7525b3ed804be2810d0af353c254b8c7ebf4d5cf5916dbfa97d5bf4615d54
4
- data.tar.gz: adfac172b3e4b45bc00092317bde87395fcedecf107b30c8500ab58097ef22d8
3
+ metadata.gz: 5d5aa65d23b2e2b68f8942617bb5ab754d6cb39e0444a61a95134590b6aadcf3
4
+ data.tar.gz: ecc576e1a0b82a0cc339d16a90cac74ecf47b7cdfb793fd5ff588fca61dc1d69
5
5
  SHA512:
6
- metadata.gz: 05150f9253685ce18dd910cf809a99e51449b7452536e6c3f39c8e37d5ce3db4806bd8e198ed3e29aa872523eb8937db5b58faee036619f8e5b43d1084eebffc
7
- data.tar.gz: 47e77a8d347c44aff4326f9927a0933c0240b5bfdb90f1b623e85ecdad8ba48c0e3443d43cd062b265aa29e2dbd47d778499be39a079d3cb93d8856cb0388108
6
+ metadata.gz: 8fe2acb803dd2b1930d3a9ecf62a6f35a2e1078fcd3e2950dfac0095e7ab5354d85e004b80581151e75a7b3cef247a0fefd442059a96ed39ffa9b10b67b12adc
7
+ data.tar.gz: 9ce3a70b368f59a310403f3c73e9ae438436dcaa174a039248d6b976f0e345a380d185d7409bdc7b84371a3708fd413fd9e96f682909b2820ddf12edb7a0dd7e
@@ -4,7 +4,7 @@ require "concurrent/array"
4
4
  class Kamal::Cli::Accessory < Kamal::Cli::Base
5
5
  desc "boot [NAME]", "Boot new accessory service on host (use NAME=all to boot all accessories)"
6
6
  def boot(name, prepare: true)
7
- with_lock do
7
+ modify(lock: true) do
8
8
  if name == "all"
9
9
  KAMAL.accessory_names.each { |accessory_name| boot(accessory_name) }
10
10
  else
@@ -42,7 +42,7 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
42
42
 
43
43
  desc "upload [NAME]", "Upload accessory files to host", hide: true
44
44
  def upload(name)
45
- with_lock do
45
+ modify(lock: true) do
46
46
  with_accessory(name) do |accessory, hosts|
47
47
  on(hosts) do
48
48
  accessory.files.each do |(local, config)|
@@ -61,7 +61,7 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
61
61
 
62
62
  desc "directories [NAME]", "Create accessory directories on host", hide: true
63
63
  def directories(name)
64
- with_lock do
64
+ modify(lock: true) do
65
65
  with_accessory(name) do |accessory, hosts|
66
66
  on(hosts) do
67
67
  accessory.directories.each do |(local, config)|
@@ -76,7 +76,7 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
76
76
 
77
77
  desc "reboot [NAME]", "Reboot existing accessory on host (stop container, remove container, start new container; use NAME=all to boot all accessories)"
78
78
  def reboot(name)
79
- with_lock do
79
+ modify(lock: true) do
80
80
  if name == "all"
81
81
  KAMAL.accessory_names.each { |accessory_name| reboot(accessory_name) }
82
82
  else
@@ -91,7 +91,7 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
91
91
 
92
92
  desc "start [NAME]", "Start existing accessory container on host"
93
93
  def start(name)
94
- with_lock do
94
+ modify(lock: true) do
95
95
  with_accessory(name) do |accessory, hosts|
96
96
  on(hosts) do
97
97
  execute *KAMAL.auditor.record("Started #{name} accessory"), verbosity: :debug
@@ -107,7 +107,7 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
107
107
 
108
108
  desc "stop [NAME]", "Stop existing accessory container on host"
109
109
  def stop(name)
110
- with_lock do
110
+ modify(lock: true) do
111
111
  with_accessory(name) do |accessory, hosts|
112
112
  on(hosts) do
113
113
  execute *KAMAL.auditor.record("Stopped #{name} accessory"), verbosity: :debug
@@ -124,7 +124,7 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
124
124
 
125
125
  desc "restart [NAME]", "Restart existing accessory container on host"
126
126
  def restart(name)
127
- with_lock do
127
+ modify(lock: true) do
128
128
  stop(name)
129
129
  start(name)
130
130
  end
@@ -146,36 +146,45 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
146
146
  desc "exec [NAME] [CMD...]", "Execute a custom command on servers within the accessory container (use --help to show options)"
147
147
  option :interactive, aliases: "-i", type: :boolean, default: false, desc: "Execute command over ssh for an interactive shell (use for console/bash)"
148
148
  option :reuse, type: :boolean, default: false, desc: "Reuse currently running container instead of starting a new one"
149
+ option :raw, type: :boolean, default: false, desc: "Output raw, unmodified stdout"
149
150
  def exec(name, *cmd)
150
- pre_connect_if_required
151
+ raw = options[:raw]
151
152
 
152
- cmd = Kamal::Utils.join_commands(cmd)
153
- quiet = options[:quiet]
153
+ if raw && options[:interactive]
154
+ raise ArgumentError, "Raw is not compatible with interactive"
155
+ end
154
156
 
155
- with_accessory(name) do |accessory, hosts|
156
- case
157
- when options[:interactive] && options[:reuse]
158
- say "Launching interactive command via SSH from existing container...", :magenta
159
- run_locally { exec accessory.execute_in_existing_container_over_ssh(cmd) }
160
-
161
- when options[:interactive]
162
- say "Launching interactive command via SSH from new container...", :magenta
163
- on(accessory.hosts.first) { execute *KAMAL.registry.login }
164
- run_locally { exec accessory.execute_in_new_container_over_ssh(cmd) }
165
-
166
- when options[:reuse]
167
- say "Launching command from existing container...", :magenta
168
- on(hosts) do |host|
169
- execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on #{name} accessory"), verbosity: :debug
170
- puts_by_host host, capture_with_info(*accessory.execute_in_existing_container(cmd)), quiet: quiet
171
- end
157
+ with_raw_output(raw) do
158
+ pre_connect_if_required
172
159
 
173
- else
174
- say "Launching command from new container...", :magenta
175
- on(hosts) do |host|
176
- execute *KAMAL.registry.login
177
- execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on #{name} accessory"), verbosity: :debug
178
- puts_by_host host, capture_with_info(*accessory.execute_in_new_container(cmd)), quiet: quiet
160
+ cmd = Kamal::Utils.join_commands(cmd)
161
+ quiet = options[:quiet]
162
+
163
+ with_accessory(name) do |accessory, hosts|
164
+ case
165
+ when options[:interactive] && options[:reuse]
166
+ say "Launching interactive command via SSH from existing container...", :magenta
167
+ run_locally { exec accessory.execute_in_existing_container_over_ssh(cmd) }
168
+
169
+ when options[:interactive]
170
+ say "Launching interactive command via SSH from new container...", :magenta
171
+ on(accessory.hosts.first) { execute *KAMAL.registry.login }
172
+ run_locally { exec accessory.execute_in_new_container_over_ssh(cmd) }
173
+
174
+ when options[:reuse]
175
+ say "Launching command from existing container...", :magenta
176
+ on(hosts) do |host|
177
+ execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on #{name} accessory"), verbosity: :debug
178
+ puts_by_host host, capture_with_info(*accessory.execute_in_existing_container(cmd), strip: !raw), quiet: quiet, raw: raw
179
+ end
180
+
181
+ else
182
+ say "Launching command from new container...", :magenta
183
+ on(hosts) do |host|
184
+ execute *KAMAL.registry.login
185
+ execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on #{name} accessory"), verbosity: :debug
186
+ puts_by_host host, capture_with_info(*accessory.execute_in_new_container(cmd), strip: !raw), quiet: quiet, raw: raw
187
+ end
179
188
  end
180
189
  end
181
190
  end
@@ -213,7 +222,7 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
213
222
 
214
223
  desc "pull_image [NAME]", "Pull accessory image on host", hide: true
215
224
  def pull_image(name)
216
- with_lock do
225
+ modify(lock: true) do
217
226
  with_accessory(name) do |accessory, hosts|
218
227
  on(hosts) do
219
228
  execute *KAMAL.auditor.record("Pull #{name} accessory image"), verbosity: :debug
@@ -227,7 +236,7 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
227
236
  option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
228
237
  def remove(name)
229
238
  confirming "This will remove all containers, images and data directories for #{name}. Are you sure?" do
230
- with_lock do
239
+ modify(lock: true) do
231
240
  if name == "all"
232
241
  KAMAL.accessory_names.each { |accessory_name| remove_accessory(accessory_name) }
233
242
  else
@@ -239,7 +248,7 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
239
248
 
240
249
  desc "remove_container [NAME]", "Remove accessory container from host", hide: true
241
250
  def remove_container(name)
242
- with_lock do
251
+ modify(lock: true) do
243
252
  with_accessory(name) do |accessory, hosts|
244
253
  on(hosts) do
245
254
  execute *KAMAL.auditor.record("Remove #{name} accessory container"), verbosity: :debug
@@ -251,7 +260,7 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
251
260
 
252
261
  desc "remove_image [NAME]", "Remove accessory image from host", hide: true
253
262
  def remove_image(name)
254
- with_lock do
263
+ modify(lock: true) do
255
264
  with_accessory(name) do |accessory, hosts|
256
265
  on(hosts) do
257
266
  execute *KAMAL.auditor.record("Removed #{name} accessory image"), verbosity: :debug
@@ -263,7 +272,7 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
263
272
 
264
273
  desc "remove_service_directory [NAME]", "Remove accessory directory used for uploaded files and data directories from host", hide: true
265
274
  def remove_service_directory(name)
266
- with_lock do
275
+ modify(lock: true) do
267
276
  with_accessory(name) do |accessory, hosts|
268
277
  on(hosts) do
269
278
  execute *accessory.remove_service_directory
@@ -277,7 +286,7 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
277
286
  option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
278
287
  def upgrade(name)
279
288
  confirming "This will restart all accessories" do
280
- with_lock do
289
+ modify(lock: true) do
281
290
  host_groups = options[:rolling] ? KAMAL.accessory_hosts : [ KAMAL.accessory_hosts ]
282
291
  host_groups.each do |hosts|
283
292
  host_list = Array(hosts).join(",")
data/lib/kamal/cli/app.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  class Kamal::Cli::App < Kamal::Cli::Base
2
2
  desc "boot", "Boot app on servers (or reboot app if already running)"
3
3
  def boot
4
- with_lock do
4
+ modify(lock: true) do
5
5
  say "Get most recent version available as an image...", :magenta unless options[:version]
6
6
  using_version(version_or_latest) do |version|
7
7
  say "Start container with version #{version} (or reboot if already running)...", :magenta
@@ -42,7 +42,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
42
42
 
43
43
  desc "start", "Start existing app container on servers"
44
44
  def start
45
- with_lock do
45
+ modify(lock: true) do
46
46
  on_roles(KAMAL.roles, hosts: KAMAL.app_hosts, parallel: KAMAL.config.boot.parallel_roles) do |host, role|
47
47
  app = KAMAL.app(role: role, host: host)
48
48
  execute *KAMAL.auditor.record("Started app version #{KAMAL.config.version}"), verbosity: :debug
@@ -61,7 +61,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
61
61
 
62
62
  desc "stop", "Stop app container on servers"
63
63
  def stop
64
- with_lock do
64
+ modify(lock: true) do
65
65
  on_roles(KAMAL.roles, hosts: KAMAL.app_hosts, parallel: KAMAL.config.boot.parallel_roles) do |host, role|
66
66
  app = KAMAL.app(role: role, host: host)
67
67
  execute *KAMAL.auditor.record("Stopped app", role: role), verbosity: :debug
@@ -93,59 +93,68 @@ class Kamal::Cli::App < Kamal::Cli::Base
93
93
  option :reuse, type: :boolean, default: false, desc: "Reuse currently running container instead of starting a new one"
94
94
  option :env, aliases: "-e", type: :hash, desc: "Set environment variables for the command"
95
95
  option :detach, type: :boolean, default: false, desc: "Execute command in a detached container"
96
+ option :raw, type: :boolean, default: false, desc: "Output raw, unmodified stdout"
96
97
  def exec(*cmd)
97
- pre_connect_if_required
98
+ raw = options[:raw]
98
99
 
99
100
  if (incompatible_options = [ :interactive, :reuse ].select { |key| options[:detach] && options[key] }.presence)
100
101
  raise ArgumentError, "Detach is not compatible with #{incompatible_options.join(" or ")}"
101
102
  end
102
103
 
104
+ if raw && (incompatible_options = [ :interactive, :detach ].select { |key| options[key] }.presence)
105
+ raise ArgumentError, "Raw is not compatible with #{incompatible_options.join(" or ")}"
106
+ end
107
+
103
108
  if cmd.empty?
104
109
  raise ArgumentError, "No command provided. You must specify a command to execute."
105
110
  end
106
111
 
107
- cmd = Kamal::Utils.join_commands(cmd)
108
- env = options[:env]
109
- detach = options[:detach]
110
- quiet = options[:quiet]
111
- case
112
- when options[:interactive] && options[:reuse]
113
- say "Get current version of running container...", :magenta unless options[:version]
114
- using_version(options[:version] || current_running_version) do |version|
115
- say "Launching interactive command with version #{version} via SSH from existing container on #{KAMAL.primary_host}...", :magenta
116
- run_locally { exec KAMAL.app(role: KAMAL.primary_role, host: KAMAL.primary_host).execute_in_existing_container_over_ssh(cmd, env: env) }
117
- end
112
+ with_raw_output(raw) do
113
+ pre_connect_if_required
114
+
115
+ cmd = Kamal::Utils.join_commands(cmd)
116
+ env = options[:env]
117
+ detach = options[:detach]
118
+ quiet = options[:quiet]
119
+ case
120
+ when options[:interactive] && options[:reuse]
121
+ say "Get current version of running container...", :magenta unless options[:version]
122
+ using_version(options[:version] || current_running_version) do |version|
123
+ say "Launching interactive command with version #{version} via SSH from existing container on #{KAMAL.primary_host}...", :magenta
124
+ run_locally { exec KAMAL.app(role: KAMAL.primary_role, host: KAMAL.primary_host).execute_in_existing_container_over_ssh(cmd, env: env) }
125
+ end
118
126
 
119
- when options[:interactive]
120
- say "Get most recent version available as an image...", :magenta unless options[:version]
121
- using_version(version_or_latest) do |version|
122
- say "Launching interactive command with version #{version} via SSH from new container on #{KAMAL.primary_host}...", :magenta
123
- on(KAMAL.primary_host) { execute *KAMAL.registry.login }
124
- run_locally do
125
- exec KAMAL.app(role: KAMAL.primary_role, host: KAMAL.primary_host).execute_in_new_container_over_ssh(cmd, env: env)
127
+ when options[:interactive]
128
+ say "Get most recent version available as an image...", :magenta unless options[:version]
129
+ using_version(version_or_latest) do |version|
130
+ say "Launching interactive command with version #{version} via SSH from new container on #{KAMAL.primary_host}...", :magenta
131
+ on(KAMAL.primary_host) { execute *KAMAL.registry.login }
132
+ run_locally do
133
+ exec KAMAL.app(role: KAMAL.primary_role, host: KAMAL.primary_host).execute_in_new_container_over_ssh(cmd, env: env)
134
+ end
126
135
  end
127
- end
128
136
 
129
- when options[:reuse]
130
- say "Get current version of running container...", :magenta unless options[:version]
131
- using_version(options[:version] || current_running_version) do |version|
132
- say "Launching command with version #{version} from existing container...", :magenta
137
+ when options[:reuse]
138
+ say "Get current version of running container...", :magenta unless options[:version]
139
+ using_version(options[:version] || current_running_version) do |version|
140
+ say "Launching command with version #{version} from existing container...", :magenta
133
141
 
134
- on_roles(KAMAL.roles, hosts: KAMAL.app_hosts) do |host, role|
135
- execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on app version #{version}", role: role), verbosity: :debug
136
- puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).execute_in_existing_container(cmd, env: env)), quiet: quiet
142
+ on_roles(KAMAL.roles, hosts: KAMAL.app_hosts) do |host, role|
143
+ execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on app version #{version}", role: role), verbosity: :debug
144
+ puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).execute_in_existing_container(cmd, env: env), strip: !raw), quiet: quiet, raw: raw
145
+ end
137
146
  end
138
- end
139
-
140
- else
141
- say "Get most recent version available as an image...", :magenta unless options[:version]
142
- using_version(version_or_latest) do |version|
143
- say "Launching command with version #{version} from new container...", :magenta
144
- on(KAMAL.app_hosts) { execute *KAMAL.registry.login }
145
147
 
146
- on_roles(KAMAL.roles, hosts: KAMAL.app_hosts) do |host, role|
147
- execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on app version #{version}"), verbosity: :debug
148
- puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).execute_in_new_container(cmd, env: env, detach: detach)), quiet: quiet
148
+ else
149
+ say "Get most recent version available as an image...", :magenta unless options[:version]
150
+ using_version(version_or_latest) do |version|
151
+ say "Launching command with version #{version} from new container...", :magenta
152
+ on(KAMAL.app_hosts) { execute *KAMAL.registry.login }
153
+
154
+ on_roles(KAMAL.roles, hosts: KAMAL.app_hosts) do |host, role|
155
+ execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on app version #{version}"), verbosity: :debug
156
+ puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).execute_in_new_container(cmd, env: env, detach: detach), strip: !raw), quiet: quiet, raw: raw
157
+ end
149
158
  end
150
159
  end
151
160
  end
@@ -233,7 +242,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
233
242
 
234
243
  desc "remove", "Remove app containers and images from servers"
235
244
  def remove
236
- with_lock do
245
+ modify(lock: true) do
237
246
  stop
238
247
  remove_containers
239
248
  remove_images
@@ -243,7 +252,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
243
252
 
244
253
  desc "live", "Set the app to live mode"
245
254
  def live
246
- with_lock do
255
+ modify(lock: true) do
247
256
  on_roles(KAMAL.roles, hosts: KAMAL.proxy_hosts) do |host, role|
248
257
  execute *KAMAL.app(role: role, host: host).live if role.running_proxy?
249
258
  end
@@ -256,7 +265,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
256
265
  def maintenance
257
266
  maintenance_options = { drain_timeout: options[:drain_timeout] || KAMAL.config.drain_timeout, message: options[:message] }
258
267
 
259
- with_lock do
268
+ modify(lock: true) do
260
269
  on_roles(KAMAL.roles, hosts: KAMAL.proxy_hosts) do |host, role|
261
270
  execute *KAMAL.app(role: role, host: host).maintenance(**maintenance_options) if role.running_proxy?
262
271
  end
@@ -265,7 +274,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
265
274
 
266
275
  desc "remove_container [VERSION]", "Remove app container with given version from servers", hide: true
267
276
  def remove_container(version)
268
- with_lock do
277
+ modify(lock: true) do
269
278
  on_roles(KAMAL.roles, hosts: KAMAL.app_hosts) do |host, role|
270
279
  execute *KAMAL.auditor.record("Removed app container with version #{version}", role: role), verbosity: :debug
271
280
  execute *KAMAL.app(role: role, host: host).remove_container(version: version)
@@ -275,7 +284,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
275
284
 
276
285
  desc "remove_containers", "Remove all app containers from servers", hide: true
277
286
  def remove_containers
278
- with_lock do
287
+ modify(lock: true) do
279
288
  on_roles(KAMAL.roles, hosts: KAMAL.app_hosts) do |host, role|
280
289
  execute *KAMAL.auditor.record("Removed all app containers", role: role), verbosity: :debug
281
290
  execute *KAMAL.app(role: role, host: host).remove_containers
@@ -285,7 +294,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
285
294
 
286
295
  desc "remove_images", "Remove all app images from servers", hide: true
287
296
  def remove_images
288
- with_lock do
297
+ modify(lock: true) do
289
298
  on(hosts_removing_all_roles) do
290
299
  execute *KAMAL.auditor.record("Removed all app images"), verbosity: :debug
291
300
  execute *KAMAL.app.remove_images
@@ -295,7 +304,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
295
304
 
296
305
  desc "remove_app_directories", "Remove the app directories from servers", hide: true
297
306
  def remove_app_directories
298
- with_lock do
307
+ modify(lock: true) do
299
308
  on(hosts_removing_all_roles) do |host|
300
309
  execute *KAMAL.server.remove_app_directory, raise_on_non_zero_exit: false
301
310
  execute *KAMAL.auditor.record("Removed #{KAMAL.config.app_directory}"), verbosity: :debug
@@ -347,7 +356,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
347
356
 
348
357
  def with_lock_if_stopping
349
358
  if options[:stop]
350
- with_lock { yield }
359
+ modify(lock: true) { yield }
351
360
  else
352
361
  yield
353
362
  end
@@ -6,6 +6,10 @@ module Kamal::Cli
6
6
  include SSHKit::DSL
7
7
 
8
8
  VERBOSITY = { verbose: :debug, quiet: :error }.freeze
9
+ AUTOMATIC_DEPLOY_LOCK_MESSAGE = "Automatic deploy lock"
10
+
11
+ class LockHeldError < StandardError; end
12
+ class LockMissingError < StandardError; end
9
13
 
10
14
  def self.exit_on_failure?() true end
11
15
  def self.dynamic_command_class() Kamal::Cli::Alias::Command end
@@ -24,6 +28,10 @@ module Kamal::Cli
24
28
 
25
29
  class_option :skip_hooks, aliases: "-H", type: :boolean, default: false, desc: "Don't run hooks"
26
30
 
31
+ class_option :lock_wait, type: :boolean, default: false, desc: "Wait for the deploy lock if it's already held instead of failing immediately"
32
+ class_option :lock_wait_timeout, type: :numeric, default: 900, desc: "Maximum seconds to wait for the deploy lock when --lock-wait is set"
33
+ class_option :lock_wait_interval, type: :numeric, default: 15, desc: "Seconds between deploy lock polls when --lock-wait is set"
34
+
27
35
  def initialize(args = [], local_options = {}, config = {})
28
36
  if config[:current_command].is_a?(Kamal::Cli::Alias::Command)
29
37
  # When Thor generates a dynamic command, it doesn't attempt to parse the arguments.
@@ -60,6 +68,10 @@ module Kamal::Cli
60
68
  commander.specific_hosts = options[:hosts]&.split(",")
61
69
  commander.specific_roles = options[:roles]&.split(",")
62
70
  commander.specific_primary! if options[:primary]
71
+
72
+ commander.lock_wait = options[:lock_wait]
73
+ commander.lock_wait_timeout = options[:lock_wait_timeout]
74
+ commander.lock_wait_interval = options[:lock_wait_interval]
63
75
  end
64
76
  end
65
77
 
@@ -72,6 +84,23 @@ module Kamal::Cli
72
84
  puts " Finished all in #{sprintf("%.1f seconds", runtime)}"
73
85
  end
74
86
 
87
+ def modify(lock: false)
88
+ KAMAL.modify(command: command, subcommand: subcommand) do
89
+ lock ? with_lock { yield } : yield
90
+ end
91
+ end
92
+
93
+ def say(message = "", *)
94
+ super unless options[:raw]
95
+ KAMAL.log(message.to_s)
96
+ end
97
+
98
+ # Raw output is written straight to stdout for piping, so silence SSHKit's
99
+ # command echoing that would otherwise corrupt the byte stream.
100
+ def with_raw_output(raw, &block)
101
+ raw ? KAMAL.with_verbosity(:error, &block) : block.call
102
+ end
103
+
75
104
  def with_lock
76
105
  if KAMAL.holding_lock?
77
106
  yield
@@ -106,31 +135,90 @@ module Kamal::Cli
106
135
  def acquire_lock
107
136
  ensure_run_directory
108
137
 
109
- raise_if_locked do
110
- say "Acquiring the deploy lock...", :magenta
111
- on(KAMAL.primary_host) { execute *KAMAL.lock.acquire("Automatic deploy lock", KAMAL.config.version), verbosity: :debug }
138
+ if KAMAL.lock_wait
139
+ acquire_lock_with_wait
140
+ else
141
+ raise_if_locked do
142
+ say "Acquiring the deploy lock...", :magenta
143
+ execute_lock_acquire(AUTOMATIC_DEPLOY_LOCK_MESSAGE)
144
+ end
112
145
  end
113
146
 
114
147
  KAMAL.holding_lock = true
115
148
  end
116
149
 
150
+ def acquire_lock_with_wait
151
+ timeout = KAMAL.lock_wait_timeout
152
+ interval = KAMAL.lock_wait_interval
153
+ deadline = Time.now + timeout
154
+ details_shown = false
155
+
156
+ say "Acquiring the deploy lock (waiting up to #{timeout}s)...", :magenta
157
+
158
+ loop do
159
+ execute_lock_acquire(AUTOMATIC_DEPLOY_LOCK_MESSAGE)
160
+ break
161
+ rescue LockHeldError
162
+ unless details_shown
163
+ status = capture_lock_status
164
+
165
+ say "Deploy lock is held by:", :magenta
166
+ puts status
167
+
168
+ unless status.include?(AUTOMATIC_DEPLOY_LOCK_MESSAGE)
169
+ raise LockError, "Deploy lock held manually, not waiting. Run 'kamal lock help' for more information"
170
+ end
171
+
172
+ details_shown = true
173
+ end
174
+
175
+ remaining = (deadline - Time.now).to_i
176
+ if remaining <= 0
177
+ say "Timed out after #{timeout}s waiting for the deploy lock", :red
178
+ raise LockError, "Timed out waiting for deploy lock"
179
+ end
180
+
181
+ say "Retrying in #{interval}s (#{remaining}s remaining)...", :magenta
182
+ sleep [ interval, remaining ].min
183
+ end
184
+ end
185
+
117
186
  def release_lock
118
187
  say "Releasing the deploy lock...", :magenta
119
- on(KAMAL.primary_host) { execute *KAMAL.lock.release, verbosity: :debug }
188
+ execute_lock_release
120
189
 
121
190
  KAMAL.holding_lock = false
122
191
  end
123
192
 
124
193
  def raise_if_locked
125
194
  yield
195
+ rescue LockHeldError
196
+ say "Deploy lock already in place!", :red
197
+ puts capture_lock_status
198
+ raise LockError, "Deploy lock found. Run 'kamal lock help' for more information"
199
+ end
200
+
201
+ def execute_lock_acquire(message)
202
+ on(KAMAL.primary_host) { execute *KAMAL.lock.acquire(message, KAMAL.config.version), verbosity: :debug }
126
203
  rescue SSHKit::Runner::ExecuteError => e
127
- if e.message =~ /cannot create directory/
128
- say "Deploy lock already in place!", :red
129
- on(KAMAL.primary_host) { puts capture_with_debug(*KAMAL.lock.status) }
130
- raise LockError, "Deploy lock found. Run 'kamal lock help' for more information"
131
- else
132
- raise e
133
- end
204
+ raise LockHeldError if e.message =~ /cannot create directory/
205
+ raise
206
+ end
207
+
208
+ def execute_lock_release
209
+ on(KAMAL.primary_host) { execute *KAMAL.lock.release, verbosity: :debug }
210
+ rescue SSHKit::Runner::ExecuteError => e
211
+ raise LockMissingError if e.message =~ /No such file or directory/
212
+ raise
213
+ end
214
+
215
+ def capture_lock_status
216
+ status = nil
217
+ on(KAMAL.primary_host) { status = capture_with_debug(*KAMAL.lock.status) }
218
+ status
219
+ rescue SSHKit::Runner::ExecuteError => e
220
+ raise LockMissingError if e.message =~ /No such file or directory/
221
+ raise
134
222
  end
135
223
 
136
224
  def run_hook(hook, **extra_details)
@@ -18,7 +18,8 @@ class Kamal::Cli::Build < Kamal::Cli::Base
18
18
  pre_connect_if_required
19
19
 
20
20
  ensure_docker_installed
21
- login_to_registry_locally if KAMAL.builder.login_to_registry_locally?
21
+ setup_local_registry if KAMAL.registry.local?
22
+ login_to_registry_locally if !KAMAL.registry.local? && KAMAL.builder.login_to_registry_locally?
22
23
 
23
24
  run_hook "pre-build"
24
25
 
@@ -194,13 +195,15 @@ class Kamal::Cli::Build < Kamal::Cli::Base
194
195
  end
195
196
  end
196
197
 
198
+ def setup_local_registry
199
+ run_locally do
200
+ execute *KAMAL.registry.setup
201
+ end
202
+ end
203
+
197
204
  def login_to_registry_locally
198
205
  run_locally do
199
- if KAMAL.registry.local?
200
- execute *KAMAL.registry.setup
201
- else
202
- execute *KAMAL.registry.login
203
- end
206
+ execute *KAMAL.registry.login
204
207
  end
205
208
  end
206
209
 
@@ -2,22 +2,17 @@ class Kamal::Cli::Lock < Kamal::Cli::Base
2
2
  desc "status", "Report lock status"
3
3
  def status
4
4
  handle_missing_lock do
5
- on(KAMAL.primary_host) do
6
- puts capture_with_debug(*KAMAL.lock.status)
7
- end
5
+ puts capture_lock_status
8
6
  end
9
7
  end
10
8
 
11
9
  desc "acquire", "Acquire the deploy lock"
12
10
  option :message, aliases: "-m", type: :string, desc: "A lock message", required: true
13
11
  def acquire
14
- message = options[:message]
15
12
  ensure_run_directory
16
13
 
17
14
  raise_if_locked do
18
- on(KAMAL.primary_host) do
19
- execute *KAMAL.lock.acquire(message, KAMAL.config.version), verbosity: :debug
20
- end
15
+ execute_lock_acquire(options[:message])
21
16
  say "Acquired the deploy lock"
22
17
  end
23
18
  end
@@ -25,9 +20,7 @@ class Kamal::Cli::Lock < Kamal::Cli::Base
25
20
  desc "release", "Release the deploy lock"
26
21
  def release
27
22
  handle_missing_lock do
28
- on(KAMAL.primary_host) do
29
- execute *KAMAL.lock.release, verbosity: :debug
30
- end
23
+ execute_lock_release
31
24
  say "Released the deploy lock"
32
25
  end
33
26
  end
@@ -35,11 +28,7 @@ class Kamal::Cli::Lock < Kamal::Cli::Base
35
28
  private
36
29
  def handle_missing_lock
37
30
  yield
38
- rescue SSHKit::Runner::ExecuteError => e
39
- if e.message =~ /No such file or directory/
40
- say "There is no deploy lock"
41
- else
42
- raise
43
- end
31
+ rescue LockMissingError
32
+ say "There is no deploy lock"
44
33
  end
45
34
  end