dash 2.12.0 → 3.0.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 +4 -4
- data/lib/kamal/cli/app/boot.rb +47 -11
- data/lib/kamal/cli/app/rollout_boot.rb +59 -0
- data/lib/kamal/cli/app/ssl_certificates.rb +12 -3
- data/lib/kamal/cli/app.rb +74 -7
- data/lib/kamal/cli/base.rb +41 -32
- data/lib/kamal/cli/doctor/config_checks.rb +28 -0
- data/lib/kamal/cli/doctor/endpoint_checks.rb +114 -0
- data/lib/kamal/cli/doctor/host_checks.rb +178 -0
- data/lib/kamal/cli/doctor.rb +112 -0
- data/lib/kamal/cli/healthcheck/drift_error.rb +7 -0
- data/lib/kamal/cli/healthcheck/poller.rb +60 -10
- data/lib/kamal/cli/main.rb +63 -0
- data/lib/kamal/cli/proxy/drift.rb +39 -0
- data/lib/kamal/cli/proxy/loadbalancer_claim.rb +70 -0
- data/lib/kamal/cli/proxy/loadbalancer_reboot.rb +41 -0
- data/lib/kamal/cli/proxy/reboot.rb +172 -0
- data/lib/kamal/cli/proxy.rb +200 -26
- data/lib/kamal/cli/prune.rb +7 -4
- data/lib/kamal/cli/templates/sample_hooks/post-app-stop.sample +9 -0
- data/lib/kamal/cli/templates/sample_hooks/post-proxy-deploy.sample +3 -0
- data/lib/kamal/cli/templates/sample_hooks/pre-app-stop.sample +12 -0
- data/lib/kamal/cli/templates/sample_hooks/pre-proxy-deploy.sample +3 -0
- data/lib/kamal/cli.rb +1 -0
- data/lib/kamal/commands/app/proxy.rb +12 -0
- data/lib/kamal/commands/app.rb +8 -0
- data/lib/kamal/commands/base.rb +11 -1
- data/lib/kamal/commands/docker.rb +5 -0
- data/lib/kamal/commands/loadbalancer.rb +76 -34
- data/lib/kamal/commands/proxy.rb +105 -11
- data/lib/kamal/commands/prune.rb +16 -2
- data/lib/kamal/commands/server.rb +5 -0
- data/lib/kamal/configuration/accessory.rb +9 -8
- data/lib/kamal/configuration/boot.rb +38 -7
- data/lib/kamal/configuration/docs/accessory.yml +28 -1
- data/lib/kamal/configuration/docs/boot.yml +17 -2
- data/lib/kamal/configuration/docs/configuration.yml +2 -1
- data/lib/kamal/configuration/docs/proxy.yml +844 -24
- data/lib/kamal/configuration/docs/role.yml +86 -0
- data/lib/kamal/configuration/loadbalancer.rb +70 -7
- data/lib/kamal/configuration/proxy/acme.rb +69 -0
- data/lib/kamal/configuration/proxy/run.rb +194 -5
- data/lib/kamal/configuration/proxy.rb +495 -18
- data/lib/kamal/configuration/role/healthcheck.rb +95 -0
- data/lib/kamal/configuration/role.rb +104 -1
- data/lib/kamal/configuration/validator/proxy.rb +623 -10
- data/lib/kamal/configuration/validator/role.rb +26 -0
- data/lib/kamal/configuration/validator.rb +26 -3
- data/lib/kamal/configuration.rb +197 -0
- data/lib/kamal/sshkit_with_ext.rb +27 -2
- data/lib/kamal/utils.rb +22 -2
- data/lib/kamal/version.rb +1 -1
- data/lib/kamal.rb +11 -0
- metadata +18 -2
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
class Kamal::Cli::Proxy::Reboot
|
|
2
|
+
READY_TIMEOUT = 30
|
|
3
|
+
|
|
4
|
+
attr_reader :host, :sshkit
|
|
5
|
+
delegate :execute, :capture_with_info, :info, :upload!, to: :sshkit
|
|
6
|
+
|
|
7
|
+
def initialize(host, sshkit)
|
|
8
|
+
@host = host
|
|
9
|
+
@sshkit = sshkit
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def run
|
|
13
|
+
execute *KAMAL.auditor.record("Rebooted proxy"), verbosity: :debug
|
|
14
|
+
execute *KAMAL.registry.login
|
|
15
|
+
|
|
16
|
+
pull_image
|
|
17
|
+
replace_container
|
|
18
|
+
wait_until_ready
|
|
19
|
+
verify_services re_register_services
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
def proxy
|
|
24
|
+
@proxy ||= KAMAL.proxy(host)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def drift
|
|
28
|
+
@drift ||= Kamal::Cli::Proxy::Drift.new(host, sshkit)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Pull before stopping the old container: a registry outage or rate limit
|
|
32
|
+
# can then no longer strand the host with no proxy at all.
|
|
33
|
+
def pull_image
|
|
34
|
+
info "Pulling kamal-proxy image on #{host}..."
|
|
35
|
+
execute *proxy.pull
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def replace_container
|
|
39
|
+
execute *proxy.ensure_proxy_directory
|
|
40
|
+
execute *proxy.ensure_apps_config_directory
|
|
41
|
+
|
|
42
|
+
if proxy.port_holder?
|
|
43
|
+
replace_generation
|
|
44
|
+
else
|
|
45
|
+
stop_and_replace
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Phase 1 replacement: stop the old container, start the new one. Brief gap.
|
|
50
|
+
def stop_and_replace
|
|
51
|
+
info "Stopping and removing kamal-proxy on #{host}, if running..."
|
|
52
|
+
execute *proxy.stop(timeout: KAMAL.config.drain_timeout + 10), raise_on_non_zero_exit: false
|
|
53
|
+
execute *proxy.remove_container
|
|
54
|
+
|
|
55
|
+
if (run_config = proxy.proxy_run_config)&.secrets?
|
|
56
|
+
upload! run_config.secrets_io, run_config.secrets_path, mode: "0600"
|
|
57
|
+
else
|
|
58
|
+
# A host keeps no secrets it no longer needs.
|
|
59
|
+
execute *proxy.remove_proxy_secrets_file, raise_on_non_zero_exit: false
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
execute *proxy.run(digest: drift.expected_digest)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Zero-downtime replacement: overlap a new generation on the port-holder's
|
|
66
|
+
# namespace, then drain the old one out.
|
|
67
|
+
def replace_generation
|
|
68
|
+
if holder_running?
|
|
69
|
+
if generation_running?
|
|
70
|
+
handoff_generation
|
|
71
|
+
else
|
|
72
|
+
execute *proxy.remove_container
|
|
73
|
+
execute *proxy.run(digest: drift.expected_digest)
|
|
74
|
+
end
|
|
75
|
+
else
|
|
76
|
+
migrate_to_holder
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def handoff_generation
|
|
81
|
+
info "Handing off kamal-proxy on #{host} to a new generation (zero downtime)..."
|
|
82
|
+
execute *proxy.remove_stopped_container(name: proxy.next_container_name), raise_on_non_zero_exit: false
|
|
83
|
+
execute *proxy.run(digest: drift.expected_digest, name: proxy.next_container_name)
|
|
84
|
+
wait_until_ready(name: proxy.next_container_name)
|
|
85
|
+
|
|
86
|
+
execute *proxy.drain(timeout: KAMAL.config.drain_timeout)
|
|
87
|
+
execute *proxy.wait_for_exit
|
|
88
|
+
execute *proxy.remove_stopped_container
|
|
89
|
+
execute *proxy.promote_next_container
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# The old proxy still owns the published ports, so the holder cannot bind
|
|
93
|
+
# them until it is gone. One final replacement with a brief gap.
|
|
94
|
+
def migrate_to_holder
|
|
95
|
+
info "Migrating kamal-proxy on #{host} to the port-holder architecture (brief gap)..."
|
|
96
|
+
execute *proxy.stop(timeout: KAMAL.config.drain_timeout + 10), raise_on_non_zero_exit: false
|
|
97
|
+
execute *proxy.remove_container
|
|
98
|
+
|
|
99
|
+
execute *proxy.run_holder
|
|
100
|
+
execute *proxy.run(digest: drift.expected_digest)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def holder_running?
|
|
104
|
+
capture_with_info(*proxy.holder_container_id, raise_on_non_zero_exit: false).strip.present?
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def generation_running?
|
|
108
|
+
capture_with_info(*proxy.container_id(only_running: true), raise_on_non_zero_exit: false).strip.present?
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def wait_until_ready(name: nil)
|
|
112
|
+
deadline = Time.now + READY_TIMEOUT
|
|
113
|
+
|
|
114
|
+
begin
|
|
115
|
+
capture_with_info(*proxy.list(**{ name: name }.compact), verbosity: :debug)
|
|
116
|
+
rescue SSHKit::Command::Failed
|
|
117
|
+
raise Kamal::Cli::BootError, "kamal-proxy on #{host} did not become ready within #{READY_TIMEOUT} seconds" if Time.now >= deadline
|
|
118
|
+
sleep 0.5
|
|
119
|
+
retry
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Re-register this app's services rather than relying solely on the
|
|
124
|
+
# proxy's own state restore — a proxy that came back with missing routes
|
|
125
|
+
# would otherwise 404 until the next deploy. Returns the registered
|
|
126
|
+
# service names for verify_services to check.
|
|
127
|
+
def re_register_services
|
|
128
|
+
registered_services = []
|
|
129
|
+
|
|
130
|
+
KAMAL.roles_on(host).select(&:running_proxy?).each do |role|
|
|
131
|
+
app = KAMAL.app(role: role, host: host)
|
|
132
|
+
|
|
133
|
+
version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip.presence
|
|
134
|
+
next unless version
|
|
135
|
+
|
|
136
|
+
endpoint = capture_with_info(*app.container_id_for_version(version, only_running: true), raise_on_non_zero_exit: false).strip.presence
|
|
137
|
+
next unless endpoint
|
|
138
|
+
|
|
139
|
+
info "Re-registering #{role.container_prefix} with kamal-proxy on #{host}..."
|
|
140
|
+
execute *app.deploy(target: endpoint)
|
|
141
|
+
registered_services << role.container_prefix
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
KAMAL.config.accessories.select(&:running_proxy?).each do |accessory_config|
|
|
145
|
+
next unless accessory_config.hosts.include?(host.to_s)
|
|
146
|
+
|
|
147
|
+
accessory = KAMAL.accessory(accessory_config.name)
|
|
148
|
+
target = capture_with_info(*accessory.container_id_for(container_name: accessory_config.service_name, only_running: true), raise_on_non_zero_exit: false).strip.presence
|
|
149
|
+
next unless target
|
|
150
|
+
|
|
151
|
+
info "Re-registering #{accessory_config.service_name} with kamal-proxy on #{host}..."
|
|
152
|
+
execute *accessory.deploy(target: target)
|
|
153
|
+
registered_services << accessory_config.service_name
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
registered_services
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# `list --json` returns {"services": {"<name>": ...}} - exact key
|
|
160
|
+
# membership, where a substring match over the human table would find one
|
|
161
|
+
# service's name inside another's ("app" in "other-app").
|
|
162
|
+
def verify_services(registered_services)
|
|
163
|
+
return if registered_services.empty?
|
|
164
|
+
|
|
165
|
+
listed = JSON.parse(capture_with_info(*proxy.list(json: true))).fetch("services", {}).keys
|
|
166
|
+
missing = registered_services - listed
|
|
167
|
+
|
|
168
|
+
if missing.any?
|
|
169
|
+
raise Kamal::Cli::BootError, "kamal-proxy on #{host} is missing services after reboot: #{missing.join(", ")}"
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
data/lib/kamal/cli/proxy.rb
CHANGED
|
@@ -14,24 +14,116 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
|
|
|
14
14
|
proxy_hosts = proxy_hosts - [ KAMAL.config.proxy.effective_loadbalancer ]
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
+
drifted_hosts = Concurrent::Array.new
|
|
18
|
+
stale_hosts = Concurrent::Array.new
|
|
19
|
+
auto_reboot = KAMAL.config.proxy.reboot_on_deploy?
|
|
20
|
+
|
|
17
21
|
on(proxy_hosts) do |host|
|
|
18
22
|
execute *KAMAL.registry.login
|
|
19
23
|
|
|
20
|
-
|
|
24
|
+
proxy = KAMAL.proxy(host)
|
|
25
|
+
drift = Kamal::Cli::Proxy::Drift.new(host, self)
|
|
26
|
+
|
|
27
|
+
if drift.drifted? && auto_reboot
|
|
28
|
+
# Leave the old proxy serving until its serial reboot slot below.
|
|
29
|
+
drifted_hosts << host.to_s
|
|
30
|
+
else
|
|
31
|
+
stale_hosts << host.to_s if drift.drifted?
|
|
32
|
+
|
|
33
|
+
version = capture_with_info(*proxy.version).strip.presence
|
|
21
34
|
|
|
22
|
-
|
|
23
|
-
|
|
35
|
+
if version && Kamal::Utils.older_version?(version, Kamal::Configuration::Proxy::Run::MINIMUM_VERSION)
|
|
36
|
+
raise "kamal-proxy version #{version} is too old, run `kamal proxy reboot` in order to update to at least #{Kamal::Configuration::Proxy::Run::MINIMUM_VERSION}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
if (run_config = proxy.proxy_run_config)&.secrets?
|
|
40
|
+
execute *proxy.ensure_proxy_directory
|
|
41
|
+
upload! run_config.secrets_io, run_config.secrets_path, mode: "0600"
|
|
42
|
+
else
|
|
43
|
+
# A host keeps no secrets it no longer needs.
|
|
44
|
+
execute *proxy.remove_proxy_secrets_file, raise_on_non_zero_exit: false
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
execute *proxy.ensure_apps_config_directory
|
|
48
|
+
execute *proxy.start_holder_or_run if proxy.port_holder?
|
|
49
|
+
execute *proxy.start_or_run(digest: drift.expected_digest)
|
|
24
50
|
end
|
|
25
|
-
|
|
26
|
-
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
if stale_hosts.any?
|
|
54
|
+
say "kamal-proxy on #{stale_hosts.sort.join(", ")} is running with a configuration that no longer matches the deploy config. " \
|
|
55
|
+
"Automatic reboot is disabled (proxy: reboot_on_deploy: false) - run `kamal proxy reboot` to apply the new configuration.", :yellow
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
drifted_hosts.sort.each do |host|
|
|
59
|
+
say "kamal-proxy configuration changed, rebooting on #{host}...", :magenta
|
|
60
|
+
run_hook "pre-proxy-reboot", hosts: host
|
|
61
|
+
on(host) do |h|
|
|
62
|
+
Kamal::Cli::Proxy::Reboot.new(h, self).run
|
|
63
|
+
end
|
|
64
|
+
run_hook "post-proxy-reboot", hosts: host
|
|
27
65
|
end
|
|
28
66
|
|
|
29
67
|
if KAMAL.config.proxy.load_balancing?
|
|
68
|
+
lb_drifted = Concurrent::Array.new
|
|
69
|
+
lb_stale = Concurrent::Array.new
|
|
70
|
+
|
|
30
71
|
on(KAMAL.config.proxy.effective_loadbalancer) do |host|
|
|
31
72
|
info "Starting loadbalancer on #{host}..."
|
|
32
73
|
execute *KAMAL.registry.login
|
|
74
|
+
|
|
75
|
+
# The load balancer terminates TLS and owns the cache, so its host
|
|
76
|
+
# needs the proxy secrets (acme credentials, cache store) just like
|
|
77
|
+
# the proxy hosts do.
|
|
78
|
+
if (lb_run = KAMAL.loadbalancer_config.run).secrets?
|
|
79
|
+
execute *KAMAL.loadbalancer.ensure_proxy_directory
|
|
80
|
+
upload! lb_run.secrets_io, lb_run.secrets_path, mode: "0600"
|
|
81
|
+
else
|
|
82
|
+
execute *KAMAL.loadbalancer.remove_proxy_secrets_file, raise_on_non_zero_exit: false
|
|
83
|
+
end
|
|
84
|
+
|
|
33
85
|
execute *KAMAL.loadbalancer.ensure_apps_config_directory
|
|
34
|
-
|
|
86
|
+
|
|
87
|
+
# TLS terminates at the load balancer, so the TLS material the app
|
|
88
|
+
# hosts get - custom certificates and the mTLS client CA - must
|
|
89
|
+
# reach this host too; the LB container reads it through the same
|
|
90
|
+
# apps-config mount the per-host proxies use.
|
|
91
|
+
KAMAL.config.roles.select(&:running_proxy?).each do |role|
|
|
92
|
+
Kamal::Cli::App::SslCertificates.new(host, role, self).run
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# The same drift detection the proxy hosts get: a loadbalancer booted
|
|
96
|
+
# with a different config digest reboots below (or warns, when
|
|
97
|
+
# automatic reboot is off) instead of serving a stale config forever.
|
|
98
|
+
container_id = capture_with_info(*KAMAL.loadbalancer.container_id, raise_on_non_zero_exit: false).strip
|
|
99
|
+
current_digest = capture_with_info(*KAMAL.loadbalancer.config_digest, raise_on_non_zero_exit: false).strip
|
|
100
|
+
|
|
101
|
+
if container_id.present? && current_digest != KAMAL.loadbalancer_config.run_config_digest
|
|
102
|
+
if auto_reboot
|
|
103
|
+
# Leave the old loadbalancer serving until its reboot below.
|
|
104
|
+
lb_drifted << host.to_s
|
|
105
|
+
else
|
|
106
|
+
lb_stale << host.to_s
|
|
107
|
+
execute *KAMAL.loadbalancer.start_or_run
|
|
108
|
+
end
|
|
109
|
+
else
|
|
110
|
+
Kamal::Cli::Proxy::LoadbalancerClaim.new(host, self).claim_run_config
|
|
111
|
+
execute *KAMAL.loadbalancer.start_or_run
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
if lb_stale.any?
|
|
116
|
+
say "The loadbalancer on #{lb_stale.sort.join(", ")} is running with a configuration that no longer matches the deploy config. " \
|
|
117
|
+
"Automatic reboot is disabled (proxy: reboot_on_deploy: false) - run `kamal proxy reboot` to apply the new configuration.", :yellow
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
lb_drifted.sort.each do |host|
|
|
121
|
+
say "Loadbalancer configuration changed, rebooting on #{host}...", :magenta
|
|
122
|
+
run_hook "pre-loadbalancer-reboot", hosts: host
|
|
123
|
+
on(host) do |h|
|
|
124
|
+
Kamal::Cli::Proxy::LoadbalancerReboot.new(h, self).run
|
|
125
|
+
end
|
|
126
|
+
run_hook "post-loadbalancer-reboot", hosts: host
|
|
35
127
|
end
|
|
36
128
|
end
|
|
37
129
|
end
|
|
@@ -137,16 +229,8 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
|
|
|
137
229
|
host_list = Array(hosts).join(",")
|
|
138
230
|
run_hook "pre-proxy-reboot", hosts: host_list
|
|
139
231
|
on(hosts) do |host|
|
|
140
|
-
proxy
|
|
141
|
-
|
|
142
|
-
execute *KAMAL.registry.login
|
|
143
|
-
|
|
144
|
-
info "Stopping and removing kamal-proxy on #{host}, if running..."
|
|
145
|
-
execute *proxy.stop, raise_on_non_zero_exit: false
|
|
146
|
-
execute *proxy.remove_container
|
|
147
|
-
execute *proxy.ensure_apps_config_directory
|
|
148
|
-
|
|
149
|
-
execute *proxy.run
|
|
232
|
+
info "Rebooting kamal-proxy on #{host}..."
|
|
233
|
+
Kamal::Cli::Proxy::Reboot.new(host, self).run
|
|
150
234
|
end
|
|
151
235
|
run_hook "post-proxy-reboot", hosts: host_list
|
|
152
236
|
end
|
|
@@ -156,15 +240,7 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
|
|
|
156
240
|
run_hook "pre-loadbalancer-reboot", hosts: lb_host
|
|
157
241
|
|
|
158
242
|
on(lb_host) do |host|
|
|
159
|
-
|
|
160
|
-
execute *KAMAL.registry.login
|
|
161
|
-
|
|
162
|
-
info "Stopping and removing #{KAMAL.loadbalancer.container_name} on #{host}, if running..."
|
|
163
|
-
execute *KAMAL.loadbalancer.stop, raise_on_non_zero_exit: false
|
|
164
|
-
execute *KAMAL.loadbalancer.remove_container
|
|
165
|
-
execute *KAMAL.loadbalancer.ensure_apps_config_directory
|
|
166
|
-
|
|
167
|
-
execute *KAMAL.loadbalancer.run
|
|
243
|
+
Kamal::Cli::Proxy::LoadbalancerReboot.new(host, self).run
|
|
168
244
|
end
|
|
169
245
|
|
|
170
246
|
run_hook "post-loadbalancer-reboot", hosts: lb_host
|
|
@@ -221,6 +297,13 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
|
|
|
221
297
|
execute *KAMAL.auditor.record("Started proxy"), verbosity: :debug
|
|
222
298
|
execute *KAMAL.proxy(host).start
|
|
223
299
|
end
|
|
300
|
+
|
|
301
|
+
if KAMAL.config.proxy.load_balancing?
|
|
302
|
+
on(KAMAL.config.proxy.effective_loadbalancer) do
|
|
303
|
+
execute *KAMAL.auditor.record("Started loadbalancer"), verbosity: :debug
|
|
304
|
+
execute *KAMAL.loadbalancer.start, raise_on_non_zero_exit: false
|
|
305
|
+
end
|
|
306
|
+
end
|
|
224
307
|
end
|
|
225
308
|
end
|
|
226
309
|
|
|
@@ -231,6 +314,16 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
|
|
|
231
314
|
execute *KAMAL.auditor.record("Stopped proxy"), verbosity: :debug
|
|
232
315
|
execute *KAMAL.proxy(host).stop, raise_on_non_zero_exit: false
|
|
233
316
|
end
|
|
317
|
+
|
|
318
|
+
# `docker container prune` only collects stopped containers, so leaving the
|
|
319
|
+
# loadbalancer running would also leave `kamal proxy remove` unable to
|
|
320
|
+
# remove it.
|
|
321
|
+
if KAMAL.config.proxy.load_balancing?
|
|
322
|
+
on(KAMAL.config.proxy.effective_loadbalancer) do
|
|
323
|
+
execute *KAMAL.auditor.record("Stopped loadbalancer"), verbosity: :debug
|
|
324
|
+
execute *KAMAL.loadbalancer.stop, raise_on_non_zero_exit: false
|
|
325
|
+
end
|
|
326
|
+
end
|
|
234
327
|
end
|
|
235
328
|
end
|
|
236
329
|
|
|
@@ -343,6 +436,7 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
|
|
|
343
436
|
end
|
|
344
437
|
|
|
345
438
|
on(KAMAL.config.proxy.effective_loadbalancer) do |host|
|
|
439
|
+
Kamal::Cli::Proxy::LoadbalancerClaim.new(host, self).claim_service
|
|
346
440
|
info "Deploying to loadbalancer on #{host} with targets: #{targets.join(', ')}"
|
|
347
441
|
execute *KAMAL.loadbalancer.deploy(targets: targets)
|
|
348
442
|
end
|
|
@@ -354,6 +448,69 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
|
|
|
354
448
|
end
|
|
355
449
|
end
|
|
356
450
|
|
|
451
|
+
desc "cache SUBCOMMAND", "Manage the response cache (stats, purge)"
|
|
452
|
+
option :count, type: :boolean, default: false, desc: "stats: measure entries and bytes per service (walks a shared store's keyspace)"
|
|
453
|
+
option :json, type: :boolean, default: false, desc: "stats: print the raw report as JSON"
|
|
454
|
+
option :path_prefix, type: :string, default: nil, desc: "purge: drop only the entries below this path, e.g. /assets"
|
|
455
|
+
def cache(subcommand)
|
|
456
|
+
count, json, path_prefix = options[:count], options[:json], options[:path_prefix]
|
|
457
|
+
|
|
458
|
+
case subcommand
|
|
459
|
+
when "stats"
|
|
460
|
+
# The cache lives where its policy applies: at the loadbalancer when
|
|
461
|
+
# load balancing (cache is edge-only in the layering contract),
|
|
462
|
+
# otherwise on each proxy host.
|
|
463
|
+
if KAMAL.config.proxy.load_balancing?
|
|
464
|
+
on(KAMAL.config.proxy.effective_loadbalancer) do |host|
|
|
465
|
+
puts_by_host host, capture_with_info(*KAMAL.loadbalancer.cache_stats(count: count, json: json)), type: "Loadbalancer"
|
|
466
|
+
end
|
|
467
|
+
else
|
|
468
|
+
on(KAMAL.proxy_hosts) do |host|
|
|
469
|
+
puts_by_host host, capture_with_info(*KAMAL.proxy(host).cache_stats(count: count, json: json)), type: "Proxy"
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
when "purge"
|
|
473
|
+
if KAMAL.config.proxy.load_balancing?
|
|
474
|
+
# The loadbalancer registers one service under the bare service name.
|
|
475
|
+
on(KAMAL.config.proxy.effective_loadbalancer) do |host|
|
|
476
|
+
execute *KAMAL.auditor.record("Purged the response cache"), verbosity: :debug
|
|
477
|
+
puts_by_host host, capture_with_info(*KAMAL.loadbalancer.cache_purge(KAMAL.config.service, path_prefix: path_prefix)), type: "Loadbalancer"
|
|
478
|
+
end
|
|
479
|
+
else
|
|
480
|
+
# Each proxied role registered its own service, so purge walks them.
|
|
481
|
+
on(KAMAL.proxy_hosts) do |host|
|
|
482
|
+
execute *KAMAL.auditor.record("Purged the response cache"), verbosity: :debug
|
|
483
|
+
|
|
484
|
+
KAMAL.roles_on(host).select(&:running_proxy?).each do |role|
|
|
485
|
+
puts_by_host host, capture_with_info(*KAMAL.proxy(host).cache_purge(role.container_prefix, path_prefix: path_prefix)), type: "Proxy"
|
|
486
|
+
end
|
|
487
|
+
end
|
|
488
|
+
end
|
|
489
|
+
else
|
|
490
|
+
puts "Unknown cache subcommand: #{subcommand}. Available: stats, purge"
|
|
491
|
+
end
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
desc "domains SUBCOMMAND", "Manage dynamic TLS domains (refresh, list, stats)"
|
|
495
|
+
def domains(subcommand)
|
|
496
|
+
case subcommand
|
|
497
|
+
when "refresh", "list", "stats"
|
|
498
|
+
# TLS terminates at the load balancer when load balancing, so the
|
|
499
|
+
# dynamic domain set lives there rather than on the per-host proxies.
|
|
500
|
+
if KAMAL.config.proxy.load_balancing?
|
|
501
|
+
on(KAMAL.config.proxy.effective_loadbalancer) do |host|
|
|
502
|
+
puts_by_host host, capture_with_info(*KAMAL.loadbalancer.domains(subcommand)), type: "Loadbalancer"
|
|
503
|
+
end
|
|
504
|
+
else
|
|
505
|
+
on(KAMAL.proxy_hosts) do |host|
|
|
506
|
+
puts_by_host host, capture_with_info(*KAMAL.proxy(host).domains(subcommand)), type: "Proxy"
|
|
507
|
+
end
|
|
508
|
+
end
|
|
509
|
+
else
|
|
510
|
+
puts "Unknown domains subcommand: #{subcommand}. Available: refresh, list, stats"
|
|
511
|
+
end
|
|
512
|
+
end
|
|
513
|
+
|
|
357
514
|
desc "remove_container", "Remove proxy container from servers", hide: true
|
|
358
515
|
def remove_container
|
|
359
516
|
modify(lock: true) do
|
|
@@ -394,12 +551,29 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
|
|
|
394
551
|
on(KAMAL.proxy_hosts) do
|
|
395
552
|
execute *KAMAL.proxy(host).remove_proxy_directory, raise_on_non_zero_exit: false
|
|
396
553
|
end
|
|
554
|
+
|
|
555
|
+
# Otherwise the ownership registry outlives the load balancer and a later
|
|
556
|
+
# boot would fail against an owner that no longer exists.
|
|
557
|
+
if KAMAL.config.proxy.load_balancing?
|
|
558
|
+
on(KAMAL.config.proxy.effective_loadbalancer) do
|
|
559
|
+
execute *KAMAL.loadbalancer.remove_directory, raise_on_non_zero_exit: false
|
|
560
|
+
end
|
|
561
|
+
end
|
|
397
562
|
end
|
|
398
563
|
end
|
|
399
564
|
|
|
400
565
|
private
|
|
566
|
+
# A shared load balancer tier is the exact case this guard exists for, so it
|
|
567
|
+
# has to cover the load balancer host too - `remove_container` and
|
|
568
|
+
# `remove_image` both act on it.
|
|
569
|
+
def removal_hosts
|
|
570
|
+
hosts = KAMAL.proxy_hosts.to_a
|
|
571
|
+
hosts |= [ KAMAL.config.proxy.effective_loadbalancer ] if KAMAL.config.proxy.load_balancing?
|
|
572
|
+
hosts
|
|
573
|
+
end
|
|
574
|
+
|
|
401
575
|
def removal_allowed?(force)
|
|
402
|
-
on(
|
|
576
|
+
on(removal_hosts) do |host|
|
|
403
577
|
app_count = capture_with_info(*KAMAL.server.app_directory_count).chomp.to_i
|
|
404
578
|
raise "The are other applications installed on #{host}" if app_count > 0
|
|
405
579
|
end
|
data/lib/kamal/cli/prune.rb
CHANGED
|
@@ -18,16 +18,19 @@ class Kamal::Cli::Prune < Kamal::Cli::Base
|
|
|
18
18
|
end
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
desc "containers", "Prune all stopped containers, except the last n (default 5)"
|
|
22
|
-
option :retain, type: :numeric, default: nil, desc: "Number of containers to retain"
|
|
21
|
+
desc "containers", "Prune all stopped containers, except the last n per role (default 5)"
|
|
22
|
+
option :retain, type: :numeric, default: nil, desc: "Number of containers to retain per role"
|
|
23
23
|
def containers
|
|
24
24
|
retain = options.fetch(:retain, KAMAL.config.retain_containers)
|
|
25
25
|
raise "retain must be at least 1" if retain < 1
|
|
26
26
|
|
|
27
27
|
modify(lock: true) do
|
|
28
|
-
on(KAMAL.hosts) do
|
|
28
|
+
on(KAMAL.hosts) do |host|
|
|
29
29
|
execute *KAMAL.auditor.record("Pruned containers"), verbosity: :debug
|
|
30
|
-
|
|
30
|
+
|
|
31
|
+
KAMAL.roles_on(host).each do |role|
|
|
32
|
+
execute *KAMAL.prune.app_containers(retain: retain, role: role)
|
|
33
|
+
end
|
|
31
34
|
end
|
|
32
35
|
end
|
|
33
36
|
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
|
|
3
|
+
# Runs on each host just after the previous container has been stopped.
|
|
4
|
+
#
|
|
5
|
+
# NOTE: $KAMAL_VERSION is the version that was STOPPED, not the one being deployed.
|
|
6
|
+
#
|
|
7
|
+
# A failure here is logged and the deploy continues.
|
|
8
|
+
|
|
9
|
+
echo "Stopped $KAMAL_ROLE version $KAMAL_VERSION on $KAMAL_HOSTS"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
|
|
3
|
+
# Runs on each host just before the previous container is stopped, once the new
|
|
4
|
+
# version is already live. Use it to drain the old container: unsubscribe a queue
|
|
5
|
+
# group, deregister from service discovery, wait for in-flight jobs.
|
|
6
|
+
#
|
|
7
|
+
# NOTE: $KAMAL_VERSION is the version being STOPPED, not the one being deployed.
|
|
8
|
+
#
|
|
9
|
+
# A failure here is logged and the deploy continues -- the old container is
|
|
10
|
+
# stopped either way.
|
|
11
|
+
|
|
12
|
+
echo "Draining $KAMAL_ROLE version $KAMAL_VERSION on $KAMAL_HOSTS..."
|
data/lib/kamal/cli.rb
CHANGED
|
@@ -9,6 +9,18 @@ module Kamal::Commands::App::Proxy
|
|
|
9
9
|
proxy_exec :remove, role.container_prefix
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
+
def rollout_deploy(target:)
|
|
13
|
+
proxy_exec :rollout, :deploy, role.container_prefix, *role.proxy.rollout_deploy_command_args(target: target)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def rollout_set(percent: nil, list: nil)
|
|
17
|
+
proxy_exec :rollout, :set, role.container_prefix, *role.proxy.rollout_set_command_args(percent: percent, list: list)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def rollout_stop
|
|
21
|
+
proxy_exec :rollout, :stop, role.container_prefix
|
|
22
|
+
end
|
|
23
|
+
|
|
12
24
|
def live
|
|
13
25
|
proxy_exec :resume, role.container_prefix
|
|
14
26
|
end
|
data/lib/kamal/commands/app.rb
CHANGED
|
@@ -30,6 +30,7 @@ class Kamal::Commands::App < Kamal::Commands::Base
|
|
|
30
30
|
*role.asset_volume_args,
|
|
31
31
|
*role.label_args,
|
|
32
32
|
*role.option_args,
|
|
33
|
+
*role.healthcheck_args,
|
|
33
34
|
config.absolute_image,
|
|
34
35
|
role.cmd
|
|
35
36
|
end
|
|
@@ -42,6 +43,13 @@ class Kamal::Commands::App < Kamal::Commands::Base
|
|
|
42
43
|
pipe container_id_for_version(version), xargs(docker(:inspect, "--format", DOCKER_HEALTH_STATUS_FORMAT))
|
|
43
44
|
end
|
|
44
45
|
|
|
46
|
+
# The `healthcheck: exec:` probe, run from the deploy host against the container that is
|
|
47
|
+
# booting. Single-quoted by #shell, so the command expands inside the container rather
|
|
48
|
+
# than in the deploy host's shell. Non-zero exit means not ready.
|
|
49
|
+
def health_probe(version:)
|
|
50
|
+
docker :exec, container_name(version), *shell([ role.healthcheck.exec ])
|
|
51
|
+
end
|
|
52
|
+
|
|
45
53
|
def stop(version: nil)
|
|
46
54
|
pipe \
|
|
47
55
|
version ? container_id_for_version(version) : current_running_container_id,
|
data/lib/kamal/commands/base.rb
CHANGED
|
@@ -2,7 +2,13 @@ module Kamal::Commands
|
|
|
2
2
|
class Base
|
|
3
3
|
delegate :sensitive, :argumentize, to: Kamal::Utils
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
# Prefixed onto the docker state when the container declares no healthcheck at all, so
|
|
6
|
+
# "nothing is probing this container" stays distinguishable from "the probe passed" —
|
|
7
|
+
# both used to reach the poller as `running`. The state is kept after the prefix so a
|
|
8
|
+
# healthcheck-less container that died still reports why.
|
|
9
|
+
NO_HEALTHCHECK = "no-healthcheck"
|
|
10
|
+
|
|
11
|
+
DOCKER_HEALTH_STATUS_FORMAT = "'{{if .State.Health}}{{.State.Health.Status}}{{else}}#{NO_HEALTHCHECK}:{{.State.Status}}{{end}}'"
|
|
6
12
|
|
|
7
13
|
attr_accessor :config
|
|
8
14
|
|
|
@@ -34,6 +40,10 @@ module Kamal::Commands
|
|
|
34
40
|
[ :rm, path ]
|
|
35
41
|
end
|
|
36
42
|
|
|
43
|
+
def read_file(file, default: nil)
|
|
44
|
+
combine [ :cat, file, "2>", "/dev/null" ], [ :echo, "\"#{default}\"" ], by: "||"
|
|
45
|
+
end
|
|
46
|
+
|
|
37
47
|
def ensure_docker_installed
|
|
38
48
|
combine \
|
|
39
49
|
ensure_local_docker_installed,
|
|
@@ -35,6 +35,11 @@ class Kamal::Commands::Docker < Kamal::Commands::Base
|
|
|
35
35
|
[ "kill -HUP $PPID" ]
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
+
# Checks that an image's manifest can be fetched from its registry. Fails if it's missing or we're unauthorized.
|
|
39
|
+
def manifest_available?(image)
|
|
40
|
+
docker :manifest, :inspect, image
|
|
41
|
+
end
|
|
42
|
+
|
|
38
43
|
def create_network
|
|
39
44
|
docker :network, :create, :kamal
|
|
40
45
|
end
|