kamal-insecure 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 +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +13 -0
  4. data/bin/kamal +18 -0
  5. data/lib/kamal/cli/accessory.rb +313 -0
  6. data/lib/kamal/cli/alias/command.rb +10 -0
  7. data/lib/kamal/cli/app/assets.rb +24 -0
  8. data/lib/kamal/cli/app/boot.rb +126 -0
  9. data/lib/kamal/cli/app/error_pages.rb +33 -0
  10. data/lib/kamal/cli/app/ssl_certificates.rb +28 -0
  11. data/lib/kamal/cli/app.rb +400 -0
  12. data/lib/kamal/cli/base.rb +223 -0
  13. data/lib/kamal/cli/build/clone.rb +61 -0
  14. data/lib/kamal/cli/build.rb +204 -0
  15. data/lib/kamal/cli/healthcheck/barrier.rb +33 -0
  16. data/lib/kamal/cli/healthcheck/error.rb +2 -0
  17. data/lib/kamal/cli/healthcheck/poller.rb +42 -0
  18. data/lib/kamal/cli/lock.rb +45 -0
  19. data/lib/kamal/cli/main.rb +277 -0
  20. data/lib/kamal/cli/proxy.rb +290 -0
  21. data/lib/kamal/cli/prune.rb +34 -0
  22. data/lib/kamal/cli/registry.rb +19 -0
  23. data/lib/kamal/cli/secrets.rb +49 -0
  24. data/lib/kamal/cli/server.rb +50 -0
  25. data/lib/kamal/cli/templates/deploy.yml +101 -0
  26. data/lib/kamal/cli/templates/sample_hooks/docker-setup.sample +3 -0
  27. data/lib/kamal/cli/templates/sample_hooks/post-app-boot.sample +3 -0
  28. data/lib/kamal/cli/templates/sample_hooks/post-deploy.sample +14 -0
  29. data/lib/kamal/cli/templates/sample_hooks/post-proxy-reboot.sample +3 -0
  30. data/lib/kamal/cli/templates/sample_hooks/pre-app-boot.sample +3 -0
  31. data/lib/kamal/cli/templates/sample_hooks/pre-build.sample +51 -0
  32. data/lib/kamal/cli/templates/sample_hooks/pre-connect.sample +47 -0
  33. data/lib/kamal/cli/templates/sample_hooks/pre-deploy.sample +122 -0
  34. data/lib/kamal/cli/templates/sample_hooks/pre-proxy-reboot.sample +3 -0
  35. data/lib/kamal/cli/templates/secrets +17 -0
  36. data/lib/kamal/cli.rb +9 -0
  37. data/lib/kamal/commander/specifics.rb +62 -0
  38. data/lib/kamal/commander.rb +167 -0
  39. data/lib/kamal/commands/accessory/proxy.rb +16 -0
  40. data/lib/kamal/commands/accessory.rb +113 -0
  41. data/lib/kamal/commands/app/assets.rb +51 -0
  42. data/lib/kamal/commands/app/containers.rb +31 -0
  43. data/lib/kamal/commands/app/error_pages.rb +9 -0
  44. data/lib/kamal/commands/app/execution.rb +32 -0
  45. data/lib/kamal/commands/app/images.rb +13 -0
  46. data/lib/kamal/commands/app/logging.rb +28 -0
  47. data/lib/kamal/commands/app/proxy.rb +32 -0
  48. data/lib/kamal/commands/app.rb +124 -0
  49. data/lib/kamal/commands/auditor.rb +39 -0
  50. data/lib/kamal/commands/base.rb +134 -0
  51. data/lib/kamal/commands/builder/base.rb +124 -0
  52. data/lib/kamal/commands/builder/clone.rb +31 -0
  53. data/lib/kamal/commands/builder/cloud.rb +22 -0
  54. data/lib/kamal/commands/builder/hybrid.rb +21 -0
  55. data/lib/kamal/commands/builder/local.rb +14 -0
  56. data/lib/kamal/commands/builder/pack.rb +46 -0
  57. data/lib/kamal/commands/builder/remote.rb +63 -0
  58. data/lib/kamal/commands/builder.rb +48 -0
  59. data/lib/kamal/commands/docker.rb +34 -0
  60. data/lib/kamal/commands/hook.rb +20 -0
  61. data/lib/kamal/commands/lock.rb +70 -0
  62. data/lib/kamal/commands/proxy.rb +127 -0
  63. data/lib/kamal/commands/prune.rb +38 -0
  64. data/lib/kamal/commands/registry.rb +16 -0
  65. data/lib/kamal/commands/server.rb +15 -0
  66. data/lib/kamal/commands.rb +2 -0
  67. data/lib/kamal/configuration/accessory.rb +241 -0
  68. data/lib/kamal/configuration/alias.rb +15 -0
  69. data/lib/kamal/configuration/boot.rb +25 -0
  70. data/lib/kamal/configuration/builder.rb +211 -0
  71. data/lib/kamal/configuration/docs/accessory.yml +128 -0
  72. data/lib/kamal/configuration/docs/alias.yml +26 -0
  73. data/lib/kamal/configuration/docs/boot.yml +19 -0
  74. data/lib/kamal/configuration/docs/builder.yml +132 -0
  75. data/lib/kamal/configuration/docs/configuration.yml +184 -0
  76. data/lib/kamal/configuration/docs/env.yml +116 -0
  77. data/lib/kamal/configuration/docs/logging.yml +21 -0
  78. data/lib/kamal/configuration/docs/proxy.yml +164 -0
  79. data/lib/kamal/configuration/docs/registry.yml +56 -0
  80. data/lib/kamal/configuration/docs/role.yml +53 -0
  81. data/lib/kamal/configuration/docs/servers.yml +27 -0
  82. data/lib/kamal/configuration/docs/ssh.yml +70 -0
  83. data/lib/kamal/configuration/docs/sshkit.yml +23 -0
  84. data/lib/kamal/configuration/env/tag.rb +13 -0
  85. data/lib/kamal/configuration/env.rb +38 -0
  86. data/lib/kamal/configuration/logging.rb +33 -0
  87. data/lib/kamal/configuration/proxy/boot.rb +129 -0
  88. data/lib/kamal/configuration/proxy.rb +124 -0
  89. data/lib/kamal/configuration/registry.rb +32 -0
  90. data/lib/kamal/configuration/role.rb +222 -0
  91. data/lib/kamal/configuration/servers.rb +25 -0
  92. data/lib/kamal/configuration/ssh.rb +57 -0
  93. data/lib/kamal/configuration/sshkit.rb +22 -0
  94. data/lib/kamal/configuration/validation.rb +27 -0
  95. data/lib/kamal/configuration/validator/accessory.rb +13 -0
  96. data/lib/kamal/configuration/validator/alias.rb +15 -0
  97. data/lib/kamal/configuration/validator/builder.rb +15 -0
  98. data/lib/kamal/configuration/validator/configuration.rb +6 -0
  99. data/lib/kamal/configuration/validator/env.rb +54 -0
  100. data/lib/kamal/configuration/validator/proxy.rb +25 -0
  101. data/lib/kamal/configuration/validator/registry.rb +25 -0
  102. data/lib/kamal/configuration/validator/role.rb +13 -0
  103. data/lib/kamal/configuration/validator/servers.rb +7 -0
  104. data/lib/kamal/configuration/validator.rb +191 -0
  105. data/lib/kamal/configuration/volume.rb +22 -0
  106. data/lib/kamal/configuration.rb +372 -0
  107. data/lib/kamal/docker.rb +30 -0
  108. data/lib/kamal/env_file.rb +44 -0
  109. data/lib/kamal/git.rb +37 -0
  110. data/lib/kamal/secrets/adapters/aws_secrets_manager.rb +51 -0
  111. data/lib/kamal/secrets/adapters/base.rb +33 -0
  112. data/lib/kamal/secrets/adapters/bitwarden.rb +81 -0
  113. data/lib/kamal/secrets/adapters/bitwarden_secrets_manager.rb +66 -0
  114. data/lib/kamal/secrets/adapters/doppler.rb +57 -0
  115. data/lib/kamal/secrets/adapters/enpass.rb +71 -0
  116. data/lib/kamal/secrets/adapters/gcp_secret_manager.rb +112 -0
  117. data/lib/kamal/secrets/adapters/last_pass.rb +40 -0
  118. data/lib/kamal/secrets/adapters/one_password.rb +104 -0
  119. data/lib/kamal/secrets/adapters/passbolt.rb +130 -0
  120. data/lib/kamal/secrets/adapters/test.rb +14 -0
  121. data/lib/kamal/secrets/adapters.rb +16 -0
  122. data/lib/kamal/secrets/dotenv/inline_command_substitution.rb +33 -0
  123. data/lib/kamal/secrets.rb +42 -0
  124. data/lib/kamal/sshkit_with_ext.rb +142 -0
  125. data/lib/kamal/tags.rb +40 -0
  126. data/lib/kamal/utils/sensitive.rb +20 -0
  127. data/lib/kamal/utils.rb +110 -0
  128. data/lib/kamal/version.rb +3 -0
  129. data/lib/kamal.rb +14 -0
  130. metadata +365 -0
