Compare commits

..

18 Commits

Author SHA1 Message Date
David Heinemeier Hansson
625be70e4d Bump version for 0.12.1 2023-05-05 14:33:25 +02:00
David Heinemeier Hansson
aafaee7ac8 Merge pull request #223 from basecamp/customizable-audit-broadcast
Allow customizing audit broadcast with env
2023-05-05 14:30:04 +02:00
David Heinemeier Hansson
97a190300d Merge pull request #270 from basecamp/fix-aggressive-prune-breaking-rollback
Fix aggressive prune breaking rollback
2023-05-05 14:28:22 +02:00
Donal McBreen
326711a3e0 Fix aggressive prune breaking rollback
In the image prune command --all overrides --dangling=true. This removes
the image git sha image tag for the latest image which prevented
us from rolling back to it.

I've updated the integration test to now test deploy, redeploy and
rollback.
2023-05-05 12:13:14 +01:00
Kevin McConnell
82be521e66 Merge branch 'main' into customizable-audit-broadcast
* main:
  Fix staging label bug
  Fix typo
  Capture container health log when unhealthy
  Bump version for 0.12.0
2023-05-05 11:40:29 +01:00
David Heinemeier Hansson
21110080d5 Merge pull request #267 from danthegoodman1/patch-1
Fix staging label bug in README
2023-05-05 11:25:22 +02:00
David Heinemeier Hansson
ef107c41b6 Merge pull request #265 from Jberczel/improve-healthcheck-logging
Improve healthcheck logging
2023-05-05 11:24:55 +02:00
Dan Goodman
1bf4b6b76f Fix staging label bug
I think this is the correct fix based on the `service-role-destination` format, but seeing as it wasn't changed I assumed it was incorrect.
2023-05-04 17:47:17 -04:00
Jeremy Daer
36a3b13bf4 Fix SSHKit #command override args mangling 2023-05-04 08:58:18 -07:00
Jberczel
01483140f5 Fix typo 2023-05-03 15:03:05 -04:00
Jberczel
0e19ead37c Capture container health log when unhealthy 2023-05-03 15:03:05 -04:00
Jeremy Daer
048aecf352 Audit details (#1)
Audit details

* Audit logs and broadcasts accept `details` whose values are included as log tags and MRSK_* env vars passed to the broadcast command
* Commands may return execution options to the CLI in their args list
* Introduce `mrsk broadcast` helper for sending audit broadcasts
* Report UTC time, not local time, in audit logs. Standardize on ISO 8601 format
2023-05-02 11:42:05 -07:00
David Heinemeier Hansson
88a7413b3e Merge branch 'main' into pr/223
* main:
  Don't run actions twice on PRs
  Further distinguish dependency verification
  Naming
  Reveal configured dockerfile path
  Style
  Distinguish from server dependencies
  Distinguish from local dependency verification
  Improve clarity and intent
  Style
  Style
  Style
  Add local dependencies check
  Bootstrap: use multi-platform installer
2023-05-02 14:44:16 +02:00
David Heinemeier Hansson
9cc73fed9a Merge branch 'main' into pr/223
* main:
  Simplify domain language to just "boot" and unscoped config keys
  Retain a fixed number of containers when pruning
  Don't assume rolling back in message
  Check all hosts before rolling back
  Ensure Traefik service name is consistent
  Extend traefik delay by 1 second
  Include traefik access logs
  Check if we are still getting a 404
  Also dump load balancer logs
  Dump traefik logs when app not booted
  Fix missing for apt-get
  Report on container health after failure
  Fix the integration test healthcheck
  Allow percentage-based rolling deployments
  Move `group_limit` & `group_wait` under `boot`
  Limit rolling deployment to boot operation
  Allow performing boot & start operations in groups
2023-05-02 14:43:17 +02:00
David Heinemeier Hansson
19527b4f65 Merge branch 'main' into customizable-audit-broadcast 2023-05-02 10:25:25 +02:00
Kevin McConnell
aceabb3824 Update README with env name change 2023-04-14 16:13:59 +01:00
Kevin McConnell
99fe31d4b4 Rename MRSK_EVENT -> MRSK_MESSAGE
It's a better name, and frees up `MRSK_EVENT` to be used later.
2023-04-14 16:11:42 +01:00
Kevin McConnell
828e56912e Allow customizing audit broadcast with env
When invoking the audit broadcast command, provide a few environment
variables so that people can customize the format of the message if they
want.

We currently provide `MRSK_PERFORMER`, `MRSK_ROLE`, `MRSK_DESTINATION` and
`MRSK_EVENT`.

Also adds the destination to the default message, which we continue to
send as the first argument as before.
2023-04-13 17:54:25 +01:00
24 changed files with 245 additions and 64 deletions

View File

@@ -1,7 +1,7 @@
PATH PATH
remote: . remote: .
specs: specs:
mrsk (0.12.0) mrsk (0.12.1)
activesupport (>= 7.0) activesupport (>= 7.0)
bcrypt_pbkdf (~> 1.0) bcrypt_pbkdf (~> 1.0)
dotenv (~> 2.8) dotenv (~> 2.8)

View File

@@ -308,7 +308,7 @@ You can specialize the default Traefik rules by setting labels on the containers
labels: labels:
traefik.http.routers.hey-web.rule: Host(`app.hey.com`) traefik.http.routers.hey-web.rule: Host(`app.hey.com`)
``` ```
Traefik rules are in the "service-role-destination" format. The default role will be `web` if no rule is specified. If the destination is not specified, it is not included. To give an example, the above rule would become "traefik.http.routers.hey-web.rule" if it was for the "staging" destination. Traefik rules are in the "service-role-destination" format. The default role will be `web` if no rule is specified. If the destination is not specified, it is not included. To give an example, the above rule would become "traefik.http.routers.hey-web-staging.rule" if it was for the "staging" destination.
Note: The backticks are needed to ensure the rule is passed in correctly and not treated as command substitution by Bash! Note: The backticks are needed to ensure the rule is passed in correctly and not treated as command substitution by Bash!
@@ -677,6 +677,21 @@ That'll post a line like follows to a preconfigured chatbot in Basecamp:
[My App] [dhh] Rolled back to version d264c4e92470ad1bd18590f04466787262f605de [My App] [dhh] Rolled back to version d264c4e92470ad1bd18590f04466787262f605de
``` ```
`MRSK_*` environment variables are available to the broadcast command for
fine-grained audit reporting, e.g. for triggering deployment reports or
firing a JSON webhook. These variables include:
- `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_MESSAGE` - the full audit message, e.g. "Deployed app@150b24f"
- `MRSK_DESTINATION` - optional: destination, e.g. "staging"
- `MRSK_ROLE` - optional: role targeted, e.g. "web"
Use `mrsk broadcast` to test and troubleshoot your broadcast command:
```bash
mrsk broadcast -m "test audit message"
```
### Healthcheck ### Healthcheck
MRSK uses Docker healtchecks to check the health of your application during deployment. Traefik uses this same healthcheck status to determine when a container is ready to receive traffic. MRSK uses Docker healtchecks to check the health of your application during deployment. Traefik uses this same healthcheck status to determine when a container is ready to receive traffic.
@@ -714,7 +729,7 @@ servers:
The healthcheck allows for an optional `max_attempts` setting, which will attempt the healthcheck up to the specified number of times before failing the deploy. This is useful for applications that take a while to start up. The default is 7. The healthcheck allows for an optional `max_attempts` setting, which will attempt the healthcheck up to the specified number of times before failing the deploy. This is useful for applications that take a while to start up. The default is 7.
Note that the HTTP health checks assume that the `curl` command is avilable inside the container. If that's not the case, use the healthcheck's `cmd` option to specify an alternative check that the container supports. Note: The HTTP health checks assume that the `curl` command is available inside the container. If that's not the case, use the healthcheck's `cmd` option to specify an alternative check that the container supports.
## Commands ## Commands

View File

@@ -60,7 +60,7 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
roles = MRSK.roles_on(host) roles = MRSK.roles_on(host)
roles.each do |role| roles.each do |role|
execute *MRSK.auditor(role: role).record("Stopped app"), verbosity: :debug execute *MRSK.auditor.record("Stopped app", role: role), verbosity: :debug
execute *MRSK.app(role: role).stop, raise_on_non_zero_exit: false execute *MRSK.app(role: role).stop, raise_on_non_zero_exit: false
end end
end end
@@ -107,7 +107,7 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
roles = MRSK.roles_on(host) roles = MRSK.roles_on(host)
roles.each do |role| roles.each do |role|
execute *MRSK.auditor(role: role).record("Executed cmd '#{cmd}' on app version #{version}"), verbosity: :debug execute *MRSK.auditor.record("Executed cmd '#{cmd}' on app version #{version}", role: role), verbosity: :debug
puts_by_host host, capture_with_info(*MRSK.app(role: role).execute_in_existing_container(cmd)) puts_by_host host, capture_with_info(*MRSK.app(role: role).execute_in_existing_container(cmd))
end end
end end
@@ -214,7 +214,7 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
roles = MRSK.roles_on(host) roles = MRSK.roles_on(host)
roles.each do |role| roles.each do |role|
execute *MRSK.auditor(role: role).record("Removed app container with version #{version}"), verbosity: :debug execute *MRSK.auditor.record("Removed app container with version #{version}", role: role), verbosity: :debug
execute *MRSK.app(role: role).remove_container(version: version) execute *MRSK.app(role: role).remove_container(version: version)
end end
end end
@@ -228,7 +228,7 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
roles = MRSK.roles_on(host) roles = MRSK.roles_on(host)
roles.each do |role| roles.each do |role|
execute *MRSK.auditor(role: role).record("Removed all app containers"), verbosity: :debug execute *MRSK.auditor.record("Removed all app containers", role: role), verbosity: :debug
execute *MRSK.app(role: role).remove_containers execute *MRSK.app(role: role).remove_containers
end end
end end

View File

@@ -9,6 +9,7 @@ class Mrsk::Cli::Healthcheck < Mrsk::Cli::Base
Mrsk::Utils::HealthcheckPoller.wait_for_healthy { capture_with_info(*MRSK.healthcheck.status) } Mrsk::Utils::HealthcheckPoller.wait_for_healthy { capture_with_info(*MRSK.healthcheck.status) }
rescue Mrsk::Utils::HealthcheckPoller::HealthcheckError => e rescue Mrsk::Utils::HealthcheckPoller::HealthcheckError => e
error capture_with_info(*MRSK.healthcheck.logs) error capture_with_info(*MRSK.healthcheck.logs)
error capture_with_pretty_json(*MRSK.healthcheck.container_health_log)
raise raise
ensure ensure
execute *MRSK.healthcheck.stop, raise_on_non_zero_exit: false execute *MRSK.healthcheck.stop, raise_on_non_zero_exit: false

View File

@@ -200,6 +200,13 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
end end
end end
desc "broadcast", "Broadcast an audit message"
option :message, aliases: "-m", type: :string, desc: "Audit mesasge", required: true
def broadcast
say "Broadcast: #{options[:message]}", :magenta
audit_broadcast options[:message]
end
desc "version", "Show MRSK version" desc "version", "Show MRSK version"
def version def version
puts Mrsk::VERSION puts Mrsk::VERSION

View File

@@ -84,8 +84,8 @@ class Mrsk::Commander
Mrsk::Commands::Accessory.new(config, name: name) Mrsk::Commands::Accessory.new(config, name: name)
end end
def auditor(role: nil) def auditor(**details)
Mrsk::Commands::Auditor.new(config, role: role) Mrsk::Commands::Auditor.new(config, **details)
end end
def builder def builder

View File

@@ -1,24 +1,24 @@
require "active_support/core_ext/time/conversions" require "time"
class Mrsk::Commands::Auditor < Mrsk::Commands::Base class Mrsk::Commands::Auditor < Mrsk::Commands::Base
attr_reader :role attr_reader :details
def initialize(config, role: nil) def initialize(config, **details)
super(config) super(config)
@role = role @details = default_details.merge(details)
end end
# Runs remotely # Runs remotely
def record(line) def record(line, **details)
append \ append \
[ :echo, tagged_record_line(line) ], [ :echo, *audit_tags(**details), line ],
audit_log_file audit_log_file
end end
# Runs locally # Runs locally
def broadcast(line) def broadcast(line, **details)
if broadcast_cmd = config.audit_broadcast_cmd if broadcast_cmd = config.audit_broadcast_cmd
[ broadcast_cmd, tagged_broadcast_line(line) ] [ broadcast_cmd, *broadcast_args(line, **details), env: env_for(event: line, **details) ]
end end
end end
@@ -31,27 +31,29 @@ class Mrsk::Commands::Auditor < Mrsk::Commands::Base
[ "mrsk", config.service, config.destination, "audit.log" ].compact.join("-") [ "mrsk", config.service, config.destination, "audit.log" ].compact.join("-")
end end
def tagged_record_line(line) def default_details
tagged_line recorded_at_tag, performer_tag, role_tag, line { recorded_at: Time.now.utc.iso8601,
performer: `whoami`.chomp,
destination: config.destination }
end end
def tagged_broadcast_line(line) def audit_tags(**details)
tagged_line performer_tag, role_tag, line tags_for **self.details.merge(details)
end end
def tagged_line(*tags_and_line) def broadcast_args(line, **details)
"'#{tags_and_line.compact.join(" ")}'" "'#{broadcast_tags(**details).join(" ")} #{line}'"
end end
def recorded_at_tag def broadcast_tags(**details)
"[#{Time.now.to_fs(:db)}]" tags_for **self.details.merge(details).except(:recorded_at)
end end
def performer_tag def tags_for(**details)
"[#{`whoami`.strip}]" details.compact.values.map { |value| "[#{value}]" }
end end
def role_tag def env_for(**details)
"[#{role}]" if role self.details.merge(details).compact.transform_keys { |detail| "MRSK_#{detail.upcase}" }
end end
end end

View File

@@ -3,6 +3,7 @@ module Mrsk::Commands
delegate :sensitive, :argumentize, to: Mrsk::Utils delegate :sensitive, :argumentize, to: Mrsk::Utils
DOCKER_HEALTH_STATUS_FORMAT = "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'" DOCKER_HEALTH_STATUS_FORMAT = "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'"
DOCKER_HEALTH_LOG_FORMAT = "'{{json .State.Health}}'"
attr_accessor :config attr_accessor :config

View File

@@ -22,6 +22,10 @@ class Mrsk::Commands::Healthcheck < Mrsk::Commands::Base
pipe container_id, xargs(docker(:inspect, "--format", DOCKER_HEALTH_STATUS_FORMAT)) pipe container_id, xargs(docker(:inspect, "--format", DOCKER_HEALTH_STATUS_FORMAT))
end end
def container_health_log
pipe container_id, xargs(docker(:inspect, "--format", DOCKER_HEALTH_LOG_FORMAT))
end
def logs def logs
pipe container_id, xargs(docker(:logs, "--tail", 50, "2>&1")) pipe container_id, xargs(docker(:logs, "--tail", 50, "2>&1"))
end end

View File

@@ -1,5 +1,5 @@
require "active_support/duration" require "active_support/duration"
require "active_support/core_ext/numeric/time" require "time"
class Mrsk::Commands::Lock < Mrsk::Commands::Base class Mrsk::Commands::Lock < Mrsk::Commands::Base
def acquire(message, version) def acquire(message, version)
@@ -49,7 +49,7 @@ class Mrsk::Commands::Lock < Mrsk::Commands::Base
def lock_details(message, version) def lock_details(message, version)
<<~DETAILS.strip <<~DETAILS.strip
Locked by: #{locked_by} at #{Time.now.gmtime} Locked by: #{locked_by} at #{Time.now.utc.iso8601}
Version: #{version} Version: #{version}
Message: #{message} Message: #{message}
DETAILS DETAILS

View File

@@ -3,7 +3,7 @@ require "active_support/core_ext/numeric/time"
class Mrsk::Commands::Prune < Mrsk::Commands::Base class Mrsk::Commands::Prune < Mrsk::Commands::Base
def images def images
docker :image, :prune, "--all", "--force", "--filter", "label=service=#{config.service}", "--filter", "dangling=true" docker :image, :prune, "--force", "--filter", "label=service=#{config.service}", "--filter", "dangling=true"
end end
def containers(keep_last: 5) def containers(keep_last: 5)

View File

@@ -1,12 +1,52 @@
require "sshkit" require "sshkit"
require "sshkit/dsl" require "sshkit/dsl"
require "active_support/core_ext/hash/deep_merge"
require "json"
class SSHKit::Backend::Abstract class SSHKit::Backend::Abstract
def capture_with_info(*args, **kwargs) def capture_with_info(*args, **kwargs)
capture(*args, **kwargs, verbosity: Logger::INFO) capture(*args, **kwargs, verbosity: Logger::INFO)
end end
def capture_with_pretty_json(*args, **kwargs)
JSON.pretty_generate(JSON.parse(capture(*args, **kwargs)))
end
def puts_by_host(host, output, type: "App") def puts_by_host(host, output, type: "App")
puts "#{type} Host: #{host}\n#{output}\n\n" puts "#{type} Host: #{host}\n#{output}\n\n"
end end
# Our execution pattern is for the CLI execute args lists returned
# from commands, but this doesn't support returning execution options
# from the command.
#
# Support this by using kwargs for CLI options and merging with the
# args-extracted options.
module CommandEnvMerge
private
# Override to merge options returned by commands in the args list with
# options passed by the CLI and pass them along as kwargs.
def command(args, options)
more_options, args = args.partition { |a| a.is_a? Hash }
more_options << options
build_command(args, **more_options.reduce(:deep_merge))
end
# Destructure options to pluck out env for merge
def build_command(args, env: nil, **options)
# Rely on native Ruby kwargs precedence rather than explicit Hash merges
SSHKit::Command.new(*args, **default_command_options, **options, env: env_for(env))
end
def default_command_options
{ in: pwd_path, host: @host, user: @user, group: @group }
end
def env_for(env)
@env.to_h.merge(env.to_h)
end
end
prepend CommandEnvMerge
end end

View File

@@ -1,3 +1,3 @@
module Mrsk module Mrsk
VERSION = "0.12.0" VERSION = "0.12.1"
end end

View File

@@ -53,6 +53,11 @@ class CliHealthcheckTest < CliTestCase
.with(:docker, :container, :ls, "--all", "--filter", "name=^healthcheck-app-999$", "--quiet", "|", :xargs, :docker, :logs, "--tail", 50, "2>&1") .with(:docker, :container, :ls, "--all", "--filter", "name=^healthcheck-app-999$", "--quiet", "|", :xargs, :docker, :logs, "--tail", 50, "2>&1")
.returns("some log output") .returns("some log output")
# Capture container health log when failing
SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_pretty_json)
.with(:docker, :container, :ls, "--all", "--filter", "name=^healthcheck-app-999$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{json .State.Health}}'")
.returns('{"Status":"unhealthy","Log":[{"ExitCode": 1,"Output": "/bin/sh: 1: curl: not found\n"}]}"')
exception = assert_raises do exception = assert_raises do
run_command("perform") run_command("perform")
end end

