Compare commits

..

1 Commits

Author SHA1 Message Date
David Heinemeier Hansson
c47de8246b Bump version for 0.8.1 2023-02-20 18:20:41 +01:00
16 changed files with 28 additions and 74 deletions

View File

@@ -1,7 +1,7 @@
PATH PATH
remote: . remote: .
specs: specs:
mrsk (0.8.2) mrsk (0.8.1)
activesupport (>= 7.0) activesupport (>= 7.0)
dotenv (~> 2.8) dotenv (~> 2.8)
sshkit (~> 1.21) sshkit (~> 1.21)

View File

@@ -498,7 +498,7 @@ If you wish to remove the entire application, including Traefik, containers, ima
## Stage of development ## Stage of development
This is beta software. Commands may still move around. But we're live in production at [37signals](https://37signals.com). This is alpha software. Lots of stuff is missing. Lots of stuff will keep moving around for a while.
## License ## License

View File

@@ -13,7 +13,7 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
execute *accessory.run execute *accessory.run
end end
audit_broadcast "Booted accessory #{name}" unless options[:skip_broadcast] audit_broadcast "Booted accessory #{name}"
end end
end end
end end

View File

@@ -5,27 +5,18 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
using_version(options[:version] || most_recent_version_available) do |version| using_version(options[:version] || most_recent_version_available) do |version|
say "Start container with version #{version} (or reboot if already running)...", :magenta say "Start container with version #{version} (or reboot if already running)...", :magenta
cli = self
MRSK.config.roles.each do |role| MRSK.config.roles.each do |role|
on(role.hosts) do |host| on(role.hosts) do |host|
execute *MRSK.auditor.record("Booted app version #{version}"), verbosity: :debug execute *MRSK.auditor.record("Booted app version #{version}"), verbosity: :debug
begin begin
old_version = capture_with_info(*MRSK.app.current_running_version).strip execute *MRSK.app.stop, raise_on_non_zero_exit: false
execute *MRSK.app.run(role: role.name) execute *MRSK.app.run(role: role.name)
cli.say "Waiting #{MRSK.config.readiness_delay}s for app to boot...", :magenta
sleep MRSK.config.readiness_delay
execute *MRSK.app.stop(version: old_version), raise_on_non_zero_exit: false if old_version.present?
rescue SSHKit::Command::Failed => e rescue SSHKit::Command::Failed => e
if e.message =~ /already in use/ if e.message =~ /already in use/
error "Rebooting container with same version #{version} already deployed on #{host} (may cause gap in zero-downtime promise!)" error "Rebooting container with same version already deployed on #{host}"
execute *MRSK.auditor.record("Rebooted app version #{version}"), verbosity: :debug execute *MRSK.auditor.record("Rebooted app version #{version}"), verbosity: :debug
execute *MRSK.app.stop(version: version)
execute *MRSK.app.remove_container(version: version) execute *MRSK.app.remove_container(version: version)
execute *MRSK.app.run(role: role.name) execute *MRSK.app.run(role: role.name)
else else

View File

@@ -20,8 +20,6 @@ module Mrsk::Cli
class_option :config_file, aliases: "-c", default: "config/deploy.yml", desc: "Path to config file" class_option :config_file, aliases: "-c", default: "config/deploy.yml", desc: "Path to config file"
class_option :destination, aliases: "-d", desc: "Specify destination to be used for config file (staging -> deploy.staging.yml)" class_option :destination, aliases: "-d", desc: "Specify destination to be used for config file (staging -> deploy.staging.yml)"
class_option :skip_broadcast, aliases: "-B", type: :boolean, default: false, desc: "Skip audit broadcasts"
def initialize(*) def initialize(*)
super super
load_envs load_envs

View File

@@ -1,7 +1,5 @@
class Mrsk::Cli::Healthcheck < Mrsk::Cli::Base class Mrsk::Cli::Healthcheck < Mrsk::Cli::Base
MAX_ATTEMPTS = 7 MAX_ATTEMPTS = 5
class HealthcheckError < StandardError; end
default_command :perform default_command :perform
@@ -20,7 +18,7 @@ class Mrsk::Cli::Healthcheck < Mrsk::Cli::Base
if status == "200" if status == "200"
info "#{target} succeeded with 200 OK!" info "#{target} succeeded with 200 OK!"
else else
raise HealthcheckError, "#{target} failed with status #{status}" raise "#{target} failed with status #{status}"
end end
rescue SSHKit::Command::Failed rescue SSHKit::Command::Failed
if attempt <= MAX_ATTEMPTS if attempt <= MAX_ATTEMPTS
@@ -33,7 +31,7 @@ class Mrsk::Cli::Healthcheck < Mrsk::Cli::Base
raise raise
end end
end end
rescue SSHKit::Command::Failed, HealthcheckError => e rescue SSHKit::Command::Failed => e
error capture_with_info(*MRSK.healthcheck.logs) error capture_with_info(*MRSK.healthcheck.logs)
if e.message =~ /curl/ if e.message =~ /curl/

