Compare commits

..

44 Commits

Author SHA1 Message Date
Donal McBreen
4435fdf6fc Allow the driver to be set 2024-08-01 11:57:43 +01:00
Donal McBreen
2dd0ccc276 docker buildx build -> docker build 2024-08-01 10:18:56 +01:00
Donal McBreen
efb2a5d7c6 Get tests passing 2024-08-01 10:18:56 +01:00
Donal McBreen
e7431f9832 Simplfy choosing a builder 2024-08-01 10:18:56 +01:00
Donal McBreen
cf80932216 Validate multiarch configuration
Remote and local are only allowed when multiarch is enabled.
Remote requires a host and arch, local only requires an arch.
2024-08-01 10:18:56 +01:00
Donal McBreen
cf81837737 Rip out context_hosts checks
The remote host is now encoded in the builder name so we don't need
to check it. We'll just do an inspect to confirm the builder exists.
2024-08-01 10:18:56 +01:00
Donal McBreen
8567ce9bf9 Move multiarch remote builder to hybrid builder
Include the host name in the builder name, so we can have one builder
per host/arch across all kamal projects.

Inherit from the remote builder. The difference in the hybrid builder
is that we create a local buildx instance and append the remote context
to it.
2024-08-01 10:18:56 +01:00
Donal McBreen
5d8e4dee13 Create a context for local builds
This ensures we use the docker-container driver and not whatever the
local default is.
2024-08-01 10:18:56 +01:00
Donal McBreen
10a7645ab8 Local build doesn't need a builder 2024-08-01 10:18:56 +01:00
Donal McBreen
c93f0f3048 Dump native builder
We already ensure that buildx is installed, so let's always use it.
2024-08-01 10:18:56 +01:00
Donal McBreen
6cedec68e3 Move native remote to just remote
It's just a remote builder, that will build whichever platform is asked
for, so let's remove the "native" part.

We'll also remove the service name from the builder name, so multiple
services can share the same builder.
2024-08-01 10:18:56 +01:00
Donal McBreen
2a4a8ac859 Combine multiarch and native/cache builders
Combine the two builders, as they are almost identical. The only
difference was whether the platforms were set.

The native cached builder wasn't using the context it created, so now
we do.

We'll set the driver to `docker-container` - it seems to be the default
but the Docker docs claim it is `docker`.
2024-08-01 10:18:56 +01:00
Donal McBreen
f48987aa03 Merge pull request #903 from basecamp/integration-test-insecure-registry
Integration test insecure registry
2024-08-01 09:57:17 +01:00
Donal McBreen
ef051eca1b Merge pull request #904 from galori/main
Fixed typo in `env.yml`: "valies" --> "values"
2024-08-01 09:57:03 +01:00
Gall Steinitz
173d44ee0a fixed typo in env.yml: valies --> values 2024-07-31 22:12:21 -07:00
Donal McBreen
4e811372f8 Integration test insecure registry
The integrations tests use their own registry so avoid hitting docker
hub rate limits.

This was using a self signed certificate but instead use
`--insecure-registry` to let the docker daemon use HTTP.
2024-07-31 16:54:00 +01:00
Donal McBreen
ec4aa45852 Bump version for 1.8.1 2024-07-29 09:09:57 +01:00
Donal McBreen
5e11a64181 Merge pull request #891 from basecamp/single-pull
Pull once from hosts that warm registry mirrors
2024-07-22 08:18:48 +01:00
Jeremy Daer
57d9ce177a Pull once from hosts that warm registry mirrors 2024-07-18 09:14:22 -07:00
Donal McBreen
8a98949634 Merge pull request #886 from guoard/patch-2
Remove `--update` flag from `apk add` command
2024-07-16 15:46:37 +01:00
Donal McBreen
0eb9f48082 Merge pull request #887 from basecamp/fix-tests-with-git-config
Fix the tests when you have a git config email set
2024-07-16 13:08:18 +01:00
Donal McBreen
9db6fc0704 Fix the tests when you have a git config email set
The ran ok on CI where we fall back to `whoami`, but failed locally
where there was a git email set.
2024-07-16 12:09:05 +01:00
Donal McBreen
27fede3caa Merge pull request #884 from basecamp/x-config
Add support for configuration extensions
2024-07-16 11:38:28 +01:00
Donal McBreen
29c723f7ec Add support for configuration extensions
Allow blocks prefixed with `x-` in the configuration as a place to
declare reusable blocks with YAML anchors and aliases.

Borrowed from the Docker Compose configuration file format -
https://github.com/compose-spec/compose-spec/blob/main/spec.md#extension

Thanks to @ruyrocha for the suggestion.
2024-07-15 20:47:55 +01:00
Ali Afsharzadeh
2755582c47 Remove --update flag from apk add command 2024-07-15 22:15:25 +03:30
Donal McBreen
fa73d722ea Bump version for 1.8.0 2024-07-15 14:21:23 +01:00
Donal McBreen
c535e4e44f Merge pull request #883 from basecamp/revert-840-main
Revert "Add x25519 gem, support Curve25519"
2024-07-15 13:56:49 +01:00
Donal McBreen
0ea07b1760 Merge pull request #878 from pagbrl/main
feat: Use git email as performer when available
2024-07-15 13:41:17 +01:00
Donal McBreen
03b531f179 Merge pull request #865 from basecamp/clean-envify-env
Ensure envify templates aren't polluted by existing env
2024-07-15 13:41:03 +01:00
Donal McBreen
d8570d1c2c Merge pull request #847 from basecamp/remove-ruby-2.7-from-ci
Remove Ruby 2.7 from CI
2024-07-15 13:40:37 +01:00
Donal McBreen
3fe70b458d Merge pull request #862 from jeromedalbert/bump-sshkit
Bump sshkit to support unbracketed IPv6 addresses
2024-07-15 13:40:18 +01:00
Donal McBreen
ade8b43599 Merge pull request #866 from acidtib/ssh-key-overwrite
Configurable SSH Identity
2024-07-15 13:39:51 +01:00
Donal McBreen
d24fc3ca4e Revert "Add x25519 gem, support Curve25519" 2024-07-15 13:36:50 +01:00
Donal McBreen
7c244bbb98 Merge pull request #879 from basecamp/seed-mirror
Seed docker mirrors by pulling once per mirror first
2024-07-15 13:30:53 +01:00
Donal McBreen
1369c46a83 Seed docker mirrors by pulling once per mirror first
Find the first registry mirror on each host. If we find any, pull the
images on one host per mirror, then do the remainder concurrently.

The initial pulls will seed the mirrors ensuring that we pull the image
from Docker Hub once each.

