kamal 1.5.2 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -5,39 +5,50 @@ class Kamal::Cli::Build < Kamal::Cli::Base
5
5
 
6
6
  desc "deliver", "Build app and push app image to registry then pull image on servers"
7
7
  def deliver
8
- mutating do
9
- push
10
- pull
11
- end
8
+ push
9
+ pull
12
10
  end
13
11
 
14
12
  desc "push", "Build and push app image to registry"
15
13
  def push
16
- mutating do
17
- cli = self
14
+ cli = self
15
+
16
+ verify_local_dependencies
17
+ run_hook "pre-build"
18
18
 
19
- verify_local_dependencies
20
- run_hook "pre-build"
19
+ uncommitted_changes = Kamal::Git.uncommitted_changes
21
20
 
22
- if (uncommitted_changes = Kamal::Git.uncommitted_changes).present?
23
- say "The following paths have uncommitted changes:\n #{uncommitted_changes}", :yellow
21
+ if KAMAL.config.builder.git_clone?
22
+ if uncommitted_changes.present?
23
+ say "Building from a local git clone, so ignoring these uncommitted changes:\n #{uncommitted_changes}", :yellow
24
24
  end
25
25
 
26
26
  run_locally do
27
- begin
28
- KAMAL.with_verbosity(:debug) do
29
- execute *KAMAL.builder.push
30
- end
31
- rescue SSHKit::Command::Failed => e
32
- if e.message =~ /(no builder)|(no such file or directory)/
33
- warn "Missing compatible builder, so creating a new one first"
27
+ Clone.new(self).prepare
28
+ end
29
+ elsif uncommitted_changes.present?
30
+ say "Building with uncommitted changes:\n #{uncommitted_changes}", :yellow
31
+ end
32
+
33
+ # Get the command here to ensure the Dir.chdir doesn't interfere with it
34
+ push = KAMAL.builder.push
35
+
36
+ run_locally do
37
+ begin
38
+ KAMAL.with_verbosity(:debug) do
39
+ Dir.chdir(KAMAL.config.builder.build_directory) { execute *push }
40
+ end
41
+ rescue SSHKit::Command::Failed => e
42
+ if e.message =~ /(no builder)|(no such file or directory)/
43
+ warn "Missing compatible builder, so creating a new one first"
34
44
 
35
- if cli.create
36
- KAMAL.with_verbosity(:debug) { execute *KAMAL.builder.push }
45
+ if cli.create
46
+ KAMAL.with_verbosity(:debug) do
47
+ Dir.chdir(KAMAL.config.builder.build_directory) { execute *push }
37
48
  end
38
- else
39
- raise
40
49
  end
50
+ else
51
+ raise
41
52
  end
42
53
  end
43
54
  end
@@ -45,34 +56,30 @@ class Kamal::Cli::Build < Kamal::Cli::Base
45
56
 
46
57
  desc "pull", "Pull app image from registry onto servers"
47
58
  def pull
48
- mutating do
49
- on(KAMAL.hosts) do
50
- execute *KAMAL.auditor.record("Pulled image with version #{KAMAL.config.version}"), verbosity: :debug
51
- execute *KAMAL.builder.clean, raise_on_non_zero_exit: false
52
- execute *KAMAL.builder.pull
53
- execute *KAMAL.builder.validate_image
54
- end
59
+ on(KAMAL.hosts) do
60
+ execute *KAMAL.auditor.record("Pulled image with version #{KAMAL.config.version}"), verbosity: :debug
61
+ execute *KAMAL.builder.clean, raise_on_non_zero_exit: false
62
+ execute *KAMAL.builder.pull
63
+ execute *KAMAL.builder.validate_image
55
64
  end
56
65
  end
57
66
 
58
67
  desc "create", "Create a build setup"
59
68
  def create
60
- mutating do
61
- if (remote_host = KAMAL.config.builder.remote_host)
62
- connect_to_remote_host(remote_host)
63
- end
69
+ if (remote_host = KAMAL.config.builder.remote_host)
70
+ connect_to_remote_host(remote_host)
71
+ end
64
72
 
