Files
kamal/lib/kamal/cli/build.rb
Donal McBreen 307750ff70 Build from within a git clone by default
Docker does not respect the .dockerignore file when building from a tar.

Instead by default we'll make a local clone into a tmp directory and
build from there. Subsequent builds will reset the clone to match the
checkout.

Compared to building directly in the repo, we'll have reproducible
builds.

Compared to using a git archive:
1. .dockerignore is respected
2. We'll have faster builds - docker can be smarter about caching the
build context on subsequent builds from a directory

To build from the repo directly, set the build context to "." in the
config.

If there are uncommitted changes, we'll warn about them either being
included or ignored depending on whether we build from the clone.
2024-05-20 09:30:56 +01:00

156 lines
4.2 KiB
Ruby

require "uri"
class Kamal::Cli::Build < Kamal::Cli::Base
class BuildError < StandardError; end
desc "deliver", "Build app and push app image to registry then pull image on servers"
def deliver
mutating do
push
pull
end
end
desc "push", "Build and push app image to registry"
def push
mutating do
cli = self
verify_local_dependencies
run_hook "pre-build"
uncommitted_changes = Kamal::Git.uncommitted_changes
if KAMAL.config.builder.git_clone?
if uncommitted_changes.present?
say "Building from a local git clone, so ignoring these uncommitted changes:\n #{uncommitted_changes}", :yellow
end
prepare_clone
elsif uncommitted_changes.present?
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
KAMAL.with_verbosity(:debug) do
Dir.chdir(KAMAL.config.builder.build_directory) { execute *push }
end
rescue SSHKit::Command::Failed => e
if e.message =~ /(no builder)|(no such file or directory)/
warn "Missing compatible builder, so creating a new one first"
if cli.create
KAMAL.with_verbosity(:debug) do
Dir.chdir(KAMAL.config.builder.build_directory) { execute *push }
end
end
else
raise
end
end
end
end
end
desc "pull", "Pull app image from registry onto servers"
def pull
mutating do
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
end
end
end
desc "create", "Create a build setup"
def create
mutating do
if (remote_host = KAMAL.config.builder.remote_host)
connect_to_remote_host(remote_host)
end
run_locally do
begin
debug "Using builder: #{KAMAL.builder.name}"
execute *KAMAL.builder.create
rescue SSHKit::Command::Failed => e
if e.message =~ /stderr=(.*)/
error "Couldn't create remote builder: #{$1}"
false
else
raise
end
end
end
end
end
desc "remove", "Remove build setup"
def remove
mutating do
run_locally do
debug "Using builder: #{KAMAL.builder.name}"
execute *KAMAL.builder.remove
end
end
end
desc "details", "Show build setup"
def details
run_locally do
puts "Builder: #{KAMAL.builder.name}"
puts capture(*KAMAL.builder.info)
end
end
private
def verify_local_dependencies
run_locally do
begin
execute *KAMAL.builder.ensure_local_dependencies_installed
rescue SSHKit::Command::Failed => e
build_error = e.message =~ /command not found/ ?
"Docker is not installed locally" :
"Docker buildx plugin is not installed locally"
raise BuildError, build_error
end
end
end
def connect_to_remote_host(remote_host)
remote_uri = URI.parse(remote_host)
if remote_uri.scheme == "ssh"
options = { user: remote_uri.user, port: remote_uri.port }.compact
on(remote_uri.host, options) do
execute "true"
end
end
end
def prepare_clone
run_locally do
begin
info "Cloning repo into build directory `#{KAMAL.config.builder.build_directory}`..."
execute *KAMAL.builder.create_clone_directory
execute *KAMAL.builder.clone
rescue SSHKit::Command::Failed => e
if e.message =~ /already exists and is not an empty directory/
info "Resetting local clone as `#{KAMAL.config.builder.build_directory}` already exists..."
KAMAL.builder.clone_reset_steps.each { |step| execute *step }
else
raise
end
end
end
end
end