kamal 1.6.0 → 1.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/lib/kamal/cli/accessory.rb +5 -3
  3. data/lib/kamal/cli/app.rb +6 -3
  4. data/lib/kamal/cli/build.rb +13 -10
  5. data/lib/kamal/cli/healthcheck/poller.rb +2 -2
  6. data/lib/kamal/cli/main.rb +14 -2
  7. data/lib/kamal/cli/registry.rb +9 -10
  8. data/lib/kamal/cli/templates/sample_hooks/docker-setup.sample +1 -1
  9. data/lib/kamal/cli/traefik.rb +5 -3
  10. data/lib/kamal/cli.rb +1 -1
  11. data/lib/kamal/commands/accessory.rb +4 -4
  12. data/lib/kamal/commands/app/logging.rb +4 -4
  13. data/lib/kamal/commands/builder/base.rb +13 -0
  14. data/lib/kamal/commands/builder/multiarch/remote.rb +10 -0
  15. data/lib/kamal/commands/builder/multiarch.rb +4 -0
  16. data/lib/kamal/commands/builder/native/cached.rb +10 -1
  17. data/lib/kamal/commands/builder/native/remote.rb +8 -0
  18. data/lib/kamal/commands/builder.rb +17 -11
  19. data/lib/kamal/commands/registry.rb +4 -13
  20. data/lib/kamal/commands/traefik.rb +8 -47
  21. data/lib/kamal/configuration/accessory.rb +30 -41
  22. data/lib/kamal/configuration/boot.rb +9 -4
  23. data/lib/kamal/configuration/builder.rb +33 -33
  24. data/lib/kamal/configuration/docs/accessory.yml +90 -0
  25. data/lib/kamal/configuration/docs/boot.yml +19 -0
  26. data/lib/kamal/configuration/docs/builder.yml +107 -0
  27. data/lib/kamal/configuration/docs/configuration.yml +157 -0
  28. data/lib/kamal/configuration/docs/env.yml +72 -0
  29. data/lib/kamal/configuration/docs/healthcheck.yml +59 -0
  30. data/lib/kamal/configuration/docs/logging.yml +21 -0
  31. data/lib/kamal/configuration/docs/registry.yml +49 -0
  32. data/lib/kamal/configuration/docs/role.yml +52 -0
  33. data/lib/kamal/configuration/docs/servers.yml +27 -0
  34. data/lib/kamal/configuration/docs/ssh.yml +46 -0
  35. data/lib/kamal/configuration/docs/sshkit.yml +23 -0
  36. data/lib/kamal/configuration/docs/traefik.yml +62 -0
  37. data/lib/kamal/configuration/env/tag.rb +1 -1
  38. data/lib/kamal/configuration/env.rb +10 -14
  39. data/lib/kamal/configuration/healthcheck.rb +63 -0
  40. data/lib/kamal/configuration/logging.rb +33 -0
  41. data/lib/kamal/configuration/registry.rb +31 -0
  42. data/lib/kamal/configuration/role.rb +53 -65
  43. data/lib/kamal/configuration/servers.rb +18 -0
  44. data/lib/kamal/configuration/ssh.rb +11 -8
  45. data/lib/kamal/configuration/sshkit.rb +9 -7
  46. data/lib/kamal/configuration/traefik.rb +60 -0
  47. data/lib/kamal/configuration/validation.rb +27 -0
  48. data/lib/kamal/configuration/validator/accessory.rb +9 -0
  49. data/lib/kamal/configuration/validator/builder.rb +9 -0
  50. data/lib/kamal/configuration/validator/env.rb +54 -0
  51. data/lib/kamal/configuration/validator/registry.rb +25 -0
  52. data/lib/kamal/configuration/validator/role.rb +11 -0
  53. data/lib/kamal/configuration/validator/servers.rb +7 -0
  54. data/lib/kamal/configuration/validator.rb +140 -0
  55. data/lib/kamal/configuration.rb +41 -66
  56. data/lib/kamal/version.rb +1 -1
  57. data/lib/kamal.rb +2 -0
  58. metadata +49 -3
