kamal 2.5.3 → 2.6.1

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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/lib/kamal/cli/accessory.rb +16 -5
  3. data/lib/kamal/cli/app/{prepare_assets.rb → assets.rb} +1 -1
  4. data/lib/kamal/cli/app/boot.rb +1 -0
  5. data/lib/kamal/cli/app/error_pages.rb +33 -0
  6. data/lib/kamal/cli/app.rb +66 -22
  7. data/lib/kamal/cli/base.rb +13 -3
  8. data/lib/kamal/cli/build.rb +24 -4
  9. data/lib/kamal/cli/main.rb +4 -7
  10. data/lib/kamal/cli/proxy.rb +57 -22
  11. data/lib/kamal/cli/server.rb +4 -2
  12. data/lib/kamal/cli/templates/sample_hooks/post-deploy.sample +1 -1
  13. data/lib/kamal/cli/templates/sample_hooks/pre-build.sample +1 -1
  14. data/lib/kamal/cli/templates/sample_hooks/pre-connect.sample +1 -1
  15. data/lib/kamal/cli/templates/sample_hooks/pre-deploy.sample +6 -5
  16. data/lib/kamal/commander/specifics.rb +9 -1
  17. data/lib/kamal/commander.rb +2 -2
  18. data/lib/kamal/commands/accessory/proxy.rb +1 -1
  19. data/lib/kamal/commands/accessory.rb +2 -3
  20. data/lib/kamal/commands/app/error_pages.rb +9 -0
  21. data/lib/kamal/commands/app/proxy.rb +13 -1
  22. data/lib/kamal/commands/app.rb +1 -1
  23. data/lib/kamal/commands/auditor.rb +11 -5
  24. data/lib/kamal/commands/base.rb +4 -0
  25. data/lib/kamal/commands/builder/base.rb +2 -1
  26. data/lib/kamal/commands/proxy.rb +55 -15
  27. data/lib/kamal/configuration/accessory.rb +24 -2
  28. data/lib/kamal/configuration/docs/accessory.yml +6 -1
  29. data/lib/kamal/configuration/docs/configuration.yml +6 -0
  30. data/lib/kamal/configuration/docs/env.yml +31 -0
  31. data/lib/kamal/configuration/docs/proxy.yml +34 -5
  32. data/lib/kamal/configuration/env.rb +13 -4
  33. data/lib/kamal/configuration/proxy/boot.rb +121 -0
  34. data/lib/kamal/configuration/proxy.rb +19 -1
  35. data/lib/kamal/configuration/servers.rb +8 -1
  36. data/lib/kamal/configuration/validator/accessory.rb +4 -2
  37. data/lib/kamal/configuration/validator/role.rb +1 -0
  38. data/lib/kamal/configuration/validator/servers.rb +1 -1
  39. data/lib/kamal/configuration/validator.rb +6 -0
  40. data/lib/kamal/configuration.rb +36 -74
  41. data/lib/kamal/secrets/adapters/aws_secrets_manager.rb +1 -0
  42. data/lib/kamal/secrets/dotenv/inline_command_substitution.rb +2 -1
  43. data/lib/kamal/version.rb +1 -1
  44. metadata +9 -10
@@ -13,7 +13,7 @@
13
13
  # KAMAL_HOSTS
14
14
  # KAMAL_COMMAND
15
15
  # KAMAL_SUBCOMMAND
16
- # KAMAL_ROLE (if set)
16
+ # KAMAL_ROLES (if set)
17
17
  # KAMAL_DESTINATION (if set)
18
18
 
19
19
  # Only check the build status for production deployments
@@ -82,11 +82,12 @@ end
82
82
 
83
83
  $stdout.sync = true
84
84
 
85
- puts "Checking build status..."
86
- attempts = 0
87
- checks = GithubStatusChecks.new
88
-
89
85
  begin
86
+ puts "Checking build status..."
87
+
88
+ attempts = 0
89
+ checks = GithubStatusChecks.new
90
+
90
91
  loop do
91
92
  case checks.state
92
93
  when "success"
@@ -11,13 +11,17 @@ class Kamal::Commander::Specifics
11
11
  @primary_role = primary_or_first_role(roles_on(primary_host))
12
12
 
13
13
  stable_sort!(roles) { |role| role == primary_role ? 0 : 1 }