@@ -0,0 +1,400 @@
1
+ class Kamal::Cli::App < Kamal::Cli::Base
2
+ desc "boot", "Boot app on servers (or reboot app if already running)"
3
+ def boot
4
+ with_lock do
5
+ say "Get most recent version available as an image...", :magenta unless options[:version]
6
+ using_version(version_or_latest) do |version|
7
+ say "Start container with version #{version} (or reboot if already running)...", :magenta
8
+
9
+ # Assets are prepared in a separate step to ensure they are on all hosts before booting
10
+ on(KAMAL.app_hosts) do
11
+ Kamal::Cli::App::ErrorPages.new(host, self).run
12
+
13
+ KAMAL.roles_on(host).each do |role|
14
+ Kamal::Cli::App::Assets.new(host, role, self).run
15
+ Kamal::Cli::App::SslCertificates.new(host, role, self).run
16
+ end
17
+ end
18
+
19
+ # Primary hosts and roles are returned first, so they can open the barrier
20
+ barrier = Kamal::Cli::Healthcheck::Barrier.new
21
+
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
30
+ end
31
+
32
+ run_hook "post-app-boot", hosts: host_list
33
+ sleep KAMAL.config.boot.wait if KAMAL.config.boot.wait
34
+ end
35
+
36
+ # Tag once the app booted on all hosts
37
+ on(KAMAL.app_hosts) do |host|
38
+ execute *KAMAL.auditor.record("Tagging #{KAMAL.config.absolute_image} as the latest image"), verbosity: :debug
39
+ execute *KAMAL.app.tag_latest_image
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ desc "start", "Start existing app container on servers"
46
+ def start
47
+ with_lock do
48
+ on(KAMAL.app_hosts) do |host|
49
+ roles = KAMAL.roles_on(host)
50
+
51
+ roles.each do |role|
52
+ app = KAMAL.app(role: role, host: host)
53
+ execute *KAMAL.auditor.record("Started app version #{KAMAL.config.version}"), verbosity: :debug
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
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ desc "stop", "Stop app container on servers"
69
+ def stop
70
+ with_lock do
71
+ on(KAMAL.app_hosts) do |host|
72
+ roles = KAMAL.roles_on(host)
73
+
74
+ roles.each do |role|
75
+ app = KAMAL.app(role: role, host: host)
76
+ execute *KAMAL.auditor.record("Stopped app", role: role), verbosity: :debug
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
87
+ end
88
+ end
89
+ end
90
+ end
91
+
92
+ # FIXME: Drop in favor of just containers?
93
+ desc "details", "Show details about app containers"
94
+ def details
95
+ on(KAMAL.app_hosts) do |host|
96
+ roles = KAMAL.roles_on(host)
97
+
98
+ roles.each do |role|
99
+ puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).info)
100
+ end
101
+ end
102
+ end
103
+
104
+ desc "exec [CMD...]", "Execute a custom command on servers within the app container (use --help to show options)"
105
+ option :interactive, aliases: "-i", type: :boolean, default: false, desc: "Execute command over ssh for an interactive shell (use for console/bash)"
106
+ option :reuse, type: :boolean, default: false, desc: "Reuse currently running container instead of starting a new one"
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"
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)
121
+ env = options[:env]
122
+ detach = options[:detach]
123
+ case
124
+ when options[:interactive] && options[:reuse]
125
+ say "Get current version of running container...", :magenta unless options[:version]
126
+ using_version(options[:version] || current_running_version) do |version|
127
+ say "Launching interactive command with version #{version} via SSH from existing container on #{KAMAL.primary_host}...", :magenta
128
+ run_locally { exec KAMAL.app(role: KAMAL.primary_role, host: KAMAL.primary_host).execute_in_existing_container_over_ssh(cmd, env: env) }
129
+ end
130
+
131
+ when options[:interactive]
132
+ say "Get most recent version available as an image...", :magenta unless options[:version]
133
+ using_version(version_or_latest) do |version|
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 }
136
+ run_locally do
137
+ exec KAMAL.app(role: KAMAL.primary_role, host: KAMAL.primary_host).execute_in_new_container_over_ssh(cmd, env: env)
138
+ end
139
+ end
140
+
141
+ when options[:reuse]
142
+ say "Get current version of running container...", :magenta unless options[:version]
143
+ using_version(options[:version] || current_running_version) do |version|
144
+ say "Launching command with version #{version} from existing container...", :magenta
145
+
146
+ on(KAMAL.app_hosts) do |host|
147
+ roles = KAMAL.roles_on(host)
148
+
149
+ roles.each do |role|
150
+ execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on app version #{version}", role: role), verbosity: :debug
151
+ puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).execute_in_existing_container(cmd, env: env))
152
+ end
153
+ end
154
+ end
155
+
156
+ else
157
+ say "Get most recent version available as an image...", :magenta unless options[:version]
158
+ using_version(version_or_latest) do |version|
159
+ say "Launching command with version #{version} from new container...", :magenta
160
+ on(KAMAL.app_hosts) do |host|
161
+ execute *KAMAL.registry.login
162
+
163
+ roles = KAMAL.roles_on(host)
164
+
165
+ roles.each do |role|
166
+ execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on app version #{version}"), verbosity: :debug
167
+ puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).execute_in_new_container(cmd, env: env, detach: detach))
168
+ end
169
+ end
170
+ end
171
+ end
172
+ end
173
+
174
+ desc "containers", "Show app containers on servers"
175
+ def containers
176
+ on(KAMAL.app_hosts) { |host| puts_by_host host, capture_with_info(*KAMAL.app.list_containers) }
177
+ end
178
+
179
+ desc "stale_containers", "Detect app stale containers"
180
+ option :stop, aliases: "-s", type: :boolean, default: false, desc: "Stop the stale containers found"
181
+ def stale_containers
182
+ stop = options[:stop]
183
+
184
+ with_lock_if_stopping do
185
+ on(KAMAL.app_hosts) do |host|
186
+ roles = KAMAL.roles_on(host)
187
+
188
+ roles.each do |role|
189
+ app = KAMAL.app(role: role, host: host)
190
+ versions = capture_with_info(*app.list_versions, raise_on_non_zero_exit: false).split("\n")
191
+ versions -= [ capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip ]
192
+
193
+ versions.each do |version|
194
+ if stop
195
+ puts_by_host host, "Stopping stale container for role #{role} with version #{version}"
196
+ execute *app.stop(version: version), raise_on_non_zero_exit: false
197
+ else
198
+ puts_by_host host, "Detected stale container for role #{role} with version #{version} (use `kamal app stale_containers --stop` to stop)"
199
+ end
200
+ end
201
+ end
202
+ end
203
+ end
204
+ end
205
+
206
+ desc "images", "Show app images on servers"
207
+ def images
208
+ on(KAMAL.app_hosts) { |host| puts_by_host host, capture_with_info(*KAMAL.app.list_images) }
209
+ end
210
+
211
+ desc "logs", "Show log lines from app on servers (use --help to show options)"
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)"
213
+ option :lines, type: :numeric, aliases: "-n", desc: "Number of lines to show from each server"
214
+ option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)"
215
+ option :grep_options, desc: "Additional options supplied to grep"
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"
219
+ def logs
220
+ # FIXME: Catch when app containers aren't running
221
+
222
+ grep = options[:grep]
223
+ grep_options = options[:grep_options]
224
+ since = options[:since]
225
+ container_id = options[:container_id]
226
+ timestamps = !options[:skip_timestamps]
227
+
228
+ if options[:follow]
229
+ lines = options[:lines].presence || ((since || grep) ? nil : 10) # Default to 10 lines if since or grep isn't set
230
+
231
+ run_locally do
232
+ info "Following logs on #{KAMAL.primary_host}..."
233
+
234
+ KAMAL.specific_roles ||= [ KAMAL.primary_role.name ]
235
+ role = KAMAL.roles_on(KAMAL.primary_host).first
236
+
237
+ app = KAMAL.app(role: role, host: host)
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)
240
+ end
241
+ else
242
+ lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set
243
+
244
+ on(KAMAL.app_hosts) do |host|
245
+ roles = KAMAL.roles_on(host)
246
+
247
+ roles.each do |role|
248
+ begin
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))
250
+ rescue SSHKit::Command::Failed
251
+ puts_by_host host, "Nothing found"
252
+ end
253
+ end
254
+ end
255
+ end
256
+ end
257
+
258
+ desc "remove", "Remove app containers and images from servers"
259
+ def remove
260
+ with_lock do
261
+ stop
262
+ remove_containers
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
295
+ end
296
+ end
297
+
298
+ desc "remove_container [VERSION]", "Remove app container with given version from servers", hide: true
299
+ def remove_container(version)
300
+ with_lock do
301
+ on(KAMAL.app_hosts) do |host|
302
+ roles = KAMAL.roles_on(host)
303
+
304
+ roles.each do |role|
305
+ execute *KAMAL.auditor.record("Removed app container with version #{version}", role: role), verbosity: :debug
306
+ execute *KAMAL.app(role: role, host: host).remove_container(version: version)
307
+ end
308
+ end
309
+ end
310
+ end
311
+
312
+ desc "remove_containers", "Remove all app containers from servers", hide: true
313
+ def remove_containers
314
+ with_lock do
315
+ on(KAMAL.app_hosts) do |host|
316
+ roles = KAMAL.roles_on(host)
317
+
318
+ roles.each do |role|
319
+ execute *KAMAL.auditor.record("Removed all app containers", role: role), verbosity: :debug
320
+ execute *KAMAL.app(role: role, host: host).remove_containers
321
+ end
322
+ end
323
+ end
324
+ end
325
+
326
+ desc "remove_images", "Remove all app images from servers", hide: true
327
+ def remove_images
328
+ with_lock do
329
+ on(KAMAL.app_hosts) do
330
+ execute *KAMAL.auditor.record("Removed all app images"), verbosity: :debug
331
+ execute *KAMAL.app.remove_images
332
+ end
333
+ end
334
+ end
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
+
353
+ desc "version", "Show app version currently running on servers"
354
+ def version
355
+ on(KAMAL.app_hosts) do |host|
356
+ role = KAMAL.roles_on(host).first
357
+ puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).current_running_version).strip
358
+ end
359
+ end
360
+
361
+ private
362
+ def using_version(new_version)
363
+ if new_version
364
+ begin
365
+ old_version = KAMAL.config.version
366
+ KAMAL.config.version = new_version
367
+ yield new_version
368
+ ensure
369
+ KAMAL.config.version = old_version
370
+ end
371
+ else
372
+ yield KAMAL.config.version
373
+ end
374
+ end
375
+
376
+ def current_running_version(host: KAMAL.primary_host)
377
+ version = nil
378
+ on(host) do
379
+ role = KAMAL.roles_on(host).first
380
+ version = capture_with_info(*KAMAL.app(role: role, host: host).current_running_version).strip
381
+ end
382
+ version.presence
383
+ end
384
+
385
+ def version_or_latest
386
+ options[:version] || KAMAL.config.latest_tag
387
+ end
388
+
389
+ def with_lock_if_stopping
390
+ if options[:stop]
391
+ with_lock { yield }
392
+ else
393
+ yield
394
+ end
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
400
+ end
@@ -0,0 +1,223 @@
1
+ require "thor"
2
+ require "kamal/sshkit_with_ext"
3
+
4
+ module Kamal::Cli
5
+ class Base < Thor
6
+ include SSHKit::DSL
7
+
8
+ def self.exit_on_failure?() true end
9
+ def self.dynamic_command_class() Kamal::Cli::Alias::Command end
10
+
11
+ class_option :verbose, type: :boolean, aliases: "-v", desc: "Detailed logging"
12
+ class_option :quiet, type: :boolean, aliases: "-q", desc: "Minimal logging"
13
+
14
+ class_option :version, desc: "Run commands against a specific app version"
15
+
16
+ class_option :primary, type: :boolean, aliases: "-p", desc: "Run commands only on primary host instead of all"
17
+ class_option :hosts, aliases: "-h", desc: "Run commands on these hosts instead of all (separate by comma, supports wildcards with *)"
18
+ class_option :roles, aliases: "-r", desc: "Run commands on these roles instead of all (separate by comma, supports wildcards with *)"
19
+
20
+ class_option :config_file, aliases: "-c", default: "config/deploy.yml", desc: "Path to config file"
21
+ class_option :destination, aliases: "-d", desc: "Specify destination to be used for config file (staging -> deploy.staging.yml)"
22
+
23
+ class_option :skip_hooks, aliases: "-H", type: :boolean, default: false, desc: "Don't run hooks"
24
+
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
32
+ end
33
+
34
+ initialize_commander unless KAMAL.configured?
35
+ end
36
+
37
+ private
38
+ def options_with_subcommand_class_options
39
+ options.merge(@_initializer.last[:class_options] || {})
40
+ end
41
+
42
+ def initialize_commander
43
+ KAMAL.tap do |commander|
44
+ if options[:verbose]
45
+ ENV["VERBOSE"] = "1" # For backtraces via cli/start
46
+ commander.verbosity = :debug
47
+ end
48
+
49
+ if options[:quiet]
50
+ commander.verbosity = :error
51
+ end
52
+
53
+ commander.configure \
54
+ config_file: Pathname.new(File.expand_path(options[:config_file])),
55
+ destination: options[:destination],
56
+ version: options[:version]
57
+
58
+ commander.specific_hosts = options[:hosts]&.split(",")
59
+ commander.specific_roles = options[:roles]&.split(",")
60
+ commander.specific_primary! if options[:primary]
61
+ end
62
+ end
63
+
64
+ def print_runtime
65
+ started_at = Time.now
66
+ yield
67
+ Time.now - started_at
68
+ ensure
69
+ runtime = Time.now - started_at
70
+ puts " Finished all in #{sprintf("%.1f seconds", runtime)}"
71
+ end
72
+
73
+ def with_lock
74
+ if KAMAL.holding_lock?
75
+ yield
76
+ else
77
+ acquire_lock
78
+
79
+ begin
80
+ yield
81
+ rescue
82
+ begin
83
+ release_lock
84
+ rescue => e
85
+ say "Error releasing the deploy lock: #{e.message}", :red
86
+ end
87
+ raise
88
+ end
89
+
90
+ release_lock
91
+ end
92
+ end
93
+
94
+ def confirming(question)
95
+ return yield if options[:confirmed]
96
+
97
+ if ask(question, limited_to: %w[ y N ], default: "N") == "y"
98
+ yield
99
+ else
100
+ say "Aborted", :red
101
+ end
102
+ end
103
+
104
+ def acquire_lock
105
+ ensure_run_directory
106
+
107
+ raise_if_locked do
108
+ say "Acquiring the deploy lock...", :magenta
109
+ on(KAMAL.primary_host) { execute *KAMAL.lock.acquire("Automatic deploy lock", KAMAL.config.version), verbosity: :debug }
110
+ end
111
+
112
+ KAMAL.holding_lock = true
113
+ end
114
+
115
+ def release_lock
116
+ say "Releasing the deploy lock...", :magenta
117
+ on(KAMAL.primary_host) { execute *KAMAL.lock.release, verbosity: :debug }
118
+
119
+ KAMAL.holding_lock = false
120
+ end
121
+
122
+ def raise_if_locked
123
+ yield
124
+ rescue SSHKit::Runner::ExecuteError => e
125
+ if e.message =~ /cannot create directory/
126
+ say "Deploy lock already in place!", :red
127
+ on(KAMAL.primary_host) { puts capture_with_debug(*KAMAL.lock.status) }
128
+ raise LockError, "Deploy lock found. Run 'kamal lock help' for more information"
129
+ else
130
+ raise e
131
+ end
132
+ end
133
+
134
+ def run_hook(hook, **extra_details)
135
+ if !options[:skip_hooks] && KAMAL.hook.hook_exists?(hook)
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
143
+
144
+ say "Running the #{hook} hook...", :magenta
145
+ with_env KAMAL.hook.env(**details, **extra_details) do
146
+ run_locally do
147
+ execute *KAMAL.hook.run(hook)
148
+ end
149
+ rescue SSHKit::Command::Failed => e
150
+ raise HookError.new("Hook `#{hook}` failed:\n#{e.message}")
151
+ end
152
+ end
153
+ end
154
+
155
+ def on(*args, &block)
156
+ pre_connect_if_required
157
+
158
+ super
159
+ end
160
+
161
+ def pre_connect_if_required
162
+ if !KAMAL.connected?
163
+ run_hook "pre-connect"
164
+ KAMAL.connected = true
165
+ end
166
+ end
167
+
168
+ def command
169
+ @kamal_command ||= begin
170
+ invocation_class, invocation_commands = *first_invocation
171
+ if invocation_class == Kamal::Cli::Main
172
+ invocation_commands[0]
173
+ else
174
+ Kamal::Cli::Main.subcommand_classes.find { |command, clazz| clazz == invocation_class }[0]
175
+ end
176
+ end
177
+ end
178
+
179
+ def subcommand
180
+ @kamal_subcommand ||= begin
181
+ invocation_class, invocation_commands = *first_invocation
182
+ invocation_commands[0] if invocation_class != Kamal::Cli::Main
183
+ end
184
+ end
185
+
186
+ def first_invocation
187
+ instance_variable_get("@_invocations").first
188
+ end
189
+
190
+ def reset_invocation(cli_class)
191
+ instance_variable_get("@_invocations")[cli_class].pop
192
+ end
193
+
194
+ def ensure_run_directory
195
+ on(KAMAL.hosts) do
196
+ execute(*KAMAL.server.ensure_run_directory)
197
+ end
198
+ end
199
+
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
220
+ end
221
+ end
222
+ end
223
+ end
@@ -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