Compare commits

..

71 Commits

Author SHA1 Message Date
David Heinemeier Hansson
c47de8246b Bump version for 0.8.1 2023-02-20 18:20:41 +01:00
David Heinemeier Hansson
1fccaf60b2 Cleanup escaping logic 2023-02-20 18:20:08 +01:00
David Heinemeier Hansson
9b02a7668d Merge branch 'main' into pr/53
* main:
  Bump version for 0.8.0
  Remove images of the same name before pulling a new one
  Changed to a timeout
  Better language
  Switch to ruby-based retry
2023-02-20 18:14:47 +01:00
David Heinemeier Hansson
f6ea287e66 Bump version for 0.8.0 2023-02-20 18:06:56 +01:00
David Heinemeier Hansson
42b343436d Remove images of the same name before pulling a new one
Or you'll end up with untagged dupes.
2023-02-20 18:06:16 +01:00
David Heinemeier Hansson
9d6ccf9889 Changed to a timeout 2023-02-20 17:59:41 +01:00
David Heinemeier Hansson
c4cc9e690b Better language 2023-02-20 17:44:55 +01:00
David Heinemeier Hansson
1ccf679ca9 Switch to ruby-based retry
Retry connection errors with backoff
2023-02-20 17:42:55 +01:00
Paul Gabriel
f81ba12aa5 fix(escape): Escape double quotes and all other characters reliably 2023-02-20 16:49:47 +01:00
Paul Gabriel
25e8b91569 fix(escape-cli-args): Always use quotes to escape CLI arguments 2023-02-20 15:02:34 +01:00
Paul Gabriel
21c6a1f1ba chore(rebase): Rebase main 2023-02-20 10:27:51 +01:00
David Heinemeier Hansson
5898fdd8f4 Expand arguments to be more self-explanatory in logs 2023-02-19 18:11:06 +01:00
David Heinemeier Hansson
5299826146 Alphabetical order 2023-02-19 17:43:56 +01:00
David Heinemeier Hansson
28be8dc0f0 Encourage registry password from ENV 2023-02-19 17:42:30 +01:00
David Heinemeier Hansson
2ed3ccc53e More readable tests 2023-02-19 17:40:41 +01:00
David Heinemeier Hansson
11c726858d Point to where secrets are from 2023-02-19 17:34:49 +01:00
David Heinemeier Hansson
8706fae2b5 Reveal all options in default config 2023-02-19 17:34:06 +01:00
David Heinemeier Hansson
67d6c3acfe Think we can drop this
Now that we rescue at the top level
2023-02-19 17:33:54 +01:00
David Heinemeier Hansson
a5fd4c76ba No need for invocation 2023-02-19 17:22:03 +01:00
David Heinemeier Hansson
f3a5845501 Remember this 2023-02-19 17:16:14 +01:00
David Heinemeier Hansson
5356f31e2e Remove also removes accessories but requires confirmation 2023-02-19 17:16:14 +01:00
David Heinemeier Hansson
67cb89b9b9 Remove requires confirmation 2023-02-19 17:16:06 +01:00
David Heinemeier Hansson
745b09051e Test app remove 2023-02-19 17:15:57 +01:00
David Heinemeier Hansson
0fa70f4688 Stop app before removing it 2023-02-19 17:15:57 +01:00
David Heinemeier Hansson
6bc2def677 No need for invoke
No double action possible
2023-02-19 17:15:57 +01:00
David Heinemeier Hansson
42bc691758 CLI doc updates
Match word

Language

Suggest what accessories are

There are also accessories

Default already shown

Better example

Warn about secrets being shown

Now also accessories

Wording

Clarifications

Clarify how to see options

General option for all

Options important here too

Hide subcommands

Implied

Simpler as just version

Be concise

Missing word

Wordsmith

Simpler and uniform words are better

Clarify what exactly we're manipulating

Wordsmithing

Implicit

Simpler language

Hide subcommands

Clarify its container management

Just one per server

Simpler
2023-02-19 17:15:44 +01:00
David Heinemeier Hansson
e5c4cb0344 Retry healthcheck for up to 10 seconds (in case container wasnt ready) 2023-02-19 15:34:36 +01:00
David Heinemeier Hansson
a0d71f3fe4 Protect against missing current version 2023-02-19 09:48:35 +01:00
David Heinemeier Hansson
389ce2f701 Only output if there's a failure 2023-02-19 09:36:04 +01:00
David Heinemeier Hansson
8e918b1906 Output logs when healthcheck fails 2023-02-19 09:33:49 +01:00
David Heinemeier Hansson
e37e5f7d09 Bump version for 0.7.2 2023-02-18 18:23:28 +01:00
David Heinemeier Hansson
7f1191bf59 Change broadcast cmd to just take an argument instead of STDIN
Simpler
2023-02-18 18:22:46 +01:00
David Heinemeier Hansson
0c03216fdf Bump version for 0.7.1 2023-02-18 16:33:28 +01:00
David Heinemeier Hansson
1973f55c58 Don't include recorded_at with broadcast line
Receiving end will already add that
2023-02-18 16:33:12 +01:00
David Heinemeier Hansson
0a51cd0899 Update for healthcheck config 2023-02-18 16:28:31 +01:00
David Heinemeier Hansson
4b0a8728f1 Bump version for 0.7.0 2023-02-18 16:27:08 +01:00
David Heinemeier Hansson
3075f8daf1 Include healthcheck in config 2023-02-18 16:26:23 +01:00
David Heinemeier Hansson
9985834bd6 Use number 2023-02-18 16:26:17 +01:00
David Heinemeier Hansson
94b4461c76 Merge pull request #52 from mrsked/health-check-with-deploy
Add healthcheck before deploy
2023-02-18 16:24:41 +01:00
David Heinemeier Hansson
7afa9e0815 Mention healthcheck as part of steps instead 2023-02-18 16:23:46 +01:00
David Heinemeier Hansson
933ece35ab Add healthcheck before deploy 2023-02-18 16:22:08 +01:00
David Heinemeier Hansson
2f80b300f0 Test rolling back to a good version too 2023-02-18 14:55:11 +01:00
David Heinemeier Hansson
2e06bf59a4 Protect against rolling back to a bad version 2023-02-18 14:33:47 +01:00
David Heinemeier Hansson
854795c2b6 Wording 2023-02-18 12:10:42 +01:00
David Heinemeier Hansson
4fe7fb705a Use same sentence style as broadcasts for audit log lines 2023-02-18 12:00:15 +01:00
David Heinemeier Hansson
270e0d0e2c Merge pull request #50 from pagbrl/labels-traefik-docs
docs(traefik-labels): Improve docs for traefik labels formatting
2023-02-18 11:42:43 +01:00
David Heinemeier Hansson
6ddc9cf017 Merge pull request #51 from mrsked/audit-broadcasts
Add audit broadcasts
2023-02-18 11:41:19 +01:00
David Heinemeier Hansson
2dcd76b2de Merge branch 'main' into audit-broadcasts
* main:
  Remove unnecessary audit recordings
