kamal 1.4.0 → 1.5.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/lib/kamal/cli/accessory.rb +3 -2
  3. data/lib/kamal/cli/app/boot.rb +67 -0
  4. data/lib/kamal/cli/app/prepare_assets.rb +24 -0
  5. data/lib/kamal/cli/app.rb +20 -61
  6. data/lib/kamal/cli/base.rb +21 -7
  7. data/lib/kamal/cli/env.rb +3 -3
  8. data/lib/kamal/cli/main.rb +1 -1
  9. data/lib/kamal/cli/templates/deploy.yml +1 -1
  10. data/lib/kamal/cli/templates/sample_hooks/docker-setup.sample +0 -0
  11. data/lib/kamal/cli/traefik.rb +15 -12
  12. data/lib/kamal/commander/specifics.rb +49 -0
  13. data/lib/kamal/commander.rb +9 -33
  14. data/lib/kamal/commands/accessory.rb +2 -2
  15. data/lib/kamal/commands/app/assets.rb +4 -4
  16. data/lib/kamal/commands/app/cord.rb +2 -2
  17. data/lib/kamal/commands/app/execution.rb +8 -6
  18. data/lib/kamal/commands/app/images.rb +1 -1
  19. data/lib/kamal/commands/app.rb +29 -8
  20. data/lib/kamal/commands/app.rb.orig +127 -0
  21. data/lib/kamal/commands/auditor.rb +1 -1
  22. data/lib/kamal/commands/base.rb +6 -2
  23. data/lib/kamal/commands/builder/base.rb +14 -4
  24. data/lib/kamal/commands/builder/multiarch.rb +9 -9
  25. data/lib/kamal/commands/builder/native/cached.rb +7 -6
  26. data/lib/kamal/commands/builder/native/remote.rb +9 -9
  27. data/lib/kamal/commands/builder/native.rb +8 -7
  28. data/lib/kamal/commands/healthcheck.rb +0 -1
  29. data/lib/kamal/commands/hook.rb +1 -1
  30. data/lib/kamal/commands/lock.rb +19 -9
  31. data/lib/kamal/commands/prune.rb +2 -2
  32. data/lib/kamal/commands/server.rb +1 -1
  33. data/lib/kamal/commands/traefik.rb +8 -14
  34. data/lib/kamal/configuration/accessory.rb +9 -19
  35. data/lib/kamal/configuration/boot.rb +1 -1
  36. data/lib/kamal/configuration/builder.rb +7 -3
  37. data/lib/kamal/configuration/env.rb +40 -0
  38. data/lib/kamal/configuration/role.rb +12 -42
  39. data/lib/kamal/configuration.rb +20 -8
  40. data/lib/kamal/env_file.rb +12 -15
  41. data/lib/kamal/utils.rb +8 -5
  42. data/lib/kamal/version.rb +1 -1
  43. data/lib/kamal.rb +1 -1
  44. metadata +7 -2
@@ -0,0 +1,40 @@
1
+ class Kamal::Configuration::Env
2
+ attr_reader :secrets_keys, :clear, :secrets_file
3
+ delegate :argumentize, to: Kamal::Utils
4
+
5
+ def self.from_config(config:, secrets_file: nil)
6
+ secrets_keys = config.fetch("secret", [])
7
+ clear = config.fetch("clear", config.key?("secret") ? {} : config)
8
+
9
+ new clear: clear, secrets_keys: secrets_keys, secrets_file: secrets_file
10
+ end
11
+
12
+ def initialize(clear:, secrets_keys:, secrets_file:)
13
+ @clear = clear
14
+ @secrets_keys = secrets_keys
15
+ @secrets_file = secrets_file
16
+ end
17
+
18
+ def args
19
+ [ "--env-file", secrets_file, *argumentize("--env", clear) ]
20
+ end
21
+
22
+ def secrets_io
23
+ StringIO.new(Kamal::EnvFile.new(secrets).to_s)
24
+ end
25
+
26
+ def secrets
27
+ @secrets ||= secrets_keys.to_h { |key| [ key, ENV.fetch(key) ] }
28
+ end
29
+
30
+ def secrets_directory
31
+ File.dirname(secrets_file)
32
+ end
33
+
34
+ def merge(other)
35
+ self.class.new \
36
+ clear: @clear.merge(other.clear),
37
+ secrets_keys: @secrets_keys | other.secrets_keys,
38
+ secrets_file: secrets_file
39
+ end
40
+ end
@@ -51,27 +51,11 @@ class Kamal::Configuration::Role
51
51
 