This works best if there is only one mirror on each host.
2024-07-11 16:20:37 +01:00
Paul Gabriel
deccf1cfaf feat: Use git email as performer when available 2024-07-11 11:19:44 +02:00
Donal McBreen
1573cebadf Merge pull request #868 from nickhammond/env/service
Add ENV['KAMAL_SERVICE'] to hooks
2024-07-10 10:26:59 +01:00
Nick Hammond
85a2926cde Remove the deprecated docker compose version (#869) 2024-06-28 15:00:23 -07:00
Nick Hammond
58a51b079e Add KAMAL_SERVICE to custom hooks and exclude from auditor 2024-06-27 10:52:55 -06:00
Nick Hammond
f1f3fc566f Add ENV['SERVICE'] to hooks 2024-06-27 10:26:11 -06:00
acidtib
44726ff65a overwrite ssh identity 2024-06-26 17:14:13 -06:00
Jerome Dalbert
fd0d4af21f Bump sshkit to support unbracketed IPv6 addresses
Set sshkit minimum version to 1.23.0, which includes an enhancement to
support unbracketed IPv6 addresses.

See https://github.com/capistrano/sshkit/pull/538
2024-06-25 12:17:40 -07:00
Jeremy Daer
13409ada5a Ensure envify templates aren't polluted by existing env
Setting `GITHUB_TOKEN` as in the docs results in reusing the existing
`GITHUB_TOKEN` since `gh` returns that env var if it's set:
```bash
GITHUB_TOKEN=junk gh config get -h github.com oauth_token
junk
```

Using the original env ensures that the templates will be evaluated the
same way regardless of whether envify had been previously invoked.
2024-06-25 11:14:34 -07:00
Donal McBreen
e160852e4d Remove Ruby 2.7 from CI
It's EOL since March 2023.
2024-06-20 08:54:55 +01:00
49 changed files with 475 additions and 429 deletions

View File

@@ -24,25 +24,12 @@ jobs:
strategy:
matrix:
ruby-version:
- "2.7"
- "3.1"
- "3.2"
- "3.3"
gemfile:
- Gemfile
- gemfiles/ruby_2.7.gemfile
- gemfiles/rails_edge.gemfile
exclude:
- ruby-version: "2.7"
gemfile: Gemfile
- ruby-version: "2.7"
gemfile: gemfiles/rails_edge.gemfile
- ruby-version: "3.1"
gemfile: gemfiles/ruby_2.7.gemfile
- ruby-version: "3.2"
gemfile: gemfiles/ruby_2.7.gemfile
- ruby-version: "3.3"
gemfile: gemfiles/ruby_2.7.gemfile
name: ${{ format('Tests (Ruby {0})', matrix.ruby-version) }}
runs-on: ubuntu-latest
continue-on-error: true

View File

@@ -1,7 +1,7 @@
# Use the official Ruby 3.2.0 Alpine image as the base image
FROM ruby:3.2.0-alpine
# Install docker/buildx-bin
# Install docker/buildx-bin
COPY --from=docker/buildx-bin /buildx /usr/libexec/docker/cli-plugins/docker-buildx
# Set the working directory to /kamal
@@ -14,7 +14,7 @@ COPY Gemfile Gemfile.lock kamal.gemspec ./
COPY lib/kamal/version.rb /kamal/lib/kamal/version.rb
# Install system dependencies
RUN apk add --no-cache --update build-base git docker openrc openssh-client-default \
RUN apk add --no-cache build-base git docker openrc openssh-client-default \
&& rc-update add docker boot \
&& gem install bundler --version=2.4.3 \
&& bundle install

View File

@@ -1,7 +1,7 @@
PATH
remote: .
specs:
kamal (1.7.3)
kamal (1.8.1)
activesupport (>= 7.0)
base64 (~> 0.2)
bcrypt_pbkdf (~> 1.0)
@@ -9,9 +9,8 @@ PATH
dotenv (~> 2.8)
ed25519 (~> 1.2)
net-ssh (~> 7.0)
sshkit (>= 1.22.2, < 2.0)
sshkit (>= 1.23.0, < 2.0)
thor (~> 1.2)
x25519 (~> 1.0, >= 1.0.10)
zeitwerk (~> 2.5)
GEM
@@ -154,9 +153,8 @@ GEM
rubocop-rails
ruby-progressbar (1.13.0)
ruby2_keywords (0.0.5)
sshkit (1.22.2)
sshkit (1.23.0)
base64
mutex_m
net-scp (>= 1.1.2)
net-sftp (>= 2.1.2)
net-ssh (>= 2.8.0)
@@ -166,7 +164,6 @@ GEM
concurrent-ruby (~> 1.0)
unicode-display_width (2.5.0)
webrick (1.8.1)
x25519 (1.0.10)
zeitwerk (2.6.12)
PLATFORMS

View File

@@ -1,6 +0,0 @@
source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
gemspec path: "../"
gem "nokogiri", "~> 1.15.0"

View File

@@ -12,13 +12,12 @@ Gem::Specification.new do |spec|
spec.executables = %w[ kamal ]
spec.add_dependency "activesupport", ">= 7.0"
spec.add_dependency "sshkit", ">= 1.22.2", "< 2.0"
spec.add_dependency "sshkit", ">= 1.23.0", "< 2.0"
spec.add_dependency "net-ssh", "~> 7.0"
spec.add_dependency "thor", "~> 1.2"
spec.add_dependency "dotenv", "~> 2.8"
spec.add_dependency "zeitwerk", "~> 2.5"
spec.add_dependency "ed25519", "~> 1.2"
spec.add_dependency "x25519", "~> 1.0", ">= 1.0.10"
spec.add_dependency "bcrypt_pbkdf", "~> 1.0"
spec.add_dependency "concurrent-ruby", "~> 1.2"
spec.add_dependency "base64", "~> 0.2"

View File

@@ -25,12 +25,17 @@ module Kamal::Cli
def initialize(*)
super
@original_env = ENV.to_h.dup
load_envs
load_env
initialize_commander(options_with_subcommand_class_options)
end
private
def load_envs
def reload_env
reset_env
load_env
end
def load_env
if destination = options[:destination]
Dotenv.load(".env.#{destination}", ".env")
else
@@ -38,10 +43,27 @@ module Kamal::Cli
end
end
def reload_envs
def reset_env
replace_env @original_env
end
def replace_env(env)
ENV.clear
ENV.update(@original_env)
load_envs
ENV.update(env)
end
def with_original_env
keeping_current_env do
reset_env
yield
end
end
def keeping_current_env
current_env = ENV.to_h.dup
yield
ensure
replace_env(current_env)
end
def options_with_subcommand_class_options

View File

@@ -30,18 +30,9 @@ class Kamal::Cli::Build < Kamal::Cli::Base
say "Building with uncommitted changes:\n #{uncommitted_changes}", :yellow
end
# Get the command here to ensure the Dir.chdir doesn't interfere with it
push = KAMAL.builder.push
run_locally do
begin
context_hosts = capture_with_info(*KAMAL.builder.context_hosts).split("\n")
if context_hosts != KAMAL.builder.config_context_hosts
warn "Context hosts have changed, so re-creating builder, was: #{context_hosts.join(", ")}], now: #{KAMAL.builder.config_context_hosts.join(", ")}"
cli.remove
cli.create
end
execute *KAMAL.builder.buildx_inspect
rescue SSHKit::Command::Failed => e
if e.message =~ /(context not found|no builder|does not exist)/
warn "Missing compatible builder, so creating a new one first"
@@ -51,6 +42,9 @@ class Kamal::Cli::Build < Kamal::Cli::Base
end
end
# Get the command here to ensure the Dir.chdir doesn't interfere with it
push = KAMAL.builder.push
KAMAL.with_verbosity(:debug) do
Dir.chdir(KAMAL.config.builder.build_directory) { execute *push }
end
@@ -59,11 +53,14 @@ class Kamal::Cli::Build < Kamal::Cli::Base
desc "pull", "Pull app image from registry onto servers"
def pull
on(KAMAL.hosts) do
execute *KAMAL.auditor.record("Pulled image with version #{KAMAL.config.version}"), verbosity: :debug
execute *KAMAL.builder.clean, raise_on_non_zero_exit: false
execute *KAMAL.builder.pull
execute *KAMAL.builder.validate_image
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
@@ -131,4 +128,28 @@ class Kamal::Cli::Build < Kamal::Cli::Base
end
end
end
def mirror_hosts
if KAMAL.hosts.many?
mirror_hosts = Concurrent::Hash.new
on(KAMAL.hosts) do |host|
first_mirror = capture_with_info(*KAMAL.builder.first_mirror).strip.presence
mirror_hosts[first_mirror] ||= host.to_s if first_mirror
rescue SSHKit::Command::Failed => e
raise unless e.message =~ /error calling index: reflect: slice index out of range/
end
mirror_hosts.values
else
[]
end
end
def pull_on_hosts(hosts)
on(hosts) do
execute *KAMAL.auditor.record("Pulled image with version #{KAMAL.config.version}"), verbosity: :debug
execute *KAMAL.builder.clean, raise_on_non_zero_exit: false
execute *KAMAL.builder.pull
execute *KAMAL.builder.validate_image
end
end
end

View File

@@ -191,10 +191,12 @@ class Kamal::Cli::Main < Kamal::Cli::Base
end
if Pathname.new(File.expand_path(env_template_path)).exist?
File.write(env_path, ERB.new(File.read(env_template_path), trim_mode: "-").result, perm: 0600)
# Ensure existing env doesn't pollute template evaluation
content = with_original_env { ERB.new(File.read(env_template_path), trim_mode: "-").result }
File.write(env_path, content, perm: 0600)
unless options[:skip_push]
reload_envs
reload_env
invoke "kamal:cli:env:push", options
end
else

View File

@@ -9,7 +9,7 @@ class Kamal::Commands::Auditor < Kamal::Commands::Base
# Runs remotely
def record(line, **details)
append \
[ :echo, audit_tags(**details).except(:version, :service_version).to_s, line ],
[ :echo, audit_tags(**details).except(:version, :service_version, :service).to_s, line ],
audit_log_file
end

View File

@@ -1,8 +1,8 @@
require "active_support/core_ext/string/filters"
class Kamal::Commands::Builder < Kamal::Commands::Base
delegate :create, :remove, :push, :clean, :pull, :info, :context_hosts, :config_context_hosts, :validate_image,
to: :target
delegate :create, :remove, :push, :clean, :pull, :info, :buildx_inspect, :validate_image, :first_mirror, to: :target
delegate :multiarch?, :local?, :remote?, to: "config.builder"
include Clone
@@ -11,43 +11,27 @@ class Kamal::Commands::Builder < Kamal::Commands::Base
end
def target
if config.builder.multiarch?
if config.builder.remote?
if config.builder.local?
multiarch_remote
else
native_remote
end
if remote?
if local?
hybrid
else
multiarch
remote
end
else
if config.builder.cached?
native_cached
else
native
end
local
end
end
def native
@native ||= Kamal::Commands::Builder::Native.new(config)
def remote
@remote ||= Kamal::Commands::Builder::Remote.new(config)
end
def native_cached
@native ||= Kamal::Commands::Builder::Native::Cached.new(config)
def local
@local ||= Kamal::Commands::Builder::Local.new(config)
end
def native_remote
@native ||= Kamal::Commands::Builder::Native::Remote.new(config)
end
def multiarch
@multiarch ||= Kamal::Commands::Builder::Multiarch.new(config)
end
def multiarch_remote
@multiarch_remote ||= Kamal::Commands::Builder::Multiarch::Remote.new(config)
def hybrid
@hybrid ||= Kamal::Commands::Builder::Hybrid.new(config)
end

View File

@@ -1,20 +1,41 @@
class Kamal::Commands::Builder::Base < Kamal::Commands::Base
class BuilderError < StandardError; end
ENDPOINT_DOCKER_HOST_INSPECT = "'{{.Endpoints.docker.Host}}'"
delegate :argumentize, to: Kamal::Utils
delegate :args, :secrets, :dockerfile, :target, :local_arch, :local_host, :remote_arch, :remote_host, :cache_from, :cache_to, :ssh, to: :builder_config
delegate \
:args, :secrets, :dockerfile, :target, :local_arch, :remote_arch, :remote_host,
:cache_from, :cache_to, :multiarch?, :ssh, :driver, :docker_driver?,
to: :builder_config
def clean
docker :image, :rm, "--force", config.absolute_image
end
def push
docker :build,
"--push",
*platform_options,
*([ "--builder", builder_name ] unless docker_driver?),
*build_options,
build_context
end
def pull
docker :pull, config.absolute_image
end
def info
combine \
docker(:context, :ls),
docker(:buildx, :ls)
end
def buildx_inspect
docker :buildx, :inspect, builder_name
end
def build_options
[ *build_tags, *build_cache, *build_labels, *build_args, *build_secrets, *build_dockerfile, *build_target, *build_ssh ]
end
@@ -32,12 +53,8 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
)
end
def context_hosts
:true
end
def config_context_hosts
[]
def first_mirror
docker(:info, "--format '{{index .RegistryConfig.Mirrors 0}}'")
end
private

