kamal 2.3.0 → 2.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 (83) hide show
  1. checksums.yaml +4 -4
  2. data/lib/kamal/cli/accessory.rb +42 -16
  3. data/lib/kamal/cli/alias/command.rb +1 -0
  4. data/lib/kamal/cli/app/{prepare_assets.rb → assets.rb} +1 -1
  5. data/lib/kamal/cli/app/boot.rb +3 -2
  6. data/lib/kamal/cli/app/error_pages.rb +33 -0
  7. data/lib/kamal/cli/app/ssl_certificates.rb +28 -0
  8. data/lib/kamal/cli/app.rb +94 -29
  9. data/lib/kamal/cli/base.rb +29 -4
  10. data/lib/kamal/cli/build.rb +60 -18
  11. data/lib/kamal/cli/main.rb +8 -10
  12. data/lib/kamal/cli/proxy.rb +58 -25
  13. data/lib/kamal/cli/registry.rb +2 -0
  14. data/lib/kamal/cli/secrets.rb +9 -3
  15. data/lib/kamal/cli/server.rb +4 -2
  16. data/lib/kamal/cli/templates/deploy.yml +6 -3
  17. data/lib/kamal/cli/templates/sample_hooks/post-app-boot.sample +3 -0
  18. data/lib/kamal/cli/templates/sample_hooks/post-deploy.sample +1 -1
  19. data/lib/kamal/cli/templates/sample_hooks/pre-app-boot.sample +3 -0
  20. data/lib/kamal/cli/templates/sample_hooks/pre-build.sample +1 -1
  21. data/lib/kamal/cli/templates/sample_hooks/pre-connect.sample +1 -1
  22. data/lib/kamal/cli/templates/sample_hooks/pre-deploy.sample +19 -6
  23. data/lib/kamal/cli.rb +1 -0
  24. data/lib/kamal/commander/specifics.rb +9 -1
  25. data/lib/kamal/commander.rb +18 -27
  26. data/lib/kamal/commands/accessory/proxy.rb +16 -0
  27. data/lib/kamal/commands/accessory.rb +9 -9
  28. data/lib/kamal/commands/app/assets.rb +4 -4
  29. data/lib/kamal/commands/app/containers.rb +2 -2
  30. data/lib/kamal/commands/app/error_pages.rb +9 -0
  31. data/lib/kamal/commands/app/execution.rb +6 -4
  32. data/lib/kamal/commands/app/images.rb +1 -1
  33. data/lib/kamal/commands/app/logging.rb +14 -4
  34. data/lib/kamal/commands/app/proxy.rb +17 -1
  35. data/lib/kamal/commands/app.rb +19 -10
  36. data/lib/kamal/commands/auditor.rb +11 -5
  37. data/lib/kamal/commands/base.rb +37 -1
  38. data/lib/kamal/commands/builder/base.rb +20 -7
  39. data/lib/kamal/commands/builder/cloud.rb +22 -0
  40. data/lib/kamal/commands/builder/pack.rb +46 -0
  41. data/lib/kamal/commands/builder.rb +11 -19
  42. data/lib/kamal/commands/proxy.rb +55 -15
  43. data/lib/kamal/commands/registry.rb +9 -7
  44. data/lib/kamal/configuration/accessory.rb +66 -11
  45. data/lib/kamal/configuration/builder.rb +20 -0
  46. data/lib/kamal/configuration/docs/accessory.yml +32 -4
  47. data/lib/kamal/configuration/docs/alias.yml +2 -2
  48. data/lib/kamal/configuration/docs/builder.yml +22 -0
  49. data/lib/kamal/configuration/docs/configuration.yml +6 -0
  50. data/lib/kamal/configuration/docs/env.yml +31 -0
  51. data/lib/kamal/configuration/docs/proxy.yml +78 -15
  52. data/lib/kamal/configuration/docs/registry.yml +4 -0
  53. data/lib/kamal/configuration/env.rb +13 -4
  54. data/lib/kamal/configuration/proxy/boot.rb +129 -0
  55. data/lib/kamal/configuration/proxy.rb +67 -5
  56. data/lib/kamal/configuration/registry.rb +6 -6
  57. data/lib/kamal/configuration/role.rb +11 -9
  58. data/lib/kamal/configuration/servers.rb +8 -1
  59. data/lib/kamal/configuration/validator/accessory.rb +6 -2
  60. data/lib/kamal/configuration/validator/builder.rb +2 -0
  61. data/lib/kamal/configuration/validator/proxy.rb +10 -0
  62. data/lib/kamal/configuration/validator/role.rb +3 -1
  63. data/lib/kamal/configuration/validator/servers.rb +1 -1
  64. data/lib/kamal/configuration/validator.rb +21 -1
  65. data/lib/kamal/configuration.rb +36 -57
  66. data/lib/kamal/docker.rb +30 -0
  67. data/lib/kamal/git.rb +10 -0
  68. data/lib/kamal/secrets/adapters/aws_secrets_manager.rb +51 -0
  69. data/lib/kamal/secrets/adapters/base.rb +13 -3
  70. data/lib/kamal/secrets/adapters/bitwarden.rb +2 -2
  71. data/lib/kamal/secrets/adapters/bitwarden_secrets_manager.rb +66 -0
  72. data/lib/kamal/secrets/adapters/doppler.rb +57 -0
  73. data/lib/kamal/secrets/adapters/enpass.rb +71 -0
  74. data/lib/kamal/secrets/adapters/gcp_secret_manager.rb +112 -0
  75. data/lib/kamal/secrets/adapters/last_pass.rb +3 -2
  76. data/lib/kamal/secrets/adapters/one_password.rb +47 -13
  77. data/lib/kamal/secrets/adapters/passbolt.rb +130 -0
  78. data/lib/kamal/secrets/adapters/test.rb +2 -2
  79. data/lib/kamal/secrets/adapters.rb +2 -0
  80. data/lib/kamal/secrets/dotenv/inline_command_substitution.rb +2 -1
  81. data/lib/kamal/secrets.rb +1 -1
  82. data/lib/kamal/version.rb +1 -1
  83. metadata +22 -10
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0d5e6961984a3361505ebf35dfc52920c49af92085dd99c923dbfa801c668a95
4
- data.tar.gz: adddf71abb26f58e5f7bb16c62553f0d3358499ac4c09f333df75b650cc37b54
3
+ metadata.gz: 4e1cf57d731a8b129a8ccbb86faddd3e813bc4d17895e6e538fe904f5bb65d27
4
+ data.tar.gz: 2d7d81b3a34f42fb427bfed18f9cf0ed1955d38e4b4783c1407bc1db6de5cef6
5
5
  SHA512:
