kamal 1.8.3 → 2.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 (130) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/lib/kamal/cli/accessory.rb +92 -38
  4. data/lib/kamal/cli/alias/command.rb +10 -0
  5. data/lib/kamal/cli/app/{prepare_assets.rb → assets.rb} +1 -1
  6. data/lib/kamal/cli/app/boot.rb +23 -16
  7. data/lib/kamal/cli/app/error_pages.rb +33 -0
  8. data/lib/kamal/cli/app/ssl_certificates.rb +28 -0
  9. data/lib/kamal/cli/app.rb +132 -30
  10. data/lib/kamal/cli/base.rb +57 -53
  11. data/lib/kamal/cli/build.rb +81 -38
  12. data/lib/kamal/cli/healthcheck/barrier.rb +2 -0
  13. data/lib/kamal/cli/healthcheck/poller.rb +18 -39
  14. data/lib/kamal/cli/lock.rb +2 -3
  15. data/lib/kamal/cli/main.rb +60 -59
  16. data/lib/kamal/cli/proxy.rb +290 -0
  17. data/lib/kamal/cli/prune.rb +0 -1
  18. data/lib/kamal/cli/registry.rb +2 -0
  19. data/lib/kamal/cli/secrets.rb +49 -0
  20. data/lib/kamal/cli/server.rb +6 -5
  21. data/lib/kamal/cli/templates/deploy.yml +53 -53
  22. data/lib/kamal/cli/templates/sample_hooks/docker-setup.sample +2 -12
  23. data/lib/kamal/cli/templates/sample_hooks/post-app-boot.sample +3 -0
  24. data/lib/kamal/cli/templates/sample_hooks/post-deploy.sample +1 -1
  25. data/lib/kamal/cli/templates/sample_hooks/post-proxy-reboot.sample +3 -0
  26. data/lib/kamal/cli/templates/sample_hooks/pre-app-boot.sample +3 -0
  27. data/lib/kamal/cli/templates/sample_hooks/pre-build.sample +1 -1
  28. data/lib/kamal/cli/templates/sample_hooks/pre-connect.sample +1 -1
  29. data/lib/kamal/cli/templates/sample_hooks/pre-deploy.sample +19 -6
  30. data/lib/kamal/cli/templates/sample_hooks/pre-proxy-reboot.sample +3 -0
  31. data/lib/kamal/cli/templates/secrets +17 -0
  32. data/lib/kamal/cli.rb +2 -0
  33. data/lib/kamal/commander/specifics.rb +19 -6
  34. data/lib/kamal/commander.rb +39 -32
  35. data/lib/kamal/commands/accessory/proxy.rb +16 -0
  36. data/lib/kamal/commands/accessory.rb +19 -19
  37. data/lib/kamal/commands/app/assets.rb +10 -10
  38. data/lib/kamal/commands/app/containers.rb +2 -2
  39. data/lib/kamal/commands/app/error_pages.rb +9 -0
  40. data/lib/kamal/commands/app/execution.rb +7 -4
  41. data/lib/kamal/commands/app/images.rb +1 -1
  42. data/lib/kamal/commands/app/logging.rb +16 -6
  43. data/lib/kamal/commands/app/proxy.rb +32 -0
  44. data/lib/kamal/commands/app.rb +25 -24
  45. data/lib/kamal/commands/auditor.rb +12 -3
  46. data/lib/kamal/commands/base.rb +54 -8
  47. data/lib/kamal/commands/builder/base.rb +46 -16
  48. data/lib/kamal/commands/builder/clone.rb +16 -14
  49. data/lib/kamal/commands/builder/cloud.rb +22 -0
  50. data/lib/kamal/commands/builder/hybrid.rb +21 -0
  51. data/lib/kamal/commands/builder/local.rb +14 -0
  52. data/lib/kamal/commands/builder/pack.rb +46 -0
  53. data/lib/kamal/commands/builder/remote.rb +63 -0
  54. data/lib/kamal/commands/builder.rb +21 -45
  55. data/lib/kamal/commands/docker.rb +4 -0
  56. data/lib/kamal/commands/hook.rb +8 -2
  57. data/lib/kamal/commands/lock.rb +2 -6
  58. data/lib/kamal/commands/proxy.rb +127 -0
  59. data/lib/kamal/commands/prune.rb +1 -9
  60. data/lib/kamal/commands/registry.rb +9 -7
  61. data/lib/kamal/commands/server.rb +11 -1
  62. data/lib/kamal/configuration/accessory.rb +89 -12
  63. data/lib/kamal/configuration/alias.rb +15 -0
  64. data/lib/kamal/configuration/builder.rb +73 -15
  65. data/lib/kamal/configuration/docs/accessory.yml +53 -15
  66. data/lib/kamal/configuration/docs/alias.yml +26 -0
  67. data/lib/kamal/configuration/docs/boot.yml +3 -3
  68. data/lib/kamal/configuration/docs/builder.yml +63 -38
  69. data/lib/kamal/configuration/docs/configuration.yml +62 -46
  70. data/lib/kamal/configuration/docs/env.yml +61 -17
  71. data/lib/kamal/configuration/docs/logging.yml +3 -3
  72. data/lib/kamal/configuration/docs/proxy.yml +168 -0
  73. data/lib/kamal/configuration/docs/registry.yml +20 -13
  74. data/lib/kamal/configuration/docs/role.yml +14 -13
  75. data/lib/kamal/configuration/docs/servers.yml +2 -2
  76. data/lib/kamal/configuration/docs/ssh.yml +23 -19
  77. data/lib/kamal/configuration/docs/sshkit.yml +4 -4
  78. data/lib/kamal/configuration/env/tag.rb +4 -3
  79. data/lib/kamal/configuration/env.rb +19 -17
  80. data/lib/kamal/configuration/proxy/boot.rb +129 -0
  81. data/lib/kamal/configuration/proxy.rb +124 -0
  82. data/lib/kamal/configuration/registry.rb +7 -6
  83. data/lib/kamal/configuration/role.rb +69 -98
  84. data/lib/kamal/configuration/servers.rb +8 -1
  85. data/lib/kamal/configuration/validator/accessory.rb +6 -2
  86. data/lib/kamal/configuration/validator/alias.rb +15 -0
  87. data/lib/kamal/configuration/validator/builder.rb +6 -0
  88. data/lib/kamal/configuration/validator/proxy.rb +25 -0
  89. data/lib/kamal/configuration/validator/role.rb +3 -1
  90. data/lib/kamal/configuration/validator/servers.rb +1 -1
  91. data/lib/kamal/configuration/validator.rb +62 -24
  92. data/lib/kamal/configuration.rb +96 -50
  93. data/lib/kamal/docker.rb +30 -0
  94. data/lib/kamal/env_file.rb +7 -1
  95. data/lib/kamal/git.rb +10 -0
  96. data/lib/kamal/secrets/adapters/aws_secrets_manager.rb +51 -0
  97. data/lib/kamal/secrets/adapters/base.rb +33 -0
  98. data/lib/kamal/secrets/adapters/bitwarden.rb +81 -0
  99. data/lib/kamal/secrets/adapters/bitwarden_secrets_manager.rb +66 -0
  100. data/lib/kamal/secrets/adapters/doppler.rb +57 -0
  101. data/lib/kamal/secrets/adapters/enpass.rb +71 -0
  102. data/lib/kamal/secrets/adapters/gcp_secret_manager.rb +112 -0
  103. data/lib/kamal/secrets/adapters/last_pass.rb +40 -0
  104. data/lib/kamal/secrets/adapters/one_password.rb +104 -0
  105. data/lib/kamal/secrets/adapters/passbolt.rb +130 -0
  106. data/lib/kamal/secrets/adapters/test.rb +14 -0
  107. data/lib/kamal/secrets/adapters.rb +16 -0
  108. data/lib/kamal/secrets/dotenv/inline_command_substitution.rb +33 -0
  109. data/lib/kamal/secrets.rb +42 -0
  110. data/lib/kamal/sshkit_with_ext.rb +1 -0
  111. data/lib/kamal/utils.rb +30 -0
  112. data/lib/kamal/version.rb +1 -1
  113. data/lib/kamal.rb +3 -1
  114. metadata +63 -36
  115. data/lib/kamal/cli/env.rb +0 -54
  116. data/lib/kamal/cli/templates/sample_hooks/post-traefik-reboot.sample +0 -3
  117. data/lib/kamal/cli/templates/sample_hooks/pre-traefik-reboot.sample +0 -3
  118. data/lib/kamal/cli/templates/template.env +0 -2
  119. data/lib/kamal/cli/traefik.rb +0 -122
  120. data/lib/kamal/commands/app/cord.rb +0 -22
  121. data/lib/kamal/commands/builder/multiarch/remote.rb +0 -65
  122. data/lib/kamal/commands/builder/multiarch.rb +0 -41
  123. data/lib/kamal/commands/builder/native/cached.rb +0 -25
  124. data/lib/kamal/commands/builder/native/remote.rb +0 -67
  125. data/lib/kamal/commands/builder/native.rb +0 -20
  126. data/lib/kamal/commands/traefik.rb +0 -85
  127. data/lib/kamal/configuration/docs/healthcheck.yml +0 -59
  128. data/lib/kamal/configuration/docs/traefik.yml +0 -62
  129. data/lib/kamal/configuration/healthcheck.rb +0 -63
  130. data/lib/kamal/configuration/traefik.rb +0 -60
