Add local dependencies check

Add checks for:

* Docker installed locally
* Docker buildx plugin installed locally
* Dockerfile exists

If checks fail, it will halt deployment and provide more specific error messages.

Also adds a cli subcommand:
`mrsk build dependencies`

Fixes: #109 and #237
This commit is contained in:
Jberczel
2023-05-01 15:26:50 -04:00
parent 4fa6a6c06d
commit bfb70b2118
5 changed files with 83 additions and 4 deletions

View File

@@ -1,4 +1,7 @@
class Mrsk::Cli::Build < Mrsk::Cli::Base 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" desc "deliver", "Build app and push app image to registry then pull image on servers"
def deliver def deliver
with_lock do with_lock do
@@ -10,16 +13,18 @@ class Mrsk::Cli::Build < Mrsk::Cli::Base
desc "push", "Build and push app image to registry" desc "push", "Build and push app image to registry"
def push def push
with_lock do with_lock do
cli = self cli_build = self
run_locally do run_locally do
begin begin
MRSK.with_verbosity(:debug) { execute *MRSK.builder.push } if cli_build.dependencies
MRSK.with_verbosity(:debug) { execute *MRSK.builder.push }
end
rescue SSHKit::Command::Failed => e rescue SSHKit::Command::Failed => e
if e.message =~ /(no builder)|(no such file or directory)/ if e.message =~ /(no builder)|(no such file or directory)/
error "Missing compatible builder, so creating a new one first" error "Missing compatible builder, so creating a new one first"
if cli.create if cli_build.create
MRSK.with_verbosity(:debug) { execute *MRSK.builder.push } MRSK.with_verbosity(:debug) { execute *MRSK.builder.push }
end end
else else
@@ -77,4 +82,19 @@ class Mrsk::Cli::Build < Mrsk::Cli::Base
puts capture(*MRSK.builder.info) puts capture(*MRSK.builder.info)
end end
end end
desc "dependencies", "Check local dependencies"
def dependencies
run_locally do
begin
execute *MRSK.builder.dependencies
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 end

View File

@@ -33,4 +33,28 @@ class Mrsk::Commands::Builder < Mrsk::Commands::Base
def multiarch_remote def multiarch_remote
@multiarch_remote ||= Mrsk::Commands::Builder::Multiarch::Remote.new(config) @multiarch_remote ||= Mrsk::Commands::Builder::Multiarch::Remote.new(config)
end end
def native_and_local?
name == 'native'
end
def dependencies
if native_and_local?
docker_version
else
combine \
docker_version,
docker_buildx_version
end
end
private
def docker_version
docker "--version"
end
def docker_buildx_version
docker :buildx, "version"
end
end end

View File

@@ -1,6 +1,9 @@
class Mrsk::Commands::Builder::Base < Mrsk::Commands::Base class Mrsk::Commands::Builder::Base < Mrsk::Commands::Base
delegate :argumentize, to: Mrsk::Utils delegate :argumentize, to: Mrsk::Utils
class BuilderError < StandardError; end
def clean def clean
docker :image, :rm, "--force", config.absolute_image docker :image, :rm, "--force", config.absolute_image
end end
@@ -17,6 +20,7 @@ class Mrsk::Commands::Builder::Base < Mrsk::Commands::Base
context context
end end
private private
def build_tags def build_tags
[ "-t", config.absolute_image, "-t", config.latest_image ] [ "-t", config.absolute_image, "-t", config.latest_image ]
@@ -35,7 +39,11 @@ class Mrsk::Commands::Builder::Base < Mrsk::Commands::Base
end end
def build_dockerfile def build_dockerfile
argumentize "--file", dockerfile if Pathname.new(File.expand_path(dockerfile)).exist?
argumentize "--file", dockerfile
else
raise BuilderError, "Missing Dockerfile"
end
end end
def args def args

View File

@@ -9,6 +9,7 @@ class CliBuildTest < CliTestCase
end end
test "push" do test "push" do
Mrsk::Cli::Build.any_instance.stubs(:dependencies).returns(true)
run_command("push").tap do |output| 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 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 end
@@ -16,6 +17,7 @@ class CliBuildTest < CliTestCase
test "push without builder" do test "push without builder" do
stub_locking stub_locking
Mrsk::Cli::Build.any_instance.stubs(:dependencies).returns(true)
SSHKit::Backend::Abstract.any_instance.stubs(:execute) SSHKit::Backend::Abstract.any_instance.stubs(:execute)
.with { |arg| arg == :docker } .with { |arg| arg == :docker }
.raises(SSHKit::Command::Failed.new("no builder")) .raises(SSHKit::Command::Failed.new("no builder"))
@@ -68,6 +70,22 @@ class CliBuildTest < CliTestCase
end end
end end
test "dependencies" do
Mrsk::Commands::Builder.any_instance.stubs(:native_and_local?).returns(false)
run_command("dependencies").tap do |output|
assert_match /docker --version && docker buildx version/, output
end
end
test "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("dependencies") }
end
private private
def run_command(*command) def run_command(*command)
stdouted { Mrsk::Cli::Build.start([*command, "-c", "test/fixtures/deploy_with_accessories.yml"]) } stdouted { Mrsk::Cli::Build.start([*command, "-c", "test/fixtures/deploy_with_accessories.yml"]) }

View File

@@ -52,12 +52,21 @@ class CommandsBuilderTest < ActiveSupport::TestCase
end end
test "build dockerfile" do test "build dockerfile" do
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", "-t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile.xyz",
builder.target.build_options.join(" ") builder.target.build_options.join(" ")
end 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 test "build context" do
builder = new_builder_command(builder: { "context" => ".." }) builder = new_builder_command(builder: { "context" => ".." })
assert_equal \ assert_equal \