kamal 1.5.2 → 1.7.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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/lib/kamal/cli/accessory.rb +30 -24
  3. data/lib/kamal/cli/app/boot.rb +70 -18
  4. data/lib/kamal/cli/app/prepare_assets.rb +1 -1
  5. data/lib/kamal/cli/app.rb +60 -47
  6. data/lib/kamal/cli/base.rb +26 -28
  7. data/lib/kamal/cli/build/clone.rb +61 -0
  8. data/lib/kamal/cli/build.rb +64 -53
  9. data/lib/kamal/cli/env.rb +5 -5
  10. data/lib/kamal/cli/healthcheck/barrier.rb +31 -0
  11. data/lib/kamal/cli/healthcheck/error.rb +2 -0
  12. data/lib/kamal/cli/healthcheck/poller.rb +6 -7
  13. data/lib/kamal/cli/main.rb +49 -44
  14. data/lib/kamal/cli/prune.rb +3 -3
  15. data/lib/kamal/cli/registry.rb +9 -10
  16. data/lib/kamal/cli/server.rb +39 -15
  17. data/lib/kamal/cli/templates/sample_hooks/docker-setup.sample +1 -1
  18. data/lib/kamal/cli/traefik.rb +13 -11
  19. data/lib/kamal/cli.rb +1 -1
  20. data/lib/kamal/commander.rb +6 -6
  21. data/lib/kamal/commands/accessory.rb +4 -4
  22. data/lib/kamal/commands/app/containers.rb +8 -0
  23. data/lib/kamal/commands/app/execution.rb +3 -3
  24. data/lib/kamal/commands/app/logging.rb +5 -5
  25. data/lib/kamal/commands/app.rb +6 -5
  26. data/lib/kamal/commands/base.rb +2 -3
  27. data/lib/kamal/commands/builder/base.rb +19 -12
  28. data/lib/kamal/commands/builder/clone.rb +28 -0
  29. data/lib/kamal/commands/builder/multiarch/remote.rb +10 -0
  30. data/lib/kamal/commands/builder/multiarch.rb +13 -9
  31. data/lib/kamal/commands/builder/native/cached.rb +14 -6
  32. data/lib/kamal/commands/builder/native/remote.rb +17 -9
  33. data/lib/kamal/commands/builder/native.rb +6 -7
  34. data/lib/kamal/commands/builder.rb +19 -11
  35. data/lib/kamal/commands/registry.rb +4 -13
  36. data/lib/kamal/commands/traefik.rb +8 -47
  37. data/lib/kamal/configuration/accessory.rb +30 -41
  38. data/lib/kamal/configuration/boot.rb +9 -4
  39. data/lib/kamal/configuration/builder.rb +61 -30
  40. data/lib/kamal/configuration/docs/accessory.yml +90 -0
  41. data/lib/kamal/configuration/docs/boot.yml +19 -0
  42. data/lib/kamal/configuration/docs/builder.yml +107 -0
  43. data/lib/kamal/configuration/docs/configuration.yml +157 -0
  44. data/lib/kamal/configuration/docs/env.yml +72 -0
  45. data/lib/kamal/configuration/docs/healthcheck.yml +59 -0
  46. data/lib/kamal/configuration/docs/logging.yml +21 -0
  47. data/lib/kamal/configuration/docs/registry.yml +49 -0
  48. data/lib/kamal/configuration/docs/role.yml +52 -0
  49. data/lib/kamal/configuration/docs/servers.yml +27 -0
  50. data/lib/kamal/configuration/docs/ssh.yml +46 -0
  51. data/lib/kamal/configuration/docs/sshkit.yml +23 -0
  52. data/lib/kamal/configuration/docs/traefik.yml +62 -0
  53. data/lib/kamal/configuration/env/tag.rb +12 -0
  54. data/lib/kamal/configuration/env.rb +10 -14
  55. data/lib/kamal/configuration/healthcheck.rb +63 -0
  56. data/lib/kamal/configuration/logging.rb +33 -0
  57. data/lib/kamal/configuration/registry.rb +31 -0
  58. data/lib/kamal/configuration/role.rb +72 -61
  59. data/lib/kamal/configuration/servers.rb +18 -0
  60. data/lib/kamal/configuration/ssh.rb +11 -8
  61. data/lib/kamal/configuration/sshkit.rb +9 -7
  62. data/lib/kamal/configuration/traefik.rb +60 -0
  63. data/lib/kamal/configuration/validation.rb +27 -0
  64. data/lib/kamal/configuration/validator/accessory.rb +9 -0
  65. data/lib/kamal/configuration/validator/builder.rb +9 -0
  66. data/lib/kamal/configuration/validator/env.rb +54 -0
  67. data/lib/kamal/configuration/validator/registry.rb +25 -0
  68. data/lib/kamal/configuration/validator/role.rb +11 -0
  69. data/lib/kamal/configuration/validator/servers.rb +7 -0
  70. data/lib/kamal/configuration/validator.rb +140 -0
  71. data/lib/kamal/configuration.rb +50 -63
  72. data/lib/kamal/git.rb +4 -0
  73. data/lib/kamal/sshkit_with_ext.rb +36 -0
  74. data/lib/kamal/version.rb +1 -1
  75. data/lib/kamal.rb +2 -0
  76. metadata +64 -9
  77. data/lib/kamal/cli/healthcheck.rb +0 -21
  78. data/lib/kamal/commands/healthcheck.rb +0 -59
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 63594154afe8efc69b08fd2a27b1efb4ff6604d1bd62f345dc8d24df7903b649
4
- data.tar.gz: d8e8cb819aeea4212bc576e2b8a5bcd2cc6fcd67c6186ba0e04b2250999985c1
3
+ metadata.gz: 30a8c6964442e363d08edf8ad8c8b5ae835308392b330ec9e1ba8c659916bb04
4
+ data.tar.gz: fce971fac71c529d90bebf6ae1b28fb617d8c34a18cc7d244e0a845238891f22
5
5
  SHA512:
6
- metadata.gz: 9da318bbc1fe8e5dac2b09cc8c89682417653de15b72567378a6998e0992fad999a1a8bca4b32f8eb2a7147f774d59efb4850b50ea15bfcde32d8426b50303fe
7
- data.tar.gz: ca156878b11110b0bc7092790565bfbf5b3cdf9206a2faf210c805856dab0aef9e57b883c09f86422c6c0821f6c281a7ed892ccf061101750dc8507326ba53d3
6
+ metadata.gz: '097ba4ef7a4956a22b89354a648319152224b48e77a8c32e0757462402508773b61d1b238b34d9b8f16f9d24c8dad12e118ba23e669b6a1388d5a09f73296925'
7
+ data.tar.gz: f13f8f4dd704a559940313c245e5bde92915c77a716d01e3f69b5f825ef2c122bd840b9a9f47a4eb54f5555a87bced74dcf8c46394f53941211fdc0d66d5567b
@@ -1,7 +1,7 @@
1
1
  class Kamal::Cli::Accessory < Kamal::Cli::Base
2
2
  desc "boot [NAME]", "Boot new accessory service on host (use NAME=all to boot all accessories)"
3
3
  def boot(name, login: true)
4
- mutating do
4
+ with_lock do
5
5
  if name == "all"
6
6
  KAMAL.accessory_names.each { |accessory_name| boot(accessory_name) }
7
7
  else
@@ -21,7 +21,7 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
21
21
 
22
22
  desc "upload [NAME]", "Upload accessory files to host", hide: true
23
23
  def upload(name)
24
- mutating do
24
+ with_lock do
25
25
  with_accessory(name) do |accessory, hosts|
26
26
  on(hosts) do
27
27
  accessory.files.each do |(local, remote)|
@@ -38,7 +38,7 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
38
38
 
39
39
  desc "directories [NAME]", "Create accessory directories on host", hide: true
40
40
  def directories(name)
41
- mutating do
41
+ with_lock do
42
42
  with_accessory(name) do |accessory, hosts|
43
43
  on(hosts) do
44
44
  accessory.directories.keys.each do |host_path|
@@ -51,7 +51,7 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
51
51
 
52
52
  desc "reboot [NAME]", "Reboot existing accessory on host (stop container, remove container, start new container; use NAME=all to boot all accessories)"
53
53
  def reboot(name)
54
- mutating do
54
+ with_lock do
55
55
  if name == "all"
56
56
  KAMAL.accessory_names.each { |accessory_name| reboot(accessory_name) }
57
57
  else
@@ -70,7 +70,7 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
70
70
 
71
71
  desc "start [NAME]", "Start existing accessory container on host"
72
72
  def start(name)
73
- mutating do
73
+ with_lock do
74
74
  with_accessory(name) do |accessory, hosts|
