kamal 1.8.3 → 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 (130) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/lib/kamal/cli/accessory.rb +92 -38
  4. data/lib/kamal/cli/alias/command.rb +10 -0
  5. data/lib/kamal/cli/app/{prepare_assets.rb → assets.rb} +1 -1
  6. data/lib/kamal/cli/app/boot.rb +23 -16
  7. data/lib/kamal/cli/app/error_pages.rb +33 -0
  8. data/lib/kamal/cli/app/ssl_certificates.rb +28 -0
  9. data/lib/kamal/cli/app.rb +132 -30
  10. data/lib/kamal/cli/base.rb +57 -53
  11. data/lib/kamal/cli/build.rb +81 -38
  12. data/lib/kamal/cli/healthcheck/barrier.rb +2 -0
  13. data/lib/kamal/cli/healthcheck/poller.rb +18 -39
  14. data/lib/kamal/cli/lock.rb +2 -3
  15. data/lib/kamal/cli/main.rb +60 -59
  16. data/lib/kamal/cli/proxy.rb +290 -0
  17. data/lib/kamal/cli/prune.rb +0 -1
  18. data/lib/kamal/cli/registry.rb +2 -0
  19. data/lib/kamal/cli/secrets.rb +49 -0
  20. data/lib/kamal/cli/server.rb +6 -5
  21. data/lib/kamal/cli/templates/deploy.yml +53 -53
  22. data/lib/kamal/cli/templates/sample_hooks/docker-setup.sample +2 -12
  23. data/lib/kamal/cli/templates/sample_hooks/post-app-boot.sample +3 -0
  24. data/lib/kamal/cli/templates/sample_hooks/post-deploy.sample +1 -1
  25. data/lib/kamal/cli/templates/sample_hooks/post-proxy-reboot.sample +3 -0
  26. data/lib/kamal/cli/templates/sample_hooks/pre-app-boot.sample +3 -0
  27. data/lib/kamal/cli/templates/sample_hooks/pre-build.sample +1 -1
  28. data/lib/kamal/cli/templates/sample_hooks/pre-connect.sample +1 -1
  29. data/lib/kamal/cli/templates/sample_hooks/pre-deploy.sample +19 -6
  30. data/lib/kamal/cli/templates/sample_hooks/pre-proxy-reboot.sample +3 -0
  31. data/lib/kamal/cli/templates/secrets +17 -0
  32. data/lib/kamal/cli.rb +2 -0
  33. data/lib/kamal/commander/specifics.rb +19 -6
  34. data/lib/kamal/commander.rb +39 -32
  35. data/lib/kamal/commands/accessory/proxy.rb +16 -0
  36. data/lib/kamal/commands/accessory.rb +19 -19
  37. data/lib/kamal/commands/app/assets.rb +10 -10
  38. data/lib/kamal/commands/app/containers.rb +2 -2
  39. data/lib/kamal/commands/app/error_pages.rb +9 -0
  40. data/lib/kamal/commands/app/execution.rb +7 -4
  41. data/lib/kamal/commands/app/images.rb +1 -1
  42. data/lib/kamal/commands/app/logging.rb +16 -6
  43. data/lib/kamal/commands/app/proxy.rb +32 -0
  44. data/lib/kamal/commands/app.rb +25 -24
  45. data/lib/kamal/commands/auditor.rb +12 -3
  46. data/lib/kamal/commands/base.rb +54 -8
  47. data/lib/kamal/commands/builder/base.rb +46 -16
  48. data/lib/kamal/commands/builder/clone.rb +16 -14
  49. data/lib/kamal/commands/builder/cloud.rb +22 -0
  50. data/lib/kamal/commands/builder/hybrid.rb +21 -0
  51. data/lib/kamal/commands/builder/local.rb +14 -0
  52. data/lib/kamal/commands/builder/pack.rb +46 -0
  53. data/lib/kamal/commands/builder/remote.rb +63 -0
  54. data/lib/kamal/commands/builder.rb +21 -45
  55. data/lib/kamal/commands/docker.rb +4 -0
  56. data/lib/kamal/commands/hook.rb +8 -2
  57. data/lib/kamal/commands/lock.rb +2 -6
  58. data/lib/kamal/commands/proxy.rb +127 -0
  59. data/lib/kamal/commands/prune.rb +1 -9
  60. data/lib/kamal/commands/registry.rb +9 -7
  61. data/lib/kamal/commands/server.rb +11 -1
  62. data/lib/kamal/configuration/accessory.rb +89 -12
  63. data/lib/kamal/configuration/alias.rb +15 -0
  64. data/lib/kamal/configuration/builder.rb +73 -15
  65. data/lib/kamal/configuration/docs/accessory.yml +53 -15
  66. data/lib/kamal/configuration/docs/alias.yml +26 -0
  67. data/lib/kamal/configuration/docs/boot.yml +3 -3
  68. data/lib/kamal/configuration/docs/builder.yml +63 -38
  69. data/lib/kamal/configuration/docs/configuration.yml +62 -46
  70. data/lib/kamal/configuration/docs/env.yml +61 -17
  71. data/lib/kamal/configuration/docs/logging.yml +3 -3
  72. data/lib/kamal/configuration/docs/proxy.yml +168 -0
  73. data/lib/kamal/configuration/docs/registry.yml +20 -13
  74. data/lib/kamal/configuration/docs/role.yml +14 -13
  75. data/lib/kamal/configuration/docs/servers.yml +2 -2
  76. data/lib/kamal/configuration/docs/ssh.yml +23 -19
  77. data/lib/kamal/configuration/docs/sshkit.yml +4 -4
  78. data/lib/kamal/configuration/env/tag.rb +4 -3
  79. data/lib/kamal/configuration/env.rb +19 -17
  80. data/lib/kamal/configuration/proxy/boot.rb +129 -0
  81. data/lib/kamal/configuration/proxy.rb +124 -0
  82. data/lib/kamal/configuration/registry.rb +7 -6
  83. data/lib/kamal/configuration/role.rb +69 -98
  84. data/lib/kamal/configuration/servers.rb +8 -1
  85. data/lib/kamal/configuration/validator/accessory.rb +6 -2
  86. data/lib/kamal/configuration/validator/alias.rb +15 -0
  87. data/lib/kamal/configuration/validator/builder.rb +6 -0
  88. data/lib/kamal/configuration/validator/proxy.rb +25 -0
  89. data/lib/kamal/configuration/validator/role.rb +3 -1
  90. data/lib/kamal/configuration/validator/servers.rb +1 -1
  91. data/lib/kamal/configuration/validator.rb +62 -24
  92. data/lib/kamal/configuration.rb +96 -50
  93. data/lib/kamal/docker.rb +30 -0
  94. data/lib/kamal/env_file.rb +7 -1
  95. data/lib/kamal/git.rb +10 -0
  96. data/lib/kamal/secrets/adapters/aws_secrets_manager.rb +51 -0
  97. data/lib/kamal/secrets/adapters/base.rb +33 -0
  98. data/lib/kamal/secrets/adapters/bitwarden.rb +81 -0
  99. data/lib/kamal/secrets/adapters/bitwarden_secrets_manager.rb +66 -0
  100. data/lib/kamal/secrets/adapters/doppler.rb +57 -0
  101. data/lib/kamal/secrets/adapters/enpass.rb +71 -0
  102. data/lib/kamal/secrets/adapters/gcp_secret_manager.rb +112 -0
  103. data/lib/kamal/secrets/adapters/last_pass.rb +40 -0
  104. data/lib/kamal/secrets/adapters/one_password.rb +104 -0
  105. data/lib/kamal/secrets/adapters/passbolt.rb +130 -0
  106. data/lib/kamal/secrets/adapters/test.rb +14 -0
  107. data/lib/kamal/secrets/adapters.rb +16 -0
  108. data/lib/kamal/secrets/dotenv/inline_command_substitution.rb +33 -0
  109. data/lib/kamal/secrets.rb +42 -0
  110. data/lib/kamal/sshkit_with_ext.rb +1 -0
  111. data/lib/kamal/utils.rb +30 -0
  112. data/lib/kamal/version.rb +1 -1
  113. data/lib/kamal.rb +3 -1
  114. metadata +63 -36
  115. data/lib/kamal/cli/env.rb +0 -54
  116. data/lib/kamal/cli/templates/sample_hooks/post-traefik-reboot.sample +0 -3
  117. data/lib/kamal/cli/templates/sample_hooks/pre-traefik-reboot.sample +0 -3
  118. data/lib/kamal/cli/templates/template.env +0 -2
  119. data/lib/kamal/cli/traefik.rb +0 -122
  120. data/lib/kamal/commands/app/cord.rb +0 -22
  121. data/lib/kamal/commands/builder/multiarch/remote.rb +0 -65
  122. data/lib/kamal/commands/builder/multiarch.rb +0 -41
  123. data/lib/kamal/commands/builder/native/cached.rb +0 -25
  124. data/lib/kamal/commands/builder/native/remote.rb +0 -67
  125. data/lib/kamal/commands/builder/native.rb +0 -20
  126. data/lib/kamal/commands/traefik.rb +0 -85
  127. data/lib/kamal/configuration/docs/healthcheck.yml +0 -59
  128. data/lib/kamal/configuration/docs/traefik.yml +0 -62
  129. data/lib/kamal/configuration/healthcheck.rb +0 -63
  130. data/lib/kamal/configuration/traefik.rb +0 -60
