kamal 1.6.0 → 1.7.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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/lib/kamal/cli/accessory.rb +5 -3
  3. data/lib/kamal/cli/app/boot.rb +2 -2
  4. data/lib/kamal/cli/app.rb +7 -4
  5. data/lib/kamal/cli/build.rb +13 -10
  6. data/lib/kamal/cli/healthcheck/poller.rb +2 -2
  7. data/lib/kamal/cli/main.rb +15 -2
  8. data/lib/kamal/cli/registry.rb +9 -10
  9. data/lib/kamal/cli/templates/sample_hooks/docker-setup.sample +1 -1
  10. data/lib/kamal/cli/traefik.rb +5 -3
  11. data/lib/kamal/cli.rb +1 -1
  12. data/lib/kamal/commands/accessory.rb +4 -4
  13. data/lib/kamal/commands/app/logging.rb +4 -4
  14. data/lib/kamal/commands/builder/base.rb +13 -0
  15. data/lib/kamal/commands/builder/multiarch/remote.rb +10 -0
  16. data/lib/kamal/commands/builder/multiarch.rb +4 -0
  17. data/lib/kamal/commands/builder/native/cached.rb +10 -1
  18. data/lib/kamal/commands/builder/native/remote.rb +8 -0
  19. data/lib/kamal/commands/builder.rb +17 -11
  20. data/lib/kamal/commands/registry.rb +4 -13
  21. data/lib/kamal/commands/traefik.rb +8 -47
  22. data/lib/kamal/configuration/accessory.rb +30 -41
  23. data/lib/kamal/configuration/boot.rb +9 -4
  24. data/lib/kamal/configuration/builder.rb +33 -33
  25. data/lib/kamal/configuration/docs/accessory.yml +90 -0
  26. data/lib/kamal/configuration/docs/boot.yml +19 -0
  27. data/lib/kamal/configuration/docs/builder.yml +107 -0
  28. data/lib/kamal/configuration/docs/configuration.yml +157 -0
  29. data/lib/kamal/configuration/docs/env.yml +72 -0
  30. data/lib/kamal/configuration/docs/healthcheck.yml +59 -0
  31. data/lib/kamal/configuration/docs/logging.yml +21 -0
  32. data/lib/kamal/configuration/docs/registry.yml +49 -0
  33. data/lib/kamal/configuration/docs/role.yml +52 -0
  34. data/lib/kamal/configuration/docs/servers.yml +27 -0
  35. data/lib/kamal/configuration/docs/ssh.yml +46 -0
  36. data/lib/kamal/configuration/docs/sshkit.yml +23 -0
  37. data/lib/kamal/configuration/docs/traefik.yml +62 -0
  38. data/lib/kamal/configuration/env/tag.rb +1 -1
  39. data/lib/kamal/configuration/env.rb +10 -14
  40. data/lib/kamal/configuration/healthcheck.rb +63 -0
  41. data/lib/kamal/configuration/logging.rb +33 -0
  42. data/lib/kamal/configuration/registry.rb +31 -0
  43. data/lib/kamal/configuration/role.rb +53 -65
  44. data/lib/kamal/configuration/servers.rb +18 -0
  45. data/lib/kamal/configuration/ssh.rb +11 -8
  46. data/lib/kamal/configuration/sshkit.rb +9 -7
  47. data/lib/kamal/configuration/traefik.rb +60 -0
  48. data/lib/kamal/configuration/validation.rb +27 -0
  49. data/lib/kamal/configuration/validator/accessory.rb +9 -0
  50. data/lib/kamal/configuration/validator/alias.rb +19 -0
  51. data/lib/kamal/configuration/validator/builder.rb +9 -0
  52. data/lib/kamal/configuration/validator/env.rb +54 -0
  53. data/lib/kamal/configuration/validator/registry.rb +25 -0
  54. data/lib/kamal/configuration/validator/role.rb +11 -0
  55. data/lib/kamal/configuration/validator/servers.rb +7 -0
  56. data/lib/kamal/configuration/validator.rb +140 -0
  57. data/lib/kamal/configuration.rb +41 -66
  58. data/lib/kamal/version.rb +1 -1
  59. data/lib/kamal.rb +2 -0
  60. metadata +50 -3
