Compare commits

..

2 Commits

Author SHA1 Message Date
David Heinemeier Hansson
2e02b1ed14 Bump version for 0.6.1 2023-02-07 15:05:52 +01:00
David Heinemeier Hansson
fdd98622ac Fix issue with removing containers triggering twice, then ensure app stop runs closer to app run on each host 2023-02-07 15:05:06 +01:00
50 changed files with 314 additions and 985 deletions

View File

@@ -6,5 +6,3 @@ gemspec
gem "debug"
gem "mocha"
gem "railties"
gem "ed25519"
gem "bcrypt_pbkdf"

View File

@@ -1,7 +1,7 @@
PATH
remote: .
specs:
mrsk (0.8.3)
mrsk (0.6.1)
activesupport (>= 7.0)
dotenv (~> 2.8)
sshkit (~> 1.21)
@@ -29,7 +29,6 @@ GEM
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
bcrypt_pbkdf (1.1.0)
builder (3.2.4)
concurrent-ruby (1.1.10)
crass (1.0.6)
@@ -37,7 +36,6 @@ GEM
irb (>= 1.5.0)
reline (>= 0.3.1)
dotenv (2.8.1)
ed25519 (1.3.0)
erubi (1.12.0)
i18n (1.12.0)
concurrent-ruby (~> 1.0)
@@ -98,9 +96,7 @@ PLATFORMS
x86_64-linux
DEPENDENCIES
bcrypt_pbkdf
debug
ed25519
mocha
mrsk!
railties

View File

