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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +13 -0
- data/bin/dash +18 -0
- data/bin/kamal +18 -0
- data/lib/kamal/cli/accessory.rb +342 -0
- data/lib/kamal/cli/alias/command.rb +10 -0
- data/lib/kamal/cli/app/assets.rb +24 -0
- data/lib/kamal/cli/app/boot.rb +126 -0
- data/lib/kamal/cli/app/error_pages.rb +33 -0
- data/lib/kamal/cli/app/ssl_certificates.rb +28 -0
- data/lib/kamal/cli/app.rb +368 -0
- data/lib/kamal/cli/base.rb +324 -0
- data/lib/kamal/cli/build/clone.rb +59 -0
- data/lib/kamal/cli/build/port_forwarding.rb +66 -0
- data/lib/kamal/cli/build.rb +242 -0
- data/lib/kamal/cli/healthcheck/barrier.rb +33 -0
- data/lib/kamal/cli/healthcheck/error.rb +2 -0
- data/lib/kamal/cli/healthcheck/poller.rb +42 -0
- data/lib/kamal/cli/lock.rb +34 -0
- data/lib/kamal/cli/main.rb +299 -0
- data/lib/kamal/cli/proxy.rb +419 -0
- data/lib/kamal/cli/prune.rb +34 -0
- data/lib/kamal/cli/registry.rb +49 -0
- data/lib/kamal/cli/secrets.rb +50 -0
- data/lib/kamal/cli/server.rb +70 -0
- data/lib/kamal/cli/templates/deploy.yml +102 -0
- data/lib/kamal/cli/templates/sample_hooks/docker-setup.sample +3 -0
- data/lib/kamal/cli/templates/sample_hooks/post-app-boot.sample +3 -0
- data/lib/kamal/cli/templates/sample_hooks/post-deploy.sample +14 -0
- data/lib/kamal/cli/templates/sample_hooks/post-proxy-reboot.sample +3 -0
- data/lib/kamal/cli/templates/sample_hooks/pre-app-boot.sample +3 -0
- data/lib/kamal/cli/templates/sample_hooks/pre-build.sample +51 -0
- data/lib/kamal/cli/templates/sample_hooks/pre-connect.sample +47 -0
- data/lib/kamal/cli/templates/sample_hooks/pre-deploy.sample +122 -0
- data/lib/kamal/cli/templates/sample_hooks/pre-proxy-reboot.sample +3 -0
- data/lib/kamal/cli/templates/secrets +22 -0
- data/lib/kamal/cli.rb +9 -0
- data/lib/kamal/commander/specifics.rb +62 -0
- data/lib/kamal/commander.rb +230 -0
- data/lib/kamal/commands/accessory/proxy.rb +16 -0
- data/lib/kamal/commands/accessory.rb +118 -0
- data/lib/kamal/commands/app/assets.rb +51 -0
- data/lib/kamal/commands/app/containers.rb +31 -0
- data/lib/kamal/commands/app/error_pages.rb +9 -0
- data/lib/kamal/commands/app/execution.rb +38 -0
- data/lib/kamal/commands/app/images.rb +13 -0
- data/lib/kamal/commands/app/logging.rb +28 -0
- data/lib/kamal/commands/app/proxy.rb +32 -0
- data/lib/kamal/commands/app.rb +125 -0
- data/lib/kamal/commands/auditor.rb +39 -0
- data/lib/kamal/commands/base.rb +147 -0
- data/lib/kamal/commands/builder/base.rb +143 -0
- data/lib/kamal/commands/builder/clone.rb +32 -0
- data/lib/kamal/commands/builder/cloud.rb +22 -0
- data/lib/kamal/commands/builder/hybrid.rb +21 -0
- data/lib/kamal/commands/builder/local.rb +20 -0
- data/lib/kamal/commands/builder/pack.rb +46 -0
- data/lib/kamal/commands/builder/remote.rb +75 -0
- data/lib/kamal/commands/builder.rb +54 -0
- data/lib/kamal/commands/docker.rb +50 -0
- data/lib/kamal/commands/hook.rb +20 -0
- data/lib/kamal/commands/loadbalancer.rb +130 -0
- data/lib/kamal/commands/lock.rb +70 -0
- data/lib/kamal/commands/proxy.rb +150 -0
- data/lib/kamal/commands/prune.rb +38 -0
- data/lib/kamal/commands/registry.rb +38 -0
- data/lib/kamal/commands/server.rb +15 -0
- data/lib/kamal/commands.rb +2 -0
- data/lib/kamal/configuration/accessory.rb +280 -0
- data/lib/kamal/configuration/alias.rb +15 -0
- data/lib/kamal/configuration/boot.rb +29 -0
- data/lib/kamal/configuration/builder.rb +218 -0
- data/lib/kamal/configuration/docs/accessory.yml +160 -0
- data/lib/kamal/configuration/docs/alias.yml +29 -0
- data/lib/kamal/configuration/docs/boot.yml +21 -0
- data/lib/kamal/configuration/docs/builder.yml +132 -0
- data/lib/kamal/configuration/docs/configuration.yml +228 -0
- data/lib/kamal/configuration/docs/env.yml +118 -0
- data/lib/kamal/configuration/docs/logging.yml +21 -0
- data/lib/kamal/configuration/docs/output.yml +25 -0
- data/lib/kamal/configuration/docs/proxy.yml +207 -0
- data/lib/kamal/configuration/docs/registry.yml +64 -0
- data/lib/kamal/configuration/docs/role.yml +54 -0
- data/lib/kamal/configuration/docs/servers.yml +27 -0
- data/lib/kamal/configuration/docs/ssh.yml +81 -0
- data/lib/kamal/configuration/docs/sshkit.yml +31 -0
- data/lib/kamal/configuration/env/tag.rb +13 -0
- data/lib/kamal/configuration/env.rb +42 -0
- data/lib/kamal/configuration/loadbalancer.rb +34 -0
- data/lib/kamal/configuration/logging.rb +33 -0
- data/lib/kamal/configuration/output.rb +34 -0
- data/lib/kamal/configuration/proxy/boot.rb +124 -0
- data/lib/kamal/configuration/proxy/run.rb +152 -0
- data/lib/kamal/configuration/proxy.rb +156 -0
- data/lib/kamal/configuration/registry.rb +40 -0
- data/lib/kamal/configuration/role.rb +247 -0
- data/lib/kamal/configuration/servers.rb +25 -0
- data/lib/kamal/configuration/ssh.rb +76 -0
- data/lib/kamal/configuration/sshkit.rb +26 -0
- data/lib/kamal/configuration/validation.rb +27 -0
- data/lib/kamal/configuration/validator/accessory.rb +13 -0
- data/lib/kamal/configuration/validator/alias.rb +15 -0
- data/lib/kamal/configuration/validator/builder.rb +15 -0
- data/lib/kamal/configuration/validator/configuration.rb +6 -0
- data/lib/kamal/configuration/validator/env.rb +54 -0
- data/lib/kamal/configuration/validator/proxy.rb +47 -0
- data/lib/kamal/configuration/validator/registry.rb +27 -0
- data/lib/kamal/configuration/validator/role.rb +13 -0
- data/lib/kamal/configuration/validator/servers.rb +7 -0
- data/lib/kamal/configuration/validator.rb +251 -0
- data/lib/kamal/configuration/volume.rb +29 -0
- data/lib/kamal/configuration.rb +465 -0
- data/lib/kamal/docker.rb +30 -0
- data/lib/kamal/env_file.rb +44 -0
- data/lib/kamal/git.rb +37 -0
- data/lib/kamal/otel_shipper.rb +176 -0
- data/lib/kamal/output/base_logger.rb +29 -0
- data/lib/kamal/output/file_logger.rb +51 -0
- data/lib/kamal/output/formatter.rb +36 -0
- data/lib/kamal/output/otel_logger.rb +70 -0
- data/lib/kamal/secrets/adapters/aws_secrets_manager.rb +59 -0
- data/lib/kamal/secrets/adapters/base.rb +33 -0
- data/lib/kamal/secrets/adapters/bitwarden.rb +81 -0
- data/lib/kamal/secrets/adapters/bitwarden_secrets_manager.rb +66 -0
- data/lib/kamal/secrets/adapters/doppler.rb +57 -0
- data/lib/kamal/secrets/adapters/enpass.rb +71 -0
- data/lib/kamal/secrets/adapters/gcp_secret_manager.rb +112 -0
- data/lib/kamal/secrets/adapters/last_pass.rb +40 -0
- data/lib/kamal/secrets/adapters/one_password.rb +104 -0
- data/lib/kamal/secrets/adapters/passbolt.rb +129 -0
- data/lib/kamal/secrets/adapters/test.rb +16 -0
- data/lib/kamal/secrets/adapters.rb +16 -0
- data/lib/kamal/secrets/dotenv/inline_command_substitution.rb +47 -0
- data/lib/kamal/secrets.rb +53 -0
- data/lib/kamal/sshkit_with_ext.rb +273 -0
- data/lib/kamal/tags.rb +40 -0
- data/lib/kamal/utils/sensitive.rb +20 -0
- data/lib/kamal/utils.rb +110 -0
- data/lib/kamal/version.rb +3 -0
- data/lib/kamal.rb +15 -0
- 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
|
data/lib/kamal/docker.rb
ADDED
|
@@ -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
|