Simplify the builders configuration

1. Add driver as an option, defaulting to `docker-container`. For a
   "native" build you can set it to `docker`
2. Set arch as a array of architectures to build for, defaulting to
   `[ "amd64", "arm64" ]` unless you are using the docker driver in
   which case we default to not setting a platform
3. Remote is now just a connection string for the remote builder
4. If remote is set, we only use it for non-local arches, if we are
   only building for the local arch, we'll ignore it.

Examples:

On arm64, build for arm64 locally, amd64 remotely or
On amd64, build for amd64 locally, arm64 remotely:

```yaml
builder:
  remote: ssh://docker@docker-builder
```

On arm64, build amd64 on remote,
On amd64 build locally:

```yaml
builder:
  arch:
    - amd64
  remote:
    host: ssh://docker@docker-builder
```

Build amd64 on local:

```yaml
builder:
  arch:
    - amd64
```

Use docker driver, building for local arch:

```yaml
builder:
  driver: docker
```
This commit is contained in:
Donal McBreen
2024-08-01 14:06:41 +01:00
committed by Donal McBreen
parent cffb6c3d7e
commit 56268d724d
19 changed files with 140 additions and 206 deletions

View File

@@ -66,7 +66,7 @@ class Kamal::Cli::Build < Kamal::Cli::Base
desc "create", "Create a build setup" desc "create", "Create a build setup"
def create def create
if (remote_host = KAMAL.config.builder.remote_host) if (remote_host = KAMAL.config.builder.remote)
connect_to_remote_host(remote_host) connect_to_remote_host(remote_host)
end end

View File

@@ -2,7 +2,7 @@ require "active_support/core_ext/string/filters"
class Kamal::Commands::Builder < Kamal::Commands::Base class Kamal::Commands::Builder < Kamal::Commands::Base
delegate :create, :remove, :push, :clean, :pull, :info, :buildx_inspect, :validate_image, :first_mirror, to: :target delegate :create, :remove, :push, :clean, :pull, :info, :buildx_inspect, :validate_image, :first_mirror, to: :target
delegate :multiarch?, :local?, :remote?, to: "config.builder" delegate :local?, :remote?, to: "config.builder"
include Clone include Clone

View File

@@ -5,8 +5,8 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
delegate :argumentize, to: Kamal::Utils delegate :argumentize, to: Kamal::Utils
delegate \ delegate \
:args, :secrets, :dockerfile, :target, :local_arch, :remote_arch, :remote_host, :args, :secrets, :dockerfile, :target, :arches, :local_arches, :remote_arches, :remote,
:cache_from, :cache_to, :multiarch?, :ssh, :driver, :docker_driver?, :cache_from, :cache_to, :ssh, :driver, :docker_driver?,
to: :builder_config to: :builder_config
def clean def clean
@@ -16,7 +16,7 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
def push def push
docker :build, docker :build,
"--push", "--push",
*platform_options, *platform_options(arches),
*([ "--builder", builder_name ] unless docker_driver?), *([ "--builder", builder_name ] unless docker_driver?),
*build_options, *build_options,
build_context build_context
@@ -33,7 +33,7 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
end end
def buildx_inspect def buildx_inspect
docker :buildx, :inspect, builder_name docker :buildx, :inspect, builder_name unless docker_driver?
end end
def build_options def build_options
@@ -104,4 +104,8 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
def context_host(builder_name) def context_host(builder_name)
docker :context, :inspect, builder_name, "--format", ENDPOINT_DOCKER_HOST_INSPECT docker :context, :inspect, builder_name, "--format", ENDPOINT_DOCKER_HOST_INSPECT
end end
def platform_options(arches)
argumentize "--platform", arches.map { |arch| "linux/#{arch}" }.join(",") if arches.any?
end
end end

View File

@@ -8,18 +8,14 @@ class Kamal::Commands::Builder::Hybrid < Kamal::Commands::Builder::Remote
private private
def builder_name def builder_name
"kamal-hybrid-#{driver}-#{local_arch}-#{remote_arch}-#{remote_host.gsub(/[^a-z0-9_-]/, "-")}" "kamal-hybrid-#{driver}-#{remote.gsub(/[^a-z0-9_-]/, "-")}"
end end
def create_local_buildx def create_local_buildx
docker :buildx, :create, "--name", builder_name, "--platform", "linux/#{local_arch}", "--driver=#{driver}" docker :buildx, :create, *platform_options(local_arches), "--name", builder_name, "--driver=#{driver}"
end end
def append_remote_buildx def append_remote_buildx
docker :buildx, :create, "--append", "--name", builder_name, builder_name, "--platform", "linux/#{remote_arch}" docker :buildx, :create, *platform_options(remote_arches), "--append", "--name", builder_name, builder_name
end
def platform
"linux/#{local_arch},linux/#{remote_arch}"
end end
end end

