kamal 2.6.1 → 2.8.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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/lib/kamal/cli/accessory.rb +15 -2
  3. data/lib/kamal/cli/app/ssl_certificates.rb +28 -0
  4. data/lib/kamal/cli/app.rb +1 -0
  5. data/lib/kamal/cli/build.rb +33 -15
  6. data/lib/kamal/cli/main.rb +7 -2
  7. data/lib/kamal/cli/port_forwarding.rb +42 -0
  8. data/lib/kamal/cli/registry.rb +16 -8
  9. data/lib/kamal/cli/templates/deploy.yml +4 -3
  10. data/lib/kamal/cli/templates/sample_hooks/pre-deploy.sample +13 -1
  11. data/lib/kamal/cli/templates/secrets +1 -1
  12. data/lib/kamal/commander.rb +1 -1
  13. data/lib/kamal/commands/accessory.rb +8 -3
  14. data/lib/kamal/commands/app/execution.rb +2 -2
  15. data/lib/kamal/commands/app/proxy.rb +4 -0
  16. data/lib/kamal/commands/app.rb +4 -2
  17. data/lib/kamal/commands/base.rb +8 -0
  18. data/lib/kamal/commands/builder/base.rb +11 -1
  19. data/lib/kamal/commands/builder/local.rb +15 -2
  20. data/lib/kamal/commands/builder/pack.rb +46 -0
  21. data/lib/kamal/commands/builder/remote.rb +9 -1
  22. data/lib/kamal/commands/builder.rb +14 -2
  23. data/lib/kamal/commands/registry.rb +22 -0
  24. data/lib/kamal/configuration/accessory.rb +2 -1
  25. data/lib/kamal/configuration/builder.rb +12 -0
  26. data/lib/kamal/configuration/docs/builder.yml +13 -0
  27. data/lib/kamal/configuration/docs/proxy.yml +39 -0
  28. data/lib/kamal/configuration/proxy/boot.rb +8 -0
  29. data/lib/kamal/configuration/proxy.rb +52 -4
  30. data/lib/kamal/configuration/registry.rb +8 -0
  31. data/lib/kamal/configuration/role.rb +5 -3
  32. data/lib/kamal/configuration/validator/accessory.rb +2 -0
  33. data/lib/kamal/configuration/validator/builder.rb +2 -0
  34. data/lib/kamal/configuration/validator/proxy.rb +10 -0
  35. data/lib/kamal/configuration/validator/registry.rb +5 -3
  36. data/lib/kamal/configuration/validator/role.rb +1 -0
  37. data/lib/kamal/configuration/validator.rb +14 -0
  38. data/lib/kamal/configuration.rb +12 -3
  39. data/lib/kamal/secrets/adapters/bitwarden_secrets_manager.rb +10 -16
  40. data/lib/kamal/secrets/adapters/one_password.rb +45 -11
  41. data/lib/kamal/secrets/adapters/passbolt.rb +130 -0
  42. data/lib/kamal/version.rb +1 -1
  43. metadata +6 -2
@@ -2,6 +2,8 @@ class Kamal::Commands::Registry < Kamal::Commands::Base
2
2
  def login(registry_config: nil)
3
3
  registry_config ||= config.registry
4
4
 
5
+ return if registry_config.local?
6
+
5
7
  docker :login,
6
8
  registry_config.server,
7
9
  "-u", sensitive(Kamal::Utils.escape_shell_value(registry_config.username)),
@@ -13,4 +15,24 @@ class Kamal::Commands::Registry < Kamal::Commands::Base
13
15
 
14
16
  docker :logout, registry_config.server
15
17
  end
18
+
19
+ def setup(registry_config: nil)
20
+ registry_config ||= config.registry
21
+
22
+ combine \
23
+ docker(:start, "kamal-docker-registry"),
24
+ docker(:run, "--detach", "-p", "127.0.0.1:#{registry_config.local_port}:5000", "--name", "kamal-docker-registry", "registry:3"),
25
+ by: "||"
26
+ end
27
+
28
+ def remove
29
+ combine \
30
+ docker(:stop, "kamal-docker-registry"),
31
+ docker(:rm, "kamal-docker-registry"),
32
+ by: "&&"
33
+ end
34
+
35
+ def local?
36
+ config.registry.local?
37
+ end
16
38
  end
