From 57e9231c5ee9e6acab58e317ed78e877117e5164 Mon Sep 17 00:00:00 2001 From: Dave Gudge Date: Fri, 10 Mar 2023 10:02:33 +0000 Subject: [PATCH 01/23] fix: Github Workflow: Docker Publish The workflow was failing with: ``` The workflow is not valid. .github/workflows/docker-publish.yml (Line: 22, Col: 14): Unexpected symbol: '|'. Located at position 12 within expression: github.ref | replace('refs/tags/', '') ``` The `set-output` command is deprecated, so the issue has been fixed by utilising the `github.ref_name` context to retrieve the version tag that triggered the workflow. > `github.ref_name`: The short ref name of the branch or tag that triggered the workflow run. This value matches the branch or tag name shown on GitHub. For example, `feature-branch-1`. https://docs.github.com/en/actions/learn-github-actions/contexts --- .github/workflows/docker-publish.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index da7e99a5..134ecac0 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -16,10 +16,6 @@ jobs: - name: Checkout uses: actions/checkout@v3 - - - name: Extract Version Number - id: extract_version - run: echo "::set-output name=version::${{ github.ref | replace('refs/tags/', '') }}" - name: Set up QEMU uses: docker/setup-qemu-action@v2 @@ -42,4 +38,4 @@ jobs: push: true tags: | ghcr.io/mrsked/mrsk:latest - ghcr.io/mrsked/mrsk:${{ steps.extract_version.outputs.version }} + ghcr.io/mrsked/mrsk:${{ github.ref_name }} From 4bf77ccd1b1e7b36b9b928c5332d16b3c7ab92cc Mon Sep 17 00:00:00 2001 From: Samuel Sieg Date: Fri, 10 Mar 2023 11:26:35 +0100 Subject: [PATCH 02/23] Allow `deploy`/`deliver` without building and pushing the image --- lib/mrsk/cli/build.rb | 3 ++- lib/mrsk/cli/main.rb | 13 +++++++------ test/cli/build_test.rb | 25 +++++++++++++++++++++++-- test/test_helper.rb | 10 ++++++++++ 4 files changed, 42 insertions(+), 9 deletions(-) diff --git a/lib/mrsk/cli/build.rb b/lib/mrsk/cli/build.rb index 3e8912d7..ec39282e 100644 --- a/lib/mrsk/cli/build.rb +++ b/lib/mrsk/cli/build.rb @@ -1,7 +1,8 @@ class Mrsk::Cli::Build < Mrsk::Cli::Base desc "deliver", "Build app and push app image to registry then pull image on servers" + option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip image build and push" def deliver - push + push unless options[:skip_push] pull end diff --git a/lib/mrsk/cli/main.rb b/lib/mrsk/cli/main.rb index 73268f50..163c0327 100644 --- a/lib/mrsk/cli/main.rb +++ b/lib/mrsk/cli/main.rb @@ -9,27 +9,28 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base end desc "deploy", "Deploy app to servers" + option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip image build and push" def deploy runtime = print_runtime do say "Ensure curl and Docker are installed...", :magenta - invoke "mrsk:cli:server:bootstrap" + invoke "mrsk:cli:server:bootstrap", [], options.without(:skip_push) say "Log into image registry...", :magenta - invoke "mrsk:cli:registry:login" + invoke "mrsk:cli:registry:login", [], options.without(:skip_push) say "Build and push app image...", :magenta invoke "mrsk:cli:build:deliver" say "Ensure Traefik is running...", :magenta - invoke "mrsk:cli:traefik:boot" + invoke "mrsk:cli:traefik:boot", [], options.without(:skip_push) say "Ensure app can pass healthcheck...", :magenta - invoke "mrsk:cli:healthcheck:perform" + invoke "mrsk:cli:healthcheck:perform", [], options.without(:skip_push) - invoke "mrsk:cli:app:boot" + invoke "mrsk:cli:app:boot", [], options.without(:skip_push) say "Prune old containers and images...", :magenta - invoke "mrsk:cli:prune:all" + invoke "mrsk:cli:prune:all", [], options.without(:skip_push) end audit_broadcast "Deployed app in #{runtime.to_i} seconds" unless options[:skip_broadcast] diff --git a/test/cli/build_test.rb b/test/cli/build_test.rb index 3d5f4431..2ded0193 100644 --- a/test/cli/build_test.rb +++ b/test/cli/build_test.rb @@ -1,10 +1,31 @@ require_relative "cli_test_case" class CliBuildTest < CliTestCase + test "deliver" do + run_command("deliver").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 image rm --force dhh\/app:999 on 1\.1\.1\.2/, output + assert_match /docker pull dhh\/app:999 on 1\.1\.1\.1/, output + end + end + + test "deliver without push" do + run_command("deliver", "--skip-push").tap do |output| + assert_match /docker image rm --force dhh\/app:999 on 1\.1\.1\.2/, output + assert_match /docker pull dhh\/app:999 on 1\.1\.1\.1/, output + end + end + + test "push" do + 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 + end + 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 image rm --force dhh\/app:999 on 1\.1\.1\.2/, output + assert_match /docker pull dhh\/app:999 on 1\.1\.1\.1/, output end end diff --git a/test/test_helper.rb b/test/test_helper.rb index 4c381df2..6f29420b 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -9,7 +9,17 @@ require "mrsk" ActiveSupport::LogSubscriber.logger = ActiveSupport::Logger.new(STDOUT) if ENV["VERBOSE"] +# Applies to remote commands only SSHKit.config.backend = SSHKit::Backend::Printer +# Ensure local commands use the printer backend too +module SSHKit + module DSL + def run_locally(&block) + SSHKit::Backend::Printer.new(SSHKit::Host.new(:local), &block).run + end + end +end + class ActiveSupport::TestCase end From e5c5e892326a404216d07f203fb5f6dc8b6f770b Mon Sep 17 00:00:00 2001 From: Richard Taylor Date: Fri, 10 Mar 2023 15:55:04 +0000 Subject: [PATCH 03/23] Use custom web options for healthcheck If the web role has custom options, ensure these are used for the healthcheck. --- lib/mrsk/commands/healthcheck.rb | 1 + test/commands/healthcheck_test.rb | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/lib/mrsk/commands/healthcheck.rb b/lib/mrsk/commands/healthcheck.rb index 40ec2af7..722d2a05 100644 --- a/lib/mrsk/commands/healthcheck.rb +++ b/lib/mrsk/commands/healthcheck.rb @@ -11,6 +11,7 @@ class Mrsk::Commands::Healthcheck < Mrsk::Commands::Base "--label", "service=#{container_name}", *web.env_args, *config.volume_args, + *web.option_args, config.absolute_image, web.cmd end diff --git a/test/commands/healthcheck_test.rb b/test/commands/healthcheck_test.rb index 05fbc8c3..acf88fbd 100644 --- a/test/commands/healthcheck_test.rb +++ b/test/commands/healthcheck_test.rb @@ -30,6 +30,13 @@ class CommandsHealthcheckTest < ActiveSupport::TestCase new_command.run.join(" ") end + test "run with custom options" do + @config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "options" => { "mount" => "somewhere" } } } + assert_equal \ + "docker run --detach --name healthcheck-app-123 --publish 3999:3000 --label service=healthcheck-app --mount \"somewhere\" dhh/app:123", + new_command.run.join(" ") + end + test "curl" do assert_equal \ "curl --silent --output /dev/null --write-out '%{http_code}' --max-time 2 http://localhost:3999/up", From 439b6813088ef968f2b6e38885afb912f05f9cc4 Mon Sep 17 00:00:00 2001 From: Jordy Schreuders <3071062+99linesofcode@users.noreply.github.com> Date: Fri, 10 Mar 2023 18:13:32 +0200 Subject: [PATCH 04/23] Neglected to install buildx inside container --- Dockerfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Dockerfile b/Dockerfile index 98dd3c69..b43c60e7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,9 @@ # Use the official Ruby 3.2.0 Alpine image as the base image FROM ruby:3.2.0-alpine +# Install docker/buildx-bin +COPY --from=docker/buildx-bin /buildx /usr/libexec/docker/cli-plugins/docker-buildx + # Set the working directory to /mrsk WORKDIR /mrsk From b2c819fe324f060fdad20ede41c1c7139c79d34f Mon Sep 17 00:00:00 2001 From: Jordy Schreuders <3071062+99linesofcode@users.noreply.github.com> Date: Fri, 10 Mar 2023 19:23:14 +0200 Subject: [PATCH 05/23] Add README section on running MRSK from Docker --- README.md | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 168bed4c..668a11e0 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ env: - RAILS_MASTER_KEY ``` -Then edit your `.env` file to add your registry password as `MRSK_REGISTRY_PASSWORD` (and your `RAILS_MASTER_KEY` for production with a Rails app). +Then edit your `.env` file to add your registry password as `MRSK_REGISTRY_PASSWORD` (and your `RAILS_MASTER_KEY` for production with a Rails app). Now you're ready to deploy to the servers: @@ -67,6 +67,16 @@ Docker Swarm is much simpler than Kubernetes, but it's still built on the same d Ultimately, there are a myriad of ways to deploy web apps, but this is the toolkit we're using at [37signals](https://37signals.com) to bring [HEY](https://www.hey.com) [home from the cloud](https://world.hey.com/dhh/why-we-re-leaving-the-cloud-654b47e0) without losing the advantages of modern containerization tooling. +## Running MRSK from Docker + +MRSK is packaged up in a Docker container similarly to [rails/docked](https://github.com/rails/docked). This will allow you to run MRSK (from your application directory) without having to install any dependencies other than Docker. Add the following alias to your profile configuration to make working with the container more convenient: + +```bash +alias mrsk="docker run -it --rm -v '${PWD}:/workdir' -v '${SSH_AUTH_SOCK}:/ssh-agent' -v /var/run/docker.sock:/var/run/docker.sock -e 'SSH_AUTH_SOCK=/ssh-agent' ghcr.io/mrsked/mrsk:latest" +``` + +Since MRSK uses SSH to establish a remote connection, it will need access to your SSH agent. The above command uses a volume mount to make it available inside the container and configures the SSH agent inside the container to make use of it. + ## Configuration ### Using .env file to load required environment variables @@ -99,9 +109,9 @@ If you need separate env variables for different destinations, you can set them #### Bitwarden as a secret store -If you are using open source secret store like bitwarden, you can create `.env.erb` as a template which looks up the secrets. +If you are using open source secret store like bitwarden, you can create `.env.erb` as a template which looks up the secrets. -You can store `SOME_SECRET` in a secure note in bitwarden vault. +You can store `SOME_SECRET` in a secure note in bitwarden vault. ``` $ bw list items --search SOME_SECRET | jq @@ -140,7 +150,7 @@ SOME_SECRET=<%= `bw get notes 123123123-1232-4224-222f-234234234234 --session #{ <% else raise ArgumentError, "session_token token missing" end %> ``` -Then everyone deploying the app can run `mrsk envify` and mrsk will generate `.env` +Then everyone deploying the app can run `mrsk envify` and mrsk will generate `.env` ### Using another registry than Docker Hub @@ -150,9 +160,9 @@ The default registry is Docker Hub, but you can change it using `registry/server ```yaml registry: server: registry.digitalocean.com - username: + username: - DOCKER_REGISTRY_TOKEN - password: + password: - DOCKER_REGISTRY_TOKEN ``` From 643cb2c520ffd27301afed3260497b281ad541d4 Mon Sep 17 00:00:00 2001 From: Chris Lowder Date: Fri, 10 Mar 2023 19:27:01 +0000 Subject: [PATCH 06/23] Include edge Rails in the build matrix Highlighting an incompatibility with the new implementation of `[ActiveSupport::OrderedOptions#dig]`. [^1]: https://github.com/rails/rails/commit/5c15b586aab85fc4fa1465499fc9a97a446f4d52 --- .github/workflows/ci.yml | 7 +++++-- .gitignore | 3 ++- Gemfile | 4 ---- gemfiles/rails_edge.gemfile | 9 +++++++++ mrsk.gemspec | 4 ++++ 5 files changed, 20 insertions(+), 7 deletions(-) create mode 100644 gemfiles/rails_edge.gemfile diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9fe7363e..1d9d2008 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,12 +8,15 @@ jobs: - "2.7" - "3.1" - "3.2" + gemfile: + - Gemfile + - gemfiles/rails_edge.gemfile continue-on-error: [false] - name: ${{ format('Tests (Ruby {0})', matrix.ruby-version) }} runs-on: ubuntu-latest continue-on-error: ${{ matrix.continue-on-error }} - + env: + BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }} steps: - uses: actions/checkout@v2 diff --git a/.gitignore b/.gitignore index 1e13116a..6100dc73 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .byebug_history *.gem coverage/* -.DS_Store \ No newline at end of file +.DS_Store +gemfiles/*.lock diff --git a/Gemfile b/Gemfile index 9c31a8f8..f015da37 100644 --- a/Gemfile +++ b/Gemfile @@ -2,7 +2,3 @@ source 'https://rubygems.org' git_source(:github) { |repo| "https://github.com/#{repo}.git" } gemspec - -gem "debug" -gem "mocha" -gem "railties" diff --git a/gemfiles/rails_edge.gemfile b/gemfiles/rails_edge.gemfile new file mode 100644 index 00000000..34a16801 --- /dev/null +++ b/gemfiles/rails_edge.gemfile @@ -0,0 +1,9 @@ +source 'https://rubygems.org' +git_source(:github) { |repo| "https://github.com/#{repo}.git" } + +git "https://github.com/rails/rails.git" do + gem "railties" + gem "activesupport" +end + +gemspec path: "../" diff --git a/mrsk.gemspec b/mrsk.gemspec index 6cd77252..d359aa28 100644 --- a/mrsk.gemspec +++ b/mrsk.gemspec @@ -19,4 +19,8 @@ Gem::Specification.new do |spec| spec.add_dependency "zeitwerk", "~> 2.5" spec.add_dependency "ed25519", "~> 1.2" spec.add_dependency "bcrypt_pbkdf", "~> 1.0" + + spec.add_development_dependency "debug" + spec.add_development_dependency "mocha" + spec.add_development_dependency "railties" end From 41a5cb2a04adb7f55d227a9c556bba053faac937 Mon Sep 17 00:00:00 2001 From: Chris Lowder Date: Fri, 10 Mar 2023 19:27:53 +0000 Subject: [PATCH 07/23] Avoid `[ActiveSupport::OrderedOptions#dig]` The implementation has been updated upstream[^1] to expect symbolized keys. MRSK relies heavily on the fact that nested keys are strings, so we're removing existing uses of `#dig`. [^1]: https://github.com/rails/rails/commit/5c15b586aab85fc4fa1465499fc9a97a446f4d52 --- lib/mrsk/commands/traefik.rb | 4 ++-- lib/mrsk/configuration.rb | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/mrsk/commands/traefik.rb b/lib/mrsk/commands/traefik.rb index ee22e64b..67ab448a 100644 --- a/lib/mrsk/commands/traefik.rb +++ b/lib/mrsk/commands/traefik.rb @@ -55,7 +55,7 @@ class Mrsk::Commands::Traefik < Mrsk::Commands::Base private def cmd_option_args - if args = config.raw_config.dig(:traefik, "args") + if args = config.traefik["args"] optionize args else [] @@ -63,6 +63,6 @@ class Mrsk::Commands::Traefik < Mrsk::Commands::Base end def host_port - config.raw_config.dig(:traefik, "host_port") || CONTAINER_PORT + config.traefik["host_port"] || CONTAINER_PORT end end diff --git a/lib/mrsk/configuration.rb b/lib/mrsk/configuration.rb index 64c116a2..03413116 100644 --- a/lib/mrsk/configuration.rb +++ b/lib/mrsk/configuration.rb @@ -165,6 +165,9 @@ class Mrsk::Configuration }.compact end + def traefik + raw_config.traefik || {} + end private # Will raise ArgumentError if any required config keys are missing From f26beeaa9f6100098f62d94e9438c8d04d05c844 Mon Sep 17 00:00:00 2001 From: Chris Lowder Date: Fri, 10 Mar 2023 20:51:14 +0000 Subject: [PATCH 08/23] Update `accessory remove` description and warning Make it clear the accessory's data directory will also be removed. --- lib/mrsk/cli/accessory.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/mrsk/cli/accessory.rb b/lib/mrsk/cli/accessory.rb index d0037cd4..834c7dec 100644 --- a/lib/mrsk/cli/accessory.rb +++ b/lib/mrsk/cli/accessory.rb @@ -149,13 +149,13 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base end end - desc "remove [NAME]", "Remove accessory container and image from host (use NAME=all to remove all accessories)" + desc "remove [NAME]", "Remove accessory container, image and data directory from host (use NAME=all to remove all accessories)" option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question" def remove(name) if name == "all" MRSK.accessory_names.each { |accessory_name| remove(accessory_name) } else - if options[:confirmed] || ask("This will remove all containers and images for #{name}. Are you sure?", limited_to: %w( y N ), default: "N") == "y" + if options[:confirmed] || ask("This will remove all containers, images and data directories for #{name}. Are you sure?", limited_to: %w( y N ), default: "N") == "y" with_accessory(name) do stop(name) remove_container(name) From bb241dea4387b94b93fbc3a1382fd60b2f3682e6 Mon Sep 17 00:00:00 2001 From: Richard Taylor Date: Fri, 10 Mar 2023 16:17:16 +0000 Subject: [PATCH 09/23] Add container name env var for containers Because the container name is generated it isn't possible to determine this inside the container. This adds the MRSK_CONTAINER_NAME env var when running the container so it can be read by the service running inside the container. --- README.md | 6 ++++++ lib/mrsk/commands/app.rb | 1 + lib/mrsk/commands/healthcheck.rb | 1 + test/commands/app_test.rb | 8 ++++---- test/commands/healthcheck_test.rb | 6 +++--- 5 files changed, 15 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 168bed4c..f4b5a4ac 100644 --- a/README.md +++ b/README.md @@ -222,6 +222,12 @@ volumes: - "/local/path:/container/path" ``` +### MRSK env variables + +The following env variables are set when your container runs: + +`MRSK_CONTAINER_NAME` : this contains the current container name and version + ### Using different roles for servers If your application uses separate hosts for running jobs or other roles beyond the default web running, you can specify these hosts in a dedicated role with a new entrypoint command like so: diff --git a/lib/mrsk/commands/app.rb b/lib/mrsk/commands/app.rb index 906a30d2..37d70476 100644 --- a/lib/mrsk/commands/app.rb +++ b/lib/mrsk/commands/app.rb @@ -7,6 +7,7 @@ class Mrsk::Commands::App < Mrsk::Commands::Base "--restart unless-stopped", "--log-opt", "max-size=#{MAX_LOG_SIZE}", "--name", service_with_version_and_destination, + "-e", "MRSK_CONTAINER_NAME=\"#{service_with_version_and_destination}\"", *role.env_args, *config.volume_args, *role.label_args, diff --git a/lib/mrsk/commands/healthcheck.rb b/lib/mrsk/commands/healthcheck.rb index 40ec2af7..d4fba27a 100644 --- a/lib/mrsk/commands/healthcheck.rb +++ b/lib/mrsk/commands/healthcheck.rb @@ -9,6 +9,7 @@ class Mrsk::Commands::Healthcheck < Mrsk::Commands::Base "--name", container_name_with_version, "--publish", "#{EXPOSED_PORT}:#{config.healthcheck["port"]}", "--label", "service=#{container_name}", + "-e", "MRSK_CONTAINER_NAME=\"#{container_name}\"", *web.env_args, *config.volume_args, config.absolute_image, diff --git a/test/commands/app_test.rb b/test/commands/app_test.rb index da436bcb..806db53b 100644 --- a/test/commands/app_test.rb +++ b/test/commands/app_test.rb @@ -14,7 +14,7 @@ class CommandsAppTest < ActiveSupport::TestCase test "run" do assert_equal \ - "docker run --detach --restart unless-stopped --log-opt max-size=10m --name app-999 -e RAILS_MASTER_KEY=\"456\" --label service=\"app\" --label role=\"web\" --label traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.services.app.loadbalancer.healthcheck.path=\"/up\" --label traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\" --label traefik.http.middlewares.app.retry.attempts=\"5\" --label traefik.http.middlewares.app.retry.initialinterval=\"500ms\" dhh/app:999", + "docker run --detach --restart unless-stopped --log-opt max-size=10m --name app-999 -e MRSK_CONTAINER_NAME=\"app-999\" -e RAILS_MASTER_KEY=\"456\" --label service=\"app\" --label role=\"web\" --label traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.services.app.loadbalancer.healthcheck.path=\"/up\" --label traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\" --label traefik.http.middlewares.app.retry.attempts=\"5\" --label traefik.http.middlewares.app.retry.initialinterval=\"500ms\" dhh/app:999", @app.run.join(" ") end @@ -22,7 +22,7 @@ class CommandsAppTest < ActiveSupport::TestCase @config[:volumes] = ["/local/path:/container/path" ] assert_equal \ - "docker run --detach --restart unless-stopped --log-opt max-size=10m --name app-999 -e RAILS_MASTER_KEY=\"456\" --volume /local/path:/container/path --label service=\"app\" --label role=\"web\" --label traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.services.app.loadbalancer.healthcheck.path=\"/up\" --label traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\" --label traefik.http.middlewares.app.retry.attempts=\"5\" --label traefik.http.middlewares.app.retry.initialinterval=\"500ms\" dhh/app:999", + "docker run --detach --restart unless-stopped --log-opt max-size=10m --name app-999 -e MRSK_CONTAINER_NAME=\"app-999\" -e RAILS_MASTER_KEY=\"456\" --volume /local/path:/container/path --label service=\"app\" --label role=\"web\" --label traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.services.app.loadbalancer.healthcheck.path=\"/up\" --label traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\" --label traefik.http.middlewares.app.retry.attempts=\"5\" --label traefik.http.middlewares.app.retry.initialinterval=\"500ms\" dhh/app:999", @app.run.join(" ") end @@ -30,7 +30,7 @@ class CommandsAppTest < ActiveSupport::TestCase @config[:healthcheck] = { "path" => "/healthz" } assert_equal \ - "docker run --detach --restart unless-stopped --log-opt max-size=10m --name app-999 -e RAILS_MASTER_KEY=\"456\" --label service=\"app\" --label role=\"web\" --label traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.services.app.loadbalancer.healthcheck.path=\"/healthz\" --label traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\" --label traefik.http.middlewares.app.retry.attempts=\"5\" --label traefik.http.middlewares.app.retry.initialinterval=\"500ms\" dhh/app:999", + "docker run --detach --restart unless-stopped --log-opt max-size=10m --name app-999 -e MRSK_CONTAINER_NAME=\"app-999\" -e RAILS_MASTER_KEY=\"456\" --label service=\"app\" --label role=\"web\" --label traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.services.app.loadbalancer.healthcheck.path=\"/healthz\" --label traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\" --label traefik.http.middlewares.app.retry.attempts=\"5\" --label traefik.http.middlewares.app.retry.initialinterval=\"500ms\" dhh/app:999", @app.run.join(" ") end @@ -39,7 +39,7 @@ class CommandsAppTest < ActiveSupport::TestCase @app = Mrsk::Commands::App.new Mrsk::Configuration.new(@config).tap { |c| c.version = "999" } assert_equal \ - "docker run --detach --restart unless-stopped --log-opt max-size=10m --name app-999 -e RAILS_MASTER_KEY=\"456\" --label service=\"app\" --label role=\"jobs\" --mount \"somewhere\" --cap-add dhh/app:999 bin/jobs", + "docker run --detach --restart unless-stopped --log-opt max-size=10m --name app-999 -e MRSK_CONTAINER_NAME=\"app-999\" -e RAILS_MASTER_KEY=\"456\" --label service=\"app\" --label role=\"jobs\" --mount \"somewhere\" --cap-add dhh/app:999 bin/jobs", @app.run(role: :jobs).join(" ") end diff --git a/test/commands/healthcheck_test.rb b/test/commands/healthcheck_test.rb index 05fbc8c3..54cb4362 100644 --- a/test/commands/healthcheck_test.rb +++ b/test/commands/healthcheck_test.rb @@ -10,7 +10,7 @@ class CommandsHealthcheckTest < ActiveSupport::TestCase test "run" do assert_equal \ - "docker run --detach --name healthcheck-app-123 --publish 3999:3000 --label service=healthcheck-app dhh/app:123", + "docker run --detach --name healthcheck-app-123 --publish 3999:3000 --label service=healthcheck-app -e MRSK_CONTAINER_NAME=\"healthcheck-app\" dhh/app:123", new_command.run.join(" ") end @@ -18,7 +18,7 @@ class CommandsHealthcheckTest < ActiveSupport::TestCase @config[:healthcheck] = { "port" => 3001 } assert_equal \ - "docker run --detach --name healthcheck-app-123 --publish 3999:3001 --label service=healthcheck-app dhh/app:123", + "docker run --detach --name healthcheck-app-123 --publish 3999:3001 --label service=healthcheck-app -e MRSK_CONTAINER_NAME=\"healthcheck-app\" dhh/app:123", new_command.run.join(" ") end @@ -26,7 +26,7 @@ class CommandsHealthcheckTest < ActiveSupport::TestCase @destination = "staging" assert_equal \ - "docker run --detach --name healthcheck-app-staging-123 --publish 3999:3000 --label service=healthcheck-app-staging dhh/app:123", + "docker run --detach --name healthcheck-app-staging-123 --publish 3999:3000 --label service=healthcheck-app-staging -e MRSK_CONTAINER_NAME=\"healthcheck-app-staging\" dhh/app:123", new_command.run.join(" ") end From 600902ef5e9d4869806d66b4e2bf6d02f211fac7 Mon Sep 17 00:00:00 2001 From: Rasmus Kjellberg <2277443+kjellberg@users.noreply.github.com> Date: Sun, 12 Mar 2023 07:39:07 +0100 Subject: [PATCH 10/23] Update README.md Backticks are handled by `Utils.optionize` --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 168bed4c..2427b0e8 100644 --- a/README.md +++ b/README.md @@ -256,12 +256,12 @@ servers: You can specialize the default Traefik rules by setting labels on the containers that are being started: -``` +```yaml labels: - traefik.http.routers.hey.rule: Host(\`app.hey.com\`) + traefik.http.routers.hey.rule: Host(`app.hey.com`) ``` -Note: The escaped backticks are needed to ensure the rule is passed in correctly and not treated as command substitution by Bash! +Note: The backticks are needed to ensure the rule is passed in correctly and not treated as command substitution by Bash! This allows you to run multiple applications on the same server sharing the same Traefik instance and port. See https://doc.traefik.io/traefik/routing/routers/#rule for a full list of available routing rules. From 9b39f2f3ab85c5fc028ed765347954b466a1d529 Mon Sep 17 00:00:00 2001 From: Samuel Sieg Date: Sun, 12 Mar 2023 10:41:04 +0100 Subject: [PATCH 11/23] Keep it simple for the proposal --- lib/mrsk/cli/build.rb | 4 ++-- lib/mrsk/cli/main.rb | 35 ++++++++++++++++++++++++----------- test/cli/build_test.rb | 21 --------------------- test/test_helper.rb | 9 --------- 4 files changed, 26 insertions(+), 43 deletions(-) diff --git a/lib/mrsk/cli/build.rb b/lib/mrsk/cli/build.rb index ec39282e..cb106741 100644 --- a/lib/mrsk/cli/build.rb +++ b/lib/mrsk/cli/build.rb @@ -1,8 +1,8 @@ class Mrsk::Cli::Build < Mrsk::Cli::Base desc "deliver", "Build app and push app image to registry then pull image on servers" - option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip image build and push" + option :use_prebuilt_image, aliases: "-P", type: :boolean, default: false, desc: "Use prebuilt image, skip build" def deliver - push unless options[:skip_push] + push unless options[:use_prebuilt_image] pull end diff --git a/lib/mrsk/cli/main.rb b/lib/mrsk/cli/main.rb index 163c0327..b77e5256 100644 --- a/lib/mrsk/cli/main.rb +++ b/lib/mrsk/cli/main.rb @@ -9,38 +9,51 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base end desc "deploy", "Deploy app to servers" - option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip image build and push" + option :use_prebuilt_image, aliases: "-P", type: :boolean, default: false, desc: "Use prebuilt image, skip build" def deploy runtime = print_runtime do + options_without_prebuilt_image = options.without(:use_prebuilt_image) + say "Ensure curl and Docker are installed...", :magenta - invoke "mrsk:cli:server:bootstrap", [], options.without(:skip_push) + invoke "mrsk:cli:server:bootstrap", [], options_without_prebuilt_image say "Log into image registry...", :magenta - invoke "mrsk:cli:registry:login", [], options.without(:skip_push) + invoke "mrsk:cli:registry:login", [], options_without_prebuilt_image - say "Build and push app image...", :magenta - invoke "mrsk:cli:build:deliver" + unless options[:use_prebuilt_image] + say "Build and push app image...", :magenta + invoke "mrsk:cli:build:push" + end + + say "Pull image onto servers...", :magenta + invoke "mrsk:cli:build:pull", [], options_without_prebuilt_image say "Ensure Traefik is running...", :magenta - invoke "mrsk:cli:traefik:boot", [], options.without(:skip_push) + invoke "mrsk:cli:traefik:boot", [], options_without_prebuilt_image say "Ensure app can pass healthcheck...", :magenta - invoke "mrsk:cli:healthcheck:perform", [], options.without(:skip_push) + invoke "mrsk:cli:healthcheck:perform", [], options_without_prebuilt_image - invoke "mrsk:cli:app:boot", [], options.without(:skip_push) + invoke "mrsk:cli:app:boot", [], options_without_prebuilt_image say "Prune old containers and images...", :magenta - invoke "mrsk:cli:prune:all", [], options.without(:skip_push) + invoke "mrsk:cli:prune:all", [], options_without_prebuilt_image end audit_broadcast "Deployed app in #{runtime.to_i} seconds" unless options[:skip_broadcast] end desc "redeploy", "Deploy app to servers without bootstrapping servers, starting Traefik, pruning, and registry login" + option :use_prebuilt_image, aliases: "-P", type: :boolean, default: false, desc: "Use prebuilt image, skip build" def redeploy runtime = print_runtime do - say "Build and push app image...", :magenta - invoke "mrsk:cli:build:deliver" + unless options[:use_prebuilt_image] + say "Build and push app image...", :magenta + invoke "mrsk:cli:build:push" + end + + say "Pull image onto servers...", :magenta + invoke "mrsk:cli:build:pull" say "Ensure app can pass healthcheck...", :magenta invoke "mrsk:cli:healthcheck:perform" diff --git a/test/cli/build_test.rb b/test/cli/build_test.rb index 2ded0193..b2ce6fd4 100644 --- a/test/cli/build_test.rb +++ b/test/cli/build_test.rb @@ -1,27 +1,6 @@ require_relative "cli_test_case" class CliBuildTest < CliTestCase - test "deliver" do - run_command("deliver").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 image rm --force dhh\/app:999 on 1\.1\.1\.2/, output - assert_match /docker pull dhh\/app:999 on 1\.1\.1\.1/, output - end - end - - test "deliver without push" do - run_command("deliver", "--skip-push").tap do |output| - assert_match /docker image rm --force dhh\/app:999 on 1\.1\.1\.2/, output - assert_match /docker pull dhh\/app:999 on 1\.1\.1\.1/, output - end - end - - test "push" do - 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 - end - test "pull" do run_command("pull").tap do |output| assert_match /docker image rm --force dhh\/app:999 on 1\.1\.1\.2/, output diff --git a/test/test_helper.rb b/test/test_helper.rb index 6f29420b..bf1f7b49 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -12,14 +12,5 @@ ActiveSupport::LogSubscriber.logger = ActiveSupport::Logger.new(STDOUT) if ENV[" # Applies to remote commands only SSHKit.config.backend = SSHKit::Backend::Printer -# Ensure local commands use the printer backend too -module SSHKit - module DSL - def run_locally(&block) - SSHKit::Backend::Printer.new(SSHKit::Host.new(:local), &block).run - end - end -end - class ActiveSupport::TestCase end From ff0170076e8c70e821869614ee46779567381a07 Mon Sep 17 00:00:00 2001 From: Samuel Sieg Date: Sun, 12 Mar 2023 10:44:33 +0100 Subject: [PATCH 12/23] Simplify --- lib/mrsk/cli/main.rb | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/lib/mrsk/cli/main.rb b/lib/mrsk/cli/main.rb index b77e5256..5d24c3f7 100644 --- a/lib/mrsk/cli/main.rb +++ b/lib/mrsk/cli/main.rb @@ -12,32 +12,25 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base option :use_prebuilt_image, aliases: "-P", type: :boolean, default: false, desc: "Use prebuilt image, skip build" def deploy runtime = print_runtime do - options_without_prebuilt_image = options.without(:use_prebuilt_image) - say "Ensure curl and Docker are installed...", :magenta - invoke "mrsk:cli:server:bootstrap", [], options_without_prebuilt_image + invoke "mrsk:cli:server:bootstrap", [], options.without(:use_prebuilt_image) say "Log into image registry...", :magenta - invoke "mrsk:cli:registry:login", [], options_without_prebuilt_image + invoke "mrsk:cli:registry:login", [], options.without(:use_prebuilt_image) - unless options[:use_prebuilt_image] - say "Build and push app image...", :magenta - invoke "mrsk:cli:build:push" - end - - say "Pull image onto servers...", :magenta - invoke "mrsk:cli:build:pull", [], options_without_prebuilt_image + say "Build and push app image...", :magenta + invoke "mrsk:cli:build:deliver" say "Ensure Traefik is running...", :magenta - invoke "mrsk:cli:traefik:boot", [], options_without_prebuilt_image + invoke "mrsk:cli:traefik:boot", [], options.without(:use_prebuilt_image) say "Ensure app can pass healthcheck...", :magenta - invoke "mrsk:cli:healthcheck:perform", [], options_without_prebuilt_image + invoke "mrsk:cli:healthcheck:perform", [], options.without(:use_prebuilt_image) - invoke "mrsk:cli:app:boot", [], options_without_prebuilt_image + invoke "mrsk:cli:app:boot", [], options.without(:use_prebuilt_image) say "Prune old containers and images...", :magenta - invoke "mrsk:cli:prune:all", [], options_without_prebuilt_image + invoke "mrsk:cli:prune:all", [], options.without(:use_prebuilt_image) end audit_broadcast "Deployed app in #{runtime.to_i} seconds" unless options[:skip_broadcast] From 47af6d94835087f3bd3e9b5e26fee2431835de05 Mon Sep 17 00:00:00 2001 From: Samuel Sieg Date: Sun, 12 Mar 2023 10:53:29 +0100 Subject: [PATCH 13/23] Is a global option better? --- lib/mrsk/cli/base.rb | 8 +++++--- lib/mrsk/cli/build.rb | 3 +-- lib/mrsk/cli/main.rb | 23 ++++++++--------------- lib/mrsk/commander.rb | 2 +- 4 files changed, 15 insertions(+), 21 deletions(-) diff --git a/lib/mrsk/cli/base.rb b/lib/mrsk/cli/base.rb index c7a87964..8fa3fc6b 100644 --- a/lib/mrsk/cli/base.rb +++ b/lib/mrsk/cli/base.rb @@ -12,6 +12,7 @@ module Mrsk::Cli class_option :quiet, type: :boolean, aliases: "-q", desc: "Minimal logging" class_option :version, desc: "Run commands against a specific app version" + class_option :use_prebuilt_image, type: :boolean, default: false, desc: "Use prebuilt image, skip building" class_option :primary, type: :boolean, aliases: "-p", desc: "Run commands only on primary host instead of all" class_option :hosts, aliases: "-h", desc: "Run commands on these hosts instead of all (separate by comma)" @@ -39,9 +40,10 @@ 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.config_file = Pathname.new(File.expand_path(options[:config_file])) + commander.destination = options[:destination] + commander.version = options[:version] + commander.use_prebuilt_image = options[:use_prebuilt_image] commander.specific_hosts = options[:hosts]&.split(",") commander.specific_roles = options[:roles]&.split(",") diff --git a/lib/mrsk/cli/build.rb b/lib/mrsk/cli/build.rb index cb106741..dd721d6f 100644 --- a/lib/mrsk/cli/build.rb +++ b/lib/mrsk/cli/build.rb @@ -1,8 +1,7 @@ class Mrsk::Cli::Build < Mrsk::Cli::Base desc "deliver", "Build app and push app image to registry then pull image on servers" - option :use_prebuilt_image, aliases: "-P", type: :boolean, default: false, desc: "Use prebuilt image, skip build" def deliver - push unless options[:use_prebuilt_image] + push unless MRSK.use_prebuilt_image pull end diff --git a/lib/mrsk/cli/main.rb b/lib/mrsk/cli/main.rb index 5d24c3f7..73268f50 100644 --- a/lib/mrsk/cli/main.rb +++ b/lib/mrsk/cli/main.rb @@ -9,44 +9,37 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base end desc "deploy", "Deploy app to servers" - option :use_prebuilt_image, aliases: "-P", type: :boolean, default: false, desc: "Use prebuilt image, skip build" def deploy runtime = print_runtime do say "Ensure curl and Docker are installed...", :magenta - invoke "mrsk:cli:server:bootstrap", [], options.without(:use_prebuilt_image) + invoke "mrsk:cli:server:bootstrap" say "Log into image registry...", :magenta - invoke "mrsk:cli:registry:login", [], options.without(:use_prebuilt_image) + invoke "mrsk:cli:registry:login" say "Build and push app image...", :magenta invoke "mrsk:cli:build:deliver" say "Ensure Traefik is running...", :magenta - invoke "mrsk:cli:traefik:boot", [], options.without(:use_prebuilt_image) + invoke "mrsk:cli:traefik:boot" say "Ensure app can pass healthcheck...", :magenta - invoke "mrsk:cli:healthcheck:perform", [], options.without(:use_prebuilt_image) + invoke "mrsk:cli:healthcheck:perform" - invoke "mrsk:cli:app:boot", [], options.without(:use_prebuilt_image) + invoke "mrsk:cli:app:boot" say "Prune old containers and images...", :magenta - invoke "mrsk:cli:prune:all", [], options.without(:use_prebuilt_image) + invoke "mrsk:cli:prune:all" end audit_broadcast "Deployed app in #{runtime.to_i} seconds" unless options[:skip_broadcast] end desc "redeploy", "Deploy app to servers without bootstrapping servers, starting Traefik, pruning, and registry login" - option :use_prebuilt_image, aliases: "-P", type: :boolean, default: false, desc: "Use prebuilt image, skip build" def redeploy runtime = print_runtime do - unless options[:use_prebuilt_image] - say "Build and push app image...", :magenta - invoke "mrsk:cli:build:push" - end - - say "Pull image onto servers...", :magenta - invoke "mrsk:cli:build:pull" + say "Build and push app image...", :magenta + invoke "mrsk:cli:build:deliver" say "Ensure app can pass healthcheck...", :magenta invoke "mrsk:cli:healthcheck:perform" diff --git a/lib/mrsk/commander.rb b/lib/mrsk/commander.rb index be2fd2e3..d9fb5347 100644 --- a/lib/mrsk/commander.rb +++ b/lib/mrsk/commander.rb @@ -1,7 +1,7 @@ require "active_support/core_ext/enumerable" class Mrsk::Commander - attr_accessor :config_file, :destination, :verbosity, :version + attr_accessor :config_file, :destination, :verbosity, :version, :use_prebuilt_image def initialize(config_file: nil, destination: nil, verbosity: :info) @config_file, @destination, @verbosity = config_file, destination, verbosity From 6232175ef8f2c516140b40e9044a2c9b054592da Mon Sep 17 00:00:00 2001 From: Samuel Sieg Date: Sun, 12 Mar 2023 10:56:12 +0100 Subject: [PATCH 14/23] Undo changes from experimenting --- test/cli/build_test.rb | 4 ++-- test/test_helper.rb | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/test/cli/build_test.rb b/test/cli/build_test.rb index b2ce6fd4..3d5f4431 100644 --- a/test/cli/build_test.rb +++ b/test/cli/build_test.rb @@ -3,8 +3,8 @@ require_relative "cli_test_case" class CliBuildTest < CliTestCase test "pull" do run_command("pull").tap do |output| - assert_match /docker image rm --force dhh\/app:999 on 1\.1\.1\.2/, output - assert_match /docker pull dhh\/app:999 on 1\.1\.1\.1/, output + assert_match /docker image rm --force dhh\/app:999/, output + assert_match /docker pull dhh\/app:999/, output end end diff --git a/test/test_helper.rb b/test/test_helper.rb index bf1f7b49..4c381df2 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -9,7 +9,6 @@ require "mrsk" ActiveSupport::LogSubscriber.logger = ActiveSupport::Logger.new(STDOUT) if ENV["VERBOSE"] -# Applies to remote commands only SSHKit.config.backend = SSHKit::Backend::Printer class ActiveSupport::TestCase From 9b666e54f3855a2eca545c61ed86a3260f7148d8 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 13 Mar 2023 10:43:44 -0400 Subject: [PATCH 15/23] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 8481e532..56d26116 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ MRSK deploys web apps anywhere from bare metal to cloud VMs using Docker with ze Watch the screencast: https://www.youtube.com/watch?v=LL1cV2FXZ5I +Join us on Discord: https://discord.gg/DQETs3Pm + ## 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: From 3e0b71b631998f80226e230a6e415aec9964f262 Mon Sep 17 00:00:00 2001 From: Richard Taylor Date: Mon, 13 Mar 2023 14:51:54 +0000 Subject: [PATCH 16/23] Fix healthcheck test Looks like the tests started failing on the options healthcheck PR after merging the container name env var PR. --- test/commands/healthcheck_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/commands/healthcheck_test.rb b/test/commands/healthcheck_test.rb index cd1f0d2d..effd3ef3 100644 --- a/test/commands/healthcheck_test.rb +++ b/test/commands/healthcheck_test.rb @@ -33,7 +33,7 @@ class CommandsHealthcheckTest < ActiveSupport::TestCase test "run with custom options" do @config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "options" => { "mount" => "somewhere" } } } assert_equal \ - "docker run --detach --name healthcheck-app-123 --publish 3999:3000 --label service=healthcheck-app --mount \"somewhere\" dhh/app:123", + "docker run --detach --name healthcheck-app-123 --publish 3999:3000 --label service=healthcheck-app -e MRSK_CONTAINER_NAME=\"healthcheck-app\" --mount \"somewhere\" dhh/app:123", new_command.run.join(" ") end From cb15800d2529cd0cb29ab916bbac029a7edbc39c Mon Sep 17 00:00:00 2001 From: Samuel Sieg Date: Mon, 13 Mar 2023 16:02:24 +0100 Subject: [PATCH 17/23] Move option to `deploy`/`redeploy`, rename to `skip-push` --- lib/mrsk/cli/base.rb | 8 +++----- lib/mrsk/cli/build.rb | 2 +- lib/mrsk/cli/main.rb | 38 +++++++++++++++++++++++++++----------- lib/mrsk/commander.rb | 2 +- 4 files changed, 32 insertions(+), 18 deletions(-) diff --git a/lib/mrsk/cli/base.rb b/lib/mrsk/cli/base.rb index 8fa3fc6b..c7a87964 100644 --- a/lib/mrsk/cli/base.rb +++ b/lib/mrsk/cli/base.rb @@ -12,7 +12,6 @@ module Mrsk::Cli class_option :quiet, type: :boolean, aliases: "-q", desc: "Minimal logging" class_option :version, desc: "Run commands against a specific app version" - class_option :use_prebuilt_image, type: :boolean, default: false, desc: "Use prebuilt image, skip building" class_option :primary, type: :boolean, aliases: "-p", desc: "Run commands only on primary host instead of all" class_option :hosts, aliases: "-h", desc: "Run commands on these hosts instead of all (separate by comma)" @@ -40,10 +39,9 @@ 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.use_prebuilt_image = options[:use_prebuilt_image] + 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(",") diff --git a/lib/mrsk/cli/build.rb b/lib/mrsk/cli/build.rb index dd721d6f..3e8912d7 100644 --- a/lib/mrsk/cli/build.rb +++ b/lib/mrsk/cli/build.rb @@ -1,7 +1,7 @@ class Mrsk::Cli::Build < Mrsk::Cli::Base desc "deliver", "Build app and push app image to registry then pull image on servers" def deliver - push unless MRSK.use_prebuilt_image + push pull end diff --git a/lib/mrsk/cli/main.rb b/lib/mrsk/cli/main.rb index 73268f50..e34cdaf3 100644 --- a/lib/mrsk/cli/main.rb +++ b/lib/mrsk/cli/main.rb @@ -9,42 +9,58 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base end 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) + runtime = print_runtime do say "Ensure curl and Docker are installed...", :magenta - invoke "mrsk:cli:server:bootstrap" + invoke "mrsk:cli:server:bootstrap", [], invoke_options say "Log into image registry...", :magenta invoke "mrsk:cli:registry:login" - say "Build and push app image...", :magenta - invoke "mrsk:cli:build:deliver" + if options[:skip_push] + say "Pull app image...", :magenta + invoke "mrsk:cli:build:pull", [], invoke_options + else + say "Build and push app image...", :magenta + invoke "mrsk:cli:build:deliver", [], invoke_options + end say "Ensure Traefik is running...", :magenta - invoke "mrsk:cli:traefik:boot" + invoke "mrsk:cli:traefik:boot", [], invoke_options say "Ensure app can pass healthcheck...", :magenta - invoke "mrsk:cli:healthcheck:perform" + invoke "mrsk:cli:healthcheck:perform", [], invoke_options - invoke "mrsk:cli:app:boot" + invoke "mrsk:cli:app:boot", [], invoke_options say "Prune old containers and images...", :magenta - invoke "mrsk:cli:prune:all" + invoke "mrsk:cli:prune:all", [], invoke_options end audit_broadcast "Deployed app in #{runtime.to_i} seconds" unless options[:skip_broadcast] end 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) + runtime = print_runtime do - say "Build and push app image...", :magenta - invoke "mrsk:cli:build:deliver" + if options[:skip_push] + say "Pull app image...", :magenta + invoke "mrsk:cli:build:pull", [], invoke_options + else + say "Build and push app image...", :magenta + invoke "mrsk:cli:build:deliver", [], invoke_options + end say "Ensure app can pass healthcheck...", :magenta - invoke "mrsk:cli:healthcheck:perform" + invoke "mrsk:cli:healthcheck:perform", [], invoke_options - invoke "mrsk:cli:app:boot" + invoke "mrsk:cli:app:boot", [], invoke_options end audit_broadcast "Redeployed app in #{runtime.to_i} seconds" unless options[:skip_broadcast] diff --git a/lib/mrsk/commander.rb b/lib/mrsk/commander.rb index d9fb5347..be2fd2e3 100644 --- a/lib/mrsk/commander.rb +++ b/lib/mrsk/commander.rb @@ -1,7 +1,7 @@ require "active_support/core_ext/enumerable" class Mrsk::Commander - attr_accessor :config_file, :destination, :verbosity, :version, :use_prebuilt_image + attr_accessor :config_file, :destination, :verbosity, :version def initialize(config_file: nil, destination: nil, verbosity: :info) @config_file, @destination, @verbosity = config_file, destination, verbosity From c29d1ddebad235df7978abbc0feb32b79c65619e Mon Sep 17 00:00:00 2001 From: Samuel Sieg Date: Mon, 13 Mar 2023 16:05:21 +0100 Subject: [PATCH 18/23] Fix --- lib/mrsk/cli/main.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mrsk/cli/main.rb b/lib/mrsk/cli/main.rb index e34cdaf3..c9d81115 100644 --- a/lib/mrsk/cli/main.rb +++ b/lib/mrsk/cli/main.rb @@ -18,7 +18,7 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base invoke "mrsk:cli:server:bootstrap", [], invoke_options say "Log into image registry...", :magenta - invoke "mrsk:cli:registry:login" + invoke "mrsk:cli:registry:login", [], invoke_options if options[:skip_push] say "Pull app image...", :magenta From 0ac2cd2a4bccd92421e1942fb8b55062f0a889e1 Mon Sep 17 00:00:00 2001 From: Samuel Sieg Date: Tue, 14 Mar 2023 11:49:31 +0100 Subject: [PATCH 19/23] Add tests for deploy/redeploy commands --- test/cli/main_test.rb | 68 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/test/cli/main_test.rb b/test/cli/main_test.rb index 9f5640e4..fe7d4d6a 100644 --- a/test/cli/main_test.rb +++ b/test/cli/main_test.rb @@ -6,6 +6,74 @@ class CliMainTest < CliTestCase assert_equal Mrsk::VERSION, version end + test "deploy" do + invoke_options = { "config_file" => "test/fixtures/deploy_with_accessories.yml", "skip_broadcast" => false, "skip_push" => false} + + 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) + Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:healthcheck:perform", [], invoke_options) + Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:app:boot", [], invoke_options) + 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 + assert_match /Ensure app can pass healthcheck/, output + assert_match /Prune old containers and images/, output + end + end + + test "deploy with skip_push" do + invoke_options = { "config_file" => "test/fixtures/deploy_with_accessories.yml", "skip_broadcast" => false, "skip_push" => true } + + 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) + Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:healthcheck:perform", [], invoke_options) + Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:app:boot", [], invoke_options) + Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:prune:all", [], invoke_options) + + run_command("deploy", "--skip_push").tap do |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 + assert_match /Ensure app can pass healthcheck/, output + assert_match /Prune old containers and images/, output + end + end + + test "redeploy" do + invoke_options = { "config_file" => "test/fixtures/deploy_with_accessories.yml", "skip_broadcast" => false, "skip_push" => false} + + 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) + Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:app:boot", [], invoke_options) + + run_command("redeploy").tap do |output| + assert_match /Build and push app image/, output + assert_match /Ensure app can pass healthcheck/, output + end + end + + test "redeploy with skip_push" do + invoke_options = { "config_file" => "test/fixtures/deploy_with_accessories.yml", "skip_broadcast" => false, "skip_push" => true } + + 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) + Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:app:boot", [], invoke_options) + + run_command("redeploy", "--skip_push").tap do |output| + assert_match /Pull app image/, output + assert_match /Ensure app can pass healthcheck/, output + end + end + test "rollback bad version" do run_command("details") # Preheat MRSK const From 50ee954ca94a7a920a08df306ff81389287c4406 Mon Sep 17 00:00:00 2001 From: Jacopo Date: Tue, 14 Mar 2023 11:58:26 +0100 Subject: [PATCH 20/23] Fix Traefik retry middleware As per [Traefik docs](https://doc.traefik.io/traefik/middlewares/overview/#configuration-example) a middleware to be activated needs to be applied to a route. Change the default settings to apply the `retry` middleware on every role with Traefik enabled. --- lib/mrsk/configuration/role.rb | 5 +++-- test/commands/app_test.rb | 6 +++--- test/configuration/role_test.rb | 4 ++-- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/mrsk/configuration/role.rb b/lib/mrsk/configuration/role.rb index cc56be3d..f3e20e9a 100644 --- a/lib/mrsk/configuration/role.rb +++ b/lib/mrsk/configuration/role.rb @@ -73,8 +73,9 @@ class Mrsk::Configuration::Role "traefik.http.routers.#{config.service}.rule" => "PathPrefix(`/`)", "traefik.http.services.#{config.service}.loadbalancer.healthcheck.path" => config.healthcheck["path"], "traefik.http.services.#{config.service}.loadbalancer.healthcheck.interval" => "1s", - "traefik.http.middlewares.#{config.service}.retry.attempts" => "5", - "traefik.http.middlewares.#{config.service}.retry.initialinterval" => "500ms" + "traefik.http.middlewares.#{config.service}-retry.retry.attempts" => "5", + "traefik.http.middlewares.#{config.service}-retry.retry.initialinterval" => "500ms", + "traefik.http.routers.#{config.service}.middlewares" => "#{config.service}-retry@docker" } else {} diff --git a/test/commands/app_test.rb b/test/commands/app_test.rb index 806db53b..c1a30f46 100644 --- a/test/commands/app_test.rb +++ b/test/commands/app_test.rb @@ -14,7 +14,7 @@ class CommandsAppTest < ActiveSupport::TestCase test "run" do assert_equal \ - "docker run --detach --restart unless-stopped --log-opt max-size=10m --name app-999 -e MRSK_CONTAINER_NAME=\"app-999\" -e RAILS_MASTER_KEY=\"456\" --label service=\"app\" --label role=\"web\" --label traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.services.app.loadbalancer.healthcheck.path=\"/up\" --label traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\" --label traefik.http.middlewares.app.retry.attempts=\"5\" --label traefik.http.middlewares.app.retry.initialinterval=\"500ms\" dhh/app:999", + "docker run --detach --restart unless-stopped --log-opt max-size=10m --name app-999 -e MRSK_CONTAINER_NAME=\"app-999\" -e RAILS_MASTER_KEY=\"456\" --label service=\"app\" --label role=\"web\" --label traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.services.app.loadbalancer.healthcheck.path=\"/up\" --label traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\" --label traefik.http.middlewares.app-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app.middlewares=\"app-retry@docker\" dhh/app:999", @app.run.join(" ") end @@ -22,7 +22,7 @@ class CommandsAppTest < ActiveSupport::TestCase @config[:volumes] = ["/local/path:/container/path" ] assert_equal \ - "docker run --detach --restart unless-stopped --log-opt max-size=10m --name app-999 -e MRSK_CONTAINER_NAME=\"app-999\" -e RAILS_MASTER_KEY=\"456\" --volume /local/path:/container/path --label service=\"app\" --label role=\"web\" --label traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.services.app.loadbalancer.healthcheck.path=\"/up\" --label traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\" --label traefik.http.middlewares.app.retry.attempts=\"5\" --label traefik.http.middlewares.app.retry.initialinterval=\"500ms\" dhh/app:999", + "docker run --detach --restart unless-stopped --log-opt max-size=10m --name app-999 -e MRSK_CONTAINER_NAME=\"app-999\" -e RAILS_MASTER_KEY=\"456\" --volume /local/path:/container/path --label service=\"app\" --label role=\"web\" --label traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.services.app.loadbalancer.healthcheck.path=\"/up\" --label traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\" --label traefik.http.middlewares.app-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app.middlewares=\"app-retry@docker\" dhh/app:999", @app.run.join(" ") end @@ -30,7 +30,7 @@ class CommandsAppTest < ActiveSupport::TestCase @config[:healthcheck] = { "path" => "/healthz" } assert_equal \ - "docker run --detach --restart unless-stopped --log-opt max-size=10m --name app-999 -e MRSK_CONTAINER_NAME=\"app-999\" -e RAILS_MASTER_KEY=\"456\" --label service=\"app\" --label role=\"web\" --label traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.services.app.loadbalancer.healthcheck.path=\"/healthz\" --label traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\" --label traefik.http.middlewares.app.retry.attempts=\"5\" --label traefik.http.middlewares.app.retry.initialinterval=\"500ms\" dhh/app:999", + "docker run --detach --restart unless-stopped --log-opt max-size=10m --name app-999 -e MRSK_CONTAINER_NAME=\"app-999\" -e RAILS_MASTER_KEY=\"456\" --label service=\"app\" --label role=\"web\" --label traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.services.app.loadbalancer.healthcheck.path=\"/healthz\" --label traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\" --label traefik.http.middlewares.app-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app.middlewares=\"app-retry@docker\" dhh/app:999", @app.run.join(" ") end diff --git a/test/configuration/role_test.rb b/test/configuration/role_test.rb index 1017221f..0f83d3df 100644 --- a/test/configuration/role_test.rb +++ b/test/configuration/role_test.rb @@ -42,7 +42,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase end test "special label args for web" do - assert_equal [ "--label", "service=\"app\"", "--label", "role=\"web\"", "--label", "traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\"", "--label", "traefik.http.services.app.loadbalancer.healthcheck.path=\"/up\"", "--label", "traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\"", "--label", "traefik.http.middlewares.app.retry.attempts=\"5\"", "--label", "traefik.http.middlewares.app.retry.initialinterval=\"500ms\""], @config.role(:web).label_args + assert_equal [ "--label", "service=\"app\"", "--label", "role=\"web\"", "--label", "traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\"", "--label", "traefik.http.services.app.loadbalancer.healthcheck.path=\"/up\"", "--label", "traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\"", "--label", "traefik.http.middlewares.app-retry.retry.attempts=\"5\"", "--label", "traefik.http.middlewares.app-retry.retry.initialinterval=\"500ms\"", "--label", "traefik.http.routers.app.middlewares=\"app-retry@docker\"" ], @config.role(:web).label_args end test "custom labels" do @@ -66,7 +66,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase c[:servers]["beta"] = { "traefik" => "true", "hosts" => [ "1.1.1.5" ] } }) - assert_equal [ "--label", "service=\"app\"", "--label", "role=\"beta\"", "--label", "traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\"", "--label", "traefik.http.services.app.loadbalancer.healthcheck.path=\"/up\"", "--label", "traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\"", "--label", "traefik.http.middlewares.app.retry.attempts=\"5\"", "--label", "traefik.http.middlewares.app.retry.initialinterval=\"500ms\"" ], config.role(:beta).label_args + assert_equal [ "--label", "service=\"app\"", "--label", "role=\"beta\"", "--label", "traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\"", "--label", "traefik.http.services.app.loadbalancer.healthcheck.path=\"/up\"", "--label", "traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\"", "--label", "traefik.http.middlewares.app-retry.retry.attempts=\"5\"", "--label", "traefik.http.middlewares.app-retry.retry.initialinterval=\"500ms\"", "--label", "traefik.http.routers.app.middlewares=\"app-retry@docker\"" ], config.role(:beta).label_args end test "env overwritten by role" do From 3ca5bc50b674607f9f594fe538dd809b75644d3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20B=C3=BChlmann?= Date: Tue, 14 Mar 2023 15:04:33 +0100 Subject: [PATCH 21/23] Properly pass traefik command options Traefik command options need to be passed as `--key=value`, not `--key value`. --- lib/mrsk/commands/traefik.rb | 2 +- lib/mrsk/utils.rb | 10 ++++++++-- test/commands/traefik_test.rb | 6 +++--- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/mrsk/commands/traefik.rb b/lib/mrsk/commands/traefik.rb index 67ab448a..b9ff19c1 100644 --- a/lib/mrsk/commands/traefik.rb +++ b/lib/mrsk/commands/traefik.rb @@ -56,7 +56,7 @@ class Mrsk::Commands::Traefik < Mrsk::Commands::Base private def cmd_option_args if args = config.traefik["args"] - optionize args + optionize args, with: "=" else [] end diff --git a/lib/mrsk/utils.rb b/lib/mrsk/utils.rb index d8860f1a..1e763250 100644 --- a/lib/mrsk/utils.rb +++ b/lib/mrsk/utils.rb @@ -24,8 +24,14 @@ module Mrsk::Utils end # Returns a list of shell-dashed option arguments. If the value is true, it's treated like a value-less option. - def optionize(args) - args.collect { |(key, value)| [ "--#{key}", value == true ? nil : escape_shell_value(value) ] }.flatten.compact + def optionize(args, with: nil) + options = if with + args.collect { |(key, value)| value == true ? "--#{key}" : "--#{key}#{with}#{escape_shell_value(value)}" } + else + args.collect { |(key, value)| [ "--#{key}", value == true ? nil : escape_shell_value(value) ] } + end + + options.flatten.compact end # Copied from SSHKit::Backend::Abstract#redact to be available inside Commands classes diff --git a/test/commands/traefik_test.rb b/test/commands/traefik_test.rb index 97310623..5ca51e13 100644 --- a/test/commands/traefik_test.rb +++ b/test/commands/traefik_test.rb @@ -4,18 +4,18 @@ class CommandsTraefikTest < ActiveSupport::TestCase setup do @config = { service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ], - traefik: { "args" => { "accesslog.format" => "json", "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } } + traefik: { "args" => { "accesslog.format" => "json", "api.insecure" => true, "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } } } end test "run" do assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --log-opt max-size=10m --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock traefik --providers.docker --log.level=DEBUG --accesslog.format \"json\" --metrics.prometheus.buckets \"0.1,0.3,1.2,5.0\"", + "docker run --name traefik --detach --restart unless-stopped --log-opt max-size=10m --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock traefik --providers.docker --log.level=DEBUG --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", new_command.run.join(" ") @config[:traefik]["host_port"] = "8080" assert_equal \ - "docker run --name traefik --detach --restart unless-stopped --log-opt max-size=10m --publish 8080:80 --volume /var/run/docker.sock:/var/run/docker.sock traefik --providers.docker --log.level=DEBUG --accesslog.format \"json\" --metrics.prometheus.buckets \"0.1,0.3,1.2,5.0\"", + "docker run --name traefik --detach --restart unless-stopped --log-opt max-size=10m --publish 8080:80 --volume /var/run/docker.sock:/var/run/docker.sock traefik --providers.docker --log.level=DEBUG --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"", new_command.run.join(" ") end From 46dad1ee6c2507b971e576963f446d68df1bdaf4 Mon Sep 17 00:00:00 2001 From: Samuel Sieg Date: Tue, 14 Mar 2023 15:58:12 +0100 Subject: [PATCH 22/23] Add tests for main CLI commands --- lib/mrsk/cli/main.rb | 6 ++- test/cli/main_test.rb | 105 ++++++++++++++++++++++++++++++++++++++++-- test/test_helper.rb | 11 +++++ 3 files changed, 116 insertions(+), 6 deletions(-) diff --git a/lib/mrsk/cli/main.rb b/lib/mrsk/cli/main.rb index c9d81115..328af480 100644 --- a/lib/mrsk/cli/main.rb +++ b/lib/mrsk/cli/main.rb @@ -135,8 +135,10 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base puts "Binstub already exists in bin/mrsk (remove first to create a new one)" else puts "Adding MRSK to Gemfile and bundle..." - `bundle add mrsk` - `bundle binstubs mrsk` + run_locally do + execute :bundle, :add, :mrsk + execute :bundle, :binstubs, :mrsk + end puts "Created binstub file in bin/mrsk" end end diff --git a/test/cli/main_test.rb b/test/cli/main_test.rb index fe7d4d6a..f72741dd 100644 --- a/test/cli/main_test.rb +++ b/test/cli/main_test.rb @@ -1,13 +1,16 @@ require_relative "cli_test_case" class CliMainTest < CliTestCase - test "version" do - version = stdouted { Mrsk::Cli::Main.new.version } - assert_equal Mrsk::VERSION, version + test "setup" do + Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:server:bootstrap") + Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:accessory:boot", [ "all" ]) + Mrsk::Cli::Main.any_instance.expects(:deploy) + + run_command("setup") 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, "skip_push" => false } 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) @@ -93,6 +96,95 @@ class CliMainTest < CliTestCase end end + test "details" do + Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:traefik:details") + Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:app:details") + Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:accessory:details", [ "all" ]) + + run_command("details") + end + + test "audit" do + run_command("audit").tap do |output| + assert_match /tail -n 50 mrsk-app-audit.log on 1.1.1.1/, output + assert_match /App Host: 1.1.1.1/, output + assert_match /tail -n 50 mrsk-app-audit.log on 1.1.1.2/, output + assert_match /App Host: 1.1.1.2/, output + end + end + + test "config" do + run_command("config").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 "dhh/app", config[:repository] + assert_equal "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) + FileUtils.stubs(:cp_r) + + run_command("init").tap do |output| + assert_match /Created configuration file in config\/deploy.yml/, output + assert_match /Created \.env file/, output + end + end + + test "init with existing config" do + Pathname.any_instance.expects(:exist?).returns(true).twice + + run_command("init").tap do |output| + assert_match /Config file already exists in config\/deploy.yml \(remove first to create a new one\)/, output + end + end + + test "init with bundle option" do + Pathname.any_instance.expects(:exist?).returns(false).times(3) + FileUtils.stubs(:mkdir_p) + FileUtils.stubs(:cp_r) + + run_command("init", "--bundle").tap do |output| + assert_match /Created configuration file in config\/deploy.yml/, output + assert_match /Created \.env file/, output + assert_match /Adding MRSK to Gemfile and bundle/, output + assert_match /bundle add mrsk/, output + assert_match /bundle binstubs mrsk/, output + assert_match /Created binstub file in bin\/mrsk/, output + end + end + + test "init with bundle option and existing binstub" do + Pathname.any_instance.expects(:exist?).returns(true).times(3) + FileUtils.stubs(:mkdir_p) + FileUtils.stubs(:cp_r) + + run_command("init", "--bundle").tap do |output| + assert_match /Config file already exists in config\/deploy.yml \(remove first to create a new one\)/, output + assert_match /Binstub already exists in bin\/mrsk \(remove first to create a new one\)/, output + end + end + + test "envify" do + File.expects(:read).with(".env.erb").returns("HELLO=<%= 'world' %>") + File.expects(:write).with(".env", "HELLO=world", perm: 0600) + + run_command("envify") + end + + test "envify with destination" do + File.expects(:read).with(".env.staging.erb").returns("HELLO=<%= 'world' %>") + File.expects(:write).with(".env.staging", "HELLO=world", perm: 0600) + + run_command("envify", "-d", "staging") + end + test "remove with confirmation" do run_command("remove", "-y").tap do |output| assert_match /docker container stop traefik/, output @@ -117,6 +209,11 @@ class CliMainTest < CliTestCase end end + test "version" do + version = stdouted { Mrsk::Cli::Main.new.version } + assert_equal Mrsk::VERSION, version + end + private def run_command(*command) stdouted { Mrsk::Cli::Main.start([*command, "-c", "test/fixtures/deploy_with_accessories.yml"]) } diff --git a/test/test_helper.rb b/test/test_helper.rb index 4c381df2..62cceb5b 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -9,7 +9,18 @@ require "mrsk" ActiveSupport::LogSubscriber.logger = ActiveSupport::Logger.new(STDOUT) if ENV["VERBOSE"] +# Applies to remote commands only, +# see https://github.com/capistrano/sshkit/blob/master/lib/sshkit/dsl.rb#L9 SSHKit.config.backend = SSHKit::Backend::Printer +# Ensure local commands use the printer backend too +module SSHKit + module DSL + def run_locally(&block) + SSHKit::Backend::Printer.new(SSHKit::Host.new(:local), &block).run + end + end +end + class ActiveSupport::TestCase end From 3fd2f3f2c540b9f2d5ee0b4aad5d256ed5ce7e13 Mon Sep 17 00:00:00 2001 From: Samuel Sieg Date: Tue, 14 Mar 2023 16:05:57 +0100 Subject: [PATCH 23/23] Improve comments --- test/test_helper.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/test_helper.rb b/test/test_helper.rb index 62cceb5b..3704e8e2 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -9,11 +9,11 @@ require "mrsk" ActiveSupport::LogSubscriber.logger = ActiveSupport::Logger.new(STDOUT) if ENV["VERBOSE"] -# Applies to remote commands only, -# see https://github.com/capistrano/sshkit/blob/master/lib/sshkit/dsl.rb#L9 +# Applies to remote commands only. SSHKit.config.backend = SSHKit::Backend::Printer -# Ensure local commands use the printer backend too +# Ensure local commands use the printer backend too. +# See https://github.com/capistrano/sshkit/blob/master/lib/sshkit/dsl.rb#L9 module SSHKit module DSL def run_locally(&block)