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



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

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



142
143
144
# File 'lib/dash/commands/proxy.rb', line 142

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



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

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



239
240
241
242
243
244
245
246
# File 'lib/dash/commands/proxy.rb', line 239

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



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

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

#container_id(only_running: false) ⇒ Object



122
123
124
# File 'lib/dash/commands/proxy.rb', line 122

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. The guard is negated and leads the chain, with || true last, because shell && and || share precedence and associate left: written as exists || legacy_exists && create && copy it would parse as ((exists || legacy_exists) && create) && copy and re-copy the legacy volume over live state on every deploy. Leading with ! exists makes the whole chain a single left-associative AND, which short-circuits correctly.



60
61
62
63
64
65
66
67
68
69
# File 'lib/dash/commands/proxy.rb', line 60

def copy_legacy_config_volume(volume: Dash::Configuration::Proxy::CONFIG_VOLUME, legacy: Dash::Configuration::Proxy::LEGACY_CONFIG_VOLUME)
  any \
    combine(
      negate(volume_exists(volume)),
      volume_exists(legacy),
      docker(:volume, :create, volume),
      copy_between_volumes(legacy, volume)
    ),
    [ :true ]
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.



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

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.



218
219
220
# File 'lib/dash/commands/proxy.rb', line 218

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

#drain(timeout: nil) ⇒ Object



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

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

#ensure_apps_config_directoryObject



256
257
258
# File 'lib/dash/commands/proxy.rb', line 256

def ensure_apps_config_directory
  make_directory config.proxy_boot.apps_directory
end

#ensure_proxy_directoryObject



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

def ensure_proxy_directory
  make_directory config.proxy_boot.host_directory
end

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



210
211
212
213
214
215
# File 'lib/dash/commands/proxy.rb', line 210

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



178
179
180
# File 'lib/dash/commands/proxy.rb', line 178

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

#infoObject



105
106
107
# File 'lib/dash/commands/proxy.rb', line 105

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

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



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

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

#loadbalancerObject



302
303
304
# File 'lib/dash/commands/proxy.rb', line 302

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



204
205
206
207
208
# File 'lib/dash/commands/proxy.rb', line 204

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.



148
149
150
# File 'lib/dash/commands/proxy.rb', line 148

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

#next_container_nameObject



158
159
160
# File 'lib/dash/commands/proxy.rb', line 158

def next_container_name
  "#{container_name}-next"
end

#port_holder?Boolean

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

Returns:

  • (Boolean)


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

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

#promote_next_containerObject



200
201
202
# File 'lib/dash/commands/proxy.rb', line 200

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

#pullObject



126
127
128
129
130
131
132
# File 'lib/dash/commands/proxy.rb', line 126

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



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

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

#read_imageObject



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

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

#read_image_versionObject



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

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

#read_run_commandObject



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

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.



227
228
229
230
231
# File 'lib/dash/commands/proxy.rb', line 227

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

#remove_imageObject



233
234
235
236
237
# File 'lib/dash/commands/proxy.rb', line 233

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.



74
75
76
77
78
79
80
81
82
# File 'lib/dash/commands/proxy.rb', line 74

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



84
85
86
87
88
89
90
91
# File 'lib/dash/commands/proxy.rb', line 84

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



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

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.



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

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



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

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

#reset_boot_optionsObject



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

def reset_boot_options
  remove_file config.proxy_boot.options_file
end

#reset_imageObject



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

def reset_image
  remove_file config.proxy_boot.image_file
end

#reset_image_versionObject



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

def reset_image_version
  remove_file config.proxy_boot.image_version_file
end

#reset_run_commandObject



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

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



162
163
164
165
166
167
168
169
170
171
172
# File 'lib/dash/commands/proxy.rb', line 162

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



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

def start
  docker :container, :start, container_name
end

#start_holder_or_runObject



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

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



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

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

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



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

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.



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

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

#wait_for_exit(name: container_name) ⇒ Object



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

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