Use local docker registry to push and pull app images
This commit is contained in:
@@ -67,16 +67,18 @@ class Kamal::Cli::Build < Kamal::Cli::Base
|
||||
|
||||
desc "pull", "Pull app image from registry onto servers"
|
||||
def pull
|
||||
login_to_registry_remotely
|
||||
login_to_registry_remotely unless KAMAL.registry.local?
|
||||
|
||||
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.app_hosts - first_hosts)
|
||||
else
|
||||
pull_on_hosts(KAMAL.app_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.app_hosts - first_hosts)
|
||||
else
|
||||
pull_on_hosts(KAMAL.app_hosts)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -192,7 +194,11 @@ class Kamal::Cli::Build < Kamal::Cli::Base
|
||||
|
||||
def login_to_registry_locally
|
||||
run_locally do
|
||||
execute *KAMAL.registry.login
|
||||
if KAMAL.registry.local?
|
||||
execute *KAMAL.registry.setup
|
||||
else
|
||||
execute *KAMAL.registry.login
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -201,4 +207,14 @@ class Kamal::Cli::Build < Kamal::Cli::Base
|
||||
execute *KAMAL.registry.login
|
||||
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
|
||||
|
||||
@@ -182,7 +182,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base
|
||||
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
|
||||
|
||||
42
lib/kamal/cli/port_forwarding.rb
Normal file
42
lib/kamal/cli/port_forwarding.rb
Normal file
@@ -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
|
||||
@@ -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 unless options[:skip_local]
|
||||
|
||||
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
|
||||
|
||||
@@ -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:
|
||||
@@ -39,7 +40,6 @@ builder:
|
||||
# Pass in additional build args needed for your Dockerfile.
|
||||
# args:
|
||||
# RUBY_VERSION: <%= ENV["RBENV_VERSION"] || ENV["rvm_ruby_string"] || "#{RUBY_ENGINE}-#{RUBY_ENGINE_VERSION}" %>
|
||||
|
||||
# Inject ENV variables into containers (secrets come from .kamal/secrets).
|
||||
#
|
||||
# env:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -21,7 +21,7 @@ class Kamal::Commander
|
||||
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
|
||||
|
||||
@@ -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
|
||||
@@ -9,6 +18,10 @@ class Kamal::Commands::Builder::Local < Kamal::Commands::Builder::Base
|
||||
|
||||
private
|
||||
def builder_name
|
||||
"kamal-local-#{driver}"
|
||||
if KAMAL.registry.local?
|
||||
"kamal-local-registry-#{driver}"
|
||||
else
|
||||
"kamal-local-#{driver}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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)),
|
||||
@@ -13,4 +15,24 @@ class Kamal::Commands::Registry < Kamal::Commands::Base
|
||||
|
||||
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"),
|
||||
by: "||"
|
||||
end
|
||||
|
||||
def remove
|
||||
combine \
|
||||
docker(:stop, "kamal-docker-registry"),
|
||||
docker(:rm, "kamal-docker-registry"),
|
||||
by: "&&"
|
||||
end
|
||||
|
||||
def local?
|
||||
config.registry.local?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,7 +6,7 @@ require "erb"
|
||||
require "net/ssh/proxy/jump"
|
||||
|
||||
class Kamal::Configuration
|
||||
delegate :service, :image, :labels, :hooks_path, to: :raw_config, allow_nil: true
|
||||
delegate :service, :labels, :hooks_path, to: :raw_config, allow_nil: true
|
||||
delegate :argumentize, :optionize, to: Kamal::Utils
|
||||
|
||||
attr_reader :destination, :raw_config, :secrets
|
||||
@@ -157,6 +157,13 @@ class Kamal::Configuration
|
||||
(proxy_roles.flat_map(&:hosts) + proxy_accessories.flat_map(&:hosts)).uniq
|
||||
end
|
||||
|
||||
def image
|
||||
name = raw_config&.image.presence
|
||||
name ||= raw_config&.service if registry.local?
|
||||
|
||||
name
|
||||
end
|
||||
|
||||
def repository
|
||||
[ registry.server, image ].compact.join("/")
|
||||
end
|
||||
@@ -282,10 +289,12 @@ class Kamal::Configuration
|
||||
end
|
||||
|
||||
def ensure_required_keys_present
|
||||
%i[ service image registry ].each do |key|
|
||||
%i[ service registry ].each do |key|
|
||||
raise Kamal::ConfigurationError, "Missing required configuration for #{key}" unless raw_config[key].present?
|
||||
end
|
||||
|
||||
raise Kamal::ConfigurationError, "Missing required configuration for image" if image.blank?
|
||||
|
||||
if raw_config.servers.nil?
|
||||
raise Kamal::ConfigurationError, "No servers or accessories specified" unless raw_config.accessories.present?
|
||||
else
|
||||
|
||||
@@ -19,6 +19,14 @@ class Kamal::Configuration::Registry
|
||||
lookup("password")
|
||||
end
|
||||
|
||||
def local?
|
||||
server.to_s.match?("^localhost[:$]")
|
||||
end
|
||||
|
||||
def local_port
|
||||
local? ? (server.split(":").last.to_i || 80) : nil
|
||||
end
|
||||
|
||||
private
|
||||
attr_reader :registry_config, :secrets
|
||||
|
||||
|
||||
@@ -15,10 +15,12 @@ class Kamal::Configuration::Validator::Registry < Kamal::Configuration::Validato
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user