Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 3 additions & 7 deletions lib/kamal/cli/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -307,16 +307,12 @@ def with_env(env)
ENV.update(current_env)
end

def ensure_docker_installed
def ensure_builder_installed
run_locally do
begin
execute *KAMAL.builder.ensure_docker_installed
execute *KAMAL.builder.ensure_installed
rescue SSHKit::Command::Failed => e
error = e.message =~ /command not found/ ?
"Docker is not installed locally" :
"Docker buildx plugin is not installed locally"

raise DependencyError, error
raise DependencyError, KAMAL.builder.install_error(e.message)
end
end
end
Expand Down
12 changes: 6 additions & 6 deletions lib/kamal/cli/build.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def push
# or the pre-build hooks.
pre_connect_if_required

ensure_docker_installed
ensure_builder_installed
setup_local_registry if KAMAL.registry.local?
login_to_registry_locally if !KAMAL.registry.local? && KAMAL.builder.login_to_registry_locally?

Expand Down Expand Up @@ -127,9 +127,9 @@ def details
def dev
cli = self

ensure_docker_installed
ensure_builder_installed

docker_included_files = Set.new(Kamal::Docker.included_files)
docker_included_files = Set.new(Kamal::Docker.included_files(builder: KAMAL.builder))
git_uncommitted_files = Set.new(Kamal::Git.uncommitted_files)
git_untracked_files = Set.new(Kamal::Git.untracked_files)

Expand All @@ -151,7 +151,7 @@ def dev
run_locally do
build = KAMAL.builder.push(cli.options[:output], tag_as_dirty: true, no_cache: cli.options[:no_cache])
KAMAL.with_verbosity(:debug) do
execute(*build)
execute(*build, env: KAMAL.builder.push_env)
end
end
end
Expand Down Expand Up @@ -197,13 +197,13 @@ def pull_on_hosts(hosts)

def setup_local_registry
run_locally do
execute *KAMAL.registry.setup
execute *KAMAL.local_registry.setup
end
end

def login_to_registry_locally
run_locally do
execute *KAMAL.registry.login
execute *KAMAL.local_registry.login
end
end

Expand Down
10 changes: 5 additions & 5 deletions lib/kamal/cli/registry.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ class Kamal::Cli::Registry < Kamal::Cli::Base
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 setup
ensure_docker_installed unless options[:skip_local]
ensure_builder_installed unless options[:skip_local]

if KAMAL.registry.local?
run_locally { execute *KAMAL.registry.setup } unless options[:skip_local]
run_locally { execute *KAMAL.local_registry.setup } unless options[:skip_local]
else
run_locally { execute *KAMAL.registry.login } unless options[:skip_local]
run_locally { execute *KAMAL.local_registry.login } unless options[:skip_local]
on(KAMAL.hosts) { execute *KAMAL.registry.login } unless options[:skip_remote]
end
end
Expand All @@ -18,9 +18,9 @@ def setup
option :skip_remote, aliases: "-R", type: :boolean, default: false, desc: "Skip remote login"
def remove
if KAMAL.registry.local?
run_locally { execute *KAMAL.registry.remove, raise_on_non_zero_exit: false } unless options[:skip_local]
run_locally { execute *KAMAL.local_registry.remove, raise_on_non_zero_exit: false } unless options[:skip_local]
else
run_locally { execute *KAMAL.registry.logout } unless options[:skip_local]
run_locally { execute *KAMAL.local_registry.logout } unless options[:skip_local]
on(KAMAL.hosts) { execute *KAMAL.registry.logout } unless options[:skip_remote]
end
end
Expand Down
5 changes: 5 additions & 0 deletions lib/kamal/commander.rb
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@ def registry
@commands[:registry] ||= Kamal::Commands::Registry.new(config)
end

def local_registry
@commands[:local_registry] ||= builder.local_registry
end

def server
@commands[:server] ||= Kamal::Commands::Server.new(config)
end
Expand Down Expand Up @@ -198,6 +202,7 @@ def configure_sshkit_with(config)
sshkit.ssh_options = config.ssh.options
end
SSHKit.config.command_map[:docker] = "docker" # No need to use /usr/bin/env, just clogs up the logs
SSHKit.config.command_map[:container] = "container"
SSHKit.config.output_verbosity = verbosity