65
- run_locally do
66
- begin
67
- debug "Using builder: #{KAMAL.builder.name}"
68
- execute *KAMAL.builder.create
69
- rescue SSHKit::Command::Failed => e
70
- if e.message =~ /stderr=(.*)/
71
- error "Couldn't create remote builder: #{$1}"
72
- false
73
- else
74
- raise
75
- end
73
+ run_locally do
74
+ begin
75
+ debug "Using builder: #{KAMAL.builder.name}"
76
+ execute *KAMAL.builder.create
77
+ rescue SSHKit::Command::Failed => e
78
+ if e.message =~ /stderr=(.*)/
79
+ error "Couldn't create remote builder: #{$1}"
80
+ false
81
+ else
82
+ raise
76
83
  end
77
84
  end
78
85
  end
@@ -80,11 +87,9 @@ class Kamal::Cli::Build < Kamal::Cli::Base
80
87
 
81
88
  desc "remove", "Remove build setup"
82
89
  def remove
83
- mutating do
84
- run_locally do
85
- debug "Using builder: #{KAMAL.builder.name}"
86
- execute *KAMAL.builder.remove
87
- end
90
+ run_locally do
91
+ debug "Using builder: #{KAMAL.builder.name}"
92
+ execute *KAMAL.builder.remove
88
93
  end
89
94
  end
90
95
 
@@ -114,8 +119,11 @@ class Kamal::Cli::Build < Kamal::Cli::Base
114
119
  def connect_to_remote_host(remote_host)
115
120
  remote_uri = URI.parse(remote_host)
116
121
  if remote_uri.scheme == "ssh"
117
- options = { user: remote_uri.user, port: remote_uri.port }.compact
118
- on(remote_uri.host, options) do
122
+ host = SSHKit::Host.new(
123
+ hostname: remote_uri.host,
124
+ ssh_options: { user: remote_uri.user, port: remote_uri.port }.compact
125
+ )
126
+ on(host, options) do
119
127
  execute "true"
120
128
  end
121
129
  end
data/lib/kamal/cli/env.rb CHANGED
@@ -3,13 +3,13 @@ require "tempfile"
3
3
  class Kamal::Cli::Env < Kamal::Cli::Base
4
4
  desc "push", "Push the env file to the remote hosts"
5
5
  def push
6
- mutating do
6
+ with_lock do
7
7
  on(KAMAL.hosts) do
8
8
  execute *KAMAL.auditor.record("Pushed env files"), verbosity: :debug
9
9
 
10
10
  KAMAL.roles_on(host).each do |role|
11
- execute *KAMAL.app(role: role).make_env_directory
12
- upload! role.env.secrets_io, role.env.secrets_file, mode: 400
11
+ execute *KAMAL.app(role: role, host: host).make_env_directory
12
+ upload! role.env(host).secrets_io, role.env(host).secrets_file, mode: 400
13
13
  end
14
14
  end
15
15
 
@@ -30,12 +30,12 @@ class Kamal::Cli::Env < Kamal::Cli::Base
30
30
 
31
31
  desc "delete", "Delete the env file from the remote hosts"
32
32
  def delete
33
- mutating do
33
+ with_lock do
34
34
  on(KAMAL.hosts) do
35
35
  execute *KAMAL.auditor.record("Deleted env files"), verbosity: :debug
36
36
 
37
37
  KAMAL.roles_on(host).each do |role|
38
- execute *KAMAL.app(role: role).remove_env_file
38
+ execute *KAMAL.app(role: role, host: host).remove_env_file
39
39
  end
40
40
  end
41
41
 