6
- metadata.gz: fdbd4d88c6fe8001def4c53a9f3ee058e871e58ce99f6697a478cbbac48f646947aab415d08074668e2c41147f8fd15fa1cb76f01919d9411aa0e85be6767aba
7
- data.tar.gz: b1716d147e84b386f8bac27deb60f70fc6a7fdfad18fc376dca1391d400de388085b654d5ec8e8e9b3b83724e0a22599bc5626d07cd3812330389add2ed915f9
6
+ metadata.gz: f3144c40082cfa24c78e2a1ebf2f433e491be3f5966e45cfc5488c3a11f5a3f513f097f7818cbc9e34d20b79986e64212055a87030fd4aa386a1d82bbb7d0efe
7
+ data.tar.gz: c6b796497a6f7a68815d340664b34fb9943f55ea7121122994aa3fdadaa5b12a18fb4078ff194cffb6060917a74611fdf31017afa75f2515d94ec259e0809251
@@ -1,4 +1,5 @@
1
1
  require "active_support/core_ext/array/conversions"
2
+ require "concurrent/array"
2
3
 
3
4
  class Kamal::Cli::Accessory < Kamal::Cli::Base
4
5
  desc "boot [NAME]", "Boot new accessory service on host (use NAME=all to boot all accessories)"
@@ -10,14 +11,29 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
10
11
  prepare(name) if prepare
11
12
 
12
13
  with_accessory(name) do |accessory, hosts|
14
+ booted_hosts = Concurrent::Array.new
15
+ on(hosts) do |host|
16
+ booted_hosts << host.to_s if capture_with_info(*accessory.info(all: true, quiet: true)).strip.presence
17
+ end
18
+
19
+ if booted_hosts.any?
20
+ say "Skipping booting `#{name}` on #{booted_hosts.sort.join(", ")}, a container already exists", :yellow
21
+ hosts -= booted_hosts
22
+ end
23
+
13
24
  directories(name)
14
25
  upload(name)
15
26
 
16
- on(hosts) do
27
+ on(hosts) do |host|
17
28
  execute *KAMAL.auditor.record("Booted #{name} accessory"), verbosity: :debug
18
29
  execute *accessory.ensure_env_directory
19
30
  upload! accessory.secrets_io, accessory.secrets_path, mode: "0600"
