Compare commits

..

86 Commits

Author SHA1 Message Date
David Heinemeier Hansson
2e02b1ed14 Bump version for 0.6.1 2023-02-07 15:05:52 +01:00
David Heinemeier Hansson
fdd98622ac Fix issue with removing containers triggering twice, then ensure app stop runs closer to app run on each host 2023-02-07 15:05:06 +01:00
David Heinemeier Hansson
9aa57dd0c7 Bump version for 0.6.0 2023-02-05 17:53:43 +01:00
David Heinemeier Hansson
cb9f57356e Load destination ENV file also 2023-02-05 17:52:57 +01:00
David Heinemeier Hansson
02a5726072 Allow destination specific envifying 2023-02-05 16:35:37 +01:00
David Heinemeier Hansson
e865e823d5 Add envify for managing .env file 2023-02-05 16:30:56 +01:00
David Heinemeier Hansson
10cad5c459 Create binstub without bundler, document it all agnostically
You can use MRSK with something other than Rails.
2023-02-05 16:23:34 +01:00
David Heinemeier Hansson
ebcb297582 Merge pull request #24 from chrisdebruin/allow-bastion-server
Allow use of bastion host
2023-02-04 15:44:30 +01:00
David Heinemeier Hansson
0a293ae4d6 Fix and expand testing 2023-02-04 15:43:45 +01:00
Chris de Bruin
bdff11e1fc Allow use of bastion host 2023-02-04 15:38:05 +01:00
David Heinemeier Hansson
9cfb6fb0a9 Merge issue 2023-02-04 15:34:48 +01:00
David Heinemeier Hansson
9ec6f9d74f Merge branch 'main' into allow-bastion-server 2023-02-04 15:33:25 +01:00
David Heinemeier Hansson
45207f0c4f Explain the dance 2023-02-04 15:27:41 +01:00
David Heinemeier Hansson
cf9a402ad8 Stop treating RAILS_MASTER_KEY as special 2023-02-04 15:26:59 +01:00
David Heinemeier Hansson
64a5a790a7 Ensure secret can be used alone 2023-02-04 15:26:43 +01:00
David Heinemeier Hansson
78d4e1e1e9 Easier to read 2023-02-04 15:12:06 +01:00
David Heinemeier Hansson
74c7a6d5de Expand app command testing 2023-02-04 10:31:04 +01:00
David Heinemeier Hansson
340929e7e7 Use a version 2023-02-04 10:20:51 +01:00
David Heinemeier Hansson
6f1a3f5524 Don't need this, just use containers 2023-02-04 10:16:24 +01:00
David Heinemeier Hansson
7077da5a64 Spacing 2023-02-04 10:15:43 +01:00
David Heinemeier Hansson
77c63dcd04 Style 2023-02-04 10:14:35 +01:00
David Heinemeier Hansson
e7ac73be5a Join in run_over_ssh instead of all over 2023-02-04 10:14:31 +01:00
David Heinemeier Hansson
dfca9d8c48 Merge branch 'main' into allow-bastion-server 2023-02-04 10:06:15 +01:00
David Heinemeier Hansson
6032d5651a Merge pull request #35 from rails/zeitwerk
Load with Zeitwerk
2023-02-04 10:05:43 +01:00
Xavier Noria
539752e9bd Load with Zeitwerk 2023-02-03 22:45:12 +01:00
David Heinemeier Hansson
94b28a1b29 Extract method 2023-02-03 20:53:33 +01:00
David Heinemeier Hansson
5911914e95 Bump version for 0.5.1 2023-02-03 20:48:21 +01:00
David Heinemeier Hansson
3daecf696a Extract proper auditor and audit everything 2023-02-03 20:45:32 +01:00
David Heinemeier Hansson
497c57e3e5 Style 2023-02-03 20:44:43 +01:00
David Heinemeier Hansson
8a42fd2f30 Fix signature 2023-02-03 20:43:22 +01:00
David Heinemeier Hansson
2182cfb5c7 Bump version for 0.5.0 2023-02-03 17:49:47 +01:00
David Heinemeier Hansson
5c9a602d76 Fixed host 2023-02-03 17:46:41 +01:00
David Heinemeier Hansson
b964e04f93 Bring accessory execution in line with app 2023-02-03 17:24:36 +01:00
David Heinemeier Hansson
1fb2c71f65 Follow same dot style 2023-02-03 17:22:55 +01:00
David Heinemeier Hansson
58417f610f Dupe comment 2023-02-03 17:20:14 +01:00
David Heinemeier Hansson
5856a77a53 Bring accessory execution in line with app 2023-02-03 17:19:20 +01:00
David Heinemeier Hansson
5ed3ea9d26 Grouping by spacing 2023-02-03 17:18:58 +01:00
David Heinemeier Hansson
59199cc69a Fix bug 2023-02-03 17:18:47 +01:00
David Heinemeier Hansson
c453b947e0 Add exec tests 2023-02-03 17:18:42 +01:00
David Heinemeier Hansson
87e54d41e4 Need two stubs! 2023-02-03 17:03:26 +01:00
David Heinemeier Hansson
64b91daab1 Drop concerns
Not enough reuse possible
2023-02-03 16:55:34 +01:00
David Heinemeier Hansson
13e22f8a34 Repository really is app specific, since it relies on versions 2023-02-03 16:45:52 +01:00
David Heinemeier Hansson
8848335fbc Extract executions into separate concern 2023-02-03 16:39:26 +01:00
David Heinemeier Hansson
a3fe8856c9 Fix test 2023-02-03 16:27:16 +01:00
David Heinemeier Hansson
d263b0ffa5 Extract xargs helper 2023-02-03 16:27:10 +01:00
David Heinemeier Hansson
3c1053fedd Clarify exec modes and drop tailored versions 2023-02-03 16:07:25 +01:00
David Heinemeier Hansson
a3d998508b Proper versioning for console and bash 2023-02-03 15:16:40 +01:00
David Heinemeier Hansson
3d71ecdf80 Only say if you're going to do it 2023-02-03 15:16:30 +01:00
David Heinemeier Hansson
37e216f2b7 Add some more tests 2023-02-03 15:08:44 +01:00
David Heinemeier Hansson
17e75ec2c9 No more reboot 2023-02-03 15:06:43 +01:00
David Heinemeier Hansson
7621784235 Bring back regular version with narration 2023-02-03 15:05:34 +01:00
David Heinemeier Hansson
687b8c9def Rely on shared --version 2023-02-03 14:41:39 +01:00
David Heinemeier Hansson
13d4eb4017 Narrate multi-stage actions 2023-02-03 14:41:30 +01:00
David Heinemeier Hansson
78f0be9c76 Only multi-stage actions should talk 2023-02-03 14:33:49 +01:00
David Heinemeier Hansson
839a0df40e Boot now does its own stopping 2023-02-03 14:31:56 +01:00
David Heinemeier Hansson
74c493def4 Don't actually need reboot now that boot can do that 2023-02-03 14:31:11 +01:00
Chris de Bruin
7d95472543 Added -J for ssh proxy 2023-02-03 14:31:09 +01:00
David Heinemeier Hansson
71681cb8be Use single string-based proxy declaration 2023-02-03 14:30:20 +01:00
Chris de Bruin
1fef6ba505 Allow use of bastion host 2023-02-03 14:30:20 +01:00
David Heinemeier Hansson
22bbedf298 Show current running version 2023-02-03 14:08:00 +01:00
David Heinemeier Hansson
15a213eec6 Escape pipe and test for xargs 2023-02-03 14:07:52 +01:00
David Heinemeier Hansson
67f9ffe961 xargs when piping 2023-02-03 14:07:37 +01:00
David Heinemeier Hansson
25e52d6c93 Fix escaping 2023-02-03 14:07:20 +01:00
David Heinemeier Hansson
2023c377ab Reboot if running 2023-02-03 13:52:31 +01:00
David Heinemeier Hansson
3bd2559c03 Version comes from config 2023-02-03 13:52:10 +01:00
David Heinemeier Hansson
ad26bce5a2 Add mocha for testing 2023-02-03 13:48:34 +01:00
David Heinemeier Hansson
aed7425b42 Streamline version handling 2023-02-03 13:21:11 +01:00
David Heinemeier Hansson
fadb73da39 Replace stub value 2023-02-03 13:20:10 +01:00
David Heinemeier Hansson
8024949fe7 Remove only specific container needed for rebooting 2023-02-03 13:20:03 +01:00
David Heinemeier Hansson
004c154abb Reset MRSK between invocations in CLI tests
Don't love having #reset, but whatever for now.
2023-02-03 13:15:14 +01:00
David Heinemeier Hansson
35b42cc885 Fix tests 2023-02-02 18:05:56 +01:00
David Heinemeier Hansson
6d80005f5d Run boot and console on relevant versions
Instead of just defaulting to local hash version
2023-02-02 18:05:03 +01:00
David Heinemeier Hansson
c8f673ef7c Add images command to see what's on the server for the service repository 2023-02-02 16:53:46 +01:00
David Heinemeier Hansson
212d5ec783 Merge pull request #31 from fschueller/accessory-class
Align config class name with file name
2023-02-02 15:50:50 +01:00
David Heinemeier Hansson
f88685a525 Extract CliTestCase 2023-02-02 15:37:41 +01:00
David Heinemeier Hansson
08908c3925 Fix test 2023-02-02 15:31:33 +01:00
David Heinemeier Hansson
48a9f599b8 It's all of them 2023-02-02 15:31:27 +01:00
David Heinemeier Hansson
7cc64299c8 Add app reboot 2023-02-02 15:28:36 +01:00
David Heinemeier Hansson
7494f08978 Cleanup 2023-02-02 15:28:36 +01:00
David Heinemeier Hansson
2b232b41ce Unbundle remove so parts can be triggered individually 2023-02-02 15:28:36 +01:00
David Heinemeier Hansson
c28065fd42 Fix doc 2023-02-02 15:28:36 +01:00
Farah Schüller
80b90ab689 Align config class name with file name
`Mrsk::Configuration::Assessory` -> `Mrsk::Configuration::Accessory` thus
aligning with the name of the file.
2023-02-02 12:44:48 +01:00
David Heinemeier Hansson
d71950f5e4 Merge pull request #30 from azolf/improve-test-coverage
Improve test coverage
2023-02-02 10:51:20 +01:00
Amirhosein Zolfaghari
5bf3c36001 added more test cases for traefik command 2023-02-01 11:53:25 +03:30
Amirhosein Zolfaghari
afb7b43f1a added registry command tests 2023-02-01 11:48:47 +03:30
Amirhosein Zolfaghari
4f57976efe ignore useless files 2023-02-01 11:48:47 +03:30
50 changed files with 891 additions and 350 deletions

