Compare commits

...

48 Commits

Author SHA1 Message Date
David Heinemeier Hansson
fb86123db4 Bump version for 0.8.2 2023-02-23 12:28:29 +01:00
David Heinemeier Hansson
bc6963e6bf Note that rebooting may cause air gap 2023-02-23 12:16:58 +01:00
David Heinemeier Hansson
f4f2b5cb17 Communicate the readiness delay 2023-02-23 12:04:57 +01:00
David Heinemeier Hansson
817336df49 No readiness delay in testing 2023-02-23 12:03:03 +01:00
David Heinemeier Hansson
4c399a74bb Update to match latest 2023-02-23 12:02:56 +01:00
David Heinemeier Hansson
e12436a1db Extract readiness_delay to config 2023-02-23 12:02:49 +01:00
David Heinemeier Hansson
b244e919bf Merge branch 'main' into more-resilient-zero-downtime-deploy
* main:
  Add option to skip audit broadcasts (useful when testing)
2023-02-23 11:52:45 +01:00
David Heinemeier Hansson
7ad416f029 Add option to skip audit broadcasts (useful when testing) 2023-02-23 10:04:35 +01:00
David Heinemeier Hansson
371f98d67f Start before stopping and longer timeouts 2023-02-22 19:04:23 +01:00
David Heinemeier Hansson
b879412a6f Upgrade to beta! 2023-02-21 15:31:28 +01:00
David Heinemeier Hansson
e678775a18 Merge pull request #54 from intrip/print-logs-for-healthcheck-status-mistmatch
Print container logs when HealthCheck response_code != 200
2023-02-21 14:34:46 +01:00
Jacopo
689b81014b Print container logs when HealthCheck response_code != 200
The Healthcheck container is shut down right after performing the check, this
makes it harder to troubleshoot configuration issues in the healthcheck
endpoint, e.g DNS rebinding error. Printing the container logs helps the troubleshooting.
2023-02-21 11:48:29 +01:00
David Heinemeier Hansson
01a4eecf98 Bump version for 0.8.1 2023-02-20 18:21:05 +01:00
David Heinemeier Hansson
6f7422af44 Merge pull request #53 from pagbrl/fix-env-concatenation
fix(escape-cli-args): Always use quotes to escape CLI arguments
2023-02-20 18:20:28 +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
43 changed files with 501 additions and 261 deletions

View File

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

View File

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

View File

