nocoffee-kamal 2.3.0.1

Sign up to get free protection for your applications and to get access to all the features.
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