dash 2.12.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 (142) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +13 -0
  4. data/bin/dash +18 -0
  5. data/bin/kamal +18 -0
  6. data/lib/kamal/cli/accessory.rb +342 -0
  7. data/lib/kamal/cli/alias/command.rb +10 -0
  8. data/lib/kamal/cli/app/assets.rb +24 -0
  9. data/lib/kamal/cli/app/boot.rb +126 -0
  10. data/lib/kamal/cli/app/error_pages.rb +33 -0
  11. data/lib/kamal/cli/app/ssl_certificates.rb +28 -0
  12. data/lib/kamal/cli/app.rb +368 -0
  13. data/lib/kamal/cli/base.rb +324 -0
  14. data/lib/kamal/cli/build/clone.rb +59 -0
  15. data/lib/kamal/cli/build/port_forwarding.rb +66 -0
  16. data/lib/kamal/cli/build.rb +242 -0
  17. data/lib/kamal/cli/healthcheck/barrier.rb +33 -0
  18. data/lib/kamal/cli/healthcheck/error.rb +2 -0
  19. data/lib/kamal/cli/healthcheck/poller.rb +42 -0
  20. data/lib/kamal/cli/lock.rb +34 -0
  21. data/lib/kamal/cli/main.rb +299 -0
  22. data/lib/kamal/cli/proxy.rb +419 -0
  23. data/lib/kamal/cli/prune.rb +34 -0
  24. data/lib/kamal/cli/registry.rb +49 -0
  25. data/lib/kamal/cli/secrets.rb +50 -0
  26. data/lib/kamal/cli/server.rb +70 -0
  27. data/lib/kamal/cli/templates/deploy.yml +102 -0
  28. data/lib/kamal/cli/templates/sample_hooks/docker-setup.sample +3 -0
  29. data/lib/kamal/cli/templates/sample_hooks/post-app-boot.sample +3 -0
  30. data/lib/kamal/cli/templates/sample_hooks/post-deploy.sample +14 -0
  31. data/lib/kamal/cli/templates/sample_hooks/post-proxy-reboot.sample +3 -0
  32. data/lib/kamal/cli/templates/sample_hooks/pre-app-boot.sample +3 -0
  33. data/lib/kamal/cli/templates/sample_hooks/pre-build.sample +51 -0
  34. data/lib/kamal/cli/templates/sample_hooks/pre-connect.sample +47 -0
  35. data/lib/kamal/cli/templates/sample_hooks/pre-deploy.sample +122 -0
  36. data/lib/kamal/cli/templates/sample_hooks/pre-proxy-reboot.sample +3 -0
  37. data/lib/kamal/cli/templates/secrets +22 -0
  38. data/lib/kamal/cli.rb +9 -0
  39. data/lib/kamal/commander/specifics.rb +62 -0
  40. data/lib/kamal/commander.rb +230 -0
  41. data/lib/kamal/commands/accessory/proxy.rb +16 -0
  42. data/lib/kamal/commands/accessory.rb +118 -0
  43. data/lib/kamal/commands/app/assets.rb +51 -0
  44. data/lib/kamal/commands/app/containers.rb +31 -0
  45. data/lib/kamal/commands/app/error_pages.rb +9 -0
  46. data/lib/kamal/commands/app/execution.rb +38 -0
  47. data/lib/kamal/commands/app/images.rb +13 -0
  48. data/lib/kamal/commands/app/logging.rb +28 -0
  49. data/lib/kamal/commands/app/proxy.rb +32 -0
  50. data/lib/kamal/commands/app.rb +125 -0
  51. data/lib/kamal/commands/auditor.rb +39 -0
  52. data/lib/kamal/commands/base.rb +147 -0
  53. data/lib/kamal/commands/builder/base.rb +143 -0
  54. data/lib/kamal/commands/builder/clone.rb +32 -0
  55. data/lib/kamal/commands/builder/cloud.rb +22 -0
  56. data/lib/kamal/commands/builder/hybrid.rb +21 -0
  57. data/lib/kamal/commands/builder/local.rb +20 -0
  58. data/lib/kamal/commands/builder/pack.rb +46 -0
  59. data/lib/kamal/commands/builder/remote.rb +75 -0
  60. data/lib/kamal/commands/builder.rb +54 -0
  61. data/lib/kamal/commands/docker.rb +50 -0
  62. data/lib/kamal/commands/hook.rb +20 -0
  63. data/lib/kamal/commands/loadbalancer.rb +130 -0
  64. data/lib/kamal/commands/lock.rb +70 -0
  65. data/lib/kamal/commands/proxy.rb +150 -0
  66. data/lib/kamal/commands/prune.rb +38 -0
  67. data/lib/kamal/commands/registry.rb +38 -0
  68. data/lib/kamal/commands/server.rb +15 -0
  69. data/lib/kamal/commands.rb +2 -0
  70. data/lib/kamal/configuration/accessory.rb +280 -0
  71. data/lib/kamal/configuration/alias.rb +15 -0
  72. data/lib/kamal/configuration/boot.rb +29 -0
  73. data/lib/kamal/configuration/builder.rb +218 -0
  74. data/lib/kamal/configuration/docs/accessory.yml +160 -0
  75. data/lib/kamal/configuration/docs/alias.yml +29 -0
  76. data/lib/kamal/configuration/docs/boot.yml +21 -0
  77. data/lib/kamal/configuration/docs/builder.yml +132 -0
  78. data/lib/kamal/configuration/docs/configuration.yml +228 -0
  79. data/lib/kamal/configuration/docs/env.yml +118 -0
  80. data/lib/kamal/configuration/docs/logging.yml +21 -0
  81. data/lib/kamal/configuration/docs/output.yml +25 -0
  82. data/lib/kamal/configuration/docs/proxy.yml +207 -0
  83. data/lib/kamal/configuration/docs/registry.yml +64 -0
  84. data/lib/kamal/configuration/docs/role.yml +54 -0
  85. data/lib/kamal/configuration/docs/servers.yml +27 -0
  86. data/lib/kamal/configuration/docs/ssh.yml +81 -0
  87. data/lib/kamal/configuration/docs/sshkit.yml +31 -0
  88. data/lib/kamal/configuration/env/tag.rb +13 -0
  89. data/lib/kamal/configuration/env.rb +42 -0
  90. data/lib/kamal/configuration/loadbalancer.rb +34 -0
  91. data/lib/kamal/configuration/logging.rb +33 -0
  92. data/lib/kamal/configuration/output.rb +34 -0
  93. data/lib/kamal/configuration/proxy/boot.rb +124 -0
  94. data/lib/kamal/configuration/proxy/run.rb +152 -0
  95. data/lib/kamal/configuration/proxy.rb +156 -0
  96. data/lib/kamal/configuration/registry.rb +40 -0
  97. data/lib/kamal/configuration/role.rb +247 -0
  98. data/lib/kamal/configuration/servers.rb +25 -0
  99. data/lib/kamal/configuration/ssh.rb +76 -0
  100. data/lib/kamal/configuration/sshkit.rb +26 -0
  101. data/lib/kamal/configuration/validation.rb +27 -0
  102. data/lib/kamal/configuration/validator/accessory.rb +13 -0
  103. data/lib/kamal/configuration/validator/alias.rb +15 -0
  104. data/lib/kamal/configuration/validator/builder.rb +15 -0
  105. data/lib/kamal/configuration/validator/configuration.rb +6 -0
  106. data/lib/kamal/configuration/validator/env.rb +54 -0
  107. data/lib/kamal/configuration/validator/proxy.rb +47 -0
  108. data/lib/kamal/configuration/validator/registry.rb +27 -0
  109. data/lib/kamal/configuration/validator/role.rb +13 -0
  110. data/lib/kamal/configuration/validator/servers.rb +7 -0
  111. data/lib/kamal/configuration/validator.rb +251 -0
  112. data/lib/kamal/configuration/volume.rb +29 -0
  113. data/lib/kamal/configuration.rb +465 -0
  114. data/lib/kamal/docker.rb +30 -0
  115. data/lib/kamal/env_file.rb +44 -0
  116. data/lib/kamal/git.rb +37 -0
  117. data/lib/kamal/otel_shipper.rb +176 -0
  118. data/lib/kamal/output/base_logger.rb +29 -0
  119. data/lib/kamal/output/file_logger.rb +51 -0
  120. data/lib/kamal/output/formatter.rb +36 -0
  121. data/lib/kamal/output/otel_logger.rb +70 -0
  122. data/lib/kamal/secrets/adapters/aws_secrets_manager.rb +59 -0
  123. data/lib/kamal/secrets/adapters/base.rb +33 -0
  124. data/lib/kamal/secrets/adapters/bitwarden.rb +81 -0
  125. data/lib/kamal/secrets/adapters/bitwarden_secrets_manager.rb +66 -0
  126. data/lib/kamal/secrets/adapters/doppler.rb +57 -0
  127. data/lib/kamal/secrets/adapters/enpass.rb +71 -0
  128. data/lib/kamal/secrets/adapters/gcp_secret_manager.rb +112 -0
  129. data/lib/kamal/secrets/adapters/last_pass.rb +40 -0
  130. data/lib/kamal/secrets/adapters/one_password.rb +104 -0
  131. data/lib/kamal/secrets/adapters/passbolt.rb +129 -0
  132. data/lib/kamal/secrets/adapters/test.rb +16 -0
  133. data/lib/kamal/secrets/adapters.rb +16 -0
  134. data/lib/kamal/secrets/dotenv/inline_command_substitution.rb +47 -0
  135. data/lib/kamal/secrets.rb +53 -0
  136. data/lib/kamal/sshkit_with_ext.rb +273 -0
  137. data/lib/kamal/tags.rb +40 -0
  138. data/lib/kamal/utils/sensitive.rb +20 -0
  139. data/lib/kamal/utils.rb +110 -0
  140. data/lib/kamal/version.rb +3 -0
  141. data/lib/kamal.rb +15 -0
  142. metadata +388 -0