View File

@@ -11,14 +11,4 @@ class Kamal::Commands::Builder::Local < Kamal::Commands::Builder::Base
def builder_name def builder_name
"kamal-local-#{driver}" "kamal-local-#{driver}"
end end
def platform_options
if multiarch?
if local_arch
[ "--platform", "linux/#{local_arch}" ]
else
[ "--platform", "linux/amd64,linux/arm64" ]
end
end
end
end end

View File

@@ -17,22 +17,13 @@ class Kamal::Commands::Builder::Remote < Kamal::Commands::Builder::Base
docker(:buildx, :ls) docker(:buildx, :ls)
end end
def push
docker :build,
"--push",
*platform_options,
"--builder", builder_name,
*build_options,
build_context
end
private private
def builder_name def builder_name
"kamal-remote-#{driver}-#{remote_arch}-#{remote_host.gsub(/[^a-z0-9_-]/, "-")}" "kamal-remote-#{driver}-#{remote.gsub(/[^a-z0-9_-]/, "-")}"
end end
def create_remote_context def create_remote_context
docker :context, :create, builder_name, "--description", "'#{builder_name} host'", "--docker", "'host=#{remote_host}'" docker :context, :create, builder_name, "--description", "'#{builder_name} host'", "--docker", "'host=#{remote}'"
end end
def remove_remote_context def remove_remote_context
@@ -40,18 +31,10 @@ class Kamal::Commands::Builder::Remote < Kamal::Commands::Builder::Base
end end
def create_buildx def create_buildx
docker :buildx, :create, "--name", builder_name, builder_name, "--platform", platform docker :buildx, :create, "--name", builder_name, builder_name
end end
def remove_buildx def remove_buildx
docker :buildx, :rm, builder_name docker :buildx, :rm, builder_name
end end
def platform_options
[ "--platform", platform ]
end
def platform
"linux/#{remote_arch}"
end
end end

View File

@@ -19,16 +19,38 @@ class Kamal::Configuration::Builder
builder_config builder_config
end end
def multiarch? def remote
builder_config["multiarch"] != false builder_config["remote"]
end end
def local? def arches
!!builder_config["local"] Array(builder_config.fetch("arch", default_arch))
end
def local_arches
@local_arches ||= if remote
uname_m = `uname -m`.strip
local_arch = uname_m == "x86_64" ? "amd64" : uname_m
arches & [ local_arch ]
else
arches
end
end
def remote_arches
@remote_arches ||= if remote
arches - local_arches
else
[]
end
end end
def remote? def remote?
!!builder_config["remote"] remote_arches.any?
end
def local?
arches.empty? || local_arches.any?
end end
def cached? def cached?
@@ -59,18 +81,6 @@ class Kamal::Configuration::Builder
builder_config.fetch("driver", "docker-container") builder_config.fetch("driver", "docker-container")
end end
def local_arch
builder_config["local"]["arch"] if local?
end
def remote_arch
builder_config["remote"]["arch"] if remote?
end
def remote_host
builder_config["remote"]["host"] if remote?
end
def cache_from def cache_from
if cached? if cached?
case builder_config["cache"]["type"] case builder_config["cache"]["type"]
@@ -120,27 +130,14 @@ class Kamal::Configuration::Builder
private private
def valid? def valid?
if multiarch? if docker_driver?
if local? raise ArgumentError, "Invalid builder configuration: the `docker` driver does not not support remote builders" if remote
raise ArgumentError, "Invalid builder configuration: local configuration, arch required" unless local_arch raise ArgumentError, "Invalid builder configuration: the `docker` driver does not not support caching" if cached?
end raise ArgumentError, "Invalid builder configuration: the `docker` driver does not not support multiple arches" if arches.many?
if remote?
raise ArgumentError, "Invalid builder configuration: remote configuration, arch required" unless remote_arch
raise ArgumentError, "Invalid builder configuration: remote configuration, arch required" unless remote_host
end
if docker_driver?
raise ArgumentError, "Invalid builder configuration: the docker driver does not support multiarch builds"
end
else
raise ArgumentError, "Invalid builder configuration: multiarch must be enabled for local configuration" if local?
raise ArgumentError, "Invalid builder configuration: multiarch must be enabled for remote configuration" if remote?
end end
if @options["cache"] && @options["cache"]["type"] if @options["cache"] && @options["cache"]["type"]
raise ArgumentError, "Invalid cache type: #{@options["cache"]["type"]}" unless [ "gha", "registry" ].include?(@options["cache"]["type"]) raise ArgumentError, "Invalid cache type: #{@options["cache"]["type"]}" unless [ "gha", "registry" ].include?(@options["cache"]["type"])
raise ArgumentError, "The docker driver does not support caching" if docker_driver?
end end
end end
@@ -179,4 +176,8 @@ class Kamal::Configuration::Builder
def pwd_sha def pwd_sha
Digest::SHA256.hexdigest(Dir.pwd)[0..12] Digest::SHA256.hexdigest(Dir.pwd)[0..12]
end end
def default_arch
docker_driver? ? [] : [ "amd64", "arm64" ]
end
end end