@@ -1,6 +1,6 @@
# MRSK
MRSK deploys web apps in containers to servers running Docker with zero downtime. It uses the dynamic reverse-proxy Traefik to hold requests while the new application container is started and the old one is stopped. It works seamlessly across multiple hosts, using SSHKit to execute commands. It was built for Rails applications, but works with any type of web app that can be bundled with Docker.
MRSK deploys web apps in containers to servers running Docker with zero downtime. It uses the dynamic reverse-proxy Traefik to hold requests while the new application container is started and the old one is stopped. It works seamlessly across multiple hosts, using SSHKit to execute commands.
## Installation
@@ -14,8 +14,7 @@ servers:
- 192.168.0.2
registry:
username: registry-user-name
password:
- MRSK_REGISTRY_PASSWORD
password: <%= ENV.fetch("MRSK_REGISTRY_PASSWORD") %>
env:
secret:
- RAILS_MASTER_KEY
@@ -38,30 +37,17 @@ This will:
5. Push the image to the registry.
6. Pull the image from the registry on the servers.
7. Ensure Traefik is running and accepting traffic on port 80.
8. Ensure your app responds with `200 OK` to `GET /up`.
9. Stop any containers running a previous versions of the app.
10. Start a new container with the version of the app that matches the current git version hash.
11. Prune unused images and stopped containers to ensure servers don't fill up.
8. Stop any containers running a previous versions of the app.
9. Start a new container with the version of the app that matches the current git version hash.
10. 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.
## 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 our own hardware, or even just have a clear migration path to do so, 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 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 structure also 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's a lot of compelling options available.
Ultimately, MRSK is meant to compress the complexity of going to production using open source tooling that isn't tied to any commercial offering. Not to zero, though. You're probably still better off with a fully managed service if basic Linux or Docker is still difficult, but from an early stage when those concepts are familiar.
## Why not just run Capistrano, Kubernetes or Docker Swarm?
## Why not just run Capistrano or Kubernetes?
MRSK basically is Capistrano for Containers, which allow us to use vanilla servers as the hosts. No need to ensure that the servers have just the right version of Ruby or other dependencies you need. That all lives in the Docker image now. You can boot a brand new Ubuntu (or whatever) server, add it to the deploy servers of MRSK, and it'll be auto-provisioned with Docker, and run right away. Docker's layer caching also allows for quicker deployments with less mucking about on the server. And the images built for MRSK can be used for CI or later introspection.
Kubernetes is a beast. Running it yourself on your own hardware is not for the faint of heart. It's a fine option if you want to run on someone else's platform, either transparently [like Render](https://thenewstack.io/render-cloud-deployment-with-less-engineering/) or explicitly on AWS/GCP, but if you'd like the freedom to move between cloud and your own hardware, or even mix the two, MRSK is much simpler. You can see everything that's going on, it's just basic Docker commands being called.
Docker Swarm is much simpler than Kubernetes, but it's still built on the same declarative model that uses state reconciliation. MRSK is intentionally designed to around imperative commands, like Capistrano.
Kubernetes is a beast. Running it yourself on your own hardware is not for the faint of heart. It's a fine option if you want to run on someone else's platform, like Render or Fly, but if you'd like the freedom to move between cloud and your own hardware, or even mix the two, MRSK is much simpler. You can see everything that's going on, it's just basic Docker commands being called.
## Configuration
@@ -185,10 +171,10 @@ You can specialize the default Traefik rules by setting labels on the containers
```
labels:
traefik.http.routers.hey.rule: Host(\`app.hey.com\`)
traefik.http.routers.hey.rule: '''Host(`app.hey.com`)'''
```
Note: The escaped backticks are needed to ensure the rule is passed in correctly and not treated as command substitution by Bash!
Note: The extra quotes are needed to ensure the rule is passed in correctly!
This allows you to run multiple applications on the same server sharing the same Traefik instance and port.
See https://doc.traefik.io/traefik/routing/routers/#rule for a full list of available routing rules.
@@ -261,15 +247,14 @@ builder:
This build secret can then be referenced in the Dockerfile:
```dockerfile
```
# Copy Gemfiles
COPY Gemfile Gemfile.lock ./
# Install dependencies, including private repositories via access token (then remove bundle cache with exposed GITHUB_TOKEN)
# Install dependencies, including private repositories via access token
RUN --mount=type=secret,id=GITHUB_TOKEN \
BUNDLE_GITHUB__COM=x-access-token:$(cat /run/secrets/GITHUB_TOKEN) \
bundle install && \
rm -rf /usr/local/bundle/cache
bundle install
```
### Using command arguments for Traefik
@@ -278,13 +263,12 @@ You can customize the traefik command line:
```yaml
traefik:
args:
accesslog: true
accesslog.format: json
metrics.prometheus: true
metrics.prometheus.buckets: 0.1,0.3,1.2,5.0
```
This will start the traefik container with `--accesslog=true accesslog.format=json`.
### Configuring build args for new images
Build arguments that aren't secret can also be configured:
@@ -330,21 +314,6 @@ accessories:
Now run `mrsk accessory start mysql` to start the MySQL server on 1.1.1.3. See `mrsk accessory` for all the commands possible.
### Using Cron
You can use a specific container to run your Cron jobs:
```yaml
servers:
cron:
hosts:
- 192.168.0.1
cmd:
bash -c "cat config/crontab | crontab - && cron -f"
```
This assumes the Cron settings are stored in `config/crontab`.
### Using a generated .env file
If you're using a centralized secret store, like 1Password, you can create `.env.erb` as a template which looks up the secrets. Example of a .env.erb file:
@@ -362,40 +331,6 @@ This template can safely be checked into git. Then everyone deploying the app ca
If you need separate env variables for different destinations, you can set them with `.env.destination.erb` for the template, which will generate `.env.staging` when run with `mrsk envify -d staging`.
### 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
```
### Using custom healthcheck path or port
MRSK defaults to checking the health of your application again `/up` on port 3000. You can tailor both with the `healthcheck` setting:
```yaml
healthcheck:
path: /healthz
port: 4000
```
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.
## Commands
### Running commands on servers
@@ -463,7 +398,7 @@ mrsk app exec -i 'bin/rails console'
```
### Running details to show state of containers
### Running details to see state of containers
You can see the state of your servers by running `mrsk details`:
@@ -513,7 +448,7 @@ If you wish to remove the entire application, including Traefik, containers, ima
## Stage of development
This is beta software. Commands may still move around. But we're live in production at [37signals](https://37signals.com).
This is alpha software. Lots of stuff is missing. Lots of stuff will keep moving around for a while.
## License

View File

@@ -1,5 +1,5 @@
class Mrsk::Cli::Accessory < Mrsk::Cli::Base
desc "boot [NAME]", "Boot new accessory service on host (use NAME=all to boot all accessories)"
desc "boot [NAME]", "Boot accessory service on host (use NAME=all to boot all accessories)"
def boot(name)
if name == "all"
MRSK.accessory_names.each { |accessory_name| boot(accessory_name) }
@@ -9,19 +9,19 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
upload(name)
on(accessory.host) do
execute *MRSK.auditor.record("Booted #{name} accessory"), verbosity: :debug
execute *MRSK.auditor.record("accessory #{name} boot"), verbosity: :debug
execute *accessory.run
end
audit_broadcast "Booted accessory #{name}" unless options[:skip_broadcast]
end
end
end
desc "upload [NAME]", "Upload accessory files to host", hide: true
desc "upload [NAME]", "Upload accessory files to host"
def upload(name)
with_accessory(name) do |accessory|
on(accessory.host) do
execute *MRSK.auditor.record("accessory #{name} upload files"), verbosity: :debug
accessory.files.each do |(local, remote)|
accessory.ensure_local_file_present(local)
@@ -33,10 +33,12 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
end
end
desc "directories [NAME]", "Create accessory directories on host", hide: true
desc "directories [NAME]", "Create accessory directories on host"
def directories(name)
with_accessory(name) do |accessory|
on(accessory.host) do
execute *MRSK.auditor.record("accessory #{name} create directories"), verbosity: :debug
accessory.directories.keys.each do |host_path|
execute *accessory.make_directory(host_path)
end
@@ -44,7 +46,7 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
end
end
desc "reboot [NAME]", "Reboot existing accessory on host (stop container, remove container, start new container)"
desc "reboot [NAME]", "Reboot accessory on host (stop container, remove container, start new container)"
def reboot(name)
with_accessory(name) do |accessory|
stop(name)
@@ -53,27 +55,27 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
end
end
desc "start [NAME]", "Start existing accessory container on host"
desc "start [NAME]", "Start existing accessory on host"
def start(name)
with_accessory(name) do |accessory|
on(accessory.host) do
execute *MRSK.auditor.record("Started #{name} accessory"), verbosity: :debug
execute *MRSK.auditor.record("accessory #{name} start"), verbosity: :debug
execute *accessory.start
end
end
end
desc "stop [NAME]", "Stop existing accessory container on host"
desc "stop [NAME]", "Stop accessory on host"
def stop(name)
with_accessory(name) do |accessory|
on(accessory.host) do
execute *MRSK.auditor.record("Stopped #{name} accessory"), verbosity: :debug
execute *MRSK.auditor.record("accessory #{name} stop"), verbosity: :debug
execute *accessory.stop, raise_on_non_zero_exit: false
end
end
end
desc "restart [NAME]", "Restart existing accessory container on host"
desc "restart [NAME]", "Restart accessory on host"
def restart(name)
with_accessory(name) do
stop(name)
@@ -81,7 +83,7 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
end
end
desc "details [NAME]", "Show details about accessory on host (use NAME=all to show all accessories)"
desc "details [NAME]", "Display details about accessory on host (use NAME=all to boot all accessories)"
def details(name)
if name == "all"
MRSK.accessory_names.each { |accessory_name| details(accessory_name) }
@@ -92,7 +94,7 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
end
end
desc "exec [NAME] [CMD]", "Execute a custom command on servers (use --help to show options)"
desc "exec [NAME] [CMD]", "Execute a custom command on servers"
option :interactive, aliases: "-i", type: :boolean, default: false, desc: "Execute command over ssh for an interactive shell (use for console/bash)"
option :reuse, type: :boolean, default: false, desc: "Reuse currently running container instead of starting a new one"
def exec(name, cmd)
@@ -109,21 +111,21 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
when options[:reuse]
say "Launching command from existing container...", :magenta
on(accessory.host) do
execute *MRSK.auditor.record("Executed cmd '#{cmd}' on #{name} accessory"), verbosity: :debug
execute *MRSK.auditor.record("accessory #{name} cmd '#{cmd}'"), verbosity: :debug
capture_with_info(*accessory.execute_in_existing_container(cmd))
end
else
say "Launching command from new container...", :magenta
on(accessory.host) do
execute *MRSK.auditor.record("Executed cmd '#{cmd}' on #{name} accessory"), verbosity: :debug
execute *MRSK.auditor.record("accessory #{name} cmd '#{cmd}'"), verbosity: :debug
capture_with_info(*accessory.execute_in_new_container(cmd))
end
end
end
end
desc "logs [NAME]", "Show log lines from accessory on host (use --help to show options)"
desc "logs [NAME]", "Show log lines from accessory on host"
option :since, aliases: "-s", desc: "Show logs since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)"
option :lines, type: :numeric, aliases: "-n", desc: "Number of log lines to pull from each server"
option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)"
@@ -149,15 +151,11 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
end
end
desc "remove [NAME]", "Remove accessory container and image from host (use NAME=all to remove all accessories)"
option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
desc "remove [NAME]", "Remove accessory container and image from host (use NAME=all to boot all accessories)"
def remove(name)
if name == "all"
if options[:confirmed] || ask("This will remove all containers and images for all accessories. Are you sure?", limited_to: %w( y N ), default: "N") == "y"
MRSK.accessory_names.each { |accessory_name| remove(accessory_name) }
end
else
if options[:confirmed] || ask("This will remove all containers and images for #{name}. Are you sure?", limited_to: %w( y N ), default: "N") == "y"
with_accessory(name) do
stop(name)
remove_container(name)
@@ -166,32 +164,32 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
end
end
end
end
desc "remove_container [NAME]", "Remove accessory container from host", hide: true
desc "remove_container [NAME]", "Remove accessory container from host"
def remove_container(name)
with_accessory(name) do |accessory|
on(accessory.host) do
execute *MRSK.auditor.record("Remove #{name} accessory container"), verbosity: :debug
execute *MRSK.auditor.record("accessory #{name} remove container"), verbosity: :debug
execute *accessory.remove_container
end
end
end
desc "remove_image [NAME]", "Remove accessory image from host", hide: true
desc "remove_image [NAME]", "Remove accessory image from host"
def remove_image(name)
with_accessory(name) do |accessory|
on(accessory.host) do
execute *MRSK.auditor.record("Removed #{name} accessory image"), verbosity: :debug
execute *MRSK.auditor.record("accessory #{name} remove image"), verbosity: :debug
execute *accessory.remove_image
end
end
end
desc "remove_service_directory [NAME]", "Remove accessory directory used for uploaded files and data directories from host", hide: true
desc "remove_service_directory [NAME]", "Remove accessory directory used for uploaded files and data directories from host"
def remove_service_directory(name)
with_accessory(name) do |accessory|
on(accessory.host) do
execute *MRSK.auditor.record("accessory #{name} remove service directory"), verbosity: :debug
execute *accessory.remove_service_directory
end
end

View File

@@ -5,27 +5,18 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
using_version(options[:version] || most_recent_version_available) do |version|
say "Start container with version #{version} (or reboot if already running)...", :magenta
cli = self
MRSK.config.roles.each do |role|
on(role.hosts) do |host|
execute *MRSK.auditor.record("Booted app version #{version}"), verbosity: :debug
execute *MRSK.auditor.record("app boot version #{version}"), verbosity: :debug
begin
old_version = capture_with_info(*MRSK.app.current_running_version).strip
execute *MRSK.app.stop, raise_on_non_zero_exit: false
execute *MRSK.app.run(role: role.name)
cli.say "Waiting #{MRSK.config.readiness_delay}s for app to boot...", :magenta
sleep MRSK.config.readiness_delay
execute *MRSK.app.stop(version: old_version), raise_on_non_zero_exit: false if old_version.present?
rescue SSHKit::Command::Failed => e
if e.message =~ /already in use/
error "Rebooting container with same version #{version} already deployed on #{host} (may cause gap in zero-downtime promise!)"
execute *MRSK.auditor.record("Rebooted app version #{version}"), verbosity: :debug
error "Rebooting container with same version already deployed on #{host}"
execute *MRSK.auditor.record("app rebooted with version #{version}"), verbosity: :debug
execute *MRSK.app.stop(version: version)
execute *MRSK.app.remove_container(version: version)
execute *MRSK.app.run(role: role.name)
else
@@ -37,29 +28,28 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
end
end
desc "start", "Start existing app container on servers"
desc "start", "Start existing app on servers (use --version=<git-hash> to designate specific version)"
def start
on(MRSK.hosts) do
execute *MRSK.auditor.record("Started app version #{MRSK.version}"), verbosity: :debug
execute *MRSK.auditor.record("app start version #{MRSK.version}"), verbosity: :debug
execute *MRSK.app.start, raise_on_non_zero_exit: false
end
end
desc "stop", "Stop app container on servers"
desc "stop", "Stop app on servers"
def stop
on(MRSK.hosts) do
execute *MRSK.auditor.record("Stopped app"), verbosity: :debug
execute *MRSK.auditor.record("app stop"), verbosity: :debug
execute *MRSK.app.stop, raise_on_non_zero_exit: false
end
end
# FIXME: Drop in favor of just containers?
desc "details", "Show details about app containers"
desc "details", "Display details about app containers"
def details
on(MRSK.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.info) }
end
desc "exec [CMD]", "Execute a custom command on servers (use --help to show options)"
desc "exec [CMD]", "Execute a custom command on servers"
option :interactive, aliases: "-i", type: :boolean, default: false, desc: "Execute command over ssh for an interactive shell (use for console/bash)"
option :reuse, type: :boolean, default: false, desc: "Reuse currently running container instead of starting a new one"
def exec(cmd)
@@ -84,7 +74,7 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
say "Launching command with version #{version} from existing container...", :magenta
on(MRSK.hosts) do |host|
execute *MRSK.auditor.record("Executed cmd '#{cmd}' on app version #{version}"), verbosity: :debug
execute *MRSK.auditor.record("app cmd '#{cmd}' with version #{version}"), verbosity: :debug
puts_by_host host, capture_with_info(*MRSK.app.execute_in_existing_container(cmd))
end
end
@@ -94,28 +84,28 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
using_version(options[:version] || most_recent_version_available) do |version|
say "Launching command with version #{version} from new container...", :magenta
on(MRSK.hosts) do |host|
execute *MRSK.auditor.record("Executed cmd '#{cmd}' on app version #{version}"), verbosity: :debug
execute *MRSK.auditor.record("app cmd '#{cmd}' with version #{version}"), verbosity: :debug
puts_by_host host, capture_with_info(*MRSK.app.execute_in_new_container(cmd))
end
end
end
end
desc "containers", "Show app containers on servers"
desc "containers", "List all the app containers currently on servers"
def containers
on(MRSK.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.list_containers) }
end
desc "images", "Show app images on servers"
desc "images", "List all the app images currently on servers"
def images
on(MRSK.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.list_images) }
end
desc "logs", "Show log lines from app on servers (use --help to show options)"
option :since, aliases: "-s", desc: "Show lines since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)"
option :lines, type: :numeric, aliases: "-n", desc: "Number of lines to show from each server"
desc "logs", "Show lines from app on servers"
option :since, aliases: "-s", desc: "Show logs since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)"
option :lines, type: :numeric, aliases: "-n", desc: "Number of log lines to pull from each server"
option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)"
option :follow, aliases: "-f", desc: "Follow log on primary server (or specific host set by --hosts)"
option :follow, aliases: "-f", desc: "Follow logs on primary server (or specific host set by --hosts)"
def logs
# FIXME: Catch when app containers aren't running
@@ -143,37 +133,36 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
desc "remove", "Remove app containers and images from servers"
def remove
stop
remove_containers
remove_images
end
desc "remove_container [VERSION]", "Remove app container with given version from servers", hide: true
desc "remove_container [VERSION]", "Remove app container with given version from servers"
def remove_container(version)
on(MRSK.hosts) do
execute *MRSK.auditor.record("Removed app container with version #{version}"), verbosity: :debug
execute *MRSK.auditor.record("app remove container #{version}"), verbosity: :debug
execute *MRSK.app.remove_container(version: version)
end
end
desc "remove_containers", "Remove all app containers from servers", hide: true
desc "remove_containers", "Remove all app containers from servers"
def remove_containers
on(MRSK.hosts) do
execute *MRSK.auditor.record("Removed all app containers"), verbosity: :debug
execute *MRSK.auditor.record("app remove containers"), verbosity: :debug
execute *MRSK.app.remove_containers
end
end
desc "remove_images", "Remove all app images from servers", hide: true
desc "remove_images", "Remove all app images from servers"
def remove_images
on(MRSK.hosts) do
execute *MRSK.auditor.record("Removed all app images"), verbosity: :debug
execute *MRSK.auditor.record("app remove images"), verbosity: :debug
execute *MRSK.app.remove_images
end
end
desc "version", "Show app version currently running on servers"
def version
desc "current_version", "Shows the version currently running"
def current_version
on(MRSK.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.current_running_version).strip }
end
@@ -195,13 +184,8 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
def most_recent_version_available(host: MRSK.primary_host)
version = nil
on(host) { version = capture_with_info(*MRSK.app.most_recent_version_from_available_images).strip }
if version == "<none>"
raise "Most recent image available was not tagged with a version (returned <none>)"
else
version.presence
end
end
def current_running_version(host: MRSK.primary_host)
version = nil

View File

@@ -17,10 +17,8 @@ module Mrsk::Cli
class_option :hosts, aliases: "-h", desc: "Run commands on these hosts instead of all (separate by comma)"
class_option :roles, aliases: "-r", desc: "Run commands on these roles instead of all (separate by comma)"
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 :config_file, aliases: "-c", default: "config/deploy.yml", desc: "Path to config file (default: config/deploy.yml)"
class_option :destination, aliases: "-d", desc: "Specify destination to be used for config file (west -> deploy.west.yml)"
def initialize(*)
super
@@ -61,14 +59,9 @@ module Mrsk::Cli
def print_runtime
started_at = Time.now
yield
return Time.now - started_at
ensure
runtime = Time.now - started_at
puts " Finished all in #{sprintf("%.1f seconds", runtime)}"
end
def audit_broadcast(line)
run_locally { execute *MRSK.auditor.broadcast(line), verbosity: :debug }
end
end
end

View File

@@ -1,11 +1,11 @@
class Mrsk::Cli::Build < Mrsk::Cli::Base
desc "deliver", "Build app and push app image to registry then pull image on servers"
desc "deliver", "Deliver a newly built app image to servers"
def deliver
push
pull
invoke :push
invoke :pull
end
desc "push", "Build and push app image to registry"
desc "push", "Build locally and push app image to registry"
def push
cli = self
@@ -26,16 +26,15 @@ class Mrsk::Cli::Build < Mrsk::Cli::Base
end
end
desc "pull", "Pull app image from registry onto servers"
desc "pull", "Pull app image from the registry onto servers"
def pull
on(MRSK.hosts) do
execute *MRSK.auditor.record("Pulled image with version #{MRSK.version}"), verbosity: :debug
execute *MRSK.builder.clean, raise_on_non_zero_exit: false
execute *MRSK.auditor.record("build pull image #{MRSK.version}"), verbosity: :debug
execute *MRSK.builder.pull
end
end
desc "create", "Create a build setup"
desc "create", "Create a local build setup"
def create
run_locally do
begin
@@ -52,7 +51,7 @@ class Mrsk::Cli::Build < Mrsk::Cli::Base
end
end
desc "remove", "Remove build setup"
desc "remove", "Remove local build setup"
def remove
run_locally do
debug "Using builder: #{MRSK.builder.name}"
@@ -60,7 +59,7 @@ class Mrsk::Cli::Build < Mrsk::Cli::Base
end
end
desc "details", "Show build setup"
desc "details", "Show the name of the configured builder"
def details
run_locally do
puts "Builder: #{MRSK.builder.name}"

View File

@@ -1,50 +0,0 @@
class Mrsk::Cli::Healthcheck < Mrsk::Cli::Base
MAX_ATTEMPTS = 7
class HealthcheckError < StandardError; end
default_command :perform
desc "perform", "Health check current app version"
def perform
on(MRSK.primary_host) do
begin
execute *MRSK.healthcheck.run
target = "Health check against #{MRSK.config.healthcheck["path"]}"
attempt = 1
begin
status = capture_with_info(*MRSK.healthcheck.curl)
if status == "200"
info "#{target} succeeded with 200 OK!"
else
raise HealthcheckError, "#{target} failed with status #{status}"
end
rescue SSHKit::Command::Failed
if attempt <= MAX_ATTEMPTS
info "#{target} failed to respond, retrying in #{attempt}s..."
sleep attempt
attempt += 1
retry
else
raise
end
end
rescue SSHKit::Command::Failed, HealthcheckError => e
error capture_with_info(*MRSK.healthcheck.logs)
if e.message =~ /curl/
raise SSHKit::Command::Failed, "#{target} failed to return 200 OK!"
else
raise
end
ensure
execute *MRSK.healthcheck.stop, raise_on_non_zero_exit: false
execute *MRSK.healthcheck.remove, raise_on_non_zero_exit: false
end
end
end
end

View File

@@ -1,5 +1,5 @@
class Mrsk::Cli::Main < Mrsk::Cli::Base
desc "setup", "Setup all accessories and deploy app to servers"
desc "setup", "Setup all accessories and deploy the app to servers"
def setup
print_runtime do
invoke "mrsk:cli:server:bootstrap"
@@ -8,9 +8,9 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
end
end
desc "deploy", "Deploy app to servers"
desc "deploy", "Deploy the app to servers"
def deploy
runtime = print_runtime do
print_runtime do
say "Ensure Docker is installed...", :magenta
invoke "mrsk:cli:server:bootstrap"
@@ -23,60 +23,37 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
say "Ensure Traefik is running...", :magenta
invoke "mrsk:cli:traefik:boot"
say "Ensure app can pass healthcheck...", :magenta
invoke "mrsk:cli:healthcheck:perform"
invoke "mrsk:cli:app:boot"
say "Prune old containers and images...", :magenta
invoke "mrsk:cli:prune:all"
end
audit_broadcast "Deployed app in #{runtime.to_i} seconds" unless options[:skip_broadcast]
end
desc "redeploy", "Deploy app to servers without bootstrapping servers, starting Traefik, pruning, and registry login"
desc "redeploy", "Deploy new version of the app to servers (without bootstrapping servers, starting Traefik, pruning, and registry login)"
def redeploy
runtime = print_runtime do
print_runtime do
say "Build and push app image...", :magenta
invoke "mrsk:cli:build:deliver"
say "Ensure app can pass healthcheck...", :magenta
invoke "mrsk:cli:healthcheck:perform"
invoke "mrsk:cli:app:boot"
end
audit_broadcast "Redeployed app in #{runtime.to_i} seconds" unless options[:skip_broadcast]
end
desc "rollback [VERSION]", "Rollback app to VERSION"
desc "rollback [VERSION]", "Rollback the app to VERSION"
def rollback(version)
MRSK.version = version
if container_name_available?(MRSK.config.service_with_version)
say "Start version #{version}, then stop the old version...", :magenta
cli = self
on(MRSK.hosts) do |host|
old_version = capture_with_info(*MRSK.app.current_running_version).strip.presence
cli.say "Stop current version, then start version #{version}...", :magenta
on(MRSK.hosts) do
execute *MRSK.app.stop, raise_on_non_zero_exit: false
execute *MRSK.app.start
cli.say "Waiting #{MRSK.config.readiness_delay}s for app to start...", :magenta
sleep MRSK.config.readiness_delay
execute *MRSK.app.stop(version: old_version), raise_on_non_zero_exit: false
end
audit_broadcast "Rolled back app to version #{version}" unless options[:skip_broadcast]
else
say "The app version '#{version}' is not available as a container (use 'mrsk app containers' for available versions)", :red
end
end
desc "details", "Show details about all containers"
desc "details", "Display details about Traefik and app containers"
def details
invoke "mrsk:cli:traefik:details"
invoke "mrsk:cli:app:details"
@@ -90,7 +67,7 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
end
end
desc "config", "Show combined config (including secrets!)"
desc "config", "Show combined config"
def config
run_locally do
puts MRSK.config.to_h.to_yaml
@@ -130,66 +107,42 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
desc "envify", "Create .env by evaluating .env.erb (or .env.staging.erb -> .env.staging when using -d staging)"
def envify
if destination = options[:destination]
env_template_path = ".env.#{destination}.erb"
env_path = ".env.#{destination}"
File.write(".env.#{destination}", ERB.new(IO.read(Pathname.new(File.expand_path(".env.#{destination}.erb")))).result)
else
env_template_path = ".env.erb"
env_path = ".env"
File.write(".env", ERB.new(IO.read(Pathname.new(File.expand_path(".env.erb")))).result)
end
end
File.write(env_path, ERB.new(File.read(env_template_path)).result, perm: 0600)
end
desc "remove", "Remove Traefik, app, accessories, and registry session from servers"
option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
desc "remove", "Remove Traefik, app, and registry session from servers"
def remove
if options[:confirmed] || ask(remove_confirmation_question, limited_to: %w( y N ), default: "N") == "y"
invoke "mrsk:cli:traefik:remove", [], options.without(:confirmed)
invoke "mrsk:cli:app:remove", [], options.without(:confirmed)
invoke "mrsk:cli:accessory:remove", [ "all" ], options
invoke "mrsk:cli:registry:logout", [], options.without(:confirmed)
end
invoke "mrsk:cli:traefik:remove"
invoke "mrsk:cli:app:remove"
invoke "mrsk:cli:registry:logout"
end
desc "version", "Show MRSK version"
desc "version", "Display the MRSK version"
def version
puts Mrsk::VERSION
end
desc "accessory", "Manage accessories (db/redis/search)"
desc "accessory", "Manage the accessories"
subcommand "accessory", Mrsk::Cli::Accessory
desc "app", "Manage application"
desc "app", "Manage the application"
subcommand "app", Mrsk::Cli::App
desc "build", "Build application image"
desc "build", "Build the application image"
subcommand "build", Mrsk::Cli::Build
desc "healthcheck", "Healthcheck application"
subcommand "healthcheck", Mrsk::Cli::Healthcheck
desc "prune", "Prune old application images and containers"
subcommand "prune", Mrsk::Cli::Prune
desc "registry", "Login and -out of the image registry"
desc "registry", "Login and out of the image registry"
subcommand "registry", Mrsk::Cli::Registry
desc "server", "Bootstrap servers with Docker"
subcommand "server", Mrsk::Cli::Server
desc "traefik", "Manage Traefik load balancer"
desc "traefik", "Manage the Traefik load balancer"
subcommand "traefik", Mrsk::Cli::Traefik
private
def container_name_available?(container_name, host: MRSK.primary_host)
container_names = nil
on(host) { container_names = capture_with_info(*MRSK.app.list_container_names).split("\n") }
Array(container_names).include?(container_name)
end
def remove_confirmation_question
"This will remove all containers and images. " +
(MRSK.config.accessories.any? ? "Including #{MRSK.config.accessories.collect(&:name).to_sentence}. " : "") +
"Are you sure?"
end
end

View File

@@ -1,22 +1,22 @@
class Mrsk::Cli::Prune < Mrsk::Cli::Base
desc "all", "Prune unused images and stopped containers"
def all
containers
images
invoke :containers
invoke :images
end
desc "images", "Prune unused images older than 7 days"
desc "images", "Prune unused images older than 30 days"
def images
on(MRSK.hosts) do
execute *MRSK.auditor.record("Pruned images"), verbosity: :debug
execute *MRSK.auditor.record("prune images"), verbosity: :debug
execute *MRSK.prune.images
end
end
desc "containers", "Prune stopped containers older than 3 days"
desc "containers", "Prune stopped containers for the service older than 3 days"
def containers
on(MRSK.hosts) do
execute *MRSK.auditor.record("Pruned containers"), verbosity: :debug
execute *MRSK.auditor.record("prune containers"), verbosity: :debug
execute *MRSK.prune.containers
end
end

View File

@@ -1,17 +1,15 @@
class Mrsk::Cli::Registry < Mrsk::Cli::Base
desc "login", "Log in to registry locally and remotely"
desc "login", "Login to the registry locally and remotely"
def login
run_locally { execute *MRSK.registry.login }
on(MRSK.hosts) { execute *MRSK.registry.login }
# FIXME: This rescue needed?
rescue ArgumentError => e
puts e.message
end
desc "logout", "Log out of registry remotely"
desc "logout", "Logout of the registry remotely"
def logout
on(MRSK.hosts) { execute *MRSK.registry.logout }
# FIXME: This rescue needed?
rescue ArgumentError => e
puts e.message
end

View File

@@ -1,5 +1,5 @@
class Mrsk::Cli::Server < Mrsk::Cli::Base
desc "bootstrap", "Ensure Docker is installed on servers"
desc "bootstrap", "Ensure Docker is installed on the servers"
def bootstrap
on(MRSK.hosts + MRSK.accessory_hosts) { execute "which docker || (apt-get update -y && apt-get install docker.io -y)" }
end

View File

@@ -1,4 +1,5 @@
# Name of your application. Used to uniquely configure containers.
# Name of your application. Used to uniquely configuring Traefik and app containers.
# Your Dockerfile should set LABEL service=the-same-value to ensure image pruning works.
service: my-app
# Name of the container image.
@@ -13,64 +14,4 @@ registry:
# Specify the registry server, if you're not using Docker Hub
# server: registry.digitalocean.com / ghcr.io / ...
username: my-user
password:
- MRSK_REGISTRY_PASSWORD
# Inject ENV variables into containers (secrets come from .env).
# env:
# clear:
# DB_HOST: 192.168.0.2
# 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
# Configure builder setup.
# builder:
# args:
# RUBY_VERSION: 3.2.0
# secrets:
# - GITHUB_TOKEN
# remote:
# arch: amd64
# host: ssh://app@192.168.0.1
# Use accessory services (secrets come from .env).
# accessories:
# db:
# image: mysql:8.0
# host: 192.168.0.2
# port: 3306
# env:
# clear:
# MYSQL_ROOT_HOST: '%'
# secret:
# - MYSQL_ROOT_PASSWORD
# files:
# - config/mysql/production.cnf:/etc/mysql/my.cnf
# - db/production.sql.erb:/docker-entrypoint-initdb.d/setup.sql
# directories:
# - data:/var/lib/mysql
# redis:
# image: redis:7.0
# host: 192.168.0.2
# port: 6379
# directories:
# - data:/data
# Configure custom arguments for Traefik
# traefik:
# args:
# accesslog: true
# accesslog.format: json
# Configure a custom healthcheck (default is /up on port 3000)
# healthcheck:
# path: /healthz
# port: 4000
password: my-password-should-go-somewhere-safe

View File

@@ -6,34 +6,34 @@ class Mrsk::Cli::Traefik < Mrsk::Cli::Base
desc "reboot", "Reboot Traefik on servers (stop container, remove container, start new container)"
def reboot
stop
remove_container
boot
invoke :stop
invoke :remove_container
invoke :boot
end
desc "start", "Start existing Traefik container on servers"
desc "start", "Start existing Traefik on servers"
def start
on(MRSK.traefik_hosts) do
execute *MRSK.auditor.record("Started traefik"), verbosity: :debug
execute *MRSK.auditor.record("traefik start"), verbosity: :debug
execute *MRSK.traefik.start, raise_on_non_zero_exit: false
end
end
desc "stop", "Stop existing Traefik container on servers"
desc "stop", "Stop Traefik on servers"
def stop
on(MRSK.traefik_hosts) do
execute *MRSK.auditor.record("Stopped traefik"), verbosity: :debug
execute *MRSK.auditor.record("traefik stop"), verbosity: :debug
execute *MRSK.traefik.stop, raise_on_non_zero_exit: false
end
end
desc "restart", "Restart existing Traefik container on servers"
desc "restart", "Restart Traefik on servers"
def restart
stop
start
invoke :stop
invoke :start
end
desc "details", "Show details about Traefik container from servers"
desc "details", "Display details about Traefik containers from servers"
def details
on(MRSK.traefik_hosts) { |host| puts_by_host host, capture_with_info(*MRSK.traefik.info), type: "Traefik" }
end
@@ -64,23 +64,23 @@ class Mrsk::Cli::Traefik < Mrsk::Cli::Base
desc "remove", "Remove Traefik container and image from servers"
def remove
stop
remove_container
remove_image
invoke :stop
invoke :remove_container
invoke :remove_image
end
desc "remove_container", "Remove Traefik container from servers", hide: true
desc "remove_container", "Remove Traefik container from servers"
def remove_container
on(MRSK.traefik_hosts) do
execute *MRSK.auditor.record("Removed traefik container"), verbosity: :debug
execute *MRSK.auditor.record("traefik remove container"), verbosity: :debug
execute *MRSK.traefik.remove_container
end
end
desc "remove_container", "Remove Traefik image from servers", hide: true
desc "remove_container", "Remove Traefik image from servers"
def remove_image
on(MRSK.traefik_hosts) do
execute *MRSK.auditor.record("Removed traefik image"), verbosity: :debug
execute *MRSK.auditor.record("traefik remove image"), verbosity: :debug
execute *MRSK.traefik.remove_image
end
end

View File

@@ -49,6 +49,22 @@ class Mrsk::Commander
@app ||= Mrsk::Commands::App.new(config)
end
def builder
@builder ||= Mrsk::Commands::Builder.new(config)
end
def traefik
@traefik ||= Mrsk::Commands::Traefik.new(config)
end
def registry
@registry ||= Mrsk::Commands::Registry.new(config)
end
def prune
@prune ||= Mrsk::Commands::Prune.new(config)
end
def accessory(name)
Mrsk::Commands::Accessory.new(config, name: name)
end
@@ -57,36 +73,12 @@ class Mrsk::Commander
@auditor ||= Mrsk::Commands::Auditor.new(config)
end
def builder
@builder ||= Mrsk::Commands::Builder.new(config)
end
def healthcheck
@healthcheck ||= Mrsk::Commands::Healthcheck.new(config)
end
def prune
@prune ||= Mrsk::Commands::Prune.new(config)
end
def registry
@registry ||= Mrsk::Commands::Registry.new(config)
end
def traefik
@traefik ||= Mrsk::Commands::Traefik.new(config)
end
def with_verbosity(level)
old_level = self.verbosity
self.verbosity = level
old_level = SSHKit.config.output_verbosity
SSHKit.config.output_verbosity = level
yield
ensure
self.verbosity = old_level
SSHKit.config.output_verbosity = old_level
end
@@ -99,15 +91,7 @@ class Mrsk::Commander
private
def cascading_version
version.presence || ENV["VERSION"] || current_commit_hash
end
def current_commit_hash
if system("git rev-parse")
`git rev-parse HEAD`.strip
else
raise "Can't use commit hash as version, no git repository found in #{Dir.pwd}"
end
version.presence || ENV["VERSION"] || `git rev-parse HEAD`.strip
end
# Lazy setup of SSHKit

View File

@@ -10,10 +10,9 @@ class Mrsk::Commands::Accessory < Mrsk::Commands::Base
def run
docker :run,
"--name", service_name,
"--detach",
"-d",
"--restart", "unless-stopped",
"--log-opt", "max-size=#{MAX_LOG_SIZE}",
"--publish", port,
"-p", port,
*env_args,
*volume_args,
*label_args,
@@ -35,14 +34,14 @@ class Mrsk::Commands::Accessory < Mrsk::Commands::Base
def logs(since: nil, lines: nil, grep: nil)
pipe \
docker(:logs, service_name, (" --since #{since}" if since), (" --tail #{lines}" if lines), "--timestamps", "2>&1"),
docker(:logs, service_name, (" --since #{since}" if since), (" -n #{lines}" if lines), "-t", "2>&1"),
("grep '#{grep}'" if grep)
end
def follow_logs(grep: nil)
run_over_ssh \
pipe \
docker(:logs, service_name, "--timestamps", "--tail", "10", "--follow", "2>&1"),
docker(:logs, service_name, "-t", "-n", "10", "-f", "2>&1"),
(%(grep "#{grep}") if grep)
end
@@ -96,11 +95,11 @@ class Mrsk::Commands::Accessory < Mrsk::Commands::Base
end
def remove_container
docker :container, :prune, "--force", *service_filter
docker :container, :prune, "-f", *service_filter
end
def remove_image
docker :image, :prune, "--all", "--force", *service_filter
docker :image, :prune, "-a", "-f", *service_filter
end
private

View File

@@ -3,9 +3,8 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
role = config.role(role)
docker :run,
"--detach",
"-d",
"--restart unless-stopped",
"--log-opt", "max-size=#{MAX_LOG_SIZE}",
"--name", service_with_version,
*role.env_args,
*config.volume_args,
@@ -18,10 +17,8 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
docker :start, service_with_version
end
def stop(version: nil)
pipe \
version ? container_id_for_version(version) : current_container_id,
xargs(docker(:stop))
def stop
pipe current_container_id, xargs(docker(:stop))
end
def info
@@ -32,7 +29,7 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
def logs(since: nil, lines: nil, grep: nil)
pipe \
current_container_id,
"xargs docker logs#{" --since #{since}" if since}#{" --tail #{lines}" if lines} 2>&1",
"xargs docker logs#{" --since #{since}" if since}#{" -n #{lines}" if lines} 2>&1",
("grep '#{grep}'" if grep)
end
@@ -40,7 +37,7 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
run_over_ssh \
pipe(
current_container_id,
"xargs docker logs --timestamps --tail 10 --follow 2>&1",
"xargs docker logs -t -n 10 -f 2>&1",
(%(grep "#{grep}") if grep)
),
host: host
@@ -74,7 +71,11 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
def current_container_id
docker :ps, "--quiet", *service_filter
docker :ps, "-q", *service_filter
end
def container_id_for(container_name:)
docker :container, :ls, "-a", "-f", "name=#{container_name}", "-q"
end
def current_running_version
@@ -91,19 +92,9 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
"head -n 1"
end
def all_versions_from_available_containers
pipe \
docker(:image, :ls, "--format", '"{{.Tag}}"', config.repository),
"head -n 1"
end
def list_containers
docker :container, :ls, "--all", *service_filter
end
def list_container_names
[ *list_containers, "--format", "'{{ .Names }}'" ]
docker :container, :ls, "-a", *service_filter
end
def remove_container(version:)
@@ -113,7 +104,7 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
end
def remove_containers
docker :container, :prune, "--force", *service_filter
docker :container, :prune, "-f", *service_filter
end
def list_images
@@ -121,7 +112,7 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
end
def remove_images
docker :image, :prune, "--all", "--force", *service_filter
docker :image, :prune, "-a", "-f", *service_filter
end
@@ -134,10 +125,6 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
end
end
def container_id_for_version(version)
container_id_for(container_name: service_with_version(version))
end
def service_filter
[ "--filter", "label=service=#{config.service}" ]
end

View File

@@ -1,20 +1,12 @@
require "active_support/core_ext/time/conversions"
class Mrsk::Commands::Auditor < Mrsk::Commands::Base
# Runs remotely
def record(line)
append \
[ :echo, tagged_record_line(line) ],
[ :echo, tagged_line(line) ],
audit_log_file
end
# Runs locally
def broadcast(line)
if broadcast_cmd = config.audit_broadcast_cmd
[ broadcast_cmd, tagged_broadcast_line(line) ]
end
end
def reveal
[ :tail, "-n", 50, audit_log_file ]
end
@@ -24,19 +16,19 @@ class Mrsk::Commands::Auditor < Mrsk::Commands::Base
"mrsk-#{config.service}-audit.log"
end
def tagged_record_line(line)
"'#{recorded_at_tag} #{performer_tag} #{line}'"
def tagged_line(line)
"'#{tags} #{line}'"
end
def tagged_broadcast_line(line)
"'#{performer_tag} #{line}'"
def tags
"[#{timestamp}] [#{performer}]"
end
def performer_tag
"[#{`whoami`.strip}]"
def performer
`whoami`.strip
end
def recorded_at_tag
"[#{Time.now.to_fs(:db)}]"
def timestamp
Time.now.to_fs(:db)
end
end

View File

@@ -2,8 +2,6 @@ module Mrsk::Commands
class Base
delegate :redact, to: Mrsk::Utils
MAX_LOG_SIZE = "10m"
attr_accessor :config
def initialize(config)
@@ -17,10 +15,6 @@ module Mrsk::Commands
end
end
def container_id_for(container_name:)
docker :container, :ls, "--all", "--filter", "name=#{container_name}", "--quiet"
end
private
def combine(*commands, by: "&&")
commands

View File

@@ -1,5 +1,5 @@
class Mrsk::Commands::Builder < Mrsk::Commands::Base
delegate :create, :remove, :push, :clean, :pull, :info, to: :target
delegate :create, :remove, :push, :pull, :info, to: :target
def name
target.class.to_s.remove("Mrsk::Commands::Builder::").underscore

View File

@@ -1,27 +1,10 @@
class Mrsk::Commands::Builder::Base < Mrsk::Commands::Base
delegate :argumentize, to: Mrsk::Utils
def clean
docker :image, :rm, "--force", config.absolute_image
end
def pull
docker :pull, config.absolute_image
end
def build_options
[ *build_tags, *build_labels, *build_args, *build_secrets ]
end
private
def build_tags
[ "-t", config.absolute_image, "-t", config.latest_image ]
end
def build_labels
argumentize "--label", { service: config.service }
end
def build_args
argumentize "--build-arg", args, redacted: true
end
@@ -30,6 +13,7 @@ class Mrsk::Commands::Builder::Base < Mrsk::Commands::Base
argumentize "--secret", secrets.collect { |secret| [ "id", secret ] }
end
private
def args
(config.builder && config.builder["args"]) || {}
end

View File

@@ -12,7 +12,9 @@ class Mrsk::Commands::Builder::Multiarch < Mrsk::Commands::Builder::Base
"--push",
"--platform", "linux/amd64,linux/arm64",
"--builder", builder_name,
*build_options,
"-t", config.absolute_image,
*build_args,
*build_secrets,
"."
end

View File

@@ -9,7 +9,7 @@ class Mrsk::Commands::Builder::Native < Mrsk::Commands::Builder::Base
def push
combine \
docker(:build, *build_options, "."),
docker(:build, "-t", *build_args, *build_secrets, config.absolute_image, "."),
docker(:push, config.absolute_image)
end

View File

@@ -16,7 +16,9 @@ class Mrsk::Commands::Builder::Native::Remote < Mrsk::Commands::Builder::Native
"--push",
"--platform", platform,
"--builder", builder_name,
*build_options,
"-t", config.absolute_image,
*build_args,
*build_secrets,
"."
end

View File

@@ -1,50 +0,0 @@
class Mrsk::Commands::Healthcheck < Mrsk::Commands::Base
EXPOSED_PORT = 3999
def run
web = config.role(:web)
docker :run,
"--detach",
"--name", container_name_with_version,
"--publish", "#{EXPOSED_PORT}:#{config.healthcheck["port"]}",
"--label", "service=#{container_name}",
*web.env_args,
*config.volume_args,
config.absolute_image,
web.cmd
end
def curl
[ :curl, "--silent", "--output", "/dev/null", "--write-out", "'%{http_code}'", "--max-time", "2", health_url ]
end
def logs
pipe container_id, xargs(docker(:logs, "--tail", 50, "2>&1"))
end
def stop
pipe container_id, xargs(docker(:stop))
end
def remove
pipe container_id, xargs(docker(:container, :rm))
end
private
def container_name
"healthcheck-#{config.service}"
end
def container_name_with_version
"healthcheck-#{config.service_with_version}"
end
def container_id
container_id_for(container_name: container_name)
end
def health_url
"http://localhost:#{EXPOSED_PORT}#{config.healthcheck["path"]}"
end
end

View File

@@ -2,11 +2,15 @@ require "active_support/duration"
require "active_support/core_ext/numeric/time"
class Mrsk::Commands::Prune < Mrsk::Commands::Base
def images(until_hours: 7.days.in_hours.to_i)
docker :image, :prune, "--all", "--force", "--filter", "label=service=#{config.service}", "--filter", "until=#{until_hours}h"
PRUNE_IMAGES_AFTER = 30.days.in_hours.to_i
PRUNE_CONTAINERS_AFTER = 3.days.in_hours.to_i
def images
docker :image, :prune, "-f", "--filter", "until=#{PRUNE_IMAGES_AFTER}h"
end
def containers(until_hours: 3.days.in_hours.to_i)
docker :container, :prune, "--force", "--filter", "label=service=#{config.service}", "--filter", "until=#{until_hours}h"
def containers
docker :image, :prune, "-f", "--filter", "until=#{PRUNE_IMAGES_AFTER}h"
docker :container, :prune, "-f", "--filter", "label=service=#{config.service}", "--filter", "'until=#{PRUNE_CONTAINERS_AFTER}h'"
end
end

View File

@@ -2,19 +2,10 @@ class Mrsk::Commands::Registry < Mrsk::Commands::Base
delegate :registry, to: :config
def login
docker :login, registry["server"], "-u", redact(registry["username"]), "-p", redact(lookup_password)
docker :login, registry["server"], "-u", redact(registry["username"]), "-p", redact(registry["password"])
end
def logout
docker :logout, registry["server"]
end
private
def lookup_password
if registry["password"].is_a?(Array)
ENV.fetch(registry["password"].first).dup
else
registry["password"]
end
end
end

View File

@@ -1,11 +1,10 @@
class Mrsk::Commands::Traefik < Mrsk::Commands::Base
def run
docker :run, "--name traefik",
"--detach",
"--restart", "unless-stopped",
"--log-opt", "max-size=#{MAX_LOG_SIZE}",
"--publish", "80:80",
"--volume", "/var/run/docker.sock:/var/run/docker.sock",
"-d",
"--restart unless-stopped",
"-p 80:80",
"-v /var/run/docker.sock:/var/run/docker.sock",
"traefik",
"--providers.docker",
"--log.level=DEBUG",
@@ -26,23 +25,23 @@ class Mrsk::Commands::Traefik < Mrsk::Commands::Base
def logs(since: nil, lines: nil, grep: nil)
pipe \
docker(:logs, "traefik", (" --since #{since}" if since), (" --tail #{lines}" if lines), "--timestamps", "2>&1"),
docker(:logs, "traefik", (" --since #{since}" if since), (" -n #{lines}" if lines), "-t", "2>&1"),
("grep '#{grep}'" if grep)
end
def follow_logs(host:, grep: nil)
run_over_ssh pipe(
docker(:logs, "traefik", "--timestamps", "--tail", "10", "--follow", "2>&1"),
docker(:logs, "traefik", "-t", "-n", "10", "-f", "2>&1"),
(%(grep "#{grep}") if grep)
).join(" "), host: host
end
def remove_container
docker :container, :prune, "--force", "--filter", "label=org.opencontainers.image.title=Traefik"
docker :container, :prune, "-f", "--filter", "label=org.opencontainers.image.title=Traefik"
end
def remove_image
docker :image, :prune, "--all", "--force", "--filter", "label=org.opencontainers.image.title=Traefik"
docker :image, :prune, "-a", "-f", "--filter", "label=org.opencontainers.image.title=Traefik"
end
private

View File

@@ -82,10 +82,6 @@ class Mrsk::Configuration
"#{repository}:#{version}"
end
def latest_image
"#{repository}:latest"
end
def service_with_version
"#{service}-#{version}"
end
@@ -107,7 +103,6 @@ class Mrsk::Configuration
end
end
def ssh_user
if raw_config.ssh.present?
raw_config.ssh["user"] || "root"
@@ -128,18 +123,6 @@ class Mrsk::Configuration
end
def audit_broadcast_cmd
raw_config.audit_broadcast_cmd
end
def healthcheck
{ "path" => "/up", "port" => 3000 }.merge(raw_config.healthcheck || {})
end
def readiness_delay
raw_config.readiness_delay || 7
end
def valid?
ensure_required_keys_present && ensure_env_available
end
@@ -158,8 +141,7 @@ class Mrsk::Configuration
volume_args: volume_args,
ssh_options: ssh_options,
builder: raw_config.builder,
accessories: raw_config.accessories,
healthcheck: healthcheck
accessories: raw_config.accessories
}.compact
end

View File

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

View File

@@ -1,14 +1,13 @@
module Mrsk::Utils
extend self
# Return a list of escaped shell arguments using the same named argument against the passed attributes (hash or array).
# Return a list of shell arguments using the same named argument against the passed attributes (hash or array).
def argumentize(argument, attributes, redacted: false)
Array(attributes).flat_map do |key, value|
if value.present?
escaped_pair = [ key, value.to_s.dump.gsub(/`/, '\\\\`') ].join("=")
[ argument, redacted ? redact(escaped_pair) : escaped_pair ]
Array(attributes).flat_map do |k, v|
if v.present?
[ argument, redacted ? redact("#{k}=#{v}") : "#{k}=#{v}" ]
else
[ argument, key ]
[ argument, k ]
end
end
end