@@ -14,7 +14,8 @@ servers:
- 192.168.0.2 - 192.168.0.2
registry: registry:
username: registry-user-name username: registry-user-name
password: <%= ENV.fetch("MRSK_REGISTRY_PASSWORD") %> password:
- MRSK_REGISTRY_PASSWORD
env: env:
secret: secret:
- RAILS_MASTER_KEY - RAILS_MASTER_KEY
@@ -348,7 +349,7 @@ If you need separate env variables for different destinations, you can set them
### Using audit broadcasts ### 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 reads the audit line from STDIN, and then does whatever with it: 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 ```yaml
audit_broadcast_cmd: audit_broadcast_cmd:
@@ -359,14 +360,13 @@ The broadcast command could look something like:
```bash ```bash
#!/usr/bin/env bash #!/usr/bin/env bash
read curl -q -d content="[My App] ${1}" https://3.basecamp.com/XXXXX/integrations/XXXXX/buckets/XXXXX/chats/XXXXX/lines
curl -q -d content="[My app] ${REPLY}" 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: That'll post a line like follows to a preconfigured chatbot in Basecamp:
``` ```
[My App] [2023-02-18 11:29:52] [dhh] Rolled back to version d264c4e92470ad1bd18590f04466787262f605de [My App] [dhh] Rolled back to version d264c4e92470ad1bd18590f04466787262f605de
``` ```
### Using custom healthcheck path or port ### Using custom healthcheck path or port
@@ -448,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`: You can see the state of your servers by running `mrsk details`:
@@ -498,7 +498,7 @@ If you wish to remove the entire application, including Traefik, containers, ima
## Stage of development ## Stage of development
This is alpha software. Lots of stuff is missing. Lots of stuff will keep moving around for a while. This is beta software. Commands may still move around. But we're live in production at [37signals](https://37signals.com).
## License ## License

View File

@@ -1,5 +1,5 @@
class Mrsk::Cli::Accessory < Mrsk::Cli::Base 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) def boot(name)
if name == "all" if name == "all"
MRSK.accessory_names.each { |accessory_name| boot(accessory_name) } MRSK.accessory_names.each { |accessory_name| boot(accessory_name) }
@@ -13,12 +13,12 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
execute *accessory.run execute *accessory.run
end end
audit_broadcast "Booted accessory #{name}" audit_broadcast "Booted accessory #{name}" unless options[:skip_broadcast]
end end
end end
end end
desc "upload [NAME]", "Upload accessory files to host" desc "upload [NAME]", "Upload accessory files to host", hide: true
def upload(name) def upload(name)
with_accessory(name) do |accessory| with_accessory(name) do |accessory|
on(accessory.host) do on(accessory.host) do
@@ -33,7 +33,7 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
end end
end end
desc "directories [NAME]", "Create accessory directories on host" desc "directories [NAME]", "Create accessory directories on host", hide: true
def directories(name) def directories(name)
with_accessory(name) do |accessory| with_accessory(name) do |accessory|
on(accessory.host) do on(accessory.host) do
@@ -44,7 +44,7 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
end end
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) def reboot(name)
with_accessory(name) do |accessory| with_accessory(name) do |accessory|
stop(name) stop(name)
@@ -53,7 +53,7 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
end end
end end
desc "start [NAME]", "Start existing accessory on host" desc "start [NAME]", "Start existing accessory container on host"
def start(name) def start(name)
with_accessory(name) do |accessory| with_accessory(name) do |accessory|
on(accessory.host) do on(accessory.host) do
@@ -63,7 +63,7 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
end end
end end
desc "stop [NAME]", "Stop accessory on host" desc "stop [NAME]", "Stop existing accessory container on host"
def stop(name) def stop(name)
with_accessory(name) do |accessory| with_accessory(name) do |accessory|
on(accessory.host) do on(accessory.host) do
@@ -73,7 +73,7 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
end end
end end
desc "restart [NAME]", "Restart accessory on host" desc "restart [NAME]", "Restart existing accessory container on host"
def restart(name) def restart(name)
with_accessory(name) do with_accessory(name) do
stop(name) stop(name)
@@ -81,7 +81,7 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
end end
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) def details(name)
if name == "all" if name == "all"
MRSK.accessory_names.each { |accessory_name| details(accessory_name) } MRSK.accessory_names.each { |accessory_name| details(accessory_name) }
@@ -92,7 +92,7 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
end end
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 :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" option :reuse, type: :boolean, default: false, desc: "Reuse currently running container instead of starting a new one"
def exec(name, cmd) def exec(name, cmd)
@@ -123,7 +123,7 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
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 :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 :lines, type: :numeric, aliases: "-n", desc: "Number of log lines to pull from each server"
option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)" option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)"
@@ -149,21 +149,26 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
end end
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) def remove(name)
if name == "all" 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 else
with_accessory(name) do if options[:confirmed] || ask("This will remove all containers and images for #{name}. Are you sure?", limited_to: %w( y N ), default: "N") == "y"
stop(name) with_accessory(name) do
remove_container(name) stop(name)
remove_image(name) remove_container(name)
remove_service_directory(name) remove_image(name)
remove_service_directory(name)
end
end end
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) def remove_container(name)
with_accessory(name) do |accessory| with_accessory(name) do |accessory|
on(accessory.host) do on(accessory.host) do
@@ -173,7 +178,7 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
end 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) def remove_image(name)
with_accessory(name) do |accessory| with_accessory(name) do |accessory|
on(accessory.host) do on(accessory.host) do
@@ -183,7 +188,7 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
end 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) def remove_service_directory(name)
with_accessory(name) do |accessory| with_accessory(name) do |accessory|
on(accessory.host) do on(accessory.host) do

View File

@@ -5,18 +5,27 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
using_version(options[:version] || most_recent_version_available) do |version| using_version(options[:version] || most_recent_version_available) do |version|
say "Start container with version #{version} (or reboot if already running)...", :magenta say "Start container with version #{version} (or reboot if already running)...", :magenta
cli = self
MRSK.config.roles.each do |role| MRSK.config.roles.each do |role|
on(role.hosts) do |host| on(role.hosts) do |host|
execute *MRSK.auditor.record("Booted app version #{version}"), verbosity: :debug execute *MRSK.auditor.record("Booted app version #{version}"), verbosity: :debug
begin begin
execute *MRSK.app.stop, raise_on_non_zero_exit: false old_version = capture_with_info(*MRSK.app.current_running_version).strip
execute *MRSK.app.run(role: role.name) execute *MRSK.app.run(role: role.name)
cli.say "Waiting #{MRSK.config.readiness_delay}s for app to boot...", :magenta
sleep MRSK.config.readiness_delay
execute *MRSK.app.stop(version: old_version), raise_on_non_zero_exit: false if old_version.present?
rescue SSHKit::Command::Failed => e rescue SSHKit::Command::Failed => e
if e.message =~ /already in use/ if e.message =~ /already in use/
error "Rebooting container with same version already deployed on #{host}" error "Rebooting container with same version #{version} already deployed on #{host} (may cause gap in zero-downtime promise!)"
execute *MRSK.auditor.record("Rebooted app version #{version}"), verbosity: :debug execute *MRSK.auditor.record("Rebooted app version #{version}"), verbosity: :debug
execute *MRSK.app.stop(version: version)
execute *MRSK.app.remove_container(version: version) execute *MRSK.app.remove_container(version: version)
execute *MRSK.app.run(role: role.name) execute *MRSK.app.run(role: role.name)
else else
@@ -28,7 +37,7 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
end end
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 def start
on(MRSK.hosts) do on(MRSK.hosts) do
execute *MRSK.auditor.record("Started app version #{MRSK.version}"), verbosity: :debug execute *MRSK.auditor.record("Started app version #{MRSK.version}"), verbosity: :debug
@@ -36,7 +45,7 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
end end
end end
desc "stop", "Stop app on servers" desc "stop", "Stop app container on servers"
def stop def stop
on(MRSK.hosts) do on(MRSK.hosts) do
execute *MRSK.auditor.record("Stopped app"), verbosity: :debug execute *MRSK.auditor.record("Stopped app"), verbosity: :debug
@@ -44,12 +53,13 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
end end
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 def details
on(MRSK.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.info) } on(MRSK.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.info) }
end 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 :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" option :reuse, type: :boolean, default: false, desc: "Reuse currently running container instead of starting a new one"
def exec(cmd) def exec(cmd)
@@ -91,21 +101,21 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
end end
end end
desc "containers", "List all the app containers currently on servers" desc "containers", "Show app containers on servers"
def containers def containers
on(MRSK.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.list_containers) } on(MRSK.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.list_containers) }
end end
desc "images", "List all the app images currently on servers" desc "images", "Show app images on servers"
def images def images
on(MRSK.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.list_images) } on(MRSK.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.list_images) }
end end
desc "logs", "Show lines from app on servers" desc "logs", "Show log lines from app on servers (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 :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 log lines to pull from each server" 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 :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 def logs
# FIXME: Catch when app containers aren't running # FIXME: Catch when app containers aren't running
@@ -133,11 +143,12 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
desc "remove", "Remove app containers and images from servers" desc "remove", "Remove app containers and images from servers"
def remove def remove
stop
remove_containers remove_containers
remove_images remove_images
end 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) def remove_container(version)
on(MRSK.hosts) do on(MRSK.hosts) do
execute *MRSK.auditor.record("Removed app container with version #{version}"), verbosity: :debug execute *MRSK.auditor.record("Removed app container with version #{version}"), verbosity: :debug
@@ -145,7 +156,7 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
end end
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 def remove_containers
on(MRSK.hosts) do on(MRSK.hosts) do
execute *MRSK.auditor.record("Removed all app containers"), verbosity: :debug execute *MRSK.auditor.record("Removed all app containers"), verbosity: :debug
@@ -153,7 +164,7 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
end end
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 def remove_images
on(MRSK.hosts) do on(MRSK.hosts) do
execute *MRSK.auditor.record("Removed all app images"), verbosity: :debug execute *MRSK.auditor.record("Removed all app images"), verbosity: :debug
@@ -161,8 +172,8 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
end end
end end
desc "current_version", "Shows the version currently running" desc "version", "Show app version currently running on servers"
def current_version def version
on(MRSK.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.current_running_version).strip } on(MRSK.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.current_running_version).strip }
end end
@@ -184,7 +195,12 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
def most_recent_version_available(host: MRSK.primary_host) def most_recent_version_available(host: MRSK.primary_host)
version = nil version = nil
on(host) { version = capture_with_info(*MRSK.app.most_recent_version_from_available_images).strip } 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 end
def current_running_version(host: MRSK.primary_host) def current_running_version(host: MRSK.primary_host)

View File

@@ -17,8 +17,10 @@ module Mrsk::Cli
class_option :hosts, aliases: "-h", desc: "Run commands on these hosts instead of all (separate by comma)" 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 :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 :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 (west -> deploy.west.yml)" class_option :destination, aliases: "-d", desc: "Specify destination to be used for config file (staging -> deploy.staging.yml)"
class_option :skip_broadcast, aliases: "-B", type: :boolean, default: false, desc: "Skip audit broadcasts"
def initialize(*) def initialize(*)
super super

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
class Mrsk::Cli::Main < Mrsk::Cli::Base 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 def setup
print_runtime do print_runtime do
invoke "mrsk:cli:server:bootstrap" invoke "mrsk:cli:server:bootstrap"
@@ -8,7 +8,7 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
end end
end end
desc "deploy", "Deploy the app to servers" desc "deploy", "Deploy app to servers"
def deploy def deploy
runtime = print_runtime do runtime = print_runtime do
say "Ensure Docker is installed...", :magenta say "Ensure Docker is installed...", :magenta
@@ -32,10 +32,10 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
invoke "mrsk:cli:prune:all" invoke "mrsk:cli:prune:all"
end end
audit_broadcast "Deployed app in #{runtime.to_i} seconds" audit_broadcast "Deployed app in #{runtime.to_i} seconds" unless options[:skip_broadcast]
end 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 def redeploy
runtime = print_runtime do runtime = print_runtime do
say "Build and push app image...", :magenta say "Build and push app image...", :magenta
@@ -47,28 +47,36 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
invoke "mrsk:cli:app:boot" invoke "mrsk:cli:app:boot"
end end
audit_broadcast "Redeployed app in #{runtime.to_i} seconds" audit_broadcast "Redeployed app in #{runtime.to_i} seconds" unless options[:skip_broadcast]
end end
desc "rollback [VERSION]", "Rollback the app to VERSION" desc "rollback [VERSION]", "Rollback app to VERSION"
def rollback(version) def rollback(version)
MRSK.version = version MRSK.version = version
if container_name_available?(MRSK.config.service_with_version) if container_name_available?(MRSK.config.service_with_version)
say "Stop current version, then start version #{version}...", :magenta say "Start version #{version}, then stop the old version...", :magenta
cli = self
on(MRSK.hosts) do |host| on(MRSK.hosts) do |host|
execute *MRSK.app.stop, raise_on_non_zero_exit: false old_version = capture_with_info(*MRSK.app.current_running_version).strip.presence
execute *MRSK.app.start execute *MRSK.app.start
cli.say "Waiting #{MRSK.config.readiness_delay}s for app to start...", :magenta
sleep MRSK.config.readiness_delay
execute *MRSK.app.stop(version: old_version), raise_on_non_zero_exit: false
end end
audit_broadcast "Rolled back app to version #{version}" audit_broadcast "Rolled back app to version #{version}" unless options[:skip_broadcast]
else else
say "The app version '#{version}' is not available as a container (use 'mrsk app containers' for available versions)", :red say "The app version '#{version}' is not available as a container (use 'mrsk app containers' for available versions)", :red
end end
end end
desc "details", "Display details about Traefik and app containers" desc "details", "Show details about all containers"
def details def details
invoke "mrsk:cli:traefik:details" invoke "mrsk:cli:traefik:details"
invoke "mrsk:cli:app:details" invoke "mrsk:cli:app:details"
@@ -82,7 +90,7 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
end end
end end
desc "config", "Show combined config" desc "config", "Show combined config (including secrets!)"
def config def config
run_locally do run_locally do
puts MRSK.config.to_h.to_yaml puts MRSK.config.to_h.to_yaml
@@ -132,40 +140,44 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
File.write(env_path, ERB.new(File.read(env_template_path)).result, perm: 0600) File.write(env_path, ERB.new(File.read(env_template_path)).result, perm: 0600)
end end
desc "remove", "Remove Traefik, app, and registry session from servers" 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 def remove
invoke "mrsk:cli:traefik:remove" if options[:confirmed] || ask(remove_confirmation_question, limited_to: %w( y N ), default: "N") == "y"
invoke "mrsk:cli:app:remove" invoke "mrsk:cli:traefik:remove", [], options.without(:confirmed)
invoke "mrsk:cli:registry:logout" invoke "mrsk:cli:app:remove", [], options.without(:confirmed)
invoke "mrsk:cli:accessory:remove", [ "all" ]
invoke "mrsk:cli:registry:logout", [], options.without(:confirmed)
end
end end
desc "version", "Display the MRSK version" desc "version", "Show MRSK version"
def version def version
puts Mrsk::VERSION puts Mrsk::VERSION
end end
desc "accessory", "Manage the accessories" desc "accessory", "Manage accessories (db/redis/search)"
subcommand "accessory", Mrsk::Cli::Accessory subcommand "accessory", Mrsk::Cli::Accessory
desc "app", "Manage the application" desc "app", "Manage application"
subcommand "app", Mrsk::Cli::App subcommand "app", Mrsk::Cli::App
desc "build", "Build the application image" desc "build", "Build application image"
subcommand "build", Mrsk::Cli::Build subcommand "build", Mrsk::Cli::Build
desc "healthcheck", "Healthcheck the application" desc "healthcheck", "Healthcheck application"
subcommand "healthcheck", Mrsk::Cli::Healthcheck subcommand "healthcheck", Mrsk::Cli::Healthcheck
desc "prune", "Prune old application images and containers" desc "prune", "Prune old application images and containers"
subcommand "prune", Mrsk::Cli::Prune 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 subcommand "registry", Mrsk::Cli::Registry
desc "server", "Bootstrap servers with Docker" desc "server", "Bootstrap servers with Docker"
subcommand "server", Mrsk::Cli::Server subcommand "server", Mrsk::Cli::Server
desc "traefik", "Manage the Traefik load balancer" desc "traefik", "Manage Traefik load balancer"
subcommand "traefik", Mrsk::Cli::Traefik subcommand "traefik", Mrsk::Cli::Traefik
private private
@@ -174,4 +186,10 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
on(host) { container_names = capture_with_info(*MRSK.app.list_container_names).split("\n") } on(host) { container_names = capture_with_info(*MRSK.app.list_container_names).split("\n") }
Array(container_names).include?(container_name) Array(container_names).include?(container_name)
end 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 end

View File

@@ -1,8 +1,8 @@
class Mrsk::Cli::Prune < Mrsk::Cli::Base class Mrsk::Cli::Prune < Mrsk::Cli::Base
desc "all", "Prune unused images and stopped containers" desc "all", "Prune unused images and stopped containers"
def all def all
invoke :containers containers
invoke :images images
end end
desc "images", "Prune unused images older than 7 days" desc "images", "Prune unused images older than 7 days"
@@ -13,7 +13,7 @@ class Mrsk::Cli::Prune < Mrsk::Cli::Base
end end
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 def containers
on(MRSK.hosts) do on(MRSK.hosts) do
execute *MRSK.auditor.record("Pruned containers"), verbosity: :debug execute *MRSK.auditor.record("Pruned containers"), verbosity: :debug

View File

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

View File

@@ -1,5 +1,5 @@
class Mrsk::Cli::Server < Mrsk::Cli::Base 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 def bootstrap
on(MRSK.hosts + MRSK.accessory_hosts) { execute "which docker || (apt-get update -y && apt-get install docker.io -y)" } on(MRSK.hosts + MRSK.accessory_hosts) { execute "which docker || (apt-get update -y && apt-get install docker.io -y)" }
end end

View File

@@ -1,5 +1,4 @@
# Name of your application. Used to uniquely configuring Traefik and app containers. # Name of your application. Used to uniquely configure containers.
# Your Dockerfile should set LABEL service=the-same-value to ensure image pruning works.
service: my-app service: my-app
# Name of the container image. # Name of the container image.
@@ -14,4 +13,64 @@ registry:
# Specify the registry server, if you're not using Docker Hub # Specify the registry server, if you're not using Docker Hub
# server: registry.digitalocean.com / ghcr.io / ... # server: registry.digitalocean.com / ghcr.io / ...
username: my-user 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,12 +6,12 @@ class Mrsk::Cli::Traefik < Mrsk::Cli::Base
desc "reboot", "Reboot Traefik on servers (stop container, remove container, start new container)" desc "reboot", "Reboot Traefik on servers (stop container, remove container, start new container)"
def reboot def reboot
invoke :stop stop
invoke :remove_container remove_container
invoke :boot boot
end end
desc "start", "Start existing Traefik on servers" desc "start", "Start existing Traefik container on servers"
def start def start
on(MRSK.traefik_hosts) do on(MRSK.traefik_hosts) do
execute *MRSK.auditor.record("Started traefik"), verbosity: :debug execute *MRSK.auditor.record("Started traefik"), verbosity: :debug
@@ -19,7 +19,7 @@ class Mrsk::Cli::Traefik < Mrsk::Cli::Base
end end
end end
desc "stop", "Stop Traefik on servers" desc "stop", "Stop existing Traefik container on servers"
def stop def stop
on(MRSK.traefik_hosts) do on(MRSK.traefik_hosts) do
execute *MRSK.auditor.record("Stopped traefik"), verbosity: :debug execute *MRSK.auditor.record("Stopped traefik"), verbosity: :debug
@@ -27,13 +27,13 @@ class Mrsk::Cli::Traefik < Mrsk::Cli::Base
end end
end end
desc "restart", "Restart Traefik on servers" desc "restart", "Restart existing Traefik container on servers"
def restart def restart
invoke :stop stop
invoke :start start
end end
desc "details", "Display details about Traefik containers from servers" desc "details", "Show details about Traefik container from servers"
def details def details
on(MRSK.traefik_hosts) { |host| puts_by_host host, capture_with_info(*MRSK.traefik.info), type: "Traefik" } on(MRSK.traefik_hosts) { |host| puts_by_host host, capture_with_info(*MRSK.traefik.info), type: "Traefik" }
end end
@@ -64,12 +64,12 @@ class Mrsk::Cli::Traefik < Mrsk::Cli::Base
desc "remove", "Remove Traefik container and image from servers" desc "remove", "Remove Traefik container and image from servers"
def remove def remove
invoke :stop stop
invoke :remove_container remove_container
invoke :remove_image remove_image
end end
desc "remove_container", "Remove Traefik container from servers" desc "remove_container", "Remove Traefik container from servers", hide: true
def remove_container def remove_container
on(MRSK.traefik_hosts) do on(MRSK.traefik_hosts) do
execute *MRSK.auditor.record("Removed traefik container"), verbosity: :debug execute *MRSK.auditor.record("Removed traefik container"), verbosity: :debug
@@ -77,7 +77,7 @@ class Mrsk::Cli::Traefik < Mrsk::Cli::Base
end end
end end
desc "remove_container", "Remove Traefik image from servers" desc "remove_container", "Remove Traefik image from servers", hide: true
def remove_image def remove_image
on(MRSK.traefik_hosts) do on(MRSK.traefik_hosts) do
execute *MRSK.auditor.record("Removed traefik image"), verbosity: :debug execute *MRSK.auditor.record("Removed traefik image"), verbosity: :debug

View File

@@ -49,22 +49,6 @@ class Mrsk::Commander
@app ||= Mrsk::Commands::App.new(config) @app ||= Mrsk::Commands::App.new(config)
end 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) def accessory(name)
Mrsk::Commands::Accessory.new(config, name: name) Mrsk::Commands::Accessory.new(config, name: name)
end end
@@ -73,10 +57,26 @@ class Mrsk::Commander
@auditor ||= Mrsk::Commands::Auditor.new(config) @auditor ||= Mrsk::Commands::Auditor.new(config)
end end
def builder
@builder ||= Mrsk::Commands::Builder.new(config)
end
def healthcheck def healthcheck
@healthcheck ||= Mrsk::Commands::Healthcheck.new(config) @healthcheck ||= Mrsk::Commands::Healthcheck.new(config)
end 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) def with_verbosity(level)
old_level = self.verbosity old_level = self.verbosity

View File

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

View File

@@ -3,7 +3,7 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
role = config.role(role) role = config.role(role)
docker :run, docker :run,
"-d", "--detach",
"--restart unless-stopped", "--restart unless-stopped",
"--log-opt", "max-size=#{MAX_LOG_SIZE}", "--log-opt", "max-size=#{MAX_LOG_SIZE}",
"--name", service_with_version, "--name", service_with_version,
@@ -18,8 +18,10 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
docker :start, service_with_version docker :start, service_with_version
end end
def stop def stop(version: nil)
pipe current_container_id, xargs(docker(:stop)) pipe \
version ? container_id_for_version(version) : current_container_id,
xargs(docker(:stop))
end end
def info def info
@@ -30,7 +32,7 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
def logs(since: nil, lines: nil, grep: nil) def logs(since: nil, lines: nil, grep: nil)
pipe \ pipe \
current_container_id, 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) ("grep '#{grep}'" if grep)
end end
@@ -38,7 +40,7 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
run_over_ssh \ run_over_ssh \
pipe( pipe(
current_container_id, 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) (%(grep "#{grep}") if grep)
), ),
host: host host: host
@@ -72,7 +74,7 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
def current_container_id def current_container_id
docker :ps, "-q", *service_filter docker :ps, "--quiet", *service_filter
end end
def current_running_version def current_running_version
@@ -97,7 +99,7 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
def list_containers def list_containers
docker :container, :ls, "-a", *service_filter docker :container, :ls, "--all", *service_filter
end end
def list_container_names def list_container_names
@@ -111,7 +113,7 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
end end
def remove_containers def remove_containers
docker :container, :prune, "-f", *service_filter docker :container, :prune, "--force", *service_filter
end end
def list_images def list_images
@@ -119,7 +121,7 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
end end
def remove_images def remove_images
docker :image, :prune, "-a", "-f", *service_filter docker :image, :prune, "--all", "--force", *service_filter
end end
@@ -132,6 +134,10 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
end end
end end
def container_id_for_version(version)
container_id_for(container_name: service_with_version(version))
end
def service_filter def service_filter
[ "--filter", "label=service=#{config.service}" ] [ "--filter", "label=service=#{config.service}" ]
end end

View File

@@ -4,16 +4,14 @@ class Mrsk::Commands::Auditor < Mrsk::Commands::Base
# Runs remotely # Runs remotely
def record(line) def record(line)
append \ append \
[ :echo, tagged_line(line) ], [ :echo, tagged_record_line(line) ],
audit_log_file audit_log_file
end end
# Runs locally # Runs locally
def broadcast(line) def broadcast(line)
if broadcast_cmd = config.audit_broadcast_cmd if broadcast_cmd = config.audit_broadcast_cmd
pipe \ [ broadcast_cmd, tagged_broadcast_line(line) ]
[ :echo, tagged_line(line) ],
broadcast_cmd
end end
end end
@@ -26,19 +24,19 @@ class Mrsk::Commands::Auditor < Mrsk::Commands::Base
"mrsk-#{config.service}-audit.log" "mrsk-#{config.service}-audit.log"
end end
def tagged_line(line) def tagged_record_line(line)
"'#{tags} #{line}'" "'#{recorded_at_tag} #{performer_tag} #{line}'"
end end
def tags def tagged_broadcast_line(line)
"[#{recorded_at}] [#{performer}]" "'#{performer_tag} #{line}'"
end end
def performer def performer_tag
@performer ||= `whoami`.strip "[#{`whoami`.strip}]"
end end
def recorded_at def recorded_at_tag
Time.now.to_fs(:db) "[#{Time.now.to_fs(:db)}]"
end end
end end

View File

@@ -18,7 +18,7 @@ module Mrsk::Commands
end end
def container_id_for(container_name:) def container_id_for(container_name:)
docker :container, :ls, "-a", "-f", "name=#{container_name}", "-q" docker :container, :ls, "--all", "--filter", "name=#{container_name}", "--quiet"
end end
private private

View File

@@ -1,5 +1,5 @@
class Mrsk::Commands::Builder < Mrsk::Commands::Base 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 def name
target.class.to_s.remove("Mrsk::Commands::Builder::").underscore target.class.to_s.remove("Mrsk::Commands::Builder::").underscore

View File

@@ -1,6 +1,10 @@
class Mrsk::Commands::Builder::Base < Mrsk::Commands::Base class Mrsk::Commands::Builder::Base < Mrsk::Commands::Base
delegate :argumentize, to: Mrsk::Utils delegate :argumentize, to: Mrsk::Utils
def clean
docker :image, :rm, "--force", config.absolute_image
end
def pull def pull
docker :pull, config.absolute_image docker :pull, config.absolute_image
end end

View File

@@ -5,9 +5,9 @@ class Mrsk::Commands::Healthcheck < Mrsk::Commands::Base
web = config.role(:web) web = config.role(:web)
docker :run, docker :run,
"-d", "--detach",
"--name", container_name_with_version, "--name", container_name_with_version,
"-p", "#{EXPOSED_PORT}:#{config.healthcheck["port"]}", "--publish", "#{EXPOSED_PORT}:#{config.healthcheck["port"]}",
"--label", "service=#{container_name}", "--label", "service=#{container_name}",
*web.env_args, *web.env_args,
*config.volume_args, *config.volume_args,
@@ -16,19 +16,19 @@ class Mrsk::Commands::Healthcheck < Mrsk::Commands::Base
end end
def curl def curl
[ :curl, "--silent", "--output", "/dev/null", "--write-out", "'%{http_code}'", health_url ] [ :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 end
def stop def stop
pipe \ pipe container_id, xargs(docker(:stop))
container_id_for(container_name: container_name),
xargs(docker(:stop))
end end
def remove def remove
pipe \ pipe container_id, xargs(docker(:container, :rm))
container_id_for(container_name: container_name),
xargs(docker(:container, :rm))
end end
private private
@@ -40,6 +40,10 @@ class Mrsk::Commands::Healthcheck < Mrsk::Commands::Base
"healthcheck-#{config.service_with_version}" "healthcheck-#{config.service_with_version}"
end end
def container_id
container_id_for(container_name: container_name)
end
def health_url def health_url
"http://localhost:#{EXPOSED_PORT}#{config.healthcheck["path"]}" "http://localhost:#{EXPOSED_PORT}#{config.healthcheck["path"]}"
end end

View File

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

View File

@@ -1,11 +1,11 @@
class Mrsk::Commands::Traefik < Mrsk::Commands::Base class Mrsk::Commands::Traefik < Mrsk::Commands::Base
def run def run
docker :run, "--name traefik", docker :run, "--name traefik",
"-d", "--detach",
"--restart", "unless-stopped", "--restart", "unless-stopped",
"--log-opt", "max-size=#{MAX_LOG_SIZE}", "--log-opt", "max-size=#{MAX_LOG_SIZE}",
"-p 80:80", "--publish", "80:80",
"-v /var/run/docker.sock:/var/run/docker.sock", "--volume", "/var/run/docker.sock:/var/run/docker.sock",
"traefik", "traefik",
"--providers.docker", "--providers.docker",
"--log.level=DEBUG", "--log.level=DEBUG",
@@ -26,23 +26,23 @@ class Mrsk::Commands::Traefik < Mrsk::Commands::Base
def logs(since: nil, lines: nil, grep: nil) def logs(since: nil, lines: nil, grep: nil)
pipe \ 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) ("grep '#{grep}'" if grep)
end end
def follow_logs(host:, grep: nil) def follow_logs(host:, grep: nil)
run_over_ssh pipe( 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) (%(grep "#{grep}") if grep)
).join(" "), host: host ).join(" "), host: host
end end
def remove_container 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 end
def remove_image 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 end
private private

View File

@@ -136,6 +136,9 @@ class Mrsk::Configuration
{ "path" => "/up", "port" => 3000 }.merge(raw_config.healthcheck || {}) { "path" => "/up", "port" => 3000 }.merge(raw_config.healthcheck || {})
end end
def readiness_delay
raw_config.readiness_delay || 7
end
def valid? def valid?
ensure_required_keys_present && ensure_env_available ensure_required_keys_present && ensure_env_available

View File

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

View File

@@ -1,13 +1,14 @@
module Mrsk::Utils module Mrsk::Utils
extend self 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) def argumentize(argument, attributes, redacted: false)
Array(attributes).flat_map do |k, v| Array(attributes).flat_map do |key, value|
if v.present? if value.present?
[ argument, redacted ? redact("#{k}=#{v}") : "#{k}=#{v}" ] escaped_pair = [ key, value.to_s.dump.gsub(/`/, '\\\\`') ].join("=")
[ argument, redacted ? redact(escaped_pair) : escaped_pair ]
else else
[ argument, k ] [ argument, key ]
end end
end end
end end

