kamal 1.5.2 → 1.6.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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/lib/kamal/cli/accessory.rb +25 -21
  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 +57 -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 +58 -50
  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 +4 -5
  13. data/lib/kamal/cli/main.rb +36 -43
  14. data/lib/kamal/cli/prune.rb +3 -3
  15. data/lib/kamal/cli/server.rb +39 -15
  16. data/lib/kamal/cli/traefik.rb +8 -8
  17. data/lib/kamal/commander.rb +6 -6
  18. data/lib/kamal/commands/app/containers.rb +8 -0
  19. data/lib/kamal/commands/app/execution.rb +3 -3
  20. data/lib/kamal/commands/app/logging.rb +2 -2
  21. data/lib/kamal/commands/app.rb +6 -5
  22. data/lib/kamal/commands/base.rb +2 -3
  23. data/lib/kamal/commands/builder/base.rb +6 -12
  24. data/lib/kamal/commands/builder/clone.rb +28 -0
  25. data/lib/kamal/commands/builder/multiarch.rb +9 -9
  26. data/lib/kamal/commands/builder/native/cached.rb +6 -7
  27. data/lib/kamal/commands/builder/native/remote.rb +9 -9
  28. data/lib/kamal/commands/builder/native.rb +6 -7
  29. data/lib/kamal/commands/builder.rb +2 -0
  30. data/lib/kamal/configuration/builder.rb +33 -2
  31. data/lib/kamal/configuration/env/tag.rb +12 -0
  32. data/lib/kamal/configuration/env.rb +1 -1
  33. data/lib/kamal/configuration/role.rb +29 -6
  34. data/lib/kamal/configuration.rb +14 -2
  35. data/lib/kamal/git.rb +4 -0
  36. data/lib/kamal/sshkit_with_ext.rb +36 -0
  37. data/lib/kamal/version.rb +1 -1
  38. metadata +18 -9
  39. data/lib/kamal/cli/healthcheck.rb +0 -21
  40. 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: 1386c468f912402e6ed544a4e65bd3c0bdca8840cc3c519c5251b7ec5e89931b
4
+ data.tar.gz: 025ffc718735a857a03bfb06ee2498e33caf3d04e1e88cfd43baa753413b2fea
5
5
  SHA512:
6
- metadata.gz: 9da318bbc1fe8e5dac2b09cc8c89682417653de15b72567378a6998e0992fad999a1a8bca4b32f8eb2a7147f774d59efb4850b50ea15bfcde32d8426b50303fe
7
- data.tar.gz: ca156878b11110b0bc7092790565bfbf5b3cdf9206a2faf210c805856dab0aef9e57b883c09f86422c6c0821f6c281a7ed892ccf061101750dc8507326ba53d3
6
+ metadata.gz: 051e11bfe7e3d23bdac103fa39a750b5ce10bfe29b9ce642239bc2a1752080bbef995b5075a2143cd0863cbe57dffb96ce97f75630863ba25f50d03a5e27cf5e
7
+ data.tar.gz: b0d979ab7b2dc0e1fd8592d39cb5856eae5b8f9aca8e49fe726785132fae5455ee39c7c607b256d8d18ba1546af576e28c4a447c334f59ce8aba8e0e1af56c5e
@@ -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)
@@ -174,17 +174,12 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
174
174
  desc "remove [NAME]", "Remove accessory container, image and data directory from host (use NAME=all to remove all accessories)"
175
175
  option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
176
176
  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
177
+ confirming "This will remove all containers, images and data directories for #{name}. Are you sure?" do
178
+ with_lock do
179
+ if name == "all"
180
+ KAMAL.accessory_names.each { |accessory_name| remove_accessory(accessory_name) }
181
+ else
182
+ remove_accessory(name)
188
183
  end
189
184
  end
190
185
  end
@@ -192,7 +187,7 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
192
187
 
193
188
  desc "remove_container [NAME]", "Remove accessory container from host", hide: true
194
189
  def remove_container(name)
195
- mutating do
190
+ with_lock do
196
191
  with_accessory(name) do |accessory, hosts|
197
192
  on(hosts) do
