From 5b506a2daa25ee6d35127ecb635020ec6d6b8eb0 Mon Sep 17 00:00:00 2001 From: Robert Starmer Date: Tue, 14 Mar 2023 14:14:02 -0700 Subject: [PATCH 1/9] add D-in-D dockerfile, update Readme --- Dockerfile.dind | 33 +++++++++++++++++++++++++++++++++ README.md | 16 ++++++++++++++-- 2 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 Dockerfile.dind diff --git a/Dockerfile.dind b/Dockerfile.dind new file mode 100644 index 00000000..d0c9d90a --- /dev/null +++ b/Dockerfile.dind @@ -0,0 +1,33 @@ +# Use the official Docker docker-in-docker Alpine image as the base image +FROM docker:dind + +# Set the working directory to /mrsk +WORKDIR /mrsk + +# Copy the Gemfile, Gemfile.lock into the container +COPY Gemfile Gemfile.lock mrsk.gemspec ./ + +# Required in mrsk.gemspec +COPY lib/mrsk/version.rb /mrsk/lib/mrsk/version.rb + +# Install system dependencies including ruby-dev +RUN apk add --no-cache --update ruby-dev build-base git docker openrc \ + && rc-update add docker boot \ + && gem install bundler --version=2.4.3 \ + && bundle install + +# Copy the rest of our application code into the container. +# We do this after bundle install, to avoid having to run bundle +# everytime we do small fixes in the source code. +COPY . . + +# Install the gem locally from the project folder +RUN gem build mrsk.gemspec && \ + gem install ./mrsk-*.gem --no-document + +# Set the working directory to /workdir +WORKDIR /workdir + +# Set the entrypoint to run the installed binary in /workdir +# Example: docker run -it -v "$PWD:/workdir" mrsk init +ENTRYPOINT ["mrsk"] diff --git a/README.md b/README.md index 168bed4c..7d4f69c5 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,19 @@ Watch the screencast: https://www.youtube.com/watch?v=LL1cV2FXZ5I ## Installation -Install MRSK globally with `gem install mrsk`. Then, inside your app directory, run `mrsk init` (or `mrsk init --bundle` within Rails apps where you want a bin/mrsk binstub). Now edit the new file `config/deploy.yml`. It could look as simple as this: +Install MRSK globally with `gem install mrsk` or build a dockerized version: + +```sh +docker build . -t mrsk:dind -t mrsk:latest -f Dockerfile.dind +``` + +If you build the docker version, add the following alias to your .${SHELL}rc file and re-source the rc file to allow ssh and docker to work within the container: + +```sh +alias mrsk='docker run --rm -it -v $HOME/.ssh:/root/.ssh -v /var/run/docker.sock:/var/run/docker.sock -v ${PWD}/:/workdir mrsk' +``` + +Then, inside your app directory, run `mrsk init` (or `mrsk init --bundle` within Rails apps where you want a bin/mrsk binstub). Now edit the new file `config/deploy.yml`. It could look as simple as this: ```yaml service: hey @@ -34,7 +46,7 @@ mrsk deploy This will: 1. Connect to the servers over SSH (using root by default, authenticated by your ssh key) -2. Install Docker on any server that might be missing it (using apt-get) +2. Install Docker on any server that might be missing it (using apt-get): root access is needed via ssh for this. 3. Log into the registry both locally and remotely 4. Build the image using the standard Dockerfile in the root of the application. 5. Push the image to the registry. From 11af999800ea127ea8f60f44ba13cd8b79211567 Mon Sep 17 00:00:00 2001 From: Robert Starmer Date: Tue, 14 Mar 2023 16:27:19 -0700 Subject: [PATCH 2/9] Remove unneeded Dockerfile.dind, update Readme --- Dockerfile.dind | 33 --------------------------------- README.md | 2 +- 2 files changed, 1 insertion(+), 34 deletions(-) delete mode 100644 Dockerfile.dind diff --git a/Dockerfile.dind b/Dockerfile.dind deleted file mode 100644 index d0c9d90a..00000000 --- a/Dockerfile.dind +++ /dev/null @@ -1,33 +0,0 @@ -# Use the official Docker docker-in-docker Alpine image as the base image -FROM docker:dind - -# Set the working directory to /mrsk -WORKDIR /mrsk - -# Copy the Gemfile, Gemfile.lock into the container -COPY Gemfile Gemfile.lock mrsk.gemspec ./ - -# Required in mrsk.gemspec -COPY lib/mrsk/version.rb /mrsk/lib/mrsk/version.rb - -# Install system dependencies including ruby-dev -RUN apk add --no-cache --update ruby-dev build-base git docker openrc \ - && rc-update add docker boot \ - && gem install bundler --version=2.4.3 \ - && bundle install - -# Copy the rest of our application code into the container. -# We do this after bundle install, to avoid having to run bundle -# everytime we do small fixes in the source code. -COPY . . - -# Install the gem locally from the project folder -RUN gem build mrsk.gemspec && \ - gem install ./mrsk-*.gem --no-document - -# Set the working directory to /workdir -WORKDIR /workdir - -# Set the entrypoint to run the installed binary in /workdir -# Example: docker run -it -v "$PWD:/workdir" mrsk init -ENTRYPOINT ["mrsk"] diff --git a/README.md b/README.md index e4ef3060..b842ee69 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Join us on Discord: https://discord.gg/DQETs3Pm Install MRSK globally with `gem install mrsk` or build a dockerized version: ```sh -docker build . -t mrsk:dind -t mrsk:latest -f Dockerfile.dind +docker build . -t mrsk:latest -f Dockerfile ``` If you build the docker version, add the following alias to your .${SHELL}rc file and re-source the rc file to allow ssh and docker to work within the container: From fb3353084f903c16d9b367d55d12adbd4f917a2d Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Wed, 22 Mar 2023 15:44:27 +0000 Subject: [PATCH 3/9] Default to deploying the config version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If we don't supply a version when deploying we'll use the result of docker image ls to decide which image to boot. But that doesn't necessarily correspond to the one we have just built. E.g. if you do something like: ``` mrsk deploy # deploys git sha AAAAAAAAAAAAAA git commit --amend # update the commit message mrsk deploy # deploys git sha BBBBBBBBBBBBBB ``` In this case running `docker image ls` will give you the same image twice (because the contents are identical) with tags for both SHAs but the image we have just built will not be returned first. Maybe the order is random, but it always seems to come second as far as I have seen. i.e you'll get something like: ``` REPOSITORY TAG IMAGE ID CREATED SIZE foo/bar AAAAAAAAAAAAAA 6272349a9619 31 minutes ago 791MB foo/bar BBBBBBBBBBBBBB 6272349a9619 31 minutes ago 791MB ``` Since we already know what version we want to deploy from the config, let's just pass that through. --- lib/mrsk/cli/main.rb | 8 ++++++-- test/cli/main_test.rb | 8 ++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/mrsk/cli/main.rb b/lib/mrsk/cli/main.rb index 328af480..e98d7604 100644 --- a/lib/mrsk/cli/main.rb +++ b/lib/mrsk/cli/main.rb @@ -11,7 +11,7 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base desc "deploy", "Deploy app to servers" option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip image build and push" def deploy - invoke_options = options.without(:skip_push) + invoke_options = deploy_options runtime = print_runtime do say "Ensure curl and Docker are installed...", :magenta @@ -46,7 +46,7 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base desc "redeploy", "Deploy app to servers without bootstrapping servers, starting Traefik, pruning, and registry login" option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip image build and push" def redeploy - invoke_options = options.without(:skip_push) + invoke_options = deploy_options runtime = print_runtime do if options[:skip_push] @@ -203,4 +203,8 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base on(host) { container_names = capture_with_info(*MRSK.app.list_container_names).split("\n") } Array(container_names).include?(container_name) end + + def deploy_options + { "version" => MRSK.config.version }.merge(options.without("skip_push")) + end end diff --git a/test/cli/main_test.rb b/test/cli/main_test.rb index f72741dd..76638725 100644 --- a/test/cli/main_test.rb +++ b/test/cli/main_test.rb @@ -10,7 +10,7 @@ class CliMainTest < CliTestCase end test "deploy" do - invoke_options = { "config_file" => "test/fixtures/deploy_with_accessories.yml", "skip_broadcast" => false, "skip_push" => false } + invoke_options = { "config_file" => "test/fixtures/deploy_with_accessories.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) @@ -31,7 +31,7 @@ class CliMainTest < CliTestCase end test "deploy with skip_push" do - invoke_options = { "config_file" => "test/fixtures/deploy_with_accessories.yml", "skip_broadcast" => false, "skip_push" => true } + invoke_options = { "config_file" => "test/fixtures/deploy_with_accessories.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) @@ -52,7 +52,7 @@ class CliMainTest < CliTestCase end test "redeploy" do - invoke_options = { "config_file" => "test/fixtures/deploy_with_accessories.yml", "skip_broadcast" => false, "skip_push" => false} + invoke_options = { "config_file" => "test/fixtures/deploy_with_accessories.yml", "skip_broadcast" => false, "version" => "999" } Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:build:deliver", [], invoke_options) Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:healthcheck:perform", [], invoke_options) @@ -65,7 +65,7 @@ class CliMainTest < CliTestCase end test "redeploy with skip_push" do - invoke_options = { "config_file" => "test/fixtures/deploy_with_accessories.yml", "skip_broadcast" => false, "skip_push" => true } + invoke_options = { "config_file" => "test/fixtures/deploy_with_accessories.yml", "skip_broadcast" => false, "version" => "999" } Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:build:pull", [], invoke_options) Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:healthcheck:perform", [], invoke_options) From 1ed4a37da2b73125c37044d04e5a9a5ba0efd62a Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Thu, 23 Mar 2023 14:35:12 +0000 Subject: [PATCH 4/9] Pull latest image tag, so we can identity it `docker image ls` doesn't tell us what the latest deployed image is (e.g if we've rolled back). Pull the latest image tag through to the server so we can use it instead. --- lib/mrsk/cli/app.rb | 21 +++++++-------------- lib/mrsk/commands/app.rb | 13 ------------- lib/mrsk/commands/builder/base.rb | 1 + test/cli/app_test.rb | 5 ++--- test/cli/build_test.rb | 2 +- test/commands/app_test.rb | 6 ------ 6 files changed, 11 insertions(+), 37 deletions(-) diff --git a/lib/mrsk/cli/app.rb b/lib/mrsk/cli/app.rb index 796cd367..9b3ea596 100644 --- a/lib/mrsk/cli/app.rb +++ b/lib/mrsk/cli/app.rb @@ -2,7 +2,7 @@ class Mrsk::Cli::App < Mrsk::Cli::Base desc "boot", "Boot app on servers (or reboot app if already running)" def boot say "Get most recent version available as an image...", :magenta unless options[:version] - using_version(options[:version] || most_recent_version_available) do |version| + using_version(version_or_latest) do |version| say "Start container with version #{version} using a #{MRSK.config.readiness_delay}s readiness delay (or reboot if already running)...", :magenta cli = self @@ -70,7 +70,7 @@ class Mrsk::Cli::App < Mrsk::Cli::Base when options[:interactive] say "Get most recent version available as an image...", :magenta unless options[:version] - using_version(options[:version] || most_recent_version_available) do |version| + using_version(version_or_latest) do |version| say "Launching interactive command with version #{version} via SSH from new container on #{MRSK.primary_host}...", :magenta run_locally { exec MRSK.app.execute_in_new_container_over_ssh(cmd, host: MRSK.primary_host) } end @@ -88,7 +88,7 @@ class Mrsk::Cli::App < Mrsk::Cli::Base else say "Get most recent version available as an image...", :magenta unless options[:version] - using_version(options[:version] || most_recent_version_available) do |version| + using_version(version_or_latest) do |version| say "Launching command with version #{version} from new container...", :magenta on(MRSK.hosts) do |host| execute *MRSK.auditor.record("Executed cmd '#{cmd}' on app version #{version}"), verbosity: :debug @@ -189,20 +189,13 @@ class Mrsk::Cli::App < Mrsk::Cli::Base end end - def most_recent_version_available(host: MRSK.primary_host) - version = nil - on(host) { version = capture_with_info(*MRSK.app.most_recent_version_from_available_images).strip } - - if version == "" - raise "Most recent image available was not tagged with a version (returned )" - else - version.presence - end - end - def current_running_version(host: MRSK.primary_host) version = nil on(host) { version = capture_with_info(*MRSK.app.current_running_version).strip } version.presence end + + def version_or_latest + options[:version] || "latest" + end end diff --git a/lib/mrsk/commands/app.rb b/lib/mrsk/commands/app.rb index e650217a..ad0a21d7 100644 --- a/lib/mrsk/commands/app.rb +++ b/lib/mrsk/commands/app.rb @@ -87,19 +87,6 @@ class Mrsk::Commands::App < Mrsk::Commands::Base "tail -n 1" end - def most_recent_version_from_available_images - pipe \ - docker(:image, :ls, "--format", '"{{.Tag}}"', config.repository), - "head -n 1" - end - - def all_versions_from_available_containers - pipe \ - docker(:image, :ls, "--format", '"{{.Tag}}"', config.repository), - "head -n 1" - end - - def list_containers docker :container, :ls, "--all", *filter_args end diff --git a/lib/mrsk/commands/builder/base.rb b/lib/mrsk/commands/builder/base.rb index 91276260..448ecca4 100644 --- a/lib/mrsk/commands/builder/base.rb +++ b/lib/mrsk/commands/builder/base.rb @@ -7,6 +7,7 @@ class Mrsk::Commands::Builder::Base < Mrsk::Commands::Base def pull docker :pull, config.absolute_image + docker :pull, config.latest_image end def build_options diff --git a/test/cli/app_test.rb b/test/cli/app_test.rb index 36fc0691..b2b6786a 100644 --- a/test/cli/app_test.rb +++ b/test/cli/app_test.rb @@ -28,9 +28,8 @@ class CliAppTest < CliTestCase .returns([ :docker, :run ]) run_command("boot").tap do |output| - assert_match "Rebooting container with same version 999 already deployed", output # Can't start what's already running - assert_match "docker container ls --all --filter name=app-999 --quiet | xargs docker container rm", output # Stop old running - assert_match "docker container ls --all --filter name=app-999 --quiet | xargs docker container rm", output # Remove old container + assert_match "Rebooting container with same version latest already deployed", output # Can't start what's already running + assert_match "docker container ls --all --filter name=app-latest --quiet | xargs docker container rm", output # Remove old container assert_match "docker run", output # Start new container end ensure diff --git a/test/cli/build_test.rb b/test/cli/build_test.rb index 44802549..d721373b 100644 --- a/test/cli/build_test.rb +++ b/test/cli/build_test.rb @@ -29,7 +29,7 @@ class CliBuildTest < CliTestCase test "pull" do run_command("pull").tap do |output| assert_match /docker image rm --force dhh\/app:999/, output - assert_match /docker pull dhh\/app:999/, output + assert_match /docker pull dhh\/app:latest/, output end end diff --git a/test/commands/app_test.rb b/test/commands/app_test.rb index 34596318..2651abdc 100644 --- a/test/commands/app_test.rb +++ b/test/commands/app_test.rb @@ -189,12 +189,6 @@ class CommandsAppTest < ActiveSupport::TestCase new_command.current_running_version.join(" ") end - test "most_recent_version_from_available_images" do - assert_equal \ - "docker image ls --format \"{{.Tag}}\" dhh/app | head -n 1", - new_command.most_recent_version_from_available_images.join(" ") - end - test "list_containers" do assert_equal \ "docker container ls --all --filter label=service=app", From 1887a6518e02c0f9ef83250f4717b01e82d37536 Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Wed, 22 Mar 2023 09:45:50 -0700 Subject: [PATCH 5/9] Commander needn't accumulate configuration Commander had version/destination solely to incrementally accumulate CLI options. Simpler to configure in one shot. Clarifies responsibility and lets us introduce things like `abbreviated_version` in one spot - Configuration. --- lib/mrsk/cli/app.rb | 2 +- lib/mrsk/cli/base.rb | 17 +++++++------- lib/mrsk/cli/build.rb | 2 +- lib/mrsk/cli/main.rb | 2 +- lib/mrsk/commander.rb | 36 +++++++++++++---------------- lib/mrsk/configuration.rb | 26 +++++++++++++++++---- test/cli/main_test.rb | 32 ++++++++++++++++++++++--- test/commander_test.rb | 14 +++-------- test/configuration_test.rb | 28 ++++++++++++++++------ test/fixtures/deploy_with_roles.yml | 5 ++-- 10 files changed, 105 insertions(+), 59 deletions(-) diff --git a/lib/mrsk/cli/app.rb b/lib/mrsk/cli/app.rb index 796cd367..0898a509 100644 --- a/lib/mrsk/cli/app.rb +++ b/lib/mrsk/cli/app.rb @@ -37,7 +37,7 @@ class Mrsk::Cli::App < Mrsk::Cli::Base desc "start", "Start existing app container on servers" def start on(MRSK.hosts) do - execute *MRSK.auditor.record("Started app version #{MRSK.version}"), verbosity: :debug + execute *MRSK.auditor.record("Started app version #{MRSK.config.version}"), verbosity: :debug execute *MRSK.app.start, raise_on_non_zero_exit: false end end diff --git a/lib/mrsk/cli/base.rb b/lib/mrsk/cli/base.rb index c7a87964..4e0f2104 100644 --- a/lib/mrsk/cli/base.rb +++ b/lib/mrsk/cli/base.rb @@ -39,14 +39,6 @@ module Mrsk::Cli def initialize_commander(options) MRSK.tap do |commander| - commander.config_file = Pathname.new(File.expand_path(options[:config_file])) - commander.destination = options[:destination] - commander.version = options[:version] - - commander.specific_hosts = options[:hosts]&.split(",") - commander.specific_roles = options[:roles]&.split(",") - commander.specific_primary! if options[:primary] - if options[:verbose] ENV["VERBOSE"] = "1" # For backtraces via cli/start commander.verbosity = :debug @@ -55,6 +47,15 @@ module Mrsk::Cli if options[:quiet] commander.verbosity = :error end + + commander.configure \ + config_file: Pathname.new(File.expand_path(options[:config_file])), + destination: options[:destination], + version: options[:version] + + commander.specific_hosts = options[:hosts]&.split(",") + commander.specific_roles = options[:roles]&.split(",") + commander.specific_primary! if options[:primary] end end diff --git a/lib/mrsk/cli/build.rb b/lib/mrsk/cli/build.rb index 3e8912d7..c9845e4f 100644 --- a/lib/mrsk/cli/build.rb +++ b/lib/mrsk/cli/build.rb @@ -29,7 +29,7 @@ class Mrsk::Cli::Build < Mrsk::Cli::Base desc "pull", "Pull app image from registry onto servers" def pull on(MRSK.hosts) do - execute *MRSK.auditor.record("Pulled image with version #{MRSK.version}"), verbosity: :debug + execute *MRSK.auditor.record("Pulled image with version #{MRSK.config.version}"), verbosity: :debug execute *MRSK.builder.clean, raise_on_non_zero_exit: false execute *MRSK.builder.pull end diff --git a/lib/mrsk/cli/main.rb b/lib/mrsk/cli/main.rb index 1524b49e..eb441d30 100644 --- a/lib/mrsk/cli/main.rb +++ b/lib/mrsk/cli/main.rb @@ -68,7 +68,7 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base desc "rollback [VERSION]", "Rollback app to VERSION" def rollback(version) - MRSK.version = version + MRSK.config.version = version if container_name_available?(MRSK.config.service_with_version) say "Start version #{version}, then wait #{MRSK.config.readiness_delay}s for app to boot before stopping the old version...", :magenta diff --git a/lib/mrsk/commander.rb b/lib/mrsk/commander.rb index be2fd2e3..5aa24fc7 100644 --- a/lib/mrsk/commander.rb +++ b/lib/mrsk/commander.rb @@ -1,19 +1,26 @@ require "active_support/core_ext/enumerable" +require "active_support/core_ext/module/delegation" class Mrsk::Commander - attr_accessor :config_file, :destination, :verbosity, :version + attr_accessor :verbosity - def initialize(config_file: nil, destination: nil, verbosity: :info) - @config_file, @destination, @verbosity = config_file, destination, verbosity + def initialize + self.verbosity = :info end + def config - @config ||= \ - Mrsk::Configuration - .create_from(config_file, destination: destination, version: cascading_version) - .tap { |config| configure_sshkit_with(config) } + @config ||= Mrsk::Configuration.create_from(**@config_kwargs).tap do |config| + @config_kwargs = nil + configure_sshkit_with(config) + end end + def configure(**kwargs) + @config, @config_kwargs = nil, kwargs + end + + attr_accessor :specific_hosts def specific_primary! @@ -90,26 +97,15 @@ class Mrsk::Commander SSHKit.config.output_verbosity = old_level end + # Test-induced damage! def reset - @config = @config_file = @destination = @version = nil + @config = nil @app = @builder = @traefik = @registry = @prune = @auditor = nil @verbosity = :info end private - def cascading_version - version.presence || ENV["VERSION"] || current_commit_hash - end - - def current_commit_hash - if system("git rev-parse") - `git rev-parse HEAD`.strip - else - raise "Can't use commit hash as version, no git repository found in #{Dir.pwd}" - end - end - # Lazy setup of SSHKit def configure_sshkit_with(config) SSHKit::Backend::Netssh.configure { |ssh| ssh.ssh_options = config.ssh_options } diff --git a/lib/mrsk/configuration.rb b/lib/mrsk/configuration.rb index 1babea37..f95de700 100644 --- a/lib/mrsk/configuration.rb +++ b/lib/mrsk/configuration.rb @@ -9,13 +9,12 @@ class Mrsk::Configuration delegate :service, :image, :servers, :env, :labels, :registry, :builder, to: :raw_config, allow_nil: true delegate :argumentize, :argumentize_env_with_secrets, to: Mrsk::Utils - attr_accessor :version attr_accessor :destination attr_accessor :raw_config class << self - def create_from(base_config_file, destination: nil, version: "missing") - raw_config = load_config_files(base_config_file, *destination_config_file(base_config_file, destination)) + def create_from(config_file:, destination: nil, version: nil) + raw_config = load_config_files(config_file, *destination_config_file(config_file, destination)) new raw_config, destination: destination, version: version end @@ -38,14 +37,22 @@ class Mrsk::Configuration end end - def initialize(raw_config, destination: nil, version: "missing", validate: true) + def initialize(raw_config, destination: nil, version: nil, validate: true) @raw_config = ActiveSupport::InheritableOptions.new(raw_config) @destination = destination - @version = version + @declared_version = version valid? if validate end + def version=(version) + @declared_version = version + end + + def version + @declared_version.presence || ENV["VERSION"] || current_commit_hash + end + def abbreviated_version Mrsk::Utils.abbreviate_version(version) end @@ -203,4 +210,13 @@ class Mrsk::Configuration def role_names raw_config.servers.is_a?(Array) ? [ "web" ] : raw_config.servers.keys.sort end + + def current_commit_hash + @current_commit_hash ||= + if system("git rev-parse") + `git rev-parse HEAD`.strip + else + raise "Can't use commit hash as version, no git repository found in #{Dir.pwd}" + end + end end diff --git a/test/cli/main_test.rb b/test/cli/main_test.rb index 086067ff..151fa7b8 100644 --- a/test/cli/main_test.rb +++ b/test/cli/main_test.rb @@ -126,7 +126,7 @@ class CliMainTest < CliTestCase end test "config" do - run_command("config").tap do |output| + run_command("config", config_file: "deploy_with_accessories").tap do |output| config = YAML.load(output) assert_equal ["web"], config[:roles] @@ -138,6 +138,32 @@ class CliMainTest < CliTestCase end end + test "config with roles" do + run_command("config", config_file: "deploy_with_roles").tap do |output| + config = YAML.load(output) + + assert_equal ["web", "workers"], config[:roles] + assert_equal ["1.1.1.1", "1.1.1.2", "1.1.1.3", "1.1.1.4"], config[:hosts] + assert_equal "999", config[:version] + assert_equal "registry.digitalocean.com/dhh/app", config[:repository] + assert_equal "registry.digitalocean.com/dhh/app:999", config[:absolute_image] + assert_equal "app-999", config[:service_with_version] + end + end + + test "config with destination" do + run_command("config", "-d", "world", config_file: "deploy_for_dest").tap do |output| + config = YAML.load(output) + + assert_equal ["web"], config[:roles] + assert_equal ["1.1.1.1", "1.1.1.2"], config[:hosts] + assert_equal "999", config[:version] + assert_equal "registry.digitalocean.com/dhh/app", config[:repository] + assert_equal "registry.digitalocean.com/dhh/app:999", config[:absolute_image] + assert_equal "app-999", config[:service_with_version] + end + end + test "init" do Pathname.any_instance.expects(:exist?).returns(false).twice FileUtils.stubs(:mkdir_p) @@ -227,7 +253,7 @@ class CliMainTest < CliTestCase end private - def run_command(*command) - stdouted { Mrsk::Cli::Main.start([*command, "-c", "test/fixtures/deploy_with_accessories.yml"]) } + def run_command(*command, config_file: "deploy_with_accessories") + stdouted { Mrsk::Cli::Main.start([*command, "-c", "test/fixtures/#{config_file}.yml"]) } end end diff --git a/test/commander_test.rb b/test/commander_test.rb index b4ce24dc..278d9896 100644 --- a/test/commander_test.rb +++ b/test/commander_test.rb @@ -2,23 +2,15 @@ require "test_helper" class CommanderTest < ActiveSupport::TestCase setup do - @mrsk = Mrsk::Commander.new config_file: Pathname.new(File.expand_path("fixtures/deploy_with_roles.yml", __dir__)) + @mrsk = Mrsk::Commander.new.tap do |mrsk| + mrsk.configure config_file: Pathname.new(File.expand_path("fixtures/deploy_with_roles.yml", __dir__)) + end end test "lazy configuration" do assert_equal Mrsk::Configuration, @mrsk.config.class end - test "commit hash as version" do - assert_equal `git rev-parse HEAD`.strip, @mrsk.config.version - end - - test "commit hash as version but not in git" do - @mrsk.expects(:system).with("git rev-parse").returns(nil) - error = assert_raises(RuntimeError) { @mrsk.config } - assert_match /no git repository found/, error.message - end - test "overwriting hosts" do assert_equal [ "1.1.1.1", "1.1.1.2", "1.1.1.3", "1.1.1.4" ], @mrsk.hosts diff --git a/test/configuration_test.rb b/test/configuration_test.rb index 5cd7856b..30c4b049 100644 --- a/test/configuration_test.rb +++ b/test/configuration_test.rb @@ -3,6 +3,7 @@ require "test_helper" class ConfigurationTest < ActiveSupport::TestCase setup do ENV["RAILS_MASTER_KEY"] = "456" + ENV["VERSION"] = "missing" @deploy = { service: "app", image: "dhh/app", @@ -21,7 +22,8 @@ class ConfigurationTest < ActiveSupport::TestCase end teardown do - ENV["RAILS_MASTER_KEY"] = nil + ENV.delete("RAILS_MASTER_KEY") + ENV.delete("VERSION") end test "ensure valid keys" do @@ -66,8 +68,20 @@ class ConfigurationTest < ActiveSupport::TestCase end test "version" do - assert_equal "missing", @config.version - assert_equal "123", Mrsk::Configuration.new(@deploy, version: "123").version + ENV.delete("VERSION") + + @config.expects(:system).with("git rev-parse").returns(nil) + error = assert_raises(RuntimeError) { @config.version} + assert_match /no git repository found/, error.message + + @config.expects(:current_commit_hash).returns("git-version") + assert_equal "git-version", @config.version + + ENV["VERSION"] = "env-version" + assert_equal "env-version", @config.version + + @config.version = "arg-version" + assert_equal "arg-version", @config.version end test "repository" do @@ -158,17 +172,17 @@ class ConfigurationTest < ActiveSupport::TestCase end test "erb evaluation of yml config" do - config = Mrsk::Configuration.create_from Pathname.new(File.expand_path("fixtures/deploy.erb.yml", __dir__)) + config = Mrsk::Configuration.create_from config_file: Pathname.new(File.expand_path("fixtures/deploy.erb.yml", __dir__)) assert_equal "my-user", config.registry["username"] end test "destination yml config merge" do dest_config_file = Pathname.new(File.expand_path("fixtures/deploy_for_dest.yml", __dir__)) - config = Mrsk::Configuration.create_from dest_config_file, destination: "world" + config = Mrsk::Configuration.create_from config_file: dest_config_file, destination: "world" assert_equal "1.1.1.1", config.all_hosts.first - config = Mrsk::Configuration.create_from dest_config_file, destination: "mars" + config = Mrsk::Configuration.create_from config_file: dest_config_file, destination: "mars" assert_equal "1.1.1.3", config.all_hosts.first end @@ -176,7 +190,7 @@ class ConfigurationTest < ActiveSupport::TestCase dest_config_file = Pathname.new(File.expand_path("fixtures/deploy_for_dest.yml", __dir__)) assert_raises(RuntimeError) do - config = Mrsk::Configuration.create_from dest_config_file, destination: "missing" + config = Mrsk::Configuration.create_from config_file: dest_config_file, destination: "missing" end end diff --git a/test/fixtures/deploy_with_roles.yml b/test/fixtures/deploy_with_roles.yml index 345c6301..0e405241 100644 --- a/test/fixtures/deploy_with_roles.yml +++ b/test/fixtures/deploy_with_roles.yml @@ -5,8 +5,9 @@ servers: - 1.1.1.1 - 1.1.1.2 workers: - - 1.1.1.3 - - 1.1.1.4 + hosts: + - 1.1.1.3 + - 1.1.1.4 env: REDIS_URL: redis://x/y registry: From 035e4afff712abfe66acb01e71105eb61c728f3b Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Wed, 22 Mar 2023 18:56:57 -0700 Subject: [PATCH 6/9] Validate that all roles have hosts --- lib/mrsk/configuration.rb | 8 +++++- lib/mrsk/configuration/role.rb | 6 +++- test/configuration_test.rb | 52 +++++++++++++++++++++++++++++----- 3 files changed, 57 insertions(+), 9 deletions(-) diff --git a/lib/mrsk/configuration.rb b/lib/mrsk/configuration.rb index f95de700..074c7cf3 100644 --- a/lib/mrsk/configuration.rb +++ b/lib/mrsk/configuration.rb @@ -80,7 +80,7 @@ class Mrsk::Configuration end def primary_web_host - role(:web).hosts.first + role(:web).primary_host end def traefik_hosts @@ -196,6 +196,12 @@ class Mrsk::Configuration raise ArgumentError, "You must specify a password for the registry in config/deploy.yml (or set the ENV variable if that's used)" end + roles.each do |role| + if role.hosts.empty? + raise ArgumentError, "No servers specified for the #{role.name} role" + end + end + true end diff --git a/lib/mrsk/configuration/role.rb b/lib/mrsk/configuration/role.rb index f3e20e9a..bfc42fbe 100644 --- a/lib/mrsk/configuration/role.rb +++ b/lib/mrsk/configuration/role.rb @@ -7,6 +7,10 @@ class Mrsk::Configuration::Role @name, @config = name.inquiry, config end + def primary_host + hosts.first + end + def hosts @hosts ||= extract_hosts_from_config end @@ -55,7 +59,7 @@ class Mrsk::Configuration::Role config.servers else servers = config.servers[name] - servers.is_a?(Array) ? servers : servers["hosts"] + servers.is_a?(Array) ? servers : Array(servers["hosts"]) end end diff --git a/test/configuration_test.rb b/test/configuration_test.rb index 30c4b049..ee381449 100644 --- a/test/configuration_test.rb +++ b/test/configuration_test.rb @@ -26,14 +26,19 @@ class ConfigurationTest < ActiveSupport::TestCase ENV.delete("VERSION") end - test "ensure valid keys" do - assert_raise(ArgumentError) do - Mrsk::Configuration.new(@deploy.tap { _1.delete(:service) }) - Mrsk::Configuration.new(@deploy.tap { _1.delete(:image) }) - Mrsk::Configuration.new(@deploy.tap { _1.delete(:registry) }) + %i[ service image registry ].each do |key| + test "#{key} config required" do + assert_raise(ArgumentError) do + Mrsk::Configuration.new @deploy.tap { _1.delete key } + end + end + end - Mrsk::Configuration.new(@deploy.tap { _1[:registry].delete("username") }) - Mrsk::Configuration.new(@deploy.tap { _1[:registry].delete("password") }) + %w[ username password ].each do |key| + test "registry #{key} required" do + assert_raise(ArgumentError) do + Mrsk::Configuration.new @deploy.tap { _1[:registry].delete key } + end end end @@ -148,6 +153,39 @@ class ConfigurationTest < ActiveSupport::TestCase test "valid config" do assert @config.valid? + assert @config_with_roles.valid? + end + + test "hosts required for all roles" do + # Empty server list for implied web role + assert_raises(ArgumentError) do + Mrsk::Configuration.new @deploy.merge(servers: []) + end + + # Empty server list + assert_raises(ArgumentError) do + Mrsk::Configuration.new @deploy.merge(servers: { "web" => [] }) + end + + # Missing hosts key + assert_raises(ArgumentError) do + Mrsk::Configuration.new @deploy.merge(servers: { "web" => {} }) + end + + # Empty hosts list + assert_raises(ArgumentError) do + Mrsk::Configuration.new @deploy.merge(servers: { "web" => { "hosts" => [] } }) + end + + # Nil hosts + assert_raises(ArgumentError) do + Mrsk::Configuration.new @deploy.merge(servers: { "web" => { "hosts" => nil } }) + end + + # One role with hosts, one without + assert_raises(ArgumentError) do + Mrsk::Configuration.new @deploy.merge(servers: { "web" => %w[ web ], "workers" => { "hosts" => %w[ ] } }) + end end test "ssh options" do From cf38feb1d64a390b75dda50ba635bded71a4279c Mon Sep 17 00:00:00 2001 From: Robert Starmer Date: Thu, 23 Mar 2023 12:35:15 -0700 Subject: [PATCH 7/9] Update readme to point to ghcr.io/mrsked/mrsk --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b842ee69..11405674 100644 --- a/README.md +++ b/README.md @@ -8,16 +8,16 @@ Join us on Discord: https://discord.gg/DQETs3Pm ## Installation -Install MRSK globally with `gem install mrsk` or build a dockerized version: +If you have a ruby environment available, you can install MRSK globally with ```sh -docker build . -t mrsk:latest -f Dockerfile +gem install mrsk ``` -If you build the docker version, add the following alias to your .${SHELL}rc file and re-source the rc file to allow ssh and docker to work within the container: +or run a dockerized version via an alias (add this to your ${SHELL}rc to simplify re-use): ```sh -alias mrsk='docker run --rm -it -v $HOME/.ssh:/root/.ssh -v /var/run/docker.sock:/var/run/docker.sock -v ${PWD}/:/workdir mrsk' +alias mrsk='docker run --rm -it -v $HOME/.ssh:/root/.ssh -v /var/run/docker.sock:/var/run/docker.sock -v ${PWD}/:/workdir ghcr.io/mrsked/mrsk' ``` Then, inside your app directory, run `mrsk init` (or `mrsk init --bundle` within Rails apps where you want a bin/mrsk binstub). Now edit the new file `config/deploy.yml`. It could look as simple as this: From e859a581abd14212cd7fdeda2d72bad7a60d905d Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Wed, 22 Mar 2023 23:57:43 -0700 Subject: [PATCH 8/9] Remove accessory images using tags rather than labels --- lib/mrsk/commands/accessory.rb | 2 +- test/cli/accessory_test.rb | 2 +- test/cli/main_test.rb | 4 ++-- test/commands/accessory_test.rb | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/mrsk/commands/accessory.rb b/lib/mrsk/commands/accessory.rb index 76d4fa96..2f745267 100644 --- a/lib/mrsk/commands/accessory.rb +++ b/lib/mrsk/commands/accessory.rb @@ -100,7 +100,7 @@ class Mrsk::Commands::Accessory < Mrsk::Commands::Base end def remove_image - docker :image, :prune, "--all", "--force", *service_filter + docker :image, :rm, "--force", image end private diff --git a/test/cli/accessory_test.rb b/test/cli/accessory_test.rb index 8d2fe435..b9833b89 100644 --- a/test/cli/accessory_test.rb +++ b/test/cli/accessory_test.rb @@ -126,7 +126,7 @@ class CliAccessoryTest < CliTestCase end test "remove_image" do - assert_match "docker image prune --all --force --filter label=service=app-mysql", run_command("remove_image", "mysql") + assert_match "docker image rm --force mysql", run_command("remove_image", "mysql") end test "remove_service_directory" do diff --git a/test/cli/main_test.rb b/test/cli/main_test.rb index 151fa7b8..799c0cb5 100644 --- a/test/cli/main_test.rb +++ b/test/cli/main_test.rb @@ -235,12 +235,12 @@ class CliMainTest < CliTestCase assert_match /docker container stop app-mysql/, output assert_match /docker container prune --force --filter label=service=app-mysql/, output - assert_match /docker image prune --all --force --filter label=service=app-mysql/, output + assert_match /docker image rm --force mysql/, output assert_match /rm -rf app-mysql/, output assert_match /docker container stop app-redis/, output assert_match /docker container prune --force --filter label=service=app-redis/, output - assert_match /docker image prune --all --force --filter label=service=app-redis/, output + assert_match /docker image rm --force redis/, output assert_match /rm -rf app-redis/, output assert_match /docker logout/, output diff --git a/test/commands/accessory_test.rb b/test/commands/accessory_test.rb index 268cc0b3..0e343d8c 100644 --- a/test/commands/accessory_test.rb +++ b/test/commands/accessory_test.rb @@ -137,7 +137,7 @@ class CommandsAccessoryTest < ActiveSupport::TestCase test "remove image" do assert_equal \ - "docker image prune --all --force --filter label=service=app-mysql", + "docker image rm --force private.registry/mysql:8.0", @mysql.remove_image.join(" ") end end From 8ebcafd3d8517ad3822746da164b62e7a54a5f63 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 24 Mar 2023 13:20:52 +0100 Subject: [PATCH 9/9] Wording --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1940412e..1ffd09f1 100644 --- a/README.md +++ b/README.md @@ -8,13 +8,13 @@ Join us on Discord: https://discord.gg/YgHVT7GCXS ## Installation -If you have a ruby environment available, you can install MRSK globally with +If you have a Ruby environment available, you can install MRSK globally with: ```sh gem install mrsk ``` -or run a dockerized version via an alias (add this to your ${SHELL}rc to simplify re-use): +...otherwise, you can run a dockerized version via an alias (add this to your ${SHELL}rc to simplify re-use): ```sh alias mrsk='docker run --rm -it -v $HOME/.ssh:/root/.ssh -v /var/run/docker.sock:/var/run/docker.sock -v ${PWD}/:/workdir ghcr.io/mrsked/mrsk'