View File

@@ -1,3 +1,3 @@
module Mrsk module Mrsk
VERSION = "0.7.0" VERSION = "0.8.2"
end end

View File

@@ -14,7 +14,7 @@ class CliAccessoryTest < CliTestCase
end end
test "boot" do 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 end
test "exec" do test "exec" do
@@ -31,6 +31,14 @@ class CliAccessoryTest < CliTestCase
end end
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 private
def run_command(*command) def run_command(*command)
stdouted { Mrsk::Cli::Accessory.start([*command, "-c", "test/fixtures/deploy_with_accessories.yml"]) } stdouted { Mrsk::Cli::Accessory.start([*command, "-c", "test/fixtures/deploy_with_accessories.yml"]) }

View File

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

@@ -10,7 +10,7 @@ class CliMainTest < CliTestCase
run_command("details") # Preheat MRSK const run_command("details") # Preheat MRSK const
run_command("rollback", "nonsense").tap do |output| run_command("rollback", "nonsense").tap do |output|
assert_match /docker container ls -a --filter label=service=app --format '{{ .Names }}'/, 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 assert_match /The app version 'nonsense' is not available as a container/, output
end end
end end
@@ -19,12 +19,35 @@ class CliMainTest < CliTestCase
Mrsk::Cli::Main.any_instance.stubs(:container_name_available?).returns(true) Mrsk::Cli::Main.any_instance.stubs(:container_name_available?).returns(true)
run_command("rollback", "123").tap do |output| run_command("rollback", "123").tap do |output|
assert_match /Stop current version, then start version 123/, output assert_match /Start version 123, then stop the old version/, output
assert_match /docker ps -q --filter label=service=app | xargs docker stop/, output assert_match /docker ps -q --filter label=service=app | xargs docker stop/, output
assert_match /docker start app-123/, output assert_match /docker start app-123/, output
end end
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 private
def run_command(*command) def run_command(*command)

