Class: Dash::Commands::Proxy

Inherits:
Base
  • Object
show all
Includes:
CertTransfer
Defined in:
lib/dash/commands/proxy.rb

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}'"

Constants included from CertTransfer

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

Methods included from CertTransfer

#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 (readonly)

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).



114
115
116
# File 'lib/dash/commands/proxy.rb', line 114

def copy_legacy_config_volume(volume: Dash::Configuration::Proxy::CONFIG_VOLUME, legacy: Dash::Configuration::Proxy::LEGACY_CONFIG_VOLUME)
  copy_legacy_volume(legacy: legacy, volume: volume, image: proxy_image)
end

#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)

Returns:

  • (Boolean)


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

#promote_next_container ⇒ Object



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



332
333
334
# File 'lib/dash/commands/proxy.rb', line 332

def read_image_version
  read_file(config.proxy_boot.image_version_file, default: Dash::Configuration::Proxy::Run::MINIMUM_VERSION)
end

#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.



281
282
283
284
285
# File 'lib/dash/commands/proxy.rb', line 281

def remove_container
  combine \
    prune_containers_titled(Dash::Configuration::Proxy::IMAGE_TITLE),
    prune_containers_titled(Dash::Configuration::Proxy::LEGACY_IMAGE_TITLE)
end

#remove_image ⇒ Object



287
288
289
290
291
# File 'lib/dash/commands/proxy.rb', line 287

def remove_image
  combine \
    prune_images_titled(Dash::Configuration::Proxy::IMAGE_TITLE),
    prune_images_titled(Dash::Configuration::Proxy::LEGACY_IMAGE_TITLE)
end

#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.



121
122
123
124
125
126
127
128
129
# File 'lib/dash/commands/proxy.rb', line 121

def remove_legacy_container(timeout: nil)
  any \
    combine(
      container_exists(Dash::Configuration::Proxy::LEGACY_CONTAINER_NAME),
      docker(:container, :stop, *("--time=#{timeout}" if timeout), Dash::Configuration::Proxy::LEGACY_CONTAINER_NAME),
      docker(:container, :rm, Dash::Configuration::Proxy::LEGACY_CONTAINER_NAME)
    ),
    [ :true ]
end

#remove_legacy_holder_container ⇒ Object



131
132
133
134
135
136
137
138
# File 'lib/dash/commands/proxy.rb', line 131

def remove_legacy_holder_container
  any \
    combine(
      container_exists(Dash::Configuration::Proxy::LEGACY_HOLDER_CONTAINER_NAME),
      docker(:container, :rm, "--force", Dash::Configuration::Proxy::LEGACY_HOLDER_CONTAINER_NAME)
    ),
    [ :true ]
end

#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.



316
317
318
# File 'lib/dash/commands/proxy.rb', line 316

def remove_proxy_secrets_file
  remove_file File.join(config.proxy_boot.host_directory, Dash::Configuration::Proxy::Run::SECRETS_FILENAME)
end

#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