@@ -0,0 +1,140 @@
1
+ class Kamal::Configuration::Validator
2
+ attr_reader :config, :example, :context
3
+
4
+ def initialize(config, example:, context:)
5
+ @config = config
6
+ @example = example
7
+ @context = context
8
+ end
9
+
10
+ def validate!
11
+ validate_against_example! config, example
12
+ end
13
+
14
+ private
15
+ def validate_against_example!(validation_config, example)
16
+ validate_type! validation_config, Hash
17
+
18
+ if (unknown_keys = validation_config.keys - example.keys).any?
19
+ unknown_keys_error unknown_keys
20
+ end
21
+
22
+ validation_config.each do |key, value|
23
+ with_context(key) do
24
+ example_value = example[key]
25
+
26
+ if example_value == "..."
27
+ validate_type! value, *(Array if key == :servers), Hash
28
+ elsif key == "hosts"
29
+ validate_servers! value
30
+ elsif example_value.is_a?(Array)
31
+ validate_array_of! value, example_value.first.class
32
+ elsif example_value.is_a?(Hash)
33
+ case key.to_s
34
+ when "options"
35
+ validate_type! value, Hash
36
+ when "args", "labels"
37
+ validate_hash_of! value, example_value.first[1].class
38
+ else
39
+ validate_against_example! value, example_value
40
+ end
41
+ else
42
+ validate_type! value, example_value.class
43
+ end
44
+ end
45
+ end
46
+ end
47
+
48
+
49
+ def valid_type?(value, type)
50
+ value.is_a?(type) ||
51
+ (type == String && stringish?(value)) ||
52
+ (boolean?(type) && boolean?(value.class))
53
+ end
54
+
55
+ def type_description(type)
56
+ if type == Integer || type == Array
57
+ "an #{type.name.downcase}"
58
+ elsif type == TrueClass || type == FalseClass
59
+ "a boolean"
60
+ else
61
+ "a #{type.name.downcase}"
62
+ end
63
+ end
64
+
65
+ def boolean?(type)
66
+ type == TrueClass || type == FalseClass
67
+ end
68
+
69
+ def stringish?(value)
70
+ value.is_a?(String) || value.is_a?(Symbol) || value.is_a?(Numeric) || value.is_a?(TrueClass) || value.is_a?(FalseClass)
71
+ end
72
+
73
+ def validate_array_of!(array, type)
74
+ validate_type! array, Array
75
+
76
+ array.each_with_index do |value, index|
77
+ with_context(index) do
78
+ validate_type! value, type
79
+ end
80
+ end
81
+ end
82
+
83
+ def validate_hash_of!(hash, type)
84
+ validate_type! hash, Hash
85
+
86
+ hash.each do |key, value|
87
+ with_context(key) do
88
+ validate_type! value, type
89
+ end
90
+ end
91
+ end
92
+
93
+ def validate_servers!(servers)
94
+ validate_type! servers, Array
95
+
96
+ servers.each_with_index do |server, index|
97
+ with_context(index) do
98
+ validate_type! server, String, Hash
99
+
100
+ if server.is_a?(Hash)
101
+ error "multiple hosts found" unless server.size == 1
102
+ host, tags = server.first
103
+
104
+ with_context(host) do
105
+ validate_type! tags, String, Array
106
+ validate_array_of! tags, String if tags.is_a?(Array)
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
112
+
113
+ def validate_type!(value, *types)
114
+ type_error(*types) unless types.any? { |type| valid_type?(value, type) }
115
+ end
116
+
117
+ def error(message)
118
+ raise Kamal::ConfigurationError, "#{error_context}#{message}"
119
+ end
120
+
121
+ def type_error(*expected_types)
122
+ error "should be #{expected_types.map { |type| type_description(type) }.join(" or ")}"
123
+ end
124
+
125
+ def unknown_keys_error(unknown_keys)
126
+ error "unknown #{"key".pluralize(unknown_keys.count)}: #{unknown_keys.join(", ")}"
127
+ end
128
+
129
+ def error_context
130
+ "#{context}: " if context.present?
131
+ end
132
+
133
+ def with_context(context)
134
+ old_context = @context
135
+ @context = [ @context, context ].select(&:present?).join("/")
136
+ yield
137
+ ensure
138
+ @context = old_context
139
+ end
140
+ end
@@ -1,15 +1,19 @@
1
1
  require "active_support/ordered_options"
2
2
  require "active_support/core_ext/string/inquiry"
3
3
  require "active_support/core_ext/module/delegation"
4
+ require "active_support/core_ext/hash/keys"
4
5
  require "pathname"
5
6
  require "erb"
6
7
  require "net/ssh/proxy/jump"
7
8
 
8
9
  class Kamal::Configuration
