Skip to content

Commit 0055338

Browse files
committed
Add local registry support for remote builders
When running with a remote builder and a local registry: 1. Forward the local registry port to the remote builder 2. Use host networking on the remote builder 3. Use a different builder name as the settings are different A remote builder can only have one port at a time forwarded, so concurrent builds on the same registry port will fail.
1 parent 2bbd3b1 commit 0055338

File tree

14 files changed

+183
-48
lines changed

14 files changed

+183
-48
lines changed

lib/kamal.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ class ConfigurationError < StandardError; end
77
require "yaml"
88
require "tmpdir"
99
require "pathname"
10+
require "uri"
1011

1112
loader = Zeitwerk::Loader.for_gem
1213
loader.ignore(File.join(__dir__, "kamal", "sshkit_with_ext.rb"))

lib/kamal/cli/build.rb

Lines changed: 33 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
require "uri"
2-
31
class Kamal::Cli::Build < Kamal::Cli::Base
42
class BuildError < StandardError; end
53

@@ -38,29 +36,31 @@ def push
3836
say "Building with uncommitted changes:\n #{uncommitted_changes}", :yellow
3937
end
4038

41-
with_env(KAMAL.config.builder.secrets) do
42-
run_locally do
43-
begin
44-
execute *KAMAL.builder.inspect_builder
45-
rescue SSHKit::Command::Failed => e
46-
if e.message =~ /(context not found|no builder|no compatible builder|does not exist)/
47-
warn "Missing compatible builder, so creating a new one first"
48-
begin
49-
cli.remove
50-
rescue SSHKit::Command::Failed
51-
raise unless e.message =~ /(context not found|no builder|does not exist)/
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
5255
end
53-
cli.create
54-
else
55-
raise
5656
end
57-
end
5857

59-
# Get the command here to ensure the Dir.chdir doesn't interfere with it
60-
push = KAMAL.builder.push(cli.options[:output], no_cache: cli.options[:no_cache])
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])
6160

62-
KAMAL.with_verbosity(:debug) do
63-
Dir.chdir(KAMAL.config.builder.build_directory) { execute *push, env: KAMAL.builder.push_env }
61+
KAMAL.with_verbosity(:debug) do
62+
Dir.chdir(KAMAL.config.builder.build_directory) { execute *push, env: KAMAL.builder.push_env }
63+
end
6464
end
6565
end
6666
end
@@ -70,7 +70,7 @@ def push
7070
def pull
7171
login_to_registry_remotely unless KAMAL.registry.local?
7272

73-
forward_local_registry_port do
73+
forward_local_registry_port(KAMAL.hosts) do
7474
if (first_hosts = mirror_hosts).any?
7575
#  Pull on a single host per mirror first to seed them
7676
say "Pulling image on #{first_hosts.join(", ")} to seed the #{"mirror".pluralize(first_hosts.count)}...", :magenta
@@ -210,11 +210,18 @@ def login_to_registry_remotely
210210
end
211211
end
212212

213-
def forward_local_registry_port(&block)
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 ], user: remote_uri.user, proxy: nil, ssh_port: remote_uri.port, &block)
217+
else
218+
yield
219+
end
220+
end
221+
222+
def forward_local_registry_port(hosts, user: KAMAL.config.ssh.user, proxy: KAMAL.config.ssh.proxy, ssh_port: nil, &block)
214223
if KAMAL.config.registry.local?
215-
Kamal::Cli::PortForwarding.
216-
new(KAMAL.hosts, KAMAL.config.registry.local_port).
217-
forward(&block)
224+
PortForwarding.new(hosts, KAMAL.config.registry.local_port, user: user, proxy: proxy, ssh_port: ssh_port).forward(&block)
218225
else
219226
yield
220227
end

lib/kamal/cli/build/clone.rb

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
require "uri"
2-
31
class Kamal::Cli::Build::Clone
42
attr_reader :sshkit
53
delegate :info, :error, :execute, :capture_with_info, to: :sshkit

lib/kamal/cli/port_forwarding.rb renamed to lib/kamal/cli/build/port_forwarding.rb

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
require "concurrent/atomic/count_down_latch"
22

3-
class Kamal::Cli::PortForwarding
4-
attr_reader :hosts, :port
3+
class Kamal::Cli::Build::PortForwarding
4+
attr_reader :hosts, :port, :user, :proxy, :ssh_port
55

6-
def initialize(hosts, port)
6+
def initialize(hosts, port, user: nil, proxy: nil, ssh_port: nil)
77
@hosts = hosts
88
@port = port
9+
@user = user
10+
@proxy = proxy
11+
@ssh_port = ssh_port
912
end
1013

1114
def forward
@@ -29,7 +32,7 @@ def forward_ports
2932

3033
@threads = hosts.map do |host|
3134
Thread.new do
32-
Net::SSH.start(host, KAMAL.config.ssh.user, **{ proxy: KAMAL.config.ssh.proxy }.compact) do |ssh|
35+
Net::SSH.start(host, user, **{ port: ssh_port, proxy: proxy }.compact) do |ssh|
3336
ssh.forward.remote(port, "localhost", port, "127.0.0.1") do |remote_port, bind_address|
3437
if remote_port == :error
3538
raise "Failed to establish port forward on #{host}"
@@ -50,6 +53,6 @@ def forward_ports
5053
end
5154
end
5255

53-
raise "Timed out waiting for port forwarding to be established" unless ready.wait(10)
56+
raise "Timed out waiting for port forwarding to be established" unless ready.wait(30)
5457
end
5558
end

lib/kamal/commands/builder/base.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,16 @@ def builder_config
127127
config.builder
128128
end
129129

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+
130140
def platform_options(arches)
131141
argumentize "--platform", arches.map { |arch| "linux/#{arch}" }.join(",") if arches.any?
132142
end

