kamal 1.5.2 → 1.7.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 (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