View File

@@ -1,3 +1,3 @@
module Mrsk
VERSION = "0.8.3"
VERSION = "0.6.1"
end

View File

@@ -6,7 +6,7 @@ Gem::Specification.new do |spec|
spec.authors = [ "David Heinemeier Hansson" ]
spec.email = "dhh@hey.com"
spec.homepage = "https://github.com/rails/mrsk"
spec.summary = "Deploy web apps in containers to servers running Docker with zero downtime."
spec.summary = "Deploy Rails apps in containers to servers running Docker with zero downtime."
spec.license = "MIT"
spec.files = Dir["lib/**/*", "MIT-LICENSE", "README.md"]

View File

@@ -14,7 +14,7 @@ class CliAccessoryTest < CliTestCase
end
test "boot" do
assert_match "Running docker run --name app-mysql --detach --restart unless-stopped --log-opt max-size=10m --publish 3306:3306 -e [REDACTED] -e MYSQL_ROOT_HOST=\"%\" --volume $PWD/app-mysql/etc/mysql/my.cnf:/etc/mysql/my.cnf --volume $PWD/app-mysql/data:/var/lib/mysql --label service=\"app-mysql\" mysql:5.7 on 1.1.1.3", run_command("boot", "mysql")
assert_match "Running docker run --name app-mysql -d --restart unless-stopped -p 3306:3306 -e [REDACTED] -e MYSQL_ROOT_HOST=% --volume $PWD/app-mysql/etc/mysql/my.cnf:/etc/mysql/my.cnf --volume $PWD/app-mysql/data:/var/lib/mysql --label service=app-mysql mysql:5.7 on 1.1.1.3", run_command("boot", "mysql")
end
test "exec" do
@@ -31,14 +31,6 @@ class CliAccessoryTest < CliTestCase
end
end
test "remove with confirmation" do
run_command("remove", "mysql", "-y").tap do |output|
assert_match /docker container stop app-mysql/, output
assert_match /docker image prune --all --force --filter label=service=app-mysql/, output
assert_match /rm -rf app-mysql/, output
end
end
private
def run_command(*command)
stdouted { Mrsk::Cli::Accessory.start([*command, "-c", "test/fixtures/deploy_with_accessories.yml"]) }