@@ -0,0 +1,31 @@
1
+ class Kamal::Cli::Healthcheck::Barrier
2
+ def initialize
3
+ @ivar = Concurrent::IVar.new
4
+ end
5
+
6
+ def close
7
+ set(false)
8
+ end
9
+
10
+ def open
11
+ set(true)
12
+ end
13
+
14
+ def wait
15
+ unless opened?
16
+ raise Kamal::Cli::Healthcheck::Error.new("Halted at barrier")
17
+ end
18
+ end
19
+
20
+ private
21
+ def opened?
22
+ @ivar.value
23
+ end
24
+
25
+ def set(value)
26
+ @ivar.set(value)
27
+ true
28
+ rescue Concurrent::MultipleAssignmentError
29
+ false
30
+ end
31
+ end
@@ -0,0 +1,2 @@
1
+ class Kamal::Cli::Healthcheck::Error < StandardError
2
+ end
@@ -3,7 +3,6 @@ module Kamal::Cli::Healthcheck::Poller
3
3
 
4
4
  TRAEFIK_UPDATE_DELAY = 5
5
5
 
6
- class HealthcheckError < StandardError; end
7
6
 
8
7
  def wait_for_healthy(pause_after_ready: false, &block)
9
8
  attempt = 1
@@ -16,9 +15,9 @@ module Kamal::Cli::Healthcheck::Poller
16
15
  when "running" # No health check configured
17
16
  sleep KAMAL.config.readiness_delay if pause_after_ready
18
17
  else
19
- raise HealthcheckError, "container not ready (#{status})"
18
+ raise Kamal::Cli::Healthcheck::Error, "container not ready (#{status})"
20
19
  end
21
- rescue HealthcheckError => e
20
+ rescue Kamal::Cli::Healthcheck::Error => e
22
21
  if attempt <= max_attempts
23
22
  info "#{e.message}, retrying in #{attempt}s (attempt #{attempt}/#{max_attempts})..."
24
23
  sleep attempt
@@ -41,9 +40,9 @@ module Kamal::Cli::Healthcheck::Poller
41
40
  when "unhealthy"
42
41
  sleep TRAEFIK_UPDATE_DELAY if pause_after_ready
43
42
  else
44
- raise HealthcheckError, "container not unhealthy (#{status})"
43
+ raise Kamal::Cli::Healthcheck::Error, "container not unhealthy (#{status})"
45
44
  end
46
- rescue HealthcheckError => e
45
+ rescue Kamal::Cli::Healthcheck::Error => e
47
46
  if attempt <= max_attempts
48
47
  info "#{e.message}, retrying in #{attempt}s (attempt #{attempt}/#{max_attempts})..."
49
48
  sleep attempt
@@ -3,14 +3,14 @@ class Kamal::Cli::Main < Kamal::Cli::Base
3
3
  option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip image build and push"
4
4
  def setup
5
5
  print_runtime do
6
- mutating do
6
+ with_lock do
7
7
  invoke_options = deploy_options
8
8
 
9
9
  say "Ensure Docker is installed...", :magenta
10
10
  invoke "kamal:cli:server:bootstrap", [], invoke_options
11
11
 
12
- say "Push env files...", :magenta
13
- invoke "kamal:cli:env:push", [], invoke_options
12
+ say "Evaluate and push env files...", :magenta
13
+ invoke "kamal:cli:main:envify", [], invoke_options
14
14
 
15
15
  invoke "kamal:cli:accessory:boot", [ "all" ], invoke_options
16
16
  deploy
@@ -22,30 +22,25 @@ class Kamal::Cli::Main < Kamal::Cli::Base
22
22
  option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip image build and push"
23
23
  def deploy
24
24
  runtime = print_runtime do
25
- mutating do
26
- invoke_options = deploy_options
25
+ invoke_options = deploy_options
27
26
 
28
- say "Log into image registry...", :magenta
29
- invoke "kamal:cli:registry:login", [], invoke_options
27
+ say "Log into image registry...", :magenta
28
+ invoke "kamal:cli:registry:login", [], invoke_options
30
29
 
31
- if options[:skip_push]
32
- say "Pull app image...", :magenta
33
- invoke "kamal:cli:build:pull", [], invoke_options
34
- else
35
- say "Build and push app image...", :magenta
36
- invoke "kamal:cli:build:deliver", [], invoke_options
37
- end
30
+ if options[:skip_push]
31
+ say "Pull app image...", :magenta
32
+ invoke "kamal:cli:build:pull", [], invoke_options
33
+ else
34
+ say "Build and push app image...", :magenta
35
+ invoke "kamal:cli:build:deliver", [], invoke_options
36
+ end
38
37
 