198
193
  execute *KAMAL.auditor.record("Remove #{name} accessory container"), verbosity: :debug
@@ -204,7 +199,7 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
204
199
 
205
200
  desc "remove_image [NAME]", "Remove accessory image from host", hide: true
206
201
  def remove_image(name)
207
- mutating do
202
+ with_lock do
208
203
  with_accessory(name) do |accessory, hosts|
209
204
  on(hosts) do
210
205
  execute *KAMAL.auditor.record("Removed #{name} accessory image"), verbosity: :debug
@@ -216,7 +211,7 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
216
211
 
217
212
  desc "remove_service_directory [NAME]", "Remove accessory directory used for uploaded files and data directories from host", hide: true
218
213
  def remove_service_directory(name)
219
- mutating do
214
+ with_lock do
220
215
  with_accessory(name) do |accessory, hosts|
221
216
  on(hosts) do
222
217
  execute *accessory.remove_service_directory
@@ -250,4 +245,13 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
250
245
  accessory.hosts
251
246
  end
252
247
  end
248
+
249
+ def remove_accessory(name)
250
+ with_accessory(name) do
251
+ stop(name)
252
+ remove_container(name)
253
+ remove_image(name)
254
+ remove_service_directory(name)
255
+ end
256
+ end
253
257
  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
@@ -180,8 +181,9 @@ class Kamal::Cli::App < Kamal::Cli::Base
180
181
  KAMAL.specific_roles ||= [ "web" ]
181
182
  role = KAMAL.roles_on(KAMAL.primary_host).first
182
183
 
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)
184
+ app = KAMAL.app(role: role, host: host)
185
+ info app.follow_logs(host: KAMAL.primary_host, lines: lines, grep: grep)
186
+ exec app.follow_logs(host: KAMAL.primary_host, lines: lines, grep: grep)
185
187
  end
186
188
  else
187
189
  lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set
@@ -191,7 +193,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
191
193
 
192
194
  roles.each do |role|
193
195
  begin
194
- puts_by_host host, capture_with_info(*KAMAL.app(role: role).logs(since: since, lines: lines, grep: grep))
196
+ puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).logs(since: since, lines: lines, grep: grep))
195
197
  rescue SSHKit::Command::Failed
196
198
  puts_by_host host, "Nothing found"
197
199
  end
@@ -202,7 +204,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
202
204
 
203
205
  desc "remove", "Remove app containers and images from servers"
204
206
  def remove
205
- mutating do
207
+ with_lock do
206
208
  stop
207
209
  remove_containers
208
210
  remove_images
@@ -211,13 +213,13 @@ class Kamal::Cli::App < Kamal::Cli::Base
211
213
 
212
214
  desc "remove_container [VERSION]", "Remove app container with given version from servers", hide: true
213
215
  def remove_container(version)
214
- mutating do
216
+ with_lock do
215
217
  on(KAMAL.hosts) do |host|
216
218
  roles = KAMAL.roles_on(host)
217
219
 
218
220
  roles.each do |role|
219
221
  execute *KAMAL.auditor.record("Removed app container with version #{version}", role: role), verbosity: :debug
220
- execute *KAMAL.app(role: role).remove_container(version: version)
222
+ execute *KAMAL.app(role: role, host: host).remove_container(version: version)
221
223
  end
222
224
  end
223
225
  end
@@ -225,13 +227,13 @@ class Kamal::Cli::App < Kamal::Cli::Base
225
227
 
226
228
  desc "remove_containers", "Remove all app containers from servers", hide: true
227
229
  def remove_containers
228
- mutating do
230
+ with_lock do
229
231
  on(KAMAL.hosts) do |host|
230
232
  roles = KAMAL.roles_on(host)
231
233
 
232
234
  roles.each do |role|
233
235
  execute *KAMAL.auditor.record("Removed all app containers", role: role), verbosity: :debug
234
- execute *KAMAL.app(role: role).remove_containers
236
+ execute *KAMAL.app(role: role, host: host).remove_containers
235
237
  end
236
238
  end
237
239
  end
@@ -239,7 +241,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
239
241
 