52
52
 
53
53
  def env
54
- if config.env && config.env["secret"]
55
- merged_env_with_secrets
56
- else
57
- merged_env
58
- end
59
- end
60
-
61
- def env_file
62
- Kamal::EnvFile.new(env)
63
- end
64
-
65
- def host_env_directory
66
- File.join config.host_env_directory, "roles"
67
- end
68
-
69
- def host_env_file_path
70
- File.join host_env_directory, "#{[config.service, name, config.destination].compact.join("-")}.env"
54
+ @env ||= base_env.merge(specialized_env)
71
55
  end
72
56
 
73
57
  def env_args
74
- argumentize "--env-file", host_env_file_path
58
+ env.args
75
59
  end
76
60
 
77
61
  def asset_volume_args
@@ -123,13 +107,13 @@ class Kamal::Configuration::Role
123
107
  end
124
108
 
125
109
  def cord_host_directory
126
- File.join config.run_directory_as_docker_volume, "cords", [container_prefix, config.run_id].join("-")
110
+ File.join config.run_directory_as_docker_volume, "cords", [ container_prefix, config.run_id ].join("-")
127
111
  end
128
112
 
129
113
  def cord_volume
130
114
  if (cord = health_check_options["cord"])
131
115
  @cord_volume ||= Kamal::Configuration::Volume.new \
132
- host_path: File.join(config.run_directory, "cords", [container_prefix, config.run_id].join("-")),
116
+ host_path: File.join(config.run_directory, "cords", [ container_prefix, config.run_id ].join("-")),
133
117
  container_path: cord
134
118
  end
135
119
  end
@@ -192,11 +176,7 @@ class Kamal::Configuration::Role
192
176
  end
193
177
 
194
178
  def default_labels
195
- if config.destination
196
- { "service" => config.service, "role" => name, "destination" => config.destination }
197
- else
198
- { "service" => config.service, "role" => name }
199
- end
179
+ { "service" => config.service, "role" => name, "destination" => config.destination }
200
180
  end
201
181
 
202
182
  def traefik_labels
@@ -217,7 +197,7 @@ class Kamal::Configuration::Role
217
197
  end
218
198
 
219
199
  def traefik_service
220
- [ config.service, name, config.destination ].compact.join("-")
200
+ container_prefix
221
201
  end
222
202
 
223
203
  def custom_labels
@@ -229,31 +209,21 @@ class Kamal::Configuration::Role
229
209
 
230
210
  def specializations
231
211
  if config.servers.is_a?(Array) || config.servers[name].is_a?(Array)
232
- { }
212
+ {}
233
213
  else
234
214
  config.servers[name].except("hosts")
235
215
  end
236
216
  end
237
217
 
238
218
  def specialized_env
239
- specializations["env"] || {}
240
- end
241
-
242
- def merged_env
243
- config.env&.merge(specialized_env) || {}
219
+ Kamal::Configuration::Env.from_config config: specializations.fetch("env", {})
244
220
  end
245
221
 
246
222
  # Secrets are stored in an array, which won't merge by default, so have to do it by hand.
247
- def merged_env_with_secrets
248
- merged_env.tap do |new_env|
249
- new_env["secret"] = Array(config.env["secret"]) + Array(specialized_env["secret"])
250
-
251
- # If there's no secret/clear split, everything is clear
252
- clear_app_env = config.env["secret"] ? Array(config.env["clear"]) : Array(config.env["clear"] || config.env)
253
- clear_role_env = specialized_env["secret"] ? Array(specialized_env["clear"]) : Array(specialized_env["clear"] || specialized_env)
254
-
255
- new_env["clear"] = clear_app_env.to_h.merge(clear_role_env.to_h)
256
- end
223
+ def base_env
224
+ Kamal::Configuration::Env.from_config \
225
+ config: config.env,
226
+ secrets_file: File.join(config.host_env_directory, "roles", "#{container_prefix}.env")
257
227
  end
258
228
 
259
229
  def http_health_check(port:, path:)
@@ -6,7 +6,7 @@ require "erb"
6
6
  require "net/ssh/proxy/jump"
7
7
 
8
8
  class Kamal::Configuration
9
- delegate :service, :image, :servers, :env, :labels, :registry, :stop_wait_time, :hooks_path, :logging, to: :raw_config, allow_nil: true
9
+ delegate :service, :image, :servers, :labels, :registry, :stop_wait_time, :hooks_path, :logging, to: :raw_config, allow_nil: true
10
10
  delegate :argumentize, :optionize, to: Kamal::Utils