20
- execute *accessory.run
31
+ execute *accessory.run(host: host)
32
+
33
+ if accessory.running_proxy?
34
+ target = capture_with_info(*accessory.container_id_for(container_name: accessory.service_name, only_running: true)).strip
35
+ execute *accessory.deploy(target: target)
36
+ end
21
37
  end
22
38
  end
23
39
  end
@@ -75,6 +91,10 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
75
91
  on(hosts) do
76
92
  execute *KAMAL.auditor.record("Started #{name} accessory"), verbosity: :debug
77
93
  execute *accessory.start
94
+ if accessory.running_proxy?
95
+ target = capture_with_info(*accessory.container_id_for(container_name: accessory.service_name, only_running: true)).strip
96
+ execute *accessory.deploy(target: target)
97
+ end
78
98
  end
79
99
  end
80
100
  end
@@ -87,6 +107,11 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
87
107
  on(hosts) do
88
108
  execute *KAMAL.auditor.record("Stopped #{name} accessory"), verbosity: :debug
89
109
  execute *accessory.stop, raise_on_non_zero_exit: false
110
+
111
+ if accessory.running_proxy?
112
+ target = capture_with_info(*accessory.container_id_for(container_name: accessory.service_name, only_running: true)).strip
113
+ execute *accessory.remove if target
114
+ end
90
115
  end
91
116
  end
92
117
  end
@@ -112,32 +137,37 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
112
137
  end
113
138
  end
114
139
 
115
- desc "exec [NAME] [CMD]", "Execute a custom command on servers (use --help to show options)"
140
+ desc "exec [NAME] [CMD...]", "Execute a custom command on servers within the accessory container (use --help to show options)"
116
141
  option :interactive, aliases: "-i", type: :boolean, default: false, desc: "Execute command over ssh for an interactive shell (use for console/bash)"
117
142
  option :reuse, type: :boolean, default: false, desc: "Reuse currently running container instead of starting a new one"
118
- def exec(name, cmd)
143
+ def exec(name, *cmd)
144
+ pre_connect_if_required
145
+
146
+ cmd = Kamal::Utils.join_commands(cmd)
119
147
  with_accessory(name) do |accessory, hosts|
120
148
  case
121
149
  when options[:interactive] && options[:reuse]
122
- say "Launching interactive command with via SSH from existing container...", :magenta
150
+ say "Launching interactive command via SSH from existing container...", :magenta
123
151
  run_locally { exec accessory.execute_in_existing_container_over_ssh(cmd) }
124
152
 
125
153
  when options[:interactive]
126
154
  say "Launching interactive command via SSH from new container...", :magenta
155
+ on(accessory.hosts.first) { execute *KAMAL.registry.login }
127
156
  run_locally { exec accessory.execute_in_new_container_over_ssh(cmd) }
128
157
 
129
158
  when options[:reuse]
130
159
  say "Launching command from existing container...", :magenta
131
- on(hosts) do
160
+ on(hosts) do |host|
132
161
  execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on #{name} accessory"), verbosity: :debug
133
- capture_with_info(*accessory.execute_in_existing_container(cmd))
162
+ puts_by_host host, capture_with_info(*accessory.execute_in_existing_container(cmd))
134
163
  end
135
164
 
136
165
  else
137
166
  say "Launching command from new container...", :magenta
138
- on(hosts) do
167
+ on(hosts) do |host|
168
+ execute *KAMAL.registry.login
139
169
  execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on #{name} accessory"), verbosity: :debug
140
- capture_with_info(*accessory.execute_in_new_container(cmd))
170
+ puts_by_host host, capture_with_info(*accessory.execute_in_new_container(cmd))
141
171
  end
142
172
  end
143
173
  end
@@ -147,7 +177,7 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
147
177
  option :since, aliases: "-s", desc: "Show logs since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)"
148
178
  option :lines, type: :numeric, aliases: "-n", desc: "Number of log lines to pull from each server"
149
179
  option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)"
150
- option :grep_options, aliases: "-o", desc: "Additional options supplied to grep"
180
+ option :grep_options, desc: "Additional options supplied to grep"
151
181
  option :follow, aliases: "-f", desc: "Follow logs on primary server (or specific host set by --hosts)"
152
182
  option :skip_timestamps, type: :boolean, aliases: "-T", desc: "Skip appending timestamps to logging output"
153
183
  def logs(name)
@@ -260,11 +290,7 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
260
290
  end
261
291
 
262
292
  def accessory_hosts(accessory)