@@ -0,0 +1,33 @@
1
+ class Kamal::Configuration::Logging
2
+ delegate :optionize, :argumentize, to: Kamal::Utils
3
+
4
+ include Kamal::Configuration::Validation
5
+
6
+ attr_reader :logging_config
7
+
8
+ def initialize(logging_config:, context: "logging")
9
+ @logging_config = logging_config || {}
10
+ validate! @logging_config, context: context
11
+ end
12
+
13
+ def driver
14
+ logging_config["driver"]
15
+ end
16
+
17
+ def options
18
+ logging_config.fetch("options", {})
19
+ end
20
+
21
+ def merge(other)
22
+ self.class.new logging_config: logging_config.deep_merge(other.logging_config)
23
+ end
24
+
25
+ def args
26
+ if driver.present? || options.present?
27
+ optionize({ "log-driver" => driver }.compact) +
28
+ argumentize("--log-opt", options)
29
+ else
30
+ argumentize("--log-opt", { "max-size" => "10m" })
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,31 @@
1
+ class Kamal::Configuration::Registry
2
+ include Kamal::Configuration::Validation
3
+
4
+ attr_reader :registry_config
5
+
6
+ def initialize(config:)
7
+ @registry_config = config.raw_config.registry || {}
8
+ validate! registry_config, with: Kamal::Configuration::Validator::Registry
9
+ end
10
+
11
+ def server
12
+ registry_config["server"]
13
+ end
14
+
15
+ def username
16
+ lookup("username")
17
+ end
18
+
19
+ def password
20
+ lookup("password")
21
+ end
22
+
23
+ private
24
+ def lookup(key)
25
+ if registry_config[key].is_a?(Array)
26
+ ENV.fetch(registry_config[key].first).dup
27
+ else
28
+ registry_config[key]
29
+ end
30
+ end
31
+ end
@@ -1,13 +1,33 @@
1
1
  class Kamal::Configuration::Role
2
+ include Kamal::Configuration::Validation
3
+
2
4
  CORD_FILE = "cord"
3
5
  delegate :argumentize, :optionize, to: Kamal::Utils
4
6
 
5
- attr_accessor :name
7
+ attr_reader :name, :config, :specialized_env, :specialized_logging, :specialized_healthcheck
8
+
6
9
  alias to_s name
7
10
 
8
11
  def initialize(name, config:)
9
12
  @name, @config = name.inquiry, config
10
- @tagged_hosts ||= extract_tagged_hosts_from_config
13
+ validate! \
14
+ specializations,
15
+ example: validation_yml["servers"]["workers"],
16
+ context: "servers/#{name}",
17
+ with: Kamal::Configuration::Validator::Role
18
+
19
+ @specialized_env = Kamal::Configuration::Env.new \
20
+ config: specializations.fetch("env", {}),
21
+ secrets_file: File.join(config.host_env_directory, "roles", "#{container_prefix}.env"),
22
+ context: "servers/#{name}/env"
23
+
24
+ @specialized_logging = Kamal::Configuration::Logging.new \
25
+ logging_config: specializations.fetch("logging", {}),
26
+ context: "servers/#{name}/logging"
27
+
28
+ @specialized_healthcheck = Kamal::Configuration::Healthcheck.new \
29
+ healthcheck_config: specializations.fetch("healthcheck", {}),
30
+ context: "servers/#{name}/healthcheck"
11
31
  end
12
32
 
13
33
  def primary_host
@@ -43,21 +63,17 @@ class Kamal::Configuration::Role
43
63
  end
44
64
 
45
65
  def logging_args
46
- args = config.logging || {}
47
- args.deep_merge!(specializations["logging"]) if specializations["logging"].present?
66
+ logging.args
67
+ end
48
68
 
49
- if args.any?
50
- optionize({ "log-driver" => args["driver"] }.compact) +
51
- argumentize("--log-opt", args["options"])
52
- else
53
- config.logging_args
54
- end
69
+ def logging
70
+ @logging ||= config.logging.merge(specialized_logging)
55
71
  end
56
72
 
57
73
 
58
74
  def env(host)
59
75
  @envs ||= {}
60
- @envs[host] ||= [ base_env, specialized_env, *env_tags(host).map(&:env) ].reduce(:merge)
76
+ @envs[host] ||= [ config.env, specialized_env, *env_tags(host).map(&:env) ].reduce(:merge)
61
77
  end