14
- stable_sort!(hosts) { |host| roles_on(host).any? { |role| role == primary_role } ? 0 : 1 }
14
+ sort_primary_role_hosts_first!(hosts)
15
15
  end
16
16
 
17
17
  def roles_on(host)
18
18
  roles.select { |role| role.hosts.include?(host.to_s) }
19
19
  end
20
20
 
21
+ def app_hosts
22
+ @app_hosts ||= sort_primary_role_hosts_first!(config.app_hosts & specified_hosts)
23
+ end
24
+
21
25
  def proxy_hosts
22
26
  config.proxy_hosts & specified_hosts
23
27
  end
@@ -51,4 +55,8 @@ class Kamal::Commander::Specifics
51
55
  specified_hosts
52
56
  end
53
57
  end
58
+
59
+ def sort_primary_role_hosts_first!(hosts)
60
+ stable_sort!(hosts) { |host| roles_on(host).any? { |role| role == primary_role } ? 0 : 1 }
61
+ end
54
62
  end
@@ -5,7 +5,7 @@ require "active_support/core_ext/object/blank"
5
5
  class Kamal::Commander
6
6
  attr_accessor :verbosity, :holding_lock, :connected
7
7
  attr_reader :specific_roles, :specific_hosts
8
- delegate :hosts, :roles, :primary_host, :primary_role, :roles_on, :proxy_hosts, :accessory_hosts, to: :specifics
8
+ delegate :hosts, :roles, :primary_host, :primary_role, :roles_on, :app_hosts, :proxy_hosts, :accessory_hosts, to: :specifics
9
9
 
10
10
  def initialize
11
11
  reset
@@ -13,7 +13,7 @@ class Kamal::Commander
13
13
 
14
14
  def reset
15
15
  self.verbosity = :info
16
- self.holding_lock = false
16
+ self.holding_lock = ENV["KAMAL_LOCK"] == "true"
17
17
  self.connected = false
18
18
  @specifics = @specific_roles = @specific_hosts = nil
19
19
  @config = @config_kwargs = nil
@@ -1,5 +1,5 @@
1
1
  module Kamal::Commands::Accessory::Proxy
2
- delegate :proxy_container_name, to: :config
2
+ delegate :container_name, to: :"config.proxy_boot", prefix: :proxy
3
3
 
4
4
  def deploy(target:)
5
5
  proxy_exec :deploy, service_name, *proxy.deploy_command_args(target: target)
@@ -6,7 +6,6 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base
6
6
  :network_args, :publish_args, :env_args, :volume_args, :label_args, :option_args,
7
7
  :secrets_io, :secrets_path, :env_directory, :proxy, :running_proxy?, :registry,
8
8
  to: :accessory_config
9
- delegate :proxy_container_name, to: :config
10
9
 
11
10
  def initialize(config, name:)
12
11
  super(config)
@@ -37,8 +36,8 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base
37
36
  docker :container, :stop, service_name
38
37
  end
39
38
 
40
- def info
41
- docker :ps, *service_filter
39
+ def info(all: false, quiet: false)
40
+ docker :ps, *("-a" if all), *("-q" if quiet), *service_filter
42
41
  end
43
42
 
44
43
  def logs(timestamps: true, since: nil, lines: nil, grep: nil, grep_options: nil)
@@ -0,0 +1,9 @@
1
+ module Kamal::Commands::App::ErrorPages
2
+ def create_error_pages_directory
3
+ make_directory(config.proxy_boot.error_pages_directory)
4
+ end
5
+
6
+ def clean_up_error_pages
7
+ [ :find, config.proxy_boot.error_pages_directory, "-mindepth", "1", "-maxdepth", "1", "!", "-name", KAMAL.config.version, "-exec", "rm", "-rf", "{} +" ]
8
+ end
9
+ end
@@ -1,5 +1,5 @@
1
1
  module Kamal::Commands::App::Proxy
2
- delegate :proxy_container_name, to: :config
2
+ delegate :container_name, to: :"config.proxy_boot", prefix: :proxy
3
3
 
4
4
  def deploy(target:)
5
5
  proxy_exec :deploy, role.container_prefix, *role.proxy.deploy_command_args(target: target)
