Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08dd468d87 | ||
|
|
9a4f502cc4 | ||
|
|
11e6f7914d | ||
|
|
bc6963e6bf | ||
|
|
f4f2b5cb17 | ||
|
|
817336df49 | ||
|
|
4c399a74bb | ||
|
|
e12436a1db | ||
|
|
b244e919bf | ||
|
|
c1013543f9 | ||
|
|
eb46d0507e | ||
|
|
7ad416f029 | ||
|
|
371f98d67f | ||
|
|
b879412a6f | ||
|
|
e678775a18 | ||
|
|
689b81014b | ||
|
|
01a4eecf98 | ||
|
|
6f7422af44 |
@@ -1,7 +1,7 @@
|
||||
PATH
|
||||
remote: .
|
||||
specs:
|
||||
mrsk (0.8.1)
|
||||
mrsk (0.8.3)
|
||||
activesupport (>= 7.0)
|
||||
dotenv (~> 2.8)
|
||||
sshkit (~> 1.21)
|
||||
|
||||
17
README.md
17
README.md
@@ -330,6 +330,21 @@ accessories:
|
||||
|
||||
Now run `mrsk accessory start mysql` to start the MySQL server on 1.1.1.3. See `mrsk accessory` for all the commands possible.
|
||||
|
||||
### Using Cron
|
||||
|
||||
You can use a specific container to run your Cron jobs:
|
||||
|
||||
```yaml
|
||||
servers:
|
||||
cron:
|
||||
hosts:
|
||||
- 192.168.0.1
|
||||
cmd:
|
||||
bash -c "cat config/crontab | crontab - && cron -f"
|
||||
```
|
||||
|
||||
This assumes the Cron settings are stored in `config/crontab`.
|
||||
|
||||
### Using a generated .env file
|
||||
|
||||
If you're using a centralized secret store, like 1Password, you can create `.env.erb` as a template which looks up the secrets. Example of a .env.erb file:
|
||||
@@ -498,7 +513,7 @@ If you wish to remove the entire application, including Traefik, containers, ima
|
||||
|
||||
## Stage of development
|
||||
|
||||
This is alpha software. Lots of stuff is missing. Lots of stuff will keep moving around for a while.
|
||||
This is beta software. Commands may still move around. But we're live in production at [37signals](https://37signals.com).
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
|
||||
execute *accessory.run
|
||||
end
|
||||
|
||||
audit_broadcast "Booted accessory #{name}"
|
||||
audit_broadcast "Booted accessory #{name}" unless options[:skip_broadcast]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,18 +5,27 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
|
||||
using_version(options[:version] || most_recent_version_available) do |version|
|
||||
say "Start container with version #{version} (or reboot if already running)...", :magenta
|
||||
|
||||
cli = self
|
||||
|
||||
MRSK.config.roles.each do |role|
|
||||
on(role.hosts) do |host|
|
||||
execute *MRSK.auditor.record("Booted app version #{version}"), verbosity: :debug
|
||||
|
||||
begin
|
||||
execute *MRSK.app.stop, raise_on_non_zero_exit: false
|
||||
old_version = capture_with_info(*MRSK.app.current_running_version).strip
|
||||
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
|
||||
if e.message =~ /already in use/
|
||||
error "Rebooting container with same version already deployed on #{host}"
|
||||
error "Rebooting container with same version #{version} already deployed on #{host} (may cause gap in zero-downtime promise!)"
|
||||
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.run(role: role.name)
|
||||
else
|
||||
|
||||
@@ -20,6 +20,8 @@ module Mrsk::Cli
|
||||
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 :skip_broadcast, aliases: "-B", type: :boolean, default: false, desc: "Skip audit broadcasts"
|
||||
|
||||
def initialize(*)
|
||||
super
|
||||
load_envs
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
class Mrsk::Cli::Healthcheck < Mrsk::Cli::Base
|
||||
MAX_ATTEMPTS = 5
|
||||
MAX_ATTEMPTS = 7
|
||||
|
||||
class HealthcheckError < StandardError; end
|
||||
|
||||
default_command :perform
|
||||
|
||||
@@ -18,7 +20,7 @@ class Mrsk::Cli::Healthcheck < Mrsk::Cli::Base
|
||||
if status == "200"
|
||||
info "#{target} succeeded with 200 OK!"
|
||||
else
|
||||
raise "#{target} failed with status #{status}"
|
||||
raise HealthcheckError, "#{target} failed with status #{status}"
|
||||
end
|
||||
rescue SSHKit::Command::Failed
|
||||
if attempt <= MAX_ATTEMPTS
|
||||
@@ -31,7 +33,7 @@ class Mrsk::Cli::Healthcheck < Mrsk::Cli::Base
|
||||
raise
|
||||
end
|
||||
end
|
||||
rescue SSHKit::Command::Failed => e
|
||||
rescue SSHKit::Command::Failed, HealthcheckError => e
|
||||
error capture_with_info(*MRSK.healthcheck.logs)
|
||||
|
||||
if e.message =~ /curl/
|
||||
|
||||
@@ -32,7 +32,7 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
|
||||
invoke "mrsk:cli:prune:all"
|
||||
end
|
||||
|
||||
audit_broadcast "Deployed app in #{runtime.to_i} seconds"
|
||||
audit_broadcast "Deployed app in #{runtime.to_i} seconds" unless options[:skip_broadcast]
|
||||
end
|
||||
|
||||
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"
|
||||
end
|
||||
|
||||
audit_broadcast "Redeployed app in #{runtime.to_i} seconds"
|
||||
audit_broadcast "Redeployed app in #{runtime.to_i} seconds" unless options[:skip_broadcast]
|
||||
end
|
||||
|
||||
desc "rollback [VERSION]", "Rollback app to VERSION"
|
||||
@@ -55,14 +55,22 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
|
||||
MRSK.version = version
|
||||
|
||||
if container_name_available?(MRSK.config.service_with_version)
|
||||
say "Stop current version, then start version #{version}...", :magenta
|
||||
say "Start version #{version}, then stop the old version...", :magenta
|
||||
|
||||
cli = self
|
||||
|
||||
on(MRSK.hosts) do |host|
|
||||
execute *MRSK.app.stop, raise_on_non_zero_exit: false
|
||||
old_version = capture_with_info(*MRSK.app.current_running_version).strip.presence
|
||||
|
||||
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
|
||||
|
||||
audit_broadcast "Rolled back app to version #{version}"
|
||||
audit_broadcast "Rolled back app to version #{version}" unless options[:skip_broadcast]
|
||||
else
|
||||
say "The app version '#{version}' is not available as a container (use 'mrsk app containers' for available versions)", :red
|
||||
end
|
||||
@@ -138,7 +146,7 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
|
||||
if options[:confirmed] || ask(remove_confirmation_question, limited_to: %w( y N ), default: "N") == "y"
|
||||
invoke "mrsk:cli:traefik:remove", [], options.without(:confirmed)
|
||||
invoke "mrsk:cli:app:remove", [], options.without(:confirmed)
|
||||
invoke "mrsk:cli:accessory:remove", [ "all" ]
|
||||
invoke "mrsk:cli:accessory:remove", [ "all" ], options
|
||||
invoke "mrsk:cli:registry:logout", [], options.without(:confirmed)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -18,8 +18,10 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
|
||||
docker :start, service_with_version
|
||||
end
|
||||
|
||||
def stop
|
||||
pipe current_container_id, xargs(docker(:stop))
|
||||
def stop(version: nil)
|
||||
pipe \
|
||||
version ? container_id_for_version(version) : current_container_id,
|
||||
xargs(docker(:stop))
|
||||
end
|
||||
|
||||
def info
|
||||
@@ -132,6 +134,10 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
|
||||
end
|
||||
end
|
||||
|
||||
def container_id_for_version(version)
|
||||
container_id_for(container_name: service_with_version(version))
|
||||
end
|
||||
|
||||
def service_filter
|
||||
[ "--filter", "label=service=#{config.service}" ]
|
||||
end
|
||||
|
||||
@@ -136,6 +136,9 @@ class Mrsk::Configuration
|
||||
{ "path" => "/up", "port" => 3000 }.merge(raw_config.healthcheck || {})
|
||||
end
|
||||
|
||||
def readiness_delay
|
||||
raw_config.readiness_delay || 7
|
||||
end
|
||||
|
||||
def valid?
|
||||
ensure_required_keys_present && ensure_env_available
|
||||
|
||||
@@ -61,7 +61,7 @@ class Mrsk::Configuration::Role
|
||||
"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.interval" => "1s",
|
||||
"traefik.http.middlewares.#{config.service}.retry.attempts" => "3",
|
||||
"traefik.http.middlewares.#{config.service}.retry.attempts" => "5",
|
||||
"traefik.http.middlewares.#{config.service}.retry.initialinterval" => "500ms"
|
||||
}
|
||||
else
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
module Mrsk
|
||||
VERSION = "0.8.1"
|
||||
VERSION = "0.8.3"
|
||||
end
|
||||
|
||||
@@ -2,7 +2,16 @@ require_relative "cli_test_case"
|
||||
|
||||
class CliAppTest < CliTestCase
|
||||
test "boot" do
|
||||
assert_match /Running docker run --detach --restart unless-stopped/, run_command("boot")
|
||||
# Stub current version fetch
|
||||
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
|
||||
|
||||
test "boot will reboot if same version is already running" do
|
||||
@@ -11,12 +20,17 @@ class CliAppTest < CliTestCase
|
||||
# Prevent expected failures from outputting to terminal
|
||||
Thread.report_on_exception = false
|
||||
|
||||
MRSK.app.stubs(:run).raises(SSHKit::Command::Failed.new("already in use")).then.returns([ :docker, :run ])
|
||||
MRSK.app.stubs(: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|
|
||||
assert_match /Rebooting container with same version already deployed/, output # Can't start what's already 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 /Rebooting container with same version 999 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 container ls --all --filter name=app-999 --quiet | xargs docker container rm/, output # Remove old container
|
||||
assert_match /docker run/, output # Start new container
|
||||
end
|
||||
ensure
|
||||
|
||||
@@ -19,7 +19,7 @@ class CliMainTest < CliTestCase
|
||||
Mrsk::Cli::Main.any_instance.stubs(:container_name_available?).returns(true)
|
||||
|
||||
run_command("rollback", "123").tap do |output|
|
||||
assert_match /Stop current version, then start version 123/, output
|
||||
assert_match /Start version 123, then stop the old version/, output
|
||||
assert_match /docker ps -q --filter label=service=app | xargs docker stop/, output
|
||||
assert_match /docker start app-123/, output
|
||||
end
|
||||
|
||||
@@ -14,7 +14,7 @@ class CommandsAppTest < ActiveSupport::TestCase
|
||||
|
||||
test "run" do
|
||||
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=\"3\" --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=\"5\" --label traefik.http.middlewares.app.retry.initialinterval=\"500ms\" dhh/app:999",
|
||||
@app.run.join(" ")
|
||||
end
|
||||
|
||||
@@ -22,7 +22,7 @@ class CommandsAppTest < ActiveSupport::TestCase
|
||||
@config[:volumes] = ["/local/path:/container/path" ]
|
||||
|
||||
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=\"3\" --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=\"5\" --label traefik.http.middlewares.app.retry.initialinterval=\"500ms\" dhh/app:999",
|
||||
@app.run.join(" ")
|
||||
end
|
||||
|
||||
@@ -30,7 +30,7 @@ class CommandsAppTest < ActiveSupport::TestCase
|
||||
@config[:healthcheck] = { "path" => "/healthz" }
|
||||
|
||||
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=\"3\" --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=\"5\" --label traefik.http.middlewares.app.retry.initialinterval=\"500ms\" dhh/app:999",
|
||||
@app.run.join(" ")
|
||||
end
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
|
||||
end
|
||||
|
||||
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=\"3\"", "--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=\"5\"", "--label", "traefik.http.middlewares.app.retry.initialinterval=\"500ms\""], @config.role(:web).label_args
|
||||
end
|
||||
|
||||
test "custom labels" do
|
||||
@@ -66,7 +66,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
|
||||
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=\"3\"", "--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=\"5\"", "--label", "traefik.http.middlewares.app.retry.initialinterval=\"500ms\"" ], config.role(:beta).label_args
|
||||
end
|
||||
|
||||
test "env overwritten by role" do
|
||||
|
||||
2
test/fixtures/deploy_with_accessories.yml
vendored
2
test/fixtures/deploy_with_accessories.yml
vendored
@@ -27,3 +27,5 @@ accessories:
|
||||
port: 6379
|
||||
directories:
|
||||
- data:/data
|
||||
|
||||
readiness_delay: 0
|
||||
Reference in New Issue
Block a user