2023-02-18 11:38:34 +01:00
David Heinemeier Hansson
a6eabd0b67 Remove unnecessary audit recordings 2023-02-18 11:36:52 +01:00
David Heinemeier Hansson
fb9357b5ba Add audit broadcasts 2023-02-18 11:36:30 +01:00
Paul Gabriel
d484cfcc31 docs(traefik-labels): Improve docs for traefik labels formatting 2023-02-18 00:25:30 +01:00
David Heinemeier Hansson
5c93642f2a Prepare for custom pruning 2023-02-15 20:34:08 +01:00
David Heinemeier Hansson
8ff206ba7e Highlight 2023-02-15 18:08:46 +01:00
David Heinemeier Hansson
e36a5e111c Make a note about the /up requirement 2023-02-15 18:08:26 +01:00
David Heinemeier Hansson
72522001e5 Merge pull request #46 from fschueller/fix-prune-desc
Adjust CLI description for prune command to mention 7 days
2023-02-15 14:09:06 +01:00
David Heinemeier Hansson
50c4bb83cb Bump version for 0.6.4 2023-02-15 13:48:10 +01:00
David Heinemeier Hansson
b2875ad056 More readable tests 2023-02-15 13:47:16 +01:00
David Heinemeier Hansson
8ec94f105c Tag images with service label so we can prune exclusively 2023-02-15 13:41:03 +01:00
David Heinemeier Hansson
90f4212a68 Stray copypasta 2023-02-15 13:39:53 +01:00
David Heinemeier Hansson
648894f9a9 No need for quoting 2023-02-15 13:32:59 +01:00
David Heinemeier Hansson
dc68639dfa Prune all unused images matching time filter 2023-02-15 13:32:50 +01:00
David Heinemeier Hansson
244cf8b3b7 Add prune command test 2023-02-15 13:30:31 +01:00
David Heinemeier Hansson
f25f506d77 Don't use abbreviations when we don't have to 2023-02-15 13:26:57 +01:00
David Heinemeier Hansson
c29a177a7a DRY the use of build options into one call 2023-02-15 13:23:14 +01:00
Farah Schüller
03328a998c Adjust CLI description for prune command to mention 7 days 2023-02-14 17:05:36 +01:00
David Heinemeier Hansson
ec5fad5bea Describe the vision 2023-02-11 14:30:23 +01:00
David Heinemeier Hansson
c671acf68f Bump version for 0.6.3 2023-02-11 13:10:47 +01:00
David Heinemeier Hansson
4f2cb5e184 Shorter 2023-02-11 13:00:22 +01:00
David Heinemeier Hansson
63a065237a Ensure .env file is only accessible to user 2023-02-11 12:56:57 +01:00
David Heinemeier Hansson
0f4e1888d9 Just delete the full cache directory, it isnt needed 2023-02-10 14:35:11 +01:00
David Heinemeier Hansson
d4d3308c34 Need to use args 2023-02-09 21:50:57 +01:00
47 changed files with 838 additions and 296 deletions

View File

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

View File