View File

@@ -3,7 +3,7 @@
# The builder configuration controls how the application is built with `docker build` # The builder configuration controls how the application is built with `docker build`
# #
# If no configuration is specified, Kamal will: # If no configuration is specified, Kamal will:
# 1. Create a buildx context called `kamal-<service>-multiarch` # 1. Create a buildx context called `kamal-local-docker-container`, using the docker-container driver
# 2. Use `docker build` to build a multiarch image for linux/amd64,linux/arm64 with that context # 2. Use `docker build` to build a multiarch image for linux/amd64,linux/arm64 with that context
# #
# See https://kamal-deploy.org/docs/configuration/builder-examples/ for more information # See https://kamal-deploy.org/docs/configuration/builder-examples/ for more information
@@ -12,41 +12,29 @@
# #
# Options go under the builder key in the root configuration. # Options go under the builder key in the root configuration.
builder: builder:
# Multiarch
#
# Enables multiarch builds, defaults to `true`
multiarch: false
# Driver # Driver
# #
# The build driver to use, defaults to `docker-container` # The build driver to use, defaults to `docker-container`
driver: docker driver: docker
# Local configuration # Arch
# #
# The build configuration for local builds, only used if multiarch is enabled (the default) # The architectures to build for, defaults to `[ amd64, arm64 ]`
# # Unless you are using the docker driver, when it defaults to the local architecture
# If there is no remote configuration, by default we build for amd64 and arm64. # You can set an array or just a single value
# If you only want to build for one architecture, you can specify it here. arch:
# The docker socket is optional and uses the default docker host socket when not specified - amd64
local:
arch: amd64
host: /var/run/docker.sock
# Remote configuration # Remote configuration
# #
# The build configuration for remote builds, also only used if multiarch is enabled. # If you have a remote builder, you can configure it here
# The arch is required and can be either amd64 or arm64. remote: ssh://docker@docker-builder
remote:
arch: arm64
host: ssh://docker@docker-builder
# Builder cache # Builder cache
# #
# The type must be either 'gha' or 'registry' # The type must be either 'gha' or 'registry'
# #
# The image is only used for registry cache # The image is only used for registry cache. Not compatible with the docker driver
cache: cache:
type: registry type: registry
options: mode=max options: mode=max

View File

@@ -28,7 +28,11 @@ class Kamal::Configuration::Validator
elsif key == "hosts" elsif key == "hosts"
validate_servers! value validate_servers! value
elsif example_value.is_a?(Array) elsif example_value.is_a?(Array)
validate_array_of! value, example_value.first.class if key == "arch"
validate_array_of_or_type! value, example_value.first.class
else
validate_array_of! value, example_value.first.class
end
elsif example_value.is_a?(Hash) elsif example_value.is_a?(Hash)
case key.to_s case key.to_s
when "options", "args" when "options", "args"
@@ -71,6 +75,16 @@ class Kamal::Configuration::Validator
value.is_a?(String) || value.is_a?(Symbol) || value.is_a?(Numeric) || value.is_a?(TrueClass) || value.is_a?(FalseClass) value.is_a?(String) || value.is_a?(Symbol) || value.is_a?(Numeric) || value.is_a?(TrueClass) || value.is_a?(FalseClass)
end end
def validate_array_of_or_type!(value, type)
if value.is_a?(Array)
validate_array_of! value, type
else
validate_type! value, type
end
rescue Kamal::ConfigurationError
type_error(Array, type)
end
def validate_array_of!(array, type) def validate_array_of!(array, type)
validate_type! array, Array validate_type! array, Array

