kamal 1.6.0 → 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.
- 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
|