@@ -1,33 +1,30 @@
1
1
  class Kamal::Configuration::Role
2
2
  include Kamal::Configuration::Validation
3
3
 
4
- CORD_FILE = "cord"
5
4
  delegate :argumentize, :optionize, to: Kamal::Utils
6
5
 
7
- attr_reader :name, :config, :specialized_env, :specialized_logging, :specialized_healthcheck
6
+ attr_reader :name, :config, :specialized_env, :specialized_logging, :specialized_proxy
8
7
 
9
8
  alias to_s name
10
9
 
11
10
  def initialize(name, config:)
12
11
  @name, @config = name.inquiry, config
13
12
  validate! \
14
- specializations,
13
+ role_config,
15
14
  example: validation_yml["servers"]["workers"],
16
15
  context: "servers/#{name}",
17
16
  with: Kamal::Configuration::Validator::Role
18
17
 
19
18
  @specialized_env = Kamal::Configuration::Env.new \
20
19
  config: specializations.fetch("env", {}),
21
- secrets_file: File.join(config.host_env_directory, "roles", "#{container_prefix}.env"),
20
+ secrets: config.secrets,
22
21
  context: "servers/#{name}/env"
23
22
 
24
23
  @specialized_logging = Kamal::Configuration::Logging.new \
25
24
  logging_config: specializations.fetch("logging", {}),
