Add a pre-connect hook

This can be used for hooks that should run before connecting to remote
hosts. An example use case is pre-warming DNS.
This commit is contained in:
Donal McBreen
2023-05-24 11:39:48 +01:00
parent 483b893018
commit 66f9ce0e90
8 changed files with 75 additions and 9 deletions

View File

@@ -880,17 +880,21 @@ fine-grained audit reporting, e.g. for triggering deployment reports or
firing a JSON webhook. These variables include: firing a JSON webhook. These variables include:
- `MRSK_RECORDED_AT` - UTC timestamp in ISO 8601 format, e.g. `2023-04-14T17:07:31Z` - `MRSK_RECORDED_AT` - UTC timestamp in ISO 8601 format, e.g. `2023-04-14T17:07:31Z`
- `MRSK_PERFORMER` - the local user performing the command (from `whoami`) - `MRSK_PERFORMER` - the local user performing the command (from `whoami`)
- `MRSK_MESSAGE` - the full audit message, e.g. "Deployed app@150b24f" - `MRSK_SERVICE_VERSION` - an abbreviated service and version for use in messages, e.g. app@150b24f
- `MRSK_SERVICE_VERSION` - an abbreviated version (for use in messages) - `MRSK_VERSION` - an full version being deployed
- `MRSK_DESTINATION` - optional: destination, e.g. "staging" - `MRSK_DESTINATION` - optional: destination, e.g. "staging"
- `MRSK_HOSTS` - a comma separated list of the hosts targeted by the command
- `MRSK_ROLE` - optional: role targeted, e.g. "web" - `MRSK_ROLE` - optional: role targeted, e.g. "web"
There are two hooks: There are three hooks:
1. pre-build 1. pre-connect
Called before taking the deploy lock. For checks that need to run before connecting to remote hosts - e.g. DNS warming.
2. pre-build
Used for pre-build checks - e.g. there are no uncommitted changes or that CI has passed. Used for pre-build checks - e.g. there are no uncommitted changes or that CI has passed.
2. post-deploy - run after a deploy, redeploy or rollback 3. post-deploy - run after a deploy, redeploy or rollback
This hook is also passed a `MRSK_RUNTIME` env variable. This hook is also passed a `MRSK_RUNTIME` env variable.

View File

@@ -76,6 +76,8 @@ module Mrsk::Cli
if MRSK.holding_lock? if MRSK.holding_lock?
yield yield
else else
run_hook "pre-connect"
acquire_lock acquire_lock
begin begin
@@ -135,7 +137,7 @@ module Mrsk::Cli
if !options[:skip_hooks] && MRSK.hook.hook_exists?(hook) if !options[:skip_hooks] && MRSK.hook.hook_exists?(hook)
say "Running the #{hook} hook...", :magenta say "Running the #{hook} hook...", :magenta
run_locally do run_locally do
MRSK.with_verbosity(:debug) { execute *MRSK.hook.run(hook, **details) } MRSK.with_verbosity(:debug) { execute *MRSK.hook.run(hook, **details, hosts: MRSK.hosts.join(",")) }
rescue SSHKit::Command::Failed rescue SSHKit::Command::Failed
raise HookError.new("Hook `#{hook}` failed") raise HookError.new("Hook `#{hook}` failed")
end end

View File

@@ -12,6 +12,8 @@
# MRSK_RECORDED_AT # MRSK_RECORDED_AT
# MRSK_PERFORMER # MRSK_PERFORMER
# MRSK_VERSION # MRSK_VERSION
# MRSK_HOSTS
# MRSK_ROLE (if set)
# MRSK_DESTINATION (if set) # MRSK_DESTINATION (if set)
# MRSK_RUNTIME # MRSK_RUNTIME

View File

@@ -12,6 +12,8 @@
# MRSK_RECORDED_AT # MRSK_RECORDED_AT
# MRSK_PERFORMER # MRSK_PERFORMER
# MRSK_VERSION # MRSK_VERSION
# MRSK_HOSTS
# MRSK_ROLE (if set)
# MRSK_DESTINATION (if set) # MRSK_DESTINATION (if set)
if [ -n "$(git status --porcelain)" ]; then if [ -n "$(git status --porcelain)" ]; then

View File

@@ -0,0 +1,47 @@
#!/usr/bin/env ruby
# A sample pre-connect check
#
# Warms DNS before connecting to hosts in parallel
#
# These environment variables are available:
# MRSK_RECORDED_AT
# MRSK_PERFORMER
# MRSK_VERSION
# MRSK_HOSTS
# MRSK_ROLE (if set)
# MRSK_DESTINATION (if set)
# MRSK_RUNTIME
hosts = ENV["MRSK_HOSTS"].split(",")
results = nil
max = 3
elapsed = Benchmark.realtime do
results = hosts.map do |host|
Thread.new do
tries = 1
begin
Socket.getaddrinfo(host, 0, Socket::AF_UNSPEC, Socket::SOCK_STREAM, nil, Socket::AI_CANONNAME)
rescue SocketError
if tries < max
puts "Retrying DNS warmup: #{host}"
tries += 1
sleep rand
retry
else
puts "DNS warmup failed: #{host}"
host
end
end
tries
end
end.map(&:value)
end
retries = results.sum - hosts.size
nopes = results.count { |r| r == max }
puts "Prewarmed %d DNS lookups in %.2f sec: %d retries, %d failures" % [ hosts.size, elapsed, retries, nopes ]

View File

@@ -23,6 +23,7 @@ class CliMainTest < CliTestCase
Mrsk::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true) Mrsk::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)
run_command("deploy").tap do |output| run_command("deploy").tap do |output|
assert_match /Running the pre-connect hook.../, output
assert_match /Log into image registry/, output assert_match /Log into image registry/, output
assert_match /Build and push app image/, output assert_match /Build and push app image/, output
assert_match /Ensure Traefik is running/, output assert_match /Ensure Traefik is running/, output

View File

@@ -0,0 +1,8 @@
#!/bin/sh
echo "About to lock..."
if [ "$MRSK_HOSTS" != "vm1,vm2" ]; then
echo "Expected hosts to be 'vm1,vm2', got $MRSK_HOSTS"
exit 1
fi
mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/pre-connect

View File

@@ -8,16 +8,16 @@ class MainTest < IntegrationTest
mrsk :deploy mrsk :deploy
assert_app_is_up version: first_version assert_app_is_up version: first_version
assert_hooks_ran "pre-build", "post-deploy" assert_hooks_ran "pre-connect", "pre-build", "post-deploy"
second_version = update_app_rev second_version = update_app_rev
mrsk :redeploy mrsk :redeploy
assert_app_is_up version: second_version assert_app_is_up version: second_version
assert_hooks_ran "pre-build", "post-deploy" assert_hooks_ran "pre-connect", "pre-build", "post-deploy"
mrsk :rollback, first_version mrsk :rollback, first_version
assert_hooks_ran "post-deploy" assert_hooks_ran "pre-connect", "post-deploy"
assert_app_is_up version: first_version assert_app_is_up version: first_version
details = mrsk :details, capture: true details = mrsk :details, capture: true