263
- if KAMAL.specific_hosts&.any?
264
- KAMAL.specific_hosts & accessory.hosts
265
- else
266
- accessory.hosts
267
- end
293
+ KAMAL.accessory_hosts & accessory.hosts
268
294
  end
269
295
 
270
296
  def remove_accessory(name)
@@ -277,7 +303,7 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
277
303
  def prepare(name)
278
304
  with_accessory(name) do |accessory, hosts|
279
305
  on(hosts) do
280
- execute *KAMAL.registry.login
306
+ execute *KAMAL.registry.login(registry_config: accessory.registry)
281
307
  execute *KAMAL.docker.create_network
282
308
  rescue SSHKit::Command::Failed => e
283
309
  raise unless e.message.include?("already exists")
@@ -1,6 +1,7 @@
1
1
  class Kamal::Cli::Alias::Command < Thor::DynamicCommand
2
2
  def run(instance, args = [])
3
3
  if (_alias = KAMAL.config.aliases[name])
4
+ KAMAL.reset
4
5
  Kamal::Cli::Main.start(Shellwords.split(_alias.command) + ARGV[1..-1])
5
6
  else
6
7
  super
@@ -1,4 +1,4 @@
1
- class Kamal::Cli::App::PrepareAssets
1
+ class Kamal::Cli::App::Assets
2
2
  attr_reader :host, :role, :sshkit
3
3
  delegate :execute, :capture_with_info, :info, to: :sshkit
4
4
  delegate :assets?, to: :role
@@ -45,7 +45,7 @@ class Kamal::Cli::App::Boot
45
45
 
46
46
  def start_new_version
47
47
  audit "Booted app version #{version}"
48
- hostname = "#{host.to_s[0...51].gsub(/\.+$/, '')}-#{SecureRandom.hex(6)}"
48
+ hostname = "#{host.to_s[0...51].chomp(".")}-#{SecureRandom.hex(6)}"
49
49
 
50
50
  execute *app.ensure_env_directory
51
51
  upload! role.secrets_io(host), role.secrets_path, mode: "0600"
@@ -70,6 +70,7 @@ class Kamal::Cli::App::Boot
70
70
  def stop_old_version(version)
71
71
  execute *app.stop(version: version), raise_on_non_zero_exit: false
72
72
  execute *app.clean_up_assets if assets?
73
+ execute *app.clean_up_error_pages if KAMAL.config.error_pages_path
73
74
  end
74
75
 
75
76
  def release_barrier
@@ -91,7 +92,7 @@ class Kamal::Cli::App::Boot
91
92
  if barrier.close
92
93
  info "First #{KAMAL.primary_role} container is unhealthy on #{host}, not booting any other roles"
93
94
  begin
94
- error capture_with_info(*app.logs(version: version))
95
+ error capture_with_info(*app.logs(container_id: app.container_id_for_version(version)))
95
96
  error capture_with_info(*app.container_health_log(version: version))
96
97
  rescue SSHKit::Command::Failed
97
98
  error "Could not fetch logs for #{version}"
@@ -0,0 +1,33 @@
1
+ class Kamal::Cli::App::ErrorPages
2
+ ERROR_PAGES_GLOB = "{4??.html,5??.html}"
3
+
4
+ attr_reader :host, :sshkit
5
+ delegate :upload!, :execute, to: :sshkit
6
+
7
+ def initialize(host, sshkit)
8
+ @host = host
9
+ @sshkit = sshkit
10
+ end
11
+
12
+ def run
13
+ if KAMAL.config.error_pages_path
14
+ with_error_pages_tmpdir do |local_error_pages_dir|
15
+ execute *KAMAL.app.create_error_pages_directory
16
+ upload! local_error_pages_dir, KAMAL.config.proxy_boot.error_pages_directory, mode: "0700", recursive: true
17
+ end
18
+ end
19
+ end
20
+
21
+ private
22
+ def with_error_pages_tmpdir
23
+ Dir.mktmpdir("kamal-error-pages") do |tmpdir|
24
+ error_pages_dir = File.join(tmpdir, KAMAL.config.version)
25
+ FileUtils.mkdir(error_pages_dir)
26
+
27
+ if (files = Dir[File.join(KAMAL.config.error_pages_path, ERROR_PAGES_GLOB)]).any?
28
+ FileUtils.cp(files, error_pages_dir)
29
+ yield error_pages_dir
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,28 @@
1
+ class Kamal::Cli::App::SslCertificates
2
+ attr_reader :host, :role, :sshkit
3
+ delegate :execute, :info, :upload!, to: :sshkit
4
+
5
+ def initialize(host, role, sshkit)
6
+ @host = host
7
+ @role = role
8
+ @sshkit = sshkit
9
+ end
10
+
11
+ def run
12
+ if role.running_proxy? && role.proxy.custom_ssl_certificate?
13
+ info "Writing SSL certificates for #{role.name} on #{host}"
14
+ execute *app.create_ssl_directory
15
+ if cert_content = role.proxy.certificate_pem_content
16
+ upload!(StringIO.new(cert_content), role.proxy.host_tls_cert, mode: "0644")
17
+ end
18
+ if key_content = role.proxy.private_key_pem_content
19
+ upload!(StringIO.new(key_content), role.proxy.host_tls_key, mode: "0644")
20
+ end
21
+ end
22
+ end
23
+
24
+ private
25
+ def app
26
+ @app ||= KAMAL.app(role: role, host: host)
27
+ end
28
+ end
data/lib/kamal/cli/app.rb CHANGED
@@ -7,23 +7,34 @@ class Kamal::Cli::App < Kamal::Cli::Base
7
7
  say "Start container with version #{version} (or reboot if already running)...", :magenta