View File

@@ -0,0 +1,25 @@
class Kamal::Commands::Builder::Hybrid < Kamal::Commands::Builder::Remote
def create
combine \
create_local_buildx,
create_remote_context,
append_remote_buildx
end
private
def builder_name
"kamal-hybrid-#{driver}-#{local_arch}-#{remote_arch}-#{remote_host.gsub(/[^a-z0-9_-]/, "-")}"
end
def create_local_buildx
docker :buildx, :create, "--name", builder_name, "--platform", "linux/#{local_arch}", "--driver=#{driver}"
end
def append_remote_buildx
docker :buildx, :create, "--append", "--name", builder_name, builder_name, "--platform", "linux/#{remote_arch}"
end
def platform
"linux/#{local_arch},linux/#{remote_arch}"
end
end

View File

@@ -0,0 +1,24 @@
class Kamal::Commands::Builder::Local < Kamal::Commands::Builder::Base
def create
docker :buildx, :create, "--name", builder_name, "--driver=#{driver}" unless docker_driver?
end
def remove
docker :buildx, :rm, builder_name unless docker_driver?
end
private
def builder_name
"kamal-local-#{driver}"
end
def platform_options
if multiarch?
if local_arch
[ "--platform", "linux/#{local_arch}" ]
else
[ "--platform", "linux/amd64,linux/arm64" ]
end
end
end
end