2
.gitignore vendored
View File

@@ -1,2 +1,4 @@
.byebug_history
*.gem
coverage/*
.DS_Store

View File

@@ -4,4 +4,5 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
gemspec
gem "debug"
gem "mocha"
gem "railties"

View File

@@ -1,11 +1,12 @@
PATH
remote: .
specs:
mrsk (0.4.0)
mrsk (0.6.1)
activesupport (>= 7.0)
dotenv (~> 2.8)
sshkit (~> 1.21)
thor (~> 1.2)
zeitwerk (~> 2.5)
GEM
remote: https://rubygems.org/
@@ -46,6 +47,8 @@ GEM
nokogiri (>= 1.5.9)
method_source (1.0.0)
minitest (5.17.0)
mocha (2.0.2)
ruby2_keywords (>= 0.0.5)
net-scp (4.0.0)
net-ssh (>= 2.6.5, < 8.0.0)
net-ssh (7.0.1)
@@ -74,6 +77,7 @@ GEM
rake (13.0.6)
reline (0.3.2)
io-console (~> 0.5)
ruby2_keywords (0.0.5)
sshkit (1.21.3)
net-scp (>= 1.1.2)
net-ssh (>= 2.8.0)
@@ -88,10 +92,12 @@ PLATFORMS
arm64-darwin-22
x86_64-darwin-20
x86_64-darwin-21
x86_64-darwin-22
x86_64-linux
DEPENDENCIES
debug
mocha
mrsk!
railties

View File

@@ -1,10 +1,10 @@
# MRSK
MRSK deploys Rails apps in containers to servers running Docker with zero downtime. It uses the dynamic reverse-proxy Traefik to hold requests while the new application container is started and the old one is stopped. It works seamlessly across multiple hosts, using SSHKit to execute commands.
MRSK deploys web apps in containers to servers running Docker with zero downtime. It uses the dynamic reverse-proxy Traefik to hold requests while the new application container is started and the old one is stopped. It works seamlessly across multiple hosts, using SSHKit to execute commands.
## Installation
Install MRSK globally with `gem install mrsk`. Then, inside your app directory, run `mrsk install`. Now edit the new file `config/deploy.yml`. It could look as simple as this:
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:
```yaml
service: hey
@@ -15,12 +15,17 @@ servers:
registry:
username: registry-user-name
password: <%= ENV.fetch("MRSK_REGISTRY_PASSWORD") %>
env:
secret:
- RAILS_MASTER_KEY
```
Now you're ready to deploy a multi-arch image to the servers:
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:
```
MRSK_REGISTRY_PASSWORD=pw mrsk deploy
mrsk deploy
```
This will:
@@ -68,10 +73,27 @@ registry:
### Using a different SSH user than root
The default SSH user is root, but you can change it using `ssh_user`:
The default SSH user is root, but you can change it using `ssh/user`:
```yaml
ssh_user: app
ssh:
user: app
```
### Using a proxy SSH host
If you need to connect to server through a proxy host, you can use `ssh/proxy`:
```yaml
ssh:
proxy: "192.168.0.1" # defaults to root as the user
```
Or with specific user:
```yaml
ssh:
proxy: "app@192.168.0.1"
```
### Using env variables
@@ -265,14 +287,6 @@ ARG RUBY_VERSION
FROM ruby:$RUBY_VERSION-slim as base
```
### Using without RAILS_MASTER_KEY
If you're using MRSK with older Rails apps that predate RAILS_MASTER_KEY, or with a non-Rails app, you can skip the default usage and reference:
```yaml
skip_master_key: true
```
### Using accessories for database, cache, search services
You can manage your accessory services via MRSK as well. The services will build off public images, and will not be automatically updated when you deploy:
@@ -300,11 +314,28 @@ accessories:
Now run `mrsk accessory start mysql` to start the MySQL server on 1.1.1.3. See `mrsk accessory` for all the commands possible.
### Using a generated .env file
If you're using a centralized secret store, like 1Password, you can create `.env.erb` as a template which looks up the secrets. Example of a .env.erb file:
```erb
<% if (session_token = `op signin --account my-one-password-account --raw`.strip) != "" %># Generated by mrsk envify
GITHUB_TOKEN=<%= `gh config get -h github.com oauth_token`.strip %>
MRSK_REGISTRY_PASSWORD=<%= `op read "op://Vault/Docker Hub/password" -n --session #{session_token}` %>
RAILS_MASTER_KEY=<%= `op read "op://Vault/My App/RAILS_MASTER_SECRET" -n --session #{session_token}` %>
MYSQL_ROOT_PASSWORD=<%= `op read "op://Vault/My App/MYSQL_ROOT_PASSWORD" -n --session #{session_token}` %>
<% else raise ArgumentError, "Session token missing" end %>
```
This template can safely be checked into git. Then everyone deploying the app can run `mrsk envify` when they setup the app for the first time or passwords change to get the correct `.env` file.
If you need separate env variables for different destinations, you can set them with `.env.destination.erb` for the template, which will generate `.env.staging` when run with `mrsk envify -d staging`.
## 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
# Runs command on all servers
@@ -347,13 +378,25 @@ Database adapter sqlite3
Database schema version 20221231233303
# 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
```
### 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

View File

@@ -3,8 +3,7 @@
# Prevent failures from being reported twice.
Thread.report_on_exception = false
require "dotenv/load"
require "mrsk/cli"
require "mrsk"
begin
Mrsk::Cli::Main.start(ARGV)

View File

@@ -1,5 +1,9 @@
module Mrsk
end
require "mrsk/version"
require "mrsk/commander"
require "zeitwerk"
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
end
# SSHKit uses instance eval, so we need a global const for ergonomics
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
desc "boot [NAME]", "Boot accessory service on host (use NAME=all to boot all accessories)"
def boot(name)
@@ -9,7 +7,11 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
with_accessory(name) do |accessory|
directories(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
@@ -18,6 +20,8 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
def upload(name)
with_accessory(name) do |accessory|
on(accessory.host) do
execute *MRSK.auditor.record("accessory #{name} upload files"), verbosity: :debug
accessory.files.each do |(local, remote)|
accessory.ensure_local_file_present(local)
@@ -33,6 +37,8 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
def directories(name)
with_accessory(name) do |accessory|
on(accessory.host) do
execute *MRSK.auditor.record("accessory #{name} create directories"), verbosity: :debug
accessory.directories.keys.each do |host_path|
execute *accessory.make_directory(host_path)
end
@@ -52,14 +58,20 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
desc "start [NAME]", "Start existing accessory on host"
def start(name)
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
desc "stop [NAME]", "Stop accessory on host"
def stop(name)
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
@@ -82,39 +94,33 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
end
end
desc "exec [NAME] [CMD]", "Execute a custom command on accessory host"
option :method, aliases: "-m", default: "exec", desc: "Execution method: [exec] perform inside container / [run] perform in new container / [ssh] perform over ssh"
desc "exec [NAME] [CMD]", "Execute a custom command on servers"
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)
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|
if runner.exec_over_ssh? || runner.run_over_ssh?
run_locally do
info "Launching command on #{accessory.host}"
exec accessory.send(runner, cmd)
end
else
case
when options[:interactive] && options[:reuse]
say "Launching interactive command with via SSH from existing container...", :magenta
run_locally { exec accessory.execute_in_existing_container_over_ssh(cmd) }
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
info "Launching command on #{accessory.host}"
execute *accessory.send(runner, cmd)
execute *MRSK.auditor.record("accessory #{name} cmd '#{cmd}'"), verbosity: :debug
capture_with_info(*accessory.execute_in_existing_container(cmd))
end
end
end
end
desc "bash [NAME]", "Start a bash session on primary host (or specific host set by --hosts)"
def bash(name)
with_accessory(name) do |accessory|
run_locally do
info "Launching bash session on #{accessory.host}"
exec accessory.bash
else
say "Launching command from new container...", :magenta
on(accessory.host) do
execute *MRSK.auditor.record("accessory #{name} cmd '#{cmd}'"), verbosity: :debug
capture_with_info(*accessory.execute_in_new_container(cmd))
end
end
end
end
@@ -162,21 +168,30 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
desc "remove_container [NAME]", "Remove accessory container from host"
def remove_container(name)
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
desc "remove_container [NAME]", "Remove accessory image from host"
desc "remove_image [NAME]", "Remove accessory image from host"
def remove_image(name)
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
desc "remove_service_directory [NAME]", "Remove accessory directory used for uploaded files and data directories from host"
def remove_service_directory(name)
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

View File

@@ -1,96 +1,106 @@
require "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
MRSK.config.roles.each do |role|
on(role.hosts) do |host|
begin
execute *MRSK.app.run(role: role.name)
rescue SSHKit::Command::Failed => e
if e.message =~ /already in use/
error "Container with same version already deployed on #{host}, starting that instead"
execute *MRSK.app.start, host: host
else
raise
say "Get most recent version available as an image...", :magenta unless options[: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|
on(role.hosts) do |host|
execute *MRSK.auditor.record("app boot version #{version}"), verbosity: :debug
begin
execute *MRSK.app.stop, raise_on_non_zero_exit: false
execute *MRSK.app.run(role: role.name)
rescue SSHKit::Command::Failed => e
if e.message =~ /already in use/
error "Rebooting container with same version already deployed on #{host}"
execute *MRSK.auditor.record("app rebooted with version #{version}"), verbosity: :debug
execute *MRSK.app.remove_container(version: version)
execute *MRSK.app.run(role: role.name)
else
raise
end
end
end
end
end
end
desc "start", "Start existing app on servers (use --version=<git-hash> to designate specific version)"
option :version, desc: "Defaults to the most recent git-hash in local repository"
def start
if (version = options[:version]).present?
on(MRSK.hosts) { execute *MRSK.app.start(version: version) }
else
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
desc "stop", "Stop app on servers"
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
desc "details", "Display details about app containers"
def details
on(MRSK.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.info) }
end
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)
runner = \
case options[:method]
when "exec" then "exec"
when "run" then "run_exec"
when "ssh" then "exec_over_ssh"
else raise "Unknown method: #{options[:method]}"
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)
case
when options[:interactive] && options[:reuse]
say "Get current version of running container...", :magenta unless options[:version]
using_version(options[:version] || current_running_version) do |version|
say "Launching interactive command with version #{version} via SSH from existing container on #{MRSK.primary_host}...", :magenta
run_locally { exec MRSK.app.execute_in_existing_container_over_ssh(cmd, host: MRSK.primary_host) }
end
when options[:interactive]
say "Get most recent version available as an image...", :magenta unless options[:version]
using_version(options[:version] || most_recent_version_available) do |version|
say "Launching interactive command with version #{version} via SSH from new container on #{MRSK.primary_host}...", :magenta
run_locally { exec MRSK.app.execute_in_new_container_over_ssh(cmd, host: MRSK.primary_host) }
end
when options[:reuse]
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
on(MRSK.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.send(runner, cmd)) }
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
desc "console", "Start Rails Console on primary host (or specific host set by --hosts)"
def console
run_locally do
info "Launching Rails console on #{MRSK.primary_host}"
exec MRSK.app.console(host: MRSK.primary_host)
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"
def containers
on(MRSK.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.list_containers) }
end
desc "current", "Return the current running container ID"
def current
on(MRSK.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.current_container_id) }
desc "images", "List all the app images currently on servers"
def images
on(MRSK.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.list_images) }
end
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 :lines, type: :numeric, aliases: "-n", desc: "Number of log lines to pull from each server"
@@ -122,16 +132,64 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
end
desc "remove", "Remove app containers and images from servers"
option :only, default: "", desc: "Use 'containers' or 'images'"
def remove
case options[:only]
when "containers"
on(MRSK.hosts) { execute *MRSK.app.remove_containers }
when "images"
on(MRSK.hosts) { execute *MRSK.app.remove_images }
else
on(MRSK.hosts) { execute *MRSK.app.remove_containers }
on(MRSK.hosts) { execute *MRSK.app.remove_images }
remove_containers
remove_images
end
desc "remove_container [VERSION]", "Remove app container with given version from servers"
def remove_container(version)
on(MRSK.hosts) do
execute *MRSK.auditor.record("app remove container #{version}"), verbosity: :debug
execute *MRSK.app.remove_container(version: version)
end
end
desc "remove_containers", "Remove all app containers from servers"
def remove_containers
on(MRSK.hosts) do
execute *MRSK.auditor.record("app remove containers"), verbosity: :debug
execute *MRSK.app.remove_containers
end
end
desc "remove_images", "Remove all app images from servers"
def 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
private
def using_version(new_version)
if new_version
begin
old_version = MRSK.config.version
MRSK.config.version = new_version
yield new_version
ensure
MRSK.config.version = old_version
end
else
yield MRSK.config.version
end
end
def most_recent_version_available(host: MRSK.primary_host)
version = nil
on(host) { version = capture_with_info(*MRSK.app.most_recent_version_from_available_images).strip }
version.presence
end
def current_running_version(host: MRSK.primary_host)
version = nil
on(host) { version = capture_with_info(*MRSK.app.current_running_version).strip }
version.presence
end
end

View File

@@ -1,4 +1,5 @@
require "thor"
require "dotenv"
require "mrsk/sshkit_with_ext"
module Mrsk::Cli
@@ -21,10 +22,19 @@ module Mrsk::Cli
def initialize(*)
super
load_envs
initialize_commander(options)
end
private
def load_envs
if destination = options[:destination]
Dotenv.load(".env.#{destination}", ".env")
else
Dotenv.load(".env")
end
end
def initialize_commander(options)
MRSK.tap do |commander|
commander.config_file = Pathname.new(File.expand_path(options[:config_file]))

View File

@@ -1,5 +1,3 @@
require "mrsk/cli/base"
class Mrsk::Cli::Build < Mrsk::Cli::Base
desc "deliver", "Deliver a newly built app image to servers"
def deliver
@@ -11,7 +9,7 @@ class Mrsk::Cli::Build < Mrsk::Cli::Base
def push
cli = self
run_locally do
run_locally do
begin
MRSK.with_verbosity(:debug) { execute *MRSK.builder.push }
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"
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
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
desc "setup", "Setup all accessories and deploy the app to servers"
def setup
@@ -21,12 +11,21 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
desc "deploy", "Deploy the app to servers"
def deploy
print_runtime do
say "Ensure Docker is installed...", :magenta
invoke "mrsk:cli:server:bootstrap"
say "Log into image registry...", :magenta
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"
invoke "mrsk:cli:app:stop"
invoke "mrsk:cli:app:boot"
say "Prune old containers and images...", :magenta
invoke "mrsk:cli:prune:all"
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)"
def redeploy
print_runtime do
say "Build and push app image...", :magenta
invoke "mrsk:cli:build:deliver"
invoke "mrsk:cli:app:stop"
invoke "mrsk:cli:app:boot"
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)
MRSK.version = version
cli = self
cli.say "Stop current version, then start version #{version}...", :magenta
on(MRSK.hosts) do
execute *MRSK.app.stop, raise_on_non_zero_exit: false
execute *MRSK.app.start(version: version)
execute *MRSK.app.start
end
end
@@ -55,6 +60,13 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
invoke "mrsk:cli:accessory:details", [ "all" ]
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"
def config
run_locally do
@@ -62,22 +74,29 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
end
end
desc "install", "Create config stub in config/deploy.yml and binstub in bin/mrsk"
option :skip_binstub, type: :boolean, default: false, desc: "Skip adding MRSK to the Gemfile and creating bin/mrsk binstub"
def install
desc "init", "Create config stub in config/deploy.yml and env stub in .env"
option :bundle, type: :boolean, default: false, desc: "Add MRSK to the Gemfile and create a bin/mrsk binstub"
def init
require "fileutils"
if (deploy_file = Pathname.new(File.expand_path("config/deploy.yml"))).exist?
puts "Config file already exists in config/deploy.yml (remove first to create a new one)"
else
FileUtils.mkdir_p deploy_file.dirname
FileUtils.cp_r Pathname.new(File.expand_path("templates/deploy.yml", __dir__)), deploy_file
puts "Created configuration file in config/deploy.yml"
end
unless options[:skip_binstub]
unless (deploy_file = Pathname.new(File.expand_path(".env"))).exist?
FileUtils.cp_r Pathname.new(File.expand_path("templates/template.env", __dir__)), deploy_file
puts "Created .env file"
end
if options[:bundle]
if (binstub = Pathname.new(File.expand_path("bin/mrsk"))).exist?
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`
puts "Created binstub file in bin/mrsk"
@@ -85,6 +104,15 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
end
end
desc "envify", "Create .env by evaluating .env.erb (or .env.staging.erb -> .env.staging when using -d staging)"
def envify
if destination = options[:destination]
File.write(".env.#{destination}", ERB.new(IO.read(Pathname.new(File.expand_path(".env.#{destination}.erb")))).result)
else
File.write(".env", ERB.new(IO.read(Pathname.new(File.expand_path(".env.erb")))).result)
end
end
desc "remove", "Remove Traefik, app, and registry session from servers"
def remove
invoke "mrsk:cli:traefik:remove"

View File

@@ -1,5 +1,3 @@
require "mrsk/cli/base"
class Mrsk::Cli::Prune < Mrsk::Cli::Base
desc "all", "Prune unused images and stopped containers"
def all
@@ -9,11 +7,17 @@ class Mrsk::Cli::Prune < Mrsk::Cli::Base
desc "images", "Prune unused images older than 30 days"
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
desc "containers", "Prune stopped containers for the service older than 3 days"
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

View File

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

View File

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

View File

@@ -0,0 +1,2 @@
MRSK_REGISTRY_PASSWORD=change-this
RAILS_MASTER_KEY=another-env

View File

@@ -1,5 +1,3 @@
require "mrsk/cli/base"
class Mrsk::Cli::Traefik < Mrsk::Cli::Base
desc "boot", "Boot Traefik on servers"
def boot
@@ -15,12 +13,18 @@ class Mrsk::Cli::Traefik < Mrsk::Cli::Base
desc "start", "Start existing Traefik on servers"
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
desc "stop", "Stop Traefik on servers"
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
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"
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
desc "remove_container", "Remove Traefik image from servers"
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

View File

@@ -1,13 +1,5 @@
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
attr_accessor :config_file, :destination, :verbosity, :version
@@ -77,8 +69,12 @@ class Mrsk::Commander
Mrsk::Commands::Accessory.new(config, name: name)
end
def auditor
@auditor ||= Mrsk::Commands::Auditor.new(config)
end
def with_verbosity(level)
def with_verbosity(level)
old_level = SSHKit.config.output_verbosity
SSHKit.config.output_verbosity = level
yield
@@ -86,6 +82,13 @@ class Mrsk::Commander
SSHKit.config.output_verbosity = old_level
end
# Test-induced damage!
def reset
@config = @config_file = @destination = @version = nil
@app = @builder = @traefik = @registry = @prune = @auditor = nil
@verbosity = :info
end
private
def cascading_version
version.presence || ENV["VERSION"] || `git rev-parse HEAD`.strip

View File

@@ -1,5 +1,3 @@
require "mrsk/commands/base"
class Mrsk::Commands::Accessory < Mrsk::Commands::Base
attr_reader :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
def run
docker :run,
docker :run,
"--name", service_name,
"-d",
"--restart", "unless-stopped",
@@ -33,6 +31,7 @@ class Mrsk::Commands::Accessory < Mrsk::Commands::Base
docker :ps, *service_filter
end
def logs(since: nil, lines: nil, grep: nil)
pipe \
docker(:logs, service_name, (" --since #{since}" if since), (" -n #{lines}" if lines), "-t", "2>&1"),
@@ -40,20 +39,21 @@ class Mrsk::Commands::Accessory < Mrsk::Commands::Base
end
def follow_logs(grep: nil)
run_over_ssh pipe(
docker(:logs, service_name, "-t", "-n", "10", "-f", "2>&1"),
(%(grep "#{grep}") if grep)
).join(" ")
run_over_ssh \
pipe \
docker(:logs, service_name, "-t", "-n", "10", "-f", "2>&1"),
(%(grep "#{grep}") if grep)
end
def exec(*command, interactive: false)
def execute_in_existing_container(*command, interactive: false)
docker :exec,
("-it" if interactive),
service_name,
*command
end
def run_exec(*command, interactive: false)
def execute_in_new_container(*command, interactive: false)
docker :run,
("-it" if interactive),
"--rm",
@@ -63,17 +63,18 @@ class Mrsk::Commands::Accessory < Mrsk::Commands::Base
*command
end
def execute_in_existing_container_over_ssh(*command)
run_over_ssh execute_in_existing_container(*command, interactive: true)
end
def execute_in_new_container_over_ssh(*command)
run_over_ssh execute_in_new_container(*command, interactive: true)
end
def run_over_ssh(command)
super command, host: host
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)
if !local_file.is_a?(StringIO) && !Pathname.new(local_file).exist?

View File

@@ -1,5 +1,3 @@
require "mrsk/commands/base"
class Mrsk::Commands::App < Mrsk::Commands::Base
def run(role: :web)
role = config.role(role)
@@ -7,8 +5,7 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
docker :run,
"-d",
"--restart unless-stopped",
"--name", config.service_with_version,
*rails_master_key_arg,
"--name", service_with_version,
*role.env_args,
*config.volume_args,
*role.label_args,
@@ -16,22 +13,19 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
role.cmd
end
def start(version: config.version)
docker :start, "#{config.service}-#{version}"
end
def current_container_id
docker :ps, "-q", *service_filter
def start
docker :start, service_with_version
end
def stop
pipe current_container_id, "xargs docker stop"
pipe current_container_id, xargs(docker(:stop))
end
def info
docker :ps, *service_filter
end
def logs(since: nil, lines: nil, grep: nil)
pipe \
current_container_id,
@@ -39,66 +33,99 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
("grep '#{grep}'" if grep)
end
def exec(*command, interactive: false)
def follow_logs(host:, grep: nil)
run_over_ssh \
pipe(
current_container_id,
"xargs docker logs -t -n 10 -f 2>&1",
(%(grep "#{grep}") if grep)
),
host: host
end
def execute_in_existing_container(*command, interactive: false)
docker :exec,
("-it" if interactive),
config.service_with_version,
*command
end
def run_exec(*command, interactive: false)
def execute_in_new_container(*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
def execute_in_existing_container_over_ssh(*command, host:)
run_over_ssh execute_in_existing_container(*command, interactive: true), host: host
end
def follow_logs(host:, grep: nil)
run_over_ssh pipe(
current_container_id,
"xargs docker logs -t -n 10 -f 2>&1",
(%(grep "#{grep}") if grep)
).join(" "), host: host
def execute_in_new_container_over_ssh(*command, host:)
run_over_ssh execute_in_new_container(*command, interactive: true), host: host
end
def console(host:)
exec_over_ssh "bin/rails", "c", host: host
def current_container_id
docker :ps, "-q", *service_filter
end
def bash(host:)
exec_over_ssh "bash", host: host
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
docker :container, :ls, "-a", *service_filter
end
def remove_container(version:)
pipe \
container_id_for(container_name: service_with_version(version)),
xargs(docker(:container, :rm))
end
def remove_containers
docker :container, :prune, "-f", *service_filter
end
def list_images
docker :image, :ls, config.repository
end
def remove_images
docker :image, :prune, "-a", "-f", *service_filter
end
private
def service_with_version(version = nil)
if version
"#{config.service}-#{version}"
else
config.service_with_version
end
end
def service_filter
[ "--filter", "label=service=#{config.service}" ]
end
def rails_master_key_arg
if master_key = config.master_key
[ "-e", redact("RAILS_MASTER_KEY=#{master_key}") ]
else
[]
end
end
end

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

@@ -8,8 +8,11 @@ module Mrsk::Commands
@config = config
end
def run_over_ssh(command, host:)
"ssh -t #{config.ssh_user}@#{host} '#{command}'"
def run_over_ssh(*command, host:)
"ssh".tap do |cmd|
cmd << " -J #{config.ssh_proxy.jump_proxies}" if config.ssh_proxy
cmd << " -t #{config.ssh_user}@#{host} '#{command.join(" ")}'"
end
end
private
@@ -28,6 +31,14 @@ module Mrsk::Commands
combine *commands, by: "|"
end
def append(*commands)
combine *commands, by: ">>"
end
def xargs(command)
[ :xargs, command ].flatten
end
def docker(*args)
args.compact.unshift :docker
end

View File

@@ -1,5 +1,3 @@
require "mrsk/commands/base"
class Mrsk::Commands::Builder < Mrsk::Commands::Base
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)
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
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
def create
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
def create
combine \

View File

@@ -1,5 +1,3 @@
require "mrsk/commands/builder/base"
class Mrsk::Commands::Builder::Native < Mrsk::Commands::Builder::Base
def create
# 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
def create
chain \

View File

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

View File

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

View File

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

View File

@@ -3,12 +3,13 @@ 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"
class Mrsk::Configuration
delegate :service, :image, :servers, :env, :labels, :registry, :builder, to: :raw_config, allow_nil: true
delegate :argumentize, :argumentize_env_with_secrets, to: Mrsk::Utils
attr_accessor :version
attr_accessor :raw_config
class << self
@@ -52,7 +53,7 @@ class Mrsk::Configuration
end
def accessories
@accessories ||= raw_config.accessories&.keys&.collect { |name| Mrsk::Configuration::Assessory.new(name, config: self) } || []
@accessories ||= raw_config.accessories&.keys&.collect { |name| Mrsk::Configuration::Accessory.new(name, config: self) } || []
end
def accessory(name)
@@ -73,10 +74,6 @@ class Mrsk::Configuration
end
def version
@version
end
def repository
[ raw_config.registry["server"], image ].compact.join("/")
end
@@ -107,17 +104,22 @@ class Mrsk::Configuration
end
def ssh_user
raw_config.ssh_user || "root"
if raw_config.ssh.present?
raw_config.ssh["user"] || "root"
else
"root"
end
end
def ssh_proxy
if raw_config.ssh.present? && raw_config.ssh["proxy"]
Net::SSH::Proxy::Jump.new \
raw_config.ssh["proxy"].include?("@") ? raw_config.ssh["proxy"] : "root@#{raw_config.ssh["proxy"]}"
end
end
def ssh_options
{ user: ssh_user, auth_methods: [ "publickey" ] }
end
def master_key
unless raw_config.skip_master_key
ENV["RAILS_MASTER_KEY"] || File.read(Pathname.new(File.expand_path("config/master.key")))
end
{ user: ssh_user, proxy: ssh_proxy, auth_methods: [ "publickey" ] }.compact
end
@@ -174,6 +176,3 @@ class Mrsk::Configuration
raw_config.servers.is_a?(Array) ? [ "web" ] : raw_config.servers.keys.sort
end
end
require "mrsk/configuration/role"
require "mrsk/configuration/accessory"

View File

@@ -1,4 +1,4 @@
class Mrsk::Configuration::Assessory
class Mrsk::Configuration::Accessory
delegate :argumentize, :argumentize_env_with_secrets, to: Mrsk::Utils
attr_accessor :name, :specifics

View File

@@ -96,7 +96,12 @@ class Mrsk::Configuration::Role
def merged_env_with_secrets
merged_env.tap do |new_env|
new_env["secret"] = Array(config.env["secret"]) + Array(specialized_env["secret"])
new_env["clear"] = (Array(config.env["clear"] || config.env) + Array(specialized_env["clear"] || specialized_env)).uniq
# If there's no secret/clear split, everything is clear
clear_app_env = config.env["secret"] ? Array(config.env["clear"]) : Array(config.env["clear"] || config.env)
clear_role_env = specialized_env["secret"] ? Array(specialized_env["clear"]) : Array(specialized_env["clear"] || specialized_env)
new_env["clear"] = (clear_app_env + clear_role_env).uniq
end
end
end

View File

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

View File

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

View File

@@ -1,34 +1,38 @@
require "test_helper"
require "active_support/testing/stream"
require "mrsk/cli"
class CliAccessoryTest < ActiveSupport::TestCase
include ActiveSupport::Testing::Stream
setup { ENV["MYSQL_ROOT_PASSWORD"] = "secret123" }
teardown { ENV["MYSQL_ROOT_PASSWORD"] = nil }
require_relative "cli_test_case"
class CliAccessoryTest < CliTestCase
test "upload" do
command = stdouted { Mrsk::Cli::Accessory.start(["upload", "mysql", "-c", "test/fixtures/deploy_with_accessories.yml"]) }
assert_match "test/fixtures/files/my.cnf app-mysql/etc/mysql/my.cnf", command
assert_match "test/fixtures/files/my.cnf app-mysql/etc/mysql/my.cnf", run_command("upload", "mysql")
end
test "directories" do
command = stdouted { Mrsk::Cli::Accessory.start(["directories", "mysql", "-c", "test/fixtures/deploy_with_accessories.yml"]) }
assert_match "mkdir -p $PWD/app-mysql/data", command
assert_match "mkdir -p $PWD/app-mysql/data", run_command("directories", "mysql")
end
test "remove service direcotry" do
command = stdouted { Mrsk::Cli::Accessory.start(["remove_service_directory", "mysql", "-c", "test/fixtures/deploy_with_accessories.yml"]) }
assert_match "rm -rf app-mysql", command
assert_match "rm -rf app-mysql", run_command("remove_service_directory", "mysql")
end
test "boot" do
command = stdouted { Mrsk::Cli::Accessory.start(["boot", "mysql", "-c", "test/fixtures/deploy_with_accessories.yml"]) }
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", command
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
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
def run_command(*command)
stdouted { Mrsk::Cli::Accessory.start([*command, "-c", "test/fixtures/deploy_with_accessories.yml"]) }
end
end

67
test/cli/app_test.rb Normal file
View File

@@ -0,0 +1,67 @@
require_relative "cli_test_case"
class CliAppTest < CliTestCase
test "boot" do
assert_match /Running docker run -d --restart unless-stopped/, run_command("boot")
end
test "boot will reboot if same version is already running" do
run_command("details") # Preheat MRSK const
# Prevent expected failures from outputting to terminal
Thread.report_on_exception = false
MRSK.app.stubs(:run).raises(SSHKit::Command::Failed.new("already in use")).then.returns([ :docker, :run ])
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 /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 \| xargs docker container rm/, output # Remove old container
assert_match /docker run/, output # Start new container
end
ensure
Thread.report_on_exception = true
end
test "start" do
run_command("start").tap do |output|
assert_match /docker start app-999/, output
end
end
test "stop" do
run_command("stop").tap do |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
test "remove_container" do
run_command("remove_container", "1234567").tap do |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
private
def run_command(*command)
stdouted { Mrsk::Cli::App.start([*command, "-c", "test/fixtures/deploy_with_accessories.yml"]) }
end
end

24
test/cli/cli_test_case.rb Normal file
View File

@@ -0,0 +1,24 @@
require "test_helper"
require "active_support/testing/stream"
class CliTestCase < ActiveSupport::TestCase
include ActiveSupport::Testing::Stream
setup do
ENV["VERSION"] = "999"
ENV["RAILS_MASTER_KEY"] = "123"
ENV["MYSQL_ROOT_PASSWORD"] = "secret123"
end
teardown do
ENV.delete("RAILS_MASTER_KEY")
ENV.delete("MYSQL_ROOT_PASSWORD")
ENV.delete("VERSION")
MRSK.reset
end
private
def stdouted
capture(:stdout) { yield }.strip
end
end

View File

@@ -1,13 +1,6 @@
require "test_helper"
require "active_support/testing/stream"
require "mrsk/cli"
class CliMainTest < ActiveSupport::TestCase
include ActiveSupport::Testing::Stream
setup do
end
require_relative "cli_test_case"
class CliMainTest < CliTestCase
test "version" do
version = stdouted { Mrsk::Cli::Main.new.version }
assert_equal Mrsk::VERSION, version

View File

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

View File

@@ -1,10 +1,8 @@
require "test_helper"
require "mrsk/configuration"
require "mrsk/commands/accessory"
class CommandsAccessoryTest < ActiveSupport::TestCase
setup do
@config = {
@config = {
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" },
servers: [ "1.1.1.1" ],
accessories: {
@@ -41,18 +39,20 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
@config = Mrsk::Configuration.new(@config)
@mysql = Mrsk::Commands::Accessory.new(@config, name: :mysql)
@redis = Mrsk::Commands::Accessory.new(@config, name: :redis)
ENV["MYSQL_ROOT_PASSWORD"] = "secret123"
end
teardown do
ENV.delete("MYSQL_ROOT_PASSWORD")
end
test "run" do
ENV["MYSQL_ROOT_PASSWORD"] = "secret123"
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
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
ensure
ENV["MYSQL_ROOT_PASSWORD"] = nil
end
test "start" do
@@ -67,6 +67,35 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
assert_equal [:docker, :ps, "--filter", "label=service=app-mysql"], @mysql.info
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.join(" ") }) 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.join(" ") }) 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
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")

View File

@@ -1,41 +1,161 @@
require "test_helper"
require "mrsk/configuration"
require "mrsk/commands/app"
class CommandsAppTest < ActiveSupport::TestCase
setup do
ENV["RAILS_MASTER_KEY"] = "456"
@config = { service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ] }
@app = Mrsk::Commands::App.new Mrsk::Configuration.new(@config)
@config = { service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ], env: { "secret" => [ "RAILS_MASTER_KEY" ] } }
@app = Mrsk::Commands::App.new Mrsk::Configuration.new(@config).tap { |c| c.version = "999" }
end
teardown do
ENV["RAILS_MASTER_KEY"] = nil
ENV.delete("RAILS_MASTER_KEY")
end
test "run" do
assert_equal \
[:docker, :run, "-d", "--restart unless-stopped", "--name", "app-missing", "-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=3", "--label", "traefik.http.middlewares.app.retry.initialinterval=500ms", "dhh/app:missing"], @app.run
"docker run -d --restart unless-stopped --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=3 --label traefik.http.middlewares.app.retry.initialinterval=500ms dhh/app:999",
@app.run.join(" ")
end
test "run with volumes" do
@config[:volumes] = ["/local/path:/container/path" ]
assert_equal \
[: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-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=3 --label traefik.http.middlewares.app.retry.initialinterval=500ms dhh/app:999",
@app.run.join(" ")
end
test "run with" do
test "start" do
assert_equal \
[ :docker, :run, "--rm", "-e", "RAILS_MASTER_KEY=456", "dhh/app:missing", "bin/rails", "db:setup" ],
@app.run_exec("bin/rails", "db:setup")
"docker start app-999",
@app.start.join(" ")
end
test "run without master key" do
ENV["RAILS_MASTER_KEY"] = nil
@app = Mrsk::Commands::App.new Mrsk::Configuration.new(@config.tap { |c| c[:skip_master_key] = true })
test "stop" do
assert_equal \
"docker ps -q --filter label=service=app | xargs docker stop",
@app.stop.join(" ")
end
assert @app.run.exclude?("RAILS_MASTER_KEY=456")
test "info" do
assert_equal \
"docker ps --filter label=service=app",
@app.info.join(" ")
end
test "logs" do
assert_equal \
"docker ps -q --filter label=service=app | xargs docker logs 2>&1",
@app.logs.join(" ")
assert_equal \
"docker ps -q --filter label=service=app | xargs docker logs --since 5m 2>&1",
@app.logs(since: "5m").join(" ")
assert_equal \
"docker ps -q --filter label=service=app | xargs docker logs -n 100 2>&1",
@app.logs(lines: "100").join(" ")
assert_equal \
"docker ps -q --filter label=service=app | xargs docker logs --since 5m -n 100 2>&1",
@app.logs(since: "5m", lines: "100").join(" ")
assert_equal \
"docker ps -q --filter label=service=app | xargs docker logs 2>&1 | grep 'my-id'",
@app.logs(grep: "my-id").join(" ")
assert_equal \
"docker ps -q --filter label=service=app | xargs docker logs --since 5m 2>&1 | grep 'my-id'",
@app.logs(since: "5m", grep: "my-id").join(" ")
end
test "follow logs" do
@app.stub(:run_over_ssh, ->(cmd, host:) { cmd.join(" ") }) do
assert_equal \
"docker ps -q --filter label=service=app | xargs docker logs -t -n 10 -f 2>&1",
@app.follow_logs(host: "app-1")
assert_equal \
"docker ps -q --filter label=service=app | xargs docker logs -t -n 10 -f 2>&1 | grep \"Completed\"",
@app.follow_logs(host: "app-1", grep: "Completed")
end
end
test "execute in new container" do
assert_equal \
"docker run --rm -e RAILS_MASTER_KEY=456 dhh/app:999 bin/rails db:setup",
@app.execute_in_new_container("bin/rails", "db:setup").join(" ")
end
test "execute in existing container" do
assert_equal \
"docker exec app-999 bin/rails db:setup",
@app.execute_in_existing_container("bin/rails", "db:setup").join(" ")
end
test "execute in new container over ssh" do
@app.stub(:run_over_ssh, ->(cmd, host:) { cmd.join(" ") }) do
assert_match %r|docker run -it --rm -e RAILS_MASTER_KEY=456 dhh/app:999 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.join(" ") }) do
assert_match %r|docker exec -it app-999 bin/rails c|,
@app.execute_in_existing_container_over_ssh("bin/rails", "c", host: "app-1")
end
end
test "run over ssh" do
assert_equal "ssh -t root@1.1.1.1 'ls'", @app.run_over_ssh("ls", host: "1.1.1.1")
end
test "run over ssh with custom user" do
@app = Mrsk::Commands::App.new Mrsk::Configuration.new(@config.tap { |c| c[:ssh] = { "user" => "app" } })
assert_equal "ssh -t app@1.1.1.1 'ls'", @app.run_over_ssh("ls", host: "1.1.1.1")
end
test "run over ssh with proxy" do
@app = Mrsk::Commands::App.new Mrsk::Configuration.new(@config.tap { |c| c[:ssh] = { "proxy" => "2.2.2.2" } })
assert_equal "ssh -J root@2.2.2.2 -t root@1.1.1.1 'ls'", @app.run_over_ssh("ls", host: "1.1.1.1")
end
test "run over ssh with proxy user" do
@app = Mrsk::Commands::App.new Mrsk::Configuration.new(@config.tap { |c| c[:ssh] = { "proxy" => "app@2.2.2.2" } })
assert_equal "ssh -J app@2.2.2.2 -t root@1.1.1.1 'ls'", @app.run_over_ssh("ls", host: "1.1.1.1")
end
test "run over ssh with custom user with proxy" do
@app = Mrsk::Commands::App.new Mrsk::Configuration.new(@config.tap { |c| c[:ssh] = { "user" => "app", "proxy" => "2.2.2.2" } })
assert_equal "ssh -J root@2.2.2.2 -t app@1.1.1.1 'ls'", @app.run_over_ssh("ls", host: "1.1.1.1")
end
test "current_container_id" do
assert_equal \
"docker ps -q --filter label=service=app",
@app.current_container_id.join(" ")
end
test "container_id_for" do
assert_equal \
"docker container ls -a -f name=app-999 -q",
@app.container_id_for(container_name: "app-999").join(" ")
end
test "current_running_version" do
assert_equal \
"docker ps --filter label=service=app --format \"{{.Names}}\" | sed 's/-/\\n/g' | tail -n 1",
@app.current_running_version.join(" ")
end
test "most_recent_version_from_available_images" do
assert_equal \
"docker image ls --format \"{{.Tag}}\" dhh/app | head -n 1",
@app.most_recent_version_from_available_images.join(" ")
end
end

View File

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

23
test/commands/registry_test.rb Executable file
View File

@@ -0,0 +1,23 @@
require "test_helper"
class CommandsRegistryTest < ActiveSupport::TestCase
setup do
@config = { service: "app",
image: "dhh/app",
registry: { "username" => "dhh",
"password" => "secret",
"server" => "hub.docker.com"
},
servers: [ "1.1.1.1" ]
}
@registry = Mrsk::Commands::Registry.new Mrsk::Configuration.new(@config)
end
test "registry login" do
assert_equal [ :docker, :login, "hub.docker.com", "-u", "dhh", "-p", "secret" ], @registry.login
end
test "registry logout" do
assert_equal [:docker, :logout, "hub.docker.com"], @registry.logout
end
end

View File

@@ -1,6 +1,4 @@
require "test_helper"
require "mrsk/configuration"
require "mrsk/commands/traefik"
class CommandsTraefikTest < ActiveSupport::TestCase
setup do
@@ -16,6 +14,61 @@ class CommandsTraefikTest < ActiveSupport::TestCase
new_command.run
end
test "traefik start" do
assert_equal \
[:docker, :container, :start, 'traefik'], new_command.start
end
test "traefik stop" do
assert_equal \
[:docker, :container, :stop, 'traefik'], new_command.stop
end
test "traefik info" do
assert_equal \
[:docker, :ps, '--filter', 'name=traefik'], new_command.info
end
test "traefik logs" do
assert_equal \
[:docker, :logs, 'traefik', '-t', '2>&1'], new_command.logs
end
test "traefik logs since 2h" do
assert_equal \
[:docker, :logs, 'traefik', ' --since 2h', '-t', '2>&1'], new_command.logs(since: '2h')
end
test "traefik logs last 10 lines" do
assert_equal \
[:docker, :logs, 'traefik', ' -n 10', '-t', '2>&1'], new_command.logs(lines: 10)
end
test "traefik logs with grep hello!" do
assert_equal \
[:docker, :logs, 'traefik', '-t', '2>&1', "|", "grep 'hello!'"], new_command.logs(grep: 'hello!')
end
test "traefik remove container" do
assert_equal \
[:docker, :container, :prune, "-f", "--filter", "label=org.opencontainers.image.title=Traefik"], new_command.remove_container
end
test "traefik remove image" do
assert_equal \
[:docker, :image, :prune, "-a", "-f", "--filter", "label=org.opencontainers.image.title=Traefik"], new_command.remove_image
end
test "traefik follow logs" do
assert_equal \
"ssh -t root@1.1.1.1 'docker logs traefik -t -n 10 -f 2>&1'", new_command.follow_logs(host: @config[:servers].first)
end
test "traefik follow logs with grep hello!" do
assert_equal \
"ssh -t root@1.1.1.1 'docker logs traefik -t -n 10 -f 2>&1 | grep \"hello!\"'", new_command.follow_logs(host: @config[:servers].first, grep: 'hello!')
end
private
def new_command
Mrsk::Commands::Traefik.new(Mrsk::Configuration.new(@config, version: "123"))

View File

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

View File

@@ -1,5 +1,4 @@
require "test_helper"
require "mrsk/configuration"
class ConfigurationRoleTest < ActiveSupport::TestCase
setup do
@@ -63,7 +62,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
end
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" ] }
})
@@ -97,7 +96,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
ENV["REDIS_PASSWORD"] = "secret456"
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
ensure
ENV["REDIS_PASSWORD"] = nil
@@ -116,7 +115,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
}
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
ensure
ENV["DB_PASSWORD"] = nil
@@ -133,7 +132,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
}
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
ensure
ENV["REDIS_PASSWORD"] = nil

View File

@@ -1,5 +1,4 @@
require "test_helper"
require "mrsk/configuration"
class ConfigurationTest < ActiveSupport::TestCase
setup do
@@ -140,17 +139,18 @@ class ConfigurationTest < ActiveSupport::TestCase
test "ssh options" do
assert_equal "root", @config.ssh_options[:user]
config = Mrsk::Configuration.new(@deploy.tap { |c| c[:ssh_user] = "app" })
config = Mrsk::Configuration.new(@deploy.tap { |c| c.merge!(ssh: { "user" => "app" }) })
assert_equal "app", @config.ssh_options[:user]
end
test "master key" do
assert_equal "456", @config.master_key
test "ssh options with proxy host" do
config = Mrsk::Configuration.new(@deploy.tap { |c| c.merge!(ssh: { "proxy" => "1.2.3.4" }) })
assert_equal "root@1.2.3.4", @config.ssh_options[:proxy].jump_proxies
end
test "skip master key" do
config = Mrsk::Configuration.new(@deploy.tap { |c| c[:skip_master_key] = true })
assert_nil @config.master_key
test "ssh options with proxy host and user" do
config = Mrsk::Configuration.new(@deploy.tap { |c| c.merge!(ssh: { "proxy" => "app@1.2.3.4" }) })
assert_equal "app@1.2.3.4", @config.ssh_options[:proxy].jump_proxies
end
test "volume_args" do

View File

@@ -2,15 +2,14 @@ require "bundler/setup"
require "active_support/test_case"
require "active_support/testing/autorun"
require "debug"
require "mocha/minitest" # using #stubs that can alter returns
require "minitest/autorun" # using #stub that take args
require "sshkit"
require "mrsk"
ActiveSupport::LogSubscriber.logger = ActiveSupport::Logger.new(STDOUT) if ENV["VERBOSE"]
SSHKit.config.backend = SSHKit::Backend::Printer
class ActiveSupport::TestCase
private
def stdouted
capture(:stdout) { yield }.strip
end
end