75
75
  on(hosts) do
76
76
  execute *KAMAL.auditor.record("Started #{name} accessory"), verbosity: :debug
@@ -82,7 +82,7 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
82
82
 
83
83
  desc "stop [NAME]", "Stop existing accessory container on host"
84
84
  def stop(name)
85
- mutating do
85
+ with_lock do
86
86
  with_accessory(name) do |accessory, hosts|
87
87
  on(hosts) do
88
88
  execute *KAMAL.auditor.record("Stopped #{name} accessory"), verbosity: :debug
@@ -94,7 +94,7 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
94
94
 
95
95
  desc "restart [NAME]", "Restart existing accessory container on host"
96
96
  def restart(name)
97
- mutating do
97
+ with_lock do
98
98
  with_accessory(name) do
99
99
  stop(name)
100
100
  start(name)
@@ -149,23 +149,25 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
149
149
  option :since, aliases: "-s", desc: "Show logs since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)"
150
150
  option :lines, type: :numeric, aliases: "-n", desc: "Number of log lines to pull from each server"
151
151
  option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)"
152
+ option :grep_options, aliases: "-o", desc: "Additional options supplied to grep"
152
153
  option :follow, aliases: "-f", desc: "Follow logs on primary server (or specific host set by --hosts)"
153
154
  def logs(name)
154
155
  with_accessory(name) do |accessory, hosts|
155
156
  grep = options[:grep]
157
+ grep_options = options[:grep_options]
156
158
 
157
159
  if options[:follow]
158
160
  run_locally do
159
161
  info "Following logs on #{hosts}..."
160
- info accessory.follow_logs(grep: grep)
161
- exec accessory.follow_logs(grep: grep)
162
+ info accessory.follow_logs(grep: grep, grep_options: grep_options)
163
+ exec accessory.follow_logs(grep: grep, grep_options: grep_options)
162
164
  end
163
165
  else
164
166
  since = options[:since]
165
167
  lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set
166
168
 
167
169
  on(hosts) do
168
- puts capture_with_info(*accessory.logs(since: since, lines: lines, grep: grep))
170
+ puts capture_with_info(*accessory.logs(since: since, lines: lines, grep: grep, grep_options: grep_options))
169
171
  end
170
172
  end
171
173
  end
@@ -174,17 +176,12 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
174
176
  desc "remove [NAME]", "Remove accessory container, image and data directory from host (use NAME=all to remove all accessories)"
175
177
  option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
176
178
  def remove(name)
177
- mutating do
178
- if name == "all"
179
- KAMAL.accessory_names.each { |accessory_name| remove(accessory_name) }
180
- else
181
- confirming "This will remove all containers, images and data directories for #{name}. Are you sure?" do
182
- with_accessory(name) do
183
- stop(name)
184
- remove_container(name)
185
- remove_image(name)
186
- remove_service_directory(name)
187
- end
179
+ confirming "This will remove all containers, images and data directories for #{name}. Are you sure?" do
180
+ with_lock do
181
+ if name == "all"
182
+ KAMAL.accessory_names.each { |accessory_name| remove_accessory(accessory_name) }
183
+ else
184
+ remove_accessory(name)
188
185
  end
189
186
  end
190
187
  end
@@ -192,7 +189,7 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
192
189
 
193
190
  desc "remove_container [NAME]", "Remove accessory container from host", hide: true
194
191
  def remove_container(name)
195
- mutating do
192
+ with_lock do
196
193
  with_accessory(name) do |accessory, hosts|
197
194
  on(hosts) do
198
195
  execute *KAMAL.auditor.record("Remove #{name} accessory container"), verbosity: :debug
@@ -204,7 +201,7 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
204
201
 
205
202
  desc "remove_image [NAME]", "Remove accessory image from host", hide: true
206
203
  def remove_image(name)
207
- mutating do
204
+ with_lock do
208
205
  with_accessory(name) do |accessory, hosts|
209
206
  on(hosts) do
210
207
  execute *KAMAL.auditor.record("Removed #{name} accessory image"), verbosity: :debug
@@ -216,7 +213,7 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
216
213
 
217
214
  desc "remove_service_directory [NAME]", "Remove accessory directory used for uploaded files and data directories from host", hide: true
218
215
  def remove_service_directory(name)
219
- mutating do
216
+ with_lock do
220
217
  with_accessory(name) do |accessory, hosts|
221
218
  on(hosts) do