View File

@@ -1,41 +0,0 @@
class Kamal::Commands::Builder::Multiarch < Kamal::Commands::Builder::Base
def create
docker :buildx, :create, "--use", "--name", builder_name
end
def remove
docker :buildx, :rm, builder_name
end
def info
combine \
docker(:context, :ls),
docker(:buildx, :ls)
end
def push
docker :buildx, :build,
"--push",
"--platform", platform_names,
"--builder", builder_name,
*build_options,
build_context
end
def context_hosts
docker :buildx, :inspect, builder_name, "> /dev/null"
end
private
def builder_name
"kamal-#{config.service}-multiarch"
end
def platform_names
if local_arch
"linux/#{local_arch}"
else
"linux/amd64,linux/arm64"
end
end
end

View File

@@ -1,61 +0,0 @@
class Kamal::Commands::Builder::Multiarch::Remote < Kamal::Commands::Builder::Multiarch
def create
combine \
create_contexts,
create_local_buildx,
append_remote_buildx
end
def remove
combine \
remove_contexts,
super
end
def context_hosts
chain \
context_host(builder_name_with_arch(local_arch)),
context_host(builder_name_with_arch(remote_arch))
end
def config_context_hosts
[ local_host, remote_host ].compact
end
private
def builder_name
super + "-remote"
end
def builder_name_with_arch(arch)
"#{builder_name}-#{arch}"
end
def create_local_buildx
docker :buildx, :create, "--name", builder_name, builder_name_with_arch(local_arch), "--platform", "linux/#{local_arch}"
end
def append_remote_buildx
docker :buildx, :create, "--append", "--name", builder_name, builder_name_with_arch(remote_arch), "--platform", "linux/#{remote_arch}"
end
def create_contexts
combine \
create_context(local_arch, local_host),
create_context(remote_arch, remote_host)
end
def create_context(arch, host)
docker :context, :create, builder_name_with_arch(arch), "--description", "'#{builder_name} #{arch} native host'", "--docker", "'host=#{host}'"
end
def remove_contexts
combine \
remove_context(local_arch),
remove_context(remote_arch)
end
def remove_context(arch)
docker :context, :rm, builder_name_with_arch(arch)
end
end

View File

@@ -1,20 +0,0 @@
class Kamal::Commands::Builder::Native < Kamal::Commands::Builder::Base
def create
# No-op on native without cache
end
def remove
# No-op on native without cache
end
def info
# No-op on native
end
def push
combine \
docker(:build, *build_options, build_context),
docker(:push, config.absolute_image),
docker(:push, config.latest_image)
end
end

View File

@@ -1,25 +0,0 @@
class Kamal::Commands::Builder::Native::Cached < Kamal::Commands::Builder::Native
def create
docker :buildx, :create, "--name", builder_name, "--use", "--driver=docker-container"
end
def remove
docker :buildx, :rm, builder_name
end
def push
docker :buildx, :build,
"--push",
*build_options,
build_context
end
def context_hosts
docker :buildx, :inspect, builder_name, "> /dev/null"
end
private
def builder_name
"kamal-#{config.service}-native-cached"
end
end

View File

@@ -1,67 +0,0 @@
class Kamal::Commands::Builder::Native::Remote < Kamal::Commands::Builder::Native
def create
chain \
create_context,
create_buildx
end
def remove
chain \
remove_context,
remove_buildx
end
def info
chain \
docker(:context, :ls),
docker(:buildx, :ls)
end
def push
docker :buildx, :build,
"--push",
"--platform", platform,
"--builder", builder_name,
*build_options,
build_context
end
def context_hosts
context_host(builder_name_with_arch)
end
def config_context_hosts
[ remote_host ]
end
private
def builder_name
"kamal-#{config.service}-native-remote"
end
def builder_name_with_arch
"#{builder_name}-#{remote_arch}"
end
def platform
"linux/#{remote_arch}"
end
def create_context
docker :context, :create,
builder_name_with_arch, "--description", "'#{builder_name} #{remote_arch} native host'", "--docker", "'host=#{remote_host}'"
end
def remove_context
docker :context, :rm, builder_name_with_arch
end
def create_buildx
docker :buildx, :create, "--name", builder_name, builder_name_with_arch, "--platform", platform
end
def remove_buildx
docker :buildx, :rm, builder_name
end
end

View File

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

View File

@@ -47,7 +47,7 @@ class Kamal::Configuration
@destination = destination
@declared_version = version
validate! raw_config, example: validation_yml.symbolize_keys, context: ""
validate! raw_config, example: validation_yml.symbolize_keys, context: "", with: Kamal::Configuration::Validator::Configuration
# Eager load config to validate it, these are first as they have dependencies later on
@servers = Servers.new(config: self)

View File

@@ -55,12 +55,12 @@ class Kamal::Configuration::Builder
builder_config["context"] || "."
end
def local_arch
builder_config["local"]["arch"] if local?
def driver
builder_config.fetch("driver", "docker-container")
end
def local_host
builder_config["local"]["host"] if local?
def local_arch
builder_config["local"]["arch"] if local?
end
def remote_arch
@@ -114,7 +114,36 @@ class Kamal::Configuration::Builder
end
end
def docker_driver?
driver == "docker"
end
private
def valid?
if multiarch?
if local?
raise ArgumentError, "Invalid builder configuration: local configuration, arch required" unless local_arch
end
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
if @options["cache"] && @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
def cache_image
builder_config["cache"]&.fetch("image", nil) || "#{image}-build-cache"
end

View File

@@ -1,10 +1,10 @@
# Builder
#
# The builder configuration controls how the application is built with `docker build` or `docker buildx build`
# The builder configuration controls how the application is built with `docker build`
#
# If no configuration is specified, Kamal will:
# 1. Create a buildx context called `kamal-<service>-multiarch`
# 2. Use `docker buildx 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
@@ -18,6 +18,11 @@ builder:
# Enables multiarch builds, defaults to `true`
multiarch: false
# Driver
#
# The build driver to use, defaults to `docker-container`
driver: docker
# Local configuration
#
# The build configuration for local builds, only used if multiarch is enabled (the default)

View File

@@ -2,13 +2,24 @@
#
# Configuration is read from the `config/deploy.yml`
#
# Destinations
#
# When running commands, you can specify a destination with the `-d` flag,
# e.g. `kamal deploy -d staging`
#
# In this case the configuration will also be read from `config/deploy.staging.yml`
# and merged with the base configuration.
# Extensions
#
# The available configuration options are explained below.
# Kamal will not accept unrecognized keys in the configuration file.
#
# However, you might want to declare a configuration block using YAML anchors
# and aliases to avoid repetition.
#
# You can use prefix a configuration section with `x-` to indicate that it is an
# extension. Kamal will ignore the extension and not raise an error.
# The service name
# This is a required value. It is used as the container name prefix.

