Class: Kamal::Configuration::Proxy::Run
- Inherits:
-
Object
- Object
- Kamal::Configuration::Proxy::Run
- Defined in:
- lib/kamal/configuration/proxy/run.rb
Constant Summary collapse
- MINIMUM_VERSION =
"v1.0.0.1"- DEFAULT_HTTP_PORT =
80- DEFAULT_HTTPS_PORT =
443- DEFAULT_LOG_MAX_SIZE =
"10m"- SECRETS_FILENAME =
One env file for everything the proxy must know but nothing may print: ACME DNS credentials and the cache store URL. Also referenced by Kamal::Commands::Proxy#remove_proxy_secrets_file, which cleans it up when the config no longer needs it.
"secrets.env"- DIGEST_SCHEMA_VERSION =
Bump when the digest serialization changes, so every host converges with exactly one reboot after upgrading kamal.
"v1"
Instance Attribute Summary collapse
-
#config ⇒ Object
readonly
Returns the value of attribute config.
-
#run_config ⇒ Object
readonly
Returns the value of attribute run_config.
Class Method Summary collapse
Instance Method Summary collapse
- #==(other) ⇒ Object (also: #eql?)
- #acme ⇒ Object
- #app_container_directory ⇒ Object
- #app_directory ⇒ Object
- #apps_container_directory ⇒ Object
- #apps_directory ⇒ Object
- #apps_volume ⇒ Object
- #apps_volume_args ⇒ Object
- #bind_ips ⇒ Object
-
#config_digest ⇒ Object
Digest of the materialized run invocation, used to detect drift between the running proxy container and the current configuration.
- #container_name ⇒ Object
- #debug? ⇒ Boolean
- #docker_options_args ⇒ Object
-
#docker_socket ⇒ Object
What proxy/sleep needs in order to stop and start containers.
-
#docker_socket_args ⇒ Object
The flag only tells kamal-proxy where to look inside its own container, so without this mount the socket is not there and a sleeping service never wakes - which the operator sees as one hung request, not as a misconfiguration.
-
#flags ⇒ Object
Anything kamal-proxy accepts that has no key of its own yet.
- #hash ⇒ Object
- #holder_container_name ⇒ Object
- #holder_docker_args ⇒ Object
- #host_directory ⇒ Object
- #http_port ⇒ Object
- #https_port ⇒ Object
- #image ⇒ Object
-
#initialize(config, run_config:, context: "proxy/run") ⇒ Run
constructor
A new instance of Run.
- #log_max_size ⇒ Object
- #logging_args ⇒ Object
- #metrics_port ⇒ Object
-
#named_run_command_options ⇒ Object
Everything the gem has a key for, as opposed to whatever
flagspasses through. - #network_args ⇒ Object
- #options_args ⇒ Object
-
#port_holder? ⇒ Boolean
Zero-downtime reboots: a minimal long-lived holder container owns the published ports, and proxy generations join its network namespace so two can overlap on the same ports during a handoff.
-
#proxy_protocol_unrestricted? ⇒ Boolean
Whether an operator turned on PROXY protocol without saying who may speak it.
- #publish? ⇒ Boolean
- #publish_args ⇒ Object
- #registry ⇒ Object
- #repository ⇒ Object
- #run_command ⇒ Object
- #run_command_options ⇒ Object
- #secrets? ⇒ Boolean
- #secrets_io ⇒ Object
-
#secrets_path ⇒ Object
Where the proxy's secrets land on the host - the ACME DNS credentials and the cache store URL, which may embed one.
- #version ⇒ Object
Constructor Details
#initialize(config, run_config:, context: "proxy/run") ⇒ Run
Returns a new instance of Run.
20 21 22 23 24 25 |
# File 'lib/kamal/configuration/proxy/run.rb', line 20 def initialize(config, run_config:, context: "proxy/run") @config = config @run_config = run_config @context = context ensure_no_conflicting_flags end |
Instance Attribute Details
#config ⇒ Object (readonly)
Returns the value of attribute config.
17 18 19 |
# File 'lib/kamal/configuration/proxy/run.rb', line 17 def config @config end |
#run_config ⇒ Object (readonly)
Returns the value of attribute run_config.
17 18 19 |
# File 'lib/kamal/configuration/proxy/run.rb', line 17 def run_config @run_config end |
Class Method Details
.digest(*parts) ⇒ Object
27 28 29 |
# File 'lib/kamal/configuration/proxy/run.rb', line 27 def self.digest(*parts) Digest::SHA256.hexdigest([ DIGEST_SCHEMA_VERSION, *parts ].join("\n")) end |
Instance Method Details
#==(other) ⇒ Object Also known as: eql?
248 249 250 |
# File 'lib/kamal/configuration/proxy/run.rb', line 248 def ==(other) other.is_a?(self.class) && run_config == other.run_config end |
#acme ⇒ Object
45 46 47 |
# File 'lib/kamal/configuration/proxy/run.rb', line 45 def acme @acme ||= Kamal::Configuration::Proxy::Acme.new(acme_config: run_config["acme"], secrets: config.secrets) end |
#app_container_directory ⇒ Object
244 245 246 |
# File 'lib/kamal/configuration/proxy/run.rb', line 244 def app_container_directory File.join apps_container_directory, config.service_and_destination end |
#app_directory ⇒ Object
240 241 242 |
# File 'lib/kamal/configuration/proxy/run.rb', line 240 def app_directory File.join apps_directory, config.service_and_destination end |
#apps_container_directory ⇒ Object
226 227 228 |
# File 'lib/kamal/configuration/proxy/run.rb', line 226 def apps_container_directory "/home/kamal-proxy/.apps-config" end |
#apps_directory ⇒ Object
222 223 224 |
# File 'lib/kamal/configuration/proxy/run.rb', line 222 def apps_directory File.join host_directory, "apps-config" end |
#apps_volume ⇒ Object
230 231 232 233 234 |
# File 'lib/kamal/configuration/proxy/run.rb', line 230 def apps_volume Kamal::Configuration::Volume.new \ host_path: apps_directory, container_path: apps_container_directory end |
#apps_volume_args ⇒ Object
236 237 238 |
# File 'lib/kamal/configuration/proxy/run.rb', line 236 def apps_volume_args [ apps_volume.docker_args ] end |
#bind_ips ⇒ Object
65 66 67 |
# File 'lib/kamal/configuration/proxy/run.rb', line 65 def bind_ips run_config.fetch("bind_ips", nil) end |
#config_digest ⇒ Object
Digest of the materialized run invocation, used to detect drift between the running proxy container and the current configuration.
The secret names ride along because --env-file names a path, not the
variables inside it: swapping one credential for another (or adding the
cache store) would otherwise leave the digest unmoved and the old proxy
running. The values deliberately stay out — the digest is published as a
docker label, and hashing secret material into a world-readable label buys
an offline guessing target for nothing. Rotating a credential's value or
the store URL still needs an explicit kamal proxy reboot.
41 42 43 |
# File 'lib/kamal/configuration/proxy/run.rb', line 41 def config_digest self.class.digest(image, run_command, *, *secret_names) end |
#container_name ⇒ Object
105 106 107 |
# File 'lib/kamal/configuration/proxy/run.rb', line 105 def container_name "kamal-proxy" end |
#debug? ⇒ Boolean
49 50 51 |
# File 'lib/kamal/configuration/proxy/run.rb', line 49 def debug? run_config.fetch("debug", nil) end |
#docker_options_args ⇒ Object
180 181 182 183 184 185 186 187 188 189 190 |
# File 'lib/kamal/configuration/proxy/run.rb', line 180 def [ *apps_volume_args, *(publish_args unless port_holder?), *logging_args, *("--expose=#{metrics_port}" if metrics_port.present?), *secrets_args, *docker_socket_args, * ].compact end |
#docker_socket ⇒ Object
What proxy/sleep needs in order to stop and start containers. Reaching this socket is root-equivalent on the host, so nothing turns it on implicitly - an operator has to name it, and naming it is also what mounts it (below).
176 177 178 |
# File 'lib/kamal/configuration/proxy/run.rb', line 176 def docker_socket run_config["docker_socket"] end |
#docker_socket_args ⇒ Object
The flag only tells kamal-proxy where to look inside its own container, so without this mount the socket is not there and a sleeping service never wakes - which the operator sees as one hung request, not as a misconfiguration.
195 196 197 198 199 |
# File 'lib/kamal/configuration/proxy/run.rb', line 195 def docker_socket_args if docker_socket.present? Kamal::Configuration::Volume.new(host_path: docker_socket, container_path: docker_socket).docker_args end end |
#flags ⇒ Object
Anything kamal-proxy accepts that has no key of its own yet. Values are
optionized as written - true is a bare flag, anything else takes a value -
because the point of an escape hatch is that the gem holds no opinion about
the flag it is forwarding.
Note the asymmetry with options, which is a docker run passthrough. The
names are deliberately different; most of why this gap went unnoticed is
that options reads like it should do this.
169 170 171 |
# File 'lib/kamal/configuration/proxy/run.rb', line 169 def flags (run_config["flags"] || {}).transform_keys(&:to_sym) end |
#hash ⇒ Object
253 254 255 |
# File 'lib/kamal/configuration/proxy/run.rb', line 253 def hash run_config.hash end |
#holder_container_name ⇒ Object
116 117 118 |
# File 'lib/kamal/configuration/proxy/run.rb', line 116 def holder_container_name "kamal-proxy-net" end |
#holder_docker_args ⇒ Object
128 129 130 |
# File 'lib/kamal/configuration/proxy/run.rb', line 128 def holder_docker_args [ *publish_args, *logging_args ].compact end |
#host_directory ⇒ Object
218 219 220 |
# File 'lib/kamal/configuration/proxy/run.rb', line 218 def host_directory File.join config.run_directory, "proxy" end |
#http_port ⇒ Object
57 58 59 |
# File 'lib/kamal/configuration/proxy/run.rb', line 57 def http_port run_config.fetch("http_port", DEFAULT_HTTP_PORT) end |
#https_port ⇒ Object
61 62 63 |
# File 'lib/kamal/configuration/proxy/run.rb', line 61 def https_port run_config.fetch("https_port", DEFAULT_HTTPS_PORT) end |
#image ⇒ Object
101 102 103 |
# File 'lib/kamal/configuration/proxy/run.rb', line 101 def image "#{[ registry, repository ].compact.join("/")}:#{version}" end |
#log_max_size ⇒ Object
81 82 83 |
# File 'lib/kamal/configuration/proxy/run.rb', line 81 def log_max_size run_config.fetch("log_max_size", DEFAULT_LOG_MAX_SIZE) end |
#logging_args ⇒ Object
85 86 87 |
# File 'lib/kamal/configuration/proxy/run.rb', line 85 def logging_args argumentize "--log-opt", "max-size=#{log_max_size}" if log_max_size.present? end |
#metrics_port ⇒ Object
142 143 144 |
# File 'lib/kamal/configuration/proxy/run.rb', line 142 def metrics_port run_config["metrics_port"] end |
#named_run_command_options ⇒ Object
Everything the gem has a key for, as opposed to whatever flags passes
through. Kept separate so the two can be checked against each other.
152 153 154 155 156 157 158 159 |
# File 'lib/kamal/configuration/proxy/run.rb', line 152 def # recheck-targets-on-restore: after a reboot, re-verify restored targets # with live health checks instead of trusting the saved state — a dead # target demotes to 503 and self-heals rather than serving 502s forever. # Available from MINIMUM_VERSION, so it is always safe to pass. { debug: debug? || nil, "metrics-port": metrics_port, "recheck-targets-on-restore": true, "docker-socket": docker_socket } .compact.merge().merge() end |
#network_args ⇒ Object
120 121 122 123 124 125 126 |
# File 'lib/kamal/configuration/proxy/run.rb', line 120 def network_args if port_holder? [ "--network", "container:#{holder_container_name}" ] else [ "--network", "kamal" ] end end |
#options_args ⇒ Object
132 133 134 135 136 |
# File 'lib/kamal/configuration/proxy/run.rb', line 132 def if args = run_config["options"] optionize args end end |
#port_holder? ⇒ Boolean
Zero-downtime reboots: a minimal long-lived holder container owns the published ports, and proxy generations join its network namespace so two can overlap on the same ports during a handoff.
112 113 114 |
# File 'lib/kamal/configuration/proxy/run.rb', line 112 def port_holder? run_config.fetch("port_holder", false) end |
#proxy_protocol_unrestricted? ⇒ Boolean
Whether an operator turned on PROXY protocol without saying who may speak it. Kamal::Configuration warns about this: the header rewrites the connecting address that allow_ips and rate_limit key on, so honouring it from anywhere is a client-IP spoofing hole.
261 262 263 |
# File 'lib/kamal/configuration/proxy/run.rb', line 261 def proxy_protocol_unrestricted? run_config["proxy_protocol"] && Array(run_config["proxy_protocol_allow_ips"]).empty? end |
#publish? ⇒ Boolean
53 54 55 |
# File 'lib/kamal/configuration/proxy/run.rb', line 53 def publish? run_config.fetch("publish", true) end |
#publish_args ⇒ Object
69 70 71 72 73 74 75 76 77 78 79 |
# File 'lib/kamal/configuration/proxy/run.rb', line 69 def publish_args if publish? (bind_ips || [ nil ]).map do |bind_ip| bind_ip = format_bind_ip(bind_ip) publish_http = [ bind_ip, http_port, DEFAULT_HTTP_PORT ].compact.join(":") publish_https = [ bind_ip, https_port, DEFAULT_HTTPS_PORT ].compact.join(":") argumentize "--publish", [ publish_http, publish_https ] end.join(" ") end end |
#registry ⇒ Object
93 94 95 |
# File 'lib/kamal/configuration/proxy/run.rb', line 93 def registry run_config.fetch("registry", nil) end |
#repository ⇒ Object
97 98 99 |
# File 'lib/kamal/configuration/proxy/run.rb', line 97 def repository run_config.fetch("repository", "ghcr.io/mhenrixon/kamal-proxy") end |
#run_command ⇒ Object
138 139 140 |
# File 'lib/kamal/configuration/proxy/run.rb', line 138 def run_command [ "kamal-proxy", "run", *optionize(), *acme.run_command_args ].join(" ") end |
#run_command_options ⇒ Object
146 147 148 |
# File 'lib/kamal/configuration/proxy/run.rb', line 146 def .merge(flags) end |
#secrets? ⇒ Boolean
210 211 212 |
# File 'lib/kamal/configuration/proxy/run.rb', line 210 def secrets? acme.credentials? || cache_store.present? end |
#secrets_io ⇒ Object
214 215 216 |
# File 'lib/kamal/configuration/proxy/run.rb', line 214 def secrets_io Kamal::EnvFile.new(acme.credentials_env.merge(cache_store_env)).to_io end |
#secrets_path ⇒ Object
Where the proxy's secrets land on the host - the ACME DNS credentials and
the cache store URL, which may embed one. Under the proxy's own directory
rather than the app's env directory, because the container is host-scoped
and shared by every app on the host - and so kamal proxy remove takes
the secrets with it.
206 207 208 |
# File 'lib/kamal/configuration/proxy/run.rb', line 206 def secrets_path File.join host_directory, SECRETS_FILENAME end |
#version ⇒ Object
89 90 91 |
# File 'lib/kamal/configuration/proxy/run.rb', line 89 def version run_config.fetch("version", MINIMUM_VERSION) end |