240
242
  desc "remove_images", "Remove all app images from servers", hide: true
241
243
  def remove_images
242
- mutating do
244
+ with_lock do
243
245
  on(KAMAL.hosts) do
244
246
  execute *KAMAL.auditor.record("Removed all app images"), verbosity: :debug
245
247
  execute *KAMAL.app.remove_images
@@ -251,7 +253,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
251
253
  def version
252
254
  on(KAMAL.hosts) do |host|
253
255
  role = KAMAL.roles_on(host).first
254
- puts_by_host host, capture_with_info(*KAMAL.app(role: role).current_running_version).strip
256
+ puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).current_running_version).strip
255
257
  end
256
258
  end
257
259
 
@@ -274,7 +276,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
274
276
  version = nil
275
277
  on(host) do
276
278
  role = KAMAL.roles_on(host).first
277
- version = capture_with_info(*KAMAL.app(role: role).current_running_version).strip
279
+ version = capture_with_info(*KAMAL.app(role: role, host: host).current_running_version).strip
278
280
  end
279
281
  version.presence
280
282
  end
@@ -282,4 +284,12 @@ class Kamal::Cli::App < Kamal::Cli::Base
282
284
  def version_or_latest
283
285
  options[:version] || KAMAL.config.latest_tag
284
286
  end
287
+
288
+ def with_lock_if_stopping
289
+ if options[:stop]
290
+ with_lock { yield }
291
+ else
292
+ yield
293
+ end
294
+ end
285
295
  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
@@ -0,0 +1,61 @@
1
+ require "uri"
2
+
3
+ class Kamal::Cli::Build::Clone
4
+ attr_reader :sshkit
5
+ delegate :info, :error, :execute, :capture_with_info, to: :sshkit
6
+
7
+ def initialize(sshkit)
8
+ @sshkit = sshkit
9
+ end
10
+
11
+ def prepare
12
+ begin
13
+ clone_repo
14
+ rescue SSHKit::Command::Failed => e
15
+ if e.message =~ /already exists and is not an empty directory/
16
+ reset
17
+ else
18
+ raise Kamal::Cli::Build::BuildError, "Failed to clone repo: #{e.message}"
19
+ end
20
+ end
21
+
22
+ validate!
23
+ rescue Kamal::Cli::Build::BuildError => e
24
+ error "Error preparing clone: #{e.message}, deleting and retrying..."
25
+
26
+ FileUtils.rm_rf KAMAL.config.builder.clone_directory
27
+ clone_repo
28
+ validate!
29
+ end
30
+
31
+ private
32
+ def clone_repo
33
+ info "Cloning repo into build directory `#{KAMAL.config.builder.build_directory}`..."
34
+
35
+ FileUtils.mkdir_p KAMAL.config.builder.clone_directory
36
+ execute *KAMAL.builder.clone
37
+ end
38
+
39
+ def reset
40
+ info "Resetting local clone as `#{KAMAL.config.builder.build_directory}` already exists..."
41
+
42
+ KAMAL.builder.clone_reset_steps.each { |step| execute *step }
43
+ rescue SSHKit::Command::Failed => e
44
+ raise Kamal::Cli::Build::BuildError, "Failed to clone repo: #{e.message}"
45
+ end
46
+
47
+ def validate!
48
+ status = capture_with_info(*KAMAL.builder.clone_status).strip
49
+
50
+ unless status.empty?
51
+ raise Kamal::Cli::Build::BuildError, "Clone in #{KAMAL.config.builder.build_directory} is dirty, #{status}"
52
+ end
53
+
54
+ revision = capture_with_info(*KAMAL.builder.clone_revision).strip
55
+ if revision != Kamal::Git.revision
56
+ raise Kamal::Cli::Build::BuildError, "Clone in #{KAMAL.config.builder.build_directory} is not on the correct revision, expected `#{Kamal::Git.revision}` but got `#{revision}`"
57
+ end
58
+ rescue SSHKit::Command::Failed => e
59
+ raise Kamal::Cli::Build::BuildError, "Failed to validate clone: #{e.message}"
60
+ end
61
+ end