Compare commits
73 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66b4a0ea40 | ||
|
|
04b39ea798 | ||
|
|
ae55a7b5d8 | ||
|
|
601cfbd95e | ||
|
|
9fdc85c2e6 | ||
|
|
222eda6085 | ||
|
|
504a09ef1d | ||
|
|
5a25f073f7 | ||
|
|
c8f521c0e8 | ||
|
|
28d6a131a9 | ||
|
|
3a9075b8ba | ||
|
|
079d9538bb | ||
|
|
8e94c21729 | ||
|
|
b536fcfa43 | ||
|
|
85005be07f | ||
|
|
fc00392d68 | ||
|
|
fe9affa349 | ||
|
|
3ecb3a4bfc | ||
|
|
787812cdc2 | ||
|
|
91fb85d6b5 | ||
|
|
db0bf6bb16 | ||
|
|
de2de19434 | ||
|
|
f9fbebaa72 | ||
|
|
1e300f3798 | ||
|
|
0373f6c4de | ||
|
|
9037088f99 | ||
|
|
ff7a1e6726 | ||
|
|
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 |
@@ -27,7 +27,7 @@ Please keep the following guidelines in mind when opening a pull request:
|
||||
- Add tests for your changes, if possible.
|
||||
- Ensure that your changes don't break existing functionality.
|
||||
|
||||
#### Commit message guidline
|
||||
#### Commit message guidelines
|
||||
|
||||
A good commit message should describe what changed and why.
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ RUN apk add --no-cache --update build-base git docker openrc openssh-client-defa
|
||||
|
||||
# Copy the rest of our application code into the container.
|
||||
# We do this after bundle install, to avoid having to run bundle
|
||||
# everytime we do small fixes in the source code.
|
||||
# every time we do small fixes in the source code.
|
||||
COPY . .
|
||||
|
||||
# Install the gem locally from the project folder
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
PATH
|
||||
remote: .
|
||||
specs:
|
||||
mrsk (0.12.1)
|
||||
mrsk (0.13.2)
|
||||
activesupport (>= 7.0)
|
||||
bcrypt_pbkdf (~> 1.0)
|
||||
dotenv (~> 2.8)
|
||||
|
||||
122
README.md
122
README.md
@@ -44,30 +44,30 @@ 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
|
||||
|
||||
In the past decade+, there's been an explosion in commercial offerings that make deploying web apps easier. Heroku kicked it off with an incredible offering that stayed ahead of the competition seemingly forever. These days we have excellent alternatives like Fly.io and Render. And hosted Kubernetes is making things easier too on AWS, GCP, Digital Ocean, and elsewhere. But these are all offerings that have you renting computers in the cloud at a premium. If you want to run on your own hardware, or even just have a clear migration path to do so in the future, you need to carefully consider how locked in you get to these commercial platforms. Preferably before the bills swallow your business whole!
|
||||
|
||||
MRSK seeks to bring the advance in ergonomics pioneered by these commercial offerings to deploying web apps anywhere. Whether that's low-cost cloud options without the managed-service markup from the likes of Digital Ocean, Hetzner, OVH, etc, or it's your own colocated bare metal. To MRSK, it's all the same. Feed the config file a list of IP addresses with vanilla Ubuntu servers that have seen no prep beyond an added SSH key, and you'll be running in literally minutes.
|
||||
MRSK seeks to bring the advance in ergonomics pioneered by these commercial offerings to deploying web apps anywhere. Whether that's low-cost cloud options without the managed-service markup from the likes of Digital Ocean, Hetzner, OVH, etc., or it's your own colocated bare metal. To MRSK, it's all the same. Feed the config file a list of IP addresses with vanilla Ubuntu servers that have seen no prep beyond an added SSH key, and you'll be running in literally minutes.
|
||||
|
||||
This approach gives you enormous portability. You can have your web app deployed on several clouds at ease like this. Or you can buy the baseline with your own hardware, then deploy to a cloud before a big seasonal spike to get more capacity. When you're not locked into a single provider from a tooling perspective, there are a lot of compelling options available.
|
||||
|
||||
@@ -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,46 +668,9 @@ 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.
|
||||
MRSK uses Docker healthchecks 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.
|
||||
|
||||
The healthcheck defaults to testing the HTTP response to the path `/up` on port 3000, up to 7 times. You can tailor this behaviour with the `healthcheck` setting:
|
||||
|
||||
@@ -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.
|
||||
@@ -863,7 +840,7 @@ Message: Automatic deploy lock
|
||||
You can also manually acquire and release the lock
|
||||
|
||||
```
|
||||
mrsk lock acquire -m "Doing maintanence"
|
||||
mrsk lock acquire -m "Doing maintenance"
|
||||
```
|
||||
|
||||
```
|
||||
@@ -888,6 +865,61 @@ 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_HOSTS` - a comma separated list of the hosts targeted by the command
|
||||
- `MRSK_COMMAND` - The command we are running
|
||||
- `MRSK_SUBCOMMAND` - optional: The subcommand we are running
|
||||
- `MRSK_DESTINATION` - optional: destination, e.g. "staging"
|
||||
- `MRSK_ROLE` - optional: role targeted, e.g. "web"
|
||||
|
||||
There are four 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. pre-deploy
|
||||
For final checks before deploying, e.g. checking CI completed
|
||||
|
||||
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(hostname: "#{host}-#{SecureRandom.hex(6)}")
|
||||
|
||||
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,40 @@ module Mrsk::Cli
|
||||
MRSK.hold_lock_on_error = false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def run_hook(hook, **extra_details)
|
||||
if !options[:skip_hooks] && MRSK.hook.hook_exists?(hook)
|
||||
details = { hosts: MRSK.hosts.join(","), command: command, subcommand: subcommand }
|
||||
|
||||
say "Running the #{hook} hook...", :magenta
|
||||
run_locally do
|
||||
MRSK.with_verbosity(:debug) { execute *MRSK.hook.run(hook, **details, **extra_details) }
|
||||
rescue SSHKit::Command::Failed
|
||||
raise HookError.new("Hook `#{hook}` failed")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def command
|
||||
@mrsk_command ||= begin
|
||||
invocation_class, invocation_commands = *first_invocation
|
||||
if invocation_class == Mrsk::Cli::Main
|
||||
invocation_commands[0]
|
||||
else
|
||||
Mrsk::Cli::Main.subcommand_classes.find { |command, clazz| clazz == invocation_class }[0]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def subcommand
|
||||
@mrsk_subcommand ||= begin
|
||||
invocation_class, invocation_commands = *first_invocation
|
||||
invocation_commands[0] if invocation_class != Mrsk::Cli::Main
|
||||
end
|
||||
end
|
||||
|
||||
def first_invocation
|
||||
instance_variable_get("@_invocations").first
|
||||
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,16 +2,16 @@ 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
|
||||
|
||||
desc "acquire", "Acquire the deploy lock"
|
||||
option :message, aliases: "-m", type: :string, desc: "A lock mesasge", required: true
|
||||
option :message, aliases: "-m", type: :string, desc: "A lock message", 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
|
||||
|
||||
@@ -28,6 +28,8 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
|
||||
invoke "mrsk:cli:build:deliver", [], invoke_options
|
||||
end
|
||||
|
||||
run_hook "pre-deploy"
|
||||
|
||||
say "Ensure Traefik is running...", :magenta
|
||||
invoke "mrsk:cli:traefik:boot", [], invoke_options
|
||||
|
||||
@@ -37,25 +39,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
|
||||
@@ -64,61 +64,43 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
|
||||
invoke "mrsk:cli:build:deliver", [], invoke_options
|
||||
end
|
||||
|
||||
run_hook "pre-deploy"
|
||||
|
||||
say "Ensure app can pass healthcheck...", :magenta
|
||||
invoke "mrsk:cli:healthcheck:perform", [], invoke_options
|
||||
|
||||
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
|
||||
run_hook "pre-deploy"
|
||||
|
||||
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 +142,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 +190,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 +207,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 +222,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 +246,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
|
||||
|
||||
@@ -12,7 +12,8 @@ class Mrsk::Cli::Prune < Mrsk::Cli::Base
|
||||
with_lock do
|
||||
on(MRSK.hosts) do
|
||||
execute *MRSK.auditor.record("Pruned images"), verbosity: :debug
|
||||
execute *MRSK.prune.images
|
||||
execute *MRSK.prune.dangling_images
|
||||
execute *MRSK.prune.tagged_images
|
||||
end
|
||||
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 ]
|
||||
82
lib/mrsk/cli/templates/sample_hooks/pre-deploy.sample
Executable file
82
lib/mrsk/cli/templates/sample_hooks/pre-deploy.sample
Executable file
@@ -0,0 +1,82 @@
|
||||
#!/bin/sh
|
||||
|
||||
# A sample pre-deploy hook
|
||||
#
|
||||
# Checks the Github status of the build, waiting for a pending build to complete for up to 720 seconds.
|
||||
#
|
||||
# Fails unless the combined status is "success"
|
||||
#
|
||||
# These environment variables are available:
|
||||
# MRSK_RECORDED_AT
|
||||
# MRSK_PERFORMER
|
||||
# MRSK_VERSION
|
||||
# MRSK_HOSTS
|
||||
# MRSK_COMMAND
|
||||
# MRSK_SUBCOMMAND
|
||||
# MRSK_ROLE (if set)
|
||||
# MRSK_DESTINATION (if set)
|
||||
|
||||
#!/usr/bin/env ruby
|
||||
|
||||
# Only check the build status for production deployments
|
||||
if ENV["MRSK_COMMAND"] == "rollback" || ENV["MRSK_DESTINATION"] != "production"
|
||||
exit 0
|
||||
end
|
||||
|
||||
require "bundler/inline"
|
||||
|
||||
# true = install gems so this is fast on repeat invocations
|
||||
gemfile(true, quiet: true) do
|
||||
source "https://rubygems.org"
|
||||
|
||||
gem "octokit"
|
||||
gem "faraday-retry"
|
||||
end
|
||||
|
||||
MAX_ATTEMPTS = 72
|
||||
ATTEMPTS_GAP = 10
|
||||
|
||||
def exit_with_error(message)
|
||||
$stderr.puts message
|
||||
exit 1
|
||||
end
|
||||
|
||||
def first_status_url(combined_status, state)
|
||||
first_status = combined_status[:statuses].find { |status| status[:state] == state }
|
||||
first_status && first_status[:target_url]
|
||||
end
|
||||
|
||||
remote_url = `git config --get remote.origin.url`.strip.delete_prefix("https://github.com/")
|
||||
git_sha = `git rev-parse HEAD`.strip
|
||||
|
||||
repository = Octokit::Repository.from_url(remote_url)
|
||||
github_client = Octokit::Client.new(access_token: ENV["GITHUB_TOKEN"])
|
||||
attempts = 0
|
||||
|
||||
begin
|
||||
loop do
|
||||
combined_status = github_client.combined_status(remote_url, git_sha)
|
||||
state = combined_status[:state]
|
||||
first_status_url = first_status_url(combined_status, state)
|
||||
|
||||
case state
|
||||
when "success"
|
||||
puts "Build passed, see #{first_status_url}"
|
||||
exit 0
|
||||
when "failure"
|
||||
exit_with_error "Build failed, see #{first_status_url}"
|
||||
when "pending"
|
||||
attempts += 1
|
||||
end
|
||||
|
||||
puts "Waiting #{ATTEMPTS_GAP} more seconds for build to complete#{", see #{first_status_url}" if first_status_url}..."
|
||||
|
||||
if attempts == MAX_ATTEMPTS
|
||||
exit_with_error "Build status is still pending, gave up after #{MAX_ATTEMPTS * ATTEMPTS_GAP} seconds"
|
||||
end
|
||||
|
||||
sleep(ATTEMPTS_GAP)
|
||||
end
|
||||
rescue Octokit::NotFound
|
||||
exit_with_error "Build status could not be found"
|
||||
end
|
||||
@@ -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,13 +8,18 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
|
||||
@role = role
|
||||
end
|
||||
|
||||
def run
|
||||
def start_or_run(hostname: nil)
|
||||
combine start, run(hostname: hostname), by: "||"
|
||||
end
|
||||
|
||||
def run(hostname: nil)
|
||||
role = config.role(self.role)
|
||||
|
||||
docker :run,
|
||||
"--detach",
|
||||
"--restart unless-stopped",
|
||||
"--name", container_name,
|
||||
*(["--hostname", hostname] if hostname),
|
||||
"-e", "MRSK_CONTAINER_NAME=\"#{container_name}\"",
|
||||
*role.env_args,
|
||||
*role.health_check_args,
|
||||
@@ -88,20 +95,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 +153,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
|
||||
@@ -2,13 +2,20 @@ require "active_support/duration"
|
||||
require "active_support/core_ext/numeric/time"
|
||||
|
||||
class Mrsk::Commands::Prune < Mrsk::Commands::Base
|
||||
def images
|
||||
def dangling_images
|
||||
docker :image, :prune, "--force", "--filter", "label=service=#{config.service}", "--filter", "dangling=true"
|
||||
end
|
||||
|
||||
def tagged_images
|
||||
pipe \
|
||||
docker(:image, :ls, *service_filter, "--format", "'{{.ID}} {{.Repository}}:{{.Tag}}'"),
|
||||
"grep -v -w \"#{active_image_list}\"",
|
||||
"while read image tag; do docker rmi $tag; done"
|
||||
end
|
||||
|
||||
def containers(keep_last: 5)
|
||||
pipe \
|
||||
docker(:ps, "-q", "-a", "--filter", "label=service=#{config.service}", *stopped_containers_filters),
|
||||
docker(:ps, "-q", "-a", *service_filter, *stopped_containers_filters),
|
||||
"tail -n +#{keep_last + 1}",
|
||||
"while read container_id; do docker rm $container_id; done"
|
||||
end
|
||||
@@ -17,4 +24,15 @@ class Mrsk::Commands::Prune < Mrsk::Commands::Base
|
||||
def stopped_containers_filters
|
||||
[ "created", "exited", "dead" ].flat_map { |status| ["--filter", "status=#{status}"] }
|
||||
end
|
||||
|
||||
def active_image_list
|
||||
# Pull the images that are used by any containers
|
||||
# Append repo:latest - to avoid deleting the latest tag
|
||||
# Append repo:<none> - to avoid deleting dangling images that are in use. Unused dangling images are deleted separately
|
||||
"$(docker container ls -a --format '{{.Image}}\\|' --filter label=service=#{config.service} | tr -d '\\n')#{config.latest_image}\\|#{config.repository}:<none>"
|
||||
end
|
||||
|
||||
def service_filter
|
||||
[ "--filter", "label=service=#{config.service}" ]
|
||||
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.2"
|
||||
end
|
||||
|
||||
@@ -2,15 +2,10 @@ 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
|
||||
assert_match /docker run --detach --restart unless-stopped --name app-web-latest --hostname 1.1.1.1-[0-9a-f]{12} /, output
|
||||
assert_match "docker container ls --all --filter name=^app-web-123$ --quiet | xargs docker stop", output
|
||||
end
|
||||
end
|
||||
@@ -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,13 +22,13 @@ 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 run --detach --restart unless-stopped", 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 --name app-web-latest --hostname 1.1.1.1-[0-9a-f]{12} /, output
|
||||
assert_match "docker container ls --all --filter name=^app-web-123$ --quiet | xargs docker stop", output
|
||||
end
|
||||
ensure
|
||||
@@ -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,23 @@ class CliBuildTest < CliTestCase
|
||||
end
|
||||
|
||||
test "push" do
|
||||
Mrsk::Cli::Build.any_instance.stubs(:verify_local_dependencies).returns(true)
|
||||
Mrsk::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)
|
||||
hook_variables = { version: 999, service_version: "app@999", hosts: "1.1.1.1,1.1.1.2,1.1.1.3,1.1.1.4", command: "build", subcommand: "push" }
|
||||
|
||||
run_command("push").tap do |output|
|
||||
assert_hook_ran "pre-build", output, **hook_variables
|
||||
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 +35,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 +94,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,43 @@ 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 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
|
||||
|
||||
def assert_hook_ran(hook, output, version:, service_version:, hosts:, command:, subcommand: nil, runtime: nil)
|
||||
performer = `whoami`.strip
|
||||
|
||||
assert_match "Running the #{hook} hook...\n", output
|
||||
|
||||
expected = %r{Running\s/usr/bin/env\s\.mrsk/hooks/#{hook}\sas\s#{performer}@localhost\n\s
|
||||
DEBUG\s\[[0-9a-f]*\]\sCommand:\s\(\sexport\s
|
||||
MRSK_RECORDED_AT=\"\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\dZ\"\s
|
||||
MRSK_PERFORMER=\"#{performer}\"\s
|
||||
MRSK_VERSION=\"#{version}\"\s
|
||||
MRSK_SERVICE_VERSION=\"#{service_version}\"\s
|
||||
MRSK_HOSTS=\"#{hosts}\"\s
|
||||
MRSK_COMMAND=\"#{command}\"\s
|
||||
#{"MRSK_SUBCOMMAND=\\\"#{subcommand}\\\"\\s" if subcommand}
|
||||
#{"MRSK_RUNTIME=\\\"#{runtime}\\\"\\s" if runtime}
|
||||
;\s/usr/bin/env\s\.mrsk/hooks/#{hook} }x
|
||||
|
||||
assert_match expected, output
|
||||
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,24 @@ 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)
|
||||
hook_variables = { version: 999, service_version: "app@999", hosts: "1.1.1.1,1.1.1.2", command: "deploy" }
|
||||
|
||||
run_command("deploy").tap do |output|
|
||||
assert_hook_ran "pre-connect", output, **hook_variables
|
||||
assert_match /Log into image registry/, output
|
||||
assert_match /Build and push app image/, output
|
||||
assert_hook_ran "pre-deploy", output, **hook_variables
|
||||
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_hook_ran "post-deploy", output, **hook_variables, runtime: 0
|
||||
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 +66,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 +86,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 +100,46 @@ 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)
|
||||
|
||||
hook_variables = { version: 999, service_version: "app@999", hosts: "1.1.1.1,1.1.1.2", command: "redeploy" }
|
||||
|
||||
run_command("redeploy").tap do |output|
|
||||
assert_hook_ran "pre-connect", output, **hook_variables
|
||||
assert_match /Build and push app image/, output
|
||||
assert_hook_ran "pre-deploy", output, **hook_variables
|
||||
assert_match /Running the pre-deploy hook.../, output
|
||||
assert_match /Ensure app can pass healthcheck/, output
|
||||
assert_hook_ran "post-deploy", output, **hook_variables, runtime: "0"
|
||||
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 +164,52 @@ 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)
|
||||
hook_variables = { version: 123, service_version: "app@123", hosts: "1.1.1.1,1.1.1.2,1.1.1.3,1.1.1.4", command: "rollback" }
|
||||
|
||||
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_hook_ran "pre-deploy", output, **hook_variables
|
||||
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_hook_ran "post-deploy", output, **hook_variables, runtime: "0"
|
||||
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 +269,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 +282,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 +290,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 +307,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 +357,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
|
||||
|
||||
@@ -10,7 +10,8 @@ class CliPruneTest < CliTestCase
|
||||
|
||||
test "images" do
|
||||
run_command("images").tap do |output|
|
||||
assert_match /docker image prune --force --filter label=service=app --filter dangling=true on 1.1.1.\d/, output
|
||||
assert_match "docker image prune --force --filter label=service=app --filter dangling=true on 1.1.1.", output
|
||||
assert_match "docker image ls --filter label=service=app --format '{{.ID}} {{.Repository}}:{{.Tag}}' | grep -v -w \"$(docker container ls -a --format '{{.Image}}\\|' --filter label=service=app | tr -d '\\n')dhh/app:latest\\|dhh/app:<none>\" | while read image tag; do docker rmi $tag; done on 1.1.1.", output
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ class CliServerTest < CliTestCase
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:docker, "-v", raise_on_non_zero_exit: false).returns(false).at_least_once
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:execute).with('[ "${EUID:-$(id -u)}" -eq 0 ]', raise_on_non_zero_exit: false).returns(false).at_least_once
|
||||
|
||||
assert_raise RuntimeError, "Docker is not installed on 1.1.1.1, 1.1.1.3, 1.1.1.4, 1.1.1.2 and can't be automatically intalled without having root access and the `curl` command available. Install Docker manually: https://docs.docker.com/engine/install/" do
|
||||
assert_raise RuntimeError, "Docker is not installed on 1.1.1.1, 1.1.1.3, 1.1.1.4, 1.1.1.2 and can't be automatically installed without having root access and the `curl` command available. Install Docker manually: https://docs.docker.com/engine/install/" do
|
||||
run_command("bootstrap")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -65,7 +65,7 @@ class CommanderTest < ActiveSupport::TestCase
|
||||
end
|
||||
|
||||
test "percentage-based group strategy" do
|
||||
configure_with(:deploy_with_precentage_boot_strategy)
|
||||
configure_with(:deploy_with_percentage_boot_strategy)
|
||||
|
||||
assert_equal({ in: :groups, limit: 1, wait: 2 }, @mrsk.boot_strategy)
|
||||
end
|
||||
|
||||
@@ -17,6 +17,12 @@ class CommandsAppTest < ActiveSupport::TestCase
|
||||
new_command.run.join(" ")
|
||||
end
|
||||
|
||||
test "run with hostname" do
|
||||
assert_equal \
|
||||
"docker run --detach --restart unless-stopped --name app-web-999 --hostname myhost -e MRSK_CONTAINER_NAME=\"app-web-999\" -e RAILS_MASTER_KEY=\"456\" --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999",
|
||||
new_command.run(hostname: "myhost").join(" ")
|
||||
end
|
||||
|
||||
test "run with volumes" do
|
||||
@config[:volumes] = ["/local/path:/container/path" ]
|
||||
|
||||
@@ -77,16 +83,28 @@ class CommandsAppTest < ActiveSupport::TestCase
|
||||
new_command.start.join(" ")
|
||||
end
|
||||
|
||||
test "start_or_run" do
|
||||
assert_equal \
|
||||
"docker start app-web-999 || docker run --detach --restart unless-stopped --name app-web-999 -e MRSK_CONTAINER_NAME=\"app-web-999\" -e RAILS_MASTER_KEY=\"456\" --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999",
|
||||
new_command.start_or_run.join(" ")
|
||||
end
|
||||
|
||||
test "start_or_run with hostname" do
|
||||
assert_equal \
|
||||
"docker start app-web-999 || docker run --detach --restart unless-stopped --name app-web-999 --hostname myhost -e MRSK_CONTAINER_NAME=\"app-web-999\" -e RAILS_MASTER_KEY=\"456\" --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999",
|
||||
new_command.start_or_run(hostname: "myhost").join(" ")
|
||||
end
|
||||
|
||||
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 +130,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 +214,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 +233,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 +243,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)
|
||||
|
||||
@@ -88,7 +88,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase
|
||||
builder.push.join(" ")
|
||||
end
|
||||
|
||||
test "native push with with build secrets" do
|
||||
test "native push with build secrets" do
|
||||
builder = new_builder_command(builder: { "multiarch" => false, "secrets" => [ "a", "b" ] })
|
||||
assert_equal \
|
||||
"docker build -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --secret id=\"a\" --secret id=\"b\" --file Dockerfile . && docker push dhh/app:123 && docker push dhh/app:latest",
|
||||
|
||||
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,10 +8,16 @@ class CommandsPruneTest < ActiveSupport::TestCase
|
||||
}
|
||||
end
|
||||
|
||||
test "images" do
|
||||
test "dangling images" do
|
||||
assert_equal \
|
||||
"docker image prune --force --filter label=service=app --filter dangling=true",
|
||||
new_command.images.join(" ")
|
||||
new_command.dangling_images.join(" ")
|
||||
end
|
||||
|
||||
test "tagged images" do
|
||||
assert_equal \
|
||||
"docker image ls --filter label=service=app --format '{{.ID}} {{.Repository}}:{{.Tag}}' | grep -v -w \"$(docker container ls -a --format '{{.Image}}\\|' --filter label=service=app | tr -d '\\n')dhh/app:latest\\|dhh/app:<none>\" | while read image tag; do docker rmi $tag; done",
|
||||
new_command.tagged_images.join(" ")
|
||||
end
|
||||
|
||||
test "containers" do
|
||||
|
||||
@@ -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
|
||||
3
test/integration/docker/deployer/app/.mrsk/hooks/pre-deploy
Executable file
3
test/integration/docker/deployer/app/.mrsk/hooks/pre-deploy
Executable file
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
echo "Deployed!"
|
||||
mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/pre-deploy
|
||||
@@ -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 10;
|
||||
proxy_send_timeout 10;
|
||||
proxy_read_timeout 10;
|
||||
send_timeout 10;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,27 +36,25 @@ 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)
|
||||
def wait_for_app_to_be_up(timeout: 20, up_count: 3)
|
||||
timeout_at = Time.now + timeout
|
||||
up_times = 0
|
||||
response = app_response
|
||||
@@ -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", "pre-deploy", "post-deploy"
|
||||
|
||||
second_version = update_app_rev
|
||||
|
||||
mrsk :redeploy
|
||||
assert_app_is_up version: second_version
|
||||
assert_hooks_ran "pre-connect", "pre-build", "pre-deploy", "post-deploy"
|
||||
|
||||
mrsk :rollback, first_version
|
||||
assert_hooks_ran "pre-connect", "pre-deploy", "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