222
219
  execute *accessory.remove_service_directory
@@ -250,4 +247,13 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
250
247
  accessory.hosts
251
248
  end
252
249
  end
250
+
251
+ def remove_accessory(name)
252
+ with_accessory(name) do
253
+ stop(name)
254
+ remove_container(name)
255
+ remove_image(name)
256
+ remove_service_directory(name)
257
+ end
258
+ end
253
259
  end
@@ -1,19 +1,30 @@
1
1
  class Kamal::Cli::App::Boot
2
- attr_reader :host, :role, :version, :sshkit
3
- delegate :execute, :capture_with_info, :info, to: :sshkit
4
- delegate :uses_cord?, :assets?, to: :role
2
+ attr_reader :host, :role, :version, :barrier, :sshkit
3
+ delegate :execute, :capture_with_info, :capture_with_pretty_json, :info, :error, to: :sshkit
4
+ delegate :uses_cord?, :assets?, :running_traefik?, to: :role
5
5
 
6
- def initialize(host, role, version, sshkit)
6
+ def initialize(host, role, sshkit, version, barrier)
7
7
  @host = host
8
8
  @role = role
9
9
  @version = version
10
+ @barrier = barrier
10
11
  @sshkit = sshkit
11
12
  end
12
13
 
13
14
  def run
14
15
  old_version = old_version_renamed_if_clashing
15
16
 
16
- start_new_version
17
+ wait_at_barrier if queuer?
18
+
19
+ begin
20
+ start_new_version
21
+ rescue => e
22
+ close_barrier if gatekeeper?
23
+ stop_new_version
24
+ raise
25
+ end
26
+
27
+ release_barrier if gatekeeper?
17
28
 
18
29
  if old_version
19
30
  stop_old_version(old_version)
@@ -21,18 +32,6 @@ class Kamal::Cli::App::Boot
21
32
  end
22
33
 
23
34
  private
24
- def app
25
- @app ||= KAMAL.app(role: role)
26
- end
27
-
28
- def auditor
29
- @auditor = KAMAL.auditor(role: role)
30
- end
31
-
32
- def audit(message)
33
- execute *auditor.record(message), verbosity: :debug
34
- end
35
-
36
35
  def old_version_renamed_if_clashing
37
36
  if capture_with_info(*app.container_id_for_version(version), raise_on_non_zero_exit: false).present?
38
37
  renamed_version = "#{version}_replaced_#{SecureRandom.hex(8)}"
@@ -46,11 +45,17 @@ class Kamal::Cli::App::Boot
46
45
 
47
46
  def start_new_version
48
47
  audit "Booted app version #{version}"
48
+
49
49
  execute *app.tie_cord(role.cord_host_file) if uses_cord?
50
- execute *app.run(hostname: "#{host}-#{SecureRandom.hex(6)}")
50
+ hostname = "#{host.to_s[0...51].gsub(/\.+$/, '')}-#{SecureRandom.hex(6)}"
51
+ execute *app.run(hostname: hostname)
51
52
  Kamal::Cli::Healthcheck::Poller.wait_for_healthy(pause_after_ready: true) { capture_with_info(*app.status(version: version)) }
52
53
  end
53
54
 
55
+ def stop_new_version
56
+ execute *app.stop(version: version), raise_on_non_zero_exit: false
57
+ end
58
+
54
59
  def stop_old_version(version)
55
60
  if uses_cord?
56
61
  cord = capture_with_info(*app.cord(version: version), raise_on_non_zero_exit: false).strip
@@ -64,4 +69,51 @@ class Kamal::Cli::App::Boot
64
69
 
65
70
  execute *app.clean_up_assets if assets?
66
71
  end