@@ -1,7 +1,7 @@
PATH
remote: .
specs:
mrsk (0.6.2)
mrsk (0.8.1)
activesupport (>= 7.0)
dotenv (~> 2.8)
sshkit (~> 1.21)
@@ -29,6 +29,7 @@ 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)
@@ -36,6 +37,7 @@ 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)
@@ -96,7 +98,9 @@ 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.
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.
## Installation
@@ -14,13 +14,14 @@ servers:
- 192.168.0.2
registry:
username: registry-user-name
password: <%= ENV.fetch("MRSK_REGISTRY_PASSWORD") %>
password:
- MRSK_REGISTRY_PASSWORD
env:
secret:
- RAILS_MASTER_KEY
```
Then edit your `.env` file to add your registry password as `MRSK_REGISTRY_PASSWORD` (and your `RAILS_MASTER_KEY` for production with a Rails app).
Then edit your `.env` file to add your registry password as `MRSK_REGISTRY_PASSWORD` (and your `RAILS_MASTER_KEY` for production with a Rails app).
Now you're ready to deploy to the servers:
@@ -37,12 +38,23 @@ 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. 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.
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.
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?
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.
@@ -173,10 +185,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 extra quotes are needed to ensure the rule is passed in correctly!
Note: The escaped backticks are needed to ensure the rule is passed in correctly and not treated as command substitution by Bash!
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.
@@ -253,11 +265,11 @@ This build secret can then be referenced in the Dockerfile:
# Copy Gemfiles
COPY Gemfile Gemfile.lock ./
# Install dependencies, including private repositories via access token (then remove git configs with exposed GITHUB_TOKEN)
# Install dependencies, including private repositories via access token (then remove bundle cache with exposed GITHUB_TOKEN)
RUN --mount=type=secret,id=GITHUB_TOKEN \
BUNDLE_GITHUB__COM=x-access-token:$(cat /run/secrets/GITHUB_TOKEN) \
bundle install && \
find /usr/local/bundle/cache/bundler/git -name "config" -delete
rm -rf /usr/local/bundle/cache
```
### Using command arguments for Traefik
@@ -266,12 +278,13 @@ You can customize the traefik command line:
```yaml
traefik:
accesslog: true
accesslog.format: json
metrics.prometheus: true
metrics.prometheus.buckets: 0.1,0.3,1.2,5.0
args:
accesslog: true
accesslog.format: json
```
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:
@@ -334,6 +347,40 @@ 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
@@ -401,7 +448,7 @@ mrsk app exec -i 'bin/rails console'
```
### Running details to see state of containers
### Running details to show state of containers
You can see the state of your servers by running `mrsk details`:

View File

@@ -1,5 +1,5 @@
class Mrsk::Cli::Accessory < Mrsk::Cli::Base
desc "boot [NAME]", "Boot accessory service on host (use NAME=all to boot all accessories)"
desc "boot [NAME]", "Boot new 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("accessory #{name} boot"), verbosity: :debug
execute *MRSK.auditor.record("Booted #{name} accessory"), verbosity: :debug
execute *accessory.run
end
audit_broadcast "Booted accessory #{name}"
end
end
end
desc "upload [NAME]", "Upload accessory files to host"
desc "upload [NAME]", "Upload accessory files to host", hide: true
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,12 +33,10 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
end
end
desc "directories [NAME]", "Create accessory directories on host"
desc "directories [NAME]", "Create accessory directories on host", hide: true
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
@@ -46,7 +44,7 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
end
end
desc "reboot [NAME]", "Reboot accessory on host (stop container, remove container, start new container)"
desc "reboot [NAME]", "Reboot existing accessory on host (stop container, remove container, start new container)"
def reboot(name)
with_accessory(name) do |accessory|
stop(name)
@@ -55,27 +53,27 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
end
end
desc "start [NAME]", "Start existing accessory on host"
desc "start [NAME]", "Start existing accessory container on host"
def start(name)
with_accessory(name) do |accessory|
on(accessory.host) do
execute *MRSK.auditor.record("accessory #{name} start"), verbosity: :debug
execute *MRSK.auditor.record("Started #{name} accessory"), verbosity: :debug
execute *accessory.start
end
end
end
desc "stop [NAME]", "Stop accessory on host"
desc "stop [NAME]", "Stop existing accessory container on host"
def stop(name)
with_accessory(name) do |accessory|
on(accessory.host) do
execute *MRSK.auditor.record("accessory #{name} stop"), verbosity: :debug
execute *MRSK.auditor.record("Stopped #{name} accessory"), verbosity: :debug
execute *accessory.stop, raise_on_non_zero_exit: false
end
end
end
desc "restart [NAME]", "Restart accessory on host"
desc "restart [NAME]", "Restart existing accessory container on host"
def restart(name)
with_accessory(name) do
stop(name)
@@ -83,7 +81,7 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
end
end
desc "details [NAME]", "Display details about accessory on host (use NAME=all to boot all accessories)"
desc "details [NAME]", "Show details about accessory on host (use NAME=all to show all accessories)"
def details(name)
if name == "all"
MRSK.accessory_names.each { |accessory_name| details(accessory_name) }
@@ -94,7 +92,7 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
end
end
desc "exec [NAME] [CMD]", "Execute a custom command on servers"
desc "exec [NAME] [CMD]", "Execute a custom command on servers (use --help to show options)"
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)
@@ -111,21 +109,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("accessory #{name} cmd '#{cmd}'"), verbosity: :debug
execute *MRSK.auditor.record("Executed cmd '#{cmd}' on #{name} accessory"), 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("accessory #{name} cmd '#{cmd}'"), verbosity: :debug
execute *MRSK.auditor.record("Executed cmd '#{cmd}' on #{name} accessory"), 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"
desc "logs [NAME]", "Show log lines from accessory on host (use --help to show options)"
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)"
@@ -151,45 +149,49 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
end
end
desc "remove [NAME]", "Remove accessory container and image from host (use NAME=all to boot all accessories)"
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"
def remove(name)
if name == "all"
MRSK.accessory_names.each { |accessory_name| remove(accessory_name) }
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
with_accessory(name) do
stop(name)
remove_container(name)
remove_image(name)
remove_service_directory(name)
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)
remove_image(name)
remove_service_directory(name)
end
end
end
end
desc "remove_container [NAME]", "Remove accessory container from host"
desc "remove_container [NAME]", "Remove accessory container from host", hide: true
def remove_container(name)
with_accessory(name) do |accessory|
on(accessory.host) do
execute *MRSK.auditor.record("accessory #{name} remove container"), verbosity: :debug
execute *MRSK.auditor.record("Remove #{name} accessory container"), verbosity: :debug
execute *accessory.remove_container
end
end
end
desc "remove_image [NAME]", "Remove accessory image from host"
desc "remove_image [NAME]", "Remove accessory image from host", hide: true
def remove_image(name)
with_accessory(name) do |accessory|
on(accessory.host) do
execute *MRSK.auditor.record("accessory #{name} remove image"), verbosity: :debug
execute *MRSK.auditor.record("Removed #{name} accessory 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"
desc "remove_service_directory [NAME]", "Remove accessory directory used for uploaded files and data directories from host", hide: true
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

@@ -7,7 +7,7 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
MRSK.config.roles.each do |role|
on(role.hosts) do |host|
execute *MRSK.auditor.record("app boot version #{version}"), verbosity: :debug
execute *MRSK.auditor.record("Booted app version #{version}"), verbosity: :debug
begin
execute *MRSK.app.stop, raise_on_non_zero_exit: false
@@ -15,7 +15,7 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
rescue SSHKit::Command::Failed => e
if e.message =~ /already in use/
error "Rebooting container with same version already deployed on #{host}"
execute *MRSK.auditor.record("app rebooted with version #{version}"), verbosity: :debug
execute *MRSK.auditor.record("Rebooted app version #{version}"), verbosity: :debug
execute *MRSK.app.remove_container(version: version)
execute *MRSK.app.run(role: role.name)
@@ -28,28 +28,29 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
end
end
desc "start", "Start existing app on servers (use --version=<git-hash> to designate specific version)"
desc "start", "Start existing app container on servers"
def start
on(MRSK.hosts) do
execute *MRSK.auditor.record("app start version #{MRSK.version}"), verbosity: :debug
execute *MRSK.auditor.record("Started app version #{MRSK.version}"), verbosity: :debug
execute *MRSK.app.start, raise_on_non_zero_exit: false
end
end
desc "stop", "Stop app on servers"
desc "stop", "Stop app container on servers"
def stop
on(MRSK.hosts) do
execute *MRSK.auditor.record("app stop"), verbosity: :debug
execute *MRSK.auditor.record("Stopped app"), verbosity: :debug
execute *MRSK.app.stop, raise_on_non_zero_exit: false
end
end
desc "details", "Display details about app containers"
# FIXME: Drop in favor of just containers?
desc "details", "Show 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"
desc "exec [CMD]", "Execute a custom command on servers (use --help to show options)"
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)
@@ -74,7 +75,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("app cmd '#{cmd}' with version #{version}"), verbosity: :debug
execute *MRSK.auditor.record("Executed cmd '#{cmd}' on app version #{version}"), verbosity: :debug
puts_by_host host, capture_with_info(*MRSK.app.execute_in_existing_container(cmd))
end
end
@@ -84,28 +85,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("app cmd '#{cmd}' with version #{version}"), verbosity: :debug
execute *MRSK.auditor.record("Executed cmd '#{cmd}' on app version #{version}"), verbosity: :debug
puts_by_host host, capture_with_info(*MRSK.app.execute_in_new_container(cmd))
end
end
end
end
desc "containers", "List all the app containers currently on servers"
desc "containers", "Show app containers on servers"
def containers
on(MRSK.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.list_containers) }
end
desc "images", "List all the app images currently on servers"
desc "images", "Show app images on servers"
def images
on(MRSK.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.list_images) }
end
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"
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"
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 logs on primary server (or specific host set by --hosts)"
option :follow, aliases: "-f", desc: "Follow log on primary server (or specific host set by --hosts)"
def logs
# FIXME: Catch when app containers aren't running
@@ -133,36 +134,37 @@ 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"
desc "remove_container [VERSION]", "Remove app container with given version from servers", hide: true
def remove_container(version)
on(MRSK.hosts) do
execute *MRSK.auditor.record("app remove container #{version}"), verbosity: :debug
execute *MRSK.auditor.record("Removed app container with version #{version}"), verbosity: :debug
execute *MRSK.app.remove_container(version: version)
end
end
desc "remove_containers", "Remove all app containers from servers"
desc "remove_containers", "Remove all app containers from servers", hide: true
def remove_containers
on(MRSK.hosts) do
execute *MRSK.auditor.record("app remove containers"), verbosity: :debug
execute *MRSK.auditor.record("Removed all app containers"), verbosity: :debug
execute *MRSK.app.remove_containers
end
end
desc "remove_images", "Remove all app images from servers"
desc "remove_images", "Remove all app images from servers", hide: true
def remove_images
on(MRSK.hosts) do
execute *MRSK.auditor.record("app remove images"), verbosity: :debug
execute *MRSK.auditor.record("Removed all app images"), verbosity: :debug
execute *MRSK.app.remove_images
end
end
desc "current_version", "Shows the version currently running"
def current_version
desc "version", "Show app version currently running on servers"
def version
on(MRSK.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.current_running_version).strip }
end
@@ -184,7 +186,12 @@ 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 }
version.presence
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)

View File

@@ -17,8 +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 (default: config/deploy.yml)"
class_option :destination, aliases: "-d", desc: "Specify destination to be used for config file (west -> deploy.west.yml)"
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)"
def initialize(*)
super
@@ -59,9 +59,14 @@ 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", "Deliver a newly built app image to servers"
desc "deliver", "Build app and push app image to registry then pull image on servers"
def deliver
invoke :push
invoke :pull
push
pull
end
desc "push", "Build locally and push app image to registry"
desc "push", "Build and push app image to registry"
def push
cli = self
@@ -26,15 +26,16 @@ class Mrsk::Cli::Build < Mrsk::Cli::Base
end
end
desc "pull", "Pull app image from the registry onto servers"
desc "pull", "Pull app image from registry onto servers"
def pull
on(MRSK.hosts) do
execute *MRSK.auditor.record("build pull image #{MRSK.version}"), verbosity: :debug
execute *MRSK.auditor.record("Pulled image with version #{MRSK.version}"), verbosity: :debug
execute *MRSK.builder.clean, raise_on_non_zero_exit: false
execute *MRSK.builder.pull
end
end
desc "create", "Create a local build setup"
desc "create", "Create a build setup"
def create
run_locally do
begin
@@ -51,7 +52,7 @@ class Mrsk::Cli::Build < Mrsk::Cli::Base
end
end
desc "remove", "Remove local build setup"
desc "remove", "Remove build setup"
def remove
run_locally do
debug "Using builder: #{MRSK.builder.name}"
@@ -59,7 +60,7 @@ class Mrsk::Cli::Build < Mrsk::Cli::Base
end
end
desc "details", "Show the name of the configured builder"
desc "details", "Show build setup"
def details
run_locally do
puts "Builder: #{MRSK.builder.name}"

View File

@@ -0,0 +1,48 @@
class Mrsk::Cli::Healthcheck < Mrsk::Cli::Base
MAX_ATTEMPTS = 5
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 "#{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 => 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 the app to servers"
desc "setup", "Setup all accessories and deploy 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 the app to servers"
desc "deploy", "Deploy app to servers"
def deploy
print_runtime do
runtime = print_runtime do
say "Ensure Docker is installed...", :magenta
invoke "mrsk:cli:server:bootstrap"
@@ -23,37 +23,52 @@ 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"
end
desc "redeploy", "Deploy new version of the app to servers (without bootstrapping servers, starting Traefik, pruning, and registry login)"
desc "redeploy", "Deploy app to servers without bootstrapping servers, starting Traefik, pruning, and registry login"
def redeploy
print_runtime do
runtime = 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"
end
desc "rollback [VERSION]", "Rollback the app to VERSION"
desc "rollback [VERSION]", "Rollback app to VERSION"
def rollback(version)
MRSK.version = version
cli = self
if container_name_available?(MRSK.config.service_with_version)
say "Stop current version, then start version #{version}...", :magenta
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
on(MRSK.hosts) do |host|
execute *MRSK.app.stop, raise_on_non_zero_exit: false
execute *MRSK.app.start
end
audit_broadcast "Rolled back app to version #{version}"
else
say "The app version '#{version}' is not available as a container (use 'mrsk app containers' for available versions)", :red
end
end
desc "details", "Display details about Traefik and app containers"
desc "details", "Show details about all containers"
def details
invoke "mrsk:cli:traefik:details"
invoke "mrsk:cli:app:details"
@@ -67,7 +82,7 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
end
end
desc "config", "Show combined config"
desc "config", "Show combined config (including secrets!)"
def config
run_locally do
puts MRSK.config.to_h.to_yaml
@@ -107,42 +122,66 @@ 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]
File.write(".env.#{destination}", ERB.new(IO.read(Pathname.new(File.expand_path(".env.#{destination}.erb")))).result)
env_template_path = ".env.#{destination}.erb"
env_path = ".env.#{destination}"
else
File.write(".env", ERB.new(IO.read(Pathname.new(File.expand_path(".env.erb")))).result)
env_template_path = ".env.erb"
env_path = ".env"
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"
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" ]
invoke "mrsk:cli:registry:logout", [], options.without(:confirmed)
end
end
desc "remove", "Remove Traefik, app, and registry session from servers"
def remove
invoke "mrsk:cli:traefik:remove"
invoke "mrsk:cli:app:remove"
invoke "mrsk:cli:registry:logout"
end
desc "version", "Display the MRSK version"
desc "version", "Show MRSK version"
def version
puts Mrsk::VERSION
end
desc "accessory", "Manage the accessories"
desc "accessory", "Manage accessories (db/redis/search)"
subcommand "accessory", Mrsk::Cli::Accessory
desc "app", "Manage the application"
desc "app", "Manage application"
subcommand "app", Mrsk::Cli::App
desc "build", "Build the application image"
desc "build", "Build 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 the Traefik load balancer"
desc "traefik", "Manage 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
invoke :containers
invoke :images
containers
images
end
desc "images", "Prune unused images older than 30 days"
desc "images", "Prune unused images older than 7 days"
def images
on(MRSK.hosts) do
execute *MRSK.auditor.record("prune images"), verbosity: :debug
execute *MRSK.auditor.record("Pruned images"), verbosity: :debug
execute *MRSK.prune.images
end
end
desc "containers", "Prune stopped containers for the service older than 3 days"
desc "containers", "Prune stopped containers older than 3 days"
def containers
on(MRSK.hosts) do
execute *MRSK.auditor.record("prune containers"), verbosity: :debug
execute *MRSK.auditor.record("Pruned containers"), verbosity: :debug
execute *MRSK.prune.containers
end
end

View File

@@ -1,15 +1,17 @@
class Mrsk::Cli::Registry < Mrsk::Cli::Base
desc "login", "Login to the registry locally and remotely"
desc "login", "Log in to 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", "Logout of the registry remotely"
desc "logout", "Log out of 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 the servers"
desc "bootstrap", "Ensure Docker is installed on 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,5 +1,4 @@
# 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.
# Name of your application. Used to uniquely configure containers.
service: my-app
# Name of the container image.
@@ -14,4 +13,64 @@ registry:
# Specify the registry server, if you're not using Docker Hub
# server: registry.digitalocean.com / ghcr.io / ...
username: my-user
password: my-password-should-go-somewhere-safe
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

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
invoke :stop
invoke :remove_container
invoke :boot
stop
remove_container
boot
end
desc "start", "Start existing Traefik on servers"
desc "start", "Start existing Traefik container on servers"
def start
on(MRSK.traefik_hosts) do
execute *MRSK.auditor.record("traefik start"), verbosity: :debug
execute *MRSK.auditor.record("Started traefik"), verbosity: :debug
execute *MRSK.traefik.start, raise_on_non_zero_exit: false
end
end
desc "stop", "Stop Traefik on servers"
desc "stop", "Stop existing Traefik container on servers"
def stop
on(MRSK.traefik_hosts) do
execute *MRSK.auditor.record("traefik stop"), verbosity: :debug
execute *MRSK.auditor.record("Stopped traefik"), verbosity: :debug
execute *MRSK.traefik.stop, raise_on_non_zero_exit: false
end
end
desc "restart", "Restart Traefik on servers"
desc "restart", "Restart existing Traefik container on servers"
def restart
invoke :stop
invoke :start
stop
start
end
desc "details", "Display details about Traefik containers from servers"
desc "details", "Show details about Traefik container 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
invoke :stop
invoke :remove_container
invoke :remove_image
stop
remove_container
remove_image
end
desc "remove_container", "Remove Traefik container from servers"
desc "remove_container", "Remove Traefik container from servers", hide: true
def remove_container
on(MRSK.traefik_hosts) do
execute *MRSK.auditor.record("traefik remove container"), verbosity: :debug
execute *MRSK.auditor.record("Removed traefik container"), verbosity: :debug
execute *MRSK.traefik.remove_container
end
end
desc "remove_container", "Remove Traefik image from servers"
desc "remove_container", "Remove Traefik image from servers", hide: true
def remove_image
on(MRSK.traefik_hosts) do
execute *MRSK.auditor.record("traefik remove image"), verbosity: :debug
execute *MRSK.auditor.record("Removed traefik image"), verbosity: :debug
execute *MRSK.traefik.remove_image
end
end

View File

@@ -49,22 +49,6 @@ 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
@@ -73,6 +57,26 @@ 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

View File

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

View File

@@ -3,7 +3,7 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
role = config.role(role)
docker :run,
"-d",
"--detach",
"--restart unless-stopped",
"--log-opt", "max-size=#{MAX_LOG_SIZE}",
"--name", service_with_version,
@@ -30,7 +30,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}#{" -n #{lines}" if lines} 2>&1",
"xargs docker logs#{" --since #{since}" if since}#{" --tail #{lines}" if lines} 2>&1",
("grep '#{grep}'" if grep)
end
@@ -38,7 +38,7 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
run_over_ssh \
pipe(
current_container_id,
"xargs docker logs -t -n 10 -f 2>&1",
"xargs docker logs --timestamps --tail 10 --follow 2>&1",
(%(grep "#{grep}") if grep)
),
host: host
@@ -72,11 +72,7 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
def current_container_id
docker :ps, "-q", *service_filter
end
def container_id_for(container_name:)
docker :container, :ls, "-a", "-f", "name=#{container_name}", "-q"
docker :ps, "--quiet", *service_filter
end
def current_running_version
@@ -93,9 +89,19 @@ 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, "-a", *service_filter
docker :container, :ls, "--all", *service_filter
end
def list_container_names
[ *list_containers, "--format", "'{{ .Names }}'" ]
end
def remove_container(version:)
@@ -105,7 +111,7 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
end
def remove_containers
docker :container, :prune, "-f", *service_filter
docker :container, :prune, "--force", *service_filter
end
def list_images
@@ -113,7 +119,7 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
end
def remove_images
docker :image, :prune, "-a", "-f", *service_filter
docker :image, :prune, "--all", "--force", *service_filter
end

View File

@@ -1,12 +1,20 @@
require "active_support/core_ext/time/conversions"
class Mrsk::Commands::Auditor < Mrsk::Commands::Base
# Runs remotely
def record(line)
append \
[ :echo, tagged_line(line) ],
[ :echo, tagged_record_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
@@ -16,19 +24,19 @@ class Mrsk::Commands::Auditor < Mrsk::Commands::Base
"mrsk-#{config.service}-audit.log"
end
def tagged_line(line)
"'#{tags} #{line}'"
def tagged_record_line(line)
"'#{recorded_at_tag} #{performer_tag} #{line}'"
end
def tags
"[#{timestamp}] [#{performer}]"
def tagged_broadcast_line(line)
"'#{performer_tag} #{line}'"
end
def performer
`whoami`.strip
def performer_tag
"[#{`whoami`.strip}]"
end
def timestamp
Time.now.to_fs(:db)
def recorded_at_tag
"[#{Time.now.to_fs(:db)}]"
end
end

View File

@@ -17,6 +17,10 @@ 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, :pull, :info, to: :target
delegate :create, :remove, :push, :clean, :pull, :info, to: :target
def name
target.class.to_s.remove("Mrsk::Commands::Builder::").underscore

View File

@@ -1,23 +1,35 @@
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_args
argumentize "--build-arg", args, redacted: true
end
def build_secrets
argumentize "--secret", secrets.collect { |secret| [ "id", secret ] }
end
def build_tags
[ "-t", config.absolute_image, "-t", config.latest_image ]
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
def build_secrets
argumentize "--secret", secrets.collect { |secret| [ "id", secret ] }
end
def args
(config.builder && config.builder["args"]) || {}
end

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,50 @@
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,15 +2,11 @@ require "active_support/duration"
require "active_support/core_ext/numeric/time"
class Mrsk::Commands::Prune < Mrsk::Commands::Base
PRUNE_IMAGES_AFTER = 7.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"
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"
end
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'"
def containers(until_hours: 3.days.in_hours.to_i)
docker :container, :prune, "--force", "--filter", "label=service=#{config.service}", "--filter", "until=#{until_hours}h"
end
end

View File

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

View File

@@ -107,6 +107,7 @@ class Mrsk::Configuration
end
end
def ssh_user
if raw_config.ssh.present?
raw_config.ssh["user"] || "root"
@@ -127,6 +128,15 @@ 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 valid?
ensure_required_keys_present && ensure_env_available
end
@@ -145,7 +155,8 @@ class Mrsk::Configuration
volume_args: volume_args,
ssh_options: ssh_options,
builder: raw_config.builder,
accessories: raw_config.accessories
accessories: raw_config.accessories,
healthcheck: healthcheck
}.compact
end

View File

@@ -58,8 +58,8 @@ 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" => "/up",
"traefik.http.routers.#{config.service}.rule" => "PathPrefix(`/`)",
"traefik.http.services.#{config.service}.loadbalancer.healthcheck.path" => config.healthcheck["path"],
"traefik.http.services.#{config.service}.loadbalancer.healthcheck.interval" => "1s",
"traefik.http.middlewares.#{config.service}.retry.attempts" => "3",
"traefik.http.middlewares.#{config.service}.retry.initialinterval" => "500ms"

View File

@@ -1,13 +1,14 @@
module Mrsk::Utils
extend self
# Return a list of shell arguments using the same named argument against the passed attributes (hash or array).
# Return a list of escaped 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 |k, v|
if v.present?
[ argument, redacted ? redact("#{k}=#{v}") : "#{k}=#{v}" ]
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 ]
else
[ argument, k ]
[ argument, key ]
end
end
end

View File

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

View File

@@ -14,7 +14,7 @@ class CliAccessoryTest < CliTestCase
end
test "boot" do
assert_match "Running docker run --name app-mysql -d --restart unless-stopped --log-opt max-size=10m -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")
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")
end
test "exec" do
@@ -31,6 +31,14 @@ 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,7 +2,7 @@ require_relative "cli_test_case"
class CliAppTest < CliTestCase
test "boot" do
assert_match /Running docker run -d --restart unless-stopped/, run_command("boot")
assert_match /Running docker run --detach --restart unless-stopped/, run_command("boot")
end
test "boot will reboot if same version is already running" do
@@ -15,8 +15,8 @@ class CliAppTest < CliTestCase
run_command("boot").tap do |output|
assert_match /Rebooting container with same version already deployed/, output # Can't start what's already running
assert_match /docker ps -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 ps --quiet --filter label=service=app \| xargs docker stop/, output # Stop what's running
assert_match /docker container ls --all --filter name=app-999 --quiet \| xargs docker container rm/, output # Remove old container
assert_match /docker run/, output # Start new container
end
ensure
@@ -31,7 +31,7 @@ class CliAppTest < CliTestCase
test "stop" do
run_command("stop").tap do |output|
assert_match /docker ps -q --filter label=service=app \| xargs docker stop/, output
assert_match /docker ps --quiet --filter label=service=app \| xargs docker stop/, output
end
end
@@ -41,9 +41,17 @@ 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 -a -f name=app-1234567 -q \| xargs docker container rm/, output
assert_match /docker container ls --all --filter name=app-1234567 --quiet \| xargs docker container rm/, output
end
end

15
test/cli/build_test.rb Normal file
View File

@@ -0,0 +1,15 @@
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,4 +5,52 @@ 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 /Stop current version, then start version 123/, 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

@@ -49,11 +49,11 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
test "run" do
assert_equal \
"docker run --name app-mysql -d --restart unless-stopped --log-opt max-size=10m -p 3306:3306 -e MYSQL_ROOT_PASSWORD=secret123 -e MYSQL_ROOT_HOST=% --label service=app-mysql mysql:8.0",
"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(" ")
assert_equal \
"docker run --name app-redis -d --restart unless-stopped --log-opt max-size=10m -p 6379:6379 -e SOMETHING=else --volume /var/lib/redis:/data --label service=app-redis --label cache=true redis:latest",
"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(" ")
end
@@ -78,7 +78,7 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
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",
"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(" ")
end
@@ -90,7 +90,7 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
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
@@ -106,29 +106,29 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
test "logs" do
assert_equal \
"docker logs app-mysql -t 2>&1",
"docker logs app-mysql --timestamps 2>&1",
@mysql.logs.join(" ")
assert_equal \
"docker logs app-mysql --since 5m -n 100 -t 2>&1 | grep 'thing'",
"docker logs app-mysql --since 5m --tail 100 --timestamps 2>&1 | grep 'thing'",
@mysql.logs(since: "5m", lines: 100, grep: "thing").join(" ")
end
test "follow logs" do
assert_equal \
"ssh -t root@1.1.1.5 'docker logs app-mysql -t -n 10 -f 2>&1'",
"ssh -t root@1.1.1.5 'docker logs app-mysql --timestamps --tail 10 --follow 2>&1'",
@mysql.follow_logs
end
test "remove container" do
assert_equal \
"docker container prune -f --filter label=service=app-mysql",
"docker container prune --force --filter label=service=app-mysql",
@mysql.remove_container.join(" ")
end
test "remove image" do
assert_equal \
"docker image prune -a -f --filter label=service=app-mysql",
"docker image prune --all --force --filter label=service=app-mysql",
@mysql.remove_image.join(" ")
end
end

View File

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

View File

@@ -0,0 +1,27 @@
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,50 +8,68 @@ 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", "."], builder.push
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(" ")
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", ".", "&&", :docker, :push, "dhh/app:123"], builder.push
assert_equal \
"docker build -t dhh/app:123 -t dhh/app:latest --label service=\"app\" . && docker push dhh/app:123",
builder.push.join(" ")
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", "."], builder.push
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(" ")
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", "."], builder.push
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(" ")
end
test "build args" do
builder = new_builder_command(builder: { "args" => { "a" => 1, "b" => 2 } })
assert_equal [ "--build-arg", "a=1", "--build-arg", "b=2" ], builder.target.build_args
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(" ")
end
test "build secrets" do
builder = new_builder_command(builder: { "secrets" => ["token_a", "token_b"] })
assert_equal [ "--secret", "id=token_a", "--secret", "id=token_b" ], builder.target.build_secrets
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(" ")
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", "--build-arg", "a=1", "--build-arg", "b=2", ".", "&&", :docker, :push, "dhh/app:123" ], builder.push
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(" ")
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", "--build-arg", "a=1", "--build-arg", "b=2", "." ], builder.push
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(" ")
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", "--secret", "id=a", "--secret", "id=b", ".", "&&", :docker, :push, "dhh/app:123" ], builder.push
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(" ")
end
private

View File

@@ -0,0 +1,55 @@
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

@@ -0,0 +1,27 @@
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,10 +14,25 @@ class CommandsRegistryTest < ActiveSupport::TestCase
end
test "registry login" do
assert_equal [ :docker, :login, "hub.docker.com", "-u", "dhh", "-p", "secret" ], @registry.login
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")
end
test "registry logout" do
assert_equal [:docker, :logout, "hub.docker.com"], @registry.logout
assert_equal \
"docker logout hub.docker.com",
@registry.logout.join(" ")
end
end

View File

@@ -10,7 +10,7 @@ class CommandsTraefikTest < ActiveSupport::TestCase
test "run" do
assert_equal \
"docker run --name traefik -d --restart unless-stopped --log-opt max-size=10m -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",
"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(" ")
end
@@ -34,49 +34,49 @@ class CommandsTraefikTest < ActiveSupport::TestCase
test "traefik logs" do
assert_equal \
"docker logs traefik -t 2>&1",
"docker logs traefik --timestamps 2>&1",
new_command.logs.join(" ")
end
test "traefik logs since 2h" do
assert_equal \
"docker logs traefik --since 2h -t 2>&1",
"docker logs traefik --since 2h --timestamps 2>&1",
new_command.logs(since: '2h').join(" ")
end
test "traefik logs last 10 lines" do
assert_equal \
"docker logs traefik -n 10 -t 2>&1",
"docker logs traefik --tail 10 --timestamps 2>&1",
new_command.logs(lines: 10).join(" ")
end
test "traefik logs with grep hello!" do
assert_equal \
"docker logs traefik -t 2>&1 | grep 'hello!'",
"docker logs traefik --timestamps 2>&1 | grep 'hello!'",
new_command.logs(grep: 'hello!').join(" ")
end
test "traefik remove container" do
assert_equal \
"docker container prune -f --filter label=org.opencontainers.image.title=Traefik",
"docker container prune --force --filter label=org.opencontainers.image.title=Traefik",
new_command.remove_container.join(" ")
end
test "traefik remove image" do
assert_equal \
"docker image prune -a -f --filter label=org.opencontainers.image.title=Traefik",
"docker image prune --all --force --filter label=org.opencontainers.image.title=Traefik",
new_command.remove_image.join(" ")
end
test "traefik follow logs" do
assert_equal \
"ssh -t root@1.1.1.1 'docker logs traefik -t -n 10 -f 2>&1'",
"ssh -t root@1.1.1.1 'docker logs traefik --timestamps --tail 10 --follow 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 -t -n 10 -f 2>&1 | grep \"hello!\"'",
"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!')
end

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=3", "--label", "traefik.http.middlewares.app.retry.initialinterval=500ms"], @config.role(:web).label_args
assert_equal [ "--label", "service=\"app\"", "--label", "role=\"web\"", "--label", "traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\"", "--label", "traefik.http.services.app.loadbalancer.healthcheck.path=\"/up\"", "--label", "traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\"", "--label", "traefik.http.middlewares.app.retry.attempts=\"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=3", "--label", "traefik.http.middlewares.app.retry.initialinterval=500ms" ], config.role(:beta).label_args
assert_equal [ "--label", "service=\"app\"", "--label", "role=\"beta\"", "--label", "traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\"", "--label", "traefik.http.services.app.loadbalancer.healthcheck.path=\"/up\"", "--label", "traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\"", "--label", "traefik.http.middlewares.app.retry.attempts=\"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"] = "secret123"
ENV["DB_PASSWORD"] = "secret&\"123"
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
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
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"] }, @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"], :healthcheck=>{"path"=>"/up", "port"=>3000 }}, @config.to_h)
end
end