From e85bd5ff63d67dac13bb86825df9b89d3e11694e Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Mon, 10 Apr 2023 10:42:29 -0700 Subject: [PATCH] Bootstrap: use multi-platform installer * Limit auto-install to root users; otherwise, give manual install guidance * Support non-Debian/Ubuntu with the multi-OS get.docker.com installer --- lib/mrsk/cli/main.rb | 3 --- lib/mrsk/cli/server.rb | 22 +++++++++++++--------- lib/mrsk/commander.rb | 4 ++++ lib/mrsk/commands/docker.rb | 21 +++++++++++++++++++++ test/cli/main_test.rb | 7 +------ test/cli/server_test.rb | 27 +++++++++++++++++++++++---- test/commands/docker_test.rb | 26 ++++++++++++++++++++++++++ 7 files changed, 88 insertions(+), 22 deletions(-) create mode 100644 lib/mrsk/commands/docker.rb create mode 100644 test/commands/docker_test.rb diff --git a/lib/mrsk/cli/main.rb b/lib/mrsk/cli/main.rb index 0a3d2ce0..96c78098 100644 --- a/lib/mrsk/cli/main.rb +++ b/lib/mrsk/cli/main.rb @@ -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 diff --git a/lib/mrsk/cli/server.rb b/lib/mrsk/cli/server.rb index 0ede5afc..6e05559c 100644 --- a/lib/mrsk/cli/server.rb +++ b/lib/mrsk/cli/server.rb @@ -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 diff --git a/lib/mrsk/commander.rb b/lib/mrsk/commander.rb index 44c96a2a..14b2c2f8 100644 --- a/lib/mrsk/commander.rb +++ b/lib/mrsk/commander.rb @@ -84,6 +84,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 diff --git a/lib/mrsk/commands/docker.rb b/lib/mrsk/commands/docker.rb new file mode 100644 index 00000000..85101d0f --- /dev/null +++ b/lib/mrsk/commands/docker.rb @@ -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 diff --git a/test/cli/main_test.rb b/test/cli/main_test.rb index 5cac1132..bcd72813 100644 --- a/test/cli/main_test.rb +++ b/test/cli/main_test.rb @@ -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? diff --git a/test/cli/server_test.rb b/test/cli/server_test.rb index bf0119f4..48d4484f 100644 --- a/test/cli/server_test.rb +++ b/test/cli/server_test.rb @@ -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 diff --git a/test/commands/docker_test.rb b/test/commands/docker_test.rb new file mode 100644 index 00000000..71d1d798 --- /dev/null +++ b/test/commands/docker_test.rb @@ -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