Compare commits

..

12 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
43 changed files with 286 additions and 621 deletions

View File

@@ -1,7 +1,7 @@
PATH PATH
remote: . remote: .
specs: specs:
kamal (2.0.0.alpha) kamal (1.8.1)
activesupport (>= 7.0) activesupport (>= 7.0)
base64 (~> 0.2) base64 (~> 0.2)
bcrypt_pbkdf (~> 1.0) bcrypt_pbkdf (~> 1.0)
@@ -10,7 +10,7 @@ PATH
ed25519 (~> 1.2) ed25519 (~> 1.2)
net-ssh (~> 7.0) net-ssh (~> 7.0)
sshkit (>= 1.23.0, < 2.0) sshkit (>= 1.23.0, < 2.0)
thor (~> 1.3) thor (~> 1.2)
zeitwerk (~> 2.5) zeitwerk (~> 2.5)
GEM GEM

View File

@@ -17,7 +17,6 @@ end
DOCS = { DOCS = {
"accessory" => "Accessories", "accessory" => "Accessories",
"alias" => "Aliases",
"boot" => "Booting", "boot" => "Booting",
"builder" => "Builders", "builder" => "Builders",
"configuration" => "Configuration overview", "configuration" => "Configuration overview",
@@ -68,27 +67,26 @@ class DocWriter
output.puts output.puts
place = :new_section place = :new_section
elsif line =~ /^ *#/ elsif line =~ /^ *#/
generate_line(line, heading: place == :new_section) generate_line(line, place: place)
place = :in_section place = :in_section
else else
output.puts "```yaml" output.puts "```yaml"
output.puts line output.print line
place = :in_yaml place = :in_yaml
end end
when :in_yaml, :in_empty_line_yaml when :in_yaml
if line =~ /^ *#/ if line =~ /^ *#/
output.puts "```" output.puts "```"
generate_line(line, heading: place == :in_empty_line_yaml) generate_line(line, place: :new_section)
place = :in_section place = :in_section
elsif line.empty?
place = :in_empty_line_yaml
else else
output.puts line output.puts
output.print line
end end
end end
end end
output.puts "```" if place == :in_yaml output.puts "\n```" if place == :in_yaml
end end
def generate_header def generate_header
@@ -100,7 +98,7 @@ class DocWriter
output.puts output.puts
end end
def generate_line(line, heading: false) def generate_line(line, place: :in_section)
line = line.gsub(/^ *#\s?/, "") line = line.gsub(/^ *#\s?/, "")
if line =~ /(.*)kamal docs ([a-z]*)(.*)/ if line =~ /(.*)kamal docs ([a-z]*)(.*)/
@@ -111,7 +109,7 @@ class DocWriter
line = "#{$1}[#{titlify($2.split("/").last)}](#{$2})#{$3}" line = "#{$1}[#{titlify($2.split("/").last)}](#{$2})#{$3}"
end end
if heading if place == :new_section
output.puts "## [#{line}](##{linkify(line)})" output.puts "## [#{line}](##{linkify(line)})"
else else
output.puts line output.puts line

View File

@@ -14,7 +14,7 @@ Gem::Specification.new do |spec|
spec.add_dependency "activesupport", ">= 7.0" spec.add_dependency "activesupport", ">= 7.0"
spec.add_dependency "sshkit", ">= 1.23.0", "< 2.0" spec.add_dependency "sshkit", ">= 1.23.0", "< 2.0"
spec.add_dependency "net-ssh", "~> 7.0" spec.add_dependency "net-ssh", "~> 7.0"
spec.add_dependency "thor", "~> 1.3" spec.add_dependency "thor", "~> 1.2"
spec.add_dependency "dotenv", "~> 2.8" spec.add_dependency "dotenv", "~> 2.8"
spec.add_dependency "zeitwerk", "~> 2.5" spec.add_dependency "zeitwerk", "~> 2.5"
spec.add_dependency "ed25519", "~> 1.2" spec.add_dependency "ed25519", "~> 1.2"

View File

@@ -1,9 +0,0 @@
class Kamal::Cli::Alias::Command < Thor::DynamicCommand
def run(instance, args = [])
if (_alias = KAMAL.config.aliases[name])
Kamal::Cli::Main.start(Shellwords.split(_alias.command) + ARGV[1..-1])
else
super
end
end
end

View File

@@ -71,12 +71,11 @@ class Kamal::Cli::App < Kamal::Cli::Base
end end
end end
desc "exec [CMD...]", "Execute a custom command on servers within the app container (use --help to show options)" desc "exec [CMD]", "Execute a custom command on servers within the app container (use --help to show options)"
option :interactive, aliases: "-i", type: :boolean, default: false, desc: "Execute command over ssh for an interactive shell (use for console/bash)" option :interactive, aliases: "-i", type: :boolean, default: false, desc: "Execute command over ssh for an interactive shell (use for console/bash)"
option :reuse, type: :boolean, default: false, desc: "Reuse currently running container instead of starting a new one" option :reuse, type: :boolean, default: false, desc: "Reuse currently running container instead of starting a new one"
option :env, aliases: "-e", type: :hash, desc: "Set environment variables for the command" option :env, aliases: "-e", type: :hash, desc: "Set environment variables for the command"
def exec(*cmd) def exec(cmd)
cmd = Kamal::Utils.join_commands(cmd)
env = options[:env] env = options[:env]
case case
when options[:interactive] && options[:reuse] when options[:interactive] && options[:reuse]

View File

@@ -6,8 +6,7 @@ module Kamal::Cli
class Base < Thor class Base < Thor
include SSHKit::DSL include SSHKit::DSL
def self.exit_on_failure?() false end def self.exit_on_failure?() true end
def self.dynamic_command_class() Kamal::Cli::Alias::Command end
class_option :verbose, type: :boolean, aliases: "-v", desc: "Detailed logging" class_option :verbose, type: :boolean, aliases: "-v", desc: "Detailed logging"
class_option :quiet, type: :boolean, aliases: "-q", desc: "Minimal logging" class_option :quiet, type: :boolean, aliases: "-q", desc: "Minimal logging"
@@ -23,14 +22,8 @@ module Kamal::Cli
class_option :skip_hooks, aliases: "-H", type: :boolean, default: false, desc: "Don't run hooks" class_option :skip_hooks, aliases: "-H", type: :boolean, default: false, desc: "Don't run hooks"
def initialize(args = [], local_options = {}, config = {}) def initialize(*)
if config[:current_command].is_a?(Kamal::Cli::Alias::Command) super
# When Thor generates a dynamic command, it doesn't attempt to parse the arguments.
# For our purposes, it means the arguments are passed in args rather than local_options.
super([], args, config)
else
super
end
@original_env = ENV.to_h.dup @original_env = ENV.to_h.dup
load_env load_env
initialize_commander(options_with_subcommand_class_options) initialize_commander(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 say "Building with uncommitted changes:\n #{uncommitted_changes}", :yellow
end end
# Get the command here to ensure the Dir.chdir doesn't interfere with it
push = KAMAL.builder.push
run_locally do run_locally do
begin begin
context_hosts = capture_with_info(*KAMAL.builder.context_hosts).split("\n") execute *KAMAL.builder.buildx_inspect
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
rescue SSHKit::Command::Failed => e rescue SSHKit::Command::Failed => e
if e.message =~ /(context not found|no builder|does not exist)/ if e.message =~ /(context not found|no builder|does not exist)/
warn "Missing compatible builder, so creating a new one first" warn "Missing compatible builder, so creating a new one first"
@@ -51,6 +42,9 @@ class Kamal::Cli::Build < Kamal::Cli::Base
end end
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 KAMAL.with_verbosity(:debug) do
Dir.chdir(KAMAL.config.builder.build_directory) { execute *push } Dir.chdir(KAMAL.config.builder.build_directory) { execute *push }
end end

View File

@@ -1,8 +1,7 @@
class Kamal::Cli::Server < Kamal::Cli::Base class Kamal::Cli::Server < Kamal::Cli::Base
desc "exec", "Run a custom command on the server (use --help to show options)" desc "exec", "Run a custom command on the server (use --help to show options)"
option :interactive, type: :boolean, aliases: "-i", default: false, desc: "Run the command interactively (use for console/bash)" option :interactive, type: :boolean, aliases: "-i", default: false, desc: "Run the command interactively (use for console/bash)"
def exec(*cmd) def exec(cmd)
cmd = Kamal::Utils.join_commands(cmd)
hosts = KAMAL.hosts | KAMAL.accessory_hosts hosts = KAMAL.hosts | KAMAL.accessory_hosts
case case

View File

@@ -27,11 +27,7 @@ class Kamal::Commander
def specific_primary! def specific_primary!
@specifics = nil @specifics = nil
if specific_roles.present? self.specific_hosts = [ config.primary_host ]
self.specific_hosts = [ specific_roles.first.primary_host ]
else
self.specific_hosts = [ config.primary_host ]
end
end end
def specific_roles=(role_names) def specific_roles=(role_names)
@@ -117,10 +113,6 @@ class Kamal::Commander
@traefik ||= Kamal::Commands::Traefik.new(config) @traefik ||= Kamal::Commands::Traefik.new(config)
end end
def alias(name)
config.aliases[name]
end
def with_verbosity(level) def with_verbosity(level)
old_level = self.verbosity old_level = self.verbosity

View File

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

View File

@@ -1,20 +1,41 @@
class Kamal::Commands::Builder::Base < Kamal::Commands::Base class Kamal::Commands::Builder::Base < Kamal::Commands::Base
class BuilderError < StandardError; end class BuilderError < StandardError; end
ENDPOINT_DOCKER_HOST_INSPECT = "'{{.Endpoints.docker.Host}}'" ENDPOINT_DOCKER_HOST_INSPECT = "'{{.Endpoints.docker.Host}}'"
delegate :argumentize, to: Kamal::Utils 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 def clean
docker :image, :rm, "--force", config.absolute_image docker :image, :rm, "--force", config.absolute_image
end end
def push
docker :build,
"--push",
*platform_options,
*([ "--builder", builder_name ] unless docker_driver?),
*build_options,
build_context
end
def pull def pull
docker :pull, config.absolute_image docker :pull, config.absolute_image
end end
def info
combine \
docker(:context, :ls),
docker(:buildx, :ls)
end
def buildx_inspect
docker :buildx, :inspect, builder_name
end
def build_options def build_options
[ *build_tags, *build_cache, *build_labels, *build_args, *build_secrets, *build_dockerfile, *build_target, *build_ssh ] [ *build_tags, *build_cache, *build_labels, *build_args, *build_secrets, *build_dockerfile, *build_target, *build_ssh ]
end end
@@ -32,14 +53,6 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
) )
end end
def context_hosts
:true
end
def config_context_hosts
[]
end
def first_mirror def first_mirror
docker(:info, "--format '{{index .RegistryConfig.Mirrors 0}}'") docker(:info, "--format '{{index .RegistryConfig.Mirrors 0}}'")
end end

View File

@@ -6,7 +6,7 @@ module Kamal::Commands::Builder::Clone
end end
def clone def clone
git :clone, Kamal::Git.root, "--recurse-submodules", path: clone_directory git :clone, Kamal::Git.root, path: clone_directory
end end
def clone_reset_steps def clone_reset_steps
@@ -14,8 +14,7 @@ module Kamal::Commands::Builder::Clone
git(:remote, "set-url", :origin, Kamal::Git.root, path: build_directory), git(:remote, "set-url", :origin, Kamal::Git.root, path: build_directory),
git(:fetch, :origin, path: build_directory), git(:fetch, :origin, path: build_directory),
git(:reset, "--hard", Kamal::Git.revision, path: build_directory), git(:reset, "--hard", Kamal::Git.revision, path: build_directory),
git(:clean, "-fdx", path: build_directory), git(:clean, "-fdx", path: build_directory)
git(:submodule, :update, "--init", path: build_directory)
] ]
end end

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