9
- delegate :service, :image, :servers, :labels, :registry, :stop_wait_time, :hooks_path, :logging, to: :raw_config, allow_nil: true
10
+ delegate :service, :image, :labels, :stop_wait_time, :hooks_path, to: :raw_config, allow_nil: true
10
11
  delegate :argumentize, :optionize, to: Kamal::Utils
11
12
 
12
13
  attr_reader :destination, :raw_config
14
+ attr_reader :accessories, :boot, :builder, :env, :healthcheck, :logging, :traefik, :servers, :ssh, :sshkit, :registry
15
+
16
+ include Validation
13
17
 
14
18
  class << self
15
19
  def create_from(config_file:, destination: nil, version: nil)
@@ -42,7 +46,29 @@ class Kamal::Configuration
42
46
  @raw_config = ActiveSupport::InheritableOptions.new(raw_config)
43
47
  @destination = destination
44
48
  @declared_version = version
45
- valid? if validate
49
+
50
+ validate! raw_config, example: validation_yml.symbolize_keys, context: ""
51
+
52
+ # Eager load config to validate it, these are first as they have dependencies later on
53
+ @servers = Servers.new(config: self)
54
+ @registry = Registry.new(config: self)
55
+
56
+ @accessories = @raw_config.accessories&.keys&.collect { |name| Accessory.new(name, config: self) } || []
57
+ @boot = Boot.new(config: self)
58
+ @builder = Builder.new(config: self)
59
+ @env = Env.new(config: @raw_config.env || {})
60
+
61
+ @healthcheck = Healthcheck.new(healthcheck_config: @raw_config.healthcheck)
62
+ @logging = Logging.new(logging_config: @raw_config.logging)
63
+ @traefik = Traefik.new(config: self)
64
+ @ssh = Ssh.new(config: self)
65
+ @sshkit = Sshkit.new(config: self)
66
+
67
+ ensure_destination_if_required
68
+ ensure_required_keys_present
69
+ ensure_valid_kamal_version
70
+ ensure_retain_containers_valid
71
+ ensure_valid_service_name
46
72
  end
47
73
 
48
74
 
@@ -71,17 +97,13 @@ class Kamal::Configuration
71
97
 
72
98
 
73
99
  def roles
74
- @roles ||= role_names.collect { |role_name| Role.new(role_name, config: self) }
100
+ servers.roles
75
101
  end
76
102
 
77
103
  def role(name)
78
104
  roles.detect { |r| r.name == name.to_s }
79
105
  end
80
106
 
81
- def accessories
82
- @accessories ||= raw_config.accessories&.keys&.collect { |name| Kamal::Configuration::Accessory.new(name, config: self) } || []
83
- end
84
-
85
107
  def accessory(name)
86
108
  accessories.detect { |a| a.name == name.to_s }
87
109
  end
@@ -120,7 +142,7 @@ class Kamal::Configuration
120
142
  end
121
143
 
122
144
  def repository
123
- [ raw_config.registry["server"], image ].compact.join("/")
145
+ [ registry.server, image ].compact.join("/")
124
146
  end
125
147
 
126
148
  def absolute_image
@@ -157,40 +179,10 @@ class Kamal::Configuration
157
179
  end
158
180
 
159
181
  def logging_args
160
- if logging.present?
161
- optionize({ "log-driver" => logging["driver"] }.compact) +
162
- argumentize("--log-opt", logging["options"])
163
- else
164
- argumentize("--log-opt", { "max-size" => "10m" })
165
- end
166
- end
167
-
168
-
169
- def boot
170
- Kamal::Configuration::Boot.new(config: self)
171
- end
172
-
173
- def builder
174
- Kamal::Configuration::Builder.new(config: self)
175
- end
176
-
177
- def traefik
178
- raw_config.traefik || {}
179
- end
180
-
181
- def ssh
182
- Kamal::Configuration::Ssh.new(config: self)
183
- end
184
-
185
- def sshkit
186
- Kamal::Configuration::Sshkit.new(config: self)
182
+ logging.args
187
183
  end
188
184
 
189
185
 
190
- def healthcheck
191
- { "path" => "/up", "port" => 3000, "max_attempts" => 7, "cord" => "/tmp/kamal-cord", "log_lines" => 50 }.merge(raw_config.healthcheck || {})
192
- end
193
-
194
186
  def healthcheck_service
195
187
  [ "healthcheck", service, destination ].compact.join("-")
196
188
  end
