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.
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.