kamal 2.6.1 → 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/lib/kamal/cli/accessory.rb +2 -2
- data/lib/kamal/cli/app/ssl_certificates.rb +28 -0
- data/lib/kamal/cli/app.rb +1 -0
- data/lib/kamal/cli/templates/sample_hooks/pre-deploy.sample +13 -1
- data/lib/kamal/commands/accessory.rb +4 -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 +3 -2
- data/lib/kamal/commands/base.rb +8 -0
- data/lib/kamal/commands/builder/base.rb +1 -0
- data/lib/kamal/commands/builder/pack.rb +46 -0
- data/lib/kamal/commands/builder.rb +7 -1
- 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 +32 -1
- data/lib/kamal/configuration/proxy/boot.rb +8 -0
- data/lib/kamal/configuration/proxy.rb +48 -4
- 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/role.rb +1 -0
- data/lib/kamal/configuration/validator.rb +15 -1
- data/lib/kamal/configuration.rb +1 -1
- 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 +4 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4e1cf57d731a8b129a8ccbb86faddd3e813bc4d17895e6e538fe904f5bb65d27
|
4
|
+
data.tar.gz: 2d7d81b3a34f42fb427bfed18f9cf0ed1955d38e4b4783c1407bc1db6de5cef6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f3144c40082cfa24c78e2a1ebf2f433e491be3f5966e45cfc5488c3a11f5a3f513f097f7818cbc9e34d20b79986e64212055a87030fd4aa386a1d82bbb7d0efe
|
7
|
+
data.tar.gz: c6b796497a6f7a68815d340664b34fb9943f55ea7121122994aa3fdadaa5b12a18fb4078ff194cffb6060917a74611fdf31017afa75f2515d94ec259e0809251
|
data/lib/kamal/cli/accessory.rb
CHANGED
@@ -24,11 +24,11 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
|
|
24
24
|
directories(name)
|
25
25
|
upload(name)
|
26
26
|
|
27
|
-
on(hosts) do
|
27
|
+
on(hosts) do |host|
|
28
28
|
execute *KAMAL.auditor.record("Booted #{name} accessory"), verbosity: :debug
|
29
29
|
execute *accessory.ensure_env_directory
|
30
30
|
upload! accessory.secrets_io, accessory.secrets_path, mode: "0600"
|
31
|
-
execute *accessory.run
|
31
|
+
execute *accessory.run(host: host)
|
32
32
|
|
33
33
|
if accessory.running_proxy?
|
34
34
|
target = capture_with_info(*accessory.container_id_for(container_name: accessory.service_name, only_running: true)).strip
|
@@ -0,0 +1,28 @@
|
|
1
|
+
class Kamal::Cli::App::SslCertificates
|
2
|
+
attr_reader :host, :role, :sshkit
|
3
|
+
delegate :execute, :info, :upload!, to: :sshkit
|
4
|
+
|
5
|
+
def initialize(host, role, sshkit)
|
6
|
+
@host = host
|
7
|
+
@role = role
|
8
|
+
@sshkit = sshkit
|
9
|
+
end
|
10
|
+
|
11
|
+
def run
|
12
|
+
if role.running_proxy? && role.proxy.custom_ssl_certificate?
|
13
|
+
info "Writing SSL certificates for #{role.name} on #{host}"
|
14
|
+
execute *app.create_ssl_directory
|
15
|
+
if cert_content = role.proxy.certificate_pem_content
|
16
|
+
upload!(StringIO.new(cert_content), role.proxy.host_tls_cert, mode: "0644")
|
17
|
+
end
|
18
|
+
if key_content = role.proxy.private_key_pem_content
|
19
|
+
upload!(StringIO.new(key_content), role.proxy.host_tls_key, mode: "0644")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
def app
|
26
|
+
@app ||= KAMAL.app(role: role, host: host)
|
27
|
+
end
|
28
|
+
end
|
data/lib/kamal/cli/app.rb
CHANGED
@@ -43,7 +43,7 @@ class GithubStatusChecks
|
|
43
43
|
attr_reader :remote_url, :git_sha, :github_client, :combined_status
|
44
44
|
|
45
45
|
def initialize
|
46
|
-
@remote_url =
|
46
|
+
@remote_url = github_repo_from_remote_url
|
47
47
|
@git_sha = `git rev-parse HEAD`.strip
|
48
48
|
@github_client = Octokit::Client.new(access_token: ENV["GITHUB_TOKEN"])
|
49
49
|
refresh!
|
@@ -77,6 +77,18 @@ class GithubStatusChecks
|
|
77
77
|
"Build not started..."
|
78
78
|
end
|
79
79
|
end
|
80
|
+
|
81
|
+
private
|
82
|
+
def github_repo_from_remote_url
|
83
|
+
url = `git config --get remote.origin.url`.strip.delete_suffix(".git")
|
84
|
+
if url.start_with?("https://github.com/")
|
85
|
+
url.delete_prefix("https://github.com/")
|
86
|
+
elsif url.start_with?("git@github.com:")
|
87
|
+
url.delete_prefix("git@github.com:")
|
88
|
+
else
|
89
|
+
url
|
90
|
+
end
|
91
|
+
end
|
80
92
|
end
|
81
93
|
|
82
94
|
|
@@ -12,7 +12,7 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base
|
|
12
12
|
@accessory_config = config.accessory(name)
|
13
13
|
end
|
14
14
|
|
15
|
-
def run
|
15
|
+
def run(host: nil)
|
16
16
|
docker :run,
|
17
17
|
"--name", service_name,
|
18
18
|
"--detach",
|
@@ -20,6 +20,7 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base
|
|
20
20
|
*network_args,
|
21
21
|
*config.logging_args,
|
22
22
|
*publish_args,
|
23
|
+
*([ "--env", "KAMAL_HOST=\"#{host}\"" ] if host),
|
23
24
|
*env_args,
|
24
25
|
*volume_args,
|
25
26
|
*label_args,
|
@@ -55,14 +56,14 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base
|
|
55
56
|
|
56
57
|
def execute_in_existing_container(*command, interactive: false)
|
57
58
|
docker :exec,
|
58
|
-
(
|
59
|
+
(docker_interactive_args if interactive),
|
59
60
|
service_name,
|
60
61
|
*command
|
61
62
|
end
|
62
63
|
|
63
64
|
def execute_in_new_container(*command, interactive: false)
|
64
65
|
docker :run,
|
65
|
-
(
|
66
|
+
(docker_interactive_args if interactive),
|
66
67
|
"--rm",
|
67
68
|
*network_args,
|
68
69
|
*env_args,
|
@@ -1,7 +1,7 @@
|
|
1
1
|
module Kamal::Commands::App::Execution
|
2
2
|
def execute_in_existing_container(*command, interactive: false, env:)
|
3
3
|
docker :exec,
|
4
|
-
(
|
4
|
+
(docker_interactive_args if interactive),
|
5
5
|
*argumentize("--env", env),
|
6
6
|
container_name,
|
7
7
|
*command
|
@@ -9,7 +9,7 @@ module Kamal::Commands::App::Execution
|
|
9
9
|
|
10
10
|
def execute_in_new_container(*command, interactive: false, detach: false, env:)
|
11
11
|
docker :run,
|
12
|
-
(
|
12
|
+
(docker_interactive_args if interactive),
|
13
13
|
("--detach" if detach),
|
14
14
|
("--rm" unless detach),
|
15
15
|
"--network", "kamal",
|
@@ -21,6 +21,10 @@ module Kamal::Commands::App::Proxy
|
|
21
21
|
remove_directory config.proxy_boot.app_directory
|
22
22
|
end
|
23
23
|
|
24
|
+
def create_ssl_directory
|
25
|
+
make_directory(File.join(config.proxy_boot.tls_directory, role.name))
|
26
|
+
end
|
27
|
+
|
24
28
|
private
|
25
29
|
def proxy_exec(*command)
|
26
30
|
docker :exec, proxy_container_name, "kamal-proxy", *command
|
data/lib/kamal/commands/app.rb
CHANGED
@@ -20,8 +20,9 @@ class Kamal::Commands::App < Kamal::Commands::Base
|
|
20
20
|
"--name", container_name,
|
21
21
|
"--network", "kamal",
|
22
22
|
*([ "--hostname", hostname ] if hostname),
|
23
|
-
"
|
24
|
-
"
|
23
|
+
"--env", "KAMAL_CONTAINER_NAME=\"#{container_name}\"",
|
24
|
+
"--env", "KAMAL_VERSION=\"#{config.version}\"",
|
25
|
+
"--env", "KAMAL_HOST=\"#{host}\"",
|
25
26
|
*role.env_args(host),
|
26
27
|
*role.logging_args,
|
27
28
|
*config.volume_args,
|
data/lib/kamal/commands/base.rb
CHANGED
@@ -84,6 +84,10 @@ module Kamal::Commands
|
|
84
84
|
args.compact.unshift :docker
|
85
85
|
end
|
86
86
|
|
87
|
+
def pack(*args)
|
88
|
+
args.compact.unshift :pack
|
89
|
+
end
|
90
|
+
|
87
91
|
def git(*args, path: nil)
|
88
92
|
[ :git, *([ "-C", path ] if path), *args.compact ]
|
89
93
|
end
|
@@ -122,5 +126,9 @@ module Kamal::Commands
|
|
122
126
|
def ensure_local_buildx_installed
|
123
127
|
docker :buildx, "version"
|
124
128
|
end
|
129
|
+
|
130
|
+
def docker_interactive_args
|
131
|
+
STDIN.isatty ? "-it" : "-i"
|
132
|
+
end
|
125
133
|
end
|
126
134
|
end
|
@@ -6,6 +6,7 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
|
|
6
6
|
delegate :argumentize, to: Kamal::Utils
|
7
7
|
delegate \
|
8
8
|
:args, :secrets, :dockerfile, :target, :arches, :local_arches, :remote_arches, :remote,
|
9
|
+
:pack?, :pack_builder, :pack_buildpacks,
|
9
10
|
:cache_from, :cache_to, :ssh, :provenance, :sbom, :driver, :docker_driver?,
|
10
11
|
to: :builder_config
|
11
12
|
|
@@ -0,0 +1,46 @@
|
|
1
|
+
class Kamal::Commands::Builder::Pack < Kamal::Commands::Builder::Base
|
2
|
+
def push(export_action = "registry")
|
3
|
+
combine \
|
4
|
+
build,
|
5
|
+
export(export_action)
|
6
|
+
end
|
7
|
+
|
8
|
+
def remove;end
|
9
|
+
|
10
|
+
def info
|
11
|
+
pack :builder, :inspect, pack_builder
|
12
|
+
end
|
13
|
+
alias_method :inspect_builder, :info
|
14
|
+
|
15
|
+
private
|
16
|
+
def build
|
17
|
+
pack(:build,
|
18
|
+
config.repository,
|
19
|
+
"--platform", platform,
|
20
|
+
"--creation-time", "now",
|
21
|
+
"--builder", pack_builder,
|
22
|
+
buildpacks,
|
23
|
+
"-t", config.absolute_image,
|
24
|
+
"-t", config.latest_image,
|
25
|
+
"--env", "BP_IMAGE_LABELS=service=#{config.service}",
|
26
|
+
*argumentize("--env", args),
|
27
|
+
*argumentize("--env", secrets, sensitive: true),
|
28
|
+
"--path", build_context)
|
29
|
+
end
|
30
|
+
|
31
|
+
def export(export_action)
|
32
|
+
return unless export_action == "registry"
|
33
|
+
|
34
|
+
combine \
|
35
|
+
docker(:push, config.absolute_image),
|
36
|
+
docker(:push, config.latest_image)
|
37
|
+
end
|
38
|
+
|
39
|
+
def platform
|
40
|
+
"linux/#{local_arches.first}"
|
41
|
+
end
|
42
|
+
|
43
|
+
def buildpacks
|
44
|
+
(pack_buildpacks << "paketo-buildpacks/image-labels").map { |buildpack| [ "--buildpack", buildpack ] }
|
45
|
+
end
|
46
|
+
end
|
@@ -2,7 +2,7 @@ require "active_support/core_ext/string/filters"
|
|
2
2
|
|
3
3
|
class Kamal::Commands::Builder < Kamal::Commands::Base
|
4
4
|
delegate :create, :remove, :dev, :push, :clean, :pull, :info, :inspect_builder, :validate_image, :first_mirror, to: :target
|
5
|
-
delegate :local?, :remote?, :cloud?, to: "config.builder"
|
5
|
+
delegate :local?, :remote?, :pack?, :cloud?, to: "config.builder"
|
6
6
|
|
7
7
|
include Clone
|
8
8
|
|
@@ -17,6 +17,8 @@ class Kamal::Commands::Builder < Kamal::Commands::Base
|
|
17
17
|
else
|
18
18
|
remote
|
19
19
|
end
|
20
|
+
elsif pack?
|
21
|
+
pack
|
20
22
|
elsif cloud?
|
21
23
|
cloud
|
22
24
|
else
|
@@ -36,6 +38,10 @@ class Kamal::Commands::Builder < Kamal::Commands::Base
|
|
36
38
|
@hybrid ||= Kamal::Commands::Builder::Hybrid.new(config)
|
37
39
|
end
|
38
40
|
|
41
|
+
def pack
|
42
|
+
@pack ||= Kamal::Commands::Builder::Pack.new(config)
|
43
|
+
end
|
44
|
+
|
39
45
|
def cloud
|
40
46
|
@cloud ||= Kamal::Commands::Builder::Cloud.new(config)
|
41
47
|
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'.
|
@@ -45,7 +45,27 @@ proxy:
|
|
45
45
|
# unless you explicitly set `forward_headers: true`
|
46
46
|
#
|
47
47
|
# Defaults to `false`:
|
48
|
-
ssl:
|
48
|
+
ssl: ...
|
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 host than one host. Or you may already
|
54
|
+
# have SSL certificates issued by a different Certificate Authority (CA).
|
55
|
+
# Kamal supports loading custom SSL certificates
|
56
|
+
# directly from secrets.
|
57
|
+
#
|
58
|
+
# Examples:
|
59
|
+
# ssl: true # Enable SSL with Let's Encrypt
|
60
|
+
# ssl: false # Disable SSL
|
61
|
+
# ssl: # Enable custom SSL
|
62
|
+
# certificate_pem: CERTIFICATE_PEM
|
63
|
+
# private_key_pem: PRIVATE_KEY_PEM
|
64
|
+
#
|
65
|
+
# ### Notes
|
66
|
+
# - If the certificate or key is missing or invalid, kamal-proxy will fail to start.
|
67
|
+
# - Always handle SSL certificates and private keys securely. Avoid hard-coding them in deploy.yml files or source control.
|
68
|
+
# - For automated certificate management, consider using the built-in Let's Encrypt integration instead.
|
49
69
|
|
50
70
|
# SSL redirect
|
51
71
|
#
|
@@ -69,6 +89,17 @@ proxy:
|
|
69
89
|
# How long to wait for requests to complete before timing out, defaults to 30 seconds:
|
70
90
|
response_timeout: 10
|
71
91
|
|
92
|
+
# Path-based routing
|
93
|
+
#
|
94
|
+
# For applications that split their traffic to different services based on the request path,
|
95
|
+
# you can use path-based routing to mount services under different path prefixes.
|
96
|
+
path_prefix: '/api'
|
97
|
+
# By default, the path prefix will be stripped from the request before it is forwarded upstream.
|
98
|
+
# So in the example above, a request to /api/users/123 will be forwarded to web-1 as /users/123.
|
99
|
+
# To instead forward the request with the original path (including the prefix),
|
100
|
+
# specify --strip-path-prefix=false
|
101
|
+
strip_path_prefix: false
|
102
|
+
|
72
103
|
# Healthcheck
|
73
104
|
#
|
74
105
|
# 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,46 @@ 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
|
+
|
30
66
|
def deploy_options
|
31
67
|
{
|
32
68
|
host: hosts,
|
33
|
-
tls:
|
69
|
+
tls: ssl? ? true : nil,
|
70
|
+
"tls-certificate-path": container_tls_cert,
|
71
|
+
"tls-private-key-path": container_tls_key,
|
34
72
|
"deploy-timeout": seconds_duration(config.deploy_timeout),
|
35
73
|
"drain-timeout": seconds_duration(config.drain_timeout),
|
36
74
|
"health-check-interval": seconds_duration(proxy_config.dig("healthcheck", "interval")),
|
@@ -42,6 +80,8 @@ class Kamal::Configuration::Proxy
|
|
42
80
|
"buffer-memory": proxy_config.dig("buffering", "memory"),
|
43
81
|
"max-request-body": proxy_config.dig("buffering", "max_request_body"),
|
44
82
|
"max-response-body": proxy_config.dig("buffering", "max_response_body"),
|
83
|
+
"path-prefix": proxy_config.dig("path_prefix"),
|
84
|
+
"strip-path-prefix": proxy_config.dig("strip_path_prefix"),
|
45
85
|
"forward-headers": proxy_config.dig("forward_headers"),
|
46
86
|
"tls-redirect": proxy_config.dig("ssl_redirect"),
|
47
87
|
"log-request-header": proxy_config.dig("logging", "request_headers") || DEFAULT_LOG_REQUEST_HEADERS,
|
@@ -66,10 +106,14 @@ class Kamal::Configuration::Proxy
|
|
66
106
|
end
|
67
107
|
|
68
108
|
def merge(other)
|
69
|
-
self.class.new config: config, proxy_config: proxy_config.deep_merge(
|
109
|
+
self.class.new config: config, proxy_config: other.proxy_config.deep_merge(proxy_config), role_name: role_name, secrets: secrets
|
70
110
|
end
|
71
111
|
|
72
112
|
private
|
113
|
+
def tls_path(directory, filename)
|
114
|
+
File.join([ directory, role_name, filename ].compact) if custom_ssl_certificate?
|
115
|
+
end
|
116
|
+
|
73
117
|
def seconds_duration(value)
|
74
118
|
value ? "#{value}s" : nil
|
75
119
|
end
|
@@ -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
|
@@ -24,7 +24,9 @@ class Kamal::Configuration::Validator
|
|
24
24
|
example_value = example[key]
|
25
25
|
|
26
26
|
if example_value == "..."
|
27
|
-
|
27
|
+
if key.to_s == "ssl"
|
28
|
+
validate_type! value, TrueClass, FalseClass, Hash
|
29
|
+
elsif key.to_s != "proxy" || !boolean?(value.class)
|
28
30
|
validate_type! value, *(Array if key == :servers), Hash
|
29
31
|
end
|
30
32
|
elsif key == "hosts"
|
@@ -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
@@ -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)
|
@@ -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) if secrets.blank?
|
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
|
|
@@ -0,0 +1,130 @@
|
|
1
|
+
class Kamal::Secrets::Adapters::Passbolt < Kamal::Secrets::Adapters::Base
|
2
|
+
def requires_account?
|
3
|
+
false
|
4
|
+
end
|
5
|
+
|
6
|
+
private
|
7
|
+
|
8
|
+
def login(*)
|
9
|
+
`passbolt verify`
|
10
|
+
raise RuntimeError, "Failed to login to Passbolt" unless $?.success?
|
11
|
+
end
|
12
|
+
|
13
|
+
def fetch_secrets(secrets, from:, **)
|
14
|
+
secrets = prefixed_secrets(secrets, from: from)
|
15
|
+
raise ArgumentError, "No secrets given to fetch" if secrets.empty?
|
16
|
+
|
17
|
+
secret_names = secrets.collect { |s| s.split("/").last }
|
18
|
+
folders = secrets_get_folders(secrets)
|
19
|
+
|
20
|
+
# build filter conditions for each secret with its corresponding folder
|
21
|
+
filter_conditions = []
|
22
|
+
secrets.each do |secret|
|
23
|
+
parts = secret.split("/")
|
24
|
+
secret_name = parts.last
|
25
|
+
|
26
|
+
if parts.size > 1
|
27
|
+
# get the folder path without the secret name
|
28
|
+
folder_path = parts[0..-2]
|
29
|
+
|
30
|
+
# find the most nested folder for this path
|
31
|
+
current_folder = nil
|
32
|
+
current_path = []
|
33
|
+
|
34
|
+
folder_path.each do |folder_name|
|
35
|
+
current_path << folder_name
|
36
|
+
matching_folders = folders.select { |f| get_folder_path(f, folders) == current_path.join("/") }
|
37
|
+
current_folder = matching_folders.first if matching_folders.any?
|
38
|
+
end
|
39
|
+
|
40
|
+
if current_folder
|
41
|
+
filter_conditions << "(Name == #{secret_name.shellescape.inspect} && FolderParentID == #{current_folder["id"].shellescape.inspect})"
|
42
|
+
end
|
43
|
+
else
|
44
|
+
# for root level secrets (no folders)
|
45
|
+
filter_conditions << "Name == #{secret_name.shellescape.inspect}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
filter_condition = filter_conditions.any? ? "--filter '#{filter_conditions.join(" || ")}'" : ""
|
50
|
+
items = `passbolt list resources #{filter_condition} #{folders.map { |item| "--folder #{item["id"]}" }.join(" ")} --json`
|
51
|
+
raise RuntimeError, "Could not read #{secrets} from Passbolt" unless $?.success?
|
52
|
+
|
53
|
+
items = JSON.parse(items)
|
54
|
+
found_names = items.map { |item| item["name"] }
|
55
|
+
missing_secrets = secret_names - found_names
|
56
|
+
raise RuntimeError, "Could not find the following secrets in Passbolt: #{missing_secrets.join(", ")}" if missing_secrets.any?
|
57
|
+
|
58
|
+
items.to_h { |item| [ item["name"], item["password"] ] }
|
59
|
+
end
|
60
|
+
|
61
|
+
def secrets_get_folders(secrets)
|
62
|
+
# extract all folder paths (both parent and nested)
|
63
|
+
folder_paths = secrets
|
64
|
+
.select { |s| s.include?("/") }
|
65
|
+
.map { |s| s.split("/")[0..-2] } # get all parts except the secret name
|
66
|
+
.uniq
|
67
|
+
|
68
|
+
return [] if folder_paths.empty?
|
69
|
+
|
70
|
+
all_folders = []
|
71
|
+
|
72
|
+
# first get all top-level folders
|
73
|
+
parent_folders = folder_paths.map(&:first).uniq
|
74
|
+
filter_condition = "--filter '#{parent_folders.map { |name| "Name == #{name.shellescape.inspect}" }.join(" || ")}'"
|
75
|
+
fetch_folders = `passbolt list folders #{filter_condition} --json`
|
76
|
+
raise RuntimeError, "Could not read folders from Passbolt" unless $?.success?
|
77
|
+
|
78
|
+
parent_folder_items = JSON.parse(fetch_folders)
|
79
|
+
all_folders.concat(parent_folder_items)
|
80
|
+
|
81
|
+
# get nested folders for each parent
|
82
|
+
folder_paths.each do |path|
|
83
|
+
next if path.size <= 1 # skip non-nested folders
|
84
|
+
|
85
|
+
parent = path[0]
|
86
|
+
parent_folder = parent_folder_items.find { |f| f["name"] == parent }
|
87
|
+
next unless parent_folder
|
88
|
+
|
89
|
+
# for each nested level, get the folders using the parent's ID
|
90
|
+
current_parent = parent_folder
|
91
|
+
path[1..-1].each do |folder_name|
|
92
|
+
filter_condition = "--filter 'Name == #{folder_name.shellescape.inspect} && FolderParentID == #{current_parent["id"].shellescape.inspect}'"
|
93
|
+
fetch_nested = `passbolt list folders #{filter_condition} --json`
|
94
|
+
next unless $?.success?
|
95
|
+
|
96
|
+
nested_folders = JSON.parse(fetch_nested)
|
97
|
+
break if nested_folders.empty?
|
98
|
+
|
99
|
+
all_folders.concat(nested_folders)
|
100
|
+
current_parent = nested_folders.first
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# check if we found all required folders
|
105
|
+
found_paths = all_folders.map { |f| get_folder_path(f, all_folders) }
|
106
|
+
missing_paths = folder_paths.map { |path| path.join("/") } - found_paths
|
107
|
+
raise RuntimeError, "Could not find the following folders in Passbolt: #{missing_paths.join(", ")}" if missing_paths.any?
|
108
|
+
|
109
|
+
all_folders
|
110
|
+
end
|
111
|
+
|
112
|
+
def get_folder_path(folder, all_folders, path = [])
|
113
|
+
path.unshift(folder["name"])
|
114
|
+
return path.join("/") if folder["folder_parent_id"].to_s.empty?
|
115
|
+
|
116
|
+
parent = all_folders.find { |f| f["id"] == folder["folder_parent_id"] }
|
117
|
+
return path.join("/") unless parent
|
118
|
+
|
119
|
+
get_folder_path(parent, all_folders, path)
|
120
|
+
end
|
121
|
+
|
122
|
+
def check_dependencies!
|
123
|
+
raise RuntimeError, "Passbolt CLI is not installed" unless cli_installed?
|
124
|
+
end
|
125
|
+
|
126
|
+
def cli_installed?
|
127
|
+
`passbolt --version 2> /dev/null`
|
128
|
+
$?.success?
|
129
|
+
end
|
130
|
+
end
|
data/lib/kamal/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: kamal
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.7.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- David Heinemeier Hansson
|
@@ -220,6 +220,7 @@ files:
|
|
220
220
|
- lib/kamal/cli/app/assets.rb
|
221
221
|
- lib/kamal/cli/app/boot.rb
|
222
222
|
- lib/kamal/cli/app/error_pages.rb
|
223
|
+
- lib/kamal/cli/app/ssl_certificates.rb
|
223
224
|
- lib/kamal/cli/base.rb
|
224
225
|
- lib/kamal/cli/build.rb
|
225
226
|
- lib/kamal/cli/build/clone.rb
|
@@ -265,6 +266,7 @@ files:
|
|
265
266
|
- lib/kamal/commands/builder/cloud.rb
|
266
267
|
- lib/kamal/commands/builder/hybrid.rb
|
267
268
|
- lib/kamal/commands/builder/local.rb
|
269
|
+
- lib/kamal/commands/builder/pack.rb
|
268
270
|
- lib/kamal/commands/builder/remote.rb
|
269
271
|
- lib/kamal/commands/docker.rb
|
270
272
|
- lib/kamal/commands/hook.rb
|
@@ -327,6 +329,7 @@ files:
|
|
327
329
|
- lib/kamal/secrets/adapters/gcp_secret_manager.rb
|
328
330
|
- lib/kamal/secrets/adapters/last_pass.rb
|
329
331
|
- lib/kamal/secrets/adapters/one_password.rb
|
332
|
+
- lib/kamal/secrets/adapters/passbolt.rb
|
330
333
|
- lib/kamal/secrets/adapters/test.rb
|
331
334
|
- lib/kamal/secrets/dotenv/inline_command_substitution.rb
|
332
335
|
- lib/kamal/sshkit_with_ext.rb
|