26
25
  context: "servers/#{name}/logging"
27
26
 
28
- @specialized_healthcheck = Kamal::Configuration::Healthcheck.new \
29
- healthcheck_config: specializations.fetch("healthcheck", {}),
30
- context: "servers/#{name}/healthcheck"
27
+ initialize_specialized_proxy
31
28
  end
32
29
 
33
30
  def primary_host
@@ -55,7 +52,7 @@ class Kamal::Configuration::Role
55
52
  end
56
53
 
57
54
  def labels
58
- default_labels.merge(traefik_labels).merge(custom_labels)
55
+ default_labels.merge(custom_labels)
59
56
  end
60
57
 
61
58
  def label_args
@@ -70,87 +67,53 @@ class Kamal::Configuration::Role
70
67
  @logging ||= config.logging.merge(specialized_logging)
71
68
  end
72
69
 
73
-
74
- def env(host)
75
- @envs ||= {}
76
- @envs[host] ||= [ config.env, specialized_env, *env_tags(host).map(&:env) ].reduce(:merge)
70
+ def proxy
71
+ @proxy ||= specialized_proxy.merge(config.proxy) if running_proxy?
77
72
  end
78
73
 
79
- def env_args(host)
80
- env(host).args
74
+ def running_proxy?
75
+ @running_proxy
81
76
  end
