kamal 2.3.0 → 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/secrets.rb +9 -3
- data/lib/kamal/cli/templates/deploy.yml +5 -2
- data/lib/kamal/commands/accessory/proxy.rb +16 -0
- data/lib/kamal/commands/accessory.rb +5 -1
- 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 +6 -2
- data/lib/kamal/configuration/accessory.rb +18 -3
- data/lib/kamal/configuration/builder.rb +4 -0
- data/lib/kamal/configuration/docs/accessory.yml +6 -2
- data/lib/kamal/configuration/docs/alias.yml +2 -2
- data/lib/kamal/configuration/docs/builder.yml +6 -0
- data/lib/kamal/configuration/docs/proxy.yml +13 -10
- data/lib/kamal/configuration/docs/registry.yml +4 -0
- data/lib/kamal/configuration.rb +2 -2
- data/lib/kamal/secrets/adapters/aws_secrets_manager.rb +42 -0
- data/lib/kamal/secrets/adapters/base.rb +8 -1
- data/lib/kamal/secrets/adapters/doppler.rb +53 -0
- data/lib/kamal/secrets/adapters/test_optional_account.rb +5 -0
- data/lib/kamal/secrets.rb +1 -1
- data/lib/kamal/version.rb +1 -1
- metadata +6 -2
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/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
|
|
@@ -16,8 +16,8 @@ servers:
|
|
16
16
|
# Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server.
|
17
17
|
# Remove this section when using multiple web servers and ensure you terminate SSL at your load balancer.
|
18
18
|
#
|
19
|
-
# Note: If using Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption.
|
20
|
-
proxy:
|
19
|
+
# Note: If using Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption.
|
20
|
+
proxy:
|
21
21
|
ssl: true
|
22
22
|
host: app.example.com
|
23
23
|
# Proxy connects to your container on port 80 by default.
|
@@ -36,6 +36,9 @@ registry:
|
|
36
36
|
# Configure builder setup.
|
37
37
|
builder:
|
38
38
|
arch: amd64
|
39
|
+
# Pass in additional build args needed for your Dockerfile.
|
40
|
+
# args:
|
41
|
+
# RUBY_VERSION: <%= File.read('.ruby-version').strip %>
|
39
42
|
|
40
43
|
# Inject ENV variables into containers (secrets come from .kamal/secrets).
|
41
44
|
#
|
@@ -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
6
|
:network_args, :publish_args, :env_args, :volume_args, :label_args, :option_args,
|
5
|
-
:secrets_io, :secrets_path, :env_directory,
|
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)
|
@@ -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, :provenance, :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, *builder_provenance ]
|
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
|
@@ -101,6 +101,10 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
|
|
101
101
|
argumentize "--provenance", provenance unless provenance.nil?
|
102
102
|
end
|
103
103
|
|
104
|
+
def builder_sbom
|
105
|
+
argumentize "--sbom", sbom unless sbom.nil?
|
106
|
+
end
|
107
|
+
|
104
108
|
def builder_config
|
105
109
|
config.builder
|
106
110
|
end
|
@@ -5,7 +5,7 @@ class Kamal::Configuration::Accessory
|
|
5
5
|
|
6
6
|
delegate :argumentize, :optionize, to: Kamal::Utils
|
7
7
|
|
8
|
-
attr_reader :name, :accessory_config, :env
|
8
|
+
attr_reader :name, :accessory_config, :env, :proxy
|
9
9
|
|
10
10
|
def initialize(name, config:)
|
11
11
|
@name, @config, @accessory_config = name.inquiry, config, config.raw_config["accessories"][name]
|
@@ -20,6 +20,8 @@ class Kamal::Configuration::Accessory
|
|
20
20
|
config: accessory_config.fetch("env", {}),
|
21
21
|
secrets: config.secrets,
|
22
22
|
context: "accessories/#{name}/env"
|
23
|
+
|
24
|
+
initialize_proxy if running_proxy?
|
23
25
|
end
|
24
26
|
|
25
27
|
def service_name
|
@@ -106,6 +108,17 @@ class Kamal::Configuration::Accessory
|
|
106
108
|
accessory_config["cmd"]
|
107
109
|
end
|
108
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
|
+
|
109
122
|
private
|
110
123
|
attr_accessor :config
|
111
124
|
|
@@ -129,7 +142,7 @@ class Kamal::Configuration::Accessory
|
|
129
142
|
end
|
130
143
|
|
131
144
|
def read_dynamic_file(local_file)
|
132
|
-
StringIO.new(ERB.new(
|
145
|
+
StringIO.new(ERB.new(File.read(local_file)).result)
|
133
146
|
end
|
134
147
|
|
135
148
|
def expand_remote_file(remote_file)
|
@@ -176,7 +189,9 @@ class Kamal::Configuration::Accessory
|
|
176
189
|
|
177
190
|
def hosts_from_roles
|
178
191
|
if accessory_config.key?("roles")
|
179
|
-
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
|
180
195
|
end
|
181
196
|
end
|
182
197
|
|
@@ -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
|
@@ -98,3 +98,7 @@ accessories:
|
|
98
98
|
# Defaults to kamal:
|
99
99
|
network: custom
|
100
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
|
@@ -108,3 +108,9 @@ builder:
|
|
108
108
|
# It is used to configure provenance attestations for the build result.
|
109
109
|
# The value can also be a boolean to enable or disable provenance attestations.
|
110
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
|
@@ -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,13 +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
|
+
|
5
7
|
check_dependencies!
|
8
|
+
|
6
9
|
session = login(account)
|
7
10
|
full_secrets = secrets.map { |secret| [ from, secret ].compact.join("/") }
|
8
11
|
fetch_secrets(full_secrets, account: account, session: session)
|
9
12
|
end
|
10
13
|
|
14
|
+
def requires_account?
|
15
|
+
true
|
16
|
+
end
|
17
|
+
|
11
18
|
private
|
12
19
|
def login(...)
|
13
20
|
raise NotImplementedError
|
@@ -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
|
data/lib/kamal/secrets.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
|
@@ -247,6 +247,7 @@ files:
|
|
247
247
|
- lib/kamal/commander/specifics.rb
|
248
248
|
- lib/kamal/commands.rb
|
249
249
|
- lib/kamal/commands/accessory.rb
|
250
|
+
- lib/kamal/commands/accessory/proxy.rb
|
250
251
|
- lib/kamal/commands/app.rb
|
251
252
|
- lib/kamal/commands/app/assets.rb
|
252
253
|
- lib/kamal/commands/app/containers.rb
|
@@ -312,11 +313,14 @@ files:
|
|
312
313
|
- lib/kamal/git.rb
|
313
314
|
- lib/kamal/secrets.rb
|
314
315
|
- lib/kamal/secrets/adapters.rb
|
316
|
+
- lib/kamal/secrets/adapters/aws_secrets_manager.rb
|
315
317
|
- lib/kamal/secrets/adapters/base.rb
|
316
318
|
- lib/kamal/secrets/adapters/bitwarden.rb
|
319
|
+
- lib/kamal/secrets/adapters/doppler.rb
|
317
320
|
- lib/kamal/secrets/adapters/last_pass.rb
|
318
321
|
- lib/kamal/secrets/adapters/one_password.rb
|
319
322
|
- lib/kamal/secrets/adapters/test.rb
|
323
|
+
- lib/kamal/secrets/adapters/test_optional_account.rb
|
320
324
|
- lib/kamal/secrets/dotenv/inline_command_substitution.rb
|
321
325
|
- lib/kamal/sshkit_with_ext.rb
|
322
326
|
- lib/kamal/tags.rb
|