kamal 2.8.1 → 2.9.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 +6 -3
- data/lib/kamal/cli/app.rb +17 -10
- data/lib/kamal/cli/build/clone.rb +0 -2
- data/lib/kamal/cli/build/port_forwarding.rb +66 -0
- data/lib/kamal/cli/build.rb +43 -26
- data/lib/kamal/cli/main.rb +2 -1
- data/lib/kamal/cli/proxy.rb +2 -1
- data/lib/kamal/cli/registry.rb +22 -0
- data/lib/kamal/cli/server.rb +2 -1
- data/lib/kamal/cli/templates/secrets +1 -0
- data/lib/kamal/commander.rb +1 -0
- data/lib/kamal/commands/app/execution.rb +7 -1
- data/lib/kamal/commands/builder/base.rb +10 -0
- data/lib/kamal/commands/builder/hybrid.rb +3 -3
- data/lib/kamal/commands/builder/local.rb +2 -9
- data/lib/kamal/commands/builder/remote.rb +6 -2
- data/lib/kamal/configuration/builder.rb +10 -3
- data/lib/kamal/configuration/docs/configuration.yml +6 -0
- data/lib/kamal/configuration/docs/registry.yml +12 -4
- data/lib/kamal/configuration/docs/ssh.yml +1 -1
- data/lib/kamal/configuration/docs/sshkit.yml +8 -0
- data/lib/kamal/configuration/role.rb +1 -1
- data/lib/kamal/configuration/ssh.rb +5 -1
- data/lib/kamal/configuration/sshkit.rb +4 -0
- data/lib/kamal/configuration/validator.rb +14 -1
- data/lib/kamal/configuration.rb +16 -1
- data/lib/kamal/secrets/adapters/passbolt.rb +1 -2
- data/lib/kamal/secrets.rb +3 -2
- data/lib/kamal/sshkit_with_ext.rb +72 -10
- data/lib/kamal/version.rb +1 -1
- data/lib/kamal.rb +1 -0
- metadata +2 -2
- data/lib/kamal/cli/port_forwarding.rb +0 -55
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bd7c6e6e18ca7cc95712642dfdb5ec45c33e06b6a02750e5ee7a3f1ac3428ea2
|
|
4
|
+
data.tar.gz: 1a8acfe5ec44636827ca70da1263289e6082359c58601acce9870d1d46ba750b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d1fb62392ddafc5b1fe6b54055121654bd73625eaf9808848ed60d7d40fdadf2938675b0bc881fca1297ee089f78b3768eefb96f5c10e9e8346265f20d9663e2
|
|
7
|
+
data.tar.gz: 6fc4f87a3ea64ebdee5c3c666cdafa5b15a0f7b243f575b402d82a2c8eec5b3802a05c0b5a80f6856eb92c24647b65a7bbdb8a68fd208dc873f1cd55d725894e
|
data/lib/kamal/cli/accessory.rb
CHANGED
|
@@ -128,12 +128,13 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
|
|
|
128
128
|
|
|
129
129
|
desc "details [NAME]", "Show details about accessory on host (use NAME=all to show all accessories)"
|
|
130
130
|
def details(name)
|
|
131
|
+
quiet = options[:quiet]
|
|
131
132
|
if name == "all"
|
|
132
133
|
KAMAL.accessory_names.each { |accessory_name| details(accessory_name) }
|
|
133
134
|
else
|
|
134
135
|
type = "Accessory #{name}"
|
|
135
136
|
with_accessory(name) do |accessory, hosts|
|
|
136
|
-
on(hosts) { puts_by_host host, capture_with_info(*accessory.info), type: type }
|
|
137
|
+
on(hosts) { puts_by_host host, capture_with_info(*accessory.info), type: type, quiet: quiet }
|
|
137
138
|
end
|
|
138
139
|
end
|
|
139
140
|
end
|
|
@@ -145,6 +146,8 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
|
|
|
145
146
|
pre_connect_if_required
|
|
146
147
|
|
|
147
148
|
cmd = Kamal::Utils.join_commands(cmd)
|
|
149
|
+
quiet = options[:quiet]
|
|
150
|
+
|
|
148
151
|
with_accessory(name) do |accessory, hosts|
|
|
149
152
|
case
|
|
150
153
|
when options[:interactive] && options[:reuse]
|
|
@@ -160,7 +163,7 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
|
|
|
160
163
|
say "Launching command from existing container...", :magenta
|
|
161
164
|
on(hosts) do |host|
|
|
162
165
|
execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on #{name} accessory"), verbosity: :debug
|
|
163
|
-
puts_by_host host, capture_with_info(*accessory.execute_in_existing_container(cmd))
|
|
166
|
+
puts_by_host host, capture_with_info(*accessory.execute_in_existing_container(cmd)), quiet: quiet
|
|
164
167
|
end
|
|
165
168
|
|
|
166
169
|
else
|
|
@@ -168,7 +171,7 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
|
|
|
168
171
|
on(hosts) do |host|
|
|
169
172
|
execute *KAMAL.registry.login
|
|
170
173
|
execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on #{name} accessory"), verbosity: :debug
|
|
171
|
-
puts_by_host host, capture_with_info(*accessory.execute_in_new_container(cmd))
|
|
174
|
+
puts_by_host host, capture_with_info(*accessory.execute_in_new_container(cmd)), quiet: quiet
|
|
172
175
|
end
|
|
173
176
|
end
|
|
174
177
|
end
|
data/lib/kamal/cli/app.rb
CHANGED
|
@@ -92,11 +92,12 @@ class Kamal::Cli::App < Kamal::Cli::Base
|
|
|
92
92
|
# FIXME: Drop in favor of just containers?
|
|
93
93
|
desc "details", "Show details about app containers"
|
|
94
94
|
def details
|
|
95
|
+
quiet = options[:quiet]
|
|
95
96
|
on(KAMAL.app_hosts) do |host|
|
|
96
97
|
roles = KAMAL.roles_on(host)
|
|
97
98
|
|
|
98
99
|
roles.each do |role|
|
|
99
|
-
puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).info)
|
|
100
|
+
puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).info), quiet: quiet
|
|
100
101
|
end
|
|
101
102
|
end
|
|
102
103
|
end
|
|
@@ -120,6 +121,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
|
|
|
120
121
|
cmd = Kamal::Utils.join_commands(cmd)
|
|
121
122
|
env = options[:env]
|
|
122
123
|
detach = options[:detach]
|
|
124
|
+
quiet = options[:quiet]
|
|
123
125
|
case
|
|
124
126
|
when options[:interactive] && options[:reuse]
|
|
125
127
|
say "Get current version of running container...", :magenta unless options[:version]
|
|
@@ -148,7 +150,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
|
|
|
148
150
|
|
|
149
151
|
roles.each do |role|
|
|
150
152
|
execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on app version #{version}", role: role), verbosity: :debug
|
|
151
|
-
puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).execute_in_existing_container(cmd, env: env))
|
|
153
|
+
puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).execute_in_existing_container(cmd, env: env)), quiet: quiet
|
|
152
154
|
end
|
|
153
155
|
end
|
|
154
156
|
end
|
|
@@ -164,7 +166,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
|
|
|
164
166
|
|
|
165
167
|
roles.each do |role|
|
|
166
168
|
execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on app version #{version}"), verbosity: :debug
|
|
167
|
-
puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).execute_in_new_container(cmd, env: env, detach: detach))
|
|
169
|
+
puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).execute_in_new_container(cmd, env: env, detach: detach)), quiet: quiet
|
|
168
170
|
end
|
|
169
171
|
end
|
|
170
172
|
end
|
|
@@ -173,12 +175,14 @@ class Kamal::Cli::App < Kamal::Cli::Base
|
|
|
173
175
|
|
|
174
176
|
desc "containers", "Show app containers on servers"
|
|
175
177
|
def containers
|
|
176
|
-
|
|
178
|
+
quiet = options[:quiet]
|
|
179
|
+
on(KAMAL.app_hosts) { |host| puts_by_host host, capture_with_info(*KAMAL.app.list_containers), quiet: quiet }
|
|
177
180
|
end
|
|
178
181
|
|
|
179
182
|
desc "stale_containers", "Detect app stale containers"
|
|
180
183
|
option :stop, aliases: "-s", type: :boolean, default: false, desc: "Stop the stale containers found"
|
|
181
184
|
def stale_containers
|
|
185
|
+
quiet = options[:quiet]
|
|
182
186
|
stop = options[:stop]
|
|
183
187
|
|
|
184
188
|
with_lock_if_stopping do
|
|
@@ -192,10 +196,10 @@ class Kamal::Cli::App < Kamal::Cli::Base
|
|
|
192
196
|
|
|
193
197
|
versions.each do |version|
|
|
194
198
|
if stop
|
|
195
|
-
puts_by_host host, "Stopping stale container for role #{role} with version #{version}"
|
|
199
|
+
puts_by_host host, "Stopping stale container for role #{role} with version #{version}", quiet: quiet
|
|
196
200
|
execute *app.stop(version: version), raise_on_non_zero_exit: false
|
|
197
201
|
else
|
|
198
|
-
puts_by_host host, "Detected stale container for role #{role} with version #{version} (use `kamal app stale_containers --stop` to stop)"
|
|
202
|
+
puts_by_host host, "Detected stale container for role #{role} with version #{version} (use `kamal app stale_containers --stop` to stop)", quiet: quiet
|
|
199
203
|
end
|
|
200
204
|
end
|
|
201
205
|
end
|
|
@@ -205,7 +209,8 @@ class Kamal::Cli::App < Kamal::Cli::Base
|
|
|
205
209
|
|
|
206
210
|
desc "images", "Show app images on servers"
|
|
207
211
|
def images
|
|
208
|
-
|
|
212
|
+
quiet = options[:quiet]
|
|
213
|
+
on(KAMAL.app_hosts) { |host| puts_by_host host, capture_with_info(*KAMAL.app.list_images), quiet: quiet }
|
|
209
214
|
end
|
|
210
215
|
|
|
211
216
|
desc "logs", "Show log lines from app on servers (use --help to show options)"
|
|
@@ -224,6 +229,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
|
|
|
224
229
|
since = options[:since]
|
|
225
230
|
container_id = options[:container_id]
|
|
226
231
|
timestamps = !options[:skip_timestamps]
|
|
232
|
+
quiet = options[:quiet]
|
|
227
233
|
|
|
228
234
|
if options[:follow]
|
|
229
235
|
lines = options[:lines].presence || ((since || grep) ? nil : 10) # Default to 10 lines if since or grep isn't set
|
|
@@ -246,9 +252,9 @@ class Kamal::Cli::App < Kamal::Cli::Base
|
|
|
246
252
|
|
|
247
253
|
roles.each do |role|
|
|
248
254
|
begin
|
|
249
|
-
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))
|
|
255
|
+
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)), quiet: quiet
|
|
250
256
|
rescue SSHKit::Command::Failed
|
|
251
|
-
puts_by_host host, "Nothing found"
|
|
257
|
+
puts_by_host host, "Nothing found", quiet: quiet
|
|
252
258
|
end
|
|
253
259
|
end
|
|
254
260
|
end
|
|
@@ -352,9 +358,10 @@ class Kamal::Cli::App < Kamal::Cli::Base
|
|
|
352
358
|
|
|
353
359
|
desc "version", "Show app version currently running on servers"
|
|
354
360
|
def version
|
|
361
|
+
quiet = options[:quiet]
|
|
355
362
|
on(KAMAL.app_hosts) do |host|
|
|
356
363
|
role = KAMAL.roles_on(host).first
|
|
357
|
-
puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).current_running_version).strip
|
|
364
|
+
puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).current_running_version).strip, quiet: quiet
|
|
358
365
|
end
|
|
359
366
|
end
|
|
360
367
|
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
require "concurrent/atomic/count_down_latch"
|
|
2
|
+
|
|
3
|
+
class Kamal::Cli::Build::PortForwarding
|
|
4
|
+
attr_reader :hosts, :port, :ssh_options
|
|
5
|
+
|
|
6
|
+
def initialize(hosts, port, **ssh_options)
|
|
7
|
+
@hosts = hosts
|
|
8
|
+
@port = port
|
|
9
|
+
@ssh_options = ssh_options
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def forward
|
|
13
|
+
@done = false
|
|
14
|
+
forward_ports
|
|
15
|
+
|
|
16
|
+
yield
|
|
17
|
+
ensure
|
|
18
|
+
stop
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
def stop
|
|
23
|
+
@done = true
|
|
24
|
+
@threads.to_a.each(&:join)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def forward_ports
|
|
28
|
+
ready = Concurrent::CountDownLatch.new(hosts.size)
|
|
29
|
+
|
|
30
|
+
@threads = hosts.map do |host|
|
|
31
|
+
Thread.new do
|
|
32
|
+
begin
|
|
33
|
+
Net::SSH.start(host, ssh_options[:user], **ssh_options.except(:user)) do |ssh|
|
|
34
|
+
ssh.forward.remote(port, "localhost", port, "127.0.0.1") do |remote_port, bind_address|
|
|
35
|
+
if remote_port == :error
|
|
36
|
+
raise "Failed to establish port forward on #{host}"
|
|
37
|
+
else
|
|
38
|
+
ready.count_down
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
ssh.loop(0.1) do
|
|
43
|
+
if @done
|
|
44
|
+
ssh.forward.cancel_remote(port, "127.0.0.1")
|
|
45
|
+
break
|
|
46
|
+
else
|
|
47
|
+
true
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
rescue Exception => e
|
|
52
|
+
error "Error setting up port forwarding to #{host}: #{e.class}: #{e.message}"
|
|
53
|
+
error e.backtrace.join("\n")
|
|
54
|
+
|
|
55
|
+
raise
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
raise "Timed out waiting for port forwarding to be established" unless ready.wait(30)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def error(message)
|
|
64
|
+
SSHKit.config.output.error(message)
|
|
65
|
+
end
|
|
66
|
+
end
|
data/lib/kamal/cli/build.rb
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
require "uri"
|
|
2
|
-
|
|
3
1
|
class Kamal::Cli::Build < Kamal::Cli::Base
|
|
4
2
|
class BuildError < StandardError; end
|
|
5
3
|
|
|
@@ -38,29 +36,31 @@ class Kamal::Cli::Build < Kamal::Cli::Base
|
|
|
38
36
|
say "Building with uncommitted changes:\n #{uncommitted_changes}", :yellow
|
|
39
37
|
end
|
|
40
38
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
39
|
+
forward_local_registry_port_for_remote_builder do
|
|
40
|
+
with_env(KAMAL.config.builder.secrets) do
|
|
41
|
+
run_locally do
|
|
42
|
+
begin
|
|
43
|
+
execute *KAMAL.builder.inspect_builder
|
|
44
|
+
rescue SSHKit::Command::Failed => e
|
|
45
|
+
if e.message =~ /(context not found|no builder|no compatible builder|does not exist)/
|
|
46
|
+
warn "Missing compatible builder, so creating a new one first"
|
|
47
|
+
begin
|
|
48
|
+
cli.remove
|
|
49
|
+
rescue SSHKit::Command::Failed
|
|
50
|
+
raise unless e.message =~ /(context not found|no builder|does not exist)/
|
|
51
|
+
end
|
|
52
|
+
cli.create
|
|
53
|
+
else
|
|
54
|
+
raise
|
|
52
55
|
end
|
|
53
|
-
cli.create
|
|
54
|
-
else
|
|
55
|
-
raise
|
|
56
56
|
end
|
|
57
|
-
end
|
|
58
57
|
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
# Get the command here to ensure the Dir.chdir doesn't interfere with it
|
|
59
|
+
push = KAMAL.builder.push(cli.options[:output], no_cache: cli.options[:no_cache])
|
|
61
60
|
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
KAMAL.with_verbosity(:debug) do
|
|
62
|
+
Dir.chdir(KAMAL.config.builder.build_directory) { execute *push, env: KAMAL.builder.push_env }
|
|
63
|
+
end
|
|
64
64
|
end
|
|
65
65
|
end
|
|
66
66
|
end
|
|
@@ -70,7 +70,7 @@ class Kamal::Cli::Build < Kamal::Cli::Base
|
|
|
70
70
|
def pull
|
|
71
71
|
login_to_registry_remotely unless KAMAL.registry.local?
|
|
72
72
|
|
|
73
|
-
forward_local_registry_port do
|
|
73
|
+
forward_local_registry_port(KAMAL.hosts, **KAMAL.config.ssh.options) do
|
|
74
74
|
if (first_hosts = mirror_hosts).any?
|
|
75
75
|
# Pull on a single host per mirror first to seed them
|
|
76
76
|
say "Pulling image on #{first_hosts.join(", ")} to seed the #{"mirror".pluralize(first_hosts.count)}...", :magenta
|
|
@@ -210,13 +210,30 @@ class Kamal::Cli::Build < Kamal::Cli::Base
|
|
|
210
210
|
end
|
|
211
211
|
end
|
|
212
212
|
|
|
213
|
-
def
|
|
213
|
+
def forward_local_registry_port_for_remote_builder(&block)
|
|
214
|
+
if KAMAL.builder.remote?
|
|
215
|
+
remote_uri = URI(KAMAL.config.builder.remote)
|
|
216
|
+
forward_local_registry_port([ remote_uri.host ], **remote_builder_ssh_options(remote_uri), &block)
|
|
217
|
+
else
|
|
218
|
+
yield
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def forward_local_registry_port(hosts, **ssh_options, &block)
|
|
214
223
|
if KAMAL.config.registry.local?
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
forward(&block)
|
|
224
|
+
say "Setting up local registry port forwarding to #{hosts.join(', ')}..."
|
|
225
|
+
PortForwarding.new(hosts, KAMAL.config.registry.local_port, **ssh_options).forward(&block)
|
|
218
226
|
else
|
|
219
227
|
yield
|
|
220
228
|
end
|
|
221
229
|
end
|
|
230
|
+
|
|
231
|
+
def remote_builder_ssh_options(remote_uri)
|
|
232
|
+
{ user: remote_uri.user,
|
|
233
|
+
port: remote_uri.port,
|
|
234
|
+
keepalive: KAMAL.config.ssh.options[:keepalive],
|
|
235
|
+
keepalive_interval: KAMAL.config.ssh.options[:keepalive_interval],
|
|
236
|
+
logger: KAMAL.config.ssh.options[:logger]
|
|
237
|
+
}.compact
|
|
238
|
+
end
|
|
222
239
|
end
|
data/lib/kamal/cli/main.rb
CHANGED
|
@@ -112,8 +112,9 @@ class Kamal::Cli::Main < Kamal::Cli::Base
|
|
|
112
112
|
|
|
113
113
|
desc "audit", "Show audit log from servers"
|
|
114
114
|
def audit
|
|
115
|
+
quiet = options[:quiet]
|
|
115
116
|
on(KAMAL.hosts) do |host|
|
|
116
|
-
puts_by_host host, capture_with_info(*KAMAL.auditor.reveal)
|
|
117
|
+
puts_by_host host, capture_with_info(*KAMAL.auditor.reveal), quiet: quiet
|
|
117
118
|
end
|
|
118
119
|
end
|
|
119
120
|
|
data/lib/kamal/cli/proxy.rb
CHANGED
|
@@ -197,7 +197,8 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
|
|
|
197
197
|
|
|
198
198
|
desc "details", "Show details about proxy container from servers"
|
|
199
199
|
def details
|
|
200
|
-
|
|
200
|
+
quiet = options[:quiet]
|
|
201
|
+
on(KAMAL.proxy_hosts) { |host| puts_by_host host, capture_with_info(*KAMAL.proxy.info), type: "Proxy", quiet: quiet }
|
|
201
202
|
end
|
|
202
203
|
|
|
203
204
|
desc "logs", "Show log lines from proxy on servers"
|
data/lib/kamal/cli/registry.rb
CHANGED
|
@@ -24,4 +24,26 @@ class Kamal::Cli::Registry < Kamal::Cli::Base
|
|
|
24
24
|
on(KAMAL.hosts) { execute *KAMAL.registry.logout } unless options[:skip_remote]
|
|
25
25
|
end
|
|
26
26
|
end
|
|
27
|
+
|
|
28
|
+
desc "login", "Log in to remote registry locally and remotely"
|
|
29
|
+
option :skip_local, aliases: "-L", type: :boolean, default: false, desc: "Skip local login"
|
|
30
|
+
option :skip_remote, aliases: "-R", type: :boolean, default: false, desc: "Skip remote login"
|
|
31
|
+
def login
|
|
32
|
+
if KAMAL.registry.local?
|
|
33
|
+
raise "Cannot use login command with a local registry. Use `kamal registry setup` instead."
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
setup
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
desc "logout", "Log out of remote registry locally and remotely"
|
|
40
|
+
option :skip_local, aliases: "-L", type: :boolean, default: false, desc: "Skip local login"
|
|
41
|
+
option :skip_remote, aliases: "-R", type: :boolean, default: false, desc: "Skip remote login"
|
|
42
|
+
def logout
|
|
43
|
+
if KAMAL.registry.local?
|
|
44
|
+
raise "Cannot use logout command with a local registry. Use `kamal registry remove` instead."
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
remove
|
|
48
|
+
end
|
|
27
49
|
end
|
data/lib/kamal/cli/server.rb
CHANGED
|
@@ -6,6 +6,7 @@ class Kamal::Cli::Server < Kamal::Cli::Base
|
|
|
6
6
|
|
|
7
7
|
cmd = Kamal::Utils.join_commands(cmd)
|
|
8
8
|
hosts = KAMAL.hosts
|
|
9
|
+
quiet = options[:quiet]
|
|
9
10
|
|
|
10
11
|
case
|
|
11
12
|
when options[:interactive]
|
|
@@ -19,7 +20,7 @@ class Kamal::Cli::Server < Kamal::Cli::Base
|
|
|
19
20
|
|
|
20
21
|
on(hosts) do |host|
|
|
21
22
|
execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on #{host}"), verbosity: :debug
|
|
22
|
-
puts_by_host host, capture_with_info(cmd)
|
|
23
|
+
puts_by_host host, capture_with_info(cmd), quiet: quiet
|
|
23
24
|
end
|
|
24
25
|
end
|
|
25
26
|
end
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
# Option 2: Read secrets via a command
|
|
9
9
|
# RAILS_MASTER_KEY=$(cat config/master.key)
|
|
10
|
+
# KAMAL_REGISTRY_PASSWORD=$(rails credentials:fetch kamal.registry_password)
|
|
10
11
|
|
|
11
12
|
# Option 3: Read secrets via kamal secrets helpers
|
|
12
13
|
# These will handle logging in and fetching the secrets in as few calls as possible
|
data/lib/kamal/commander.rb
CHANGED
|
@@ -155,6 +155,7 @@ class Kamal::Commander
|
|
|
155
155
|
SSHKit::Backend::Netssh.pool.idle_timeout = config.sshkit.pool_idle_timeout
|
|
156
156
|
SSHKit::Backend::Netssh.configure do |sshkit|
|
|
157
157
|
sshkit.max_concurrent_starts = config.sshkit.max_concurrent_starts
|
|
158
|
+
sshkit.dns_retries = config.sshkit.dns_retries
|
|
158
159
|
sshkit.ssh_options = config.ssh.options
|
|
159
160
|
end
|
|
160
161
|
SSHKit.config.command_map[:docker] = "docker" # No need to use /usr/bin/env, just clogs up the logs
|
|
@@ -12,6 +12,7 @@ module Kamal::Commands::App::Execution
|
|
|
12
12
|
(docker_interactive_args if interactive),
|
|
13
13
|
("--detach" if detach),
|
|
14
14
|
("--rm" unless detach),
|
|
15
|
+
"--name", container_name_for_exec,
|
|
15
16
|
"--network", "kamal",
|
|
16
17
|
*role&.env_args(host),
|
|
17
18
|
*argumentize("--env", env),
|
|
@@ -22,11 +23,16 @@ module Kamal::Commands::App::Execution
|
|
|
22
23
|
*command
|
|
23
24
|
end
|
|
24
25
|
|
|
25
|
-
def execute_in_existing_container_over_ssh(*command,
|
|
26
|
+
def execute_in_existing_container_over_ssh(*command, env:)
|
|
26
27
|
run_over_ssh execute_in_existing_container(*command, interactive: true, env: env), host: host
|
|
27
28
|
end
|
|
28
29
|
|
|
29
30
|
def execute_in_new_container_over_ssh(*command, env:)
|
|
30
31
|
run_over_ssh execute_in_new_container(*command, interactive: true, env: env), host: host
|
|
31
32
|
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
def container_name_for_exec
|
|
36
|
+
[ role.container_prefix, "exec", config.version, SecureRandom.hex(3) ].compact.join("-")
|
|
37
|
+
end
|
|
32
38
|
end
|
|
@@ -127,6 +127,16 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
|
|
|
127
127
|
config.builder
|
|
128
128
|
end
|
|
129
129
|
|
|
130
|
+
def registry_config
|
|
131
|
+
config.registry
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def driver_options
|
|
135
|
+
if registry_config.local?
|
|
136
|
+
[ "--driver-opt", "network=host" ]
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
130
140
|
def platform_options(arches)
|
|
131
141
|
argumentize "--platform", arches.map { |arch| "linux/#{arch}" }.join(",") if arches.any?
|
|
132
142
|
end
|
|
@@ -8,14 +8,14 @@ class Kamal::Commands::Builder::Hybrid < Kamal::Commands::Builder::Remote
|
|
|
8
8
|
|
|
9
9
|
private
|
|
10
10
|
def builder_name
|
|
11
|
-
"kamal-hybrid-#{driver}-#{
|
|
11
|
+
"kamal-hybrid-#{driver}-#{remote_builder_name_suffix}"
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def create_local_buildx
|
|
15
|
-
docker :buildx, :create, *platform_options(local_arches), "--name", builder_name, "--driver=#{driver}"
|
|
15
|
+
docker :buildx, :create, *platform_options(local_arches), "--name", builder_name, "--driver=#{driver}", *driver_options
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def append_remote_buildx
|
|
19
|
-
docker :buildx, :create, *platform_options(remote_arches), "--append", "--name", builder_name, remote_context_name
|
|
19
|
+
docker :buildx, :create, *platform_options(remote_arches), "--append", "--name", builder_name, *driver_options, remote_context_name
|
|
20
20
|
end
|
|
21
21
|
end
|
|
@@ -2,14 +2,7 @@ class Kamal::Commands::Builder::Local < Kamal::Commands::Builder::Base
|
|
|
2
2
|
def create
|
|
3
3
|
return if docker_driver?
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
if KAMAL.registry.local?
|
|
7
|
-
"--driver=#{driver} --driver-opt network=host"
|
|
8
|
-
else
|
|
9
|
-
"--driver=#{driver}"
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
docker :buildx, :create, "--name", builder_name, options
|
|
5
|
+
docker :buildx, :create, "--name", builder_name, "--driver=#{driver}", *driver_options
|
|
13
6
|
end
|
|
14
7
|
|
|
15
8
|
def remove
|
|
@@ -18,7 +11,7 @@ class Kamal::Commands::Builder::Local < Kamal::Commands::Builder::Base
|
|
|
18
11
|
|
|
19
12
|
private
|
|
20
13
|
def builder_name
|
|
21
|
-
if
|
|
14
|
+
if registry_config.local?
|
|
22
15
|
"kamal-local-registry-#{driver}"
|
|
23
16
|
else
|
|
24
17
|
"kamal-local-#{driver}"
|
|
@@ -34,13 +34,17 @@ class Kamal::Commands::Builder::Remote < Kamal::Commands::Builder::Base
|
|
|
34
34
|
|
|
35
35
|
private
|
|
36
36
|
def builder_name
|
|
37
|
-
"kamal-remote-#{
|
|
37
|
+
"kamal-remote-#{remote_builder_name_suffix}"
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
def remote_context_name
|
|
41
41
|
"#{builder_name}-context"
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
+
def remote_builder_name_suffix
|
|
45
|
+
"#{remote.gsub(/[^a-z0-9_-]/, "-")}#{registry_config.local? ? "-local-registry" : "" }"
|
|
46
|
+
end
|
|
47
|
+
|
|
44
48
|
def inspect_buildx
|
|
45
49
|
pipe \
|
|
46
50
|
docker(:buildx, :inspect, builder_name),
|
|
@@ -62,7 +66,7 @@ class Kamal::Commands::Builder::Remote < Kamal::Commands::Builder::Base
|
|
|
62
66
|
end
|
|
63
67
|
|
|
64
68
|
def create_buildx
|
|
65
|
-
docker :buildx, :create, "--name", builder_name, remote_context_name
|
|
69
|
+
docker :buildx, :create, "--name", builder_name, *driver_options, remote_context_name
|
|
66
70
|
end
|
|
67
71
|
|
|
68
72
|
def remove_buildx
|
|
@@ -177,8 +177,15 @@ class Kamal::Configuration::Builder
|
|
|
177
177
|
[ server, cache_image ].compact.join("/")
|
|
178
178
|
end
|
|
179
179
|
|
|
180
|
+
def cache_options
|
|
181
|
+
builder_config["cache"]&.fetch("options", nil)
|
|
182
|
+
end
|
|
183
|
+
|
|
180
184
|
def cache_from_config_for_gha
|
|
181
|
-
|
|
185
|
+
individual_options = cache_options&.split(",") || []
|
|
186
|
+
allowed_options = individual_options.select { |option| option =~ /^(url|url_v2|token|scope|timeout)=/ }
|
|
187
|
+
|
|
188
|
+
[ "type=gha", *allowed_options ].compact.join(",")
|
|
182
189
|
end
|
|
183
190
|
|
|
184
191
|
def cache_from_config_for_registry
|
|
@@ -186,11 +193,11 @@ class Kamal::Configuration::Builder
|
|
|
186
193
|
end
|
|
187
194
|
|
|
188
195
|
def cache_to_config_for_gha
|
|
189
|
-
[ "type=gha",
|
|
196
|
+
[ "type=gha", cache_options ].compact.join(",")
|
|
190
197
|
end
|
|
191
198
|
|
|
192
199
|
def cache_to_config_for_registry
|
|
193
|
-
[ "type=registry", "ref=#{cache_image_ref}",
|
|
200
|
+
[ "type=registry", "ref=#{cache_image_ref}", cache_options ].compact.join(",")
|
|
194
201
|
end
|
|
195
202
|
|
|
196
203
|
def repo_basename
|
|
@@ -82,6 +82,12 @@ asset_path: /path/to/assets
|
|
|
82
82
|
# See https://kamal-deploy.org/docs/hooks for more information:
|
|
83
83
|
hooks_path: /user_home/kamal/hooks
|
|
84
84
|
|
|
85
|
+
# Secrets path
|
|
86
|
+
#
|
|
87
|
+
# Path to secrets, defaults to `.kamal/secrets`.
|
|
88
|
+
# Kamal will look for `<secrets_path>-common` and `<secrets_path>` (or `<secrets_path>.<destination>` when using destinations):
|
|
89
|
+
secrets_path: /user_home/kamal/secrets
|
|
90
|
+
|
|
85
91
|
# Error pages
|
|
86
92
|
#
|
|
87
93
|
# A directory relative to the app root to find error pages for the proxy to serve.
|
|
@@ -1,19 +1,27 @@
|
|
|
1
1
|
# Registry
|
|
2
2
|
#
|
|
3
3
|
# The default registry is Docker Hub, but you can change it using `registry/server`.
|
|
4
|
+
|
|
5
|
+
# Using a local container registry
|
|
6
|
+
#
|
|
7
|
+
# If the registry server starts with `localhost`, Kamal will start a local Docker registry
|
|
8
|
+
# on that port and push the app image to it.
|
|
9
|
+
registry:
|
|
10
|
+
server: localhost:5555
|
|
11
|
+
|
|
12
|
+
# Using Docker Hub as the container registry
|
|
4
13
|
#
|
|
5
14
|
# By default, Docker Hub creates public repositories. To avoid making your images public,
|
|
6
15
|
# set up a private repository before deploying, or change the default repository privacy
|
|
7
16
|
# settings to private in your [Docker Hub settings](https://hub.docker.com/repository-settings/default-privacy).
|
|
8
17
|
#
|
|
9
|
-
# A reference to a secret (in this case, `
|
|
18
|
+
# A reference to a secret (in this case, `KAMAL_REGISTRY_PASSWORD`) will look up the secret
|
|
10
19
|
# in the local environment:
|
|
11
20
|
registry:
|
|
12
|
-
server: registry.digitalocean.com
|
|
13
21
|
username:
|
|
14
|
-
-
|
|
22
|
+
- <your docker hub username>
|
|
15
23
|
password:
|
|
16
|
-
-
|
|
24
|
+
- KAMAL_REGISTRY_PASSWORD
|
|
17
25
|
|
|
18
26
|
# Using AWS ECR as the container registry
|
|
19
27
|
#
|
|
@@ -67,4 +67,4 @@ ssh:
|
|
|
67
67
|
# Set to true to load the default OpenSSH config files (~/.ssh/config,
|
|
68
68
|
# /etc/ssh_config), to false ignore config files, or to a file path
|
|
69
69
|
# (or array of paths) to load specific configuration. Defaults to true.
|
|
70
|
-
config:
|
|
70
|
+
config: [ "~/.ssh/myconfig" ]
|
|
@@ -21,3 +21,11 @@ sshkit:
|
|
|
21
21
|
# Kamal sets a long idle timeout of 900 seconds on connections to try to avoid
|
|
22
22
|
# re-connection storms after an idle period, such as building an image or waiting for CI.
|
|
23
23
|
pool_idle_timeout: 300
|
|
24
|
+
|
|
25
|
+
# DNS retry settings
|
|
26
|
+
#
|
|
27
|
+
# Some resolvers (mDNSResponder, systemd-resolved, Tailscale) can drop lookups during
|
|
28
|
+
# bursts of concurrent SSH starts. Kamal will retry DNS failures automatically.
|
|
29
|
+
#
|
|
30
|
+
# Number of retries after the initial attempt. Set to 0 to disable.
|
|
31
|
+
dns_retries: 3
|
|
@@ -38,8 +38,12 @@ class Kamal::Configuration::Ssh
|
|
|
38
38
|
ssh_config["key_data"]
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
+
def config
|
|
42
|
+
ssh_config["config"]
|
|
43
|
+
end
|
|
44
|
+
|
|
41
45
|
def options
|
|
42
|
-
{ user: user, port: port, proxy: proxy, logger: logger, keepalive: true, keepalive_interval: 30, keys_only: keys_only, keys: keys, key_data: key_data }.compact
|
|
46
|
+
{ user: user, port: port, proxy: proxy, logger: logger, keepalive: true, keepalive_interval: 30, keys_only: keys_only, keys: keys, key_data: key_data, config: config }.compact
|
|
43
47
|
end
|
|
44
48
|
|
|
45
49
|
def to_h
|
|
@@ -34,6 +34,8 @@ class Kamal::Configuration::Validator
|
|
|
34
34
|
elsif example_value.is_a?(Array)
|
|
35
35
|
if key == "arch"
|
|
36
36
|
validate_array_of_or_type! value, example_value.first.class
|
|
37
|
+
elsif key.to_s == "config"
|
|
38
|
+
validate_ssh_config!(value)
|
|
37
39
|
else
|
|
38
40
|
validate_array_of! value, example_value.first.class
|
|
39
41
|
end
|
|
@@ -129,6 +131,16 @@ class Kamal::Configuration::Validator
|
|
|
129
131
|
end
|
|
130
132
|
end
|
|
131
133
|
|
|
134
|
+
def validate_ssh_config!(config)
|
|
135
|
+
if config.is_a?(Array)
|
|
136
|
+
validate_array_of! config, String
|
|
137
|
+
elsif boolean?(config.class) || config.is_a?(String)
|
|
138
|
+
# Booleans and Strings are allowed
|
|
139
|
+
else
|
|
140
|
+
type_error(TrueClass, FalseClass, String, Array)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
132
144
|
def validate_type!(value, *types)
|
|
133
145
|
type_error(*types) unless types.any? { |type| valid_type?(value, type) }
|
|
134
146
|
end
|
|
@@ -138,7 +150,8 @@ class Kamal::Configuration::Validator
|
|
|
138
150
|
end
|
|
139
151
|
|
|
140
152
|
def type_error(*expected_types)
|
|
141
|
-
|
|
153
|
+
descriptions = expected_types.map { |type| type_description(type) }.uniq
|
|
154
|
+
error "should be #{descriptions.join(" or ")}"
|
|
142
155
|
end
|
|
143
156
|
|
|
144
157
|
def unknown_keys_error(unknown_keys)
|
data/lib/kamal/configuration.rb
CHANGED
|
@@ -50,7 +50,7 @@ class Kamal::Configuration
|
|
|
50
50
|
|
|
51
51
|
validate! raw_config, example: validation_yml.symbolize_keys, context: "", with: Kamal::Configuration::Validator::Configuration
|
|
52
52
|
|
|
53
|
-
@secrets = Kamal::Secrets.new(destination: destination)
|
|
53
|
+
@secrets = Kamal::Secrets.new(destination: destination, secrets_path: secrets_path)
|
|
54
54
|
|
|
55
55
|
# Eager load config to validate it, these are first as they have dependencies later on
|
|
56
56
|
@servers = Servers.new(config: self)
|
|
@@ -76,6 +76,7 @@ class Kamal::Configuration
|
|
|
76
76
|
ensure_no_traefik_reboot_hooks
|
|
77
77
|
ensure_one_host_for_ssl_roles
|
|
78
78
|
ensure_unique_hosts_for_ssl_roles
|
|
79
|
+
ensure_local_registry_remote_builder_has_ssh_url
|
|
79
80
|
end
|
|
80
81
|
|
|
81
82
|
def version=(version)
|
|
@@ -240,6 +241,10 @@ class Kamal::Configuration
|
|
|
240
241
|
raw_config.hooks_path || ".kamal/hooks"
|
|
241
242
|
end
|
|
242
243
|
|
|
244
|
+
def secrets_path
|
|
245
|
+
raw_config.secrets_path || ".kamal/secrets"
|
|
246
|
+
end
|
|
247
|
+
|
|
243
248
|
def asset_path
|
|
244
249
|
raw_config.asset_path
|
|
245
250
|
end
|
|
@@ -363,6 +368,16 @@ class Kamal::Configuration
|
|
|
363
368
|
true
|
|
364
369
|
end
|
|
365
370
|
|
|
371
|
+
def ensure_local_registry_remote_builder_has_ssh_url
|
|
372
|
+
if registry.local? && builder.remote?
|
|
373
|
+
unless URI(builder.remote).scheme == "ssh"
|
|
374
|
+
raise Kamal::ConfigurationError, "Local registry with remote builder requires an SSH URL (e.g., ssh://user@host)"
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
true
|
|
379
|
+
end
|
|
380
|
+
|
|
366
381
|
def role_names
|
|
367
382
|
raw_config.servers.is_a?(Array) ? [ "web" ] : raw_config.servers.keys.sort
|
|
368
383
|
end
|
|
@@ -47,9 +47,8 @@ class Kamal::Secrets::Adapters::Passbolt < Kamal::Secrets::Adapters::Base
|
|
|
47
47
|
end
|
|
48
48
|
|
|
49
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`
|
|
50
|
+
items = `passbolt list resources #{filter_condition} #{folders.map { |item| "--folder #{item["id"].to_s.shellescape}" }.join(" ")} --json`
|
|
51
51
|
raise RuntimeError, "Could not read #{secrets} from Passbolt" unless $?.success?
|
|
52
|
-
|
|
53
52
|
items = JSON.parse(items)
|
|
54
53
|
found_names = items.map { |item| item["name"] }
|
|
55
54
|
missing_secrets = secret_names - found_names
|
data/lib/kamal/secrets.rb
CHANGED
|
@@ -3,8 +3,9 @@ require "dotenv"
|
|
|
3
3
|
class Kamal::Secrets
|
|
4
4
|
Kamal::Secrets::Dotenv::InlineCommandSubstitution.install!
|
|
5
5
|
|
|
6
|
-
def initialize(destination: nil)
|
|
6
|
+
def initialize(destination: nil, secrets_path:)
|
|
7
7
|
@destination = destination
|
|
8
|
+
@secrets_path = secrets_path
|
|
8
9
|
@mutex = Mutex.new
|
|
9
10
|
end
|
|
10
11
|
|
|
@@ -37,6 +38,6 @@ class Kamal::Secrets
|
|
|
37
38
|
end
|
|
38
39
|
|
|
39
40
|
def secrets_filenames
|
|
40
|
-
[ "
|
|
41
|
+
[ "#{@secrets_path}-common", "#{@secrets_path}#{(".#{@destination}" if @destination)}" ]
|
|
41
42
|
end
|
|
42
43
|
end
|
|
@@ -3,6 +3,7 @@ require "sshkit/dsl"
|
|
|
3
3
|
require "net/scp"
|
|
4
4
|
require "active_support/core_ext/hash/deep_merge"
|
|
5
5
|
require "json"
|
|
6
|
+
require "resolv"
|
|
6
7
|
require "concurrent/atomic/semaphore"
|
|
7
8
|
|
|
8
9
|
class SSHKit::Backend::Abstract
|
|
@@ -18,8 +19,11 @@ class SSHKit::Backend::Abstract
|
|
|
18
19
|
JSON.pretty_generate(JSON.parse(capture(*args, **kwargs)))
|
|
19
20
|
end
|
|
20
21
|
|
|
21
|
-
def puts_by_host(host, output, type: "App")
|
|
22
|
-
|
|
22
|
+
def puts_by_host(host, output, type: "App", quiet: false)
|
|
23
|
+
unless quiet
|
|
24
|
+
puts "#{type} Host: #{host}"
|
|
25
|
+
end
|
|
26
|
+
puts "#{output}\n\n"
|
|
23
27
|
end
|
|
24
28
|
|
|
25
29
|
# Our execution pattern is for the CLI execute args lists returned
|
|
@@ -58,10 +62,50 @@ class SSHKit::Backend::Abstract
|
|
|
58
62
|
end
|
|
59
63
|
|
|
60
64
|
class SSHKit::Backend::Netssh::Configuration
|
|
61
|
-
attr_accessor :max_concurrent_starts
|
|
65
|
+
attr_accessor :max_concurrent_starts, :dns_retries
|
|
62
66
|
end
|
|
63
67
|
|
|
64
68
|
class SSHKit::Backend::Netssh
|
|
69
|
+
module DnsRetriable
|
|
70
|
+
DNS_RETRY_BASE = 0.1
|
|
71
|
+
DNS_RETRY_MAX = 2.0
|
|
72
|
+
DNS_RETRY_JITTER = 0.1
|
|
73
|
+
DNS_ERROR_MESSAGE = /getaddrinfo|Temporary failure in name resolution|Name or service not known|nodename nor servname provided|No address associated|failed to look up|resolve/i
|
|
74
|
+
|
|
75
|
+
def with_dns_retry(hostname, retries: config.dns_retries, base: DNS_RETRY_BASE, max_sleep: DNS_RETRY_MAX, jitter: DNS_RETRY_JITTER)
|
|
76
|
+
attempts = 0
|
|
77
|
+
begin
|
|
78
|
+
attempts += 1
|
|
79
|
+
yield
|
|
80
|
+
rescue => error
|
|
81
|
+
raise unless retryable_dns_error?(error) && attempts <= retries
|
|
82
|
+
|
|
83
|
+
delay = dns_retry_sleep(attempts, base: base, jitter: jitter, max_sleep: max_sleep)
|
|
84
|
+
SSHKit.config.output.warn("Retrying DNS for #{hostname} (attempt #{attempts}/#{retries}) in #{format("%0.2f", delay)}s: #{error.message}")
|
|
85
|
+
sleep delay
|
|
86
|
+
retry
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
def retryable_dns_error?(error)
|
|
92
|
+
case error
|
|
93
|
+
when Resolv::ResolvError, Resolv::ResolvTimeout
|
|
94
|
+
true
|
|
95
|
+
when SocketError
|
|
96
|
+
error.message =~ DNS_ERROR_MESSAGE
|
|
97
|
+
else
|
|
98
|
+
error.cause && retryable_dns_error?(error.cause)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def dns_retry_sleep(attempt, base:, jitter:, max_sleep:)
|
|
103
|
+
sleep_for = [ base * (2 ** (attempt - 1)), max_sleep ].min
|
|
104
|
+
sleep_for += Kernel.rand * jitter
|
|
105
|
+
sleep_for
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
65
109
|
module LimitConcurrentStartsClass
|
|
66
110
|
attr_reader :start_semaphore
|
|
67
111
|
|
|
@@ -76,14 +120,31 @@ class SSHKit::Backend::Netssh
|
|
|
76
120
|
|
|
77
121
|
class << self
|
|
78
122
|
prepend LimitConcurrentStartsClass
|
|
123
|
+
prepend DnsRetriable
|
|
79
124
|
end
|
|
80
125
|
|
|
126
|
+
module ConnectSsh
|
|
127
|
+
private
|
|
128
|
+
def connect_ssh(...)
|
|
129
|
+
Net::SSH.start(...)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
include ConnectSsh
|
|
133
|
+
|
|
134
|
+
module DnsRetriableConnection
|
|
135
|
+
private
|
|
136
|
+
def connect_ssh(...)
|
|
137
|
+
self.class.with_dns_retry(host.hostname) { super }
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
prepend DnsRetriableConnection
|
|
141
|
+
|
|
81
142
|
module LimitConcurrentStartsInstance
|
|
82
143
|
private
|
|
83
144
|
def with_ssh(&block)
|
|
84
145
|
host.ssh_options = self.class.config.ssh_options.merge(host.ssh_options || {})
|
|
85
146
|
self.class.pool.with(
|
|
86
|
-
method(:
|
|
147
|
+
method(:connect_ssh),
|
|
87
148
|
String(host.hostname),
|
|
88
149
|
host.username,
|
|
89
150
|
host.netssh_options,
|
|
@@ -91,17 +152,18 @@ class SSHKit::Backend::Netssh
|
|
|
91
152
|
)
|
|
92
153
|
end
|
|
93
154
|
|
|
94
|
-
def
|
|
155
|
+
def connect_ssh(...)
|
|
156
|
+
with_concurrency_limit { super }
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def with_concurrency_limit(&block)
|
|
95
160
|
if self.class.start_semaphore
|
|
96
|
-
self.class.start_semaphore.acquire
|
|
97
|
-
Net::SSH.start(*args)
|
|
98
|
-
end
|
|
161
|
+
self.class.start_semaphore.acquire(&block)
|
|
99
162
|
else
|
|
100
|
-
|
|
163
|
+
yield
|
|
101
164
|
end
|
|
102
165
|
end
|
|
103
166
|
end
|
|
104
|
-
|
|
105
167
|
prepend LimitConcurrentStartsInstance
|
|
106
168
|
end
|
|
107
169
|
|
data/lib/kamal/version.rb
CHANGED
data/lib/kamal.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.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- David Heinemeier Hansson
|
|
@@ -224,12 +224,12 @@ files:
|
|
|
224
224
|
- lib/kamal/cli/base.rb
|
|
225
225
|
- lib/kamal/cli/build.rb
|
|
226
226
|
- lib/kamal/cli/build/clone.rb
|
|
227
|
+
- lib/kamal/cli/build/port_forwarding.rb
|
|
227
228
|
- lib/kamal/cli/healthcheck/barrier.rb
|
|
228
229
|
- lib/kamal/cli/healthcheck/error.rb
|
|
229
230
|
- lib/kamal/cli/healthcheck/poller.rb
|
|
230
231
|
- lib/kamal/cli/lock.rb
|
|
231
232
|
- lib/kamal/cli/main.rb
|
|
232
|
-
- lib/kamal/cli/port_forwarding.rb
|
|
233
233
|
- lib/kamal/cli/proxy.rb
|
|
234
234
|
- lib/kamal/cli/prune.rb
|
|
235
235
|
- lib/kamal/cli/registry.rb
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
require "concurrent/atomic/count_down_latch"
|
|
2
|
-
|
|
3
|
-
class Kamal::Cli::PortForwarding
|
|
4
|
-
attr_reader :hosts, :port
|
|
5
|
-
|
|
6
|
-
def initialize(hosts, port)
|
|
7
|
-
@hosts = hosts
|
|
8
|
-
@port = port
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
def forward
|
|
12
|
-
@done = false
|
|
13
|
-
forward_ports
|
|
14
|
-
|
|
15
|
-
yield
|
|
16
|
-
ensure
|
|
17
|
-
stop
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
private
|
|
21
|
-
|
|
22
|
-
def stop
|
|
23
|
-
@done = true
|
|
24
|
-
@threads.to_a.each(&:join)
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def forward_ports
|
|
28
|
-
ready = Concurrent::CountDownLatch.new(hosts.size)
|
|
29
|
-
|
|
30
|
-
@threads = hosts.map do |host|
|
|
31
|
-
Thread.new do
|
|
32
|
-
Net::SSH.start(host, KAMAL.config.ssh.user, **{ proxy: KAMAL.config.ssh.proxy }.compact) do |ssh|
|
|
33
|
-
ssh.forward.remote(port, "localhost", port, "127.0.0.1") do |remote_port, bind_address|
|
|
34
|
-
if remote_port == :error
|
|
35
|
-
raise "Failed to establish port forward on #{host}"
|
|
36
|
-
else
|
|
37
|
-
ready.count_down
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
ssh.loop(0.1) do
|
|
42
|
-
if @done
|
|
43
|
-
ssh.forward.cancel_remote(port, "127.0.0.1")
|
|
44
|
-
break
|
|
45
|
-
else
|
|
46
|
-
true
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
raise "Timed out waiting for port forwarding to be established" unless ready.wait(10)
|
|
54
|
-
end
|
|
55
|
-
end
|