Skip to content

Commit

Permalink
feat: Introduce a build dev command
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
flavorjones committed Jan 20, 2025
1 parent 24e4347 commit 93644af
Show file tree
Hide file tree
Showing 4 changed files with 58 additions and 5 deletions.
22 changes: 22 additions & 0 deletions lib/kamal/cli/build.rb
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,28 @@ def details
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
def connect_to_remote_host(remote_host)
remote_uri = URI.parse(remote_host)
Expand Down
2 changes: 1 addition & 1 deletion lib/kamal/commands/builder.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
require "active_support/core_ext/string/filters"

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"

include Clone
Expand Down
15 changes: 11 additions & 4 deletions lib/kamal/commands/builder/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@ def clean
docker :image, :rm, "--force", config.absolute_image
end

def push(export_action = "registry")
def push(export_action = "registry", tag_as_dirty: false)
docker :buildx, :build,
"--output=type=#{export_action}",
*platform_options(arches),
*([ "--builder", builder_name ] unless docker_driver?),
*build_tag_options(tag_as_dirty: tag_as_dirty),
*build_options,
build_context
end
Expand All @@ -37,7 +38,7 @@ def inspect_builder
end

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

def build_context
Expand All @@ -58,8 +59,14 @@ def first_mirror
end

private
def build_tags
[ "-t", config.absolute_image, "-t", config.latest_image ]
def build_tag_names(tag_as_dirty: false)
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

def build_cache
Expand Down
24 changes: 24 additions & 0 deletions test/cli/build_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,30 @@ class CliBuildTest < CliTestCase
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
def run_command(*command, fixture: :with_accessories)
stdouted { Kamal::Cli::Build.start([ *command, "-c", "test/fixtures/deploy_#{fixture}.yml" ]) }
Expand Down

0 comments on commit 93644af

Please sign in to comment.