@@ -11,7 +11,7 @@ class Kamal::Configuration
delegate :argumentize, :optionize, to: Kamal::Utils delegate :argumentize, :optionize, to: Kamal::Utils
attr_reader :destination, :raw_config attr_reader :destination, :raw_config
attr_reader :accessories, :aliases, :boot, :builder, :env, :healthcheck, :logging, :traefik, :servers, :ssh, :sshkit, :registry attr_reader :accessories, :boot, :builder, :env, :healthcheck, :logging, :traefik, :servers, :ssh, :sshkit, :registry
include Validation include Validation
@@ -54,7 +54,6 @@ class Kamal::Configuration
@registry = Registry.new(config: self) @registry = Registry.new(config: self)
@accessories = @raw_config.accessories&.keys&.collect { |name| Accessory.new(name, config: self) } || [] @accessories = @raw_config.accessories&.keys&.collect { |name| Accessory.new(name, config: self) } || []
@aliases = @raw_config.aliases&.keys&.to_h { |name| [ name, Alias.new(name, config: self) ] } || {}
@boot = Boot.new(config: self) @boot = Boot.new(config: self)
@builder = Builder.new(config: self) @builder = Builder.new(config: self)
@env = Env.new(config: @raw_config.env || {}) @env = Env.new(config: @raw_config.env || {})

