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_RENAME_MARKER =
Written into the run directory once a host is verifiably past the 3c rename, so the bridge costs it nothing but a
test -fon every deploy after. Deleted in stage 3d. ".legacy-renamed"- 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.
203 204 205 206 207 208 209 210 211 212 |
# File 'lib/dash/configuration/proxy.rb', line 203 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.
198 199 200 |
# File 'lib/dash/configuration/proxy.rb', line 198 def config @config end |
#proxy_config ⇒ Object (readonly)
Returns the value of attribute proxy_config.
198 199 200 |
# File 'lib/dash/configuration/proxy.rb', line 198 def proxy_config @proxy_config end |
#role_name ⇒ Object (readonly)
Returns the value of attribute role_name.
198 199 200 |
# File 'lib/dash/configuration/proxy.rb', line 198 def role_name @role_name end |
#run ⇒ Object (readonly)
Returns the value of attribute run.
198 199 200 |
# File 'lib/dash/configuration/proxy.rb', line 198 def run @run end |
#secrets ⇒ Object (readonly)
Returns the value of attribute secrets.
198 199 200 |
# File 'lib/dash/configuration/proxy.rb', line 198 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.
189 190 191 192 193 194 |
# File 'lib/dash/configuration/proxy.rb', line 189 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.
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 390 391 392 |
# File 'lib/dash/configuration/proxy.rb', line 351 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
214 215 216 |
# File 'lib/dash/configuration/proxy.rb', line 214 def app_port proxy_config.fetch("app_port", 80) end |
#certificate_pem_content ⇒ Object
263 264 265 266 267 |
# File 'lib/dash/configuration/proxy.rb', line 263 def certificate_pem_content ssl = proxy_config["ssl"] return nil unless ssl.is_a?(Hash) secrets[ssl["certificate_pem"]] end |
#client_ca? ⇒ Boolean
311 312 313 |
# File 'lib/dash/configuration/proxy.rb', line 311 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.
307 308 309 |
# File 'lib/dash/configuration/proxy.rb', line 307 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.
319 320 321 322 323 324 325 |
# File 'lib/dash/configuration/proxy.rb', line 319 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
331 332 333 |
# File 'lib/dash/configuration/proxy.rb', line 331 def container_client_ca tls_file_path(config.proxy_boot.tls_container_directory, CLIENT_CA_FILENAME) if client_ca? end |
#container_tls_cert ⇒ Object
283 284 285 |
# File 'lib/dash/configuration/proxy.rb', line 283 def container_tls_cert tls_path(config.proxy_boot.tls_container_directory, "cert.pem") end |
#container_tls_key ⇒ Object
287 288 289 |
# File 'lib/dash/configuration/proxy.rb', line 287 def container_tls_key tls_path(config.proxy_boot.tls_container_directory, "key.pem") if custom_ssl_certificate? end |
#custom_ssl_certificate? ⇒ Boolean
257 258 259 260 261 |
# File 'lib/dash/configuration/proxy.rb', line 257 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
394 395 396 |
# File 'lib/dash/configuration/proxy.rb', line 394 def deploy_command_args(target:) optionize ({ target: "#{target}:#{app_port}" }).merge(), with: "=" end |
#deploy_options ⇒ Object
344 345 346 |
# File 'lib/dash/configuration/proxy.rb', line 344 def .select { |key, _| retained_dispositions.include?(self.class.disposition(key)) } end |
#effective_loadbalancer ⇒ Object
243 244 245 246 247 248 249 250 251 |
# File 'lib/dash/configuration/proxy.rb', line 243 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.
340 341 342 |
# File 'lib/dash/configuration/proxy.rb', line 340 def healthcheck_path proxy_config.dig("healthcheck", "path") end |
#host_client_ca ⇒ Object
327 328 329 |
# File 'lib/dash/configuration/proxy.rb', line 327 def host_client_ca tls_file_path(config.proxy_boot.tls_directory, CLIENT_CA_FILENAME) if client_ca? end |
#host_tls_cert ⇒ Object
275 276 277 |
# File 'lib/dash/configuration/proxy.rb', line 275 def host_tls_cert tls_path(config.proxy_boot.tls_directory, "cert.pem") end |
#host_tls_key ⇒ Object
279 280 281 |
# File 'lib/dash/configuration/proxy.rb', line 279 def host_tls_key tls_path(config.proxy_boot.tls_directory, "key.pem") end |
#hosts ⇒ Object
222 223 224 |
# File 'lib/dash/configuration/proxy.rb', line 222 def hosts proxy_config["hosts"] || proxy_config["host"]&.split(",") || [] end |
#load_balanced? ⇒ Boolean
239 240 241 |
# File 'lib/dash/configuration/proxy.rb', line 239 def load_balanced? @load_balanced end |
#load_balancing? ⇒ Boolean
235 236 237 |
# File 'lib/dash/configuration/proxy.rb', line 235 def load_balancing? effective_loadbalancer.present? end |
#loadbalancer ⇒ Object
226 227 228 |
# File 'lib/dash/configuration/proxy.rb', line 226 def loadbalancer proxy_config["loadbalancer"] end |
#loadbalancer_on_proxy_host? ⇒ Boolean
253 254 255 |
# File 'lib/dash/configuration/proxy.rb', line 253 def loadbalancer_on_proxy_host? load_balancing? && config.proxy_hosts.include?(effective_loadbalancer) end |
#merge(other) ⇒ Object
426 427 428 |
# File 'lib/dash/configuration/proxy.rb', line 426 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
298 299 300 |
# File 'lib/dash/configuration/proxy.rb', line 298 def on_demand_url ssl_config["on_demand_url"] end |
#path_prefixes ⇒ Object
335 336 337 |
# File 'lib/dash/configuration/proxy.rb', line 335 def path_prefixes proxy_config["path_prefixes"] || proxy_config["path_prefix"]&.split(",") || [] end |
#private_key_pem_content ⇒ Object
269 270 271 272 273 |
# File 'lib/dash/configuration/proxy.rb', line 269 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.
231 232 233 |
# File 'lib/dash/configuration/proxy.rb', line 231 def reboot_on_deploy? proxy_config.fetch("reboot_on_deploy", true) end |
#rollout_deploy_command_args(target:) ⇒ Object
407 408 409 |
# File 'lib/dash/configuration/proxy.rb', line 407 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.
400 401 402 403 404 405 |
# File 'lib/dash/configuration/proxy.rb', line 400 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
411 412 413 |
# File 'lib/dash/configuration/proxy.rb', line 411 def rollout_set_command_args(percent: nil, list: nil) optionize({ percent: percent, list: list }.compact, with: "=") end |
#ssl? ⇒ Boolean
218 219 220 |
# File 'lib/dash/configuration/proxy.rb', line 218 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.
294 295 296 |
# File 'lib/dash/configuration/proxy.rb', line 294 def ssl_config proxy_config["ssl"].is_a?(Hash) ? proxy_config["ssl"] : {} end |
#stop_command_args(**options) ⇒ Object
422 423 424 |
# File 'lib/dash/configuration/proxy.rb', line 422 def stop_command_args(**) optionize (**), with: "=" end |
#stop_options(drain_timeout: nil, message: nil) ⇒ Object
415 416 417 418 419 420 |
# File 'lib/dash/configuration/proxy.rb', line 415 def (drain_timeout: nil, message: nil) { "drain-timeout": seconds_duration(drain_timeout), message: }.compact end |