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



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



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



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.



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



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



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

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



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

#versionObject



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

def version
  pipe \
    docker(:inspect, container_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