View File

@@ -49,11 +49,11 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
test "run" do test "run" do
assert_equal \ 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(" ") @mysql.run.join(" ")
assert_equal \ 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(" ") @redis.run.join(" ")
end end
@@ -78,7 +78,7 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
test "execute in new container" do test "execute in new container" do
assert_equal \ 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(" ") @mysql.execute_in_new_container("mysql", "-u", "root").join(" ")
end end
@@ -90,7 +90,7 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
test "execute in new container over ssh" do test "execute in new container over ssh" do
@mysql.stub(:run_over_ssh, ->(cmd) { cmd.join(" ") }) 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") @mysql.execute_in_new_container_over_ssh("mysql", "-u", "root")
end end
end end
@@ -106,29 +106,29 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
test "logs" do test "logs" do
assert_equal \ assert_equal \
"docker logs app-mysql -t 2>&1", "docker logs app-mysql --timestamps 2>&1",
@mysql.logs.join(" ") @mysql.logs.join(" ")
assert_equal \ 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(" ") @mysql.logs(since: "5m", lines: 100, grep: "thing").join(" ")
end end
test "follow logs" do test "follow logs" do
assert_equal \ 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 @mysql.follow_logs
end end
test "remove container" do test "remove container" do
assert_equal \ assert_equal \
"docker container prune -f --filter label=service=app-mysql", "docker container prune --force --filter label=service=app-mysql",
@mysql.remove_container.join(" ") @mysql.remove_container.join(" ")
end end
test "remove image" do test "remove image" do
assert_equal \ 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(" ") @mysql.remove_image.join(" ")
end end
end end