View File

@@ -29,7 +29,7 @@ env:
# To pass the secrets you should list them under the `secret` key. When you do this the
# other variables need to be moved under the `clear` key.
#
# Unlike clear valies, secrets are not passed directly to the container,
# Unlike clear values, secrets are not passed directly to the container,
# but are stored in an env file on the host
# The file is not updated when deploying, only when running `kamal envify` or `kamal env push`.
env:

View File

@@ -44,3 +44,23 @@ ssh:
# Defaults to `fatal`. Set this to debug if you are having
# SSH connection issues.
log_level: debug
# Keys Only
#
# Set to true to use only private keys from keys and key_data parameters,
# even if ssh-agent offers more identities. This option is intended for
# situations where ssh-agent offers many different identites or you have
# a need to overwrite all identites and force a single one.
keys_only: false
# Keys
#
# An array of file names of private keys to use for publickey
# and hostbased authentication
keys: [ "~/.ssh/id.pem" ]
# Key Data
#
# An array of strings, with each element of the array being
# a raw private key in PEM format.
key_data: [ "-----BEGIN OPENSSH PRIVATE KEY-----" ]

View File

@@ -26,8 +26,20 @@ class Kamal::Configuration::Ssh
end
end
def keys_only
ssh_config["keys_only"]
end
def keys
ssh_config["keys"]
end
def key_data
ssh_config["key_data"]
end
def options
{ user: user, port: port, proxy: proxy, logger: logger, keepalive: true, keepalive_interval: 30 }.compact
{ user: user, port: port, proxy: proxy, logger: logger, keepalive: true, keepalive_interval: 30, keys_only: keys_only, keys: keys, key_data: key_data }.compact
end
def to_h

View File

@@ -15,11 +15,10 @@ class Kamal::Configuration::Validator
def validate_against_example!(validation_config, example)
validate_type! validation_config, Hash
if (unknown_keys = validation_config.keys - example.keys).any?
unknown_keys_error unknown_keys
end
check_unknown_keys! validation_config, example
validation_config.each do |key, value|
next if extension?(key)
with_context(key) do
example_value = example[key]
@@ -137,4 +136,18 @@ class Kamal::Configuration::Validator
ensure
@context = old_context
end
def allow_extensions?
false
end
def extension?(key)
key.to_s.start_with?("x-")
end
def check_unknown_keys!(config, example)
unknown_keys = config.keys - example.keys
unknown_keys.reject! { |key| extension?(key) } if allow_extensions?
unknown_keys_error unknown_keys if unknown_keys.present?
end
end

View File

@@ -0,0 +1,6 @@
class Kamal::Configuration::Validator::Configuration < Kamal::Configuration::Validator
private
def allow_extensions?
true
end
end

View File

@@ -9,6 +9,10 @@ module Kamal::Git
`git config user.name`.strip
end
def email
`git config user.email`.strip
end
def revision
`git rev-parse HEAD`.strip
end

View File

@@ -10,10 +10,11 @@ class Kamal::Tags
def default_tags(config)
{ recorded_at: Time.now.utc.iso8601,
performer: `whoami`.chomp,
performer: Kamal::Git.email.presence || `whoami`.chomp,
destination: config.destination,
version: config.version,
service_version: service_version(config) }
service_version: service_version(config),
service: config.service }
end
def service_version(config)

View File

@@ -1,3 +1,3 @@
module Kamal
VERSION = "1.7.3"
VERSION = "1.8.1"
end

View File

@@ -21,16 +21,12 @@ class CliBuildTest < CliTestCase
.with(:git, "-C", anything, :status, "--porcelain")
.returns("")
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :buildx, :inspect, "kamal-app-multiarch", "> /dev/null")
.returns("")
run_command("push", "--verbose").tap do |output|
assert_hook_ran "pre-build", output, **hook_variables
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 /docker --version && docker buildx version/, output
assert_match /docker buildx build --push --platform linux\/amd64,linux\/arm64 --builder kamal-app-multiarch -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 -t dhh\/app:999 -t dhh\/app:latest --label service="app" --file Dockerfile \. as .*@localhost/, output
end
end
end
@@ -52,7 +48,7 @@ class CliBuildTest < CliTestCase
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:git, "-C", build_directory, :clean, "-fdx")
SSHKit::Backend::Abstract.any_instance.expects(:execute)
.with(:docker, :buildx, :build, "--push", "--platform", "linux/amd64,linux/arm64", "--builder", "kamal-app-multiarch", "-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", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".")
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:git, "-C", anything, :"rev-parse", :HEAD)
@@ -77,7 +73,7 @@ class CliBuildTest < CliTestCase
assert_no_match /Cloning repo into build directory/, output
assert_hook_ran "pre-build", output, **hook_variables
assert_match /docker --version && docker buildx version/, output
assert_match /docker buildx build --push --platform linux\/amd64,linux\/arm64 --builder kamal-app-multiarch -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 -t dhh\/app:999 -t dhh\/app:latest --label service="app" --file Dockerfile . as .*@localhost/, output
end
end
@@ -123,10 +119,10 @@ class CliBuildTest < CliTestCase
.with(:docker, "--version", "&&", :docker, :buildx, "version")
SSHKit::Backend::Abstract.any_instance.expects(:execute)
.with(:docker, :buildx, :create, "--use", "--name", "kamal-app-multiarch")
.with(:docker, :buildx, :create, "--name", "kamal-local", "--driver=docker-container")
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :buildx, :inspect, "kamal-app-multiarch", "> /dev/null")
SSHKit::Backend::Abstract.any_instance.expects(:execute)
.with(:docker, :buildx, :inspect, "kamal-local")
.raises(SSHKit::Command::Failed.new("no builder"))
SSHKit::Backend::Abstract.any_instance.expects(:execute).with { |*args| args.first.start_with?("git") }
@@ -140,7 +136,7 @@ class CliBuildTest < CliTestCase
.returns("")
SSHKit::Backend::Abstract.any_instance.expects(:execute)
.with(:docker, :buildx, :build, "--push", "--platform", "linux/amd64,linux/arm64", "--builder", "kamal-app-multiarch", "-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", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".")
run_command("push").tap do |output|
assert_match /WARN Missing compatible builder, so creating a new one first/, output
@@ -164,36 +160,65 @@ class CliBuildTest < CliTestCase
error = assert_raises(Kamal::Cli::HookError) { run_command("push") }
assert_equal "Hook `pre-build` failed:\nfailed", error.message
assert @executions.none? { |args| args[0..2] == [ :docker, :buildx, :build ] }
assert @executions.none? { |args| args[0..2] == [ :docker, :build ] }
end
test "pull" do
run_command("pull").tap do |output|
assert_match /docker info --format '{{index .RegistryConfig.Mirrors 0}}'/, output
assert_match /docker image rm --force dhh\/app:999/, output
assert_match /docker pull dhh\/app:999/, output
assert_match "docker inspect -f '{{ .Config.Labels.service }}' dhh/app:999 | grep -x app || (echo \"Image dhh/app:999 is missing the 'service' label\" && exit 1)", output
end
end
test "pull with mirror" do
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :info, "--format '{{index .RegistryConfig.Mirrors 0}}'")
.returns("registry-mirror.example.com")
.at_least_once
run_command("pull").tap do |output|
assert_match /Pulling image on 1\.1\.1\.\d to seed the mirror\.\.\./, output
assert_match "Pulling image on remaining hosts...", output
assert_equal 4, output.scan(/docker pull dhh\/app:999/).size, output
assert_match "docker inspect -f '{{ .Config.Labels.service }}' dhh/app:999 | grep -x app || (echo \"Image dhh/app:999 is missing the 'service' label\" && exit 1)", output
end
end
test "pull with mirrors" do
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :info, "--format '{{index .RegistryConfig.Mirrors 0}}'")
.returns("registry-mirror.example.com", "registry-mirror2.example.com")
.at_least_once
run_command("pull").tap do |output|
assert_match /Pulling image on 1\.1\.1\.\d, 1\.1\.1\.\d to seed the mirrors\.\.\./, output
assert_match "Pulling image on remaining hosts...", output
assert_equal 4, output.scan(/docker pull dhh\/app:999/).size, output
assert_match "docker inspect -f '{{ .Config.Labels.service }}' dhh/app:999 | grep -x app || (echo \"Image dhh/app:999 is missing the 'service' label\" && exit 1)", output
end
end
test "create" do
run_command("create").tap do |output|
assert_match /docker buildx create --use --name kamal-app-multiarch/, output
assert_match /docker buildx create --name kamal-local --driver=docker-container/, output
end
end
test "create remote" do
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 "docker context create kamal-app-native-remote-amd64 --description 'kamal-app-native-remote amd64 native host' --docker 'host=ssh://app@1.1.1.5'", output
assert_match "docker buildx create --name kamal-app-native-remote kamal-app-native-remote-amd64 --platform linux/amd64", 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 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
end
end
test "create remote with custom ports" do
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 "docker context create kamal-app-native-remote-amd64 --description 'kamal-app-native-remote amd64 native host' --docker 'host=ssh://app@1.1.1.5:2122'", output
assert_match "docker buildx create --name kamal-app-native-remote kamal-app-native-remote-amd64 --platform linux/amd64", 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 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
end
end
@@ -210,7 +235,7 @@ class CliBuildTest < CliTestCase
test "remove" do
run_command("remove").tap do |output|
assert_match /docker buildx rm kamal-app-multiarch/, output
assert_match /docker buildx rm kamal-local/, output
end
end
@@ -220,7 +245,7 @@ class CliBuildTest < CliTestCase
.returns("docker builder info")
run_command("details").tap do |output|
assert_match /Builder: multiarch/, output
assert_match /Builder: local/, output
assert_match /docker builder info/, output
end
end

