kamal 2.7.0 → 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/lib/kamal/cli/accessory.rb +27 -7
  4. data/lib/kamal/cli/alias/command.rb +2 -2
  5. data/lib/kamal/cli/app/boot.rb +1 -1
  6. data/lib/kamal/cli/app.rb +74 -115
  7. data/lib/kamal/cli/base.rb +19 -6
  8. data/lib/kamal/cli/build/clone.rb +0 -2
  9. data/lib/kamal/cli/build/port_forwarding.rb +66 -0
  10. data/lib/kamal/cli/build.rb +70 -35
  11. data/lib/kamal/cli/healthcheck/poller.rb +1 -1
  12. data/lib/kamal/cli/main.rb +9 -3
  13. data/lib/kamal/cli/proxy.rb +42 -35
  14. data/lib/kamal/cli/registry.rb +37 -7
  15. data/lib/kamal/cli/secrets.rb +2 -1
  16. data/lib/kamal/cli/server.rb +12 -1
  17. data/lib/kamal/cli/templates/deploy.yml +4 -3
  18. data/lib/kamal/cli/templates/secrets +2 -1
  19. data/lib/kamal/commander.rb +21 -19
  20. data/lib/kamal/commands/accessory.rb +5 -0
  21. data/lib/kamal/commands/app/execution.rb +7 -1
  22. data/lib/kamal/commands/app.rb +1 -0
  23. data/lib/kamal/commands/base.rb +15 -2
  24. data/lib/kamal/commands/builder/base.rb +20 -1
  25. data/lib/kamal/commands/builder/hybrid.rb +3 -3
  26. data/lib/kamal/commands/builder/local.rb +8 -2
  27. data/lib/kamal/commands/builder/pack.rb +5 -5
  28. data/lib/kamal/commands/builder/remote.rb +15 -3
  29. data/lib/kamal/commands/builder.rb +8 -2
  30. data/lib/kamal/commands/docker.rb +17 -1
  31. data/lib/kamal/commands/proxy.rb +22 -3
  32. data/lib/kamal/commands/registry.rb +22 -0
  33. data/lib/kamal/configuration/accessory.rb +56 -25
  34. data/lib/kamal/configuration/boot.rb +4 -0
  35. data/lib/kamal/configuration/builder.rb +10 -3
  36. data/lib/kamal/configuration/docs/accessory.yml +37 -5
  37. data/lib/kamal/configuration/docs/alias.yml +3 -0
  38. data/lib/kamal/configuration/docs/boot.yml +12 -10
  39. data/lib/kamal/configuration/docs/configuration.yml +30 -1
  40. data/lib/kamal/configuration/docs/proxy.yml +48 -16
  41. data/lib/kamal/configuration/docs/registry.yml +12 -4
  42. data/lib/kamal/configuration/docs/ssh.yml +7 -4
  43. data/lib/kamal/configuration/docs/sshkit.yml +8 -0
  44. data/lib/kamal/configuration/env.rb +7 -3
  45. data/lib/kamal/configuration/proxy/boot.rb +4 -9
  46. data/lib/kamal/configuration/proxy/run.rb +143 -0
  47. data/lib/kamal/configuration/proxy.rb +7 -3
  48. data/lib/kamal/configuration/registry.rb +8 -0
  49. data/lib/kamal/configuration/role.rb +15 -3
  50. data/lib/kamal/configuration/ssh.rb +18 -3
  51. data/lib/kamal/configuration/sshkit.rb +4 -0
  52. data/lib/kamal/configuration/validator/proxy.rb +20 -0
  53. data/lib/kamal/configuration/validator/registry.rb +5 -3
  54. data/lib/kamal/configuration/validator.rb +52 -4
  55. data/lib/kamal/configuration/volume.rb +11 -4
  56. data/lib/kamal/configuration.rb +89 -5
  57. data/lib/kamal/secrets/adapters/one_password.rb +1 -1
  58. data/lib/kamal/secrets/adapters/passbolt.rb +1 -2
  59. data/lib/kamal/secrets/adapters/test.rb +3 -1
  60. data/lib/kamal/secrets/dotenv/inline_command_substitution.rb +15 -1
  61. data/lib/kamal/secrets.rb +17 -6
  62. data/lib/kamal/sshkit_with_ext.rb +135 -10
  63. data/lib/kamal/utils.rb +3 -3
  64. data/lib/kamal/version.rb +1 -1
  65. data/lib/kamal.rb +1 -0
  66. metadata +18 -2
