dash 2.12.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +87 -0
- data/lib/kamal/cli/proxy/reboot.rb +173 -0
- data/lib/kamal/cli/proxy.rb +201 -34
- 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 +111 -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 +81 -6
- 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
|
@@ -9,17 +9,18 @@ class Kamal::Commands::Loadbalancer < Kamal::Commands::Base
|
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
def run
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
12
|
+
docker \
|
|
13
|
+
:run,
|
|
14
|
+
"--name", container_name,
|
|
15
|
+
"--network", "kamal",
|
|
16
|
+
"--detach",
|
|
17
|
+
"--restart", "unless-stopped",
|
|
18
|
+
"--label", label,
|
|
19
|
+
"--label", "#{Kamal::Commands::Proxy::CONFIG_DIGEST_LABEL}=#{loadbalancer_config.run_config_digest}",
|
|
20
|
+
*config_volume,
|
|
21
|
+
*run_args,
|
|
22
|
+
*loadbalancer_config.run.image,
|
|
23
|
+
*loadbalancer_config.run.run_command
|
|
23
24
|
end
|
|
24
25
|
|
|
25
26
|
def start
|
|
@@ -35,16 +36,35 @@ class Kamal::Commands::Loadbalancer < Kamal::Commands::Base
|
|
|
35
36
|
end
|
|
36
37
|
|
|
37
38
|
def deploy(targets: [])
|
|
38
|
-
|
|
39
|
+
docker :exec, container_name, "kamal-proxy", "deploy", loadbalancer_config.config.service,
|
|
40
|
+
*loadbalancer_config.deploy_command_args(targets: targets)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def domains(subcommand)
|
|
44
|
+
docker :exec, container_name, "kamal-proxy", "domains", subcommand
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def list(json: false)
|
|
48
|
+
docker :exec, container_name, "kamal-proxy", :list, *("--json" if json)
|
|
49
|
+
end
|
|
39
50
|
|
|
40
|
-
|
|
51
|
+
# Cache policy is edge-only under load balancing (see the layering contract),
|
|
52
|
+
# so the cache admin surface lives here - registered under the bare service
|
|
53
|
+
# name, unlike the per-role services on the proxy hosts.
|
|
54
|
+
def cache_stats(count: false, json: false)
|
|
55
|
+
docker :exec, container_name, "kamal-proxy", :cache, :stats, *optionize({ count: count || nil, json: json || nil }.compact)
|
|
56
|
+
end
|
|
41
57
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
58
|
+
def cache_purge(service, path_prefix: nil)
|
|
59
|
+
docker :exec, container_name, "kamal-proxy", :cache, :purge, service, *optionize({ "path-prefix": path_prefix }.compact)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def config_digest
|
|
63
|
+
docker :inspect, container_name, "--format", "'{{ index .Config.Labels \"#{Kamal::Commands::Proxy::CONFIG_DIGEST_LABEL}\" }}'"
|
|
64
|
+
end
|
|
46
65
|
|
|
47
|
-
|
|
66
|
+
def container_id
|
|
67
|
+
container_id_for(container_name: container_name)
|
|
48
68
|
end
|
|
49
69
|
|
|
50
70
|
def info
|
|
@@ -70,22 +90,49 @@ class Kamal::Commands::Loadbalancer < Kamal::Commands::Base
|
|
|
70
90
|
).join(" "), host: host
|
|
71
91
|
end
|
|
72
92
|
|
|
93
|
+
# Prune by the label the container was actually created with - on a shared
|
|
94
|
+
# proxy host that is the kamal-proxy title, and pruning by the loadbalancer
|
|
95
|
+
# title would leave the container behind for `run` to collide with.
|
|
73
96
|
def remove_container
|
|
74
|
-
docker :container, :prune, "--force", "--filter", "label
|
|
97
|
+
docker :container, :prune, "--force", "--filter", "label=#{label}"
|
|
75
98
|
end
|
|
76
99
|
|
|
100
|
+
# Image label filters match labels baked into the image, and the load balancer
|
|
101
|
+
# runs the kamal-proxy image whichever host it sits on.
|
|
77
102
|
def remove_image
|
|
78
|
-
docker :image, :prune, "--all", "--force", "--filter", "label=org.opencontainers.image.title=kamal-
|
|
103
|
+
docker :image, :prune, "--all", "--force", "--filter", "label=org.opencontainers.image.title=kamal-proxy"
|
|
79
104
|
end
|
|
80
105
|
|
|
81
106
|
def ensure_directory
|
|
82
107
|
make_directory loadbalancer_config.directory
|
|
83
108
|
end
|
|
84
109
|
|
|
110
|
+
# Where the proxy secrets env file lands (see Proxy::Run#secrets_path) -
|
|
111
|
+
# the same .kamal/proxy directory the per-app proxy hosts use.
|
|
112
|
+
def ensure_proxy_directory
|
|
113
|
+
make_directory loadbalancer_config.run.host_directory
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def remove_proxy_secrets_file
|
|
117
|
+
remove_file loadbalancer_config.run.secrets_path
|
|
118
|
+
end
|
|
119
|
+
|
|
85
120
|
def ensure_apps_config_directory
|
|
86
121
|
make_directory config.proxy_boot.apps_directory
|
|
87
122
|
end
|
|
88
123
|
|
|
124
|
+
def ensure_services_directory
|
|
125
|
+
make_directory loadbalancer_config.services_directory
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def read_service_owner
|
|
129
|
+
read_file loadbalancer_config.service_owner_file
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def read_run_config_record
|
|
133
|
+
read_file loadbalancer_config.run_config_file
|
|
134
|
+
end
|
|
135
|
+
|
|
89
136
|
def remove_directory
|
|
90
137
|
super(loadbalancer_config.directory)
|
|
91
138
|
end
|
|
@@ -95,11 +142,8 @@ class Kamal::Commands::Loadbalancer < Kamal::Commands::Base
|
|
|
95
142
|
end
|
|
96
143
|
|
|
97
144
|
private
|
|
98
|
-
def
|
|
99
|
-
|
|
100
|
-
loadbalancer_config.config.proxy_boot.image_default,
|
|
101
|
-
Kamal::Configuration::Proxy::Run::MINIMUM_VERSION
|
|
102
|
-
].join(":")
|
|
145
|
+
def run_args
|
|
146
|
+
loadbalancer_config.run_args
|
|
103
147
|
end
|
|
104
148
|
|
|
105
149
|
def on_proxy_host?
|
|
@@ -114,17 +158,15 @@ class Kamal::Commands::Loadbalancer < Kamal::Commands::Base
|
|
|
114
158
|
end
|
|
115
159
|
end
|
|
116
160
|
|
|
117
|
-
|
|
161
|
+
# kamal-proxy keeps its state under /home/kamal-proxy/.config/kamal-proxy
|
|
162
|
+
# whichever host it runs on - only the volume name differs, so a dedicated
|
|
163
|
+
# load balancer and a shared proxy host never fight over the same volume.
|
|
164
|
+
# (The apps-config mount comes with run_args, via the proxy's run surface.)
|
|
165
|
+
def config_volume
|
|
118
166
|
if on_proxy_host?
|
|
119
|
-
|
|
120
|
-
[
|
|
121
|
-
"--volume", "kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy",
|
|
122
|
-
"--volume", "$PWD/#{config.proxy_boot.apps_directory}:/home/kamal-proxy/.apps-config"
|
|
123
|
-
]
|
|
167
|
+
[ "--volume", "kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy" ]
|
|
124
168
|
else
|
|
125
|
-
[
|
|
126
|
-
"--volume", "kamal-loadbalancer-config:/home/kamal-loadbalancer/.config/kamal-loadbalancer"
|
|
127
|
-
]
|
|
169
|
+
[ "--volume", "kamal-loadbalancer-config:/home/kamal-proxy/.config/kamal-proxy" ]
|
|
128
170
|
end
|
|
129
171
|
end
|
|
130
172
|
end
|
data/lib/kamal/commands/proxy.rb
CHANGED
|
@@ -2,25 +2,28 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base
|
|
|
2
2
|
delegate :argumentize, :optionize, to: Kamal::Utils
|
|
3
3
|
attr_reader :proxy_run_config
|
|
4
4
|
|
|
5
|
+
CONFIG_DIGEST_LABEL = "org.kamal.proxy-config-digest"
|
|
6
|
+
|
|
5
7
|
def initialize(config, host:)
|
|
6
8
|
super(config)
|
|
7
9
|
@proxy_run_config = config.proxy_run(host)
|
|
8
10
|
end
|
|
9
11
|
|
|
10
|
-
def run
|
|
12
|
+
def run(digest: nil, name: nil)
|
|
11
13
|
if proxy_run_config
|
|
12
14
|
docker \
|
|
13
15
|
:run,
|
|
14
|
-
"--name", container_name,
|
|
15
|
-
|
|
16
|
+
"--name", name || container_name,
|
|
17
|
+
*proxy_run_config.network_args,
|
|
16
18
|
"--detach",
|
|
17
19
|
"--restart", "unless-stopped",
|
|
18
20
|
"--volume", "kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy",
|
|
21
|
+
*config_digest_label_args(digest),
|
|
19
22
|
*proxy_run_config.docker_options_args,
|
|
20
23
|
*proxy_run_config.image,
|
|
21
24
|
*proxy_run_config.run_command
|
|
22
25
|
else
|
|
23
|
-
pipe boot_config, xargs(docker_run)
|
|
26
|
+
pipe boot_config, xargs(docker_run(digest: digest))
|
|
24
27
|
end
|
|
25
28
|
end
|
|
26
29
|
|
|
@@ -28,12 +31,12 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base
|
|
|
28
31
|
docker :container, :start, container_name
|
|
29
32
|
end
|
|
30
33
|
|
|
31
|
-
def stop(name: container_name)
|
|
32
|
-
docker :container, :stop, name
|
|
34
|
+
def stop(name: container_name, timeout: nil)
|
|
35
|
+
docker :container, :stop, *("--time #{timeout}" if timeout), name
|
|
33
36
|
end
|
|
34
37
|
|
|
35
|
-
def start_or_run
|
|
36
|
-
combine start, run, by: "||"
|
|
38
|
+
def start_or_run(digest: nil)
|
|
39
|
+
combine start, run(digest: digest), by: "||"
|
|
37
40
|
end
|
|
38
41
|
|
|
39
42
|
def info
|
|
@@ -46,6 +49,92 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base
|
|
|
46
49
|
[ :awk, "-F:", "'{print \$NF}'" ]
|
|
47
50
|
end
|
|
48
51
|
|
|
52
|
+
def config_digest
|
|
53
|
+
docker :inspect, container_name, "--format", "'{{ index .Config.Labels \"#{CONFIG_DIGEST_LABEL}\" }}'"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def container_id(only_running: false)
|
|
57
|
+
container_id_for(container_name: container_name, only_running: only_running)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def pull
|
|
61
|
+
if proxy_run_config
|
|
62
|
+
docker :pull, proxy_run_config.image
|
|
63
|
+
else
|
|
64
|
+
docker :pull, "#{substitute(read_image)}:#{substitute(read_image_version)}"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def list(name: container_name, json: false)
|
|
69
|
+
docker :exec, name, "kamal-proxy", :list, *("--json" if json)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def cache_stats(count: false, json: false)
|
|
73
|
+
docker :exec, container_name, "kamal-proxy", :cache, :stats, *optionize({ count: count || nil, json: json || nil }.compact)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def cache_purge(service, path_prefix: nil)
|
|
77
|
+
docker :exec, container_name, "kamal-proxy", :cache, :purge, service, *optionize({ "path-prefix": path_prefix }.compact)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# One mount destination per line - what the running container was actually
|
|
81
|
+
# booted with, as opposed to what the current configuration would mount.
|
|
82
|
+
def mount_destinations
|
|
83
|
+
docker :inspect, container_name, "--format", "'{{range .Mounts}}{{println .Destination}}{{end}}'"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Zero-downtime handoff commands (proxy/run port_holder mode)
|
|
87
|
+
|
|
88
|
+
def port_holder?
|
|
89
|
+
proxy_run_config&.port_holder? || false
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def next_container_name
|
|
93
|
+
"#{container_name}-next"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def run_holder
|
|
97
|
+
docker \
|
|
98
|
+
:run,
|
|
99
|
+
"--name", proxy_run_config.holder_container_name,
|
|
100
|
+
"--network", "kamal",
|
|
101
|
+
"--detach",
|
|
102
|
+
"--restart", "unless-stopped",
|
|
103
|
+
*proxy_run_config.holder_docker_args,
|
|
104
|
+
*proxy_run_config.image,
|
|
105
|
+
"kamal-proxy", "hold"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def start_holder_or_run
|
|
109
|
+
combine docker(:container, :start, proxy_run_config.holder_container_name), run_holder, by: "||"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def holder_container_id
|
|
113
|
+
container_id_for(container_name: proxy_run_config.holder_container_name, only_running: true)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Cancel the restart policy before draining: drain makes the proxy exit on
|
|
117
|
+
# its own, which - unlike `docker stop` - an active restart policy would undo.
|
|
118
|
+
def disable_restart
|
|
119
|
+
docker :update, "--restart=no", container_name
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def drain(timeout: nil)
|
|
123
|
+
docker :exec, container_name, "kamal-proxy", :drain, *("--drain-timeout=#{timeout}s" if timeout)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def wait_for_exit(name: container_name)
|
|
127
|
+
docker :wait, name
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def remove_stopped_container(name: container_name)
|
|
131
|
+
docker :container, :rm, name
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def promote_next_container
|
|
135
|
+
docker :container, :rename, next_container_name, container_name
|
|
136
|
+
end
|
|
137
|
+
|
|
49
138
|
def logs(timestamps: true, since: nil, lines: nil, grep: nil, grep_options: nil)
|
|
50
139
|
pipe \
|
|
51
140
|
docker(:logs, container_name, ("--since #{since}" if since), ("--tail #{lines}" if lines), ("--timestamps" if timestamps), "2>&1"),
|
|
@@ -59,6 +148,10 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base
|
|
|
59
148
|
).join(" "), host: host
|
|
60
149
|
end
|
|
61
150
|
|
|
151
|
+
def domains(subcommand)
|
|
152
|
+
docker :exec, container_name, "kamal-proxy", "domains", subcommand
|
|
153
|
+
end
|
|
154
|
+
|
|
62
155
|
def remove_container
|
|
63
156
|
docker :container, :prune, "--force", "--filter", "label=org.opencontainers.image.title=kamal-proxy"
|
|
64
157
|
end
|
|
@@ -88,6 +181,12 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base
|
|
|
88
181
|
make_directory config.proxy_boot.apps_directory
|
|
89
182
|
end
|
|
90
183
|
|
|
184
|
+
# Static path rather than proxy_run_config.secrets_path: the file must be
|
|
185
|
+
# removable precisely when the run config (or its secrets) is gone.
|
|
186
|
+
def remove_proxy_secrets_file
|
|
187
|
+
remove_file File.join(config.proxy_boot.host_directory, Kamal::Configuration::Proxy::Run::SECRETS_FILENAME)
|
|
188
|
+
end
|
|
189
|
+
|
|
91
190
|
def boot_config
|
|
92
191
|
[ :echo, "#{substitute(read_boot_options)} #{substitute(read_image)}:#{substitute(read_image_version)} #{substitute(read_run_command)}" ]
|
|
93
192
|
end
|
|
@@ -133,11 +232,11 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base
|
|
|
133
232
|
config.proxy_boot.container_name
|
|
134
233
|
end
|
|
135
234
|
|
|
136
|
-
def
|
|
137
|
-
|
|
235
|
+
def config_digest_label_args(digest)
|
|
236
|
+
[ "--label", "#{CONFIG_DIGEST_LABEL}=#{digest}" ] if digest
|
|
138
237
|
end
|
|
139
238
|
|
|
140
|
-
def docker_run
|
|
239
|
+
def docker_run(digest: nil)
|
|
141
240
|
docker \
|
|
142
241
|
:run,
|
|
143
242
|
"--name", container_name,
|
|
@@ -145,6 +244,7 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base
|
|
|
145
244
|
"--detach",
|
|
146
245
|
"--restart", "unless-stopped",
|
|
147
246
|
"--volume", "kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy",
|
|
247
|
+
*config_digest_label_args(digest),
|
|
148
248
|
*config.proxy_boot.apps_volume.docker_args
|
|
149
249
|
end
|
|
150
250
|
end
|
data/lib/kamal/commands/prune.rb
CHANGED
|
@@ -13,9 +13,15 @@ class Kamal::Commands::Prune < Kamal::Commands::Base
|
|
|
13
13
|
"while read image tag; do docker rmi $tag; done"
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
# Scoped to one role so a busy sibling role cannot push another role's newest
|
|
17
|
+
# container past the retain window. That matters beyond disk hygiene: a
|
|
18
|
+
# container kamal-proxy has put to sleep is `exited`, so it is a removal
|
|
19
|
+
# candidate, and once it is gone every wake 404s. With `retain >= 1` a role's
|
|
20
|
+
# newest container always survives, and the slept one is always the newest —
|
|
21
|
+
# sleeping happens to the current release.
|
|
22
|
+
def app_containers(retain:, role:)
|
|
17
23
|
pipe \
|
|
18
|
-
docker(:ps, "-q", "-a", *service_filter, *stopped_containers_filters),
|
|
24
|
+
docker(:ps, "-q", "-a", *service_filter, *destination_filter, *role_filter(role), *stopped_containers_filters),
|
|
19
25
|
"tail -n +#{retain + 1}",
|
|
20
26
|
"while read container_id; do docker rm $container_id; done"
|
|
21
27
|
end
|
|
@@ -35,4 +41,12 @@ class Kamal::Commands::Prune < Kamal::Commands::Base
|
|
|
35
41
|
def service_filter
|
|
36
42
|
[ "--filter", "label=service=#{config.service}" ]
|
|
37
43
|
end
|
|
44
|
+
|
|
45
|
+
def destination_filter
|
|
46
|
+
[ "--filter", "label=destination=#{config.destination}" ]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def role_filter(role)
|
|
50
|
+
[ "--filter", "label=role=#{role}" ]
|
|
51
|
+
end
|
|
38
52
|
end
|
|
@@ -12,4 +12,9 @@ class Kamal::Commands::Server < Kamal::Commands::Base
|
|
|
12
12
|
[ :ls, config.apps_directory ],
|
|
13
13
|
[ :wc, "-l" ]
|
|
14
14
|
end
|
|
15
|
+
|
|
16
|
+
# Lists TCP listeners on the given port, one line per listener, no header.
|
|
17
|
+
def listeners_on(port)
|
|
18
|
+
[ :ss, "-ltnH", :sport, "=", ":#{port}" ]
|
|
19
|
+
end
|
|
15
20
|
end
|
|
@@ -127,12 +127,16 @@ class Kamal::Configuration::Accessory
|
|
|
127
127
|
context: "accessories/#{name}/env"
|
|
128
128
|
end
|
|
129
129
|
|
|
130
|
+
# The load balancer fans the app's own service out to role targets only
|
|
131
|
+
# (see Kamal::Cli::Proxy#loadbalancer), so an accessory is never behind it
|
|
132
|
+
# and must keep the host/TLS it registers with kamal-proxy directly.
|
|
130
133
|
def initialize_proxy
|
|
131
134
|
Kamal::Configuration::Proxy.new \
|
|
132
135
|
config: config,
|
|
133
136
|
proxy_config: accessory_config["proxy"],
|
|
134
137
|
context: "accessories/#{name}/proxy",
|
|
135
|
-
secrets: config.secrets
|
|
138
|
+
secrets: config.secrets,
|
|
139
|
+
load_balanced: false
|
|
136
140
|
end
|
|
137
141
|
|
|
138
142
|
def initialize_registry
|
|
@@ -256,14 +260,11 @@ class Kamal::Configuration::Accessory
|
|
|
256
260
|
end
|
|
257
261
|
end
|
|
258
262
|
|
|
263
|
+
# Ask each role for its own tagged hosts instead of re-parsing raw_config.servers here:
|
|
264
|
+
# a role may be written as a bare list or as a `hosts:` mapping, and Role already reads
|
|
265
|
+
# both. A second parser with a shape assumption dropped tagged hosts silently.
|
|
259
266
|
def extract_hosts_from_config_with_tag(tag)
|
|
260
|
-
|
|
261
|
-
servers_with_roles.flat_map do |role, servers_in_role|
|
|
262
|
-
servers_in_role.filter_map do |host|
|
|
263
|
-
host.keys.first if host.is_a?(Hash) && host.values.first.include?(tag)
|
|
264
|
-
end
|
|
265
|
-
end
|
|
266
|
-
end
|
|
267
|
+
config.roles.flat_map { |role| role.hosts_with_tag(tag) }
|
|
267
268
|
end
|
|
268
269
|
|
|
269
270
|
def network
|
|
@@ -1,24 +1,36 @@
|
|
|
1
1
|
class Kamal::Configuration::Boot
|
|
2
2
|
include Kamal::Configuration::Validation
|
|
3
3
|
|
|
4
|
-
attr_reader :boot_config
|
|
4
|
+
attr_reader :boot_config
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
# Pass boot_config/context to scope this to a single role: the role's own `boot:` hash,
|
|
7
|
+
# and a context that points at the role in validation errors.
|
|
8
|
+
def initialize(config:, boot_config: nil, context: nil)
|
|
9
|
+
@boot_config = boot_config || config.raw_config.boot || {}
|
|
10
|
+
validate! @boot_config, context: context
|
|
10
11
|
end
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
# The group size to slice `hosts` by. A percentage only means something against the set
|
|
14
|
+
# it will actually be applied to, and that set is not known until run time — `--roles`
|
|
15
|
+
# and `--hosts` narrow it, and accessories are never booted at all. So callers pass the
|
|
16
|
+
# hosts they are about to group rather than Boot reaching for a host list of its own.
|
|
17
|
+
def limit_for(hosts)
|
|
13
18
|
limit = boot_config["limit"]
|
|
14
19
|
|
|
15
20
|
if limit.to_s.end_with?("%")
|
|
16
|
-
[
|
|
21
|
+
[ hosts.count * limit.to_i / 100, 1 ].max
|
|
17
22
|
else
|
|
18
23
|
limit
|
|
19
24
|
end
|
|
20
25
|
end
|
|
21
26
|
|
|
27
|
+
# Whether a limit is configured at all. Deliberately not a reader for the raw value:
|
|
28
|
+
# a percentage means nothing without the hosts it slices, so anything that needs the
|
|
29
|
+
# group size has to go through limit_for.
|
|
30
|
+
def limit?
|
|
31
|
+
boot_config["limit"].present?
|
|
32
|
+
end
|
|
33
|
+
|
|
22
34
|
def wait
|
|
23
35
|
boot_config["wait"]
|
|
24
36
|
end
|
|
@@ -26,4 +38,23 @@ class Kamal::Configuration::Boot
|
|
|
26
38
|
def parallel_roles
|
|
27
39
|
boot_config["parallel_roles"]
|
|
28
40
|
end
|
|
41
|
+
|
|
42
|
+
# SSHKit runner options expressing this pacing over `hosts`. Without a limit there is
|
|
43
|
+
# nothing to pace and `on` keeps its default parallel runner.
|
|
44
|
+
#
|
|
45
|
+
# `wait` is always explicit: SSHKit::Runner::Sequential and ::Group both fall back to a
|
|
46
|
+
# 2 second interval, where Kamal's own boot wait means "no sleep unless you asked for
|
|
47
|
+
# one". A limit of 1 goes through Sequential rather than Group, which runs one host per
|
|
48
|
+
# slice without spawning a thread to do it. Neither trails a wait past the last group —
|
|
49
|
+
# Sequential pops its final host, and Kamal patches Group to match (sshkit_with_ext.rb).
|
|
50
|
+
def runner_options_for(hosts)
|
|
51
|
+
limit = limit_for(hosts)
|
|
52
|
+
return {} unless limit
|
|
53
|
+
|
|
54
|
+
if limit == 1
|
|
55
|
+
{ in: :sequence, wait: wait.to_i }
|
|
56
|
+
else
|
|
57
|
+
{ in: :groups, limit: limit, wait: wait.to_i }
|
|
58
|
+
end
|
|
59
|
+
end
|
|
29
60
|
end
|
|
@@ -155,6 +155,33 @@ accessories:
|
|
|
155
155
|
|
|
156
156
|
# Proxy
|
|
157
157
|
#
|
|
158
|
-
# You can run your accessory behind the Kamal proxy
|
|
158
|
+
# You can run your accessory behind the Kamal proxy for hostname-based
|
|
159
|
+
# routing and automatic SSL. The block accepts the same schema as the
|
|
160
|
+
# app-level `proxy:` (see `kamal docs proxy`), though root-only keys such as
|
|
161
|
+
# `loadbalancer` and `reboot_on_deploy` have no effect inside an accessory.
|
|
162
|
+
# Declaring a proxy here makes the accessory's hosts proxy hosts:
|
|
163
|
+
# `kamal proxy boot` (and `kamal deploy`) will run kamal-proxy on them
|
|
164
|
+
# automatically.
|
|
165
|
+
#
|
|
166
|
+
# Accessories are never load balanced. The loadbalancer fans the app's own
|
|
167
|
+
# service out to its role targets, so an accessory always registers its
|
|
168
|
+
# hostname and TLS with the kamal-proxy on its own host — whether or not the
|
|
169
|
+
# loadbalancer is active for the app. Point the accessory's DNS record at the
|
|
170
|
+
# accessory host, not at the loadbalancer.
|
|
171
|
+
#
|
|
172
|
+
# One caveat: accessory restarts are stop-then-start (no old/new container
|
|
173
|
+
# overlap), so expect a brief interruption when restarting or rebooting.
|
|
174
|
+
#
|
|
175
|
+
# For example:
|
|
176
|
+
#
|
|
177
|
+
# proxy:
|
|
178
|
+
# host: mysql-admin.example.com # Route this hostname to the accessory
|
|
179
|
+
# ssl: true # Terminate SSL with a Let's Encrypt cert
|
|
180
|
+
# app_port: 8080 # Container port the accessory listens on (default: 80)
|
|
181
|
+
# healthcheck:
|
|
182
|
+
# path: /health # Health check endpoint (default: /up)
|
|
183
|
+
#
|
|
184
|
+
# The `...` below is a placeholder: accessory proxy configuration accepts the
|
|
185
|
+
# full proxy schema and is validated against it when the accessory is loaded.
|
|
159
186
|
proxy:
|
|
160
187
|
...
|
|
@@ -3,19 +3,34 @@
|
|
|
3
3
|
# When deploying to large numbers of hosts, you might prefer not to restart your services on every host at the same time.
|
|
4
4
|
#
|
|
5
5
|
# Kamal’s default is to boot new containers on all hosts in parallel. However, you can control this with the boot configuration.
|
|
6
|
+
#
|
|
7
|
+
# This is a whole-deploy setting: `limit` slices the combined host list of every role, so it cannot
|
|
8
|
+
# hold one role back while another keeps booting. To pace a single role's hosts, set `boot` on that
|
|
9
|
+
# role instead (see kamal docs role).
|
|
6
10
|
|
|
7
11
|
boot:
|
|
8
12
|
|
|
9
|
-
# The number or percentage of hosts to boot at a time.
|
|
13
|
+
# The number or percentage of hosts to boot at a time, across all roles.
|
|
10
14
|
# This can be an integer (e.g., 3) or a percentage string (e.g., 25%).
|
|
15
|
+
#
|
|
16
|
+
# A percentage counts the hosts this run is actually booting: app hosts only — accessories
|
|
17
|
+
# are not booted here — and narrowed by `--roles` / `--hosts`. So `25%` with `--roles web`
|
|
18
|
+
# is a quarter of web's hosts, not a quarter of the file.
|
|
11
19
|
limit: 25%
|
|
12
20
|
|
|
13
21
|
# The number of seconds to wait between booting each group of hosts.
|
|
22
|
+
#
|
|
23
|
+
# This fills the gap between one group and the next, so it needs `limit` to mean anything:
|
|
24
|
+
# without one, every host boots in a single group and there is no gap to fill. Kamal warns
|
|
25
|
+
# if you set `wait` on its own. Nothing waits after the final group either — the deploy
|
|
26
|
+
# moves on as soon as the last host is up.
|
|
14
27
|
wait: 10
|
|
15
28
|
|
|
16
29
|
# Whether to boot roles in parallel on a host.
|
|
17
30
|
#
|
|
18
31
|
# If a host has multiple roles, control whether they are booted in parallel or sequentially on that host.
|
|
19
32
|
#
|
|
20
|
-
# Defaults to false.
|
|
33
|
+
# Defaults to false. A role that sets its own `boot` needs this on, since per-role pacing has no
|
|
34
|
+
# meaning when each host walks its roles in turn — Kamal turns it on for you if you have not set
|
|
35
|
+
# it, and refuses the deploy if you set it to false.
|
|
21
36
|
parallel_roles: true
|
|
@@ -136,7 +136,8 @@ allow_empty_roles: false
|
|
|
136
136
|
|
|
137
137
|
# Retain containers
|
|
138
138
|
#
|
|
139
|
-
# How many old containers and images we retain,
|
|
139
|
+
# How many old containers we retain per role, and how many images we retain,
|
|
140
|
+
# defaults to 5:
|
|
140
141
|
retain_containers: 3
|
|
141
142
|
|
|
142
143
|
# Minimum version
|