kamal 2.8.2 → 2.10.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 +4 -4
- data/lib/kamal/cli/accessory.rb +14 -7
- data/lib/kamal/cli/app/boot.rb +1 -1
- data/lib/kamal/cli/app.rb +74 -115
- data/lib/kamal/cli/healthcheck/poller.rb +1 -1
- data/lib/kamal/cli/main.rb +2 -1
- data/lib/kamal/cli/proxy.rb +42 -35
- data/lib/kamal/cli/secrets.rb +2 -1
- data/lib/kamal/cli/server.rb +2 -1
- data/lib/kamal/cli/templates/secrets +1 -0
- data/lib/kamal/commander.rb +3 -2
- data/lib/kamal/commands/app/execution.rb +7 -1
- data/lib/kamal/commands/app.rb +1 -1
- data/lib/kamal/commands/proxy.rb +21 -2
- data/lib/kamal/configuration/accessory.rb +63 -26
- data/lib/kamal/configuration/boot.rb +4 -0
- data/lib/kamal/configuration/builder.rb +10 -3
- data/lib/kamal/configuration/docs/accessory.yml +37 -5
- data/lib/kamal/configuration/docs/boot.yml +12 -10
- data/lib/kamal/configuration/docs/configuration.yml +10 -1
- data/lib/kamal/configuration/docs/proxy.yml +24 -0
- data/lib/kamal/configuration/docs/ssh.yml +7 -4
- data/lib/kamal/configuration/docs/sshkit.yml +8 -0
- data/lib/kamal/configuration/env.rb +7 -3
- data/lib/kamal/configuration/proxy/boot.rb +4 -9
- data/lib/kamal/configuration/proxy/run.rb +143 -0
- data/lib/kamal/configuration/proxy.rb +2 -2
- data/lib/kamal/configuration/role.rb +15 -3
- data/lib/kamal/configuration/ssh.rb +18 -3
- data/lib/kamal/configuration/sshkit.rb +4 -0
- data/lib/kamal/configuration/validator/proxy.rb +20 -0
- data/lib/kamal/configuration/validator.rb +34 -1
- data/lib/kamal/configuration/volume.rb +11 -4
- data/lib/kamal/configuration.rb +32 -1
- data/lib/kamal/secrets/adapters/passbolt.rb +1 -2
- data/lib/kamal/secrets/adapters/test.rb +3 -1
- data/lib/kamal/secrets/dotenv/inline_command_substitution.rb +15 -1
- data/lib/kamal/secrets.rb +17 -6
- data/lib/kamal/sshkit_with_ext.rb +127 -10
- data/lib/kamal/utils.rb +3 -3
- data/lib/kamal/version.rb +1 -1
- metadata +2 -1
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
class Kamal::Configuration::Proxy::Run
|
|
2
|
+
MINIMUM_VERSION = "v0.9.0"
|
|
3
|
+
DEFAULT_HTTP_PORT = 80
|
|
4
|
+
DEFAULT_HTTPS_PORT = 443
|
|
5
|
+
DEFAULT_LOG_MAX_SIZE = "10m"
|
|
6
|
+
|
|
7
|
+
attr_reader :config, :run_config
|
|
8
|
+
delegate :argumentize, :optionize, to: Kamal::Utils
|
|
9
|
+
|
|
10
|
+
def initialize(config, run_config:, context: "proxy/run")
|
|
11
|
+
@config = config
|
|
12
|
+
@run_config = run_config
|
|
13
|
+
@context = context
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def debug?
|
|
17
|
+
run_config.fetch("debug", nil)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def publish?
|
|
21
|
+
run_config.fetch("publish", true)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def http_port
|
|
25
|
+
run_config.fetch("http_port", DEFAULT_HTTP_PORT)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def https_port
|
|
29
|
+
run_config.fetch("https_port", DEFAULT_HTTPS_PORT)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def bind_ips
|
|
33
|
+
run_config.fetch("bind_ips", nil)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def publish_args
|
|
37
|
+
if publish?
|
|
38
|
+
(bind_ips || [ nil ]).map do |bind_ip|
|
|
39
|
+
bind_ip = format_bind_ip(bind_ip)
|
|
40
|
+
publish_http = [ bind_ip, http_port, DEFAULT_HTTP_PORT ].compact.join(":")
|
|
41
|
+
publish_https = [ bind_ip, https_port, DEFAULT_HTTPS_PORT ].compact.join(":")
|
|
42
|
+
|
|
43
|
+
argumentize "--publish", [ publish_http, publish_https ]
|
|
44
|
+
end.join(" ")
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def log_max_size
|
|
49
|
+
run_config.fetch("log_max_size", DEFAULT_LOG_MAX_SIZE)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def logging_args
|
|
53
|
+
argumentize "--log-opt", "max-size=#{log_max_size}" if log_max_size.present?
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def version
|
|
57
|
+
run_config.fetch("version", MINIMUM_VERSION)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def registry
|
|
61
|
+
run_config.fetch("registry", nil)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def repository
|
|
65
|
+
run_config.fetch("repository", "basecamp/kamal-proxy")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def image
|
|
69
|
+
"#{[ registry, repository ].compact.join("/")}:#{version}"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def container_name
|
|
73
|
+
"kamal-proxy"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def options_args
|
|
77
|
+
if args = run_config["options"]
|
|
78
|
+
optionize args
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def run_command
|
|
83
|
+
[ "kamal-proxy", "run", *optionize(run_command_options) ].join(" ")
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def metrics_port
|
|
87
|
+
run_config["metrics_port"]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def run_command_options
|
|
91
|
+
{ debug: debug? || nil, "metrics-port": metrics_port }.compact
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def docker_options_args
|
|
95
|
+
[
|
|
96
|
+
*apps_volume_args,
|
|
97
|
+
*publish_args,
|
|
98
|
+
*logging_args,
|
|
99
|
+
*("--expose=#{metrics_port}" if metrics_port.present?),
|
|
100
|
+
*options_args
|
|
101
|
+
].compact
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def host_directory
|
|
105
|
+
File.join config.run_directory, "proxy"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def apps_directory
|
|
109
|
+
File.join host_directory, "apps-config"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def apps_container_directory
|
|
113
|
+
"/home/kamal-proxy/.apps-config"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def apps_volume
|
|
117
|
+
Kamal::Configuration::Volume.new \
|
|
118
|
+
host_path: apps_directory,
|
|
119
|
+
container_path: apps_container_directory
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def apps_volume_args
|
|
123
|
+
[ apps_volume.docker_args ]
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def app_directory
|
|
127
|
+
File.join apps_directory, config.service_and_destination
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def app_container_directory
|
|
131
|
+
File.join apps_container_directory, config.service_and_destination
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
private
|
|
135
|
+
def format_bind_ip(ip)
|
|
136
|
+
# Ensure IPv6 address inside square brackets - e.g. [::1]
|
|
137
|
+
if ip =~ Resolv::IPv6::Regex && ip !~ /\A\[.*\]\z/
|
|
138
|
+
"[#{ip}]"
|
|
139
|
+
else
|
|
140
|
+
ip
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
@@ -6,8 +6,7 @@ class Kamal::Configuration::Proxy
|
|
|
6
6
|
|
|
7
7
|
delegate :argumentize, :optionize, to: Kamal::Utils
|
|
8
8
|
|
|
9
|
-
attr_reader :config, :proxy_config, :role_name, :secrets
|
|
10
|
-
|
|
9
|
+
attr_reader :config, :proxy_config, :role_name, :run, :secrets
|
|
11
10
|
def initialize(config:, proxy_config:, role_name: nil, secrets:, context: "proxy")
|
|
12
11
|
@config = config
|
|
13
12
|
@proxy_config = proxy_config
|
|
@@ -15,6 +14,7 @@ class Kamal::Configuration::Proxy
|
|
|
15
14
|
@role_name = role_name
|
|
16
15
|
@secrets = secrets
|
|
17
16
|
validate! @proxy_config, with: Kamal::Configuration::Validator::Proxy, context: context
|
|
17
|
+
@run = Kamal::Configuration::Proxy::Run.new(config, run_config: @proxy_config["run"], context: "#{context}/run") if @proxy_config && @proxy_config["run"].present?
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def app_port
|
|
@@ -36,7 +36,7 @@ class Kamal::Configuration::Role
|
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def env_tags(host)
|
|
39
|
-
tagged_hosts.fetch(host).collect { |tag| config.env_tag(tag) }
|
|
39
|
+
tagged_hosts.fetch(host).collect { |tag| config.env_tag(tag) }.compact
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
def cmd
|
|
@@ -127,7 +127,7 @@ class Kamal::Configuration::Role
|
|
|
127
127
|
|
|
128
128
|
|
|
129
129
|
def asset_path
|
|
130
|
-
|
|
130
|
+
asset_path_config&.dig(0)
|
|
131
131
|
end
|
|
132
132
|
|
|
133
133
|
def assets?
|
|
@@ -137,10 +137,14 @@ class Kamal::Configuration::Role
|
|
|
137
137
|
def asset_volume(version = config.version)
|
|
138
138
|
if assets?
|
|
139
139
|
Kamal::Configuration::Volume.new \
|
|
140
|
-
host_path: asset_volume_directory(version), container_path: asset_path
|
|
140
|
+
host_path: asset_volume_directory(version), container_path: asset_path, options: asset_path_options
|
|
141
141
|
end
|
|
142
142
|
end
|
|
143
143
|
|
|
144
|
+
def asset_path_options
|
|
145
|
+
asset_path_config&.dig(1)
|
|
146
|
+
end
|
|
147
|
+
|
|
144
148
|
def asset_extracted_directory(version = config.version)
|
|
145
149
|
File.join config.assets_directory, "extracted", [ name, version ].join("-")
|
|
146
150
|
end
|
|
@@ -219,4 +223,12 @@ class Kamal::Configuration::Role
|
|
|
219
223
|
labels.merge!(specializations["labels"]) if specializations["labels"].present?
|
|
220
224
|
end
|
|
221
225
|
end
|
|
226
|
+
|
|
227
|
+
def asset_path_config
|
|
228
|
+
raw_path = specializations["asset_path"] || config.asset_path
|
|
229
|
+
return nil unless raw_path.present?
|
|
230
|
+
|
|
231
|
+
parts = raw_path.split(":", 2)
|
|
232
|
+
[ parts[0], parts[1] ]
|
|
233
|
+
end
|
|
222
234
|
end
|
|
@@ -3,10 +3,11 @@ class Kamal::Configuration::Ssh
|
|
|
3
3
|
|
|
4
4
|
include Kamal::Configuration::Validation
|
|
5
5
|
|
|
6
|
-
attr_reader :ssh_config
|
|
6
|
+
attr_reader :ssh_config, :secrets
|
|
7
7
|
|
|
8
8
|
def initialize(config:)
|
|
9
9
|
@ssh_config = config.raw_config.ssh || {}
|
|
10
|
+
@secrets = config.secrets
|
|
10
11
|
validate! ssh_config
|
|
11
12
|
end
|
|
12
13
|
|
|
@@ -35,11 +36,25 @@ class Kamal::Configuration::Ssh
|
|
|
35
36
|
end
|
|
36
37
|
|
|
37
38
|
def key_data
|
|
38
|
-
ssh_config["key_data"]
|
|
39
|
+
key_data = ssh_config["key_data"]
|
|
40
|
+
return unless key_data
|
|
41
|
+
|
|
42
|
+
key_data.map do |k|
|
|
43
|
+
if secrets.key?(k)
|
|
44
|
+
secrets[k]
|
|
45
|
+
else
|
|
46
|
+
warn "Inline key_data usage is deprecated and will be removed in Kamal 3. Please store your key_data in a secret."
|
|
47
|
+
k
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def config
|
|
53
|
+
ssh_config["config"]
|
|
39
54
|
end
|
|
40
55
|
|
|
41
56
|
def options
|
|
42
|
-
{ user: user, port: port, proxy: proxy, logger: logger, keepalive: true, keepalive_interval: 30, keys_only: keys_only, keys: keys, key_data: key_data }.compact
|
|
57
|
+
{ user: user, port: port, proxy: proxy, logger: logger, keepalive: true, keepalive_interval: 30, keys_only: keys_only, keys: keys, key_data: key_data, config: config }.compact
|
|
43
58
|
end
|
|
44
59
|
|
|
45
60
|
def to_h
|
|
@@ -20,6 +20,26 @@ class Kamal::Configuration::Validator::Proxy < Kamal::Configuration::Validator
|
|
|
20
20
|
error "Missing certificate_pem setting (required when private_key_pem is present)"
|
|
21
21
|
end
|
|
22
22
|
end
|
|
23
|
+
|
|
24
|
+
if run_config = config["run"]
|
|
25
|
+
if run_config["bind_ips"].present?
|
|
26
|
+
ensure_valid_bind_ips(config["bind_ips"])
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
if run_config["publish"] == false
|
|
30
|
+
if run_config["bind_ips"].present? || run_config["http_port"].present? || run_config["https_port"].present?
|
|
31
|
+
error "Cannot set http_port, https_port or bind_ips when publish is false"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
23
35
|
end
|
|
24
36
|
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
def ensure_valid_bind_ips(bind_ips)
|
|
40
|
+
bind_ips.present? && bind_ips.each do |ip|
|
|
41
|
+
next if ip =~ Resolv::IPv4::Regex || ip =~ Resolv::IPv6::Regex
|
|
42
|
+
error "Invalid publish IP address: #{ip}"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
25
45
|
end
|
|
@@ -34,6 +34,10 @@ class Kamal::Configuration::Validator
|
|
|
34
34
|
elsif example_value.is_a?(Array)
|
|
35
35
|
if key == "arch"
|
|
36
36
|
validate_array_of_or_type! value, example_value.first.class
|
|
37
|
+
elsif key.to_s == "config"
|
|
38
|
+
validate_ssh_config!(value)
|
|
39
|
+
elsif key.to_s == "files" || key.to_s == "directories"
|
|
40
|
+
validate_paths!(value)
|
|
37
41
|
else
|
|
38
42
|
validate_array_of! value, example_value.first.class
|
|
39
43
|
end
|
|
@@ -129,6 +133,34 @@ class Kamal::Configuration::Validator
|
|
|
129
133
|
end
|
|
130
134
|
end
|
|
131
135
|
|
|
136
|
+
def validate_ssh_config!(config)
|
|
137
|
+
if config.is_a?(Array)
|
|
138
|
+
validate_array_of! config, String
|
|
139
|
+
elsif boolean?(config.class) || config.is_a?(String)
|
|
140
|
+
# Booleans and Strings are allowed
|
|
141
|
+
else
|
|
142
|
+
type_error(TrueClass, FalseClass, String, Array)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def validate_paths!(paths)
|
|
147
|
+
validate_type! paths, Array
|
|
148
|
+
|
|
149
|
+
paths.each_with_index do |path, index|
|
|
150
|
+
with_context(index) do
|
|
151
|
+
validate_type! path, String, Hash
|
|
152
|
+
|
|
153
|
+
if path.is_a?(Hash)
|
|
154
|
+
%w[local remote mode owner options].each do |key|
|
|
155
|
+
with_context(key) do
|
|
156
|
+
validate_type! path[key], String if path.key?(key)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
132
164
|
def validate_type!(value, *types)
|
|
133
165
|
type_error(*types) unless types.any? { |type| valid_type?(value, type) }
|
|
134
166
|
end
|
|
@@ -138,7 +170,8 @@ class Kamal::Configuration::Validator
|
|
|
138
170
|
end
|
|
139
171
|
|
|
140
172
|
def type_error(*expected_types)
|
|
141
|
-
|
|
173
|
+
descriptions = expected_types.map { |type| type_description(type) }.uniq
|
|
174
|
+
error "should be #{descriptions.join(" or ")}"
|
|
142
175
|
end
|
|
143
176
|
|
|
144
177
|
def unknown_keys_error(unknown_keys)
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
class Kamal::Configuration::Volume
|
|
2
|
-
attr_reader :host_path, :container_path
|
|
2
|
+
attr_reader :host_path, :container_path, :options
|
|
3
3
|
delegate :argumentize, to: Kamal::Utils
|
|
4
4
|
|
|
5
|
-
def initialize(host_path:, container_path:)
|
|
5
|
+
def initialize(host_path:, container_path:, options: nil)
|
|
6
6
|
@host_path = host_path
|
|
7
7
|
@container_path = container_path
|
|
8
|
+
@options = options
|
|
8
9
|
end
|
|
9
10
|
|
|
10
11
|
def docker_args
|
|
11
|
-
argumentize "--volume",
|
|
12
|
+
argumentize "--volume", docker_args_string
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def docker_args_string
|
|
16
|
+
volume_string = "#{host_path_for_docker_volume}:#{container_path}"
|
|
17
|
+
volume_string += ":#{options}" if options.present?
|
|
18
|
+
volume_string
|
|
12
19
|
end
|
|
13
20
|
|
|
14
21
|
private
|
|
@@ -16,7 +23,7 @@ class Kamal::Configuration::Volume
|
|
|
16
23
|
if Pathname.new(host_path).absolute?
|
|
17
24
|
host_path
|
|
18
25
|
else
|
|
19
|
-
|
|
26
|
+
"$PWD/#{host_path}"
|
|
20
27
|
end
|
|
21
28
|
end
|
|
22
29
|
end
|
data/lib/kamal/configuration.rb
CHANGED
|
@@ -50,7 +50,7 @@ class Kamal::Configuration
|
|
|
50
50
|
|
|
51
51
|
validate! raw_config, example: validation_yml.symbolize_keys, context: "", with: Kamal::Configuration::Validator::Configuration
|
|
52
52
|
|
|
53
|
-
@secrets = Kamal::Secrets.new(destination: destination)
|
|
53
|
+
@secrets = Kamal::Secrets.new(destination: destination, secrets_path: secrets_path)
|
|
54
54
|
|
|
55
55
|
# Eager load config to validate it, these are first as they have dependencies later on
|
|
56
56
|
@servers = Servers.new(config: self)
|
|
@@ -77,6 +77,7 @@ class Kamal::Configuration
|
|
|
77
77
|
ensure_one_host_for_ssl_roles
|
|
78
78
|
ensure_unique_hosts_for_ssl_roles
|
|
79
79
|
ensure_local_registry_remote_builder_has_ssh_url
|
|
80
|
+
ensure_no_conflicting_proxy_runs
|
|
80
81
|
end
|
|
81
82
|
|
|
82
83
|
def version=(version)
|
|
@@ -122,6 +123,14 @@ class Kamal::Configuration
|
|
|
122
123
|
(roles + accessories).flat_map(&:hosts).uniq
|
|
123
124
|
end
|
|
124
125
|
|
|
126
|
+
def host_roles(host)
|
|
127
|
+
roles.select { |role| role.hosts.include?(host) }
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def host_accessories(host)
|
|
131
|
+
accessories.select { |accessory| accessory.hosts.include?(host) }
|
|
132
|
+
end
|
|
133
|
+
|
|
125
134
|
def app_hosts
|
|
126
135
|
roles.flat_map(&:hosts).uniq
|
|
127
136
|
end
|
|
@@ -165,6 +174,11 @@ class Kamal::Configuration
|
|
|
165
174
|
name
|
|
166
175
|
end
|
|
167
176
|
|
|
177
|
+
def proxy_run(host)
|
|
178
|
+
# We validate that all the config are identical for a host
|
|
179
|
+
proxy_runs(host.to_s).first
|
|
180
|
+
end
|
|
181
|
+
|
|
168
182
|
def repository
|
|
169
183
|
[ registry.server, image ].compact.join("/")
|
|
170
184
|
end
|
|
@@ -241,6 +255,10 @@ class Kamal::Configuration
|
|
|
241
255
|
raw_config.hooks_path || ".kamal/hooks"
|
|
242
256
|
end
|
|
243
257
|
|
|
258
|
+
def secrets_path
|
|
259
|
+
raw_config.secrets_path || ".kamal/secrets"
|
|
260
|
+
end
|
|
261
|
+
|
|
244
262
|
def asset_path
|
|
245
263
|
raw_config.asset_path
|
|
246
264
|
end
|
|
@@ -374,6 +392,19 @@ class Kamal::Configuration
|
|
|
374
392
|
true
|
|
375
393
|
end
|
|
376
394
|
|
|
395
|
+
def ensure_no_conflicting_proxy_runs
|
|
396
|
+
all_hosts.each do |host|
|
|
397
|
+
run_configs = proxy_runs(host)
|
|
398
|
+
if run_configs.uniq.size > 1
|
|
399
|
+
raise Kamal::ConfigurationError, "Conflicting proxy run configurations for host #{host}"
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def proxy_runs(host)
|
|
405
|
+
(host_roles(host) + host_accessories(host)).map(&:proxy).compact.map(&:run).compact
|
|
406
|
+
end
|
|
407
|
+
|
|
377
408
|
def role_names
|
|
378
409
|
raw_config.servers.is_a?(Array) ? [ "web" ] : raw_config.servers.keys.sort
|
|
379
410
|
end
|
|
@@ -47,9 +47,8 @@ class Kamal::Secrets::Adapters::Passbolt < Kamal::Secrets::Adapters::Base
|
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
filter_condition = filter_conditions.any? ? "--filter '#{filter_conditions.join(" || ")}'" : ""
|
|
50
|
-
items = `passbolt list resources #{filter_condition} #{folders.map { |item| "--folder #{item["id"]}" }.join(" ")} --json`
|
|
50
|
+
items = `passbolt list resources #{filter_condition} #{folders.map { |item| "--folder #{item["id"].to_s.shellescape}" }.join(" ")} --json`
|
|
51
51
|
raise RuntimeError, "Could not read #{secrets} from Passbolt" unless $?.success?
|
|
52
|
-
|
|
53
52
|
items = JSON.parse(items)
|
|
54
53
|
found_names = items.map { |item| item["name"] }
|
|
55
54
|
missing_secrets = secret_names - found_names
|
|
@@ -5,7 +5,9 @@ class Kamal::Secrets::Adapters::Test < Kamal::Secrets::Adapters::Base
|
|
|
5
5
|
end
|
|
6
6
|
|
|
7
7
|
def fetch_secrets(secrets, from:, account:, session:)
|
|
8
|
-
prefixed_secrets(secrets, from: from).to_h
|
|
8
|
+
prefixed_secrets(secrets, from: from).to_h do |secret|
|
|
9
|
+
[ secret, secret.gsub("LPAREN", "(").gsub("RPAREN", ")").reverse ]
|
|
10
|
+
end
|
|
9
11
|
end
|
|
10
12
|
|
|
11
13
|
def check_dependencies!
|
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
class Kamal::Secrets::Dotenv::InlineCommandSubstitution
|
|
2
|
+
# Unlike dotenv, this regex does not match escaped
|
|
3
|
+
# parentheses when looking for command substitutions.
|
|
4
|
+
INTERPOLATED_SHELL_COMMAND = /
|
|
5
|
+
(?<backslash>\\)? # is it escaped with a backslash?
|
|
6
|
+
\$ # literal $
|
|
7
|
+
(?<cmd> # collect command content for eval
|
|
8
|
+
\( # require opening paren
|
|
9
|
+
(?:\\.|[^()\\]|\g<cmd>)+ # allow any number of non-parens or escaped
|
|
10
|
+
# parens (by nesting the <cmd> expression
|
|
11
|
+
# recursively)
|
|
12
|
+
\) # require closing paren
|
|
13
|
+
)
|
|
14
|
+
/x
|
|
15
|
+
|
|
2
16
|
class << self
|
|
3
17
|
def install!
|
|
4
18
|
::Dotenv::Parser.substitutions.map! { |sub| sub == ::Dotenv::Substitutions::Command ? self : sub }
|
|
@@ -6,7 +20,7 @@ class Kamal::Secrets::Dotenv::InlineCommandSubstitution
|
|
|
6
20
|
|
|
7
21
|
def call(value, env, overwrite: false)
|
|
8
22
|
# Process interpolated shell commands
|
|
9
|
-
value.gsub(
|
|
23
|
+
value.gsub(INTERPOLATED_SHELL_COMMAND) do |*|
|
|
10
24
|
# Eliminate opening and closing parentheses
|
|
11
25
|
command = $LAST_MATCH_INFO[:cmd][1..-2]
|
|
12
26
|
|
data/lib/kamal/secrets.rb
CHANGED
|
@@ -3,16 +3,14 @@ require "dotenv"
|
|
|
3
3
|
class Kamal::Secrets
|
|
4
4
|
Kamal::Secrets::Dotenv::InlineCommandSubstitution.install!
|
|
5
5
|
|
|
6
|
-
def initialize(destination: nil)
|
|
6
|
+
def initialize(destination: nil, secrets_path:)
|
|
7
7
|
@destination = destination
|
|
8
|
+
@secrets_path = secrets_path
|
|
8
9
|
@mutex = Mutex.new
|
|
9
10
|
end
|
|
10
11
|
|
|
11
12
|
def [](key)
|
|
12
|
-
|
|
13
|
-
@mutex.synchronize do
|
|
14
|
-
secrets.fetch(key)
|
|
15
|
-
end
|
|
13
|
+
synchronized_fetch(key)
|
|
16
14
|
rescue KeyError
|
|
17
15
|
if secrets_files.present?
|
|
18
16
|
raise Kamal::ConfigurationError, "Secret '#{key}' not found in #{secrets_files.join(", ")}"
|
|
@@ -29,6 +27,12 @@ class Kamal::Secrets
|
|
|
29
27
|
@secrets_files ||= secrets_filenames.select { |f| File.exist?(f) }
|
|
30
28
|
end
|
|
31
29
|
|
|
30
|
+
def key?(key)
|
|
31
|
+
synchronized_fetch(key).present?
|
|
32
|
+
rescue KeyError
|
|
33
|
+
false
|
|
34
|
+
end
|
|
35
|
+
|
|
32
36
|
private
|
|
33
37
|
def secrets
|
|
34
38
|
@secrets ||= secrets_files.inject({}) do |secrets, secrets_file|
|
|
@@ -37,6 +41,13 @@ class Kamal::Secrets
|
|
|
37
41
|
end
|
|
38
42
|
|
|
39
43
|
def secrets_filenames
|
|
40
|
-
[ "
|
|
44
|
+
[ "#{@secrets_path}-common", "#{@secrets_path}#{(".#{@destination}" if @destination)}" ]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def synchronized_fetch(key)
|
|
48
|
+
# Fetching secrets may ask the user for input, so ensure only one thread does that
|
|
49
|
+
@mutex.synchronize do
|
|
50
|
+
secrets.fetch(key)
|
|
51
|
+
end
|
|
41
52
|
end
|
|
42
53
|
end
|