View File

@@ -1,15 +0,0 @@
class Kamal::Configuration::Alias
include Kamal::Configuration::Validation
attr_reader :name, :command
def initialize(name, config:)
@name, @command = name.inquiry, config.raw_config["aliases"][name]
validate! \
command,
example: validation_yml["aliases"]["uname"],
context: "aliases/#{name}",
with: Kamal::Configuration::Validator::Alias
end
end

View File

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

View File

@@ -1,26 +0,0 @@
# Aliases
#
# Aliases are shortcuts for Kamal commands.
#
# For example, for a Rails app, you might open a console with:
#
# ```shell
# kamal app exec -i -r console "rails console"
# ```
#
# By defining an alias, like this:
aliases:
console: app exec -r console -i "rails console"
# You can now open the console with:
# ```shell
# kamal console
# ```
# Configuring aliases
#
# Aliases are defined in the root config under the alias key
#
# Each alias is named and can only contain lowercase letters, numbers, dashes and underscores.
aliases:
uname: app exec -p -q -r web "uname -a"

View File

@@ -1,10 +1,10 @@
# Builder # 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: # If no configuration is specified, Kamal will:
# 1. Create a buildx context called `kamal-<service>-multiarch` # 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 # See https://kamal-deploy.org/docs/configuration/builder-examples/ for more information
@@ -18,6 +18,11 @@ builder:
# Enables multiarch builds, defaults to `true` # Enables multiarch builds, defaults to `true`
multiarch: false multiarch: false
# Driver
#
# The build driver to use, defaults to `docker-container`
driver: docker
# Local configuration # Local configuration
# #
# The build configuration for local builds, only used if multiarch is enabled (the default) # The build configuration for local builds, only used if multiarch is enabled (the default)