38
+ with_lock do
39
39
  run_hook "pre-deploy"
40
40
 
41
41
  say "Ensure Traefik is running...", :magenta
42
42
  invoke "kamal:cli:traefik:boot", [], invoke_options
43
43
 
44
- if KAMAL.config.role(KAMAL.config.primary_role).running_traefik?
45
- say "Ensure app can pass healthcheck...", :magenta
46
- invoke "kamal:cli:healthcheck:perform", [], invoke_options
47
- end
48
-
49
44
  say "Detect stale containers...", :magenta
50
45
  invoke "kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true)
51
46
 
@@ -63,22 +58,19 @@ class Kamal::Cli::Main < Kamal::Cli::Base
63
58
  option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip image build and push"
64
59
  def redeploy
65
60
  runtime = print_runtime do
66
- mutating do
67
- invoke_options = deploy_options
61
+ invoke_options = deploy_options
68
62
 
69
- if options[:skip_push]
70
- say "Pull app image...", :magenta
71
- invoke "kamal:cli:build:pull", [], invoke_options
72
- else
73
- say "Build and push app image...", :magenta
74
- invoke "kamal:cli:build:deliver", [], invoke_options
75
- end
63
+ if options[:skip_push]
64
+ say "Pull app image...", :magenta
65
+ invoke "kamal:cli:build:pull", [], invoke_options
66
+ else
67
+ say "Build and push app image...", :magenta
68
+ invoke "kamal:cli:build:deliver", [], invoke_options
69
+ end
76
70
 
71
+ with_lock do
77
72
  run_hook "pre-deploy"
78
73
 
79
- say "Ensure app can pass healthcheck...", :magenta
80
- invoke "kamal:cli:healthcheck:perform", [], invoke_options
81
-
82
74
  say "Detect stale containers...", :magenta
83
75
  invoke "kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true)
84
76
 
@@ -93,7 +85,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base
93
85
  def rollback(version)
94
86
  rolled_back = false
95
87
  runtime = print_runtime do
96
- mutating do
88
+ with_lock do
97
89
  invoke_options = deploy_options
98
90
 
99
91
  KAMAL.config.version = version
@@ -185,19 +177,23 @@ class Kamal::Cli::Main < Kamal::Cli::Base
185
177
  env_path = ".env"
186
178
  end
187
179
 
188
- File.write(env_path, ERB.new(File.read(env_template_path), trim_mode: "-").result, perm: 0600)
180
+ if Pathname.new(File.expand_path(env_template_path)).exist?
181
+ File.write(env_path, ERB.new(File.read(env_template_path), trim_mode: "-").result, perm: 0600)
189
182
 
190
- unless options[:skip_push]
191
- reload_envs
192
- invoke "kamal:cli:env:push", options
183
+ unless options[:skip_push]
184
+ reload_envs
185
+ invoke "kamal:cli:env:push", options
186
+ end
187
+ else
188
+ puts "Skipping envify (no #{env_template_path} exist)"
193
189
  end
194
190
  end
195
191
 
196
192
  desc "remove", "Remove Traefik, app, accessories, and registry session from servers"
197
193
  option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
198
194
  def remove
199
- mutating do
200
- confirming "This will remove all containers and images. Are you sure?" do
195
+ confirming "This will remove all containers and images. Are you sure?" do
196
+ with_lock do
201
197
  invoke "kamal:cli:traefik:remove", [], options.without(:confirmed)
202
198
  invoke "kamal:cli:app:remove", [], options.without(:confirmed)
203
199
  invoke "kamal:cli:accessory:remove", [ "all" ], options
@@ -223,9 +219,6 @@ class Kamal::Cli::Main < Kamal::Cli::Base
223
219
  desc "env", "Manage environment files"
224
220
  subcommand "env", Kamal::Cli::Env
