kamal 1.5.2 → 1.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/lib/kamal/cli/accessory.rb +30 -24
  3. data/lib/kamal/cli/app/boot.rb +70 -18
  4. data/lib/kamal/cli/app/prepare_assets.rb +1 -1
  5. data/lib/kamal/cli/app.rb +60 -47
  6. data/lib/kamal/cli/base.rb +26 -28
  7. data/lib/kamal/cli/build/clone.rb +61 -0
  8. data/lib/kamal/cli/build.rb +64 -53
  9. data/lib/kamal/cli/env.rb +5 -5
  10. data/lib/kamal/cli/healthcheck/barrier.rb +31 -0
  11. data/lib/kamal/cli/healthcheck/error.rb +2 -0
  12. data/lib/kamal/cli/healthcheck/poller.rb +6 -7
  13. data/lib/kamal/cli/main.rb +49 -44
  14. data/lib/kamal/cli/prune.rb +3 -3
  15. data/lib/kamal/cli/registry.rb +9 -10
  16. data/lib/kamal/cli/server.rb +39 -15
  17. data/lib/kamal/cli/templates/sample_hooks/docker-setup.sample +1 -1
  18. data/lib/kamal/cli/traefik.rb +13 -11
  19. data/lib/kamal/cli.rb +1 -1
  20. data/lib/kamal/commander.rb +6 -6
  21. data/lib/kamal/commands/accessory.rb +4 -4
  22. data/lib/kamal/commands/app/containers.rb +8 -0
  23. data/lib/kamal/commands/app/execution.rb +3 -3
  24. data/lib/kamal/commands/app/logging.rb +5 -5
  25. data/lib/kamal/commands/app.rb +6 -5
  26. data/lib/kamal/commands/base.rb +2 -3
  27. data/lib/kamal/commands/builder/base.rb +19 -12
  28. data/lib/kamal/commands/builder/clone.rb +28 -0
  29. data/lib/kamal/commands/builder/multiarch/remote.rb +10 -0
  30. data/lib/kamal/commands/builder/multiarch.rb +13 -9
  31. data/lib/kamal/commands/builder/native/cached.rb +14 -6
  32. data/lib/kamal/commands/builder/native/remote.rb +17 -9
  33. data/lib/kamal/commands/builder/native.rb +6 -7
  34. data/lib/kamal/commands/builder.rb +19 -11
  35. data/lib/kamal/commands/registry.rb +4 -13
  36. data/lib/kamal/commands/traefik.rb +8 -47
  37. data/lib/kamal/configuration/accessory.rb +30 -41
  38. data/lib/kamal/configuration/boot.rb +9 -4
  39. data/lib/kamal/configuration/builder.rb +61 -30
  40. data/lib/kamal/configuration/docs/accessory.yml +90 -0
  41. data/lib/kamal/configuration/docs/boot.yml +19 -0
  42. data/lib/kamal/configuration/docs/builder.yml +107 -0
  43. data/lib/kamal/configuration/docs/configuration.yml +157 -0
  44. data/lib/kamal/configuration/docs/env.yml +72 -0
  45. data/lib/kamal/configuration/docs/healthcheck.yml +59 -0
  46. data/lib/kamal/configuration/docs/logging.yml +21 -0
  47. data/lib/kamal/configuration/docs/registry.yml +49 -0
  48. data/lib/kamal/configuration/docs/role.yml +52 -0
  49. data/lib/kamal/configuration/docs/servers.yml +27 -0
  50. data/lib/kamal/configuration/docs/ssh.yml +46 -0
  51. data/lib/kamal/configuration/docs/sshkit.yml +23 -0
  52. data/lib/kamal/configuration/docs/traefik.yml +62 -0
  53. data/lib/kamal/configuration/env/tag.rb +12 -0
  54. data/lib/kamal/configuration/env.rb +10 -14
  55. data/lib/kamal/configuration/healthcheck.rb +63 -0
  56. data/lib/kamal/configuration/logging.rb +33 -0
  57. data/lib/kamal/configuration/registry.rb +31 -0
  58. data/lib/kamal/configuration/role.rb +72 -61
  59. data/lib/kamal/configuration/servers.rb +18 -0
  60. data/lib/kamal/configuration/ssh.rb +11 -8
  61. data/lib/kamal/configuration/sshkit.rb +9 -7
  62. data/lib/kamal/configuration/traefik.rb +60 -0
  63. data/lib/kamal/configuration/validation.rb +27 -0
  64. data/lib/kamal/configuration/validator/accessory.rb +9 -0
  65. data/lib/kamal/configuration/validator/builder.rb +9 -0
  66. data/lib/kamal/configuration/validator/env.rb +54 -0
  67. data/lib/kamal/configuration/validator/registry.rb +25 -0
  68. data/lib/kamal/configuration/validator/role.rb +11 -0
  69. data/lib/kamal/configuration/validator/servers.rb +7 -0
  70. data/lib/kamal/configuration/validator.rb +140 -0
  71. data/lib/kamal/configuration.rb +50 -63
  72. data/lib/kamal/git.rb +4 -0
  73. data/lib/kamal/sshkit_with_ext.rb +36 -0
  74. data/lib/kamal/version.rb +1 -1
  75. data/lib/kamal.rb +2 -0
  76. metadata +64 -9
  77. data/lib/kamal/cli/healthcheck.rb +0 -21
  78. data/lib/kamal/commands/healthcheck.rb +0 -59