View File

@@ -321,6 +321,19 @@ class CliMainTest < CliTestCase
end end
end end
test "broadcast" do
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 ] &&
verbosity == :debug
end.returns("Broadcast audit message: message")
run_command("broadcast", "-m", "message").tap do |output|
assert_match "Broadcast: message", output
end
end
test "version" do test "version" do
version = stdouted { Mrsk::Cli::Main.new.version } version = stdouted { Mrsk::Cli::Main.new.version }
assert_equal Mrsk::VERSION, version assert_equal Mrsk::VERSION, version

View File

@@ -10,7 +10,7 @@ class CliPruneTest < CliTestCase
test "images" do test "images" do
run_command("images").tap do |output| run_command("images").tap do |output|
assert_match /docker image prune --all --force --filter label=service=app --filter dangling=true on 1.1.1.\d/, output assert_match /docker image prune --force --filter label=service=app --filter dangling=true on 1.1.1.\d/, output
end end
end end

View File

@@ -6,38 +6,65 @@ class CommandsAuditorTest < ActiveSupport::TestCase
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ], service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ],
audit_broadcast_cmd: "bin/audit_broadcast" audit_broadcast_cmd: "bin/audit_broadcast"
} }
@auditor = new_command
end end
test "record" do test "record" do
assert_match \ assert_equal [
/echo '.* app removed container' >> mrsk-app-audit.log/, :echo,
new_command.record("app removed container").join(" ") "[#{@auditor.details[:recorded_at]}]", "[#{@auditor.details[:performer]}]",
"app removed container",
">>", "mrsk-app-audit.log"
], @auditor.record("app removed container")
end end
test "record with destination" do test "record with destination" do
@destination = "staging" new_command(destination: "staging").tap do |auditor|
assert_equal [
assert_match \ :echo,
/echo '.* app removed container' >> mrsk-app-staging-audit.log/, "[#{auditor.details[:recorded_at]}]", "[#{auditor.details[:performer]}]", "[#{auditor.details[:destination]}]",
new_command.record("app removed container").join(" ") "app removed container",
">>", "mrsk-app-staging-audit.log"
], auditor.record("app removed container")
end
end end
test "record with role" do test "record with command details" do
@role = "web" new_command(role: "web").tap do |auditor|
assert_equal [
:echo,
"[#{auditor.details[:recorded_at]}]", "[#{auditor.details[:performer]}]", "[#{auditor.details[:role]}]",
"app removed container",
">>", "mrsk-app-audit.log"
], auditor.record("app removed container")
end
end
assert_match \ test "record with arg details" do
/echo '.* \[web\] app removed container' >> mrsk-app-audit.log/, assert_equal [
new_command.record("app removed container").join(" ") :echo,
"[#{@auditor.details[:recorded_at]}]", "[#{@auditor.details[:performer]}]", "[value]",
"app removed container",
">>", "mrsk-app-audit.log"
], @auditor.record("app removed container", detail: "value")
end end
test "broadcast" do test "broadcast" do
assert_match \ assert_equal [
/bin\/audit_broadcast '\[.*\] app removed container'/, "bin/audit_broadcast",
new_command.broadcast("app removed container").join(" ") "'[#{@auditor.details[:performer]}] [value] app removed container'",
env: {
"MRSK_RECORDED_AT" => @auditor.details[:recorded_at],
"MRSK_PERFORMER" => @auditor.details[:performer],
"MRSK_EVENT" => "app removed container",
"MRSK_DETAIL" => "value"
}
], @auditor.broadcast("app removed container", detail: "value")
end end
private private
def new_command def new_command(destination: nil, **details)
Mrsk::Commands::Auditor.new(Mrsk::Configuration.new(@config, destination: @destination, version: "123"), role: @role) Mrsk::Commands::Auditor.new(Mrsk::Configuration.new(@config, destination: destination, version: "123"), **details)
end end
end end