72
+
73
+ def release_barrier
74
+ if barrier.open
75
+ info "First #{KAMAL.primary_role} container is healthy on #{host}, booting other roles"
76
+ end
77
+ end
78
+
79
+ def wait_at_barrier
80
+ info "Waiting for the first healthy #{KAMAL.primary_role} container before booting #{role} on #{host}..."
81
+ barrier.wait
82
+ info "First #{KAMAL.primary_role} container is healthy, booting #{role} on #{host}..."
83
+ rescue Kamal::Cli::Healthcheck::Error
84
+ info "First #{KAMAL.primary_role} container is unhealthy, not booting #{role} on #{host}"
85
+ raise
86
+ end
87
+
88
+ def close_barrier
89
+ if barrier.close
90
+ info "First #{KAMAL.primary_role} container is unhealthy on #{host}, not booting other roles"
91
+ error capture_with_info(*app.logs(version: version))
92
+ error capture_with_info(*app.container_health_log(version: version))
93
+ end
94
+ end
95
+
96
+ def barrier_role?
97
+ role == KAMAL.primary_role
98
+ end
99
+
100
+ def app
101
+ @app ||= KAMAL.app(role: role, host: host)
102
+ end
103
+
104
+ def auditor
105
+ @auditor = KAMAL.auditor(role: role)
106
+ end
107
+
108
+ def audit(message)
109
+ execute *auditor.record(message), verbosity: :debug
110
+ end
111
+
112
+ def gatekeeper?
113
+ barrier && barrier_role?
114
+ end
115
+
116
+ def queuer?
117
+ barrier && !barrier_role?
118
+ end
67
119
  end
@@ -19,6 +19,6 @@ class Kamal::Cli::App::PrepareAssets
19
19
 
20
20
  private
21
21
  def app
22
- @app ||= KAMAL.app(role: role)
22
+ @app ||= KAMAL.app(role: role, host: host)
23
23
  end
24
24
  end
data/lib/kamal/cli/app.rb CHANGED
@@ -1,43 +1,45 @@
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
- mutating do
5
- hold_lock_on_error do
6
- say "Get most recent version available as an image...", :magenta unless options[:version]
7
- using_version(version_or_latest) do |version|
8
- say "Start container with version #{version} using a #{KAMAL.config.readiness_delay}s readiness delay (or reboot if already running)...", :magenta
9
-
10
- # Assets are prepared in a separate step to ensure they are on all hosts before booting
11
- on(KAMAL.hosts) do
12
- KAMAL.roles_on(host).each do |role|
13
- Kamal::Cli::App::PrepareAssets.new(host, role, self).run
14
- end
15
- end
4
+ with_lock do
5
+ say "Get most recent version available as an image...", :magenta unless options[:version]
6
+ using_version(version_or_latest) do |version|
7
+ say "Start container with version #{version} using a #{KAMAL.config.readiness_delay}s readiness delay (or reboot if already running)...", :magenta
16
8
 
17
- on(KAMAL.hosts, **KAMAL.boot_strategy) do |host|
18
- KAMAL.roles_on(host).each do |role|
19
- Kamal::Cli::App::Boot.new(host, role, version, self).run
20
- end
9
+ # Assets are prepared in a separate step to ensure they are on all hosts before booting
10
+ on(KAMAL.hosts) do
11
+ KAMAL.roles_on(host).each do |role|
12
+ Kamal::Cli::App::PrepareAssets.new(host, role, self).run
21
13
  end
14
+ end
22
15
 
23
- on(KAMAL.hosts) do |host|
24
- execute *KAMAL.auditor.record("Tagging #{KAMAL.config.absolute_image} as the latest image"), verbosity: :debug
25
- execute *KAMAL.app.tag_latest_image
16
+ # Primary hosts and roles are returned first, so they can open the barrier
17
+ barrier = Kamal::Cli::Healthcheck::Barrier.new if KAMAL.roles.many?
18
+
19
+ on(KAMAL.hosts, **KAMAL.boot_strategy) do |host|
20
+ KAMAL.roles_on(host).each do |role|
21
+ Kamal::Cli::App::Boot.new(host, role, self, version, barrier).run
26
22
  end
27
23
  end
24
+
25
+ # Tag once the app booted on all hosts
26
+ on(KAMAL.hosts) do |host|
27
+ execute *KAMAL.auditor.record("Tagging #{KAMAL.config.absolute_image} as the latest image"), verbosity: :debug
28
+ execute *KAMAL.app.tag_latest_image
29
+ end
28
30
  end
29
31
  end
30
32
  end
31
33
 
32
34
  desc "start", "Start existing app container on servers"
33
35
  def start
34
- mutating do
36
+ with_lock do
35
37
  on(KAMAL.hosts) do |host|
36
38
  roles = KAMAL.roles_on(host)
37
39
 
38
40
  roles.each do |role|
39
41
  execute *KAMAL.auditor.record("Started app version #{KAMAL.config.version}"), verbosity: :debug
40
- execute *KAMAL.app(role: role).start, raise_on_non_zero_exit: false
42
+ execute *KAMAL.app(role: role, host: host).start, raise_on_non_zero_exit: false
41
43
  end
