kamal 1.6.0 → 1.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/kamal/cli/accessory.rb +5 -3
- data/lib/kamal/cli/app.rb +6 -3
- data/lib/kamal/cli/build.rb +13 -10
- data/lib/kamal/cli/healthcheck/poller.rb +2 -2
- data/lib/kamal/cli/main.rb +14 -2
- data/lib/kamal/cli/registry.rb +9 -10
- data/lib/kamal/cli/templates/sample_hooks/docker-setup.sample +1 -1
- data/lib/kamal/cli/traefik.rb +5 -3
- data/lib/kamal/cli.rb +1 -1
- data/lib/kamal/commands/accessory.rb +4 -4
- data/lib/kamal/commands/app/logging.rb +4 -4
- data/lib/kamal/commands/builder/base.rb +13 -0
- data/lib/kamal/commands/builder/multiarch/remote.rb +10 -0
- data/lib/kamal/commands/builder/multiarch.rb +4 -0
- data/lib/kamal/commands/builder/native/cached.rb +10 -1
- data/lib/kamal/commands/builder/native/remote.rb +8 -0
- data/lib/kamal/commands/builder.rb +17 -11
- data/lib/kamal/commands/registry.rb +4 -13
- data/lib/kamal/commands/traefik.rb +8 -47
- data/lib/kamal/configuration/accessory.rb +30 -41
- data/lib/kamal/configuration/boot.rb +9 -4
- data/lib/kamal/configuration/builder.rb +33 -33
- data/lib/kamal/configuration/docs/accessory.yml +90 -0
- data/lib/kamal/configuration/docs/boot.yml +19 -0
- data/lib/kamal/configuration/docs/builder.yml +107 -0
- data/lib/kamal/configuration/docs/configuration.yml +157 -0
- data/lib/kamal/configuration/docs/env.yml +72 -0
- data/lib/kamal/configuration/docs/healthcheck.yml +59 -0
- data/lib/kamal/configuration/docs/logging.yml +21 -0
- data/lib/kamal/configuration/docs/registry.yml +49 -0
- data/lib/kamal/configuration/docs/role.yml +52 -0
- data/lib/kamal/configuration/docs/servers.yml +27 -0
- data/lib/kamal/configuration/docs/ssh.yml +46 -0
- data/lib/kamal/configuration/docs/sshkit.yml +23 -0
- data/lib/kamal/configuration/docs/traefik.yml +62 -0
- data/lib/kamal/configuration/env/tag.rb +1 -1
- data/lib/kamal/configuration/env.rb +10 -14
- data/lib/kamal/configuration/healthcheck.rb +63 -0
- data/lib/kamal/configuration/logging.rb +33 -0
- data/lib/kamal/configuration/registry.rb +31 -0
- data/lib/kamal/configuration/role.rb +53 -65
- data/lib/kamal/configuration/servers.rb +18 -0
- data/lib/kamal/configuration/ssh.rb +11 -8
- data/lib/kamal/configuration/sshkit.rb +9 -7
- data/lib/kamal/configuration/traefik.rb +60 -0
- data/lib/kamal/configuration/validation.rb +27 -0
- data/lib/kamal/configuration/validator/accessory.rb +9 -0
- data/lib/kamal/configuration/validator/builder.rb +9 -0
- data/lib/kamal/configuration/validator/env.rb +54 -0
- data/lib/kamal/configuration/validator/registry.rb +25 -0
- data/lib/kamal/configuration/validator/role.rb +11 -0
- data/lib/kamal/configuration/validator/servers.rb +7 -0
- data/lib/kamal/configuration/validator.rb +140 -0
- data/lib/kamal/configuration.rb +41 -66
- data/lib/kamal/version.rb +1 -1
- data/lib/kamal.rb +2 -0
- metadata +49 -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
|
-
|
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
|
-
|
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
|
47
|
-
|
66
|
+
logging.args
|
67
|
+
end
|
48
68
|
|
49
|
-
|
50
|
-
|
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] ||= [
|
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
|
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" =>
|
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" =>
|
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
|
86
|
-
|
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
|
-
"(#{
|
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 &&
|
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 =
|
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
|
-
|
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)
|
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
|
-
@
|
9
|
+
@ssh_config = config.raw_config.ssh || {}
|
10
|
+
validate! ssh_config
|
6
11
|
end
|
7
12
|
|
8
13
|
def user
|
9
|
-
|
14
|
+
ssh_config.fetch("user", "root")
|
10
15
|
end
|
11
16
|
|
12
17
|
def port
|
13
|
-
|
18
|
+
ssh_config.fetch("port", 22)
|
14
19
|
end
|
15
20
|
|
16
21
|
def proxy
|
17
|
-
if (proxy =
|
22
|
+
if (proxy = ssh_config["proxy"])
|
18
23
|
Net::SSH::Proxy::Jump.new(proxy.include?("@") ? proxy : "root@#{proxy}")
|
19
|
-
elsif (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
|
-
|
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
|
-
@
|
7
|
+
@sshkit_config = config.raw_config.sshkit || {}
|
8
|
+
validate! sshkit_config
|
4
9
|
end
|
5
10
|
|
6
11
|
def max_concurrent_starts
|
7
|
-
|
12
|
+
sshkit_config.fetch("max_concurrent_starts", 30)
|
8
13
|
end
|
9
14
|
|
10
15
|
def pool_idle_timeout
|
11
|
-
|
16
|
+
sshkit_config.fetch("pool_idle_timeout", 900)
|
12
17
|
end
|
13
18
|
|
14
19
|
def to_h
|
15
|
-
|
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::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
|