View File

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

View File

@@ -1,15 +0,0 @@
require_relative "cli_test_case"
class CliBuildTest < CliTestCase
test "pull" do
run_command("pull").tap do |output|
assert_match /docker image rm --force dhh\/app:999/, output
assert_match /docker pull dhh\/app:999/, output
end
end
private
def run_command(*command)
stdouted { Mrsk::Cli::Build.start([*command, "-c", "test/fixtures/deploy_with_accessories.yml"]) }
end
end

View File

@@ -5,52 +5,4 @@ class CliMainTest < CliTestCase
version = stdouted { Mrsk::Cli::Main.new.version }
assert_equal Mrsk::VERSION, version
end
test "rollback bad version" do
run_command("details") # Preheat MRSK const
run_command("rollback", "nonsense").tap do |output|
assert_match /docker container ls --all --filter label=service=app --format '{{ .Names }}'/, output
assert_match /The app version 'nonsense' is not available as a container/, output
end
end
test "rollback good version" do
Mrsk::Cli::Main.any_instance.stubs(:container_name_available?).returns(true)
run_command("rollback", "123").tap do |output|
assert_match /Start version 123, then stop the old version/, output
assert_match /docker ps -q --filter label=service=app | xargs docker stop/, output
assert_match /docker start app-123/, output
end
end
test "remove with confirmation" do
run_command("remove", "-y").tap do |output|
assert_match /docker container stop traefik/, output
assert_match /docker container prune --force --filter label=org.opencontainers.image.title=Traefik/, output
assert_match /docker image prune --all --force --filter label=org.opencontainers.image.title=Traefik/, output
assert_match /docker ps --quiet --filter label=service=app | xargs docker stop/, output
assert_match /docker container prune --force --filter label=service=app/, output
assert_match /docker image prune --all --force --filter label=service=app/, output
assert_match /docker container stop app-mysql/, output
assert_match /docker container prune --force --filter label=service=app-mysql/, output
assert_match /docker image prune --all --force --filter label=service=app-mysql/, output
assert_match /rm -rf app-mysql/, output
assert_match /docker container stop app-redis/, output
assert_match /docker container prune --force --filter label=service=app-redis/, output
assert_match /docker image prune --all --force --filter label=service=app-redis/, output
assert_match /rm -rf app-redis/, output
assert_match /docker logout/, output
end
end
private
def run_command(*command)
stdouted { Mrsk::Cli::Main.start([*command, "-c", "test/fixtures/deploy_with_accessories.yml"]) }
end
end