View File

@@ -14,7 +14,7 @@ class CommandsAppTest < ActiveSupport::TestCase
test "run" do test "run" do
assert_equal \ 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=\"5\" --label traefik.http.middlewares.app.retry.initialinterval=\"500ms\" dhh/app:999",
@app.run.join(" ") @app.run.join(" ")
end end
@@ -22,7 +22,7 @@ class CommandsAppTest < ActiveSupport::TestCase
@config[:volumes] = ["/local/path:/container/path" ] @config[:volumes] = ["/local/path:/container/path" ]
assert_equal \ 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=\"5\" --label traefik.http.middlewares.app.retry.initialinterval=\"500ms\" dhh/app:999",
@app.run.join(" ") @app.run.join(" ")
end end
@@ -30,7 +30,7 @@ class CommandsAppTest < ActiveSupport::TestCase
@config[:healthcheck] = { "path" => "/healthz" } @config[:healthcheck] = { "path" => "/healthz" }
assert_equal \ 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=/healthz --label traefik.http.services.app.loadbalancer.healthcheck.interval=1s --label traefik.http.middlewares.app.retry.attempts=3 --label traefik.http.middlewares.app.retry.initialinterval=500ms dhh/app:999", "docker run --detach --restart unless-stopped --log-opt max-size=10m --name app-999 -e RAILS_MASTER_KEY=\"456\" --label service=\"app\" --label role=\"web\" --label traefik.http.routers.app.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.services.app.loadbalancer.healthcheck.path=\"/healthz\" --label traefik.http.services.app.loadbalancer.healthcheck.interval=\"1s\" --label traefik.http.middlewares.app.retry.attempts=\"5\" --label traefik.http.middlewares.app.retry.initialinterval=\"500ms\" dhh/app:999",
@app.run.join(" ") @app.run.join(" ")
end end
@@ -42,7 +42,7 @@ class CommandsAppTest < ActiveSupport::TestCase
test "stop" do test "stop" do
assert_equal \ 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(" ") @app.stop.join(" ")
end end
@@ -55,38 +55,38 @@ class CommandsAppTest < ActiveSupport::TestCase
test "logs" do test "logs" do
assert_equal \ 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(" ") @app.logs.join(" ")
assert_equal \ 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(" ") @app.logs(since: "5m").join(" ")
assert_equal \ 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(" ") @app.logs(lines: "100").join(" ")
assert_equal \ 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(" ") @app.logs(since: "5m", lines: "100").join(" ")
assert_equal \ 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(" ") @app.logs(grep: "my-id").join(" ")
assert_equal \ 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(" ") @app.logs(since: "5m", grep: "my-id").join(" ")
end end
test "follow logs" do test "follow logs" do
@app.stub(:run_over_ssh, ->(cmd, host:) { cmd.join(" ") }) do @app.stub(:run_over_ssh, ->(cmd, host:) { cmd.join(" ") }) do
assert_equal \ 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") @app.follow_logs(host: "app-1")
assert_equal \ 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") @app.follow_logs(host: "app-1", grep: "Completed")
end end
end end
@@ -94,7 +94,7 @@ class CommandsAppTest < ActiveSupport::TestCase
test "execute in new container" do test "execute in new container" do
assert_equal \ 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(" ") @app.execute_in_new_container("bin/rails", "db:setup").join(" ")
end end
@@ -106,7 +106,7 @@ class CommandsAppTest < ActiveSupport::TestCase
test "execute in new container over ssh" do test "execute in new container over ssh" do
@app.stub(:run_over_ssh, ->(cmd, host:) { cmd.join(" ") }) 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") @app.execute_in_new_container_over_ssh("bin/rails", "c", host: "app-1")
end end
end end
@@ -145,13 +145,13 @@ class CommandsAppTest < ActiveSupport::TestCase
test "current_container_id" do test "current_container_id" do
assert_equal \ assert_equal \
"docker ps -q --filter label=service=app", "docker ps --quiet --filter label=service=app",
@app.current_container_id.join(" ") @app.current_container_id.join(" ")
end end
test "container_id_for" do test "container_id_for" do
assert_equal \ 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(" ") @app.container_id_for(container_name: "app-999").join(" ")
end end

