Deploy locks
Add a deploy lock for commands that are unsafe to run concurrently.
The lock is taken by creating a `mrsk_lock` directory on the primary
host. Details of who took the lock are added to a details file in that
directory.
Additional CLI commands have been added to manual release and acquire
the lock and to check its status.
```
Commands:
mrsk lock acquire -m, --message=MESSAGE # Acquire the deploy lock
mrsk lock help [COMMAND] # Describe subcommands or one specific subcommand
mrsk lock release # Release the deploy lock
mrsk lock status # Report lock status
Options:
-v, [--verbose], [--no-verbose] # Detailed logging
-q, [--quiet], [--no-quiet] # Minimal logging
[--version=VERSION] # Run commands against a specific app version
-p, [--primary], [--no-primary] # Run commands only on primary host instead of all
-h, [--hosts=HOSTS] # Run commands on these hosts instead of all (separate by comma)
-r, [--roles=ROLES] # Run commands on these roles instead of all (separate by comma)
-c, [--config-file=CONFIG_FILE] # Path to config file
# Default: config/deploy.yml
-d, [--destination=DESTINATION] # Specify destination to be used for config file (staging -> deploy.staging.yml)
-B, [--skip-broadcast], [--no-skip-broadcast] # Skip audit broadcasts
```
If we add support for running multiple deployments on a single server
we'll need to extend the locking to lock per deployment.
This commit is contained in:
24
README.md
24
README.md
@@ -692,6 +692,30 @@ Note that by default old containers are pruned after 3 days when you run `mrsk d
|
|||||||
|
|
||||||
If you wish to remove the entire application, including Traefik, containers, images, and registry session, you can run `mrsk remove`. This will leave the servers clean.
|
If you wish to remove the entire application, including Traefik, containers, images, and registry session, you can run `mrsk remove`. This will leave the servers clean.
|
||||||
|
|
||||||
|
## Locking
|
||||||
|
|
||||||
|
Commands that are unsafe to run concurrently will take a deploy lock while they run. The lock is the `mrsk_lock` directory on the primary server.
|
||||||
|
|
||||||
|
You can check the lock status with:
|
||||||
|
|
||||||
|
```
|
||||||
|
mrsk lock status
|
||||||
|
|
||||||
|
Locked by: AN Other at 2023-03-24 09:49:03 UTC
|
||||||
|
Version: 77f45c0686811c68989d6576748475a60bf53fc2
|
||||||
|
Message: Automatic deploy lock
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also manually acquire and release the lock
|
||||||
|
|
||||||
|
```
|
||||||
|
mrsk lock acquire -m "Doing maintanence"
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
mrsk lock release
|
||||||
|
```
|
||||||
|
|
||||||
## Stage of development
|
## Stage of development
|
||||||
|
|
||||||
This is beta software. Commands may still move around. But we're live in production at [37signals](https://37signals.com).
|
This is beta software. Commands may still move around. But we're live in production at [37signals](https://37signals.com).
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
class Mrsk::Cli::Accessory < Mrsk::Cli::Base
|
class Mrsk::Cli::Accessory < Mrsk::Cli::Base
|
||||||
desc "boot [NAME]", "Boot new accessory service on host (use NAME=all to boot all accessories)"
|
desc "boot [NAME]", "Boot new accessory service on host (use NAME=all to boot all accessories)"
|
||||||
def boot(name)
|
def boot(name)
|
||||||
|
with_lock do
|
||||||
if name == "all"
|
if name == "all"
|
||||||
MRSK.accessory_names.each { |accessory_name| boot(accessory_name) }
|
MRSK.accessory_names.each { |accessory_name| boot(accessory_name) }
|
||||||
else
|
else
|
||||||
@@ -18,9 +19,11 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
desc "upload [NAME]", "Upload accessory files to host", hide: true
|
desc "upload [NAME]", "Upload accessory files to host", hide: true
|
||||||
def upload(name)
|
def upload(name)
|
||||||
|
with_lock do
|
||||||
with_accessory(name) do |accessory|
|
with_accessory(name) do |accessory|
|
||||||
on(accessory.host) do
|
on(accessory.host) do
|
||||||
accessory.files.each do |(local, remote)|
|
accessory.files.each do |(local, remote)|
|
||||||
@@ -33,9 +36,11 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
desc "directories [NAME]", "Create accessory directories on host", hide: true
|
desc "directories [NAME]", "Create accessory directories on host", hide: true
|
||||||
def directories(name)
|
def directories(name)
|
||||||
|
with_lock do
|
||||||
with_accessory(name) do |accessory|
|
with_accessory(name) do |accessory|
|
||||||
on(accessory.host) do
|
on(accessory.host) do
|
||||||
accessory.directories.keys.each do |host_path|
|
accessory.directories.keys.each do |host_path|
|
||||||
@@ -44,18 +49,22 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
desc "reboot [NAME]", "Reboot existing accessory on host (stop container, remove container, start new container)"
|
desc "reboot [NAME]", "Reboot existing accessory on host (stop container, remove container, start new container)"
|
||||||
def reboot(name)
|
def reboot(name)
|
||||||
|
with_lock do
|
||||||
with_accessory(name) do |accessory|
|
with_accessory(name) do |accessory|
|
||||||
stop(name)
|
stop(name)
|
||||||
remove_container(name)
|
remove_container(name)
|
||||||
boot(name)
|
boot(name)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
desc "start [NAME]", "Start existing accessory container on host"
|
desc "start [NAME]", "Start existing accessory container on host"
|
||||||
def start(name)
|
def start(name)
|
||||||
|
with_lock do
|
||||||
with_accessory(name) do |accessory|
|
with_accessory(name) do |accessory|
|
||||||
on(accessory.host) do
|
on(accessory.host) do
|
||||||
execute *MRSK.auditor.record("Started #{name} accessory"), verbosity: :debug
|
execute *MRSK.auditor.record("Started #{name} accessory"), verbosity: :debug
|
||||||
@@ -63,9 +72,11 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
desc "stop [NAME]", "Stop existing accessory container on host"
|
desc "stop [NAME]", "Stop existing accessory container on host"
|
||||||
def stop(name)
|
def stop(name)
|
||||||
|
with_lock do
|
||||||
with_accessory(name) do |accessory|
|
with_accessory(name) do |accessory|
|
||||||
on(accessory.host) do
|
on(accessory.host) do
|
||||||
execute *MRSK.auditor.record("Stopped #{name} accessory"), verbosity: :debug
|
execute *MRSK.auditor.record("Stopped #{name} accessory"), verbosity: :debug
|
||||||
@@ -73,14 +84,17 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
desc "restart [NAME]", "Restart existing accessory container on host"
|
desc "restart [NAME]", "Restart existing accessory container on host"
|
||||||
def restart(name)
|
def restart(name)
|
||||||
|
with_lock do
|
||||||
with_accessory(name) do
|
with_accessory(name) do
|
||||||
stop(name)
|
stop(name)
|
||||||
start(name)
|
start(name)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
desc "details [NAME]", "Show details about accessory on host (use NAME=all to show all accessories)"
|
desc "details [NAME]", "Show details about accessory on host (use NAME=all to show all accessories)"
|
||||||
def details(name)
|
def details(name)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
class Mrsk::Cli::App < Mrsk::Cli::Base
|
class Mrsk::Cli::App < Mrsk::Cli::Base
|
||||||
desc "boot", "Boot app on servers (or reboot app if already running)"
|
desc "boot", "Boot app on servers (or reboot app if already running)"
|
||||||
def boot
|
def boot
|
||||||
|
with_lock do
|
||||||
say "Get most recent version available as an image...", :magenta unless options[:version]
|
say "Get most recent version available as an image...", :magenta unless options[:version]
|
||||||
using_version(version_or_latest) do |version|
|
using_version(version_or_latest) do |version|
|
||||||
say "Start container with version #{version} using a #{MRSK.config.readiness_delay}s readiness delay (or reboot if already running)...", :magenta
|
say "Start container with version #{version} using a #{MRSK.config.readiness_delay}s readiness delay (or reboot if already running)...", :magenta
|
||||||
@@ -33,22 +34,27 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
desc "start", "Start existing app container on servers"
|
desc "start", "Start existing app container on servers"
|
||||||
def start
|
def start
|
||||||
|
with_lock do
|
||||||
on(MRSK.hosts) do
|
on(MRSK.hosts) do
|
||||||
execute *MRSK.auditor.record("Started app version #{MRSK.config.version}"), verbosity: :debug
|
execute *MRSK.auditor.record("Started app version #{MRSK.config.version}"), verbosity: :debug
|
||||||
execute *MRSK.app.start, raise_on_non_zero_exit: false
|
execute *MRSK.app.start, raise_on_non_zero_exit: false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
desc "stop", "Stop app container on servers"
|
desc "stop", "Stop app container on servers"
|
||||||
def stop
|
def stop
|
||||||
|
with_lock do
|
||||||
on(MRSK.hosts) do
|
on(MRSK.hosts) do
|
||||||
execute *MRSK.auditor.record("Stopped app"), verbosity: :debug
|
execute *MRSK.auditor.record("Stopped app"), verbosity: :debug
|
||||||
execute *MRSK.app.stop, raise_on_non_zero_exit: false
|
execute *MRSK.app.stop, raise_on_non_zero_exit: false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# FIXME: Drop in favor of just containers?
|
# FIXME: Drop in favor of just containers?
|
||||||
desc "details", "Show details about app containers"
|
desc "details", "Show details about app containers"
|
||||||
@@ -140,34 +146,42 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
|
|||||||
|
|
||||||
desc "remove", "Remove app containers and images from servers"
|
desc "remove", "Remove app containers and images from servers"
|
||||||
def remove
|
def remove
|
||||||
|
with_lock do
|
||||||
stop
|
stop
|
||||||
remove_containers
|
remove_containers
|
||||||
remove_images
|
remove_images
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
desc "remove_container [VERSION]", "Remove app container with given version from servers", hide: true
|
desc "remove_container [VERSION]", "Remove app container with given version from servers", hide: true
|
||||||
def remove_container(version)
|
def remove_container(version)
|
||||||
|
with_lock do
|
||||||
on(MRSK.hosts) do
|
on(MRSK.hosts) do
|
||||||
execute *MRSK.auditor.record("Removed app container with version #{version}"), verbosity: :debug
|
execute *MRSK.auditor.record("Removed app container with version #{version}"), verbosity: :debug
|
||||||
execute *MRSK.app.remove_container(version: version)
|
execute *MRSK.app.remove_container(version: version)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
desc "remove_containers", "Remove all app containers from servers", hide: true
|
desc "remove_containers", "Remove all app containers from servers", hide: true
|
||||||
def remove_containers
|
def remove_containers
|
||||||
|
with_lock do
|
||||||
on(MRSK.hosts) do
|
on(MRSK.hosts) do
|
||||||
execute *MRSK.auditor.record("Removed all app containers"), verbosity: :debug
|
execute *MRSK.auditor.record("Removed all app containers"), verbosity: :debug
|
||||||
execute *MRSK.app.remove_containers
|
execute *MRSK.app.remove_containers
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
desc "remove_images", "Remove all app images from servers", hide: true
|
desc "remove_images", "Remove all app images from servers", hide: true
|
||||||
def remove_images
|
def remove_images
|
||||||
|
with_lock do
|
||||||
on(MRSK.hosts) do
|
on(MRSK.hosts) do
|
||||||
execute *MRSK.auditor.record("Removed all app images"), verbosity: :debug
|
execute *MRSK.auditor.record("Removed all app images"), verbosity: :debug
|
||||||
execute *MRSK.app.remove_images
|
execute *MRSK.app.remove_images
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
desc "version", "Show app version currently running on servers"
|
desc "version", "Show app version currently running on servers"
|
||||||
def version
|
def version
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ module Mrsk::Cli
|
|||||||
class Base < Thor
|
class Base < Thor
|
||||||
include SSHKit::DSL
|
include SSHKit::DSL
|
||||||
|
|
||||||
|
class LockError < StandardError; end
|
||||||
|
|
||||||
def self.exit_on_failure?() true end
|
def self.exit_on_failure?() true end
|
||||||
|
|
||||||
class_option :verbose, type: :boolean, aliases: "-v", desc: "Detailed logging"
|
class_option :verbose, type: :boolean, aliases: "-v", desc: "Detailed logging"
|
||||||
@@ -71,5 +73,35 @@ module Mrsk::Cli
|
|||||||
def audit_broadcast(line)
|
def audit_broadcast(line)
|
||||||
run_locally { execute *MRSK.auditor.broadcast(line), verbosity: :debug }
|
run_locally { execute *MRSK.auditor.broadcast(line), verbosity: :debug }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def with_lock
|
||||||
|
acquire_lock
|
||||||
|
|
||||||
|
yield
|
||||||
|
ensure
|
||||||
|
release_lock
|
||||||
|
end
|
||||||
|
|
||||||
|
def acquire_lock
|
||||||
|
if MRSK.lock_count == 0
|
||||||
|
say "Acquiring the deploy lock"
|
||||||
|
on(MRSK.primary_host) { execute *MRSK.lock.acquire("Automatic deploy lock", MRSK.config.version) }
|
||||||
|
end
|
||||||
|
MRSK.lock_count += 1
|
||||||
|
rescue SSHKit::Runner::ExecuteError => e
|
||||||
|
if e.message =~ /cannot create directory/
|
||||||
|
invoke "mrsk:cli:lock:status", []
|
||||||
|
end
|
||||||
|
|
||||||
|
raise LockError, "Deploy lock found"
|
||||||
|
end
|
||||||
|
|
||||||
|
def release_lock
|
||||||
|
MRSK.lock_count -= 1
|
||||||
|
if MRSK.lock_count == 0
|
||||||
|
say "Releasing the deploy lock"
|
||||||
|
on(MRSK.primary_host) { execute *MRSK.lock.release }
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
class Mrsk::Cli::Build < Mrsk::Cli::Base
|
class Mrsk::Cli::Build < Mrsk::Cli::Base
|
||||||
desc "deliver", "Build app and push app image to registry then pull image on servers"
|
desc "deliver", "Build app and push app image to registry then pull image on servers"
|
||||||
def deliver
|
def deliver
|
||||||
|
with_lock do
|
||||||
push
|
push
|
||||||
pull
|
pull
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
desc "push", "Build and push app image to registry"
|
desc "push", "Build and push app image to registry"
|
||||||
def push
|
def push
|
||||||
|
with_lock do
|
||||||
cli = self
|
cli = self
|
||||||
|
|
||||||
run_locally do
|
run_locally do
|
||||||
@@ -25,18 +28,22 @@ class Mrsk::Cli::Build < Mrsk::Cli::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
desc "pull", "Pull app image from registry onto servers"
|
desc "pull", "Pull app image from registry onto servers"
|
||||||
def pull
|
def pull
|
||||||
|
with_lock do
|
||||||
on(MRSK.hosts) do
|
on(MRSK.hosts) do
|
||||||
execute *MRSK.auditor.record("Pulled image with version #{MRSK.config.version}"), verbosity: :debug
|
execute *MRSK.auditor.record("Pulled image with version #{MRSK.config.version}"), verbosity: :debug
|
||||||
execute *MRSK.builder.clean, raise_on_non_zero_exit: false
|
execute *MRSK.builder.clean, raise_on_non_zero_exit: false
|
||||||
execute *MRSK.builder.pull
|
execute *MRSK.builder.pull
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
desc "create", "Create a build setup"
|
desc "create", "Create a build setup"
|
||||||
def create
|
def create
|
||||||
|
with_lock do
|
||||||
run_locally do
|
run_locally do
|
||||||
begin
|
begin
|
||||||
debug "Using builder: #{MRSK.builder.name}"
|
debug "Using builder: #{MRSK.builder.name}"
|
||||||
@@ -51,14 +58,17 @@ class Mrsk::Cli::Build < Mrsk::Cli::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
desc "remove", "Remove build setup"
|
desc "remove", "Remove build setup"
|
||||||
def remove
|
def remove
|
||||||
|
with_lock do
|
||||||
run_locally do
|
run_locally do
|
||||||
debug "Using builder: #{MRSK.builder.name}"
|
debug "Using builder: #{MRSK.builder.name}"
|
||||||
execute *MRSK.builder.remove
|
execute *MRSK.builder.remove
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
desc "details", "Show build setup"
|
desc "details", "Show build setup"
|
||||||
def details
|
def details
|
||||||
|
|||||||
37
lib/mrsk/cli/lock.rb
Normal file
37
lib/mrsk/cli/lock.rb
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
class Mrsk::Cli::Lock < Mrsk::Cli::Base
|
||||||
|
desc "status", "Report lock status"
|
||||||
|
def status
|
||||||
|
handle_missing_lock do
|
||||||
|
on(MRSK.primary_host) { puts capture_with_info(*MRSK.lock.status) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "acquire", "Acquire the deploy lock"
|
||||||
|
option :message, aliases: "-m", type: :string, desc: "A lock mesasge", required: true
|
||||||
|
def acquire
|
||||||
|
message = options[:message]
|
||||||
|
handle_missing_lock do
|
||||||
|
on(MRSK.primary_host) { execute *MRSK.lock.acquire(message, MRSK.config.version) }
|
||||||
|
say "Set the deploy lock"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "release", "Release the deploy lock"
|
||||||
|
def release
|
||||||
|
handle_missing_lock do
|
||||||
|
on(MRSK.primary_host) { execute *MRSK.lock.release }
|
||||||
|
say "Removed the deploy lock"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def handle_missing_lock
|
||||||
|
yield
|
||||||
|
rescue SSHKit::Runner::ExecuteError => e
|
||||||
|
if e.message =~ /No such file or directory/
|
||||||
|
say "There is no deploy lock"
|
||||||
|
else
|
||||||
|
raise
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,16 +1,19 @@
|
|||||||
class Mrsk::Cli::Main < Mrsk::Cli::Base
|
class Mrsk::Cli::Main < Mrsk::Cli::Base
|
||||||
desc "setup", "Setup all accessories and deploy app to servers"
|
desc "setup", "Setup all accessories and deploy app to servers"
|
||||||
def setup
|
def setup
|
||||||
|
with_lock do
|
||||||
print_runtime do
|
print_runtime do
|
||||||
invoke "mrsk:cli:server:bootstrap"
|
invoke "mrsk:cli:server:bootstrap"
|
||||||
invoke "mrsk:cli:accessory:boot", [ "all" ]
|
invoke "mrsk:cli:accessory:boot", [ "all" ]
|
||||||
deploy
|
deploy
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
desc "deploy", "Deploy app to servers"
|
desc "deploy", "Deploy app to servers"
|
||||||
option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip image build and push"
|
option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip image build and push"
|
||||||
def deploy
|
def deploy
|
||||||
|
with_lock do
|
||||||
invoke_options = deploy_options
|
invoke_options = deploy_options
|
||||||
|
|
||||||
runtime = print_runtime do
|
runtime = print_runtime do
|
||||||
@@ -42,10 +45,12 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
|
|||||||
|
|
||||||
audit_broadcast "Deployed #{service_version} in #{runtime.round} seconds" unless options[:skip_broadcast]
|
audit_broadcast "Deployed #{service_version} in #{runtime.round} seconds" unless options[:skip_broadcast]
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
desc "redeploy", "Deploy app to servers without bootstrapping servers, starting Traefik, pruning, and registry login"
|
desc "redeploy", "Deploy app to servers without bootstrapping servers, starting Traefik, pruning, and registry login"
|
||||||
option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip image build and push"
|
option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip image build and push"
|
||||||
def redeploy
|
def redeploy
|
||||||
|
with_lock do
|
||||||
invoke_options = deploy_options
|
invoke_options = deploy_options
|
||||||
|
|
||||||
runtime = print_runtime do
|
runtime = print_runtime do
|
||||||
@@ -65,9 +70,11 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
|
|||||||
|
|
||||||
audit_broadcast "Redeployed #{service_version} in #{runtime.round} seconds" unless options[:skip_broadcast]
|
audit_broadcast "Redeployed #{service_version} in #{runtime.round} seconds" unless options[:skip_broadcast]
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
desc "rollback [VERSION]", "Rollback app to VERSION"
|
desc "rollback [VERSION]", "Rollback app to VERSION"
|
||||||
def rollback(version)
|
def rollback(version)
|
||||||
|
with_lock do
|
||||||
MRSK.config.version = version
|
MRSK.config.version = version
|
||||||
|
|
||||||
if container_name_available?(MRSK.config.service_with_version)
|
if container_name_available?(MRSK.config.service_with_version)
|
||||||
@@ -93,6 +100,7 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
|
|||||||
say "The app version '#{version}' is not available as a container (use 'mrsk app containers' for available versions)", :red
|
say "The app version '#{version}' is not available as a container (use 'mrsk app containers' for available versions)", :red
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
desc "details", "Show details about all containers"
|
desc "details", "Show details about all containers"
|
||||||
def details
|
def details
|
||||||
@@ -163,6 +171,7 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
|
|||||||
desc "remove", "Remove Traefik, app, accessories, and registry session from servers"
|
desc "remove", "Remove Traefik, app, accessories, and registry session from servers"
|
||||||
option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
|
option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
|
||||||
def remove
|
def remove
|
||||||
|
with_lock do
|
||||||
if options[:confirmed] || ask("This will remove all containers and images. Are you sure?", limited_to: %w( y N ), default: "N") == "y"
|
if options[:confirmed] || ask("This will remove all containers and images. Are you sure?", limited_to: %w( y N ), default: "N") == "y"
|
||||||
invoke "mrsk:cli:traefik:remove", [], options.without(:confirmed)
|
invoke "mrsk:cli:traefik:remove", [], options.without(:confirmed)
|
||||||
invoke "mrsk:cli:app:remove", [], options.without(:confirmed)
|
invoke "mrsk:cli:app:remove", [], options.without(:confirmed)
|
||||||
@@ -170,6 +179,7 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
|
|||||||
invoke "mrsk:cli:registry:logout", [], options.without(:confirmed)
|
invoke "mrsk:cli:registry:logout", [], options.without(:confirmed)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
desc "version", "Show MRSK version"
|
desc "version", "Show MRSK version"
|
||||||
def version
|
def version
|
||||||
@@ -200,6 +210,9 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
|
|||||||
desc "traefik", "Manage Traefik load balancer"
|
desc "traefik", "Manage Traefik load balancer"
|
||||||
subcommand "traefik", Mrsk::Cli::Traefik
|
subcommand "traefik", Mrsk::Cli::Traefik
|
||||||
|
|
||||||
|
desc "lock", "Manage the deploy lock"
|
||||||
|
subcommand "lock", Mrsk::Cli::Lock
|
||||||
|
|
||||||
private
|
private
|
||||||
def container_name_available?(container_name, host: MRSK.primary_host)
|
def container_name_available?(container_name, host: MRSK.primary_host)
|
||||||
container_names = nil
|
container_names = nil
|
||||||
|
|||||||
@@ -1,23 +1,29 @@
|
|||||||
class Mrsk::Cli::Prune < Mrsk::Cli::Base
|
class Mrsk::Cli::Prune < Mrsk::Cli::Base
|
||||||
desc "all", "Prune unused images and stopped containers"
|
desc "all", "Prune unused images and stopped containers"
|
||||||
def all
|
def all
|
||||||
|
with_lock do
|
||||||
containers
|
containers
|
||||||
images
|
images
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
desc "images", "Prune unused images older than 7 days"
|
desc "images", "Prune unused images older than 7 days"
|
||||||
def images
|
def images
|
||||||
|
with_lock do
|
||||||
on(MRSK.hosts) do
|
on(MRSK.hosts) do
|
||||||
execute *MRSK.auditor.record("Pruned images"), verbosity: :debug
|
execute *MRSK.auditor.record("Pruned images"), verbosity: :debug
|
||||||
execute *MRSK.prune.images
|
execute *MRSK.prune.images
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
desc "containers", "Prune stopped containers older than 3 days"
|
desc "containers", "Prune stopped containers older than 3 days"
|
||||||
def containers
|
def containers
|
||||||
|
with_lock do
|
||||||
on(MRSK.hosts) do
|
on(MRSK.hosts) do
|
||||||
execute *MRSK.auditor.record("Pruned containers"), verbosity: :debug
|
execute *MRSK.auditor.record("Pruned containers"), verbosity: :debug
|
||||||
execute *MRSK.prune.containers
|
execute *MRSK.prune.containers
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
class Mrsk::Cli::Server < Mrsk::Cli::Base
|
class Mrsk::Cli::Server < Mrsk::Cli::Base
|
||||||
desc "bootstrap", "Ensure curl and Docker are installed on servers"
|
desc "bootstrap", "Ensure curl and Docker are installed on servers"
|
||||||
def bootstrap
|
def bootstrap
|
||||||
|
with_lock do
|
||||||
on(MRSK.hosts + MRSK.accessory_hosts) do
|
on(MRSK.hosts + MRSK.accessory_hosts) do
|
||||||
dependencies_to_install = Array.new.tap do |dependencies|
|
dependencies_to_install = Array.new.tap do |dependencies|
|
||||||
dependencies << "curl" unless execute "which curl", raise_on_non_zero_exit: false
|
dependencies << "curl" unless execute "which curl", raise_on_non_zero_exit: false
|
||||||
@@ -12,4 +13,5 @@ class Mrsk::Cli::Server < Mrsk::Cli::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,37 +1,47 @@
|
|||||||
class Mrsk::Cli::Traefik < Mrsk::Cli::Base
|
class Mrsk::Cli::Traefik < Mrsk::Cli::Base
|
||||||
desc "boot", "Boot Traefik on servers"
|
desc "boot", "Boot Traefik on servers"
|
||||||
def boot
|
def boot
|
||||||
|
with_lock do
|
||||||
on(MRSK.traefik_hosts) { execute *MRSK.traefik.run, raise_on_non_zero_exit: false }
|
on(MRSK.traefik_hosts) { execute *MRSK.traefik.run, raise_on_non_zero_exit: false }
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
desc "reboot", "Reboot Traefik on servers (stop container, remove container, start new container)"
|
desc "reboot", "Reboot Traefik on servers (stop container, remove container, start new container)"
|
||||||
def reboot
|
def reboot
|
||||||
|
with_lock do
|
||||||
stop
|
stop
|
||||||
remove_container
|
remove_container
|
||||||
boot
|
boot
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
desc "start", "Start existing Traefik container on servers"
|
desc "start", "Start existing Traefik container on servers"
|
||||||
def start
|
def start
|
||||||
|
with_lock do
|
||||||
on(MRSK.traefik_hosts) do
|
on(MRSK.traefik_hosts) do
|
||||||
execute *MRSK.auditor.record("Started traefik"), verbosity: :debug
|
execute *MRSK.auditor.record("Started traefik"), verbosity: :debug
|
||||||
execute *MRSK.traefik.start, raise_on_non_zero_exit: false
|
execute *MRSK.traefik.start, raise_on_non_zero_exit: false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
desc "stop", "Stop existing Traefik container on servers"
|
desc "stop", "Stop existing Traefik container on servers"
|
||||||
def stop
|
def stop
|
||||||
|
with_lock do
|
||||||
on(MRSK.traefik_hosts) do
|
on(MRSK.traefik_hosts) do
|
||||||
execute *MRSK.auditor.record("Stopped traefik"), verbosity: :debug
|
execute *MRSK.auditor.record("Stopped traefik"), verbosity: :debug
|
||||||
execute *MRSK.traefik.stop, raise_on_non_zero_exit: false
|
execute *MRSK.traefik.stop, raise_on_non_zero_exit: false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
desc "restart", "Restart existing Traefik container on servers"
|
desc "restart", "Restart existing Traefik container on servers"
|
||||||
def restart
|
def restart
|
||||||
|
with_lock do
|
||||||
stop
|
stop
|
||||||
start
|
start
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
desc "details", "Show details about Traefik container from servers"
|
desc "details", "Show details about Traefik container from servers"
|
||||||
def details
|
def details
|
||||||
@@ -64,24 +74,30 @@ class Mrsk::Cli::Traefik < Mrsk::Cli::Base
|
|||||||
|
|
||||||
desc "remove", "Remove Traefik container and image from servers"
|
desc "remove", "Remove Traefik container and image from servers"
|
||||||
def remove
|
def remove
|
||||||
|
with_lock do
|
||||||
stop
|
stop
|
||||||
remove_container
|
remove_container
|
||||||
remove_image
|
remove_image
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
desc "remove_container", "Remove Traefik container from servers", hide: true
|
desc "remove_container", "Remove Traefik container from servers", hide: true
|
||||||
def remove_container
|
def remove_container
|
||||||
|
with_lock do
|
||||||
on(MRSK.traefik_hosts) do
|
on(MRSK.traefik_hosts) do
|
||||||
execute *MRSK.auditor.record("Removed traefik container"), verbosity: :debug
|
execute *MRSK.auditor.record("Removed traefik container"), verbosity: :debug
|
||||||
execute *MRSK.traefik.remove_container
|
execute *MRSK.traefik.remove_container
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
desc "remove_container", "Remove Traefik image from servers", hide: true
|
desc "remove_container", "Remove Traefik image from servers", hide: true
|
||||||
def remove_image
|
def remove_image
|
||||||
|
with_lock do
|
||||||
on(MRSK.traefik_hosts) do
|
on(MRSK.traefik_hosts) do
|
||||||
execute *MRSK.auditor.record("Removed traefik image"), verbosity: :debug
|
execute *MRSK.auditor.record("Removed traefik image"), verbosity: :debug
|
||||||
execute *MRSK.traefik.remove_image
|
execute *MRSK.traefik.remove_image
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ require "active_support/core_ext/enumerable"
|
|||||||
require "active_support/core_ext/module/delegation"
|
require "active_support/core_ext/module/delegation"
|
||||||
|
|
||||||
class Mrsk::Commander
|
class Mrsk::Commander
|
||||||
attr_accessor :verbosity
|
attr_accessor :verbosity, :lock_count
|
||||||
|
|
||||||
def initialize
|
def initialize
|
||||||
self.verbosity = :info
|
self.verbosity = :info
|
||||||
|
self.lock_count = 0
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
@@ -84,6 +85,9 @@ class Mrsk::Commander
|
|||||||
@traefik ||= Mrsk::Commands::Traefik.new(config)
|
@traefik ||= Mrsk::Commands::Traefik.new(config)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def lock
|
||||||
|
@lock ||= Mrsk::Commands::Lock.new(config)
|
||||||
|
end
|
||||||
|
|
||||||
def with_verbosity(level)
|
def with_verbosity(level)
|
||||||
old_level = self.verbosity
|
old_level = self.verbosity
|
||||||
@@ -97,14 +101,6 @@ class Mrsk::Commander
|
|||||||
SSHKit.config.output_verbosity = old_level
|
SSHKit.config.output_verbosity = old_level
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
# Test-induced damage!
|
|
||||||
def reset
|
|
||||||
@config = nil
|
|
||||||
@app = @builder = @traefik = @registry = @prune = @auditor = nil
|
|
||||||
@verbosity = :info
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
# Lazy setup of SSHKit
|
# Lazy setup of SSHKit
|
||||||
def configure_sshkit_with(config)
|
def configure_sshkit_with(config)
|
||||||
|
|||||||
@@ -41,6 +41,10 @@ module Mrsk::Commands
|
|||||||
combine *commands, by: ">>"
|
combine *commands, by: ">>"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def write(*commands)
|
||||||
|
combine *commands, by: ">"
|
||||||
|
end
|
||||||
|
|
||||||
def xargs(command)
|
def xargs(command)
|
||||||
[ :xargs, command ].flatten
|
[ :xargs, command ].flatten
|
||||||
end
|
end
|
||||||
|
|||||||
63
lib/mrsk/commands/lock.rb
Normal file
63
lib/mrsk/commands/lock.rb
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
require "active_support/duration"
|
||||||
|
require "active_support/core_ext/numeric/time"
|
||||||
|
|
||||||
|
class Mrsk::Commands::Lock < Mrsk::Commands::Base
|
||||||
|
def acquire(message, version)
|
||||||
|
combine \
|
||||||
|
[:mkdir, lock_dir],
|
||||||
|
write_lock_details(message, version)
|
||||||
|
end
|
||||||
|
|
||||||
|
def release
|
||||||
|
combine \
|
||||||
|
[:rm, lock_details_file],
|
||||||
|
[:rm, "-r", lock_dir]
|
||||||
|
end
|
||||||
|
|
||||||
|
def status
|
||||||
|
combine \
|
||||||
|
stat_lock_dir,
|
||||||
|
read_lock_details
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def write_lock_details(message, version)
|
||||||
|
write \
|
||||||
|
[:echo, "\"#{Base64.encode64(lock_details(message, version))}\""],
|
||||||
|
lock_details_file
|
||||||
|
end
|
||||||
|
|
||||||
|
def read_lock_details
|
||||||
|
pipe \
|
||||||
|
[:cat, lock_details_file],
|
||||||
|
[:base64, "-d"]
|
||||||
|
end
|
||||||
|
|
||||||
|
def stat_lock_dir
|
||||||
|
write \
|
||||||
|
[:stat, lock_dir],
|
||||||
|
"/dev/null"
|
||||||
|
end
|
||||||
|
|
||||||
|
def lock_dir
|
||||||
|
:mrsk_lock
|
||||||
|
end
|
||||||
|
|
||||||
|
def lock_details_file
|
||||||
|
[lock_dir, :details].join("/")
|
||||||
|
end
|
||||||
|
|
||||||
|
def lock_details(message, version)
|
||||||
|
<<~DETAILS.strip
|
||||||
|
Locked by: #{locked_by} at #{Time.now.gmtime}
|
||||||
|
Version: #{version}
|
||||||
|
Message: #{message}
|
||||||
|
DETAILS
|
||||||
|
end
|
||||||
|
|
||||||
|
def locked_by
|
||||||
|
`git config user.name`.strip
|
||||||
|
rescue Errno::ENOENT
|
||||||
|
"Unknown"
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -15,8 +15,9 @@ class CliBuildTest < CliTestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "push without builder" do
|
test "push without builder" do
|
||||||
Mrsk::Cli::Build.any_instance.stubs(:create).returns(true)
|
stub_locking
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||||
|
.with { |arg| arg == :docker }
|
||||||
.raises(SSHKit::Command::Failed.new("no builder"))
|
.raises(SSHKit::Command::Failed.new("no builder"))
|
||||||
.then
|
.then
|
||||||
.returns(true)
|
.returns(true)
|
||||||
@@ -40,7 +41,9 @@ class CliBuildTest < CliTestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "create with error" do
|
test "create with error" do
|
||||||
|
stub_locking
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||||
|
.with { |arg| arg == :docker }
|
||||||
.raises(SSHKit::Command::Failed.new("stderr=error"))
|
.raises(SSHKit::Command::Failed.new("stderr=error"))
|
||||||
|
|
||||||
run_command("create").tap do |output|
|
run_command("create").tap do |output|
|
||||||
@@ -69,4 +72,11 @@ class CliBuildTest < CliTestCase
|
|||||||
def run_command(*command)
|
def run_command(*command)
|
||||||
stdouted { Mrsk::Cli::Build.start([*command, "-c", "test/fixtures/deploy_with_accessories.yml"]) }
|
stdouted { Mrsk::Cli::Build.start([*command, "-c", "test/fixtures/deploy_with_accessories.yml"]) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def stub_locking
|
||||||
|
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||||
|
.with { |arg1, arg2| arg1 == :mkdir && arg2 == :mrsk_lock }
|
||||||
|
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||||
|
.with { |arg1, arg2| arg1 == :rm && arg2 == "mrsk_lock/details" }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -8,13 +8,14 @@ class CliTestCase < ActiveSupport::TestCase
|
|||||||
ENV["VERSION"] = "999"
|
ENV["VERSION"] = "999"
|
||||||
ENV["RAILS_MASTER_KEY"] = "123"
|
ENV["RAILS_MASTER_KEY"] = "123"
|
||||||
ENV["MYSQL_ROOT_PASSWORD"] = "secret123"
|
ENV["MYSQL_ROOT_PASSWORD"] = "secret123"
|
||||||
|
Object.send(:remove_const, :MRSK)
|
||||||
|
Object.const_set(:MRSK, Mrsk::Commander.new)
|
||||||
end
|
end
|
||||||
|
|
||||||
teardown do
|
teardown do
|
||||||
ENV.delete("RAILS_MASTER_KEY")
|
ENV.delete("RAILS_MASTER_KEY")
|
||||||
ENV.delete("MYSQL_ROOT_PASSWORD")
|
ENV.delete("MYSQL_ROOT_PASSWORD")
|
||||||
ENV.delete("VERSION")
|
ENV.delete("VERSION")
|
||||||
MRSK.reset
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
20
test/cli/lock_test.rb
Normal file
20
test/cli/lock_test.rb
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
require_relative "cli_test_case"
|
||||||
|
|
||||||
|
class CliLockTest < CliTestCase
|
||||||
|
test "status" do
|
||||||
|
run_command("status") do |output|
|
||||||
|
assert_match "stat lock", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "release" do
|
||||||
|
run_command("release") do |output|
|
||||||
|
assert_match "rm -rf lock", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def run_command(*command)
|
||||||
|
stdouted { Mrsk::Cli::Lock.start([*command, "-c", "test/fixtures/deploy_with_accessories.yml"]) }
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -42,12 +42,14 @@ class CliMainTest < CliTestCase
|
|||||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:prune:all", [], invoke_options)
|
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:prune:all", [], invoke_options)
|
||||||
|
|
||||||
run_command("deploy", "--skip_push").tap do |output|
|
run_command("deploy", "--skip_push").tap do |output|
|
||||||
|
assert_match /Acquiring the deploy lock/, output
|
||||||
assert_match /Ensure curl and Docker are installed/, output
|
assert_match /Ensure curl and Docker are installed/, output
|
||||||
assert_match /Log into image registry/, output
|
assert_match /Log into image registry/, output
|
||||||
assert_match /Pull app image/, output
|
assert_match /Pull app image/, output
|
||||||
assert_match /Ensure Traefik is running/, output
|
assert_match /Ensure Traefik is running/, output
|
||||||
assert_match /Ensure app can pass healthcheck/, output
|
assert_match /Ensure app can pass healthcheck/, output
|
||||||
assert_match /Prune old containers and images/, output
|
assert_match /Prune old containers and images/, output
|
||||||
|
assert_match /Releasing the deploy lock/, output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
33
test/commands/lock_test.rb
Normal file
33
test/commands/lock_test.rb
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class CommandsLockTest < 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 "status" do
|
||||||
|
assert_equal \
|
||||||
|
"stat mrsk_lock > /dev/null && cat mrsk_lock/details | base64 -d",
|
||||||
|
new_command.status.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "acquire" do
|
||||||
|
assert_match \
|
||||||
|
/mkdir mrsk_lock && echo ".*" > mrsk_lock\/details/m,
|
||||||
|
new_command.acquire("Hello", "123").join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "release" do
|
||||||
|
assert_match \
|
||||||
|
"rm mrsk_lock/details && rm -r mrsk_lock",
|
||||||
|
new_command.release.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def new_command
|
||||||
|
Mrsk::Commands::Lock.new(Mrsk::Configuration.new(@config, version: "123"))
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user