View File

@@ -51,6 +51,12 @@ class CommandsHealthcheckTest < ActiveSupport::TestCase
new_command.status.join(" ") new_command.status.join(" ")
end end
test "container_health_log" do
assert_equal \
"docker container ls --all --filter name=^healthcheck-app-123$ --quiet | xargs docker inspect --format '{{json .State.Health}}'",
new_command.container_health_log.join(" ")
end
test "stop" do test "stop" do
assert_equal \ assert_equal \
"docker container ls --all --filter name=^healthcheck-app-123$ --quiet | xargs docker stop", "docker container ls --all --filter name=^healthcheck-app-123$ --quiet | xargs docker stop",

View File

@@ -10,7 +10,7 @@ class CommandsPruneTest < ActiveSupport::TestCase
test "images" do test "images" do
assert_equal \ assert_equal \
"docker image prune --all --force --filter label=service=app --filter dangling=true", "docker image prune --force --filter label=service=app --filter dangling=true",
new_command.images.join(" ") new_command.images.join(" ")
end end

View File

@@ -6,3 +6,4 @@ servers:
registry: registry:
username: user username: user
password: pw password: pw
audit_broadcast_cmd: "bin/audit_broadcast"

View File

@@ -13,11 +13,36 @@ class DeployTest < ActiveSupport::TestCase
end end
test "deploy" do test "deploy" do
first_version = latest_app_version
assert_app_is_down assert_app_is_down
mrsk :deploy mrsk :deploy
assert_app_is_up assert_app_is_up version: first_version
second_version = update_app_rev
mrsk :redeploy
assert_app_is_up version: second_version
mrsk :rollback, first_version
assert_app_is_up version: first_version
details = mrsk :details, capture: true
assert_match /Traefik Host: vm1/, details
assert_match /Traefik Host: vm2/, details
assert_match /App Host: vm1/, details
assert_match /App Host: vm2/, details
assert_match /traefik:v2.9/, details
assert_match /registry:4443\/app:#{first_version}/, details
audit = mrsk :audit, capture: true
assert_match /Booted app version #{first_version}.*Booted app version #{second_version}.*Booted app version #{first_version}.*/m, audit
end end
private private
@@ -34,23 +59,19 @@ class DeployTest < ActiveSupport::TestCase
result result
end end
def deployer_exec(*commands, capture: false) def deployer_exec(*commands, **options)
if capture docker_compose("exec deployer #{commands.join(" ")}", **options)
stdouted { docker_compose("exec deployer #{commands.join(" ")}") }
else
docker_compose("exec deployer #{commands.join(" ")}", capture: capture)
end
end end
def mrsk(*commands, capture: false) def mrsk(*commands, **options)
deployer_exec(:mrsk, *commands, capture: capture) deployer_exec(:mrsk, *commands, **options)
end end
def assert_app_is_down def assert_app_is_down
assert_equal "502", app_response.code assert_equal "502", app_response.code
end end
def assert_app_is_up def assert_app_is_up(version: nil)
code = app_response.code code = app_response.code
if code != "200" if code != "200"
puts "Got response code #{code}, here are the traefik logs:" puts "Got response code #{code}, here are the traefik logs:"
@@ -60,12 +81,44 @@ class DeployTest < ActiveSupport::TestCase
puts "Tried to get the response code again and got #{app_response.code}" puts "Tried to get the response code again and got #{app_response.code}"
end end
assert_equal "200", code assert_equal "200", code
assert_app_version(version) if version
end
def assert_app_not_found
assert_equal "404", app_response.code
end
def wait_for_app_to_be_up(timeout: 10, up_count: 3)
timeout_at = Time.now + timeout
up_times = 0
response = app_response
while up_times < up_count && timeout_at > Time.now
sleep 0.1
up_times += 1 if response.code == "200"
response = app_response
end
assert_equal up_times, up_count
end end
def app_response def app_response
Net::HTTP.get_response(URI.parse("http://localhost:12345")) Net::HTTP.get_response(URI.parse("http://localhost:12345"))
end end
def update_app_rev
deployer_exec "./update_app_rev.sh"
latest_app_version
end
def latest_app_version
deployer_exec("cat version", capture: true)
end
def assert_app_version(version)
actual_version = Net::HTTP.get_response(URI.parse("http://localhost:12345/version")).body.strip
assert_equal version, actual_version
end
def wait_for_healthy(timeout: 20) def wait_for_healthy(timeout: 20)
timeout_at = Time.now + timeout timeout_at = Time.now + timeout
while docker_compose("ps -a | tail -n +2 | grep -v '(healthy)' | wc -l", capture: true) != "0" while docker_compose("ps -a | tail -n +2 | grep -v '(healthy)' | wc -l", capture: true) != "0"