View File

@@ -166,9 +166,3 @@ healthcheck:
# Docker logging configuration, see kamal docs logging # Docker logging configuration, see kamal docs logging
logging: logging:
... ...
# Aliases
#
# Alias configuration, see kamal docs alias
aliases:
...

View File

@@ -13,34 +13,32 @@ class Kamal::Configuration::Validator
private private
def validate_against_example!(validation_config, example) def validate_against_example!(validation_config, example)
validate_type! validation_config, example.class validate_type! validation_config, Hash
if example.class == Hash check_unknown_keys! validation_config, example
check_unknown_keys! validation_config, example
validation_config.each do |key, value| validation_config.each do |key, value|
next if extension?(key) next if extension?(key)
with_context(key) do with_context(key) do
example_value = example[key] example_value = example[key]
if example_value == "..." if example_value == "..."
validate_type! value, *(Array if key == :servers), Hash validate_type! value, *(Array if key == :servers), Hash
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 validate_array_of! value, example_value.first.class
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"
validate_type! value, Hash validate_type! value, Hash
when "labels" when "labels"
validate_hash_of! value, example_value.first[1].class validate_hash_of! value, example_value.first[1].class
else
validate_against_example! value, example_value
end
else else
validate_type! value, example_value.class validate_against_example! value, example_value
end end
else
validate_type! value, example_value.class
end end
end end
end end

View File

@@ -1,15 +0,0 @@
class Kamal::Configuration::Validator::Alias < Kamal::Configuration::Validator
def validate!
super
name = context.delete_prefix("aliases/")
if name !~ /\A[a-z0-9_-]+\z/
error "Invalid alias name: '#{name}'. Must only contain lowercase letters, alphanumeric, hyphens and underscores."
end
if Kamal::Cli::Main.commands.include?(name)
error "Alias '#{name}' conflicts with a built-in command."
end
end
end

View File

@@ -77,8 +77,4 @@ module Kamal::Utils
def stable_sort!(elements, &block) def stable_sort!(elements, &block)
elements.sort_by!.with_index { |element, index| [ block.call(element), index ] } elements.sort_by!.with_index { |element, index| [ block.call(element), index ] }
end end
def join_commands(commands)
commands.map(&:strip).join(" ")
end
end end

View File

@@ -1,3 +1,3 @@
module Kamal module Kamal
VERSION = "2.0.0.alpha" VERSION = "1.8.1"
end end

View File

@@ -247,12 +247,6 @@ class CliAppTest < CliTestCase
end end
end end
test "exec separate arguments" do
run_command("exec", "ruby", " -v").tap do |output|
assert_match "docker run --rm --env-file .kamal/env/roles/app-web.env dhh/app:latest ruby -v", output
end
end
test "exec with reuse" do test "exec with reuse" do
run_command("exec", "--reuse", "ruby -v").tap do |output| run_command("exec", "--reuse", "ruby -v").tap do |output|
assert_match "sh -c 'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | while read line; do echo ${line#app-web-}; done", output # Get current version assert_match "sh -c 'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | while read line; do echo ${line#app-web-}; done", output # Get current version