View File

@@ -26,7 +26,7 @@ class CliBuildTest < CliTestCase
assert_match /Cloning repo into build directory/, output assert_match /Cloning repo into build directory/, output
assert_match /git -C #{Dir.tmpdir}\/kamal-clones\/app-#{pwd_sha} clone #{Dir.pwd}/, output assert_match /git -C #{Dir.tmpdir}\/kamal-clones\/app-#{pwd_sha} clone #{Dir.pwd}/, output
assert_match /docker --version && docker buildx version/, output assert_match /docker --version && docker buildx version/, output
assert_match /docker build --push --platform linux\/amd64,linux\/arm64 --builder kamal-local -t dhh\/app:999 -t dhh\/app:latest --label service="app" --file Dockerfile \. as .*@localhost/, output assert_match /docker build --push --platform linux\/amd64,linux\/arm64 --builder kamal-local-docker-container -t dhh\/app:999 -t dhh\/app:latest --label service="app" --file Dockerfile \. as .*@localhost/, output
end end
end end
end end
@@ -49,7 +49,7 @@ class CliBuildTest < CliTestCase
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:git, "-C", build_directory, :submodule, :update, "--init") SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:git, "-C", build_directory, :submodule, :update, "--init")
SSHKit::Backend::Abstract.any_instance.expects(:execute) SSHKit::Backend::Abstract.any_instance.expects(:execute)
.with(:docker, :build, "--push", "--platform", "linux/amd64,linux/arm64", "--builder", "kamal-local", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".") .with(:docker, :build, "--push", "--platform", "linux/amd64,linux/arm64", "--builder", "kamal-local-docker-container", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".")
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:git, "-C", anything, :"rev-parse", :HEAD) .with(:git, "-C", anything, :"rev-parse", :HEAD)
@@ -74,7 +74,7 @@ class CliBuildTest < CliTestCase
assert_no_match /Cloning repo into build directory/, output assert_no_match /Cloning repo into build directory/, output
assert_hook_ran "pre-build", output, **hook_variables assert_hook_ran "pre-build", output, **hook_variables
assert_match /docker --version && docker buildx version/, output assert_match /docker --version && docker buildx version/, output
assert_match /docker build --push --platform linux\/amd64,linux\/arm64 --builder kamal-local -t dhh\/app:999 -t dhh\/app:latest --label service="app" --file Dockerfile . as .*@localhost/, output assert_match /docker build --push --platform linux\/amd64,linux\/arm64 --builder kamal-local-docker-container -t dhh\/app:999 -t dhh\/app:latest --label service="app" --file Dockerfile . as .*@localhost/, output
end end
end end
@@ -120,10 +120,10 @@ class CliBuildTest < CliTestCase
.with(:docker, "--version", "&&", :docker, :buildx, "version") .with(:docker, "--version", "&&", :docker, :buildx, "version")
SSHKit::Backend::Abstract.any_instance.expects(:execute) SSHKit::Backend::Abstract.any_instance.expects(:execute)
.with(:docker, :buildx, :create, "--name", "kamal-local", "--driver=docker-container") .with(:docker, :buildx, :create, "--name", "kamal-local-docker-container", "--driver=docker-container")
SSHKit::Backend::Abstract.any_instance.expects(:execute) SSHKit::Backend::Abstract.any_instance.expects(:execute)
.with(:docker, :buildx, :inspect, "kamal-local") .with(:docker, :buildx, :inspect, "kamal-local-docker-container")
.raises(SSHKit::Command::Failed.new("no builder")) .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(:execute).with { |*args| args.first.start_with?("git") }
@@ -137,7 +137,7 @@ class CliBuildTest < CliTestCase
.returns("") .returns("")
SSHKit::Backend::Abstract.any_instance.expects(:execute) SSHKit::Backend::Abstract.any_instance.expects(:execute)
.with(:docker, :build, "--push", "--platform", "linux/amd64,linux/arm64", "--builder", "kamal-local", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".") .with(:docker, :build, "--push", "--platform", "linux/amd64,linux/arm64", "--builder", "kamal-local-docker-container", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".")
run_command("push").tap do |output| run_command("push").tap do |output|
assert_match /WARN Missing compatible builder, so creating a new one first/, output assert_match /WARN Missing compatible builder, so creating a new one first/, output
@@ -203,23 +203,23 @@ class CliBuildTest < CliTestCase
test "create" do test "create" do
run_command("create").tap do |output| run_command("create").tap do |output|
assert_match /docker buildx create --name kamal-local --driver=docker-container/, output assert_match /docker buildx create --name kamal-local-docker-container --driver=docker-container/, output
end end
end end
test "create remote" do test "create remote" do
run_command("create", fixture: :with_remote_builder).tap do |output| run_command("create", fixture: :with_remote_builder).tap do |output|
assert_match "Running /usr/bin/env true on 1.1.1.5", output assert_match "Running /usr/bin/env true on 1.1.1.5", output
assert_match "docker context create kamal-remote-amd64-ssh---app-1-1-1-5 --description 'kamal-remote-amd64-ssh---app-1-1-1-5 host' --docker 'host=ssh://app@1.1.1.5'", output assert_match "docker context create kamal-remote-docker-container-ssh---app-1-1-1-5 --description 'kamal-remote-docker-container-ssh---app-1-1-1-5 host' --docker 'host=ssh://app@1.1.1.5'", output
assert_match "docker buildx create --name kamal-remote-amd64-ssh---app-1-1-1-5 kamal-remote-amd64-ssh---app-1-1-1-5 --platform linux/amd64", output assert_match "docker buildx create --name kamal-remote-docker-container-ssh---app-1-1-1-5 kamal-remote-docker-container-ssh---app-1-1-1-5", output
end end
end end
test "create remote with custom ports" do test "create remote with custom ports" do
run_command("create", fixture: :with_remote_builder_and_custom_ports).tap do |output| run_command("create", fixture: :with_remote_builder_and_custom_ports).tap do |output|
assert_match "Running /usr/bin/env true on 1.1.1.5", output assert_match "Running /usr/bin/env true on 1.1.1.5", output
assert_match "docker context create kamal-remote-amd64-ssh---app-1-1-1-5-2122 --description 'kamal-remote-amd64-ssh---app-1-1-1-5-2122 host' --docker 'host=ssh://app@1.1.1.5:2122'", output assert_match "docker context create kamal-remote-docker-container-ssh---app-1-1-1-5-2122 --description 'kamal-remote-docker-container-ssh---app-1-1-1-5-2122 host' --docker 'host=ssh://app@1.1.1.5:2122'", output
assert_match "docker buildx create --name kamal-remote-amd64-ssh---app-1-1-1-5-2122 kamal-remote-amd64-ssh---app-1-1-1-5-2122 --platform linux/amd64", output assert_match "docker buildx create --name kamal-remote-docker-container-ssh---app-1-1-1-5-2122 kamal-remote-docker-container-ssh---app-1-1-1-5-2122", output
end end
end end