@@ -9,6 +9,18 @@ module Kamal::Commands::App::Proxy
9
9
  proxy_exec :remove, role.container_prefix
10
10
  end
11
11
 
12
+ def live
13
+ proxy_exec :resume, role.container_prefix
14
+ end
15
+
16
+ def maintenance(**options)
17
+ proxy_exec :stop, role.container_prefix, *role.proxy.stop_command_args(**options)
18
+ end
19
+
20
+ def remove_proxy_app_directory
21
+ remove_directory config.proxy_boot.app_directory
22
+ end
23
+
12
24
  private
13
25
  def proxy_exec(*command)
14
26
  docker :exec, proxy_container_name, "kamal-proxy", *command
@@ -1,5 +1,5 @@
1
1
  class Kamal::Commands::App < Kamal::Commands::Base
2
- include Assets, Containers, Execution, Images, Logging, Proxy
2
+ include Assets, Containers, ErrorPages, Execution, Images, Logging, Proxy
3
3
 
4
4
  ACTIVE_DOCKER_STATUSES = [ :running, :restarting ]
5
5
 
@@ -1,5 +1,6 @@
1
1
  class Kamal::Commands::Auditor < Kamal::Commands::Base
2
2
  attr_reader :details
3
+ delegate :escape_shell_value, to: Kamal::Utils
3
4
 
4
5
  def initialize(config, **details)
5
6
  super(config)
@@ -9,11 +10,8 @@ class Kamal::Commands::Auditor < Kamal::Commands::Base
9
10
  # Runs remotely
10
11
  def record(line, **details)
11
12
  combine \
12
- [ :mkdir, "-p", config.run_directory ],
13
- append(
14
- [ :echo, audit_tags(**details).except(:version, :service_version, :service).to_s, line ],
15
- audit_log_file
16
- )
13
+ make_run_directory,
14
+ append([ :echo, escape_shell_value(audit_line(line, **details)) ], audit_log_file)
17
15
  end
18
16
 
19
17
  def reveal
@@ -30,4 +28,12 @@ class Kamal::Commands::Auditor < Kamal::Commands::Base
30
28
  def audit_tags(**details)
31
29
  tags(**self.details, **details)
32
30
  end
31
+
32
+ def make_run_directory
33
+ [ :mkdir, "-p", config.run_directory ]
34
+ end
35
+
36
+ def audit_line(line, **details)
37
+ "#{audit_tags(**details).except(:version, :service_version, :service)} #{line}"
38
+ end
33
39
  end
@@ -68,6 +68,10 @@ module Kamal::Commands
68
68
  combine *commands, by: "||"
69
69
  end
70
70
 
71
+ def substitute(*commands)
72
+ "\$\(#{commands.join(" ")}\)"
73
+ end
74
+
71
75
  def xargs(command)
72
76
  [ :xargs, command ].flatten
73
77
  end
@@ -20,7 +20,8 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
20
20
  *([ "--builder", builder_name ] unless docker_driver?),
21
21
  *build_tag_options(tag_as_dirty: tag_as_dirty),
22
22
  *build_options,
23
- build_context
23
+ build_context,
24
+ "2>&1"
24
25
  end
25
26
 
26
27
  def pull
@@ -2,14 +2,7 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base
2
2
  delegate :argumentize, :optionize, to: Kamal::Utils
3
3
 
4
4
  def run
5
- docker :run,
6
- "--name", container_name,
7
- "--network", "kamal",
8
- "--detach",
9
- "--restart", "unless-stopped",
10
- "--volume", "kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy",
11
- "\$\(#{get_boot_options.join(" ")}\)",
12
- config.proxy_image
5
+ pipe boot_config, xargs(docker_run)
13
6
  end
14
7
 
15
8
  def start
@@ -31,7 +24,7 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base
31
24
  def version
32
25
  pipe \
33
26
  docker(:inspect, container_name, "--format '{{.Config.Image}}'"),
34
- [ :cut, "-d:", "-f2" ]
27
+ [ :awk, "-F:", "'{print \$NF}'" ]
35
28
  end
36
29
 
37
30
  def logs(timestamps: true, since: nil, lines: nil, grep: nil, grep_options: nil)
@@ -65,23 +58,70 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base
65
58
  end
66
59
 
67
60
  def ensure_proxy_directory