62
78
 
63
79
  def env_args(host)
@@ -70,28 +86,29 @@ class Kamal::Configuration::Role
70
86
 
71
87
 
72
88
  def health_check_args(cord: true)
73
- if health_check_cmd.present?
89
+ if running_traefik? || healthcheck.set_port_or_path?
74
90
  if cord && uses_cord?
75
- optionize({ "health-cmd" => health_check_cmd_with_cord, "health-interval" => health_check_interval })
91
+ optionize({ "health-cmd" => health_check_cmd_with_cord, "health-interval" => healthcheck.interval })
76
92
  .concat(cord_volume.docker_args)
77
93
  else
78
- optionize({ "health-cmd" => health_check_cmd, "health-interval" => health_check_interval })
94
+ optionize({ "health-cmd" => healthcheck.cmd, "health-interval" => healthcheck.interval })
79
95
  end
80
96
  else
81
97
  []
82
98
  end
83
99
  end
84
100
 
85
- def health_check_cmd
86
- health_check_options["cmd"] || http_health_check(port: health_check_options["port"], path: health_check_options["path"])
101
+ def healthcheck
102
+ @healthcheck ||=
103
+ if running_traefik?
104
+ config.healthcheck.merge(specialized_healthcheck)
105
+ else
106
+ specialized_healthcheck
107
+ end
87
108
  end
88
109
 
89
110
  def health_check_cmd_with_cord
90
- "(#{health_check_cmd}) && (stat #{cord_container_file} > /dev/null || exit 1)"
91
- end
92
-
93
- def health_check_interval
94
- health_check_options["interval"] || "1s"
111
+ "(#{healthcheck.cmd}) && (stat #{cord_container_file} > /dev/null || exit 1)"
95
112
  end
96
113
 
97
114
 
@@ -109,7 +126,7 @@ class Kamal::Configuration::Role
109
126
 
110
127
 
111
128
  def uses_cord?
112
- running_traefik? && cord_volume && health_check_cmd.present?
129
+ running_traefik? && cord_volume && healthcheck.cmd.present?
113
130
  end
114
131
 
115
132
  def cord_host_directory
@@ -117,7 +134,7 @@ class Kamal::Configuration::Role
117
134
  end
118
135
 
119
136
  def cord_volume
120
- if (cord = health_check_options["cord"])
137
+ if (cord = healthcheck.cord)
121
138
  @cord_volume ||= Kamal::Configuration::Volume.new \
122
139
  host_path: File.join(config.run_directory, "cords", [ container_prefix, config.run_id ].join("-")),
123
140
  container_path: cord
@@ -170,30 +187,24 @@ class Kamal::Configuration::Role
170
187
  end
171
188
 
172
189
  private
173
- attr_accessor :config, :tagged_hosts
174
-
175
- def extract_tagged_hosts_from_config
190
+ def tagged_hosts
176
191
  {}.tap do |tagged_hosts|
177
192
  extract_hosts_from_config.map do |host_config|
178
193
  if host_config.is_a?(Hash)
179
- raise ArgumentError, "Multiple hosts found: #{host_config.inspect}" unless host_config.size == 1
180
-
181
194
  host, tags = host_config.first
182
195
  tagged_hosts[host] = Array(tags)
183
- elsif host_config.is_a?(String) || host_config.is_a?(Symbol)
196
+ elsif host_config.is_a?(String)
184
197
  tagged_hosts[host_config] = []
185
- else
186
- raise ArgumentError, "Invalid host config: #{host_config.inspect}"
187
198
  end
188
199
  end
189
200
  end
190
201
  end
191
202
 
192
203
  def extract_hosts_from_config
193
- if config.servers.is_a?(Array)
194
- config.servers
204
+ if config.raw_config.servers.is_a?(Array)
205
+ config.raw_config.servers
195
206
  else
196
- servers = config.servers[name]
207
+ servers = config.raw_config.servers[name]
197
208
  servers.is_a?(Array) ? servers : Array(servers["hosts"])
198
209
  end
199
210
  end
@@ -202,6 +213,14 @@ class Kamal::Configuration::Role
202
213
  { "service" => config.service, "role" => name, "destination" => config.destination }