View File

@@ -37,7 +37,7 @@ class CliTestCase < ActiveSupport::TestCase
SSHKit::Backend::Abstract.any_instance.stubs(:execute) SSHKit::Backend::Abstract.any_instance.stubs(:execute)
.with { |arg1, arg2| arg1 == :rm && arg2 == ".kamal/locks/app/details" } .with { |arg1, arg2| arg1 == :rm && arg2 == ".kamal/locks/app/details" }
SSHKit::Backend::Abstract.any_instance.stubs(:execute) SSHKit::Backend::Abstract.any_instance.stubs(:execute)
.with(:docker, :buildx, :inspect, "kamal-local") .with(:docker, :buildx, :inspect, "kamal-local-docker-container")
end end
def assert_hook_ran(hook, output, version:, service_version:, hosts:, command:, subcommand: nil, runtime: false) def assert_hook_ran(hook, output, version:, service_version:, hosts:, command:, subcommand: nil, runtime: false)

View File

@@ -5,51 +5,51 @@ class CommandsBuilderTest < ActiveSupport::TestCase
@config = { service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ] } @config = { service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ] }
end end
test "target multiarch by default" do test "target linux/amd64,linux/arm64 locally by default" do
builder = new_builder_command(builder: { "cache" => { "type" => "gha" } }) builder = new_builder_command(builder: { "cache" => { "type" => "gha" } })
assert_equal "local", builder.name assert_equal "local", builder.name
assert_equal \ assert_equal \
"docker build --push --platform linux/amd64,linux/arm64 --builder kamal-local -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile .", "docker build --push --platform linux/amd64,linux/arm64 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile .",
builder.push.join(" ") builder.push.join(" ")
end end
test "target native when multiarch is off" do test "target specified arch locally by default" do
builder = new_builder_command(builder: { "multiarch" => false }) builder = new_builder_command(builder: { "arch" => [ "amd64" ] })
assert_equal "local", builder.name assert_equal "local", builder.name
assert_equal \ assert_equal \
"docker build --push --builder kamal-local -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile .", "docker build --push --platform linux/amd64 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile .",
builder.push.join(" ") builder.push.join(" ")
end end
test "target native cached when multiarch is off and cache is set" do test "build with caching" do
builder = new_builder_command(builder: { "multiarch" => false, "cache" => { "type" => "gha" } }) builder = new_builder_command(builder: { "cache" => { "type" => "gha" } })
assert_equal "local", builder.name assert_equal "local", builder.name
assert_equal \ assert_equal \
"docker build --push --builder kamal-local -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile .", "docker build --push --platform linux/amd64,linux/arm64 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile .",
builder.push.join(" ") builder.push.join(" ")
end end
test "target multiarch remote when local and remote is set" do test "hybrid build if remote is set" do
builder = new_builder_command(builder: { "local" => { "arch" => "arm64" }, "remote" => { "arch" => "amd64", "host" => "ssh://app@127.0.0.1" }, "cache" => { "type" => "gha" } }) builder = new_builder_command(builder: { "remote" => "ssh://app@127.0.0.1", "cache" => { "type" => "gha" } })
assert_equal "hybrid", builder.name assert_equal "hybrid", builder.name
assert_equal \ assert_equal \
"docker build --push --platform linux/arm64,linux/amd64 --builder kamal-hybrid-arm64-amd64-ssh---app-127-0-0-1 -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile .", "docker build --push --platform linux/amd64,linux/arm64 --builder kamal-hybrid-docker-container-ssh---app-127-0-0-1 -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile .",
builder.push.join(" ") builder.push.join(" ")
end end
test "target multiarch local when arch is set" do test "target remote when remote set and arch is non local" do
builder = new_builder_command(builder: { "local" => { "arch" => "amd64" } }) builder = new_builder_command(builder: { "arch" => [ "#{remote_arch}" ], "remote" => "ssh://app@host", "cache" => { "type" => "gha" } })
assert_equal "local", builder.name
assert_equal \
"docker build --push --platform linux/amd64 --builder kamal-local -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile .",
builder.push.join(" ")
end
test "target native remote when only remote is set" do
builder = new_builder_command(builder: { "remote" => { "arch" => "amd64", "host" => "ssh://app@host" }, "cache" => { "type" => "gha" } })
assert_equal "remote", builder.name assert_equal "remote", builder.name
assert_equal \ assert_equal \
"docker build --push --platform linux/amd64 --builder kamal-remote-amd64-ssh---app-host -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile .", "docker build --push --platform linux/#{remote_arch} --builder kamal-remote-docker-container-ssh---app-host -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile .",
builder.push.join(" ")
end
test "target local when remote set and arch is local" do
builder = new_builder_command(builder: { "arch" => [ "#{local_arch}" ], "remote" => "ssh://app@host", "cache" => { "type" => "gha" } })
assert_equal "local", builder.name
assert_equal \
"docker build --push --platform linux/#{local_arch} --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile .",
builder.push.join(" ") builder.push.join(" ")
end end
@@ -93,28 +93,21 @@ class CommandsBuilderTest < ActiveSupport::TestCase
test "build context" do test "build context" do
builder = new_builder_command(builder: { "context" => ".." }) builder = new_builder_command(builder: { "context" => ".." })
assert_equal \ assert_equal \
"docker build --push --platform linux/amd64,linux/arm64 --builder kamal-local -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ..", "docker build --push --platform linux/amd64,linux/arm64 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ..",
builder.push.join(" ") builder.push.join(" ")
end end
test "native push with build args" do test "push with build args" do
builder = new_builder_command(builder: { "multiarch" => false, "args" => { "a" => 1, "b" => 2 } })
assert_equal \
"docker build --push --builder kamal-local -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --build-arg a=\"1\" --build-arg b=\"2\" --file Dockerfile .",
builder.push.join(" ")
end
test "multiarch push with build args" do
builder = new_builder_command(builder: { "args" => { "a" => 1, "b" => 2 } }) builder = new_builder_command(builder: { "args" => { "a" => 1, "b" => 2 } })
assert_equal \ assert_equal \
"docker build --push --platform linux/amd64,linux/arm64 --builder kamal-local -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --build-arg a=\"1\" --build-arg b=\"2\" --file Dockerfile .", "docker build --push --platform linux/amd64,linux/arm64 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --build-arg a=\"1\" --build-arg b=\"2\" --file Dockerfile .",
builder.push.join(" ") builder.push.join(" ")
end end
test "native push with build secrets" do test "push with build secrets" do
builder = new_builder_command(builder: { "multiarch" => false, "secrets" => [ "a", "b" ] }) builder = new_builder_command(builder: { "secrets" => [ "a", "b" ] })
assert_equal \ assert_equal \
"docker build --push --builder kamal-local -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --secret id=\"a\" --secret id=\"b\" --file Dockerfile .", "docker build --push --platform linux/amd64,linux/arm64 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --secret id=\"a\" --secret id=\"b\" --file Dockerfile .",
builder.push.join(" ") builder.push.join(" ")
end end
@@ -130,31 +123,10 @@ class CommandsBuilderTest < ActiveSupport::TestCase
assert_equal "docker inspect -f '{{ .Config.Labels.service }}' dhh/app:123 | grep -x app || (echo \"Image dhh/app:123 is missing the 'service' label\" && exit 1)", new_builder_command.validate_image.join(" ") assert_equal "docker inspect -f '{{ .Config.Labels.service }}' dhh/app:123 | grep -x app || (echo \"Image dhh/app:123 is missing the 'service' label\" && exit 1)", new_builder_command.validate_image.join(" ")
end end
test "multiarch context build" do test "context build" do
builder = new_builder_command(builder: { "context" => "./foo" }) builder = new_builder_command(builder: { "context" => "./foo" })
assert_equal \ assert_equal \
"docker build --push --platform linux/amd64,linux/arm64 --builder kamal-local -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ./foo", "docker build --push --platform linux/amd64,linux/arm64 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ./foo",
builder.push.join(" ")
end
test "native context build" do
builder = new_builder_command(builder: { "multiarch" => false, "context" => "./foo" })
assert_equal \
"docker build --push --builder kamal-local -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ./foo",
builder.push.join(" ")
end
test "cached context build" do
builder = new_builder_command(builder: { "multiarch" => false, "context" => "./foo", "cache" => { "type" => "gha" } })
assert_equal \
"docker build --push --builder kamal-local -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile ./foo",
builder.push.join(" ")
end
test "remote context build" do
builder = new_builder_command(builder: { "remote" => { "arch" => "amd64", "host" => "ssh://app@host" }, "context" => "./foo" })
assert_equal \
"docker build --push --platform linux/amd64 --builder kamal-remote-amd64-ssh---app-host -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ./foo",
builder.push.join(" ") builder.push.join(" ")
end end
@@ -171,4 +143,12 @@ class CommandsBuilderTest < ActiveSupport::TestCase
def build_directory def build_directory
"#{Dir.tmpdir}/kamal-clones/app/kamal/" "#{Dir.tmpdir}/kamal-clones/app/kamal/"
end end
def local_arch
`uname -m`.strip == "x86_64" ? "amd64" : "arm64"
end
def remote_arch
`uname -m`.strip == "x86_64" ? "arm64" : "amd64"
end
end end

