Add audit broadcasts

This commit is contained in:
David Heinemeier Hansson
2023-02-18 11:36:30 +01:00
parent 5c93642f2a
commit fb9357b5ba
6 changed files with 80 additions and 5 deletions

View File

@@ -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`. 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 ## Commands
### Running commands on servers ### Running commands on servers

View File

@@ -59,9 +59,14 @@ module Mrsk::Cli
def print_runtime def print_runtime
started_at = Time.now started_at = Time.now
yield yield
return Time.now - started_at
ensure ensure
runtime = Time.now - started_at runtime = Time.now - started_at
puts " Finished all in #{sprintf("%.1f seconds", runtime)}" puts " Finished all in #{sprintf("%.1f seconds", runtime)}"
end end
def audit_broadcast(line)
run_locally { execute *MRSK.auditor.broadcast(line), verbosity: :debug }
end
end end
end end

View File

@@ -10,7 +10,7 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
desc "deploy", "Deploy the app to servers" desc "deploy", "Deploy the app to servers"
def deploy def deploy
print_runtime do runtime = print_runtime do
say "Ensure Docker is installed...", :magenta say "Ensure Docker is installed...", :magenta
invoke "mrsk:cli:server:bootstrap" invoke "mrsk:cli:server:bootstrap"
@@ -28,16 +28,20 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
say "Prune old containers and images...", :magenta say "Prune old containers and images...", :magenta
invoke "mrsk:cli:prune:all" invoke "mrsk:cli:prune:all"
end end
audit_broadcast "Deployed in #{runtime.to_i} seconds"
end end
desc "redeploy", "Deploy new version of the app to servers (without bootstrapping servers, starting Traefik, pruning, and registry login)" desc "redeploy", "Deploy new version of the app to servers (without bootstrapping servers, starting Traefik, pruning, and registry login)"
def redeploy def redeploy
print_runtime do runtime = print_runtime do
say "Build and push app image...", :magenta say "Build and push app image...", :magenta
invoke "mrsk:cli:build:deliver" invoke "mrsk:cli:build:deliver"
invoke "mrsk:cli:app:boot" invoke "mrsk:cli:app:boot"
end end
audit_broadcast "Redeployed in #{runtime.to_i} seconds"
end end
desc "rollback [VERSION]", "Rollback the app to VERSION" 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.stop, raise_on_non_zero_exit: false
execute *MRSK.app.start execute *MRSK.app.start
end end
audit_broadcast "Rolled back to version #{version}"
end end
desc "details", "Display details about Traefik and app containers" desc "details", "Display details about Traefik and app containers"

View File

@@ -1,12 +1,22 @@
require "active_support/core_ext/time/conversions" require "active_support/core_ext/time/conversions"
class Mrsk::Commands::Auditor < Mrsk::Commands::Base class Mrsk::Commands::Auditor < Mrsk::Commands::Base
# Runs remotely
def record(line) def record(line)
append \ append \
[ :echo, tagged_line(line) ], [ :echo, tagged_line(line) ],
audit_log_file audit_log_file
end end
# Runs locally
def broadcast(line)
if broadcast_cmd = config.audit_broadcast_cmd
pipe \
[ :echo, tagged_line(line) ],
broadcast_cmd
end
end
def reveal def reveal
[ :tail, "-n", 50, audit_log_file ] [ :tail, "-n", 50, audit_log_file ]
end end
@@ -21,14 +31,14 @@ class Mrsk::Commands::Auditor < Mrsk::Commands::Base
end end
def tags def tags
"[#{timestamp}] [#{performer}]" "[#{recorded_at}] [#{performer}]"
end end
def performer def performer
`whoami`.strip @performer ||= `whoami`.strip
end end
def timestamp def recorded_at
Time.now.to_fs(:db) Time.now.to_fs(:db)
end end
end end

View File

@@ -126,6 +126,10 @@ class Mrsk::Configuration
{ user: ssh_user, proxy: ssh_proxy, auth_methods: [ "publickey" ] }.compact { user: ssh_user, proxy: ssh_proxy, auth_methods: [ "publickey" ] }.compact
end end
def audit_broadcast_cmd
raw_config.audit_broadcast_cmd
end
def valid? def valid?
ensure_required_keys_present && ensure_env_available ensure_required_keys_present && ensure_env_available

View File

@@ -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