kamal 1.8.3 → 2.7.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/README.md +1 -1
- data/lib/kamal/cli/accessory.rb +92 -38
- data/lib/kamal/cli/alias/command.rb +10 -0
- data/lib/kamal/cli/app/{prepare_assets.rb → assets.rb} +1 -1
- data/lib/kamal/cli/app/boot.rb +23 -16
- 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 +132 -30
- data/lib/kamal/cli/base.rb +57 -53
- data/lib/kamal/cli/build.rb +81 -38
- 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 +60 -59
- data/lib/kamal/cli/proxy.rb +290 -0
- data/lib/kamal/cli/prune.rb +0 -1
- data/lib/kamal/cli/registry.rb +2 -0
- data/lib/kamal/cli/secrets.rb +49 -0
- data/lib/kamal/cli/server.rb +6 -5
- data/lib/kamal/cli/templates/deploy.yml +53 -53
- data/lib/kamal/cli/templates/sample_hooks/docker-setup.sample +2 -12
- data/lib/kamal/cli/templates/sample_hooks/post-app-boot.sample +3 -0
- data/lib/kamal/cli/templates/sample_hooks/post-deploy.sample +1 -1
- 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 +1 -1
- data/lib/kamal/cli/templates/sample_hooks/pre-connect.sample +1 -1
- data/lib/kamal/cli/templates/sample_hooks/pre-deploy.sample +19 -6
- data/lib/kamal/cli/templates/sample_hooks/pre-proxy-reboot.sample +3 -0
- data/lib/kamal/cli/templates/secrets +17 -0
- data/lib/kamal/cli.rb +2 -0
- data/lib/kamal/commander/specifics.rb +19 -6
- data/lib/kamal/commander.rb +39 -32
- data/lib/kamal/commands/accessory/proxy.rb +16 -0
- data/lib/kamal/commands/accessory.rb +19 -19
- data/lib/kamal/commands/app/assets.rb +10 -10
- data/lib/kamal/commands/app/containers.rb +2 -2
- data/lib/kamal/commands/app/error_pages.rb +9 -0
- data/lib/kamal/commands/app/execution.rb +7 -4
- data/lib/kamal/commands/app/images.rb +1 -1
- data/lib/kamal/commands/app/logging.rb +16 -6
- data/lib/kamal/commands/app/proxy.rb +32 -0
- data/lib/kamal/commands/app.rb +25 -24
- data/lib/kamal/commands/auditor.rb +12 -3
- data/lib/kamal/commands/base.rb +54 -8
- data/lib/kamal/commands/builder/base.rb +46 -16
- data/lib/kamal/commands/builder/clone.rb +16 -14
- 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 +14 -0
- data/lib/kamal/commands/builder/pack.rb +46 -0
- data/lib/kamal/commands/builder/remote.rb +63 -0
- data/lib/kamal/commands/builder.rb +21 -45
- 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 +127 -0
- data/lib/kamal/commands/prune.rb +1 -9
- data/lib/kamal/commands/registry.rb +9 -7
- data/lib/kamal/commands/server.rb +11 -1
- data/lib/kamal/configuration/accessory.rb +89 -12
- data/lib/kamal/configuration/alias.rb +15 -0
- data/lib/kamal/configuration/builder.rb +73 -15
- data/lib/kamal/configuration/docs/accessory.yml +53 -15
- data/lib/kamal/configuration/docs/alias.yml +26 -0
- data/lib/kamal/configuration/docs/boot.yml +3 -3
- data/lib/kamal/configuration/docs/builder.yml +63 -38
- data/lib/kamal/configuration/docs/configuration.yml +62 -46
- data/lib/kamal/configuration/docs/env.yml +61 -17
- data/lib/kamal/configuration/docs/logging.yml +3 -3
- data/lib/kamal/configuration/docs/proxy.yml +168 -0
- data/lib/kamal/configuration/docs/registry.yml +20 -13
- data/lib/kamal/configuration/docs/role.yml +14 -13
- data/lib/kamal/configuration/docs/servers.yml +2 -2
- data/lib/kamal/configuration/docs/ssh.yml +23 -19
- data/lib/kamal/configuration/docs/sshkit.yml +4 -4
- data/lib/kamal/configuration/env/tag.rb +4 -3
- data/lib/kamal/configuration/env.rb +19 -17
- data/lib/kamal/configuration/proxy/boot.rb +129 -0
- data/lib/kamal/configuration/proxy.rb +124 -0
- data/lib/kamal/configuration/registry.rb +7 -6
- data/lib/kamal/configuration/role.rb +69 -98
- data/lib/kamal/configuration/servers.rb +8 -1
- data/lib/kamal/configuration/validator/accessory.rb +6 -2
- data/lib/kamal/configuration/validator/alias.rb +15 -0
- data/lib/kamal/configuration/validator/builder.rb +6 -0
- data/lib/kamal/configuration/validator/proxy.rb +25 -0
- data/lib/kamal/configuration/validator/role.rb +3 -1
- data/lib/kamal/configuration/validator/servers.rb +1 -1
- data/lib/kamal/configuration/validator.rb +62 -24
- data/lib/kamal/configuration.rb +96 -50
- data/lib/kamal/docker.rb +30 -0
- data/lib/kamal/env_file.rb +7 -1
- data/lib/kamal/git.rb +10 -0
- data/lib/kamal/secrets/adapters/aws_secrets_manager.rb +51 -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 +130 -0
- data/lib/kamal/secrets/adapters/test.rb +14 -0
- data/lib/kamal/secrets/adapters.rb +16 -0
- data/lib/kamal/secrets/dotenv/inline_command_substitution.rb +33 -0
- data/lib/kamal/secrets.rb +42 -0
- data/lib/kamal/sshkit_with_ext.rb +1 -0
- data/lib/kamal/utils.rb +30 -0
- data/lib/kamal/version.rb +1 -1
- data/lib/kamal.rb +3 -1
- metadata +63 -36
- 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/sample_hooks/pre-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/builder/multiarch/remote.rb +0 -65
- data/lib/kamal/commands/builder/multiarch.rb +0 -41
- data/lib/kamal/commands/builder/native/cached.rb +0 -25
- data/lib/kamal/commands/builder/native/remote.rb +0 -67
- data/lib/kamal/commands/builder/native.rb +0 -20
- 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/configuration.rb
CHANGED
@@ -2,21 +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, :boot, :builder, :env, :
|
12
|
+
attr_reader :destination, :raw_config, :secrets
|
13
|
+
attr_reader :accessories, :aliases, :boot, :builder, :env, :logging, :proxy, :proxy_boot, :servers, :ssh, :sshkit, :registry
|
15
14
|
|
16
15
|
include Validation
|
17
16
|
|
18
17
|
class << self
|
19
18
|
def create_from(config_file:, destination: nil, version: nil)
|
19
|
+
ENV["KAMAL_DESTINATION"] = destination
|
20
|
+
|
20
21
|
raw_config = load_config_files(config_file, *destination_config_file(config_file, destination))
|
21
22
|
|
22
23
|
new raw_config, destination: destination, version: version
|
@@ -31,7 +32,7 @@ class Kamal::Configuration
|
|
31
32
|
if file.exist?
|
32
33
|
# Newer Psych doesn't load aliases by default
|
33
34
|
load_method = YAML.respond_to?(:unsafe_load) ? :unsafe_load : :load
|
34
|
-
YAML.send(load_method, ERB.new(
|
35
|
+
YAML.send(load_method, ERB.new(File.read(file)).result).symbolize_keys
|
35
36
|
else
|
36
37
|
raise "Configuration file not found in #{file}"
|
37
38
|
end
|
@@ -49,18 +50,21 @@ class Kamal::Configuration
|
|
49
50
|
|
50
51
|
validate! raw_config, example: validation_yml.symbolize_keys, context: "", with: Kamal::Configuration::Validator::Configuration
|
51
52
|
|
53
|
+
@secrets = Kamal::Secrets.new(destination: destination)
|
54
|
+
|
52
55
|
# Eager load config to validate it, these are first as they have dependencies later on
|
53
56
|
@servers = Servers.new(config: self)
|
54
|
-
@registry = Registry.new(config:
|
57
|
+
@registry = Registry.new(config: @raw_config, secrets: secrets)
|
55
58
|
|
56
59
|
@accessories = @raw_config.accessories&.keys&.collect { |name| Accessory.new(name, config: self) } || []
|
60
|
+
@aliases = @raw_config.aliases&.keys&.to_h { |name| [ name, Alias.new(name, config: self) ] } || {}
|
57
61
|
@boot = Boot.new(config: self)
|
58
62
|
@builder = Builder.new(config: self)
|
59
|
-
@env = Env.new(config: @raw_config.env || {})
|
63
|
+
@env = Env.new(config: @raw_config.env || {}, secrets: secrets)
|
60
64
|
|
61
|
-
@healthcheck = Healthcheck.new(healthcheck_config: @raw_config.healthcheck)
|
62
65
|
@logging = Logging.new(logging_config: @raw_config.logging)
|
63
|
-
@
|
66
|
+
@proxy = Proxy.new(config: self, proxy_config: @raw_config.proxy, secrets: secrets)
|
67
|
+
@proxy_boot = Proxy::Boot.new(config: self)
|
64
68
|
@ssh = Ssh.new(config: self)
|
65
69
|
@sshkit = Sshkit.new(config: self)
|
66
70
|
|
@@ -69,9 +73,11 @@ class Kamal::Configuration
|
|
69
73
|
ensure_valid_kamal_version
|
70
74
|
ensure_retain_containers_valid
|
71
75
|
ensure_valid_service_name
|
76
|
+
ensure_no_traefik_reboot_hooks
|
77
|
+
ensure_one_host_for_ssl_roles
|
78
|
+
ensure_unique_hosts_for_ssl_roles
|
72
79
|
end
|
73
80
|
|
74
|
-
|
75
81
|
def version=(version)
|
76
82
|
@declared_version = version
|
77
83
|
end
|
@@ -95,6 +101,9 @@ class Kamal::Configuration
|
|
95
101
|
raw_config.minimum_version
|
96
102
|
end
|
97
103
|
|
104
|
+
def service_and_destination
|
105
|
+
[ service, destination ].compact.join("-")
|
106
|
+
end
|
98
107
|
|
99
108
|
def roles
|
100
109
|
servers.roles
|
@@ -108,11 +117,14 @@ class Kamal::Configuration
|
|
108
117
|
accessories.detect { |a| a.name == name.to_s }
|
109
118
|
end
|
110
119
|
|
111
|
-
|
112
120
|
def all_hosts
|
113
121
|
(roles + accessories).flat_map(&:hosts).uniq
|
114
122
|
end
|
115
123
|
|
124
|
+
def app_hosts
|
125
|
+
roles.flat_map(&:hosts).uniq
|
126
|
+
end
|
127
|
+
|
116
128
|
def primary_host
|
117
129
|
primary_role&.primary_host
|
118
130
|
end
|
@@ -129,16 +141,20 @@ class Kamal::Configuration
|
|
129
141
|
raw_config.allow_empty_roles
|
130
142
|
end
|
131
143
|
|
132
|
-
def
|
133
|
-
roles.select(&:
|
144
|
+
def proxy_roles
|
145
|
+
roles.select(&:running_proxy?)
|
146
|
+
end
|
147
|
+
|
148
|
+
def proxy_role_names
|
149
|
+
proxy_roles.flat_map(&:name)
|
134
150
|
end
|
135
151
|
|
136
|
-
def
|
137
|
-
|
152
|
+
def proxy_accessories
|
153
|
+
accessories.select(&:running_proxy?)
|
138
154
|
end
|
139
155
|
|
140
|
-
def
|
141
|
-
|
156
|
+
def proxy_hosts
|
157
|
+
(proxy_roles.flat_map(&:hosts) + proxy_accessories.flat_map(&:hosts)).uniq
|
142
158
|
end
|
143
159
|
|
144
160
|
def repository
|
@@ -169,7 +185,6 @@ class Kamal::Configuration
|
|
169
185
|
raw_config.retain_containers || 5
|
170
186
|
end
|
171
187
|
|
172
|
-
|
173
188
|
def volume_args
|
174
189
|
if raw_config.volumes.present?
|
175
190
|
argumentize "--volume", raw_config.volumes
|
@@ -182,30 +197,36 @@ class Kamal::Configuration
|
|
182
197
|
logging.args
|
183
198
|
end
|
184
199
|
|
185
|
-
|
186
|
-
def healthcheck_service
|
187
|
-
[ "healthcheck", service, destination ].compact.join("-")
|
188
|
-
end
|
189
|
-
|
190
200
|
def readiness_delay
|
191
201
|
raw_config.readiness_delay || 7
|
192
202
|
end
|
193
203
|
|
194
|
-
def
|
195
|
-
|
204
|
+
def deploy_timeout
|
205
|
+
raw_config.deploy_timeout || 30
|
196
206
|
end
|
197
207
|
|
208
|
+
def drain_timeout
|
209
|
+
raw_config.drain_timeout || 30
|
210
|
+
end
|
198
211
|
|
199
212
|
def run_directory
|
200
|
-
|
213
|
+
".kamal"
|
201
214
|
end
|
202
215
|
|
203
|
-
def
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
216
|
+
def apps_directory
|
217
|
+
File.join run_directory, "apps"
|
218
|
+
end
|
219
|
+
|
220
|
+
def app_directory
|
221
|
+
File.join apps_directory, service_and_destination
|
222
|
+
end
|
223
|
+
|
224
|
+
def env_directory
|
225
|
+
File.join app_directory, "env"
|
226
|
+
end
|
227
|
+
|
228
|
+
def assets_directory
|
229
|
+
File.join app_directory, "assets"
|
209
230
|
end
|
210
231
|
|
211
232
|
def hooks_path
|
@@ -216,14 +237,13 @@ class Kamal::Configuration
|
|
216
237
|
raw_config.asset_path
|
217
238
|
end
|
218
239
|
|
219
|
-
|
220
|
-
|
221
|
-
File.join(run_directory, "env")
|
240
|
+
def error_pages_path
|
241
|
+
raw_config.error_pages_path
|
222
242
|
end
|
223
243
|
|
224
244
|
def env_tags
|
225
245
|
@env_tags ||= if (tags = raw_config.env["tags"])
|
226
|
-
tags.collect { |name, config| Env::Tag.new(name, config: config) }
|
246
|
+
tags.collect { |name, config| Env::Tag.new(name, config: config, secrets: secrets) }
|
227
247
|
else
|
228
248
|
[]
|
229
249
|
end
|
@@ -233,7 +253,6 @@ class Kamal::Configuration
|
|
233
253
|
env_tags.detect { |t| t.name == name.to_s }
|
234
254
|
end
|
235
255
|
|
236
|
-
|
237
256
|
def to_h
|
238
257
|
{
|
239
258
|
roles: role_names,
|
@@ -248,8 +267,7 @@ class Kamal::Configuration
|
|
248
267
|
sshkit: sshkit.to_h,
|
249
268
|
builder: builder.to_h,
|
250
269
|
accessories: raw_config.accessories,
|
251
|
-
logging: logging_args
|
252
|
-
healthcheck: healthcheck.to_h
|
270
|
+
logging: logging_args
|
253
271
|
}.compact
|
254
272
|
end
|
255
273
|
|
@@ -264,22 +282,26 @@ class Kamal::Configuration
|
|
264
282
|
end
|
265
283
|
|
266
284
|
def ensure_required_keys_present
|
267
|
-
%i[ service image registry
|
285
|
+
%i[ service image registry ].each do |key|
|
268
286
|
raise Kamal::ConfigurationError, "Missing required configuration for #{key}" unless raw_config[key].present?
|
269
287
|
end
|
270
288
|
|
271
|
-
|
272
|
-
raise Kamal::ConfigurationError, "
|
273
|
-
|
289
|
+
if raw_config.servers.nil?
|
290
|
+
raise Kamal::ConfigurationError, "No servers or accessories specified" unless raw_config.accessories.present?
|
291
|
+
else
|
292
|
+
unless role(primary_role_name).present?
|
293
|
+
raise Kamal::ConfigurationError, "The primary_role #{primary_role_name} isn't defined"
|
294
|
+
end
|
274
295
|
|
275
|
-
|
276
|
-
|
277
|
-
|
296
|
+
if primary_role.hosts.empty?
|
297
|
+
raise Kamal::ConfigurationError, "No servers specified for the #{primary_role.name} primary_role"
|
298
|
+
end
|
278
299
|
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
300
|
+
unless allow_empty_roles?
|
301
|
+
roles.each do |role|
|
302
|
+
if role.hosts.empty?
|
303
|
+
raise Kamal::ConfigurationError, "No servers specified for the #{role.name} role. You can ignore this with allow_empty_roles: true"
|
304
|
+
end
|
283
305
|
end
|
284
306
|
end
|
285
307
|
end
|
@@ -307,6 +329,30 @@ class Kamal::Configuration
|
|
307
329
|
true
|
308
330
|
end
|
309
331
|
|
332
|
+
def ensure_no_traefik_reboot_hooks
|
333
|
+
hooks = %w[ pre-traefik-reboot post-traefik-reboot ].select { |hook_file| File.exist?(File.join(hooks_path, hook_file)) }
|
334
|
+
|
335
|
+
if hooks.any?
|
336
|
+
raise Kamal::ConfigurationError, "Found #{hooks.join(", ")}, these should be renamed to (pre|post)-proxy-reboot"
|
337
|
+
end
|
338
|
+
|
339
|
+
true
|
340
|
+
end
|
341
|
+
|
342
|
+
def ensure_one_host_for_ssl_roles
|
343
|
+
roles.each(&:ensure_one_host_for_ssl)
|
344
|
+
|
345
|
+
true
|
346
|
+
end
|
347
|
+
|
348
|
+
def ensure_unique_hosts_for_ssl_roles
|
349
|
+
hosts = roles.select(&:ssl?).flat_map { |role| role.proxy.hosts }
|
350
|
+
duplicates = hosts.tally.filter_map { |host, count| host if count > 1 }
|
351
|
+
|
352
|
+
raise Kamal::ConfigurationError, "Different roles can't share the same host for SSL: #{duplicates.join(", ")}" if duplicates.any?
|
353
|
+
|
354
|
+
true
|
355
|
+
end
|
310
356
|
|
311
357
|
def role_names
|
312
358
|
raw_config.servers.is_a?(Array) ? [ "web" ] : raw_config.servers.keys.sort
|
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
|
data/lib/kamal/env_file.rb
CHANGED
@@ -15,6 +15,10 @@ class Kamal::EnvFile
|
|
15
15
|
env_file.presence || "\n"
|
16
16
|
end
|
17
17
|
|
18
|
+
def to_io
|
19
|
+
StringIO.new(to_s)
|
20
|
+
end
|
21
|
+
|
18
22
|
alias to_str to_s
|
19
23
|
|
20
24
|
private
|
@@ -33,6 +37,8 @@ class Kamal::EnvFile
|
|
33
37
|
def escape_docker_env_file_ascii_value(value)
|
34
38
|
# Doublequotes are treated literally in docker env files
|
35
39
|
# so remove leading and trailing ones and unescape any others
|
36
|
-
value.to_s.dump[1..-2]
|
40
|
+
value.to_s.dump[1..-2]
|
41
|
+
.gsub(/\\"/, "\"")
|
42
|
+
.gsub(/\\#/, "#")
|
37
43
|
end
|
38
44
|
end
|
data/lib/kamal/git.rb
CHANGED
@@ -24,4 +24,14 @@ module Kamal::Git
|
|
24
24
|
def root
|
25
25
|
`git rev-parse --show-toplevel`.strip
|
26
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
|
27
37
|
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
class Kamal::Secrets::Adapters::AwsSecretsManager < Kamal::Secrets::Adapters::Base
|
2
|
+
def requires_account?
|
3
|
+
false
|
4
|
+
end
|
5
|
+
|
6
|
+
private
|
7
|
+
def login(_account)
|
8
|
+
nil
|
9
|
+
end
|
10
|
+
|
11
|
+
def fetch_secrets(secrets, from:, account: nil, session:)
|
12
|
+
{}.tap do |results|
|
13
|
+
get_from_secrets_manager(prefixed_secrets(secrets, from: from), account: account).each do |secret|
|
14
|
+
secret_name = secret["Name"]
|
15
|
+
secret_string = JSON.parse(secret["SecretString"])
|
16
|
+
|
17
|
+
secret_string.each do |key, value|
|
18
|
+
results["#{secret_name}/#{key}"] = value
|
19
|
+
end
|
20
|
+
rescue JSON::ParserError
|
21
|
+
results["#{secret_name}"] = secret["SecretString"]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def get_from_secrets_manager(secrets, account: nil)
|
27
|
+
args = [ "aws", "secretsmanager", "batch-get-secret-value", "--secret-id-list" ] + secrets.map(&:shellescape)
|
28
|
+
args += [ "--profile", account.shellescape ] if account
|
29
|
+
args += [ "--output", "json" ]
|
30
|
+
cmd = args.join(" ")
|
31
|
+
|
32
|
+
`#{cmd}`.tap do |secrets|
|
33
|
+
raise RuntimeError, "Could not read #{secrets} from AWS Secrets Manager" unless $?.success?
|
34
|
+
|
35
|
+
secrets = JSON.parse(secrets)
|
36
|
+
|
37
|
+
return secrets["SecretValues"] unless secrets["Errors"].present?
|
38
|
+
|
39
|
+
raise RuntimeError, secrets["Errors"].map { |error| "#{error['SecretId']}: #{error['Message']}" }.join(" ")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def check_dependencies!
|
44
|
+
raise RuntimeError, "AWS CLI is not installed" unless cli_installed?
|
45
|
+
end
|
46
|
+
|
47
|
+
def cli_installed?
|
48
|
+
`aws --version 2> /dev/null`
|
49
|
+
$?.success?
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
class Kamal::Secrets::Adapters::Base
|
2
|
+
delegate :optionize, to: Kamal::Utils
|
3
|
+
|
4
|
+
def fetch(secrets, account: nil, from: nil)
|
5
|
+
raise RuntimeError, "Missing required option '--account'" if requires_account? && account.blank?
|
6
|
+
|
7
|
+
check_dependencies!
|
8
|
+
|
9
|
+
session = login(account)
|
10
|
+
fetch_secrets(secrets, from: from, account: account, session: session)
|
11
|
+
end
|
12
|
+
|
13
|
+
def requires_account?
|
14
|
+
true
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
def login(...)
|
19
|
+
raise NotImplementedError
|
20
|
+
end
|
21
|
+
|
22
|
+
def fetch_secrets(...)
|
23
|
+
raise NotImplementedError
|
24
|
+
end
|
25
|
+
|
26
|
+
def check_dependencies!
|
27
|
+
raise NotImplementedError
|
28
|
+
end
|
29
|
+
|
30
|
+
def prefixed_secrets(secrets, from:)
|
31
|
+
secrets.map { |secret| [ from, secret ].compact.join("/") }
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,81 @@
|
|
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, from:, account:, session:)
|
25
|
+
{}.tap do |results|
|
26
|
+
items_fields(prefixed_secrets(secrets, from: from)).each do |item, fields|
|
27
|
+
item_json = run_command("get item #{item.shellescape}", session: session, raw: true)
|
28
|
+
raise RuntimeError, "Could not read #{item} from Bitwarden" unless $?.success?
|
29
|
+
item_json = JSON.parse(item_json)
|
30
|
+
if fields.any?
|
31
|
+
results.merge! fetch_secrets_from_fields(fields, item, item_json)
|
32
|
+
elsif item_json.dig("login", "password")
|
33
|
+
results[item] = item_json.dig("login", "password")
|
34
|
+
elsif item_json["fields"]&.any?
|
35
|
+
fields = item_json["fields"].pluck("name")
|
36
|
+
results.merge! fetch_secrets_from_fields(fields, item, item_json)
|
37
|
+
else
|
38
|
+
raise RuntimeError, "Item #{item} is not a login type item and no fields were specified"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def fetch_secrets_from_fields(fields, item, item_json)
|
45
|
+
fields.to_h do |field|
|
46
|
+
item_field = item_json["fields"].find { |f| f["name"] == field }
|
47
|
+
raise RuntimeError, "Could not find field #{field} in item #{item} in Bitwarden" unless item_field
|
48
|
+
value = item_field["value"]
|
49
|
+
[ "#{item}/#{field}", value ]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def items_fields(secrets)
|
54
|
+
{}.tap do |items|
|
55
|
+
secrets.each do |secret|
|
56
|
+
item, field = secret.split("/")
|
57
|
+
items[item] ||= []
|
58
|
+
items[item] << field
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def signedin?(account)
|
64
|
+
run_command("status")["status"] != "unauthenticated"
|
65
|
+
end
|
66
|
+
|
67
|
+
def run_command(command, session: nil, raw: false)
|
68
|
+
full_command = [ *("BW_SESSION=#{session.shellescape}" if session), "bw", command ].join(" ")
|
69
|
+
result = `#{full_command}`.strip
|
70
|
+
raw ? result : JSON.parse(result)
|
71
|
+
end
|
72
|
+
|
73
|
+
def check_dependencies!
|
74
|
+
raise RuntimeError, "Bitwarden CLI is not installed" unless cli_installed?
|
75
|
+
end
|
76
|
+
|
77
|
+
def cli_installed?
|
78
|
+
`bw --version 2> /dev/null`
|
79
|
+
$?.success?
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
class Kamal::Secrets::Adapters::BitwardenSecretsManager < Kamal::Secrets::Adapters::Base
|
2
|
+
def requires_account?
|
3
|
+
false
|
4
|
+
end
|
5
|
+
|
6
|
+
private
|
7
|
+
LIST_ALL_SELECTOR = "all"
|
8
|
+
LIST_ALL_FROM_PROJECT_SUFFIX = "/all"
|
9
|
+
LIST_COMMAND = "secret list"
|
10
|
+
GET_COMMAND = "secret get"
|
11
|
+
|
12
|
+
def fetch_secrets(secrets, from:, account:, session:)
|
13
|
+
raise RuntimeError, "You must specify what to retrieve from Bitwarden Secrets Manager" if secrets.length == 0
|
14
|
+
|
15
|
+
secrets = prefixed_secrets(secrets, from: from)
|
16
|
+
command, project = extract_command_and_project(secrets)
|
17
|
+
|
18
|
+
{}.tap do |results|
|
19
|
+
if command.nil?
|
20
|
+
secrets.each do |secret_uuid|
|
21
|
+
item_json = run_command("#{GET_COMMAND} #{secret_uuid.shellescape}")
|
22
|
+
raise RuntimeError, "Could not read #{secret_uuid} from Bitwarden Secrets Manager" unless $?.success?
|
23
|
+
item_json = JSON.parse(item_json)
|
24
|
+
results[item_json["key"]] = item_json["value"]
|
25
|
+
end
|
26
|
+
else
|
27
|
+
items_json = run_command(command)
|
28
|
+
raise RuntimeError, "Could not read secrets from Bitwarden Secrets Manager" unless $?.success?
|
29
|
+
|
30
|
+
JSON.parse(items_json).each do |item_json|
|
31
|
+
results[item_json["key"]] = item_json["value"]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def extract_command_and_project(secrets)
|
38
|
+
if secrets.length == 1
|
39
|
+
if secrets[0] == LIST_ALL_SELECTOR
|
40
|
+
[ LIST_COMMAND, nil ]
|
41
|
+
elsif secrets[0].end_with?(LIST_ALL_FROM_PROJECT_SUFFIX)
|
42
|
+
project = secrets[0].split(LIST_ALL_FROM_PROJECT_SUFFIX).first
|
43
|
+
[ "#{LIST_COMMAND} #{project.shellescape}", project ]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def run_command(command, session: nil)
|
49
|
+
full_command = [ "bws", command ].join(" ")
|
50
|
+
`#{full_command}`
|
51
|
+
end
|
52
|
+
|
53
|
+
def login(account)
|
54
|
+
run_command("project list")
|
55
|
+
raise RuntimeError, "Could not authenticate to Bitwarden Secrets Manager. Did you set a valid access token?" unless $?.success?
|
56
|
+
end
|
57
|
+
|
58
|
+
def check_dependencies!
|
59
|
+
raise RuntimeError, "Bitwarden Secrets Manager CLI is not installed" unless cli_installed?
|
60
|
+
end
|
61
|
+
|
62
|
+
def cli_installed?
|
63
|
+
`bws --version 2> /dev/null`
|
64
|
+
$?.success?
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
class Kamal::Secrets::Adapters::Doppler < Kamal::Secrets::Adapters::Base
|
2
|
+
def requires_account?
|
3
|
+
false
|
4
|
+
end
|
5
|
+
|
6
|
+
private
|
7
|
+
def login(*)
|
8
|
+
unless loggedin?
|
9
|
+
`doppler login -y`
|
10
|
+
raise RuntimeError, "Failed to login to Doppler" unless $?.success?
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def loggedin?
|
15
|
+
`doppler me --json 2> /dev/null`
|
16
|
+
$?.success?
|
17
|
+
end
|
18
|
+
|
19
|
+
def fetch_secrets(secrets, from:, **)
|
20
|
+
secrets = prefixed_secrets(secrets, from: from)
|
21
|
+
flags = secrets_get_flags(secrets)
|
22
|
+
|
23
|
+
secret_names = secrets.collect { |s| s.split("/").last }
|
24
|
+
|
25
|
+
items = `doppler secrets get #{secret_names.map(&:shellescape).join(" ")} --json #{flags}`
|
26
|
+
raise RuntimeError, "Could not read #{secrets} from Doppler" unless $?.success?
|
27
|
+
|
28
|
+
items = JSON.parse(items)
|
29
|
+
|
30
|
+
items.transform_values { |value| value["computed"] }
|
31
|
+
end
|
32
|
+
|
33
|
+
def secrets_get_flags(secrets)
|
34
|
+
unless service_token_set?
|
35
|
+
project, config, _ = secrets.first.split("/")
|
36
|
+
|
37
|
+
unless project && config
|
38
|
+
raise RuntimeError, "Missing project or config from '--from=project/config' option"
|
39
|
+
end
|
40
|
+
|
41
|
+
project_and_config_flags = "-p #{project.shellescape} -c #{config.shellescape}"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def service_token_set?
|
46
|
+
ENV["DOPPLER_TOKEN"] && ENV["DOPPLER_TOKEN"][0, 5] == "dp.st"
|
47
|
+
end
|
48
|
+
|
49
|
+
def check_dependencies!
|
50
|
+
raise RuntimeError, "Doppler CLI is not installed" unless cli_installed?
|
51
|
+
end
|
52
|
+
|
53
|
+
def cli_installed?
|
54
|
+
`doppler --version 2> /dev/null`
|
55
|
+
$?.success?
|
56
|
+
end
|
57
|
+
end
|