225
221
 
226
- desc "healthcheck", "Healthcheck application"
227
- subcommand "healthcheck", Kamal::Cli::Healthcheck
228
-
229
222
  desc "lock", "Manage the deploy lock"
230
223
  subcommand "lock", Kamal::Cli::Lock
231
224
 
@@ -246,11 +239,11 @@ class Kamal::Cli::Main < Kamal::Cli::Base
246
239
  begin
247
240
  on(KAMAL.hosts) do
248
241
  KAMAL.roles_on(host).each do |role|
249
- container_id = capture_with_info(*KAMAL.app(role: role).container_id_for_version(version))
242
+ container_id = capture_with_info(*KAMAL.app(role: role, host: host).container_id_for_version(version))
250
243
  raise "Container not found" unless container_id.present?
251
244
  end
252
245
  end
253
- rescue SSHKit::Runner::ExecuteError => e
246
+ rescue SSHKit::Runner::ExecuteError, SSHKit::Runner::MultipleExecuteError => e
254
247
  if e.message =~ /Container not found/
255
248
  say "Error looking for container version #{version}: #{e.message}"
256
249
  return false
@@ -1,7 +1,7 @@
1
1
  class Kamal::Cli::Prune < Kamal::Cli::Base
2
2
  desc "all", "Prune unused images and stopped containers"
3
3
  def all
4
- mutating do
4
+ with_lock do
5
5
  containers
6
6
  images
7
7
  end
@@ -9,7 +9,7 @@ class Kamal::Cli::Prune < Kamal::Cli::Base
9
9
 
10
10
  desc "images", "Prune unused images"
11
11
  def images
12
- mutating do
12
+ with_lock do
13
13
  on(KAMAL.hosts) do
14
14
  execute *KAMAL.auditor.record("Pruned images"), verbosity: :debug
15
15
  execute *KAMAL.prune.dangling_images
@@ -24,7 +24,7 @@ class Kamal::Cli::Prune < Kamal::Cli::Base
24
24
  retain = options.fetch(:retain, KAMAL.config.retain_containers)
25
25
  raise "retain must be at least 1" if retain < 1
26
26
 
27
- mutating do
27
+ with_lock do
28
28
  on(KAMAL.hosts) do
29
29
  execute *KAMAL.auditor.record("Pruned containers"), verbosity: :debug
30
30
  execute *KAMAL.prune.app_containers(retain: retain)
@@ -1,25 +1,49 @@
1
1
  class Kamal::Cli::Server < Kamal::Cli::Base
2
+ desc "exec", "Run a custom command on the server (use --help to show options)"
3
+ option :interactive, type: :boolean, aliases: "-i", default: false, desc: "Run the command interactively (use for console/bash)"
4
+ def exec(cmd)
5
+ hosts = KAMAL.hosts | KAMAL.accessory_hosts
6
+
7
+ case
8
+ when options[:interactive]
9
+ host = KAMAL.primary_host
10
+
11
+ say "Running '#{cmd}' on #{host} interactively...", :magenta
12
+
13
+ run_locally { exec KAMAL.server.run_over_ssh(cmd, host: host) }
14
+ else
15
+ say "Running '#{cmd}' on #{hosts.join(', ')}...", :magenta
16
+
17
+ on(hosts) do |host|
18
+ execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on #{host}"), verbosity: :debug
19
+ puts_by_host host, capture_with_info(cmd)
20
+ end
21
+ end
22
+ end
23
+
2
24
  desc "bootstrap", "Set up Docker to run Kamal apps"
3
25
  def bootstrap