42
44
  end
43
45
  end
@@ -45,13 +47,13 @@ class Kamal::Cli::App < Kamal::Cli::Base
45
47
 
46
48
  desc "stop", "Stop app container on servers"
47
49
  def stop
48
- mutating do
50
+ with_lock do
49
51
  on(KAMAL.hosts) do |host|
50
52
  roles = KAMAL.roles_on(host)
51
53
 
52
54
  roles.each do |role|
53
55
  execute *KAMAL.auditor.record("Stopped app", role: role), verbosity: :debug
54
- execute *KAMAL.app(role: role).stop, raise_on_non_zero_exit: false
56
+ execute *KAMAL.app(role: role, host: host).stop, raise_on_non_zero_exit: false
55
57
  end
56
58
  end
57
59
  end
@@ -64,12 +66,12 @@ class Kamal::Cli::App < Kamal::Cli::Base
64
66
  roles = KAMAL.roles_on(host)
65
67
 
66
68
  roles.each do |role|
67
- puts_by_host host, capture_with_info(*KAMAL.app(role: role).info)
69
+ puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).info)
68
70
  end
69
71
  end
70
72
  end
71
73
 
72
- desc "exec [CMD]", "Execute a custom command on servers (use --help to show options)"
74
+ desc "exec [CMD]", "Execute a custom command on servers within the app container (use --help to show options)"
73
75
  option :interactive, aliases: "-i", type: :boolean, default: false, desc: "Execute command over ssh for an interactive shell (use for console/bash)"
74
76
  option :reuse, type: :boolean, default: false, desc: "Reuse currently running container instead of starting a new one"
75
77
  option :env, aliases: "-e", type: :hash, desc: "Set environment variables for the command"
@@ -80,7 +82,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
80
82
  say "Get current version of running container...", :magenta unless options[:version]
81
83
  using_version(options[:version] || current_running_version) do |version|
82
84
  say "Launching interactive command with version #{version} via SSH from existing container on #{KAMAL.primary_host}...", :magenta
83
- run_locally { exec KAMAL.app(role: KAMAL.primary_role).execute_in_existing_container_over_ssh(cmd, host: KAMAL.primary_host, env: env) }
85
+ run_locally { exec KAMAL.app(role: KAMAL.primary_role, host: KAMAL.primary_host).execute_in_existing_container_over_ssh(cmd, env: env) }
84
86
  end
85
87
 
86
88
  when options[:interactive]
@@ -88,7 +90,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
88
90
  using_version(version_or_latest) do |version|
89
91
  say "Launching interactive command with version #{version} via SSH from new container on #{KAMAL.primary_host}...", :magenta
90
92
  run_locally do
91
- exec KAMAL.app(role: KAMAL.primary_role).execute_in_new_container_over_ssh(cmd, host: KAMAL.primary_host, env: env)
93
+ exec KAMAL.app(role: KAMAL.primary_role, host: KAMAL.primary_host).execute_in_new_container_over_ssh(cmd, env: env)
92
94
  end
93
95
  end
94
96
 
@@ -102,7 +104,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
102
104
 
103
105
  roles.each do |role|
104
106
  execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on app version #{version}", role: role), verbosity: :debug
105
- puts_by_host host, capture_with_info(*KAMAL.app(role: role).execute_in_existing_container(cmd, env: env))
107
+ puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).execute_in_existing_container(cmd, env: env))
106
108
  end
107
109
  end
108
110
  end
@@ -116,7 +118,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
116
118
 
117
119
  roles.each do |role|
118
120
  execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on app version #{version}"), verbosity: :debug
119
- puts_by_host host, capture_with_info(*KAMAL.app(role: role).execute_in_new_container(cmd, env: env))
121
+ puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).execute_in_new_container(cmd, env: env))
120
122
  end
121
123
  end
122
124
  end
@@ -131,22 +133,21 @@ class Kamal::Cli::App < Kamal::Cli::Base
131
133
  desc "stale_containers", "Detect app stale containers"
132
134
  option :stop, aliases: "-s", type: :boolean, default: false, desc: "Stop the stale containers found"
133
135
  def stale_containers
134
- mutating do
135
- stop = options[:stop]
136
-
137
- cli = self
136
+ stop = options[:stop]
138
137
 
138
+ with_lock_if_stopping do
139
139
  on(KAMAL.hosts) do |host|
140
140
  roles = KAMAL.roles_on(host)