@@ -125,7 +125,8 @@ class Kamal::Configuration::Accessory
125
125
  Kamal::Configuration::Proxy.new \
126
126
  config: config,
127
127
  proxy_config: accessory_config["proxy"],
128
- context: "accessories/#{name}/proxy"
128
+ context: "accessories/#{name}/proxy",
129
+ secrets: config.secrets
129
130
  end
130
131
 
131
132
  def initialize_registry
@@ -61,6 +61,10 @@ class Kamal::Configuration::Builder
61
61
  !!builder_config["cache"]
62
62
  end
63
63
 
64
+ def pack?
65
+ !!builder_config["pack"]
66
+ end
67
+
64
68
  def args
65
69
  builder_config["args"] || {}
66
70
  end
@@ -85,6 +89,14 @@ class Kamal::Configuration::Builder
85
89
  builder_config.fetch("driver", "docker-container")
86
90
  end
87
91
 
92
+ def pack_builder
93
+ builder_config["pack"]["builder"] if pack?
94
+ end
95
+
96
+ def pack_buildpacks
97
+ builder_config["pack"]["buildpacks"] if pack?
98
+ end
99
+
88
100
  def local_disabled?
89
101
  builder_config["local"] == false
90
102
  end
@@ -31,6 +31,19 @@ builder:
31
31
  # Defaults to true:
32
32
  local: true
33
33
 
34
+ # Buildpack configuration
35
+ #
36
+ # The build configuration for using pack to build a Cloud Native Buildpack image.
37
+ #
38
+ # For additional buildpack customization options you can create a project descriptor
39
+ # file(project.toml) that the Pack CLI will automatically use.
40
+ # See https://buildpacks.io/docs/for-app-developers/how-to/build-inputs/use-project-toml/ for more information.
41
+ pack:
42
+ builder: heroku/builder:24
43
+ buildpacks:
44
+ - heroku/ruby
45
+ - heroku/procfile
46
+
34
47
  # Builder cache
35
48
  #
36
49
  # The type must be either 'gha' or 'registry'.
@@ -47,6 +47,22 @@ proxy:
47
47
  # Defaults to `false`:
48
48
  ssl: true
49
49
 
50
+ # Custom SSL certificate
51
+ #
52
+ # In some cases, using Let's Encrypt for automatic certificate management is not an
53
+ # option, for example if you are running from more than one host.
54
+ #
55
+ # Or you may already have SSL certificates issued by a different Certificate Authority (CA).
56
+ #
57
+ # Kamal supports loading custom SSL certificates directly from secrets. You should
58
+ # pass a hash mapping the `certificate_pem` and `private_key_pem` to the secret names.
59
+ ssl:
60
+ certificate_pem: CERTIFICATE_PEM
61
+ private_key_pem: PRIVATE_KEY_PEM
62
+ # ### Notes
63
+ # - If the certificate or key is missing or invalid, deployments will fail.
64
+ # - Always handle SSL certificates and private keys securely. Avoid hard-coding them in source control.
65
+
50
66
  # SSL redirect
51
67
  #
52
68
  # By default, kamal-proxy will redirect all HTTP requests to HTTPS when SSL is enabled.
@@ -69,6 +85,29 @@ proxy:
69
85
  # How long to wait for requests to complete before timing out, defaults to 30 seconds:
70
86
  response_timeout: 10
71
87
 