data/lib/kamal/cli/app.rb CHANGED
@@ -4,26 +4,37 @@ class Kamal::Cli::App < Kamal::Cli::Base
4
4
  with_lock do
5
5
  say "Get most recent version available as an image...", :magenta unless options[:version]
6
6
  using_version(version_or_latest) do |version|
7
- say "Start container with version #{version} using a #{KAMAL.config.readiness_delay}s readiness delay (or reboot if already running)...", :magenta
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,12 +45,21 @@ 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|
52
+ app = KAMAL.app(role: role, host: host)
41
53
  execute *KAMAL.auditor.record("Started app version #{KAMAL.config.version}"), verbosity: :debug
42
- execute *KAMAL.app(role: role, host: host).start, raise_on_non_zero_exit: false
54
+ execute *app.start, raise_on_non_zero_exit: false
55
+
56
+ if role.running_proxy?
57
+ version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
58
+ endpoint = capture_with_info(*app.container_id_for_version(version)).strip
59
+ raise Kamal::Cli::BootError, "Failed to get endpoint for #{role} on #{host}, did the container boot?" if endpoint.empty?
60
+
61
+ execute *app.deploy(target: endpoint)
62
+ end
43
63
  end
44
64
  end
45
65
  end
@@ -48,12 +68,22 @@ class Kamal::Cli::App < Kamal::Cli::Base
48
68
  desc "stop", "Stop app container on servers"