68
- make_directory config.proxy_directory
61
+ make_directory config.proxy_boot.host_directory
69
62
  end
70
63
 
71
64
  def remove_proxy_directory
72
- remove_directory config.proxy_directory
65
+ remove_directory config.proxy_boot.host_directory
73
66
  end
74
67
 
75
- def get_boot_options
76
- combine [ :cat, config.proxy_options_file ], [ :echo, "\"#{config.proxy_options_default.join(" ")}\"" ], by: "||"
68
+ def ensure_apps_config_directory
69
+ make_directory config.proxy_boot.apps_directory
70
+ end
71
+
72
+ def boot_config
73
+ [ :echo, "#{substitute(read_boot_options)} #{substitute(read_image)}:#{substitute(read_image_version)} #{substitute(read_run_command)}" ]
74
+ end
75
+
76
+ def read_boot_options
77
+ read_file(config.proxy_boot.options_file, default: config.proxy_boot.default_boot_options.join(" "))
78
+ end
79
+
80
+ def read_image
81
+ read_file(config.proxy_boot.image_file, default: config.proxy_boot.image_default)
82
+ end
83
+
84
+ def read_image_version
85
+ read_file(config.proxy_boot.image_version_file, default: Kamal::Configuration::Proxy::Boot::MINIMUM_VERSION)
86
+ end
87
+
88
+ def read_run_command
89
+ read_file(config.proxy_boot.run_command_file)
77
90
  end
78
91
 
79
92
  def reset_boot_options
80
- remove_file config.proxy_options_file
93
+ remove_file config.proxy_boot.options_file
94
+ end
95
+
96
+ def reset_image
97
+ remove_file config.proxy_boot.image_file
98
+ end
99
+
100
+ def reset_image_version
101
+ remove_file config.proxy_boot.image_version_file
102
+ end
103
+
104
+ def reset_run_command
105
+ remove_file config.proxy_boot.run_command_file
81
106
  end
82
107
 
83
108
  private
84
109
  def container_name
85
- config.proxy_container_name
110
+ config.proxy_boot.container_name
111
+ end
112
+
113
+ def read_file(file, default: nil)
114
+ combine [ :cat, file, "2>", "/dev/null" ], [ :echo, "\"#{default}\"" ], by: "||"
115
+ end
116
+
117
+ def docker_run
118
+ docker \
119
+ :run,
120
+ "--name", container_name,
121
+ "--network", "kamal",
122
+ "--detach",
123
+ "--restart", "unless-stopped",
124
+ "--volume", "kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy",
125
+ *config.proxy_boot.apps_volume.docker_args
86
126
  end
87
127
  end
@@ -32,7 +32,7 @@ class Kamal::Configuration::Accessory
32
32
  end
33
33
 
34
34
  def hosts
35
- hosts_from_host || hosts_from_hosts || hosts_from_roles
35
+ hosts_from_host || hosts_from_hosts || hosts_from_roles || hosts_from_tags
36
36
  end
37
37
 
38
38
  def port
@@ -201,11 +201,31 @@ class Kamal::Configuration::Accessory
201
201
  end
202
202
 
203
203
  def hosts_from_roles
204
- if accessory_config.key?("roles")
204
+ if accessory_config.key?("role")
205
+ config.role(accessory_config["role"])&.hosts
206
+ elsif accessory_config.key?("roles")
205
207
  accessory_config["roles"].flat_map { |role| config.role(role)&.hosts }
206
208
  end
207
209
  end
208
210
 
211
+ def hosts_from_tags
212
+ if accessory_config.key?("tag")
213
+ extract_hosts_from_config_with_tag(accessory_config["tag"])
214
+ elsif accessory_config.key?("tags")
215
+ accessory_config["tags"].flat_map { |tag| extract_hosts_from_config_with_tag(tag) }
216
+ end
217
+ end
218
+
219
+ def extract_hosts_from_config_with_tag(tag)
220
+ if (servers_with_roles = config.raw_config.servers).is_a?(Hash)
221
+ servers_with_roles.flat_map do |role, servers_in_role|
222
+ servers_in_role.filter_map do |host|
223
+ host.keys.first if host.is_a?(Hash) && host.values.first.include?(tag)
224
+ end
225
+ end
226
+ end
227
+ end
228
+
209
229
  def network
