Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use local docker registry to push and pull app images #1355

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
28 changes: 20 additions & 8 deletions lib/kamal/cli/build.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,16 @@ def push

desc "pull", "Pull app image from registry onto servers"
def pull
if (first_hosts = mirror_hosts).any?
#  Pull on a single host per mirror first to seed them
say "Pulling image on #{first_hosts.join(", ")} to seed the #{"mirror".pluralize(first_hosts.count)}...", :magenta
pull_on_hosts(first_hosts)
say "Pulling image on remaining hosts...", :magenta
pull_on_hosts(KAMAL.hosts - first_hosts)
else
pull_on_hosts(KAMAL.hosts)
forward_local_registry_port do
if (first_hosts = mirror_hosts).any?
#  Pull on a single host per mirror first to seed them
say "Pulling image on #{first_hosts.join(", ")} to seed the #{"mirror".pluralize(first_hosts.count)}...", :magenta
pull_on_hosts(first_hosts)
say "Pulling image on remaining hosts...", :magenta
pull_on_hosts(KAMAL.hosts - first_hosts)
else
pull_on_hosts(KAMAL.hosts)
end
end
end

Expand Down Expand Up @@ -145,4 +147,14 @@ def pull_on_hosts(hosts)
execute *KAMAL.builder.validate_image
end
end

def forward_local_registry_port(&block)
if KAMAL.config.registry.local?
Kamal::Cli::PortForwarding.
new(KAMAL.hosts, KAMAL.config.registry.local_port).
forward(&block)
else
yield
end
end
end
4 changes: 2 additions & 2 deletions lib/kamal/cli/main.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def deploy(boot_accessories: false)
invoke_options = deploy_options

say "Log into image registry...", :magenta
invoke "kamal:cli:registry:login", [], invoke_options.merge(skip_local: options[:skip_push])
invoke "kamal:cli:registry:setup", [], invoke_options.merge(skip_local: options[:skip_push])

if options[:skip_push]
say "Pull app image...", :magenta
Expand Down Expand Up @@ -185,7 +185,7 @@ def remove
invoke "kamal:cli:app:remove", [], options.without(:confirmed)
invoke "kamal:cli:proxy:remove", [], options.without(:confirmed)
invoke "kamal:cli:accessory:remove", [ "all" ], options
invoke "kamal:cli:registry:logout", [], options.without(:confirmed).merge(skip_local: true)
invoke "kamal:cli:registry:remove", [], options.without(:confirmed).merge(skip_local: true)
end
end
end
Expand Down
42 changes: 42 additions & 0 deletions lib/kamal/cli/port_forwarding.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
class Kamal::Cli::PortForwarding
attr_reader :hosts, :port

def initialize(hosts, port)
@hosts = hosts
@port = port
end

def forward
@done = false
forward_ports

yield
ensure
stop
end

private

def stop
@done = true
@threads.to_a.each(&:join)
end

def forward_ports
@threads = hosts.map do |host|
Thread.new do
Net::SSH.start(host, KAMAL.config.ssh.user) do |ssh|
ssh.forward.remote(port, "localhost", port, "localhost")
ssh.loop(0.1) do
if @done
ssh.forward.cancel_remote(port, "localhost")
break
else
true
end
end
end
end
end
end
end
24 changes: 16 additions & 8 deletions lib/kamal/cli/registry.rb
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
class Kamal::Cli::Registry < Kamal::Cli::Base
desc "login", "Log in to registry locally and remotely"
desc "setup", "Setup local registry or log in to remote registry locally and remotely"
option :skip_local, aliases: "-L", type: :boolean, default: false, desc: "Skip local login"
option :skip_remote, aliases: "-R", type: :boolean, default: false, desc: "Skip remote login"
def login
def setup
ensure_docker_installed

run_locally { execute *KAMAL.registry.login } unless options[:skip_local]
on(KAMAL.hosts) { execute *KAMAL.registry.login } unless options[:skip_remote]
if KAMAL.registry.local?
run_locally { execute *KAMAL.registry.setup } unless options[:skip_local]
else
run_locally { execute *KAMAL.registry.login } unless options[:skip_local]
on(KAMAL.hosts) { execute *KAMAL.registry.login } unless options[:skip_remote]
end
end

desc "logout", "Log out of registry locally and remotely"
desc "remove", "Remove local registry or log out of remote registry locally and remotely"
option :skip_local, aliases: "-L", type: :boolean, default: false, desc: "Skip local login"
option :skip_remote, aliases: "-R", type: :boolean, default: false, desc: "Skip remote login"
def logout
run_locally { execute *KAMAL.registry.logout } unless options[:skip_local]
on(KAMAL.hosts) { execute *KAMAL.registry.logout } unless options[:skip_remote]
def remove
if KAMAL.registry.local?
run_locally { execute *KAMAL.registry.remove, raise_on_non_zero_exit: false } unless options[:skip_local]
else
run_locally { execute *KAMAL.registry.logout } unless options[:skip_local]
on(KAMAL.hosts) { execute *KAMAL.registry.logout } unless options[:skip_remote]
end
end
end
7 changes: 4 additions & 3 deletions lib/kamal/cli/templates/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,14 @@ proxy:

# Credentials for your image host.
registry:
server: localhost:5555
# Specify the registry server, if you're not using Docker Hub
# server: registry.digitalocean.com / ghcr.io / ...
username: my-user
# username: my-user