49
69
  def stop
50
70
  with_lock do
51
- on(KAMAL.hosts) do |host|
71
+ on(KAMAL.app_hosts) do |host|
52
72
  roles = KAMAL.roles_on(host)
53
73
 
54
74
  roles.each do |role|
75
+ app = KAMAL.app(role: role, host: host)
55
76
  execute *KAMAL.auditor.record("Stopped app", role: role), verbosity: :debug
56
- execute *KAMAL.app(role: role, host: host).stop, raise_on_non_zero_exit: false
77
+
78
+ if role.running_proxy?
79
+ version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
80
+ endpoint = capture_with_info(*app.container_id_for_version(version)).strip
81
+ if endpoint.present?
82
+ execute *app.remove, raise_on_non_zero_exit: false
83
+ end
84
+ end
85
+
86
+ execute *app.stop, raise_on_non_zero_exit: false
57
87
  end
58
88
  end
59
89
  end
@@ -62,7 +92,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
62
92
  # FIXME: Drop in favor of just containers?
63
93
  desc "details", "Show details about app containers"
64
94
  def details
65
- on(KAMAL.hosts) do |host|
95
+ on(KAMAL.app_hosts) do |host|
66
96
  roles = KAMAL.roles_on(host)
67
97
 
68
98
  roles.each do |role|
@@ -71,12 +101,25 @@ class Kamal::Cli::App < Kamal::Cli::Base
71
101
  end
72
102
  end
73
103
 
74
- desc "exec [CMD]", "Execute a custom command on servers within the app container (use --help to show options)"
104
+ desc "exec [CMD...]", "Execute a custom command on servers within the app container (use --help to show options)"
75
105
  option :interactive, aliases: "-i", type: :boolean, default: false, desc: "Execute command over ssh for an interactive shell (use for console/bash)"
76
106
  option :reuse, type: :boolean, default: false, desc: "Reuse currently running container instead of starting a new one"