210
230
  accessory_config["network"] || DEFAULT_NETWORK
211
231
  end
@@ -213,6 +233,8 @@ class Kamal::Configuration::Accessory
213
233
  def ensure_valid_roles
214
234
  if accessory_config["roles"] && (missing_roles = accessory_config["roles"] - config.roles.map(&:name)).any?
215
235
  raise Kamal::ConfigurationError, "accessories/#{name}: unknown roles #{missing_roles.join(", ")}"
236
+ elsif accessory_config["role"] && !config.role(accessory_config["role"])
237
+ raise Kamal::ConfigurationError, "accessories/#{name}: unknown role #{accessory_config["role"]}"
216
238
  end
217
239
  end
218
240
  end
@@ -46,13 +46,18 @@ accessories:
46
46
 
47
47
  # Accessory hosts
48
48
  #
49
- # Specify one of `host`, `hosts`, or `roles`:
49
+ # Specify one of `host`, `hosts`, `role`, `roles`, `tag` or `tags`:
50
50
  host: mysql-db1
51
51
  hosts:
52
52
  - mysql-db1
53
53
  - mysql-db2
54
+ role: mysql
54
55
  roles:
55
56
  - mysql
57
+ tag: writer
58
+ tags:
59
+ - writer
60
+ - reader
56
61
 
57
62
  # Custom command
58
63
  #
@@ -82,6 +82,12 @@ asset_path: /path/to/assets
82
82
  # See https://kamal-deploy.org/docs/hooks for more information:
83
83
  hooks_path: /user_home/kamal/hooks
84
84
 
85
+ # Error pages
86
+ #
87
+ # A directory relative to the app root to find error pages for the proxy to serve.
88
+ # Any files in the format 4xx.html or 5xx.html will be copied to the hosts.
89
+ error_pages_path: public
90
+
85
91
  # Require destinations
86
92
  #
87
93
  # Whether deployments require a destination to be specified, defaults to `false`:
@@ -51,6 +51,37 @@ env:
51
51
  secret:
52
52
  - DB_PASSWORD
53
53
 
54
+ # Aliased secrets
55
+ #
56
+ # You can also alias secrets to other secrets using a `:` separator.
57
+ #
58
+ # This is useful when the ENV name is different from the secret name. For example, if you have two
59
+ # places where you need to define the ENV variable `DB_PASSWORD`, but the value is different depending
60
+ # on the context.
61
+ #
62
+ # ```shell
63
+ # SECRETS=$(kamal secrets fetch ...)
64
+ #
65
+ # MAIN_DB_PASSWORD=$(kamal secrets extract MAIN_DB_PASSWORD $SECRETS)
66
+ # SECONDARY_DB_PASSWORD=$(kamal secrets extract SECONDARY_DB_PASSWORD $SECRETS)
67
+ # ```
68
+ env:
69
+ secret:
70
+ - DB_PASSWORD:MAIN_DB_PASSWORD
71
+ tags:
72
+ secondary_db:
73
+ secret:
74
+ - DB_PASSWORD:SECONDARY_DB_PASSWORD
75
+ accessories:
76
+ main_db_accessory:
77
+ env:
78
+ secret:
79
+ - DB_PASSWORD:MAIN_DB_PASSWORD
80
+ secondary_db_accessory:
81
+ env:
82
+ secret:
83
+ - DB_PASSWORD:SECONDARY_DB_PASSWORD
84
+
54
85
  # Tags
55
86
  #
56
87
  # Tags are used to add extra env variables to specific hosts.
@@ -10,11 +10,6 @@
10
10
  # They are application-specific, so they are not shared when multiple applications
11
11
  # run on the same proxy.
12
12
  #
13
- # The proxy is enabled by default on the primary role but can be disabled by
14
- # setting `proxy: false`.
15
- #
16
- # It is disabled by default on all other roles but can be enabled by setting
17
- # `proxy: true` or providing a proxy configuration.
18
13
  proxy:
19
14
 
20
15
  # Hosts
@@ -52,6 +47,13 @@ proxy:
52
47
  # Defaults to `false`:
53
48
  ssl: true
54
49
 
