Class: Dash::Commands::Proxy

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

Defined Under Namespace

Modules: CertTransfer

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_FORMAT =
"'{{ with index .Config.Labels \"#{CONFIG_DIGEST_LABEL}\" }}{{ . }}" \
"{{ else }}{{ index .Config.Labels \"#{LEGACY_CONFIG_DIGEST_LABEL}\" }}{{ end }}'"

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::NO_HEALTHCHECK

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

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



18
19
20
21
# File 'lib/dash/commands/proxy.rb', line 18

def initialize(config, host:)
  super(config)
  @proxy_run_config = config.proxy_run(host)
end

Instance Attribute Details

#proxy_run_configObject (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_configObject



263
264
265
# File 'lib/dash/commands/proxy.rb', line 263

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



139
140
141
# File 'lib/dash/commands/proxy.rb', line 139

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



135
136
137
# File 'lib/dash/commands/proxy.rb', line 135

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_traefikObject



236
237
238
239
240
241
242
243
# File 'lib/dash/commands/proxy.rb', line 236

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_digestObject



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

def config_digest
  docker :inspect, container_name, "--format", CONFIG_DIGEST_FORMAT
end

#container_id(only_running: false) ⇒ Object



119
120
121
# File 'lib/dash/commands/proxy.rb', line 119

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.



64
65
66
# File 'lib/dash/commands/proxy.rb', line 64

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_restartObject

Cancel the restart policy before draining: drain makes the proxy exit on its own, which - unlike docker stop - an active restart policy would undo.



181
182
183
# File 'lib/dash/commands/proxy.rb', line 181

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.



215
216
217
# File 'lib/dash/commands/proxy.rb', line 215

def domains(subcommand, *args)
  docker :exec, container_name, "dash-proxy", "domains", subcommand, *args
end

#drain(timeout: nil) ⇒ Object



185
186
187
# File 'lib/dash/commands/proxy.rb', line 185

def drain(timeout: nil)
  docker :exec, container_name, "dash-proxy", :drain, *("--drain-timeout=#{timeout}s" if timeout)
end

#ensure_apps_config_directoryObject



253
254
255
# File 'lib/dash/commands/proxy.rb', line 253

def ensure_apps_config_directory
  make_directory config.proxy_boot.apps_directory
end

#ensure_proxy_directoryObject



245
246
247
# File 'lib/dash/commands/proxy.rb', line 245

def ensure_proxy_directory
  make_directory config.proxy_boot.host_directory
end

#follow_logs(host:, timestamps: true, grep: nil, grep_options: nil) ⇒ Object



207
208
209
210
211
212
# File 'lib/dash/commands/proxy.rb', line 207

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_idObject



175
176
177
# File 'lib/dash/commands/proxy.rb', line 175

def holder_container_id
  container_id_for(container_name: proxy_run_config.holder_container_name, only_running: true)
end

#infoObject



102
103
104
# File 'lib/dash/commands/proxy.rb', line 102

def info
  docker :ps, "--filter", "'name=^#{container_name}$'"
end

#list(name: container_name, json: false) ⇒ Object



131
132
133
# File 'lib/dash/commands/proxy.rb', line 131

def list(name: container_name, json: false)
  docker :exec, name, "dash-proxy", :list, *("--json" if json)
end

#loadbalancerObject



299
300
301
# File 'lib/dash/commands/proxy.rb', line 299

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



201
202
203
204
205
# File 'lib/dash/commands/proxy.rb', line 201

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_destinationsObject

One mount destination per line - what the running container was actually booted with, as opposed to what the current configuration would mount.



145
146
147
# File 'lib/dash/commands/proxy.rb', line 145

def mount_destinations
  docker :inspect, container_name, "--format", "'{{range .Mounts}}{{println .Destination}}{{end}}'"
end

#next_container_nameObject



155
156
157
# File 'lib/dash/commands/proxy.rb', line 155

def next_container_name
  "#{container_name}-next"
end

#port_holder?Boolean