82
77
 
83
- def asset_volume_args
84
- asset_volume&.docker_args
78
+ def ssl?
79
+ running_proxy? && proxy.ssl?
85
80
  end
86
81
 
82
+ def stop_args
83
+ # When deploying with the proxy, kamal-proxy will drain request before returning so we don't need to wait.
84
+ timeout = running_proxy? ? nil : config.drain_timeout
87
85
 
88
- def health_check_args(cord: true)
89
- if running_traefik? || healthcheck.set_port_or_path?
90
- if cord && uses_cord?
91
- optionize({ "health-cmd" => health_check_cmd_with_cord, "health-interval" => healthcheck.interval })
92
- .concat(cord_volume.docker_args)
93
- else
94
- optionize({ "health-cmd" => healthcheck.cmd, "health-interval" => healthcheck.interval })
95
- end
96
- else
97
- []
98
- end
86
+ [ *argumentize("-t", timeout) ]
99
87
  end
100
88
 
101
- def healthcheck
102
- @healthcheck ||=
103
- if running_traefik?
104
- config.healthcheck.merge(specialized_healthcheck)
105
- else
106
- specialized_healthcheck
107
- end
108
- end
109
-
110
- def health_check_cmd_with_cord
111
- "(#{healthcheck.cmd}) && (stat #{cord_container_file} > /dev/null || exit 1)"
112
- end
113
-
114
-
115
- def running_traefik?
116
- if specializations["traefik"].nil?
117
- primary?
118
- else
119
- specializations["traefik"]
120
- end
89
+ def env(host)
90
+ @envs ||= {}
91
+ @envs[host] ||= [ config.env, specialized_env, *env_tags(host).map(&:env) ].reduce(:merge)
121
92
  end
122
93
 
123
- def primary?
124
- self == @config.primary_role
94
+ def env_args(host)
95
+ [ *env(host).clear_args, *argumentize("--env-file", secrets_path) ]
125
96
  end
126
97
 
127
-
128
- def uses_cord?
129
- running_traefik? && cord_volume && healthcheck.cmd.present?
98
+ def env_directory
99
+ File.join(config.env_directory, "roles")
130
100
  end
131
101
 
132
- def cord_host_directory
133
- File.join config.run_directory_as_docker_volume, "cords", [ container_prefix, config.run_id ].join("-")
102
+ def secrets_io(host)
103
+ env(host).secrets_io
134
104
  end
135
105
 
136
- def cord_volume
137
- if (cord = healthcheck.cord)
138
- @cord_volume ||= Kamal::Configuration::Volume.new \
139
- host_path: File.join(config.run_directory, "cords", [ container_prefix, config.run_id ].join("-")),
140
- container_path: cord
141
- end
106
+ def secrets_path
107
+ File.join(config.env_directory, "roles", "#{name}.env")
142
108
  end
143
109
 
144
- def cord_host_file
145
- File.join cord_volume.host_path, CORD_FILE
110
+ def asset_volume_args
111
+ asset_volume&.docker_args
146
112
  end
147
113
 
148
- def cord_container_directory
149
- health_check_options.fetch("cord", nil)
150
- end
151
114
 
152
- def cord_container_file
153
- File.join cord_volume.container_path, CORD_FILE
115
+ def primary?
116
+ name == @config.primary_role_name
154
117
  end
