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.
- checksums.yaml +4 -4
 - data/lib/kamal/cli/accessory.rb +15 -2
 - data/lib/kamal/cli/app/ssl_certificates.rb +28 -0
 - data/lib/kamal/cli/app.rb +1 -0
 - data/lib/kamal/cli/build.rb +33 -15
 - data/lib/kamal/cli/main.rb +7 -2
 - data/lib/kamal/cli/port_forwarding.rb +42 -0
 - data/lib/kamal/cli/registry.rb +16 -8
 - data/lib/kamal/cli/templates/deploy.yml +4 -3
 - data/lib/kamal/cli/templates/sample_hooks/pre-deploy.sample +13 -1
 - data/lib/kamal/cli/templates/secrets +1 -1
 - data/lib/kamal/commander.rb +1 -1
 - data/lib/kamal/commands/accessory.rb +8 -3
 - data/lib/kamal/commands/app/execution.rb +2 -2
 - data/lib/kamal/commands/app/proxy.rb +4 -0
 - data/lib/kamal/commands/app.rb +4 -2
 - data/lib/kamal/commands/base.rb +8 -0
 - data/lib/kamal/commands/builder/base.rb +11 -1
 - data/lib/kamal/commands/builder/local.rb +15 -2
 - data/lib/kamal/commands/builder/pack.rb +46 -0
 - data/lib/kamal/commands/builder/remote.rb +9 -1
 - data/lib/kamal/commands/builder.rb +14 -2
 - data/lib/kamal/commands/registry.rb +22 -0
 - data/lib/kamal/configuration/accessory.rb +2 -1
 - data/lib/kamal/configuration/builder.rb +12 -0
 - data/lib/kamal/configuration/docs/builder.yml +13 -0
 - data/lib/kamal/configuration/docs/proxy.yml +39 -0
 - data/lib/kamal/configuration/proxy/boot.rb +8 -0
 - data/lib/kamal/configuration/proxy.rb +52 -4
 - data/lib/kamal/configuration/registry.rb +8 -0
 - data/lib/kamal/configuration/role.rb +5 -3
 - data/lib/kamal/configuration/validator/accessory.rb +2 -0
 - data/lib/kamal/configuration/validator/builder.rb +2 -0
 - data/lib/kamal/configuration/validator/proxy.rb +10 -0
 - data/lib/kamal/configuration/validator/registry.rb +5 -3
 - data/lib/kamal/configuration/validator/role.rb +1 -0
 - data/lib/kamal/configuration/validator.rb +14 -0
 - data/lib/kamal/configuration.rb +12 -3
 - data/lib/kamal/secrets/adapters/bitwarden_secrets_manager.rb +10 -16
 - data/lib/kamal/secrets/adapters/one_password.rb +45 -11
 - data/lib/kamal/secrets/adapters/passbolt.rb +130 -0
 - data/lib/kamal/version.rb +1 -1
 - 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:  
     | 
| 
      
 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( 
     | 
| 
      
 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 
     | 
| 
      
 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
         
     | 
| 
         @@ -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 
     | 
    
         
            -
                     
     | 
| 
      
 18 
     | 
    
         
            +
                    unless config["server"]&.match?("^localhost[:$]")
         
     | 
| 
      
 19 
     | 
    
         
            +
                      error "is required" unless value.present?
         
     | 
| 
       19 
20 
     | 
    
         | 
| 
       20 
     | 
    
         
            -
             
     | 
| 
       21 
     | 
    
         
            -
             
     | 
| 
      
 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
         
     | 
| 
         @@ -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"]
         
     | 
    
        data/lib/kamal/configuration.rb
    CHANGED
    
    | 
         @@ -6,7 +6,7 @@ require "erb" 
     | 
|
| 
       6 
6 
     | 
    
         
             
            require "net/ssh/proxy/jump"
         
     | 
| 
       7 
7 
     | 
    
         | 
| 
       8 
8 
     | 
    
         
             
            class Kamal::Configuration
         
     | 
| 
       9 
     | 
    
         
            -
              delegate :service, : 
     | 
| 
      
 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  
     | 
| 
      
 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 
     | 
| 
       10 
     | 
    
         
            -
                GET_COMMAND = "secret get 
     | 
| 
      
 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 
     | 
    
         
            -
                         
     | 
| 
      
 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 
     | 
    
         
            -
                         
     | 
| 
       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 
     | 
    
         
            -
                       
     | 
| 
      
 27 
     | 
    
         
            +
                      items_json = run_command(command)
         
     | 
| 
       28 
28 
     | 
    
         
             
                      raise RuntimeError, "Could not read secrets from Bitwarden Secrets Manager" unless $?.success?
         
     | 
| 
       29 
     | 
    
         
            -
             
     | 
| 
       30 
     | 
    
         
            -
             
     | 
| 
       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(" 
     | 
| 
      
 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 
     | 
| 
       26 
     | 
    
         
            -
             
     | 
| 
       27 
     | 
    
         
            -
             
     | 
| 
       28 
     | 
    
         
            -
             
     | 
| 
       29 
     | 
    
         
            -
             
     | 
| 
      
 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  
     | 
| 
       54 
     | 
    
         
            -
                   
     | 
| 
       55 
     | 
    
         
            -
                   
     | 
| 
      
 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 
     | 
    
         |