@@ -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,12 +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
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"
10
31
  end
11
32
 
12
33
  def primary_host
@@ -14,7 +35,11 @@ class Kamal::Configuration::Role
14
35
  end
15
36
 
16
37
  def hosts
17
- @hosts ||= extract_hosts_from_config
38
+ tagged_hosts.keys
39
+ end
40
+
41
+ def env_tags(host)
42
+ tagged_hosts.fetch(host).collect { |tag| config.env_tag(tag) }
18
43
  end
19
44
 
20
45
  def cmd
@@ -38,24 +63,21 @@ class Kamal::Configuration::Role
38
63
  end
39
64
 
40
65
  def logging_args
41
- args = config.logging || {}
42
- args.deep_merge!(specializations["logging"]) if specializations["logging"].present?
66
+ logging.args
67
+ end
43
68
 
44
- if args.any?
45
- optionize({ "log-driver" => args["driver"] }.compact) +
46
- argumentize("--log-opt", args["options"])
47
- else
48
- config.logging_args
49
- end
69
+ def logging
70
+ @logging ||= config.logging.merge(specialized_logging)
50
71
  end
51
72
 
52
73
 
53
- def env
54
- @env ||= base_env.merge(specialized_env)
74
+ def env(host)
75
+ @envs ||= {}
76
+ @envs[host] ||= [ config.env, specialized_env, *env_tags(host).map(&:env) ].reduce(:merge)
55
77
  end
56
78
 
57
- def env_args
58
- env.args
79
+ def env_args(host)
80
+ env(host).args
59
81
  end
60
82
 
61
83
  def asset_volume_args
@@ -64,28 +86,29 @@ class Kamal::Configuration::Role
64
86
 
65
87
 
66
88
  def health_check_args(cord: true)
67
- if health_check_cmd.present?
89
+ if running_traefik? || healthcheck.set_port_or_path?
68
90
  if cord && uses_cord?
69
- 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 })
70
92
  .concat(cord_volume.docker_args)
71
93
  else
72
- optionize({ "health-cmd" => health_check_cmd, "health-interval" => health_check_interval })
94
+ optionize({ "health-cmd" => healthcheck.cmd, "health-interval" => healthcheck.interval })
73
95
  end
74
96
  else
75
97
  []
76
98
  end
77
99
  end