View File

@@ -14,45 +14,29 @@ class ConfigurationBuilderTest < ActiveSupport::TestCase
} }
end end
test "multiarch?" do
assert_equal true, config.builder.multiarch?
end
test "setting multiarch to false" do
@deploy_with_builder_option[:builder] = { "multiarch" => false }
assert_equal false, config_with_builder_option.builder.multiarch?
end
test "local?" do test "local?" do
assert_equal false, config.builder.local? assert_equal true, config.builder.local?
end end
test "remote?" do test "remote?" do
assert_equal false, config.builder.remote? assert_equal false, config.builder.remote?
end end
test "remote_arch" do test "remote" do
assert_nil config.builder.remote_arch assert_nil config.builder.remote
end
test "remote_host" do
assert_nil config.builder.remote_host
end end
test "setting both local and remote configs" do test "setting both local and remote configs" do
@deploy_with_builder_option[:builder] = { @deploy_with_builder_option[:builder] = {
"local" => { "arch" => "arm64" }, "arch" => [ "amd64", "arm64" ],
"remote" => { "arch" => "amd64", "host" => "ssh://root@192.168.0.1" } "remote" => "ssh://root@192.168.0.1"
} }
assert_equal true, config_with_builder_option.builder.local? assert_equal true, config_with_builder_option.builder.local?
assert_equal true, config_with_builder_option.builder.remote? assert_equal true, config_with_builder_option.builder.remote?
assert_equal "amd64", config_with_builder_option.builder.remote_arch assert_equal [ "amd64", "arm64" ], config_with_builder_option.builder.arches
assert_equal "ssh://root@192.168.0.1", config_with_builder_option.builder.remote_host assert_equal "ssh://root@192.168.0.1", config_with_builder_option.builder.remote
assert_equal "arm64", config_with_builder_option.builder.local_arch
end end
test "cached?" do test "cached?" do

