nocoffee-kamal 2.3.0.1

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 (114) 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 +287 -0
  6. data/lib/kamal/cli/alias/command.rb +9 -0
  7. data/lib/kamal/cli/app/boot.rb +125 -0
  8. data/lib/kamal/cli/app/prepare_assets.rb +24 -0
  9. data/lib/kamal/cli/app.rb +335 -0
  10. data/lib/kamal/cli/base.rb +198 -0
  11. data/lib/kamal/cli/build/clone.rb +61 -0
  12. data/lib/kamal/cli/build.rb +162 -0
  13. data/lib/kamal/cli/healthcheck/barrier.rb +33 -0
  14. data/lib/kamal/cli/healthcheck/error.rb +2 -0
  15. data/lib/kamal/cli/healthcheck/poller.rb +42 -0
  16. data/lib/kamal/cli/lock.rb +45 -0
  17. data/lib/kamal/cli/main.rb +279 -0
  18. data/lib/kamal/cli/proxy.rb +257 -0
  19. data/lib/kamal/cli/prune.rb +34 -0
  20. data/lib/kamal/cli/registry.rb +17 -0
  21. data/lib/kamal/cli/secrets.rb +43 -0
  22. data/lib/kamal/cli/server.rb +48 -0
  23. data/lib/kamal/cli/templates/deploy.yml +98 -0
  24. data/lib/kamal/cli/templates/sample_hooks/docker-setup.sample +3 -0
  25. data/lib/kamal/cli/templates/sample_hooks/post-deploy.sample +14 -0
  26. data/lib/kamal/cli/templates/sample_hooks/post-proxy-reboot.sample +3 -0
  27. data/lib/kamal/cli/templates/sample_hooks/pre-build.sample +51 -0
  28. data/lib/kamal/cli/templates/sample_hooks/pre-connect.sample +47 -0
  29. data/lib/kamal/cli/templates/sample_hooks/pre-deploy.sample +109 -0
  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 +8 -0
  33. data/lib/kamal/commander/specifics.rb +54 -0
  34. data/lib/kamal/commander.rb +176 -0
  35. data/lib/kamal/commands/accessory.rb +113 -0
  36. data/lib/kamal/commands/app/assets.rb +51 -0
  37. data/lib/kamal/commands/app/containers.rb +31 -0
  38. data/lib/kamal/commands/app/execution.rb +30 -0
  39. data/lib/kamal/commands/app/images.rb +13 -0
  40. data/lib/kamal/commands/app/logging.rb +18 -0
  41. data/lib/kamal/commands/app/proxy.rb +16 -0
  42. data/lib/kamal/commands/app.rb +115 -0
  43. data/lib/kamal/commands/auditor.rb +33 -0
  44. data/lib/kamal/commands/base.rb +98 -0
  45. data/lib/kamal/commands/builder/base.rb +111 -0
  46. data/lib/kamal/commands/builder/clone.rb +31 -0
  47. data/lib/kamal/commands/builder/hybrid.rb +21 -0
  48. data/lib/kamal/commands/builder/local.rb +14 -0
  49. data/lib/kamal/commands/builder/remote.rb +63 -0
  50. data/lib/kamal/commands/builder.rb +56 -0
  51. data/lib/kamal/commands/docker.rb +34 -0
  52. data/lib/kamal/commands/hook.rb +20 -0
  53. data/lib/kamal/commands/lock.rb +70 -0
  54. data/lib/kamal/commands/proxy.rb +87 -0
  55. data/lib/kamal/commands/prune.rb +38 -0
  56. data/lib/kamal/commands/registry.rb +14 -0
  57. data/lib/kamal/commands/server.rb +15 -0
  58. data/lib/kamal/commands.rb +2 -0
  59. data/lib/kamal/configuration/accessory.rb +186 -0
  60. data/lib/kamal/configuration/alias.rb +15 -0
  61. data/lib/kamal/configuration/boot.rb +25 -0
  62. data/lib/kamal/configuration/builder.rb +191 -0
  63. data/lib/kamal/configuration/docs/accessory.yml +100 -0
  64. data/lib/kamal/configuration/docs/alias.yml +26 -0
  65. data/lib/kamal/configuration/docs/boot.yml +19 -0
  66. data/lib/kamal/configuration/docs/builder.yml +110 -0
  67. data/lib/kamal/configuration/docs/configuration.yml +178 -0
  68. data/lib/kamal/configuration/docs/env.yml +85 -0
  69. data/lib/kamal/configuration/docs/logging.yml +21 -0
  70. data/lib/kamal/configuration/docs/proxy.yml +110 -0
  71. data/lib/kamal/configuration/docs/registry.yml +52 -0
  72. data/lib/kamal/configuration/docs/role.yml +53 -0
  73. data/lib/kamal/configuration/docs/servers.yml +27 -0
  74. data/lib/kamal/configuration/docs/ssh.yml +70 -0
  75. data/lib/kamal/configuration/docs/sshkit.yml +23 -0
  76. data/lib/kamal/configuration/env/tag.rb +13 -0
  77. data/lib/kamal/configuration/env.rb +29 -0
  78. data/lib/kamal/configuration/logging.rb +33 -0
  79. data/lib/kamal/configuration/proxy.rb +63 -0
  80. data/lib/kamal/configuration/registry.rb +32 -0
  81. data/lib/kamal/configuration/role.rb +220 -0
  82. data/lib/kamal/configuration/servers.rb +18 -0
  83. data/lib/kamal/configuration/ssh.rb +57 -0
  84. data/lib/kamal/configuration/sshkit.rb +22 -0
  85. data/lib/kamal/configuration/validation.rb +27 -0
  86. data/lib/kamal/configuration/validator/accessory.rb +9 -0
  87. data/lib/kamal/configuration/validator/alias.rb +15 -0
  88. data/lib/kamal/configuration/validator/builder.rb +13 -0
  89. data/lib/kamal/configuration/validator/configuration.rb +6 -0
  90. data/lib/kamal/configuration/validator/env.rb +54 -0
  91. data/lib/kamal/configuration/validator/proxy.rb +15 -0
  92. data/lib/kamal/configuration/validator/registry.rb +25 -0
  93. data/lib/kamal/configuration/validator/role.rb +11 -0
  94. data/lib/kamal/configuration/validator/servers.rb +7 -0
  95. data/lib/kamal/configuration/validator.rb +171 -0
  96. data/lib/kamal/configuration/volume.rb +22 -0
  97. data/lib/kamal/configuration.rb +393 -0
  98. data/lib/kamal/env_file.rb +44 -0
  99. data/lib/kamal/git.rb +27 -0
  100. data/lib/kamal/secrets/adapters/base.rb +23 -0
  101. data/lib/kamal/secrets/adapters/bitwarden.rb +81 -0
  102. data/lib/kamal/secrets/adapters/last_pass.rb +39 -0
  103. data/lib/kamal/secrets/adapters/one_password.rb +70 -0
  104. data/lib/kamal/secrets/adapters/test.rb +14 -0
  105. data/lib/kamal/secrets/adapters.rb +14 -0
  106. data/lib/kamal/secrets/dotenv/inline_command_substitution.rb +32 -0
  107. data/lib/kamal/secrets.rb +42 -0
  108. data/lib/kamal/sshkit_with_ext.rb +142 -0
  109. data/lib/kamal/tags.rb +40 -0
  110. data/lib/kamal/utils/sensitive.rb +20 -0
  111. data/lib/kamal/utils.rb +110 -0
  112. data/lib/kamal/version.rb +3 -0
  113. data/lib/kamal.rb +14 -0
  114. metadata +349 -0
