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
@@ -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
  #