View File

@@ -16,7 +16,7 @@ class CommandsAuditorTest < ActiveSupport::TestCase
test "broadcast" do test "broadcast" do
assert_match \ assert_match \
/echo '.* app removed container' \| bin\/audit_broadcast/, /bin\/audit_broadcast '\[.*\] app removed container'/,
new_command.broadcast("app removed container").join(" ") new_command.broadcast("app removed container").join(" ")
end end

View File

@@ -9,7 +9,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase
builder = new_builder_command builder = new_builder_command
assert_equal "multiarch", builder.name assert_equal "multiarch", builder.name
assert_equal \ 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 .", "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(" ") builder.push.join(" ")
end end
@@ -17,7 +17,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase
builder = new_builder_command(builder: { "multiarch" => false }) builder = new_builder_command(builder: { "multiarch" => false })
assert_equal "native", builder.name assert_equal "native", builder.name
assert_equal \ assert_equal \
"docker build -t dhh/app:123 -t dhh/app:latest --label service=app . && docker push dhh/app:123", "docker build -t dhh/app:123 -t dhh/app:latest --label service=\"app\" . && docker push dhh/app:123",
builder.push.join(" ") builder.push.join(" ")
end end
@@ -25,7 +25,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase
builder = new_builder_command(builder: { "local" => { }, "remote" => { } }) builder = new_builder_command(builder: { "local" => { }, "remote" => { } })
assert_equal "multiarch/remote", builder.name assert_equal "multiarch/remote", builder.name
assert_equal \ 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 .", "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(" ") builder.push.join(" ")
end end
@@ -33,42 +33,42 @@ class CommandsBuilderTest < ActiveSupport::TestCase
builder = new_builder_command(builder: { "remote" => { "arch" => "amd64" } }) builder = new_builder_command(builder: { "remote" => { "arch" => "amd64" } })
assert_equal "native/remote", builder.name assert_equal "native/remote", builder.name
assert_equal \ 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 .", "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(" ") builder.push.join(" ")
end end
test "build args" do test "build args" do
builder = new_builder_command(builder: { "args" => { "a" => 1, "b" => 2 } }) builder = new_builder_command(builder: { "args" => { "a" => 1, "b" => 2 } })
assert_equal \ assert_equal \
"-t dhh/app:123 -t dhh/app:latest --label service=app --build-arg a=1 --build-arg b=2", "-t dhh/app:123 -t dhh/app:latest --label service=\"app\" --build-arg a=\"1\" --build-arg b=\"2\"",
builder.target.build_options.join(" ") builder.target.build_options.join(" ")
end end
test "build secrets" do test "build secrets" do
builder = new_builder_command(builder: { "secrets" => ["token_a", "token_b"] }) builder = new_builder_command(builder: { "secrets" => ["token_a", "token_b"] })
assert_equal \ assert_equal \
"-t dhh/app:123 -t dhh/app:latest --label service=app --secret id=token_a --secret id=token_b", "-t dhh/app:123 -t dhh/app:latest --label service=\"app\" --secret id=\"token_a\" --secret id=\"token_b\"",
builder.target.build_options.join(" ") builder.target.build_options.join(" ")
end end
test "native push with build args" do test "native push with build args" do
builder = new_builder_command(builder: { "multiarch" => false, "args" => { "a" => 1, "b" => 2 } }) builder = new_builder_command(builder: { "multiarch" => false, "args" => { "a" => 1, "b" => 2 } })
assert_equal \ 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", "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(" ") builder.push.join(" ")
end end
test "multiarch push with build args" do test "multiarch push with build args" do
builder = new_builder_command(builder: { "args" => { "a" => 1, "b" => 2 } }) builder = new_builder_command(builder: { "args" => { "a" => 1, "b" => 2 } })
assert_equal \ 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 .", "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(" ") builder.push.join(" ")
end end
test "native push with with build secrets" do test "native push with with build secrets" do
builder = new_builder_command(builder: { "multiarch" => false, "secrets" => [ "a", "b" ] }) builder = new_builder_command(builder: { "multiarch" => false, "secrets" => [ "a", "b" ] })
assert_equal \ 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", "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(" ") builder.push.join(" ")
end end