View File

@@ -9,16 +9,6 @@ class CommanderTest < ActiveSupport::TestCase
assert_equal Mrsk::Configuration, @mrsk.config.class
end
test "commit hash as version" do
assert_equal `git rev-parse HEAD`.strip, @mrsk.config.version
end
test "commit hash as version but not in git" do
@mrsk.expects(:system).with("git rev-parse").returns(nil)
error = assert_raises(RuntimeError) { @mrsk.config }
assert_match /no git repository found/, error.message
end
test "overwriting hosts" do
assert_equal [ "1.1.1.1", "1.1.1.2", "1.1.1.3", "1.1.1.4" ], @mrsk.hosts

View File

@@ -49,48 +49,40 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
test "run" do
assert_equal \
"docker run --name app-mysql --detach --restart unless-stopped --log-opt max-size=10m --publish 3306:3306 -e MYSQL_ROOT_PASSWORD=\"secret123\" -e MYSQL_ROOT_HOST=\"%\" --label service=\"app-mysql\" mysql:8.0",
@mysql.run.join(" ")
[:docker, :run, "--name", "app-mysql", "-d", "--restart", "unless-stopped", "-p", "3306:3306", "-e", "MYSQL_ROOT_PASSWORD=secret123", "-e", "MYSQL_ROOT_HOST=%", "--label", "service=app-mysql", "mysql:8.0"], @mysql.run
assert_equal \
"docker run --name app-redis --detach --restart unless-stopped --log-opt max-size=10m --publish 6379:6379 -e SOMETHING=\"else\" --volume /var/lib/redis:/data --label service=\"app-redis\" --label cache=\"true\" redis:latest",
@redis.run.join(" ")
[:docker, :run, "--name", "app-redis", "-d", "--restart", "unless-stopped", "-p", "6379:6379", "-e", "SOMETHING=else", "--volume", "/var/lib/redis:/data", "--label", "service=app-redis", "--label", "cache=true", "redis:latest"], @redis.run
end
test "start" do
assert_equal \
"docker container start app-mysql",
@mysql.start.join(" ")
assert_equal [:docker, :container, :start, "app-mysql"], @mysql.start
end
test "stop" do
assert_equal \
"docker container stop app-mysql",
@mysql.stop.join(" ")
assert_equal [:docker, :container, :stop, "app-mysql"], @mysql.stop
end
test "info" do
assert_equal \
"docker ps --filter label=service=app-mysql",
@mysql.info.join(" ")
assert_equal [:docker, :ps, "--filter", "label=service=app-mysql"], @mysql.info
end
test "execute in new container" do
assert_equal \
"docker run --rm -e MYSQL_ROOT_PASSWORD=\"secret123\" -e MYSQL_ROOT_HOST=\"%\" mysql:8.0 mysql -u root",
@mysql.execute_in_new_container("mysql", "-u", "root").join(" ")
[ :docker, :run, "--rm", "-e", "MYSQL_ROOT_PASSWORD=secret123", "-e", "MYSQL_ROOT_HOST=%", "mysql:8.0", "mysql", "-u", "root" ],
@mysql.execute_in_new_container("mysql", "-u", "root")
end
test "execute in existing container" do
assert_equal \
"docker exec app-mysql mysql -u root",
@mysql.execute_in_existing_container("mysql", "-u", "root").join(" ")
[ :docker, :exec, "app-mysql", "mysql", "-u", "root" ],
@mysql.execute_in_existing_container("mysql", "-u", "root")
end
test "execute in new container over ssh" do
@mysql.stub(:run_over_ssh, ->(cmd) { cmd.join(" ") }) do
assert_match %r|docker run -it --rm -e MYSQL_ROOT_PASSWORD=\"secret123\" -e MYSQL_ROOT_HOST=\"%\" mysql:8.0 mysql -u root|,
assert_match %r|docker run -it --rm -e MYSQL_ROOT_PASSWORD=secret123 -e MYSQL_ROOT_HOST=% mysql:8.0 mysql -u root|,
@mysql.execute_in_new_container_over_ssh("mysql", "-u", "root")
end
end
@@ -105,30 +97,19 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
test "logs" do
assert_equal \
"docker logs app-mysql --timestamps 2>&1",
@mysql.logs.join(" ")
assert_equal \
"docker logs app-mysql --since 5m --tail 100 --timestamps 2>&1 | grep 'thing'",
@mysql.logs(since: "5m", lines: 100, grep: "thing").join(" ")
assert_equal [:docker, :logs, "app-mysql", "-t", "2>&1"], @mysql.logs
assert_equal [:docker, :logs, "app-mysql", " --since 5m", " -n 100", "-t", "2>&1", "|", "grep 'thing'"], @mysql.logs(since: "5m", lines: 100, grep: "thing")
end
test "follow logs" do
assert_equal \
"ssh -t root@1.1.1.5 'docker logs app-mysql --timestamps --tail 10 --follow 2>&1'",
@mysql.follow_logs
assert_equal "ssh -t root@1.1.1.5 'docker logs app-mysql -t -n 10 -f 2>&1'", @mysql.follow_logs
end
test "remove container" do
assert_equal \
"docker container prune --force --filter label=service=app-mysql",
@mysql.remove_container.join(" ")
assert_equal [:docker, :container, :prune, "-f", "--filter", "label=service=app-mysql"], @mysql.remove_container
end
test "remove image" do
assert_equal \
"docker image prune --all --force --filter label=service=app-mysql",
@mysql.remove_image.join(" ")
assert_equal [:docker, :image, :prune, "-a", "-f", "--filter", "label=service=app-mysql"], @mysql.remove_image
end
end