50
+ # SSL redirect
51
+ #
52
+ # By default, kamal-proxy will redirect all HTTP requests to HTTPS when SSL is enabled.
53
+ # If you prefer that HTTP traffic is passed through to your application (along with
54
+ # HTTPS traffic), you can disable this redirect by setting `ssl_redirect: false`:
55
+ ssl_redirect: false
56
+
55
57
  # Forward headers
56
58
  #
57
59
  # Whether to forward the `X-Forwarded-For` and `X-Forwarded-Proto` headers.
@@ -106,3 +108,30 @@ proxy:
106
108
  response_headers:
107
109
  - X-Request-ID
108
110
  - X-Request-Start
111
+
112
+ # Enabling/disabling the proxy on roles
113
+ #
114
+ # The proxy is enabled by default on the primary role but can be disabled by
115
+ # setting `proxy: false` in the primary role's configuration.
116
+ #
117
+ # ```yaml
118
+ # servers:
119
+ # web:
120
+ # hosts:
121
+ # - ...
122
+ # proxy: false
123
+ # ```
124
+ #
125
+ # It is disabled by default on all other roles but can be enabled by setting
126
+ # `proxy: true` or providing a proxy configuration for that role.
127
+ #
128
+ # ```yaml
129
+ # servers:
130
+ # web:
131
+ # hosts:
132
+ # - ...
133
+ # web2:
134
+ # hosts:
135
+ # - ...
136
+ # proxy: true
137
+ # ```
@@ -1,8 +1,7 @@
1
1
  class Kamal::Configuration::Env
2
2
  include Kamal::Configuration::Validation
3
3
 
4
- attr_reader :context, :secrets
5
- attr_reader :clear, :secret_keys
4
+ attr_reader :context, :clear, :secret_keys
6
5
  delegate :argumentize, to: Kamal::Utils
7
6
 
8
7
  def initialize(config:, secrets:, context: "env")
@@ -18,12 +17,22 @@ class Kamal::Configuration::Env
18
17
  end
19
18
 
20
19
  def secrets_io
21
- Kamal::EnvFile.new(secret_keys.to_h { |key| [ key, secrets[key] ] }).to_io
20
+ Kamal::EnvFile.new(aliased_secrets).to_io
22
21
  end
23
22
 
24
23
  def merge(other)
25
24
  self.class.new \
26
25
  config: { "clear" => clear.merge(other.clear), "secret" => secret_keys | other.secret_keys },
27
- secrets: secrets
26
+ secrets: @secrets
28
27
  end
28
+
29
+ private
30
+ def aliased_secrets
31
+ secret_keys.to_h { |key| extract_alias(key) }.transform_values { |secret_key| @secrets[secret_key] }
32
+ end
33
+
34
+ def extract_alias(key)
35
+ key_name, key_aliased_to = key.split(":", 2)
36
+ [ key_name, key_aliased_to || key_name ]
37
+ end
29
38
  end