155
118
 
156
119
 
@@ -168,25 +131,54 @@ class Kamal::Configuration::Role
168
131
  end
169
132
 
170
133
  def assets?
171
- asset_path.present? && running_traefik?
134
+ asset_path.present? && running_proxy?
172
135
  end
173
136
 
174
- def asset_volume(version = nil)
137
+ def asset_volume(version = config.version)
175
138
  if assets?
176
139
  Kamal::Configuration::Volume.new \
177
- host_path: asset_volume_path(version), container_path: asset_path
140
+ host_path: asset_volume_directory(version), container_path: asset_path
178
141
  end
179
142
  end
180
143
 
181
- def asset_extracted_path(version = nil)
182
- File.join config.run_directory, "assets", "extracted", container_name(version)
144
+ def asset_extracted_directory(version = config.version)
145
+ File.join config.assets_directory, "extracted", [ name, version ].join("-")
146
+ end
147
+
148
+ def asset_volume_directory(version = config.version)
149
+ File.join config.assets_directory, "volumes", [ name, version ].join("-")
183
150
  end
184
151
 
185
- def asset_volume_path(version = nil)
186
- File.join config.run_directory, "assets", "volumes", container_name(version)
152
+ def ensure_one_host_for_ssl
153
+ if running_proxy? && proxy.ssl? && hosts.size > 1 && !proxy.custom_ssl_certificate?
154
+ raise Kamal::ConfigurationError, "SSL is only supported on a single server unless you provide custom certificates, found #{hosts.size} servers for role #{name}"
155
+ end
187
156
  end
188
157
 
189
158
  private
159
+ def initialize_specialized_proxy
160
+ proxy_specializations = specializations["proxy"]
161
+
162
+ if primary?
163
+ # only false means no proxy for non-primary roles
164
+ @running_proxy = proxy_specializations != false
165
+ else
166
+ # false and nil both mean no proxy for non-primary roles
167
+ @running_proxy = !!proxy_specializations
168
+ end
169
+
170
+ if running_proxy?
171
+ proxy_config = proxy_specializations == true || proxy_specializations.nil? ? {} : proxy_specializations
172
+
173
+ @specialized_proxy = Kamal::Configuration::Proxy.new \
174
+ config: config,
175
+ proxy_config: proxy_config,
176
+ secrets: config.secrets,
177
+ role_name: name,
178
+ context: "servers/#{name}/proxy"
179
+ end
180
+ end
181
+
190
182
  def tagged_hosts
191
183
  {}.tap do |tagged_hosts|
192
184
  extract_hosts_from_config.map do |host_config|
@@ -214,32 +206,11 @@ class Kamal::Configuration::Role
214
206
  end
215
207
 
216
208
  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
-
224
- def traefik_labels
225
- if running_traefik?
226
- {
227
- # Setting a service property ensures that the generated service name will be consistent between versions
228
- "traefik.http.services.#{traefik_service}.loadbalancer.server.scheme" => "http",
229
-
230
- "traefik.http.routers.#{traefik_service}.rule" => "PathPrefix(`/`)",
231
- "traefik.http.routers.#{traefik_service}.priority" => "2",
232
- "traefik.http.middlewares.#{traefik_service}-retry.retry.attempts" => "5",
233
- "traefik.http.middlewares.#{traefik_service}-retry.retry.initialinterval" => "500ms",
234
- "traefik.http.routers.#{traefik_service}.middlewares" => "#{traefik_service}-retry@docker"
235
- }
236
- else
237
- {}
238
- end
209
+ @specializations ||= role_config.is_a?(Array) ? {} : role_config
239
210
  end
240
211
 
241
- def traefik_service
242
- container_prefix
212
+ def role_config
213
+ @role_config ||= config.raw_config.servers.is_a?(Array) ? {} : config.raw_config.servers[name]
243
214
  end
