kamal 2.0.0.alpha → 2.0.0.beta2
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 +21 -49
- data/lib/kamal/cli/build.rb +21 -14
- 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 +213 -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/execution.rb +1 -0
- 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 +8 -2
- data/lib/kamal/commands/lock.rb +2 -6
- data/lib/kamal/commands/proxy.rb +72 -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 +80 -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.4.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,40 @@ 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
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
211
|
+
def apps_directory
|
212
|
+
File.join run_directory, "apps"
|
213
|
+
end
|
214
|
+
|
215
|
+
def app_directory
|
216
|
+
File.join apps_directory, [ service, destination ].compact.join("-")
|
210
217
|
end
|
211
218
|
|
219
|
+
def env_directory
|
220
|
+
File.join app_directory, "env"
|
221
|
+
end
|
222
|
+
|
223
|
+
def assets_directory
|
224
|
+
File.join app_directory, "assets"
|
225
|
+
end
|
226
|
+
|
227
|
+
|
212
228
|
def hooks_path
|
213
229
|
raw_config.hooks_path || ".kamal/hooks"
|
214
230
|
end
|
@@ -218,13 +234,9 @@ class Kamal::Configuration
|
|
218
234
|
end
|
219
235
|
|
220
236
|
|
221
|
-
def host_env_directory
|
222
|
-
File.join(run_directory, "env")
|
223
|
-
end
|
224
|
-
|
225
237
|
def env_tags
|
226
238
|
@env_tags ||= if (tags = raw_config.env["tags"])
|
227
|
-
tags.collect { |name, config| Env::Tag.new(name, config: config) }
|
239
|
+
tags.collect { |name, config| Env::Tag.new(name, config: config, secrets: secrets) }
|
228
240
|
else
|
229
241
|
[]
|
230
242
|
end
|
@@ -234,6 +246,18 @@ class Kamal::Configuration
|
|
234
246
|
env_tags.detect { |t| t.name == name.to_s }
|
235
247
|
end
|
236
248
|
|
249
|
+
def proxy_publish_args
|
250
|
+
argumentize "--publish", [ "#{PROXY_HTTP_PORT}:#{PROXY_HTTP_PORT}", "#{PROXY_HTTPS_PORT}:#{PROXY_HTTPS_PORT}" ]
|
251
|
+
end
|
252
|
+
|
253
|
+
def proxy_image
|
254
|
+
"basecamp/kamal-proxy:#{PROXY_MINIMUM_VERSION}"
|
255
|
+
end
|
256
|
+
|
257
|
+
def proxy_container_name
|
258
|
+
"kamal-proxy"
|
259
|
+
end
|
260
|
+
|
237
261
|
|
238
262
|
def to_h
|
239
263
|
{
|
@@ -249,8 +273,7 @@ class Kamal::Configuration
|
|
249
273
|
sshkit: sshkit.to_h,
|
250
274
|
builder: builder.to_h,
|
251
275
|
accessories: raw_config.accessories,
|
252
|
-
logging: logging_args
|
253
|
-
healthcheck: healthcheck.to_h
|
276
|
+
logging: logging_args
|
254
277
|
}.compact
|
255
278
|
end
|
256
279
|
|
@@ -308,6 +331,30 @@ class Kamal::Configuration
|
|
308
331
|
true
|
309
332
|
end
|
310
333
|
|
334
|
+
def ensure_no_traefik_reboot_hooks
|
335
|
+
hooks = %w[ pre-traefik-reboot post-traefik-reboot ].select { |hook_file| File.exist?(File.join(hooks_path, hook_file)) }
|
336
|
+
|
337
|
+
if hooks.any?
|
338
|
+
raise Kamal::ConfigurationError, "Found #{hooks.join(", ")}, these should be renamed to (pre|post)-proxy-reboot"
|
339
|
+
end
|
340
|
+
|
341
|
+
true
|
342
|
+
end
|
343
|
+
|
344
|
+
def ensure_one_host_for_ssl_roles
|
345
|
+
roles.each(&:ensure_one_host_for_ssl)
|
346
|
+
|
347
|
+
true
|
348
|
+
end
|
349
|
+
|
350
|
+
def ensure_unique_hosts_for_ssl_roles
|
351
|
+
hosts = roles.select(&:ssl?).map { |role| role.proxy.host }
|
352
|
+
duplicates = hosts.tally.filter_map { |host, count| host if count > 1 }
|
353
|
+
|
354
|
+
raise Kamal::ConfigurationError, "Different roles can't share the same host for SSL: #{duplicates.join(", ")}" if duplicates.any?
|
355
|
+
|
356
|
+
true
|
357
|
+
end
|
311
358
|
|
312
359
|
def role_names
|
313
360
|
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.
|