88
+ # Path-based routing
89
+ #
90
+ # For applications that split their traffic to different services based on the request path,
91
+ # you can use path-based routing to mount services under different path prefixes.
92
+ # Usage sample: path_prefix: '/api'
93
+ #
94
+ # You can also specify multiple paths in two ways.
95
+ #
96
+ # When using path_prefix you can supply multiple routes separated by commas.
97
+ path_prefix: "/api,/oauth_callback"
98
+ # You can also specify paths as a list of paths, the configuration will be
99
+ # rolled together into a comma separated string.
100
+ path_prefixes:
101
+ - "/api"
102
+ - "/oauth_callback"
103
+ # By default, the path prefix will be stripped from the request before it is forwarded upstream.
104
+ #
105
+ # So in the example above, a request to /api/users/123 will be forwarded to web-1 as /users/123.
106
+ #
107
+ # To instead forward the request with the original path (including the prefix),
108
+ # specify --strip-path-prefix=false
109
+ strip_path_prefix: false
110
+
72
111
  # Healthcheck
73
112
  #
74
113
  # When deploying, the proxy will by default hit `/up` once every second until we hit
@@ -100,6 +100,14 @@ class Kamal::Configuration::Proxy::Boot
100
100
  File.join app_container_directory, "error_pages"
101
101
  end
102
102
 
103
+ def tls_directory
104
+ File.join app_directory, "tls"
105
+ end
106
+
107
+ def tls_container_directory
108
+ File.join app_container_directory, "tls"
109
+ end
110
+
103
111
  private
104
112
  def ensure_valid_bind_ips(bind_ips)
105
113
  bind_ips.present? && bind_ips.each do |ip|
@@ -6,12 +6,14 @@ class Kamal::Configuration::Proxy
6
6
 
7
7
  delegate :argumentize, :optionize, to: Kamal::Utils
8
8
 
9
- attr_reader :config, :proxy_config
9
+ attr_reader :config, :proxy_config, :role_name, :secrets
10
10
 
11
- def initialize(config:, proxy_config:, context: "proxy")
11
+ def initialize(config:, proxy_config:, role_name: nil, secrets:, context: "proxy")
12
12
  @config = config
13
13
  @proxy_config = proxy_config
14
14
  @proxy_config = {} if @proxy_config.nil?
15
+ @role_name = role_name
16
+ @secrets = secrets
15
17
  validate! @proxy_config, with: Kamal::Configuration::Validator::Proxy, context: context
16
18
  end
17
19
 
@@ -27,10 +29,50 @@ class Kamal::Configuration::Proxy
27
29
  proxy_config["hosts"] || proxy_config["host"]&.split(",") || []
28
30
  end
29
31
 
32
+ def custom_ssl_certificate?
33
+ ssl = proxy_config["ssl"]
34
+ return false unless ssl.is_a?(Hash)
35
+ ssl["certificate_pem"].present? && ssl["private_key_pem"].present?
36
+ end
37
+
38
+ def certificate_pem_content
39
+ ssl = proxy_config["ssl"]
40
+ return nil unless ssl.is_a?(Hash)
41
+ secrets[ssl["certificate_pem"]]
42
+ end
43
+
44
+ def private_key_pem_content
45
+ ssl = proxy_config["ssl"]
46
+ return nil unless ssl.is_a?(Hash)
47
+ secrets[ssl["private_key_pem"]]
48
+ end
49
+
50
+ def host_tls_cert
51
+ tls_path(config.proxy_boot.tls_directory, "cert.pem")
52
+ end
53
+
54
+ def host_tls_key
55
+ tls_path(config.proxy_boot.tls_directory, "key.pem")
56
+ end
57
+
58
+ def container_tls_cert
59
+ tls_path(config.proxy_boot.tls_container_directory, "cert.pem")
60
+ end
61
+
62
+ def container_tls_key
63
+ tls_path(config.proxy_boot.tls_container_directory, "key.pem") if custom_ssl_certificate?
64
+ end
65
+
66
+ def path_prefixes
67
+ proxy_config["path_prefixes"] || proxy_config["path_prefix"]&.split(",") || []
68
+ end
69
+
30
70
  def deploy_options