View File

@@ -21,16 +21,12 @@ class CliBuildTest < CliTestCase
.with(:git, "-C", anything, :status, "--porcelain") .with(:git, "-C", anything, :status, "--porcelain")
.returns("") .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| run_command("push", "--verbose").tap do |output|
assert_hook_ran "pre-build", output, **hook_variables assert_hook_ran "pre-build", output, **hook_variables
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 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 end
end end
@@ -42,7 +38,7 @@ class CliBuildTest < CliTestCase
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:docker, "--version", "&&", :docker, :buildx, "version") SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:docker, "--version", "&&", :docker, :buildx, "version")
SSHKit::Backend::Abstract.any_instance.expects(:execute) SSHKit::Backend::Abstract.any_instance.expects(:execute)
.with(:git, "-C", "#{Dir.tmpdir}/kamal-clones/app-#{pwd_sha}", :clone, Dir.pwd, "--recurse-submodules") .with(:git, "-C", "#{Dir.tmpdir}/kamal-clones/app-#{pwd_sha}", :clone, Dir.pwd)
.raises(SSHKit::Command::Failed.new("fatal: destination path 'kamal' already exists and is not an empty directory")) .raises(SSHKit::Command::Failed.new("fatal: destination path 'kamal' already exists and is not an empty directory"))
.then .then
.returns(true) .returns(true)
@@ -50,10 +46,9 @@ class CliBuildTest < CliTestCase
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:git, "-C", build_directory, :fetch, :origin) SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:git, "-C", build_directory, :fetch, :origin)
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:git, "-C", build_directory, :reset, "--hard", Kamal::Git.revision) SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:git, "-C", build_directory, :reset, "--hard", Kamal::Git.revision)
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:git, "-C", build_directory, :clean, "-fdx") SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:git, "-C", build_directory, :clean, "-fdx")
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, :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) SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:git, "-C", anything, :"rev-parse", :HEAD) .with(:git, "-C", anything, :"rev-parse", :HEAD)
@@ -78,7 +73,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 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 end
@@ -89,7 +84,7 @@ class CliBuildTest < CliTestCase
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:docker, "--version", "&&", :docker, :buildx, "version") SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:docker, "--version", "&&", :docker, :buildx, "version")
SSHKit::Backend::Abstract.any_instance.expects(:execute) SSHKit::Backend::Abstract.any_instance.expects(:execute)
.with(:git, "-C", "#{Dir.tmpdir}/kamal-clones/app-#{pwd_sha}", :clone, Dir.pwd, "--recurse-submodules") .with(:git, "-C", "#{Dir.tmpdir}/kamal-clones/app-#{pwd_sha}", :clone, Dir.pwd)
.raises(SSHKit::Command::Failed.new("fatal: destination path 'kamal' already exists and is not an empty directory")) .raises(SSHKit::Command::Failed.new("fatal: destination path 'kamal' already exists and is not an empty directory"))
.then .then
.returns(true) .returns(true)
@@ -124,10 +119,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, "--use", "--name", "kamal-app-multiarch") .with(:docker, :buildx, :create, "--name", "kamal-local", "--driver=docker-container")
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) SSHKit::Backend::Abstract.any_instance.expects(:execute)
.with(:docker, :buildx, :inspect, "kamal-app-multiarch", "> /dev/null") .with(:docker, :buildx, :inspect, "kamal-local")
.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") }
@@ -141,7 +136,7 @@ class CliBuildTest < CliTestCase
.returns("") .returns("")
SSHKit::Backend::Abstract.any_instance.expects(:execute) 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| 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
@@ -165,7 +160,7 @@ class CliBuildTest < CliTestCase
error = assert_raises(Kamal::Cli::HookError) { run_command("push") } error = assert_raises(Kamal::Cli::HookError) { run_command("push") }
assert_equal "Hook `pre-build` failed:\nfailed", error.message 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 end
test "pull" do test "pull" do
@@ -207,23 +202,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 --use --name kamal-app-multiarch/, output assert_match /docker buildx create --name kamal-local --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-app-native-remote-amd64 --description 'kamal-app-native-remote amd64 native host' --docker 'host=ssh://app@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 buildx create --name kamal-app-native-remote kamal-app-native-remote-amd64 --platform linux/amd64", 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
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-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 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-app-native-remote kamal-app-native-remote-amd64 --platform linux/amd64", 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
end end
@@ -240,7 +235,7 @@ class CliBuildTest < CliTestCase
test "remove" do test "remove" do
run_command("remove").tap do |output| 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
end end
@@ -250,7 +245,7 @@ class CliBuildTest < CliTestCase
.returns("docker builder info") .returns("docker builder info")
run_command("details").tap do |output| run_command("details").tap do |output|
assert_match /Builder: multiarch/, output assert_match /Builder: local/, output
assert_match /docker builder info/, output assert_match /docker builder info/, output
end end
end end