Zero-downtime handoff commands (proxy/run port_holder mode)

Returns:

  • (Boolean)


151
152
153
# File 'lib/dash/commands/proxy.rb', line 151

def port_holder?
  proxy_run_config&.port_holder? || false
end

#promote_next_containerObject



197
198
199
# File 'lib/dash/commands/proxy.rb', line 197

def promote_next_container
  docker :container, :rename, next_container_name, container_name
end

#pullObject



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

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_optionsObject



267
268
269
# File 'lib/dash/commands/proxy.rb', line 267

def read_boot_options
  read_file(config.proxy_boot.options_file, default: config.proxy_boot.default_boot_options.join(" "))
end

#read_imageObject



271
272
273
# File 'lib/dash/commands/proxy.rb', line 271

def read_image
  read_file(config.proxy_boot.image_file, default: config.proxy_boot.image_default)
end

#read_image_versionObject



275
276
277
# File 'lib/dash/commands/proxy.rb', line 275

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

#read_run_commandObject



279
280
281
# File 'lib/dash/commands/proxy.rb', line 279

def read_run_command
  read_file(config.proxy_boot.run_command_file)
end

#remove_containerObject

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.



224
225
226
227
228
# File 'lib/dash/commands/proxy.rb', line 224

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

#remove_imageObject



230
231
232
233
234
# File 'lib/dash/commands/proxy.rb', line 230

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.



71
72
73
74
75
76
77
78
79
# File 'lib/dash/commands/proxy.rb', line 71

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_containerObject



81
82
83
84
85
86
87
88
# File 'lib/dash/commands/proxy.rb', line 81

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_directoryObject



249
250
251
# File 'lib/dash/commands/proxy.rb', line 249

def remove_proxy_directory
  remove_directory config.proxy_boot.host_directory
end

#remove_proxy_secrets_fileObject

Static path rather than proxy_run_config.secrets_path: the file must be removable precisely when the run config (or its secrets) is gone.



259
260
261
# File 'lib/dash/commands/proxy.rb', line 259

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



193
194
195
# File 'lib/dash/commands/proxy.rb', line 193

def remove_stopped_container(name: container_name)
  docker :container, :rm, name
end

#reset_boot_optionsObject



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

def reset_boot_options
  remove_file config.proxy_boot.options_file
end

#reset_imageObject



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

def reset_image
  remove_file config.proxy_boot.image_file
end

#reset_image_versionObject



291
292
293
# File 'lib/dash/commands/proxy.rb', line 291

def reset_image_version
  remove_file config.proxy_boot.image_version_file
end

#reset_run_commandObject



295
296
297
# File 'lib/dash/commands/proxy.rb', line 295

def reset_run_command
  remove_file config.proxy_boot.run_command_file
end

#run(digest: nil, name: nil) ⇒ Object



23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# File 'lib/dash/commands/proxy.rb', line 23

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_holderObject



159
160
161
162
163
164
165
166
167
168
169
# File 'lib/dash/commands/proxy.rb', line 159

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

#startObject



90
91
92
# File 'lib/dash/commands/proxy.rb', line 90

def start
  docker :container, :start, container_name
end

#start_holder_or_runObject



171
172
173
# File 'lib/dash/commands/proxy.rb', line 171

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



98
99
100
# File 'lib/dash/commands/proxy.rb', line 98

def start_or_run(digest: nil)
  combine start, run(digest: digest), by: "||"
end

#stop(name: container_name, timeout: nil) ⇒ Object



94
95
96
# File 'lib/dash/commands/proxy.rb', line 94

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.



109
110
111
112
113
# File 'lib/dash/commands/proxy.rb', line 109

def version(name: container_name)
  pipe \
    docker(:inspect, name, "--format '{{.Config.Image}}'"),
    [ :awk, "-F:", "'{print \$NF}'" ]
end

#wait_for_exit(name: container_name) ⇒ Object



189
190
191
# File 'lib/dash/commands/proxy.rb', line 189

def wait_for_exit(name: container_name)
  docker :wait, name
end