8
8
 
9
9
  # Assets are prepared in a separate step to ensure they are on all hosts before booting
10
- on(KAMAL.hosts) do
10
+ on(KAMAL.app_hosts) do
11
+ Kamal::Cli::App::ErrorPages.new(host, self).run
12
+
11
13
  KAMAL.roles_on(host).each do |role|
12
- Kamal::Cli::App::PrepareAssets.new(host, role, self).run
14
+ Kamal::Cli::App::Assets.new(host, role, self).run
15
+ Kamal::Cli::App::SslCertificates.new(host, role, self).run
13
16
  end
14
17
  end
15
18
 
16
19
  # Primary hosts and roles are returned first, so they can open the barrier
17
20
  barrier = Kamal::Cli::Healthcheck::Barrier.new
18
21
 
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
22
+ host_boot_groups.each do |hosts|
23
+ host_list = Array(hosts).join(",")
24
+ run_hook "pre-app-boot", hosts: host_list
25
+
26
+ on(hosts) do |host|
27
+ KAMAL.roles_on(host).each do |role|
28
+ Kamal::Cli::App::Boot.new(host, role, self, version, barrier).run
29
+ end
22
30
  end
31
+
32
+ run_hook "post-app-boot", hosts: host_list
33
+ sleep KAMAL.config.boot.wait if KAMAL.config.boot.wait
23
34
  end
24
35
 
25
36
  # Tag once the app booted on all hosts
26
- on(KAMAL.hosts) do |host|
37
+ on(KAMAL.app_hosts) do |host|
27
38
  execute *KAMAL.auditor.record("Tagging #{KAMAL.config.absolute_image} as the latest image"), verbosity: :debug
28
39
  execute *KAMAL.app.tag_latest_image
29
40
  end
@@ -34,7 +45,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
34
45
  desc "start", "Start existing app container on servers"
35
46
  def start
36
47
  with_lock do
37
- on(KAMAL.hosts) do |host|
48
+ on(KAMAL.app_hosts) do |host|
38
49
  roles = KAMAL.roles_on(host)
39
50
 
40
51
  roles.each do |role|
@@ -57,7 +68,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
57
68
  desc "stop", "Stop app container on servers"
58
69
  def stop
59
70
  with_lock do
60
- on(KAMAL.hosts) do |host|
71
+ on(KAMAL.app_hosts) do |host|
61
72
  roles = KAMAL.roles_on(host)
62
73
 
63
74
  roles.each do |role|
@@ -81,7 +92,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
81
92
  # FIXME: Drop in favor of just containers?
82
93
  desc "details", "Show details about app containers"
83
94
  def details
84
- on(KAMAL.hosts) do |host|
95
+ on(KAMAL.app_hosts) do |host|
85
96
  roles = KAMAL.roles_on(host)
86
97
 
87
98
  roles.each do |role|
@@ -94,9 +105,21 @@ class Kamal::Cli::App < Kamal::Cli::Base
94
105
  option :interactive, aliases: "-i", type: :boolean, default: false, desc: "Execute command over ssh for an interactive shell (use for console/bash)"
95
106
  option :reuse, type: :boolean, default: false, desc: "Reuse currently running container instead of starting a new one"
96
107
  option :env, aliases: "-e", type: :hash, desc: "Set environment variables for the command"
108
+ option :detach, type: :boolean, default: false, desc: "Execute command in a detached container"
97
109
  def exec(*cmd)