77
107
  option :env, aliases: "-e", type: :hash, desc: "Set environment variables for the command"
78
- def exec(cmd)
108
+ option :detach, type: :boolean, default: false, desc: "Execute command in a detached container"
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
+
120
+ cmd = Kamal::Utils.join_commands(cmd)
79
121
  env = options[:env]
122
+ detach = options[:detach]
80
123
  case
81
124
  when options[:interactive] && options[:reuse]
82
125
  say "Get current version of running container...", :magenta unless options[:version]
@@ -89,6 +132,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
89
132
  say "Get most recent version available as an image...", :magenta unless options[:version]
90
133
  using_version(version_or_latest) do |version|
91
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 }
92
136
  run_locally do
93
137
  exec KAMAL.app(role: KAMAL.primary_role, host: KAMAL.primary_host).execute_in_new_container_over_ssh(cmd, env: env)
94
138
  end
@@ -99,7 +143,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
99
143
  using_version(options[:version] || current_running_version) do |version|
100
144
  say "Launching command with version #{version} from existing container...", :magenta
101
145
 
102
- on(KAMAL.hosts) do |host|
146
+ on(KAMAL.app_hosts) do |host|
103
147
  roles = KAMAL.roles_on(host)
104
148
 
105
149
  roles.each do |role|
@@ -113,12 +157,14 @@ class Kamal::Cli::App < Kamal::Cli::Base
113
157
  say "Get most recent version available as an image...", :magenta unless options[:version]
114
158
  using_version(version_or_latest) do |version|
115
159
  say "Launching command with version #{version} from new container...", :magenta
116
- on(KAMAL.hosts) do |host|
160
+ on(KAMAL.app_hosts) do |host|
161
+ execute *KAMAL.registry.login
162
+
117
163
  roles = KAMAL.roles_on(host)
118
164
 
119
165
  roles.each do |role|
120
166
  execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on app version #{version}"), verbosity: :debug
121
- 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))
122
168
  end
123
169
  end
124
170
  end
@@ -127,7 +173,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
127
173
 
128
174
  desc "containers", "Show app containers on servers"
129
175
  def containers
130
- 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) }
131
177
  end
132
178
 
133
179
  desc "stale_containers", "Detect app stale containers"
@@ -136,7 +182,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
136
182
  stop = options[:stop]
137
183
 
138
184
  with_lock_if_stopping do
139
- on(KAMAL.hosts) do |host|
185
+ on(KAMAL.app_hosts) do |host|
140
186
  roles = KAMAL.roles_on(host)
141
187
 
142
188
  roles.each do |role|
@@ -159,21 +205,25 @@ class Kamal::Cli::App < Kamal::Cli::Base
159
205
 
160
206
  desc "images", "Show app images on servers"
161
207
  def images
162
- 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) }
163
209
  end
164
210
 
165
211
  desc "logs", "Show log lines from app on servers (use --help to show options)"
166
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)"
167
213
  option :lines, type: :numeric, aliases: "-n", desc: "Number of lines to show from each server"
168
214
  option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)"
169
- option :grep_options, aliases: "-o", desc: "Additional options supplied to grep"
215
+ option :grep_options, desc: "Additional options supplied to grep"
170
216
  option :follow, aliases: "-f", desc: "Follow log on primary server (or specific host set by --hosts)"
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"
171
219
  def logs
172
220
  # FIXME: Catch when app containers aren't running
173
221
 
174
222
  grep = options[:grep]
175
223
  grep_options = options[:grep_options]
176
224
  since = options[:since]
225
+ container_id = options[:container_id]
226
+ timestamps = !options[:skip_timestamps]
177
227
 
178
228
  if options[:follow]
179
229
  lines = options[:lines].presence || ((since || grep) ? nil : 10) # Default to 10 lines if since or grep isn't set
@@ -181,22 +231,22 @@ class Kamal::Cli::App < Kamal::Cli::Base
181
231
  run_locally do
182
232
  info "Following logs on #{KAMAL.primary_host}..."
183
233
 
184
- KAMAL.specific_roles ||= [ "web" ]
234
+ KAMAL.specific_roles ||= [ KAMAL.primary_role.name ]
185
235
  role = KAMAL.roles_on(KAMAL.primary_host).first
186
236
 
187
237
  app = KAMAL.app(role: role, host: host)