@@ -73,7 +73,10 @@ env:
73
73
  # This requires that file names change when the contents change
74
74
  # (e.g., by including a hash of the contents in the name).
75
75
  #
76
- # To configure this, set the path to the assets:
76
+ # To configure this, set the path to the assets.
77
+ #
78
+ # You can also specify mount options after a colon, such as `ro` for read-only
79
+ # or `z`/`Z` for SELinux labels
77
80
  asset_path: /path/to/assets
78
81
 
79
82
  # Hooks path
@@ -82,6 +85,32 @@ asset_path: /path/to/assets
82
85
  # See https://kamal-deploy.org/docs/hooks for more information:
83
86
  hooks_path: /user_home/kamal/hooks
84
87
 
88
+ # Hook output
89
+ #
90
+ # Hook output visibility. Can be set globally or per-hook.
91
+ # CLI flags (`-v`, `-q`) override these settings.
92
+ #
93
+ # - `:quiet` - hook output is hidden
94
+ # - `:verbose` - hook output is shown
95
+ #
96
+ # With no setting, hook output follows CLI verbosity flags.
97
+ #
98
+ # Note: Failed hooks always show output in the error message regardless of setting.
99
+ #
100
+ # Global setting for all hooks:
101
+ hooks_output: :verbose
102
+
103
+ # Or per-hook settings:
104
+ hooks_output:
105
+ pre-deploy: :verbose
106
+ pre-build: :quiet
107
+
108
+ # Secrets path
109
+ #
110
+ # Path to secrets, defaults to `.kamal/secrets`.
111
+ # Kamal will look for `<secrets_path>-common` and `<secrets_path>` (or `<secrets_path>.<destination>` when using destinations):
112
+ secrets_path: /user_home/kamal/secrets
113
+
85
114
  # Error pages
86
115
  #
87
116
  # A directory relative to the app root to find error pages for the proxy to serve.
@@ -45,27 +45,23 @@ proxy:
45
45
  # unless you explicitly set `forward_headers: true`
46
46
  #
47
47
  # Defaults to `false`:
48
- ssl: ...
48
+ ssl: true
49
49
 
50
50
  # Custom SSL certificate
51
51
  #
52
52
  # In some cases, using Let's Encrypt for automatic certificate management is not an
53
- # option, for example if you are running from host than one host. Or you may already
54
- # have SSL certificates issued by a different Certificate Authority (CA).
55
- # Kamal supports loading custom SSL certificates
56
- # directly from secrets.
57
- #
58
- # Examples:
59
- # ssl: true # Enable SSL with Let's Encrypt
60
- # ssl: false # Disable SSL
61
- # ssl: # Enable custom SSL
62
- # certificate_pem: CERTIFICATE_PEM
63
- # private_key_pem: PRIVATE_KEY_PEM
53
+ # option, for example if you are running from more than one host.
64
54
  #
55
+ # Or you may already have SSL certificates issued by a different Certificate Authority (CA).
56
+ #
57
+ # Kamal supports loading custom SSL certificates directly from secrets. You should
58
+ # pass a hash mapping the `certificate_pem` and `private_key_pem` to the secret names.
59
+ ssl:
60
+ certificate_pem: CERTIFICATE_PEM
61
+ private_key_pem: PRIVATE_KEY_PEM
65
62
  # ### Notes
66
- # - If the certificate or key is missing or invalid, kamal-proxy will fail to start.
67
- # - Always handle SSL certificates and private keys securely. Avoid hard-coding them in deploy.yml files or source control.
68
- # - For automated certificate management, consider using the built-in Let's Encrypt integration instead.
63
+ # - If the certificate or key is missing or invalid, deployments will fail.
64
+ # - Always handle SSL certificates and private keys securely. Avoid hard-coding them in source control.
69
65
 
70
66
  # SSL redirect
71
67
  #
@@ -93,9 +89,21 @@ proxy:
93
89
  #
94
90
  # For applications that split their traffic to different services based on the request path,
95
91
  # you can use path-based routing to mount services under different path prefixes.