View File

@@ -14,7 +14,7 @@ class CommandsAppTest < ActiveSupport::TestCase
test "run" do
assert_equal \
"docker run --detach --restart unless-stopped --log-opt max-size=10m --name app-999 -e RAILS_MASTER_KEY=\"456\" --label service=\"app\" --label role=\"web\" --label traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.services.app.loadbalancer.healthcheck.path=\"/up\" --label traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\" --label traefik.http.middlewares.app.retry.attempts=\"5\" --label traefik.http.middlewares.app.retry.initialinterval=\"500ms\" dhh/app:999",
"docker run -d --restart unless-stopped --name app-999 -e RAILS_MASTER_KEY=456 --label service=app --label role=web --label traefik.http.routers.app.rule='PathPrefix(`/`)' --label traefik.http.services.app.loadbalancer.healthcheck.path=/up --label traefik.http.services.app.loadbalancer.healthcheck.interval=1s --label traefik.http.middlewares.app.retry.attempts=3 --label traefik.http.middlewares.app.retry.initialinterval=500ms dhh/app:999",
@app.run.join(" ")
end
@@ -22,15 +22,7 @@ class CommandsAppTest < ActiveSupport::TestCase
@config[:volumes] = ["/local/path:/container/path" ]
assert_equal \
"docker run --detach --restart unless-stopped --log-opt max-size=10m --name app-999 -e RAILS_MASTER_KEY=\"456\" --volume /local/path:/container/path --label service=\"app\" --label role=\"web\" --label traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.services.app.loadbalancer.healthcheck.path=\"/up\" --label traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\" --label traefik.http.middlewares.app.retry.attempts=\"5\" --label traefik.http.middlewares.app.retry.initialinterval=\"500ms\" dhh/app:999",
@app.run.join(" ")
end
test "run with custom healthcheck path" do
@config[:healthcheck] = { "path" => "/healthz" }
assert_equal \
"docker run --detach --restart unless-stopped --log-opt max-size=10m --name app-999 -e RAILS_MASTER_KEY=\"456\" --label service=\"app\" --label role=\"web\" --label traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.services.app.loadbalancer.healthcheck.path=\"/healthz\" --label traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\" --label traefik.http.middlewares.app.retry.attempts=\"5\" --label traefik.http.middlewares.app.retry.initialinterval=\"500ms\" dhh/app:999",
"docker run -d --restart unless-stopped --name app-999 -e RAILS_MASTER_KEY=456 --volume /local/path:/container/path --label service=app --label role=web --label traefik.http.routers.app.rule='PathPrefix(`/`)' --label traefik.http.services.app.loadbalancer.healthcheck.path=/up --label traefik.http.services.app.loadbalancer.healthcheck.interval=1s --label traefik.http.middlewares.app.retry.attempts=3 --label traefik.http.middlewares.app.retry.initialinterval=500ms dhh/app:999",
@app.run.join(" ")
end
@@ -42,7 +34,7 @@ class CommandsAppTest < ActiveSupport::TestCase
test "stop" do
assert_equal \
"docker ps --quiet --filter label=service=app | xargs docker stop",
"docker ps -q --filter label=service=app | xargs docker stop",
@app.stop.join(" ")
end
@@ -55,38 +47,38 @@ class CommandsAppTest < ActiveSupport::TestCase
test "logs" do
assert_equal \
"docker ps --quiet --filter label=service=app | xargs docker logs 2>&1",
"docker ps -q --filter label=service=app | xargs docker logs 2>&1",
@app.logs.join(" ")
assert_equal \
"docker ps --quiet --filter label=service=app | xargs docker logs --since 5m 2>&1",
"docker ps -q --filter label=service=app | xargs docker logs --since 5m 2>&1",
@app.logs(since: "5m").join(" ")
assert_equal \
"docker ps --quiet --filter label=service=app | xargs docker logs --tail 100 2>&1",
"docker ps -q --filter label=service=app | xargs docker logs -n 100 2>&1",
@app.logs(lines: "100").join(" ")
assert_equal \
"docker ps --quiet --filter label=service=app | xargs docker logs --since 5m --tail 100 2>&1",
"docker ps -q --filter label=service=app | xargs docker logs --since 5m -n 100 2>&1",
@app.logs(since: "5m", lines: "100").join(" ")
assert_equal \
"docker ps --quiet --filter label=service=app | xargs docker logs 2>&1 | grep 'my-id'",
"docker ps -q --filter label=service=app | xargs docker logs 2>&1 | grep 'my-id'",
@app.logs(grep: "my-id").join(" ")
assert_equal \
"docker ps --quiet --filter label=service=app | xargs docker logs --since 5m 2>&1 | grep 'my-id'",
"docker ps -q --filter label=service=app | xargs docker logs --since 5m 2>&1 | grep 'my-id'",
@app.logs(since: "5m", grep: "my-id").join(" ")
end
test "follow logs" do
@app.stub(:run_over_ssh, ->(cmd, host:) { cmd.join(" ") }) do
assert_equal \
"docker ps --quiet --filter label=service=app | xargs docker logs --timestamps --tail 10 --follow 2>&1",
"docker ps -q --filter label=service=app | xargs docker logs -t -n 10 -f 2>&1",
@app.follow_logs(host: "app-1")
assert_equal \
"docker ps --quiet --filter label=service=app | xargs docker logs --timestamps --tail 10 --follow 2>&1 | grep \"Completed\"",
"docker ps -q --filter label=service=app | xargs docker logs -t -n 10 -f 2>&1 | grep \"Completed\"",
@app.follow_logs(host: "app-1", grep: "Completed")
end
end
@@ -94,7 +86,7 @@ class CommandsAppTest < ActiveSupport::TestCase
test "execute in new container" do
assert_equal \
"docker run --rm -e RAILS_MASTER_KEY=\"456\" dhh/app:999 bin/rails db:setup",
"docker run --rm -e RAILS_MASTER_KEY=456 dhh/app:999 bin/rails db:setup",
@app.execute_in_new_container("bin/rails", "db:setup").join(" ")
end
@@ -106,7 +98,7 @@ class CommandsAppTest < ActiveSupport::TestCase
test "execute in new container over ssh" do
@app.stub(:run_over_ssh, ->(cmd, host:) { cmd.join(" ") }) do
assert_match %r|docker run -it --rm -e RAILS_MASTER_KEY=\"456\" dhh/app:999 bin/rails c|,
assert_match %r|docker run -it --rm -e RAILS_MASTER_KEY=456 dhh/app:999 bin/rails c|,
@app.execute_in_new_container_over_ssh("bin/rails", "c", host: "app-1")
end
end
@@ -145,13 +137,13 @@ class CommandsAppTest < ActiveSupport::TestCase
test "current_container_id" do
assert_equal \
"docker ps --quiet --filter label=service=app",
"docker ps -q --filter label=service=app",
@app.current_container_id.join(" ")
end
test "container_id_for" do
assert_equal \
"docker container ls --all --filter name=app-999 --quiet",
"docker container ls -a -f name=app-999 -q",
@app.container_id_for(container_name: "app-999").join(" ")
end