@@ -0,0 +1,465 @@
1
+ require "active_support/ordered_options"
2
+ require "active_support/core_ext/string/inquiry"
3
+ require "active_support/core_ext/module/delegation"
4
+ require "active_support/core_ext/hash/keys"
5
+ require "erb"
6
+ require "net/ssh/proxy/jump"
7
+
8
+ class Kamal::Configuration
9
+ HOOKS_OUTPUT_LEVELS = [ :quiet, :verbose ].freeze
10
+
11
+ delegate :service, :labels, :hooks_path, to: :raw_config, allow_nil: true
12
+ delegate :argumentize, :optionize, to: Kamal::Utils
13
+
14
+ attr_reader :destination, :raw_config, :secrets
15
+ attr_reader :accessories, :aliases, :boot, :builder, :env, :logging, :output, :proxy, :proxy_boot, :servers, :ssh, :sshkit, :registry
16
+
17
+ include Validation
18
+
19
+ class << self
20
+ def create_from(config_file:, destination: nil, version: nil)
21
+ ENV["KAMAL_DESTINATION"] = destination
22
+
23
+ raw_config = load_raw_config(config_file: config_file, destination: destination)
24
+
25
+ new raw_config, destination: destination, version: version
26
+ end
27
+
28
+ def load_raw_config(config_file:, destination: nil)
29
+ load_config_files(config_file, *destination_config_file(config_file, destination))
30
+ end
31
+
32
+ private
33
+ def load_config_files(*files)
34
+ files.inject({}) { |config, file| config.deep_merge! load_config_file(file) }
35
+ end
36
+
37
+ def load_config_file(file)
38
+ if file.exist?
39
+ # Newer Psych doesn't load aliases by default
40
+ load_method = YAML.respond_to?(:unsafe_load) ? :unsafe_load : :load
41
+ template = File.read(file)
42
+ rendered = ERB.new(template, trim_mode: "-").result
43
+ YAML.send(load_method, rendered).symbolize_keys
44
+ else
45
+ raise "Configuration file not found in #{file}"
46
+ end
47
+ end
48
+
49
+ def destination_config_file(base_config_file, destination)
50
+ base_config_file.sub_ext(".#{destination}.yml") if destination
51
+ end
52
+ end
53
+
54
+ def initialize(raw_config, destination: nil, version: nil, validate: true)
55
+ @raw_config = ActiveSupport::InheritableOptions.new(raw_config)
56
+ @destination = destination
57
+ @declared_version = version
58
+
59
+ validate! raw_config, example: validation_yml.symbolize_keys, context: "", with: Kamal::Configuration::Validator::Configuration
60
+
61
+ @secrets = Kamal::Secrets.new(destination: destination, secrets_path: secrets_path)
62
+
63
+ # Eager load config to validate it, these are first as they have dependencies later on
64
+ @servers = Servers.new(config: self)
65
+ @registry = Registry.new(config: @raw_config, secrets: secrets)
66
+
67
+ @accessories = @raw_config.accessories&.keys&.collect { |name| Accessory.new(name, config: self) } || []
68
+ @aliases = @raw_config.aliases&.keys&.to_h { |name| [ name, Alias.new(name, config: self) ] } || {}
69
+ @boot = Boot.new(config: self)
70
+ @builder = Builder.new(config: self)
71
+ @env = Env.new(config: @raw_config.env || {}, secrets: secrets)
72
+
73
+ @logging = Logging.new(logging_config: @raw_config.logging)
74
+ @output = Output.new(config: self)
75
+ @proxy = Proxy.new(config: self, proxy_config: @raw_config.proxy, secrets: secrets)
76
+ @proxy_boot = Proxy::Boot.new(config: self)
77
+ @ssh = Ssh.new(config: self)
78
+ @sshkit = Sshkit.new(config: self)
79
+
80
+ ensure_destination_if_required
81
+ ensure_required_keys_present
82
+ ensure_valid_kamal_version
83
+ ensure_retain_containers_valid
84
+ ensure_valid_service_name
85
+ ensure_no_traefik_reboot_hooks
86
+ ensure_one_host_for_ssl_roles
87
+ ensure_unique_hosts_for_ssl_roles
88
+ ensure_local_registry_remote_builder_has_ssh_url
89
+ ensure_no_conflicting_proxy_runs
90
+ ensure_valid_hooks_output!
91
+ end
92
+
93
+ def version=(version)
94
+ @declared_version = version
95
+ end
96
+
97
+ def version
98
+ @declared_version.presence || ENV["VERSION"] || git_version
99
+ end
100
+
101
+ def abbreviated_version
102
+ if version
103
+ # Don't abbreviate <sha>_uncommitted_<etc>
104
+ if version.include?("_")
105
+ version
106
+ else
107
+ version[0...7]
108
+ end
109
+ end
110
+ end
111
+
112
+ def minimum_version
113
+ raw_config.minimum_version
114
+ end
115
+
116
+ def service_and_destination
117
+ [ service, destination ].compact.join("-")
118
+ end
119
+
120
+ def roles
121
+ servers.roles
122
+ end
123
+
124
+ def role(name)
125
+ roles.detect { |r| r.name == name.to_s }
126
+ end
127
+
128
+ def accessory(name)
129
+ accessories.detect { |a| a.name == name.to_s }
130
+ end
131
+
132
+ def all_hosts
133
+ (roles + accessories).flat_map(&:hosts).uniq
134
+ end
135
+
136
+ def host_roles(host)
137
+ roles.select { |role| role.hosts.include?(host) }
138
+ end
139
+
140
+ def host_accessories(host)
141
+ accessories.select { |accessory| accessory.hosts.include?(host) }
142
+ end
143
+
144
+ def app_hosts
145
+ roles.flat_map(&:hosts).uniq
146
+ end
147
+
148
+ def primary_host
149
+ primary_role&.primary_host
150
+ end
151
+
152
+ def primary_role_name
153
+ raw_config.primary_role || "web"
154
+ end
155
+
156
+ def primary_role
157
+ role(primary_role_name)
158
+ end
159
+
160
+ def allow_empty_roles?
161
+ raw_config.allow_empty_roles
162
+ end
163
+
164
+ def proxy_roles
165
+ roles.select(&:running_proxy?)
166
+ end
167
+
168
+ def load_balancing?
169
+ proxy&.load_balancing?
170
+ end
171
+
172
+ def proxy_role_names
173
+ proxy_roles.flat_map(&:name)
174
+ end
175
+
176
+ def proxy_accessories
177
+ accessories.select(&:running_proxy?)
178
+ end
179
+
180
+ def proxy_hosts
181
+ (proxy_roles.flat_map(&:hosts) + proxy_accessories.flat_map(&:hosts)).uniq
182
+ end
183
+
184
+ def image
185
+ name = raw_config&.image.presence
186
+ name ||= raw_config&.service if registry.local?
187
+
188
+ name
189
+ end
190
+
191
+ def proxy_run(host)
192
+ # We validate that all the config are identical for a host
193
+ proxy_runs(host.to_s).first
194
+ end
195
+
196
+ def repository
197
+ [ registry.server, image ].compact.join("/")
198
+ end
199
+
200
+ def absolute_image
201
+ "#{repository}:#{version}"
202
+ end
203
+
204
+ def latest_image
205
+ "#{repository}:#{latest_tag}"
206
+ end
207
+
208
+ def latest_tag
209
+ [ "latest", *destination ].join("-")
210
+ end
211
+
212
+ def service_with_version
213
+ "#{service}-#{version}"
214
+ end
215
+
216
+ def require_destination?
217
+ raw_config.require_destination
218
+ end
219
+
220
+ def retain_containers
221
+ raw_config.retain_containers || 5
222
+ end
223
+
224
+ def volume_args
225
+ if raw_config.volumes.present?
226
+ argumentize "--volume", raw_config.volumes
227
+ else
228
+ []
229
+ end
230
+ end
231
+
232
+ def logging_args
233
+ logging.args
234
+ end
235
+
236
+ def readiness_delay
237
+ raw_config.readiness_delay || 7
238
+ end
239
+
240
+ def deploy_timeout
241
+ raw_config.deploy_timeout || 30
242
+ end
243
+
244
+ def drain_timeout
245
+ raw_config.drain_timeout || 30
246
+ end
247
+
248
+ def stop_timeout
249
+ raw_config.stop_timeout
250
+ end
251
+
252
+ def run_directory
253
+ ".kamal"
254
+ end
255
+
256
+ def apps_directory
257
+ File.join run_directory, "apps"
258
+ end
259
+
260
+ def app_directory
261
+ File.join apps_directory, service_and_destination
262
+ end
263
+
264
+ def env_directory
265
+ File.join app_directory, "env"
266
+ end
267
+
268
+ def assets_directory
269
+ File.join app_directory, "assets"
270
+ end
271
+
272
+ def hooks_path
273
+ raw_config.hooks_path || ".kamal/hooks"
274
+ end
275
+
276
+ def secrets_path
277
+ raw_config.secrets_path || ".kamal/secrets"
278
+ end
279
+
280
+ def asset_path
281
+ raw_config.asset_path
282
+ end
283
+
284
+ def error_pages_path
285
+ raw_config.error_pages_path
286
+ end
287
+
288
+ def env_tags
289
+ @env_tags ||= if (tags = raw_config.env["tags"])
290
+ tags.collect { |name, config| Env::Tag.new(name, config: config, secrets: secrets) }
291
+ else
292
+ []
293
+ end
294
+ end
295
+
296
+ def env_tag(name)
297
+ env_tags.detect { |t| t.name == name.to_s }
298
+ end
299
+
300
+ def hooks_output_for(hook)
301
+ case raw_config.hooks_output
302
+ when Symbol, String
303
+ raw_config.hooks_output.to_sym
304
+ when Hash
305
+ raw_config.hooks_output[hook]&.to_sym
306
+ end
307
+ end
308
+
309
+ def to_h
310
+ {
311
+ roles: role_names,
312
+ hosts: all_hosts,
313
+ primary_host: primary_host,
314
+ version: version,
315
+ repository: repository,
316
+ absolute_image: absolute_image,
317
+ service_with_version: service_with_version,
318
+ volume_args: volume_args,
319
+ ssh_options: ssh.to_h,
320
+ sshkit: sshkit.to_h,
321
+ builder: builder.to_h,
322
+ accessories: raw_config.accessories,
323
+ logging: logging_args
324
+ }.compact
325
+ end
326
+
327
+ private
328
+ # Will raise ArgumentError if any required config keys are missing
329
+ def ensure_destination_if_required
330
+ if require_destination? && destination.nil?
331
+ raise ArgumentError, "You must specify a destination"
332
+ end
333
+
334
+ true
335
+ end
336
+
337
+ def ensure_required_keys_present
338
+ %i[ service registry ].each do |key|
339
+ raise Kamal::ConfigurationError, "Missing required configuration for #{key}" unless raw_config[key].present?
340
+ end
341
+
342
+ raise Kamal::ConfigurationError, "Missing required configuration for image" if image.blank?
343
+
344
+ if raw_config.servers.nil?
345
+ raise Kamal::ConfigurationError, "No servers or accessories specified" unless raw_config.accessories.present?
346
+ else
347
+ unless role(primary_role_name).present?
348
+ raise Kamal::ConfigurationError, "The primary_role #{primary_role_name} isn't defined"
349
+ end
350
+
351
+ if primary_role.hosts.empty?
352
+ raise Kamal::ConfigurationError, "No servers specified for the #{primary_role.name} primary_role"
353
+ end
354
+
355
+ unless allow_empty_roles?
356
+ roles.each do |role|
357
+ if role.hosts.empty?
358
+ raise Kamal::ConfigurationError, "No servers specified for the #{role.name} role. You can ignore this with allow_empty_roles: true"
359
+ end
360
+ end
361
+ end
362
+ end
363
+
364
+ true
365
+ end
366
+
367
+ def ensure_valid_service_name
368
+ raise Kamal::ConfigurationError, "Service name can only include alphanumeric characters, hyphens, and underscores" unless raw_config[:service] =~ /^[a-z0-9_-]+$/i
369
+
370
+ true
371
+ end
372
+
373
+ def ensure_valid_kamal_version
374
+ if minimum_version && Gem::Version.new(minimum_version) > Gem::Version.new(Kamal::VERSION)
375
+ raise Kamal::ConfigurationError, "Current version is #{Kamal::VERSION}, minimum required is #{minimum_version}"
376
+ end
377
+
378
+ true
379
+ end
380
+
381
+ def ensure_retain_containers_valid
382
+ raise Kamal::ConfigurationError, "Must retain at least 1 container" if retain_containers < 1
383
+
384
+ true
385
+ end
386
+
387
+ def ensure_no_traefik_reboot_hooks
388
+ hooks = %w[ pre-traefik-reboot post-traefik-reboot ].select { |hook_file| File.exist?(File.join(hooks_path, hook_file)) }
389
+
390
+ if hooks.any?
391
+ raise Kamal::ConfigurationError, "Found #{hooks.join(", ")}, these should be renamed to (pre|post)-proxy-reboot"
392
+ end
393
+
394
+ true
395
+ end
396
+
397
+ def ensure_one_host_for_ssl_roles
398
+ roles.each(&:ensure_one_host_for_ssl)
399
+
400
+ true
401
+ end
402
+
403
+ def ensure_unique_hosts_for_ssl_roles
404
+ hosts = roles.select(&:ssl?).flat_map { |role| role.proxy.hosts }
405
+ duplicates = hosts.tally.filter_map { |host, count| host if count > 1 }
406
+
407
+ raise Kamal::ConfigurationError, "Different roles can't share the same host for SSL: #{duplicates.join(", ")}" if duplicates.any?
408
+
409
+ true
410
+ end
411
+
412
+ def ensure_local_registry_remote_builder_has_ssh_url
413
+ if registry.local? && builder.remote?
414
+ unless URI(builder.remote).scheme == "ssh"
415
+ raise Kamal::ConfigurationError, "Local registry with remote builder requires an SSH URL (e.g., ssh://user@host)"
416
+ end
417
+ end
418
+
419
+ true
420
+ end
421
+
422
+ def ensure_no_conflicting_proxy_runs
423
+ all_hosts.each do |host|
424
+ run_configs = proxy_runs(host)
425
+ if run_configs.uniq.size > 1
426
+ raise Kamal::ConfigurationError, "Conflicting proxy run configurations for host #{host}"
427
+ end
428
+ end
429
+ end
430
+
431
+ def proxy_runs(host)
432
+ (host_roles(host) + host_accessories(host)).map(&:proxy).compact.map(&:run).compact
433
+ end
434
+
435
+ def role_names
436
+ raw_config.servers.is_a?(Array) ? [ "web" ] : raw_config.servers.keys.sort
437
+ end
438
+
439
+ def ensure_valid_hooks_output!
440
+ case raw_config.hooks_output
441
+ when Symbol, String
442
+ validate_hooks_output_level!(raw_config.hooks_output.to_sym)
443
+ when Hash
444
+ raw_config.hooks_output.each { |hook, level| validate_hooks_output_level!(level.to_sym, hook) }
445
+ end
446
+ end
447
+
448
+ def validate_hooks_output_level!(level, hook = nil)
449
+ return if HOOKS_OUTPUT_LEVELS.include?(level)
450
+ context = hook ? " for hook '#{hook}'" : ""
451
+ raise Kamal::ConfigurationError, "Invalid hooks_output '#{level}'#{context}, must be one of: #{HOOKS_OUTPUT_LEVELS.join(', ')}"
452
+ end
453
+
454
+ def git_version
455
+ @git_version ||=
456
+ if Kamal::Git.used?
457
+ if Kamal::Git.uncommitted_changes.present? && !builder.git_clone?
458
+ uncommitted_suffix = "_uncommitted_#{SecureRandom.hex(8)}"
459
+ end
460
+ [ Kamal::Git.revision, uncommitted_suffix ].compact.join
461
+ else
462
+ raise "Can't use commit hash as version, no git repository found in #{Dir.pwd}"
463
+ end
464
+ end
465
+ end
@@ -0,0 +1,30 @@
1
+ require "tempfile"
2
+ require "open3"
3
+
4
+ module Kamal::Docker
5
+ extend self
6
+ BUILD_CHECK_TAG = "kamal-local-build-check"
7
+
8
+ def included_files
9
+ Tempfile.create do |dockerfile|
10
+ dockerfile.write(<<~DOCKERFILE)
11
+ FROM busybox
12
+ COPY . app
13
+ WORKDIR app
14
+ CMD find . -type f | sed "s|^\./||"
15
+ DOCKERFILE
16
+ dockerfile.close
17
+
18
+ cmd = "docker buildx build -t=#{BUILD_CHECK_TAG} -f=#{dockerfile.path} ."
19
+ system(cmd) || raise("failed to build check image")
20
+ end
21
+
22
+ cmd = "docker run --rm #{BUILD_CHECK_TAG}"
23
+ out, err, status = Open3.capture3(cmd)
24
+ unless status
25
+ raise "failed to run check image:\n#{err}"
26
+ end
27
+
28
+ out.lines.map(&:strip)
29
+ end
30
+ end
@@ -0,0 +1,44 @@
1
+ # Encode an env hash as a string where secret values have been looked up and all values escaped for Docker.
2
+ class Kamal::EnvFile
3
+ def initialize(env)
4
+ @env = env
5
+ end
6
+
7
+ def to_s
8
+ env_file = StringIO.new.tap do |contents|
9
+ @env.each do |key, value|
10
+ contents << docker_env_file_line(key, value)
11
+ end
12
+ end.string
13
+
14
+ # Ensure the file has some contents to avoid the SSHKIT empty file warning
15
+ env_file.presence || "\n"
16
+ end
17
+
18
+ def to_io
19
+ StringIO.new(to_s)
20
+ end
21
+
22
+ alias to_str to_s
23
+
24
+ private
25
+ def docker_env_file_line(key, value)
26
+ "#{key}=#{escape_docker_env_file_value(value)}\n"
27
+ end
28
+
29
+ # Escape a value to make it safe to dump in a docker file.
30
+ def escape_docker_env_file_value(value)
31
+ # keep non-ascii(UTF-8) characters as it is
32
+ value.to_s.scan(/[\x00-\x7F]+|[^\x00-\x7F]+/).map do |part|
33
+ part.ascii_only? ? escape_docker_env_file_ascii_value(part) : part
34
+ end.join
35
+ end
36
+
37
+ def escape_docker_env_file_ascii_value(value)
38
+ # Doublequotes are treated literally in docker env files
39
+ # so remove leading and trailing ones and unescape any others
40
+ value.to_s.dump[1..-2]
41
+ .gsub(/\\"/, "\"")
42
+ .gsub(/\\#/, "#")
43
+ end
44
+ end
data/lib/kamal/git.rb ADDED
@@ -0,0 +1,37 @@
1
+ module Kamal::Git
2
+ extend self
3
+
4
+ def used?
5
+ system("git rev-parse")
6
+ end
7
+
8
+ def user_name
9
+ `git config user.name`.force_encoding(Encoding::UTF_8).strip
10
+ end
11
+
12
+ def email
13
+ `git config user.email`.strip
14
+ end
15
+
16
+ def revision
17
+ `git rev-parse HEAD`.strip
18
+ end
19
+
20
+ def uncommitted_changes
21
+ `git status --porcelain`.strip
22
+ end
23
+
24
+ def root
25
+ `git rev-parse --show-toplevel`.strip
26
+ end
27
+
28
+ # returns an array of relative path names of files with uncommitted changes
29
+ def uncommitted_files
30
+ `git ls-files --modified`.lines.map(&:strip)
31
+ end
32
+
33
+ # returns an array of relative path names of untracked files, including gitignored files
34
+ def untracked_files
35
+ `git ls-files --others`.lines.map(&:strip)
36
+ end
37
+ end