96
- path_prefix: '/api'
92
+ # Usage sample: path_prefix: '/api'
93
+ #
94
+ # You can also specify multiple paths in two ways.
95
+ #
96
+ # When using path_prefix you can supply multiple routes separated by commas.
97
+ path_prefix: "/api,/oauth_callback"
98
+ # You can also specify paths as a list of paths, the configuration will be
99
+ # rolled together into a comma separated string.
100
+ path_prefixes:
101
+ - "/api"
102
+ - "/oauth_callback"
97
103
  # By default, the path prefix will be stripped from the request before it is forwarded upstream.
104
+ #
98
105
  # So in the example above, a request to /api/users/123 will be forwarded to web-1 as /users/123.
106
+ #
99
107
  # To instead forward the request with the original path (including the prefix),
100
108
  # specify --strip-path-prefix=false
101
109
  strip_path_prefix: false
@@ -140,6 +148,30 @@ proxy:
140
148
  - X-Request-ID
141
149
  - X-Request-Start
142
150
 
151
+ # Run configuration
152
+ #
153
+ # These options are used when booting the proxy container.
154
+ #
155
+ run:
156
+ http_port: 8080 # HTTP port to use (default 80)
157
+ https_port: 8443 # HTTPS port to use (default 443)
158
+ metrics_port: 9090 # Port for Prometheus metrics
159
+ debug: true # Debug logging (default: false)
160
+ log_max_size: "30m" # Maximum log file size (default: "10m")
161
+ publish: false # Publish ports to the host (default: true)
162
+ bind_ips: # List of IPs to bind to when publishing ports
163
+ - 0.0.0.0
164
+ registry: registry:4443 # Container registry for the kamal-proxy image
165
+ # (defaults to Docker Hub)
166
+ repository: myrepo/kamal-proxy # Container repository for the kamal-proxy image
167
+ # (defaults to `basecamp/kamal-proxy`)
168
+ version: v0.8.0 # Version tag of the kamal-proxy image to use
169
+ options: # Additional options to pass to `docker run`
170
+ label:
171
+ - custom.label=kamal-proxy
172
+ memory: 512m
173
+ cpus: 0.5
174
+
143
175
  # Enabling/disabling the proxy on roles
144
176
  #
145
177
  # The proxy is enabled by default on the primary role but can be disabled by
@@ -1,19 +1,27 @@
1
1
  # Registry
2
2
  #
3
3
  # The default registry is Docker Hub, but you can change it using `registry/server`.
4
+
5
+ # Using a local container registry
6
+ #
7
+ # If the registry server starts with `localhost`, Kamal will start a local Docker registry
8
+ # on that port and push the app image to it.
9
+ registry:
10
+ server: localhost:5555
11
+
12
+ # Using Docker Hub as the container registry
4
13
  #
5
14
  # By default, Docker Hub creates public repositories. To avoid making your images public,
6
15
  # set up a private repository before deploying, or change the default repository privacy
7
16
  # settings to private in your [Docker Hub settings](https://hub.docker.com/repository-settings/default-privacy).
8
17
  #
9
- # A reference to a secret (in this case, `DOCKER_REGISTRY_TOKEN`) will look up the secret
18
+ # A reference to a secret (in this case, `KAMAL_REGISTRY_PASSWORD`) will look up the secret
10
19
  # in the local environment:
11
20
  registry:
12
- server: registry.digitalocean.com
13
21
  username:
14
- - DOCKER_REGISTRY_TOKEN
22
+ - <your docker hub username>
15
23
  password:
16
- - DOCKER_REGISTRY_TOKEN
24
+ - KAMAL_REGISTRY_PASSWORD
17
25
 
18
26
  # Using AWS ECR as the container registry
19
27
  #
@@ -58,13 +58,16 @@ ssh:
58
58
 
59
59
  # Key data
60
60
  #
61
- # An array of strings, with each element of the array being
62
- # a raw private key in PEM format.
63
- key_data: [ "-----BEGIN OPENSSH PRIVATE KEY-----" ]
61
+ # An array of strings, with each element of the array being a secret name.
62
+ key_data:
63
+ - SSH_PRIVATE_KEY
64
+ # You can also provide raw private key in PEM format, but this is deprecated.
65
+ key_data:
66
+ - "-----BEGIN OPENSSH PRIVATE KEY----- ..."
64
67
 
65
68
  # Config
66
69
  #