View File

@@ -14,7 +14,7 @@ RUN echo \
RUN apt-get update --fix-missing && apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin RUN apt-get update --fix-missing && apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
COPY boot.sh . COPY *.sh .
COPY app/ . COPY app/ .
RUN ln -s /shared/ssh /root/.ssh RUN ln -s /shared/ssh /root/.ssh
@@ -23,6 +23,7 @@ RUN mkdir -p /etc/docker/certs.d/registry:4443 && ln -s /shared/certs/domain.crt
RUN git config --global user.email "deployer@example.com" RUN git config --global user.email "deployer@example.com"
RUN git config --global user.name "Deployer" RUN git config --global user.name "Deployer"
RUN git init && git add . && git commit -am "Initial version" RUN git init && git add . && git commit -am "Initial version"
RUN git rev-parse HEAD > version
HEALTHCHECK --interval=1s CMD pgrep sleep HEALTHCHECK --interval=1s CMD pgrep sleep

View File

@@ -1,3 +1,4 @@
FROM nginx:1-alpine-slim FROM nginx:1-alpine-slim
COPY default.conf /etc/nginx/conf.d/default.conf COPY default.conf /etc/nginx/conf.d/default.conf
COPY version /usr/share/nginx/html/version

View File

@@ -0,0 +1,4 @@
#!/bin/bash
git commit -am 'Update rev' --amend
git rev-parse HEAD > version