11
11
 
12
12
  attr_reader :destination, :raw_config
@@ -88,7 +88,7 @@ class Kamal::Configuration
88
88
 
89
89
 
90
90
  def all_hosts
91
- roles.flat_map(&:hosts).uniq
91
+ (roles + accessories).flat_map(&:hosts).uniq
92
92
  end
93
93
 
94
94
  def primary_host
@@ -128,7 +128,11 @@ class Kamal::Configuration
128
128
  end
129
129
 
130
130
  def latest_image
131
- "#{repository}:latest"
131
+ "#{repository}:#{latest_tag}"
132
+ end
133
+
134
+ def latest_tag
135
+ [ "latest", *destination ].join("-")
132
136
  end
133
137
 
134
138
  def service_with_version
@@ -216,12 +220,17 @@ class Kamal::Configuration
216
220
  raw_config.hooks_path || ".kamal/hooks"
217
221
  end
218
222
 
223
+ def asset_path
224
+ raw_config.asset_path
225
+ end
226
+
227
+
219
228
  def host_env_directory
220
- "#{run_directory}/env"
229
+ File.join(run_directory, "env")
221
230
  end
222
231
 
223
- def asset_path
224
- raw_config.asset_path
232
+ def env
233
+ raw_config.env || {}
225
234
  end
226
235
 
227
236
 
@@ -292,7 +301,7 @@ class Kamal::Configuration
292
301
  end
293
302
 
294
303
  def ensure_valid_service_name
295
- raise ArgumentError, "Service name can only include alphanumeric characters, hyphens, and underscores" unless raw_config[:service] =~ /^[a-z0-9_-]+$/
304
+ raise ArgumentError, "Service name can only include alphanumeric characters, hyphens, and underscores" unless raw_config[:service] =~ /^[a-z0-9_-]+$/i
296
305
 
297
306
  true
298
307
  end
@@ -319,7 +328,10 @@ class Kamal::Configuration
319
328
  def git_version
320
329
  @git_version ||=
321
330
  if Kamal::Git.used?
322
- [ Kamal::Git.revision, Kamal::Git.uncommitted_changes.present? ? "_uncommitted_#{SecureRandom.hex(8)}" : "" ].join
331
+ if Kamal::Git.uncommitted_changes.present? && !builder.git_archive?
332
+ uncommitted_suffix = "_uncommitted_#{SecureRandom.hex(8)}"
333
+ end
334
+ [ Kamal::Git.revision, uncommitted_suffix ].compact.join
323
335
  else
324
336
  raise "Can't use commit hash as version, no git repository found in #{Dir.pwd}"
325
337
  end
@@ -3,21 +3,11 @@ class Kamal::EnvFile
3
3
  def initialize(env)
4
4
  @env = env
5
5
  end
6
-
6
+
7
7
  def to_s
8
8
  env_file = StringIO.new.tap do |contents|
9
- if (secrets = @env["secret"]).present?
10
- @env.fetch("secret", @env)&.each do |key|
11
- contents << docker_env_file_line(key, ENV.fetch(key))
12
- end
13
-
14
- @env["clear"]&.each do |key, value|
15
- contents << docker_env_file_line(key, value)
16
- end
17
- else
18
- @env.fetch("clear", @env)&.each do |key, value|
19
- contents << docker_env_file_line(key, value)
20
- end
9
+ @env.each do |key, value|
10
+ contents << docker_env_file_line(key, value)
21
11
  end
22
12
  end.string
23
13
 
@@ -26,14 +16,21 @@ class Kamal::EnvFile
26
16
  end
27
17
 
28
18
  alias to_str to_s
29
-
19
+
30
20
  private
31
21
  def docker_env_file_line(key, value)
32
- "#{key.to_s}=#{escape_docker_env_file_value(value)}\n"
22
+ "#{key}=#{escape_docker_env_file_value(value)}\n"
33
23
  end
34
24
 
35
25
  # Escape a value to make it safe to dump in a docker file.
36
26
  def escape_docker_env_file_value(value)
27
+ # keep non-ascii(UTF-8) characters as it is
28
+ value.to_s.scan(/[\x00-\x7F]+|[^\x00-\x7F]+/).map do |part|
29
+ part.ascii_only? ? escape_docker_env_file_ascii_value(part) : part
30
+ end.join
31
+ end
32
+
33
+ def escape_docker_env_file_ascii_value(value)
37
34
  # Doublequotes are treated literally in docker env files