View File

@@ -1,27 +0,0 @@
require "test_helper"
class CommandsAuditorTest < ActiveSupport::TestCase
setup do
@config = {
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ],
audit_broadcast_cmd: "bin/audit_broadcast"
}
end
test "record" do
assert_match \
/echo '.* app removed container' >> mrsk-app-audit.log/,
new_command.record("app removed container").join(" ")
end
test "broadcast" do
assert_match \
/bin\/audit_broadcast '\[.*\] app removed container'/,
new_command.broadcast("app removed container").join(" ")
end
private
def new_command
Mrsk::Commands::Auditor.new(Mrsk::Configuration.new(@config, version: "123"))
end
end

View File

@@ -8,68 +8,50 @@ class CommandsBuilderTest < ActiveSupport::TestCase
test "target multiarch by default" do
builder = new_builder_command
assert_equal "multiarch", builder.name
assert_equal \
"docker buildx build --push --platform linux/amd64,linux/arm64 --builder mrsk-app-multiarch -t dhh/app:123 -t dhh/app:latest --label service=\"app\" .",
builder.push.join(" ")
assert_equal [:docker, :buildx, :build, "--push", "--platform", "linux/amd64,linux/arm64", "--builder", "mrsk-app-multiarch", "-t", "dhh/app:123", "."], builder.push
end
test "target native when multiarch is off" do
builder = new_builder_command(builder: { "multiarch" => false })
assert_equal "native", builder.name
assert_equal \
"docker build -t dhh/app:123 -t dhh/app:latest --label service=\"app\" . && docker push dhh/app:123",
builder.push.join(" ")
assert_equal [:docker, :build, "-t", "dhh/app:123", ".", "&&", :docker, :push, "dhh/app:123"], builder.push
end
test "target multiarch remote when local and remote is set" do
builder = new_builder_command(builder: { "local" => { }, "remote" => { } })
assert_equal "multiarch/remote", builder.name
assert_equal \
"docker buildx build --push --platform linux/amd64,linux/arm64 --builder mrsk-app-multiarch-remote -t dhh/app:123 -t dhh/app:latest --label service=\"app\" .",
builder.push.join(" ")
assert_equal [:docker, :buildx, :build, "--push", "--platform", "linux/amd64,linux/arm64", "--builder", "mrsk-app-multiarch-remote", "-t", "dhh/app:123", "."], builder.push
end
test "target native remote when only remote is set" do
builder = new_builder_command(builder: { "remote" => { "arch" => "amd64" } })
assert_equal "native/remote", builder.name
assert_equal \
"docker buildx build --push --platform linux/amd64 --builder mrsk-app-native-remote -t dhh/app:123 -t dhh/app:latest --label service=\"app\" .",
builder.push.join(" ")
assert_equal [:docker, :buildx, :build, "--push", "--platform", "linux/amd64", "--builder", "mrsk-app-native-remote", "-t", "dhh/app:123", "."], builder.push
end
test "build args" do
builder = new_builder_command(builder: { "args" => { "a" => 1, "b" => 2 } })
assert_equal \
"-t dhh/app:123 -t dhh/app:latest --label service=\"app\" --build-arg a=\"1\" --build-arg b=\"2\"",
builder.target.build_options.join(" ")
assert_equal [ "--build-arg", "a=1", "--build-arg", "b=2" ], builder.target.build_args
end
test "build secrets" do
builder = new_builder_command(builder: { "secrets" => ["token_a", "token_b"] })
assert_equal \
"-t dhh/app:123 -t dhh/app:latest --label service=\"app\" --secret id=\"token_a\" --secret id=\"token_b\"",
builder.target.build_options.join(" ")
assert_equal [ "--secret", "id=token_a", "--secret", "id=token_b" ], builder.target.build_secrets
end
test "native push with build args" do
builder = new_builder_command(builder: { "multiarch" => false, "args" => { "a" => 1, "b" => 2 } })
assert_equal \
"docker build -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --build-arg a=\"1\" --build-arg b=\"2\" . && docker push dhh/app:123",
builder.push.join(" ")
assert_equal [ :docker, :build, "-t", "--build-arg", "a=1", "--build-arg", "b=2", "dhh/app:123", ".", "&&", :docker, :push, "dhh/app:123" ], builder.push
end
test "multiarch push with build args" do
builder = new_builder_command(builder: { "args" => { "a" => 1, "b" => 2 } })
assert_equal \
"docker buildx build --push --platform linux/amd64,linux/arm64 --builder mrsk-app-multiarch -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --build-arg a=\"1\" --build-arg b=\"2\" .",
builder.push.join(" ")
assert_equal [ :docker, :buildx, :build, "--push", "--platform", "linux/amd64,linux/arm64", "--builder", "mrsk-app-multiarch", "-t", "dhh/app:123", "--build-arg", "a=1", "--build-arg", "b=2", "." ], builder.push
end
test "native push with 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\" . && docker push dhh/app:123",
builder.push.join(" ")
assert_equal [ :docker, :build, "-t", "--secret", "id=a", "--secret", "id=b", "dhh/app:123", ".", "&&", :docker, :push, "dhh/app:123" ], builder.push
end
private

View File

@@ -1,55 +0,0 @@
require "test_helper"
class CommandsHealthcheckTest < ActiveSupport::TestCase
setup do
@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" } }
}
end
test "run" do
assert_equal \
"docker run --detach --name healthcheck-app-123 --publish 3999:3000 --label service=healthcheck-app dhh/app:123",
new_command.run.join(" ")
end
test "run with custom port" do
@config[:healthcheck] = { "port" => 3001 }
assert_equal \
"docker run --detach --name healthcheck-app-123 --publish 3999:3001 --label service=healthcheck-app dhh/app:123",
new_command.run.join(" ")
end
test "curl" do
assert_equal \
"curl --silent --output /dev/null --write-out '%{http_code}' --max-time 2 http://localhost:3999/up",
new_command.curl.join(" ")
end
test "curl with custom path" do
@config[:healthcheck] = { "path" => "/healthz" }
assert_equal \
"curl --silent --output /dev/null --write-out '%{http_code}' --max-time 2 http://localhost:3999/healthz",
new_command.curl.join(" ")
end
test "stop" do
assert_equal \
"docker container ls --all --filter name=healthcheck-app --quiet | xargs docker stop",
new_command.stop.join(" ")
end
test "remove" do
assert_equal \
"docker container ls --all --filter name=healthcheck-app --quiet | xargs docker container rm",
new_command.remove.join(" ")
end
private
def new_command
Mrsk::Commands::Healthcheck.new(Mrsk::Configuration.new(@config, version: "123"))
end
end

View File

@@ -1,27 +0,0 @@
require "test_helper"
class CommandsPruneTest < ActiveSupport::TestCase
setup do
@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" } }
}
end
test "images" do
assert_equal \
"docker image prune --all --force --filter label=service=app --filter until=168h",
new_command.images.join(" ")
end
test "containers" do
assert_equal \
"docker container prune --force --filter label=service=app --filter until=72h",
new_command.containers.join(" ")
end
private
def new_command
Mrsk::Commands::Prune.new(Mrsk::Configuration.new(@config, version: "123"))
end
end

View File

@@ -14,25 +14,10 @@ class CommandsRegistryTest < ActiveSupport::TestCase
end
test "registry login" do
assert_equal \
"docker login hub.docker.com -u dhh -p secret",
@registry.login.join(" ")
end
test "registry login with ENV password" do
ENV["MRSK_REGISTRY_PASSWORD"] = "more-secret"
@config[:registry]["password"] = [ "MRSK_REGISTRY_PASSWORD" ]
assert_equal \
"docker login hub.docker.com -u dhh -p more-secret",
@registry.login.join(" ")
ensure
ENV.delete("MRSK_REGISTRY_PASSWORD")
assert_equal [ :docker, :login, "hub.docker.com", "-u", "dhh", "-p", "secret" ], @registry.login
end
test "registry logout" do
assert_equal \
"docker logout hub.docker.com",
@registry.logout.join(" ")
assert_equal [:docker, :logout, "hub.docker.com"], @registry.logout
end
end

View File

@@ -10,74 +10,63 @@ class CommandsTraefikTest < ActiveSupport::TestCase
test "run" do
assert_equal \
"docker run --name traefik --detach --restart unless-stopped --log-opt max-size=10m --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock traefik --providers.docker --log.level=DEBUG --accesslog.format json --metrics.prometheus.buckets 0.1,0.3,1.2,5.0",
new_command.run.join(" ")
[:docker, :run, "--name traefik", "-d", "--restart unless-stopped", "-p 80:80", "-v /var/run/docker.sock:/var/run/docker.sock", "traefik", "--providers.docker", "--log.level=DEBUG", "--accesslog.format", "json", "--metrics.prometheus.buckets", "0.1,0.3,1.2,5.0"],
new_command.run
end
test "traefik start" do
assert_equal \
"docker container start traefik",
new_command.start.join(" ")
[:docker, :container, :start, 'traefik'], new_command.start
end
test "traefik stop" do
assert_equal \
"docker container stop traefik",
new_command.stop.join(" ")
[:docker, :container, :stop, 'traefik'], new_command.stop
end
test "traefik info" do
assert_equal \
"docker ps --filter name=traefik",
new_command.info.join(" ")
[:docker, :ps, '--filter', 'name=traefik'], new_command.info
end
test "traefik logs" do
assert_equal \
"docker logs traefik --timestamps 2>&1",
new_command.logs.join(" ")
[:docker, :logs, 'traefik', '-t', '2>&1'], new_command.logs
end
test "traefik logs since 2h" do
assert_equal \
"docker logs traefik --since 2h --timestamps 2>&1",
new_command.logs(since: '2h').join(" ")
[:docker, :logs, 'traefik', ' --since 2h', '-t', '2>&1'], new_command.logs(since: '2h')
end
test "traefik logs last 10 lines" do
assert_equal \
"docker logs traefik --tail 10 --timestamps 2>&1",
new_command.logs(lines: 10).join(" ")
[:docker, :logs, 'traefik', ' -n 10', '-t', '2>&1'], new_command.logs(lines: 10)
end
test "traefik logs with grep hello!" do
assert_equal \
"docker logs traefik --timestamps 2>&1 | grep 'hello!'",
new_command.logs(grep: 'hello!').join(" ")
[:docker, :logs, 'traefik', '-t', '2>&1', "|", "grep 'hello!'"], new_command.logs(grep: 'hello!')
end
test "traefik remove container" do
assert_equal \
"docker container prune --force --filter label=org.opencontainers.image.title=Traefik",
new_command.remove_container.join(" ")
[:docker, :container, :prune, "-f", "--filter", "label=org.opencontainers.image.title=Traefik"], new_command.remove_container
end
test "traefik remove image" do
assert_equal \
"docker image prune --all --force --filter label=org.opencontainers.image.title=Traefik",
new_command.remove_image.join(" ")
[:docker, :image, :prune, "-a", "-f", "--filter", "label=org.opencontainers.image.title=Traefik"], new_command.remove_image
end
test "traefik follow logs" do
assert_equal \
"ssh -t root@1.1.1.1 'docker logs traefik --timestamps --tail 10 --follow 2>&1'",
new_command.follow_logs(host: @config[:servers].first)
"ssh -t root@1.1.1.1 'docker logs traefik -t -n 10 -f 2>&1'", new_command.follow_logs(host: @config[:servers].first)
end
test "traefik follow logs with grep hello!" do
assert_equal \
"ssh -t root@1.1.1.1 'docker logs traefik --timestamps --tail 10 --follow 2>&1 | grep \"hello!\"'",
new_command.follow_logs(host: @config[:servers].first, grep: 'hello!')
"ssh -t root@1.1.1.1 'docker logs traefik -t -n 10 -f 2>&1 | grep \"hello!\"'", new_command.follow_logs(host: @config[:servers].first, grep: 'hello!')
end
private

