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.
- 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 +313 -0
- data/lib/kamal/cli/alias/command.rb +10 -0
- data/lib/kamal/cli/app/assets.rb +24 -0
- data/lib/kamal/cli/app/boot.rb +126 -0
- data/lib/kamal/cli/app/error_pages.rb +33 -0
- data/lib/kamal/cli/app/ssl_certificates.rb +28 -0
- data/lib/kamal/cli/app.rb +400 -0
- data/lib/kamal/cli/base.rb +223 -0
- data/lib/kamal/cli/build/clone.rb +61 -0
- data/lib/kamal/cli/build.rb +204 -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 +277 -0
- data/lib/kamal/cli/proxy.rb +290 -0
- data/lib/kamal/cli/prune.rb +34 -0
- data/lib/kamal/cli/registry.rb +19 -0
- data/lib/kamal/cli/secrets.rb +49 -0
- data/lib/kamal/cli/server.rb +50 -0
- data/lib/kamal/cli/templates/deploy.yml +101 -0
- data/lib/kamal/cli/templates/sample_hooks/docker-setup.sample +3 -0
- data/lib/kamal/cli/templates/sample_hooks/post-app-boot.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-app-boot.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 +122 -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 +9 -0
- data/lib/kamal/commander/specifics.rb +62 -0
- data/lib/kamal/commander.rb +167 -0
- data/lib/kamal/commands/accessory/proxy.rb +16 -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/error_pages.rb +9 -0
- data/lib/kamal/commands/app/execution.rb +32 -0
- data/lib/kamal/commands/app/images.rb +13 -0
- data/lib/kamal/commands/app/logging.rb +28 -0
- data/lib/kamal/commands/app/proxy.rb +32 -0
- data/lib/kamal/commands/app.rb +124 -0
- data/lib/kamal/commands/auditor.rb +39 -0
- data/lib/kamal/commands/base.rb +134 -0
- data/lib/kamal/commands/builder/base.rb +124 -0
- data/lib/kamal/commands/builder/clone.rb +31 -0
- data/lib/kamal/commands/builder/cloud.rb +22 -0
- data/lib/kamal/commands/builder/hybrid.rb +21 -0
- data/lib/kamal/commands/builder/local.rb +14 -0
- data/lib/kamal/commands/builder/pack.rb +46 -0
- data/lib/kamal/commands/builder/remote.rb +63 -0
- data/lib/kamal/commands/builder.rb +48 -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 +127 -0
- data/lib/kamal/commands/prune.rb +38 -0
- data/lib/kamal/commands/registry.rb +16 -0
- data/lib/kamal/commands/server.rb +15 -0
- data/lib/kamal/commands.rb +2 -0
- data/lib/kamal/configuration/accessory.rb +241 -0
- data/lib/kamal/configuration/alias.rb +15 -0
- data/lib/kamal/configuration/boot.rb +25 -0
- data/lib/kamal/configuration/builder.rb +211 -0
- data/lib/kamal/configuration/docs/accessory.yml +128 -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 +132 -0
- data/lib/kamal/configuration/docs/configuration.yml +184 -0
- data/lib/kamal/configuration/docs/env.yml +116 -0
- data/lib/kamal/configuration/docs/logging.yml +21 -0
- data/lib/kamal/configuration/docs/proxy.yml +164 -0
- data/lib/kamal/configuration/docs/registry.yml +56 -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 +38 -0
- data/lib/kamal/configuration/logging.rb +33 -0
- data/lib/kamal/configuration/proxy/boot.rb +129 -0
- data/lib/kamal/configuration/proxy.rb +124 -0
- data/lib/kamal/configuration/registry.rb +32 -0
- data/lib/kamal/configuration/role.rb +222 -0
- data/lib/kamal/configuration/servers.rb +25 -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 +13 -0
- data/lib/kamal/configuration/validator/alias.rb +15 -0
- data/lib/kamal/configuration/validator/builder.rb +15 -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 +25 -0
- data/lib/kamal/configuration/validator/registry.rb +25 -0
- data/lib/kamal/configuration/validator/role.rb +13 -0
- data/lib/kamal/configuration/validator/servers.rb +7 -0
- data/lib/kamal/configuration/validator.rb +191 -0
- data/lib/kamal/configuration/volume.rb +22 -0
- data/lib/kamal/configuration.rb +372 -0
- data/lib/kamal/docker.rb +30 -0
- data/lib/kamal/env_file.rb +44 -0
- data/lib/kamal/git.rb +37 -0
- data/lib/kamal/secrets/adapters/aws_secrets_manager.rb +51 -0
- data/lib/kamal/secrets/adapters/base.rb +33 -0
- data/lib/kamal/secrets/adapters/bitwarden.rb +81 -0
- data/lib/kamal/secrets/adapters/bitwarden_secrets_manager.rb +66 -0
- data/lib/kamal/secrets/adapters/doppler.rb +57 -0
- data/lib/kamal/secrets/adapters/enpass.rb +71 -0
- data/lib/kamal/secrets/adapters/gcp_secret_manager.rb +112 -0
- data/lib/kamal/secrets/adapters/last_pass.rb +40 -0
- data/lib/kamal/secrets/adapters/one_password.rb +104 -0
- data/lib/kamal/secrets/adapters/passbolt.rb +130 -0
- data/lib/kamal/secrets/adapters/test.rb +14 -0
- data/lib/kamal/secrets/adapters.rb +16 -0
- data/lib/kamal/secrets/dotenv/inline_command_substitution.rb +33 -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 +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
|