View File

@@ -90,10 +90,8 @@ class ConfigurationValidationTest < ActiveSupport::TestCase
test "builder" do test "builder" do
assert_error "builder: unknown key: foo", builder: { "foo" => "bar" } assert_error "builder: unknown key: foo", builder: { "foo" => "bar" }
assert_error "builder/remote: should be a hash", builder: { "remote" => true } assert_error "builder/remote: should be a string", builder: { "remote" => { "foo" => "bar" } }
assert_error "builder/remote: unknown key: foo", builder: { "remote" => { "foo" => "bar" } } assert_error "builder/arch: should be an array or a string", builder: { "arch" => {} }
assert_error "builder/local: unknown key: foo", builder: { "local" => { "foo" => "bar" } }
assert_error "builder/remote/arch: should be a string", builder: { "remote" => { "arch" => [] } }
assert_error "builder/args: should be a hash", builder: { "args" => [ "foo" ] } assert_error "builder/args: should be a hash", builder: { "args" => [ "foo" ] }
assert_error "builder/cache/options: should be a string", builder: { "cache" => { "options" => [] } } assert_error "builder/cache/options: should be a string", builder: { "cache" => { "options" => [] } }
end end

View File

@@ -36,6 +36,5 @@ accessories:
readiness_delay: 0 readiness_delay: 0
builder: builder:
remote: arch: <%= `uname -m`.strip == "x86_64" ? "arm64" : "amd64" %>
arch: amd64 remote: ssh://app@1.1.1.5
host: ssh://app@1.1.1.5