67
70
  # Set to true to load the default OpenSSH config files (~/.ssh/config,
68
71
  # /etc/ssh_config), to false ignore config files, or to a file path
69
72
  # (or array of paths) to load specific configuration. Defaults to true.
70
- config: true
73
+ config: [ "~/.ssh/myconfig" ]
@@ -21,3 +21,11 @@ sshkit:
21
21
  # Kamal sets a long idle timeout of 900 seconds on connections to try to avoid
22
22
  # re-connection storms after an idle period, such as building an image or waiting for CI.
23
23
  pool_idle_timeout: 300
24
+
25
+ # DNS retry settings
26
+ #
27
+ # Some resolvers (mDNSResponder, systemd-resolved, Tailscale) can drop lookups during
28
+ # bursts of concurrent SSH starts. Kamal will retry DNS failures automatically.
29
+ #
30
+ # Number of retries after the initial attempt. Set to 0 to disable.
31
+ dns_retries: 3
@@ -1,7 +1,7 @@
1
1
  class Kamal::Configuration::Env
2
2
  include Kamal::Configuration::Validation
3
3
 
4
- attr_reader :context, :clear, :secret_keys
4
+ attr_reader :context, :clear, :secrets, :secret_keys
5
5
  delegate :argumentize, to: Kamal::Utils
6
6
 
7
7
  def initialize(config:, secrets:, context: "env")
@@ -23,12 +23,16 @@ class Kamal::Configuration::Env
23
23
  def merge(other)
24
24
  self.class.new \
25
25
  config: { "clear" => clear.merge(other.clear), "secret" => secret_keys | other.secret_keys },
26
- secrets: @secrets
26
+ secrets: secrets
27
+ end
28
+
29
+ def to_h
30
+ clear.merge(aliased_secrets)
27
31
  end
28
32
 
29
33
  private
30
34
  def aliased_secrets
31
- secret_keys.to_h { |key| extract_alias(key) }.transform_values { |secret_key| @secrets[secret_key] }
35
+ secret_keys.to_h { |key| extract_alias(key) }.transform_values { |secret_key| secrets[secret_key] }
32
36
  end
33
37
 
34
38
  def extract_alias(key)
@@ -1,9 +1,4 @@
1
1
  class Kamal::Configuration::Proxy::Boot
2
- MINIMUM_VERSION = "v0.9.0"
3
- DEFAULT_HTTP_PORT = 80
4
- DEFAULT_HTTPS_PORT = 443
5
- DEFAULT_LOG_MAX_SIZE = "10m"
6
-
7
2
  attr_reader :config
8
3
  delegate :argumentize, :optionize, to: Kamal::Utils
9
4
 
@@ -16,8 +11,8 @@ class Kamal::Configuration::Proxy::Boot
16
11
 
17
12
  (bind_ips || [ nil ]).map do |bind_ip|
18
13
  bind_ip = format_bind_ip(bind_ip)
19
- publish_http = [ bind_ip, http_port, DEFAULT_HTTP_PORT ].compact.join(":")
20
- publish_https = [ bind_ip, https_port, DEFAULT_HTTPS_PORT ].compact.join(":")
14
+ publish_http = [ bind_ip, http_port, Kamal::Configuration::Proxy::Run::DEFAULT_HTTP_PORT ].compact.join(":")
15
+ publish_https = [ bind_ip, https_port, Kamal::Configuration::Proxy::Run::DEFAULT_HTTPS_PORT ].compact.join(":")
21
16
 
22
17
  argumentize "--publish", [ publish_http, publish_https ]
23
18
  end.join(" ")
@@ -29,8 +24,8 @@ class Kamal::Configuration::Proxy::Boot
29
24
 
30
25
  def default_boot_options
31
26
  [
32
- *(publish_args(DEFAULT_HTTP_PORT, DEFAULT_HTTPS_PORT, nil)),
33
- *(logging_args(DEFAULT_LOG_MAX_SIZE))
27
+ *(publish_args(Kamal::Configuration::Proxy::Run::DEFAULT_HTTP_PORT, Kamal::Configuration::Proxy::Run::DEFAULT_HTTPS_PORT, nil)),
28
+ *(logging_args(Kamal::Configuration::Proxy::Run::DEFAULT_LOG_MAX_SIZE))
34
29
  ]
35
30
  end