View File

@@ -32,7 +32,7 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
invoke "mrsk:cli:prune:all" invoke "mrsk:cli:prune:all"
end end
audit_broadcast "Deployed app in #{runtime.to_i} seconds" unless options[:skip_broadcast] audit_broadcast "Deployed app in #{runtime.to_i} seconds"
end end
desc "redeploy", "Deploy app to servers without bootstrapping servers, starting Traefik, pruning, and registry login" desc "redeploy", "Deploy app to servers without bootstrapping servers, starting Traefik, pruning, and registry login"
@@ -47,7 +47,7 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
invoke "mrsk:cli:app:boot" invoke "mrsk:cli:app:boot"
end end
audit_broadcast "Redeployed app in #{runtime.to_i} seconds" unless options[:skip_broadcast] audit_broadcast "Redeployed app in #{runtime.to_i} seconds"
end end
desc "rollback [VERSION]", "Rollback app to VERSION" desc "rollback [VERSION]", "Rollback app to VERSION"
@@ -55,22 +55,14 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
MRSK.version = version MRSK.version = version
if container_name_available?(MRSK.config.service_with_version) if container_name_available?(MRSK.config.service_with_version)
say "Start version #{version}, then stop the old version...", :magenta say "Stop current version, then start version #{version}...", :magenta
cli = self
on(MRSK.hosts) do |host| on(MRSK.hosts) do |host|
old_version = capture_with_info(*MRSK.app.current_running_version).strip.presence execute *MRSK.app.stop, raise_on_non_zero_exit: false
execute *MRSK.app.start execute *MRSK.app.start
cli.say "Waiting #{MRSK.config.readiness_delay}s for app to start...", :magenta
sleep MRSK.config.readiness_delay
execute *MRSK.app.stop(version: old_version), raise_on_non_zero_exit: false
end end
audit_broadcast "Rolled back app to version #{version}" unless options[:skip_broadcast] audit_broadcast "Rolled back app to version #{version}"
else else
say "The app version '#{version}' is not available as a container (use 'mrsk app containers' for available versions)", :red say "The app version '#{version}' is not available as a container (use 'mrsk app containers' for available versions)", :red
end end

View File

@@ -18,10 +18,8 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
docker :start, service_with_version docker :start, service_with_version
end end
def stop(version: nil) def stop
pipe \ pipe current_container_id, xargs(docker(:stop))
version ? container_id_for_version(version) : current_container_id,
xargs(docker(:stop))
end end
def info def info
@@ -134,10 +132,6 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
end end
end end
def container_id_for_version(version)
container_id_for(container_name: service_with_version(version))
end
def service_filter def service_filter
[ "--filter", "label=service=#{config.service}" ] [ "--filter", "label=service=#{config.service}" ]
end end

View File

@@ -136,9 +136,6 @@ class Mrsk::Configuration
{ "path" => "/up", "port" => 3000 }.merge(raw_config.healthcheck || {}) { "path" => "/up", "port" => 3000 }.merge(raw_config.healthcheck || {})
end end
def readiness_delay
raw_config.readiness_delay || 7
end
def valid? def valid?
ensure_required_keys_present && ensure_env_available ensure_required_keys_present && ensure_env_available

View File

@@ -61,7 +61,7 @@ class Mrsk::Configuration::Role
"traefik.http.routers.#{config.service}.rule" => "PathPrefix(`/`)", "traefik.http.routers.#{config.service}.rule" => "PathPrefix(`/`)",
"traefik.http.services.#{config.service}.loadbalancer.healthcheck.path" => config.healthcheck["path"], "traefik.http.services.#{config.service}.loadbalancer.healthcheck.path" => config.healthcheck["path"],
"traefik.http.services.#{config.service}.loadbalancer.healthcheck.interval" => "1s", "traefik.http.services.#{config.service}.loadbalancer.healthcheck.interval" => "1s",
"traefik.http.middlewares.#{config.service}.retry.attempts" => "5", "traefik.http.middlewares.#{config.service}.retry.attempts" => "3",
"traefik.http.middlewares.#{config.service}.retry.initialinterval" => "500ms" "traefik.http.middlewares.#{config.service}.retry.initialinterval" => "500ms"
} }
else else