141
141
 
142
142
  roles.each do |role|
143
- versions = capture_with_info(*KAMAL.app(role: role).list_versions, raise_on_non_zero_exit: false).split("\n")
144
- versions -= [ capture_with_info(*KAMAL.app(role: role).current_running_version, raise_on_non_zero_exit: false).strip ]
143
+ app = KAMAL.app(role: role, host: host)
144
+ versions = capture_with_info(*app.list_versions, raise_on_non_zero_exit: false).split("\n")
145
+ versions -= [ capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip ]
145
146
 
146
147
  versions.each do |version|
147
148
  if stop
148
149
  puts_by_host host, "Stopping stale container for role #{role} with version #{version}"
149
- execute *KAMAL.app(role: role).stop(version: version), raise_on_non_zero_exit: false
150
+ execute *app.stop(version: version), raise_on_non_zero_exit: false
150
151
  else
151
152
  puts_by_host host, "Detected stale container for role #{role} with version #{version} (use `kamal app stale_containers --stop` to stop)"
152
153
  end
@@ -165,12 +166,15 @@ class Kamal::Cli::App < Kamal::Cli::Base
165
166
  option :since, aliases: "-s", desc: "Show lines since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)"
166
167
  option :lines, type: :numeric, aliases: "-n", desc: "Number of lines to show from each server"
167
168
  option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)"
169
+ option :grep_options, aliases: "-o", desc: "Additional options supplied to grep"
168
170
  option :follow, aliases: "-f", desc: "Follow log on primary server (or specific host set by --hosts)"
169
171
  def logs
170
172
  # FIXME: Catch when app containers aren't running
171
173
 
172
174
  grep = options[:grep]
175
+ grep_options = options[:grep_options]
173
176
  since = options[:since]
177
+
174
178
  if options[:follow]
175
179
  lines = options[:lines].presence || ((since || grep) ? nil : 10) # Default to 10 lines if since or grep isn't set
176
180
 
@@ -180,8 +184,9 @@ class Kamal::Cli::App < Kamal::Cli::Base
180
184
  KAMAL.specific_roles ||= [ "web" ]
181
185
  role = KAMAL.roles_on(KAMAL.primary_host).first
182
186
 
183
- info KAMAL.app(role: role).follow_logs(host: KAMAL.primary_host, lines: lines, grep: grep)
184
- exec KAMAL.app(role: role).follow_logs(host: KAMAL.primary_host, lines: lines, grep: grep)
187
+ app = KAMAL.app(role: role, host: host)
188
+ info app.follow_logs(host: KAMAL.primary_host, lines: lines, grep: grep, grep_options: grep_options)
189
+ exec app.follow_logs(host: KAMAL.primary_host, lines: lines, grep: grep, grep_options: grep_options)
185
190
  end
186
191
  else
187
192
  lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set
@@ -191,7 +196,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
191
196
 
192
197
  roles.each do |role|
193
198
  begin
194
- puts_by_host host, capture_with_info(*KAMAL.app(role: role).logs(since: since, lines: lines, grep: grep))
199
+ puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).logs(since: since, lines: lines, grep: grep, grep_options: grep_options))
195
200
  rescue SSHKit::Command::Failed
196
201
  puts_by_host host, "Nothing found"
197
202
  end
@@ -202,7 +207,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
202
207
 
203
208
  desc "remove", "Remove app containers and images from servers"
204
209
  def remove
205
- mutating do
210
+ with_lock do
206
211
  stop
207
212
  remove_containers
208
213
  remove_images
@@ -211,13 +216,13 @@ class Kamal::Cli::App < Kamal::Cli::Base
211
216
 
212
217
  desc "remove_container [VERSION]", "Remove app container with given version from servers", hide: true
213
218
  def remove_container(version)
214
- mutating do
219
+ with_lock do
215
220
  on(KAMAL.hosts) do |host|
216
221
  roles = KAMAL.roles_on(host)
217
222
 
218
223
  roles.each do |role|
219
224
  execute *KAMAL.auditor.record("Removed app container with version #{version}", role: role), verbosity: :debug
220
- execute *KAMAL.app(role: role).remove_container(version: version)
225
+ execute *KAMAL.app(role: role, host: host).remove_container(version: version)
221
226
  end
222
227
  end
223
228
  end
@@ -225,13 +230,13 @@ class Kamal::Cli::App < Kamal::Cli::Base
225
230
 
