feat: Introduce a build dev command
which will build a "dirty" image using the working directory. This command is different from `build push` in two important ways: - the image tags will have a suffix of `-dirty` - the export action is "docker", pushing to the local docker image store The command also supports the `--output` option just added to `build push` to override that default. This command is intended to allow developers to quickly iterate on a docker image built from their local working directory while avoiding any confusion with a pristine image built from a git clone, and keeping those images on the local dev system by default.
This commit is contained in:
@@ -109,6 +109,28 @@ class Kamal::Cli::Build < Kamal::Cli::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
desc "dev", "Build using the working directory, tag it as dirty, and push to local image store."
|
||||||
|
option :output, type: :string, default: "docker", banner: "export_type", desc: "Exported type for the build result, and may be any exported type supported by 'buildx --output'."
|
||||||
|
def dev
|
||||||
|
cli = self
|
||||||
|
|
||||||
|
ensure_docker_installed
|
||||||
|
|
||||||
|
uncommitted_changes = Kamal::Git.uncommitted_changes
|
||||||
|
if uncommitted_changes.present?
|
||||||
|
say "WARNING: building with uncommitted changes:\n #{uncommitted_changes}", :yellow
|
||||||
|
end
|
||||||
|
|
||||||
|
with_env(KAMAL.config.builder.secrets) do
|
||||||
|
run_locally do
|
||||||
|
build = KAMAL.builder.push(cli.options[:output], tag_as_dirty: true)
|
||||||
|
KAMAL.with_verbosity(:debug) do
|
||||||
|
execute(*build)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def connect_to_remote_host(remote_host)
|
def connect_to_remote_host(remote_host)
|
||||||
remote_uri = URI.parse(remote_host)
|
remote_uri = URI.parse(remote_host)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
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, :inspect_builder, :validate_image, :first_mirror, to: :target
|
delegate :create, :remove, :dev, :push, :clean, :pull, :info, :inspect_builder, :validate_image, :first_mirror, to: :target
|
||||||
delegate :local?, :remote?, :cloud?, to: "config.builder"
|
delegate :local?, :remote?, :cloud?, to: "config.builder"
|
||||||
|
|
||||||
include Clone
|
include Clone
|
||||||
|
|||||||
@@ -13,11 +13,12 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
|
|||||||
docker :image, :rm, "--force", config.absolute_image
|
docker :image, :rm, "--force", config.absolute_image
|
||||||
end
|
end
|
||||||
|
|
||||||
def push(export_action = "registry")
|
def push(export_action = "registry", tag_as_dirty: false)
|
||||||
docker :buildx, :build,
|
docker :buildx, :build,
|
||||||
"--output=type=#{export_action}",
|
"--output=type=#{export_action}",
|
||||||
*platform_options(arches),
|
*platform_options(arches),
|
||||||
*([ "--builder", builder_name ] unless docker_driver?),
|
*([ "--builder", builder_name ] unless docker_driver?),
|
||||||
|
*build_tag_options(tag_as_dirty: tag_as_dirty),
|
||||||
*build_options,
|
*build_options,
|
||||||
build_context
|
build_context
|
||||||
end
|
end
|
||||||
@@ -37,7 +38,7 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def build_options
|
def build_options
|
||||||
[ *build_tags, *build_cache, *build_labels, *build_args, *build_secrets, *build_dockerfile, *build_target, *build_ssh, *builder_provenance, *builder_sbom ]
|
[ *build_cache, *build_labels, *build_args, *build_secrets, *build_dockerfile, *build_target, *build_ssh, *builder_provenance, *builder_sbom ]
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_context
|
def build_context
|
||||||
@@ -58,8 +59,14 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def build_tags
|
def build_tag_names(tag_as_dirty: false)
|
||||||
[ "-t", config.absolute_image, "-t", config.latest_image ]
|
tag_names = [ config.absolute_image, config.latest_image ]
|
||||||
|
tag_names.map! { |t| "#{t}-dirty" } if tag_as_dirty
|
||||||
|
tag_names
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_tag_options(tag_as_dirty: false)
|
||||||
|
build_tag_names(tag_as_dirty: tag_as_dirty).flat_map { |name| [ "-t", name ] }
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_cache
|
def build_cache
|
||||||
|
|||||||
@@ -298,6 +298,30 @@ class CliBuildTest < CliTestCase
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "dev" do
|
||||||
|
with_build_directory do |build_directory|
|
||||||
|
Kamal::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)
|
||||||
|
|
||||||
|
run_command("dev", "--verbose").tap do |output|
|
||||||
|
assert_no_match(/Cloning repo into build directory/, output)
|
||||||
|
assert_match(/docker --version && docker buildx version/, output)
|
||||||
|
assert_match(/docker buildx build --output=type=docker --platform linux\/amd64 --builder kamal-local-docker-container -t dhh\/app:999-dirty -t dhh\/app:latest-dirty --label service="app" --file Dockerfile \. as .*@localhost/, output)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "dev --output=local" do
|
||||||
|
with_build_directory do |build_directory|
|
||||||
|
Kamal::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)
|
||||||
|
|
||||||
|
run_command("dev", "--output=local", "--verbose").tap do |output|
|
||||||
|
assert_no_match(/Cloning repo into build directory/, output)
|
||||||
|
assert_match(/docker --version && docker buildx version/, output)
|
||||||
|
assert_match(/docker buildx build --output=type=local --platform linux\/amd64 --builder kamal-local-docker-container -t dhh\/app:999-dirty -t dhh\/app:latest-dirty --label service="app" --file Dockerfile \. as .*@localhost/, output)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def run_command(*command, fixture: :with_accessories)
|
def run_command(*command, fixture: :with_accessories)
|
||||||
stdouted { Kamal::Cli::Build.start([ *command, "-c", "test/fixtures/deploy_#{fixture}.yml" ]) }
|
stdouted { Kamal::Cli::Build.start([ *command, "-c", "test/fixtures/deploy_#{fixture}.yml" ]) }
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase
|
|||||||
test "build args" do
|
test "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 \
|
||||||
"-t dhh/app:123 -t dhh/app:latest --label service=\"app\" --build-arg a=\"1\" --build-arg b=\"2\" --file Dockerfile",
|
"--label service=\"app\" --build-arg a=\"1\" --build-arg b=\"2\" --file Dockerfile",
|
||||||
builder.target.build_options.join(" ")
|
builder.target.build_options.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase
|
|||||||
FileUtils.touch("Dockerfile")
|
FileUtils.touch("Dockerfile")
|
||||||
builder = new_builder_command(builder: { "secrets" => [ "token_a", "token_b" ] })
|
builder = new_builder_command(builder: { "secrets" => [ "token_a", "token_b" ] })
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"-t dhh/app:123 -t dhh/app:latest --label service=\"app\" --secret id=\"token_a\" --secret id=\"token_b\" --file Dockerfile",
|
"--label service=\"app\" --secret id=\"token_a\" --secret id=\"token_b\" --file Dockerfile",
|
||||||
builder.target.build_options.join(" ")
|
builder.target.build_options.join(" ")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -90,7 +90,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase
|
|||||||
Pathname.any_instance.expects(:exist?).returns(true).once
|
Pathname.any_instance.expects(:exist?).returns(true).once
|
||||||
builder = new_builder_command(builder: { "dockerfile" => "Dockerfile.xyz" })
|
builder = new_builder_command(builder: { "dockerfile" => "Dockerfile.xyz" })
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"-t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile.xyz",
|
"--label service=\"app\" --file Dockerfile.xyz",
|
||||||
builder.target.build_options.join(" ")
|
builder.target.build_options.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase
|
|||||||
test "build target" do
|
test "build target" do
|
||||||
builder = new_builder_command(builder: { "target" => "prod" })
|
builder = new_builder_command(builder: { "target" => "prod" })
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"-t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile --target prod",
|
"--label service=\"app\" --file Dockerfile --target prod",
|
||||||
builder.target.build_options.join(" ")
|
builder.target.build_options.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -137,7 +137,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase
|
|||||||
builder = new_builder_command(builder: { "ssh" => "default=$SSH_AUTH_SOCK" })
|
builder = new_builder_command(builder: { "ssh" => "default=$SSH_AUTH_SOCK" })
|
||||||
|
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"-t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile --ssh default=$SSH_AUTH_SOCK",
|
"--label service=\"app\" --file Dockerfile --ssh default=$SSH_AUTH_SOCK",
|
||||||
builder.target.build_options.join(" ")
|
builder.target.build_options.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user