4
- missing = []
5
-
6
- on(KAMAL.hosts | KAMAL.accessory_hosts) do |host|
7
- unless execute(*KAMAL.docker.installed?, raise_on_non_zero_exit: false)
8
- if execute(*KAMAL.docker.superuser?, raise_on_non_zero_exit: false)
9
- info "Missing Docker on #{host}. Installing…"
10
- execute *KAMAL.docker.install
11
- else
12
- missing << host
26
+ with_lock do
27
+ missing = []
28
+
29
+ on(KAMAL.hosts | KAMAL.accessory_hosts) do |host|
30
+ unless execute(*KAMAL.docker.installed?, raise_on_non_zero_exit: false)
31
+ if execute(*KAMAL.docker.superuser?, raise_on_non_zero_exit: false)
32
+ info "Missing Docker on #{host}. Installing…"
33
+ execute *KAMAL.docker.install
34
+ else
35
+ missing << host
36
+ end
13
37
  end
38
+
39
+ execute(*KAMAL.server.ensure_run_directory)
14
40
  end
15
41
 
16
- execute(*KAMAL.server.ensure_run_directory)
17
- end
42
+ if missing.any?
43
+ raise "Docker is not installed on #{missing.join(", ")} and can't be automatically installed without having root access and either `wget` or `curl`. Install Docker manually: https://docs.docker.com/engine/install/"
44
+ end
18
45
 
19
- if missing.any?
20
- raise "Docker is not installed on #{missing.join(", ")} and can't be automatically installed without having root access and either `wget` or `curl`. Install Docker manually: https://docs.docker.com/engine/install/"
46
+ run_hook "docker-setup"
21
47
  end
22
-
23
- run_hook "docker-setup"
24
48
  end
25
49
  end
@@ -1,7 +1,7 @@
1
1
  class Kamal::Cli::Traefik < Kamal::Cli::Base
2
2
  desc "boot", "Boot Traefik on servers"
3
3
  def boot
4
- mutating do
4
+ with_lock do
5
5
  on(KAMAL.traefik_hosts) do
6
6
  execute *KAMAL.registry.login
7
7
  execute *KAMAL.traefik.start_or_run
@@ -14,7 +14,7 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
14
14
  option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
15
15
  def reboot
16
16
  confirming "This will cause a brief outage on each host. Are you sure?" do
17
- mutating do
17
+ with_lock do
18
18
  host_groups = options[:rolling] ? KAMAL.traefik_hosts : [ KAMAL.traefik_hosts ]
19
19
  host_groups.each do |hosts|
20
20
  host_list = Array(hosts).join(",")
@@ -34,7 +34,7 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
34
34
 
35
35
  desc "start", "Start existing Traefik container on servers"
36
36
  def start
37
- mutating do
37
+ with_lock do
38
38
  on(KAMAL.traefik_hosts) do
39
39
  execute *KAMAL.auditor.record("Started traefik"), verbosity: :debug
40
40
  execute *KAMAL.traefik.start
@@ -44,7 +44,7 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
44
44
 
45
45
  desc "stop", "Stop existing Traefik container on servers"
46
46
  def stop
47
- mutating do
47
+ with_lock do
48
48
  on(KAMAL.traefik_hosts) do
49
49
  execute *KAMAL.auditor.record("Stopped traefik"), verbosity: :debug
50
50
  execute *KAMAL.traefik.stop, raise_on_non_zero_exit: false
@@ -54,7 +54,7 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
54
54
 
55
55
  desc "restart", "Restart existing Traefik container on servers"
56
56
  def restart
57
- mutating do
57
+ with_lock do
58
58
  stop
59
59
  start
60
60
  end
@@ -91,7 +91,7 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
91
91
 
92
92
  desc "remove", "Remove Traefik container and image from servers"
93
93
  def remove
94
- mutating do
94
+ with_lock do
95
95
  stop
96
96
  remove_container
97
97
  remove_image
@@ -100,7 +100,7 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
100
100
 
101
101
  desc "remove_container", "Remove Traefik container from servers", hide: true
102
102
  def remove_container
103
- mutating do
103
+ with_lock do
104
104
  on(KAMAL.traefik_hosts) do
105
105
  execute *KAMAL.auditor.record("Removed traefik container"), verbosity: :debug
106
106
  execute *KAMAL.traefik.remove_container
@@ -110,7 +110,7 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
110
110
 
111
111
  desc "remove_image", "Remove Traefik image from servers", hide: true
112
112
  def remove_image
