Class: Dash::Configuration::Proxy
- Inherits:
-
Object
- Object
- Dash::Configuration::Proxy
- Includes:
- Validation
- Defined in:
- lib/dash/configuration/proxy.rb
Direct Known Subclasses
Defined Under Namespace
Constant Summary collapse
- DEFAULT_LOG_REQUEST_HEADERS =
[ "Cache-Control", "Last-Modified", "User-Agent" ]
- CONTAINER_NAME =
"dash-proxy"- LOADBALANCER_CONTAINER_NAME =
"dash-loadbalancer"- CLIENT_CA_FILENAME =
"client-ca.pem"- IMAGE_TITLE =
The image label the prune filters key on, set by dash-proxy's publish workflow.
"dash-proxy"- LOADBALANCER_IMAGE_TITLE =
"dash-loadbalancer"- NETWORK =
"dash"- CONFIG_VOLUME =
"dash-proxy-config"- LOADBALANCER_CONFIG_VOLUME =
"dash-loadbalancer-config"- LEGACY_CONTAINER_NAME =
Pre-rename names, kept only so the stage-3c migration can find what a host already has: the container to replace, the volume to copy, the network to bridge, and the image label to keep pruning by. Nothing constructs a resource with these — stage 3d deletes them.
"kamal-proxy"- LEGACY_LOADBALANCER_CONTAINER_NAME =
"kamal-loadbalancer"- LEGACY_HOLDER_CONTAINER_NAME =
"kamal-proxy-net"- LEGACY_NETWORK =
"kamal"- LEGACY_CONFIG_VOLUME =
"kamal-proxy-config"- LEGACY_LOADBALANCER_CONFIG_VOLUME =
"kamal-loadbalancer-config"- LEGACY_IMAGE_TITLE =
"kamal-proxy"- LEGACY_LOADBALANCER_IMAGE_TITLE =
"kamal-loadbalancer"- DEFAULT_COMPRESSION_ENCODINGS =
What
compress: trueoffers. dash-proxy has no "on" state without an explicit list - --compress is the list - so the shorthand has to pick. Best ratio first, matching the proxy's own default ordering; the client's Accept-Encoding q-values still outrank this preference. %w[ zstd br gzip ].freeze
- SUPPORTED_COMPRESSION_ENCODINGS =
%w[ gzip br zstd ].freeze
- COMPRESSION_ENCODING_ALIASES =
dash-proxy maps
brotlionto thebrtoken that travels in Content-Encoding. { "brotli" => "br" }.freeze
- DEPLOY_OPTION_DISPOSITIONS =
The layering contract. When the fork's load balancer fronts the per-host proxies, every deploy option lives at exactly one layer — or at both, on purpose. Nothing is allowed to be undecided: #deploy_options refuses to emit a key that has no disposition here, and test/proxy_layering_test.rb fails the build if a new option is added without one.
:edge — only where clients connect. Stripped from the per-app deploy, applied by the load balancer. :per_app — only next to the app. Applied per-app, stripped from the load balancer. :both — each layer genuinely has its own copy of the concern.Without load balancing the single proxy is every layer at once and the whole surface applies to it.
{ # --- Edge: TLS terminates where the handshake happens, and dash-proxy # gates TLSRedirect on TLSEnabled, so the whole family travels together. host: :edge, tls: :edge, "tls-staging": :edge, "tls-certificate-path": :edge, "tls-private-key-path": :edge, "tls-redirect": :edge, "tls-domains-source": :edge, "tls-domains-interval": :edge, "tls-domains-batch-size": :edge, "tls-on-demand-url": :edge, "tls-client-ca-path": :edge, # --- Edge: the load balancer is the only proxy that ever sees the real # client address — an allow list on a per-app proxy would refuse every # request (its peer is the LB) and one limiter would count the whole # fleet as a single client. "allow-ip": :edge, "deny-ip": :edge, "deny-user-agent": :edge, "trusted-proxy": :edge, "client-ip-header": :edge, "rate-limit": :edge, "rate-limit-burst": :edge, "rate-limit-exempt": :edge, # --- Edge: dash-proxy deletes the Authorization header once a service # enforces basic auth, so an inner proxy would 401 the credential-less # request the load balancer forwards. Credentials belong at the edge only. "basic-auth": :edge, # --- Edge: both layers used to pin with the same cookie name but separate # HMAC keys, so the inner proxy clobbered the edge pin every other request. # Only the edge pin can stick. "session-affinity": :edge, "session-affinity-cookie": :edge, # --- Edge: redirectURLIfNeeded consults r.TLS only, so behind the LB a # per-app redirect emits http:// Locations to HTTPS clients. The dynamic # redirect map answers where clients connect, for the same reason. "canonical-host": :edge, redirect: :edge, "redirects-source": :edge, "redirects-interval": :edge, # --- Edge: one response cache, at the edge — two layers of cache would # double the storage and let the inner cache serve entries the edge # already invalidated. The store it writes into is proxy-wide (proxy/run). cache: :edge, "cache-max-ttl": :edge, "cache-max-body": :edge, "cache-max-variants": :edge, "cache-vary-header": :edge, "cache-vary-cookie": :edge, "cache-allow-set-cookie": :edge, # --- Edge: splitting reads from writes is a fleet-level routing decision; # per-app proxies each front a single host and have nothing to split. "read-target": :edge, "read-target-websockets": :edge, "writer-affinity-timeout": :edge, # --- Per-app: applied next to the app, exactly once. The LB forwards to # the per-host proxies, so running these at both layers would add a header # twice or run a rewrite over its own output. "set-request-header": :per_app, "add-request-header": :per_app, "remove-request-header": :per_app, "set-response-header": :per_app, "add-response-header": :per_app, "remove-response-header": :per_app, rewrite: :per_app, "intercept-errors": :per_app, # --- Per-app: sleep stops and starts app containers through the docker # socket — the LB has neither the socket nor the containers, and its # targets are host addresses, so a sleep flag there fails the deploy. "sleep-after": :per_app, "wake-timeout": :per_app, "sleep-container": :per_app, # --- Per-app: compress once, next to the app. Double-running was only # safe by accident of the Content-Encoding guard. compress: :per_app, "compress-content-type": :per_app, "compress-min-length": :per_app, # --- Both, deliberately: each layer has a real connection pool to its own # targets (LB -> per-host proxies, per-host proxy -> app containers), so # pool tuning and request deadlines apply to each hop. "target-timeout": :both, "target-max-conns": :both, "target-max-idle-conns": :both, "target-idle-conn-timeout": :both, "target-dial-timeout": :both, "target-disable-keep-alives": :both, "target-try-duration": :both, "target-try-interval": :both, "path-timeout": :both, "request-timeout": :both, "path-request-timeout": :both, "deploy-timeout": :both, "drain-timeout": :both, # --- Both: each layer health-checks its own targets, buffers its own # connections, routes its own paths and writes its own logs. "health-check-interval": :both, "health-check-timeout": :both, "health-check-path": :both, "health-check-port": :both, "health-check-host": :both, "buffer-requests": :both, "buffer-responses": :both, "buffer-memory": :both, "max-request-body": :both, "max-response-body": :both, "path-prefix": :both, "strip-path-prefix": :both, "forward-headers": :both, "log-request-header": :both, "log-response-header": :both, "error-pages": :both, "exclude-metrics-path": :both }.freeze
Instance Attribute Summary collapse
-
#config ⇒ Object
readonly
Returns the value of attribute config.
-
#proxy_config ⇒ Object
readonly
Returns the value of attribute proxy_config.
-
#role_name ⇒ Object
readonly
Returns the value of attribute role_name.
-
#run ⇒ Object
readonly
Returns the value of attribute run.
-
#secrets ⇒ Object
readonly
Returns the value of attribute secrets.
Class Method Summary collapse
-
.disposition(key) ⇒ Object
Refusing beats guessing: a deploy option nobody placed would silently land on both layers, which is how session affinity broke in the only topology where it matters.
Instance Method Summary collapse
-
#all_deploy_options ⇒ Object
The full option surface before the layering contract is applied — what a single proxy (no load balancer) deploys with.
- #app_port ⇒ Object
- #certificate_pem_content ⇒ Object
- #client_ca? ⇒ Boolean
-
#client_ca_pem ⇒ Object
The name of a secret in .dash/secrets holding the CA bundle client certificates must chain to - mirroring ssl.certificate_pem, not a local file path.
-
#client_ca_pem_content ⇒ Object
Resolved at upload time, not config time, so
dash app logsand friends work on machines without the secret. - #container_client_ca ⇒ Object
- #container_tls_cert ⇒ Object
- #container_tls_key ⇒ Object
- #custom_ssl_certificate? ⇒ Boolean
- #deploy_command_args(target:) ⇒ Object
- #deploy_options ⇒ Object
- #effective_loadbalancer ⇒ Object
-
#healthcheck_path ⇒ Object
Nil when unset: the default lives in dash-proxy, not here.
- #host_client_ca ⇒ Object
- #host_tls_cert ⇒ Object
- #host_tls_key ⇒ Object
- #hosts ⇒ Object
-
#initialize(config:, proxy_config:, role_name: nil, secrets:, context: "proxy", load_balanced: true) ⇒ Proxy
constructor
load_balanced: falsemarks a registration the fork's load balancer can never front - accessories, whose targets it does not collect. - #load_balanced? ⇒ Boolean
- #load_balancing? ⇒ Boolean
- #loadbalancer ⇒ Object
- #loadbalancer_on_proxy_host? ⇒ Boolean
- #merge(other) ⇒ Object
- #on_demand_url ⇒ Object
- #path_prefixes ⇒ Object
- #private_key_pem_content ⇒ Object
-
#reboot_on_deploy? ⇒ Boolean
Root-level
proxysetting only; ignored inside role-specific proxy blocks. - #rollout_deploy_command_args(target:) ⇒ Object
-
#rollout_deploy_options ⇒ Object
dash-proxy rollout deploy only accepts the target and the timeouts - the service already exists, so it keeps the host, TLS, buffering and logging options of the live deploy.
- #rollout_set_command_args(percent: nil, list: nil) ⇒ Object
- #ssl? ⇒ Boolean
-
#ssl_config ⇒ Object
Everything TLS lives in the one
sslhash - certificate material, on-demand issuance and the mTLS client CA. - #stop_command_args(**options) ⇒ Object
- #stop_options(drain_timeout: nil, message: nil) ⇒ Object
Methods included from Validation
Constructor Details
#initialize(config:, proxy_config:, role_name: nil, secrets:, context: "proxy", load_balanced: true) ⇒ Proxy
load_balanced: false marks a registration the fork's load balancer can
never front - accessories, whose targets it does not collect. Such a proxy
keeps its own host/TLS/basic-auth instead of deferring them to the edge.
200 201 202 203 204 205 206 207 208 209 |
# File 'lib/dash/configuration/proxy.rb', line 200 def initialize(config:, proxy_config:, role_name: nil, secrets:, context: "proxy", load_balanced: true) @config = config @proxy_config = proxy_config @proxy_config = {} if @proxy_config.nil? @role_name = role_name @load_balanced = load_balanced @secrets = secrets validate! @proxy_config, with: Dash::Configuration::Validator::Proxy, context: context @run = Dash::Configuration::Proxy::Run.new(config, run_config: @proxy_config["run"], context: "#{context}/run") if @proxy_config && @proxy_config["run"].present? end |
Instance Attribute Details
#config ⇒ Object (readonly)
Returns the value of attribute config.
195 196 197 |
# File 'lib/dash/configuration/proxy.rb', line 195 def config @config end |
#proxy_config ⇒ Object (readonly)
Returns the value of attribute proxy_config.
195 196 197 |
# File 'lib/dash/configuration/proxy.rb', line 195 def proxy_config @proxy_config end |
#role_name ⇒ Object (readonly)
Returns the value of attribute role_name.
195 196 197 |
# File 'lib/dash/configuration/proxy.rb', line 195 def role_name @role_name end |
#run ⇒ Object (readonly)
Returns the value of attribute run.
195 196 197 |
# File 'lib/dash/configuration/proxy.rb', line 195 def run @run end |
#secrets ⇒ Object (readonly)
Returns the value of attribute secrets.
195 196 197 |
# File 'lib/dash/configuration/proxy.rb', line 195 def secrets @secrets end |
Class Method Details
.disposition(key) ⇒ Object
Refusing beats guessing: a deploy option nobody placed would silently land on both layers, which is how session affinity broke in the only topology where it matters.
186 187 188 189 190 191 |
# File 'lib/dash/configuration/proxy.rb', line 186 def self.disposition(key) DEPLOY_OPTION_DISPOSITIONS.fetch(key) do raise Dash::ConfigurationError, "proxy deploy option --#{key} has no layering disposition - add it to Dash::Configuration::Proxy::DEPLOY_OPTION_DISPOSITIONS" end end |
Instance Method Details
#all_deploy_options ⇒ Object
The full option surface before the layering contract is applied — what a single proxy (no load balancer) deploys with. Public so the layering canary can enumerate every key the gem emits.
348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 |
# File 'lib/dash/configuration/proxy.rb', line 348 def { host: hosts, tls: ssl? ? true : nil, "tls-staging": proxy_config["ssl_staging"] ? true : nil, "tls-certificate-path": container_tls_cert, "tls-private-key-path": container_tls_key, "deploy-timeout": seconds_duration(config.deploy_timeout), "drain-timeout": seconds_duration(config.drain_timeout), "health-check-interval": seconds_duration(proxy_config.dig("healthcheck", "interval")), "health-check-timeout": seconds_duration(proxy_config.dig("healthcheck", "timeout")), "health-check-path": healthcheck_path, "health-check-port": proxy_config.dig("healthcheck", "port"), "health-check-host": proxy_config.dig("healthcheck", "host"), "target-timeout": seconds_duration(proxy_config["response_timeout"]), "read-target": proxy_config.dig("read_routing", "targets").presence, "read-target-websockets": proxy_config.dig("read_routing", "websockets") ? true : nil, "writer-affinity-timeout": seconds_duration(proxy_config.dig("read_routing", "writer_affinity_timeout")), "path-timeout": path_timeout_args("path_response_timeouts"), "request-timeout": seconds_duration(proxy_config["request_timeout"]), "path-request-timeout": path_timeout_args("path_request_timeouts"), "buffer-requests": proxy_config.fetch("buffering", { "requests": true }).fetch("requests", true), "buffer-responses": proxy_config.fetch("buffering", { "responses": true }).fetch("responses", true), "buffer-memory": proxy_config.dig("buffering", "memory"), "max-request-body": proxy_config.dig("buffering", "max_request_body"), "max-response-body": proxy_config.dig("buffering", "max_response_body"), "path-prefix": path_prefixes, "strip-path-prefix": proxy_config.dig("strip_path_prefix"), "forward-headers": proxy_config.dig("forward_headers"), "tls-redirect": proxy_config.dig("ssl_redirect"), "basic-auth": basic_auth_credential, "log-request-header": proxy_config.dig("logging", "request_headers") || DEFAULT_LOG_REQUEST_HEADERS, "log-response-header": proxy_config.dig("logging", "response_headers"), "error-pages": error_pages, # A deploy flag despite reading like metrics configuration: where the # metrics are served and who may read them are proxy-wide and live under # proxy/run, but which of *this service's* paths are counted is per service. "exclude-metrics-path": proxy_config["exclude_metrics_paths"].presence }.merge().merge().merge().merge() .merge().merge().merge() .merge().compact end |
#app_port ⇒ Object
211 212 213 |
# File 'lib/dash/configuration/proxy.rb', line 211 def app_port proxy_config.fetch("app_port", 80) end |
#certificate_pem_content ⇒ Object
260 261 262 263 264 |
# File 'lib/dash/configuration/proxy.rb', line 260 def certificate_pem_content ssl = proxy_config["ssl"] return nil unless ssl.is_a?(Hash) secrets[ssl["certificate_pem"]] end |
#client_ca? ⇒ Boolean
308 309 310 |
# File 'lib/dash/configuration/proxy.rb', line 308 def client_ca? client_ca_pem.present? end |
#client_ca_pem ⇒ Object
The name of a secret in .dash/secrets holding the CA bundle client certificates must chain to - mirroring ssl.certificate_pem, not a local file path. Kamal uploads the content into the app's TLS directory, which the proxy container already mounts, and hands the proxy the path it sees there.
304 305 306 |
# File 'lib/dash/configuration/proxy.rb', line 304 def client_ca_pem ssl_config["client_ca_pem"] end |
#client_ca_pem_content ⇒ Object
Resolved at upload time, not config time, so dash app logs and friends
work on machines without the secret. A blank secret raises like
basic_auth.password_secret - silently deploying without the client CA
would turn mTLS off.
316 317 318 319 320 321 322 |
# File 'lib/dash/configuration/proxy.rb', line 316 def client_ca_pem_content secrets[client_ca_pem].tap do |content| if content.blank? raise Dash::ConfigurationError, "proxy/ssl: client_ca_pem secret '#{client_ca_pem}' is empty" end end end |
#container_client_ca ⇒ Object
328 329 330 |
# File 'lib/dash/configuration/proxy.rb', line 328 def container_client_ca tls_file_path(config.proxy_boot.tls_container_directory, CLIENT_CA_FILENAME) if client_ca? end |
#container_tls_cert ⇒ Object
280 281 282 |
# File 'lib/dash/configuration/proxy.rb', line 280 def container_tls_cert tls_path(config.proxy_boot.tls_container_directory, "cert.pem") end |
#container_tls_key ⇒ Object
284 285 286 |
# File 'lib/dash/configuration/proxy.rb', line 284 def container_tls_key tls_path(config.proxy_boot.tls_container_directory, "key.pem") if custom_ssl_certificate? end |
#custom_ssl_certificate? ⇒ Boolean
254 255 256 257 258 |
# File 'lib/dash/configuration/proxy.rb', line 254 def custom_ssl_certificate? ssl = proxy_config["ssl"] return false unless ssl.is_a?(Hash) ssl["certificate_pem"].present? && ssl["private_key_pem"].present? end |
#deploy_command_args(target:) ⇒ Object
391 392 393 |
# File 'lib/dash/configuration/proxy.rb', line 391 def deploy_command_args(target:) optionize ({ target: "#{target}:#{app_port}" }).merge(), with: "=" end |
#deploy_options ⇒ Object
341 342 343 |
# File 'lib/dash/configuration/proxy.rb', line 341 def .select { |key, _| retained_dispositions.include?(self.class.disposition(key)) } end |
#effective_loadbalancer ⇒ Object
240 241 242 243 244 245 246 247 248 |
# File 'lib/dash/configuration/proxy.rb', line 240 def effective_loadbalancer return nil unless load_balanced? return false if loadbalancer == false return primary_role_first_host if loadbalancer == true return loadbalancer if loadbalancer.present? return primary_role_first_host if auto_load_balanced_primary_role? nil end |
#healthcheck_path ⇒ Object
Nil when unset: the default lives in dash-proxy, not here.
337 338 339 |
# File 'lib/dash/configuration/proxy.rb', line 337 def healthcheck_path proxy_config.dig("healthcheck", "path") end |
#host_client_ca ⇒ Object
324 325 326 |
# File 'lib/dash/configuration/proxy.rb', line 324 def host_client_ca tls_file_path(config.proxy_boot.tls_directory, CLIENT_CA_FILENAME) if client_ca? end |
#host_tls_cert ⇒ Object
272 273 274 |
# File 'lib/dash/configuration/proxy.rb', line 272 def host_tls_cert tls_path(config.proxy_boot.tls_directory, "cert.pem") end |
#host_tls_key ⇒ Object
276 277 278 |
# File 'lib/dash/configuration/proxy.rb', line 276 def host_tls_key tls_path(config.proxy_boot.tls_directory, "key.pem") end |
#hosts ⇒ Object
219 220 221 |
# File 'lib/dash/configuration/proxy.rb', line 219 def hosts proxy_config["hosts"] || proxy_config["host"]&.split(",") || [] end |
#load_balanced? ⇒ Boolean
236 237 238 |
# File 'lib/dash/configuration/proxy.rb', line 236 def load_balanced? @load_balanced end |
#load_balancing? ⇒ Boolean
232 233 234 |
# File 'lib/dash/configuration/proxy.rb', line 232 def load_balancing? effective_loadbalancer.present? end |
#loadbalancer ⇒ Object
223 224 225 |
# File 'lib/dash/configuration/proxy.rb', line 223 def loadbalancer proxy_config["loadbalancer"] end |
#loadbalancer_on_proxy_host? ⇒ Boolean
250 251 252 |
# File 'lib/dash/configuration/proxy.rb', line 250 def loadbalancer_on_proxy_host? load_balancing? && config.proxy_hosts.include?(effective_loadbalancer) end |
#merge(other) ⇒ Object
423 424 425 |
# File 'lib/dash/configuration/proxy.rb', line 423 def merge(other) self.class.new config: config, proxy_config: other.proxy_config.deep_merge(proxy_config), role_name: role_name, secrets: secrets, load_balanced: load_balanced? end |
#on_demand_url ⇒ Object
295 296 297 |
# File 'lib/dash/configuration/proxy.rb', line 295 def on_demand_url ssl_config["on_demand_url"] end |
#path_prefixes ⇒ Object
332 333 334 |
# File 'lib/dash/configuration/proxy.rb', line 332 def path_prefixes proxy_config["path_prefixes"] || proxy_config["path_prefix"]&.split(",") || [] end |
#private_key_pem_content ⇒ Object
266 267 268 269 270 |
# File 'lib/dash/configuration/proxy.rb', line 266 def private_key_pem_content ssl = proxy_config["ssl"] return nil unless ssl.is_a?(Hash) secrets[ssl["private_key_pem"]] end |
#reboot_on_deploy? ⇒ Boolean
Root-level proxy setting only; ignored inside role-specific proxy blocks.
228 229 230 |
# File 'lib/dash/configuration/proxy.rb', line 228 def reboot_on_deploy? proxy_config.fetch("reboot_on_deploy", true) end |
#rollout_deploy_command_args(target:) ⇒ Object
404 405 406 |
# File 'lib/dash/configuration/proxy.rb', line 404 def rollout_deploy_command_args(target:) optionize ({ target: "#{target}:#{app_port}" }).merge(), with: "=" end |
#rollout_deploy_options ⇒ Object
dash-proxy rollout deploy only accepts the target and the timeouts - the service already exists, so it keeps the host, TLS, buffering and logging options of the live deploy.
397 398 399 400 401 402 |
# File 'lib/dash/configuration/proxy.rb', line 397 def { "deploy-timeout": seconds_duration(config.deploy_timeout), "drain-timeout": seconds_duration(config.drain_timeout) }.compact end |
#rollout_set_command_args(percent: nil, list: nil) ⇒ Object
408 409 410 |
# File 'lib/dash/configuration/proxy.rb', line 408 def rollout_set_command_args(percent: nil, list: nil) optionize({ percent: percent, list: list }.compact, with: "=") end |
#ssl? ⇒ Boolean
215 216 217 |
# File 'lib/dash/configuration/proxy.rb', line 215 def ssl? proxy_config.fetch("ssl", false) end |
#ssl_config ⇒ Object
Everything TLS lives in the one ssl hash - certificate material,
on-demand issuance and the mTLS client CA. One naming family instead of a
separate tls: block.
291 292 293 |
# File 'lib/dash/configuration/proxy.rb', line 291 def ssl_config proxy_config["ssl"].is_a?(Hash) ? proxy_config["ssl"] : {} end |
#stop_command_args(**options) ⇒ Object
419 420 421 |
# File 'lib/dash/configuration/proxy.rb', line 419 def stop_command_args(**) optionize (**), with: "=" end |
#stop_options(drain_timeout: nil, message: nil) ⇒ Object
412 413 414 415 416 417 |
# File 'lib/dash/configuration/proxy.rb', line 412 def (drain_timeout: nil, message: nil) { "drain-timeout": seconds_duration(drain_timeout), message: }.compact end |