kamal 1.5.2 → 1.7.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -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
@@ -5,74 +5,84 @@ 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"
34
-
35
- if cli.create
36
- KAMAL.with_verbosity(:debug) { execute *KAMAL.builder.push }
37
- end
38
- else
39
- raise
40
- end
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
+ context_hosts = capture_with_info(*KAMAL.builder.context_hosts).split("\n")
39
+
40
+ if context_hosts != KAMAL.builder.config_context_hosts
41
+ warn "Context hosts have changed, so re-creating builder, was: #{context_hosts.join(", ")}], now: #{KAMAL.builder.config_context_hosts.join(", ")}"
42
+ cli.remove
43
+ cli.create
41
44
  end
45
+ rescue SSHKit::Command::Failed => e
46
+ warn "Missing compatible builder, so creating a new one first"
47
+ if e.message =~ /(context not found|no builder)/
48
+ cli.create
49
+ else
50
+ raise
51
+ end
52
+ end
53
+
54
+ KAMAL.with_verbosity(:debug) do
55
+ Dir.chdir(KAMAL.config.builder.build_directory) { execute *push }
42
56
  end
43
57
  end
44
58
  end
45
59
 
46
60
  desc "pull", "Pull app image from registry onto servers"
47
61
  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
62
+ on(KAMAL.hosts) do
63
+ execute *KAMAL.auditor.record("Pulled image with version #{KAMAL.config.version}"), verbosity: :debug
64
+ execute *KAMAL.builder.clean, raise_on_non_zero_exit: false
65
+ execute *KAMAL.builder.pull
66
+ execute *KAMAL.builder.validate_image
55
67
  end
56
68
  end
57
69
 
58
70
  desc "create", "Create a build setup"
59
71
  def create
60
- mutating do
61
- if (remote_host = KAMAL.config.builder.remote_host)
62
- connect_to_remote_host(remote_host)
63
- end
72
+ if (remote_host = KAMAL.config.builder.remote_host)
73
+ connect_to_remote_host(remote_host)
74
+ end
64
75
 
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
76
+ run_locally do
77
+ begin
78
+ debug "Using builder: #{KAMAL.builder.name}"
79
+ execute *KAMAL.builder.create
80
+ rescue SSHKit::Command::Failed => e
81
+ if e.message =~ /stderr=(.*)/
82
+ error "Couldn't create remote builder: #{$1}"
83
+ false
84
+ else
85
+ raise
76
86
  end
77
87
  end
78
88
  end
@@ -80,11 +90,9 @@ class Kamal::Cli::Build < Kamal::Cli::Base
80
90
 
81
91
  desc "remove", "Remove build setup"
82
92
  def remove
83
- mutating do
84
- run_locally do
85
- debug "Using builder: #{KAMAL.builder.name}"
86
- execute *KAMAL.builder.remove
87
- end
93
+ run_locally do
94
+ debug "Using builder: #{KAMAL.builder.name}"
95
+ execute *KAMAL.builder.remove
88
96
  end
89
97
  end
90
98
 
@@ -114,8 +122,11 @@ class Kamal::Cli::Build < Kamal::Cli::Base
114
122
  def connect_to_remote_host(remote_host)
115
123
  remote_uri = URI.parse(remote_host)
116
124
  if remote_uri.scheme == "ssh"
117
- options = { user: remote_uri.user, port: remote_uri.port }.compact
118
- on(remote_uri.host, options) do
125
+ host = SSHKit::Host.new(
126
+ hostname: remote_uri.host,
127
+ ssh_options: { user: remote_uri.user, port: remote_uri.port }.compact
128
+ )
129
+ on(host, options) do
119
130
  execute "true"
120
131
  end
121
132
  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,11 +3,10 @@ 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
10
- max_attempts = KAMAL.config.healthcheck["max_attempts"]
9
+ max_attempts = KAMAL.config.healthcheck.max_attempts
11
10
 
12
11
  begin
13
12
  case status = block.call
@@ -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
@@ -34,16 +33,16 @@ module Kamal::Cli::Healthcheck::Poller
34
33
 
35
34
  def wait_for_unhealthy(pause_after_ready: false, &block)
36
35
  attempt = 1
37
- max_attempts = KAMAL.config.healthcheck["max_attempts"]
36
+ max_attempts = KAMAL.config.healthcheck.max_attempts
38
37
 
39
38
  begin
40
39
  case status = block.call
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.merge(skip_local: options[:skip_push])
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
@@ -134,6 +126,18 @@ class Kamal::Cli::Main < Kamal::Cli::Base
134
126
  end
135
127
  end
136
128
 
129
+ desc "docs", "Show Kamal documentation for configuration setting"
130
+ def docs(section = nil)
131
+ case section
132
+ when NilClass
133
+ puts Kamal::Configuration.validation_doc
134
+ else
135
+ puts Kamal::Configuration.const_get(section.titlecase.to_sym).validation_doc
136
+ end
137
+ rescue NameError
138
+ puts "No documentation found for #{section}"
139
+ end
140
+
137
141
  desc "init", "Create config stub in config/deploy.yml and env stub in .env"
