Compare commits
133 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9c6d2966b | ||
|
|
f371cda8d8 | ||
|
|
9eaf0f3b8f | ||
|
|
a80289d046 | ||
|
|
aae45afb1b | ||
|
|
f4157c95c4 | ||
|
|
bb5176673b | ||
|
|
e9cb5b64b3 | ||
|
|
0433619518 | ||
|
|
110bf44a3b | ||
|
|
fbdf39a733 | ||
|
|
f99ff47f75 | ||
|
|
bb18189b01 | ||
|
|
18bdb33de2 | ||
|
|
1ec016ecad | ||
|
|
bd61e04088 | ||
|
|
0da2a6408b | ||
|
|
9697a9a6e0 | ||
|
|
32d52b024c | ||
|
|
2fe01f13df | ||
|
|
554a3558ab | ||
|
|
9aa57dd0c7 | ||
|
|
cb9f57356e | ||
|
|
02a5726072 | ||
|
|
e865e823d5 | ||
|
|
10cad5c459 | ||
|
|
ebcb297582 | ||
|
|
0a293ae4d6 | ||
|
|
bdff11e1fc | ||
|
|
9cfb6fb0a9 | ||
|
|
9ec6f9d74f | ||
|
|
45207f0c4f | ||
|
|
cf9a402ad8 | ||
|
|
64a5a790a7 | ||
|
|
78d4e1e1e9 | ||
|
|
74c7a6d5de | ||
|
|
340929e7e7 | ||
|
|
6f1a3f5524 | ||
|
|
7077da5a64 | ||
|
|
77c63dcd04 | ||
|
|
e7ac73be5a | ||
|
|
dfca9d8c48 | ||
|
|
6032d5651a | ||
|
|
539752e9bd | ||
|
|
94b28a1b29 | ||
|
|
5911914e95 | ||
|
|
3daecf696a | ||
|
|
497c57e3e5 | ||
|
|
8a42fd2f30 | ||
|
|
2182cfb5c7 | ||
|
|
5c9a602d76 | ||
|
|
b964e04f93 | ||
|
|
1fb2c71f65 | ||
|
|
58417f610f | ||
|
|
5856a77a53 | ||
|
|
5ed3ea9d26 | ||
|
|
59199cc69a | ||
|
|
c453b947e0 | ||
|
|
87e54d41e4 | ||
|
|
64b91daab1 | ||
|
|
13e22f8a34 | ||
|
|
8848335fbc | ||
|
|
a3fe8856c9 | ||
|
|
d263b0ffa5 | ||
|
|
3c1053fedd | ||
|
|
a3d998508b | ||
|
|
3d71ecdf80 | ||
|
|
37e216f2b7 | ||
|
|
17e75ec2c9 | ||
|
|
7621784235 | ||
|
|
687b8c9def | ||
|
|
13d4eb4017 | ||
|
|
78f0be9c76 | ||
|
|
839a0df40e | ||
|
|
74c493def4 | ||
|
|
7d95472543 | ||
|
|
71681cb8be | ||
|
|
1fef6ba505 | ||
|
|
22bbedf298 | ||
|
|
15a213eec6 | ||
|
|
67f9ffe961 | ||
|
|
25e52d6c93 | ||
|
|
2023c377ab | ||
|
|
3bd2559c03 | ||
|
|
ad26bce5a2 | ||
|
|
aed7425b42 | ||
|
|
fadb73da39 | ||
|
|
8024949fe7 | ||
|
|
004c154abb | ||
|
|
35b42cc885 | ||
|
|
6d80005f5d | ||
|
|
c8f673ef7c | ||
|
|
212d5ec783 | ||
|
|
f88685a525 | ||
|
|
08908c3925 | ||
|
|
48a9f599b8 | ||
|
|
7cc64299c8 | ||
|
|
7494f08978 | ||
|
|
2b232b41ce | ||
|
|
c28065fd42 | ||
|
|
80b90ab689 | ||
|
|
d71950f5e4 | ||
|
|
00d194e3f3 | ||
|
|
3f44e25b63 | ||
|
|
4c8b1a3e04 | ||
|
|
f06d639583 | ||
|
|
cdd77445d0 | ||
|
|
71f8f164ca | ||
|
|
1840f667d3 | ||
|
|
00afd5c6fc | ||
|
|
e17a7e28cb | ||
|
|
88b5e52b9f | ||
|
|
bc0ae84eb1 | ||
|
|
cb6fdbefc8 | ||
|
|
5bf3c36001 | ||
|
|
afb7b43f1a | ||
|
|
4f57976efe | ||
|
|
444e33721a | ||
|
|
ca86573d89 | ||
|
|
e317935ab3 | ||
|
|
767991afe3 | ||
|
|
7e191dc267 | ||
|
|
0f0529c785 | ||
|
|
3ebf8d7777 | ||
|
|
cd8570d776 | ||
|
|
7c72dfcb5d | ||
|
|
52d75508ea | ||
|
|
ea6144e664 | ||
|
|
d1559949ba | ||
|
|
60c2d45bdc | ||
|
|
afefd32379 | ||
|
|
4937673aac | ||
|
|
c1cf834dfc |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,2 +1,4 @@
|
||||
.byebug_history
|
||||
*.gem
|
||||
coverage/*
|
||||
.DS_Store
|
||||
1
Gemfile
1
Gemfile
@@ -4,4 +4,5 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
|
||||
gemspec
|
||||
|
||||
gem "debug"
|
||||
gem "mocha"
|
||||
gem "railties"
|
||||
|
||||
10
Gemfile.lock
10
Gemfile.lock
@@ -1,10 +1,12 @@
|
||||
PATH
|
||||
remote: .
|
||||
specs:
|
||||
mrsk (0.3.1)
|
||||
mrsk (0.6.2)
|
||||
activesupport (>= 7.0)
|
||||
dotenv (~> 2.8)
|
||||
sshkit (~> 1.21)
|
||||
thor (~> 1.2)
|
||||
zeitwerk (~> 2.5)
|
||||
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
@@ -33,6 +35,7 @@ GEM
|
||||
debug (1.7.1)
|
||||
irb (>= 1.5.0)
|
||||
reline (>= 0.3.1)
|
||||
dotenv (2.8.1)
|
||||
erubi (1.12.0)
|
||||
i18n (1.12.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
@@ -44,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)
|
||||
@@ -72,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)
|
||||
@@ -86,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
|
||||
|
||||
|
||||
115
README.md
115
README.md
@@ -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:
|
||||
@@ -38,14 +43,25 @@ This will:
|
||||
|
||||
Voila! All the servers are now serving the app on port 80. If you're just running a single server, you're ready to go. If you're running multiple servers, you need to put a load balancer in front of them.
|
||||
|
||||
## Why not just run Capistrano or Kubernetes?
|
||||
## Why not just run Capistrano, Kubernetes or Docker Swarm?
|
||||
|
||||
MRSK basically is Capistrano for Containers, which allow us to use vanilla servers as the hosts. No need to ensure that the servers have just the right version of Ruby or other dependencies you need. That all lives in the Docker image now. You can boot a brand new Ubuntu (or whatever) server, add it to the deploy servers of MRSK, and it'll be auto-provisioned with Docker, and run right away. Docker's layer caching also allows for quicker deployments with less mucking about on the server. And the images built for MRSK can be used for CI or later introspection.
|
||||
|
||||
Kubernetes is a beast. Running it yourself on your own hardware is not for the faint of heart. It's a fine option if you want to run on someone else's platform, like Render or Fly, but if you'd like the freedom to move between cloud and your own hardware, or even mix the two, MRSK is much simpler. You can see everything that's going on, it's just basic Docker commands being called.
|
||||
Kubernetes is a beast. Running it yourself on your own hardware is not for the faint of heart. It's a fine option if you want to run on someone else's platform, either transparently [like Render](https://thenewstack.io/render-cloud-deployment-with-less-engineering/) or explicitly on AWS/GCP, but if you'd like the freedom to move between cloud and your own hardware, or even mix the two, MRSK is much simpler. You can see everything that's going on, it's just basic Docker commands being called.
|
||||
|
||||
Docker Swarm is much simpler than Kubernetes, but it's still built on the same declarative model that uses state reconciliation. MRSK is intentionally designed to around imperative commands, like Capistrano.
|
||||
|
||||
## Configuration
|
||||
|
||||
### Using .env file to load required environment variables
|
||||
|
||||
MRSK uses [dotenv](https://github.com/bkeepers/dotenv) to automatically load environment variables set in the `.env` file present in the application root. This file can be used to set variables like `MRSK_REGISTRY_PASSWORD` or database passwords. But for this reason you must ensure that .env files are not checked into Git or included in your Dockerfile! The format is just key-value like:
|
||||
|
||||
```bash
|
||||
MRSK_REGISTRY_PASSWORD=pw
|
||||
DB_PASSWORD=secret123
|
||||
```
|
||||
|
||||
### Using another registry than Docker Hub
|
||||
|
||||
The default registry is Docker Hub, but you can change it using `registry/server`:
|
||||
@@ -59,10 +75,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
|
||||
@@ -216,14 +249,27 @@ builder:
|
||||
|
||||
This build secret can then be referenced in the Dockerfile:
|
||||
|
||||
```
|
||||
```dockerfile
|
||||
# Copy Gemfiles
|
||||
COPY Gemfile Gemfile.lock ./
|
||||
|
||||
# Install dependencies, including private repositories via access token
|
||||
# Install dependencies, including private repositories via access token (then remove git configs with exposed GITHUB_TOKEN)
|
||||
RUN --mount=type=secret,id=GITHUB_TOKEN \
|
||||
BUNDLE_GITHUB__COM=x-access-token:$(cat /run/secrets/GITHUB_TOKEN) \
|
||||
bundle install
|
||||
bundle install && \
|
||||
find /usr/local/bundle/cache/bundler/git -name "config" -delete
|
||||
```
|
||||
|
||||
### Using command arguments for Traefik
|
||||
|
||||
You can customize the traefik command line:
|
||||
|
||||
```yaml
|
||||
traefik:
|
||||
accesslog: true
|
||||
accesslog.format: json
|
||||
metrics.prometheus: true
|
||||
metrics.prometheus.buckets: 0.1,0.3,1.2,5.0
|
||||
```
|
||||
|
||||
### Configuring build args for new images
|
||||
@@ -244,14 +290,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:
|
||||
@@ -279,11 +317,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
|
||||
@@ -326,13 +381,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
|
||||
|
||||
|
||||
5
bin/mrsk
5
bin/mrsk
@@ -3,11 +3,14 @@
|
||||
# Prevent failures from being reported twice.
|
||||
Thread.report_on_exception = false
|
||||
|
||||
require "mrsk/cli"
|
||||
require "mrsk"
|
||||
|
||||
begin
|
||||
Mrsk::Cli::Main.start(ARGV)
|
||||
rescue SSHKit::Runner::ExecuteError => e
|
||||
puts " \e[31mERROR (#{e.cause.class}): #{e.cause.message}\e[0m"
|
||||
puts e.cause.backtrace if ENV["VERBOSE"]
|
||||
rescue => e
|
||||
puts " \e[31mERROR (#{e.class}): #{e.message}\e[0m"
|
||||
puts e.backtrace if ENV["VERBOSE"]
|
||||
end
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,21 +94,33 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
|
||||
end
|
||||
end
|
||||
|
||||
desc "exec [NAME] [CMD]", "Execute a custom command on accessory host"
|
||||
option :run, type: :boolean, default: false, desc: "Start a new container to run the command rather than reusing existing"
|
||||
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)
|
||||
with_accessory(name) do |accessory|
|
||||
runner = options[:run] ? :run_exec : :exec
|
||||
on(accessory.host) { |host| puts_by_host host, capture_with_info(*accessory.send(runner, cmd)) }
|
||||
end
|
||||
end
|
||||
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) }
|
||||
|
||||
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(host: accessory.host)
|
||||
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
|
||||
execute *MRSK.auditor.record("accessory #{name} cmd '#{cmd}'"), verbosity: :debug
|
||||
capture_with_info(*accessory.execute_in_existing_container(cmd))
|
||||
end
|
||||
|
||||
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
|
||||
@@ -118,7 +142,7 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
|
||||
end
|
||||
else
|
||||
since = options[:since]
|
||||
lines = options[:lines]
|
||||
lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set
|
||||
|
||||
on(accessory.host) do
|
||||
puts capture_with_info(*accessory.logs(since: since, lines: lines, grep: grep))
|
||||
@@ -144,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
|
||||
|
||||
|
||||
@@ -1,18 +1,27 @@
|
||||
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
|
||||
@@ -20,18 +29,19 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
|
||||
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"
|
||||
@@ -40,55 +50,55 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
|
||||
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"
|
||||
@@ -109,7 +119,7 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
|
||||
end
|
||||
else
|
||||
since = options[:since]
|
||||
lines = options[:lines]
|
||||
lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set
|
||||
|
||||
on(MRSK.hosts) do |host|
|
||||
begin
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
require "thor"
|
||||
require "dotenv"
|
||||
require "mrsk/sshkit_with_ext"
|
||||
|
||||
module Mrsk::Cli
|
||||
@@ -8,6 +9,7 @@ module Mrsk::Cli
|
||||
def self.exit_on_failure?() true end
|
||||
|
||||
class_option :verbose, type: :boolean, aliases: "-v", desc: "Detailed logging"
|
||||
class_option :quiet, type: :boolean, aliases: "-q", desc: "Minimal logging"
|
||||
|
||||
class_option :version, desc: "Run commands against a specific app version"
|
||||
|
||||
@@ -20,20 +22,37 @@ 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]))
|
||||
commander.destination = options[:destination]
|
||||
commander.verbose = options[:verbose]
|
||||
commander.version = options[:version]
|
||||
|
||||
commander.specific_hosts = options[:hosts]&.split(",")
|
||||
commander.specific_roles = options[:roles]&.split(",")
|
||||
commander.specific_primary! if options[:primary]
|
||||
|
||||
if options[:verbose]
|
||||
ENV["VERBOSE"] = "1" # For backtraces via cli/start
|
||||
commander.verbosity = :debug
|
||||
end
|
||||
|
||||
if options[:quiet]
|
||||
commander.verbosity = :error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -9,18 +7,17 @@ class Mrsk::Cli::Build < Mrsk::Cli::Base
|
||||
|
||||
desc "push", "Build locally and push app image to registry"
|
||||
def push
|
||||
verbose = options[:verbose]
|
||||
cli = self
|
||||
|
||||
run_locally do
|
||||
begin
|
||||
MRSK.verbosity(:debug) { execute *MRSK.builder.push }
|
||||
MRSK.with_verbosity(:debug) { execute *MRSK.builder.push }
|
||||
rescue SSHKit::Command::Failed => e
|
||||
if e.message =~ /(no builder)|(no such file or directory)/
|
||||
error "Missing compatible builder, so creating a new one first"
|
||||
|
||||
if cli.create
|
||||
MRSK.verbosity(:debug) { execute *MRSK.builder.push }
|
||||
MRSK.with_verbosity(:debug) { execute *MRSK.builder.push }
|
||||
end
|
||||
else
|
||||
raise
|
||||
@@ -31,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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
2
lib/mrsk/cli/templates/template.env
Normal file
2
lib/mrsk/cli/templates/template.env
Normal file
@@ -0,0 +1,2 @@
|
||||
MRSK_REGISTRY_PASSWORD=change-this
|
||||
RAILS_MASTER_KEY=another-env
|
||||
@@ -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"
|
||||
@@ -50,7 +54,7 @@ class Mrsk::Cli::Traefik < Mrsk::Cli::Base
|
||||
end
|
||||
else
|
||||
since = options[:since]
|
||||
lines = options[:lines]
|
||||
lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set
|
||||
|
||||
on(MRSK.traefik_hosts) do |host|
|
||||
puts_by_host host, capture(*MRSK.traefik.logs(since: since, lines: lines, grep: grep)), type: "Traefik"
|
||||
@@ -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
|
||||
|
||||
@@ -1,18 +1,10 @@
|
||||
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, :verbose, :version
|
||||
attr_accessor :config_file, :destination, :verbosity, :version
|
||||
|
||||
def initialize(config_file: nil, destination: nil, verbose: false)
|
||||
@config_file, @destination, @verbose = config_file, destination, verbose
|
||||
def initialize(config_file: nil, destination: nil, verbosity: :info)
|
||||
@config_file, @destination, @verbosity = config_file, destination, verbosity
|
||||
end
|
||||
|
||||
def config
|
||||
@@ -77,24 +69,47 @@ class Mrsk::Commander
|
||||
Mrsk::Commands::Accessory.new(config, name: name)
|
||||
end
|
||||
|
||||
def auditor
|
||||
@auditor ||= Mrsk::Commands::Auditor.new(config)
|
||||
end
|
||||
|
||||
def verbosity(level)
|
||||
old_level = SSHKit.config.output_verbosity
|
||||
|
||||
def with_verbosity(level)
|
||||
old_level = self.verbosity
|
||||
|
||||
self.verbosity = level
|
||||
SSHKit.config.output_verbosity = level
|
||||
|
||||
yield
|
||||
ensure
|
||||
self.verbosity = old_level
|
||||
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
|
||||
version.presence || ENV["VERSION"] || current_commit_hash
|
||||
end
|
||||
|
||||
def current_commit_hash
|
||||
if system("git rev-parse")
|
||||
`git rev-parse HEAD`.strip
|
||||
else
|
||||
raise "Can't use commit hash as version, no git repository found in #{Dir.pwd}"
|
||||
end
|
||||
end
|
||||
|
||||
# Lazy setup of SSHKit
|
||||
def configure_sshkit_with(config)
|
||||
SSHKit::Backend::Netssh.configure { |ssh| ssh.ssh_options = config.ssh_options }
|
||||
SSHKit.config.command_map[:docker] = "docker" # No need to use /usr/bin/env, just clogs up the logs
|
||||
SSHKit.config.output_verbosity = :debug if verbose
|
||||
SSHKit.config.output_verbosity = verbosity
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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
|
||||
@@ -14,6 +12,7 @@ class Mrsk::Commands::Accessory < Mrsk::Commands::Base
|
||||
"--name", service_name,
|
||||
"-d",
|
||||
"--restart", "unless-stopped",
|
||||
"--log-opt", "max-size=#{MAX_LOG_SIZE}",
|
||||
"-p", port,
|
||||
*env_args,
|
||||
*volume_args,
|
||||
@@ -33,6 +32,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,22 +40,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(" "), host: host
|
||||
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),
|
||||
*env_args,
|
||||
*volume_args,
|
||||
service_name,
|
||||
*command
|
||||
end
|
||||
|
||||
def run_exec(*command, interactive: false)
|
||||
def execute_in_new_container(*command, interactive: false)
|
||||
docker :run,
|
||||
("-it" if interactive),
|
||||
"--rm",
|
||||
@@ -65,10 +64,19 @@ class Mrsk::Commands::Accessory < Mrsk::Commands::Base
|
||||
*command
|
||||
end
|
||||
|
||||
def bash(host:)
|
||||
exec_over_ssh "bash", host: host
|
||||
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 ensure_local_file_present(local_file)
|
||||
if !local_file.is_a?(StringIO) && !Pathname.new(local_file).exist?
|
||||
raise "Missing file: #{local_file}"
|
||||
@@ -96,10 +104,6 @@ class Mrsk::Commands::Accessory < Mrsk::Commands::Base
|
||||
end
|
||||
|
||||
private
|
||||
def exec_over_ssh(*command, host:)
|
||||
run_over_ssh run_exec(*command, interactive: true).join(" "), host: host
|
||||
end
|
||||
|
||||
def service_filter
|
||||
[ "--filter", "label=service=#{service_name}" ]
|
||||
end
|
||||
|
||||
@@ -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,8 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
|
||||
docker :run,
|
||||
"-d",
|
||||
"--restart unless-stopped",
|
||||
"--name", config.service_with_version,
|
||||
*rails_master_key_arg,
|
||||
"--log-opt", "max-size=#{MAX_LOG_SIZE}",
|
||||
"--name", service_with_version,
|
||||
*role.env_args,
|
||||
*config.volume_args,
|
||||
*role.label_args,
|
||||
@@ -16,92 +14,119 @@ 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,
|
||||
"xargs docker logs#{" --since #{since}" if since}#{" -n #{lines}" if lines} -t 2>&1",
|
||||
"xargs docker logs#{" --since #{since}" if since}#{" -n #{lines}" if lines} 2>&1",
|
||||
("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),
|
||||
*rails_master_key_arg,
|
||||
*config.env_args,
|
||||
*config.volume_args,
|
||||
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
|
||||
|
||||
34
lib/mrsk/commands/auditor.rb
Normal file
34
lib/mrsk/commands/auditor.rb
Normal 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
|
||||
@@ -2,12 +2,21 @@ module Mrsk::Commands
|
||||
class Base
|
||||
delegate :redact, to: Mrsk::Utils
|
||||
|
||||
MAX_LOG_SIZE = "10m"
|
||||
|
||||
attr_accessor :config
|
||||
|
||||
def initialize(config)
|
||||
@config = config
|
||||
end
|
||||
|
||||
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
|
||||
def combine(*commands, by: "&&")
|
||||
commands
|
||||
@@ -24,12 +33,16 @@ 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
|
||||
|
||||
def run_over_ssh(command, host:)
|
||||
"ssh -t #{config.ssh_user}@#{host} '#{command}'"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
require "mrsk/commands/base"
|
||||
|
||||
class Mrsk::Commands::Builder::Base < Mrsk::Commands::Base
|
||||
delegate :argumentize, to: Mrsk::Utils
|
||||
|
||||
@@ -15,6 +13,10 @@ class Mrsk::Commands::Builder::Base < Mrsk::Commands::Base
|
||||
argumentize "--secret", secrets.collect { |secret| [ "id", secret ] }
|
||||
end
|
||||
|
||||
def build_tags
|
||||
[ "-t", config.absolute_image, "-t", config.latest_image ]
|
||||
end
|
||||
|
||||
private
|
||||
def args
|
||||
(config.builder && config.builder["args"]) || {}
|
||||
|
||||
@@ -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
|
||||
@@ -14,7 +12,7 @@ class Mrsk::Commands::Builder::Multiarch < Mrsk::Commands::Builder::Base
|
||||
"--push",
|
||||
"--platform", "linux/amd64,linux/arm64",
|
||||
"--builder", builder_name,
|
||||
"-t", config.absolute_image,
|
||||
*build_tags,
|
||||
*build_args,
|
||||
*build_secrets,
|
||||
"."
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
require "mrsk/commands/builder/multiarch"
|
||||
|
||||
class Mrsk::Commands::Builder::Multiarch::Remote < Mrsk::Commands::Builder::Multiarch
|
||||
def create
|
||||
combine \
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
require "mrsk/commands/builder/base"
|
||||
|
||||
class Mrsk::Commands::Builder::Native < Mrsk::Commands::Builder::Base
|
||||
def create
|
||||
# No-op on native
|
||||
@@ -11,7 +9,7 @@ class Mrsk::Commands::Builder::Native < Mrsk::Commands::Builder::Base
|
||||
|
||||
def push
|
||||
combine \
|
||||
docker(:build, "-t", *build_args, *build_secrets, config.absolute_image, "."),
|
||||
docker(:build, *build_tags, *build_args, *build_secrets, "."),
|
||||
docker(:push, config.absolute_image)
|
||||
end
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
require "mrsk/commands/builder/native"
|
||||
|
||||
class Mrsk::Commands::Builder::Native::Remote < Mrsk::Commands::Builder::Native
|
||||
def create
|
||||
chain \
|
||||
@@ -18,7 +16,7 @@ class Mrsk::Commands::Builder::Native::Remote < Mrsk::Commands::Builder::Native
|
||||
"--push",
|
||||
"--platform", platform,
|
||||
"--builder", builder_name,
|
||||
"-t", config.absolute_image,
|
||||
*build_tags,
|
||||
*build_args,
|
||||
*build_secrets,
|
||||
"."
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
require "mrsk/commands/base"
|
||||
require "active_support/duration"
|
||||
require "active_support/core_ext/numeric/time"
|
||||
|
||||
class Mrsk::Commands::Prune < Mrsk::Commands::Base
|
||||
PRUNE_IMAGES_AFTER = 30.days.in_hours.to_i
|
||||
PRUNE_CONTAINERS_AFTER = 3.days.in_hours.to_i
|
||||
PRUNE_IMAGES_AFTER = 7.days.in_hours.to_i
|
||||
PRUNE_CONTAINERS_AFTER = 3.days.in_hours.to_i
|
||||
|
||||
def images
|
||||
docker :image, :prune, "-f", "--filter", "until=#{PRUNE_IMAGES_AFTER}h"
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
require "mrsk/commands/base"
|
||||
|
||||
class Mrsk::Commands::Registry < Mrsk::Commands::Base
|
||||
delegate :registry, to: :config
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
require "mrsk/commands/base"
|
||||
|
||||
class Mrsk::Commands::Traefik < Mrsk::Commands::Base
|
||||
def run
|
||||
docker :run, "--name traefik",
|
||||
"-d",
|
||||
"--restart unless-stopped",
|
||||
"--restart", "unless-stopped",
|
||||
"--log-opt", "max-size=#{MAX_LOG_SIZE}",
|
||||
"-p 80:80",
|
||||
"-v /var/run/docker.sock:/var/run/docker.sock",
|
||||
"traefik",
|
||||
"--providers.docker",
|
||||
"--log.level=DEBUG"
|
||||
"--log.level=DEBUG",
|
||||
*cmd_args
|
||||
end
|
||||
|
||||
def start
|
||||
@@ -33,7 +33,7 @@ class Mrsk::Commands::Traefik < Mrsk::Commands::Base
|
||||
def follow_logs(host:, grep: nil)
|
||||
run_over_ssh pipe(
|
||||
docker(:logs, "traefik", "-t", "-n", "10", "-f", "2>&1"),
|
||||
("grep '#{grep}'" if grep)
|
||||
(%(grep "#{grep}") if grep)
|
||||
).join(" "), host: host
|
||||
end
|
||||
|
||||
@@ -44,4 +44,9 @@ class Mrsk::Commands::Traefik < Mrsk::Commands::Base
|
||||
def remove_image
|
||||
docker :image, :prune, "-a", "-f", "--filter", "label=org.opencontainers.image.title=Traefik"
|
||||
end
|
||||
|
||||
private
|
||||
def cmd_args
|
||||
(config.raw_config.dig(:traefik, "args") || { }).collect { |(key, value)| [ "--#{key}", value ] }.flatten
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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
|
||||
@@ -39,7 +40,7 @@ class Mrsk::Configuration
|
||||
def initialize(raw_config, version: "missing", validate: true)
|
||||
@raw_config = ActiveSupport::InheritableOptions.new(raw_config)
|
||||
@version = version
|
||||
ensure_required_keys_present if validate
|
||||
valid? if validate
|
||||
end
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -85,6 +82,10 @@ class Mrsk::Configuration
|
||||
"#{repository}:#{version}"
|
||||
end
|
||||
|
||||
def latest_image
|
||||
"#{repository}:latest"
|
||||
end
|
||||
|
||||
def service_with_version
|
||||
"#{service}-#{version}"
|
||||
end
|
||||
@@ -107,19 +108,30 @@ 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" ] }
|
||||
{ user: ssh_user, proxy: ssh_proxy, auth_methods: [ "publickey" ] }.compact
|
||||
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
|
||||
|
||||
def valid?
|
||||
ensure_required_keys_present && ensure_env_available
|
||||
end
|
||||
|
||||
|
||||
def to_h
|
||||
{
|
||||
roles: role_names,
|
||||
@@ -139,6 +151,7 @@ class Mrsk::Configuration
|
||||
|
||||
|
||||
private
|
||||
# Will raise ArgumentError if any required config keys are missing
|
||||
def ensure_required_keys_present
|
||||
%i[ service image registry servers ].each do |key|
|
||||
raise ArgumentError, "Missing required configuration for #{key}" unless raw_config[key].present?
|
||||
@@ -151,12 +164,19 @@ class Mrsk::Configuration
|
||||
if raw_config.registry["password"].blank?
|
||||
raise ArgumentError, "You must specify a password for the registry in config/deploy.yml (or set the ENV variable if that's used)"
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
# Will raise KeyError if any secret ENVs are missing
|
||||
def ensure_env_available
|
||||
env_args
|
||||
roles.each(&:env_args)
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def role_names
|
||||
raw_config.servers.is_a?(Array) ? [ "web" ] : raw_config.servers.keys.sort
|
||||
end
|
||||
end
|
||||
|
||||
require "mrsk/configuration/role"
|
||||
require "mrsk/configuration/accessory"
|
||||
|
||||
@@ -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
|
||||
@@ -74,12 +74,19 @@ class Mrsk::Configuration::Assessory
|
||||
|
||||
def expand_local_file(local_file)
|
||||
if local_file.end_with?("erb")
|
||||
read_dynamic_file(local_file)
|
||||
with_clear_env_loaded { read_dynamic_file(local_file) }
|
||||
else
|
||||
Pathname.new(File.expand_path(local_file)).to_s
|
||||
end
|
||||
end
|
||||
|
||||
def with_clear_env_loaded
|
||||
(env["clear"] || env).each { |k, v| ENV[k] = v }
|
||||
yield
|
||||
ensure
|
||||
(env["clear"] || env).each { |k, v| ENV.delete(k) }
|
||||
end
|
||||
|
||||
def read_dynamic_file(local_file)
|
||||
StringIO.new(ERB.new(IO.read(local_file)).result)
|
||||
end
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -18,7 +18,7 @@ module Mrsk::Utils
|
||||
if (secrets = env["secret"]).present?
|
||||
argumentize("-e", secrets.to_h { |key| [ key, ENV.fetch(key) ] }, redacted: true) + argumentize("-e", env["clear"])
|
||||
else
|
||||
argumentize "-e", env
|
||||
argumentize "-e", env.fetch("clear", env)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
module Mrsk
|
||||
VERSION = "0.3.1"
|
||||
VERSION = "0.6.2"
|
||||
end
|
||||
|
||||
@@ -6,7 +6,7 @@ Gem::Specification.new do |spec|
|
||||
spec.authors = [ "David Heinemeier Hansson" ]
|
||||
spec.email = "dhh@hey.com"
|
||||
spec.homepage = "https://github.com/rails/mrsk"
|
||||
spec.summary = "Deploy Rails apps in containers to servers running Docker with zero downtime."
|
||||
spec.summary = "Deploy web apps in containers to servers running Docker with zero downtime."
|
||||
spec.license = "MIT"
|
||||
|
||||
spec.files = Dir["lib/**/*", "MIT-LICENSE", "README.md"]
|
||||
@@ -15,4 +15,6 @@ Gem::Specification.new do |spec|
|
||||
spec.add_dependency "activesupport", ">= 7.0"
|
||||
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
|
||||
|
||||
@@ -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 --log-opt max-size=10m -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
67
test/cli/app_test.rb
Normal 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
24
test/cli/cli_test_case.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
require "test_helper"
|
||||
require "mrsk/commander"
|
||||
|
||||
class CommanderTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
@@ -10,6 +9,16 @@ class CommanderTest < ActiveSupport::TestCase
|
||||
assert_equal Mrsk::Configuration, @mrsk.config.class
|
||||
end
|
||||
|
||||
test "commit hash as version" do
|
||||
assert_equal `git rev-parse HEAD`.strip, @mrsk.config.version
|
||||
end
|
||||
|
||||
test "commit hash as version but not in git" do
|
||||
@mrsk.expects(:system).with("git rev-parse").returns(nil)
|
||||
error = assert_raises(RuntimeError) { @mrsk.config }
|
||||
assert_match /no git repository found/, error.message
|
||||
end
|
||||
|
||||
test "overwriting hosts" do
|
||||
assert_equal [ "1.1.1.1", "1.1.1.2", "1.1.1.3", "1.1.1.4" ], @mrsk.hosts
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
require "test_helper"
|
||||
require "mrsk/configuration"
|
||||
require "mrsk/commands/accessory"
|
||||
|
||||
class CommandsAccessoryTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
@@ -41,46 +39,96 @@ 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 --log-opt max-size=10m -p 3306:3306 -e MYSQL_ROOT_PASSWORD=secret123 -e MYSQL_ROOT_HOST=% --label service=app-mysql mysql:8.0",
|
||||
@mysql.run.join(" ")
|
||||
|
||||
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
|
||||
"docker run --name app-redis -d --restart unless-stopped --log-opt max-size=10m -p 6379:6379 -e SOMETHING=else --volume /var/lib/redis:/data --label service=app-redis --label cache=true redis:latest",
|
||||
@redis.run.join(" ")
|
||||
end
|
||||
|
||||
test "start" do
|
||||
assert_equal [:docker, :container, :start, "app-mysql"], @mysql.start
|
||||
assert_equal \
|
||||
"docker container start app-mysql",
|
||||
@mysql.start.join(" ")
|
||||
end
|
||||
|
||||
test "stop" do
|
||||
assert_equal [:docker, :container, :stop, "app-mysql"], @mysql.stop
|
||||
assert_equal \
|
||||
"docker container stop app-mysql",
|
||||
@mysql.stop.join(" ")
|
||||
end
|
||||
|
||||
test "info" do
|
||||
assert_equal [:docker, :ps, "--filter", "label=service=app-mysql"], @mysql.info
|
||||
assert_equal \
|
||||
"docker ps --filter label=service=app-mysql",
|
||||
@mysql.info.join(" ")
|
||||
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").join(" ")
|
||||
end
|
||||
|
||||
test "execute in existing container" do
|
||||
assert_equal \
|
||||
"docker exec app-mysql mysql -u root",
|
||||
@mysql.execute_in_existing_container("mysql", "-u", "root").join(" ")
|
||||
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")
|
||||
assert_equal \
|
||||
"docker logs app-mysql -t 2>&1",
|
||||
@mysql.logs.join(" ")
|
||||
|
||||
assert_equal \
|
||||
"docker logs app-mysql --since 5m -n 100 -t 2>&1 | grep 'thing'",
|
||||
@mysql.logs(since: "5m", lines: 100, grep: "thing").join(" ")
|
||||
end
|
||||
|
||||
test "follow logs" do
|
||||
assert_equal "ssh -t root@1.1.1.5 'docker logs app-mysql -t -n 10 -f 2>&1'", @mysql.follow_logs
|
||||
assert_equal \
|
||||
"ssh -t root@1.1.1.5 'docker logs app-mysql -t -n 10 -f 2>&1'",
|
||||
@mysql.follow_logs
|
||||
end
|
||||
|
||||
test "remove container" do
|
||||
assert_equal [:docker, :container, :prune, "-f", "--filter", "label=service=app-mysql"], @mysql.remove_container
|
||||
assert_equal \
|
||||
"docker container prune -f --filter label=service=app-mysql",
|
||||
@mysql.remove_container.join(" ")
|
||||
end
|
||||
|
||||
test "remove image" do
|
||||
assert_equal [:docker, :image, :prune, "-a", "-f", "--filter", "label=service=app-mysql"], @mysql.remove_image
|
||||
assert_equal \
|
||||
"docker image prune -a -f --filter label=service=app-mysql",
|
||||
@mysql.remove_image.join(" ")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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 --log-opt max-size=10m --name app-999 -e RAILS_MASTER_KEY=456 --label service=app --label role=web --label traefik.http.routers.app.rule='PathPrefix(`/`)' --label traefik.http.services.app.loadbalancer.healthcheck.path=/up --label traefik.http.services.app.loadbalancer.healthcheck.interval=1s --label traefik.http.middlewares.app.retry.attempts=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 --log-opt max-size=10m --name app-999 -e RAILS_MASTER_KEY=456 --volume /local/path:/container/path --label service=app --label role=web --label traefik.http.routers.app.rule='PathPrefix(`/`)' --label traefik.http.services.app.loadbalancer.healthcheck.path=/up --label traefik.http.services.app.loadbalancer.healthcheck.interval=1s --label traefik.http.middlewares.app.retry.attempts=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
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
require "test_helper"
|
||||
require "mrsk/configuration"
|
||||
require "mrsk/commands/builder"
|
||||
|
||||
class CommandsBuilderTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
@@ -10,25 +8,25 @@ class CommandsBuilderTest < ActiveSupport::TestCase
|
||||
test "target multiarch by default" do
|
||||
builder = new_builder_command
|
||||
assert_equal "multiarch", builder.name
|
||||
assert_equal [:docker, :buildx, :build, "--push", "--platform", "linux/amd64,linux/arm64", "--builder", "mrsk-app-multiarch", "-t", "dhh/app:123", "."], builder.push
|
||||
assert_equal [:docker, :buildx, :build, "--push", "--platform", "linux/amd64,linux/arm64", "--builder", "mrsk-app-multiarch", "-t", "dhh/app:123", "-t", "dhh/app:latest", "."], builder.push
|
||||
end
|
||||
|
||||
test "target native when multiarch is off" do
|
||||
builder = new_builder_command(builder: { "multiarch" => false })
|
||||
assert_equal "native", builder.name
|
||||
assert_equal [:docker, :build, "-t", "dhh/app:123", ".", "&&", :docker, :push, "dhh/app:123"], builder.push
|
||||
assert_equal [:docker, :build, "-t", "dhh/app:123", "-t", "dhh/app:latest", ".", "&&", :docker, :push, "dhh/app:123"], builder.push
|
||||
end
|
||||
|
||||
test "target multiarch remote when local and remote is set" do
|
||||
builder = new_builder_command(builder: { "local" => { }, "remote" => { } })
|
||||
assert_equal "multiarch/remote", builder.name
|
||||
assert_equal [:docker, :buildx, :build, "--push", "--platform", "linux/amd64,linux/arm64", "--builder", "mrsk-app-multiarch-remote", "-t", "dhh/app:123", "."], builder.push
|
||||
assert_equal [:docker, :buildx, :build, "--push", "--platform", "linux/amd64,linux/arm64", "--builder", "mrsk-app-multiarch-remote", "-t", "dhh/app:123", "-t", "dhh/app:latest", "."], builder.push
|
||||
end
|
||||
|
||||
test "target native remote when only remote is set" do
|
||||
builder = new_builder_command(builder: { "remote" => { "arch" => "amd64" } })
|
||||
assert_equal "native/remote", builder.name
|
||||
assert_equal [:docker, :buildx, :build, "--push", "--platform", "linux/amd64", "--builder", "mrsk-app-native-remote", "-t", "dhh/app:123", "."], builder.push
|
||||
assert_equal [:docker, :buildx, :build, "--push", "--platform", "linux/amd64", "--builder", "mrsk-app-native-remote", "-t", "dhh/app:123", "-t", "dhh/app:latest", "."], builder.push
|
||||
end
|
||||
|
||||
test "build args" do
|
||||
@@ -43,17 +41,17 @@ class CommandsBuilderTest < ActiveSupport::TestCase
|
||||
|
||||
test "native push with build args" do
|
||||
builder = new_builder_command(builder: { "multiarch" => false, "args" => { "a" => 1, "b" => 2 } })
|
||||
assert_equal [ :docker, :build, "-t", "--build-arg", "a=1", "--build-arg", "b=2", "dhh/app:123", ".", "&&", :docker, :push, "dhh/app:123" ], builder.push
|
||||
assert_equal [ :docker, :build, "-t", "dhh/app:123", "-t", "dhh/app:latest", "--build-arg", "a=1", "--build-arg", "b=2", ".", "&&", :docker, :push, "dhh/app:123" ], builder.push
|
||||
end
|
||||
|
||||
test "multiarch push with build args" do
|
||||
builder = new_builder_command(builder: { "args" => { "a" => 1, "b" => 2 } })
|
||||
assert_equal [ :docker, :buildx, :build, "--push", "--platform", "linux/amd64,linux/arm64", "--builder", "mrsk-app-multiarch", "-t", "dhh/app:123", "--build-arg", "a=1", "--build-arg", "b=2", "." ], builder.push
|
||||
assert_equal [ :docker, :buildx, :build, "--push", "--platform", "linux/amd64,linux/arm64", "--builder", "mrsk-app-multiarch", "-t", "dhh/app:123", "-t", "dhh/app:latest", "--build-arg", "a=1", "--build-arg", "b=2", "." ], builder.push
|
||||
end
|
||||
|
||||
test "native push with with build secrets" do
|
||||
builder = new_builder_command(builder: { "multiarch" => false, "secrets" => [ "a", "b" ] })
|
||||
assert_equal [ :docker, :build, "-t", "--secret", "id=a", "--secret", "id=b", "dhh/app:123", ".", "&&", :docker, :push, "dhh/app:123" ], builder.push
|
||||
assert_equal [ :docker, :build, "-t", "dhh/app:123", "-t", "dhh/app:latest", "--secret", "id=a", "--secret", "id=b", ".", "&&", :docker, :push, "dhh/app:123" ], builder.push
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
23
test/commands/registry_test.rb
Executable file
23
test/commands/registry_test.rb
Executable 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
|
||||
87
test/commands/traefik_test.rb
Normal file
87
test/commands/traefik_test.rb
Normal file
@@ -0,0 +1,87 @@
|
||||
require "test_helper"
|
||||
|
||||
class CommandsTraefikTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
@config = {
|
||||
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ],
|
||||
traefik: { "args" => { "accesslog.format" => "json", "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } }
|
||||
}
|
||||
end
|
||||
|
||||
test "run" do
|
||||
assert_equal \
|
||||
"docker run --name traefik -d --restart unless-stopped --log-opt max-size=10m -p 80:80 -v /var/run/docker.sock:/var/run/docker.sock traefik --providers.docker --log.level=DEBUG --accesslog.format json --metrics.prometheus.buckets 0.1,0.3,1.2,5.0",
|
||||
new_command.run.join(" ")
|
||||
end
|
||||
|
||||
test "traefik start" do
|
||||
assert_equal \
|
||||
"docker container start traefik",
|
||||
new_command.start.join(" ")
|
||||
end
|
||||
|
||||
test "traefik stop" do
|
||||
assert_equal \
|
||||
"docker container stop traefik",
|
||||
new_command.stop.join(" ")
|
||||
end
|
||||
|
||||
test "traefik info" do
|
||||
assert_equal \
|
||||
"docker ps --filter name=traefik",
|
||||
new_command.info.join(" ")
|
||||
end
|
||||
|
||||
test "traefik logs" do
|
||||
assert_equal \
|
||||
"docker logs traefik -t 2>&1",
|
||||
new_command.logs.join(" ")
|
||||
end
|
||||
|
||||
test "traefik logs since 2h" do
|
||||
assert_equal \
|
||||
"docker logs traefik --since 2h -t 2>&1",
|
||||
new_command.logs(since: '2h').join(" ")
|
||||
end
|
||||
|
||||
test "traefik logs last 10 lines" do
|
||||
assert_equal \
|
||||
"docker logs traefik -n 10 -t 2>&1",
|
||||
new_command.logs(lines: 10).join(" ")
|
||||
end
|
||||
|
||||
test "traefik logs with grep hello!" do
|
||||
assert_equal \
|
||||
"docker logs traefik -t 2>&1 | grep 'hello!'",
|
||||
new_command.logs(grep: 'hello!').join(" ")
|
||||
end
|
||||
|
||||
test "traefik remove container" do
|
||||
assert_equal \
|
||||
"docker container prune -f --filter label=org.opencontainers.image.title=Traefik",
|
||||
new_command.remove_container.join(" ")
|
||||
end
|
||||
|
||||
test "traefik remove image" do
|
||||
assert_equal \
|
||||
"docker image prune -a -f --filter label=org.opencontainers.image.title=Traefik",
|
||||
new_command.remove_image.join(" ")
|
||||
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"))
|
||||
end
|
||||
end
|
||||
@@ -1,5 +1,4 @@
|
||||
require "test_helper"
|
||||
require "mrsk/configuration"
|
||||
|
||||
class ConfigurationAccessoryTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
@@ -98,7 +97,8 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase
|
||||
@deploy[:accessories]["mysql"]["files"] << "test/fixtures/files/structure.sql.erb:/docker-entrypoint-initdb.d/structure.sql"
|
||||
@config = Mrsk::Configuration.new(@deploy)
|
||||
|
||||
assert_equal "This was dynamically expanded", @config.accessory(:mysql).files.keys[2].read
|
||||
assert_match "This was dynamically expanded", @config.accessory(:mysql).files.keys[2].read
|
||||
assert_match "%", @config.accessory(:mysql).files.keys[2].read
|
||||
end
|
||||
|
||||
test "directories" do
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
require "test_helper"
|
||||
require "mrsk/configuration"
|
||||
|
||||
class ConfigurationRoleTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
require "test_helper"
|
||||
require "mrsk/configuration"
|
||||
|
||||
class ConfigurationTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
@@ -105,6 +104,14 @@ class ConfigurationTest < ActiveSupport::TestCase
|
||||
ENV["PASSWORD"] = nil
|
||||
end
|
||||
|
||||
test "env args with only clear" do
|
||||
config = Mrsk::Configuration.new(@deploy.tap { |c| c.merge!({
|
||||
env: { "clear" => { "PORT" => "3000" } }
|
||||
}) })
|
||||
|
||||
assert_equal [ "-e", "PORT=3000" ], config.env_args
|
||||
end
|
||||
|
||||
test "env args with only secrets" do
|
||||
ENV["PASSWORD"] = "secret123"
|
||||
config = Mrsk::Configuration.new(@deploy.tap { |c| c.merge!({
|
||||
@@ -118,29 +125,32 @@ class ConfigurationTest < ActiveSupport::TestCase
|
||||
end
|
||||
|
||||
test "env args with missing secret" do
|
||||
config = Mrsk::Configuration.new(@deploy.tap { |c| c.merge!({
|
||||
env: { "secret" => [ "PASSWORD" ] }
|
||||
}) })
|
||||
|
||||
assert_raises(KeyError) do
|
||||
assert_equal [ "-e", "PASSWORD=secret123" ], config.env_args
|
||||
config = Mrsk::Configuration.new(@deploy.tap { |c| c.merge!({
|
||||
env: { "secret" => [ "PASSWORD" ] }
|
||||
}) })
|
||||
end
|
||||
end
|
||||
|
||||
test "valid config" do
|
||||
assert @config.valid?
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
1
test/fixtures/files/structure.sql.erb
vendored
1
test/fixtures/files/structure.sql.erb
vendored
@@ -1 +1,2 @@
|
||||
<%= "This was dynamically expanded" %>
|
||||
<%= ENV["MYSQL_ROOT_HOST"] %>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user