36
31
 
@@ -0,0 +1,143 @@
1
+ class Kamal::Configuration::Proxy::Run
2
+ MINIMUM_VERSION = "v0.9.2"
3
+ DEFAULT_HTTP_PORT = 80
4
+ DEFAULT_HTTPS_PORT = 443
5
+ DEFAULT_LOG_MAX_SIZE = "10m"
6
+
7
+ attr_reader :config, :run_config
8
+ delegate :argumentize, :optionize, to: Kamal::Utils
9
+
10
+ def initialize(config, run_config:, context: "proxy/run")
11
+ @config = config
12
+ @run_config = run_config
13
+ @context = context
14
+ end
15
+
16
+ def debug?
17
+ run_config.fetch("debug", nil)
18
+ end
19
+
20
+ def publish?
21
+ run_config.fetch("publish", true)
22
+ end
23
+
24
+ def http_port
25
+ run_config.fetch("http_port", DEFAULT_HTTP_PORT)
26
+ end
27
+
28
+ def https_port
29
+ run_config.fetch("https_port", DEFAULT_HTTPS_PORT)
30
+ end
31
+
32
+ def bind_ips
33
+ run_config.fetch("bind_ips", nil)
34
+ end
35
+
36
+ def publish_args
37
+ if publish?
38
+ (bind_ips || [ nil ]).map do |bind_ip|
39
+ bind_ip = format_bind_ip(bind_ip)
40
+ publish_http = [ bind_ip, http_port, DEFAULT_HTTP_PORT ].compact.join(":")
41
+ publish_https = [ bind_ip, https_port, DEFAULT_HTTPS_PORT ].compact.join(":")
42
+
43
+ argumentize "--publish", [ publish_http, publish_https ]
44
+ end.join(" ")
45
+ end
46
+ end
47
+
48
+ def log_max_size
49
+ run_config.fetch("log_max_size", DEFAULT_LOG_MAX_SIZE)
50
+ end
51
+
52
+ def logging_args
53
+ argumentize "--log-opt", "max-size=#{log_max_size}" if log_max_size.present?
54
+ end
55
+
56
+ def version
57
+ run_config.fetch("version", MINIMUM_VERSION)
58
+ end
59
+
60
+ def registry
61
+ run_config.fetch("registry", nil)
62
+ end
63
+
64
+ def repository
65
+ run_config.fetch("repository", "basecamp/kamal-proxy")
66
+ end
67
+
68
+ def image
69
+ "#{[ registry, repository ].compact.join("/")}:#{version}"
70
+ end
71
+
72
+ def container_name
73
+ "kamal-proxy"
74
+ end
75
+
76
+ def options_args
77
+ if args = run_config["options"]
78
+ optionize args
79
+ end
80
+ end
81
+
82
+ def run_command
83
+ [ "kamal-proxy", "run", *optionize(run_command_options) ].join(" ")
84
+ end
85
+
86
+ def metrics_port
87
+ run_config["metrics_port"]
88
+ end
89
+
90
+ def run_command_options
91
+ { debug: debug? || nil, "metrics-port": metrics_port }.compact
92
+ end
93
+
94
+ def docker_options_args
95
+ [
96
+ *apps_volume_args,
97
+ *publish_args,
98
+ *logging_args,
99
+ *("--expose=#{metrics_port}" if metrics_port.present?),
100
+ *options_args
101
+ ].compact
102
+ end
103
+
104
+ def host_directory
105
+ File.join config.run_directory, "proxy"
106
+ end
107
+
108
+ def apps_directory
109
+ File.join host_directory, "apps-config"
110
+ end
111
+
112
+ def apps_container_directory
113
+ "/home/kamal-proxy/.apps-config"
114
+ end
115
+
116
+ def apps_volume
117
+ Kamal::Configuration::Volume.new \
118
+ host_path: apps_directory,
119
+ container_path: apps_container_directory
120
+ end
121
+
122
+ def apps_volume_args
123
+ [ apps_volume.docker_args ]
124
+ end
125
+
126
+ def app_directory
127
+ File.join apps_directory, config.service_and_destination
128
+ end
129
+
130
+ def app_container_directory
131
+ File.join apps_container_directory, config.service_and_destination
132
+ end
133
+
134
+ private
135
+ def format_bind_ip(ip)
136
+ # Ensure IPv6 address inside square brackets - e.g. [::1]
137
+ if ip =~ Resolv::IPv6::Regex && ip !~ /\A\[.*\]\z/
138
+ "[#{ip}]"
139
+ else
140
+ ip
141
+ end
142
+ end
143
+ end
@@ -6,8 +6,7 @@ class Kamal::Configuration::Proxy
6
6
 