configure_output_with(config)
Expand Down
4 changes: 4 additions & 0 deletions lib/kamal/commands/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ def docker(*args)
args.compact.unshift :docker
end

def apple_container(*args)
args.compact.unshift :container
end

def pack(*args)
args.compact.unshift :pack
end
Expand Down
7 changes: 6 additions & 1 deletion lib/kamal/commands/builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
class Kamal::Commands::Builder < Kamal::Commands::Base
delegate \
:create, :remove, :dev, :push, :clean, :pull, :info, :inspect_builder,
:ensure_installed, :install_error, :local_registry, :build_check_commands,
:validate_image, :first_mirror, :login_to_registry_locally?, :push_env,
to: :target

Expand Down Expand Up @@ -37,7 +38,11 @@ def remote
end

def local
@local ||= Kamal::Commands::Builder::Local.new(config)
@local ||= if config.builder.apple_container?
Kamal::Commands::Builder::AppleContainer.new(config)
else
Kamal::Commands::Builder::Local.new(config)
end
end

def hybrid
Expand Down
100 changes: 100 additions & 0 deletions lib/kamal/commands/builder/apple_container.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
class Kamal::Commands::Builder::AppleContainer < Kamal::Commands::Builder::Base
def create
apple_container :builder, :start
end

def remove
apple_container :builder, :stop
end

def ensure_installed
combine \
apple_container("--version"),
apple_container(:system, :status)
end

def install_error(output)
output.match?(/command not found/) ?
"Apple container is not installed locally" :
"Apple container system service is not running locally"
end

def local_registry
Kamal::Commands::Registry::AppleContainer.new(config)
end

def info
apple_container :builder, :status
end
alias_method :inspect_builder, :info

def build_check_commands(dockerfile:, tag:)
{ build: [ "container", "build", "--tag", tag, "--file", dockerfile, "." ],
run: [ "container", "run", "--rm", tag ] }
end

def push(export_action = "registry", tag_as_dirty: false, no_cache: false)
build = apple_container :build,
*platform_options(arches),
*build_tag_options(tag_as_dirty: tag_as_dirty),
*build_options,
*([ "--no-cache" ] if no_cache),
build_context,
"2>&1"

case export_action
when "registry"
combine build, *build_tag_names(tag_as_dirty: tag_as_dirty).map { |tag|
apple_container(:image, :push, *registry_scheme_options, tag)
}
when "docker"
build
else
raise BuilderError, "The apple-container engine only supports registry and local image-store output"
end
end

def build_options
[ *build_labels, *build_args, *build_secrets, *build_dockerfile, *build_target, *build_ssh ]
end

def push_env
if (socket = ssh_socket)
{ "SSH_AUTH_SOCK" => socket }
else
{}
end
end
Comment on lines +63 to +69

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, fixed in f33e335.

The suggested patch raises NameError though — Ruby parses socket in the body as a
method call, since the assignment is lexically later on the line. Assigning first works:

socket = ssh_socket

{ "CONTAINER_DEFAULT_PLATFORM" => "" }.tap do |env|
  env["SSH_AUTH_SOCK"] = socket if socket
end


private
def build_ssh
[ "--ssh", "default" ] if ssh.present?
end

def build_secrets
secrets.keys.flat_map do |secret|
[ "--secret", "id=#{Kamal::Utils.escape_shell_value(secret)},env=#{Kamal::Utils.escape_shell_value(secret)}" ]
end
end

def registry_scheme_options
[ "--scheme", registry_config.scheme ] if registry_config.scheme.present?
end

def ssh_socket
source = ssh&.split("=", 2)&.[](1)

case source
when /\A\$(\w+)\z/
ENV[Regexp.last_match(1)]
when /\A\$\{(\w+)\}\z/
ENV[Regexp.last_match(1)]
else
source
end
end

def platform_options(arches)
arches.flat_map { |arch| [ "--platform", "linux/#{arch}" ] }
end
end
21 changes: 21 additions & 0 deletions lib/kamal/commands/builder/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,27 @@ def clean
docker :image, :rm, "--force", config.absolute_image
end

def ensure_installed
ensure_docker_installed
end

def install_error(output)
output.match?(/command not found/) ?
"Docker is not installed locally" :
"Docker buildx plugin is not installed locally"
end

