MRSK hooks
Adds hooks to MRSK. Currently just two hooks, pre-build and post-push. We could break the build and push into two separate commands if we found the need for post-build and/or pre-push hooks. Hooks are stored in `.mrsk/hooks`. Running `mrsk init` will now create that folder and add sample hook scripts. Hooks returning non-zero exit codes will abort the current command. Further potential work here: - We could replace the audit broadcast command with a post-deploy/post-rollback hook or similar - Maybe provide pre-command/post-command hooks that run after every mrsk invocation - Also look for hooks in `~/.mrsk/hooks`
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
module Mrsk::Cli
|
||||
class LockError < StandardError; end
|
||||
class HookError < StandardError; end
|
||||
end
|
||||
|
||||
# SSHKit uses instance eval, so we need a global const for ergonomics
|
||||
|
||||
@@ -134,5 +134,17 @@ module Mrsk::Cli
|
||||
MRSK.hold_lock_on_error = false
|
||||
end
|
||||
end
|
||||
|
||||
def run_hook(hook)
|
||||
run_locally do
|
||||
if MRSK.hook.hook_exists?(hook)
|
||||
begin
|
||||
MRSK.with_verbosity(:debug) { execute(*MRSK.hook.run(hook)) }
|
||||
rescue SSHKit::Command::Failed
|
||||
raise HookError.new("Hook `#{hook}` failed")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -16,6 +16,7 @@ class Mrsk::Cli::Build < Mrsk::Cli::Base
|
||||
|
||||
verify_local_dependencies
|
||||
|
||||
run_hook("pre-build")
|
||||
run_locally do
|
||||
begin
|
||||
MRSK.with_verbosity(:debug) { execute *MRSK.builder.push }
|
||||
@@ -31,6 +32,7 @@ class Mrsk::Cli::Build < Mrsk::Cli::Base
|
||||
end
|
||||
end
|
||||
end
|
||||
run_hook("post-push")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -132,6 +132,14 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
|
||||
puts "Created .env file"
|
||||
end
|
||||
|
||||
unless (hooks_dir = Pathname.new(File.expand_path(".mrsk/hooks"))).exist?
|
||||
hooks_dir.mkpath
|
||||
Pathname.new(File.expand_path("templates/sample_hooks", __dir__)).each_child do |sample_hook|
|
||||
FileUtils.cp sample_hook, hooks_dir
|
||||
end
|
||||
puts "Created sample hooks in .mrsk/hooks"
|
||||
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)"
|
||||
|
||||
20
lib/mrsk/cli/templates/sample_hooks/post-push.sample
Executable file
20
lib/mrsk/cli/templates/sample_hooks/post-push.sample
Executable file
@@ -0,0 +1,20 @@
|
||||
#!/bin/sh
|
||||
|
||||
# A sample post-push hook
|
||||
#
|
||||
# Checks:
|
||||
# 1. We have a clean checkout
|
||||
#
|
||||
# These environment variables are available:
|
||||
# MRSK_RECORDED_AT
|
||||
# MRSK_PERFORMER
|
||||
# MRSK_VERSION
|
||||
# MRSK_DESTINATION (if set)
|
||||
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
echo "Git checkout is not clean, aborting..."
|
||||
git status --porcelain >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exit 0
|
||||
49
lib/mrsk/cli/templates/sample_hooks/pre-build.sample
Executable file
49
lib/mrsk/cli/templates/sample_hooks/pre-build.sample
Executable file
@@ -0,0 +1,49 @@
|
||||
#!/bin/sh
|
||||
|
||||
# A sample pre-build hook
|
||||
#
|
||||
# Checks:
|
||||
# 1. We have a clean checkout
|
||||
# 2. A remote is configured
|
||||
# 3. The branch has been pushed to the remote
|
||||
# 4. The version we are deploying matches the remote
|
||||
#
|
||||
# These environment variables are available:
|
||||
# MRSK_RECORDED_AT
|
||||
# MRSK_PERFORMER
|
||||
# MRSK_VERSION
|
||||
# MRSK_DESTINATION (if set)
|
||||
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
echo "Git checkout is not clean, aborting..." >&2
|
||||
git status --porcelain >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
first_remote=$(git remote)
|
||||
|
||||
if [ -z "$first_remote" ]; then
|
||||
echo "No git remote set, aborting..." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
current_branch=$(git branch --show-current)
|
||||
|
||||
if [ -z "$current_branch" ]; then
|
||||
echo "No git remote set, aborting..." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
remote_head=$(git ls-remote $first_remote --tags $current_branch | cut -f1)
|
||||
|
||||
if [ -z "$remote_head" ]; then
|
||||
echo "Branch not pushed to remote, aborting..." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$MRSK_VERSION" != "$remote_head" ]; then
|
||||
echo "Version ($MRSK_VERSION) does not match remote HEAD ($remote_head), aborting..." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@@ -100,6 +100,14 @@ class Mrsk::Commander
|
||||
@healthcheck ||= Mrsk::Commands::Healthcheck.new(config)
|
||||
end
|
||||
|
||||
def hook
|
||||
@hook ||= Mrsk::Commands::Hook.new(config)
|
||||
end
|
||||
|
||||
def lock
|
||||
@lock ||= Mrsk::Commands::Lock.new(config)
|
||||
end
|
||||
|
||||
def prune
|
||||
@prune ||= Mrsk::Commands::Prune.new(config)
|
||||
end
|
||||
@@ -112,10 +120,6 @@ class Mrsk::Commander
|
||||
@traefik ||= Mrsk::Commands::Traefik.new(config)
|
||||
end
|
||||
|
||||
def lock
|
||||
@lock ||= Mrsk::Commands::Lock.new(config)
|
||||
end
|
||||
|
||||
def with_verbosity(level)
|
||||
old_level = self.verbosity
|
||||
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
require "time"
|
||||
|
||||
class Mrsk::Commands::Auditor < Mrsk::Commands::Base
|
||||
attr_reader :details
|
||||
|
||||
def initialize(config, **details)
|
||||
super(config)
|
||||
@details = default_details.merge(details)
|
||||
@details = details
|
||||
end
|
||||
|
||||
# Runs remotely
|
||||
def record(line, **details)
|
||||
append \
|
||||
[ :echo, *audit_tags(**details), line ],
|
||||
[ :echo, audit_tags(**details).except(:version).to_s, line ],
|
||||
audit_log_file
|
||||
end
|
||||
|
||||
# Runs locally
|
||||
def broadcast(line, **details)
|
||||
if broadcast_cmd = config.audit_broadcast_cmd
|
||||
[ broadcast_cmd, *broadcast_args(line, **details), env: env_for(event: line, **details) ]
|
||||
tags = audit_tags(**details, event: line)
|
||||
[ broadcast_cmd, "'#{tags.except(:recorded_at, :event, :version)} #{line}'", env: tags.env ]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -31,29 +30,7 @@ class Mrsk::Commands::Auditor < Mrsk::Commands::Base
|
||||
[ "mrsk", config.service, config.destination, "audit.log" ].compact.join("-")
|
||||
end
|
||||
|
||||
def default_details
|
||||
{ recorded_at: Time.now.utc.iso8601,
|
||||
performer: `whoami`.chomp,
|
||||
destination: config.destination }
|
||||
end
|
||||
|
||||
def audit_tags(**details)
|
||||
tags_for **self.details.merge(details)
|
||||
end
|
||||
|
||||
def broadcast_args(line, **details)
|
||||
"'#{broadcast_tags(**details).join(" ")} #{line}'"
|
||||
end
|
||||
|
||||
def broadcast_tags(**details)
|
||||
tags_for **self.details.merge(details).except(:recorded_at)
|
||||
end
|
||||
|
||||
def tags_for(**details)
|
||||
details.compact.values.map { |value| "[#{value}]" }
|
||||
end
|
||||
|
||||
def env_for(**details)
|
||||
self.details.merge(details).compact.transform_keys { |detail| "MRSK_#{detail.upcase}" }
|
||||
tags(**self.details, **details)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -53,5 +53,9 @@ module Mrsk::Commands
|
||||
def docker(*args)
|
||||
args.compact.unshift :docker
|
||||
end
|
||||
|
||||
def tags(**details)
|
||||
Mrsk::Tags.from_config(config, **details)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
14
lib/mrsk/commands/hook.rb
Normal file
14
lib/mrsk/commands/hook.rb
Normal file
@@ -0,0 +1,14 @@
|
||||
class Mrsk::Commands::Hook < Mrsk::Commands::Base
|
||||
def run(hook, **details)
|
||||
[ ".mrsk/hooks/#{hook}", env: tags(**details).env ]
|
||||
end
|
||||
|
||||
def hook_exists?(hook)
|
||||
Pathname.new(hook_file(hook)).exist?
|
||||
end
|
||||
|
||||
private
|
||||
def hook_file(hook)
|
||||
".mrsk/hooks/#{hook}"
|
||||
end
|
||||
end
|
||||
34
lib/mrsk/tags.rb
Normal file
34
lib/mrsk/tags.rb
Normal file
@@ -0,0 +1,34 @@
|
||||
require "time"
|
||||
|
||||
class Mrsk::Tags
|
||||
attr_reader :config, :tags
|
||||
|
||||
class << self
|
||||
def from_config(config, **extra)
|
||||
new(**default_tags(config), **extra)
|
||||
end
|
||||
|
||||
def default_tags(config)
|
||||
{ recorded_at: Time.now.utc.iso8601,
|
||||
performer: `whoami`.chomp,
|
||||
destination: config.destination,
|
||||
version: config.version }
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(**tags)
|
||||
@tags = tags.compact
|
||||
end
|
||||
|
||||
def env
|
||||
tags.transform_keys { |detail| "MRSK_#{detail.upcase}" }
|
||||
end
|
||||
|
||||
def to_s
|
||||
tags.values.map { |value| "[#{value}]" }.join(" ")
|
||||
end
|
||||
|
||||
def except(*tags)
|
||||
self.class.new(**self.tags.except(*tags))
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user