Class: Kamal::Configuration::Proxy::Run

Inherits:
Object
  • Object
show all
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

Class Method Summary collapse

Instance Method Summary collapse

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

#configObject (readonly)

Returns the value of attribute config.



17
18
19
# File 'lib/kamal/configuration/proxy/run.rb', line 17

def config
  @config
end

#run_configObject (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

#acmeObject



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_directoryObject



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_directoryObject



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_directoryObject



226
227
228
# File 'lib/kamal/configuration/proxy/run.rb', line 226

def apps_container_directory
  "/home/kamal-proxy/.apps-config"
end

#apps_directoryObject



222
223
224
# File 'lib/kamal/configuration/proxy/run.rb', line 222

def apps_directory
  File.join host_directory, "apps-config"
end

#apps_volumeObject



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_argsObject



236
237
238
# File 'lib/kamal/configuration/proxy/run.rb', line 236

def apps_volume_args
  [ apps_volume.docker_args ]
end

#bind_ipsObject



65
66
67
# File 'lib/kamal/configuration/proxy/run.rb', line 65

def bind_ips
  run_config.fetch("bind_ips", nil)
end

#config_digestObject

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, *docker_options_args, *secret_names)
end

#container_nameObject



105
106
107
# File 'lib/kamal/configuration/proxy/run.rb', line 105

def container_name
  "kamal-proxy"
end

#debug?Boolean

Returns:

  • (Boolean)


49
50
51
# File 'lib/kamal/configuration/proxy/run.rb', line 49

def debug?
  run_config.fetch("debug", nil)
end

#docker_options_argsObject



180
181
182
183
184
185
186
187
188
189
190
# File 'lib/kamal/configuration/proxy/run.rb', line 180

def docker_options_args
  [
    *apps_volume_args,
    *(publish_args unless port_holder?),
    *logging_args,
    *("--expose=#{metrics_port}" if metrics_port.present?),
    *secrets_args,
    *docker_socket_args,
    *options_args
  ].compact
end

#docker_socketObject

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_argsObject

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

#flagsObject

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

#hashObject



253
254
255
# File 'lib/kamal/configuration/proxy/run.rb', line 253

def hash
  run_config.hash
end

#holder_container_nameObject



116
117
118
# File 'lib/kamal/configuration/proxy/run.rb', line 116

def holder_container_name
  "kamal-proxy-net"
end

#holder_docker_argsObject



128
129
130
# File 'lib/kamal/configuration/proxy/run.rb', line 128

def holder_docker_args
  [ *publish_args, *logging_args ].compact
end

#host_directoryObject



218
219
220
# File 'lib/kamal/configuration/proxy/run.rb', line 218

def host_directory
  File.join config.run_directory, "proxy"
end

#http_portObject



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_portObject



61
62
63
# File 'lib/kamal/configuration/proxy/run.rb', line 61

def https_port
  run_config.fetch("https_port", DEFAULT_HTTPS_PORT)
end

#imageObject



101
102
103
# File 'lib/kamal/configuration/proxy/run.rb', line 101

def image
  "#{[ registry, repository ].compact.join("/")}:#{version}"
end

#log_max_sizeObject



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_argsObject



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_portObject



142
143
144
# File 'lib/kamal/configuration/proxy/run.rb', line 142

def metrics_port
  run_config["metrics_port"]
end

#named_run_command_optionsObject

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 named_run_command_options
  # 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(cache_options).merge(server_options)
end

#network_argsObject



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_argsObject



132
133
134
135
136
# File 'lib/kamal/configuration/proxy/run.rb', line 132

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

Returns:

  • (Boolean)


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.

Returns:

  • (Boolean)


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

Returns:

  • (Boolean)


53
54
55
# File 'lib/kamal/configuration/proxy/run.rb', line 53

def publish?
  run_config.fetch("publish", true)
end

#publish_argsObject



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

#registryObject



93
94
95
# File 'lib/kamal/configuration/proxy/run.rb', line 93

def registry
  run_config.fetch("registry", nil)
end

#repositoryObject



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_commandObject



138
139
140
# File 'lib/kamal/configuration/proxy/run.rb', line 138

def run_command
  [ "kamal-proxy", "run", *optionize(run_command_options), *acme.run_command_args ].join(" ")
end

#run_command_optionsObject



146
147
148
# File 'lib/kamal/configuration/proxy/run.rb', line 146

def run_command_options
  named_run_command_options.merge(flags)
end

#secrets?Boolean

Returns:

  • (Boolean)


210
211
212
# File 'lib/kamal/configuration/proxy/run.rb', line 210

def secrets?
  acme.credentials? || cache_store.present?
end

#secrets_ioObject



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_pathObject

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

#versionObject



89
90
91
# File 'lib/kamal/configuration/proxy/run.rb', line 89

def version
  run_config.fetch("version", MINIMUM_VERSION)
end