7
7
  delegate :argumentize, :optionize, to: Kamal::Utils
8
8
 
9
- attr_reader :config, :proxy_config, :role_name, :secrets
10
-
9
+ attr_reader :config, :proxy_config, :role_name, :run, :secrets
11
10
  def initialize(config:, proxy_config:, role_name: nil, secrets:, context: "proxy")
12
11
  @config = config
13
12
  @proxy_config = proxy_config
@@ -15,6 +14,7 @@ class Kamal::Configuration::Proxy
15
14
  @role_name = role_name
16
15
  @secrets = secrets
17
16
  validate! @proxy_config, with: Kamal::Configuration::Validator::Proxy, context: context
17
+ @run = Kamal::Configuration::Proxy::Run.new(config, run_config: @proxy_config["run"], context: "#{context}/run") if @proxy_config && @proxy_config["run"].present?
18
18
  end
19
19
 
20
20
  def app_port
@@ -63,6 +63,10 @@ class Kamal::Configuration::Proxy
63
63
  tls_path(config.proxy_boot.tls_container_directory, "key.pem") if custom_ssl_certificate?
64
64
  end
65
65
 
66
+ def path_prefixes
67
+ proxy_config["path_prefixes"] || proxy_config["path_prefix"]&.split(",") || []
68
+ end
69
+
66
70
  def deploy_options