View File

@@ -36,9 +36,8 @@ class CliTestCase < ActiveSupport::TestCase
.with { |arg1, arg2| arg1 == :mkdir && arg2 == ".kamal/locks/app" } .with { |arg1, arg2| arg1 == :mkdir && arg2 == ".kamal/locks/app" }
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(:capture_with_info) SSHKit::Backend::Abstract.any_instance.stubs(:execute)
.with { |*args| args[0..2] == [ :docker, :buildx, :inspect ] } .with(:docker, :buildx, :inspect, "kamal-local")
.returns("")
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)
@@ -63,12 +62,4 @@ class CliTestCase < ActiveSupport::TestCase
assert_match expected, output assert_match expected, output
end end
def with_argv(*argv)
old_argv = ARGV
ARGV.replace(*argv)
yield
ensure
ARGV.replace(old_argv)
end
end end

View File

@@ -121,10 +121,6 @@ class CliMainTest < CliTestCase
.with(:git, "-C", anything, :status, "--porcelain") .with(:git, "-C", anything, :status, "--porcelain")
.returns("") .returns("")
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :buildx, :inspect, "kamal-app-multiarch", "> /dev/null")
.returns("")
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :info, "--format '{{index .RegistryConfig.Mirrors 0}}'") .with(:docker, :info, "--format '{{index .RegistryConfig.Mirrors 0}}'")
.returns("") .returns("")
@@ -159,10 +155,6 @@ class CliMainTest < CliTestCase
.with(:git, "-C", anything, :status, "--porcelain") .with(:git, "-C", anything, :status, "--porcelain")
.returns("") .returns("")
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :buildx, :inspect, "kamal-app-multiarch", "> /dev/null")
.returns("")
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :info, "--format '{{index .RegistryConfig.Mirrors 0}}'") .with(:docker, :info, "--format '{{index .RegistryConfig.Mirrors 0}}'")
.returns("") .returns("")
@@ -537,47 +529,9 @@ class CliMainTest < CliTestCase
assert_equal Kamal::VERSION, version assert_equal Kamal::VERSION, version
end end
test "run an alias for details" do
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:traefik:details")
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:details")
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:accessory:details", [ "all" ])
run_command("info", config_file: "deploy_with_aliases")
end
test "run an alias for a console" do
run_command("console", config_file: "deploy_with_aliases").tap do |output|
assert_match "docker exec app-console-999 bin/console on 1.1.1.5", output
assert_match "App Host: 1.1.1.5", output
end
end
test "run an alias for a console overriding role" do
run_command("console", "-r", "workers", config_file: "deploy_with_aliases").tap do |output|
assert_match "docker exec app-workers-999 bin/console on 1.1.1.3", output
assert_match "App Host: 1.1.1.3", output
end
end
test "run an alias for a console passing command" do
run_command("exec", "bin/job", config_file: "deploy_with_aliases").tap do |output|
assert_match "docker exec app-console-999 bin/job on 1.1.1.5", output
assert_match "App Host: 1.1.1.5", output
end
end
test "append to command with an alias" do
run_command("rails", "db:migrate:status", config_file: "deploy_with_aliases").tap do |output|
assert_match "docker exec app-console-999 rails db:migrate:status on 1.1.1.5", output
assert_match "App Host: 1.1.1.5", output
end
end
private private
def run_command(*command, config_file: "deploy_simple") def run_command(*command, config_file: "deploy_simple")
with_argv([ *command, "-c", "test/fixtures/#{config_file}.yml" ]) do stdouted { Kamal::Cli::Main.start([ *command, "-c", "test/fixtures/#{config_file}.yml" ]) }
stdouted { Kamal::Cli::Main.start }
end
end end
def with_test_dotenv(**files) def with_test_dotenv(**files)

View File

