Merge branch 'main' into allow-bastion-server

This commit is contained in:
David Heinemeier Hansson
2023-02-04 10:06:15 +01:00
committed by GitHub
44 changed files with 429 additions and 297 deletions

View File

@@ -1,11 +1,12 @@
PATH PATH
remote: . remote: .
specs: specs:
mrsk (0.4.0) mrsk (0.5.1)
activesupport (>= 7.0) activesupport (>= 7.0)
dotenv (~> 2.8) dotenv (~> 2.8)
sshkit (~> 1.21) sshkit (~> 1.21)
thor (~> 1.2) thor (~> 1.2)
zeitwerk (~> 2.5)
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
@@ -91,6 +92,7 @@ PLATFORMS
arm64-darwin-22 arm64-darwin-22
x86_64-darwin-20 x86_64-darwin-20
x86_64-darwin-21 x86_64-darwin-21
x86_64-darwin-22
x86_64-linux x86_64-linux
DEPENDENCIES DEPENDENCIES

View File

@@ -319,9 +319,9 @@ Now run `mrsk accessory start mysql` to start the MySQL server on 1.1.1.3. See `
## Commands ## Commands
### Running remote execution and runners ### Running commands on servers
If you need to execute commands inside the Rails containers, you can use `mrsk app exec` and `mrsk app runner`. Examples: You can execute one-off commands on the servers:
```bash ```bash
# Runs command on all servers # Runs command on all servers
@@ -364,13 +364,25 @@ Database adapter sqlite3
Database schema version 20221231233303 Database schema version 20221231233303
# Run Rails runner on primary server # Run Rails runner on primary server
mrsk app runner -p 'puts Rails.application.config.time_zone' mrsk app exec -p 'bin/rails runner "puts Rails.application.config.time_zone"'
UTC UTC
``` ```
### Running a Rails console ### Running interactive commands over SSH
You can run interactive commands, like a Rails console or a bash session, on a server (default is primary, use `--hosts` to connect to another):
```bash
# Starts a bash session in a new container made from the most recent app image
mrsk app exec -i bash
# Starts a bash session in the currently running container for the app
mrsk app exec -i --reuse bash
# Starts a Rails console in a new container made from the most recent app image
mrsk app exec -i 'bin/rails console'
```
If you need to interact with the production console for the app, you can use `mrsk app console`, which will start a Rails console session on the primary host. You can start the console on a different host using `mrsk app console --host 192.168.0.2`. Be mindful that this is a live wire! Any changes made to the production database will take effect immeditately.
### Running details to see state of containers ### Running details to see state of containers

View File

@@ -4,7 +4,7 @@
Thread.report_on_exception = false Thread.report_on_exception = false
require "dotenv/load" require "dotenv/load"
require "mrsk/cli" require "mrsk"
begin begin
Mrsk::Cli::Main.start(ARGV) Mrsk::Cli::Main.start(ARGV)

View File

@@ -1,5 +1,9 @@
module Mrsk module Mrsk
end end
require "mrsk/version" require "zeitwerk"
require "mrsk/commander"
loader = Zeitwerk::Loader.for_gem
loader.ignore("#{__dir__}/mrsk/sshkit_with_ext.rb")
loader.setup
loader.eager_load # We need all commands loaded.

View File

@@ -1,9 +1,5 @@
require "mrsk"
module Mrsk::Cli module Mrsk::Cli
end end
# SSHKit uses instance eval, so we need a global const for ergonomics # SSHKit uses instance eval, so we need a global const for ergonomics
MRSK = Mrsk::Commander.new MRSK = Mrsk::Commander.new
require "mrsk/cli/main"

View File

@@ -1,5 +1,3 @@
require "mrsk/cli/base"
class Mrsk::Cli::Accessory < Mrsk::Cli::Base class Mrsk::Cli::Accessory < Mrsk::Cli::Base
desc "boot [NAME]", "Boot accessory service on host (use NAME=all to boot all accessories)" desc "boot [NAME]", "Boot accessory service on host (use NAME=all to boot all accessories)"
def boot(name) def boot(name)
@@ -9,7 +7,10 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
with_accessory(name) do |accessory| with_accessory(name) do |accessory|
directories(name) directories(name)
upload(name) upload(name)
on(accessory.host) { execute *accessory.run } on(accessory.host) do
execute *MRSK.auditor.record("accessory #{name} boot"), verbosity: :debug
execute *accessory.run
end
end end
end end
end end
@@ -18,6 +19,8 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
def upload(name) def upload(name)
with_accessory(name) do |accessory| with_accessory(name) do |accessory|
on(accessory.host) do on(accessory.host) do
execute *MRSK.auditor.record("accessory #{name} upload files"), verbosity: :debug
accessory.files.each do |(local, remote)| accessory.files.each do |(local, remote)|
accessory.ensure_local_file_present(local) accessory.ensure_local_file_present(local)
@@ -33,6 +36,8 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
def directories(name) def directories(name)
with_accessory(name) do |accessory| with_accessory(name) do |accessory|
on(accessory.host) do on(accessory.host) do
execute *MRSK.auditor.record("accessory #{name} create directories"), verbosity: :debug
accessory.directories.keys.each do |host_path| accessory.directories.keys.each do |host_path|
execute *accessory.make_directory(host_path) execute *accessory.make_directory(host_path)
end end
@@ -52,14 +57,20 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
desc "start [NAME]", "Start existing accessory on host" desc "start [NAME]", "Start existing accessory on host"
def start(name) def start(name)
with_accessory(name) do |accessory| with_accessory(name) do |accessory|
on(accessory.host) { execute *accessory.start } on(accessory.host) do
execute *MRSK.auditor.record("accessory #{name} start"), verbosity: :debug
execute *accessory.start
end
end end
end end
desc "stop [NAME]", "Stop accessory on host" desc "stop [NAME]", "Stop accessory on host"
def stop(name) def stop(name)
with_accessory(name) do |accessory| with_accessory(name) do |accessory|
on(accessory.host) { execute *accessory.stop, raise_on_non_zero_exit: false } on(accessory.host) do
execute *MRSK.auditor.record("accessory #{name} stop"), verbosity: :debug
execute *accessory.stop, raise_on_non_zero_exit: false
end
end end
end end
@@ -82,39 +93,33 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
end end
end end
desc "exec [NAME] [CMD]", "Execute a custom command on accessory host" desc "exec [NAME] [CMD]", "Execute a custom command on servers"
option :method, aliases: "-m", default: "exec", desc: "Execution method: [exec] perform inside container / [run] perform in new container / [ssh] perform over ssh" option :interactive, aliases: "-i", type: :boolean, default: false, desc: "Execute command over ssh for an interactive shell (use for console/bash)"
option :reuse, type: :boolean, default: false, desc: "Reuse currently running container instead of starting a new one"
def exec(name, cmd) def exec(name, cmd)
runner = \
case options[:method]
when "exec" then "exec"
when "run" then "run_exec"
when "ssh_exec" then "exec_over_ssh"
when "ssh_run" then "run_over_ssh"
else raise "Unknown method: #{options[:method]}"
end.inquiry
with_accessory(name) do |accessory| with_accessory(name) do |accessory|
if runner.exec_over_ssh? || runner.run_over_ssh? case
run_locally do when options[:interactive] && options[:reuse]
info "Launching command on #{accessory.host}" say "Launching interactive command with via SSH from existing container...", :magenta
exec accessory.send(runner, cmd) run_locally { exec accessory.execute_in_existing_container_over_ssh(cmd) }
end
else when options[:interactive]
say "Launching interactive command via SSH from new container...", :magenta
run_locally { exec accessory.execute_in_new_container_over_ssh(cmd) }
when options[:reuse]
say "Launching command from existing container...", :magenta
on(accessory.host) do on(accessory.host) do
info "Launching command on #{accessory.host}" execute *MRSK.auditor.record("accessory #{name} cmd '#{cmd}'"), verbosity: :debug
execute *accessory.send(runner, cmd) capture_with_info(*accessory.execute_in_existing_container(cmd))
end end
end
end
end
desc "bash [NAME]", "Start a bash session on primary host (or specific host set by --hosts)" else
def bash(name) say "Launching command from new container...", :magenta
with_accessory(name) do |accessory| on(accessory.host) do
run_locally do execute *MRSK.auditor.record("accessory #{name} cmd '#{cmd}'"), verbosity: :debug
info "Launching bash session on #{accessory.host}" capture_with_info(*accessory.execute_in_new_container(cmd))
exec accessory.bash end
end end
end end
end end
@@ -162,21 +167,30 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
desc "remove_container [NAME]", "Remove accessory container from host" desc "remove_container [NAME]", "Remove accessory container from host"
def remove_container(name) def remove_container(name)
with_accessory(name) do |accessory| with_accessory(name) do |accessory|
on(accessory.host) { execute *accessory.remove_container } on(accessory.host) do
execute *MRSK.auditor.record("accessory #{name} remove container"), verbosity: :debug
execute *accessory.remove_container
end
end end
end end
desc "remove_image [NAME]", "Remove accessory image from host" desc "remove_image [NAME]", "Remove accessory image from host"
def remove_image(name) def remove_image(name)
with_accessory(name) do |accessory| with_accessory(name) do |accessory|
on(accessory.host) { execute *accessory.remove_image } on(accessory.host) do
execute *MRSK.auditor.record("accessory #{name} remove image"), verbosity: :debug
execute *accessory.remove_image
end
end end
end end
desc "remove_service_directory [NAME]", "Remove accessory directory used for uploaded files and data directories from host" desc "remove_service_directory [NAME]", "Remove accessory directory used for uploaded files and data directories from host"
def remove_service_directory(name) def remove_service_directory(name)
with_accessory(name) do |accessory| with_accessory(name) do |accessory|
on(accessory.host) { execute *accessory.remove_service_directory } on(accessory.host) do
execute *MRSK.auditor.record("accessory #{name} remove service directory"), verbosity: :debug
execute *accessory.remove_service_directory
end
end end
end end

View File

@@ -1,22 +1,26 @@
require "mrsk/cli/base"
class Mrsk::Cli::App < Mrsk::Cli::Base class Mrsk::Cli::App < Mrsk::Cli::Base
desc "boot", "Boot app on servers (or start them if they've already been booted)" desc "boot", "Boot app on servers (or reboot app if already running)"
def boot def boot
cli = self cli = self
say "Ensure no other version of the app is running...", :magenta
stop
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(options[:version] || most_recent_version_available) do |version|
say "Start container with version #{version} (or reboot if already running)...", :magenta
MRSK.config.roles.each do |role| MRSK.config.roles.each do |role|
on(role.hosts) do |host| on(role.hosts) do |host|
execute *MRSK.auditor.record("app boot version #{version}"), verbosity: :debug
begin begin
execute *MRSK.app.run(role: role.name) execute *MRSK.app.run(role: role.name)
rescue SSHKit::Command::Failed => e rescue SSHKit::Command::Failed => e
if e.message =~ /already in use/ if e.message =~ /already in use/
error "Rebooting container with same version already deployed on #{host}" error "Rebooting container with same version already deployed on #{host}"
cli.stop
cli.remove_container version cli.remove_container version
execute *MRSK.app.run(role: role.name) execute *MRSK.app.run(role: role.name)
else else
raise raise
@@ -27,78 +31,69 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
end end
end end
desc "reboot", "Reboot app on host (stop container, remove container, start new container with latest image)"
def reboot
old_version = current_running_version
stop
remove_container old_version
boot
end
desc "start", "Start existing app on servers (use --version=<git-hash> to designate specific version)" desc "start", "Start existing app on servers (use --version=<git-hash> to designate specific version)"
def start def start
on(MRSK.hosts) { execute *MRSK.app.start, raise_on_non_zero_exit: false } on(MRSK.hosts) do
execute *MRSK.auditor.record("app start version #{MRSK.version}"), verbosity: :debug
execute *MRSK.app.start, raise_on_non_zero_exit: false
end
end end
desc "stop", "Stop app on servers" desc "stop", "Stop app on servers"
def stop def stop
on(MRSK.hosts) { execute *MRSK.app.stop, raise_on_non_zero_exit: false } on(MRSK.hosts) do
execute *MRSK.auditor.record("app stop"), verbosity: :debug
execute *MRSK.app.stop, raise_on_non_zero_exit: false
end
end end
desc "details", "Display details about app containers" desc "details", "Display details about app containers"
def details def details
on(MRSK.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.info) } on(MRSK.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.info) }
end end
desc "exec [CMD]", "Execute a custom command on servers" desc "exec [CMD]", "Execute a custom command on servers"
option :method, aliases: "-m", default: "exec", desc: "Execution method: [exec] perform inside app container / [run] perform in new container / [ssh] perform over ssh" option :interactive, aliases: "-i", type: :boolean, default: false, desc: "Execute command over ssh for an interactive shell (use for console/bash)"
option :reuse, type: :boolean, default: false, desc: "Reuse currently running container instead of starting a new one"
def exec(cmd) def exec(cmd)
runner = \ case
case options[:method] when options[:interactive] && options[:reuse]
when "exec" then "exec" say "Get current version of running container...", :magenta unless options[:version]
when "run" then "run_exec" using_version(options[:version] || current_running_version) do |version|
when "ssh" then "exec_over_ssh" say "Launching interactive command with version #{version} via SSH from existing container on #{MRSK.primary_host}...", :magenta
else raise "Unknown method: #{options[:method]}" run_locally { exec MRSK.app.execute_in_existing_container_over_ssh(cmd, host: MRSK.primary_host) }
end.inquiry
if runner.exec_over_ssh?
run_locally do
info "Launching command on #{MRSK.primary_host}"
exec MRSK.app.exec_over_ssh(cmd, host: MRSK.primary_host)
end end
else
on(MRSK.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.send(runner, cmd)) }
end
end
desc "console", "Start Rails Console on primary host (or specific host set by --hosts)" when options[:interactive]
def console say "Get most recent version available as an image...", :magenta unless options[:version]
using_version(options[:version] || most_recent_version_available) do using_version(options[:version] || most_recent_version_available) do |version|
run_locally do say "Launching interactive command with version #{version} via SSH from new container on #{MRSK.primary_host}...", :magenta
if version run_locally { exec MRSK.app.execute_in_new_container_over_ssh(cmd, host: MRSK.primary_host) }
info "Launching Rails console on #{MRSK.primary_host} [Version: #{version}]" end
exec MRSK.app.console(host: MRSK.primary_host)
else when options[:reuse]
error "No image available for #{MRSK.config.repository}" say "Get current version of running container...", :magenta unless options[:version]
using_version(options[:version] || current_running_version) do |version|
say "Launching command with version #{version} from existing container...", :magenta
on(MRSK.hosts) do |host|
execute *MRSK.auditor.record("app cmd '#{cmd}' with version #{version}"), verbosity: :debug
puts_by_host host, capture_with_info(*MRSK.app.execute_in_existing_container(cmd))
end
end
else
say "Get most recent version available as an image...", :magenta unless options[:version]
using_version(options[:version] || most_recent_version_available) do |version|
say "Launching command with version #{version} from new container...", :magenta
on(MRSK.hosts) do |host|
execute *MRSK.auditor.record("app cmd '#{cmd}' with version #{version}"), verbosity: :debug
puts_by_host host, capture_with_info(*MRSK.app.execute_in_new_container(cmd))
end end
end end
end end
end end
desc "bash", "Start a bash session on primary host (or specific host set by --hosts)"
def bash
run_locally do
info "Launching bash session on #{MRSK.primary_host}"
exec MRSK.app.bash(host: MRSK.primary_host)
end
end
desc "runner [EXPRESSION]", "Execute Rails runner with given expression"
def runner(expression)
on(MRSK.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.exec("bin/rails", "runner", "'#{expression}'")) }
end
desc "containers", "List all the app containers currently on servers" desc "containers", "List all the app containers currently on servers"
def containers def containers
on(MRSK.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.list_containers) } on(MRSK.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.list_containers) }
@@ -113,7 +108,7 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
def current def current
on(MRSK.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.current_container_id) } on(MRSK.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.current_container_id) }
end end
desc "logs", "Show lines from app on servers" desc "logs", "Show lines from app on servers"
option :since, aliases: "-s", desc: "Show logs since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)" option :since, aliases: "-s", desc: "Show logs since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)"
option :lines, type: :numeric, aliases: "-n", desc: "Number of log lines to pull from each server" option :lines, type: :numeric, aliases: "-n", desc: "Number of log lines to pull from each server"
@@ -152,17 +147,31 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
desc "remove_container [VERSION]", "Remove app container with given version from servers" desc "remove_container [VERSION]", "Remove app container with given version from servers"
def remove_container(version) def remove_container(version)
on(MRSK.hosts) { execute *MRSK.app.remove_container(version: version) } on(MRSK.hosts) do
execute *MRSK.auditor.record("app remove container #{version}"), verbosity: :debug
execute *MRSK.app.remove_container(version: version)
end
end end
desc "remove_containers", "Remove all app containers from servers" desc "remove_containers", "Remove all app containers from servers"
def remove_containers def remove_containers
on(MRSK.hosts) { execute *MRSK.app.remove_containers } on(MRSK.hosts) do
execute *MRSK.auditor.record("app remove containers"), verbosity: :debug
execute *MRSK.app.remove_containers
end
end end
desc "remove_images", "Remove all app images from servers" desc "remove_images", "Remove all app images from servers"
def remove_images def remove_images
on(MRSK.hosts) { execute *MRSK.app.remove_images } on(MRSK.hosts) do
execute *MRSK.auditor.record("app remove images"), verbosity: :debug
execute *MRSK.app.remove_images
end
end
desc "current_version", "Shows the version currently running"
def current_version
on(MRSK.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.current_running_version).strip }
end end
private private

View File

@@ -1,5 +1,3 @@
require "mrsk/cli/base"
class Mrsk::Cli::Build < Mrsk::Cli::Base class Mrsk::Cli::Build < Mrsk::Cli::Base
desc "deliver", "Deliver a newly built app image to servers" desc "deliver", "Deliver a newly built app image to servers"
def deliver def deliver
@@ -11,7 +9,7 @@ class Mrsk::Cli::Build < Mrsk::Cli::Base
def push def push
cli = self cli = self
run_locally do run_locally do
begin begin
MRSK.with_verbosity(:debug) { execute *MRSK.builder.push } MRSK.with_verbosity(:debug) { execute *MRSK.builder.push }
rescue SSHKit::Command::Failed => e rescue SSHKit::Command::Failed => e
@@ -30,7 +28,10 @@ class Mrsk::Cli::Build < Mrsk::Cli::Base
desc "pull", "Pull app image from the registry onto servers" desc "pull", "Pull app image from the registry onto servers"
def pull def pull
on(MRSK.hosts) { execute *MRSK.builder.pull } on(MRSK.hosts) do
execute *MRSK.auditor.record("build pull image #{MRSK.version}"), verbosity: :debug
execute *MRSK.builder.pull
end
end end
desc "create", "Create a local build setup" desc "create", "Create a local build setup"

View File

@@ -1,13 +1,3 @@
require "mrsk/cli/base"
require "mrsk/cli/accessory"
require "mrsk/cli/app"
require "mrsk/cli/build"
require "mrsk/cli/prune"
require "mrsk/cli/registry"
require "mrsk/cli/server"
require "mrsk/cli/traefik"
class Mrsk::Cli::Main < Mrsk::Cli::Base class Mrsk::Cli::Main < Mrsk::Cli::Base
desc "setup", "Setup all accessories and deploy the app to servers" desc "setup", "Setup all accessories and deploy the app to servers"
def setup def setup
@@ -21,12 +11,21 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
desc "deploy", "Deploy the app to servers" desc "deploy", "Deploy the app to servers"
def deploy def deploy
print_runtime do print_runtime do
say "Ensure Docker is installed...", :magenta
invoke "mrsk:cli:server:bootstrap" invoke "mrsk:cli:server:bootstrap"
say "Log into image registry...", :magenta
invoke "mrsk:cli:registry:login" invoke "mrsk:cli:registry:login"
say "Build and push app image...", :magenta
invoke "mrsk:cli:build:deliver" invoke "mrsk:cli:build:deliver"
say "Ensure Traefik is running...", :magenta
invoke "mrsk:cli:traefik:boot" invoke "mrsk:cli:traefik:boot"
invoke "mrsk:cli:app:stop"
invoke "mrsk:cli:app:boot" invoke "mrsk:cli:app:boot"
say "Prune old containers and images...", :magenta
invoke "mrsk:cli:prune:all" invoke "mrsk:cli:prune:all"
end end
end end
@@ -34,17 +33,23 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
desc "redeploy", "Deploy new version of the app to servers (without bootstrapping servers, starting Traefik, pruning, and registry login)" desc "redeploy", "Deploy new version of the app to servers (without bootstrapping servers, starting Traefik, pruning, and registry login)"
def redeploy def redeploy
print_runtime do print_runtime do
say "Build and push app image...", :magenta
invoke "mrsk:cli:build:deliver" invoke "mrsk:cli:build:deliver"
invoke "mrsk:cli:app:stop"
invoke "mrsk:cli:app:boot" invoke "mrsk:cli:app:boot"
end end
end end
desc "rollback [VERSION]", "Rollback the app to VERSION (that must already be on servers)" desc "rollback [VERSION]", "Rollback the app to VERSION"
def rollback(version) def rollback(version)
MRSK.version = version
cli = self
cli.say "Stop current version, then start version #{version}...", :magenta
on(MRSK.hosts) do on(MRSK.hosts) do
execute *MRSK.app.stop, raise_on_non_zero_exit: false execute *MRSK.app.stop, raise_on_non_zero_exit: false
execute *MRSK.app.start(version: version) execute *MRSK.app.start
end end
end end
@@ -55,6 +60,13 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
invoke "mrsk:cli:accessory:details", [ "all" ] invoke "mrsk:cli:accessory:details", [ "all" ]
end end
desc "audit", "Show audit log from servers"
def audit
on(MRSK.hosts) do |host|
puts_by_host host, capture_with_info(*MRSK.auditor.reveal)
end
end
desc "config", "Show combined config" desc "config", "Show combined config"
def config def config
run_locally do run_locally do

View File

@@ -1,5 +1,3 @@
require "mrsk/cli/base"
class Mrsk::Cli::Prune < Mrsk::Cli::Base class Mrsk::Cli::Prune < Mrsk::Cli::Base
desc "all", "Prune unused images and stopped containers" desc "all", "Prune unused images and stopped containers"
def all def all
@@ -9,11 +7,17 @@ class Mrsk::Cli::Prune < Mrsk::Cli::Base
desc "images", "Prune unused images older than 30 days" desc "images", "Prune unused images older than 30 days"
def images def images
on(MRSK.hosts) { execute *MRSK.prune.images } on(MRSK.hosts) do
execute *MRSK.auditor.record("prune images"), verbosity: :debug
execute *MRSK.prune.images
end
end end
desc "containers", "Prune stopped containers for the service older than 3 days" desc "containers", "Prune stopped containers for the service older than 3 days"
def containers def containers
on(MRSK.hosts) { execute *MRSK.prune.containers } on(MRSK.hosts) do
execute *MRSK.auditor.record("prune containers"), verbosity: :debug
execute *MRSK.prune.containers
end
end end
end end

View File

@@ -1,9 +1,7 @@
require "mrsk/cli/base"
class Mrsk::Cli::Registry < Mrsk::Cli::Base class Mrsk::Cli::Registry < Mrsk::Cli::Base
desc "login", "Login to the registry locally and remotely" desc "login", "Login to the registry locally and remotely"
def login def login
run_locally { execute *MRSK.registry.login } run_locally { execute *MRSK.registry.login }
on(MRSK.hosts) { execute *MRSK.registry.login } on(MRSK.hosts) { execute *MRSK.registry.login }
rescue ArgumentError => e rescue ArgumentError => e
puts e.message puts e.message

View File

@@ -1,5 +1,3 @@
require "mrsk/cli/base"
class Mrsk::Cli::Server < Mrsk::Cli::Base class Mrsk::Cli::Server < Mrsk::Cli::Base
desc "bootstrap", "Ensure Docker is installed on the servers" desc "bootstrap", "Ensure Docker is installed on the servers"
def bootstrap def bootstrap

View File

@@ -1,5 +1,3 @@
require "mrsk/cli/base"
class Mrsk::Cli::Traefik < Mrsk::Cli::Base class Mrsk::Cli::Traefik < Mrsk::Cli::Base
desc "boot", "Boot Traefik on servers" desc "boot", "Boot Traefik on servers"
def boot def boot
@@ -15,12 +13,18 @@ class Mrsk::Cli::Traefik < Mrsk::Cli::Base
desc "start", "Start existing Traefik on servers" desc "start", "Start existing Traefik on servers"
def start def start
on(MRSK.traefik_hosts) { execute *MRSK.traefik.start, raise_on_non_zero_exit: false } on(MRSK.traefik_hosts) do
execute *MRSK.auditor.record("traefik start"), verbosity: :debug
execute *MRSK.traefik.start, raise_on_non_zero_exit: false
end
end end
desc "stop", "Stop Traefik on servers" desc "stop", "Stop Traefik on servers"
def stop def stop
on(MRSK.traefik_hosts) { execute *MRSK.traefik.stop, raise_on_non_zero_exit: false } on(MRSK.traefik_hosts) do
execute *MRSK.auditor.record("traefik stop"), verbosity: :debug
execute *MRSK.traefik.stop, raise_on_non_zero_exit: false
end
end end
desc "restart", "Restart Traefik on servers" desc "restart", "Restart Traefik on servers"
@@ -67,11 +71,17 @@ class Mrsk::Cli::Traefik < Mrsk::Cli::Base
desc "remove_container", "Remove Traefik container from servers" desc "remove_container", "Remove Traefik container from servers"
def remove_container def remove_container
on(MRSK.traefik_hosts) { execute *MRSK.traefik.remove_container } on(MRSK.traefik_hosts) do
execute *MRSK.auditor.record("traefik remove container"), verbosity: :debug
execute *MRSK.traefik.remove_container
end
end end
desc "remove_container", "Remove Traefik image from servers" desc "remove_container", "Remove Traefik image from servers"
def remove_image def remove_image
on(MRSK.traefik_hosts) { execute *MRSK.traefik.remove_image } on(MRSK.traefik_hosts) do
execute *MRSK.auditor.record("traefik remove image"), verbosity: :debug
execute *MRSK.traefik.remove_image
end
end end
end end

View File

@@ -1,13 +1,5 @@
require "active_support/core_ext/enumerable" require "active_support/core_ext/enumerable"
require "mrsk/configuration"
require "mrsk/commands/accessory"
require "mrsk/commands/app"
require "mrsk/commands/builder"
require "mrsk/commands/prune"
require "mrsk/commands/traefik"
require "mrsk/commands/registry"
class Mrsk::Commander class Mrsk::Commander
attr_accessor :config_file, :destination, :verbosity, :version attr_accessor :config_file, :destination, :verbosity, :version
@@ -77,8 +69,12 @@ class Mrsk::Commander
Mrsk::Commands::Accessory.new(config, name: name) Mrsk::Commands::Accessory.new(config, name: name)
end end
def auditor
@auditor ||= Mrsk::Commands::Auditor.new(config)
end
def with_verbosity(level)
def with_verbosity(level)
old_level = SSHKit.config.output_verbosity old_level = SSHKit.config.output_verbosity
SSHKit.config.output_verbosity = level SSHKit.config.output_verbosity = level
yield yield
@@ -89,7 +85,7 @@ class Mrsk::Commander
# Test-induced damage! # Test-induced damage!
def reset def reset
@config = @config_file = @destination = @version = nil @config = @config_file = @destination = @version = nil
@app = @builder = @traefik = @registry = @prune = nil @app = @builder = @traefik = @registry = @prune = @auditor = nil
@verbosity = :info @verbosity = :info
end end

View File

@@ -1,5 +1,3 @@
require "mrsk/commands/base"
class Mrsk::Commands::Accessory < Mrsk::Commands::Base class Mrsk::Commands::Accessory < Mrsk::Commands::Base
attr_reader :accessory_config attr_reader :accessory_config
delegate :service_name, :image, :host, :port, :files, :directories, :env_args, :volume_args, :label_args, to: :accessory_config delegate :service_name, :image, :host, :port, :files, :directories, :env_args, :volume_args, :label_args, to: :accessory_config
@@ -10,7 +8,7 @@ class Mrsk::Commands::Accessory < Mrsk::Commands::Base
end end
def run def run
docker :run, docker :run,
"--name", service_name, "--name", service_name,
"-d", "-d",
"--restart", "unless-stopped", "--restart", "unless-stopped",
@@ -33,6 +31,7 @@ class Mrsk::Commands::Accessory < Mrsk::Commands::Base
docker :ps, *service_filter docker :ps, *service_filter
end end
def logs(since: nil, lines: nil, grep: nil) def logs(since: nil, lines: nil, grep: nil)
pipe \ pipe \
docker(:logs, service_name, (" --since #{since}" if since), (" -n #{lines}" if lines), "-t", "2>&1"), docker(:logs, service_name, (" --since #{since}" if since), (" -n #{lines}" if lines), "-t", "2>&1"),
@@ -46,14 +45,15 @@ class Mrsk::Commands::Accessory < Mrsk::Commands::Base
).join(" ") ).join(" ")
end end
def exec(*command, interactive: false)
def execute_in_existing_container(*command, interactive: false)
docker :exec, docker :exec,
("-it" if interactive), ("-it" if interactive),
service_name, service_name,
*command *command
end end
def run_exec(*command, interactive: false) def execute_in_new_container(*command, interactive: false)
docker :run, docker :run,
("-it" if interactive), ("-it" if interactive),
"--rm", "--rm",
@@ -63,17 +63,18 @@ class Mrsk::Commands::Accessory < Mrsk::Commands::Base
*command *command
end end
def execute_in_existing_container_over_ssh(*command)
run_over_ssh execute_in_existing_container(*command, interactive: true).join(" ")
end
def execute_in_new_container_over_ssh(*command)
run_over_ssh execute_in_new_container(*command, interactive: true).join(" ")
end
def run_over_ssh(command) def run_over_ssh(command)
super command, host: host super command, host: host
end end
def exec_over_ssh(*command)
run_over_ssh run_exec(*command, interactive: true).join(" ")
end
def bash
exec_over_ssh "bash"
end
def ensure_local_file_present(local_file) def ensure_local_file_present(local_file)
if !local_file.is_a?(StringIO) && !Pathname.new(local_file).exist? if !local_file.is_a?(StringIO) && !Pathname.new(local_file).exist?

View File

@@ -1,9 +1,4 @@
require "mrsk/commands/base"
require "mrsk/commands/concerns/repository"
class Mrsk::Commands::App < Mrsk::Commands::Base class Mrsk::Commands::App < Mrsk::Commands::Base
include Mrsk::Commands::Concerns::Repository
def run(role: :web) def run(role: :web)
role = config.role(role) role = config.role(role)
@@ -23,18 +18,15 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
docker :start, service_with_version docker :start, service_with_version
end end
def current_container_id
docker :ps, "-q", *service_filter
end
def stop def stop
pipe current_container_id, "xargs docker stop" pipe current_container_id, xargs(docker(:stop))
end end
def info def info
docker :ps, *service_filter docker :ps, *service_filter
end end
def logs(since: nil, lines: nil, grep: nil) def logs(since: nil, lines: nil, grep: nil)
pipe \ pipe \
current_container_id, current_container_id,
@@ -42,28 +34,6 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
("grep '#{grep}'" if grep) ("grep '#{grep}'" if grep)
end end
def exec(*command, interactive: false)
docker :exec,
("-it" if interactive),
config.service_with_version,
*command
end
def run_exec(*command, interactive: false)
docker :run,
("-it" if interactive),
"--rm",
*rails_master_key_arg,
*config.env_args,
*config.volume_args,
config.absolute_image,
*command
end
def exec_over_ssh(*command, host:)
run_over_ssh run_exec(*command, interactive: true).join(" "), host: host
end
def follow_logs(host:, grep: nil) def follow_logs(host:, grep: nil)
run_over_ssh pipe( run_over_ssh pipe(
current_container_id, current_container_id,
@@ -72,14 +42,57 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
).join(" "), host: host ).join(" "), host: host
end end
def console(host:)
exec_over_ssh "bin/rails", "c", host: host def execute_in_existing_container(*command, interactive: false)
docker :exec,
("-it" if interactive),
config.service_with_version,
*command
end end
def bash(host:) def execute_in_new_container(*command, interactive: false)
exec_over_ssh "bash", host: host docker :run,
("-it" if interactive),
"--rm",
*rails_master_key_arg,
*config.env_args,
*config.volume_args,
config.absolute_image,
*command
end end
def execute_in_existing_container_over_ssh(*command, host:)
run_over_ssh execute_in_existing_container(*command, interactive: true).join(" "), host: host
end
def execute_in_new_container_over_ssh(*command, host:)
run_over_ssh execute_in_new_container(*command, interactive: true).join(" "), host: host
end
def current_container_id
docker :ps, "-q", *service_filter
end
def container_id_for(container_name:)
docker :container, :ls, "-a", "-f", "name=#{container_name}", "-q"
end
def current_running_version
# FIXME: Find more graceful way to extract the version from "app-version" than using sed and tail!
pipe \
docker(:ps, "--filter", "label=service=#{config.service}", "--format", '"{{.Names}}"'),
%(sed 's/-/\\n/g'),
"tail -n 1"
end
def most_recent_version_from_available_images
pipe \
docker(:image, :ls, "--format", '"{{.Tag}}"', config.repository),
"head -n 1"
end
def list_containers def list_containers
docker :container, :ls, "-a", *service_filter docker :container, :ls, "-a", *service_filter
end end
@@ -87,7 +100,7 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
def remove_container(version:) def remove_container(version:)
pipe \ pipe \
container_id_for(container_name: service_with_version(version)), container_id_for(container_name: service_with_version(version)),
docker(:container, :rm) xargs(docker(:container, :rm))
end end
def remove_containers def remove_containers
@@ -102,6 +115,7 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
docker :image, :prune, "-a", "-f", *service_filter docker :image, :prune, "-a", "-f", *service_filter
end end
private private
def service_with_version(version = nil) def service_with_version(version = nil)
if version if version

View File

@@ -0,0 +1,34 @@
require "active_support/core_ext/time/conversions"
class Mrsk::Commands::Auditor < Mrsk::Commands::Base
def record(line)
append \
[ :echo, tagged_line(line) ],
audit_log_file
end
def reveal
[ :tail, "-n", 50, audit_log_file ]
end
private
def audit_log_file
"mrsk-#{config.service}-audit.log"
end
def tagged_line(line)
"'#{tags} #{line}'"
end
def tags
"[#{timestamp}] [#{performer}]"
end
def performer
`whoami`.strip
end
def timestamp
Time.now.to_fs(:db)
end
end

View File

@@ -34,6 +34,14 @@ module Mrsk::Commands
combine *commands, by: "|" combine *commands, by: "|"
end end
def append(*commands)
combine *commands, by: ">>"
end
def xargs(command)
[ :xargs, command ].flatten
end
def docker(*args) def docker(*args)
args.compact.unshift :docker args.compact.unshift :docker
end end

View File

@@ -1,5 +1,3 @@
require "mrsk/commands/base"
class Mrsk::Commands::Builder < Mrsk::Commands::Base class Mrsk::Commands::Builder < Mrsk::Commands::Base
delegate :create, :remove, :push, :pull, :info, to: :target delegate :create, :remove, :push, :pull, :info, to: :target
@@ -36,8 +34,3 @@ class Mrsk::Commands::Builder < Mrsk::Commands::Base
@multiarch_remote ||= Mrsk::Commands::Builder::Multiarch::Remote.new(config) @multiarch_remote ||= Mrsk::Commands::Builder::Multiarch::Remote.new(config)
end end
end end
require "mrsk/commands/builder/native"
require "mrsk/commands/builder/native/remote"
require "mrsk/commands/builder/multiarch"
require "mrsk/commands/builder/multiarch/remote"

View File

@@ -1,5 +1,3 @@
require "mrsk/commands/base"
class Mrsk::Commands::Builder::Base < Mrsk::Commands::Base class Mrsk::Commands::Builder::Base < Mrsk::Commands::Base
delegate :argumentize, to: Mrsk::Utils delegate :argumentize, to: Mrsk::Utils

View File

@@ -1,5 +1,3 @@
require "mrsk/commands/builder/base"
class Mrsk::Commands::Builder::Multiarch < Mrsk::Commands::Builder::Base class Mrsk::Commands::Builder::Multiarch < Mrsk::Commands::Builder::Base
def create def create
docker :buildx, :create, "--use", "--name", builder_name docker :buildx, :create, "--use", "--name", builder_name

View File

@@ -1,5 +1,3 @@
require "mrsk/commands/builder/multiarch"
class Mrsk::Commands::Builder::Multiarch::Remote < Mrsk::Commands::Builder::Multiarch class Mrsk::Commands::Builder::Multiarch::Remote < Mrsk::Commands::Builder::Multiarch
def create def create
combine \ combine \

View File

@@ -1,5 +1,3 @@
require "mrsk/commands/builder/base"
class Mrsk::Commands::Builder::Native < Mrsk::Commands::Builder::Base class Mrsk::Commands::Builder::Native < Mrsk::Commands::Builder::Base
def create def create
# No-op on native # No-op on native

View File

@@ -1,5 +1,3 @@
require "mrsk/commands/builder/native"
class Mrsk::Commands::Builder::Native::Remote < Mrsk::Commands::Builder::Native class Mrsk::Commands::Builder::Native::Remote < Mrsk::Commands::Builder::Native
def create def create
chain \ chain \

View File

@@ -1,21 +0,0 @@
module Mrsk::Commands::Concerns
module Repository
def container_id_for(container_name:)
docker :container, :ls, "-a", "-f", "name=#{container_name}", "-q"
end
def current_running_version
# FIXME: Find more graceful way to extract the version from "app-version" than using sed and tail!
pipe \
docker(:ps, "--filter", "label=service=#{config.service}", "--format", '"{{.Names}}"'),
"sed 's/-/\n/g'",
"tail -n 1"
end
def most_recent_version_from_available_images
pipe \
docker(:image, :ls, "--format", '"{{.Tag}}"', config.repository),
"head -n 1"
end
end
end

View File

@@ -1,4 +1,3 @@
require "mrsk/commands/base"
require "active_support/duration" require "active_support/duration"
require "active_support/core_ext/numeric/time" require "active_support/core_ext/numeric/time"

View File

@@ -1,5 +1,3 @@
require "mrsk/commands/base"
class Mrsk::Commands::Registry < Mrsk::Commands::Base class Mrsk::Commands::Registry < Mrsk::Commands::Base
delegate :registry, to: :config delegate :registry, to: :config

View File

@@ -1,5 +1,3 @@
require "mrsk/commands/base"
class Mrsk::Commands::Traefik < Mrsk::Commands::Base class Mrsk::Commands::Traefik < Mrsk::Commands::Base
def run def run
docker :run, "--name traefik", docker :run, "--name traefik",

View File

@@ -1,9 +1,3 @@
require "active_support/ordered_options"
require "active_support/core_ext/string/inquiry"
require "active_support/core_ext/module/delegation"
require "pathname"
require "erb"
require "mrsk/utils"
require "net/ssh/proxy/jump" require "net/ssh/proxy/jump"
class Mrsk::Configuration class Mrsk::Configuration
@@ -183,6 +177,3 @@ class Mrsk::Configuration
raw_config.servers.is_a?(Array) ? [ "web" ] : raw_config.servers.keys.sort raw_config.servers.is_a?(Array) ? [ "web" ] : raw_config.servers.keys.sort
end end
end end
require "mrsk/configuration/role"
require "mrsk/configuration/accessory"

View File

@@ -1,3 +1,3 @@
module Mrsk module Mrsk
VERSION = "0.4.0" VERSION = "0.5.1"
end end

View File

@@ -16,4 +16,5 @@ Gem::Specification.new do |spec|
spec.add_dependency "sshkit", "~> 1.21" spec.add_dependency "sshkit", "~> 1.21"
spec.add_dependency "thor", "~> 1.2" spec.add_dependency "thor", "~> 1.2"
spec.add_dependency "dotenv", "~> 2.8" spec.add_dependency "dotenv", "~> 2.8"
spec.add_dependency "zeitwerk", "~> 2.5"
end end

View File

@@ -17,6 +17,20 @@ class CliAccessoryTest < CliTestCase
assert_match "Running docker run --name app-mysql -d --restart unless-stopped -p 3306:3306 -e [REDACTED] -e MYSQL_ROOT_HOST=% --volume $PWD/app-mysql/etc/mysql/my.cnf:/etc/mysql/my.cnf --volume $PWD/app-mysql/data:/var/lib/mysql --label service=app-mysql mysql:5.7 on 1.1.1.3", run_command("boot", "mysql") assert_match "Running docker run --name app-mysql -d --restart unless-stopped -p 3306:3306 -e [REDACTED] -e MYSQL_ROOT_HOST=% --volume $PWD/app-mysql/etc/mysql/my.cnf:/etc/mysql/my.cnf --volume $PWD/app-mysql/data:/var/lib/mysql --label service=app-mysql mysql:5.7 on 1.1.1.3", run_command("boot", "mysql")
end end
test "exec" do
run_command("exec", "mysql", "mysql -v").tap do |output|
assert_match /Launching command from new container/, output
assert_match /mysql -v/, output
end
end
test "exec with reuse" do
run_command("exec", "mysql", "--reuse", "mysql -v").tap do |output|
assert_match /Launching command from existing container/, output
assert_match %r[docker exec app-mysql mysql -v], output
end
end
private private
def run_command(*command) def run_command(*command)
stdouted { Mrsk::Cli::Accessory.start([*command, "-c", "test/fixtures/deploy_with_accessories.yml"]) } stdouted { Mrsk::Cli::Accessory.start([*command, "-c", "test/fixtures/deploy_with_accessories.yml"]) }

View File

@@ -15,32 +15,48 @@ class CliAppTest < CliTestCase
run_command("boot").tap do |output| run_command("boot").tap do |output|
assert_match /Rebooting container with same version already deployed/, output # Can't start what's already running assert_match /Rebooting container with same version already deployed/, output # Can't start what's already running
assert_match /docker ps -q --filter label=service=app | xargs docker stop/, output # Stop what's running assert_match /docker ps -q --filter label=service=app \| xargs docker stop/, output # Stop what's running
assert_match /docker container ls -a -f name=app-999 -q | docker container rm/, output # Remove old container assert_match /docker container ls -a -f name=app-999 -q \| xargs docker container rm/, output # Remove old container
assert_match /docker run/, output # Start new container assert_match /docker run/, output # Start new container
end end
ensure ensure
Thread.report_on_exception = true Thread.report_on_exception = true
end end
test "reboot to default version" do test "start" do
run_command("reboot").tap do |output| run_command("start").tap do |output|
assert_match /docker ps --filter label=service=app/, output # Find current container assert_match /docker start app-999/, output
assert_match /docker stop/, output # Stop old container
assert_match /docker container rm/, output # Remove old container
assert_match /docker run -d --restart unless-stopped .* dhh\/app:999/, output # Start new container
end end
end end
test "reboot to specific version" do test "stop" do
run_command("reboot", "--version", "456").tap do |output| run_command("stop").tap do |output|
assert_match /docker run -d --restart unless-stopped .* dhh\/app:456/, output assert_match /docker ps -q --filter label=service=app \| xargs docker stop/, output
end
end
test "details" do
run_command("details").tap do |output|
assert_match /docker ps --filter label=service=app/, output
end end
end end
test "remove_container" do test "remove_container" do
run_command("remove_container", "1234567").tap do |output| run_command("remove_container", "1234567").tap do |output|
assert_match /docker container ls -a -f name=app-1234567 -q | docker container rm/, output assert_match /docker container ls -a -f name=app-1234567 -q \| xargs docker container rm/, output
end
end
test "exec" do
run_command("exec", "ruby -v").tap do |output|
assert_match /ruby -v/, output
end
end
test "exec with reuse" do
run_command("exec", "--reuse", "ruby -v").tap do |output|
assert_match %r[docker ps --filter label=service=app --format \"{{.Names}}\" | sed 's/-/\\n/g' | tail -n 1], output # Get current version
assert_match %r[docker exec app-999 ruby -v], output
end end
end end

View File

@@ -1,6 +1,5 @@
require "test_helper" require "test_helper"
require "active_support/testing/stream" require "active_support/testing/stream"
require "mrsk/cli"
class CliTestCase < ActiveSupport::TestCase class CliTestCase < ActiveSupport::TestCase
include ActiveSupport::Testing::Stream include ActiveSupport::Testing::Stream

View File

@@ -1,5 +1,4 @@
require "test_helper" require "test_helper"
require "mrsk/commander"
class CommanderTest < ActiveSupport::TestCase class CommanderTest < ActiveSupport::TestCase
setup do setup do

View File

@@ -1,10 +1,8 @@
require "test_helper" require "test_helper"
require "mrsk/configuration"
require "mrsk/commands/accessory"
class CommandsAccessoryTest < ActiveSupport::TestCase class CommandsAccessoryTest < ActiveSupport::TestCase
setup do setup do
@config = { @config = {
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" },
servers: [ "1.1.1.1" ], servers: [ "1.1.1.1" ],
accessories: { accessories: {
@@ -41,18 +39,20 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
@config = Mrsk::Configuration.new(@config) @config = Mrsk::Configuration.new(@config)
@mysql = Mrsk::Commands::Accessory.new(@config, name: :mysql) @mysql = Mrsk::Commands::Accessory.new(@config, name: :mysql)
@redis = Mrsk::Commands::Accessory.new(@config, name: :redis) @redis = Mrsk::Commands::Accessory.new(@config, name: :redis)
ENV["MYSQL_ROOT_PASSWORD"] = "secret123"
end
teardown do
ENV.delete("MYSQL_ROOT_PASSWORD")
end end
test "run" do test "run" do
ENV["MYSQL_ROOT_PASSWORD"] = "secret123"
assert_equal \ assert_equal \
[:docker, :run, "--name", "app-mysql", "-d", "--restart", "unless-stopped", "-p", "3306:3306", "-e", "MYSQL_ROOT_PASSWORD=secret123", "-e", "MYSQL_ROOT_HOST=%", "--label", "service=app-mysql", "mysql:8.0"], @mysql.run [:docker, :run, "--name", "app-mysql", "-d", "--restart", "unless-stopped", "-p", "3306:3306", "-e", "MYSQL_ROOT_PASSWORD=secret123", "-e", "MYSQL_ROOT_HOST=%", "--label", "service=app-mysql", "mysql:8.0"], @mysql.run
assert_equal \ assert_equal \
[:docker, :run, "--name", "app-redis", "-d", "--restart", "unless-stopped", "-p", "6379:6379", "-e", "SOMETHING=else", "--volume", "/var/lib/redis:/data", "--label", "service=app-redis", "--label", "cache=true", "redis:latest"], @redis.run [:docker, :run, "--name", "app-redis", "-d", "--restart", "unless-stopped", "-p", "6379:6379", "-e", "SOMETHING=else", "--volume", "/var/lib/redis:/data", "--label", "service=app-redis", "--label", "cache=true", "redis:latest"], @redis.run
ensure
ENV["MYSQL_ROOT_PASSWORD"] = nil
end end
test "start" do test "start" do
@@ -67,6 +67,35 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
assert_equal [:docker, :ps, "--filter", "label=service=app-mysql"], @mysql.info assert_equal [:docker, :ps, "--filter", "label=service=app-mysql"], @mysql.info
end end
test "execute in new container" do
assert_equal \
[ :docker, :run, "--rm", "-e", "MYSQL_ROOT_PASSWORD=secret123", "-e", "MYSQL_ROOT_HOST=%", "mysql:8.0", "mysql", "-u", "root" ],
@mysql.execute_in_new_container("mysql", "-u", "root")
end
test "execute in existing container" do
assert_equal \
[ :docker, :exec, "app-mysql", "mysql", "-u", "root" ],
@mysql.execute_in_existing_container("mysql", "-u", "root")
end
test "execute in new container over ssh" do
@mysql.stub(:run_over_ssh, ->(cmd) { cmd }) do
assert_match %r|docker run -it --rm -e MYSQL_ROOT_PASSWORD=secret123 -e MYSQL_ROOT_HOST=% mysql:8.0 mysql -u root|,
@mysql.execute_in_new_container_over_ssh("mysql", "-u", "root")
end
end
test "execute in existing container over ssh" do
@mysql.stub(:run_over_ssh, ->(cmd) { cmd }) do
assert_match %r|docker exec -it app-mysql mysql -u root|,
@mysql.execute_in_existing_container_over_ssh("mysql", "-u", "root")
end
end
test "logs" do test "logs" do
assert_equal [:docker, :logs, "app-mysql", "-t", "2>&1"], @mysql.logs assert_equal [:docker, :logs, "app-mysql", "-t", "2>&1"], @mysql.logs
assert_equal [:docker, :logs, "app-mysql", " --since 5m", " -n 100", "-t", "2>&1", "|", "grep 'thing'"], @mysql.logs(since: "5m", lines: 100, grep: "thing") assert_equal [:docker, :logs, "app-mysql", " --since 5m", " -n 100", "-t", "2>&1", "|", "grep 'thing'"], @mysql.logs(since: "5m", lines: 100, grep: "thing")

View File

@@ -1,6 +1,4 @@
require "test_helper" require "test_helper"
require "mrsk/configuration"
require "mrsk/commands/app"
class CommandsAppTest < ActiveSupport::TestCase class CommandsAppTest < ActiveSupport::TestCase
setup do setup do
@@ -26,12 +24,34 @@ class CommandsAppTest < ActiveSupport::TestCase
[:docker, :run, "-d", "--restart unless-stopped", "--name", "app-missing", "-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=3", "--label", "traefik.http.middlewares.app.retry.initialinterval=500ms", "dhh/app:missing"], @app.run [:docker, :run, "-d", "--restart unless-stopped", "--name", "app-missing", "-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=3", "--label", "traefik.http.middlewares.app.retry.initialinterval=500ms", "dhh/app:missing"], @app.run
end end
test "run with" do
test "execute in new container" do
assert_equal \ assert_equal \
[ :docker, :run, "--rm", "-e", "RAILS_MASTER_KEY=456", "dhh/app:missing", "bin/rails", "db:setup" ], [ :docker, :run, "--rm", "-e", "RAILS_MASTER_KEY=456", "dhh/app:missing", "bin/rails", "db:setup" ],
@app.run_exec("bin/rails", "db:setup") @app.execute_in_new_container("bin/rails", "db:setup")
end end
test "execute in existing container" do
assert_equal \
[ :docker, :exec, "app-missing", "bin/rails", "db:setup" ],
@app.execute_in_existing_container("bin/rails", "db:setup")
end
test "execute in new container over ssh" do
@app.stub(:run_over_ssh, ->(cmd, host:) { cmd }) do
assert_match %r|docker run -it --rm -e RAILS_MASTER_KEY=456 dhh/app:missing bin/rails c|,
@app.execute_in_new_container_over_ssh("bin/rails", "c", host: "app-1")
end
end
test "execute in existing container over ssh" do
@app.stub(:run_over_ssh, ->(cmd, host:) { cmd }) do
assert_match %r|docker exec -it app-missing bin/rails c|,
@app.execute_in_existing_container_over_ssh("bin/rails", "c", host: "app-1")
end
end
test "run without master key" do test "run without master key" do
ENV["RAILS_MASTER_KEY"] = nil ENV["RAILS_MASTER_KEY"] = nil
@app = Mrsk::Commands::App.new Mrsk::Configuration.new(@config.tap { |c| c[:skip_master_key] = true }) @app = Mrsk::Commands::App.new Mrsk::Configuration.new(@config.tap { |c| c[:skip_master_key] = true })

View File

@@ -1,6 +1,4 @@
require "test_helper" require "test_helper"
require "mrsk/configuration"
require "mrsk/commands/builder"
class CommandsBuilderTest < ActiveSupport::TestCase class CommandsBuilderTest < ActiveSupport::TestCase
setup do setup do

View File

@@ -1,10 +1,8 @@
require "test_helper" require "test_helper"
require "mrsk/configuration"
require "mrsk/commands/registry"
class CommandsRegistryTest < ActiveSupport::TestCase class CommandsRegistryTest < ActiveSupport::TestCase
setup do setup do
@config = { service: "app", @config = { service: "app",
image: "dhh/app", image: "dhh/app",
registry: { "username" => "dhh", registry: { "username" => "dhh",
"password" => "secret", "password" => "secret",

View File

@@ -1,6 +1,4 @@
require "test_helper" require "test_helper"
require "mrsk/configuration"
require "mrsk/commands/traefik"
class CommandsTraefikTest < ActiveSupport::TestCase class CommandsTraefikTest < ActiveSupport::TestCase
setup do setup do

View File

@@ -1,5 +1,4 @@
require "test_helper" require "test_helper"
require "mrsk/configuration"
class ConfigurationAccessoryTest < ActiveSupport::TestCase class ConfigurationAccessoryTest < ActiveSupport::TestCase
setup do setup do
@@ -66,7 +65,7 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase
test "missing host" do test "missing host" do
@deploy[:accessories]["mysql"]["host"] = nil @deploy[:accessories]["mysql"]["host"] = nil
@config = Mrsk::Configuration.new(@deploy) @config = Mrsk::Configuration.new(@deploy)
assert_raises(ArgumentError) do assert_raises(ArgumentError) do
@config.accessory(:mysql).host @config.accessory(:mysql).host
end end

View File

@@ -1,5 +1,4 @@
require "test_helper" require "test_helper"
require "mrsk/configuration"
class ConfigurationRoleTest < ActiveSupport::TestCase class ConfigurationRoleTest < ActiveSupport::TestCase
setup do setup do
@@ -63,7 +62,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
end end
test "default traefik label on non-web role" do test "default traefik label on non-web role" do
config = Mrsk::Configuration.new(@deploy_with_roles.tap { |c| config = Mrsk::Configuration.new(@deploy_with_roles.tap { |c|
c[:servers]["beta"] = { "traefik" => "true", "hosts" => [ "1.1.1.5" ] } c[:servers]["beta"] = { "traefik" => "true", "hosts" => [ "1.1.1.5" ] }
}) })
@@ -97,7 +96,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
ENV["REDIS_PASSWORD"] = "secret456" ENV["REDIS_PASSWORD"] = "secret456"
ENV["DB_PASSWORD"] = "secret123" ENV["DB_PASSWORD"] = "secret123"
assert_equal ["-e", "REDIS_PASSWORD=secret456", "-e", "DB_PASSWORD=secret123", "-e", "REDIS_URL=redis://a/b", "-e", "WEB_CONCURRENCY=4"], @config_with_roles.role(:workers).env_args assert_equal ["-e", "REDIS_PASSWORD=secret456", "-e", "DB_PASSWORD=secret123", "-e", "REDIS_URL=redis://a/b", "-e", "WEB_CONCURRENCY=4"], @config_with_roles.role(:workers).env_args
ensure ensure
ENV["REDIS_PASSWORD"] = nil ENV["REDIS_PASSWORD"] = nil
@@ -116,7 +115,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
} }
ENV["DB_PASSWORD"] = "secret123" ENV["DB_PASSWORD"] = "secret123"
assert_equal ["-e", "DB_PASSWORD=secret123", "-e", "REDIS_URL=redis://a/b", "-e", "WEB_CONCURRENCY=4"], @config_with_roles.role(:workers).env_args assert_equal ["-e", "DB_PASSWORD=secret123", "-e", "REDIS_URL=redis://a/b", "-e", "WEB_CONCURRENCY=4"], @config_with_roles.role(:workers).env_args
ensure ensure
ENV["DB_PASSWORD"] = nil ENV["DB_PASSWORD"] = nil
@@ -133,7 +132,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
} }
ENV["REDIS_PASSWORD"] = "secret456" ENV["REDIS_PASSWORD"] = "secret456"
assert_equal ["-e", "REDIS_PASSWORD=secret456", "-e", "REDIS_URL=redis://a/b", "-e", "WEB_CONCURRENCY=4"], @config_with_roles.role(:workers).env_args assert_equal ["-e", "REDIS_PASSWORD=secret456", "-e", "REDIS_URL=redis://a/b", "-e", "WEB_CONCURRENCY=4"], @config_with_roles.role(:workers).env_args
ensure ensure
ENV["REDIS_PASSWORD"] = nil ENV["REDIS_PASSWORD"] = nil

View File

@@ -1,5 +1,4 @@
require "test_helper" require "test_helper"
require "mrsk/configuration"
class ConfigurationTest < ActiveSupport::TestCase class ConfigurationTest < ActiveSupport::TestCase
setup do setup do

View File

@@ -2,8 +2,10 @@ require "bundler/setup"
require "active_support/test_case" require "active_support/test_case"
require "active_support/testing/autorun" require "active_support/testing/autorun"
require "debug" require "debug"
require "mocha/minitest" require "mocha/minitest" # using #stubs that can alter returns
require "minitest/autorun" # using #stub that take args
require "sshkit" require "sshkit"
require "mrsk"
ActiveSupport::LogSubscriber.logger = ActiveSupport::Logger.new(STDOUT) if ENV["VERBOSE"] ActiveSupport::LogSubscriber.logger = ActiveSupport::Logger.new(STDOUT) if ENV["VERBOSE"]