nocoffee-kamal 2.3.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +13 -0
- data/bin/kamal +18 -0
- data/lib/kamal/cli/accessory.rb +287 -0
- data/lib/kamal/cli/alias/command.rb +9 -0
- data/lib/kamal/cli/app/boot.rb +125 -0
- data/lib/kamal/cli/app/prepare_assets.rb +24 -0
- data/lib/kamal/cli/app.rb +335 -0
- data/lib/kamal/cli/base.rb +198 -0
- data/lib/kamal/cli/build/clone.rb +61 -0
- data/lib/kamal/cli/build.rb +162 -0
- data/lib/kamal/cli/healthcheck/barrier.rb +33 -0
- data/lib/kamal/cli/healthcheck/error.rb +2 -0
- data/lib/kamal/cli/healthcheck/poller.rb +42 -0
- data/lib/kamal/cli/lock.rb +45 -0
- data/lib/kamal/cli/main.rb +279 -0
- data/lib/kamal/cli/proxy.rb +257 -0
- data/lib/kamal/cli/prune.rb +34 -0
- data/lib/kamal/cli/registry.rb +17 -0
- data/lib/kamal/cli/secrets.rb +43 -0
- data/lib/kamal/cli/server.rb +48 -0
- data/lib/kamal/cli/templates/deploy.yml +98 -0
- data/lib/kamal/cli/templates/sample_hooks/docker-setup.sample +3 -0
- data/lib/kamal/cli/templates/sample_hooks/post-deploy.sample +14 -0
- data/lib/kamal/cli/templates/sample_hooks/post-proxy-reboot.sample +3 -0
- data/lib/kamal/cli/templates/sample_hooks/pre-build.sample +51 -0
- data/lib/kamal/cli/templates/sample_hooks/pre-connect.sample +47 -0
- data/lib/kamal/cli/templates/sample_hooks/pre-deploy.sample +109 -0
- data/lib/kamal/cli/templates/sample_hooks/pre-proxy-reboot.sample +3 -0
- data/lib/kamal/cli/templates/secrets +17 -0
- data/lib/kamal/cli.rb +8 -0
- data/lib/kamal/commander/specifics.rb +54 -0
- data/lib/kamal/commander.rb +176 -0
- data/lib/kamal/commands/accessory.rb +113 -0
- data/lib/kamal/commands/app/assets.rb +51 -0
- data/lib/kamal/commands/app/containers.rb +31 -0
- data/lib/kamal/commands/app/execution.rb +30 -0
- data/lib/kamal/commands/app/images.rb +13 -0
- data/lib/kamal/commands/app/logging.rb +18 -0
- data/lib/kamal/commands/app/proxy.rb +16 -0
- data/lib/kamal/commands/app.rb +115 -0
- data/lib/kamal/commands/auditor.rb +33 -0
- data/lib/kamal/commands/base.rb +98 -0
- data/lib/kamal/commands/builder/base.rb +111 -0
- data/lib/kamal/commands/builder/clone.rb +31 -0
- data/lib/kamal/commands/builder/hybrid.rb +21 -0
- data/lib/kamal/commands/builder/local.rb +14 -0
- data/lib/kamal/commands/builder/remote.rb +63 -0
- data/lib/kamal/commands/builder.rb +56 -0
- data/lib/kamal/commands/docker.rb +34 -0
- data/lib/kamal/commands/hook.rb +20 -0
- data/lib/kamal/commands/lock.rb +70 -0
- data/lib/kamal/commands/proxy.rb +87 -0
- data/lib/kamal/commands/prune.rb +38 -0
- data/lib/kamal/commands/registry.rb +14 -0
- data/lib/kamal/commands/server.rb +15 -0
- data/lib/kamal/commands.rb +2 -0
- data/lib/kamal/configuration/accessory.rb +186 -0
- data/lib/kamal/configuration/alias.rb +15 -0
- data/lib/kamal/configuration/boot.rb +25 -0
- data/lib/kamal/configuration/builder.rb +191 -0
- data/lib/kamal/configuration/docs/accessory.yml +100 -0
- data/lib/kamal/configuration/docs/alias.yml +26 -0
- data/lib/kamal/configuration/docs/boot.yml +19 -0
- data/lib/kamal/configuration/docs/builder.yml +110 -0
- data/lib/kamal/configuration/docs/configuration.yml +178 -0
- data/lib/kamal/configuration/docs/env.yml +85 -0
- data/lib/kamal/configuration/docs/logging.yml +21 -0
- data/lib/kamal/configuration/docs/proxy.yml +110 -0
- data/lib/kamal/configuration/docs/registry.yml +52 -0
- data/lib/kamal/configuration/docs/role.yml +53 -0
- data/lib/kamal/configuration/docs/servers.yml +27 -0
- data/lib/kamal/configuration/docs/ssh.yml +70 -0
- data/lib/kamal/configuration/docs/sshkit.yml +23 -0
- data/lib/kamal/configuration/env/tag.rb +13 -0
- data/lib/kamal/configuration/env.rb +29 -0
- data/lib/kamal/configuration/logging.rb +33 -0
- data/lib/kamal/configuration/proxy.rb +63 -0
- data/lib/kamal/configuration/registry.rb +32 -0
- data/lib/kamal/configuration/role.rb +220 -0
- data/lib/kamal/configuration/servers.rb +18 -0
- data/lib/kamal/configuration/ssh.rb +57 -0
- data/lib/kamal/configuration/sshkit.rb +22 -0
- data/lib/kamal/configuration/validation.rb +27 -0
- data/lib/kamal/configuration/validator/accessory.rb +9 -0
- data/lib/kamal/configuration/validator/alias.rb +15 -0
- data/lib/kamal/configuration/validator/builder.rb +13 -0
- data/lib/kamal/configuration/validator/configuration.rb +6 -0
- data/lib/kamal/configuration/validator/env.rb +54 -0
- data/lib/kamal/configuration/validator/proxy.rb +15 -0
- data/lib/kamal/configuration/validator/registry.rb +25 -0
- data/lib/kamal/configuration/validator/role.rb +11 -0
- data/lib/kamal/configuration/validator/servers.rb +7 -0
- data/lib/kamal/configuration/validator.rb +171 -0
- data/lib/kamal/configuration/volume.rb +22 -0
- data/lib/kamal/configuration.rb +393 -0
- data/lib/kamal/env_file.rb +44 -0
- data/lib/kamal/git.rb +27 -0
- data/lib/kamal/secrets/adapters/base.rb +23 -0
- data/lib/kamal/secrets/adapters/bitwarden.rb +81 -0
- data/lib/kamal/secrets/adapters/last_pass.rb +39 -0
- data/lib/kamal/secrets/adapters/one_password.rb +70 -0
- data/lib/kamal/secrets/adapters/test.rb +14 -0
- data/lib/kamal/secrets/adapters.rb +14 -0
- data/lib/kamal/secrets/dotenv/inline_command_substitution.rb +32 -0
- data/lib/kamal/secrets.rb +42 -0
- data/lib/kamal/sshkit_with_ext.rb +142 -0
- data/lib/kamal/tags.rb +40 -0
- data/lib/kamal/utils/sensitive.rb +20 -0
- data/lib/kamal/utils.rb +110 -0
- data/lib/kamal/version.rb +3 -0
- data/lib/kamal.rb +14 -0
- 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
|