78
100
 
79
- def health_check_cmd
80
- 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
81
108
  end
82
109
 
83
110
  def health_check_cmd_with_cord
84
- "(#{health_check_cmd}) && (stat #{cord_container_file} > /dev/null || exit 1)"
85
- end
86
-
87
- def health_check_interval
88
- health_check_options["interval"] || "1s"
111
+ "(#{healthcheck.cmd}) && (stat #{cord_container_file} > /dev/null || exit 1)"
89
112
  end
90
113
 
91
114
 
@@ -103,7 +126,7 @@ class Kamal::Configuration::Role
103
126
 
104
127
 
105
128
  def uses_cord?
106
- running_traefik? && cord_volume && health_check_cmd.present?
129
+ running_traefik? && cord_volume && healthcheck.cmd.present?
107
130
  end
108
131
 
109
132
  def cord_host_directory
@@ -111,7 +134,7 @@ class Kamal::Configuration::Role
111
134
  end
112
135
 
113
136
  def cord_volume
114
- if (cord = health_check_options["cord"])
137
+ if (cord = healthcheck.cord)
115
138
  @cord_volume ||= Kamal::Configuration::Volume.new \
116
139
  host_path: File.join(config.run_directory, "cords", [ container_prefix, config.run_id ].join("-")),
117
140
  container_path: cord
@@ -164,13 +187,24 @@ class Kamal::Configuration::Role
164
187
  end
165
188
 
166
189
  private
167
- attr_accessor :config
190
+ def tagged_hosts
191
+ {}.tap do |tagged_hosts|
192
+ extract_hosts_from_config.map do |host_config|
193
+ if host_config.is_a?(Hash)
194
+ host, tags = host_config.first
195
+ tagged_hosts[host] = Array(tags)
196
+ elsif host_config.is_a?(String)
197
+ tagged_hosts[host_config] = []
198
+ end
199
+ end
200
+ end
201
+ end
168
202
 
169
203
  def extract_hosts_from_config
170
- if config.servers.is_a?(Array)
171
- config.servers
204
+ if config.raw_config.servers.is_a?(Array)
205
+ config.raw_config.servers
172
206
  else
173
- servers = config.servers[name]
207
+ servers = config.raw_config.servers[name]
174
208
  servers.is_a?(Array) ? servers : Array(servers["hosts"])
175
209
  end
176
210
  end
@@ -179,6 +213,14 @@ class Kamal::Configuration::Role
179
213
  { "service" => config.service, "role" => name, "destination" => config.destination }
180
214
  end
181
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
+
182
224
  def traefik_labels
183
225
  if running_traefik?
184
226
  {
@@ -206,35 +248,4 @@ class Kamal::Configuration::Role
206
248
  labels.merge!(specializations["labels"]) if specializations["labels"].present?
207
249
  end
208
250
  end
209
-
210
- def specializations
211
- if config.servers.is_a?(Array) || config.servers[name].is_a?(Array)
212
- {}
213
- else
214
- config.servers[name].except("hosts")
215
- end
216
- end
217
-
218
- def specialized_env
219
- Kamal::Configuration::Env.from_config config: specializations.fetch("env", {})
220
- end
221
-
222
- # Secrets are stored in an array, which won't merge by default, so have to do it by hand.
223
- def base_env
224
- Kamal::Configuration::Env.from_config \
225
- config: config.env,
226
- secrets_file: File.join(config.host_env_directory, "roles", "#{container_prefix}.env")
227
- end
228
-
229
- def http_health_check(port:, path:)
230
- "curl -f #{URI.join("http://localhost:#{port}", path)} || exit 1" if path.present? || port.present?
231
- end
232
-
233
- def health_check_options
234
- @health_check_options ||= begin
235
- options = specializations["healthcheck"] || {}
236
- options = config.healthcheck.merge(options) if running_traefik?
237
- options
238
- end
239
- end
240
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,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