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:
Donal McBreen
2023-05-05 15:08:28 +01:00
parent 340ed94fa9
commit 58c1096a90
23 changed files with 292 additions and 51 deletions

View File

@@ -2,12 +2,7 @@ require_relative "cli_test_case"
class CliAppTest < CliTestCase
test "boot" do
SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info).returns("123") # old version
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'")
.returns("running") # health check
stub_running
run_command("boot").tap do |output|
assert_match "docker tag dhh/app:latest dhh/app:latest", output
assert_match "docker run --detach --restart unless-stopped", output
@@ -183,4 +178,12 @@ class CliAppTest < CliTestCase
def run_command(*command, config: :with_accessories)
stdouted { Mrsk::Cli::App.start([*command, "-c", "test/fixtures/deploy_#{config}.yml", "--hosts", "1.1.1.1"]) }
end
def stub_running
SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info).returns("123") # old version
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'")
.returns("running") # health check
end
end

View File

@@ -41,6 +41,22 @@ class CliBuildTest < CliTestCase
assert_raises(Mrsk::Cli::Build::BuildError) { run_command("push") }
end
test "push pre-build hook failure" do
fail_hook("pre-build")
assert_raises(Mrsk::Cli::HookError) { run_command("push") }
assert @executions.none? { |args| args[0..2] == [:docker, :buildx, :build] }
end
test "push post-push hook failure" do
fail_hook("post-push")
assert_raises(Mrsk::Cli::HookError) { run_command("push") }
assert @executions.any? { |args| args[0..2] == [:docker, :buildx, :build] }
end
test "pull" do
run_command("pull").tap do |output|
assert_match /docker image rm --force dhh\/app:999/, output
@@ -93,4 +109,11 @@ class CliBuildTest < CliTestCase
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
.with { |arg1, arg2| arg1 == :rm && arg2 == "mrsk_lock/details" }
end
def stub_dependency_checks
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
.with(:docker, "--version", "&&", :docker, :buildx, "version")
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
.with { |*args| args[0..1] == [:docker, :buildx] }
end
end

View File

@@ -14,4 +14,17 @@ class CliTestCase < ActiveSupport::TestCase
ENV.delete("MYSQL_ROOT_PASSWORD")
ENV.delete("VERSION")
end
private
def fail_hook(hook)
@executions = []
Mrsk::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
.with { |*args| @executions << args; args != [".mrsk/hooks/#{hook}"] }
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
.with { |*args| args.first == ".mrsk/hooks/#{hook}" }
.raises(SSHKit::Command::Failed.new("failed"))
end
end

View File

@@ -235,9 +235,11 @@ class CliMainTest < CliTestCase
end
test "init" do
Pathname.any_instance.expects(:exist?).returns(false).twice
Pathname.any_instance.expects(:exist?).returns(false).times(3)
Pathname.any_instance.stubs(:mkpath)
FileUtils.stubs(:mkdir_p)
FileUtils.stubs(:cp_r)
FileUtils.stubs(:cp)
run_command("init").tap do |output|
assert_match /Created configuration file in config\/deploy.yml/, output
@@ -246,7 +248,7 @@ class CliMainTest < CliTestCase
end
test "init with existing config" do
Pathname.any_instance.expects(:exist?).returns(true).twice
Pathname.any_instance.expects(:exist?).returns(true).times(3)
run_command("init").tap do |output|
assert_match /Config file already exists in config\/deploy.yml \(remove first to create a new one\)/, output
@@ -254,9 +256,11 @@ class CliMainTest < CliTestCase
end
test "init with bundle option" do
Pathname.any_instance.expects(:exist?).returns(false).times(3)
Pathname.any_instance.expects(:exist?).returns(false).times(4)
Pathname.any_instance.stubs(:mkpath)
FileUtils.stubs(:mkdir_p)
FileUtils.stubs(:cp_r)
FileUtils.stubs(:cp)
run_command("init", "--bundle").tap do |output|
assert_match /Created configuration file in config\/deploy.yml/, output
@@ -269,9 +273,11 @@ class CliMainTest < CliTestCase
end
test "init with bundle option and existing binstub" do
Pathname.any_instance.expects(:exist?).returns(true).times(3)
Pathname.any_instance.expects(:exist?).returns(true).times(4)
Pathname.any_instance.stubs(:mkpath)
FileUtils.stubs(:mkdir_p)
FileUtils.stubs(:cp_r)
FileUtils.stubs(:cp)
run_command("init", "--bundle").tap do |output|
assert_match /Config file already exists in config\/deploy.yml \(remove first to create a new one\)/, output
@@ -321,7 +327,7 @@ class CliMainTest < CliTestCase
SSHKit::Backend::Abstract.any_instance.expects(:execute).with do |command, line, options, verbosity:|
command == "bin/audit_broadcast" &&
line =~ /\A'\[[^\]]+\] message'\z/ &&
options[:env].keys == %w[ MRSK_RECORDED_AT MRSK_PERFORMER MRSK_EVENT ] &&
options[:env].keys == %w[ MRSK_RECORDED_AT MRSK_PERFORMER MRSK_VERSION MRSK_EVENT ] &&
verbosity == :debug
end.returns("Broadcast audit message: message")