View File

@@ -10,7 +10,7 @@ class CommandsHealthcheckTest < ActiveSupport::TestCase
test "run" do test "run" do
assert_equal \ assert_equal \
"docker run -d --name healthcheck-app-123 -p 3999:3000 --label service=healthcheck-app dhh/app:123", "docker run --detach --name healthcheck-app-123 --publish 3999:3000 --label service=healthcheck-app dhh/app:123",
new_command.run.join(" ") new_command.run.join(" ")
end end
@@ -18,13 +18,13 @@ class CommandsHealthcheckTest < ActiveSupport::TestCase
@config[:healthcheck] = { "port" => 3001 } @config[:healthcheck] = { "port" => 3001 }
assert_equal \ assert_equal \
"docker run -d --name healthcheck-app-123 -p 3999:3001 --label service=healthcheck-app dhh/app:123", "docker run --detach --name healthcheck-app-123 --publish 3999:3001 --label service=healthcheck-app dhh/app:123",
new_command.run.join(" ") new_command.run.join(" ")
end end
test "curl" do test "curl" do
assert_equal \ assert_equal \
"curl --silent --output /dev/null --write-out '%{http_code}' http://localhost:3999/up", "curl --silent --output /dev/null --write-out '%{http_code}' --max-time 2 http://localhost:3999/up",
new_command.curl.join(" ") new_command.curl.join(" ")
end end
@@ -32,19 +32,19 @@ class CommandsHealthcheckTest < ActiveSupport::TestCase
@config[:healthcheck] = { "path" => "/healthz" } @config[:healthcheck] = { "path" => "/healthz" }
assert_equal \ assert_equal \
"curl --silent --output /dev/null --write-out '%{http_code}' http://localhost:3999/healthz", "curl --silent --output /dev/null --write-out '%{http_code}' --max-time 2 http://localhost:3999/healthz",
new_command.curl.join(" ") new_command.curl.join(" ")
end end
test "stop" do test "stop" do
assert_equal \ assert_equal \
"docker container ls -a -f name=healthcheck-app -q | xargs docker stop", "docker container ls --all --filter name=healthcheck-app --quiet | xargs docker stop",
new_command.stop.join(" ") new_command.stop.join(" ")
end end
test "remove" do test "remove" do
assert_equal \ assert_equal \
"docker container ls -a -f name=healthcheck-app -q | xargs docker container rm", "docker container ls --all --filter name=healthcheck-app --quiet | xargs docker container rm",
new_command.remove.join(" ") new_command.remove.join(" ")
end end