113
- mutating do
113
+ with_lock do
114
114
  on(KAMAL.traefik_hosts) do
115
115
  execute *KAMAL.auditor.record("Removed traefik image"), verbosity: :debug
116
116
  execute *KAMAL.traefik.remove_image
@@ -2,13 +2,13 @@ require "active_support/core_ext/enumerable"
2
2
  require "active_support/core_ext/module/delegation"
3
3
 
4
4
  class Kamal::Commander
5
- attr_accessor :verbosity, :holding_lock, :hold_lock_on_error
5
+ attr_accessor :verbosity, :holding_lock, :connected
6
6
  delegate :hosts, :roles, :primary_host, :primary_role, :roles_on, :traefik_hosts, :accessory_hosts, to: :specifics
7
7
 
8
8
  def initialize
9
9
  self.verbosity = :info
10
10
  self.holding_lock = false
11
- self.hold_lock_on_error = false
11
+ self.connected = false
12
12
  @specifics = nil
13
13
  end
14
14
 
@@ -65,8 +65,8 @@ class Kamal::Commander
65
65
  end
66
66
 
67
67
 
68
- def app(role: nil)
69
- Kamal::Commands::App.new(config, role: role)
68
+ def app(role: nil, host: nil)
69
+ Kamal::Commands::App.new(config, role: role, host: host)
70
70
  end
71
71
 
72
72
  def accessory(name)
@@ -138,8 +138,8 @@ class Kamal::Commander
138
138
  self.holding_lock
139
139
  end
140
140
 
141
- def hold_lock_on_error?
142
- self.hold_lock_on_error
141
+ def connected?
142
+ self.connected
143
143
  end
144
144
 
145
145
  private
@@ -1,4 +1,6 @@
1
1
  module Kamal::Commands::App::Containers
2
+ DOCKER_HEALTH_LOG_FORMAT = "'{{json .State.Health}}'"
3
+
2
4
  def list_containers
3
5
  docker :container, :ls, "--all", *filter_args
4
6
  end
@@ -20,4 +22,10 @@ module Kamal::Commands::App::Containers
20
22
  def remove_containers
21
23
  docker :container, :prune, "--force", *filter_args
22
24
  end
25
+
26
+ def container_health_log(version:)
27
+ pipe \
28
+ container_id_for(container_name: container_name(version)),
29
+ xargs(docker(:inspect, "--format", DOCKER_HEALTH_LOG_FORMAT))
30
+ end
23
31
  end
@@ -11,7 +11,7 @@ module Kamal::Commands::App::Execution
11
11
  docker :run,
12
12
  ("-it" if interactive),
13
13
  "--rm",
14
- *role&.env_args,
14
+ *role&.env_args(host),
15
15
  *argumentize("--env", env),
16
16
  *config.volume_args,
17
17
  *role&.option_args,
@@ -19,11 +19,11 @@ module Kamal::Commands::App::Execution
19
19
  *command
20
20
  end
21
21
 
22
- def execute_in_existing_container_over_ssh(*command, host:, env:)
22
+ def execute_in_existing_container_over_ssh(*command, env:)
23
23
  run_over_ssh execute_in_existing_container(*command, interactive: true, env: env), host: host
24
24
  end
25
25
 
26
- def execute_in_new_container_over_ssh(*command, host:, env:)
26
+ def execute_in_new_container_over_ssh(*command, env:)
27
27
  run_over_ssh execute_in_new_container(*command, interactive: true, env: env), host: host
28
28
  end
29
29
  end
@@ -1,7 +1,7 @@
1
1
  module Kamal::Commands::App::Logging
2
- def logs(since: nil, lines: nil, grep: nil)
2
+ def logs(version: nil, since: nil, lines: nil, grep: nil)
3
3
  pipe \
4
- current_running_container_id,
4
+ version ? container_id_for_version(version) : current_running_container_id,
5
5
  "xargs docker logs#{" --since #{since}" if since}#{" --tail #{lines}" if lines} 2>&1",
6
6
  ("grep '#{grep}'" if grep)
7
7
  end