31
71
  {
32
72
  host: hosts,
33
- tls: proxy_config["ssl"].presence,
73
+ tls: ssl? ? true : nil,
74
+ "tls-certificate-path": container_tls_cert,
75
+ "tls-private-key-path": container_tls_key,
34
76
  "deploy-timeout": seconds_duration(config.deploy_timeout),
35
77
  "drain-timeout": seconds_duration(config.drain_timeout),
36
78
  "health-check-interval": seconds_duration(proxy_config.dig("healthcheck", "interval")),
@@ -42,6 +84,8 @@ class Kamal::Configuration::Proxy
42
84
  "buffer-memory": proxy_config.dig("buffering", "memory"),
43
85
  "max-request-body": proxy_config.dig("buffering", "max_request_body"),
44
86
  "max-response-body": proxy_config.dig("buffering", "max_response_body"),
87
+ "path-prefix": path_prefixes,
88
+ "strip-path-prefix": proxy_config.dig("strip_path_prefix"),
45
89
  "forward-headers": proxy_config.dig("forward_headers"),
46
90
  "tls-redirect": proxy_config.dig("ssl_redirect"),
47
91
  "log-request-header": proxy_config.dig("logging", "request_headers") || DEFAULT_LOG_REQUEST_HEADERS,
@@ -66,10 +110,14 @@ class Kamal::Configuration::Proxy
66
110
  end
67
111
 
68
112
  def merge(other)
69
- self.class.new config: config, proxy_config: proxy_config.deep_merge(other.proxy_config)
113
+ self.class.new config: config, proxy_config: other.proxy_config.deep_merge(proxy_config), role_name: role_name, secrets: secrets
70
114
  end
71
115
 
72
116
  private
117
+ def tls_path(directory, filename)
118
+ File.join([ directory, role_name, filename ].compact) if custom_ssl_certificate?
119
+ end
120
+
73
121
  def seconds_duration(value)
74
122
  value ? "#{value}s" : nil
75
123
  end
@@ -19,6 +19,14 @@ class Kamal::Configuration::Registry
19
19
  lookup("password")
20
20
  end
21
21
 
22
+ def local?
23
+ server.to_s.match?("^localhost[:$]")
24
+ end
25
+
26
+ def local_port
27
+ local? ? (server.split(":").last.to_i || 80) : nil
28
+ end
29
+
22
30
  private
23
31
  attr_reader :registry_config, :secrets
24
32
 
@@ -68,7 +68,7 @@ class Kamal::Configuration::Role
68
68
  end
69
69
 
70
70
  def proxy
71
- @proxy ||= config.proxy.merge(specialized_proxy) if running_proxy?
71
+ @proxy ||= specialized_proxy.merge(config.proxy) if running_proxy?
72
72
  end
73
73
 
74
74
  def running_proxy?
@@ -150,8 +150,8 @@ class Kamal::Configuration::Role
150
150
  end
151
151
 
152
152
  def ensure_one_host_for_ssl
153
- if running_proxy? && proxy.ssl? && hosts.size > 1
154
- raise Kamal::ConfigurationError, "SSL is only supported on a single server, found #{hosts.size} servers for role #{name}"
153
+ if running_proxy? && proxy.ssl? && hosts.size > 1 && !proxy.custom_ssl_certificate?
154
+ raise Kamal::ConfigurationError, "SSL is only supported on a single server unless you provide custom certificates, found #{hosts.size} servers for role #{name}"
155
155
  end
156
156
  end
157
157
 
@@ -173,6 +173,8 @@ class Kamal::Configuration::Role
173
173
  @specialized_proxy = Kamal::Configuration::Proxy.new \
174
174
  config: config,
175
175
  proxy_config: proxy_config,
176
+ secrets: config.secrets,
177
+ role_name: name,
176
178
  context: "servers/#{name}/proxy"
177
179
  end
178
180
  end
@@ -6,6 +6,8 @@ class Kamal::Configuration::Validator::Accessory < Kamal::Configuration::Validat
6
6
  error "specify one of `host`, `hosts`, `role`, `roles`, `tag` or `tags`"
7
7
  end
8
8
 
9
+ validate_labels!(config["labels"])
10
+
9
11
  validate_docker_options!(config["options"])
10
12
  end
11
13
  end
@@ -8,6 +8,8 @@ class Kamal::Configuration::Validator::Builder < Kamal::Configuration::Validator
8
8
 
9
9
  error "Builder arch not set" unless config["arch"].present?
10
10
 
11
+ error "buildpacks only support building for one arch" if config["pack"] && config["arch"].is_a?(Array) && config["arch"].size > 1
12
+
11
13
  error "Cannot disable local builds, no remote is set" if config["local"] == false && config["remote"].blank?
12
14
  end
13
15
  end
@@ -10,6 +10,16 @@ class Kamal::Configuration::Validator::Proxy < Kamal::Configuration::Validator
10
10
  if (config.keys & [ "host", "hosts" ]).size > 1
11
11
  error "Specify one of 'host' or 'hosts', not both"
12
12
  end
13
+
14
+ if config["ssl"].is_a?(Hash)
15
+ if config["ssl"]["certificate_pem"].present? && config["ssl"]["private_key_pem"].blank?
16
+ error "Missing private_key_pem setting (required when certificate_pem is present)"
17
+ end
18
+
19
+ if config["ssl"]["private_key_pem"].present? && config["ssl"]["certificate_pem"].blank?
20
+ error "Missing certificate_pem setting (required when private_key_pem is present)"
21
+ end
22
+ end
13
23
  end
14
24
  end
15
25
  end
@@ -15,10 +15,12 @@ class Kamal::Configuration::Validator::Registry < Kamal::Configuration::Validato
15
15
  with_context(key) do
16
16
  value = config[key]
17
17
 
18
- error "is required" unless value.present?
18
+ unless config["server"]&.match?("^localhost[:$]")
19
+ error "is required" unless value.present?
19
20
 
20
- unless value.is_a?(String) || (value.is_a?(Array) && value.size == 1 && value.first.is_a?(String))
21
- error "should be a string or an array with one string (for secret lookup)"
21
+ unless value.is_a?(String) || (value.is_a?(Array) && value.size == 1 && value.first.is_a?(String))
22
+ error "should be a string or an array with one string (for secret lookup)"
23
+ end
22
24
  end
23
25
  end
24
26
  end
@@ -6,6 +6,7 @@ class Kamal::Configuration::Validator::Role < Kamal::Configuration::Validator
6
6
  validate_servers!(config)
7
7
  else
8
8
  super
9
+ validate_labels!(config["labels"])
9
10
  validate_docker_options!(config["options"])
10
11
  end
11
12
  end
@@ -27,6 +27,8 @@ class Kamal::Configuration::Validator
27
27
  unless key.to_s == "proxy" && boolean?(value.class)
28
28
  validate_type! value, *(Array if key == :servers), Hash
29
29
  end
30
+ elsif key.to_s == "ssl"
31
+ validate_type! value, TrueClass, FalseClass, Hash
30
32
  elsif key == "hosts"
31
33
  validate_servers! value
32
34
  elsif example_value.is_a?(Array)
@@ -169,6 +171,18 @@ class Kamal::Configuration::Validator
169
171
  unknown_keys_error unknown_keys if unknown_keys.present?
170
172
  end
171
173
 
174
+ def validate_labels!(labels)
175
+ return true if labels.blank?
176
+
177
+ with_context("labels") do
178
+ labels.each do |key, _|
179
+ with_context(key) do
180
+ error "invalid label. destination, role, and service are reserved labels" if %w[destination role service].include?(key)
181
+ end
182
+ end
183
+ end
184
+ end
185
+
172
186
  def validate_docker_options!(options)
173
187
  if options
174
188
  error "Cannot set restart policy in docker options, unless-stopped is required" if options["restart"]
@@ -6,7 +6,7 @@ require "erb"
6
6
  require "net/ssh/proxy/jump"
7
7
 
8
8
  class Kamal::Configuration
9
- delegate :service, :image, :labels, :hooks_path, to: :raw_config, allow_nil: true
9
+ delegate :service, :labels, :hooks_path, to: :raw_config, allow_nil: true
10
10
  delegate :argumentize, :optionize, to: Kamal::Utils
11
11
 
12
12
  attr_reader :destination, :raw_config, :secrets
@@ -63,7 +63,7 @@ class Kamal::Configuration
63
63
  @env = Env.new(config: @raw_config.env || {}, secrets: secrets)
64
64
 
65
65
  @logging = Logging.new(logging_config: @raw_config.logging)
66
- @proxy = Proxy.new(config: self, proxy_config: @raw_config.proxy)
66
+ @proxy = Proxy.new(config: self, proxy_config: @raw_config.proxy, secrets: secrets)
67
67
  @proxy_boot = Proxy::Boot.new(config: self)
68
68
  @ssh = Ssh.new(config: self)
69
69
  @sshkit = Sshkit.new(config: self)
@@ -157,6 +157,13 @@ class Kamal::Configuration
157
157
  (proxy_roles.flat_map(&:hosts) + proxy_accessories.flat_map(&:hosts)).uniq
158
158
  end
159
159
 
160
+ def image
161
+ name = raw_config&.image.presence
162
+ name ||= raw_config&.service if registry.local?
163
+
164
+ name
165
+ end
166
+
160
167
  def repository
161
168
  [ registry.server, image ].compact.join("/")
162
169
  end
@@ -282,10 +289,12 @@ class Kamal::Configuration
282
289
  end
283
290
 
284
291
  def ensure_required_keys_present
285
- %i[ service image registry ].each do |key|
292
+ %i[ service registry ].each do |key|
286
293
  raise Kamal::ConfigurationError, "Missing required configuration for #{key}" unless raw_config[key].present?
287
294
  end
288
295
 
296
+ raise Kamal::ConfigurationError, "Missing required configuration for image" if image.blank?
297
+
289
298
  if raw_config.servers.nil?
290
299
  raise Kamal::ConfigurationError, "No servers or accessories specified" unless raw_config.accessories.present?
291
300
  else
@@ -6,8 +6,8 @@ class Kamal::Secrets::Adapters::BitwardenSecretsManager < Kamal::Secrets::Adapte
6
6
  private
7
7
  LIST_ALL_SELECTOR = "all"
8
8
  LIST_ALL_FROM_PROJECT_SUFFIX = "/all"
9
- LIST_COMMAND = "secret list -o env"
10
- GET_COMMAND = "secret get -o env"
9
+ LIST_COMMAND = "secret list"
10
+ GET_COMMAND = "secret get"
11
11
 
12
12
  def fetch_secrets(secrets, from:, account:, session:)
13
13
  raise RuntimeError, "You must specify what to retrieve from Bitwarden Secrets Manager" if secrets.length == 0
@@ -18,17 +18,17 @@ class Kamal::Secrets::Adapters::BitwardenSecretsManager < Kamal::Secrets::Adapte
18
18
  {}.tap do |results|
19
19
  if command.nil?
20
20
  secrets.each do |secret_uuid|
21
- secret = run_command("#{GET_COMMAND} #{secret_uuid.shellescape}")
21
+ item_json = run_command("#{GET_COMMAND} #{secret_uuid.shellescape}")
22
22
  raise RuntimeError, "Could not read #{secret_uuid} from Bitwarden Secrets Manager" unless $?.success?
23
- key, value = parse_secret(secret)
24
- results[key] = value
23
+ item_json = JSON.parse(item_json)
24
+ results[item_json["key"]] = item_json["value"]
25
25
  end
26
26
  else
27
- secrets = run_command(command)
27
+ items_json = run_command(command)
28
28
  raise RuntimeError, "Could not read secrets from Bitwarden Secrets Manager" unless $?.success?
29
- secrets.split("\n").each do |secret|
30
- key, value = parse_secret(secret)
31
- results[key] = value
29
+
30
+ JSON.parse(items_json).each do |item_json|
31
+ results[item_json["key"]] = item_json["value"]
32
32
  end
33
33
  end
34
34
  end
@@ -45,19 +45,13 @@ class Kamal::Secrets::Adapters::BitwardenSecretsManager < Kamal::Secrets::Adapte
45
45
  end
46
46
  end
47
47
 
48
- def parse_secret(secret)
49
- key, value = secret.split("=", 2)
50
- value = value.gsub(/^"|"$/, "")
51
- [ key, value ]
52
- end
53
-
54
48
  def run_command(command, session: nil)
55
49
  full_command = [ "bws", command ].join(" ")
56
50
  `#{full_command}`
57
51
  end
58
52
 
59
53
  def login(account)
60
- run_command("run 'echo OK'")
54
+ run_command("project list")
61
55
  raise RuntimeError, "Could not authenticate to Bitwarden Secrets Manager. Did you set a valid access token?" unless $?.success?
62
56
  end
63
57
 
@@ -16,17 +16,33 @@ class Kamal::Secrets::Adapters::OnePassword < Kamal::Secrets::Adapters::Base
16
16
  end
17
17
 
18
18
  def fetch_secrets(secrets, from:, account:, session:)
19
+ if secrets.blank?
20
+ fetch_all_secrets(from: from, account: account, session: session)
21
+ else
22
+ fetch_specified_secrets(secrets, from: from, account: account, session: session)
23
+ end
24
+ end
25
+
26
+ def fetch_specified_secrets(secrets, from:, account:, session:)
19
27
  {}.tap do |results|
20
28
  vaults_items_fields(prefixed_secrets(secrets, from: from)).map do |vault, items|
21
29
  items.each do |item, fields|
22
- fields_json = JSON.parse(op_item_get(vault, item, fields, account: account, session: session))
30
+ fields_json = JSON.parse(op_item_get(vault, item, fields: fields, account: account, session: session))
23
31
  fields_json = [ fields_json ] if fields.one?
24
32
 
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
33
+ results.merge!(fields_map(fields_json))
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ def fetch_all_secrets(from:, account:, session:)
40
+ {}.tap do |results|
41
+ vault_items(from).each do |vault, items|
42
+ items.each do |item|
43
+ fields_json = JSON.parse(op_item_get(vault, item, account: account, session: session)).fetch("fields")
44
+
45
+ results.merge!(fields_map(fields_json))
30
46
  end
31
47
  end
32
48
  end
@@ -50,12 +66,30 @@ class Kamal::Secrets::Adapters::OnePassword < Kamal::Secrets::Adapters::Base
50
66
  end
51
67
  end
52
68
 
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)
69
+ def vault_items(from)
70
+ from = from.delete_prefix("op://")
71
+ vault, item = from.split("/")
72
+ { vault => [ item ] }
73
+ end
74
+
75
+ def fields_map(fields_json)
76
+ fields_json.to_h do |field_json|
77
+ # The reference is in the form `op://vault/item/field[/field]`
78
+ field = field_json["reference"].delete_prefix("op://").delete_suffix("/password")
79
+ [ field, field_json["value"] ]
80
+ end
81
+ end
82
+
83
+ def op_item_get(vault, item, fields: nil, account:, session:)
84
+ options = { vault: vault, format: "json", account: account, session: session.presence }
85
+
86
+ if fields.present?
87
+ labels = fields.map { |field| "label=#{field}" }.join(",")
88
+ options.merge!(fields: labels)
89
+ end
56
90
 
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?
91
+ `op item get #{item.shellescape} #{to_options(**options)}`.tap do
92
+ raise RuntimeError, "Could not read #{"#{fields.join(", ")} " if fields.present?}from #{item} in the #{vault} 1Password vault" unless $?.success?
59
93
  end
60
94
  end
61
95