244
215
 
245
216
  def custom_labels
@@ -13,6 +13,13 @@ class Kamal::Configuration::Servers
13
13
 
14
14
  private
15
15
  def role_names
16
- servers_config.is_a?(Array) ? [ "web" ] : servers_config.keys.sort
16
+ case servers_config
17
+ when Array
18
+ [ "web" ]
19
+ when NilClass
20
+ []
21
+ else
22
+ servers_config.keys.sort
23
+ end
17
24
  end
18
25
  end
@@ -2,8 +2,12 @@ class Kamal::Configuration::Validator::Accessory < Kamal::Configuration::Validat
2
2
  def validate!
3
3
  super
4
4
 
5
- if (config.keys & [ "host", "hosts", "roles" ]).size != 1
6
- error "specify one of `host`, `hosts` or `roles`"
5
+ if (config.keys & [ "host", "hosts", "role", "roles", "tag", "tags" ]).size != 1
6
+ error "specify one of `host`, `hosts`, `role`, `roles`, `tag` or `tags`"
7
7
  end
8
+
9
+ validate_labels!(config["labels"])
10
+
11
+ validate_docker_options!(config["options"])
8
12
  end
9
13
  end
@@ -0,0 +1,15 @@
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.commands.include?(name)
12
+ error "Alias '#{name}' conflicts with a built-in command."
13
+ end
14
+ end
15
+ end
@@ -5,5 +5,11 @@ class Kamal::Configuration::Validator::Builder < Kamal::Configuration::Validator
5
5
  if config["cache"] && config["cache"]["type"]
6
6
  error "Invalid cache type: #{config["cache"]["type"]}" unless [ "gha", "registry" ].include?(config["cache"]["type"])
7
7
  end
8
+
9
+ error "Builder arch not set" unless config["arch"].present?
10
+
11
+ error "buildpacks only support building for one arch" if config["pack"] && config["arch"].is_a?(Array) && config["arch"].size > 1
12
+
13
+ error "Cannot disable local builds, no remote is set" if config["local"] == false && config["remote"].blank?
8
14
  end
9
15
  end
@@ -0,0 +1,25 @@
1
+ class Kamal::Configuration::Validator::Proxy < Kamal::Configuration::Validator
2
+ def validate!
3
+ unless config.nil?
4
+ super
5
+
6
+ if config["host"].blank? && config["hosts"].blank? && config["ssl"]
7
+ error "Must set a host to enable automatic SSL"
8
+ end
9
+
10
+ if (config.keys & [ "host", "hosts" ]).size > 1
11
+ error "Specify one of 'host' or 'hosts', not both"
12
+ end
13
+
14
+ if config["ssl"].is_a?(Hash)
15
+ if config["ssl"]["certificate_pem"].present? && config["ssl"]["private_key_pem"].blank?
16
+ error "Missing private_key_pem setting (required when certificate_pem is present)"
17
+ end
18
+
19
+ if config["ssl"]["private_key_pem"].present? && config["ssl"]["certificate_pem"].blank?
20
+ error "Missing certificate_pem setting (required when private_key_pem is present)"
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -3,9 +3,11 @@ class Kamal::Configuration::Validator::Role < Kamal::Configuration::Validator
3
3
  validate_type! config, Array, Hash
4
4
 
5
5
  if config.is_a?(Array)
6
- validate_servers! "servers", config
6
+ validate_servers!(config)
7
7
  else
8
8
  super
9
+ validate_labels!(config["labels"])
10
+ validate_docker_options!(config["options"])
9
11
  end
10
12
  end
11
13
  end
@@ -1,6 +1,6 @@
1
1
  class Kamal::Configuration::Validator::Servers < Kamal::Configuration::Validator
2
2
  def validate!
3
- validate_type! config, Array, Hash
3
+ validate_type! config, Array, Hash, NilClass
4
4
 
5
5
  validate_servers! config if config.is_a?(Array)