View File

@@ -36,22 +36,24 @@ class CliTestCase < ActiveSupport::TestCase
.with { |arg1, arg2| arg1 == :mkdir && arg2 == ".kamal/locks/app" }
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
.with { |arg1, arg2| arg1 == :rm && arg2 == ".kamal/locks/app/details" }
SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info)
.with { |*args| args[0..2] == [ :docker, :buildx, :inspect ] }
.returns("")
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
.with(:docker, :buildx, :inspect, "kamal-local")
end
def assert_hook_ran(hook, output, version:, service_version:, hosts:, command:, subcommand: nil, runtime: false)
performer = `whoami`.strip
whoami = `whoami`.chomp
performer = Kamal::Git.email.presence || whoami
service = service_version.split("@").first
assert_match "Running the #{hook} hook...\n", output
expected = %r{Running\s/usr/bin/env\s\.kamal/hooks/#{hook}\sas\s#{performer}@localhost\n\s
expected = %r{Running\s/usr/bin/env\s\.kamal/hooks/#{hook}\sas\s#{whoami}@localhost\n\s
DEBUG\s\[[0-9a-f]*\]\sCommand:\s\(\sexport\s
KAMAL_RECORDED_AT=\"\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\dZ\"\s
KAMAL_PERFORMER=\"#{performer}\"\s
KAMAL_VERSION=\"#{version}\"\s
KAMAL_SERVICE_VERSION=\"#{service_version}\"\s
KAMAL_SERVICE=\"#{service}\"\s
KAMAL_HOSTS=\"#{hosts}\"\s
KAMAL_COMMAND=\"#{command}\"\s
#{"KAMAL_SUBCOMMAND=\\\"#{subcommand}\\\"\\s" if subcommand}

View File

@@ -1,6 +1,9 @@
require_relative "cli_test_case"
class CliMainTest < CliTestCase
setup { @original_env = ENV.to_h.dup }
teardown { ENV.clear; ENV.update @original_env }
test "setup" do
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "version" => "999", "skip_hooks" => false }
@@ -119,8 +122,9 @@ class CliMainTest < CliTestCase
.returns("")
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :buildx, :inspect, "kamal-app-multiarch", "> /dev/null")
.with(:docker, :info, "--format '{{index .RegistryConfig.Mirrors 0}}'")
.returns("")
.at_least_once
assert_raises(Kamal::Cli::LockError) do
run_command("deploy")
@@ -152,8 +156,9 @@ class CliMainTest < CliTestCase
.returns("")
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :buildx, :inspect, "kamal-app-multiarch", "> /dev/null")
.with(:docker, :info, "--format '{{index .RegistryConfig.Mirrors 0}}'")
.returns("")
.at_least_once
assert_raises(SSHKit::Runner::ExecuteError) do
run_command("deploy")
@@ -434,7 +439,7 @@ class CliMainTest < CliTestCase
end
test "envify" do
with_test_dot_env_erb(contents: "HELLO=<%= 'world' %>") do
with_test_dotenv(".env.erb": "HELLO=<%= 'world' %>") do
run_command("envify")
assert_equal("HELLO=world", File.read(".env"))
end
@@ -448,14 +453,14 @@ class CliMainTest < CliTestCase
<% end -%>
EOF
with_test_dot_env_erb(contents: file) do
with_test_dotenv(".env.erb": file) do
run_command("envify")
assert_equal("HELLO=world\nKEY=value\n", File.read(".env"))
end
end
test "envify with destination" do
with_test_dot_env_erb(contents: "HELLO=<%= 'world' %>", file: ".env.world.erb") do
with_test_dotenv(".env.world.erb": "HELLO=<%= 'world' %>") do
run_command("envify", "-d", "world", config_file: "deploy_for_dest")
assert_equal "HELLO=world", File.read(".env.world")
end
@@ -470,6 +475,13 @@ class CliMainTest < CliTestCase
run_command("envify", "--skip-push")
end
test "envify with clean env" do
with_test_dotenv(".env": "HELLO=already", ".env.erb": "HELLO=<%= ENV.fetch 'HELLO', 'never' %>") do
run_command("envify", "--skip-push")
assert_equal "HELLO=never", File.read(".env")
end
end
test "remove with confirmation" do
run_command("remove", "-y", config_file: "deploy_with_accessories").tap do |output|
assert_match /docker container stop traefik/, output
@@ -522,14 +534,16 @@ class CliMainTest < CliTestCase
stdouted { Kamal::Cli::Main.start([ *command, "-c", "test/fixtures/#{config_file}.yml" ]) }
end
def with_test_dot_env_erb(contents:, file: ".env.erb")
def with_test_dotenv(**files)
Dir.mktmpdir do |dir|
fixtures_dup = File.join(dir, "test")
FileUtils.mkdir_p(fixtures_dup)
FileUtils.cp_r("test/fixtures/", fixtures_dup)
Dir.chdir(dir) do
File.write(file, contents)
files.each do |filename, contents|
File.binwrite(filename.to_s, contents)
end
yield
end
end