# Always use an access token rather than real password (pulled from .kamal/secrets).
password:
- KAMAL_REGISTRY_PASSWORD
# password:
# - KAMAL_REGISTRY_PASSWORD

# Configure builder setup.
builder:
Expand Down
2 changes: 1 addition & 1 deletion lib/kamal/cli/templates/secrets
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# password manager, ENV, or a file. DO NOT ENTER RAW CREDENTIALS HERE! This file needs to be safe for git.

# Option 1: Read secrets from the environment
KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD
# KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD

# Option 2: Read secrets via a command
# RAILS_MASTER_KEY=$(cat config/master.key)
Expand Down
2 changes: 1 addition & 1 deletion lib/kamal/commander.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def initialize
end

def config
@config ||= Kamal::Configuration.create_from(**@config_kwargs).tap do |config|
@config ||= Kamal::Configuration.create_from(**@config_kwargs.to_h).tap do |config|
@config_kwargs = nil
configure_sshkit_with(config)
end
Expand Down
17 changes: 15 additions & 2 deletions lib/kamal/commands/builder/local.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
class Kamal::Commands::Builder::Local < Kamal::Commands::Builder::Base
def create
docker :buildx, :create, "--name", builder_name, "--driver=#{driver}" unless docker_driver?
return if docker_driver?

options =
if KAMAL.registry.local?
"--driver=#{driver} --driver-opt network=host"
else
"--driver=#{driver}"
end

docker :buildx, :create, "--name", builder_name, options
end

def remove
Expand All @@ -9,6 +18,10 @@ def remove

private
def builder_name
"kamal-local-#{driver}"
if KAMAL.registry.local?
"kamal-local-registry-#{driver}"
else
"kamal-local-#{driver}"
end
end
end
22 changes: 22 additions & 0 deletions lib/kamal/commands/registry.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ class Kamal::Commands::Registry < Kamal::Commands::Base
def login(registry_config: nil)
registry_config ||= config.registry

return if registry_config.local?

docker :login,
registry_config.server,
"-u", sensitive(Kamal::Utils.escape_shell_value(registry_config.username)),
Expand All @@ -13,4 +15,24 @@ def logout(registry_config: nil)

docker :logout, registry_config.server
end

def setup(registry_config: nil)
registry_config ||= config.registry

combine \
docker(:start, "kamal-docker-registry"),
docker(:run, "--detach", "-p", "127.0.0.1:#{registry_config.local_port}:5000", "--name", "kamal-docker-registry", "registry:3.0.0-rc.2"),
by: "||"
end

def remove
combine \
docker(:stop, "kamal-docker-registry"),
docker(:rm, "kamal-docker-registry"),
by: "&&"
end

def local?
config.registry.local?
end
end
8 changes: 8 additions & 0 deletions lib/kamal/configuration/registry.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ def password
lookup("password")
end

def local?
server&.match?("^localhost[:$]")
end

def local_port
local? ? (server.split(":").last.to_i || 80) : nil
end

private
attr_reader :registry_config, :secrets

Expand Down
8 changes: 5 additions & 3 deletions lib/kamal/configuration/validator/registry.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ def validate_string_or_one_item_array!(key)
with_context(key) do
value = config[key]

error "is required" unless value.present?
unless config["server"]&.match?("^localhost[:$]")
error "is required" unless value.present?

unless value.is_a?(String) || (value.is_a?(Array) && value.size == 1 && value.first.is_a?(String))
error "should be a string or an array with one string (for secret lookup)"
unless value.is_a?(String) || (value.is_a?(Array) && value.size == 1 && value.first.is_a?(String))
error "should be a string or an array with one string (for secret lookup)"
end
end
end
end
Expand Down
36 changes: 36 additions & 0 deletions test/cli/build_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,42 @@ class CliBuildTest < CliTestCase
end
end

test "push without builder for local registry" do
with_build_directory do |build_directory|
stub_setup

SSHKit::Backend::Abstract.any_instance.expects(:execute)
.with(:docker, "--version", "&&", :docker, :buildx, "version")

SSHKit::Backend::Abstract.any_instance.expects(:execute)
.with(:docker, :buildx, :rm, "kamal-local-registry-docker-container")

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

SSHKit::Backend::Abstract.any_instance.expects(:execute)
.with(:docker, :buildx, :inspect, "kamal-local-registry-docker-container")
.raises(SSHKit::Command::Failed.new("no builder"))

SSHKit::Backend::Abstract.any_instance.expects(:execute).with { |*args| args.first.start_with?("git") }

SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:git, "-C", anything, :"rev-parse", :HEAD)
.returns(Kamal::Git.revision)

SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:git, "-C", anything, :status, "--porcelain")
.returns("")

SSHKit::Backend::Abstract.any_instance.expects(:execute)
.with(:docker, :buildx, :build, "--push", "--platform", "linux/amd64", "--builder", "kamal-local-registry-docker-container", "-t", "localhost:5000/dhh/app:999", "-t", "localhost:5000/dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".")

run_command("push", fixture: :with_local_registry_and_accessories).tap do |output|
assert_match /WARN Missing compatible builder, so creating a new one first/, output
end
end
end

test "push without builder" do
with_build_directory do |build_directory|
stub_setup
Expand Down
Loading
Loading