6
6
  end
@@ -13,32 +13,42 @@ class Kamal::Configuration::Validator
13
13
 
14
14
  private
15
15
  def validate_against_example!(validation_config, example)
16
- validate_type! validation_config, Hash
17
-
18
- check_unknown_keys! validation_config, example
19
-
20
- validation_config.each do |key, value|
21
- next if extension?(key)
22
- with_context(key) do
23
- example_value = example[key]
24
-
25
- if example_value == "..."
26
- validate_type! value, *(Array if key == :servers), Hash
27
- elsif key == "hosts"
28
- validate_servers! value
29
- elsif example_value.is_a?(Array)
30
- validate_array_of! value, example_value.first.class
31
- elsif example_value.is_a?(Hash)
32
- case key.to_s
33
- when "options", "args"
34
- validate_type! value, Hash
35
- when "labels"
36
- validate_hash_of! value, example_value.first[1].class
16
+ validate_type! validation_config, example.class
17
+
18
+ if example.class == Hash
19
+ check_unknown_keys! validation_config, example
20
+
21
+ validation_config.each do |key, value|
22
+ next if extension?(key)
23
+ with_context(key) do
24
+ example_value = example[key]
25
+
26
+ if example_value == "..."
27
+ if key.to_s == "ssl"
28
+ validate_type! value, TrueClass, FalseClass, Hash
29
+ elsif key.to_s != "proxy" || !boolean?(value.class)
30
+ validate_type! value, *(Array if key == :servers), Hash
31
+ end
32
+ elsif key == "hosts"
33
+ validate_servers! value
34
+ elsif example_value.is_a?(Array)
35
+ if key == "arch"
36
+ validate_array_of_or_type! value, example_value.first.class
37
+ else
38
+ validate_array_of! value, example_value.first.class
39
+ end
40
+ elsif example_value.is_a?(Hash)
41
+ case key.to_s
42
+ when "options", "args"
43
+ validate_type! value, Hash
44
+ when "labels"
45
+ validate_hash_of! value, example_value.first[1].class
46
+ else
47
+ validate_against_example! value, example_value
48
+ end
37
49
  else
38
- validate_against_example! value, example_value
50
+ validate_type! value, example_value.class
39
51
  end
40
- else
41
- validate_type! value, example_value.class
42
52
  end
43
53
  end
44
54
  end
@@ -69,6 +79,16 @@ class Kamal::Configuration::Validator
69
79
  value.is_a?(String) || value.is_a?(Symbol) || value.is_a?(Numeric) || value.is_a?(TrueClass) || value.is_a?(FalseClass)
70
80
  end
71
81
 
82
+ def validate_array_of_or_type!(value, type)
83
+ if value.is_a?(Array)
84
+ validate_array_of! value, type
85
+ else
86
+ validate_type! value, type
87
+ end
88
+ rescue Kamal::ConfigurationError
89
+ type_error(Array, type)
90
+ end
91
+
72
92
  def validate_array_of!(array, type)
73
93
  validate_type! array, Array
74
94
 
@@ -150,4 +170,22 @@ class Kamal::Configuration::Validator
150
170
  unknown_keys.reject! { |key| extension?(key) } if allow_extensions?
151
171
  unknown_keys_error unknown_keys if unknown_keys.present?
152
172
  end
173
+
174
+ def validate_labels!(labels)
175
+ return true if labels.blank?
176
+
177
+ with_context("labels") do
178
+ labels.each do |key, _|
179
+ with_context(key) do
180
+ error "invalid label. destination, role, and service are reserved labels" if %w[destination role service].include?(key)
181
+ end
182
+ end
183
+ end
184
+ end
185
+
186
+ def validate_docker_options!(options)
187
+ if options
188
+ error "Cannot set restart policy in docker options, unless-stopped is required" if options["restart"]
189
+ end
190
+ end
153
191
  end