110
+ pre_connect_if_required
111
+
112
+ if (incompatible_options = [ :interactive, :reuse ].select { |key| options[:detach] && options[key] }.presence)
113
+ raise ArgumentError, "Detach is not compatible with #{incompatible_options.join(" or ")}"
114
+ end
115
+
116
+ if cmd.empty?
117
+ raise ArgumentError, "No command provided. You must specify a command to execute."
118
+ end
119
+
98
120
  cmd = Kamal::Utils.join_commands(cmd)
99
121
  env = options[:env]
122
+ detach = options[:detach]
100
123
  case
101
124
  when options[:interactive] && options[:reuse]
102
125
  say "Get current version of running container...", :magenta unless options[:version]
@@ -109,6 +132,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
109
132
  say "Get most recent version available as an image...", :magenta unless options[:version]
110
133
  using_version(version_or_latest) do |version|
111
134
  say "Launching interactive command with version #{version} via SSH from new container on #{KAMAL.primary_host}...", :magenta
135
+ on(KAMAL.primary_host) { execute *KAMAL.registry.login }
112
136
  run_locally do
113
137
  exec KAMAL.app(role: KAMAL.primary_role, host: KAMAL.primary_host).execute_in_new_container_over_ssh(cmd, env: env)
114
138
  end
@@ -119,7 +143,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
119
143
  using_version(options[:version] || current_running_version) do |version|
120
144
  say "Launching command with version #{version} from existing container...", :magenta
121
145
 
122
- on(KAMAL.hosts) do |host|
146
+ on(KAMAL.app_hosts) do |host|
123
147
  roles = KAMAL.roles_on(host)
124
148
 
125
149
  roles.each do |role|
@@ -133,12 +157,14 @@ class Kamal::Cli::App < Kamal::Cli::Base
133
157
  say "Get most recent version available as an image...", :magenta unless options[:version]
134
158
  using_version(version_or_latest) do |version|
135
159
  say "Launching command with version #{version} from new container...", :magenta
136
- on(KAMAL.hosts) do |host|
160
+ on(KAMAL.app_hosts) do |host|
161
+ execute *KAMAL.registry.login
162
+
137
163
  roles = KAMAL.roles_on(host)
138
164
 
139
165
  roles.each do |role|
140
166
  execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on app version #{version}"), verbosity: :debug
141
- puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).execute_in_new_container(cmd, env: env))
167
+ puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).execute_in_new_container(cmd, env: env, detach: detach))
142
168
  end
143
169
  end
144
170
  end
@@ -147,7 +173,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
147
173
 
148
174
  desc "containers", "Show app containers on servers"
149
175
  def containers
150
- on(KAMAL.hosts) { |host| puts_by_host host, capture_with_info(*KAMAL.app.list_containers) }
176
+ on(KAMAL.app_hosts) { |host| puts_by_host host, capture_with_info(*KAMAL.app.list_containers) }
151
177
  end
152
178
 
153
179
  desc "stale_containers", "Detect app stale containers"
@@ -156,7 +182,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
156
182
  stop = options[:stop]
157
183
 
158
184
  with_lock_if_stopping do
159
- on(KAMAL.hosts) do |host|
185
+ on(KAMAL.app_hosts) do |host|
160
186
  roles = KAMAL.roles_on(host)
161
187
 
162
188
  roles.each do |role|
@@ -179,22 +205,24 @@ class Kamal::Cli::App < Kamal::Cli::Base
179
205
 
180
206
  desc "images", "Show app images on servers"
181
207
  def images
182
- on(KAMAL.hosts) { |host| puts_by_host host, capture_with_info(*KAMAL.app.list_images) }
208
+ on(KAMAL.app_hosts) { |host| puts_by_host host, capture_with_info(*KAMAL.app.list_images) }
183
209
  end
184
210
 
185
211
  desc "logs", "Show log lines from app on servers (use --help to show options)"
186
212
  option :since, aliases: "-s", desc: "Show lines since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)"
187
213
  option :lines, type: :numeric, aliases: "-n", desc: "Number of lines to show from each server"
188
214
  option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)"
189
- option :grep_options, aliases: "-o", desc: "Additional options supplied to grep"
215
+ option :grep_options, desc: "Additional options supplied to grep"
190
216
  option :follow, aliases: "-f", desc: "Follow log on primary server (or specific host set by --hosts)"
191
217
  option :skip_timestamps, type: :boolean, aliases: "-T", desc: "Skip appending timestamps to logging output"
218
+ option :container_id, desc: "Docker container ID to fetch logs"
192
219
  def logs
193
220
  # FIXME: Catch when app containers aren't running
