Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
60a19f0b30 | ||
|
|
2d0a7e1b67 | ||
|
|
49df19fb0d | ||
|
|
cef8fddfb4 | ||
|
|
c59eb00dd0 | ||
|
|
43f7409de0 | ||
|
|
448ea7719f | ||
|
|
ab54dbdb8b | ||
|
|
ac3771447a | ||
|
|
daa0c9b5be | ||
|
|
d3936363d0 | ||
|
|
cfc8fa0590 | ||
|
|
161ebe4bc1 | ||
|
|
514b2aa243 | ||
|
|
18031bc552 | ||
|
|
d8c61004e4 | ||
|
|
c4df440c79 | ||
|
|
fb1718ca6d | ||
|
|
7d17a6c3b5 | ||
|
|
f4133de896 | ||
|
|
a9488e935d | ||
|
|
ac61528dfc | ||
|
|
0eb7a8d087 | ||
|
|
7559f439e9 | ||
|
|
54a5b90d8f | ||
|
|
a245adfad2 | ||
|
|
f386c3bdab | ||
|
|
2a3e576182 | ||
|
|
f3e3196ce5 | ||
|
|
fca5b11682 | ||
|
|
d09cddde8d | ||
|
|
3969f56fa6 | ||
|
|
c60cc92dfe | ||
|
|
cb3c5a53f4 | ||
|
|
ef04410d77 | ||
|
|
bd8f13dd5e | ||
|
|
2146f6d0ec | ||
|
|
52d8c112d3 | ||
|
|
c9afd66222 | ||
|
|
36c458407f | ||
|
|
c137b38c87 | ||
|
|
f851d6528d | ||
|
|
12632aa7f9 | ||
|
|
2f97bc488f |
14
Gemfile.lock
14
Gemfile.lock
@@ -1,11 +1,12 @@
|
|||||||
PATH
|
PATH
|
||||||
remote: .
|
remote: .
|
||||||
specs:
|
specs:
|
||||||
mrsk (0.10.1)
|
mrsk (0.11.0)
|
||||||
activesupport (>= 7.0)
|
activesupport (>= 7.0)
|
||||||
bcrypt_pbkdf (~> 1.0)
|
bcrypt_pbkdf (~> 1.0)
|
||||||
dotenv (~> 2.8)
|
dotenv (~> 2.8)
|
||||||
ed25519 (~> 1.2)
|
ed25519 (~> 1.2)
|
||||||
|
net-ssh (~> 7.0)
|
||||||
sshkit (~> 1.21)
|
sshkit (~> 1.21)
|
||||||
thor (~> 1.2)
|
thor (~> 1.2)
|
||||||
zeitwerk (~> 2.5)
|
zeitwerk (~> 2.5)
|
||||||
@@ -35,13 +36,18 @@ GEM
|
|||||||
builder (3.2.4)
|
builder (3.2.4)
|
||||||
concurrent-ruby (1.2.2)
|
concurrent-ruby (1.2.2)
|
||||||
crass (1.0.6)
|
crass (1.0.6)
|
||||||
debug (1.7.1)
|
debug (1.7.2)
|
||||||
|
irb (>= 1.5.0)
|
||||||
|
reline (>= 0.3.1)
|
||||||
dotenv (2.8.1)
|
dotenv (2.8.1)
|
||||||
ed25519 (1.3.0)
|
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)
|
||||||
loofah (2.19.1)
|
io-console (0.6.0)
|
||||||
|
irb (1.6.3)
|
||||||
|
reline (>= 0.3.0)
|
||||||
|
loofah (2.20.0)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.5.9)
|
nokogiri (>= 1.5.9)
|
||||||
method_source (1.0.0)
|
method_source (1.0.0)
|
||||||
@@ -74,6 +80,8 @@ GEM
|
|||||||
thor (~> 1.0)
|
thor (~> 1.0)
|
||||||
zeitwerk (~> 2.5)
|
zeitwerk (~> 2.5)
|
||||||
rake (13.0.6)
|
rake (13.0.6)
|
||||||
|
reline (0.3.3)
|
||||||
|
io-console (~> 0.5)
|
||||||
ruby2_keywords (0.0.5)
|
ruby2_keywords (0.0.5)
|
||||||
sshkit (1.21.4)
|
sshkit (1.21.4)
|
||||||
net-scp (>= 1.1.2)
|
net-scp (>= 1.1.2)
|
||||||
|
|||||||
91
README.md
91
README.md
@@ -6,6 +6,8 @@ Watch the screencast: https://www.youtube.com/watch?v=LL1cV2FXZ5I
|
|||||||
|
|
||||||
Join us on Discord: https://discord.gg/YgHVT7GCXS
|
Join us on Discord: https://discord.gg/YgHVT7GCXS
|
||||||
|
|
||||||
|
Ask questions: https://github.com/mrsked/mrsk/discussions
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
If you have a Ruby environment available, you can install MRSK globally with:
|
If you have a Ruby environment available, you can install MRSK globally with:
|
||||||
@@ -14,13 +16,13 @@ If you have a Ruby environment available, you can install MRSK globally with:
|
|||||||
gem install mrsk
|
gem install mrsk
|
||||||
```
|
```
|
||||||
|
|
||||||
...otherwise, you can run a dockerized version via an alias (add this to your ${SHELL}rc to simplify re-use):
|
...otherwise, you can run a dockerized version via an alias (add this to your .bashrc or similar to simplify re-use):
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
alias mrsk='docker run --rm -it -v $HOME/.ssh:/root/.ssh -v /var/run/docker.sock:/var/run/docker.sock -v ${PWD}/:/workdir ghcr.io/mrsked/mrsk'
|
alias mrsk='docker run --rm -it -v $HOME/.ssh:/root/.ssh -v /var/run/docker.sock:/var/run/docker.sock -v ${PWD}/:/workdir ghcr.io/mrsked/mrsk'
|
||||||
```
|
```
|
||||||
|
|
||||||
Then, inside your app directory, run `mrsk init` (or `mrsk init --bundle` within Rails apps where you want a bin/mrsk binstub). Now edit the new file `config/deploy.yml`. It could look as simple as this:
|
Then, inside your app directory, run `mrsk init` (or `mrsk init --bundle` within Rails 7+ apps where you want a bin/mrsk binstub). Now edit the new file `config/deploy.yml`. It could look as simple as this:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
service: hey
|
service: hey
|
||||||
@@ -191,6 +193,15 @@ ssh:
|
|||||||
user: app
|
user: app
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If you are using non-root user, you need to bootstrap your servers manually, before using them with MRSK. On Ubuntu, you'd do:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt update
|
||||||
|
sudo apt upgrade -y
|
||||||
|
sudo apt install -y docker.io curl git
|
||||||
|
sudo usermod -a -G docker ubuntu
|
||||||
|
```
|
||||||
|
|
||||||
### Using a proxy SSH host
|
### Using a proxy SSH host
|
||||||
|
|
||||||
If you need to connect to server through a proxy host, you can use `ssh/proxy`:
|
If you need to connect to server through a proxy host, you can use `ssh/proxy`:
|
||||||
@@ -207,6 +218,13 @@ ssh:
|
|||||||
proxy: "app@192.168.0.1"
|
proxy: "app@192.168.0.1"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Also if you need specific proxy command to connect to the server:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
ssh:
|
||||||
|
proxy_command: aws ssm start-session --target %h --document-name AWS-StartSSHSession --parameters 'portNumber=%p' --region=us-east-1 ## ssh via aws ssm
|
||||||
|
```
|
||||||
|
|
||||||
### Using env variables
|
### Using env variables
|
||||||
|
|
||||||
You can inject env variables into the app containers using `env`:
|
You can inject env variables into the app containers using `env`:
|
||||||
@@ -288,8 +306,9 @@ You can specialize the default Traefik rules by setting labels on the containers
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
labels:
|
labels:
|
||||||
traefik.http.routers.hey.rule: Host(`app.hey.com`)
|
traefik.http.routers.hey-web.rule: Host(`app.hey.com`)
|
||||||
```
|
```
|
||||||
|
Traefik rules are in the "service-role-destination" format. The default role will be `web` if no rule is specified. If the destination is not specified, it is not included. To give an example, the above rule would become "traefik.http.routers.hey-web.rule" if it was for the "staging" destination.
|
||||||
|
|
||||||
Note: The backticks are needed to ensure the rule is passed in correctly and not treated as command substitution by Bash!
|
Note: The backticks are needed to ensure the rule is passed in correctly and not treated as command substitution by Bash!
|
||||||
|
|
||||||
@@ -439,9 +458,9 @@ RUN --mount=type=secret,id=GITHUB_TOKEN \
|
|||||||
rm -rf /usr/local/bundle/cache
|
rm -rf /usr/local/bundle/cache
|
||||||
```
|
```
|
||||||
|
|
||||||
### Using command arguments for Traefik
|
### Traefik command arguments
|
||||||
|
|
||||||
You can customize the traefik command line:
|
Customize the Traefik command line using `args`:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
traefik:
|
traefik:
|
||||||
@@ -450,37 +469,70 @@ traefik:
|
|||||||
accesslog.format: json
|
accesslog.format: json
|
||||||
```
|
```
|
||||||
|
|
||||||
This will start the traefik container with `--accesslog=true accesslog.format=json`.
|
This starts the Traefik container with `--accesslog=true --accesslog.format=json` arguments.
|
||||||
|
|
||||||
### Traefik's host port binding
|
### Traefik host port binding
|
||||||
|
|
||||||
By default Traefik binds to port 80 of the host machine, it can be configured to use an alternative port:
|
Traefik binds to port 80 by default. Specify an alternative port using `host_port`:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
traefik:
|
traefik:
|
||||||
host_port: 8080
|
host_port: 8080
|
||||||
```
|
```
|
||||||
|
|
||||||
### Configure docker options for traefik
|
### Traefik version, upgrades, and custom images
|
||||||
|
|
||||||
We allow users to pass additional docker options to the trafik container like
|
MRSK runs the traefik:v2.9 image to track Traefik 2.9.x releases.
|
||||||
|
|
||||||
|
To pin Traefik to a specific version or an image published to your registry,
|
||||||
|
specify `image`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
traefik:
|
||||||
|
image: traefik:v2.10.0-rc1
|
||||||
|
```
|
||||||
|
|
||||||
|
This is useful for downgrading Traefik if there's an unexpected breaking
|
||||||
|
change in a minor version release, upgrading Traefik to test forthcoming
|
||||||
|
releases, or running your own Traefik-derived image.
|
||||||
|
|
||||||
|
MRSK has not been tested for compatibility with Traefik 3 betas. Please do!
|
||||||
|
|
||||||
|
### Traefik container configuration
|
||||||
|
|
||||||
|
Pass additional Docker configuration for the Traefik container using `options`:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
traefik:
|
traefik:
|
||||||
options:
|
options:
|
||||||
publish:
|
publish:
|
||||||
- 8080:8080
|
- 8080:8080
|
||||||
volumes:
|
volumes:
|
||||||
- /tmp/example.json:/tmp/example.json
|
- /tmp/example.json:/tmp/example.json
|
||||||
memory: 512m
|
memory: 512m
|
||||||
```
|
```
|
||||||
|
|
||||||
This will start the traefik container with a command like: `docker run ... --volume /tmp/example.json:/tmp/example.json --publish 8080:8080 `
|
This starts the Traefik container with `--volume /tmp/example.json:/tmp/example.json --publish 8080:8080 --memory 512m` arguments to `docker run`.
|
||||||
|
|
||||||
|
### Traefik container lables
|
||||||
|
|
||||||
### Configure alternate entrypoints for traefik
|
Add labels to Traefik Docker container.
|
||||||
|
|
||||||
You can configure multiple entrypoints for traefik like so:
|
```yaml
|
||||||
|
traefik:
|
||||||
|
lables:
|
||||||
|
- traefik.enable: true
|
||||||
|
- traefik.http.routers.dashboard.rule: Host(`traefik.example.com`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))
|
||||||
|
- traefik.http.routers.dashboard.service: api@internal
|
||||||
|
- traefik.http.routers.dashboard.middlewares: auth
|
||||||
|
- traefik.http.middlewares.auth.basicauth.users: test:$2y$05$H2o72tMaO.TwY1wNQUV1K.fhjRgLHRDWohFvUZOJHBEtUXNKrqUKi # test:password
|
||||||
|
```
|
||||||
|
|
||||||
|
This labels Traefik container with `--label traefik.http.routers.dashboard.middlewares=\"auth\"` and so on.
|
||||||
|
|
||||||
|
### Traefik alternate entrypoints
|
||||||
|
|
||||||
|
You can configure multiple entrypoints for Traefik like so:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
service: myservice
|
service: myservice
|
||||||
@@ -540,7 +592,7 @@ accessories:
|
|||||||
memory: "2GB"
|
memory: "2GB"
|
||||||
redis:
|
redis:
|
||||||
image: redis:latest
|
image: redis:latest
|
||||||
role:
|
roles:
|
||||||
- web
|
- web
|
||||||
port: "36379:6379"
|
port: "36379:6379"
|
||||||
volumes:
|
volumes:
|
||||||
@@ -610,18 +662,21 @@ That'll post a line like follows to a preconfigured chatbot in Basecamp:
|
|||||||
[My App] [dhh] Rolled back to version d264c4e92470ad1bd18590f04466787262f605de
|
[My App] [dhh] Rolled back to version d264c4e92470ad1bd18590f04466787262f605de
|
||||||
```
|
```
|
||||||
|
|
||||||
### Using custom healthcheck path or port
|
### Custom healthcheck
|
||||||
|
|
||||||
MRSK defaults to checking the health of your application again `/up` on port 3000. You can tailor both with the `healthcheck` setting:
|
MRSK defaults to checking the health of your application again `/up` on port 3000 up to 7 times. You can tailor the behaviour with the `healthcheck` setting:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
healthcheck:
|
healthcheck:
|
||||||
path: /healthz
|
path: /healthz
|
||||||
port: 4000
|
port: 4000
|
||||||
|
max_attempts: 7
|
||||||
```
|
```
|
||||||
|
|
||||||
This will ensure your application is configured with a traefik label for the healthcheck against `/healthz` and that the pre-deploy healthcheck that MRSK performs is done against the same path on port 4000.
|
This will ensure your application is configured with a traefik label for the healthcheck against `/healthz` and that the pre-deploy healthcheck that MRSK performs is done against the same path on port 4000.
|
||||||
|
|
||||||
|
The healthcheck also allows for an optional `max_attempts` setting, which will attempt the healthcheck up to the specified number of times before failing the deploy. This is useful for applications that take a while to start up. The default is 7.
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
### Running commands on servers
|
### Running commands on servers
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
module Mrsk::Cli
|
module Mrsk::Cli
|
||||||
|
class LockError < StandardError; end
|
||||||
end
|
end
|
||||||
|
|
||||||
# SSHKit uses instance eval, so we need a global const for ergonomics
|
# SSHKit uses instance eval, so we need a global const for ergonomics
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ module Mrsk::Cli
|
|||||||
class Base < Thor
|
class Base < Thor
|
||||||
include SSHKit::DSL
|
include SSHKit::DSL
|
||||||
|
|
||||||
class LockError < StandardError; end
|
|
||||||
|
|
||||||
def self.exit_on_failure?() true end
|
def self.exit_on_failure?() true end
|
||||||
|
|
||||||
class_option :verbose, type: :boolean, aliases: "-v", desc: "Detailed logging"
|
class_option :verbose, type: :boolean, aliases: "-v", desc: "Detailed logging"
|
||||||
@@ -82,8 +80,11 @@ module Mrsk::Cli
|
|||||||
acquire_lock
|
acquire_lock
|
||||||
|
|
||||||
yield
|
yield
|
||||||
ensure
|
|
||||||
release_lock
|
release_lock
|
||||||
|
rescue
|
||||||
|
error " \e[31mDeploy lock was not released\e[0m" if MRSK.lock_count > 0
|
||||||
|
raise
|
||||||
end
|
end
|
||||||
|
|
||||||
def acquire_lock
|
def acquire_lock
|
||||||
@@ -95,9 +96,10 @@ module Mrsk::Cli
|
|||||||
rescue SSHKit::Runner::ExecuteError => e
|
rescue SSHKit::Runner::ExecuteError => e
|
||||||
if e.message =~ /cannot create directory/
|
if e.message =~ /cannot create directory/
|
||||||
invoke "mrsk:cli:lock:status", []
|
invoke "mrsk:cli:lock:status", []
|
||||||
|
raise LockError, "Deploy lock found"
|
||||||
|
else
|
||||||
|
raise e
|
||||||
end
|
end
|
||||||
|
|
||||||
raise LockError, "Deploy lock found"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def release_lock
|
def release_lock
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
class Mrsk::Cli::Healthcheck < Mrsk::Cli::Base
|
class Mrsk::Cli::Healthcheck < Mrsk::Cli::Base
|
||||||
MAX_ATTEMPTS = 7
|
|
||||||
|
|
||||||
class HealthcheckError < StandardError; end
|
class HealthcheckError < StandardError; end
|
||||||
|
|
||||||
@@ -13,6 +12,7 @@ class Mrsk::Cli::Healthcheck < Mrsk::Cli::Base
|
|||||||
|
|
||||||
target = "Health check against #{MRSK.config.healthcheck["path"]}"
|
target = "Health check against #{MRSK.config.healthcheck["path"]}"
|
||||||
attempt = 1
|
attempt = 1
|
||||||
|
max_attempts = MRSK.config.healthcheck["max_attempts"]
|
||||||
|
|
||||||
begin
|
begin
|
||||||
status = capture_with_info(*MRSK.healthcheck.curl)
|
status = capture_with_info(*MRSK.healthcheck.curl)
|
||||||
@@ -23,8 +23,8 @@ class Mrsk::Cli::Healthcheck < Mrsk::Cli::Base
|
|||||||
raise HealthcheckError, "#{target} failed with status #{status}"
|
raise HealthcheckError, "#{target} failed with status #{status}"
|
||||||
end
|
end
|
||||||
rescue SSHKit::Command::Failed
|
rescue SSHKit::Command::Failed
|
||||||
if attempt <= MAX_ATTEMPTS
|
if attempt <= max_attempts
|
||||||
info "#{target} failed to respond, retrying in #{attempt}s..."
|
info "#{target} failed to respond, retrying in #{attempt}s (attempt #{attempt}/#{max_attempts})..."
|
||||||
sleep attempt
|
sleep attempt
|
||||||
attempt += 1
|
attempt += 1
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class Mrsk::Cli::Lock < Mrsk::Cli::Base
|
|||||||
message = options[:message]
|
message = options[:message]
|
||||||
handle_missing_lock do
|
handle_missing_lock do
|
||||||
on(MRSK.primary_host) { execute *MRSK.lock.acquire(message, MRSK.config.version) }
|
on(MRSK.primary_host) { execute *MRSK.lock.acquire(message, MRSK.config.version) }
|
||||||
say "Set the deploy lock"
|
say "Acquired the deploy lock"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ class Mrsk::Cli::Lock < Mrsk::Cli::Base
|
|||||||
def release
|
def release
|
||||||
handle_missing_lock do
|
handle_missing_lock do
|
||||||
on(MRSK.primary_host) { execute *MRSK.lock.release }
|
on(MRSK.primary_host) { execute *MRSK.lock.release }
|
||||||
say "Removed the deploy lock"
|
say "Released the deploy lock"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -77,21 +77,26 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
|
|||||||
with_lock do
|
with_lock do
|
||||||
MRSK.config.version = version
|
MRSK.config.version = version
|
||||||
|
|
||||||
if container_name_available?(MRSK.config.service_with_version)
|
if container_available?(version)
|
||||||
say "Start version #{version}, then wait #{MRSK.config.readiness_delay}s for app to boot before stopping the old version...", :magenta
|
say "Start version #{version}, then wait #{MRSK.config.readiness_delay}s for app to boot before stopping the old version...", :magenta
|
||||||
|
|
||||||
cli = self
|
cli = self
|
||||||
old_version = nil
|
old_version = nil
|
||||||
|
|
||||||
on(MRSK.hosts) do |host|
|
on(MRSK.hosts) do |host|
|
||||||
old_version = capture_with_info(*MRSK.app.current_running_version).strip.presence
|
roles = MRSK.roles_on(host)
|
||||||
|
|
||||||
execute *MRSK.app.start
|
roles.each do |role|
|
||||||
|
app = MRSK.app(role: role)
|
||||||
|
old_version = capture_with_info(*app.current_running_version).strip.presence
|
||||||
|
|
||||||
if old_version
|
execute *app.start
|
||||||
sleep MRSK.config.readiness_delay
|
|
||||||
|
|
||||||
execute *MRSK.app.stop(version: old_version), raise_on_non_zero_exit: false
|
if old_version
|
||||||
|
sleep MRSK.config.readiness_delay
|
||||||
|
|
||||||
|
execute *app.stop(version: old_version), raise_on_non_zero_exit: false
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -119,7 +124,7 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
|
|||||||
desc "config", "Show combined config (including secrets!)"
|
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::Utils.redacted(MRSK.config.to_h).to_yaml
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -214,10 +219,15 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
|
|||||||
subcommand "lock", Mrsk::Cli::Lock
|
subcommand "lock", Mrsk::Cli::Lock
|
||||||
|
|
||||||
private
|
private
|
||||||
def container_name_available?(container_name, host: MRSK.primary_host)
|
def container_available?(version, host: MRSK.primary_host)
|
||||||
container_names = nil
|
available = nil
|
||||||
on(host) { container_names = capture_with_info(*MRSK.app.list_container_names).split("\n") }
|
|
||||||
Array(container_names).include?(container_name)
|
on(host) do
|
||||||
|
first_role = MRSK.roles_on(host).first
|
||||||
|
available = capture_with_info(*MRSK.app(role: first_role).container_id_for_version(version)).present?
|
||||||
|
end
|
||||||
|
|
||||||
|
available
|
||||||
end
|
end
|
||||||
|
|
||||||
def deploy_options
|
def deploy_options
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ class Mrsk::Cli::Traefik < Mrsk::Cli::Base
|
|||||||
desc "boot", "Boot Traefik on servers"
|
desc "boot", "Boot Traefik on servers"
|
||||||
def boot
|
def boot
|
||||||
with_lock do
|
with_lock do
|
||||||
on(MRSK.traefik_hosts) { execute *MRSK.traefik.run, raise_on_non_zero_exit: false }
|
on(MRSK.traefik_hosts) do
|
||||||
|
execute *MRSK.registry.login
|
||||||
|
execute *MRSK.traefik.run, raise_on_non_zero_exit: false
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
module Mrsk::Commands
|
module Mrsk::Commands
|
||||||
class Base
|
class Base
|
||||||
delegate :redact, :argumentize, to: Mrsk::Utils
|
delegate :sensitive, :argumentize, to: Mrsk::Utils
|
||||||
|
|
||||||
attr_accessor :config
|
attr_accessor :config
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ class Mrsk::Commands::Builder::Base < Mrsk::Commands::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def build_args
|
def build_args
|
||||||
argumentize "--build-arg", args, redacted: true
|
argumentize "--build-arg", args, sensitive: true
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_secrets
|
def build_secrets
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ 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(lookup("username")), "-p", redact(lookup("password"))
|
docker :login, registry["server"], "-u", sensitive(lookup("username")), "-p", sensitive(lookup("password"))
|
||||||
end
|
end
|
||||||
|
|
||||||
def logout
|
def logout
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
class Mrsk::Commands::Traefik < Mrsk::Commands::Base
|
class Mrsk::Commands::Traefik < Mrsk::Commands::Base
|
||||||
delegate :optionize, to: Mrsk::Utils
|
delegate :argumentize, :optionize, to: Mrsk::Utils
|
||||||
|
|
||||||
IMAGE = "traefik:v2.9.9"
|
DEFAULT_IMAGE = "traefik:v2.9"
|
||||||
CONTAINER_PORT = 80
|
CONTAINER_PORT = 80
|
||||||
|
|
||||||
def run
|
def run
|
||||||
@@ -11,8 +11,9 @@ class Mrsk::Commands::Traefik < Mrsk::Commands::Base
|
|||||||
"--publish", port,
|
"--publish", port,
|
||||||
"--volume", "/var/run/docker.sock:/var/run/docker.sock",
|
"--volume", "/var/run/docker.sock:/var/run/docker.sock",
|
||||||
*config.logging_args,
|
*config.logging_args,
|
||||||
|
*label_args,
|
||||||
*docker_options_args,
|
*docker_options_args,
|
||||||
IMAGE,
|
image,
|
||||||
"--providers.docker",
|
"--providers.docker",
|
||||||
"--log.level=DEBUG",
|
"--log.level=DEBUG",
|
||||||
*cmd_option_args
|
*cmd_option_args
|
||||||
@@ -56,6 +57,18 @@ class Mrsk::Commands::Traefik < Mrsk::Commands::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
def label_args
|
||||||
|
argumentize "--label", labels
|
||||||
|
end
|
||||||
|
|
||||||
|
def labels
|
||||||
|
config.traefik["labels"] || []
|
||||||
|
end
|
||||||
|
|
||||||
|
def image
|
||||||
|
config.traefik.fetch("image") { DEFAULT_IMAGE }
|
||||||
|
end
|
||||||
|
|
||||||
def docker_options_args
|
def docker_options_args
|
||||||
optionize(config.traefik["options"] || {})
|
optionize(config.traefik["options"] || {})
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -143,6 +143,8 @@ class Mrsk::Configuration
|
|||||||
if raw_config.ssh.present? && raw_config.ssh["proxy"]
|
if raw_config.ssh.present? && raw_config.ssh["proxy"]
|
||||||
Net::SSH::Proxy::Jump.new \
|
Net::SSH::Proxy::Jump.new \
|
||||||
raw_config.ssh["proxy"].include?("@") ? raw_config.ssh["proxy"] : "root@#{raw_config.ssh["proxy"]}"
|
raw_config.ssh["proxy"].include?("@") ? raw_config.ssh["proxy"] : "root@#{raw_config.ssh["proxy"]}"
|
||||||
|
elsif raw_config.ssh.present? && raw_config.ssh["proxy_command"]
|
||||||
|
Net::SSH::Proxy::Command.new(raw_config.ssh["proxy_command"])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -156,7 +158,7 @@ class Mrsk::Configuration
|
|||||||
end
|
end
|
||||||
|
|
||||||
def healthcheck
|
def healthcheck
|
||||||
{ "path" => "/up", "port" => 3000 }.merge(raw_config.healthcheck || {})
|
{ "path" => "/up", "port" => 3000, "max_attempts" => 7 }.merge(raw_config.healthcheck || {})
|
||||||
end
|
end
|
||||||
|
|
||||||
def readiness_delay
|
def readiness_delay
|
||||||
|
|||||||
@@ -74,18 +74,22 @@ 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.#{traefik_service}.rule" => "PathPrefix(`/`)",
|
||||||
"traefik.http.services.#{config.service}.loadbalancer.healthcheck.path" => config.healthcheck["path"],
|
"traefik.http.services.#{traefik_service}.loadbalancer.healthcheck.path" => config.healthcheck["path"],
|
||||||
"traefik.http.services.#{config.service}.loadbalancer.healthcheck.interval" => "1s",
|
"traefik.http.services.#{traefik_service}.loadbalancer.healthcheck.interval" => "1s",
|
||||||
"traefik.http.middlewares.#{config.service}-retry.retry.attempts" => "5",
|
"traefik.http.middlewares.#{traefik_service}-retry.retry.attempts" => "5",
|
||||||
"traefik.http.middlewares.#{config.service}-retry.retry.initialinterval" => "500ms",
|
"traefik.http.middlewares.#{traefik_service}-retry.retry.initialinterval" => "500ms",
|
||||||
"traefik.http.routers.#{config.service}.middlewares" => "#{config.service}-retry@docker"
|
"traefik.http.routers.#{traefik_service}.middlewares" => "#{traefik_service}-retry@docker"
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{}
|
{}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def traefik_service
|
||||||
|
[ config.service, name, config.destination ].compact.join("-")
|
||||||
|
end
|
||||||
|
|
||||||
def custom_labels
|
def custom_labels
|
||||||
Hash.new.tap do |labels|
|
Hash.new.tap do |labels|
|
||||||
labels.merge!(config.labels) if config.labels.present?
|
labels.merge!(config.labels) if config.labels.present?
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ module Mrsk::Utils
|
|||||||
extend self
|
extend self
|
||||||
|
|
||||||
# Return a list of escaped shell arguments using the same named argument against the passed attributes (hash or array).
|
# Return a list of 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, sensitive: false)
|
||||||
Array(attributes).flat_map do |key, value|
|
Array(attributes).flat_map do |key, value|
|
||||||
if value.present?
|
if value.present?
|
||||||
escaped_pair = [ key, escape_shell_value(value) ].join("=")
|
attr = "#{key}=#{escape_shell_value(value)}"
|
||||||
[ argument, redacted ? redact(escaped_pair) : escaped_pair ]
|
attr = self.sensitive(attr, redaction: "#{key}=[REDACTED]") if sensitive
|
||||||
|
[ argument, attr]
|
||||||
else
|
else
|
||||||
[ argument, key ]
|
[ argument, key ]
|
||||||
end
|
end
|
||||||
@@ -17,7 +18,7 @@ module Mrsk::Utils
|
|||||||
# but redacts and expands secrets.
|
# but redacts and expands secrets.
|
||||||
def argumentize_env_with_secrets(env)
|
def argumentize_env_with_secrets(env)
|
||||||
if (secrets = env["secret"]).present?
|
if (secrets = env["secret"]).present?
|
||||||
argumentize("-e", secrets.to_h { |key| [ key, ENV.fetch(key) ] }, redacted: true) + argumentize("-e", env["clear"])
|
argumentize("-e", secrets.to_h { |key| [ key, ENV.fetch(key) ] }, sensitive: true) + argumentize("-e", env["clear"])
|
||||||
else
|
else
|
||||||
argumentize "-e", env.fetch("clear", env)
|
argumentize "-e", env.fetch("clear", env)
|
||||||
end
|
end
|
||||||
@@ -39,9 +40,37 @@ module Mrsk::Utils
|
|||||||
args.flat_map { |key, value| value.try(:map) { |entry| [key, entry] } || [ [ key, value ] ] }
|
args.flat_map { |key, value| value.try(:map) { |entry| [key, entry] } || [ [ key, value ] ] }
|
||||||
end
|
end
|
||||||
|
|
||||||
# Copied from SSHKit::Backend::Abstract#redact to be available inside Commands classes
|
# Marks sensitive values for redaction in logs and human-visible output.
|
||||||
def redact(arg) # Used in execute_command to hide redact() args a user passes in
|
# Pass `redaction:` to change the default `"[REDACTED]"` redaction, e.g.
|
||||||
arg.to_s.extend(SSHKit::Redaction) # to_s due to our inability to extend Integer, etc
|
# `sensitive "#{arg}=#{secret}", redaction: "#{arg}=xxxx"
|
||||||
|
def sensitive(...)
|
||||||
|
Mrsk::Utils::Sensitive.new(...)
|
||||||
|
end
|
||||||
|
|
||||||
|
def redacted(value)
|
||||||
|
case
|
||||||
|
when value.respond_to?(:redaction)
|
||||||
|
value.redaction
|
||||||
|
when value.respond_to?(:transform_values)
|
||||||
|
value.transform_values { |value| redacted value }
|
||||||
|
when value.respond_to?(:map)
|
||||||
|
value.map { |element| redacted element }
|
||||||
|
else
|
||||||
|
value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def unredacted(value)
|
||||||
|
case
|
||||||
|
when value.respond_to?(:unredacted)
|
||||||
|
value.unredacted
|
||||||
|
when value.respond_to?(:transform_values)
|
||||||
|
value.transform_values { |value| unredacted value }
|
||||||
|
when value.respond_to?(:map)
|
||||||
|
value.map { |element| unredacted element }
|
||||||
|
else
|
||||||
|
value
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Escape a value to make it safe for shell use.
|
# Escape a value to make it safe for shell use.
|
||||||
|
|||||||
19
lib/mrsk/utils/sensitive.rb
Normal file
19
lib/mrsk/utils/sensitive.rb
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
require "active_support/core_ext/module/delegation"
|
||||||
|
|
||||||
|
class Mrsk::Utils::Sensitive
|
||||||
|
# So SSHKit knows to redact these values.
|
||||||
|
include SSHKit::Redaction
|
||||||
|
|
||||||
|
attr_reader :unredacted, :redaction
|
||||||
|
delegate :to_s, to: :unredacted
|
||||||
|
delegate :inspect, to: :redaction
|
||||||
|
|
||||||
|
def initialize(value, redaction: "[REDACTED]")
|
||||||
|
@unredacted, @redaction = value, redaction
|
||||||
|
end
|
||||||
|
|
||||||
|
# Sensitive values won't leak into YAML output.
|
||||||
|
def encode_with(coder)
|
||||||
|
coder.represent_scalar nil, redaction
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
module Mrsk
|
module Mrsk
|
||||||
VERSION = "0.10.1"
|
VERSION = "0.11.0"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ Gem::Specification.new do |spec|
|
|||||||
|
|
||||||
spec.add_dependency "activesupport", ">= 7.0"
|
spec.add_dependency "activesupport", ">= 7.0"
|
||||||
spec.add_dependency "sshkit", "~> 1.21"
|
spec.add_dependency "sshkit", "~> 1.21"
|
||||||
|
spec.add_dependency "net-ssh", "~> 7.0"
|
||||||
spec.add_dependency "thor", "~> 1.2"
|
spec.add_dependency "thor", "~> 1.2"
|
||||||
spec.add_dependency "dotenv", "~> 2.8"
|
spec.add_dependency "dotenv", "~> 2.8"
|
||||||
spec.add_dependency "zeitwerk", "~> 2.5"
|
spec.add_dependency "zeitwerk", "~> 2.5"
|
||||||
|
|||||||
@@ -22,4 +22,8 @@ class CliTestCase < ActiveSupport::TestCase
|
|||||||
def stdouted
|
def stdouted
|
||||||
capture(:stdout) { yield }.strip
|
capture(:stdout) { yield }.strip
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
def stderred
|
||||||
|
capture(:stderr) { yield }.strip
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ class CliHealthcheckTest < CliTestCase
|
|||||||
.returns("200")
|
.returns("200")
|
||||||
|
|
||||||
run_command("perform").tap do |output|
|
run_command("perform").tap do |output|
|
||||||
assert_match "Health check against /up failed to respond, retrying in 1s...", output
|
assert_match "Health check against /up failed to respond, retrying in 1s (attempt 1/7)...", output
|
||||||
assert_match "Health check against /up failed to respond, retrying in 2s...", output
|
assert_match "Health check against /up failed to respond, retrying in 2s (attempt 2/7)...", output
|
||||||
assert_match "Health check against /up succeeded with 200 OK!", output
|
assert_match "Health check against /up succeeded with 200 OK!", output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -53,6 +53,46 @@ class CliMainTest < CliTestCase
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "deploy when locked" do
|
||||||
|
Thread.report_on_exception = false
|
||||||
|
|
||||||
|
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||||
|
.with { |*arg| arg[0..1] == [:mkdir, :mrsk_lock] }
|
||||||
|
.raises(RuntimeError, "mkdir: cannot create directory ‘mrsk_lock’: File exists")
|
||||||
|
|
||||||
|
Mrsk::Cli::Base.any_instance.expects(:invoke).with("mrsk:cli:lock:status", [])
|
||||||
|
|
||||||
|
assert_raises(Mrsk::Cli::LockError) do
|
||||||
|
run_command("deploy")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "deploy error when locking" do
|
||||||
|
Thread.report_on_exception = false
|
||||||
|
|
||||||
|
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||||
|
.with { |*arg| arg[0..1] == [:mkdir, :mrsk_lock] }
|
||||||
|
.raises(SocketError, "getaddrinfo: nodename nor servname provided, or not known")
|
||||||
|
|
||||||
|
assert_raises(SSHKit::Runner::ExecuteError) do
|
||||||
|
run_command("deploy")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "deploy errors leave lock in place" do
|
||||||
|
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "skip_broadcast" => false, "version" => "999" }
|
||||||
|
|
||||||
|
Mrsk::Cli::Main.any_instance.expects(:invoke)
|
||||||
|
.with("mrsk:cli:server:bootstrap", [], invoke_options)
|
||||||
|
.raises(RuntimeError)
|
||||||
|
|
||||||
|
assert_equal 0, MRSK.lock_count
|
||||||
|
assert_raises(RuntimeError) do
|
||||||
|
stderred { run_command("deploy") }
|
||||||
|
end
|
||||||
|
assert_equal 1, MRSK.lock_count
|
||||||
|
end
|
||||||
|
|
||||||
test "redeploy" do
|
test "redeploy" do
|
||||||
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "skip_broadcast" => false, "version" => "999" }
|
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "skip_broadcast" => false, "version" => "999" }
|
||||||
|
|
||||||
@@ -80,32 +120,34 @@ class CliMainTest < CliTestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "rollback bad version" do
|
test "rollback bad version" do
|
||||||
|
# Mrsk::Cli::Main.any_instance.stubs(:container_available?).returns(false)
|
||||||
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 --all --filter label=service=app --format '{{ .Names }}'/, output
|
assert_match /docker container ls --all --filter name=\^app-web-nonsense\$ --quiet/, 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
|
||||||
|
|
||||||
test "rollback good version" do
|
test "rollback good version" do
|
||||||
Mrsk::Cli::Main.any_instance.stubs(:container_name_available?).returns(true)
|
Mrsk::Cli::Main.any_instance.stubs(:container_available?).returns(true)
|
||||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info).with(:docker, :ps, "--filter", "label=service=app", "--format", "\"{{.Names}}\"", "|", "sed 's/-/\\n/g'", "|", "tail -n 1").returns("version-to-rollback\n").at_least_once
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info).with(:docker, :ps, "--filter", "label=service=app", "--filter", "label=role=web", "--format", "\"{{.Names}}\"", "|", "sed 's/-/\\n/g'", "|", "tail -n 1").returns("version-to-rollback\n").at_least_once
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info).with(:docker, :ps, "--filter", "label=service=app", "--filter", "label=role=workers", "--format", "\"{{.Names}}\"", "|", "sed 's/-/\\n/g'", "|", "tail -n 1").returns("version-to-rollback\n").at_least_once
|
||||||
|
|
||||||
run_command("rollback", "123", config_file: "deploy_with_accessories").tap do |output|
|
run_command("rollback", "123", config_file: "deploy_with_accessories").tap do |output|
|
||||||
assert_match "Start version 123", output
|
assert_match "Start version 123", output
|
||||||
assert_match "docker start app-123", output
|
assert_match "docker start app-web-123", output
|
||||||
assert_match "docker container ls --all --filter name=^app-version-to-rollback$ --quiet | xargs docker stop", output, "Should stop the container that was previously running"
|
assert_match "docker container ls --all --filter name=^app-web-version-to-rollback$ --quiet | xargs docker stop", output, "Should stop the container that was previously running"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "rollback without old version" do
|
test "rollback without old version" do
|
||||||
Mrsk::Cli::Main.any_instance.stubs(:container_name_available?).returns(true)
|
Mrsk::Cli::Main.any_instance.stubs(:container_available?).returns(true)
|
||||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info).with(:docker, :ps, "--filter", "label=service=app", "--format", "\"{{.Names}}\"", "|", "sed 's/-/\\n/g'", "|", "tail -n 1").returns("").at_least_once
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info).with(:docker, :ps, "--filter", "label=service=app", "--filter", "label=role=web", "--format", "\"{{.Names}}\"", "|", "sed 's/-/\\n/g'", "|", "tail -n 1").returns("").at_least_once
|
||||||
|
|
||||||
run_command("rollback", "123").tap do |output|
|
run_command("rollback", "123").tap do |output|
|
||||||
assert_match "Start version 123", output
|
assert_match "Start version 123", output
|
||||||
assert_match "docker start app-123", output
|
assert_match "docker start app-web-123", output
|
||||||
assert_no_match "docker stop", output
|
assert_no_match "docker stop", output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ require_relative "cli_test_case"
|
|||||||
class CliTraefikTest < CliTestCase
|
class CliTraefikTest < CliTestCase
|
||||||
test "boot" do
|
test "boot" do
|
||||||
run_command("boot").tap do |output|
|
run_command("boot").tap do |output|
|
||||||
assert_match "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --log-opt max-size=\"10m\" traefik:v2.9.9 --providers.docker --log.level=DEBUG", output
|
assert_match "docker login", output
|
||||||
|
assert_match "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --log-opt max-size=\"10m\" #{Mrsk::Commands::Traefik::DEFAULT_IMAGE} --providers.docker --log.level=DEBUG", output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class CommandsAppTest < ActiveSupport::TestCase
|
|||||||
|
|
||||||
test "run" do
|
test "run" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker run --detach --restart unless-stopped --name app-web-999 -e MRSK_CONTAINER_NAME=\"app-web-999\" -e RAILS_MASTER_KEY=\"456\" --log-opt max-size=\"10m\" --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.retry.attempts=\"5\" --label traefik.http.middlewares.app-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app.middlewares=\"app-retry@docker\" dhh/app:999",
|
"docker run --detach --restart unless-stopped --name app-web-999 -e MRSK_CONTAINER_NAME=\"app-web-999\" -e RAILS_MASTER_KEY=\"456\" --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.services.app-web.loadbalancer.healthcheck.path=\"/up\" --label traefik.http.services.app-web.loadbalancer.healthcheck.interval=\"1s\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999",
|
||||||
new_command.run.join(" ")
|
new_command.run.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ class CommandsAppTest < ActiveSupport::TestCase
|
|||||||
@config[:volumes] = ["/local/path:/container/path" ]
|
@config[:volumes] = ["/local/path:/container/path" ]
|
||||||
|
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker run --detach --restart unless-stopped --name app-web-999 -e MRSK_CONTAINER_NAME=\"app-web-999\" -e RAILS_MASTER_KEY=\"456\" --log-opt max-size=\"10m\" --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.retry.attempts=\"5\" --label traefik.http.middlewares.app-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app.middlewares=\"app-retry@docker\" dhh/app:999",
|
"docker run --detach --restart unless-stopped --name app-web-999 -e MRSK_CONTAINER_NAME=\"app-web-999\" -e RAILS_MASTER_KEY=\"456\" --log-opt max-size=\"10m\" --volume /local/path:/container/path --label service=\"app\" --label role=\"web\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.services.app-web.loadbalancer.healthcheck.path=\"/up\" --label traefik.http.services.app-web.loadbalancer.healthcheck.interval=\"1s\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999",
|
||||||
new_command.run.join(" ")
|
new_command.run.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ class CommandsAppTest < ActiveSupport::TestCase
|
|||||||
@config[:healthcheck] = { "path" => "/healthz" }
|
@config[:healthcheck] = { "path" => "/healthz" }
|
||||||
|
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker run --detach --restart unless-stopped --name app-web-999 -e MRSK_CONTAINER_NAME=\"app-web-999\" -e RAILS_MASTER_KEY=\"456\" --log-opt max-size=\"10m\" --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.retry.attempts=\"5\" --label traefik.http.middlewares.app-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app.middlewares=\"app-retry@docker\" dhh/app:999",
|
"docker run --detach --restart unless-stopped --name app-web-999 -e MRSK_CONTAINER_NAME=\"app-web-999\" -e RAILS_MASTER_KEY=\"456\" --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.services.app-web.loadbalancer.healthcheck.path=\"/healthz\" --label traefik.http.services.app-web.loadbalancer.healthcheck.interval=\"1s\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999",
|
||||||
new_command.run.join(" ")
|
new_command.run.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ class CommandsAppTest < ActiveSupport::TestCase
|
|||||||
@config[:logging] = { "driver" => "local", "options" => { "max-size" => "100m", "max-file" => "3" } }
|
@config[:logging] = { "driver" => "local", "options" => { "max-size" => "100m", "max-file" => "3" } }
|
||||||
|
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker run --detach --restart unless-stopped --name app-web-999 -e MRSK_CONTAINER_NAME=\"app-web-999\" -e RAILS_MASTER_KEY=\"456\" --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --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.retry.attempts=\"5\" --label traefik.http.middlewares.app-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app.middlewares=\"app-retry@docker\" dhh/app:999",
|
"docker run --detach --restart unless-stopped --name app-web-999 -e MRSK_CONTAINER_NAME=\"app-web-999\" -e RAILS_MASTER_KEY=\"456\" --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --label service=\"app\" --label role=\"web\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.services.app-web.loadbalancer.healthcheck.path=\"/up\" --label traefik.http.services.app-web.loadbalancer.healthcheck.interval=\"1s\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999",
|
||||||
new_command.run.join(" ")
|
new_command.run.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -2,53 +2,66 @@ require "test_helper"
|
|||||||
|
|
||||||
class CommandsTraefikTest < ActiveSupport::TestCase
|
class CommandsTraefikTest < ActiveSupport::TestCase
|
||||||
setup do
|
setup do
|
||||||
|
@image = "traefik:test"
|
||||||
|
|
||||||
@config = {
|
@config = {
|
||||||
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ],
|
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ],
|
||||||
traefik: { "args" => { "accesslog.format" => "json", "api.insecure" => true, "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } }
|
traefik: { "image" => @image, "args" => { "accesslog.format" => "json", "api.insecure" => true, "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } }
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
test "run" do
|
test "run" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --log-opt max-size=\"10m\" traefik:v2.9.9 --providers.docker --log.level=DEBUG --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
|
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --log-opt max-size=\"10m\" #{@image} --providers.docker --log.level=DEBUG --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
|
||||||
new_command.run.join(" ")
|
new_command.run.join(" ")
|
||||||
|
|
||||||
@config[:traefik]["host_port"] = "8080"
|
@config[:traefik]["host_port"] = "8080"
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker run --name traefik --detach --restart unless-stopped --publish 8080:80 --volume /var/run/docker.sock:/var/run/docker.sock --log-opt max-size=\"10m\" traefik:v2.9.9 --providers.docker --log.level=DEBUG --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
|
"docker run --name traefik --detach --restart unless-stopped --publish 8080:80 --volume /var/run/docker.sock:/var/run/docker.sock --log-opt max-size=\"10m\" #{@image} --providers.docker --log.level=DEBUG --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
|
||||||
new_command.run.join(" ")
|
new_command.run.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "run with ports configured" do
|
test "run with ports configured" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --log-opt max-size=\"10m\" traefik:v2.9.9 --providers.docker --log.level=DEBUG --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
|
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --log-opt max-size=\"10m\" #{@image} --providers.docker --log.level=DEBUG --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
|
||||||
new_command.run.join(" ")
|
new_command.run.join(" ")
|
||||||
|
|
||||||
@config[:traefik]["options"] = {"publish" => %w[9000:9000 9001:9001]}
|
@config[:traefik]["options"] = {"publish" => %w[9000:9000 9001:9001]}
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --log-opt max-size=\"10m\" --publish \"9000:9000\" --publish \"9001:9001\" traefik:v2.9.9 --providers.docker --log.level=DEBUG --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
|
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --log-opt max-size=\"10m\" --publish \"9000:9000\" --publish \"9001:9001\" #{@image} --providers.docker --log.level=DEBUG --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
|
||||||
new_command.run.join(" ")
|
new_command.run.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "run with volumes configured" do
|
test "run with volumes configured" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --log-opt max-size=\"10m\" traefik:v2.9.9 --providers.docker --log.level=DEBUG --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
|
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --log-opt max-size=\"10m\" #{@image} --providers.docker --log.level=DEBUG --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
|
||||||
new_command.run.join(" ")
|
new_command.run.join(" ")
|
||||||
|
|
||||||
@config[:traefik]["options"] = {"volume" => %w[./letsencrypt/acme.json:/letsencrypt/acme.json] }
|
@config[:traefik]["options"] = {"volume" => %w[./letsencrypt/acme.json:/letsencrypt/acme.json] }
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --log-opt max-size=\"10m\" --volume \"./letsencrypt/acme.json:/letsencrypt/acme.json\" traefik:v2.9.9 --providers.docker --log.level=DEBUG --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
|
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --log-opt max-size=\"10m\" --volume \"./letsencrypt/acme.json:/letsencrypt/acme.json\" #{@image} --providers.docker --log.level=DEBUG --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
|
||||||
new_command.run.join(" ")
|
new_command.run.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "run with several options configured" do
|
test "run with several options configured" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --log-opt max-size=\"10m\" traefik:v2.9.9 --providers.docker --log.level=DEBUG --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
|
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --log-opt max-size=\"10m\" #{@image} --providers.docker --log.level=DEBUG --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
|
||||||
new_command.run.join(" ")
|
new_command.run.join(" ")
|
||||||
|
|
||||||
@config[:traefik]["options"] = {"volume" => %w[./letsencrypt/acme.json:/letsencrypt/acme.json], "publish" => %w[8080:8080], "memory" => "512m"}
|
@config[:traefik]["options"] = {"volume" => %w[./letsencrypt/acme.json:/letsencrypt/acme.json], "publish" => %w[8080:8080], "memory" => "512m"}
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --log-opt max-size=\"10m\" --volume \"./letsencrypt/acme.json:/letsencrypt/acme.json\" --publish \"8080:8080\" --memory \"512m\" traefik:v2.9.9 --providers.docker --log.level=DEBUG --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
|
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --log-opt max-size=\"10m\" --volume \"./letsencrypt/acme.json:/letsencrypt/acme.json\" --publish \"8080:8080\" --memory \"512m\" #{@image} --providers.docker --log.level=DEBUG --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
|
||||||
|
new_command.run.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "run with labels configured" do
|
||||||
|
assert_equal \
|
||||||
|
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --log-opt max-size=\"10m\" #{@image} --providers.docker --log.level=DEBUG --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
|
||||||
|
new_command.run.join(" ")
|
||||||
|
|
||||||
|
@config[:traefik]["labels"] = { "traefik.http.routers.dashboard.service" => "api@internal", "traefik.http.routers.dashboard.middlewares" => "auth" }
|
||||||
|
assert_equal \
|
||||||
|
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --log-opt max-size=\"10m\" --label traefik.http.routers.dashboard.service=\"api@internal\" --label traefik.http.routers.dashboard.middlewares=\"auth\" #{@image} --providers.docker --log.level=DEBUG --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
|
||||||
new_command.run.join(" ")
|
new_command.run.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -56,7 +69,7 @@ class CommandsTraefikTest < ActiveSupport::TestCase
|
|||||||
@config.delete(:traefik)
|
@config.delete(:traefik)
|
||||||
|
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --log-opt max-size=\"10m\" traefik:v2.9.9 --providers.docker --log.level=DEBUG",
|
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --log-opt max-size=\"10m\" #{Mrsk::Commands::Traefik::DEFAULT_IMAGE} --providers.docker --log.level=DEBUG",
|
||||||
new_command.run.join(" ")
|
new_command.run.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -64,7 +77,7 @@ class CommandsTraefikTest < ActiveSupport::TestCase
|
|||||||
@config[:logging] = { "driver" => "local", "options" => { "max-size" => "100m", "max-file" => "3" } }
|
@config[:logging] = { "driver" => "local", "options" => { "max-size" => "100m", "max-file" => "3" } }
|
||||||
|
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" traefik:v2.9.9 --providers.docker --log.level=DEBUG --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
|
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" #{@image} --providers.docker --log.level=DEBUG --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
|
||||||
new_command.run.join(" ")
|
new_command.run.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -112,8 +112,11 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase
|
|||||||
|
|
||||||
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 @config.accessory(:mysql).env_args[1].is_a?(SSHKit::Redaction)
|
@config.accessory(:mysql).env_args.tap do |env_args|
|
||||||
|
assert_equal ["-e", "MYSQL_ROOT_PASSWORD=\"secret123\"", "-e", "MYSQL_ROOT_HOST=\"%\""], Mrsk::Utils.unredacted(env_args)
|
||||||
|
assert_equal ["-e", "MYSQL_ROOT_PASSWORD=[REDACTED]", "-e", "MYSQL_ROOT_HOST=\"%\""], Mrsk::Utils.redacted(env_args)
|
||||||
|
end
|
||||||
ensure
|
ensure
|
||||||
ENV["MYSQL_ROOT_PASSWORD"] = nil
|
ENV["MYSQL_ROOT_PASSWORD"] = nil
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
|
|||||||
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.retry.attempts=\"5\"", "--label", "traefik.http.middlewares.app-retry.retry.initialinterval=\"500ms\"", "--label", "traefik.http.routers.app.middlewares=\"app-retry@docker\"" ], @config.role(:web).label_args
|
assert_equal [ "--label", "service=\"app\"", "--label", "role=\"web\"", "--label", "traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\"", "--label", "traefik.http.services.app-web.loadbalancer.healthcheck.path=\"/up\"", "--label", "traefik.http.services.app-web.loadbalancer.healthcheck.interval=\"1s\"", "--label", "traefik.http.middlewares.app-web-retry.retry.attempts=\"5\"", "--label", "traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\"", "--label", "traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\"" ], @config.role(:web).label_args
|
||||||
end
|
end
|
||||||
|
|
||||||
test "custom labels" do
|
test "custom labels" do
|
||||||
@@ -66,7 +66,15 @@ 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.retry.attempts=\"5\"", "--label", "traefik.http.middlewares.app-retry.retry.initialinterval=\"500ms\"", "--label", "traefik.http.routers.app.middlewares=\"app-retry@docker\"" ], config.role(:beta).label_args
|
assert_equal [ "--label", "service=\"app\"", "--label", "role=\"beta\"", "--label", "traefik.http.routers.app-beta.rule=\"PathPrefix(\\`/\\`)\"", "--label", "traefik.http.services.app-beta.loadbalancer.healthcheck.path=\"/up\"", "--label", "traefik.http.services.app-beta.loadbalancer.healthcheck.interval=\"1s\"", "--label", "traefik.http.middlewares.app-beta-retry.retry.attempts=\"5\"", "--label", "traefik.http.middlewares.app-beta-retry.retry.initialinterval=\"500ms\"", "--label", "traefik.http.routers.app-beta.middlewares=\"app-beta-retry@docker\"" ], config.role(:beta).label_args
|
||||||
|
end
|
||||||
|
|
||||||
|
test "default traefik label for non-web role with destination" do
|
||||||
|
config = Mrsk::Configuration.new(@deploy_with_roles.tap { |c|
|
||||||
|
c[:servers]["beta"] = { "traefik" => "true", "hosts" => [ "1.1.1.5" ] }
|
||||||
|
}, destination: "staging")
|
||||||
|
|
||||||
|
assert_equal [ "--label", "service=\"app\"", "--label", "role=\"beta\"", "--label", "destination=\"staging\"", "--label", "traefik.http.routers.app-beta-staging.rule=\"PathPrefix(\\`/\\`)\"", "--label", "traefik.http.services.app-beta-staging.loadbalancer.healthcheck.path=\"/up\"", "--label", "traefik.http.services.app-beta-staging.loadbalancer.healthcheck.interval=\"1s\"", "--label", "traefik.http.middlewares.app-beta-staging-retry.retry.attempts=\"5\"", "--label", "traefik.http.middlewares.app-beta-staging-retry.retry.initialinterval=\"500ms\"", "--label", "traefik.http.routers.app-beta-staging.middlewares=\"app-beta-staging-retry@docker\"" ], config.role(:beta).label_args
|
||||||
end
|
end
|
||||||
|
|
||||||
test "env overwritten by role" do
|
test "env overwritten by role" do
|
||||||
@@ -97,7 +105,10 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
|
|||||||
ENV["REDIS_PASSWORD"] = "secret456"
|
ENV["REDIS_PASSWORD"] = "secret456"
|
||||||
ENV["DB_PASSWORD"] = "secret&\"123"
|
ENV["DB_PASSWORD"] = "secret&\"123"
|
||||||
|
|
||||||
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
|
@config_with_roles.role(:workers).env_args.tap do |env_args|
|
||||||
|
assert_equal ["-e", "REDIS_PASSWORD=\"secret456\"", "-e", "DB_PASSWORD=\"secret&\\\"123\"", "-e", "REDIS_URL=\"redis://a/b\"", "-e", "WEB_CONCURRENCY=\"4\""], Mrsk::Utils.unredacted(env_args)
|
||||||
|
assert_equal ["-e", "REDIS_PASSWORD=[REDACTED]", "-e", "DB_PASSWORD=[REDACTED]", "-e", "REDIS_URL=\"redis://a/b\"", "-e", "WEB_CONCURRENCY=\"4\""], Mrsk::Utils.redacted(env_args)
|
||||||
|
end
|
||||||
ensure
|
ensure
|
||||||
ENV["REDIS_PASSWORD"] = nil
|
ENV["REDIS_PASSWORD"] = nil
|
||||||
ENV["DB_PASSWORD"] = nil
|
ENV["DB_PASSWORD"] = nil
|
||||||
@@ -116,7 +127,10 @@ 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
|
@config_with_roles.role(:workers).env_args.tap do |env_args|
|
||||||
|
assert_equal ["-e", "DB_PASSWORD=\"secret123\"", "-e", "REDIS_URL=\"redis://a/b\"", "-e", "WEB_CONCURRENCY=\"4\""], Mrsk::Utils.unredacted(env_args)
|
||||||
|
assert_equal ["-e", "DB_PASSWORD=[REDACTED]", "-e", "REDIS_URL=\"redis://a/b\"", "-e", "WEB_CONCURRENCY=\"4\""], Mrsk::Utils.redacted(env_args)
|
||||||
|
end
|
||||||
ensure
|
ensure
|
||||||
ENV["DB_PASSWORD"] = nil
|
ENV["DB_PASSWORD"] = nil
|
||||||
end
|
end
|
||||||
@@ -133,7 +147,10 @@ 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
|
@config_with_roles.role(:workers).env_args.tap do |env_args|
|
||||||
|
assert_equal ["-e", "REDIS_PASSWORD=\"secret456\"", "-e", "REDIS_URL=\"redis://a/b\"", "-e", "WEB_CONCURRENCY=\"4\""], Mrsk::Utils.unredacted(env_args)
|
||||||
|
assert_equal ["-e", "REDIS_PASSWORD=[REDACTED]", "-e", "REDIS_URL=\"redis://a/b\"", "-e", "WEB_CONCURRENCY=\"4\""], Mrsk::Utils.redacted(env_args)
|
||||||
|
end
|
||||||
ensure
|
ensure
|
||||||
ENV["REDIS_PASSWORD"] = nil
|
ENV["REDIS_PASSWORD"] = nil
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -113,12 +113,13 @@ class ConfigurationTest < ActiveSupport::TestCase
|
|||||||
|
|
||||||
test "env args with clear and secrets" do
|
test "env args with clear and secrets" do
|
||||||
ENV["PASSWORD"] = "secret123"
|
ENV["PASSWORD"] = "secret123"
|
||||||
|
|
||||||
config = Mrsk::Configuration.new(@deploy.tap { |c| c.merge!({
|
config = Mrsk::Configuration.new(@deploy.tap { |c| c.merge!({
|
||||||
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\"" ], Mrsk::Utils.unredacted(config.env_args)
|
||||||
assert config.env_args[1].is_a?(SSHKit::Redaction)
|
assert_equal [ "-e", "PASSWORD=[REDACTED]", "-e", "PORT=\"3000\"" ], Mrsk::Utils.redacted(config.env_args)
|
||||||
ensure
|
ensure
|
||||||
ENV["PASSWORD"] = nil
|
ENV["PASSWORD"] = nil
|
||||||
end
|
end
|
||||||
@@ -133,12 +134,13 @@ class ConfigurationTest < ActiveSupport::TestCase
|
|||||||
|
|
||||||
test "env args with only secrets" do
|
test "env args with only secrets" do
|
||||||
ENV["PASSWORD"] = "secret123"
|
ENV["PASSWORD"] = "secret123"
|
||||||
|
|
||||||
config = Mrsk::Configuration.new(@deploy.tap { |c| c.merge!({
|
config = Mrsk::Configuration.new(@deploy.tap { |c| c.merge!({
|
||||||
env: { "secret" => [ "PASSWORD" ] }
|
env: { "secret" => [ "PASSWORD" ] }
|
||||||
}) })
|
}) })
|
||||||
|
|
||||||
assert_equal [ "-e", "PASSWORD=\"secret123\"" ], config.env_args
|
assert_equal [ "-e", "PASSWORD=\"secret123\"" ], Mrsk::Utils.unredacted(config.env_args)
|
||||||
assert config.env_args[1].is_a?(SSHKit::Redaction)
|
assert_equal [ "-e", "PASSWORD=[REDACTED]" ], Mrsk::Utils.redacted(config.env_args)
|
||||||
ensure
|
ensure
|
||||||
ENV["PASSWORD"] = nil
|
ENV["PASSWORD"] = nil
|
||||||
end
|
end
|
||||||
@@ -247,6 +249,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"], :logging=>["--log-opt", "max-size=\"10m\""], :healthcheck=>{"path"=>"/up", "port"=>3000 }}, @config.to_h)
|
assert_equal({ :roles=>["web"], :hosts=>["1.1.1.1", "1.1.1.2"], :primary_host=>"1.1.1.1", :version=>"missing", :repository=>"dhh/app", :absolute_image=>"dhh/app:missing", :service_with_version=>"app-missing", :env_args=>["-e", "REDIS_URL=\"redis://x/y\""], :ssh_options=>{:user=>"root", :auth_methods=>["publickey"]}, :volume_args=>["--volume", "/local/path:/container/path"], :logging=>["--log-opt", "max-size=\"10m\""], :healthcheck=>{"path"=>"/up", "port"=>3000, "max_attempts" => 7 }}, @config.to_h)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -8,13 +8,16 @@ class UtilsTest < ActiveSupport::TestCase
|
|||||||
|
|
||||||
test "argumentize with redacted" do
|
test "argumentize with redacted" do
|
||||||
assert_kind_of SSHKit::Redaction, \
|
assert_kind_of SSHKit::Redaction, \
|
||||||
Mrsk::Utils.argumentize("--label", { foo: "bar" }, redacted: true).last
|
Mrsk::Utils.argumentize("--label", { foo: "bar" }, sensitive: true).last
|
||||||
end
|
end
|
||||||
|
|
||||||
test "argumentize_env_with_secrets" do
|
test "argumentize_env_with_secrets" do
|
||||||
ENV.expects(:fetch).with("FOO").returns("secret")
|
ENV.expects(:fetch).with("FOO").returns("secret")
|
||||||
assert_equal [ "-e", "FOO=\"secret\"", "-e", "BAZ=\"qux\"" ], \
|
|
||||||
Mrsk::Utils.argumentize_env_with_secrets({ "secret" => [ "FOO" ], "clear" => { BAZ: "qux" } })
|
args = Mrsk::Utils.argumentize_env_with_secrets({ "secret" => [ "FOO" ], "clear" => { BAZ: "qux" } })
|
||||||
|
|
||||||
|
assert_equal [ "-e", "FOO=[REDACTED]", "-e", "BAZ=\"qux\"" ], Mrsk::Utils.redacted(args)
|
||||||
|
assert_equal [ "-e", "FOO=\"secret\"", "-e", "BAZ=\"qux\"" ], Mrsk::Utils.unredacted(args)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "optionize" do
|
test "optionize" do
|
||||||
@@ -27,9 +30,20 @@ class UtilsTest < ActiveSupport::TestCase
|
|||||||
Mrsk::Utils.optionize({ foo: "bar", baz: "qux", quux: true }, with: "=")
|
Mrsk::Utils.optionize({ foo: "bar", baz: "qux", quux: true }, with: "=")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "redact" do
|
test "no redaction from #to_s" do
|
||||||
assert_kind_of SSHKit::Redaction, Mrsk::Utils.redact("secret")
|
assert_equal "secret", Mrsk::Utils.sensitive("secret").to_s
|
||||||
assert_equal "secret", Mrsk::Utils.redact("secret")
|
end
|
||||||
|
|
||||||
|
test "redact from #inspect" do
|
||||||
|
assert_equal "[REDACTED]".inspect, Mrsk::Utils.sensitive("secret").inspect
|
||||||
|
end
|
||||||
|
|
||||||
|
test "redact from SSHKit output" do
|
||||||
|
assert_kind_of SSHKit::Redaction, Mrsk::Utils.sensitive("secret")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "redact from YAML output" do
|
||||||
|
assert_equal "--- ! '[REDACTED]'\n", YAML.dump(Mrsk::Utils.sensitive("secret"))
|
||||||
end
|
end
|
||||||
|
|
||||||
test "escape_shell_value" do
|
test "escape_shell_value" do
|
||||||
|
|||||||
Reference in New Issue
Block a user