View File

@@ -40,6 +40,5 @@ ssh:
port: 22 port: 22
builder: builder:
remote: arch: <%= `uname -m`.strip == "x86_64" ? "arm64" : "amd64" %>
arch: amd64 remote: ssh://app@1.1.1.5:2122
host: ssh://app@1.1.1.5:2122

View File

@@ -23,7 +23,6 @@ registry:
username: root username: root
password: root password: root
builder: builder:
multiarch: false
driver: docker driver: docker
args: args:
COMMIT_SHA: <%= `git rev-parse HEAD` %> COMMIT_SHA: <%= `git rev-parse HEAD` %>

View File

@@ -17,7 +17,6 @@ registry:
username: root username: root
password: root password: root
builder: builder:
multiarch: false
driver: docker driver: docker
args: args:
COMMIT_SHA: <%= `git rev-parse HEAD` %> COMMIT_SHA: <%= `git rev-parse HEAD` %>

View File

@@ -77,7 +77,7 @@ class MainTest < IntegrationTest
assert_equal "app-#{version}", config[:service_with_version] assert_equal "app-#{version}", config[:service_with_version]
assert_equal [], config[:volume_args] assert_equal [], config[:volume_args]
assert_equal({ user: "root", port: 22, keepalive: true, keepalive_interval: 30, log_level: :fatal }, config[:ssh_options]) assert_equal({ user: "root", port: 22, keepalive: true, keepalive_interval: 30, log_level: :fatal }, config[:ssh_options])
assert_equal({ "multiarch" => false, "args" => { "COMMIT_SHA" => version } }, config[:builder]) assert_equal({ "driver" => "docker", "args" => { "COMMIT_SHA" => version } }, config[:builder])
assert_equal [ "--log-opt", "max-size=\"10m\"" ], config[:logging] assert_equal [ "--log-opt", "max-size=\"10m\"" ], config[:logging]
assert_equal({ "cmd"=>"wget -qO- http://localhost > /dev/null || exit 1", "interval"=>"1s", "max_attempts"=>3, "port"=>3000, "path"=>"/up", "cord"=>"/tmp/kamal-cord", "log_lines"=>50 }, config[:healthcheck]) assert_equal({ "cmd"=>"wget -qO- http://localhost > /dev/null || exit 1", "interval"=>"1s", "max_attempts"=>3, "port"=>3000, "path"=>"/up", "cord"=>"/tmp/kamal-cord", "log_lines"=>50 }, config[:healthcheck])
end end