@@ -3,8 +3,8 @@ require_relative "cli_test_case"
class CliServerTest < CliTestCase class CliServerTest < CliTestCase
test "running a command with exec" do test "running a command with exec" do
SSHKit::Backend::Abstract.any_instance.stubs(:capture) SSHKit::Backend::Abstract.any_instance.stubs(:capture)
.with("date", verbosity: 1) .with("date", verbosity: 1)
.returns("Today") .returns("Today")
hosts = "1.1.1.1".."1.1.1.4" hosts = "1.1.1.1".."1.1.1.4"
run_command("exec", "date").tap do |output| run_command("exec", "date").tap do |output|
@@ -15,20 +15,6 @@ class CliServerTest < CliTestCase
end end
end end
test "running a command with exec multiple arguments" do
SSHKit::Backend::Abstract.any_instance.stubs(:capture)
.with("date -j", verbosity: 1)
.returns("Today")
hosts = "1.1.1.1".."1.1.1.4"
run_command("exec", "date", "-j").tap do |output|
hosts.map do |host|
assert_match "Running 'date -j' on #{hosts.to_a.join(', ')}...", output
assert_match "App Host: #{host}\nToday", output
end
end
end
test "bootstrap already installed" do test "bootstrap already installed" do
stub_setup stub_setup
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:docker, "-v", raise_on_non_zero_exit: false).returns(true).at_least_once SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:docker, "-v", raise_on_non_zero_exit: false).returns(true).at_least_once

View File

@@ -7,49 +7,49 @@ class CommandsBuilderTest < ActiveSupport::TestCase
test "target multiarch by default" do test "target multiarch by default" do
builder = new_builder_command(builder: { "cache" => { "type" => "gha" } }) builder = new_builder_command(builder: { "cache" => { "type" => "gha" } })
assert_equal "multiarch", builder.name assert_equal "local", builder.name
assert_equal \ 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(" ") builder.push.join(" ")
end end
test "target native when multiarch is off" do test "target native when multiarch is off" do
builder = new_builder_command(builder: { "multiarch" => false }) builder = new_builder_command(builder: { "multiarch" => false })
assert_equal "native", builder.name assert_equal "local", builder.name
assert_equal \ 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(" ") builder.push.join(" ")
end end
test "target native cached when multiarch is off and cache is set" do test "target native cached when multiarch is off and cache is set" do
builder = new_builder_command(builder: { "multiarch" => false, "cache" => { "type" => "gha" } }) builder = new_builder_command(builder: { "multiarch" => false, "cache" => { "type" => "gha" } })
assert_equal "native/cached", builder.name assert_equal "local", builder.name
assert_equal \ 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(" ") builder.push.join(" ")
end end
test "target multiarch remote when local and remote is set" do test "target multiarch remote when local and remote is set" do
builder = new_builder_command(builder: { "local" => {}, "remote" => {}, "cache" => { "type" => "gha" } }) builder = new_builder_command(builder: { "local" => { "arch" => "arm64" }, "remote" => { "arch" => "amd64", "host" => "ssh://app@127.0.0.1" }, "cache" => { "type" => "gha" } })
assert_equal "multiarch/remote", builder.name assert_equal "hybrid", builder.name
assert_equal \ 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(" ") builder.push.join(" ")
end end
test "target multiarch local when arch is set" do test "target multiarch local when arch is set" do
builder = new_builder_command(builder: { "local" => { "arch" => "amd64" } }) builder = new_builder_command(builder: { "local" => { "arch" => "amd64" } })
assert_equal "multiarch", builder.name assert_equal "local", builder.name
assert_equal \ 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(" ") builder.push.join(" ")
end end
test "target native remote when only remote is set" do test "target native remote when only remote is set" do
builder = new_builder_command(builder: { "remote" => { "arch" => "amd64" }, "cache" => { "type" => "gha" } }) builder = new_builder_command(builder: { "remote" => { "arch" => "amd64", "host" => "ssh://app@host" }, "cache" => { "type" => "gha" } })
assert_equal "native/remote", builder.name assert_equal "remote", builder.name
assert_equal \ 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(" ") builder.push.join(" ")
end end
@@ -93,28 +93,28 @@ 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 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(" ") builder.push.join(" ")
end end
test "native push with build args" do test "native push with build args" do
builder = new_builder_command(builder: { "multiarch" => false, "args" => { "a" => 1, "b" => 2 } }) builder = new_builder_command(builder: { "multiarch" => false, "args" => { "a" => 1, "b" => 2 } })
assert_equal \ 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(" ") builder.push.join(" ")
end end
test "multiarch push with build args" do 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 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(" ") builder.push.join(" ")
end end
test "native push with build secrets" do test "native push with build secrets" do
builder = new_builder_command(builder: { "multiarch" => false, "secrets" => [ "a", "b" ] }) builder = new_builder_command(builder: { "multiarch" => false, "secrets" => [ "a", "b" ] })
assert_equal \ 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(" ") builder.push.join(" ")
end end
@@ -133,73 +133,31 @@ class CommandsBuilderTest < ActiveSupport::TestCase
test "multiarch context build" do test "multiarch context build" do
builder = new_builder_command(builder: { "context" => "./foo" }) builder = new_builder_command(builder: { "context" => "./foo" })
assert_equal \ 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(" ") builder.push.join(" ")
end end
test "native context build" do test "native context build" do
builder = new_builder_command(builder: { "multiarch" => false, "context" => "./foo" }) builder = new_builder_command(builder: { "multiarch" => false, "context" => "./foo" })
assert_equal \ 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(" ") builder.push.join(" ")
end end
test "cached context build" do test "cached context build" do
builder = new_builder_command(builder: { "multiarch" => false, "context" => "./foo", "cache" => { "type" => "gha" } }) builder = new_builder_command(builder: { "multiarch" => false, "context" => "./foo", "cache" => { "type" => "gha" } })
assert_equal \ 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(" ") builder.push.join(" ")
end end
test "remote context build" do 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 \ 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(" ") builder.push.join(" ")
end end
test "multiarch context hosts" 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
end
test "mirror count" do test "mirror count" do
command = new_builder_command command = new_builder_command
assert_equal "docker info --format '{{index .RegistryConfig.Mirrors 0}}'", command.first_mirror.join(" ") assert_equal "docker info --format '{{index .RegistryConfig.Mirrors 0}}'", command.first_mirror.join(" ")

