From fb9357b5ba0e50ed43a6dbea406389bdfdf44684 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sat, 18 Feb 2023 11:36:30 +0100 Subject: [PATCH] Add audit broadcasts --- README.md | 23 +++++++++++++++++++++++ lib/mrsk/cli/base.rb | 5 +++++ lib/mrsk/cli/main.rb | 10 ++++++++-- lib/mrsk/commands/auditor.rb | 16 +++++++++++++--- lib/mrsk/configuration.rb | 4 ++++ test/commands/auditor_test.rb | 27 +++++++++++++++++++++++++++ 6 files changed, 80 insertions(+), 5 deletions(-) create mode 100644 test/commands/auditor_test.rb diff --git a/README.md b/README.md index 6a8bc124..41a1ba3c 100644 --- a/README.md +++ b/README.md @@ -347,6 +347,29 @@ This template can safely be checked into git. Then everyone deploying the app ca 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`. +### Using audit broadcasts + +If you'd like to broadcast audits of deploys, rollbacks, etc to a chatroom or elsewhere, you can configure the `audit_broadcast_cmd` setting with the path to a bin file that reads the audit line from STDIN, and then does whatever with it: + +```yaml +audit_broadcast_cmd: + bin/audit_broadcast +``` + +The broadcast command could look something like: + +```bash +#!/usr/bin/env bash +read +curl -q -d content="[My app] ${REPLY}" https://3.basecamp.com/XXXXX/integrations/XXXXX/buckets/XXXXX/chats/XXXXX/lines +``` + +That'll post a line like follows to a preconfigured chatbot in Basecamp: + +``` +[My App] [2023-02-18 11:29:52] [dhh] Rolled back to version d264c4e92470ad1bd18590f04466787262f605de +``` + ## Commands ### Running commands on servers diff --git a/lib/mrsk/cli/base.rb b/lib/mrsk/cli/base.rb index 1e28e956..e89f4591 100644 --- a/lib/mrsk/cli/base.rb +++ b/lib/mrsk/cli/base.rb @@ -59,9 +59,14 @@ module Mrsk::Cli def print_runtime started_at = Time.now yield + return Time.now - started_at ensure runtime = Time.now - started_at puts " Finished all in #{sprintf("%.1f seconds", runtime)}" end + + def audit_broadcast(line) + run_locally { execute *MRSK.auditor.broadcast(line), verbosity: :debug } + end end end diff --git a/lib/mrsk/cli/main.rb b/lib/mrsk/cli/main.rb index ce2299ff..b1a0f32e 100644 --- a/lib/mrsk/cli/main.rb +++ b/lib/mrsk/cli/main.rb @@ -10,7 +10,7 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base desc "deploy", "Deploy the app to servers" def deploy - print_runtime do + runtime = print_runtime do say "Ensure Docker is installed...", :magenta invoke "mrsk:cli:server:bootstrap" @@ -28,16 +28,20 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base say "Prune old containers and images...", :magenta invoke "mrsk:cli:prune:all" end + + audit_broadcast "Deployed in #{runtime.to_i} seconds" end desc "redeploy", "Deploy new version of the app to servers (without bootstrapping servers, starting Traefik, pruning, and registry login)" def redeploy - print_runtime do + runtime = print_runtime do say "Build and push app image...", :magenta invoke "mrsk:cli:build:deliver" invoke "mrsk:cli:app:boot" end + + audit_broadcast "Redeployed in #{runtime.to_i} seconds" end desc "rollback [VERSION]", "Rollback the app to VERSION" @@ -51,6 +55,8 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base execute *MRSK.app.stop, raise_on_non_zero_exit: false execute *MRSK.app.start end + + audit_broadcast "Rolled back to version #{version}" end desc "details", "Display details about Traefik and app containers" diff --git a/lib/mrsk/commands/auditor.rb b/lib/mrsk/commands/auditor.rb index 692844c8..dbd8698e 100644 --- a/lib/mrsk/commands/auditor.rb +++ b/lib/mrsk/commands/auditor.rb @@ -1,12 +1,22 @@ require "active_support/core_ext/time/conversions" class Mrsk::Commands::Auditor < Mrsk::Commands::Base + # Runs remotely def record(line) append \ [ :echo, tagged_line(line) ], audit_log_file end + # Runs locally + def broadcast(line) + if broadcast_cmd = config.audit_broadcast_cmd + pipe \ + [ :echo, tagged_line(line) ], + broadcast_cmd + end + end + def reveal [ :tail, "-n", 50, audit_log_file ] end @@ -21,14 +31,14 @@ class Mrsk::Commands::Auditor < Mrsk::Commands::Base end def tags - "[#{timestamp}] [#{performer}]" + "[#{recorded_at}] [#{performer}]" end def performer - `whoami`.strip + @performer ||= `whoami`.strip end - def timestamp + def recorded_at Time.now.to_fs(:db) end end diff --git a/lib/mrsk/configuration.rb b/lib/mrsk/configuration.rb index 7c299eef..b17bb8c0 100644 --- a/lib/mrsk/configuration.rb +++ b/lib/mrsk/configuration.rb @@ -126,6 +126,10 @@ class Mrsk::Configuration { user: ssh_user, proxy: ssh_proxy, auth_methods: [ "publickey" ] }.compact end + def audit_broadcast_cmd + raw_config.audit_broadcast_cmd + end + def valid? ensure_required_keys_present && ensure_env_available diff --git a/test/commands/auditor_test.rb b/test/commands/auditor_test.rb new file mode 100644 index 00000000..c5f085c7 --- /dev/null +++ b/test/commands/auditor_test.rb @@ -0,0 +1,27 @@ +require "test_helper" + +class CommandsAuditorTest < ActiveSupport::TestCase + setup do + @config = { + service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ], + audit_broadcast_cmd: "bin/audit_broadcast" + } + end + + test "record" do + assert_match \ + /echo '.* app removed container' >> mrsk-app-audit.log/, + new_command.record("app removed container").join(" ") + end + + test "broadcast" do + assert_match \ + /echo '.* app removed container' \| bin\/audit_broadcast/, + new_command.broadcast("app removed container").join(" ") + end + + private + def new_command + Mrsk::Commands::Auditor.new(Mrsk::Configuration.new(@config, version: "123")) + end +end