View File

@@ -1,3 +1,3 @@
module Mrsk module Mrsk
VERSION = "0.8.2" VERSION = "0.8.1"
end end

View File

@@ -2,16 +2,7 @@ require_relative "cli_test_case"
class CliAppTest < CliTestCase class CliAppTest < CliTestCase
test "boot" do test "boot" do
# Stub current version fetch assert_match /Running docker run --detach --restart unless-stopped/, run_command("boot")
SSHKit::Backend::Abstract.any_instance.stubs(:capture)
.returns("999") # new version
.then
.returns("123") # old version
run_command("boot").tap do |output|
assert_match /docker run --detach --restart unless-stopped/, output
assert_match /docker container ls --all --filter name=app-123 --quiet | xargs docker stop/, output
end
end end
test "boot will reboot if same version is already running" do test "boot will reboot if same version is already running" do
@@ -20,17 +11,12 @@ class CliAppTest < CliTestCase
# Prevent expected failures from outputting to terminal # Prevent expected failures from outputting to terminal
Thread.report_on_exception = false Thread.report_on_exception = false
MRSK.app.stubs(:run) MRSK.app.stubs(:run).raises(SSHKit::Command::Failed.new("already in use")).then.returns([ :docker, :run ])
.raises(SSHKit::Command::Failed.new("already in use"))
.then
.raises(SSHKit::Command::Failed.new("already in use"))
.then
.returns([ :docker, :run ])
run_command("boot").tap do |output| run_command("boot").tap do |output|
assert_match /Rebooting container with same version 999 already deployed/, output # Can't start what's already running assert_match /Rebooting container with same version already deployed/, output # Can't start what's already running
assert_match /docker container ls --all --filter name=app-999 --quiet | xargs docker container rm/, output # Stop old running assert_match /docker ps --quiet --filter label=service=app \| xargs docker stop/, output # Stop what's running
assert_match /docker container ls --all --filter name=app-999 --quiet | xargs docker container rm/, output # Remove old container assert_match /docker container ls --all --filter name=app-999 --quiet \| xargs docker container rm/, output # Remove old container
assert_match /docker run/, output # Start new container assert_match /docker run/, output # Start new container
end end
ensure ensure

View File

@@ -19,7 +19,7 @@ class CliMainTest < CliTestCase
Mrsk::Cli::Main.any_instance.stubs(:container_name_available?).returns(true) Mrsk::Cli::Main.any_instance.stubs(:container_name_available?).returns(true)
run_command("rollback", "123").tap do |output| run_command("rollback", "123").tap do |output|
assert_match /Start version 123, then stop the old version/, output assert_match /Stop current version, then start version 123/, output
assert_match /docker ps -q --filter label=service=app | xargs docker stop/, output assert_match /docker ps -q --filter label=service=app | xargs docker stop/, output
assert_match /docker start app-123/, output assert_match /docker start app-123/, output
end end

View File