38
35
  # so remove leading and trailing ones and unescape any others
39
36
  value.to_s.dump[1..-2].gsub(/\\"/, "\"")
data/lib/kamal/utils.rb CHANGED
@@ -9,7 +9,7 @@ module Kamal::Utils
9
9
  if value.present?
10
10
  attr = "#{key}=#{escape_shell_value(value)}"
11
11
  attr = self.sensitive(attr, redaction: "#{key}=[REDACTED]") if sensitive
12
- [ argument, attr]
12
+ [ argument, attr ]
13
13
  else
14
14
  [ argument, key ]
15
15
  end
@@ -29,7 +29,7 @@ module Kamal::Utils
29
29
 
30
30
  # Flattens a one-to-many structure into an array of two-element arrays each containing a key-value pair
31
31
  def flatten_args(args)
32
- args.flat_map { |key, value| value.try(:map) { |entry| [key, entry] } || [ [ key, value ] ] }
32
+ args.flat_map { |key, value| value.try(:map) { |entry| [ key, entry ] } || [ [ key, value ] ] }
33
33
  end
34
34
 
35
35
  # Marks sensitive values for redaction in logs and human-visible output.
@@ -66,12 +66,15 @@ module Kamal::Utils
66
66
  Array(filters).select do |filter|
67
67
  matches += Array(items).select do |item|
68
68
  # Only allow * for a wildcard
69
- pattern = Regexp.escape(filter).gsub('\*', '.*')
70
69
  # items are roles or hosts
71
- (item.respond_to?(:name) ? item.name : item).match(/^#{pattern}$/)
70
+ File.fnmatch(filter, item.to_s, File::FNM_EXTGLOB)
72
71
  end
73
72
  end
74
73
 
75
- matches
74
+ matches.uniq
75
+ end
76
+
77
+ def stable_sort!(elements, &block)
78
+ elements.sort_by!.with_index { |element, index| [ block.call(element), index ] }
76
79
  end
77
80
  end
data/lib/kamal/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Kamal
2
- VERSION = "1.4.0"
2
+ VERSION = "1.5.1"
3
3
  end
data/lib/kamal.rb CHANGED
@@ -5,6 +5,6 @@ require "active_support"
5
5
  require "zeitwerk"
6
6
 
7
7
  loader = Zeitwerk::Loader.for_gem
8
- loader.ignore("#{__dir__}/kamal/sshkit_with_ext.rb")
8
+ loader.ignore(File.join(__dir__, "kamal", "sshkit_with_ext.rb"))
9
9
  loader.setup
10
10
  loader.eager_load # We need all commands loaded.
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.4.0
4
+ version: 1.5.1
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-03-20 00:00:00.000000000 Z
11
+ date: 2024-04-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -206,6 +206,8 @@ files:
206
206
  - lib/kamal/cli.rb
207
207
  - lib/kamal/cli/accessory.rb
208
208
  - lib/kamal/cli/app.rb
209
+ - lib/kamal/cli/app/boot.rb
210
+ - lib/kamal/cli/app/prepare_assets.rb
209
211
  - lib/kamal/cli/base.rb
210
212
  - lib/kamal/cli/build.rb
211
213
  - lib/kamal/cli/env.rb
@@ -227,9 +229,11 @@ files:
227
229
  - lib/kamal/cli/templates/template.env
228
230
  - lib/kamal/cli/traefik.rb
229
231
  - lib/kamal/commander.rb
232
+ - lib/kamal/commander/specifics.rb
230
233
  - lib/kamal/commands.rb
231
234
  - lib/kamal/commands/accessory.rb
232
235
  - lib/kamal/commands/app.rb
236
+ - lib/kamal/commands/app.rb.orig
233
237
  - lib/kamal/commands/app/assets.rb
234
238
  - lib/kamal/commands/app/containers.rb
235
239
  - lib/kamal/commands/app/cord.rb
@@ -257,6 +261,7 @@ files:
257
261
  - lib/kamal/configuration/accessory.rb
258
262
  - lib/kamal/configuration/boot.rb
259
263
  - lib/kamal/configuration/builder.rb
264
+ - lib/kamal/configuration/env.rb
260
265
  - lib/kamal/configuration/role.rb
261
266
  - lib/kamal/configuration/ssh.rb
262
267
  - lib/kamal/configuration/sshkit.rb