kamal 2.5.3 → 2.6.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 (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 +20 -4
  9. data/lib/kamal/cli/main.rb +4 -7
  10. data/lib/kamal/cli/proxy.rb +57 -10
  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 +4 -0
  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 +7 -0
  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 +18 -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
@@ -18,6 +18,10 @@ class Kamal::Commander::Specifics
18
18
  roles.select { |role| role.hosts.include?(host.to_s) }
19
19
  end
20
20
 
21
+ def app_hosts
22
+ config.app_hosts & specified_hosts
23
+ end
24
+
21
25
  def proxy_hosts
22
26
  config.proxy_hosts & specified_hosts
23
27
  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.
@@ -52,6 +52,13 @@ proxy:
52
52
  # Defaults to `false`:
53
53
  ssl: true
54
54
 
55
+ # SSL redirect
56
+ #
57
+ # By default, kamal-proxy will redirect all HTTP requests to HTTPS when SSL is enabled.
58
+ # If you prefer that HTTP traffic is passed through to your application (along with
59
+ # HTTPS traffic), you can disable this redirect by setting `ssl_redirect: false`:
60
+ ssl_redirect: false
61
+
55
62
  # Forward headers
56
63
  #
57
64
  # Whether to forward the `X-Forwarded-For` and `X-Forwarded-Proto` headers.
@@ -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
@@ -42,8 +42,10 @@ class Kamal::Configuration::Proxy
42
42
  "max-request-body": proxy_config.dig("buffering", "max_request_body"),
43
43
  "max-response-body": proxy_config.dig("buffering", "max_response_body"),
44
44
  "forward-headers": proxy_config.dig("forward_headers"),
45
+ "tls-redirect": proxy_config.dig("ssl_redirect"),
45
46
  "log-request-header": proxy_config.dig("logging", "request_headers") || DEFAULT_LOG_REQUEST_HEADERS,
46
- "log-response-header": proxy_config.dig("logging", "response_headers")
47
+ "log-response-header": proxy_config.dig("logging", "response_headers"),
48
+ "error-pages": error_pages
47
49
  }.compact
48
50
  end
49
51
 
@@ -51,6 +53,17 @@ class Kamal::Configuration::Proxy
51
53
  optionize ({ target: "#{target}:#{app_port}" }).merge(deploy_options), with: "="
52
54
  end
53
55
 
56
+ def stop_options(drain_timeout: nil, message: nil)
57
+ {
58
+ "drain-timeout": seconds_duration(drain_timeout),
59
+ message: message
60
+ }.compact
61
+ end
62
+
63
+ def stop_command_args(**options)
64
+ optionize stop_options(**options), with: "="
65
+ end
66
+
54
67
  def merge(other)
55
68
  self.class.new config: config, proxy_config: proxy_config.deep_merge(other.proxy_config)
56
69
  end
@@ -59,4 +72,8 @@ class Kamal::Configuration::Proxy
59
72
  def seconds_duration(value)
60
73
  value ? "#{value}s" : nil
61
74
  end
75
+
76
+ def error_pages
77
+ File.join config.proxy_boot.error_pages_container_directory, config.version if config.error_pages_path
78
+ end
62
79
  end
@@ -13,6 +13,13 @@ class Kamal::Configuration::Servers
13
13
 
14
14
  private
15
15
  def role_names
16
- servers_config.is_a?(Array) ? [ "web" ] : servers_config.keys.sort
16
+ case servers_config
17
+ when Array
18
+ [ "web" ]
19
+ when NilClass
20
+ []
21
+ else
22
+ servers_config.keys.sort
23
+ end
17
24
  end
18
25
  end
@@ -2,8 +2,10 @@ class Kamal::Configuration::Validator::Accessory < Kamal::Configuration::Validat
2
2
  def validate!
3
3
  super
4
4
 
5
- if (config.keys & [ "host", "hosts", "roles" ]).size != 1
6
- error "specify one of `host`, `hosts` or `roles`"
5
+ if (config.keys & [ "host", "hosts", "role", "roles", "tag", "tags" ]).size != 1
6
+ error "specify one of `host`, `hosts`, `role`, `roles`, `tag` or `tags`"
7
7
  end
8
+
9
+ validate_docker_options!(config["options"])
8
10
  end
9
11
  end
@@ -6,6 +6,7 @@ class Kamal::Configuration::Validator::Role < Kamal::Configuration::Validator
6
6
  validate_servers!(config)
7
7
  else
8
8
  super
9
+ validate_docker_options!(config["options"])
9
10
  end
10
11
  end
11
12
  end
@@ -1,6 +1,6 @@
1
1
  class Kamal::Configuration::Validator::Servers < Kamal::Configuration::Validator
2
2
  def validate!
3
- validate_type! config, Array, Hash
3
+ validate_type! config, Array, Hash, NilClass
4
4
 
5
5
  validate_servers! config if config.is_a?(Array)
6
6
  end
@@ -168,4 +168,10 @@ class Kamal::Configuration::Validator
168
168
  unknown_keys.reject! { |key| extension?(key) } if allow_extensions?
169
169
  unknown_keys_error unknown_keys if unknown_keys.present?
170
170
  end
171
+
172
+ def validate_docker_options!(options)
173
+ if options
174
+ error "Cannot set restart policy in docker options, unless-stopped is required" if options["restart"]
175
+ end
176
+ end
171
177
  end