lib/kamal/commands/builder/hybrid.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@ def create
88

99
private
1010
def builder_name
11-
"kamal-hybrid-#{driver}-#{remote.gsub(/[^a-z0-9_-]/, "-")}"
11+
"kamal-hybrid-#{driver}-#{remote_builder_name_suffix}"
1212
end
1313

1414
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
1616
end
1717

1818
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
2020
end
2121
end

lib/kamal/commands/builder/local.rb

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,7 @@ class Kamal::Commands::Builder::Local < Kamal::Commands::Builder::Base
22
def create
33
return if docker_driver?
44

5-
options =
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
136
end
147

158
def remove
@@ -18,7 +11,7 @@ def remove
1811

1912
private
2013
def builder_name
21-
if KAMAL.registry.local?
14+
if registry_config.local?
2215
"kamal-local-registry-#{driver}"
2316
else
2417
"kamal-local-#{driver}"

lib/kamal/commands/builder/remote.rb

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,17 @@ def push_env
3434

3535
private
3636
def builder_name
37-
"kamal-remote-#{remote.gsub(/[^a-z0-9_-]/, "-")}"
37+
"kamal-remote-#{remote_builder_name_suffix}"
3838
end
3939

4040
def remote_context_name
4141
"#{builder_name}-context"
4242
end
4343

44+
def remote_builder_name_suffix
45+
"#{remote.gsub(/[^a-z0-9_-]/, "-")}#{registry_config.local? ? "-local-registry" : "" }"
46+
end
47+
4448
def inspect_buildx
4549
pipe \
4650
docker(:buildx, :inspect, builder_name),
@@ -62,7 +66,7 @@ def remove_remote_context
6266
end
6367

6468
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
6670
end
6771

6872
def remove_buildx

lib/kamal/configuration.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ def initialize(raw_config, destination: nil, version: nil, validate: true)
7676
ensure_no_traefik_reboot_hooks
7777
ensure_one_host_for_ssl_roles
7878
ensure_unique_hosts_for_ssl_roles
79+
ensure_local_registry_remote_builder_has_ssh_url
7980
end
8081

8182
def version=(version)
@@ -363,6 +364,16 @@ def ensure_unique_hosts_for_ssl_roles
363364
true
364365
end
365366

367+
def ensure_local_registry_remote_builder_has_ssh_url
368+
if registry.local? && builder.remote?
369+
unless URI(builder.remote).scheme == "ssh"
370+
raise Kamal::ConfigurationError, "Local registry with remote builder requires an SSH URL (e.g., ssh://user@host)"
371+
end
372+
end
373+
374+
true
375+
end
376+
366377
def role_names
367378
raw_config.servers.is_a?(Array) ? [ "web" ] : raw_config.servers.keys.sort
368379
end

test/cli/build_test.rb

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ class CliBuildTest < CliTestCase
182182
.with(:docker, :buildx, :rm, "kamal-local-registry-docker-container")
183183

184184
SSHKit::Backend::Abstract.any_instance.expects(:execute)
185-
.with(:docker, :buildx, :create, "--name", "kamal-local-registry-docker-container", "--driver=docker-container --driver-opt network=host")
185+
.with(:docker, :buildx, :create, "--name", "kamal-local-registry-docker-container", "--driver=docker-container", "--driver-opt", "network=host")
186186

187187
SSHKit::Backend::Abstract.any_instance.expects(:execute)
188188
.with(:docker, :buildx, :inspect, "kamal-local-registry-docker-container")
@@ -409,6 +409,41 @@ class CliBuildTest < CliTestCase
409409
end
410410
end
411411

412+
test "create with local registry" do
413+
run_command("create", fixture: :with_local_registry).tap do |output|
414+
assert_match /docker buildx create --name kamal-local-registry-docker-container --driver=docker-container --driver-opt network=host/, output
415+
end
416+
end
417+
418+
test "create with local registry and remote builder" do
419+
run_command("create", fixture: :with_local_registry_and_remote_builder).tap do |output|
420+
# Verify remote builder with local-registry in name
421+
assert_match /docker buildx create --name kamal-remote-ssh---app-1-1-1-5-local-registry/, output
422+
assert_match /--driver-opt network=host/, output
423+
end
424+
end
425+
426+
test "pull with local registry" do
427+
# Verify port forwarding is established for all app hosts
428+
port_forwarding_mock = mock("port_forwarding")
429+
port_forwarding_mock.expects(:forward).yields
430+
Kamal::Cli::Build::PortForwarding.expects(:new)
431+
.with([ "1.1.1.1", "1.1.1.2" ], 5000, user: "root", proxy: nil, ssh_port: nil)
432+
.returns(port_forwarding_mock)
433+
434+
run_command("pull", fixture: :with_local_registry).tap do |output|
435+
assert_match /docker pull localhost:5000\/dhh\/app:999/, output
436+
end
437+
end
438+
439+
test "create with local registry and remote builder with custom port" do
440+
run_command("create", fixture: :with_local_registry_and_remote_builder_with_port).tap do |output|
441+
# Verify remote builder with local-registry in name includes custom port in context name
442+
assert_match /docker buildx create --name kamal-remote-ssh---app-1-1-1-5-2222-local-registry/, output
443+
assert_match /--driver-opt network=host/, output
444+
end
445+
end
446+
412447
private
413448
def run_command(*command, fixture: :with_accessories)
414449
stdouted { stderred { Kamal::Cli::Build.start([ *command, "-c", "test/fixtures/deploy_#{fixture}.yml" ]) } }

0 commit comments

Comments
 (0)