138
142
  option :bundle, type: :boolean, default: false, desc: "Add Kamal to the Gemfile and create a bin/kamal binstub"
139
143
  def init
@@ -185,23 +189,27 @@ class Kamal::Cli::Main < Kamal::Cli::Base
185
189
  env_path = ".env"
186
190
  end
187
191
 
188
- File.write(env_path, ERB.new(File.read(env_template_path), trim_mode: "-").result, perm: 0600)
192
+ if Pathname.new(File.expand_path(env_template_path)).exist?
193
+ File.write(env_path, ERB.new(File.read(env_template_path), trim_mode: "-").result, perm: 0600)
189
194
 
190
- unless options[:skip_push]
191
- reload_envs
192
- invoke "kamal:cli:env:push", options
195
+ unless options[:skip_push]
196
+ reload_envs
197
+ invoke "kamal:cli:env:push", options
198
+ end
199
+ else
200
+ puts "Skipping envify (no #{env_template_path} exist)"
193
201
  end
194
202
  end
195
203
 
196
204
  desc "remove", "Remove Traefik, app, accessories, and registry session from servers"
197
205
  option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
198
206
  def remove
199
- mutating do
200
- confirming "This will remove all containers and images. Are you sure?" do
207
+ confirming "This will remove all containers and images. Are you sure?" do
208
+ with_lock do
201
209
  invoke "kamal:cli:traefik:remove", [], options.without(:confirmed)
202
210
  invoke "kamal:cli:app:remove", [], options.without(:confirmed)
203
211
  invoke "kamal:cli:accessory:remove", [ "all" ], options
204
- invoke "kamal:cli:registry:logout", [], options.without(:confirmed)
212
+ invoke "kamal:cli:registry:logout", [], options.without(:confirmed).merge(skip_local: true)
205
213
  end
206
214
  end
207
215
  end
@@ -223,9 +231,6 @@ class Kamal::Cli::Main < Kamal::Cli::Base
223
231
  desc "env", "Manage environment files"
224
232
  subcommand "env", Kamal::Cli::Env
225
233
 
226
- desc "healthcheck", "Healthcheck application"
227
- subcommand "healthcheck", Kamal::Cli::Healthcheck
228
-
229
234
  desc "lock", "Manage the deploy lock"
230
235
  subcommand "lock", Kamal::Cli::Lock
231
236
 
@@ -246,11 +251,11 @@ class Kamal::Cli::Main < Kamal::Cli::Base
246
251
  begin
247
252
  on(KAMAL.hosts) do
248
253
  KAMAL.roles_on(host).each do |role|
249
- container_id = capture_with_info(*KAMAL.app(role: role).container_id_for_version(version))
254
+ container_id = capture_with_info(*KAMAL.app(role: role, host: host).container_id_for_version(version))
250
255
  raise "Container not found" unless container_id.present?
251
256
  end
252
257
  end
253
- rescue SSHKit::Runner::ExecuteError => e
258
+ rescue SSHKit::Runner::ExecuteError, SSHKit::Runner::MultipleExecuteError => e
254
259
  if e.message =~ /Container not found/
255
260
  say "Error looking for container version #{version}: #{e.message}"
256
261
  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,18 +1,17 @@
1
1
  class Kamal::Cli::Registry < Kamal::Cli::Base
2
2
  desc "login", "Log in to registry locally and remotely"
3
+ option :skip_local, aliases: "-L", type: :boolean, default: false, desc: "Skip local login"
4
+ option :skip_remote, aliases: "-R", type: :boolean, default: false, desc: "Skip remote login"
3
5
  def login
4
- run_locally { execute *KAMAL.registry.login }
5
- on(KAMAL.hosts) { execute *KAMAL.registry.login }
6
- # FIXME: This rescue needed?
7
- rescue ArgumentError => e
8
- puts e.message
6
+ run_locally { execute *KAMAL.registry.login } unless options[:skip_local]
7
+ on(KAMAL.hosts) { execute *KAMAL.registry.login } unless options[:skip_remote]
9
8
  end
10
9
 
11
- desc "logout", "Log out of registry remotely"
10
+ desc "logout", "Log out of registry locally and remotely"
11
+ option :skip_local, aliases: "-L", type: :boolean, default: false, desc: "Skip local login"
12
+ option :skip_remote, aliases: "-R", type: :boolean, default: false, desc: "Skip remote login"
12
13
  def logout
13
- on(KAMAL.hosts) { execute *KAMAL.registry.logout }
14
- # FIXME: This rescue needed?
15
- rescue ArgumentError => e
16
- puts e.message
14
+ run_locally { execute *KAMAL.registry.logout } unless options[:skip_local]
15
+ on(KAMAL.hosts) { execute *KAMAL.registry.logout } unless options[:skip_remote]
17
16
  end
18
17
  end
@@ -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,4 +1,4 @@
1
- #!/usr/bin/env ruby
1
+ #!/bin/sh
2
2
 
3
3
  # A sample docker-setup hook
4
4
  #