diff --git a/lib/mrsk/cli/build.rb b/lib/mrsk/cli/build.rb index 289f0bcb..0e6e9a7c 100644 --- a/lib/mrsk/cli/build.rb +++ b/lib/mrsk/cli/build.rb @@ -1,4 +1,6 @@ class Mrsk::Cli::Build < Mrsk::Cli::Base + class BuildError < StandardError; end + desc "deliver", "Build app and push app image to registry then pull image on servers" def deliver with_lock do @@ -14,7 +16,9 @@ class Mrsk::Cli::Build < Mrsk::Cli::Base run_locally do begin - MRSK.with_verbosity(:debug) { execute *MRSK.builder.push } + if cli.verify_local_dependencies + MRSK.with_verbosity(:debug) { execute *MRSK.builder.push } + end rescue SSHKit::Command::Failed => e if e.message =~ /(no builder)|(no such file or directory)/ error "Missing compatible builder, so creating a new one first" @@ -77,4 +81,22 @@ class Mrsk::Cli::Build < Mrsk::Cli::Base puts capture(*MRSK.builder.info) end end + + + desc "", "" # Really a private method, but needed to be invoked from #push + def verify_local_dependencies + run_locally do + begin + execute *MRSK.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 + + true + end end diff --git a/lib/mrsk/commands/builder.rb b/lib/mrsk/commands/builder.rb index 85dbce0a..58ca626e 100644 --- a/lib/mrsk/commands/builder.rb +++ b/lib/mrsk/commands/builder.rb @@ -2,7 +2,7 @@ class Mrsk::Commands::Builder < Mrsk::Commands::Base delegate :create, :remove, :push, :clean, :pull, :info, to: :target def name - target.class.to_s.remove("Mrsk::Commands::Builder::").underscore + target.class.to_s.remove("Mrsk::Commands::Builder::").underscore.inquiry end def target @@ -33,4 +33,24 @@ class Mrsk::Commands::Builder < Mrsk::Commands::Base def multiarch_remote @multiarch_remote ||= Mrsk::Commands::Builder::Multiarch::Remote.new(config) end + + + def ensure_local_dependencies_installed + if name.native? + ensure_local_docker_installed + else + combine \ + ensure_local_docker_installed, + ensure_local_buildx_installed + end + end + + private + def ensure_local_docker_installed + docker "--version" + end + + def ensure_local_buildx_installed + docker :buildx, "version" + end end diff --git a/lib/mrsk/commands/builder/base.rb b/lib/mrsk/commands/builder/base.rb index 15c6b188..32dcba4f 100644 --- a/lib/mrsk/commands/builder/base.rb +++ b/lib/mrsk/commands/builder/base.rb @@ -1,4 +1,7 @@ + class Mrsk::Commands::Builder::Base < Mrsk::Commands::Base + class BuilderError < StandardError; end + delegate :argumentize, to: Mrsk::Utils def clean @@ -17,6 +20,7 @@ class Mrsk::Commands::Builder::Base < Mrsk::Commands::Base context end + private def build_tags [ "-t", config.absolute_image, "-t", config.latest_image ] @@ -35,7 +39,11 @@ class Mrsk::Commands::Builder::Base < Mrsk::Commands::Base end def build_dockerfile - argumentize "--file", dockerfile + if Pathname.new(File.expand_path(dockerfile)).exist? + argumentize "--file", dockerfile + else + raise BuilderError, "Missing #{dockerfile}" + end end def args diff --git a/test/cli/build_test.rb b/test/cli/build_test.rb index a610d849..0835262c 100644 --- a/test/cli/build_test.rb +++ b/test/cli/build_test.rb @@ -9,6 +9,7 @@ class CliBuildTest < CliTestCase end test "push" do + Mrsk::Cli::Build.any_instance.stubs(:verify_local_dependencies).returns(true) run_command("push").tap do |output| assert_match /docker buildx build --push --platform linux\/amd64,linux\/arm64 --builder mrsk-app-multiarch -t dhh\/app:999 -t dhh\/app:latest --label service="app" --file Dockerfile \. as .*@localhost/, output end @@ -16,6 +17,7 @@ class CliBuildTest < CliTestCase test "push without builder" do stub_locking + Mrsk::Cli::Build.any_instance.stubs(:verify_local_dependencies).returns(true) SSHKit::Backend::Abstract.any_instance.stubs(:execute) .with { |arg| arg == :docker } .raises(SSHKit::Command::Failed.new("no builder")) @@ -68,6 +70,23 @@ class CliBuildTest < CliTestCase end end + test "verify local dependencies" do + Mrsk::Commands::Builder.any_instance.stubs(:name).returns("remote".inquiry) + + run_command("verify_local_dependencies").tap do |output| + assert_match /docker --version && docker buildx version/, output + end + end + + test "verify local dependencies with no buildx plugin" do + SSHKit::Backend::Abstract.any_instance.stubs(:execute) + .with(:docker, "--version", "&&", :docker, :buildx, "version") + .raises(SSHKit::Command::Failed.new("no buildx")) + + Mrsk::Commands::Builder.any_instance.stubs(:native_and_local?).returns(false) + assert_raises(Mrsk::Cli::Build::BuildError) { run_command("verify_local_dependencies") } + end + private def run_command(*command) stdouted { Mrsk::Cli::Build.start([*command, "-c", "test/fixtures/deploy_with_accessories.yml"]) } diff --git a/test/commands/builder_test.rb b/test/commands/builder_test.rb index 1ecea021..e5f83f1c 100644 --- a/test/commands/builder_test.rb +++ b/test/commands/builder_test.rb @@ -52,12 +52,21 @@ class CommandsBuilderTest < ActiveSupport::TestCase end test "build dockerfile" do + Pathname.any_instance.expects(:exist?).returns(true).once builder = new_builder_command(builder: { "dockerfile" => "Dockerfile.xyz" }) assert_equal \ "-t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile.xyz", builder.target.build_options.join(" ") end + test "missing dockerfile" do + Pathname.any_instance.expects(:exist?).returns(false).once + builder = new_builder_command(builder: { "dockerfile" => "Dockerfile.xyz" }) + assert_raises(Mrsk::Commands::Builder::Base::BuilderError) do + builder.target.build_options.join(" ") + end + end + test "build context" do builder = new_builder_command(builder: { "context" => ".." }) assert_equal \ diff --git a/test/configuration_test.rb b/test/configuration_test.rb index 2697673b..eda85484 100644 --- a/test/configuration_test.rb +++ b/test/configuration_test.rb @@ -48,7 +48,7 @@ class ConfigurationTest < ActiveSupport::TestCase end test "role" do - assert_equal "web", @config.role(:web).name + assert @config.role(:web).name.web? assert_equal "workers", @config_with_roles.role(:workers).name assert_nil @config.role(:missing) end