View File

@@ -14,10 +14,25 @@ class CommandsRegistryTest < ActiveSupport::TestCase
end end
test "registry login" do 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 end
test "registry logout" do 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
end end

View File

@@ -10,7 +10,7 @@ class CommandsTraefikTest < ActiveSupport::TestCase
test "run" do test "run" do
assert_equal \ 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(" ") new_command.run.join(" ")
end end
@@ -34,49 +34,49 @@ class CommandsTraefikTest < ActiveSupport::TestCase
test "traefik logs" do test "traefik logs" do
assert_equal \ assert_equal \
"docker logs traefik -t 2>&1", "docker logs traefik --timestamps 2>&1",
new_command.logs.join(" ") new_command.logs.join(" ")
end end
test "traefik logs since 2h" do test "traefik logs since 2h" do
assert_equal \ assert_equal \
"docker logs traefik --since 2h -t 2>&1", "docker logs traefik --since 2h --timestamps 2>&1",
new_command.logs(since: '2h').join(" ") new_command.logs(since: '2h').join(" ")
end end
test "traefik logs last 10 lines" do test "traefik logs last 10 lines" do
assert_equal \ assert_equal \
"docker logs traefik -n 10 -t 2>&1", "docker logs traefik --tail 10 --timestamps 2>&1",
new_command.logs(lines: 10).join(" ") new_command.logs(lines: 10).join(" ")
end end
test "traefik logs with grep hello!" do test "traefik logs with grep hello!" do
assert_equal \ assert_equal \
"docker logs traefik -t 2>&1 | grep 'hello!'", "docker logs traefik --timestamps 2>&1 | grep 'hello!'",
new_command.logs(grep: 'hello!').join(" ") new_command.logs(grep: 'hello!').join(" ")
end end
test "traefik remove container" do test "traefik remove container" do
assert_equal \ 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(" ") new_command.remove_container.join(" ")
end end
test "traefik remove image" do test "traefik remove image" do
assert_equal \ 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(" ") new_command.remove_image.join(" ")
end end
test "traefik follow logs" do test "traefik follow logs" do
assert_equal \ 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) new_command.follow_logs(host: @config[:servers].first)
end end
test "traefik follow logs with grep hello!" do test "traefik follow logs with grep hello!" do
assert_equal \ 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!') new_command.follow_logs(host: @config[:servers].first, grep: 'hello!')
end end

View File

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

View File

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

View File

@@ -89,7 +89,7 @@ class ConfigurationTest < ActiveSupport::TestCase
end end
test "env args" do 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 end
test "env args with clear and secrets" do test "env args with clear and secrets" do
@@ -98,7 +98,7 @@ class ConfigurationTest < ActiveSupport::TestCase
env: { "clear" => { "PORT" => "3000" }, "secret" => [ "PASSWORD" ] } 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) assert config.env_args[1].is_a?(SSHKit::Redaction)
ensure ensure
ENV["PASSWORD"] = nil ENV["PASSWORD"] = nil
@@ -109,7 +109,7 @@ class ConfigurationTest < ActiveSupport::TestCase
env: { "clear" => { "PORT" => "3000" } } env: { "clear" => { "PORT" => "3000" } }
}) }) }) })
assert_equal [ "-e", "PORT=3000" ], config.env_args assert_equal [ "-e", "PORT=\"3000\"" ], config.env_args
end end
test "env args with only secrets" do test "env args with only secrets" do
@@ -118,7 +118,7 @@ class ConfigurationTest < ActiveSupport::TestCase
env: { "secret" => [ "PASSWORD" ] } 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) assert config.env_args[1].is_a?(SSHKit::Redaction)
ensure ensure
ENV["PASSWORD"] = nil ENV["PASSWORD"] = nil
@@ -181,6 +181,6 @@ class ConfigurationTest < ActiveSupport::TestCase
end end
test "to_h" do 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
end end

View File

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