203
214
  end
204
215
 
216
+ def specializations
217
+ if config.raw_config.servers.is_a?(Array) || config.raw_config.servers[name].is_a?(Array)
218
+ {}
219
+ else
220
+ config.raw_config.servers[name]
221
+ end
222
+ end
223
+
205
224
  def traefik_labels
206
225
  if running_traefik?
207
226
  {
@@ -229,35 +248,4 @@ class Kamal::Configuration::Role
229
248
  labels.merge!(specializations["labels"]) if specializations["labels"].present?
230
249
  end
231
250
  end
232
-
233
- def specializations
234
- if config.servers.is_a?(Array) || config.servers[name].is_a?(Array)
235
- {}
236
- else
237
- config.servers[name].except("hosts")
238
- end
239
- end
240
-
241
- def specialized_env
242
- Kamal::Configuration::Env.from_config config: specializations.fetch("env", {})
243
- end
244
-
245
- # Secrets are stored in an array, which won't merge by default, so have to do it by hand.
246
- def base_env
247
- Kamal::Configuration::Env.from_config \
248
- config: config.env,
249
- secrets_file: File.join(config.host_env_directory, "roles", "#{container_prefix}.env")
250
- end
251
-
252
- def http_health_check(port:, path:)
253
- "curl -f #{URI.join("http://localhost:#{port}", path)} || exit 1" if path.present? || port.present?
254
- end
255
-
256
- def health_check_options
257
- @health_check_options ||= begin
258
- options = specializations["healthcheck"] || {}
259
- options = config.healthcheck.merge(options) if running_traefik?
260
- options
261
- end
262
- end
263
251
  end
@@ -0,0 +1,18 @@
1
+ class Kamal::Configuration::Servers
2
+ include Kamal::Configuration::Validation
3
+
4
+ attr_reader :config, :servers_config, :roles
5
+
6
+ def initialize(config:)
7
+ @config = config
8
+ @servers_config = config.raw_config.servers
9
+ validate! servers_config, with: Kamal::Configuration::Validator::Servers
10
+
11
+ @roles = role_names.map { |role_name| Kamal::Configuration::Role.new role_name, config: config }
12
+ end
13
+
14
+ private
15
+ def role_names
16
+ servers_config.is_a?(Array) ? [ "web" ] : servers_config.keys.sort
17
+ end
18
+ end
@@ -1,22 +1,27 @@
1
1
  class Kamal::Configuration::Ssh
2
2
  LOGGER = ::Logger.new(STDERR)
3
3
 
4
+ include Kamal::Configuration::Validation
5
+
6
+ attr_reader :ssh_config
7
+
4
8
  def initialize(config:)
5
- @config = config.raw_config.ssh || {}
9
+ @ssh_config = config.raw_config.ssh || {}
10
+ validate! ssh_config
6
11
  end
7
12
 
8
13
  def user
9
- config.fetch("user", "root")
14
+ ssh_config.fetch("user", "root")
10
15
  end
11
16
 
12
17
  def port
13
- config.fetch("port", 22)
18
+ ssh_config.fetch("port", 22)
14
19
  end
15
20
 
16
21
  def proxy
17
- if (proxy = config["proxy"])
22
+ if (proxy = ssh_config["proxy"])
18
23
  Net::SSH::Proxy::Jump.new(proxy.include?("@") ? proxy : "root@#{proxy}")
19
- elsif (proxy_command = config["proxy_command"])
24
+ elsif (proxy_command = ssh_config["proxy_command"])
20
25
  Net::SSH::Proxy::Command.new(proxy_command)
21
26
  end
22
27
  end
@@ -30,13 +35,11 @@ class Kamal::Configuration::Ssh
30
35
  end
31
36
 
32
37
  private
33
- attr_accessor :config
34
-
35
38
  def logger
36
39
  LOGGER.tap { |logger| logger.level = log_level }
37
40
  end
38
41
 
39
42
  def log_level
40
- config.fetch("log_level", :fatal)
43
+ ssh_config.fetch("log_level", :fatal)
41
44
  end
42
45
  end
@@ -1,20 +1,22 @@
1
1
  class Kamal::Configuration::Sshkit
2
+ include Kamal::Configuration::Validation
3
+
4
+ attr_reader :sshkit_config
5
+
2
6
  def initialize(config:)
3
- @options = config.raw_config.sshkit || {}
7
+ @sshkit_config = config.raw_config.sshkit || {}
8
+ validate! sshkit_config
4
9
  end
5
10
 
6
11
  def max_concurrent_starts
7
- options.fetch("max_concurrent_starts", 30)
12
+ sshkit_config.fetch("max_concurrent_starts", 30)
8
13
  end
9
14
 
10
15
  def pool_idle_timeout
11
- options.fetch("pool_idle_timeout", 900)
16
+ sshkit_config.fetch("pool_idle_timeout", 900)
12
17
  end
13
18
 
14
19
  def to_h
15
- options
20
+ sshkit_config
16
21
  end
17
-
18
- private
19
- attr_accessor :options
20
22
  end
@@ -0,0 +1,60 @@
1
+ class Kamal::Configuration::Traefik
2
+ DEFAULT_IMAGE = "traefik:v2.10"
3
+ CONTAINER_PORT = 80
4
+ DEFAULT_ARGS = {
5
+ "log.level" => "DEBUG"
6
+ }
7
+ DEFAULT_LABELS = {
8
+ # These ensure we serve a 502 rather than a 404 if no containers are available
9
+ "traefik.http.routers.catchall.entryPoints" => "http",
10
+ "traefik.http.routers.catchall.rule" => "PathPrefix(`/`)",
11
+ "traefik.http.routers.catchall.service" => "unavailable",
12
+ "traefik.http.routers.catchall.priority" => 1,
13
+ "traefik.http.services.unavailable.loadbalancer.server.port" => "0"
14
+ }
15
+
16
+ include Kamal::Configuration::Validation
17
+
18
+ attr_reader :config, :traefik_config
19
+
20
+ def initialize(config:)
21
+ @config = config
22
+ @traefik_config = config.raw_config.traefik || {}
23
+ validate! traefik_config
24
+ end
25
+
26
+ def publish?
27
+ traefik_config["publish"] != false
28
+ end
29
+
30
+ def labels
31
+ DEFAULT_LABELS.merge(traefik_config["labels"] || {})
32
+ end
33
+
34
+ def env
35
+ Kamal::Configuration::Env.new \
36
+ config: traefik_config.fetch("env", {}),
37
+ secrets_file: File.join(config.host_env_directory, "traefik", "traefik.env"),
38
+ context: "traefik/env"
39
+ end
40
+
41
+ def host_port
42
+ traefik_config.fetch("host_port", CONTAINER_PORT)
43
+ end
44
+
45
+ def options
46
+ traefik_config.fetch("options", {})
47
+ end
48
+
49
+ def port
50
+ "#{host_port}:#{CONTAINER_PORT}"
51
+ end
52
+
53
+ def args
54
+ DEFAULT_ARGS.merge(traefik_config.fetch("args", {}))
55
+ end
56
+
57
+ def image
58
+ traefik_config.fetch("image", DEFAULT_IMAGE)
59
+ end
60
+ end
@@ -0,0 +1,27 @@
1
+ require "yaml"
2
+ require "active_support/inflector"
3
+
4
+ module Kamal::Configuration::Validation
5
+ extend ActiveSupport::Concern
6
+
7
+ class_methods do
8
+ def validation_doc
9
+ @validation_doc ||= File.read(File.join(File.dirname(__FILE__), "docs", "#{validation_config_key}.yml"))
10
+ end
11
+
12
+ def validation_config_key
13
+ @validation_config_key ||= name.demodulize.underscore
14
+ end
15
+ end
16
+
17
+ def validate!(config, example: nil, context: nil, with: Kamal::Configuration::Validator)
18
+ context ||= self.class.validation_config_key
19
+ example ||= validation_yml[self.class.validation_config_key]
20
+
21
+ with.new(config, example: example, context: context).validate!
22
+ end
23
+
24
+ def validation_yml
25
+ @validation_yml ||= YAML.load(self.class.validation_doc)
26
+ end
27
+ end
@@ -0,0 +1,9 @@
1
+ class Kamal::Configuration::Validator::Accessory < Kamal::Configuration::Validator
2
+ def validate!
3
+ super
4
+
5
+ if (config.keys & [ "host", "hosts", "roles" ]).size != 1
6
+ error "specify one of `host`, `hosts` or `roles`"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,19 @@
1
+ class Kamal::Configuration::Validator::Alias < Kamal::Configuration::Validator
2
+ def validate!
3
+ super
4
+
5
+ name = context.delete_prefix("aliases/")
6
+
7
+ if name !~ /\A[a-z0-9_-]+\z/
8
+ error "Invalid alias name: '#{name}'. Must only contain lowercase letters, alphanumeric, hyphens and underscores."
9
+ end
10
+
11
+ if Kamal::Cli::Main.original_commands.include?(name)
12
+ error "Alias '#{name}' conflicts with a built-in command."
13
+ end
14
+
15
+ if config["command"].empty?
16
+ error "Alias '#{name}': command is required."
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,9 @@
1
+ class Kamal::Configuration::Validator::Builder < Kamal::Configuration::Validator
2
+ def validate!
3
+ super
4
+
5
+ if config["cache"] && config["cache"]["type"]
6
+ error "Invalid cache type: #{config["cache"]["type"]}" unless [ "gha", "registry" ].include?(config["cache"]["type"])
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,54 @@
1
+ class Kamal::Configuration::Validator::Env < Kamal::Configuration::Validator
2
+ SPECIAL_KEYS = [ "clear", "secret", "tags" ]
3
+
4
+ def validate!
5
+ if known_keys.any?
6
+ validate_complex_env!
7
+ else
8
+ validate_simple_env!
9
+ end
10
+ end
11
+
12
+ private
13
+ def validate_simple_env!
14
+ validate_hash_of!(config, String)
15
+ end
16
+
17
+ def validate_complex_env!
18
+ unknown_keys_error unknown_keys if unknown_keys.any?
19
+
20
+ with_context("clear") { validate_hash_of!(config["clear"], String) } if config.key?("clear")
21
+ with_context("secret") { validate_array_of!(config["secret"], String) } if config.key?("secret")
22
+ validate_tags! if config.key?("tags")
23
+ end
24
+
25
+ def known_keys
26
+ @known_keys ||= config.keys & SPECIAL_KEYS
27
+ end
28
+
29
+ def unknown_keys
30
+ @unknown_keys ||= config.keys - SPECIAL_KEYS
31
+ end
32
+
33
+ def validate_tags!
34
+ if context == "env"
35
+ with_context("tags") do
36
+ validate_type! config["tags"], Hash
37
+
38
+ config["tags"].each do |tag, value|
39
+ with_context(tag) do
40
+ validate_type! value, Hash
41
+
42
+ Kamal::Configuration::Validator::Env.new(
43
+ value,
44
+ example: example["tags"].values[1],
45
+ context: context
46
+ ).validate!
47
+ end
48
+ end
49
+ end
50
+ else
51
+ error "tags are only allowed in the root env"
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,25 @@
1
+ class Kamal::Configuration::Validator::Registry < Kamal::Configuration::Validator
2
+ STRING_OR_ONE_ITEM_ARRAY_KEYS = [ "username", "password" ]
3
+
4
+ def validate!
5
+ validate_against_example! \
6
+ config.except(*STRING_OR_ONE_ITEM_ARRAY_KEYS),
7
+ example.except(*STRING_OR_ONE_ITEM_ARRAY_KEYS)
8
+
9
+ validate_string_or_one_item_array! "username"
10
+ validate_string_or_one_item_array! "password"
11
+ end
12
+
13
+ private
14
+ def validate_string_or_one_item_array!(key)
15
+ with_context(key) do
16
+ value = config[key]
17
+
18
+ error "is required" unless value.present?
19
+
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)"
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,11 @@
1
+ class Kamal::Configuration::Validator::Role < Kamal::Configuration::Validator
2
+ def validate!
3
+ validate_type! config, Array, Hash
4
+
5
+ if config.is_a?(Array)
6
+ validate_servers! "servers", config
7
+ else
8
+ super
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,7 @@
1
+ class Kamal::Configuration::Validator::Servers < Kamal::Configuration::Validator
2
+ def validate!
3
+ validate_type! config, Array, Hash
4
+
5
+ validate_servers! config if config.is_a?(Array)
6
+ end
7
+ end