View File

@@ -12,7 +12,7 @@ class CommandsAuditorTest < ActiveSupport::TestCase
}
@auditor = new_command
@performer = `whoami`.strip
@performer = Kamal::Git.email.presence || `whoami`.chomp
@recorded_at = Time.now.utc.iso8601
end

View File

@@ -7,49 +7,49 @@ class CommandsBuilderTest < ActiveSupport::TestCase
test "target multiarch by default" do
builder = new_builder_command(builder: { "cache" => { "type" => "gha" } })
assert_equal "multiarch", builder.name
assert_equal "local", builder.name
assert_equal \
"docker buildx build --push --platform linux/amd64,linux/arm64 --builder kamal-app-multiarch -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 -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 native when multiarch is off" do
builder = new_builder_command(builder: { "multiarch" => false })
assert_equal "native", builder.name
assert_equal "local", builder.name
assert_equal \
"docker build -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile . && docker push dhh/app:123 && docker push dhh/app:latest",
"docker build --push --builder kamal-local -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile .",
builder.push.join(" ")
end
test "target native cached when multiarch is off and cache is set" do
builder = new_builder_command(builder: { "multiarch" => false, "cache" => { "type" => "gha" } })
assert_equal "native/cached", builder.name
assert_equal "local", builder.name
assert_equal \
"docker buildx build --push -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile .",
"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 .",
builder.push.join(" ")
end
test "target multiarch remote when local and remote is set" do
builder = new_builder_command(builder: { "local" => {}, "remote" => {}, "cache" => { "type" => "gha" } })
assert_equal "multiarch/remote", builder.name
builder = new_builder_command(builder: { "local" => { "arch" => "arm64" }, "remote" => { "arch" => "amd64", "host" => "ssh://app@127.0.0.1" }, "cache" => { "type" => "gha" } })
assert_equal "hybrid", builder.name
assert_equal \
"docker buildx build --push --platform linux/amd64,linux/arm64 --builder kamal-app-multiarch-remote -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/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 .",
builder.push.join(" ")
end
test "target multiarch local when arch is set" do
builder = new_builder_command(builder: { "local" => { "arch" => "amd64" } })
assert_equal "multiarch", builder.name
assert_equal "local", builder.name
assert_equal \
"docker buildx build --push --platform linux/amd64 --builder kamal-app-multiarch -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile .",
"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" }, "cache" => { "type" => "gha" } })
assert_equal "native/remote", builder.name
builder = new_builder_command(builder: { "remote" => { "arch" => "amd64", "host" => "ssh://app@host" }, "cache" => { "type" => "gha" } })
assert_equal "remote", builder.name
assert_equal \
"docker buildx build --push --platform linux/amd64 --builder kamal-app-native-remote -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 --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 .",
builder.push.join(" ")
end
@@ -93,28 +93,28 @@ class CommandsBuilderTest < ActiveSupport::TestCase
test "build context" do
builder = new_builder_command(builder: { "context" => ".." })
assert_equal \
"docker buildx build --push --platform linux/amd64,linux/arm64 --builder kamal-app-multiarch -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ..",
"docker build --push --platform linux/amd64,linux/arm64 --builder kamal-local -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ..",
builder.push.join(" ")
end
test "native push with build args" do
builder = new_builder_command(builder: { "multiarch" => false, "args" => { "a" => 1, "b" => 2 } })
assert_equal \
"docker build -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --build-arg a=\"1\" --build-arg b=\"2\" --file Dockerfile . && docker push dhh/app:123 && docker push dhh/app:latest",
"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 } })
assert_equal \
"docker buildx build --push --platform linux/amd64,linux/arm64 --builder kamal-app-multiarch -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 -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 "native push with build secrets" do
builder = new_builder_command(builder: { "multiarch" => false, "secrets" => [ "a", "b" ] })
assert_equal \
"docker build -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --secret id=\"a\" --secret id=\"b\" --file Dockerfile . && docker push dhh/app:123 && docker push dhh/app:latest",
"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 .",
builder.push.join(" ")
end
@@ -133,71 +133,34 @@ class CommandsBuilderTest < ActiveSupport::TestCase
test "multiarch context build" do
builder = new_builder_command(builder: { "context" => "./foo" })
assert_equal \
"docker buildx build --push --platform linux/amd64,linux/arm64 --builder kamal-app-multiarch -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 -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 -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ./foo && docker push dhh/app:123 && docker push dhh/app:latest",
"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 buildx build --push -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile ./foo",
"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" }, "context" => "./foo" })
builder = new_builder_command(builder: { "remote" => { "arch" => "amd64", "host" => "ssh://app@host" }, "context" => "./foo" })
assert_equal \
"docker buildx build --push --platform linux/amd64 --builder kamal-app-native-remote -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ./foo",
"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(" ")
end
test "multiarch context hosts" do
test "mirror count" do
command = new_builder_command
assert_equal "docker buildx inspect kamal-app-multiarch > /dev/null", command.context_hosts.join(" ")
assert_equal "", command.config_context_hosts.join(" ")
end
test "native context hosts" do
command = new_builder_command(builder: { "multiarch" => false })
assert_equal :true, command.context_hosts
assert_equal "", command.config_context_hosts.join(" ")
end
test "native cached context hosts" do
command = new_builder_command(builder: { "multiarch" => false, "cache" => { "type" => "registry" } })
assert_equal "docker buildx inspect kamal-app-native-cached > /dev/null", command.context_hosts.join(" ")
assert_equal "", command.config_context_hosts.join(" ")
end
test "native remote context hosts" do
command = new_builder_command(builder: { "remote" => { "arch" => "amd64", "host" => "ssh://host" } })
assert_equal "docker context inspect kamal-app-native-remote-amd64 --format '{{.Endpoints.docker.Host}}'", command.context_hosts.join(" ")
assert_equal [ "ssh://host" ], command.config_context_hosts
end
test "multiarch remote context hosts" do
command = new_builder_command(builder: {
"remote" => { "arch" => "amd64", "host" => "ssh://host" },
"local" => { "arch" => "arm64" }
})
assert_equal "docker context inspect kamal-app-multiarch-remote-arm64 --format '{{.Endpoints.docker.Host}}' ; docker context inspect kamal-app-multiarch-remote-amd64 --format '{{.Endpoints.docker.Host}}'", command.context_hosts.join(" ")
assert_equal [ "ssh://host" ], command.config_context_hosts
end
test "multiarch remote context hosts with local host" do
command = new_builder_command(builder: {
"remote" => { "arch" => "amd64", "host" => "ssh://host" },
"local" => { "arch" => "arm64", "host" => "unix:///var/run/docker.sock" }
})
assert_equal "docker context inspect kamal-app-multiarch-remote-arm64 --format '{{.Endpoints.docker.Host}}' ; docker context inspect kamal-app-multiarch-remote-amd64 --format '{{.Endpoints.docker.Host}}'", command.context_hosts.join(" ")
assert_equal [ "unix:///var/run/docker.sock", "ssh://host" ], command.config_context_hosts
assert_equal "docker info --format '{{index .RegistryConfig.Mirrors 0}}'", command.first_mirror.join(" ")
end
private