194
221
 
195
222
  grep = options[:grep]
196
223
  grep_options = options[:grep_options]
197
224
  since = options[:since]
225
+ container_id = options[:container_id]
198
226
  timestamps = !options[:skip_timestamps]
199
227
 
200
228
  if options[:follow]
@@ -207,18 +235,18 @@ class Kamal::Cli::App < Kamal::Cli::Base
207
235
  role = KAMAL.roles_on(KAMAL.primary_host).first
208
236
 
209
237
  app = KAMAL.app(role: role, host: host)
210
- info app.follow_logs(host: KAMAL.primary_host, timestamps: timestamps, lines: lines, grep: grep, grep_options: grep_options)
211
- exec app.follow_logs(host: KAMAL.primary_host, timestamps: timestamps, lines: lines, grep: grep, grep_options: grep_options)
238
+ info app.follow_logs(host: KAMAL.primary_host, container_id: container_id, timestamps: timestamps, lines: lines, grep: grep, grep_options: grep_options)
239
+ exec app.follow_logs(host: KAMAL.primary_host, container_id: container_id, timestamps: timestamps, lines: lines, grep: grep, grep_options: grep_options)
212
240
  end
213
241
  else
214
242
  lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set
215
243
 
216
- on(KAMAL.hosts) do |host|
244
+ on(KAMAL.app_hosts) do |host|
217
245
  roles = KAMAL.roles_on(host)
218
246
 
219
247
  roles.each do |role|
220
248
  begin
221
- puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).logs(timestamps: timestamps, since: since, lines: lines, grep: grep, grep_options: grep_options))
249
+ puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).logs(container_id: container_id, timestamps: timestamps, since: since, lines: lines, grep: grep, grep_options: grep_options))
222
250
  rescue SSHKit::Command::Failed
223
251
  puts_by_host host, "Nothing found"
224
252
  end
@@ -233,14 +261,44 @@ class Kamal::Cli::App < Kamal::Cli::Base
233
261
  stop
234
262
  remove_containers
235
263
  remove_images
236
- remove_app_directory
264
+ remove_app_directories
265
+ end
266
+ end
267
+
268
+ desc "live", "Set the app to live mode"
269
+ def live
270
+ with_lock do
271
+ on(KAMAL.proxy_hosts) do |host|
272
+ roles = KAMAL.roles_on(host)
273
+
274
+ roles.each do |role|
275
+ execute *KAMAL.app(role: role, host: host).live if role.running_proxy?
276
+ end
277
+ end
278
+ end
279
+ end
280
+
281
+ desc "maintenance", "Set the app to maintenance mode"
282
+ option :drain_timeout, type: :numeric, desc: "How long to allow in-flight requests to complete (defaults to drain_timeout from config)"
283
+ option :message, type: :string, desc: "Message to display to clients while stopped"
284
+ def maintenance
285
+ maintenance_options = { drain_timeout: options[:drain_timeout] || KAMAL.config.drain_timeout, message: options[:message] }
286
+
287
+ with_lock do
288
+ on(KAMAL.proxy_hosts) do |host|
289
+ roles = KAMAL.roles_on(host)
290
+
291
+ roles.each do |role|
292
+ execute *KAMAL.app(role: role, host: host).maintenance(**maintenance_options) if role.running_proxy?
293
+ end
294
+ end
237
295
  end
238
296
  end
239
297
 
240
298
  desc "remove_container [VERSION]", "Remove app container with given version from servers", hide: true
241
299
  def remove_container(version)
242
300
  with_lock do
243
- on(KAMAL.hosts) do |host|
301
+ on(KAMAL.app_hosts) do |host|
244
302
  roles = KAMAL.roles_on(host)
245
303
 
246
304
  roles.each do |role|
@@ -254,7 +312,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
254
312
  desc "remove_containers", "Remove all app containers from servers", hide: true
255
313
  def remove_containers
256
314
  with_lock do
257
- on(KAMAL.hosts) do |host|
315
+ on(KAMAL.app_hosts) do |host|
258
316
  roles = KAMAL.roles_on(host)
259
317
 
260
318
  roles.each do |role|
@@ -268,30 +326,33 @@ class Kamal::Cli::App < Kamal::Cli::Base
268
326
  desc "remove_images", "Remove all app images from servers", hide: true
269
327
  def remove_images
270
328
  with_lock do
271
- on(KAMAL.hosts) do
329
+ on(KAMAL.app_hosts) do
272
330
  execute *KAMAL.auditor.record("Removed all app images"), verbosity: :debug