# The local registry runs alongside the builder, so it speaks the builder's engine.
def local_registry
Kamal::Commands::Registry.new(config)
end

# Plain argv, not SSHKit commands: these run through Kernel#system and Open3.
def build_check_commands(dockerfile:, tag:)
{ build: [ "docker", "buildx", "build", "--tag", tag, "--file", dockerfile, "." ],
run: [ "docker", "run", "--rm", tag ] }
end

def push(export_action = "registry", tag_as_dirty: false, no_cache: false)
docker :buildx, :build,
"--output=type=#{export_action}",
Expand Down
10 changes: 6 additions & 4 deletions lib/kamal/commands/registry.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
class Kamal::Commands::Registry < Kamal::Commands::Base
LOCAL_REGISTRY_CONTAINER = "kamal-docker-registry"

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

Expand All @@ -20,15 +22,15 @@ 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"),
docker(:start, LOCAL_REGISTRY_CONTAINER),
docker(:run, "--detach", "-p", "127.0.0.1:#{registry_config.local_port}:5000", "--name", LOCAL_REGISTRY_CONTAINER, "registry:3"),
by: "||"
end

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

Expand Down
48 changes: 48 additions & 0 deletions lib/kamal/commands/registry/apple_container.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
class Kamal::Commands::Registry::AppleContainer < Kamal::Commands::Registry
def login(registry_config: nil)
registry_config ||= config.registry

return if registry_config.local?

pipe \
[ :echo, sensitive(Kamal::Utils.escape_shell_value(registry_config.password)) ],
apple_container(
:registry, :login,
*registry_scheme_options(registry_config),
"--username", sensitive(Kamal::Utils.escape_shell_value(registry_config.username)),
"--password-stdin",
server_for(registry_config))
end

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

apple_container :registry, :logout, server_for(registry_config)
end

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

combine \
apple_container(:start, LOCAL_REGISTRY_CONTAINER),
apple_container(:run, "--detach", "-p", "127.0.0.1:#{registry_config.local_port}:5000", "--name", LOCAL_REGISTRY_CONTAINER, "registry:3"),
by: "||"
end

def remove
combine \
apple_container(:stop, LOCAL_REGISTRY_CONTAINER),
apple_container(:delete, LOCAL_REGISTRY_CONTAINER),
by: "&&"
end

private
def registry_scheme_options(registry_config)
[ "--scheme", registry_config.scheme ] if registry_config.scheme.present?
end

# `container` has no implicit Docker Hub default.
def server_for(registry_config)
registry_config.server.presence || "docker.io"
end
end
8 changes: 8 additions & 0 deletions lib/kamal/configuration/builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,14 @@ def driver
builder_config.fetch("driver", "docker-container")
end

def engine
builder_config.fetch("engine", "docker")
end

def apple_container?
engine == "apple-container"
end

def pack_builder
builder_config["pack"]["builder"] if pack?
end
Expand Down
15 changes: 15 additions & 0 deletions lib/kamal/configuration/docs/builder.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,21 @@
# Options go under the builder key in the root configuration.
builder:

# Engine
#
# The local container engine used to build and push images. Set this to
# `apple-container` to build with Apple's `container` CLI instead of Docker;
# the deployment servers still use Docker. Needs Apple silicon and macOS 26,
# and is tested against `container` 1.2.2.
#
# It supports no remote or non-local builds, buildpacks, cache exports,
# attestations, custom drivers, or custom SSH agents. `build remove` stops its
# shared builder rather than deleting it, and a localhost registry has to be
# removed before switching engines.
#
# Defaults to docker:
engine: docker

# Arch
#
# The architectures to build for — you can set an array or just a single value.
Expand Down
4 changes: 4 additions & 0 deletions lib/kamal/configuration/docs/registry.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ registry:
# Here’s the final configuration:
registry:
server: <your registry region>-docker.pkg.dev
# Only Apple's `container` CLI reads this; Docker ignores it. A localhost
# registry already uses plain HTTP, so set this to `http` only for a
# plain-HTTP registry on another host. One of `auto`, `http`, or `https`:
scheme: auto
username: _json_key_base64
password:
- KAMAL_REGISTRY_PASSWORD
Expand Down
Loading