View File

@@ -11,7 +11,7 @@ class CommandsHookTest < ActiveSupport::TestCase
traefik: { "args" => { "accesslog.format" => "json", "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } }
}
@performer = `whoami`.strip
@performer = Kamal::Git.email.presence || `whoami`.chomp
@recorded_at = Time.now.utc.iso8601
end
@@ -22,7 +22,8 @@ class CommandsHookTest < ActiveSupport::TestCase
"KAMAL_RECORDED_AT" => @recorded_at,
"KAMAL_PERFORMER" => @performer,
"KAMAL_VERSION" => "123",
"KAMAL_SERVICE_VERSION" => "app@123" } }
"KAMAL_SERVICE_VERSION" => "app@123",
"KAMAL_SERVICE" => "app" } }
], new_command.run("foo")
end
@@ -33,7 +34,8 @@ class CommandsHookTest < ActiveSupport::TestCase
"KAMAL_RECORDED_AT" => @recorded_at,
"KAMAL_PERFORMER" => @performer,
"KAMAL_VERSION" => "123",
"KAMAL_SERVICE_VERSION" => "app@123" } }
"KAMAL_SERVICE_VERSION" => "app@123",
"KAMAL_SERVICE" => "app" } }
], new_command(hooks_path: "custom/hooks/path").run("foo")
end

View File

@@ -42,7 +42,7 @@ class ConfigurationBuilderTest < ActiveSupport::TestCase
test "setting both local and remote configs" do
@deploy_with_builder_option[:builder] = {
"local" => { "arch" => "arm64", "host" => "unix:///Users/<%= `whoami`.strip %>/.docker/run/docker.sock" },
"local" => { "arch" => "arm64" },
"remote" => { "arch" => "amd64", "host" => "ssh://root@192.168.0.1" }
}
@@ -53,7 +53,6 @@ class ConfigurationBuilderTest < ActiveSupport::TestCase
assert_equal "ssh://root@192.168.0.1", config_with_builder_option.builder.remote_host
assert_equal "arm64", config_with_builder_option.builder.local_arch
assert_equal "unix:///Users/<%= `whoami`.strip %>/.docker/run/docker.sock", config_with_builder_option.builder.local_host
end
test "cached?" do

View File

@@ -344,4 +344,12 @@ class ConfigurationTest < ActiveSupport::TestCase
assert_raises(Kamal::ConfigurationError) { Kamal::Configuration.new(@deploy_with_roles.merge(retain_containers: 0)) }
end
test "extensions" do
dest_config_file = Pathname.new(File.expand_path("fixtures/deploy_with_extensions.yml", __dir__))
config = Kamal::Configuration.create_from config_file: dest_config_file
assert_equal config.role(:web_tokyo).running_traefik?, true
assert_equal config.role(:web_chicago).running_traefik?, true
end
end

View File

@@ -0,0 +1,24 @@
x-web: &web
traefik: true
service: app
image: dhh/app
servers:
web_chicago:
<<: *web
hosts:
- 1.1.1.1
- 1.1.1.2
web_tokyo:
<<: *web
hosts:
- 1.1.1.3
- 1.1.1.4
env:
REDIS_URL: redis://x/y
registry:
server: registry.digitalocean.com
username: user
password: pw
primary_role: web_tokyo

View File

@@ -1,4 +1,3 @@
version: "3.7"
name: "kamal-test"
volumes:
@@ -30,8 +29,6 @@ services:
context: docker/registry
environment:
- REGISTRY_HTTP_ADDR=0.0.0.0:4443
- REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt
- REGISTRY_HTTP_TLS_KEY=/certs/domain.key
volumes:
- shared:/shared
- registry:/var/lib/registry/

View File

@@ -22,7 +22,6 @@ COPY app_with_roles/ app_with_roles/
RUN rm -rf /root/.ssh
RUN ln -s /shared/ssh /root/.ssh
RUN mkdir -p /etc/docker/certs.d/registry:4443 && ln -s /shared/certs/domain.crt /etc/docker/certs.d/registry:4443/ca.crt
RUN git config --global user.email "deployer@example.com"
RUN git config --global user.name "Deployer"

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
#!/bin/bash
dockerd --max-concurrent-downloads 1 &
dockerd --max-concurrent-downloads 1 --insecure-registry registry:4443 &
exec sleep infinity

View File

@@ -1,5 +1,3 @@
#!/bin/sh
while [ ! -f /certs/domain.crt ]; do sleep 1; done
exec /entrypoint.sh /etc/docker/registry/config.yml

View File

@@ -10,8 +10,6 @@ RUN mkdir ssh && \
COPY registry-dns.conf .
COPY boot.sh .
RUN mkdir certs && openssl req -newkey rsa:4096 -nodes -sha256 -keyout certs/domain.key -x509 -days 365 -out certs/domain.crt -subj '/CN=registry' -extensions EXT -config registry-dns.conf
HEALTHCHECK --interval=1s CMD pgrep sleep
CMD ["./boot.sh"]

View File

@@ -5,7 +5,6 @@ WORKDIR /work
RUN apt-get update --fix-missing && apt-get -y install openssh-client openssh-server docker.io
RUN mkdir /root/.ssh && ln -s /shared/ssh/id_rsa.pub /root/.ssh/authorized_keys
RUN mkdir -p /etc/docker/certs.d/registry:4443 && ln -s /shared/certs/domain.crt /etc/docker/certs.d/registry:4443/ca.crt
RUN echo "HOST_TOKEN=abcd" >> /etc/environment

View File

@@ -4,6 +4,6 @@ while [ ! -f /root/.ssh/authorized_keys ]; do echo "Waiting for ssh keys"; sleep
service ssh restart
dockerd --max-concurrent-downloads 1 &
dockerd --max-concurrent-downloads 1 --insecure-registry registry:4443 &
exec sleep infinity