@@ -0,0 +1,335 @@
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.hosts) do
11
+ KAMAL.roles_on(host).each do |role|
12
+ Kamal::Cli::App::PrepareAssets.new(host, role, self).run
13
+ end
14
+ end
15
+
16
+ # Primary hosts and roles are returned first, so they can open the barrier
17
+ barrier = Kamal::Cli::Healthcheck::Barrier.new
18
+
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
+ end
23
+ end
24
+
25
+ # Tag once the app booted on all hosts
26
+ on(KAMAL.hosts) do |host|
27
+ execute *KAMAL.auditor.record("Tagging #{KAMAL.config.absolute_image} as the latest image"), verbosity: :debug
28
+ execute *KAMAL.app.tag_latest_image
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ desc "start", "Start existing app container on servers"
35
+ def start
36
+ with_lock do
37
+ on(KAMAL.hosts) do |host|
38
+ roles = KAMAL.roles_on(host)
39
+
40
+ roles.each do |role|
41
+ app = KAMAL.app(role: role, host: host)
42
+ execute *KAMAL.auditor.record("Started app version #{KAMAL.config.version}"), verbosity: :debug
43
+ execute *app.start, raise_on_non_zero_exit: false
44
+
45
+ if role.running_proxy?
46
+ version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
47
+ endpoint = capture_with_info(*app.container_id_for_version(version)).strip
48
+ raise Kamal::Cli::BootError, "Failed to get endpoint for #{role} on #{host}, did the container boot?" if endpoint.empty?
49
+
50
+ execute *app.deploy(target: endpoint)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+
57
+ desc "stop", "Stop app container on servers"
58
+ def stop
59
+ with_lock do
60
+ on(KAMAL.hosts) do |host|
61
+ roles = KAMAL.roles_on(host)
62
+
63
+ roles.each do |role|
64
+ app = KAMAL.app(role: role, host: host)
65
+ execute *KAMAL.auditor.record("Stopped app", role: role), verbosity: :debug
66
+
67
+ if role.running_proxy?
68
+ version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
69
+ endpoint = capture_with_info(*app.container_id_for_version(version)).strip
70
+ if endpoint.present?
71
+ execute *app.remove, raise_on_non_zero_exit: false
72
+ end
73
+ end
74
+
75
+ execute *app.stop, raise_on_non_zero_exit: false
76
+ end
77
+ end
78
+ end
79
+ end
80
+
81
+ # FIXME: Drop in favor of just containers?
82
+ desc "details", "Show details about app containers"
83
+ def details
84
+ on(KAMAL.hosts) do |host|
85
+ roles = KAMAL.roles_on(host)
86
+
87
+ roles.each do |role|
88
+ puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).info)
89
+ end
90
+ end
91
+ end
92
+
93
+ desc "exec [CMD...]", "Execute a custom command on servers within the app container (use --help to show options)"
94
+ option :interactive, aliases: "-i", type: :boolean, default: false, desc: "Execute command over ssh for an interactive shell (use for console/bash)"
95
+ option :reuse, type: :boolean, default: false, desc: "Reuse currently running container instead of starting a new one"
96
+ option :env, aliases: "-e", type: :hash, desc: "Set environment variables for the command"
97
+ def exec(*cmd)
98
+ cmd = Kamal::Utils.join_commands(cmd)
99
+ env = options[:env]
100
+ case
101
+ when options[:interactive] && options[:reuse]
102
+ say "Get current version of running container...", :magenta unless options[:version]
103
+ using_version(options[:version] || current_running_version) do |version|
104
+ say "Launching interactive command with version #{version} via SSH from existing container on #{KAMAL.primary_host}...", :magenta
105
+ run_locally { exec KAMAL.app(role: KAMAL.primary_role, host: KAMAL.primary_host).execute_in_existing_container_over_ssh(cmd, env: env) }
106
+ end
107
+
108
+ when options[:interactive]
109
+ say "Get most recent version available as an image...", :magenta unless options[:version]
110
+ using_version(version_or_latest) do |version|
111
+ say "Launching interactive command with version #{version} via SSH from new container on #{KAMAL.primary_host}...", :magenta
112
+ run_locally do
113
+ exec KAMAL.app(role: KAMAL.primary_role, host: KAMAL.primary_host).execute_in_new_container_over_ssh(cmd, env: env)
114
+ end
115
+ end
116
+
117
+ when options[:reuse]
118
+ say "Get current version of running container...", :magenta unless options[:version]
119
+ using_version(options[:version] || current_running_version) do |version|
120
+ say "Launching command with version #{version} from existing container...", :magenta
121
+
122
+ on(KAMAL.hosts) do |host|
123
+ roles = KAMAL.roles_on(host)
124
+
125
+ roles.each do |role|
126
+ execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on app version #{version}", role: role), verbosity: :debug
127
+ puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).execute_in_existing_container(cmd, env: env))
128
+ end
129
+ end
130
+ end
131
+
132
+ else
133
+ say "Get most recent version available as an image...", :magenta unless options[:version]
134
+ using_version(version_or_latest) do |version|
135
+ say "Launching command with version #{version} from new container...", :magenta
136
+ on(KAMAL.hosts) do |host|
137
+ roles = KAMAL.roles_on(host)
138
+
139
+ roles.each do |role|
140
+ 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))
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
147
+
148
+ desc "containers", "Show app containers on servers"
149
+ def containers
150
+ on(KAMAL.hosts) { |host| puts_by_host host, capture_with_info(*KAMAL.app.list_containers) }
151
+ end
152
+
153
+ desc "stale_containers", "Detect app stale containers"
154
+ option :stop, aliases: "-s", type: :boolean, default: false, desc: "Stop the stale containers found"
155
+ def stale_containers
156
+ stop = options[:stop]
157
+
158
+ with_lock_if_stopping do
159
+ on(KAMAL.hosts) do |host|
160
+ roles = KAMAL.roles_on(host)
161
+
162
+ roles.each do |role|
163
+ app = KAMAL.app(role: role, host: host)
164
+ versions = capture_with_info(*app.list_versions, raise_on_non_zero_exit: false).split("\n")
165
+ versions -= [ capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip ]
166
+
167
+ versions.each do |version|
168
+ if stop
169
+ puts_by_host host, "Stopping stale container for role #{role} with version #{version}"
170
+ execute *app.stop(version: version), raise_on_non_zero_exit: false
171
+ else
172
+ puts_by_host host, "Detected stale container for role #{role} with version #{version} (use `kamal app stale_containers --stop` to stop)"
173
+ end
174
+ end
175
+ end
176
+ end
177
+ end
178
+ end
179
+
180
+ desc "images", "Show app images on servers"
181
+ def images
182
+ on(KAMAL.hosts) { |host| puts_by_host host, capture_with_info(*KAMAL.app.list_images) }
183
+ end
184
+
185
+ desc "logs", "Show log lines from app on servers (use --help to show options)"
186
+ 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
+ option :lines, type: :numeric, aliases: "-n", desc: "Number of lines to show from each server"
188
+ 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"
190
+ option :follow, aliases: "-f", desc: "Follow log on primary server (or specific host set by --hosts)"
191
+ option :skip_timestamps, type: :boolean, aliases: "-T", desc: "Skip appending timestamps to logging output"
192
+ def logs
193
+ # FIXME: Catch when app containers aren't running
194
+
195
+ grep = options[:grep]
196
+ grep_options = options[:grep_options]
197
+ since = options[:since]
198
+ timestamps = !options[:skip_timestamps]
199
+
200
+ if options[:follow]
201
+ lines = options[:lines].presence || ((since || grep) ? nil : 10) # Default to 10 lines if since or grep isn't set
202
+
203
+ run_locally do
204
+ info "Following logs on #{KAMAL.primary_host}..."
205
+
206
+ KAMAL.specific_roles ||= [ KAMAL.primary_role.name ]
207
+ role = KAMAL.roles_on(KAMAL.primary_host).first
208
+
209
+ 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)
212
+ end
213
+ else
214
+ lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set
215
+
216
+ on(KAMAL.hosts) do |host|
217
+ roles = KAMAL.roles_on(host)
218
+
219
+ roles.each do |role|
220
+ 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))
222
+ rescue SSHKit::Command::Failed
223
+ puts_by_host host, "Nothing found"
224
+ end
225
+ end
226
+ end
227
+ end
228
+ end
229
+
230
+ desc "remove", "Remove app containers and images from servers"
231
+ def remove
232
+ with_lock do
233
+ stop
234
+ remove_containers
235
+ remove_images
236
+ remove_app_directory
237
+ end
238
+ end
239
+
240
+ desc "remove_container [VERSION]", "Remove app container with given version from servers", hide: true
241
+ def remove_container(version)
242
+ with_lock do
243
+ on(KAMAL.hosts) do |host|
244
+ roles = KAMAL.roles_on(host)
245
+
246
+ roles.each do |role|
247
+ execute *KAMAL.auditor.record("Removed app container with version #{version}", role: role), verbosity: :debug
248
+ execute *KAMAL.app(role: role, host: host).remove_container(version: version)
249
+ end
250
+ end
251
+ end
252
+ end
253
+
254
+ desc "remove_containers", "Remove all app containers from servers", hide: true
255
+ def remove_containers
256
+ with_lock do
257
+ on(KAMAL.hosts) do |host|
258
+ roles = KAMAL.roles_on(host)
259
+
260
+ roles.each do |role|
261
+ execute *KAMAL.auditor.record("Removed all app containers", role: role), verbosity: :debug
262
+ execute *KAMAL.app(role: role, host: host).remove_containers
263
+ end
264
+ end
265
+ end
266
+ end
267
+
268
+ desc "remove_images", "Remove all app images from servers", hide: true
269
+ def remove_images
270
+ with_lock do
271
+ on(KAMAL.hosts) do
272
+ execute *KAMAL.auditor.record("Removed all app images"), verbosity: :debug
273
+ execute *KAMAL.app.remove_images
274
+ end
275
+ end
276
+ end
277
+
278
+ desc "remove_app_directory", "Remove the service directory from servers", hide: true
279
+ def remove_app_directory
280
+ with_lock do
281
+ on(KAMAL.hosts) do |host|
282
+ roles = KAMAL.roles_on(host)
283
+
284
+ roles.each do |role|
285
+ execute *KAMAL.auditor.record("Removed #{KAMAL.config.app_directory} on all servers", role: role), verbosity: :debug
286
+ execute *KAMAL.server.remove_app_directory, raise_on_non_zero_exit: false
287
+ end
288
+ end
289
+ end
290
+ end
291
+
292
+ desc "version", "Show app version currently running on servers"
293
+ def version
294
+ on(KAMAL.hosts) do |host|
295
+ role = KAMAL.roles_on(host).first
296
+ puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).current_running_version).strip
297
+ end
298
+ end
299
+
300
+ private
301
+ def using_version(new_version)
302
+ if new_version
303
+ begin
304
+ old_version = KAMAL.config.version
305
+ KAMAL.config.version = new_version
306
+ yield new_version
307
+ ensure
308
+ KAMAL.config.version = old_version
309
+ end
310
+ else
311
+ yield KAMAL.config.version
312
+ end
313
+ end
314
+
315
+ def current_running_version(host: KAMAL.primary_host)
316
+ version = nil
317
+ on(host) do
318
+ role = KAMAL.roles_on(host).first
319
+ version = capture_with_info(*KAMAL.app(role: role, host: host).current_running_version).strip
320
+ end
321
+ version.presence
322
+ end
323
+
324
+ def version_or_latest
325
+ options[:version] || KAMAL.config.latest_tag
326
+ end
327
+
328
+ def with_lock_if_stopping
329
+ if options[:stop]
330
+ with_lock { yield }
331
+ else
332
+ yield
333
+ end
334
+ end
335
+ end
@@ -0,0 +1,198 @@
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?() false 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
+ initialize_commander unless KAMAL.configured?
34
+ end
35
+
36
+ private
37
+ def options_with_subcommand_class_options
38
+ options.merge(@_initializer.last[:class_options] || {})
39
+ end
40
+
41
+ def initialize_commander
42
+ KAMAL.tap do |commander|
43
+ if options[:verbose]
44
+ ENV["VERBOSE"] = "1" # For backtraces via cli/start
45
+ commander.verbosity = :debug
46
+ end
47
+
48
+ if options[:quiet]
49
+ commander.verbosity = :error
50
+ end
51
+
52
+ commander.configure \
53
+ config_file: Pathname.new(File.expand_path(options[:config_file])),
54
+ destination: options[:destination],
55
+ version: options[:version]
56
+
57
+ commander.specific_hosts = options[:hosts]&.split(",")
58
+ commander.specific_roles = options[:roles]&.split(",")
59
+ commander.specific_primary! if options[:primary]
60
+ end
61
+ end
62
+
63
+ def print_runtime
64
+ started_at = Time.now
65
+ yield
66
+ Time.now - started_at
67
+ ensure
68
+ runtime = Time.now - started_at
69
+ puts " Finished all in #{sprintf("%.1f seconds", runtime)}"
70
+ end
71
+
72
+ def with_lock
73
+ if KAMAL.holding_lock?
74
+ yield
75
+ else
76
+ acquire_lock
77
+
78
+ begin
79
+ yield
80
+ rescue
81
+ begin
82
+ release_lock
83
+ rescue => e
84
+ say "Error releasing the deploy lock: #{e.message}", :red
85
+ end
86
+ raise
87
+ end
88
+
89
+ release_lock
90
+ end
91
+ end
92
+
93
+ def confirming(question)
94
+ return yield if options[:confirmed]
95
+
96
+ if ask(question, limited_to: %w[ y N ], default: "N") == "y"
97
+ yield
98
+ else
99
+ say "Aborted", :red
100
+ end
101
+ end
102
+
103
+ def acquire_lock
104
+ ensure_run_directory
105
+
106
+ raise_if_locked do
107
+ say "Acquiring the deploy lock...", :magenta
108
+ on(KAMAL.primary_host) { execute *KAMAL.lock.acquire("Automatic deploy lock", KAMAL.config.version), verbosity: :debug }
109
+ end
110
+
111
+ KAMAL.holding_lock = true
112
+ end
113
+
114
+ def release_lock
115
+ say "Releasing the deploy lock...", :magenta
116
+ on(KAMAL.primary_host) { execute *KAMAL.lock.release, verbosity: :debug }
117
+
118
+ KAMAL.holding_lock = false
119
+ end
120
+
121
+ def raise_if_locked
122
+ yield
123
+ rescue SSHKit::Runner::ExecuteError => e
124
+ if e.message =~ /cannot create directory/
125
+ say "Deploy lock already in place!", :red
126
+ on(KAMAL.primary_host) { puts capture_with_debug(*KAMAL.lock.status) }
127
+ raise LockError, "Deploy lock found. Run 'kamal lock help' for more information"
128
+ else
129
+ raise e
130
+ end
131
+ end
132
+
133
+ def run_hook(hook, **extra_details)
134
+ if !options[:skip_hooks] && KAMAL.hook.hook_exists?(hook)
135
+ details = { hosts: KAMAL.hosts.join(","), command: command, subcommand: subcommand }
136
+
137
+ say "Running the #{hook} hook...", :magenta
138
+ with_env KAMAL.hook.env(**details, **extra_details) do
139
+ run_locally do
140
+ execute *KAMAL.hook.run(hook)
141
+ end
142
+ rescue SSHKit::Command::Failed => e
143
+ raise HookError.new("Hook `#{hook}` failed:\n#{e.message}")
144
+ end
145
+ end
146
+ end
147
+
148
+ def on(*args, &block)
149
+ if !KAMAL.connected?
150
+ run_hook "pre-connect"
151
+ KAMAL.connected = true
152
+ end
153
+
154
+ super
155
+ end
156
+
157
+ def command
158
+ @kamal_command ||= begin
159
+ invocation_class, invocation_commands = *first_invocation
160
+ if invocation_class == Kamal::Cli::Main
161
+ invocation_commands[0]
162
+ else
163
+ Kamal::Cli::Main.subcommand_classes.find { |command, clazz| clazz == invocation_class }[0]
164
+ end
165
+ end
166
+ end
167
+
168
+ def subcommand
169
+ @kamal_subcommand ||= begin
170
+ invocation_class, invocation_commands = *first_invocation
171
+ invocation_commands[0] if invocation_class != Kamal::Cli::Main
172
+ end
173
+ end
174
+
175
+ def first_invocation
176
+ instance_variable_get("@_invocations").first
177
+ end
178
+
179
+ def reset_invocation(cli_class)
180
+ instance_variable_get("@_invocations")[cli_class].pop
181
+ end
182
+
183
+ def ensure_run_directory
184
+ on(KAMAL.hosts) do
185
+ execute(*KAMAL.server.ensure_run_directory)
186
+ end
187
+ end
188
+
189
+ def with_env(env)
190
+ current_env = ENV.to_h.dup
191
+ ENV.update(env)
192
+ yield
193
+ ensure
194
+ ENV.clear
195
+ ENV.update(current_env)
196
+ end
197
+ end
198
+ 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