188
- info app.follow_logs(host: KAMAL.primary_host, lines: lines, grep: grep, grep_options: grep_options)
189
- exec app.follow_logs(host: KAMAL.primary_host, 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)
190
240
  end
191
241
  else
192
242
  lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set
193
243
 
194
- on(KAMAL.hosts) do |host|
244
+ on(KAMAL.app_hosts) do |host|
195
245
  roles = KAMAL.roles_on(host)
196
246
 
197
247
  roles.each do |role|
198
248
  begin
199
- puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).logs(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))
200
250
  rescue SSHKit::Command::Failed
201
251
  puts_by_host host, "Nothing found"
202
252
  end
@@ -211,13 +261,44 @@ class Kamal::Cli::App < Kamal::Cli::Base
211
261
  stop
212
262
  remove_containers
213
263
  remove_images
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
214
295
  end
215
296
  end
216
297
 
217
298
  desc "remove_container [VERSION]", "Remove app container with given version from servers", hide: true
218
299
  def remove_container(version)
219
300
  with_lock do
220
- on(KAMAL.hosts) do |host|
301
+ on(KAMAL.app_hosts) do |host|
221
302
  roles = KAMAL.roles_on(host)
222
303
 
223
304
  roles.each do |role|
@@ -231,7 +312,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
231
312
  desc "remove_containers", "Remove all app containers from servers", hide: true
232
313
  def remove_containers
233
314
  with_lock do
234
- on(KAMAL.hosts) do |host|
315
+ on(KAMAL.app_hosts) do |host|
235
316
  roles = KAMAL.roles_on(host)
236
317
 
237
318
  roles.each do |role|
@@ -245,16 +326,33 @@ class Kamal::Cli::App < Kamal::Cli::Base
245
326
  desc "remove_images", "Remove all app images from servers", hide: true
246
327
  def remove_images
247
328
  with_lock do
248
- on(KAMAL.hosts) do
329
+ on(KAMAL.app_hosts) do
249
330
  execute *KAMAL.auditor.record("Removed all app images"), verbosity: :debug
250
331
  execute *KAMAL.app.remove_images
251
332
  end
252
333
  end
253
334
  end
254
335
 
336
+ desc "remove_app_directories", "Remove the app directories from servers", hide: true
337
+ def remove_app_directories
338
+ with_lock do
339
+ on(KAMAL.app_hosts) do |host|
340
+ roles = KAMAL.roles_on(host)
341
+
342
+ roles.each do |role|
343
+ execute *KAMAL.auditor.record("Removed #{KAMAL.config.app_directory}", role: role), verbosity: :debug
344
+ execute *KAMAL.server.remove_app_directory, raise_on_non_zero_exit: false
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
349
+ end
350
+ end
351
+ end
352
+
255
353
  desc "version", "Show app version currently running on servers"
256
354
  def version
257
- on(KAMAL.hosts) do |host|
355
+ on(KAMAL.app_hosts) do |host|
258
356
  role = KAMAL.roles_on(host).first
259
357
  puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).current_running_version).strip
260
358
  end
@@ -295,4 +393,8 @@ class Kamal::Cli::App < Kamal::Cli::Base
295
393
  yield
296
394
  end
297
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
298
400
  end
@@ -1,5 +1,4 @@
1
1
  require "thor"
2
- require "dotenv"
3
2
  require "kamal/sshkit_with_ext"
4
3
 
5
4
  module Kamal::Cli
@@ -7,6 +6,7 @@ module Kamal::Cli
7
6
  include SSHKit::DSL
8
7
 
9
8
  def self.exit_on_failure?() true end
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"
12
12
  class_option :quiet, type: :boolean, aliases: "-q", desc: "Minimal logging"
@@ -22,55 +22,24 @@ module Kamal::Cli
22
22
 
23
23
  class_option :skip_hooks, aliases: "-H", type: :boolean, default: false, desc: "Don't run hooks"
24
24
 