67
71
  {
68
72
  host: hosts,
@@ -80,7 +84,7 @@ class Kamal::Configuration::Proxy
80
84
  "buffer-memory": proxy_config.dig("buffering", "memory"),
81
85
  "max-request-body": proxy_config.dig("buffering", "max_request_body"),
82
86
  "max-response-body": proxy_config.dig("buffering", "max_response_body"),
83
- "path-prefix": proxy_config.dig("path_prefix"),
87
+ "path-prefix": path_prefixes,
84
88
  "strip-path-prefix": proxy_config.dig("strip_path_prefix"),
85
89
  "forward-headers": proxy_config.dig("forward_headers"),
86
90
  "tls-redirect": proxy_config.dig("ssl_redirect"),
@@ -19,6 +19,14 @@ class Kamal::Configuration::Registry
19
19
  lookup("password")
20
20
  end
21
21
 
22
+ def local?
23
+ server.to_s.match?("^localhost[:$]")
24
+ end
25
+
26
+ def local_port
27
+ local? ? (server.split(":").last.to_i || 80) : nil
28
+ end
29
+
22
30
  private
23
31
  attr_reader :registry_config, :secrets
24
32
 
@@ -36,7 +36,7 @@ class Kamal::Configuration::Role
36
36
  end
37
37
 
38
38
  def env_tags(host)
39
- tagged_hosts.fetch(host).collect { |tag| config.env_tag(tag) }
39
+ tagged_hosts.fetch(host).collect { |tag| config.env_tag(tag) }.compact
40
40
  end
41
41
 
42
42
  def cmd
@@ -127,7 +127,7 @@ class Kamal::Configuration::Role
127
127
 
128
128
 
129
129
  def asset_path
130
- specializations["asset_path"] || config.asset_path
130
+ asset_path_config&.dig(0)
131
131
  end
132
132
 
133
133
  def assets?
@@ -137,10 +137,14 @@ class Kamal::Configuration::Role
137
137
  def asset_volume(version = config.version)
138
138
  if assets?
139
139
  Kamal::Configuration::Volume.new \
140
- host_path: asset_volume_directory(version), container_path: asset_path
140
+ host_path: asset_volume_directory(version), container_path: asset_path, options: asset_path_options
141
141
  end
142
142
  end
143
143
 
144
+ def asset_path_options
145
+ asset_path_config&.dig(1)
146
+ end
147
+
144
148
  def asset_extracted_directory(version = config.version)
145
149
  File.join config.assets_directory, "extracted", [ name, version ].join("-")
146
150
  end
@@ -219,4 +223,12 @@ class Kamal::Configuration::Role
219
223
  labels.merge!(specializations["labels"]) if specializations["labels"].present?
220
224
  end
221
225
  end
226
+
227
+ def asset_path_config
228
+ raw_path = specializations["asset_path"] || config.asset_path
229
+ return nil unless raw_path.present?
230
+
231
+ parts = raw_path.split(":", 2)
232
+ [ parts[0], parts[1] ]
233
+ end
222
234
  end
@@ -3,10 +3,11 @@ class Kamal::Configuration::Ssh
3
3
 
4
4
  include Kamal::Configuration::Validation
5
5
 
6
- attr_reader :ssh_config
6
+ attr_reader :ssh_config, :secrets
7
7
 
8
8
  def initialize(config:)
9
9
  @ssh_config = config.raw_config.ssh || {}
10
+ @secrets = config.secrets
10
11
  validate! ssh_config
11
12
  end
12
13
 
@@ -35,11 +36,25 @@ class Kamal::Configuration::Ssh
35
36
  end
36
37
 
37
38
  def key_data
38
- ssh_config["key_data"]
39
+ key_data = ssh_config["key_data"]
40
+ return unless key_data
41
+
42
+ key_data.map do |k|
43
+ if secrets.key?(k)
44
+ secrets[k]
45
+ else
46
+ warn "Inline key_data usage is deprecated and will be removed in Kamal 3. Please store your key_data in a secret."
47
+ k
48
+ end
49
+ end
50
+ end
51
+
52
+ def config
53
+ ssh_config["config"]
39
54
  end
40
55
 
41
56
  def options
42
- { user: user, port: port, proxy: proxy, logger: logger, keepalive: true, keepalive_interval: 30, keys_only: keys_only, keys: keys, key_data: key_data }.compact
57
+ { user: user, port: port, proxy: proxy, logger: logger, keepalive: true, keepalive_interval: 30, keys_only: keys_only, keys: keys, key_data: key_data, config: config }.compact
43
58
  end
44
59
 
45
60
  def to_h
@@ -16,6 +16,10 @@ class Kamal::Configuration::Sshkit
16
16
  sshkit_config.fetch("pool_idle_timeout", 900)
17
17
  end
18
18
 
19
+ def dns_retries
20
+ Integer(sshkit_config.fetch("dns_retries", 3))
21
+ end
22
+
19
23
  def to_h
20
24
  sshkit_config
21
25
  end
@@ -20,6 +20,26 @@ class Kamal::Configuration::Validator::Proxy < Kamal::Configuration::Validator
20
20
  error "Missing certificate_pem setting (required when private_key_pem is present)"
21
21
  end
22
22
  end
23
+
24
+ if run_config = config["run"]
25
+ if run_config["bind_ips"].present?
26
+ ensure_valid_bind_ips(config["bind_ips"])
27
+ end
28
+
29
+ if run_config["publish"] == false
30
+ if run_config["bind_ips"].present? || run_config["http_port"].present? || run_config["https_port"].present?
31
+ error "Cannot set http_port, https_port or bind_ips when publish is false"
32
+ end
33
+ end
34
+ end
23
35
  end
24
36
  end
37
+
38
+ private
39
+ def ensure_valid_bind_ips(bind_ips)
40
+ bind_ips.present? && bind_ips.each do |ip|
41
+ next if ip =~ Resolv::IPv4::Regex || ip =~ Resolv::IPv6::Regex
42
+ error "Invalid publish IP address: #{ip}"
43
+ end
44
+ end
25
45
  end
@@ -15,10 +15,12 @@ class Kamal::Configuration::Validator::Registry < Kamal::Configuration::Validato
15
15
  with_context(key) do
16
16
  value = config[key]
17
17
 
18
- error "is required" unless value.present?
18
+ unless config["server"]&.match?("^localhost[:$]")
19
+ error "is required" unless value.present?
19
20
 
20
- unless value.is_a?(String) || (value.is_a?(Array) && value.size == 1 && value.first.is_a?(String))
21
- error "should be a string or an array with one string (for secret lookup)"
21
+ unless value.is_a?(String) || (value.is_a?(Array) && value.size == 1 && value.first.is_a?(String))
22
+ error "should be a string or an array with one string (for secret lookup)"
23
+ end
22
24
  end
23
25
  end
24
26
  end