View File

@@ -72,20 +72,20 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase
end
test "label args" do
assert_equal ["--label", "service=\"app-mysql\""], @config.accessory(:mysql).label_args
assert_equal ["--label", "service=\"app-redis\"", "--label", "cache=\"true\""], @config.accessory(:redis).label_args
assert_equal ["--label", "service=app-mysql"], @config.accessory(:mysql).label_args
assert_equal ["--label", "service=app-redis", "--label", "cache=true"], @config.accessory(:redis).label_args
end
test "env args with secret" do
ENV["MYSQL_ROOT_PASSWORD"] = "secret123"
assert_equal ["-e", "MYSQL_ROOT_PASSWORD=\"secret123\"", "-e", "MYSQL_ROOT_HOST=\"%\""], @config.accessory(:mysql).env_args
assert_equal ["-e", "MYSQL_ROOT_PASSWORD=secret123", "-e", "MYSQL_ROOT_HOST=%"], @config.accessory(:mysql).env_args
assert @config.accessory(:mysql).env_args[1].is_a?(SSHKit::Redaction)
ensure
ENV["MYSQL_ROOT_PASSWORD"] = nil
end
test "env args without secret" do
assert_equal ["-e", "SOMETHING=\"else\""], @config.accessory(:redis).env_args
assert_equal ["-e", "SOMETHING=else"], @config.accessory(:redis).env_args
end
test "volume args" do

View File

@@ -38,11 +38,11 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
end
test "label args" do
assert_equal [ "--label", "service=\"app\"", "--label", "role=\"workers\"" ], @config_with_roles.role(:workers).label_args
assert_equal [ "--label", "service=app", "--label", "role=workers" ], @config_with_roles.role(:workers).label_args
end
test "special label args for web" do
assert_equal [ "--label", "service=\"app\"", "--label", "role=\"web\"", "--label", "traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\"", "--label", "traefik.http.services.app.loadbalancer.healthcheck.path=\"/up\"", "--label", "traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\"", "--label", "traefik.http.middlewares.app.retry.attempts=\"5\"", "--label", "traefik.http.middlewares.app.retry.initialinterval=\"500ms\""], @config.role(:web).label_args
assert_equal [ "--label", "service=app", "--label", "role=web", "--label", "traefik.http.routers.app.rule='PathPrefix(`/`)'", "--label", "traefik.http.services.app.loadbalancer.healthcheck.path=/up", "--label", "traefik.http.services.app.loadbalancer.healthcheck.interval=1s", "--label", "traefik.http.middlewares.app.retry.attempts=3", "--label", "traefik.http.middlewares.app.retry.initialinterval=500ms"], @config.role(:web).label_args
end
test "custom labels" do
@@ -57,8 +57,8 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
end
test "overwriting default traefik label" do
@deploy[:labels] = { "traefik.http.routers.app.rule" => "\"Host(\\`example.com\\`) || (Host(\\`example.org\\`) && Path(\\`/traefik\\`))\"" }
assert_equal "\"Host(\\`example.com\\`) || (Host(\\`example.org\\`) && Path(\\`/traefik\\`))\"", @config.role(:web).labels["traefik.http.routers.app.rule"]
@deploy[:labels] = { "traefik.http.routers.app.rule" => "'Host(`example.com`) || (Host(`example.org`) && Path(`/traefik`))'" }
assert_equal "'Host(`example.com`) || (Host(`example.org`) && Path(`/traefik`))'", @config.role(:web).labels["traefik.http.routers.app.rule"]
end
test "default traefik label on non-web role" do
@@ -66,12 +66,12 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
c[:servers]["beta"] = { "traefik" => "true", "hosts" => [ "1.1.1.5" ] }
})
assert_equal [ "--label", "service=\"app\"", "--label", "role=\"beta\"", "--label", "traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\"", "--label", "traefik.http.services.app.loadbalancer.healthcheck.path=\"/up\"", "--label", "traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\"", "--label", "traefik.http.middlewares.app.retry.attempts=\"5\"", "--label", "traefik.http.middlewares.app.retry.initialinterval=\"500ms\"" ], config.role(:beta).label_args
assert_equal [ "--label", "service=app", "--label", "role=beta", "--label", "traefik.http.routers.app.rule='PathPrefix(`/`)'", "--label", "traefik.http.services.app.loadbalancer.healthcheck.path=/up", "--label", "traefik.http.services.app.loadbalancer.healthcheck.interval=1s", "--label", "traefik.http.middlewares.app.retry.attempts=3", "--label", "traefik.http.middlewares.app.retry.initialinterval=500ms" ], config.role(:beta).label_args
end
test "env overwritten by role" do
assert_equal "redis://a/b", @config_with_roles.role(:workers).env["REDIS_URL"]
assert_equal ["-e", "REDIS_URL=\"redis://a/b\"", "-e", "WEB_CONCURRENCY=\"4\""], @config_with_roles.role(:workers).env_args
assert_equal ["-e", "REDIS_URL=redis://a/b", "-e", "WEB_CONCURRENCY=4"], @config_with_roles.role(:workers).env_args
end
test "env secret overwritten by role" do
@@ -95,9 +95,9 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
}
ENV["REDIS_PASSWORD"] = "secret456"
ENV["DB_PASSWORD"] = "secret&\"123"
ENV["DB_PASSWORD"] = "secret123"
assert_equal ["-e", "REDIS_PASSWORD=\"secret456\"", "-e", "DB_PASSWORD=\"secret&\\\"123\"", "-e", "REDIS_URL=\"redis://a/b\"", "-e", "WEB_CONCURRENCY=\"4\""], @config_with_roles.role(:workers).env_args
assert_equal ["-e", "REDIS_PASSWORD=secret456", "-e", "DB_PASSWORD=secret123", "-e", "REDIS_URL=redis://a/b", "-e", "WEB_CONCURRENCY=4"], @config_with_roles.role(:workers).env_args
ensure
ENV["REDIS_PASSWORD"] = nil
ENV["DB_PASSWORD"] = nil
@@ -116,7 +116,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
ENV["DB_PASSWORD"] = "secret123"
assert_equal ["-e", "DB_PASSWORD=\"secret123\"", "-e", "REDIS_URL=\"redis://a/b\"", "-e", "WEB_CONCURRENCY=\"4\""], @config_with_roles.role(:workers).env_args
assert_equal ["-e", "DB_PASSWORD=secret123", "-e", "REDIS_URL=redis://a/b", "-e", "WEB_CONCURRENCY=4"], @config_with_roles.role(:workers).env_args
ensure
ENV["DB_PASSWORD"] = nil
end
@@ -133,7 +133,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
ENV["REDIS_PASSWORD"] = "secret456"
assert_equal ["-e", "REDIS_PASSWORD=\"secret456\"", "-e", "REDIS_URL=\"redis://a/b\"", "-e", "WEB_CONCURRENCY=\"4\""], @config_with_roles.role(:workers).env_args
assert_equal ["-e", "REDIS_PASSWORD=secret456", "-e", "REDIS_URL=redis://a/b", "-e", "WEB_CONCURRENCY=4"], @config_with_roles.role(:workers).env_args
ensure
ENV["REDIS_PASSWORD"] = nil
end

View File

@@ -89,7 +89,7 @@ class ConfigurationTest < ActiveSupport::TestCase
end
test "env args" do
assert_equal [ "-e", "REDIS_URL=\"redis://x/y\"" ], @config.env_args
assert_equal [ "-e", "REDIS_URL=redis://x/y" ], @config.env_args
end
test "env args with clear and secrets" do
@@ -98,7 +98,7 @@ class ConfigurationTest < ActiveSupport::TestCase
env: { "clear" => { "PORT" => "3000" }, "secret" => [ "PASSWORD" ] }
}) })
assert_equal [ "-e", "PASSWORD=\"secret123\"", "-e", "PORT=\"3000\"" ], config.env_args
assert_equal [ "-e", "PASSWORD=secret123", "-e", "PORT=3000" ], config.env_args
assert config.env_args[1].is_a?(SSHKit::Redaction)
ensure
ENV["PASSWORD"] = nil
@@ -109,7 +109,7 @@ class ConfigurationTest < ActiveSupport::TestCase
env: { "clear" => { "PORT" => "3000" } }
}) })
assert_equal [ "-e", "PORT=\"3000\"" ], config.env_args
assert_equal [ "-e", "PORT=3000" ], config.env_args
end
test "env args with only secrets" do
@@ -118,7 +118,7 @@ class ConfigurationTest < ActiveSupport::TestCase
env: { "secret" => [ "PASSWORD" ] }
}) })
assert_equal [ "-e", "PASSWORD=\"secret123\"" ], config.env_args
assert_equal [ "-e", "PASSWORD=secret123" ], config.env_args
assert config.env_args[1].is_a?(SSHKit::Redaction)
ensure
ENV["PASSWORD"] = nil
@@ -181,6 +181,6 @@ class ConfigurationTest < ActiveSupport::TestCase
end
test "to_h" do
assert_equal({ :roles=>["web"], :hosts=>["1.1.1.1", "1.1.1.2"], :primary_host=>"1.1.1.1", :version=>"missing", :repository=>"dhh/app", :absolute_image=>"dhh/app:missing", :service_with_version=>"app-missing", :env_args=>["-e", "REDIS_URL=\"redis://x/y\""], :ssh_options=>{:user=>"root", :auth_methods=>["publickey"]}, :volume_args=>["--volume", "/local/path:/container/path"], :healthcheck=>{"path"=>"/up", "port"=>3000 }}, @config.to_h)
assert_equal({ :roles=>["web"], :hosts=>["1.1.1.1", "1.1.1.2"], :primary_host=>"1.1.1.1", :version=>"missing", :repository=>"dhh/app", :absolute_image=>"dhh/app:missing", :service_with_version=>"app-missing", :env_args=>["-e", "REDIS_URL=redis://x/y"], :ssh_options=>{:user=>"root", :auth_methods=>["publickey"]}, :volume_args=>["--volume", "/local/path:/container/path"] }, @config.to_h)
end
end

View File

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