kamal 2.2.2 → 2.4.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 +23 -8
- data/lib/kamal/cli/app/boot.rb +2 -2
- data/lib/kamal/cli/app.rb +13 -5
- data/lib/kamal/cli/proxy.rb +1 -1
- data/lib/kamal/cli/secrets.rb +9 -3
- data/lib/kamal/cli/templates/deploy.yml +10 -6
- data/lib/kamal/commands/accessory/proxy.rb +16 -0
- data/lib/kamal/commands/accessory.rb +8 -4
- data/lib/kamal/commands/app/containers.rb +2 -2
- data/lib/kamal/commands/app/execution.rb +4 -2
- data/lib/kamal/commands/app/images.rb +1 -1
- data/lib/kamal/commands/app/logging.rb +14 -4
- data/lib/kamal/commands/app.rb +15 -7
- data/lib/kamal/commands/base.rb +11 -1
- data/lib/kamal/commands/builder/base.rb +10 -2
- data/lib/kamal/configuration/accessory.rb +28 -3
- data/lib/kamal/configuration/builder.rb +9 -1
- data/lib/kamal/configuration/docs/accessory.yml +14 -2
- data/lib/kamal/configuration/docs/alias.yml +2 -2
- data/lib/kamal/configuration/docs/builder.yml +12 -0
- data/lib/kamal/configuration/docs/proxy.yml +13 -10
- data/lib/kamal/configuration/docs/registry.yml +4 -0
- data/lib/kamal/configuration.rb +3 -3
- data/lib/kamal/env_file.rb +3 -1
- data/lib/kamal/secrets/adapters/aws_secrets_manager.rb +42 -0
- data/lib/kamal/secrets/adapters/base.rb +13 -1
- data/lib/kamal/secrets/adapters/bitwarden.rb +23 -8
- data/lib/kamal/secrets/adapters/doppler.rb +53 -0
- data/lib/kamal/secrets/adapters/last_pass.rb +9 -0
- data/lib/kamal/secrets/adapters/one_password.rb +9 -0
- data/lib/kamal/secrets/adapters/test.rb +4 -0
- data/lib/kamal/secrets/adapters/test_optional_account.rb +5 -0
- data/lib/kamal/secrets.rb +12 -7
- data/lib/kamal/utils.rb +2 -0
- data/lib/kamal/version.rb +1 -1
- metadata +18 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 246d570a9a6f85698245aa14237986648b39f5e32750bd6f76fd401f96e2398a
|
4
|
+
data.tar.gz: 02a6b0c3aff13021ee86381e5c10bd4f5b7708d8f9c13c92e03ddd46ed5fa9d5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 31365397457be212570677a8dfa4fd3aac1701a4f251bc989100f4ea043edc0f29265d92c9bdde432b323ceadb62c7ccc491c8f6bc9c88b49d0835d193447f83
|
7
|
+
data.tar.gz: cc6f0cbce224c0234bbaba0ed5bf78001cbb9e888a969154e2728b07b36e8fb6e7a24fa1db2b5a52159f49de5fcee66d28de816ba5b9a8f95285a0e75f08a090
|
data/lib/kamal/cli/accessory.rb
CHANGED
@@ -18,6 +18,11 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
|
|
18
18
|
execute *accessory.ensure_env_directory
|
19
19
|
upload! accessory.secrets_io, accessory.secrets_path, mode: "0600"
|
20
20
|
execute *accessory.run
|
21
|
+
|
22
|
+
if accessory.running_proxy?
|
23
|
+
target = capture_with_info(*accessory.container_id_for(container_name: accessory.service_name, only_running: true)).strip
|
24
|
+
execute *accessory.deploy(target: target)
|
25
|
+
end
|
21
26
|
end
|
22
27
|
end
|
23
28
|
end
|
@@ -75,6 +80,10 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
|
|
75
80
|
on(hosts) do
|
76
81
|
execute *KAMAL.auditor.record("Started #{name} accessory"), verbosity: :debug
|
77
82
|
execute *accessory.start
|
83
|
+
if accessory.running_proxy?
|
84
|
+
target = capture_with_info(*accessory.container_id_for(container_name: accessory.service_name, only_running: true)).strip
|
85
|
+
execute *accessory.deploy(target: target)
|
86
|
+
end
|
78
87
|
end
|
79
88
|
end
|
80
89
|
end
|
@@ -87,6 +96,11 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
|
|
87
96
|
on(hosts) do
|
88
97
|
execute *KAMAL.auditor.record("Stopped #{name} accessory"), verbosity: :debug
|
89
98
|
execute *accessory.stop, raise_on_non_zero_exit: false
|
99
|
+
|
100
|
+
if accessory.running_proxy?
|
101
|
+
target = capture_with_info(*accessory.container_id_for(container_name: accessory.service_name, only_running: true)).strip
|
102
|
+
execute *accessory.remove if target
|
103
|
+
end
|
90
104
|
end
|
91
105
|
end
|
92
106
|
end
|
@@ -112,14 +126,15 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
|
|
112
126
|
end
|
113
127
|
end
|
114
128
|
|
115
|
-
desc "exec [NAME] [CMD]", "Execute a custom command on servers (use --help to show options)"
|
129
|
+
desc "exec [NAME] [CMD...]", "Execute a custom command on servers within the accessory container (use --help to show options)"
|
116
130
|
option :interactive, aliases: "-i", type: :boolean, default: false, desc: "Execute command over ssh for an interactive shell (use for console/bash)"
|
117
131
|
option :reuse, type: :boolean, default: false, desc: "Reuse currently running container instead of starting a new one"
|
118
|
-
def exec(name, cmd)
|
132
|
+
def exec(name, *cmd)
|
133
|
+
cmd = Kamal::Utils.join_commands(cmd)
|
119
134
|
with_accessory(name) do |accessory, hosts|
|
120
135
|
case
|
121
136
|
when options[:interactive] && options[:reuse]
|
122
|
-
say "Launching interactive command
|
137
|
+
say "Launching interactive command via SSH from existing container...", :magenta
|
123
138
|
run_locally { exec accessory.execute_in_existing_container_over_ssh(cmd) }
|
124
139
|
|
125
140
|
when options[:interactive]
|
@@ -128,16 +143,16 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
|
|
128
143
|
|
129
144
|
when options[:reuse]
|
130
145
|
say "Launching command from existing container...", :magenta
|
131
|
-
on(hosts) do
|
146
|
+
on(hosts) do |host|
|
132
147
|
execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on #{name} accessory"), verbosity: :debug
|
133
|
-
capture_with_info(*accessory.execute_in_existing_container(cmd))
|
148
|
+
puts_by_host host, capture_with_info(*accessory.execute_in_existing_container(cmd))
|
134
149
|
end
|
135
150
|
|
136
151
|
else
|
137
152
|
say "Launching command from new container...", :magenta
|
138
|
-
on(hosts) do
|
153
|
+
on(hosts) do |host|
|
139
154
|
execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on #{name} accessory"), verbosity: :debug
|
140
|
-
capture_with_info(*accessory.execute_in_new_container(cmd))
|
155
|
+
puts_by_host host, capture_with_info(*accessory.execute_in_new_container(cmd))
|
141
156
|
end
|
142
157
|
end
|
143
158
|
end
|
@@ -147,7 +162,7 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
|
|
147
162
|
option :since, aliases: "-s", desc: "Show logs since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)"
|
148
163
|
option :lines, type: :numeric, aliases: "-n", desc: "Number of log lines to pull from each server"
|
149
164
|
option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)"
|
150
|
-
option :grep_options,
|
165
|
+
option :grep_options, desc: "Additional options supplied to grep"
|
151
166
|
option :follow, aliases: "-f", desc: "Follow logs on primary server (or specific host set by --hosts)"
|
152
167
|
option :skip_timestamps, type: :boolean, aliases: "-T", desc: "Skip appending timestamps to logging output"
|
153
168
|
def logs(name)
|
data/lib/kamal/cli/app/boot.rb
CHANGED
@@ -45,7 +45,7 @@ class Kamal::Cli::App::Boot
|
|
45
45
|
|
46
46
|
def start_new_version
|
47
47
|
audit "Booted app version #{version}"
|
48
|
-
hostname = "#{host.to_s[0...51].
|
48
|
+
hostname = "#{host.to_s[0...51].chomp(".")}-#{SecureRandom.hex(6)}"
|
49
49
|
|
50
50
|
execute *app.ensure_env_directory
|
51
51
|
upload! role.secrets_io(host), role.secrets_path, mode: "0600"
|
@@ -91,7 +91,7 @@ class Kamal::Cli::App::Boot
|
|
91
91
|
if barrier.close
|
92
92
|
info "First #{KAMAL.primary_role} container is unhealthy on #{host}, not booting any other roles"
|
93
93
|
begin
|
94
|
-
error capture_with_info(*app.logs(
|
94
|
+
error capture_with_info(*app.logs(container_id: app.container_id_for_version(version)))
|
95
95
|
error capture_with_info(*app.container_health_log(version: version))
|
96
96
|
rescue SSHKit::Command::Failed
|
97
97
|
error "Could not fetch logs for #{version}"
|
data/lib/kamal/cli/app.rb
CHANGED
@@ -94,9 +94,15 @@ class Kamal::Cli::App < Kamal::Cli::Base
|
|
94
94
|
option :interactive, aliases: "-i", type: :boolean, default: false, desc: "Execute command over ssh for an interactive shell (use for console/bash)"
|
95
95
|
option :reuse, type: :boolean, default: false, desc: "Reuse currently running container instead of starting a new one"
|
96
96
|
option :env, aliases: "-e", type: :hash, desc: "Set environment variables for the command"
|
97
|
+
option :detach, type: :boolean, default: false, desc: "Execute command in a detached container"
|
97
98
|
def exec(*cmd)
|
99
|
+
if (incompatible_options = [ :interactive, :reuse ].select { |key| options[:detach] && options[key] }.presence)
|
100
|
+
raise ArgumentError, "Detach is not compatible with #{incompatible_options.join(" or ")}"
|
101
|
+
end
|
102
|
+
|
98
103
|
cmd = Kamal::Utils.join_commands(cmd)
|
99
104
|
env = options[:env]
|
105
|
+
detach = options[:detach]
|
100
106
|
case
|
101
107
|
when options[:interactive] && options[:reuse]
|
102
108
|
say "Get current version of running container...", :magenta unless options[:version]
|
@@ -138,7 +144,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
|
|
138
144
|
|
139
145
|
roles.each do |role|
|
140
146
|
execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on app version #{version}"), verbosity: :debug
|
141
|
-
puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).execute_in_new_container(cmd, env: env))
|
147
|
+
puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).execute_in_new_container(cmd, env: env, detach: detach))
|
142
148
|
end
|
143
149
|
end
|
144
150
|
end
|
@@ -186,15 +192,17 @@ class Kamal::Cli::App < Kamal::Cli::Base
|
|
186
192
|
option :since, aliases: "-s", desc: "Show lines since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)"
|
187
193
|
option :lines, type: :numeric, aliases: "-n", desc: "Number of lines to show from each server"
|
188
194
|
option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)"
|
189
|
-
option :grep_options,
|
195
|
+
option :grep_options, desc: "Additional options supplied to grep"
|
190
196
|
option :follow, aliases: "-f", desc: "Follow log on primary server (or specific host set by --hosts)"
|
191
197
|
option :skip_timestamps, type: :boolean, aliases: "-T", desc: "Skip appending timestamps to logging output"
|
198
|
+
option :container_id, desc: "Docker container ID to fetch logs"
|
192
199
|
def logs
|
193
200
|
# FIXME: Catch when app containers aren't running
|
194
201
|
|
195
202
|
grep = options[:grep]
|
196
203
|
grep_options = options[:grep_options]
|
197
204
|
since = options[:since]
|
205
|
+
container_id = options[:container_id]
|
198
206
|
timestamps = !options[:skip_timestamps]
|
199
207
|
|
200
208
|
if options[:follow]
|
@@ -207,8 +215,8 @@ class Kamal::Cli::App < Kamal::Cli::Base
|
|
207
215
|
role = KAMAL.roles_on(KAMAL.primary_host).first
|
208
216
|
|
209
217
|
app = KAMAL.app(role: role, host: host)
|
210
|
-
info app.follow_logs(host: KAMAL.primary_host, timestamps: timestamps, lines: lines, grep: grep, grep_options: grep_options)
|
211
|
-
exec app.follow_logs(host: KAMAL.primary_host, timestamps: timestamps, lines: lines, grep: grep, grep_options: grep_options)
|
218
|
+
info app.follow_logs(host: KAMAL.primary_host, container_id: container_id, timestamps: timestamps, lines: lines, grep: grep, grep_options: grep_options)
|
219
|
+
exec app.follow_logs(host: KAMAL.primary_host, container_id: container_id, timestamps: timestamps, lines: lines, grep: grep, grep_options: grep_options)
|
212
220
|
end
|
213
221
|
else
|
214
222
|
lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set
|
@@ -218,7 +226,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
|
|
218
226
|
|
219
227
|
roles.each do |role|
|
220
228
|
begin
|
221
|
-
puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).logs(timestamps: timestamps, since: since, lines: lines, grep: grep, grep_options: grep_options))
|
229
|
+
puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).logs(container_id: container_id, timestamps: timestamps, since: since, lines: lines, grep: grep, grep_options: grep_options))
|
222
230
|
rescue SSHKit::Command::Failed
|
223
231
|
puts_by_host host, "Nothing found"
|
224
232
|
end
|
data/lib/kamal/cli/proxy.rb
CHANGED
@@ -14,7 +14,7 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
|
|
14
14
|
version = capture_with_info(*KAMAL.proxy.version).strip.presence
|
15
15
|
|
16
16
|
if version && Kamal::Utils.older_version?(version, Kamal::Configuration::PROXY_MINIMUM_VERSION)
|
17
|
-
raise "kamal-proxy version #{version} is too old,
|
17
|
+
raise "kamal-proxy version #{version} is too old, run `kamal proxy reboot` in order to update to at least #{Kamal::Configuration::PROXY_MINIMUM_VERSION}"
|
18
18
|
end
|
19
19
|
execute *KAMAL.proxy.start_or_run
|
20
20
|
end
|
data/lib/kamal/cli/secrets.rb
CHANGED
@@ -1,11 +1,17 @@
|
|
1
1
|
class Kamal::Cli::Secrets < Kamal::Cli::Base
|
2
2
|
desc "fetch [SECRETS...]", "Fetch secrets from a vault"
|
3
3
|
option :adapter, type: :string, aliases: "-a", required: true, desc: "Which vault adapter to use"
|
4
|
-
option :account, type: :string, required:
|
4
|
+
option :account, type: :string, required: false, desc: "The account identifier or username"
|
5
5
|
option :from, type: :string, required: false, desc: "A vault or folder to fetch the secrets from"
|
6
6
|
option :inline, type: :boolean, required: false, hidden: true
|
7
7
|
def fetch(*secrets)
|
8
|
-
|
8
|
+
adapter = initialize_adapter(options[:adapter])
|
9
|
+
|
10
|
+
if adapter.requires_account? && options[:account].blank?
|
11
|
+
return puts "No value provided for required options '--account'"
|
12
|
+
end
|
13
|
+
|
14
|
+
results = adapter.fetch(secrets, **options.slice(:account, :from).symbolize_keys)
|
9
15
|
|
10
16
|
return_or_puts JSON.dump(results).shellescape, inline: options[:inline]
|
11
17
|
end
|
@@ -29,7 +35,7 @@ class Kamal::Cli::Secrets < Kamal::Cli::Base
|
|
29
35
|
end
|
30
36
|
|
31
37
|
private
|
32
|
-
def
|
38
|
+
def initialize_adapter(adapter)
|
33
39
|
Kamal::Secrets::Adapters.lookup(adapter)
|
34
40
|
end
|
35
41
|
|
@@ -13,13 +13,14 @@ servers:
|
|
13
13
|
# - 192.168.0.1
|
14
14
|
# cmd: bin/jobs
|
15
15
|
|
16
|
-
# Enable SSL auto certification via Let's Encrypt
|
17
|
-
#
|
18
|
-
#
|
19
|
-
|
16
|
+
# Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server.
|
17
|
+
# Remove this section when using multiple web servers and ensure you terminate SSL at your load balancer.
|
18
|
+
#
|
19
|
+
# Note: If using Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption.
|
20
|
+
proxy:
|
20
21
|
ssl: true
|
21
22
|
host: app.example.com
|
22
|
-
#
|
23
|
+
# Proxy connects to your container on port 80 by default.
|
23
24
|
# app_port: 3000
|
24
25
|
|
25
26
|
# Credentials for your image host.
|
@@ -35,6 +36,9 @@ registry:
|
|
35
36
|
# Configure builder setup.
|
36
37
|
builder:
|
37
38
|
arch: amd64
|
39
|
+
# Pass in additional build args needed for your Dockerfile.
|
40
|
+
# args:
|
41
|
+
# RUBY_VERSION: <%= File.read('.ruby-version').strip %>
|
38
42
|
|
39
43
|
# Inject ENV variables into containers (secrets come from .kamal/secrets).
|
40
44
|
#
|
@@ -90,7 +94,7 @@ builder:
|
|
90
94
|
# directories:
|
91
95
|
# - data:/var/lib/mysql
|
92
96
|
# redis:
|
93
|
-
# image:
|
97
|
+
# image: valkey/valkey:8
|
94
98
|
# host: 192.168.0.2
|
95
99
|
# port: 6379
|
96
100
|
# directories:
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Kamal::Commands::Accessory::Proxy
|
2
|
+
delegate :proxy_container_name, to: :config
|
3
|
+
|
4
|
+
def deploy(target:)
|
5
|
+
proxy_exec :deploy, service_name, *proxy.deploy_command_args(target: target)
|
6
|
+
end
|
7
|
+
|
8
|
+
def remove
|
9
|
+
proxy_exec :remove, service_name
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
def proxy_exec(*command)
|
14
|
+
docker :exec, proxy_container_name, "kamal-proxy", *command
|
15
|
+
end
|
16
|
+
end
|
@@ -1,9 +1,13 @@
|
|
1
1
|
class Kamal::Commands::Accessory < Kamal::Commands::Base
|
2
|
+
include Proxy
|
3
|
+
|
2
4
|
attr_reader :accessory_config
|
3
5
|
delegate :service_name, :image, :hosts, :port, :files, :directories, :cmd,
|
4
|
-
:publish_args, :env_args, :volume_args, :label_args, :option_args,
|
5
|
-
:secrets_io, :secrets_path, :env_directory,
|
6
|
+
:network_args, :publish_args, :env_args, :volume_args, :label_args, :option_args,
|
7
|
+
:secrets_io, :secrets_path, :env_directory, :proxy, :running_proxy?,
|
6
8
|
to: :accessory_config
|
9
|
+
delegate :proxy_container_name, to: :config
|
10
|
+
|
7
11
|
|
8
12
|
def initialize(config, name:)
|
9
13
|
super(config)
|
@@ -15,7 +19,7 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base
|
|
15
19
|
"--name", service_name,
|
16
20
|
"--detach",
|
17
21
|
"--restart", "unless-stopped",
|
18
|
-
|
22
|
+
*network_args,
|
19
23
|
*config.logging_args,
|
20
24
|
*publish_args,
|
21
25
|
*env_args,
|
@@ -64,7 +68,7 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base
|
|
64
68
|
docker :run,
|
65
69
|
("-it" if interactive),
|
66
70
|
"--rm",
|
67
|
-
|
71
|
+
*network_args,
|
68
72
|
*env_args,
|
69
73
|
*volume_args,
|
70
74
|
image,
|
@@ -2,7 +2,7 @@ module Kamal::Commands::App::Containers
|
|
2
2
|
DOCKER_HEALTH_LOG_FORMAT = "'{{json .State.Health}}'"
|
3
3
|
|
4
4
|
def list_containers
|
5
|
-
docker :container, :ls, "--all", *
|
5
|
+
docker :container, :ls, "--all", *container_filter_args
|
6
6
|
end
|
7
7
|
|
8
8
|
def list_container_names
|
@@ -20,7 +20,7 @@ module Kamal::Commands::App::Containers
|
|
20
20
|
end
|
21
21
|
|
22
22
|
def remove_containers
|
23
|
-
docker :container, :prune, "--force", *
|
23
|
+
docker :container, :prune, "--force", *container_filter_args
|
24
24
|
end
|
25
25
|
|
26
26
|
def container_health_log(version:)
|
@@ -7,13 +7,15 @@ module Kamal::Commands::App::Execution
|
|
7
7
|
*command
|
8
8
|
end
|
9
9
|
|
10
|
-
def execute_in_new_container(*command, interactive: false, env:)
|
10
|
+
def execute_in_new_container(*command, interactive: false, detach: false, env:)
|
11
11
|
docker :run,
|
12
12
|
("-it" if interactive),
|
13
|
-
"--
|
13
|
+
("--detach" if detach),
|
14
|
+
("--rm" unless detach),
|
14
15
|
"--network", "kamal",
|
15
16
|
*role&.env_args(host),
|
16
17
|
*argumentize("--env", env),
|
18
|
+
*role.logging_args,
|
17
19
|
*config.volume_args,
|
18
20
|
*role&.option_args,
|
19
21
|
config.absolute_image,
|
@@ -1,18 +1,28 @@
|
|
1
1
|
module Kamal::Commands::App::Logging
|
2
|
-
def logs(
|
2
|
+
def logs(container_id: nil, timestamps: true, since: nil, lines: nil, grep: nil, grep_options: nil)
|
3
3
|
pipe \
|
4
|
-
|
4
|
+
container_id_command(container_id),
|
5
5
|
"xargs docker logs#{" --timestamps" if timestamps}#{" --since #{since}" if since}#{" --tail #{lines}" if lines} 2>&1",
|
6
6
|
("grep '#{grep}'#{" #{grep_options}" if grep_options}" if grep)
|
7
7
|
end
|
8
8
|
|
9
|
-
def follow_logs(host:, timestamps: true, lines: nil, grep: nil, grep_options: nil)
|
9
|
+
def follow_logs(host:, container_id: nil, timestamps: true, lines: nil, grep: nil, grep_options: nil)
|
10
10
|
run_over_ssh \
|
11
11
|
pipe(
|
12
|
-
|
12
|
+
container_id_command(container_id),
|
13
13
|
"xargs docker logs#{" --timestamps" if timestamps}#{" --tail #{lines}" if lines} --follow 2>&1",
|
14
14
|
(%(grep "#{grep}"#{" #{grep_options}" if grep_options}) if grep)
|
15
15
|
),
|
16
16
|
host: host
|
17
17
|
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def container_id_command(container_id)
|
22
|
+
case container_id
|
23
|
+
when Array then container_id
|
24
|
+
when String, Symbol then "echo #{container_id}"
|
25
|
+
else current_running_container_id
|
26
|
+
end
|
27
|
+
end
|
18
28
|
end
|
data/lib/kamal/commands/app.rb
CHANGED
@@ -47,7 +47,7 @@ class Kamal::Commands::App < Kamal::Commands::Base
|
|
47
47
|
end
|
48
48
|
|
49
49
|
def info
|
50
|
-
docker :ps, *
|
50
|
+
docker :ps, *container_filter_args
|
51
51
|
end
|
52
52
|
|
53
53
|
|
@@ -67,7 +67,7 @@ class Kamal::Commands::App < Kamal::Commands::Base
|
|
67
67
|
|
68
68
|
def list_versions(*docker_args, statuses: nil)
|
69
69
|
pipe \
|
70
|
-
docker(:ps, *
|
70
|
+
docker(:ps, *container_filter_args(statuses: statuses), *docker_args, "--format", '"{{.Names}}"'),
|
71
71
|
extract_version_from_name
|
72
72
|
end
|
73
73
|
|
@@ -91,11 +91,15 @@ class Kamal::Commands::App < Kamal::Commands::Base
|
|
91
91
|
end
|
92
92
|
|
93
93
|
def latest_container(format:, filters: nil)
|
94
|
-
docker :ps, "--latest", *format, *
|
94
|
+
docker :ps, "--latest", *format, *container_filter_args(statuses: ACTIVE_DOCKER_STATUSES), argumentize("--filter", filters)
|
95
95
|
end
|
96
96
|
|
97
|
-
def
|
98
|
-
argumentize "--filter",
|
97
|
+
def container_filter_args(statuses: nil)
|
98
|
+
argumentize "--filter", container_filters(statuses: statuses)
|
99
|
+
end
|
100
|
+
|
101
|
+
def image_filter_args
|
102
|
+
argumentize "--filter", image_filters
|
99
103
|
end
|
100
104
|
|
101
105
|
def extract_version_from_name
|
@@ -103,13 +107,17 @@ class Kamal::Commands::App < Kamal::Commands::Base
|
|
103
107
|
%(while read line; do echo ${line##{role.container_prefix}-}; done)
|
104
108
|
end
|
105
109
|
|
106
|
-
def
|
110
|
+
def container_filters(statuses: nil)
|
107
111
|
[ "label=service=#{config.service}" ].tap do |filters|
|
108
|
-
filters << "label=destination=#{config.destination}"
|
112
|
+
filters << "label=destination=#{config.destination}"
|
109
113
|
filters << "label=role=#{role}" if role
|
110
114
|
statuses&.each do |status|
|
111
115
|
filters << "status=#{status}"
|
112
116
|
end
|
113
117
|
end
|
114
118
|
end
|
119
|
+
|
120
|
+
def image_filters
|
121
|
+
[ "label=service=#{config.service}" ]
|
122
|
+
end
|
115
123
|
end
|
data/lib/kamal/commands/base.rb
CHANGED
@@ -11,7 +11,7 @@ module Kamal::Commands
|
|
11
11
|
end
|
12
12
|
|
13
13
|
def run_over_ssh(*command, host:)
|
14
|
-
"ssh#{ssh_proxy_args} -t #{config.ssh.user}@#{host} -p #{config.ssh.port} '#{command.join(" ").gsub("'", "'\\\\''")}'"
|
14
|
+
"ssh#{ssh_proxy_args}#{ssh_keys_args} -t #{config.ssh.user}@#{host} -p #{config.ssh.port} '#{command.join(" ").gsub("'", "'\\\\''")}'"
|
15
15
|
end
|
16
16
|
|
17
17
|
def container_id_for(container_name:, only_running: false)
|
@@ -94,5 +94,15 @@ module Kamal::Commands
|
|
94
94
|
" -o ProxyCommand='#{config.ssh.proxy.command_line_template}'"
|
95
95
|
end
|
96
96
|
end
|
97
|
+
|
98
|
+
def ssh_keys_args
|
99
|
+
"#{ ssh_keys.join("") if ssh_keys}" + "#{" -o IdentitiesOnly=yes" if config.ssh&.keys_only}"
|
100
|
+
end
|
101
|
+
|
102
|
+
def ssh_keys
|
103
|
+
config.ssh.keys&.map do |key|
|
104
|
+
" -i #{key}"
|
105
|
+
end
|
106
|
+
end
|
97
107
|
end
|
98
108
|
end
|
@@ -6,7 +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
|
-
:cache_from, :cache_to, :ssh, :driver, :docker_driver?,
|
9
|
+
:cache_from, :cache_to, :ssh, :provenance, :sbom, :driver, :docker_driver?,
|
10
10
|
to: :builder_config
|
11
11
|
|
12
12
|
def clean
|
@@ -37,7 +37,7 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
|
|
37
37
|
end
|
38
38
|
|
39
39
|
def build_options
|
40
|
-
[ *build_tags, *build_cache, *build_labels, *build_args, *build_secrets, *build_dockerfile, *build_target, *build_ssh ]
|
40
|
+
[ *build_tags, *build_cache, *build_labels, *build_args, *build_secrets, *build_dockerfile, *build_target, *build_ssh, *builder_provenance, *builder_sbom ]
|
41
41
|
end
|
42
42
|
|
43
43
|
def build_context
|
@@ -97,6 +97,14 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
|
|
97
97
|
argumentize "--ssh", ssh if ssh.present?
|
98
98
|
end
|
99
99
|
|
100
|
+
def builder_provenance
|
101
|
+
argumentize "--provenance", provenance unless provenance.nil?
|
102
|
+
end
|
103
|
+
|
104
|
+
def builder_sbom
|
105
|
+
argumentize "--sbom", sbom unless sbom.nil?
|
106
|
+
end
|
107
|
+
|
100
108
|
def builder_config
|
101
109
|
config.builder
|
102
110
|
end
|
@@ -1,9 +1,11 @@
|
|
1
1
|
class Kamal::Configuration::Accessory
|
2
2
|
include Kamal::Configuration::Validation
|
3
3
|
|
4
|
+
DEFAULT_NETWORK = "kamal"
|
5
|
+
|
4
6
|
delegate :argumentize, :optionize, to: Kamal::Utils
|
5
7
|
|
6
|
-
attr_reader :name, :accessory_config, :env
|
8
|
+
attr_reader :name, :accessory_config, :env, :proxy
|
7
9
|
|
8
10
|
def initialize(name, config:)
|
9
11
|
@name, @config, @accessory_config = name.inquiry, config, config.raw_config["accessories"][name]
|
@@ -18,6 +20,8 @@ class Kamal::Configuration::Accessory
|
|
18
20
|
config: accessory_config.fetch("env", {}),
|
19
21
|
secrets: config.secrets,
|
20
22
|
context: "accessories/#{name}/env"
|
23
|
+
|
24
|
+
initialize_proxy if running_proxy?
|
21
25
|
end
|
22
26
|
|
23
27
|
def service_name
|
@@ -38,6 +42,10 @@ class Kamal::Configuration::Accessory
|
|
38
42
|
end
|
39
43
|
end
|
40
44
|
|
45
|
+
def network_args
|
46
|
+
argumentize "--network", network
|
47
|
+
end
|
48
|
+
|
41
49
|
def publish_args
|
42
50
|
argumentize "--publish", port if port
|
43
51
|
end
|
@@ -100,6 +108,17 @@ class Kamal::Configuration::Accessory
|
|
100
108
|
accessory_config["cmd"]
|
101
109
|
end
|
102
110
|
|
111
|
+
def running_proxy?
|
112
|
+
@accessory_config["proxy"].present?
|
113
|
+
end
|
114
|
+
|
115
|
+
def initialize_proxy
|
116
|
+
@proxy = Kamal::Configuration::Proxy.new \
|
117
|
+
config: config,
|
118
|
+
proxy_config: accessory_config["proxy"],
|
119
|
+
context: "accessories/#{name}/proxy"
|
120
|
+
end
|
121
|
+
|
103
122
|
private
|
104
123
|
attr_accessor :config
|
105
124
|
|
@@ -123,7 +142,7 @@ class Kamal::Configuration::Accessory
|
|
123
142
|
end
|
124
143
|
|
125
144
|
def read_dynamic_file(local_file)
|
126
|
-
StringIO.new(ERB.new(
|
145
|
+
StringIO.new(ERB.new(File.read(local_file)).result)
|
127
146
|
end
|
128
147
|
|
129
148
|
def expand_remote_file(remote_file)
|
@@ -170,7 +189,13 @@ class Kamal::Configuration::Accessory
|
|
170
189
|
|
171
190
|
def hosts_from_roles
|
172
191
|
if accessory_config.key?("roles")
|
173
|
-
accessory_config["roles"].flat_map
|
192
|
+
accessory_config["roles"].flat_map do |role|
|
193
|
+
config.role(role)&.hosts || raise(Kamal::ConfigurationError, "Unknown role in accessories config: '#{role}'")
|
194
|
+
end
|
174
195
|
end
|
175
196
|
end
|
197
|
+
|
198
|
+
def network
|
199
|
+
accessory_config["network"] || DEFAULT_NETWORK
|
200
|
+
end
|
176
201
|
end
|
@@ -111,6 +111,14 @@ class Kamal::Configuration::Builder
|
|
111
111
|
builder_config["ssh"]
|
112
112
|
end
|
113
113
|
|
114
|
+
def provenance
|
115
|
+
builder_config["provenance"]
|
116
|
+
end
|
117
|
+
|
118
|
+
def sbom
|
119
|
+
builder_config["sbom"]
|
120
|
+
end
|
121
|
+
|
114
122
|
def git_clone?
|
115
123
|
Kamal::Git.used? && builder_config["context"].nil?
|
116
124
|
end
|
@@ -166,7 +174,7 @@ class Kamal::Configuration::Builder
|
|
166
174
|
end
|
167
175
|
|
168
176
|
def cache_to_config_for_registry
|
169
|
-
[ "type=registry", builder_config["cache"]&.fetch("options", nil)
|
177
|
+
[ "type=registry", "ref=#{cache_image_ref}", builder_config["cache"]&.fetch("options", nil) ].compact.join(",")
|
170
178
|
end
|
171
179
|
|
172
180
|
def repo_basename
|
@@ -43,8 +43,8 @@ accessories:
|
|
43
43
|
|
44
44
|
# Port mappings
|
45
45
|
#
|
46
|
-
# See https://docs.docker.com/network
|
47
|
-
# implications of exposing ports publicly.
|
46
|
+
# See [https://docs.docker.com/network/](https://docs.docker.com/network/), and
|
47
|
+
# especially note the warning about the security implications of exposing ports publicly.
|
48
48
|
port: "127.0.0.1:3306:3306"
|
49
49
|
|
50
50
|
# Labels
|
@@ -90,3 +90,15 @@ accessories:
|
|
90
90
|
# They are not created or copied before mounting:
|
91
91
|
volumes:
|
92
92
|
- /path/to/mysql-logs:/var/log/mysql
|
93
|
+
|
94
|
+
# Network
|
95
|
+
#
|
96
|
+
# The network the accessory will be attached to.
|
97
|
+
#
|
98
|
+
# Defaults to kamal:
|
99
|
+
network: custom
|
100
|
+
|
101
|
+
# Proxy
|
102
|
+
#
|
103
|
+
proxy:
|
104
|
+
...
|
@@ -5,12 +5,12 @@
|
|
5
5
|
# For example, for a Rails app, you might open a console with:
|
6
6
|
#
|
7
7
|
# ```shell
|
8
|
-
# kamal app exec -i
|
8
|
+
# kamal app exec -i --reuse "bin/rails console"
|
9
9
|
# ```
|
10
10
|
#
|
11
11
|
# By defining an alias, like this:
|
12
12
|
aliases:
|
13
|
-
console: app exec -
|
13
|
+
console: app exec -i --reuse "bin/rails console"
|
14
14
|
# You can now open the console with:
|
15
15
|
#
|
16
16
|
# ```shell
|
@@ -102,3 +102,15 @@ builder:
|
|
102
102
|
#
|
103
103
|
# The build driver to use, defaults to `docker-container`:
|
104
104
|
driver: docker
|
105
|
+
|
106
|
+
# Provenance
|
107
|
+
#
|
108
|
+
# It is used to configure provenance attestations for the build result.
|
109
|
+
# The value can also be a boolean to enable or disable provenance attestations.
|
110
|
+
provenance: mode=max
|
111
|
+
|
112
|
+
# SBOM (Software Bill of Materials)
|
113
|
+
#
|
114
|
+
# It is used to configure SBOM generation for the build result.
|
115
|
+
# The value can also be a boolean to enable or disable SBOM generation.
|
116
|
+
sbom: true
|
@@ -46,9 +46,22 @@ proxy:
|
|
46
46
|
# The host value must point to the server we are deploying to, and port 443 must be
|
47
47
|
# open for the Let's Encrypt challenge to succeed.
|
48
48
|
#
|
49
|
+
# If you set `ssl` to `true`, `kamal-proxy` will stop forwarding headers to your app,
|
50
|
+
# unless you explicitly set `forward_headers: true`
|
51
|
+
#
|
49
52
|
# Defaults to `false`:
|
50
53
|
ssl: true
|
51
54
|
|
55
|
+
# Forward headers
|
56
|
+
#
|
57
|
+
# Whether to forward the `X-Forwarded-For` and `X-Forwarded-Proto` headers.
|
58
|
+
#
|
59
|
+
# If you are behind a trusted proxy, you can set this to `true` to forward the headers.
|
60
|
+
#
|
61
|
+
# By default, kamal-proxy will not forward the headers if the `ssl` option is set to `true`, and
|
62
|
+
# will forward them if it is set to `false`.
|
63
|
+
forward_headers: true
|
64
|
+
|
52
65
|
# Response timeout
|
53
66
|
#
|
54
67
|
# How long to wait for requests to complete before timing out, defaults to 30 seconds:
|
@@ -93,13 +106,3 @@ proxy:
|
|
93
106
|
response_headers:
|
94
107
|
- X-Request-ID
|
95
108
|
- X-Request-Start
|
96
|
-
|
97
|
-
# Forward headers
|
98
|
-
#
|
99
|
-
# Whether to forward the `X-Forwarded-For` and `X-Forwarded-Proto` headers.
|
100
|
-
#
|
101
|
-
# If you are behind a trusted proxy, you can set this to `true` to forward the headers.
|
102
|
-
#
|
103
|
-
# By default, kamal-proxy will not forward the headers if the `ssl` option is set to `true`, and
|
104
|
-
# will forward them if it is set to `false`.
|
105
|
-
forward_headers: true
|
@@ -2,6 +2,10 @@
|
|
2
2
|
#
|
3
3
|
# The default registry is Docker Hub, but you can change it using `registry/server`.
|
4
4
|
#
|
5
|
+
# By default, Docker Hub creates public repositories. To avoid making your images public,
|
6
|
+
# set up a private repository before deploying, or change the default repository privacy
|
7
|
+
# settings to private in your [Docker Hub settings](https://hub.docker.com/repository-settings/default-privacy).
|
8
|
+
#
|
5
9
|
# A reference to a secret (in this case, `DOCKER_REGISTRY_TOKEN`) will look up the secret
|
6
10
|
# in the local environment:
|
7
11
|
registry:
|
data/lib/kamal/configuration.rb
CHANGED
@@ -14,7 +14,7 @@ class Kamal::Configuration
|
|
14
14
|
|
15
15
|
include Validation
|
16
16
|
|
17
|
-
PROXY_MINIMUM_VERSION = "v0.8.
|
17
|
+
PROXY_MINIMUM_VERSION = "v0.8.4"
|
18
18
|
PROXY_HTTP_PORT = 80
|
19
19
|
PROXY_HTTPS_PORT = 443
|
20
20
|
PROXY_LOG_MAX_SIZE = "10m"
|
@@ -37,7 +37,7 @@ class Kamal::Configuration
|
|
37
37
|
if file.exist?
|
38
38
|
# Newer Psych doesn't load aliases by default
|
39
39
|
load_method = YAML.respond_to?(:unsafe_load) ? :unsafe_load : :load
|
40
|
-
YAML.send(load_method, ERB.new(
|
40
|
+
YAML.send(load_method, ERB.new(File.read(file)).result).symbolize_keys
|
41
41
|
else
|
42
42
|
raise "Configuration file not found in #{file}"
|
43
43
|
end
|
@@ -254,7 +254,7 @@ class Kamal::Configuration
|
|
254
254
|
end
|
255
255
|
|
256
256
|
def proxy_logging_args(max_size)
|
257
|
-
argumentize "--log-opt", "max-size=#{max_size}"
|
257
|
+
argumentize "--log-opt", "max-size=#{max_size}" if max_size.present?
|
258
258
|
end
|
259
259
|
|
260
260
|
def proxy_options_default
|
data/lib/kamal/env_file.rb
CHANGED
@@ -37,6 +37,8 @@ class Kamal::EnvFile
|
|
37
37
|
def escape_docker_env_file_ascii_value(value)
|
38
38
|
# Doublequotes are treated literally in docker env files
|
39
39
|
# so remove leading and trailing ones and unescape any others
|
40
|
-
value.to_s.dump[1..-2]
|
40
|
+
value.to_s.dump[1..-2]
|
41
|
+
.gsub(/\\"/, "\"")
|
42
|
+
.gsub(/\\#/, "#")
|
41
43
|
end
|
42
44
|
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
class Kamal::Secrets::Adapters::AwsSecretsManager < Kamal::Secrets::Adapters::Base
|
2
|
+
private
|
3
|
+
def login(_account)
|
4
|
+
nil
|
5
|
+
end
|
6
|
+
|
7
|
+
def fetch_secrets(secrets, account:, session:)
|
8
|
+
{}.tap do |results|
|
9
|
+
get_from_secrets_manager(secrets, account: account).each do |secret|
|
10
|
+
secret_name = secret["Name"]
|
11
|
+
secret_string = JSON.parse(secret["SecretString"])
|
12
|
+
|
13
|
+
secret_string.each do |key, value|
|
14
|
+
results["#{secret_name}/#{key}"] = value
|
15
|
+
end
|
16
|
+
rescue JSON::ParserError
|
17
|
+
results["#{secret_name}"] = secret["SecretString"]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def get_from_secrets_manager(secrets, account:)
|
23
|
+
`aws secretsmanager batch-get-secret-value --secret-id-list #{secrets.map(&:shellescape).join(" ")} --profile #{account.shellescape}`.tap do |secrets|
|
24
|
+
raise RuntimeError, "Could not read #{secrets} from AWS Secrets Manager" unless $?.success?
|
25
|
+
|
26
|
+
secrets = JSON.parse(secrets)
|
27
|
+
|
28
|
+
return secrets["SecretValues"] unless secrets["Errors"].present?
|
29
|
+
|
30
|
+
raise RuntimeError, secrets["Errors"].map { |error| "#{error['SecretId']}: #{error['Message']}" }.join(" ")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def check_dependencies!
|
35
|
+
raise RuntimeError, "AWS CLI is not installed" unless cli_installed?
|
36
|
+
end
|
37
|
+
|
38
|
+
def cli_installed?
|
39
|
+
`aws --version 2> /dev/null`
|
40
|
+
$?.success?
|
41
|
+
end
|
42
|
+
end
|
@@ -1,12 +1,20 @@
|
|
1
1
|
class Kamal::Secrets::Adapters::Base
|
2
2
|
delegate :optionize, to: Kamal::Utils
|
3
3
|
|
4
|
-
def fetch(secrets, account
|
4
|
+
def fetch(secrets, account: nil, from: nil)
|
5
|
+
raise RuntimeError, "Missing required option '--account'" if requires_account? && account.blank?
|
6
|
+
|
7
|
+
check_dependencies!
|
8
|
+
|
5
9
|
session = login(account)
|
6
10
|
full_secrets = secrets.map { |secret| [ from, secret ].compact.join("/") }
|
7
11
|
fetch_secrets(full_secrets, account: account, session: session)
|
8
12
|
end
|
9
13
|
|
14
|
+
def requires_account?
|
15
|
+
true
|
16
|
+
end
|
17
|
+
|
10
18
|
private
|
11
19
|
def login(...)
|
12
20
|
raise NotImplementedError
|
@@ -15,4 +23,8 @@ class Kamal::Secrets::Adapters::Base
|
|
15
23
|
def fetch_secrets(...)
|
16
24
|
raise NotImplementedError
|
17
25
|
end
|
26
|
+
|
27
|
+
def check_dependencies!
|
28
|
+
raise NotImplementedError
|
29
|
+
end
|
18
30
|
end
|
@@ -25,18 +25,15 @@ class Kamal::Secrets::Adapters::Bitwarden < Kamal::Secrets::Adapters::Base
|
|
25
25
|
{}.tap do |results|
|
26
26
|
items_fields(secrets).each do |item, fields|
|
27
27
|
item_json = run_command("get item #{item.shellescape}", session: session, raw: true)
|
28
|
-
raise RuntimeError, "Could not read #{
|
28
|
+
raise RuntimeError, "Could not read #{item} from Bitwarden" unless $?.success?
|
29
29
|
item_json = JSON.parse(item_json)
|
30
|
-
|
31
30
|
if fields.any?
|
32
|
-
|
33
|
-
item_field = item_json["fields"].find { |f| f["name"] == field }
|
34
|
-
raise RuntimeError, "Could not find field #{field} in item #{item} in Bitwarden" unless item_field
|
35
|
-
value = item_field["value"]
|
36
|
-
results["#{item}/#{field}"] = value
|
37
|
-
end
|
31
|
+
results.merge! fetch_secrets_from_fields(fields, item, item_json)
|
38
32
|
elsif item_json.dig("login", "password")
|
39
33
|
results[item] = item_json.dig("login", "password")
|
34
|
+
elsif item_json["fields"]&.any?
|
35
|
+
fields = item_json["fields"].pluck("name")
|
36
|
+
results.merge! fetch_secrets_from_fields(fields, item, item_json)
|
40
37
|
else
|
41
38
|
raise RuntimeError, "Item #{item} is not a login type item and no fields were specified"
|
42
39
|
end
|
@@ -44,6 +41,15 @@ class Kamal::Secrets::Adapters::Bitwarden < Kamal::Secrets::Adapters::Base
|
|
44
41
|
end
|
45
42
|
end
|
46
43
|
|
44
|
+
def fetch_secrets_from_fields(fields, item, item_json)
|
45
|
+
fields.to_h do |field|
|
46
|
+
item_field = item_json["fields"].find { |f| f["name"] == field }
|
47
|
+
raise RuntimeError, "Could not find field #{field} in item #{item} in Bitwarden" unless item_field
|
48
|
+
value = item_field["value"]
|
49
|
+
[ "#{item}/#{field}", value ]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
47
53
|
def items_fields(secrets)
|
48
54
|
{}.tap do |items|
|
49
55
|
secrets.each do |secret|
|
@@ -63,4 +69,13 @@ class Kamal::Secrets::Adapters::Bitwarden < Kamal::Secrets::Adapters::Base
|
|
63
69
|
result = `#{full_command}`.strip
|
64
70
|
raw ? result : JSON.parse(result)
|
65
71
|
end
|
72
|
+
|
73
|
+
def check_dependencies!
|
74
|
+
raise RuntimeError, "Bitwarden CLI is not installed" unless cli_installed?
|
75
|
+
end
|
76
|
+
|
77
|
+
def cli_installed?
|
78
|
+
`bw --version 2> /dev/null`
|
79
|
+
$?.success?
|
80
|
+
end
|
66
81
|
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
class Kamal::Secrets::Adapters::Doppler < Kamal::Secrets::Adapters::Base
|
2
|
+
def requires_account?
|
3
|
+
false
|
4
|
+
end
|
5
|
+
|
6
|
+
private
|
7
|
+
def login(*)
|
8
|
+
unless loggedin?
|
9
|
+
`doppler login -y`
|
10
|
+
raise RuntimeError, "Failed to login to Doppler" unless $?.success?
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def loggedin?
|
15
|
+
`doppler me --json 2> /dev/null`
|
16
|
+
$?.success?
|
17
|
+
end
|
18
|
+
|
19
|
+
def fetch_secrets(secrets, **)
|
20
|
+
project_and_config_flags = ""
|
21
|
+
unless service_token_set?
|
22
|
+
project, config, _ = secrets.first.split("/")
|
23
|
+
|
24
|
+
unless project && config
|
25
|
+
raise RuntimeError, "Missing project or config from '--from=project/config' option"
|
26
|
+
end
|
27
|
+
|
28
|
+
project_and_config_flags = "-p #{project.shellescape} -c #{config.shellescape}"
|
29
|
+
end
|
30
|
+
|
31
|
+
secret_names = secrets.collect { |s| s.split("/").last }
|
32
|
+
|
33
|
+
items = `doppler secrets get #{secret_names.map(&:shellescape).join(" ")} --json #{project_and_config_flags}`
|
34
|
+
raise RuntimeError, "Could not read #{secrets} from Doppler" unless $?.success?
|
35
|
+
|
36
|
+
items = JSON.parse(items)
|
37
|
+
|
38
|
+
items.transform_values { |value| value["computed"] }
|
39
|
+
end
|
40
|
+
|
41
|
+
def service_token_set?
|
42
|
+
ENV["DOPPLER_TOKEN"] && ENV["DOPPLER_TOKEN"][0, 5] == "dp.st"
|
43
|
+
end
|
44
|
+
|
45
|
+
def check_dependencies!
|
46
|
+
raise RuntimeError, "Doppler CLI is not installed" unless cli_installed?
|
47
|
+
end
|
48
|
+
|
49
|
+
def cli_installed?
|
50
|
+
`doppler --version 2> /dev/null`
|
51
|
+
$?.success?
|
52
|
+
end
|
53
|
+
end
|
@@ -27,4 +27,13 @@ class Kamal::Secrets::Adapters::LastPass < Kamal::Secrets::Adapters::Base
|
|
27
27
|
end
|
28
28
|
end
|
29
29
|
end
|
30
|
+
|
31
|
+
def check_dependencies!
|
32
|
+
raise RuntimeError, "LastPass CLI is not installed" unless cli_installed?
|
33
|
+
end
|
34
|
+
|
35
|
+
def cli_installed?
|
36
|
+
`lpass --version 2> /dev/null`
|
37
|
+
$?.success?
|
38
|
+
end
|
30
39
|
end
|
@@ -58,4 +58,13 @@ class Kamal::Secrets::Adapters::OnePassword < Kamal::Secrets::Adapters::Base
|
|
58
58
|
raise RuntimeError, "Could not read #{fields.join(", ")} from #{item} in the #{vault} 1Password vault" unless $?.success?
|
59
59
|
end
|
60
60
|
end
|
61
|
+
|
62
|
+
def check_dependencies!
|
63
|
+
raise RuntimeError, "1Password CLI is not installed" unless cli_installed?
|
64
|
+
end
|
65
|
+
|
66
|
+
def cli_installed?
|
67
|
+
`op --version 2> /dev/null`
|
68
|
+
$?.success?
|
69
|
+
end
|
61
70
|
end
|
data/lib/kamal/secrets.rb
CHANGED
@@ -1,13 +1,10 @@
|
|
1
1
|
require "dotenv"
|
2
2
|
|
3
3
|
class Kamal::Secrets
|
4
|
-
attr_reader :secrets_files
|
5
|
-
|
6
4
|
Kamal::Secrets::Dotenv::InlineCommandSubstitution.install!
|
7
5
|
|
8
6
|
def initialize(destination: nil)
|
9
|
-
@
|
10
|
-
[ ".kamal/secrets-common", ".kamal/secrets#{(".#{destination}" if destination)}" ].select { |f| File.exist?(f) }
|
7
|
+
@destination = destination
|
11
8
|
@mutex = Mutex.new
|
12
9
|
end
|
13
10
|
|
@@ -17,10 +14,10 @@ class Kamal::Secrets
|
|
17
14
|
secrets.fetch(key)
|
18
15
|
end
|
19
16
|
rescue KeyError
|
20
|
-
if secrets_files
|
17
|
+
if secrets_files.present?
|
21
18
|
raise Kamal::ConfigurationError, "Secret '#{key}' not found in #{secrets_files.join(", ")}"
|
22
19
|
else
|
23
|
-
raise Kamal::ConfigurationError, "Secret '#{key}' not found, no secret files provided"
|
20
|
+
raise Kamal::ConfigurationError, "Secret '#{key}' not found, no secret files (#{secrets_filenames.join(", ")}) provided"
|
24
21
|
end
|
25
22
|
end
|
26
23
|
|
@@ -28,10 +25,18 @@ class Kamal::Secrets
|
|
28
25
|
secrets
|
29
26
|
end
|
30
27
|
|
28
|
+
def secrets_files
|
29
|
+
@secrets_files ||= secrets_filenames.select { |f| File.exist?(f) }
|
30
|
+
end
|
31
|
+
|
31
32
|
private
|
32
33
|
def secrets
|
33
34
|
@secrets ||= secrets_files.inject({}) do |secrets, secrets_file|
|
34
|
-
secrets.merge!(::Dotenv.parse(secrets_file))
|
35
|
+
secrets.merge!(::Dotenv.parse(secrets_file, overwrite: true))
|
35
36
|
end
|
36
37
|
end
|
38
|
+
|
39
|
+
def secrets_filenames
|
40
|
+
[ ".kamal/secrets-common", ".kamal/secrets#{(".#{@destination}" if @destination)}" ]
|
41
|
+
end
|
37
42
|
end
|
data/lib/kamal/utils.rb
CHANGED
data/lib/kamal/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: kamal
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- David Heinemeier Hansson
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-12-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -50,14 +50,14 @@ dependencies:
|
|
50
50
|
requirements:
|
51
51
|
- - "~>"
|
52
52
|
- !ruby/object:Gem::Version
|
53
|
-
version: '7.
|
53
|
+
version: '7.3'
|
54
54
|
type: :runtime
|
55
55
|
prerelease: false
|
56
56
|
version_requirements: !ruby/object:Gem::Requirement
|
57
57
|
requirements:
|
58
58
|
- - "~>"
|
59
59
|
- !ruby/object:Gem::Version
|
60
|
-
version: '7.
|
60
|
+
version: '7.3'
|
61
61
|
- !ruby/object:Gem::Dependency
|
62
62
|
name: thor
|
63
63
|
requirement: !ruby/object:Gem::Requirement
|
@@ -90,16 +90,22 @@ dependencies:
|
|
90
90
|
name: zeitwerk
|
91
91
|
requirement: !ruby/object:Gem::Requirement
|
92
92
|
requirements:
|
93
|
-
- - "
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: 2.6.18
|
96
|
+
- - "<"
|
94
97
|
- !ruby/object:Gem::Version
|
95
|
-
version: '
|
98
|
+
version: '3.0'
|
96
99
|
type: :runtime
|
97
100
|
prerelease: false
|
98
101
|
version_requirements: !ruby/object:Gem::Requirement
|
99
102
|
requirements:
|
100
|
-
- - "
|
103
|
+
- - ">="
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
version: 2.6.18
|
106
|
+
- - "<"
|
101
107
|
- !ruby/object:Gem::Version
|
102
|
-
version: '
|
108
|
+
version: '3.0'
|
103
109
|
- !ruby/object:Gem::Dependency
|
104
110
|
name: ed25519
|
105
111
|
requirement: !ruby/object:Gem::Requirement
|
@@ -241,6 +247,7 @@ files:
|
|
241
247
|
- lib/kamal/commander/specifics.rb
|
242
248
|
- lib/kamal/commands.rb
|
243
249
|
- lib/kamal/commands/accessory.rb
|
250
|
+
- lib/kamal/commands/accessory/proxy.rb
|
244
251
|
- lib/kamal/commands/app.rb
|
245
252
|
- lib/kamal/commands/app/assets.rb
|
246
253
|
- lib/kamal/commands/app/containers.rb
|
@@ -306,11 +313,14 @@ files:
|
|
306
313
|
- lib/kamal/git.rb
|
307
314
|
- lib/kamal/secrets.rb
|
308
315
|
- lib/kamal/secrets/adapters.rb
|
316
|
+
- lib/kamal/secrets/adapters/aws_secrets_manager.rb
|
309
317
|
- lib/kamal/secrets/adapters/base.rb
|
310
318
|
- lib/kamal/secrets/adapters/bitwarden.rb
|
319
|
+
- lib/kamal/secrets/adapters/doppler.rb
|
311
320
|
- lib/kamal/secrets/adapters/last_pass.rb
|
312
321
|
- lib/kamal/secrets/adapters/one_password.rb
|
313
322
|
- lib/kamal/secrets/adapters/test.rb
|
323
|
+
- lib/kamal/secrets/adapters/test_optional_account.rb
|
314
324
|
- lib/kamal/secrets/dotenv/inline_command_substitution.rb
|
315
325
|
- lib/kamal/sshkit_with_ext.rb
|
316
326
|
- lib/kamal/tags.rb
|