@@ -229,13 +221,9 @@ class Kamal::Configuration
229
221
  File.join(run_directory, "env")
230
222
  end
231
223
 
232
- def env
233
- raw_config.env || {}
234
- end
235
-
236
224
  def env_tags
237
225
  @env_tags ||= if (tags = raw_config.env["tags"])
238
- tags.collect { |name, config| Kamal::Configuration::Env::Tag.new(name, config: config) }
226
+ tags.collect { |name, config| Env::Tag.new(name, config: config) }
239
227
  else
240
228
  []
241
229
  end
@@ -246,10 +234,6 @@ class Kamal::Configuration
246
234
  end
247
235
 
248
236
 
249
- def valid?
250
- ensure_destination_if_required && ensure_required_keys_present && ensure_valid_kamal_version && ensure_retain_containers_valid && ensure_valid_service_name
251
- end
252
-
253
237
  def to_h
254
238
  {
255
239
  roles: role_names,
@@ -265,11 +249,10 @@ class Kamal::Configuration
265
249
  builder: builder.to_h,
266
250
  accessories: raw_config.accessories,
267
251
  logging: logging_args,
268
- healthcheck: healthcheck
252
+ healthcheck: healthcheck.to_h
269
253
  }.compact
270
254
  end
271
255
 
272
-
273
256
  private
274
257
  # Will raise ArgumentError if any required config keys are missing
275
258
  def ensure_destination_if_required
@@ -282,29 +265,21 @@ class Kamal::Configuration
282
265
 
283
266
  def ensure_required_keys_present
284
267
  %i[ service image registry servers ].each do |key|
285
- raise ArgumentError, "Missing required configuration for #{key}" unless raw_config[key].present?
286
- end
287
-
288
- if raw_config.registry["username"].blank?
289
- raise ArgumentError, "You must specify a username for the registry in config/deploy.yml"
290
- end
291
-
292
- if raw_config.registry["password"].blank?
293
- raise ArgumentError, "You must specify a password for the registry in config/deploy.yml (or set the ENV variable if that's used)"
268
+ raise Kamal::ConfigurationError, "Missing required configuration for #{key}" unless raw_config[key].present?
294
269
  end
295
270
 
296
- unless role_names.include?(primary_role_name)
297
- raise ArgumentError, "The primary_role #{primary_role_name} isn't defined"
271
+ unless role(primary_role_name).present?
272
+ raise Kamal::ConfigurationError, "The primary_role #{primary_role_name} isn't defined"
298
273
  end
299
274
 
300
275
  if primary_role.hosts.empty?
301
- raise ArgumentError, "No servers specified for the #{primary_role.name} primary_role"
276
+ raise Kamal::ConfigurationError, "No servers specified for the #{primary_role.name} primary_role"
302
277
  end
303
278
 
304
279
  unless allow_empty_roles?
305
280
  roles.each do |role|
306
281
  if role.hosts.empty?
307
- raise ArgumentError, "No servers specified for the #{role.name} role. You can ignore this with allow_empty_roles: true"
282
+ raise Kamal::ConfigurationError, "No servers specified for the #{role.name} role. You can ignore this with allow_empty_roles: true"
308
283
  end
309
284
  end
310
285
  end
@@ -313,21 +288,21 @@ class Kamal::Configuration
313
288
  end
314
289
 
315
290
  def ensure_valid_service_name
316
- raise ArgumentError, "Service name can only include alphanumeric characters, hyphens, and underscores" unless raw_config[:service] =~ /^[a-z0-9_-]+$/i
291
+ raise Kamal::ConfigurationError, "Service name can only include alphanumeric characters, hyphens, and underscores" unless raw_config[:service] =~ /^[a-z0-9_-]+$/i
317
292
 
318
293
  true
319
294
  end
320
295
 
321
296
  def ensure_valid_kamal_version
322
297
  if minimum_version && Gem::Version.new(minimum_version) > Gem::Version.new(Kamal::VERSION)
323
- raise ArgumentError, "Current version is #{Kamal::VERSION}, minimum required is #{minimum_version}"
298
+ raise Kamal::ConfigurationError, "Current version is #{Kamal::VERSION}, minimum required is #{minimum_version}"
324
299
  end
325
300
 
326
301
  true
327
302
  end
328
303
 
329
304
  def ensure_retain_containers_valid
330
- raise ArgumentError, "Must retain at least 1 container" if retain_containers < 1
305
+ raise Kamal::ConfigurationError, "Must retain at least 1 container" if retain_containers < 1
331
306
 