226
231
  desc "remove_containers", "Remove all app containers from servers", hide: true
227
232
  def remove_containers
228
- mutating do
233
+ with_lock do
229
234
  on(KAMAL.hosts) do |host|
230
235
  roles = KAMAL.roles_on(host)
231
236
 
232
237
  roles.each do |role|
233
238
  execute *KAMAL.auditor.record("Removed all app containers", role: role), verbosity: :debug
234
- execute *KAMAL.app(role: role).remove_containers
239
+ execute *KAMAL.app(role: role, host: host).remove_containers
235
240
  end
236
241
  end
237
242
  end
@@ -239,7 +244,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
239
244
 
240
245
  desc "remove_images", "Remove all app images from servers", hide: true
241
246
  def remove_images
242
- mutating do
247
+ with_lock do
243
248
  on(KAMAL.hosts) do
244
249
  execute *KAMAL.auditor.record("Removed all app images"), verbosity: :debug
245
250
  execute *KAMAL.app.remove_images
@@ -251,7 +256,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
251
256
  def version
252
257
  on(KAMAL.hosts) do |host|
253
258
  role = KAMAL.roles_on(host).first
254
- puts_by_host host, capture_with_info(*KAMAL.app(role: role).current_running_version).strip
259
+ puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).current_running_version).strip
255
260
  end
256
261
  end
257
262
 
@@ -274,7 +279,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
274
279
  version = nil
275
280
  on(host) do
276
281
  role = KAMAL.roles_on(host).first
277
- version = capture_with_info(*KAMAL.app(role: role).current_running_version).strip
282
+ version = capture_with_info(*KAMAL.app(role: role, host: host).current_running_version).strip
278
283
  end
279
284
  version.presence
280
285
  end
@@ -282,4 +287,12 @@ class Kamal::Cli::App < Kamal::Cli::Base
282
287
  def version_or_latest
283
288
  options[:version] || KAMAL.config.latest_tag
284
289
  end
290
+
291
+ def with_lock_if_stopping
292
+ if options[:stop]
293
+ with_lock { yield }
294
+ else
295
+ yield
296
+ end
297
+ end
285
298
  end
@@ -79,28 +79,27 @@ module Kamal::Cli
79
79
  puts " Finished all in #{sprintf("%.1f seconds", runtime)}"
80
80
  end
81
81
 
82
- def mutating
83
- return yield if KAMAL.holding_lock?
84
-
85
- run_hook "pre-connect"
86
-
87
- ensure_run_and_locks_directory
88
-
89
- acquire_lock
90
-
91
- begin
82
+ def with_lock
83
+ if KAMAL.holding_lock?
92
84
  yield
93
- rescue
94
- if KAMAL.hold_lock_on_error?
95
- error " \e[31mDeploy lock was not released\e[0m"
96
- else
97
- release_lock
85
+ else
86
+ ensure_run_and_locks_directory
87
+
88
+ acquire_lock
89
+
90
+ begin
91
+ yield
92
+ rescue
93
+ begin
94
+ release_lock
95
+ rescue => e
96
+ say "Error releasing the deploy lock: #{e.message}", :red
97
+ end
98
+ raise
98
99
  end
99
100
 
100
- raise
101
+ release_lock
101
102
  end
102
-
103
- release_lock
104
103
  end
105
104
 
106
105
  def confirming(question)
@@ -141,16 +140,6 @@ module Kamal::Cli
141
140
  end
142
141
  end
143
142
 
144
- def hold_lock_on_error
145
- if KAMAL.hold_lock_on_error?
146
- yield
147
- else
148
- KAMAL.hold_lock_on_error = true
149
- yield
150
- KAMAL.hold_lock_on_error = false
151
- end
152
- end
153
-
154
143
  def run_hook(hook, **extra_details)
155
144
  if !options[:skip_hooks] && KAMAL.hook.hook_exists?(hook)
156
145
  details = { hosts: KAMAL.hosts.join(","), command: command, subcommand: subcommand }
@@ -164,6 +153,15 @@ module Kamal::Cli
164
153
  end
165
154
  end
166
155
 
156
+ def on(*args, &block)
157
+ if !KAMAL.connected?
158
+ run_hook "pre-connect"
159
+ KAMAL.connected = true
160
+ end
161
+
162
+ super
163
+ end
164
+
167
165
  def command
168
166
  @kamal_command ||= begin
169
167
  invocation_class, invocation_commands = *first_invocation