@@ -0,0 +1,121 @@
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
+ attr_reader :config
8
+ delegate :argumentize, :optionize, to: Kamal::Utils
9
+
10
+ def initialize(config:)
11
+ @config = config
12
+ end
13
+
14
+ def publish_args(http_port, https_port, bind_ips = nil)
15
+ ensure_valid_bind_ips(bind_ips)
16
+
17
+ (bind_ips || [ nil ]).map do |bind_ip|
18
+ 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(":")
21
+
22
+ argumentize "--publish", [ publish_http, publish_https ]
23
+ end.join(" ")
24
+ end
25
+
26
+ def logging_args(max_size)
27
+ argumentize "--log-opt", "max-size=#{max_size}" if max_size.present?
28
+ end
29
+
30
+ def default_boot_options
31
+ [
32
+ *(publish_args(DEFAULT_HTTP_PORT, DEFAULT_HTTPS_PORT, nil)),
33
+ *(logging_args(DEFAULT_LOG_MAX_SIZE))
34
+ ]
35
+ end
36
+
37
+ def repository_name
38
+ "basecamp"
39
+ end
40
+
41
+ def image_name
42
+ "kamal-proxy"
43
+ end
44
+
45
+ def image_default
46
+ "#{repository_name}/#{image_name}"
47
+ end
48
+
49
+ def container_name
50
+ "kamal-proxy"
51
+ end
52
+
53
+ def host_directory
54
+ File.join config.run_directory, "proxy"
55
+ end
56
+
57
+ def options_file
58
+ File.join host_directory, "options"
59
+ end
60
+
61
+ def image_file
62
+ File.join host_directory, "image"
63
+ end
64
+
65
+ def image_version_file
66
+ File.join host_directory, "image_version"
67
+ end
68
+
69
+ def run_command_file
70
+ File.join host_directory, "run_command"
71
+ end
72
+
73
+ def apps_directory
74
+ File.join host_directory, "apps-config"
75
+ end
76
+
77
+ def apps_container_directory
78
+ "/home/kamal-proxy/.apps-config"
79
+ end
80
+
81
+ def apps_volume
82
+ Kamal::Configuration::Volume.new \
83
+ host_path: apps_directory,
84
+ container_path: apps_container_directory
85
+ end
86
+
87
+ def app_directory
88
+ File.join apps_directory, config.service_and_destination
89
+ end
90
+
91
+ def app_container_directory
92
+ File.join apps_container_directory, config.service_and_destination
93
+ end
94
+
95
+ def error_pages_directory
96
+ File.join app_directory, "error_pages"
97
+ end
98
+
99
+ def error_pages_container_directory
100
+ File.join app_container_directory, "error_pages"
101
+ end
102
+
103
+ private
104
+ def ensure_valid_bind_ips(bind_ips)
105
+ bind_ips.present? && bind_ips.each do |ip|
106
+ next if ip =~ Resolv::IPv4::Regex || ip =~ Resolv::IPv6::Regex
107
+ raise ArgumentError, "Invalid publish IP address: #{ip}"
108
+ end
109
+
110
+ true
111
+ end
112
+
113
+ def format_bind_ip(ip)
114
+ # Ensure IPv6 address inside square brackets - e.g. [::1]
115
+ if ip =~ Resolv::IPv6::Regex && ip !~ /\A\[.*\]\z/
116
+ "[#{ip}]"
117
+ else
118
+ ip
119
+ end
120
+ end
121
+ end
@@ -11,6 +11,7 @@ class Kamal::Configuration::Proxy
11
11
  def initialize(config:, proxy_config:, context: "proxy")
12
12
  @config = config
13
13
  @proxy_config = proxy_config
14
+ @proxy_config = {} if @proxy_config.nil?
14
15
  validate! @proxy_config, with: Kamal::Configuration::Validator::Proxy, context: context
15
16
  end
16
17
 
@@ -42,8 +43,10 @@ class Kamal::Configuration::Proxy
42
43
  "max-request-body": proxy_config.dig("buffering", "max_request_body"),
43
44
  "max-response-body": proxy_config.dig("buffering", "max_response_body"),
44
45
  "forward-headers": proxy_config.dig("forward_headers"),
46
+ "tls-redirect": proxy_config.dig("ssl_redirect"),
45
47
  "log-request-header": proxy_config.dig("logging", "request_headers") || DEFAULT_LOG_REQUEST_HEADERS,
46
- "log-response-header": proxy_config.dig("logging", "response_headers")
48
+ "log-response-header": proxy_config.dig("logging", "response_headers"),
49
+ "error-pages": error_pages
47
50
  }.compact
48
51
  end
49
52
 
@@ -51,6 +54,17 @@ class Kamal::Configuration::Proxy
51
54
  optionize ({ target: "#{target}:#{app_port}" }).merge(deploy_options), with: "="
52
55
  end
53
56
 
57
+ def stop_options(drain_timeout: nil, message: nil)
58
+ {
59
+ "drain-timeout": seconds_duration(drain_timeout),
60
+ message: message
61
+ }.compact
62
+ end
63
+
64
+ def stop_command_args(**options)
65
+ optionize stop_options(**options), with: "="
66
+ end
67
+
54
68
  def merge(other)
55
69
  self.class.new config: config, proxy_config: proxy_config.deep_merge(other.proxy_config)
56
70
  end
@@ -59,4 +73,8 @@ class Kamal::Configuration::Proxy
59
73
  def seconds_duration(value)
60
74
  value ? "#{value}s" : nil
61
75
  end
76
+
77
+ def error_pages
78
+ File.join config.proxy_boot.error_pages_container_directory, config.version if config.error_pages_path
79
+ end
62
80
  end