332
307
  true
333
308
  end
data/lib/kamal/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Kamal
2
- VERSION = "1.6.0"
2
+ VERSION = "1.7.0"
3
3
  end
data/lib/kamal.rb CHANGED
@@ -1,8 +1,10 @@
1
1
  module Kamal
2
+ class ConfigurationError < StandardError; end
2
3
  end
3
4
 
4
5
  require "active_support"
5
6
  require "zeitwerk"
7
+ require "yaml"
6
8
 
7
9
  loader = Zeitwerk::Loader.for_gem
8
10
  loader.ignore(File.join(__dir__, "kamal", "sshkit_with_ext.rb"))
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kamal
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.0
4
+ version: 1.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Heinemeier Hansson
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-06-03 00:00:00.000000000 Z
11
+ date: 2024-06-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -114,6 +114,26 @@ dependencies:
114
114
  - - "~>"
115
115
  - !ruby/object:Gem::Version
116
116
  version: '1.2'
117
+ - !ruby/object:Gem::Dependency
118
+ name: x25519
119
+ requirement: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: '1.0'
124
+ - - ">="
125
+ - !ruby/object:Gem::Version
126
+ version: 1.0.10
127
+ type: :runtime
128
+ prerelease: false
129
+ version_requirements: !ruby/object:Gem::Requirement
130
+ requirements:
131
+ - - "~>"
132
+ - !ruby/object:Gem::Version
133
+ version: '1.0'
134
+ - - ">="
135
+ - !ruby/object:Gem::Version
136
+ version: 1.0.10
117
137
  - !ruby/object:Gem::Dependency
118
138
  name: bcrypt_pbkdf
119
139
  requirement: !ruby/object:Gem::Requirement
@@ -268,11 +288,37 @@ files:
268
288
  - lib/kamal/configuration/accessory.rb
269
289
  - lib/kamal/configuration/boot.rb
270
290
  - lib/kamal/configuration/builder.rb
291
+ - lib/kamal/configuration/docs/accessory.yml
292
+ - lib/kamal/configuration/docs/boot.yml
293
+ - lib/kamal/configuration/docs/builder.yml
294
+ - lib/kamal/configuration/docs/configuration.yml
295
+ - lib/kamal/configuration/docs/env.yml
296
+ - lib/kamal/configuration/docs/healthcheck.yml
297
+ - lib/kamal/configuration/docs/logging.yml
298
+ - lib/kamal/configuration/docs/registry.yml
299
+ - lib/kamal/configuration/docs/role.yml
300
+ - lib/kamal/configuration/docs/servers.yml
301
+ - lib/kamal/configuration/docs/ssh.yml
302
+ - lib/kamal/configuration/docs/sshkit.yml
303
+ - lib/kamal/configuration/docs/traefik.yml
271
304
  - lib/kamal/configuration/env.rb
272
305
  - lib/kamal/configuration/env/tag.rb
306
+ - lib/kamal/configuration/healthcheck.rb
307
+ - lib/kamal/configuration/logging.rb
308
+ - lib/kamal/configuration/registry.rb
273
309
  - lib/kamal/configuration/role.rb
310
+ - lib/kamal/configuration/servers.rb
274
311
  - lib/kamal/configuration/ssh.rb
275
312
  - lib/kamal/configuration/sshkit.rb
313
+ - lib/kamal/configuration/traefik.rb
314
+ - lib/kamal/configuration/validation.rb
315
+ - lib/kamal/configuration/validator.rb
316
+ - lib/kamal/configuration/validator/accessory.rb
317
+ - lib/kamal/configuration/validator/builder.rb
318
+ - lib/kamal/configuration/validator/env.rb
319
+ - lib/kamal/configuration/validator/registry.rb
320
+ - lib/kamal/configuration/validator/role.rb
321
+ - lib/kamal/configuration/validator/servers.rb
276
322
  - lib/kamal/configuration/volume.rb
277
323
  - lib/kamal/env_file.rb
278
324
  - lib/kamal/git.rb
@@ -300,7 +346,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
300
346
  - !ruby/object:Gem::Version
301
347
  version: '0'
302
348
  requirements: []
303
- rubygems_version: 3.5.10
349
+ rubygems_version: 3.5.11
304
350
  signing_key:
305
351
  specification_version: 4
306
352
  summary: Deploy web apps in containers to servers running Docker with zero downtime.