25
- def initialize(*)
26
- super
27
- @original_env = ENV.to_h.dup
28
- load_env
29
- initialize_commander(options_with_subcommand_class_options)
30
- end
31
-
32
- private
33
- def reload_env
34
- reset_env
35
- load_env
36
- end
37
-
38
- def load_env
39
- if destination = options[:destination]
40
- Dotenv.load(".env.#{destination}", ".env")
41
- else
42
- Dotenv.load(".env")
43
- end
44
- end
45
-
46
- def reset_env
47
- replace_env @original_env
48
- end
49
-
50
- def replace_env(env)
51
- ENV.clear
52
- ENV.update(env)
53
- end
54
-
55
- def with_original_env
56
- keeping_current_env do
57
- reset_env
58
- yield
59
- end
25
+ def initialize(args = [], local_options = {}, config = {})
26
+ if config[:current_command].is_a?(Kamal::Cli::Alias::Command)
27
+ # When Thor generates a dynamic command, it doesn't attempt to parse the arguments.
28
+ # For our purposes, it means the arguments are passed in args rather than local_options.
29
+ super([], args, config)
30
+ else
31
+ super
60
32
  end
61
33
 
62
- def keeping_current_env
63
- current_env = ENV.to_h.dup
64
- yield
65
- ensure
66
- replace_env(current_env)
67
- end
34
+ initialize_commander unless KAMAL.configured?
35
+ end
68
36
 
37
+ private
69
38
  def options_with_subcommand_class_options
70
39
  options.merge(@_initializer.last[:class_options] || {})
71
40
  end
72
41
 
73
- def initialize_commander(options)
42
+ def initialize_commander
74
43
  KAMAL.tap do |commander|
75
44
  if options[:verbose]
76
45
  ENV["VERBOSE"] = "1" # For backtraces via cli/start
@@ -105,8 +74,6 @@ module Kamal::Cli
105
74
  if KAMAL.holding_lock?
106
75
  yield
107
76
  else
108
- ensure_run_and_locks_directory
109
-
110
77
  acquire_lock
111
78
 
112
79
  begin
@@ -135,6 +102,8 @@ module Kamal::Cli
135
102
  end
136
103
 
137
104
  def acquire_lock
105
+ ensure_run_directory
106
+
138
107
  raise_if_locked do
139
108
  say "Acquiring the deploy lock...", :magenta
140
109
  on(KAMAL.primary_host) { execute *KAMAL.lock.acquire("Automatic deploy lock", KAMAL.config.version), verbosity: :debug }
@@ -164,11 +133,19 @@ module Kamal::Cli
164
133
 
165
134
  def run_hook(hook, **extra_details)
166
135
  if !options[:skip_hooks] && KAMAL.hook.hook_exists?(hook)
167
- 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
168
143
 
169
144
  say "Running the #{hook} hook...", :magenta
170
- run_locally do
171
- execute *KAMAL.hook.run(hook, **details, **extra_details)
145
+ with_env KAMAL.hook.env(**details, **extra_details) do
146
+ run_locally do
147
+ execute *KAMAL.hook.run(hook)
148
+ end
172
149
  rescue SSHKit::Command::Failed => e
173
150
  raise HookError.new("Hook `#{hook}` failed:\n#{e.message}")
174
151
  end
@@ -176,12 +153,16 @@ module Kamal::Cli
176
153
  end
177
154
 
178
155
  def on(*args, &block)
156
+ pre_connect_if_required
157
+
158
+ super
159
+ end
160
+
161
+ def pre_connect_if_required
179
162
  if !KAMAL.connected?
180
163
  run_hook "pre-connect"
181
164
  KAMAL.connected = true
182
165
  end
183
-
184
- super
185
166
  end
186
167
 
187
168
  def command
@@ -206,13 +187,36 @@ module Kamal::Cli
206
187
  instance_variable_get("@_invocations").first
207
188
  end
208
189
 
209
- def ensure_run_and_locks_directory
190
+ def reset_invocation(cli_class)
191
+ instance_variable_get("@_invocations")[cli_class].pop
192
+ end
193
+
194
+ def ensure_run_directory
210
195
  on(KAMAL.hosts) do
211
196
  execute(*KAMAL.server.ensure_run_directory)
212
197
  end
198
+ end
213
199
 
214
- on(KAMAL.primary_host) do
215
- execute(*KAMAL.lock.ensure_locks_directory)
200
+ def with_env(env)
201
+ current_env = ENV.to_h.dup
202
+ ENV.update(env)
203
+ yield
204
+ ensure
205
+ ENV.clear
206
+ ENV.update(current_env)
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
216
220
  end
217
221
  end
218
222
  end