Class: Dash::Commands::Proxy
Defined Under Namespace
Modules: CertTransfer
Classes: State
Constant Summary
collapse
- CONFIG_DIGEST_LABEL =
"org.dash.proxy-config-digest"
- LEGACY_CONFIG_DIGEST_LABEL =
Containers booted before the stage-3b rename carry the old key. New ones are
labelled with CONFIG_DIGEST_LABEL only, but reads fall back to the legacy key
so upgrading doesn't read as config drift and reboot every proxy for nothing.
Both the legacy constant and the fallback go away in stage 3d.
"org.kamal.proxy-config-digest"
- CONFIG_DIGEST_TEMPLATE =
"{{ with index .Config.Labels \"#{CONFIG_DIGEST_LABEL}\" }}{{ . }}" \
"{{ else }}{{ index .Config.Labels \"#{LEGACY_CONFIG_DIGEST_LABEL}\" }}{{ end }}"
- CONFIG_DIGEST_FORMAT =
"'#{CONFIG_DIGEST_TEMPLATE}'"
- STATE_FORMAT =
Everything Dash::Cli::Proxy::Drift and the minimum-version gate need, in one format.
"'{{.Id}} {{.Config.Image}} #{CONFIG_DIGEST_TEMPLATE}'"
CertTransfer::CERT_ARCHIVE_FILENAME, CertTransfer::CERT_IMPORT_STAGING_FILENAME, CertTransfer::CONTAINER_IMPORT_PATH
Constants inherited
from Base
Base::DOCKER_HEALTH_STATUS_FORMAT, Base::EXEC_PROBE_FAILED, Base::NO_HEALTHCHECK, Base::READINESS_PROGRESS_PREFIX, Base::READY_STATUSES
Instance Attribute Summary collapse
Attributes inherited from Base
#config
Instance Method Summary
collapse
-
#boot_config ⇒ Object
-
#cache_purge(service, path_prefix: nil) ⇒ Object
-
#cache_stats(count: false, json: false) ⇒ Object
-
#cleanup_traefik ⇒ Object
-
#config_digest ⇒ Object
-
#container_id(only_running: false) ⇒ Object
-
#copy_legacy_config_volume(volume: Dash::Configuration::Proxy::CONFIG_VOLUME, legacy: Dash::Configuration::Proxy::LEGACY_CONFIG_VOLUME) ⇒ Object
Copies the pre-rename config volume into the new one, before anything starts.
-
#disable_restart ⇒ Object
Cancel the restart policy before draining: drain makes the proxy exit on its own, which - unlike docker stop - an active restart policy would undo.
-
#domains(subcommand, *args) ⇒ Object
retry takes a host, or --all; the rest take no arguments.
-
#drain(timeout: nil) ⇒ Object
-
#ensure_apps_config_directory ⇒ Object
-
#ensure_proxy_directory ⇒ Object
-
#follow_logs(host:, timestamps: true, grep: nil, grep_options: nil) ⇒ Object
-
#holder_container_id ⇒ Object
-
#info ⇒ Object
-
#initialize(config, host:) ⇒ Proxy
constructor
-
#inspect_state ⇒ Object
One read for container id, image tag and config digest - parsed by Dash::Commands::Proxy::State.
-
#legacy_rename ⇒ Object
The whole stage-3c bridge as one command, skipped outright by a host that has already been through it - or was installed fresh on 4.x and never had a kamal-proxy.
-
#list(name: container_name, json: false) ⇒ Object
-
#loadbalancer ⇒ Object
-
#logs(timestamps: true, since: nil, lines: nil, grep: nil, grep_options: nil) ⇒ Object
-
#mount_destinations ⇒ Object
One mount destination per line - what the running container was actually booted with, as opposed to what the current configuration would mount.
-
#next_container_name ⇒ Object
-
#port_holder? ⇒ Boolean
Zero-downtime handoff commands (proxy/run port_holder mode).
-
#prepare_boot ⇒ Object
Everything a proxy host needs before anything reads its container, volume or network, in the one round trip it already pays for the apps-config directory.
-
#promote_next_container ⇒ Object
-
#pull ⇒ Object
-
#read_boot_options ⇒ Object
-
#read_image ⇒ Object
-
#read_image_version ⇒ Object
-
#read_run_command ⇒ Object
-
#remove_container ⇒ Object
Docker ANDs multiple --filter label= values, so matching both the current and the pre-rename image title takes two commands rather than one filter with two values.
-
#remove_image ⇒ Object
-
#remove_legacy_container(timeout: nil) ⇒ Object
Stops and removes a pre-rename proxy container so the renamed one can claim ports 80/443.
-
#remove_legacy_holder_container ⇒ Object
-
#remove_proxy_directory ⇒ Object
-
#remove_proxy_secrets_file ⇒ Object
Static path rather than proxy_run_config.secrets_path: the file must be removable precisely when the run config (or its secrets) is gone.
-
#remove_stopped_container(name: container_name) ⇒ Object
-
#reset_boot_options ⇒ Object
-
#reset_image ⇒ Object
-
#reset_image_version ⇒ Object
-
#reset_run_command ⇒ Object
-
#run(digest: nil, name: nil) ⇒ Object
-
#run_holder ⇒ Object
-
#start ⇒ Object
-
#start_holder_or_run ⇒ Object
-
#start_or_run(digest: nil) ⇒ Object
-
#stop(name: container_name, timeout: nil) ⇒ Object
-
#version(name: container_name) ⇒ Object
name: so the doctor can also ask about the pre-rename container: during the stage-3c transition a host still runs kamal-proxy, and a check that only ever looks at dash-proxy concludes no proxy is running.
-
#wait_for_exit(name: container_name) ⇒ Object
#certs_archive_container_path, #certs_archive_host_path, #certs_import_host_path, #export_certs, #export_certs_offline, #import_certs, #remove_certs_archive, #remove_certs_import
Methods inherited from Base
#confirmed_empty?, #container_id_for, #ensure_docker_installed, #ensure_run_directory, #make_directory, #make_directory_for, #read_file, #remove_directory, #remove_file, #run_over_ssh
Constructor Details
#initialize(config, host:) ⇒ Proxy
Returns a new instance of Proxy.
23
24
25
26
|
# File 'lib/dash/commands/proxy.rb', line 23
def initialize(config, host:)
super(config)
@proxy_run_config = config.proxy_run(host)
end
|
Instance Attribute Details
#proxy_run_config ⇒ Object
Returns the value of attribute proxy_run_config.
5
6
7
|
# File 'lib/dash/commands/proxy.rb', line 5
def proxy_run_config
@proxy_run_config
end
|
Instance Method Details
#boot_config ⇒ Object
320
321
322
|
# File 'lib/dash/commands/proxy.rb', line 320
def boot_config
[ :echo, "#{substitute(read_boot_options)} #{substitute(read_image)}:#{substitute(read_image_version)} #{substitute(read_run_command)}" ]
end
|
#cache_purge(service, path_prefix: nil) ⇒ Object
196
197
198
|
# File 'lib/dash/commands/proxy.rb', line 196
def cache_purge(service, path_prefix: nil)
docker :exec, container_name, "dash-proxy", :cache, :purge, service, *optionize({ "path-prefix": path_prefix }.compact)
end
|
#cache_stats(count: false, json: false) ⇒ Object
192
193
194
|
# File 'lib/dash/commands/proxy.rb', line 192
def cache_stats(count: false, json: false)
docker :exec, container_name, "dash-proxy", :cache, :stats, *optionize({ count: count || nil, json: json || nil }.compact)
end
|
#cleanup_traefik ⇒ Object
293
294
295
296
297
298
299
300
|
# File 'lib/dash/commands/proxy.rb', line 293
def cleanup_traefik
chain \
docker(:container, :stop, "traefik"),
combine(
docker(:container, :prune, "--force", "--filter", "label=org.opencontainers.image.title=Traefik"),
docker(:image, :prune, "--all", "--force", "--filter", "label=org.opencontainers.image.title=Traefik")
)
end
|
#config_digest ⇒ Object
165
166
167
|
# File 'lib/dash/commands/proxy.rb', line 165
def config_digest
docker :inspect, container_name, "--format", CONFIG_DIGEST_FORMAT
end
|
#container_id(only_running: false) ⇒ Object
176
177
178
|
# File 'lib/dash/commands/proxy.rb', line 176
def container_id(only_running: false)
container_id_for(container_name: container_name, only_running: only_running)
end
|
#copy_legacy_config_volume(volume: Dash::Configuration::Proxy::CONFIG_VOLUME, legacy: Dash::Configuration::Proxy::LEGACY_CONFIG_VOLUME) ⇒ Object
Copies the pre-rename config volume into the new one, before anything
starts. The volume holds the routing table and the ACME account and
certificate cache; losing it means re-issuing every certificate and
spending Let's Encrypt rate limits to get back where we were.
Runs in the dash-proxy image itself — already pulled by this point in the
boot sequence, and its ubuntu base has sh and cp. --user root because the
image's own user cannot write the destination volume; cp -a preserves the
uid, which the rename leaves at 1001.
Shape: exists || ! legacy_exists || ( create && copy ). The chain has to
start with a real program: SSHKit prefixes the first word with
/usr/bin/env, and env ! is "No such file or directory" (exit 127), not
shell negation. 4.0.0 led with ! docker volume inspect …, so the whole
chain failed silently into || true, the proxy booted onto a volume docker
created empty, and every host lost its routing table and ACME cache on the
first deploy. The subshell groups create-and-copy because && and ||
share precedence and associate left — without it a host that already has
the new volume would still run the copy over live state.
The source volume is still mounted by the legacy container while this runs -
deliberately, on every path (see Dash::Cli::Proxy::LegacyRename's step order), and
safe because every writer into it renames into place: the routing table through
writeFileAtomic, the dynamic domain and redirect state through their own temp +
rename, the response cache through CreateTemp + Rename, and the ACME cache through
autocert.DirCache. cp -a reads a complete file either way. Skew across files is
possible and harmless - an unused certificate, or a route whose certificate reissues -
and --recheck-targets-on-restore re-verifies the targets on the way back up
(zoolutions/dash#169 review).
#disable_restart ⇒ Object
Cancel the restart policy before draining: drain makes the proxy exit on
its own, which - unlike docker stop - an active restart policy would undo.
238
239
240
|
# File 'lib/dash/commands/proxy.rb', line 238
def disable_restart
docker :update, "--restart=no", container_name
end
|
#domains(subcommand, *args) ⇒ Object
retry takes a host, or --all; the rest take no arguments.
272
273
274
|
# File 'lib/dash/commands/proxy.rb', line 272
def domains(subcommand, *args)
docker :exec, container_name, "dash-proxy", "domains", subcommand, *args
end
|
#drain(timeout: nil) ⇒ Object
242
243
244
|
# File 'lib/dash/commands/proxy.rb', line 242
def drain(timeout: nil)
docker :exec, container_name, "dash-proxy", :drain, *("--drain-timeout=#{timeout}s" if timeout)
end
|
#ensure_apps_config_directory ⇒ Object
310
311
312
|
# File 'lib/dash/commands/proxy.rb', line 310
def ensure_apps_config_directory
make_directory config.proxy_boot.apps_directory
end
|
#ensure_proxy_directory ⇒ Object
302
303
304
|
# File 'lib/dash/commands/proxy.rb', line 302
def ensure_proxy_directory
make_directory config.proxy_boot.host_directory
end
|
#follow_logs(host:, timestamps: true, grep: nil, grep_options: nil) ⇒ Object
264
265
266
267
268
269
|
# File 'lib/dash/commands/proxy.rb', line 264
def follow_logs(host:, timestamps: true, grep: nil, grep_options: nil)
run_over_ssh pipe(
docker(:logs, container_name, ("--timestamps" if timestamps), "--tail", "10", "--follow", "2>&1"),
(%(grep "#{grep}"#{" #{grep_options}" if grep_options}) if grep)
).join(" "), host: host
end
|
#holder_container_id ⇒ Object
232
233
234
|
# File 'lib/dash/commands/proxy.rb', line 232
def holder_container_id
container_id_for(container_name: proxy_run_config.holder_container_name, only_running: true)
end
|
#info ⇒ Object
152
153
154
|
# File 'lib/dash/commands/proxy.rb', line 152
def info
docker :ps, "--filter", "'name=^#{container_name}$'"
end
|
#inspect_state ⇒ Object
One read for container id, image tag and config digest - parsed by
Dash::Commands::Proxy::State. Capture it with raise_on_non_zero_exit: false;
a host with no proxy container inspects to nothing, which is an answer.
172
173
174
|
# File 'lib/dash/commands/proxy.rb', line 172
def inspect_state
docker :inspect, container_name, "--format", STATE_FORMAT
end
|
#legacy_rename ⇒ Object
The whole stage-3c bridge as one command, skipped outright by a host that has
already been through it - or was installed fresh on 4.x and never had a kamal-proxy.
The three steps keep their own bodies and their documented order (see
Dash::Cli::Proxy::LegacyRename); each is wrapped in its own subshell because they
all mix && and || at one precedence level, and composing them flat would
re-associate across the volume copy's guard - the chain 4.0.0 got wrong.
The marker is written on verified absence of both legacy containers, never on the
chain's exit status: the two removals end in || true, so a host whose stop failed
would otherwise record itself as migrated and never retry. Its own || true keeps
that failure as quiet as it is today, while a failed volume copy still exits
non-zero through the && chain and aborts the boot exactly as it does now.
73
74
75
76
77
78
79
80
81
82
83
|
# File 'lib/dash/commands/proxy.rb', line 73
def legacy_rename
any \
[ :test, "-f", legacy_rename_marker ],
group(
group(docker_commands.connect_legacy_network_containers),
group(copy_legacy_config_volume),
group(remove_legacy_container(timeout: config.drain_timeout)),
group(remove_legacy_holder_container),
group(any(mark_legacy_renamed, [ :true ]))
)
end
|
#list(name: container_name, json: false) ⇒ Object
188
189
190
|
# File 'lib/dash/commands/proxy.rb', line 188
def list(name: container_name, json: false)
docker :exec, name, "dash-proxy", :list, *("--json" if json)
end
|
#loadbalancer ⇒ Object
356
357
358
|
# File 'lib/dash/commands/proxy.rb', line 356
def loadbalancer
@loadbalancer ||= Dash::Commands::Loadbalancer.new(config, loadbalancer_config: DASH.loadbalancer_config)
end
|
#logs(timestamps: true, since: nil, lines: nil, grep: nil, grep_options: nil) ⇒ Object
258
259
260
261
262
|
# File 'lib/dash/commands/proxy.rb', line 258
def logs(timestamps: true, since: nil, lines: nil, grep: nil, grep_options: nil)
pipe \
docker(:logs, container_name, ("--since #{since}" if since), ("--tail #{lines}" if lines), ("--timestamps" if timestamps), "2>&1"),
("grep '#{grep}'#{" #{grep_options}" if grep_options}" if grep)
end
|
#mount_destinations ⇒ Object
One mount destination per line - what the running container was actually
booted with, as opposed to what the current configuration would mount.
202
203
204
|
# File 'lib/dash/commands/proxy.rb', line 202
def mount_destinations
docker :inspect, container_name, "--format", "'{{range .Mounts}}{{println .Destination}}{{end}}'"
end
|
#next_container_name ⇒ Object
212
213
214
|
# File 'lib/dash/commands/proxy.rb', line 212
def next_container_name
"#{container_name}-next"
end
|
#port_holder? ⇒ Boolean
Zero-downtime handoff commands (proxy/run port_holder mode)
208
209
210
|
# File 'lib/dash/commands/proxy.rb', line 208
def port_holder?
proxy_run_config&.port_holder? || false
end
|
#prepare_boot ⇒ Object
Everything a proxy host needs before anything reads its container, volume or
network, in the one round trip it already pays for the apps-config directory.
a || b && c is (a || b) && c, so the mkdir runs whichever way the guard went -
and the guard has to be the first word rather than a parenthesised group, because
SSHKit prefixes the first word with /usr/bin/env and passes only test through.
Stage 3d drops the legacy_rename half and leaves the mkdir.
57
58
59
|
# File 'lib/dash/commands/proxy.rb', line 57
def prepare_boot
combine legacy_rename, ensure_apps_config_directory
end
|
254
255
256
|
# File 'lib/dash/commands/proxy.rb', line 254
def promote_next_container
docker :container, :rename, next_container_name, container_name
end
|
#pull ⇒ Object
180
181
182
183
184
185
186
|
# File 'lib/dash/commands/proxy.rb', line 180
def pull
if proxy_run_config
docker :pull, proxy_run_config.image
else
docker :pull, "#{substitute(read_image)}:#{substitute(read_image_version)}"
end
end
|
#read_boot_options ⇒ Object
324
325
326
|
# File 'lib/dash/commands/proxy.rb', line 324
def read_boot_options
read_file(config.proxy_boot.options_file, default: config.proxy_boot.default_boot_options.join(" "))
end
|
#read_image ⇒ Object
328
329
330
|
# File 'lib/dash/commands/proxy.rb', line 328
def read_image
read_file(config.proxy_boot.image_file, default: config.proxy_boot.image_default)
end
|
#read_image_version ⇒ Object
#read_run_command ⇒ Object
336
337
338
|
# File 'lib/dash/commands/proxy.rb', line 336
def read_run_command
read_file(config.proxy_boot.run_command_file)
end
|
#remove_container ⇒ Object
Docker ANDs multiple --filter label= values, so matching both the current
and the pre-rename image title takes two commands rather than one filter
with two values. Without the legacy pass, dash proxy remove on a host that
has not yet been through the rename silently leaves the old container and
image behind. Stage 3d drops the legacy half.
#remove_legacy_container(timeout: nil) ⇒ Object
Stops and removes a pre-rename proxy container so the renamed one can claim
ports 80/443. No port-holder handoff spans two container names, which is
why this stage accepts a brief outage per host.
#remove_legacy_holder_container ⇒ Object
#remove_proxy_directory ⇒ Object
306
307
308
|
# File 'lib/dash/commands/proxy.rb', line 306
def remove_proxy_directory
remove_directory config.proxy_boot.host_directory
end
|
#remove_proxy_secrets_file ⇒ Object
Static path rather than proxy_run_config.secrets_path: the file must be
removable precisely when the run config (or its secrets) is gone.
#remove_stopped_container(name: container_name) ⇒ Object
250
251
252
|
# File 'lib/dash/commands/proxy.rb', line 250
def remove_stopped_container(name: container_name)
docker :container, :rm, name
end
|
#reset_boot_options ⇒ Object
340
341
342
|
# File 'lib/dash/commands/proxy.rb', line 340
def reset_boot_options
remove_file config.proxy_boot.options_file
end
|
#reset_image ⇒ Object
344
345
346
|
# File 'lib/dash/commands/proxy.rb', line 344
def reset_image
remove_file config.proxy_boot.image_file
end
|
#reset_image_version ⇒ Object
348
349
350
|
# File 'lib/dash/commands/proxy.rb', line 348
def reset_image_version
remove_file config.proxy_boot.image_version_file
end
|
#reset_run_command ⇒ Object
352
353
354
|
# File 'lib/dash/commands/proxy.rb', line 352
def reset_run_command
remove_file config.proxy_boot.run_command_file
end
|
#run(digest: nil, name: nil) ⇒ Object
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
# File 'lib/dash/commands/proxy.rb', line 28
def run(digest: nil, name: nil)
if proxy_run_config
docker \
:run,
"--name", name || container_name,
*proxy_run_config.network_args,
"--detach",
"--restart", "unless-stopped",
"--volume", "dash-proxy-config:/home/dash-proxy/.config/dash-proxy",
*config_digest_label_args(digest),
*proxy_run_config.docker_options_args,
*proxy_run_config.image,
*proxy_run_config.run_command
else
pipe boot_config, xargs(docker_run(digest: digest))
end
end
|
#run_holder ⇒ Object
216
217
218
219
220
221
222
223
224
225
226
|
# File 'lib/dash/commands/proxy.rb', line 216
def run_holder
docker \
:run,
"--name", proxy_run_config.holder_container_name,
"--network", "dash",
"--detach",
"--restart", "unless-stopped",
*proxy_run_config.holder_docker_args,
*proxy_run_config.image,
"dash-proxy", "hold"
end
|
#start ⇒ Object
140
141
142
|
# File 'lib/dash/commands/proxy.rb', line 140
def start
docker :container, :start, container_name
end
|
#start_holder_or_run ⇒ Object
228
229
230
|
# File 'lib/dash/commands/proxy.rb', line 228
def start_holder_or_run
combine docker(:container, :start, proxy_run_config.holder_container_name), run_holder, by: "||"
end
|
#start_or_run(digest: nil) ⇒ Object
148
149
150
|
# File 'lib/dash/commands/proxy.rb', line 148
def start_or_run(digest: nil)
combine start, run(digest: digest), by: "||"
end
|
#stop(name: container_name, timeout: nil) ⇒ Object
144
145
146
|
# File 'lib/dash/commands/proxy.rb', line 144
def stop(name: container_name, timeout: nil)
docker :container, :stop, *("--time #{timeout}" if timeout), name
end
|
#version(name: container_name) ⇒ Object
name: so the doctor can also ask about the pre-rename container: during
the stage-3c transition a host still runs kamal-proxy, and a check that only
ever looks at dash-proxy concludes no proxy is running.
159
160
161
162
163
|
# File 'lib/dash/commands/proxy.rb', line 159
def version(name: container_name)
pipe \
docker(:inspect, name, "--format '{{.Config.Image}}'"),
[ :awk, "-F:", "'{print \$NF}'" ]
end
|
#wait_for_exit(name: container_name) ⇒ Object
246
247
248
|
# File 'lib/dash/commands/proxy.rb', line 246
def wait_for_exit(name: container_name)
docker :wait, name
end
|