@@ -14,7 +14,7 @@ class CommandsAppTest < ActiveSupport::TestCase
test "run" do test "run" do
assert_equal \ assert_equal \
"docker run --detach --restart unless-stopped --log-opt max-size=10m --name app-999 -e RAILS_MASTER_KEY=\"456\" --label service=\"app\" --label role=\"web\" --label traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.services.app.loadbalancer.healthcheck.path=\"/up\" --label traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\" --label traefik.http.middlewares.app.retry.attempts=\"5\" --label traefik.http.middlewares.app.retry.initialinterval=\"500ms\" dhh/app:999", "docker run --detach --restart unless-stopped --log-opt max-size=10m --name app-999 -e RAILS_MASTER_KEY=\"456\" --label service=\"app\" --label role=\"web\" --label traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.services.app.loadbalancer.healthcheck.path=\"/up\" --label traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\" --label traefik.http.middlewares.app.retry.attempts=\"3\" --label traefik.http.middlewares.app.retry.initialinterval=\"500ms\" dhh/app:999",
@app.run.join(" ") @app.run.join(" ")
end end
@@ -22,7 +22,7 @@ class CommandsAppTest < ActiveSupport::TestCase
@config[:volumes] = ["/local/path:/container/path" ] @config[:volumes] = ["/local/path:/container/path" ]
assert_equal \ assert_equal \
"docker run --detach --restart unless-stopped --log-opt max-size=10m --name app-999 -e RAILS_MASTER_KEY=\"456\" --volume /local/path:/container/path --label service=\"app\" --label role=\"web\" --label traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.services.app.loadbalancer.healthcheck.path=\"/up\" --label traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\" --label traefik.http.middlewares.app.retry.attempts=\"5\" --label traefik.http.middlewares.app.retry.initialinterval=\"500ms\" dhh/app:999", "docker run --detach --restart unless-stopped --log-opt max-size=10m --name app-999 -e RAILS_MASTER_KEY=\"456\" --volume /local/path:/container/path --label service=\"app\" --label role=\"web\" --label traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.services.app.loadbalancer.healthcheck.path=\"/up\" --label traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\" --label traefik.http.middlewares.app.retry.attempts=\"3\" --label traefik.http.middlewares.app.retry.initialinterval=\"500ms\" dhh/app:999",
@app.run.join(" ") @app.run.join(" ")
end end
@@ -30,7 +30,7 @@ class CommandsAppTest < ActiveSupport::TestCase
@config[:healthcheck] = { "path" => "/healthz" } @config[:healthcheck] = { "path" => "/healthz" }
assert_equal \ assert_equal \
"docker run --detach --restart unless-stopped --log-opt max-size=10m --name app-999 -e RAILS_MASTER_KEY=\"456\" --label service=\"app\" --label role=\"web\" --label traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.services.app.loadbalancer.healthcheck.path=\"/healthz\" --label traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\" --label traefik.http.middlewares.app.retry.attempts=\"5\" --label traefik.http.middlewares.app.retry.initialinterval=\"500ms\" dhh/app:999", "docker run --detach --restart unless-stopped --log-opt max-size=10m --name app-999 -e RAILS_MASTER_KEY=\"456\" --label service=\"app\" --label role=\"web\" --label traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.services.app.loadbalancer.healthcheck.path=\"/healthz\" --label traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\" --label traefik.http.middlewares.app.retry.attempts=\"3\" --label traefik.http.middlewares.app.retry.initialinterval=\"500ms\" dhh/app:999",
@app.run.join(" ") @app.run.join(" ")
end end

View File

@@ -42,7 +42,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
end end
test "special label args for web" do test "special label args for web" do
assert_equal [ "--label", "service=\"app\"", "--label", "role=\"web\"", "--label", "traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\"", "--label", "traefik.http.services.app.loadbalancer.healthcheck.path=\"/up\"", "--label", "traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\"", "--label", "traefik.http.middlewares.app.retry.attempts=\"5\"", "--label", "traefik.http.middlewares.app.retry.initialinterval=\"500ms\""], @config.role(:web).label_args assert_equal [ "--label", "service=\"app\"", "--label", "role=\"web\"", "--label", "traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\"", "--label", "traefik.http.services.app.loadbalancer.healthcheck.path=\"/up\"", "--label", "traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\"", "--label", "traefik.http.middlewares.app.retry.attempts=\"3\"", "--label", "traefik.http.middlewares.app.retry.initialinterval=\"500ms\""], @config.role(:web).label_args
end end
test "custom labels" do test "custom labels" do
@@ -66,7 +66,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
c[:servers]["beta"] = { "traefik" => "true", "hosts" => [ "1.1.1.5" ] } c[:servers]["beta"] = { "traefik" => "true", "hosts" => [ "1.1.1.5" ] }
}) })
assert_equal [ "--label", "service=\"app\"", "--label", "role=\"beta\"", "--label", "traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\"", "--label", "traefik.http.services.app.loadbalancer.healthcheck.path=\"/up\"", "--label", "traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\"", "--label", "traefik.http.middlewares.app.retry.attempts=\"5\"", "--label", "traefik.http.middlewares.app.retry.initialinterval=\"500ms\"" ], config.role(:beta).label_args assert_equal [ "--label", "service=\"app\"", "--label", "role=\"beta\"", "--label", "traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\"", "--label", "traefik.http.services.app.loadbalancer.healthcheck.path=\"/up\"", "--label", "traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\"", "--label", "traefik.http.middlewares.app.retry.attempts=\"3\"", "--label", "traefik.http.middlewares.app.retry.initialinterval=\"500ms\"" ], config.role(:beta).label_args
end end
test "env overwritten by role" do test "env overwritten by role" do

View File

@@ -27,5 +27,3 @@ accessories:
port: 6379 port: 6379
directories: directories:
- data:/data - data:/data
readiness_delay: 0