View File

@@ -42,7 +42,7 @@ class ConfigurationBuilderTest < ActiveSupport::TestCase
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", "host" => "unix:///Users/<%= `whoami`.strip %>/.docker/run/docker.sock" }, "local" => { "arch" => "arm64" },
"remote" => { "arch" => "amd64", "host" => "ssh://root@192.168.0.1" } "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 "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 "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 end
test "cached?" do test "cached?" do

View File

@@ -1,21 +0,0 @@
service: app
image: dhh/app
servers:
web:
- 1.1.1.1
- 1.1.1.2
workers:
hosts:
- 1.1.1.3
- 1.1.1.4
console:
hosts:
- 1.1.1.5
registry:
username: user
password: pw
aliases:
info: details
console: app exec --reuse -p -r console "bin/console"
exec: app exec --reuse -p -r console
rails: app exec --reuse -p -r console rails

View File

@@ -1,4 +1,8 @@
#!/bin/sh #!/bin/sh
echo "About to lock..." echo "About to lock..."
if [ "$KAMAL_HOSTS" != "vm1,vm2" ]; then
echo "Expected hosts to be 'vm1,vm2', got $KAMAL_HOSTS"
exit 1
fi
mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/pre-connect mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/pre-connect

View File

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

View File

@@ -1,4 +1,8 @@
#!/bin/sh #!/bin/sh
echo "About to lock..." echo "About to lock..."
if [ "$KAMAL_HOSTS" != "vm1,vm2,vm3" ]; then
echo "Expected hosts to be 'vm1,vm2,vm3', got $KAMAL_HOSTS"
exit 1
fi
mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/pre-connect mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/pre-connect

View File

@@ -18,6 +18,7 @@ registry:
password: root password: root
builder: builder:
multiarch: false multiarch: false
driver: docker
args: args:
COMMIT_SHA: <%= `git rev-parse HEAD` %> COMMIT_SHA: <%= `git rev-parse HEAD` %>
healthcheck: healthcheck:
@@ -37,7 +38,3 @@ accessories:
- web - web
stop_wait_time: 1 stop_wait_time: 1
readiness_delay: 0 readiness_delay: 0
aliases:
whome: version
worker_hostname: app exec -r workers -q --reuse hostname
uname: server exec -q -p uname

View File

@@ -82,22 +82,6 @@ class MainTest < IntegrationTest
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
test "aliases" do
@app = "app_with_roles"
kamal :envify
kamal :deploy
output = kamal :whome, capture: true
assert_equal Kamal::VERSION, output
output = kamal :worker_hostname, capture: true
assert_match /App Host: vm3\nvm3-[0-9a-f]{12}$/, output
output = kamal :uname, "-o", capture: true
assert_match "App Host: vm1\nGNU/Linux", output
end
test "setup and remove" do test "setup and remove" do
# Check remove completes when nothing has been setup yet # Check remove completes when nothing has been setup yet
kamal :remove, "-y" kamal :remove, "-y"