273
331
  execute *KAMAL.app.remove_images
274
332
  end
275
333
  end
276
334
  end
277
335
 
278
- desc "remove_app_directory", "Remove the service directory from servers", hide: true
279
- def remove_app_directory
336
+ desc "remove_app_directories", "Remove the app directories from servers", hide: true
337
+ def remove_app_directories
280
338
  with_lock do
281
- on(KAMAL.hosts) do |host|
339
+ on(KAMAL.app_hosts) do |host|
282
340
  roles = KAMAL.roles_on(host)
283
341
 
284
342
  roles.each do |role|
285
- execute *KAMAL.auditor.record("Removed #{KAMAL.config.app_directory} on all servers", role: role), verbosity: :debug
343
+ execute *KAMAL.auditor.record("Removed #{KAMAL.config.app_directory}", role: role), verbosity: :debug
286
344
  execute *KAMAL.server.remove_app_directory, raise_on_non_zero_exit: false
287
345
  end
346
+
347
+ execute *KAMAL.auditor.record("Removed #{KAMAL.config.app_directory}"), verbosity: :debug
348
+ execute *KAMAL.app.remove_proxy_app_directory, raise_on_non_zero_exit: false
288
349
  end
289
350
  end
290
351
  end
291
352
 
292
353
  desc "version", "Show app version currently running on servers"
293
354
  def version
294
- on(KAMAL.hosts) do |host|
355
+ on(KAMAL.app_hosts) do |host|
295
356
  role = KAMAL.roles_on(host).first
296
357
  puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).current_running_version).strip
297
358
  end
@@ -332,4 +393,8 @@ class Kamal::Cli::App < Kamal::Cli::Base
332
393
  yield
333
394
  end
334
395
  end
396
+
397
+ def host_boot_groups
398
+ KAMAL.config.boot.limit ? KAMAL.app_hosts.each_slice(KAMAL.config.boot.limit).to_a : [ KAMAL.app_hosts ]
399
+ end
335
400
  end
@@ -5,7 +5,7 @@ module Kamal::Cli
5
5
  class Base < Thor
6
6
  include SSHKit::DSL
7
7
 
8
- def self.exit_on_failure?() false end
8
+ def self.exit_on_failure?() true end
9
9
  def self.dynamic_command_class() Kamal::Cli::Alias::Command end
10
10
 
11
11
  class_option :verbose, type: :boolean, aliases: "-v", desc: "Detailed logging"
@@ -30,6 +30,7 @@ module Kamal::Cli
30
30
  else
31
31
  super
32
32
  end
33
+
33
34
  initialize_commander unless KAMAL.configured?
34
35
  end
35
36
 
@@ -132,7 +133,13 @@ module Kamal::Cli
132
133
 
133
134
  def run_hook(hook, **extra_details)
134
135
  if !options[:skip_hooks] && KAMAL.hook.hook_exists?(hook)
135
- details = { hosts: KAMAL.hosts.join(","), command: command, subcommand: subcommand }
136
+ details = {
137
+ hosts: KAMAL.hosts.join(","),
138
+ roles: KAMAL.specific_roles&.join(","),
139
+ lock: KAMAL.holding_lock?.to_s,
140
+ command: command,
141
+ subcommand: subcommand
142
+ }.compact
136
143
 
137
144
  say "Running the #{hook} hook...", :magenta
138
145
  with_env KAMAL.hook.env(**details, **extra_details) do
@@ -146,12 +153,16 @@ module Kamal::Cli
146
153
  end
147
154
 
148
155
  def on(*args, &block)
156
+ pre_connect_if_required
157
+
158
+ super
159
+ end
160
+
161
+ def pre_connect_if_required
149
162
  if !KAMAL.connected?
150
163
  run_hook "pre-connect"
151
164
  KAMAL.connected = true
152
165
  end
153
-
154
- super
155
166
  end
156
167
 
157
168
  def command
@@ -194,5 +205,19 @@ module Kamal::Cli
194
205
  ENV.clear
195
206
  ENV.update(current_env)
196
207
  end
208
+
209
+ def ensure_docker_installed
210
+ run_locally do
211
+ begin
212
+ execute *KAMAL.builder.ensure_docker_installed
213
+ rescue SSHKit::Command::Failed => e
214
+ error = e.message =~ /command not found/ ?
215
+ "Docker is not installed locally" :
216
+ "Docker buildx plugin is not installed locally"
217
+
218
+ raise DependencyError, error
219
+ end
220
+ end
221
+ end
197
222
  end
198
223
  end