Merge branch 'main' into pr/223
* main: Don't run actions twice on PRs Further distinguish dependency verification Naming Reveal configured dockerfile path Style Distinguish from server dependencies Distinguish from local dependency verification Improve clarity and intent Style Style Style Add local dependencies check Bootstrap: use multi-platform installer
This commit is contained in:
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -1,5 +1,9 @@
|
||||
name: CI
|
||||
on: [push, pull_request]
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
jobs:
|
||||
tests:
|
||||
strategy:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -17,9 +17,6 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
|
||||
invoke_options = deploy_options
|
||||
|
||||
runtime = print_runtime do
|
||||
say "Ensure curl and Docker are installed...", :magenta
|
||||
invoke "mrsk:cli:server:bootstrap", [], invoke_options
|
||||
|
||||
say "Log into image registry...", :magenta
|
||||
invoke "mrsk:cli:registry:login", [], invoke_options
|
||||
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
class Mrsk::Cli::Server < Mrsk::Cli::Base
|
||||
desc "bootstrap", "Ensure curl and Docker are installed on servers"
|
||||
desc "bootstrap", "Set up Docker to run MRSK apps"
|
||||
def bootstrap
|
||||
with_lock do
|
||||
on(MRSK.hosts + MRSK.accessory_hosts) do
|
||||
dependencies_to_install = Array.new.tap do |dependencies|
|
||||
dependencies << "curl" unless execute "which curl", raise_on_non_zero_exit: false
|
||||
dependencies << "docker.io" unless execute "which docker", raise_on_non_zero_exit: false
|
||||
end
|
||||
missing = []
|
||||
|
||||
if dependencies_to_install.any?
|
||||
execute "apt-get update -y && apt-get install #{dependencies_to_install.join(" ")} -y"
|
||||
on(MRSK.hosts | MRSK.accessory_hosts) do |host|
|
||||
unless execute(*MRSK.docker.installed?, raise_on_non_zero_exit: false)
|
||||
if execute(*MRSK.docker.superuser?, raise_on_non_zero_exit: false)
|
||||
info "Missing Docker on #{host}. Installing…"
|
||||
execute *MRSK.docker.install
|
||||
else
|
||||
missing << host
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if missing.any?
|
||||
raise "Docker is not installed on #{missing.join(", ")} and can't be automatically installed without having root access and the `curl` command available. Install Docker manually: https://docs.docker.com/engine/install/"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -92,6 +92,10 @@ class Mrsk::Commander
|
||||
@builder ||= Mrsk::Commands::Builder.new(config)
|
||||
end
|
||||
|
||||
def docker
|
||||
@docker ||= Mrsk::Commands::Docker.new(config)
|
||||
end
|
||||
|
||||
def healthcheck
|
||||
@healthcheck ||= Mrsk::Commands::Healthcheck.new(config)
|
||||
end
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
21
lib/mrsk/commands/docker.rb
Normal file
21
lib/mrsk/commands/docker.rb
Normal file
@@ -0,0 +1,21 @@
|
||||
class Mrsk::Commands::Docker < Mrsk::Commands::Base
|
||||
# Install Docker using the https://github.com/docker/docker-install convenience script.
|
||||
def install
|
||||
pipe [ :curl, "-fsSL", "https://get.docker.com" ], :sh
|
||||
end
|
||||
|
||||
# Checks the Docker client version. Fails if Docker is not installed.
|
||||
def installed?
|
||||
docker "-v"
|
||||
end
|
||||
|
||||
# Checks the Docker server version. Fails if Docker is not running.
|
||||
def running?
|
||||
docker :version
|
||||
end
|
||||
|
||||
# Do we have superuser access to install Docker and start system services?
|
||||
def superuser?
|
||||
[ '[ "${EUID:-$(id -u)}" -eq 0 ]' ]
|
||||
end
|
||||
end
|
||||
@@ -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"]) }
|
||||
|
||||
@@ -12,7 +12,6 @@ class CliMainTest < CliTestCase
|
||||
test "deploy" do
|
||||
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "skip_broadcast" => false, "version" => "999" }
|
||||
|
||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:server:bootstrap", [], invoke_options)
|
||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:registry:login", [], invoke_options)
|
||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:build:deliver", [], invoke_options)
|
||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:traefik:boot", [], invoke_options)
|
||||
@@ -22,7 +21,6 @@ class CliMainTest < CliTestCase
|
||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:prune:all", [], invoke_options)
|
||||
|
||||
run_command("deploy").tap do |output|
|
||||
assert_match /Ensure curl and Docker are installed/, output
|
||||
assert_match /Log into image registry/, output
|
||||
assert_match /Build and push app image/, output
|
||||
assert_match /Ensure Traefik is running/, output
|
||||
@@ -35,7 +33,6 @@ class CliMainTest < CliTestCase
|
||||
test "deploy with skip_push" do
|
||||
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "skip_broadcast" => false, "version" => "999" }
|
||||
|
||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:server:bootstrap", [], invoke_options)
|
||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:registry:login", [], invoke_options)
|
||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:build:pull", [], invoke_options)
|
||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:traefik:boot", [], invoke_options)
|
||||
@@ -46,7 +43,6 @@ class CliMainTest < CliTestCase
|
||||
|
||||
run_command("deploy", "--skip_push").tap do |output|
|
||||
assert_match /Acquiring the deploy lock/, output
|
||||
assert_match /Ensure curl and Docker are installed/, output
|
||||
assert_match /Log into image registry/, output
|
||||
assert_match /Pull app image/, output
|
||||
assert_match /Ensure Traefik is running/, output
|
||||
@@ -87,7 +83,6 @@ class CliMainTest < CliTestCase
|
||||
test "deploy errors during critical section leave lock in place" do
|
||||
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "skip_broadcast" => false, "version" => "999" }
|
||||
|
||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:server:bootstrap", [], invoke_options)
|
||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:registry:login", [], invoke_options)
|
||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:build:deliver", [], invoke_options)
|
||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:app:stale_containers", [], invoke_options)
|
||||
@@ -106,7 +101,7 @@ class CliMainTest < CliTestCase
|
||||
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "skip_broadcast" => false, "version" => "999" }
|
||||
|
||||
Mrsk::Cli::Main.any_instance.expects(:invoke)
|
||||
.with("mrsk:cli:server:bootstrap", [], invoke_options)
|
||||
.with("mrsk:cli:registry:login", [], invoke_options)
|
||||
.raises(RuntimeError)
|
||||
|
||||
assert !MRSK.holding_lock?
|
||||
|
||||
@@ -1,11 +1,30 @@
|
||||
require_relative "cli_test_case"
|
||||
|
||||
class CliServerTest < CliTestCase
|
||||
test "bootstrap" do
|
||||
test "bootstrap already installed" do
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:docker, "-v", raise_on_non_zero_exit: false).returns(true).at_least_once
|
||||
|
||||
assert_equal "", run_command("bootstrap")
|
||||
end
|
||||
|
||||
test "bootstrap install as non-root user" do
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:docker, "-v", raise_on_non_zero_exit: false).returns(false).at_least_once
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:execute).with('[ "${EUID:-$(id -u)}" -eq 0 ]', raise_on_non_zero_exit: false).returns(false).at_least_once
|
||||
|
||||
assert_raise RuntimeError, "Docker is not installed on 1.1.1.1, 1.1.1.3, 1.1.1.4, 1.1.1.2 and can't be automatically intalled without having root access and the `curl` command available. Install Docker manually: https://docs.docker.com/engine/install/" do
|
||||
run_command("bootstrap")
|
||||
end
|
||||
end
|
||||
|
||||
test "bootstrap install as root user" do
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:docker, "-v", raise_on_non_zero_exit: false).returns(false).at_least_once
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:execute).with('[ "${EUID:-$(id -u)}" -eq 0 ]', raise_on_non_zero_exit: false).returns(true).at_least_once
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:curl, "-fsSL", "https://get.docker.com", "|", :sh).at_least_once
|
||||
|
||||
run_command("bootstrap").tap do |output|
|
||||
assert_match /which curl/, output
|
||||
assert_match /which docker/, output
|
||||
assert_match /apt-get update -y && apt-get install curl docker.io -y/, output
|
||||
("1.1.1.1".."1.1.1.4").map do |host|
|
||||
assert_match "Missing Docker on #{host}. Installing…", output
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -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 \
|
||||
|
||||
26
test/commands/docker_test.rb
Normal file
26
test/commands/docker_test.rb
Normal file
@@ -0,0 +1,26 @@
|
||||
require "test_helper"
|
||||
|
||||
class CommandsDockerTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
@config = {
|
||||
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ]
|
||||
}
|
||||
@docker = Mrsk::Commands::Docker.new(Mrsk::Configuration.new(@config))
|
||||
end
|
||||
|
||||
test "install" do
|
||||
assert_equal "curl -fsSL https://get.docker.com | sh", @docker.install.join(" ")
|
||||
end
|
||||
|
||||
test "installed?" do
|
||||
assert_equal "docker -v", @docker.installed?.join(" ")
|
||||
end
|
||||
|
||||
test "running?" do
|
||||
assert_equal "docker version", @docker.running?.join(" ")
|
||||
end
|
||||
|
||||
test "superuser?" do
|
||||
assert_equal '[ "${EUID:-$(id -u)}" -eq 0 ]', @docker.superuser?.join(" ")
|
||||
end
|
||||
end
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user