kamal 2.0.0.alpha → 2.0.0.beta1
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 +4 -4
- data/README.md +1 -1
- data/lib/kamal/cli/accessory.rb +44 -20
- data/lib/kamal/cli/app/boot.rb +22 -16
- data/lib/kamal/cli/app.rb +37 -3
- data/lib/kamal/cli/base.rb +9 -48
- data/lib/kamal/cli/build.rb +8 -3
- data/lib/kamal/cli/healthcheck/barrier.rb +2 -0
- data/lib/kamal/cli/healthcheck/poller.rb +18 -39
- data/lib/kamal/cli/lock.rb +2 -3
- data/lib/kamal/cli/main.rb +54 -51
- data/lib/kamal/cli/proxy.rb +224 -0
- data/lib/kamal/cli/prune.rb +0 -1
- data/lib/kamal/cli/secrets.rb +36 -0
- data/lib/kamal/cli/server.rb +0 -2
- data/lib/kamal/cli/templates/deploy.yml +0 -11
- data/lib/kamal/cli/templates/sample_hooks/post-proxy-reboot.sample +3 -0
- data/lib/kamal/cli/templates/secrets +16 -0
- data/lib/kamal/cli.rb +1 -0
- data/lib/kamal/commander/specifics.rb +3 -3
- data/lib/kamal/commander.rb +17 -9
- data/lib/kamal/commands/accessory.rb +7 -7
- data/lib/kamal/commands/app/assets.rb +8 -8
- data/lib/kamal/commands/app/proxy.rb +16 -0
- data/lib/kamal/commands/app.rb +7 -15
- data/lib/kamal/commands/auditor.rb +6 -3
- data/lib/kamal/commands/base.rb +8 -0
- data/lib/kamal/commands/builder/base.rb +2 -6
- data/lib/kamal/commands/builder/hybrid.rb +1 -1
- data/lib/kamal/commands/builder/remote.rb +27 -4
- data/lib/kamal/commands/builder.rb +1 -1
- data/lib/kamal/commands/docker.rb +4 -0
- data/lib/kamal/commands/hook.rb +5 -2
- data/lib/kamal/commands/lock.rb +2 -6
- data/lib/kamal/commands/proxy.rb +77 -0
- data/lib/kamal/commands/prune.rb +1 -9
- data/lib/kamal/commands/server.rb +11 -1
- data/lib/kamal/configuration/accessory.rb +14 -2
- data/lib/kamal/configuration/builder.rb +9 -3
- data/lib/kamal/configuration/docs/builder.yml +20 -10
- data/lib/kamal/configuration/docs/configuration.yml +16 -16
- data/lib/kamal/configuration/docs/env.yml +10 -11
- data/lib/kamal/configuration/docs/proxy.yml +100 -0
- data/lib/kamal/configuration/docs/registry.yml +4 -2
- data/lib/kamal/configuration/docs/role.yml +3 -5
- data/lib/kamal/configuration/env/tag.rb +4 -3
- data/lib/kamal/configuration/env.rb +10 -17
- data/lib/kamal/configuration/proxy.rb +66 -0
- data/lib/kamal/configuration/registry.rb +3 -2
- data/lib/kamal/configuration/role.rb +63 -94
- data/lib/kamal/configuration/validator/builder.rb +2 -0
- data/lib/kamal/configuration/validator/proxy.rb +11 -0
- data/lib/kamal/configuration/validator.rb +3 -1
- data/lib/kamal/configuration.rb +90 -33
- data/lib/kamal/env_file.rb +4 -0
- data/lib/kamal/secrets/adapters/base.rb +18 -0
- data/lib/kamal/secrets/adapters/bitwarden.rb +64 -0
- data/lib/kamal/secrets/adapters/last_pass.rb +30 -0
- data/lib/kamal/secrets/adapters/one_password.rb +61 -0
- data/lib/kamal/secrets/adapters/test.rb +10 -0
- data/lib/kamal/secrets/adapters.rb +14 -0
- data/lib/kamal/secrets/dotenv/inline_command_substitution.rb +32 -0
- data/lib/kamal/secrets.rb +37 -0
- data/lib/kamal/sshkit_with_ext.rb +1 -0
- data/lib/kamal/utils.rb +12 -0
- data/lib/kamal/version.rb +1 -1
- data/lib/kamal.rb +3 -1
- metadata +23 -16
- data/lib/kamal/cli/env.rb +0 -54
- data/lib/kamal/cli/templates/sample_hooks/post-traefik-reboot.sample +0 -3
- data/lib/kamal/cli/templates/template.env +0 -2
- data/lib/kamal/cli/traefik.rb +0 -122
- data/lib/kamal/commands/app/cord.rb +0 -22
- data/lib/kamal/commands/traefik.rb +0 -85
- data/lib/kamal/configuration/docs/healthcheck.yml +0 -59
- data/lib/kamal/configuration/docs/traefik.yml +0 -62
- data/lib/kamal/configuration/healthcheck.rb +0 -63
- data/lib/kamal/configuration/traefik.rb +0 -60
- /data/lib/kamal/cli/templates/sample_hooks/{pre-traefik-reboot.sample → pre-proxy-reboot.sample} +0 -0
data/lib/kamal/configuration.rb
CHANGED
@@ -2,19 +2,22 @@ require "active_support/ordered_options"
|
|
2
2
|
require "active_support/core_ext/string/inquiry"
|
3
3
|
require "active_support/core_ext/module/delegation"
|
4
4
|
require "active_support/core_ext/hash/keys"
|
5
|
-
require "pathname"
|
6
5
|
require "erb"
|
7
6
|
require "net/ssh/proxy/jump"
|
8
7
|
|
9
8
|
class Kamal::Configuration
|
10
|
-
delegate :service, :image, :labels, :
|
9
|
+
delegate :service, :image, :labels, :hooks_path, to: :raw_config, allow_nil: true
|
11
10
|
delegate :argumentize, :optionize, to: Kamal::Utils
|
12
11
|
|
13
|
-
attr_reader :destination, :raw_config
|
14
|
-
attr_reader :accessories, :aliases, :boot, :builder, :env, :
|
12
|
+
attr_reader :destination, :raw_config, :secrets
|
13
|
+
attr_reader :accessories, :aliases, :boot, :builder, :env, :logging, :proxy, :servers, :ssh, :sshkit, :registry
|
15
14
|
|
16
15
|
include Validation
|
17
16
|
|
17
|
+
PROXY_MINIMUM_VERSION = "v0.3.0"
|
18
|
+
PROXY_HTTP_PORT = 80
|
19
|
+
PROXY_HTTPS_PORT = 443
|
20
|
+
|
18
21
|
class << self
|
19
22
|
def create_from(config_file:, destination: nil, version: nil)
|
20
23
|
raw_config = load_config_files(config_file, *destination_config_file(config_file, destination))
|
@@ -49,6 +52,8 @@ class Kamal::Configuration
|
|
49
52
|
|
50
53
|
validate! raw_config, example: validation_yml.symbolize_keys, context: "", with: Kamal::Configuration::Validator::Configuration
|
51
54
|
|
55
|
+
@secrets = Kamal::Secrets.new(destination: destination)
|
56
|
+
|
52
57
|
# Eager load config to validate it, these are first as they have dependencies later on
|
53
58
|
@servers = Servers.new(config: self)
|
54
59
|
@registry = Registry.new(config: self)
|
@@ -57,11 +62,10 @@ class Kamal::Configuration
|
|
57
62
|
@aliases = @raw_config.aliases&.keys&.to_h { |name| [ name, Alias.new(name, config: self) ] } || {}
|
58
63
|
@boot = Boot.new(config: self)
|
59
64
|
@builder = Builder.new(config: self)
|
60
|
-
@env = Env.new(config: @raw_config.env || {})
|
65
|
+
@env = Env.new(config: @raw_config.env || {}, secrets: secrets)
|
61
66
|
|
62
|
-
@healthcheck = Healthcheck.new(healthcheck_config: @raw_config.healthcheck)
|
63
67
|
@logging = Logging.new(logging_config: @raw_config.logging)
|
64
|
-
@
|
68
|
+
@proxy = Proxy.new(config: self, proxy_config: @raw_config.proxy || {})
|
65
69
|
@ssh = Ssh.new(config: self)
|
66
70
|
@sshkit = Sshkit.new(config: self)
|
67
71
|
|
@@ -70,6 +74,9 @@ class Kamal::Configuration
|
|
70
74
|
ensure_valid_kamal_version
|
71
75
|
ensure_retain_containers_valid
|
72
76
|
ensure_valid_service_name
|
77
|
+
ensure_no_traefik_reboot_hooks
|
78
|
+
ensure_one_host_for_ssl_roles
|
79
|
+
ensure_unique_hosts_for_ssl_roles
|
73
80
|
end
|
74
81
|
|
75
82
|
|
@@ -130,16 +137,16 @@ class Kamal::Configuration
|
|
130
137
|
raw_config.allow_empty_roles
|
131
138
|
end
|
132
139
|
|
133
|
-
def
|
134
|
-
roles.select(&:
|
140
|
+
def proxy_roles
|
141
|
+
roles.select(&:running_proxy?)
|
135
142
|
end
|
136
143
|
|
137
|
-
def
|
138
|
-
|
144
|
+
def proxy_role_names
|
145
|
+
proxy_roles.flat_map(&:name)
|
139
146
|
end
|
140
147
|
|
141
|
-
def
|
142
|
-
|
148
|
+
def proxy_hosts
|
149
|
+
proxy_roles.flat_map(&:hosts).uniq
|
143
150
|
end
|
144
151
|
|
145
152
|
def repository
|
@@ -184,31 +191,44 @@ class Kamal::Configuration
|
|
184
191
|
end
|
185
192
|
|
186
193
|
|
187
|
-
def healthcheck_service
|
188
|
-
[ "healthcheck", service, destination ].compact.join("-")
|
189
|
-
end
|
190
|
-
|
191
194
|
def readiness_delay
|
192
195
|
raw_config.readiness_delay || 7
|
193
196
|
end
|
194
197
|
|
195
|
-
def
|
196
|
-
|
198
|
+
def deploy_timeout
|
199
|
+
raw_config.deploy_timeout || 30
|
200
|
+
end
|
201
|
+
|
202
|
+
def drain_timeout
|
203
|
+
raw_config.drain_timeout || 30
|
197
204
|
end
|
198
205
|
|
199
206
|
|
200
207
|
def run_directory
|
201
|
-
|
208
|
+
".kamal"
|
202
209
|
end
|
203
210
|
|
204
|
-
def
|
205
|
-
|
206
|
-
run_directory
|
207
|
-
else
|
208
|
-
File.join "$(pwd)", run_directory
|
209
|
-
end
|
211
|
+
def apps_directory
|
212
|
+
File.join run_directory, "apps"
|
210
213
|
end
|
211
214
|
|
215
|
+
def app_directory
|
216
|
+
File.join apps_directory, [ service, destination ].compact.join("-")
|
217
|
+
end
|
218
|
+
|
219
|
+
def proxy_directory
|
220
|
+
File.join run_directory, "proxy"
|
221
|
+
end
|
222
|
+
|
223
|
+
def env_directory
|
224
|
+
File.join app_directory, "env"
|
225
|
+
end
|
226
|
+
|
227
|
+
def assets_directory
|
228
|
+
File.join app_directory, "assets"
|
229
|
+
end
|
230
|
+
|
231
|
+
|
212
232
|
def hooks_path
|
213
233
|
raw_config.hooks_path || ".kamal/hooks"
|
214
234
|
end
|
@@ -218,13 +238,9 @@ class Kamal::Configuration
|
|
218
238
|
end
|
219
239
|
|
220
240
|
|
221
|
-
def host_env_directory
|
222
|
-
File.join(run_directory, "env")
|
223
|
-
end
|
224
|
-
|
225
241
|
def env_tags
|
226
242
|
@env_tags ||= if (tags = raw_config.env["tags"])
|
227
|
-
tags.collect { |name, config| Env::Tag.new(name, config: config) }
|
243
|
+
tags.collect { |name, config| Env::Tag.new(name, config: config, secrets: secrets) }
|
228
244
|
else
|
229
245
|
[]
|
230
246
|
end
|
@@ -234,6 +250,24 @@ class Kamal::Configuration
|
|
234
250
|
env_tags.detect { |t| t.name == name.to_s }
|
235
251
|
end
|
236
252
|
|
253
|
+
def proxy_publish_args
|
254
|
+
argumentize "--publish", [ "#{PROXY_HTTP_PORT}:#{PROXY_HTTP_PORT}", "#{PROXY_HTTPS_PORT}:#{PROXY_HTTPS_PORT}" ]
|
255
|
+
end
|
256
|
+
|
257
|
+
def proxy_image
|
258
|
+
"basecamp/kamal-proxy:#{PROXY_MINIMUM_VERSION}"
|
259
|
+
end
|
260
|
+
|
261
|
+
def proxy_container_name
|
262
|
+
"kamal-proxy"
|
263
|
+
end
|
264
|
+
|
265
|
+
def proxy_config_volume
|
266
|
+
Kamal::Configuration::Volume.new \
|
267
|
+
host_path: File.join(proxy_directory, "config"),
|
268
|
+
container_path: "/home/kamal-proxy/.config/kamal-proxy"
|
269
|
+
end
|
270
|
+
|
237
271
|
|
238
272
|
def to_h
|
239
273
|
{
|
@@ -249,8 +283,7 @@ class Kamal::Configuration
|
|
249
283
|
sshkit: sshkit.to_h,
|
250
284
|
builder: builder.to_h,
|
251
285
|
accessories: raw_config.accessories,
|
252
|
-
logging: logging_args
|
253
|
-
healthcheck: healthcheck.to_h
|
286
|
+
logging: logging_args
|
254
287
|
}.compact
|
255
288
|
end
|
256
289
|
|
@@ -308,6 +341,30 @@ class Kamal::Configuration
|
|
308
341
|
true
|
309
342
|
end
|
310
343
|
|
344
|
+
def ensure_no_traefik_reboot_hooks
|
345
|
+
hooks = %w[ pre-traefik-reboot post-traefik-reboot ].select { |hook_file| File.exist?(File.join(hooks_path, hook_file)) }
|
346
|
+
|
347
|
+
if hooks.any?
|
348
|
+
raise Kamal::ConfigurationError, "Found #{hooks.join(", ")}, these should be renamed to (pre|post)-proxy-reboot"
|
349
|
+
end
|
350
|
+
|
351
|
+
true
|
352
|
+
end
|
353
|
+
|
354
|
+
def ensure_one_host_for_ssl_roles
|
355
|
+
roles.each(&:ensure_one_host_for_ssl)
|
356
|
+
|
357
|
+
true
|
358
|
+
end
|
359
|
+
|
360
|
+
def ensure_unique_hosts_for_ssl_roles
|
361
|
+
hosts = roles.select(&:ssl?).map { |role| role.proxy.host }
|
362
|
+
duplicates = hosts.tally.filter_map { |host, count| host if count > 1 }
|
363
|
+
|
364
|
+
raise Kamal::ConfigurationError, "Different roles can't share the same host for SSL: #{duplicates.join(", ")}" if duplicates.any?
|
365
|
+
|
366
|
+
true
|
367
|
+
end
|
311
368
|
|
312
369
|
def role_names
|
313
370
|
raw_config.servers.is_a?(Array) ? [ "web" ] : raw_config.servers.keys.sort
|
data/lib/kamal/env_file.rb
CHANGED
@@ -0,0 +1,18 @@
|
|
1
|
+
class Kamal::Secrets::Adapters::Base
|
2
|
+
delegate :optionize, to: Kamal::Utils
|
3
|
+
|
4
|
+
def fetch(secrets, account:, from: nil)
|
5
|
+
session = login(account)
|
6
|
+
full_secrets = secrets.map { |secret| [ from, secret ].compact.join("/") }
|
7
|
+
fetch_secrets(full_secrets, account: account, session: session)
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
def login(...)
|
12
|
+
raise NotImplementedError
|
13
|
+
end
|
14
|
+
|
15
|
+
def fetch_secrets(...)
|
16
|
+
raise NotImplementedError
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
class Kamal::Secrets::Adapters::Bitwarden < Kamal::Secrets::Adapters::Base
|
2
|
+
private
|
3
|
+
def login(account)
|
4
|
+
status = run_command("status")
|
5
|
+
|
6
|
+
if status["status"] == "unauthenticated"
|
7
|
+
run_command("login #{account.shellescape}", raw: true)
|
8
|
+
status = run_command("status")
|
9
|
+
end
|
10
|
+
|
11
|
+
if status["status"] == "locked"
|
12
|
+
session = run_command("unlock --raw", raw: true).presence
|
13
|
+
status = run_command("status", session: session)
|
14
|
+
end
|
15
|
+
|
16
|
+
raise RuntimeError, "Failed to login to and unlock Bitwarden" unless status["status"] == "unlocked"
|
17
|
+
|
18
|
+
run_command("sync", session: session, raw: true)
|
19
|
+
raise RuntimeError, "Failed to sync Bitwarden" unless $?.success?
|
20
|
+
|
21
|
+
session
|
22
|
+
end
|
23
|
+
|
24
|
+
def fetch_secrets(secrets, account:, session:)
|
25
|
+
{}.tap do |results|
|
26
|
+
items_fields(secrets).each do |item, fields|
|
27
|
+
item_json = run_command("get item #{item.shellescape}", session: session, raw: true)
|
28
|
+
raise RuntimeError, "Could not read #{secret} from Bitwarden" unless $?.success?
|
29
|
+
item_json = JSON.parse(item_json)
|
30
|
+
|
31
|
+
if fields.any?
|
32
|
+
fields.each do |field|
|
33
|
+
item_field = item_json["fields"].find { |f| f["name"] == field }
|
34
|
+
raise RuntimeError, "Could not find field #{field} in item #{item} in Bitwarden" unless item_field
|
35
|
+
value = item_field["value"]
|
36
|
+
results["#{item}/#{field}"] = value
|
37
|
+
end
|
38
|
+
else
|
39
|
+
results[item] = item_json["login"]["password"]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def items_fields(secrets)
|
46
|
+
{}.tap do |items|
|
47
|
+
secrets.each do |secret|
|
48
|
+
item, field = secret.split("/")
|
49
|
+
items[item] ||= []
|
50
|
+
items[item] << field
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def signedin?(account)
|
56
|
+
run_command("status")["status"] != "unauthenticated"
|
57
|
+
end
|
58
|
+
|
59
|
+
def run_command(command, session: nil, raw: false)
|
60
|
+
full_command = [ *("BW_SESSION=#{session.shellescape}" if session), "bw", command ].join(" ")
|
61
|
+
result = `#{full_command}`.strip
|
62
|
+
raw ? result : JSON.parse(result)
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
class Kamal::Secrets::Adapters::LastPass < Kamal::Secrets::Adapters::Base
|
2
|
+
private
|
3
|
+
def login(account)
|
4
|
+
unless loggedin?(account)
|
5
|
+
`lpass login #{account.shellescape}`
|
6
|
+
raise RuntimeError, "Failed to login to 1Password" unless $?.success?
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def loggedin?(account)
|
11
|
+
`lpass status --color never`.strip == "Logged in as #{account}."
|
12
|
+
end
|
13
|
+
|
14
|
+
def fetch_secrets(secrets, account:, session:)
|
15
|
+
items = `lpass show #{secrets.map(&:shellescape).join(" ")} --json`
|
16
|
+
raise RuntimeError, "Could not read #{secrets} from 1Password" unless $?.success?
|
17
|
+
|
18
|
+
items = JSON.parse(items)
|
19
|
+
|
20
|
+
{}.tap do |results|
|
21
|
+
items.each do |item|
|
22
|
+
results[item["fullname"]] = item["password"]
|
23
|
+
end
|
24
|
+
|
25
|
+
if (missing_items = secrets - results.keys).any?
|
26
|
+
raise RuntimeError, "Could not find #{missing_items.join(", ")} in LassPass"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
class Kamal::Secrets::Adapters::OnePassword < Kamal::Secrets::Adapters::Base
|
2
|
+
delegate :optionize, to: Kamal::Utils
|
3
|
+
|
4
|
+
private
|
5
|
+
def login(account)
|
6
|
+
unless loggedin?(account)
|
7
|
+
`op signin #{to_options(account: account, force: true, raw: true)}`.tap do
|
8
|
+
raise RuntimeError, "Failed to login to 1Password" unless $?.success?
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def loggedin?(account)
|
14
|
+
`op account get --account #{account.shellescape} 2> /dev/null`
|
15
|
+
$?.success?
|
16
|
+
end
|
17
|
+
|
18
|
+
def fetch_secrets(secrets, account:, session:)
|
19
|
+
{}.tap do |results|
|
20
|
+
vaults_items_fields(secrets).map do |vault, items|
|
21
|
+
items.each do |item, fields|
|
22
|
+
fields_json = JSON.parse(op_item_get(vault, item, fields, account: account, session: session))
|
23
|
+
fields_json = [ fields_json ] if fields.one?
|
24
|
+
|
25
|
+
fields_json.each do |field_json|
|
26
|
+
# The reference is in the form `op://vault/item/field[/field]`
|
27
|
+
field = field_json["reference"].delete_prefix("op://").delete_suffix("/password")
|
28
|
+
results[field] = field_json["value"]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_options(**options)
|
36
|
+
optionize(options.compact).join(" ")
|
37
|
+
end
|
38
|
+
|
39
|
+
def vaults_items_fields(secrets)
|
40
|
+
{}.tap do |vaults|
|
41
|
+
secrets.each do |secret|
|
42
|
+
secret = secret.delete_prefix("op://")
|
43
|
+
vault, item, *fields = secret.split("/")
|
44
|
+
fields << "password" if fields.empty?
|
45
|
+
|
46
|
+
vaults[vault] ||= {}
|
47
|
+
vaults[vault][item] ||= []
|
48
|
+
vaults[vault][item] << fields.join(".")
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def op_item_get(vault, item, fields, account:, session:)
|
54
|
+
labels = fields.map { |field| "label=#{field}" }.join(",")
|
55
|
+
options = to_options(vault: vault, fields: labels, format: "json", account: account, session: session.presence)
|
56
|
+
|
57
|
+
`op item get #{item.shellescape} #{options}`.tap do
|
58
|
+
raise RuntimeError, "Could not read #{fields.join(", ")} from #{item} in the #{vault} 1Password vault" unless $?.success?
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require "active_support/core_ext/string/inflections"
|
2
|
+
module Kamal::Secrets::Adapters
|
3
|
+
def self.lookup(name)
|
4
|
+
name = "one_password" if name.downcase == "1password"
|
5
|
+
name = "last_pass" if name.downcase == "lastpass"
|
6
|
+
adapter_class(name)
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.adapter_class(name)
|
10
|
+
Object.const_get("Kamal::Secrets::Adapters::#{name.camelize}").new
|
11
|
+
rescue NameError => e
|
12
|
+
raise RuntimeError, "Unknown secrets adapter: #{name}"
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
class Kamal::Secrets::Dotenv::InlineCommandSubstitution
|
2
|
+
class << self
|
3
|
+
def install!
|
4
|
+
::Dotenv::Parser.substitutions.map! { |sub| sub == ::Dotenv::Substitutions::Command ? self : sub }
|
5
|
+
end
|
6
|
+
|
7
|
+
def call(value, _env, overwrite: false)
|
8
|
+
# Process interpolated shell commands
|
9
|
+
value.gsub(Dotenv::Substitutions::Command.singleton_class::INTERPOLATED_SHELL_COMMAND) do |*|
|
10
|
+
# Eliminate opening and closing parentheses
|
11
|
+
command = $LAST_MATCH_INFO[:cmd][1..-2]
|
12
|
+
|
13
|
+
if $LAST_MATCH_INFO[:backslash]
|
14
|
+
# Command is escaped, don't replace it.
|
15
|
+
$LAST_MATCH_INFO[0][1..]
|
16
|
+
else
|
17
|
+
if command =~ /\A\s*kamal\s*secrets\s+/
|
18
|
+
# Inline the command
|
19
|
+
inline_secrets_command(command)
|
20
|
+
else
|
21
|
+
# Execute the command and return the value
|
22
|
+
`#{command}`.chomp
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def inline_secrets_command(command)
|
29
|
+
Kamal::Cli::Main.start(command.shellsplit[1..] + [ "--inline" ]).chomp
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require "dotenv"
|
2
|
+
|
3
|
+
class Kamal::Secrets
|
4
|
+
attr_reader :secrets_files
|
5
|
+
|
6
|
+
Kamal::Secrets::Dotenv::InlineCommandSubstitution.install!
|
7
|
+
|
8
|
+
def initialize(destination: nil)
|
9
|
+
@secrets_files = \
|
10
|
+
[ ".kamal/secrets-common", ".kamal/secrets#{(".#{destination}" if destination)}" ].select { |f| File.exist?(f) }
|
11
|
+
@mutex = Mutex.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def [](key)
|
15
|
+
# Fetching secrets may ask the user for input, so ensure only one thread does that
|
16
|
+
@mutex.synchronize do
|
17
|
+
secrets.fetch(key)
|
18
|
+
end
|
19
|
+
rescue KeyError
|
20
|
+
if secrets_files
|
21
|
+
raise Kamal::ConfigurationError, "Secret '#{key}' not found in #{secrets_files.join(", ")}"
|
22
|
+
else
|
23
|
+
raise Kamal::ConfigurationError, "Secret '#{key}' not found, no secret files provided"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_h
|
28
|
+
secrets
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
def secrets
|
33
|
+
@secrets ||= secrets_files.inject({}) do |secrets, secrets_file|
|
34
|
+
secrets.merge!(::Dotenv.parse(secrets_file))
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/lib/kamal/utils.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require "active_support/core_ext/object/try"
|
2
|
+
|
1
3
|
module Kamal::Utils
|
2
4
|
extend self
|
3
5
|
|
@@ -54,6 +56,12 @@ module Kamal::Utils
|
|
54
56
|
|
55
57
|
# Escape a value to make it safe for shell use.
|
56
58
|
def escape_shell_value(value)
|
59
|
+
value.to_s.scan(/[\x00-\x7F]+|[^\x00-\x7F]+/) \
|
60
|
+
.map { |part| part.ascii_only? ? escape_ascii_shell_value(part) : part }
|
61
|
+
.join
|
62
|
+
end
|
63
|
+
|
64
|
+
def escape_ascii_shell_value(value)
|
57
65
|
value.to_s.dump
|
58
66
|
.gsub(/`/, '\\\\`')
|
59
67
|
.gsub(DOLLAR_SIGN_WITHOUT_SHELL_EXPANSION_REGEX, '\$')
|
@@ -93,4 +101,8 @@ module Kamal::Utils
|
|
93
101
|
arch
|
94
102
|
end
|
95
103
|
end
|
104
|
+
|
105
|
+
def older_version?(version, other_version)
|
106
|
+
Gem::Version.new(version.delete_prefix("v")) < Gem::Version.new(other_version.delete_prefix("v"))
|
107
|
+
end
|
96
108
|
end
|
data/lib/kamal/version.rb
CHANGED
data/lib/kamal.rb
CHANGED
@@ -5,8 +5,10 @@ end
|
|
5
5
|
require "active_support"
|
6
6
|
require "zeitwerk"
|
7
7
|
require "yaml"
|
8
|
+
require "tmpdir"
|
9
|
+
require "pathname"
|
8
10
|
|
9
11
|
loader = Zeitwerk::Loader.for_gem
|
10
12
|
loader.ignore(File.join(__dir__, "kamal", "sshkit_with_ext.rb"))
|
11
13
|
loader.setup
|
12
|
-
loader.
|
14
|
+
loader.eager_load_namespace(Kamal::Cli) # We need all commands loaded.
|