Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
602aa43496 | ||
|
|
e35334e5fe | ||
|
|
cedb8d900f | ||
|
|
8f0b7829ce | ||
|
|
57e4f08c4c | ||
|
|
a8bfe90fbe | ||
|
|
f114dd71f6 | ||
|
|
d1b5b9cf7a | ||
|
|
66f9ce0e90 | ||
|
|
956ab3560b | ||
|
|
483b893018 | ||
|
|
19f0f40adf | ||
|
|
f9cb87e55a | ||
|
|
cc2b321d93 | ||
|
|
004f1b04e6 | ||
|
|
3b695ae127 | ||
|
|
258887a451 | ||
|
|
9fd184dc32 | ||
|
|
38023fe538 | ||
|
|
0bc1fbfb74 | ||
|
|
5ab630cb03 | ||
|
|
910f14e9c0 | ||
|
|
f3ec9f19c8 | ||
|
|
58c1096a90 | ||
|
|
340ed94fa9 | ||
|
|
4e9c39f26d | ||
|
|
d08aacadac | ||
|
|
702490d10f | ||
|
|
13079dd2a3 | ||
|
|
7daee9a0df | ||
|
|
f7c5840473 | ||
|
|
a7d869ad40 | ||
|
|
7cd25fd163 | ||
|
|
ee25f200d7 | ||
|
|
059388cb02 | ||
|
|
a5ef1f254f | ||
|
|
15e8ac0ced | ||
|
|
9a31c20321 | ||
|
|
44b83151e3 | ||
|
|
0defcbb640 | ||
|
|
5d33fb6c33 | ||
|
|
e9d838ec46 | ||
|
|
ee319fee1c | ||
|
|
5646f6cc64 | ||
|
|
31aaa82991 | ||
|
|
5ea552be40 |
@@ -1,7 +1,7 @@
|
||||
PATH
|
||||
remote: .
|
||||
specs:
|
||||
mrsk (0.12.1)
|
||||
mrsk (0.13.1)
|
||||
activesupport (>= 7.0)
|
||||
bcrypt_pbkdf (~> 1.0)
|
||||
dotenv (~> 2.8)
|
||||
|
||||
111
README.md
111
README.md
@@ -44,24 +44,24 @@ Then edit your `.env` file to add your registry password as `MRSK_REGISTRY_PASSW
|
||||
Now you're ready to deploy to the servers:
|
||||
|
||||
```
|
||||
mrsk deploy
|
||||
mrsk setup
|
||||
```
|
||||
|
||||
This will:
|
||||
|
||||
1. Connect to the servers over SSH (using root by default, authenticated by your ssh key)
|
||||
2. Install Docker on any server that might be missing it (using apt-get): root access is needed via ssh for this.
|
||||
2. Install Docker and curl on any server that might be missing it (using apt-get): root access is needed via ssh for this.
|
||||
3. Log into the registry both locally and remotely
|
||||
4. Build the image using the standard Dockerfile in the root of the application.
|
||||
5. Push the image to the registry.
|
||||
6. Pull the image from the registry onto the servers.
|
||||
7. Ensure Traefik is running and accepting traffic on port 80.
|
||||
8. Ensure your app responds with `200 OK` to `GET /up`.
|
||||
8. Ensure your app responds with `200 OK` to `GET /up` (you must have curl installed inside your app image!).
|
||||
9. Start a new container with the version of the app that matches the current git version hash.
|
||||
10. Stop the old container running the previous version of the app.
|
||||
11. Prune unused images and stopped containers to ensure servers don't fill up.
|
||||
|
||||
Voila! All the servers are now serving the app on port 80. If you're just running a single server, you're ready to go. If you're running multiple servers, you need to put a load balancer in front of them.
|
||||
Voila! All the servers are now serving the app on port 80. If you're just running a single server, you're ready to go. If you're running multiple servers, you need to put a load balancer in front of them. For subsequent deploys, or if your servers already have Docker and curl installed, you can just run `mrsk deploy`.
|
||||
|
||||
## Vision
|
||||
|
||||
@@ -184,6 +184,19 @@ registry:
|
||||
|
||||
A reference to secret `DOCKER_REGISTRY_TOKEN` will look for `ENV["DOCKER_REGISTRY_TOKEN"]` on the machine running MRSK.
|
||||
|
||||
#### Using AWS ECR as the container registry
|
||||
|
||||
AWS ECR's access token is only valid for 12hrs. In order to not have to manually regenerate the token every time, you can use ERB in the `deploy.yml` file to shell out to the `aws` cli command, and obtain the token:
|
||||
|
||||
```yaml
|
||||
registry:
|
||||
server: <your aws account id>.dkr.ecr.<your aws region id>.amazonaws.com
|
||||
username: AWS
|
||||
password: <%= %x(aws ecr get-login-password) %>
|
||||
```
|
||||
|
||||
You will need to have the `aws` CLI installed locally for this to work.
|
||||
|
||||
### Using a different SSH user than root
|
||||
|
||||
The default SSH user is root, but you can change it using `ssh/user`:
|
||||
@@ -522,7 +535,7 @@ traefik:
|
||||
options:
|
||||
publish:
|
||||
- 8080:8080
|
||||
volumes:
|
||||
volume:
|
||||
- /tmp/example.json:/tmp/example.json
|
||||
memory: 512m
|
||||
```
|
||||
@@ -655,43 +668,6 @@ servers:
|
||||
|
||||
This assumes the Cron settings are stored in `config/crontab`.
|
||||
|
||||
### 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 will be passed the audit line as the first argument:
|
||||
|
||||
```yaml
|
||||
audit_broadcast_cmd:
|
||||
bin/audit_broadcast
|
||||
```
|
||||
|
||||
The broadcast command could look something like:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
curl -q -d content="[My App] ${1}" 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] [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
|
||||
|
||||
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.
|
||||
@@ -703,6 +679,7 @@ healthcheck:
|
||||
path: /healthz
|
||||
port: 4000
|
||||
max_attempts: 7
|
||||
interval: 20s
|
||||
```
|
||||
|
||||
This will ensure your application is configured with a traefik label for the healthcheck against `/healthz` and that the pre-deploy healthcheck that MRSK performs is done against the same path on port 4000.
|
||||
@@ -888,6 +865,56 @@ When `limit` is specified, containers will be booted on, at most, `limit` hosts
|
||||
|
||||
These settings only apply when booting containers (using `mrsk deploy`, or `mrsk app boot`). For other commands, MRSK continues to run commands in parallel across all hosts.
|
||||
|
||||
## Hooks
|
||||
|
||||
You can run custom scripts at specific points with hooks.
|
||||
|
||||
Hooks should be stored in the .mrsk/hooks folder. Running mrsk init will build that folder and add some sample scripts.
|
||||
|
||||
You can change their location by setting `hooks_path` in the configuration file.
|
||||
|
||||
If the script returns a non-zero exit code the command will be aborted.
|
||||
|
||||
`MRSK_*` environment variables are available to the hooks 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_SERVICE_VERSION` - an abbreviated service and version for use in messages, e.g. app@150b24f
|
||||
- `MRSK_VERSION` - an full version being deployed
|
||||
- `MRSK_DESTINATION` - optional: destination, e.g. "staging"
|
||||
- `MRSK_HOSTS` - a comma separated list of the hosts targeted by the command
|
||||
- `MRSK_ROLE` - optional: role targeted, e.g. "web"
|
||||
|
||||
There are three hooks:
|
||||
|
||||
1. pre-connect
|
||||
Called before taking the deploy lock. For checks that need to run before connecting to remote hosts - e.g. DNS warming.
|
||||
|
||||
2. pre-build
|
||||
Used for pre-build checks - e.g. there are no uncommitted changes or that CI has passed.
|
||||
|
||||
3. post-deploy - run after a deploy, redeploy or rollback
|
||||
|
||||
This hook is also passed a `MRSK_RUNTIME` env variable.
|
||||
|
||||
This could be used to broadcast a deployment message, or register the new version with an APM.
|
||||
|
||||
The command could look something like:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
curl -q -d content="[My App] ${MRSK_PERFORMER} Rolled back to version ${MRSK_VERSION}" 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] [dhh] Rolled back to version d264c4e92470ad1bd18590f04466787262f605de
|
||||
```
|
||||
|
||||
Set `--skip_hooks` to avoid running the hooks.
|
||||
|
||||
## Stage of development
|
||||
|
||||
This is beta software. Commands may still move around. But we're live in production at [37signals](https://37signals.com).
|
||||
|
||||
2
bin/mrsk
2
bin/mrsk
@@ -8,7 +8,7 @@ require "mrsk"
|
||||
begin
|
||||
Mrsk::Cli::Main.start(ARGV)
|
||||
rescue SSHKit::Runner::ExecuteError => e
|
||||
puts " \e[31mERROR (#{e.cause.class}): #{e.cause.message}\e[0m"
|
||||
puts " \e[31mERROR (#{e.cause.class}): #{e.message}\e[0m"
|
||||
puts e.cause.backtrace if ENV["VERBOSE"]
|
||||
exit 1
|
||||
rescue => e
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
module Mrsk::Cli
|
||||
class LockError < StandardError; end
|
||||
class HookError < StandardError; end
|
||||
end
|
||||
|
||||
# SSHKit uses instance eval, so we need a global const for ergonomics
|
||||
|
||||
@@ -14,8 +14,6 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
|
||||
execute *MRSK.auditor.record("Booted #{name} accessory"), verbosity: :debug
|
||||
execute *accessory.run
|
||||
end
|
||||
|
||||
audit_broadcast "Booted accessory #{name}" unless options[:skip_broadcast]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,37 +2,39 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
|
||||
desc "boot", "Boot app on servers (or reboot app if already running)"
|
||||
def boot
|
||||
with_lock do
|
||||
say "Get most recent version available as an image...", :magenta unless options[:version]
|
||||
using_version(version_or_latest) do |version|
|
||||
say "Start container with version #{version} using a #{MRSK.config.readiness_delay}s readiness delay (or reboot if already running)...", :magenta
|
||||
hold_lock_on_error do
|
||||
say "Get most recent version available as an image...", :magenta unless options[:version]
|
||||
using_version(version_or_latest) do |version|
|
||||
say "Start container with version #{version} using a #{MRSK.config.readiness_delay}s readiness delay (or reboot if already running)...", :magenta
|
||||
|
||||
on(MRSK.hosts) do
|
||||
execute *MRSK.auditor.record("Tagging #{MRSK.config.absolute_image} as the latest image"), verbosity: :debug
|
||||
execute *MRSK.app.tag_current_as_latest
|
||||
end
|
||||
on(MRSK.hosts) do
|
||||
execute *MRSK.auditor.record("Tagging #{MRSK.config.absolute_image} as the latest image"), verbosity: :debug
|
||||
execute *MRSK.app.tag_current_as_latest
|
||||
end
|
||||
|
||||
on(MRSK.hosts, **MRSK.boot_strategy) do |host|
|
||||
roles = MRSK.roles_on(host)
|
||||
on(MRSK.hosts, **MRSK.boot_strategy) do |host|
|
||||
roles = MRSK.roles_on(host)
|
||||
|
||||
roles.each do |role|
|
||||
app = MRSK.app(role: role)
|
||||
auditor = MRSK.auditor(role: role)
|
||||
roles.each do |role|
|
||||
app = MRSK.app(role: role)
|
||||
auditor = MRSK.auditor(role: role)
|
||||
|
||||
execute *auditor.record("Booted app version #{version}"), verbosity: :debug
|
||||
if capture_with_info(*app.container_id_for_version(version, only_running: true), raise_on_non_zero_exit: false).present?
|
||||
tmp_version = "#{version}_replaced_#{SecureRandom.hex(8)}"
|
||||
info "Renaming container #{version} to #{tmp_version} as already deployed on #{host}"
|
||||
execute *auditor.record("Renaming container #{version} to #{tmp_version}"), verbosity: :debug
|
||||
execute *app.rename_container(version: version, new_version: tmp_version)
|
||||
end
|
||||
|
||||
if capture_with_info(*app.container_id_for_version(version), raise_on_non_zero_exit: false).present?
|
||||
tmp_version = "#{version}_#{SecureRandom.hex(8)}"
|
||||
info "Renaming container #{version} to #{tmp_version} as already deployed on #{host}"
|
||||
execute *auditor.record("Renaming container #{version} to #{tmp_version}"), verbosity: :debug
|
||||
execute *app.rename_container(version: version, new_version: tmp_version)
|
||||
execute *auditor.record("Booted app version #{version}"), verbosity: :debug
|
||||
|
||||
old_version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
|
||||
execute *app.start_or_run
|
||||
|
||||
Mrsk::Utils::HealthcheckPoller.wait_for_healthy(pause_after_ready: true) { capture_with_info(*app.status(version: version)) }
|
||||
|
||||
execute *app.stop(version: old_version), raise_on_non_zero_exit: false if old_version.present?
|
||||
end
|
||||
|
||||
old_version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
|
||||
execute *app.run
|
||||
|
||||
Mrsk::Utils::HealthcheckPoller.wait_for_healthy(pause_after_ready: true) { capture_with_info(*app.status(version: version)) }
|
||||
|
||||
execute *app.stop(version: old_version), raise_on_non_zero_exit: false if old_version.present?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -20,7 +20,7 @@ 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"
|
||||
class_option :skip_hooks, aliases: "-H", type: :boolean, default: false, desc: "Don't run hooks"
|
||||
|
||||
def initialize(*)
|
||||
super
|
||||
@@ -72,14 +72,12 @@ module Mrsk::Cli
|
||||
puts " Finished all in #{sprintf("%.1f seconds", runtime)}"
|
||||
end
|
||||
|
||||
def audit_broadcast(line)
|
||||
run_locally { execute *MRSK.auditor.broadcast(line), verbosity: :debug }
|
||||
end
|
||||
|
||||
def with_lock
|
||||
if MRSK.holding_lock?
|
||||
yield
|
||||
else
|
||||
run_hook "pre-connect"
|
||||
|
||||
acquire_lock
|
||||
|
||||
begin
|
||||
@@ -99,26 +97,32 @@ module Mrsk::Cli
|
||||
end
|
||||
|
||||
def acquire_lock
|
||||
say "Acquiring the deploy lock"
|
||||
on(MRSK.primary_host) { execute *MRSK.lock.acquire("Automatic deploy lock", MRSK.config.version) }
|
||||
raise_if_locked do
|
||||
say "Acquiring the deploy lock...", :magenta
|
||||
on(MRSK.primary_host) { execute *MRSK.lock.acquire("Automatic deploy lock", MRSK.config.version), verbosity: :debug }
|
||||
end
|
||||
|
||||
MRSK.holding_lock = true
|
||||
end
|
||||
|
||||
def release_lock
|
||||
say "Releasing the deploy lock...", :magenta
|
||||
on(MRSK.primary_host) { execute *MRSK.lock.release, verbosity: :debug }
|
||||
|
||||
MRSK.holding_lock = false
|
||||
end
|
||||
|
||||
def raise_if_locked
|
||||
yield
|
||||
rescue SSHKit::Runner::ExecuteError => e
|
||||
if e.message =~ /cannot create directory/
|
||||
on(MRSK.primary_host) { execute *MRSK.lock.status }
|
||||
on(MRSK.primary_host) { puts capture_with_debug(*MRSK.lock.status) }
|
||||
raise LockError, "Deploy lock found"
|
||||
else
|
||||
raise e
|
||||
end
|
||||
end
|
||||
|
||||
def release_lock
|
||||
say "Releasing the deploy lock"
|
||||
on(MRSK.primary_host) { execute *MRSK.lock.release }
|
||||
|
||||
MRSK.holding_lock = false
|
||||
end
|
||||
|
||||
def hold_lock_on_error
|
||||
if MRSK.hold_lock_on_error?
|
||||
yield
|
||||
@@ -128,5 +132,16 @@ module Mrsk::Cli
|
||||
MRSK.hold_lock_on_error = false
|
||||
end
|
||||
end
|
||||
|
||||
def run_hook(hook, **details)
|
||||
if !options[:skip_hooks] && MRSK.hook.hook_exists?(hook)
|
||||
say "Running the #{hook} hook...", :magenta
|
||||
run_locally do
|
||||
MRSK.with_verbosity(:debug) { execute *MRSK.hook.run(hook, **details, hosts: MRSK.hosts.join(",")) }
|
||||
rescue SSHKit::Command::Failed
|
||||
raise HookError.new("Hook `#{hook}` failed")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -14,11 +14,12 @@ class Mrsk::Cli::Build < Mrsk::Cli::Base
|
||||
with_lock do
|
||||
cli = self
|
||||
|
||||
verify_local_dependencies
|
||||
run_hook "pre-build"
|
||||
|
||||
run_locally do
|
||||
begin
|
||||
if cli.verify_local_dependencies
|
||||
MRSK.with_verbosity(:debug) { execute *MRSK.builder.push }
|
||||
end
|
||||
MRSK.with_verbosity(:debug) { execute *MRSK.builder.push }
|
||||
rescue SSHKit::Command::Failed => e
|
||||
if e.message =~ /(no builder)|(no such file or directory)/
|
||||
error "Missing compatible builder, so creating a new one first"
|
||||
@@ -82,21 +83,18 @@ class Mrsk::Cli::Build < Mrsk::Cli::Base
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def verify_local_dependencies
|
||||
run_locally do
|
||||
begin
|
||||
execute *MRSK.builder.ensure_local_dependencies_installed
|
||||
rescue SSHKit::Command::Failed => e
|
||||
build_error = e.message =~ /command not found/ ?
|
||||
"Docker is not installed locally" :
|
||||
"Docker buildx plugin is not installed locally"
|
||||
|
||||
desc "", "" # Really a private method, but needed to be invoked from #push
|
||||
def verify_local_dependencies
|
||||
run_locally do
|
||||
begin
|
||||
execute *MRSK.builder.ensure_local_dependencies_installed
|
||||
rescue SSHKit::Command::Failed => e
|
||||
build_error = e.message =~ /command not found/ ?
|
||||
"Docker is not installed locally" :
|
||||
"Docker buildx plugin is not installed locally"
|
||||
|
||||
raise BuildError, build_error
|
||||
raise BuildError, build_error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,7 +2,7 @@ class Mrsk::Cli::Lock < Mrsk::Cli::Base
|
||||
desc "status", "Report lock status"
|
||||
def status
|
||||
handle_missing_lock do
|
||||
on(MRSK.primary_host) { puts capture_with_info(*MRSK.lock.status) }
|
||||
on(MRSK.primary_host) { puts capture_with_debug(*MRSK.lock.status) }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -10,8 +10,8 @@ class Mrsk::Cli::Lock < Mrsk::Cli::Base
|
||||
option :message, aliases: "-m", type: :string, desc: "A lock mesasge", required: true
|
||||
def acquire
|
||||
message = options[:message]
|
||||
handle_missing_lock do
|
||||
on(MRSK.primary_host) { execute *MRSK.lock.acquire(message, MRSK.config.version) }
|
||||
raise_if_locked do
|
||||
on(MRSK.primary_host) { execute *MRSK.lock.acquire(message, MRSK.config.version), verbosity: :debug }
|
||||
say "Acquired the deploy lock"
|
||||
end
|
||||
end
|
||||
@@ -19,7 +19,7 @@ class Mrsk::Cli::Lock < Mrsk::Cli::Base
|
||||
desc "release", "Release the deploy lock"
|
||||
def release
|
||||
handle_missing_lock do
|
||||
on(MRSK.primary_host) { execute *MRSK.lock.release }
|
||||
on(MRSK.primary_host) { execute *MRSK.lock.release, verbosity: :debug }
|
||||
say "Released the deploy lock"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
class Mrsk::Cli::Main < Mrsk::Cli::Base
|
||||
desc "setup", "Setup all accessories and deploy app to servers"
|
||||
def setup
|
||||
with_lock do
|
||||
print_runtime do
|
||||
print_runtime do
|
||||
with_lock do
|
||||
invoke "mrsk:cli:server:bootstrap"
|
||||
invoke "mrsk:cli:accessory:boot", [ "all" ]
|
||||
deploy
|
||||
@@ -13,10 +13,10 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
|
||||
desc "deploy", "Deploy app to servers"
|
||||
option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip image build and push"
|
||||
def deploy
|
||||
with_lock do
|
||||
invoke_options = deploy_options
|
||||
runtime = print_runtime do
|
||||
with_lock do
|
||||
invoke_options = deploy_options
|
||||
|
||||
runtime = print_runtime do
|
||||
say "Log into image registry...", :magenta
|
||||
invoke "mrsk:cli:registry:login", [], invoke_options
|
||||
|
||||
@@ -37,25 +37,23 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
|
||||
say "Detect stale containers...", :magenta
|
||||
invoke "mrsk:cli:app:stale_containers", [], invoke_options
|
||||
|
||||
hold_lock_on_error do
|
||||
invoke "mrsk:cli:app:boot", [], invoke_options
|
||||
end
|
||||
invoke "mrsk:cli:app:boot", [], invoke_options
|
||||
|
||||
say "Prune old containers and images...", :magenta
|
||||
invoke "mrsk:cli:prune:all", [], invoke_options
|
||||
end
|
||||
|
||||
audit_broadcast "Deployed #{service_version} in #{runtime.round} seconds" unless options[:skip_broadcast]
|
||||
end
|
||||
|
||||
run_hook "post-deploy", runtime: runtime.round
|
||||
end
|
||||
|
||||
desc "redeploy", "Deploy app to servers without bootstrapping servers, starting Traefik, pruning, and registry login"
|
||||
option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip image build and push"
|
||||
def redeploy
|
||||
with_lock do
|
||||
invoke_options = deploy_options
|
||||
runtime = print_runtime do
|
||||
with_lock do
|
||||
invoke_options = deploy_options
|
||||
|
||||
runtime = print_runtime do
|
||||
if options[:skip_push]
|
||||
say "Pull app image...", :magenta
|
||||
invoke "mrsk:cli:build:pull", [], invoke_options
|
||||
@@ -70,55 +68,33 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
|
||||
say "Detect stale containers...", :magenta
|
||||
invoke "mrsk:cli:app:stale_containers", [], invoke_options
|
||||
|
||||
hold_lock_on_error do
|
||||
invoke "mrsk:cli:app:boot", [], invoke_options
|
||||
end
|
||||
invoke "mrsk:cli:app:boot", [], invoke_options
|
||||
end
|
||||
|
||||
audit_broadcast "Redeployed #{service_version} in #{runtime.round} seconds" unless options[:skip_broadcast]
|
||||
end
|
||||
|
||||
run_hook "post-deploy", runtime: runtime.round
|
||||
end
|
||||
|
||||
desc "rollback [VERSION]", "Rollback app to VERSION"
|
||||
def rollback(version)
|
||||
with_lock do
|
||||
invoke_options = deploy_options
|
||||
rolled_back = false
|
||||
runtime = print_runtime do
|
||||
with_lock do
|
||||
invoke_options = deploy_options
|
||||
|
||||
hold_lock_on_error do
|
||||
MRSK.config.version = version
|
||||
old_version = nil
|
||||
|
||||
if container_available?(version)
|
||||
say "Start version #{version}, then wait #{MRSK.config.readiness_delay}s for app to boot before stopping the old version...", :magenta
|
||||
|
||||
on(MRSK.hosts) do
|
||||
execute *MRSK.auditor.record("Tagging #{MRSK.config.absolute_image} as the latest image"), verbosity: :debug
|
||||
execute *MRSK.app.tag_current_as_latest
|
||||
end
|
||||
|
||||
on(MRSK.hosts) do |host|
|
||||
roles = MRSK.roles_on(host)
|
||||
|
||||
roles.each do |role|
|
||||
app = MRSK.app(role: role)
|
||||
old_version = capture_with_info(*app.current_running_version).strip.presence
|
||||
|
||||
execute *app.start
|
||||
|
||||
if old_version
|
||||
sleep MRSK.config.readiness_delay
|
||||
|
||||
execute *app.stop(version: old_version), raise_on_non_zero_exit: false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
audit_broadcast "Rolled back #{service_version(Mrsk::Utils.abbreviate_version(old_version))} to #{service_version}" unless options[:skip_broadcast]
|
||||
invoke "mrsk:cli:app:boot", [], invoke_options.merge(version: version)
|
||||
rolled_back = true
|
||||
else
|
||||
say "The app version '#{version}' is not available as a container (use 'mrsk app containers' for available versions)", :red
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
run_hook "post-deploy", runtime: runtime.round if rolled_back
|
||||
end
|
||||
|
||||
desc "details", "Show details about all containers"
|
||||
@@ -160,6 +136,14 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
|
||||
puts "Created .env file"
|
||||
end
|
||||
|
||||
unless (hooks_dir = Pathname.new(File.expand_path(".mrsk/hooks"))).exist?
|
||||
hooks_dir.mkpath
|
||||
Pathname.new(File.expand_path("templates/sample_hooks", __dir__)).each_child do |sample_hook|
|
||||
FileUtils.cp sample_hook, hooks_dir, preserve: true
|
||||
end
|
||||
puts "Created sample hooks in .mrsk/hooks"
|
||||
end
|
||||
|
||||
if options[:bundle]
|
||||
if (binstub = Pathname.new(File.expand_path("bin/mrsk"))).exist?
|
||||
puts "Binstub already exists in bin/mrsk (remove first to create a new one)"
|
||||
@@ -200,13 +184,6 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
|
||||
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"
|
||||
def version
|
||||
puts Mrsk::VERSION
|
||||
@@ -224,6 +201,9 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
|
||||
desc "healthcheck", "Healthcheck application"
|
||||
subcommand "healthcheck", Mrsk::Cli::Healthcheck
|
||||
|
||||
desc "lock", "Manage the deploy lock"
|
||||
subcommand "lock", Mrsk::Cli::Lock
|
||||
|
||||
desc "prune", "Prune old application images and containers"
|
||||
subcommand "prune", Mrsk::Cli::Prune
|
||||
|
||||
@@ -236,9 +216,6 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
|
||||
desc "traefik", "Manage Traefik load balancer"
|
||||
subcommand "traefik", Mrsk::Cli::Traefik
|
||||
|
||||
desc "lock", "Manage the deploy lock"
|
||||
subcommand "lock", Mrsk::Cli::Lock
|
||||
|
||||
private
|
||||
def container_available?(version)
|
||||
begin
|
||||
@@ -263,8 +240,4 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
|
||||
def deploy_options
|
||||
{ "version" => MRSK.config.version }.merge(options.without("skip_push"))
|
||||
end
|
||||
|
||||
def service_version(version = MRSK.config.abbreviated_version)
|
||||
[ MRSK.config.service, version ].compact.join("@")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -25,10 +25,6 @@ registry:
|
||||
# secret:
|
||||
# - RAILS_MASTER_KEY
|
||||
|
||||
# Call a broadcast command on deploys.
|
||||
# audit_broadcast_cmd:
|
||||
# bin/broadcast_to_bc
|
||||
|
||||
# Use a different ssh user than root
|
||||
# ssh:
|
||||
# user: app
|
||||
|
||||
14
lib/mrsk/cli/templates/sample_hooks/post-deploy.sample
Executable file
14
lib/mrsk/cli/templates/sample_hooks/post-deploy.sample
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/bin/sh
|
||||
|
||||
# A sample post-deploy hook
|
||||
#
|
||||
# These environment variables are available:
|
||||
# MRSK_RECORDED_AT
|
||||
# MRSK_PERFORMER
|
||||
# MRSK_VERSION
|
||||
# MRSK_HOSTS
|
||||
# MRSK_ROLE (if set)
|
||||
# MRSK_DESTINATION (if set)
|
||||
# MRSK_RUNTIME
|
||||
|
||||
echo "$MRSK_PERFORMER deployed $MRSK_VERSION to $MRSK_DESTINATION in $MRSK_RUNTIME seconds"
|
||||
51
lib/mrsk/cli/templates/sample_hooks/pre-build.sample
Executable file
51
lib/mrsk/cli/templates/sample_hooks/pre-build.sample
Executable file
@@ -0,0 +1,51 @@
|
||||
#!/bin/sh
|
||||
|
||||
# A sample pre-build hook
|
||||
#
|
||||
# Checks:
|
||||
# 1. We have a clean checkout
|
||||
# 2. A remote is configured
|
||||
# 3. The branch has been pushed to the remote
|
||||
# 4. The version we are deploying matches the remote
|
||||
#
|
||||
# These environment variables are available:
|
||||
# MRSK_RECORDED_AT
|
||||
# MRSK_PERFORMER
|
||||
# MRSK_VERSION
|
||||
# MRSK_HOSTS
|
||||
# MRSK_ROLE (if set)
|
||||
# MRSK_DESTINATION (if set)
|
||||
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
echo "Git checkout is not clean, aborting..." >&2
|
||||
git status --porcelain >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
first_remote=$(git remote)
|
||||
|
||||
if [ -z "$first_remote" ]; then
|
||||
echo "No git remote set, aborting..." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
current_branch=$(git branch --show-current)
|
||||
|
||||
if [ -z "$current_branch" ]; then
|
||||
echo "No git remote set, aborting..." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
remote_head=$(git ls-remote $first_remote --tags $current_branch | cut -f1)
|
||||
|
||||
if [ -z "$remote_head" ]; then
|
||||
echo "Branch not pushed to remote, aborting..." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$MRSK_VERSION" != "$remote_head" ]; then
|
||||
echo "Version ($MRSK_VERSION) does not match remote HEAD ($remote_head), aborting..." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exit 0
|
||||
47
lib/mrsk/cli/templates/sample_hooks/pre-connect.sample
Executable file
47
lib/mrsk/cli/templates/sample_hooks/pre-connect.sample
Executable file
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env ruby
|
||||
|
||||
# A sample pre-connect check
|
||||
#
|
||||
# Warms DNS before connecting to hosts in parallel
|
||||
#
|
||||
# These environment variables are available:
|
||||
# MRSK_RECORDED_AT
|
||||
# MRSK_PERFORMER
|
||||
# MRSK_VERSION
|
||||
# MRSK_HOSTS
|
||||
# MRSK_ROLE (if set)
|
||||
# MRSK_DESTINATION (if set)
|
||||
# MRSK_RUNTIME
|
||||
|
||||
hosts = ENV["MRSK_HOSTS"].split(",")
|
||||
results = nil
|
||||
max = 3
|
||||
|
||||
elapsed = Benchmark.realtime do
|
||||
results = hosts.map do |host|
|
||||
Thread.new do
|
||||
tries = 1
|
||||
|
||||
begin
|
||||
Socket.getaddrinfo(host, 0, Socket::AF_UNSPEC, Socket::SOCK_STREAM, nil, Socket::AI_CANONNAME)
|
||||
rescue SocketError
|
||||
if tries < max
|
||||
puts "Retrying DNS warmup: #{host}"
|
||||
tries += 1
|
||||
sleep rand
|
||||
retry
|
||||
else
|
||||
puts "DNS warmup failed: #{host}"
|
||||
host
|
||||
end
|
||||
end
|
||||
|
||||
tries
|
||||
end
|
||||
end.map(&:value)
|
||||
end
|
||||
|
||||
retries = results.sum - hosts.size
|
||||
nopes = results.count { |r| r == max }
|
||||
|
||||
puts "Prewarmed %d DNS lookups in %.2f sec: %d retries, %d failures" % [ hosts.size, elapsed, retries, nopes ]
|
||||
@@ -100,6 +100,14 @@ class Mrsk::Commander
|
||||
@healthcheck ||= Mrsk::Commands::Healthcheck.new(config)
|
||||
end
|
||||
|
||||
def hook
|
||||
@hook ||= Mrsk::Commands::Hook.new(config)
|
||||
end
|
||||
|
||||
def lock
|
||||
@lock ||= Mrsk::Commands::Lock.new(config)
|
||||
end
|
||||
|
||||
def prune
|
||||
@prune ||= Mrsk::Commands::Prune.new(config)
|
||||
end
|
||||
@@ -112,10 +120,6 @@ class Mrsk::Commander
|
||||
@traefik ||= Mrsk::Commands::Traefik.new(config)
|
||||
end
|
||||
|
||||
def lock
|
||||
@lock ||= Mrsk::Commands::Lock.new(config)
|
||||
end
|
||||
|
||||
def with_verbosity(level)
|
||||
old_level = self.verbosity
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
class Mrsk::Commands::App < Mrsk::Commands::Base
|
||||
ACTIVE_DOCKER_STATUSES = [ :running, :restarting ]
|
||||
|
||||
attr_reader :role
|
||||
|
||||
def initialize(config, role: nil)
|
||||
@@ -6,6 +8,10 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
|
||||
@role = role
|
||||
end
|
||||
|
||||
def start_or_run
|
||||
combine start, run, by: "||"
|
||||
end
|
||||
|
||||
def run
|
||||
role = config.role(self.role)
|
||||
|
||||
@@ -88,20 +94,20 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
|
||||
|
||||
|
||||
def current_running_container_id
|
||||
docker :ps, "--quiet", *filter_args(status: :running), "--latest"
|
||||
docker :ps, "--quiet", *filter_args(statuses: ACTIVE_DOCKER_STATUSES), "--latest"
|
||||
end
|
||||
|
||||
def container_id_for_version(version)
|
||||
container_id_for(container_name: container_name(version))
|
||||
def container_id_for_version(version, only_running: false)
|
||||
container_id_for(container_name: container_name(version), only_running: only_running)
|
||||
end
|
||||
|
||||
def current_running_version
|
||||
list_versions("--latest", status: :running)
|
||||
list_versions("--latest", statuses: ACTIVE_DOCKER_STATUSES)
|
||||
end
|
||||
|
||||
def list_versions(*docker_args, status: nil)
|
||||
def list_versions(*docker_args, statuses: nil)
|
||||
pipe \
|
||||
docker(:ps, *filter_args(status: status), *docker_args, "--format", '"{{.Names}}"'),
|
||||
docker(:ps, *filter_args(statuses: statuses), *docker_args, "--format", '"{{.Names}}"'),
|
||||
%(grep -oE "\\-[^-]+$"), # Extract SHA from "service-role-dest-SHA"
|
||||
%(cut -c 2-)
|
||||
end
|
||||
@@ -146,15 +152,17 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
|
||||
[ config.service, role, config.destination, version || config.version ].compact.join("-")
|
||||
end
|
||||
|
||||
def filter_args(status: nil)
|
||||
argumentize "--filter", filters(status: status)
|
||||
def filter_args(statuses: nil)
|
||||
argumentize "--filter", filters(statuses: statuses)
|
||||
end
|
||||
|
||||
def filters(status: nil)
|
||||
def filters(statuses: nil)
|
||||
[ "label=service=#{config.service}" ].tap do |filters|
|
||||
filters << "label=destination=#{config.destination}" if config.destination
|
||||
filters << "label=role=#{role}" if role
|
||||
filters << "status=#{status}" if status
|
||||
statuses&.each do |status|
|
||||
filters << "status=#{status}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,27 +1,18 @@
|
||||
require "time"
|
||||
|
||||
class Mrsk::Commands::Auditor < Mrsk::Commands::Base
|
||||
attr_reader :details
|
||||
|
||||
def initialize(config, **details)
|
||||
super(config)
|
||||
@details = default_details.merge(details)
|
||||
@details = details
|
||||
end
|
||||
|
||||
# Runs remotely
|
||||
def record(line, **details)
|
||||
append \
|
||||
[ :echo, *audit_tags(**details), line ],
|
||||
[ :echo, audit_tags(**details).except(:version, :service_version).to_s, line ],
|
||||
audit_log_file
|
||||
end
|
||||
|
||||
# Runs locally
|
||||
def broadcast(line, **details)
|
||||
if broadcast_cmd = config.audit_broadcast_cmd
|
||||
[ broadcast_cmd, *broadcast_args(line, **details), env: env_for(event: line, **details) ]
|
||||
end
|
||||
end
|
||||
|
||||
def reveal
|
||||
[ :tail, "-n", 50, audit_log_file ]
|
||||
end
|
||||
@@ -31,29 +22,7 @@ class Mrsk::Commands::Auditor < Mrsk::Commands::Base
|
||||
[ "mrsk", config.service, config.destination, "audit.log" ].compact.join("-")
|
||||
end
|
||||
|
||||
def default_details
|
||||
{ recorded_at: Time.now.utc.iso8601,
|
||||
performer: `whoami`.chomp,
|
||||
destination: config.destination }
|
||||
end
|
||||
|
||||
def audit_tags(**details)
|
||||
tags_for **self.details.merge(details)
|
||||
end
|
||||
|
||||
def broadcast_args(line, **details)
|
||||
"'#{broadcast_tags(**details).join(" ")} #{line}'"
|
||||
end
|
||||
|
||||
def broadcast_tags(**details)
|
||||
tags_for **self.details.merge(details).except(:recorded_at)
|
||||
end
|
||||
|
||||
def tags_for(**details)
|
||||
details.compact.values.map { |value| "[#{value}]" }
|
||||
end
|
||||
|
||||
def env_for(**details)
|
||||
self.details.merge(details).compact.transform_keys { |detail| "MRSK_#{detail.upcase}" }
|
||||
tags(**self.details, **details)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -18,8 +18,8 @@ module Mrsk::Commands
|
||||
end
|
||||
end
|
||||
|
||||
def container_id_for(container_name:)
|
||||
docker :container, :ls, "--all", "--filter", "name=^#{container_name}$", "--quiet"
|
||||
def container_id_for(container_name:, only_running: false)
|
||||
docker :container, :ls, *("--all" unless only_running), "--filter", "name=^#{container_name}$", "--quiet"
|
||||
end
|
||||
|
||||
private
|
||||
@@ -53,5 +53,9 @@ module Mrsk::Commands
|
||||
def docker(*args)
|
||||
args.compact.unshift :docker
|
||||
end
|
||||
|
||||
def tags(**details)
|
||||
Mrsk::Tags.from_config(config, **details)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
14
lib/mrsk/commands/hook.rb
Normal file
14
lib/mrsk/commands/hook.rb
Normal file
@@ -0,0 +1,14 @@
|
||||
class Mrsk::Commands::Hook < Mrsk::Commands::Base
|
||||
def run(hook, **details)
|
||||
[ hook_file(hook), env: tags(**details).env ]
|
||||
end
|
||||
|
||||
def hook_exists?(hook)
|
||||
Pathname.new(hook_file(hook)).exist?
|
||||
end
|
||||
|
||||
private
|
||||
def hook_file(hook)
|
||||
"#{config.hooks_path}/#{hook}"
|
||||
end
|
||||
end
|
||||
@@ -1,5 +1,5 @@
|
||||
class Mrsk::Commands::Traefik < Mrsk::Commands::Base
|
||||
delegate :argumentize, :optionize, to: Mrsk::Utils
|
||||
delegate :argumentize, :argumentize_env_with_secrets, :optionize, to: Mrsk::Utils
|
||||
|
||||
DEFAULT_IMAGE = "traefik:v2.9"
|
||||
CONTAINER_PORT = 80
|
||||
@@ -10,6 +10,7 @@ class Mrsk::Commands::Traefik < Mrsk::Commands::Base
|
||||
"--restart", "unless-stopped",
|
||||
"--publish", port,
|
||||
"--volume", "/var/run/docker.sock:/var/run/docker.sock",
|
||||
*env_args,
|
||||
*config.logging_args,
|
||||
*label_args,
|
||||
*docker_options_args,
|
||||
@@ -61,6 +62,16 @@ class Mrsk::Commands::Traefik < Mrsk::Commands::Base
|
||||
argumentize "--label", labels
|
||||
end
|
||||
|
||||
def env_args
|
||||
env_config = config.traefik["env"] || {}
|
||||
|
||||
if env_config.present?
|
||||
argumentize_env_with_secrets(env_config)
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def labels
|
||||
config.traefik["labels"] || []
|
||||
end
|
||||
|
||||
@@ -6,7 +6,7 @@ require "erb"
|
||||
require "net/ssh/proxy/jump"
|
||||
|
||||
class Mrsk::Configuration
|
||||
delegate :service, :image, :servers, :env, :labels, :registry, :builder, :stop_wait_time, to: :raw_config, allow_nil: true
|
||||
delegate :service, :image, :servers, :env, :labels, :registry, :builder, :stop_wait_time, :hooks_path, to: :raw_config, allow_nil: true
|
||||
delegate :argumentize, :argumentize_env_with_secrets, :optionize, to: Mrsk::Utils
|
||||
|
||||
attr_accessor :destination
|
||||
@@ -50,7 +50,7 @@ class Mrsk::Configuration
|
||||
end
|
||||
|
||||
def version
|
||||
@declared_version.presence || ENV["VERSION"] || current_commit_hash
|
||||
@declared_version.presence || ENV["VERSION"] || git_version
|
||||
end
|
||||
|
||||
def abbreviated_version
|
||||
@@ -157,10 +157,6 @@ class Mrsk::Configuration
|
||||
end
|
||||
|
||||
|
||||
def audit_broadcast_cmd
|
||||
raw_config.audit_broadcast_cmd
|
||||
end
|
||||
|
||||
def healthcheck
|
||||
{ "path" => "/up", "port" => 3000, "max_attempts" => 7 }.merge(raw_config.healthcheck || {})
|
||||
end
|
||||
@@ -197,6 +193,10 @@ class Mrsk::Configuration
|
||||
raw_config.traefik || {}
|
||||
end
|
||||
|
||||
def hooks_path
|
||||
raw_config.hooks_path || ".mrsk/hooks"
|
||||
end
|
||||
|
||||
private
|
||||
# Will raise ArgumentError if any required config keys are missing
|
||||
def ensure_required_keys_present
|
||||
@@ -233,10 +233,12 @@ class Mrsk::Configuration
|
||||
raw_config.servers.is_a?(Array) ? [ "web" ] : raw_config.servers.keys.sort
|
||||
end
|
||||
|
||||
def current_commit_hash
|
||||
@current_commit_hash ||=
|
||||
def git_version
|
||||
@git_version ||=
|
||||
if system("git rev-parse")
|
||||
`git rev-parse HEAD`.strip
|
||||
uncommitted_suffix = `git status --porcelain`.strip.present? ? "_uncommitted_#{SecureRandom.hex(8)}" : ""
|
||||
|
||||
"#{`git rev-parse HEAD`.strip}#{uncommitted_suffix}"
|
||||
else
|
||||
raise "Can't use commit hash as version, no git repository found in #{Dir.pwd}"
|
||||
end
|
||||
|
||||
@@ -37,7 +37,7 @@ class Mrsk::Configuration::Role
|
||||
|
||||
def health_check_args
|
||||
if health_check_cmd.present?
|
||||
optionize({ "health-cmd" => health_check_cmd, "health-interval" => "1s" })
|
||||
optionize({ "health-cmd" => health_check_cmd, "health-interval" => health_check_interval })
|
||||
else
|
||||
[]
|
||||
end
|
||||
@@ -50,6 +50,13 @@ class Mrsk::Configuration::Role
|
||||
options["cmd"] || http_health_check(port: options["port"], path: options["path"])
|
||||
end
|
||||
|
||||
def health_check_interval
|
||||
options = specializations["healthcheck"] || {}
|
||||
options = config.healthcheck.merge(options) if running_traefik?
|
||||
|
||||
options["interval"] || "1s"
|
||||
end
|
||||
|
||||
def cmd
|
||||
specializations["cmd"]
|
||||
end
|
||||
|
||||
@@ -8,6 +8,10 @@ class SSHKit::Backend::Abstract
|
||||
capture(*args, **kwargs, verbosity: Logger::INFO)
|
||||
end
|
||||
|
||||
def capture_with_debug(*args, **kwargs)
|
||||
capture(*args, **kwargs, verbosity: Logger::DEBUG)
|
||||
end
|
||||
|
||||
def capture_with_pretty_json(*args, **kwargs)
|
||||
JSON.pretty_generate(JSON.parse(capture(*args, **kwargs)))
|
||||
end
|
||||
|
||||
39
lib/mrsk/tags.rb
Normal file
39
lib/mrsk/tags.rb
Normal file
@@ -0,0 +1,39 @@
|
||||
require "time"
|
||||
|
||||
class Mrsk::Tags
|
||||
attr_reader :config, :tags
|
||||
|
||||
class << self
|
||||
def from_config(config, **extra)
|
||||
new(**default_tags(config), **extra)
|
||||
end
|
||||
|
||||
def default_tags(config)
|
||||
{ recorded_at: Time.now.utc.iso8601,
|
||||
performer: `whoami`.chomp,
|
||||
destination: config.destination,
|
||||
version: config.version,
|
||||
service_version: service_version(config) }
|
||||
end
|
||||
|
||||
def service_version(config)
|
||||
[ config.service, config.abbreviated_version ].compact.join("@")
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(**tags)
|
||||
@tags = tags.compact
|
||||
end
|
||||
|
||||
def env
|
||||
tags.transform_keys { |detail| "MRSK_#{detail.upcase}" }
|
||||
end
|
||||
|
||||
def to_s
|
||||
tags.values.map { |value| "[#{value}]" }.join(" ")
|
||||
end
|
||||
|
||||
def except(*tags)
|
||||
self.class.new(**self.tags.except(*tags))
|
||||
end
|
||||
end
|
||||
@@ -84,6 +84,13 @@ module Mrsk::Utils
|
||||
|
||||
# Abbreviate a git revhash for concise display
|
||||
def abbreviate_version(version)
|
||||
version[0...7] if version
|
||||
if version
|
||||
# Don't abbreviate <sha>_uncommitted_<etc>
|
||||
if version.include?("_")
|
||||
version
|
||||
else
|
||||
version[0...7]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
module Mrsk
|
||||
VERSION = "0.12.1"
|
||||
VERSION = "0.13.1"
|
||||
end
|
||||
|
||||
@@ -2,12 +2,7 @@ require_relative "cli_test_case"
|
||||
|
||||
class CliAppTest < CliTestCase
|
||||
test "boot" do
|
||||
SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info).returns("123") # old version
|
||||
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'")
|
||||
.returns("running") # health check
|
||||
|
||||
stub_running
|
||||
run_command("boot").tap do |output|
|
||||
assert_match "docker tag dhh/app:latest dhh/app:latest", output
|
||||
assert_match "docker run --detach --restart unless-stopped", output
|
||||
@@ -19,7 +14,7 @@ class CliAppTest < CliTestCase
|
||||
run_command("details") # Preheat MRSK const
|
||||
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", raise_on_non_zero_exit: false)
|
||||
.with(:docker, :container, :ls, "--filter", "name=^app-web-latest$", "--quiet", raise_on_non_zero_exit: false)
|
||||
.returns("12345678") # running version
|
||||
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||
@@ -27,12 +22,12 @@ class CliAppTest < CliTestCase
|
||||
.returns("running") # health check
|
||||
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||
.with(:docker, :ps, "--filter", "label=service=app", "--filter", "label=role=web", "--filter", "status=running", "--latest", "--format", "\"{{.Names}}\"", "|", "grep -oE \"\\-[^-]+$\"", "|", "cut -c 2-", raise_on_non_zero_exit: false)
|
||||
.with(:docker, :ps, "--filter", "label=service=app", "--filter", "label=role=web", "--filter", "status=running", "--filter", "status=restarting", "--latest", "--format", "\"{{.Names}}\"", "|", "grep -oE \"\\-[^-]+$\"", "|", "cut -c 2-", raise_on_non_zero_exit: false)
|
||||
.returns("123") # old version
|
||||
|
||||
run_command("boot").tap do |output|
|
||||
assert_match /Renaming container .* to .* as already deployed on 1.1.1.1/, output # Rename
|
||||
assert_match /docker rename .* .*/, output
|
||||
assert_match /docker rename app-web-latest app-web-latest_replaced_[0-9a-f]{16}/, output
|
||||
assert_match "docker run --detach --restart unless-stopped", output
|
||||
assert_match "docker container ls --all --filter name=^app-web-123$ --quiet | xargs docker stop", output
|
||||
end
|
||||
@@ -50,6 +45,18 @@ class CliAppTest < CliTestCase
|
||||
run_command("boot", config: :with_boot_strategy)
|
||||
end
|
||||
|
||||
test "boot errors leave lock in place" do
|
||||
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "version" => "999" }
|
||||
|
||||
Mrsk::Cli::App.any_instance.expects(:using_version).raises(RuntimeError)
|
||||
|
||||
assert !MRSK.holding_lock?
|
||||
assert_raises(RuntimeError) do
|
||||
stderred { run_command("boot") }
|
||||
end
|
||||
assert MRSK.holding_lock?
|
||||
end
|
||||
|
||||
test "start" do
|
||||
run_command("start").tap do |output|
|
||||
assert_match "docker start app-web-999", output
|
||||
@@ -58,7 +65,7 @@ class CliAppTest < CliTestCase
|
||||
|
||||
test "stop" do
|
||||
run_command("stop").tap do |output|
|
||||
assert_match "docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --latest | xargs docker stop", output
|
||||
assert_match "docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | xargs docker stop", output
|
||||
end
|
||||
end
|
||||
|
||||
@@ -91,7 +98,7 @@ class CliAppTest < CliTestCase
|
||||
|
||||
test "remove" do
|
||||
run_command("remove").tap do |output|
|
||||
assert_match /#{Regexp.escape("docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --latest | xargs docker stop")}/, output
|
||||
assert_match /#{Regexp.escape("docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | xargs docker stop")}/, output
|
||||
assert_match /#{Regexp.escape("docker container prune --force --filter label=service=app")}/, output
|
||||
assert_match /#{Regexp.escape("docker image prune --all --force --filter label=service=app")}/, output
|
||||
end
|
||||
@@ -123,7 +130,7 @@ class CliAppTest < CliTestCase
|
||||
|
||||
test "exec with reuse" do
|
||||
run_command("exec", "--reuse", "ruby -v").tap do |output|
|
||||
assert_match "docker ps --filter label=service=app --filter status=running --latest --format \"{{.Names}}\" | grep -oE \"\\-[^-]+$\" | cut -c 2-", output # Get current version
|
||||
assert_match "docker ps --filter label=service=app --filter status=running --filter status=restarting --latest --format \"{{.Names}}\" | grep -oE \"\\-[^-]+$\" | cut -c 2-", output # Get current version
|
||||
assert_match "docker exec app-web-999 ruby -v", output
|
||||
end
|
||||
end
|
||||
@@ -142,28 +149,28 @@ class CliAppTest < CliTestCase
|
||||
|
||||
test "logs" do
|
||||
SSHKit::Backend::Abstract.any_instance.stubs(:exec)
|
||||
.with("ssh -t root@1.1.1.1 'docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --latest| xargs docker logs --timestamps --tail 10 2>&1'")
|
||||
.with("ssh -t root@1.1.1.1 'docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest| xargs docker logs --timestamps --tail 10 2>&1'")
|
||||
|
||||
assert_match "docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --latest | xargs docker logs --tail 100 2>&1", run_command("logs")
|
||||
assert_match "docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | xargs docker logs --tail 100 2>&1", run_command("logs")
|
||||
end
|
||||
|
||||
test "logs with follow" do
|
||||
SSHKit::Backend::Abstract.any_instance.stubs(:exec)
|
||||
.with("ssh -t root@1.1.1.1 'docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --latest | xargs docker logs --timestamps --tail 10 --follow 2>&1'")
|
||||
.with("ssh -t root@1.1.1.1 'docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | xargs docker logs --timestamps --tail 10 --follow 2>&1'")
|
||||
|
||||
assert_match "docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --latest | xargs docker logs --timestamps --tail 10 --follow 2>&1", run_command("logs", "--follow")
|
||||
assert_match "docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | xargs docker logs --timestamps --tail 10 --follow 2>&1", run_command("logs", "--follow")
|
||||
end
|
||||
|
||||
test "version" do
|
||||
run_command("version").tap do |output|
|
||||
assert_match "docker ps --filter label=service=app --filter status=running --latest --format \"{{.Names}}\" | grep -oE \"\\-[^-]+$\" | cut -c 2-", output
|
||||
assert_match "docker ps --filter label=service=app --filter status=running --filter status=restarting --latest --format \"{{.Names}}\" | grep -oE \"\\-[^-]+$\" | cut -c 2-", output
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
test "version through main" do
|
||||
stdouted { Mrsk::Cli::Main.start(["app", "version", "-c", "test/fixtures/deploy_with_accessories.yml", "--hosts", "1.1.1.1"]) }.tap do |output|
|
||||
assert_match "docker ps --filter label=service=app --filter status=running --latest --format \"{{.Names}}\" | grep -oE \"\\-[^-]+$\" | cut -c 2-", output
|
||||
assert_match "docker ps --filter label=service=app --filter status=running --filter status=restarting --latest --format \"{{.Names}}\" | grep -oE \"\\-[^-]+$\" | cut -c 2-", output
|
||||
end
|
||||
end
|
||||
|
||||
@@ -171,4 +178,12 @@ class CliAppTest < CliTestCase
|
||||
def run_command(*command, config: :with_accessories)
|
||||
stdouted { Mrsk::Cli::App.start([*command, "-c", "test/fixtures/deploy_#{config}.yml", "--hosts", "1.1.1.1"]) }
|
||||
end
|
||||
|
||||
def stub_running
|
||||
SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info).returns("123") # old version
|
||||
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'")
|
||||
.returns("running") # health check
|
||||
end
|
||||
end
|
||||
|
||||
@@ -9,17 +9,19 @@ class CliBuildTest < CliTestCase
|
||||
end
|
||||
|
||||
test "push" do
|
||||
Mrsk::Cli::Build.any_instance.stubs(:verify_local_dependencies).returns(true)
|
||||
run_command("push").tap do |output|
|
||||
assert_match /docker --version && docker buildx version/, output
|
||||
assert_match /docker buildx build --push --platform linux\/amd64,linux\/arm64 --builder mrsk-app-multiarch -t dhh\/app:999 -t dhh\/app:latest --label service="app" --file Dockerfile \. as .*@localhost/, output
|
||||
end
|
||||
end
|
||||
|
||||
test "push without builder" do
|
||||
stub_locking
|
||||
Mrsk::Cli::Build.any_instance.stubs(:verify_local_dependencies).returns(true)
|
||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||
.with { |arg| arg == :docker }
|
||||
.with(:docker, "--version", "&&", :docker, :buildx, "version")
|
||||
|
||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||
.with { |*args| args[0..1] == [:docker, :buildx] }
|
||||
.raises(SSHKit::Command::Failed.new("no builder"))
|
||||
.then
|
||||
.returns(true)
|
||||
@@ -29,6 +31,24 @@ class CliBuildTest < CliTestCase
|
||||
end
|
||||
end
|
||||
|
||||
test "push with no buildx plugin" do
|
||||
stub_locking
|
||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||
.with(:docker, "--version", "&&", :docker, :buildx, "version")
|
||||
.raises(SSHKit::Command::Failed.new("no buildx"))
|
||||
|
||||
Mrsk::Commands::Builder.any_instance.stubs(:native_and_local?).returns(false)
|
||||
assert_raises(Mrsk::Cli::Build::BuildError) { run_command("push") }
|
||||
end
|
||||
|
||||
test "push pre-build hook failure" do
|
||||
fail_hook("pre-build")
|
||||
|
||||
assert_raises(Mrsk::Cli::HookError) { run_command("push") }
|
||||
|
||||
assert @executions.none? { |args| args[0..2] == [:docker, :buildx, :build] }
|
||||
end
|
||||
|
||||
test "pull" do
|
||||
run_command("pull").tap do |output|
|
||||
assert_match /docker image rm --force dhh\/app:999/, output
|
||||
@@ -70,32 +90,15 @@ class CliBuildTest < CliTestCase
|
||||
end
|
||||
end
|
||||
|
||||
test "verify local dependencies" do
|
||||
Mrsk::Commands::Builder.any_instance.stubs(:name).returns("remote".inquiry)
|
||||
|
||||
run_command("verify_local_dependencies").tap do |output|
|
||||
assert_match /docker --version && docker buildx version/, output
|
||||
end
|
||||
end
|
||||
|
||||
test "verify local dependencies with no buildx plugin" do
|
||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||
.with(:docker, "--version", "&&", :docker, :buildx, "version")
|
||||
.raises(SSHKit::Command::Failed.new("no buildx"))
|
||||
|
||||
Mrsk::Commands::Builder.any_instance.stubs(:native_and_local?).returns(false)
|
||||
assert_raises(Mrsk::Cli::Build::BuildError) { run_command("verify_local_dependencies") }
|
||||
end
|
||||
|
||||
private
|
||||
def run_command(*command)
|
||||
stdouted { Mrsk::Cli::Build.start([*command, "-c", "test/fixtures/deploy_with_accessories.yml"]) }
|
||||
end
|
||||
|
||||
def stub_locking
|
||||
def stub_dependency_checks
|
||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||
.with { |arg1, arg2| arg1 == :mkdir && arg2 == :mrsk_lock }
|
||||
.with(:docker, "--version", "&&", :docker, :buildx, "version")
|
||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||
.with { |arg1, arg2| arg1 == :rm && arg2 == "mrsk_lock/details" }
|
||||
.with { |*args| args[0..1] == [:docker, :buildx] }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
require "test_helper"
|
||||
|
||||
class CliTestCase < ActiveSupport::TestCase
|
||||
include ActiveSupport::Testing::Stream
|
||||
|
||||
setup do
|
||||
ENV["VERSION"] = "999"
|
||||
ENV["RAILS_MASTER_KEY"] = "123"
|
||||
@@ -16,4 +14,32 @@ class CliTestCase < ActiveSupport::TestCase
|
||||
ENV.delete("MYSQL_ROOT_PASSWORD")
|
||||
ENV.delete("VERSION")
|
||||
end
|
||||
|
||||
private
|
||||
def fail_hook(hook)
|
||||
@executions = []
|
||||
Mrsk::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)
|
||||
|
||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||
.with { |*args| @executions << args; args != [".mrsk/hooks/#{hook}"] }
|
||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||
.with { |*args| args.first == ".mrsk/hooks/#{hook}" }
|
||||
.raises(SSHKit::Command::Failed.new("failed"))
|
||||
end
|
||||
|
||||
def ensure_hook_runs(hook)
|
||||
Mrsk::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)
|
||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||
.with { |*args| args != [".mrsk/hooks/#{hook}"] }
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:execute)
|
||||
.with { |*args| args.first == ".mrsk/hooks/#{hook}" }
|
||||
.once
|
||||
end
|
||||
|
||||
def stub_locking
|
||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||
.with { |arg1, arg2| arg1 == :mkdir && arg2 == :mrsk_lock }
|
||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||
.with { |arg1, arg2| arg1 == :rm && arg2 == "mrsk_lock/details" }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -10,7 +10,7 @@ class CliMainTest < CliTestCase
|
||||
end
|
||||
|
||||
test "deploy" do
|
||||
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "skip_broadcast" => false, "version" => "999" }
|
||||
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "version" => "999", "skip_hooks" => false }
|
||||
|
||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:registry:login", [], invoke_options)
|
||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:build:deliver", [], invoke_options)
|
||||
@@ -20,18 +20,22 @@ class CliMainTest < CliTestCase
|
||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:app:boot", [], invoke_options)
|
||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:prune:all", [], invoke_options)
|
||||
|
||||
Mrsk::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)
|
||||
|
||||
run_command("deploy").tap do |output|
|
||||
assert_match /Running the pre-connect hook.../, output
|
||||
assert_match /Log into image registry/, output
|
||||
assert_match /Build and push app image/, output
|
||||
assert_match /Ensure Traefik is running/, output
|
||||
assert_match /Ensure app can pass healthcheck/, output
|
||||
assert_match /Detect stale containers/, output
|
||||
assert_match /Prune old containers and images/, output
|
||||
assert_match /Running the post-deploy hook.../, output
|
||||
end
|
||||
end
|
||||
|
||||
test "deploy with skip_push" do
|
||||
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "skip_broadcast" => false, "version" => "999" }
|
||||
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "version" => "999", "skip_hooks" => false }
|
||||
|
||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:registry:login", [], invoke_options)
|
||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:build:pull", [], invoke_options)
|
||||
@@ -60,7 +64,7 @@ class CliMainTest < CliTestCase
|
||||
.with { |*arg| arg[0..1] == [:mkdir, :mrsk_lock] }
|
||||
.raises(RuntimeError, "mkdir: cannot create directory ‘mrsk_lock’: File exists")
|
||||
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:execute)
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_debug)
|
||||
.with(:stat, :mrsk_lock, ">", "/dev/null", "&&", :cat, "mrsk_lock/details", "|", :base64, "-d")
|
||||
|
||||
assert_raises(Mrsk::Cli::LockError) do
|
||||
@@ -80,25 +84,8 @@ class CliMainTest < CliTestCase
|
||||
end
|
||||
end
|
||||
|
||||
test "deploy errors during critical section leave lock in place" do
|
||||
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "skip_broadcast" => false, "version" => "999" }
|
||||
|
||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:registry:login", [], invoke_options)
|
||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:build:deliver", [], invoke_options)
|
||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:app:stale_containers", [], invoke_options)
|
||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:traefik:boot", [], invoke_options)
|
||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:healthcheck:perform", [], invoke_options)
|
||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:app:boot", [], invoke_options).raises(RuntimeError)
|
||||
|
||||
assert !MRSK.holding_lock?
|
||||
assert_raises(RuntimeError) do
|
||||
stderred { run_command("deploy") }
|
||||
end
|
||||
assert MRSK.holding_lock?
|
||||
end
|
||||
|
||||
test "deploy errors during outside section leave remove lock" do
|
||||
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "skip_broadcast" => false, "version" => "999" }
|
||||
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "version" => "999", "skip_hooks" => false }
|
||||
|
||||
Mrsk::Cli::Main.any_instance.expects(:invoke)
|
||||
.with("mrsk:cli:registry:login", [], invoke_options)
|
||||
@@ -111,22 +98,41 @@ class CliMainTest < CliTestCase
|
||||
assert !MRSK.holding_lock?
|
||||
end
|
||||
|
||||
test "deploy with skipped hooks" do
|
||||
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "version" => "999", "skip_hooks" => true }
|
||||
|
||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:registry:login", [], invoke_options)
|
||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:build:deliver", [], invoke_options)
|
||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:traefik:boot", [], invoke_options)
|
||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:healthcheck:perform", [], invoke_options)
|
||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:app:stale_containers", [], invoke_options)
|
||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:app:boot", [], invoke_options)
|
||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:prune:all", [], invoke_options)
|
||||
|
||||
run_command("deploy", "--skip_hooks") do
|
||||
refute_match /Running the post-deploy hook.../, output
|
||||
end
|
||||
end
|
||||
|
||||
test "redeploy" do
|
||||
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "skip_broadcast" => false, "version" => "999" }
|
||||
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "version" => "999", "skip_hooks" => false }
|
||||
|
||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:build:deliver", [], invoke_options)
|
||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:healthcheck:perform", [], invoke_options)
|
||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:app:stale_containers", [], invoke_options)
|
||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:app:boot", [], invoke_options)
|
||||
|
||||
Mrsk::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)
|
||||
|
||||
run_command("redeploy").tap do |output|
|
||||
assert_match /Build and push app image/, output
|
||||
assert_match /Ensure app can pass healthcheck/, output
|
||||
assert_match /Running the post-deploy hook.../, output
|
||||
end
|
||||
end
|
||||
|
||||
test "redeploy with skip_push" do
|
||||
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "skip_broadcast" => false, "version" => "999" }
|
||||
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "version" => "999", "skip_hooks" => false }
|
||||
|
||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:build:pull", [], invoke_options)
|
||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:healthcheck:perform", [], invoke_options)
|
||||
@@ -151,35 +157,50 @@ class CliMainTest < CliTestCase
|
||||
end
|
||||
|
||||
test "rollback good version" do
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-123$", "--quiet")
|
||||
.returns("version-to-rollback\n").at_least_once
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-workers-123$", "--quiet")
|
||||
.returns("version-to-rollback\n").at_least_once
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||
.with(:docker, :ps, "--filter", "label=service=app", "--filter", "label=role=web", "--filter", "status=running", "--latest", "--format", "\"{{.Names}}\"", "|", "grep -oE \"\\-[^-]+$\"", "|", "cut -c 2-")
|
||||
.returns("version-to-rollback\n").at_least_once
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||
.with(:docker, :ps, "--filter", "label=service=app", "--filter", "label=role=workers", "--filter", "status=running", "--latest", "--format", "\"{{.Names}}\"", "|", "grep -oE \"\\-[^-]+$\"", "|", "cut -c 2-")
|
||||
.returns("version-to-rollback\n").at_least_once
|
||||
[ "web", "workers" ].each do |role|
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||
.with(:docker, :container, :ls, "--filter", "name=^app-#{role}-123$", "--quiet", raise_on_non_zero_exit: false)
|
||||
.returns("").at_least_once
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-#{role}-123$", "--quiet")
|
||||
.returns("version-to-rollback\n").at_least_once
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||
.with(:docker, :ps, "--filter", "label=service=app", "--filter", "label=role=#{role}", "--filter", "status=running", "--filter", "status=restarting", "--latest", "--format", "\"{{.Names}}\"", "|", "grep -oE \"\\-[^-]+$\"", "|", "cut -c 2-", raise_on_non_zero_exit: false)
|
||||
.returns("version-to-rollback\n").at_least_once
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-#{role}-123$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'")
|
||||
.returns("running").at_least_once # health check
|
||||
end
|
||||
|
||||
Mrsk::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)
|
||||
|
||||
run_command("rollback", "123", config_file: "deploy_with_accessories").tap do |output|
|
||||
assert_match "Start version 123", output
|
||||
assert_match "Start container with version 123", output
|
||||
assert_match "docker tag dhh/app:123 dhh/app:latest", output
|
||||
assert_match "docker start app-web-123", output
|
||||
assert_match "docker container ls --all --filter name=^app-web-version-to-rollback$ --quiet | xargs docker stop", output, "Should stop the container that was previously running"
|
||||
assert_match "Running the post-deploy hook...", output
|
||||
end
|
||||
end
|
||||
|
||||
test "rollback without old version" do
|
||||
Mrsk::Cli::Main.any_instance.stubs(:container_available?).returns(true)
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info).with(:docker, :ps, "--filter", "label=service=app", "--filter", "label=role=web", "--filter", "status=running", "--latest", "--format", "\"{{.Names}}\"", "|", "grep -oE \"\\-[^-]+$\"", "|", "cut -c 2-").returns("").at_least_once
|
||||
|
||||
Mrsk::Utils::HealthcheckPoller.stubs(:sleep)
|
||||
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||
.with(:docker, :container, :ls, "--filter", "name=^app-web-123$", "--quiet", raise_on_non_zero_exit: false)
|
||||
.returns("").at_least_once
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||
.with(:docker, :ps, "--filter", "label=service=app", "--filter", "label=role=web", "--filter", "status=running", "--filter", "status=restarting", "--latest", "--format", "\"{{.Names}}\"", "|", "grep -oE \"\\-[^-]+$\"", "|", "cut -c 2-", raise_on_non_zero_exit: false)
|
||||
.returns("").at_least_once
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-123$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'")
|
||||
.returns("running").at_least_once # health check
|
||||
|
||||
run_command("rollback", "123").tap do |output|
|
||||
assert_match "Start version 123", output
|
||||
assert_match "docker start app-web-123", output
|
||||
assert_match "Start container with version 123", output
|
||||
assert_match "docker start app-web-123 || docker run --detach --restart unless-stopped --name app-web-123", output
|
||||
assert_no_match "docker stop", output
|
||||
end
|
||||
end
|
||||
@@ -239,9 +260,11 @@ class CliMainTest < CliTestCase
|
||||
end
|
||||
|
||||
test "init" do
|
||||
Pathname.any_instance.expects(:exist?).returns(false).twice
|
||||
Pathname.any_instance.expects(:exist?).returns(false).times(3)
|
||||
Pathname.any_instance.stubs(:mkpath)
|
||||
FileUtils.stubs(:mkdir_p)
|
||||
FileUtils.stubs(:cp_r)
|
||||
FileUtils.stubs(:cp)
|
||||
|
||||
run_command("init").tap do |output|
|
||||
assert_match /Created configuration file in config\/deploy.yml/, output
|
||||
@@ -250,7 +273,7 @@ class CliMainTest < CliTestCase
|
||||
end
|
||||
|
||||
test "init with existing config" do
|
||||
Pathname.any_instance.expects(:exist?).returns(true).twice
|
||||
Pathname.any_instance.expects(:exist?).returns(true).times(3)
|
||||
|
||||
run_command("init").tap do |output|
|
||||
assert_match /Config file already exists in config\/deploy.yml \(remove first to create a new one\)/, output
|
||||
@@ -258,9 +281,11 @@ class CliMainTest < CliTestCase
|
||||
end
|
||||
|
||||
test "init with bundle option" do
|
||||
Pathname.any_instance.expects(:exist?).returns(false).times(3)
|
||||
Pathname.any_instance.expects(:exist?).returns(false).times(4)
|
||||
Pathname.any_instance.stubs(:mkpath)
|
||||
FileUtils.stubs(:mkdir_p)
|
||||
FileUtils.stubs(:cp_r)
|
||||
FileUtils.stubs(:cp)
|
||||
|
||||
run_command("init", "--bundle").tap do |output|
|
||||
assert_match /Created configuration file in config\/deploy.yml/, output
|
||||
@@ -273,9 +298,11 @@ class CliMainTest < CliTestCase
|
||||
end
|
||||
|
||||
test "init with bundle option and existing binstub" do
|
||||
Pathname.any_instance.expects(:exist?).returns(true).times(3)
|
||||
Pathname.any_instance.expects(:exist?).returns(true).times(4)
|
||||
Pathname.any_instance.stubs(:mkpath)
|
||||
FileUtils.stubs(:mkdir_p)
|
||||
FileUtils.stubs(:cp_r)
|
||||
FileUtils.stubs(:cp)
|
||||
|
||||
run_command("init", "--bundle").tap do |output|
|
||||
assert_match /Config file already exists in config\/deploy.yml \(remove first to create a new one\)/, output
|
||||
@@ -321,19 +348,6 @@ class CliMainTest < CliTestCase
|
||||
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
|
||||
version = stdouted { Mrsk::Cli::Main.new.version }
|
||||
assert_equal Mrsk::VERSION, version
|
||||
|
||||
@@ -79,14 +79,14 @@ class CommandsAppTest < ActiveSupport::TestCase
|
||||
|
||||
test "stop" do
|
||||
assert_equal \
|
||||
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --latest | xargs docker stop",
|
||||
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | xargs docker stop",
|
||||
new_command.stop.join(" ")
|
||||
end
|
||||
|
||||
test "stop with custom stop wait time" do
|
||||
@config[:stop_wait_time] = 30
|
||||
assert_equal \
|
||||
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --latest | xargs docker stop -t 30",
|
||||
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | xargs docker stop -t 30",
|
||||
new_command.stop.join(" ")
|
||||
end
|
||||
|
||||
@@ -112,37 +112,37 @@ class CommandsAppTest < ActiveSupport::TestCase
|
||||
|
||||
test "logs" do
|
||||
assert_equal \
|
||||
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --latest | xargs docker logs 2>&1",
|
||||
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | xargs docker logs 2>&1",
|
||||
new_command.logs.join(" ")
|
||||
|
||||
assert_equal \
|
||||
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --latest | xargs docker logs --since 5m 2>&1",
|
||||
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | xargs docker logs --since 5m 2>&1",
|
||||
new_command.logs(since: "5m").join(" ")
|
||||
|
||||
assert_equal \
|
||||
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --latest | xargs docker logs --tail 100 2>&1",
|
||||
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | xargs docker logs --tail 100 2>&1",
|
||||
new_command.logs(lines: "100").join(" ")
|
||||
|
||||
assert_equal \
|
||||
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --latest | xargs docker logs --since 5m --tail 100 2>&1",
|
||||
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | xargs docker logs --since 5m --tail 100 2>&1",
|
||||
new_command.logs(since: "5m", lines: "100").join(" ")
|
||||
|
||||
assert_equal \
|
||||
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --latest | xargs docker logs 2>&1 | grep 'my-id'",
|
||||
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | xargs docker logs 2>&1 | grep 'my-id'",
|
||||
new_command.logs(grep: "my-id").join(" ")
|
||||
|
||||
assert_equal \
|
||||
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --latest | xargs docker logs --since 5m 2>&1 | grep 'my-id'",
|
||||
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | xargs docker logs --since 5m 2>&1 | grep 'my-id'",
|
||||
new_command.logs(since: "5m", grep: "my-id").join(" ")
|
||||
end
|
||||
|
||||
test "follow logs" do
|
||||
assert_match \
|
||||
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --latest | xargs docker logs --timestamps --tail 10 --follow 2>&1",
|
||||
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | xargs docker logs --timestamps --tail 10 --follow 2>&1",
|
||||
new_command.follow_logs(host: "app-1")
|
||||
|
||||
assert_match \
|
||||
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --latest | xargs docker logs --timestamps --tail 10 --follow 2>&1 | grep \"Completed\"",
|
||||
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | xargs docker logs --timestamps --tail 10 --follow 2>&1 | grep \"Completed\"",
|
||||
new_command.follow_logs(host: "app-1", grep: "Completed")
|
||||
end
|
||||
|
||||
@@ -196,14 +196,14 @@ class CommandsAppTest < ActiveSupport::TestCase
|
||||
|
||||
test "current_running_container_id" do
|
||||
assert_equal \
|
||||
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --latest",
|
||||
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest",
|
||||
new_command.current_running_container_id.join(" ")
|
||||
end
|
||||
|
||||
test "current_running_container_id with destination" do
|
||||
@destination = "staging"
|
||||
assert_equal \
|
||||
"docker ps --quiet --filter label=service=app --filter label=destination=staging --filter label=role=web --filter status=running --latest",
|
||||
"docker ps --quiet --filter label=service=app --filter label=destination=staging --filter label=role=web --filter status=running --filter status=restarting --latest",
|
||||
new_command.current_running_container_id.join(" ")
|
||||
end
|
||||
|
||||
@@ -215,7 +215,7 @@ class CommandsAppTest < ActiveSupport::TestCase
|
||||
|
||||
test "current_running_version" do
|
||||
assert_equal \
|
||||
"docker ps --filter label=service=app --filter label=role=web --filter status=running --latest --format \"{{.Names}}\" | grep -oE \"\\-[^-]+$\" | cut -c 2-",
|
||||
"docker ps --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest --format \"{{.Names}}\" | grep -oE \"\\-[^-]+$\" | cut -c 2-",
|
||||
new_command.current_running_version.join(" ")
|
||||
end
|
||||
|
||||
@@ -225,8 +225,8 @@ class CommandsAppTest < ActiveSupport::TestCase
|
||||
new_command.list_versions.join(" ")
|
||||
|
||||
assert_equal \
|
||||
"docker ps --filter label=service=app --filter label=role=web --filter status=running --latest --format \"{{.Names}}\" | grep -oE \"\\-[^-]+$\" | cut -c 2-",
|
||||
new_command.list_versions("--latest", status: :running).join(" ")
|
||||
"docker ps --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest --format \"{{.Names}}\" | grep -oE \"\\-[^-]+$\" | cut -c 2-",
|
||||
new_command.list_versions("--latest", statuses: [ :running, :restarting ]).join(" ")
|
||||
end
|
||||
|
||||
test "list_containers" do
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
require "test_helper"
|
||||
require "active_support/testing/time_helpers"
|
||||
|
||||
class CommandsAuditorTest < ActiveSupport::TestCase
|
||||
include ActiveSupport::Testing::TimeHelpers
|
||||
|
||||
setup do
|
||||
freeze_time
|
||||
|
||||
@config = {
|
||||
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ],
|
||||
audit_broadcast_cmd: "bin/audit_broadcast"
|
||||
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ]
|
||||
}
|
||||
|
||||
@auditor = new_command
|
||||
@performer = `whoami`.strip
|
||||
@recorded_at = Time.now.utc.iso8601
|
||||
end
|
||||
|
||||
test "record" do
|
||||
assert_equal [
|
||||
:echo,
|
||||
"[#{@auditor.details[:recorded_at]}]", "[#{@auditor.details[:performer]}]",
|
||||
"[#{@recorded_at}] [#{@performer}]",
|
||||
"app removed container",
|
||||
">>", "mrsk-app-audit.log"
|
||||
], @auditor.record("app removed container")
|
||||
@@ -23,7 +29,7 @@ class CommandsAuditorTest < ActiveSupport::TestCase
|
||||
new_command(destination: "staging").tap do |auditor|
|
||||
assert_equal [
|
||||
:echo,
|
||||
"[#{auditor.details[:recorded_at]}]", "[#{auditor.details[:performer]}]", "[#{auditor.details[:destination]}]",
|
||||
"[#{@recorded_at}] [#{@performer}] [staging]",
|
||||
"app removed container",
|
||||
">>", "mrsk-app-staging-audit.log"
|
||||
], auditor.record("app removed container")
|
||||
@@ -34,7 +40,7 @@ class CommandsAuditorTest < ActiveSupport::TestCase
|
||||
new_command(role: "web").tap do |auditor|
|
||||
assert_equal [
|
||||
:echo,
|
||||
"[#{auditor.details[:recorded_at]}]", "[#{auditor.details[:performer]}]", "[#{auditor.details[:role]}]",
|
||||
"[#{@recorded_at}] [#{@performer}] [web]",
|
||||
"app removed container",
|
||||
">>", "mrsk-app-audit.log"
|
||||
], auditor.record("app removed container")
|
||||
@@ -44,24 +50,12 @@ class CommandsAuditorTest < ActiveSupport::TestCase
|
||||
test "record with arg details" do
|
||||
assert_equal [
|
||||
:echo,
|
||||
"[#{@auditor.details[:recorded_at]}]", "[#{@auditor.details[:performer]}]", "[value]",
|
||||
"[#{@recorded_at}] [#{@performer}] [value]",
|
||||
"app removed container",
|
||||
">>", "mrsk-app-audit.log"
|
||||
], @auditor.record("app removed container", detail: "value")
|
||||
end
|
||||
|
||||
test "broadcast" do
|
||||
assert_equal [
|
||||
"bin/audit_broadcast",
|
||||
"'[#{@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
|
||||
|
||||
private
|
||||
def new_command(destination: nil, **details)
|
||||
|
||||
44
test/commands/hook_test.rb
Normal file
44
test/commands/hook_test.rb
Normal file
@@ -0,0 +1,44 @@
|
||||
require "test_helper"
|
||||
|
||||
class CommandsHookTest < ActiveSupport::TestCase
|
||||
include ActiveSupport::Testing::TimeHelpers
|
||||
|
||||
setup do
|
||||
freeze_time
|
||||
|
||||
@config = {
|
||||
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ],
|
||||
traefik: { "args" => { "accesslog.format" => "json", "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } }
|
||||
}
|
||||
|
||||
@performer = `whoami`.strip
|
||||
@recorded_at = Time.now.utc.iso8601
|
||||
end
|
||||
|
||||
test "run" do
|
||||
assert_equal [
|
||||
".mrsk/hooks/foo",
|
||||
{ env: {
|
||||
"MRSK_RECORDED_AT" => @recorded_at,
|
||||
"MRSK_PERFORMER" => @performer,
|
||||
"MRSK_VERSION" => "123",
|
||||
"MRSK_SERVICE_VERSION" => "app@123" } }
|
||||
], new_command.run("foo")
|
||||
end
|
||||
|
||||
test "run with custom hooks_path" do
|
||||
assert_equal [
|
||||
"custom/hooks/path/foo",
|
||||
{ env: {
|
||||
"MRSK_RECORDED_AT" => @recorded_at,
|
||||
"MRSK_PERFORMER" => @performer,
|
||||
"MRSK_VERSION" => "123",
|
||||
"MRSK_SERVICE_VERSION" => "app@123" } }
|
||||
], new_command(hooks_path: "custom/hooks/path").run("foo")
|
||||
end
|
||||
|
||||
private
|
||||
def new_command(**extra_config)
|
||||
Mrsk::Commands::Hook.new(Mrsk::Configuration.new(@config.merge(**extra_config), version: "123"))
|
||||
end
|
||||
end
|
||||
@@ -8,6 +8,12 @@ class CommandsTraefikTest < ActiveSupport::TestCase
|
||||
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ],
|
||||
traefik: { "image" => @image, "args" => { "accesslog.format" => "json", "api.insecure" => true, "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } }
|
||||
}
|
||||
|
||||
ENV["EXAMPLE_API_KEY"] = "456"
|
||||
end
|
||||
|
||||
teardown do
|
||||
ENV.delete("EXAMPLE_API_KEY")
|
||||
end
|
||||
|
||||
test "run" do
|
||||
@@ -65,6 +71,17 @@ class CommandsTraefikTest < ActiveSupport::TestCase
|
||||
new_command.run.join(" ")
|
||||
end
|
||||
|
||||
test "run with env configured" do
|
||||
assert_equal \
|
||||
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --log-opt max-size=\"10m\" #{@image} --providers.docker --log.level=DEBUG --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
|
||||
new_command.run.join(" ")
|
||||
|
||||
@config[:traefik]["env"] = { "secret" => %w[EXAMPLE_API_KEY] }
|
||||
assert_equal \
|
||||
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock -e EXAMPLE_API_KEY=\"456\" --log-opt max-size=\"10m\" #{@image} --providers.docker --log.level=DEBUG --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
|
||||
new_command.run.join(" ")
|
||||
end
|
||||
|
||||
test "run without configuration" do
|
||||
@config.delete(:traefik)
|
||||
|
||||
|
||||
@@ -72,19 +72,36 @@ class ConfigurationTest < ActiveSupport::TestCase
|
||||
assert_equal [ "1.1.1.1", "1.1.1.2", "1.1.1.3" ], config.traefik_hosts
|
||||
end
|
||||
|
||||
test "version" do
|
||||
test "version no git repo" do
|
||||
ENV.delete("VERSION")
|
||||
|
||||
@config.expects(:system).with("git rev-parse").returns(nil)
|
||||
error = assert_raises(RuntimeError) { @config.version}
|
||||
assert_match /no git repository found/, error.message
|
||||
end
|
||||
|
||||
@config.expects(:current_commit_hash).returns("git-version")
|
||||
test "version from git committed" do
|
||||
ENV.delete("VERSION")
|
||||
|
||||
@config.expects(:`).with("git rev-parse HEAD").returns("git-version")
|
||||
@config.expects(:`).with("git status --porcelain").returns("")
|
||||
assert_equal "git-version", @config.version
|
||||
end
|
||||
|
||||
test "version from git uncommitted" do
|
||||
ENV.delete("VERSION")
|
||||
|
||||
@config.expects(:`).with("git rev-parse HEAD").returns("git-version")
|
||||
@config.expects(:`).with("git status --porcelain").returns("M file\n")
|
||||
assert_match /^git-version_uncommitted_[0-9a-f]{16}$/, @config.version
|
||||
end
|
||||
|
||||
test "version from env" do
|
||||
ENV["VERSION"] = "env-version"
|
||||
assert_equal "env-version", @config.version
|
||||
end
|
||||
|
||||
test "version from arg" do
|
||||
@config.version = "arg-version"
|
||||
assert_equal "arg-version", @config.version
|
||||
end
|
||||
|
||||
1
test/fixtures/deploy_simple.yml
vendored
1
test/fixtures/deploy_simple.yml
vendored
@@ -6,4 +6,3 @@ servers:
|
||||
registry:
|
||||
username: user
|
||||
password: pw
|
||||
audit_broadcast_cmd: "bin/audit_broadcast"
|
||||
|
||||
36
test/integration/accessory_test.rb
Normal file
36
test/integration/accessory_test.rb
Normal file
@@ -0,0 +1,36 @@
|
||||
require_relative "integration_test"
|
||||
|
||||
class AccessoryTest < IntegrationTest
|
||||
test "boot, stop, start, restart, logs, remove" do
|
||||
mrsk :accessory, :boot, :busybox
|
||||
assert_accessory_running :busybox
|
||||
|
||||
mrsk :accessory, :stop, :busybox
|
||||
assert_accessory_not_running :busybox
|
||||
|
||||
mrsk :accessory, :start, :busybox
|
||||
assert_accessory_running :busybox
|
||||
|
||||
mrsk :accessory, :restart, :busybox
|
||||
assert_accessory_running :busybox
|
||||
|
||||
logs = mrsk :accessory, :logs, :busybox, capture: true
|
||||
assert_match /Starting busybox.../, logs
|
||||
|
||||
mrsk :accessory, :remove, :busybox, "-y"
|
||||
assert_accessory_not_running :busybox
|
||||
end
|
||||
|
||||
private
|
||||
def assert_accessory_running(name)
|
||||
assert_match /registry:4443\/busybox:1.36.0 "sh -c 'echo \\"Start/, accessory_details(name)
|
||||
end
|
||||
|
||||
def assert_accessory_not_running(name)
|
||||
refute_match /registry:4443\/busybox:1.36.0 "sh -c 'echo \\"Start/, accessory_details(name)
|
||||
end
|
||||
|
||||
def accessory_details(name)
|
||||
mrsk :accessory, :details, name, capture: true
|
||||
end
|
||||
end
|
||||
55
test/integration/app_test.rb
Normal file
55
test/integration/app_test.rb
Normal file
@@ -0,0 +1,55 @@
|
||||
require_relative "integration_test"
|
||||
|
||||
class AppTest < IntegrationTest
|
||||
test "stop, start, boot, logs, images, containers, exec, remove" do
|
||||
mrsk :deploy
|
||||
|
||||
assert_app_is_up
|
||||
|
||||
mrsk :app, :stop
|
||||
|
||||
# traefik is up and returns 404s when it can't match a route
|
||||
assert_app_not_found
|
||||
|
||||
mrsk :app, :start
|
||||
|
||||
# mrsk app start does not wait
|
||||
wait_for_app_to_be_up
|
||||
|
||||
mrsk :app, :boot
|
||||
|
||||
wait_for_app_to_be_up
|
||||
|
||||
logs = mrsk :app, :logs, capture: true
|
||||
assert_match /App Host: vm1/, logs
|
||||
assert_match /App Host: vm2/, logs
|
||||
assert_match /GET \/ HTTP\/1.1/, logs
|
||||
|
||||
images = mrsk :app, :images, capture: true
|
||||
assert_match /App Host: vm1/, images
|
||||
assert_match /App Host: vm2/, images
|
||||
assert_match /registry:4443\/app\s+#{latest_app_version}/, images
|
||||
assert_match /registry:4443\/app\s+latest/, images
|
||||
|
||||
containers = mrsk :app, :containers, capture: true
|
||||
assert_match /App Host: vm1/, containers
|
||||
assert_match /App Host: vm2/, containers
|
||||
assert_match /registry:4443\/app:#{latest_app_version}/, containers
|
||||
assert_match /registry:4443\/app:latest/, containers
|
||||
|
||||
exec_output = mrsk :app, :exec, :ps, capture: true
|
||||
assert_match /App Host: vm1/, exec_output
|
||||
assert_match /App Host: vm2/, exec_output
|
||||
assert_match /1 root 0:\d\d ps/, exec_output
|
||||
|
||||
exec_output = mrsk :app, :exec, "--reuse", :ps, capture: true
|
||||
assert_match /App Host: vm1/, exec_output
|
||||
assert_match /App Host: vm2/, exec_output
|
||||
assert_match /1 root 0:\d\d nginx/, exec_output
|
||||
|
||||
mrsk :app, :remove
|
||||
|
||||
# traefik is up and returns 404s when it can't match a route
|
||||
assert_app_not_found
|
||||
end
|
||||
end
|
||||
@@ -3,6 +3,8 @@ name: "mrsk-test"
|
||||
|
||||
volumes:
|
||||
shared:
|
||||
registry:
|
||||
deployer_bundle:
|
||||
|
||||
services:
|
||||
shared:
|
||||
@@ -15,9 +17,13 @@ services:
|
||||
privileged: true
|
||||
build:
|
||||
context: docker/deployer
|
||||
environment:
|
||||
- TEST_ID=${TEST_ID}
|
||||
volumes:
|
||||
- ../..:/mrsk
|
||||
- shared:/shared
|
||||
- registry:/registry
|
||||
- deployer_bundle:/usr/local/bundle/
|
||||
|
||||
registry:
|
||||
build:
|
||||
@@ -28,6 +34,7 @@ services:
|
||||
- REGISTRY_HTTP_TLS_KEY=/certs/domain.key
|
||||
volumes:
|
||||
- shared:/shared
|
||||
- registry:/var/lib/registry/
|
||||
|
||||
vm1:
|
||||
privileged: true
|
||||
|
||||
@@ -23,7 +23,6 @@ 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.name "Deployer"
|
||||
RUN git init && git add . && git commit -am "Initial version"
|
||||
RUN git rev-parse HEAD > version
|
||||
|
||||
HEALTHCHECK --interval=1s CMD pgrep sleep
|
||||
|
||||
|
||||
1
test/integration/docker/deployer/app/.env.erb
Normal file
1
test/integration/docker/deployer/app/.env.erb
Normal file
@@ -0,0 +1 @@
|
||||
SECRET_TOKEN=1234
|
||||
3
test/integration/docker/deployer/app/.mrsk/hooks/post-deploy
Executable file
3
test/integration/docker/deployer/app/.mrsk/hooks/post-deploy
Executable file
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
echo "Deployed!"
|
||||
mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/post-deploy
|
||||
3
test/integration/docker/deployer/app/.mrsk/hooks/pre-build
Executable file
3
test/integration/docker/deployer/app/.mrsk/hooks/pre-build
Executable file
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
echo "About to build and push..."
|
||||
mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/pre-build
|
||||
8
test/integration/docker/deployer/app/.mrsk/hooks/pre-connect
Executable file
8
test/integration/docker/deployer/app/.mrsk/hooks/pre-connect
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/bin/sh
|
||||
|
||||
echo "About to lock..."
|
||||
if [ "$MRSK_HOSTS" != "vm1,vm2" ]; then
|
||||
echo "Expected hosts to be 'vm1,vm2', got $MRSK_HOSTS"
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/pre-connect
|
||||
@@ -1,4 +1,7 @@
|
||||
FROM nginx:1-alpine-slim
|
||||
FROM registry:4443/nginx:1-alpine-slim
|
||||
|
||||
COPY default.conf /etc/nginx/conf.d/default.conf
|
||||
COPY version /usr/share/nginx/html/version
|
||||
|
||||
ARG COMMIT_SHA
|
||||
RUN echo $COMMIT_SHA > /usr/share/nginx/html/version
|
||||
|
||||
|
||||
@@ -9,9 +9,19 @@ registry:
|
||||
password: root
|
||||
builder:
|
||||
multiarch: false
|
||||
args:
|
||||
COMMIT_SHA: <%= `git rev-parse HEAD` %>
|
||||
healthcheck:
|
||||
cmd: wget -qO- http://localhost > /dev/null
|
||||
traefik:
|
||||
args:
|
||||
accesslog: true
|
||||
accesslog.format: json
|
||||
image: registry:4443/traefik:v2.9
|
||||
accessories:
|
||||
busybox:
|
||||
image: registry:4443/busybox:1.36.0
|
||||
cmd: sh -c 'echo "Starting busybox..."; trap exit term; while true; do sleep 1; done'
|
||||
roles:
|
||||
- web
|
||||
stop_wait_time: 1
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
cd /mrsk && gem build mrsk.gemspec -o /tmp/mrsk.gem && gem install /tmp/mrsk.gem
|
||||
dockerd --max-concurrent-downloads 1 &
|
||||
|
||||
dockerd &
|
||||
|
||||
trap "pkill -f sleep" term
|
||||
|
||||
sleep infinity & wait
|
||||
exec sleep infinity
|
||||
|
||||
23
test/integration/docker/deployer/setup.sh
Executable file
23
test/integration/docker/deployer/setup.sh
Executable file
@@ -0,0 +1,23 @@
|
||||
#!/bin/bash
|
||||
|
||||
install_mrsk() {
|
||||
cd /mrsk && gem build mrsk.gemspec -o /tmp/mrsk.gem && gem install /tmp/mrsk.gem
|
||||
}
|
||||
|
||||
# Push the images to a persistent volume on the registry container
|
||||
# This is to work around docker hub rate limits
|
||||
push_image_to_registry_4443() {
|
||||
# Check if the image is in the registry without having to pull it
|
||||
if ! stat /registry/docker/registry/v2/repositories/$1/_manifests/tags/$2/current/link > /dev/null; then
|
||||
hub_tag=$1:$2
|
||||
registry_4443_tag=registry:4443/$1:$2
|
||||
docker pull $hub_tag
|
||||
docker tag $hub_tag $registry_4443_tag
|
||||
docker push $registry_4443_tag
|
||||
fi
|
||||
}
|
||||
|
||||
install_mrsk
|
||||
push_image_to_registry_4443 nginx 1-alpine-slim
|
||||
push_image_to_registry_4443 traefik v2.9
|
||||
push_image_to_registry_4443 busybox 1.36.0
|
||||
@@ -1,4 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
git commit -am 'Update rev' --amend
|
||||
git rev-parse HEAD > version
|
||||
|
||||
@@ -8,5 +8,9 @@ server {
|
||||
|
||||
location / {
|
||||
proxy_pass http://loadbalancer;
|
||||
proxy_connect_timeout 5;
|
||||
proxy_send_timeout 5;
|
||||
proxy_read_timeout 5;
|
||||
send_timeout 5;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,4 @@
|
||||
|
||||
while [ ! -f /certs/domain.crt ]; do sleep 1; done
|
||||
|
||||
trap "pkill -f registry" term
|
||||
|
||||
/entrypoint.sh /etc/docker/registry/config.yml & wait
|
||||
exec /entrypoint.sh /etc/docker/registry/config.yml
|
||||
|
||||
@@ -2,6 +2,4 @@
|
||||
|
||||
cp -r * /shared
|
||||
|
||||
trap "pkill -f sleep" term
|
||||
|
||||
sleep infinity & wait
|
||||
exec sleep infinity
|
||||
|
||||
@@ -4,8 +4,6 @@ while [ ! -f /root/.ssh/authorized_keys ]; do echo "Waiting for ssh keys"; sleep
|
||||
|
||||
service ssh restart
|
||||
|
||||
dockerd &
|
||||
dockerd --max-concurrent-downloads 1 &
|
||||
|
||||
trap "pkill -f sleep" term
|
||||
|
||||
sleep infinity & wait
|
||||
exec sleep infinity
|
||||
|
||||
@@ -1,53 +1,21 @@
|
||||
require "net/http"
|
||||
require "test_helper"
|
||||
|
||||
class DeployTest < ActiveSupport::TestCase
|
||||
|
||||
class IntegrationTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
docker_compose "up --build --force-recreate -d"
|
||||
ENV["TEST_ID"] = SecureRandom.hex
|
||||
docker_compose "up --build -d"
|
||||
wait_for_healthy
|
||||
setup_deployer
|
||||
end
|
||||
|
||||
teardown do
|
||||
docker_compose "down -v"
|
||||
end
|
||||
|
||||
test "deploy" do
|
||||
first_version = latest_app_version
|
||||
|
||||
assert_app_is_down
|
||||
|
||||
mrsk :deploy
|
||||
|
||||
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
|
||||
docker_compose "down -t 1"
|
||||
end
|
||||
|
||||
private
|
||||
def docker_compose(*commands, capture: false)
|
||||
command = "docker compose #{commands.join(" ")}"
|
||||
def docker_compose(*commands, capture: false, raise_on_error: true)
|
||||
command = "TEST_ID=#{ENV["TEST_ID"]} docker compose #{commands.join(" ")}"
|
||||
succeeded = false
|
||||
if capture
|
||||
result = stdouted { succeeded = system("cd test/integration && #{command}") }
|
||||
@@ -55,7 +23,7 @@ class DeployTest < ActiveSupport::TestCase
|
||||
succeeded = system("cd test/integration && #{command}")
|
||||
end
|
||||
|
||||
raise "Command `#{command}` failed with error code `#{$?}`" unless succeeded
|
||||
raise "Command `#{command}` failed with error code `#{$?}`" if !succeeded && raise_on_error
|
||||
result
|
||||
end
|
||||
|
||||
@@ -68,24 +36,22 @@ class DeployTest < ActiveSupport::TestCase
|
||||
end
|
||||
|
||||
def assert_app_is_down
|
||||
assert_equal "502", app_response.code
|
||||
response = app_response
|
||||
debug_response_code(response, "502")
|
||||
assert_equal "502", response.code
|
||||
end
|
||||
|
||||
def assert_app_is_up(version: nil)
|
||||
code = app_response.code
|
||||
if code != "200"
|
||||
puts "Got response code #{code}, here are the traefik logs:"
|
||||
mrsk :traefik, :logs
|
||||
puts "And here are the load balancer logs"
|
||||
docker_compose :logs, :load_balancer
|
||||
puts "Tried to get the response code again and got #{app_response.code}"
|
||||
end
|
||||
assert_equal "200", code
|
||||
assert_app_version(version) if version
|
||||
response = app_response
|
||||
debug_response_code(response, "200")
|
||||
assert_equal "200", response.code
|
||||
assert_app_version(version, response) if version
|
||||
end
|
||||
|
||||
def assert_app_not_found
|
||||
assert_equal "404", app_response.code
|
||||
response = app_response
|
||||
debug_response_code(response, "404")
|
||||
assert_equal "404", response.code
|
||||
end
|
||||
|
||||
def wait_for_app_to_be_up(timeout: 10, up_count: 3)
|
||||
@@ -101,7 +67,7 @@ class DeployTest < ActiveSupport::TestCase
|
||||
end
|
||||
|
||||
def app_response
|
||||
Net::HTTP.get_response(URI.parse("http://localhost:12345"))
|
||||
Net::HTTP.get_response(URI.parse("http://localhost:12345/version"))
|
||||
end
|
||||
|
||||
def update_app_rev
|
||||
@@ -110,13 +76,30 @@ class DeployTest < ActiveSupport::TestCase
|
||||
end
|
||||
|
||||
def latest_app_version
|
||||
deployer_exec("cat version", capture: true)
|
||||
deployer_exec("git rev-parse HEAD", capture: true)
|
||||
end
|
||||
|
||||
def assert_app_version(version)
|
||||
actual_version = Net::HTTP.get_response(URI.parse("http://localhost:12345/version")).body.strip
|
||||
def assert_app_version(version, response)
|
||||
assert_equal version, response.body.strip
|
||||
end
|
||||
|
||||
assert_equal version, actual_version
|
||||
def assert_hooks_ran(*hooks)
|
||||
hooks.each do |hook|
|
||||
file = "/tmp/#{ENV["TEST_ID"]}/#{hook}"
|
||||
assert_equal "removed '#{file}'", deployer_exec("rm -v #{file}", capture: true).strip
|
||||
end
|
||||
end
|
||||
|
||||
def assert_200(response)
|
||||
code = response.code
|
||||
if code != "200"
|
||||
puts "Got response code #{code}, here are the traefik logs:"
|
||||
mrsk :traefik, :logs
|
||||
puts "And here are the load balancer logs"
|
||||
docker_compose :logs, :load_balancer
|
||||
puts "Tried to get the response code again and got #{app_response.code}"
|
||||
end
|
||||
assert_equal "200", code
|
||||
end
|
||||
|
||||
def wait_for_healthy(timeout: 20)
|
||||
@@ -129,4 +112,20 @@ class DeployTest < ActiveSupport::TestCase
|
||||
sleep 0.1
|
||||
end
|
||||
end
|
||||
|
||||
def setup_deployer
|
||||
deployer_exec("./setup.sh") unless $DEPLOYER_SETUP
|
||||
$DEPLOYER_SETUP = true
|
||||
end
|
||||
|
||||
def debug_response_code(app_response, expected_code)
|
||||
code = app_response.code
|
||||
if code != expected_code
|
||||
puts "Got response code #{code}, here are the traefik logs:"
|
||||
mrsk :traefik, :logs
|
||||
puts "And here are the load balancer logs"
|
||||
docker_compose :logs, :load_balancer
|
||||
puts "Tried to get the response code again and got #{app_response.code}"
|
||||
end
|
||||
end
|
||||
end
|
||||
18
test/integration/lock_test.rb
Normal file
18
test/integration/lock_test.rb
Normal file
@@ -0,0 +1,18 @@
|
||||
require_relative "integration_test"
|
||||
|
||||
class LockTest < IntegrationTest
|
||||
test "acquire, release, status" do
|
||||
mrsk :lock, :acquire, "-m 'Integration Tests'"
|
||||
|
||||
status = mrsk :lock, :status, capture: true
|
||||
assert_match /Locked by: Deployer at .*\nVersion: #{latest_app_version}\nMessage: Integration Tests/m, status
|
||||
|
||||
error = mrsk :deploy, capture: true, raise_on_error: false
|
||||
assert_match /Deploy lock found/m, error
|
||||
|
||||
mrsk :lock, :release
|
||||
|
||||
status = mrsk :lock, :status, capture: true
|
||||
assert_match /There is no deploy lock/m, status
|
||||
end
|
||||
end
|
||||
59
test/integration/main_test.rb
Normal file
59
test/integration/main_test.rb
Normal file
@@ -0,0 +1,59 @@
|
||||
require_relative "integration_test"
|
||||
|
||||
class MainTest < IntegrationTest
|
||||
test "deploy, redeploy, rollback, details and audit" do
|
||||
first_version = latest_app_version
|
||||
|
||||
assert_app_is_down
|
||||
|
||||
mrsk :deploy
|
||||
assert_app_is_up version: first_version
|
||||
assert_hooks_ran "pre-connect", "pre-build", "post-deploy"
|
||||
|
||||
second_version = update_app_rev
|
||||
|
||||
mrsk :redeploy
|
||||
assert_app_is_up version: second_version
|
||||
assert_hooks_ran "pre-connect", "pre-build", "post-deploy"
|
||||
|
||||
mrsk :rollback, first_version
|
||||
assert_hooks_ran "pre-connect", "post-deploy"
|
||||
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
|
||||
|
||||
test "envify" do
|
||||
mrsk :envify
|
||||
|
||||
assert_equal "SECRET_TOKEN=1234", deployer_exec("cat .env", capture: true)
|
||||
end
|
||||
|
||||
test "config" do
|
||||
config = YAML.load(mrsk(:config, capture: true))
|
||||
version = latest_app_version
|
||||
|
||||
assert_equal [ "web" ], config[:roles]
|
||||
assert_equal [ "vm1", "vm2" ], config[:hosts]
|
||||
assert_equal "vm1", config[:primary_host]
|
||||
assert_equal version, config[:version]
|
||||
assert_equal "registry:4443/app", config[:repository]
|
||||
assert_equal "registry:4443/app:#{version}", config[:absolute_image]
|
||||
assert_equal "app-#{version}", config[:service_with_version]
|
||||
assert_equal [], config[:env_args]
|
||||
assert_equal [], config[:volume_args]
|
||||
assert_equal({ user: "root", auth_methods: [ "publickey" ] }, config[:ssh_options])
|
||||
assert_equal({ "multiarch" => false, "args" => { "COMMIT_SHA" => version } }, config[:builder])
|
||||
assert_equal [ "--log-opt", "max-size=\"10m\"" ], config[:logging]
|
||||
assert_equal({ "path" => "/up", "port" => 3000, "max_attempts" => 7, "cmd" => "wget -qO- http://localhost > /dev/null" }, config[:healthcheck])
|
||||
end
|
||||
end
|
||||
36
test/integration/traefik_test.rb
Normal file
36
test/integration/traefik_test.rb
Normal file
@@ -0,0 +1,36 @@
|
||||
require_relative "integration_test"
|
||||
|
||||
class TraefikTest < IntegrationTest
|
||||
test "boot, stop, start, restart, logs, remove" do
|
||||
mrsk :traefik, :boot
|
||||
assert_traefik_running
|
||||
|
||||
mrsk :traefik, :stop
|
||||
assert_traefik_not_running
|
||||
|
||||
mrsk :traefik, :start
|
||||
assert_traefik_running
|
||||
|
||||
mrsk :traefik, :restart
|
||||
assert_traefik_running
|
||||
|
||||
logs = mrsk :traefik, :logs, capture: true
|
||||
assert_match /Traefik version [\d.]+ built on/, logs
|
||||
|
||||
mrsk :traefik, :remove
|
||||
assert_traefik_not_running
|
||||
end
|
||||
|
||||
private
|
||||
def assert_traefik_running
|
||||
assert_match /traefik:v2.9 "\/entrypoint.sh/, traefik_details
|
||||
end
|
||||
|
||||
def assert_traefik_not_running
|
||||
refute_match /traefik:v2.9 "\/entrypoint.sh/, traefik_details
|
||||
end
|
||||
|
||||
def traefik_details
|
||||
mrsk :traefik, :details, capture: true
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user