Compare commits
189 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f0b7829ce | ||
|
|
57e4f08c4c | ||
|
|
a8bfe90fbe | ||
|
|
f114dd71f6 | ||
|
|
d1b5b9cf7a | ||
|
|
66f9ce0e90 | ||
|
|
956ab3560b | ||
|
|
483b893018 | ||
|
|
19f0f40adf | ||
|
|
f9cb87e55a | ||
|
|
cc2b321d93 | ||
|
|
004f1b04e6 | ||
|
|
3b695ae127 | ||
|
|
258887a451 | ||
|
|
9fd184dc32 | ||
|
|
38023fe538 | ||
|
|
0bc1fbfb74 | ||
|
|
5ab630cb03 | ||
|
|
910f14e9c0 | ||
|
|
f3ec9f19c8 | ||
|
|
58c1096a90 | ||
|
|
340ed94fa9 | ||
|
|
4e9c39f26d | ||
|
|
d08aacadac | ||
|
|
702490d10f | ||
|
|
13079dd2a3 | ||
|
|
7daee9a0df | ||
|
|
f7c5840473 | ||
|
|
a7d869ad40 | ||
|
|
7cd25fd163 | ||
|
|
ee25f200d7 | ||
|
|
059388cb02 | ||
|
|
a5ef1f254f | ||
|
|
15e8ac0ced | ||
|
|
9a31c20321 | ||
|
|
44b83151e3 | ||
|
|
0defcbb640 | ||
|
|
5d33fb6c33 | ||
|
|
e9d838ec46 | ||
|
|
ee319fee1c | ||
|
|
5646f6cc64 | ||
|
|
31aaa82991 | ||
|
|
5ea552be40 | ||
|
|
625be70e4d | ||
|
|
aafaee7ac8 | ||
|
|
97a190300d | ||
|
|
326711a3e0 | ||
|
|
82be521e66 | ||
|
|
21110080d5 | ||
|
|
ef107c41b6 | ||
|
|
1bf4b6b76f | ||
|
|
36a3b13bf4 | ||
|
|
01483140f5 | ||
|
|
0e19ead37c | ||
|
|
048aecf352 | ||
|
|
38c85e8021 | ||
|
|
88a7413b3e | ||
|
|
9cc73fed9a | ||
|
|
787ef96639 | ||
|
|
1e8edc25e2 | ||
|
|
b7877c59b4 | ||
|
|
35b5b317af | ||
|
|
4c448f7eb1 | ||
|
|
263a24afe3 | ||
|
|
a2d99e48bf | ||
|
|
a22e27dbf8 | ||
|
|
bb74a74dc4 | ||
|
|
c611a1616a | ||
|
|
98e7b995d5 | ||
|
|
ae2effb80c | ||
|
|
f719540e0c | ||
|
|
cbda851436 | ||
|
|
8854bb63a1 | ||
|
|
35ea9f3c81 | ||
|
|
18312f5191 | ||
|
|
71bc9bcf54 | ||
|
|
c83b74dcb7 | ||
|
|
971a91da15 | ||
|
|
86d6f8d674 | ||
|
|
7fe24d5048 | ||
|
|
a72f95f44d | ||
|
|
dc3be30b16 | ||
|
|
54881a0298 | ||
|
|
19527b4f65 | ||
|
|
bfb70b2118 | ||
|
|
e85bd5ff63 | ||
|
|
d0f66db33c | ||
|
|
650f9b1fbf | ||
|
|
1170e2311e | ||
|
|
94f87edded | ||
|
|
548a1019c1 | ||
|
|
ca2e2bac2e | ||
|
|
494a1ae089 | ||
|
|
a77428143f | ||
|
|
4fa6a6c06d | ||
|
|
2ad0dc0703 | ||
|
|
df067e4893 | ||
|
|
cd668066ff | ||
|
|
1a7d123746 | ||
|
|
52ca5b846a | ||
|
|
126e0bbd06 | ||
|
|
9ec3895dab | ||
|
|
a6245a6bc9 | ||
|
|
0d80709e2d | ||
|
|
aceabb3824 | ||
|
|
99fe31d4b4 | ||
|
|
bcf8a927f5 | ||
|
|
f055766918 | ||
|
|
a8726be20e | ||
|
|
100b72e4b4 | ||
|
|
828e56912e | ||
|
|
df202d6ef4 | ||
|
|
f530009a6e | ||
|
|
4b36df5dab | ||
|
|
79d46ceb16 | ||
|
|
bc8875e020 | ||
|
|
d4a72da9d8 | ||
|
|
04a04c05e0 | ||
|
|
cff8b058af | ||
|
|
b6f7d94ac3 | ||
|
|
3ab16c8994 | ||
|
|
b6743e5e1c | ||
|
|
9ddb181f50 | ||
|
|
fbe1458478 | ||
|
|
2f1393cd92 | ||
|
|
76673c0c1b | ||
|
|
fb62f2e6e1 | ||
|
|
051556674f | ||
|
|
3cbf4aea46 | ||
|
|
5ed431b807 | ||
|
|
60a19f0b30 | ||
|
|
2d0a7e1b67 | ||
|
|
49df19fb0d | ||
|
|
cef8fddfb4 | ||
|
|
c59eb00dd0 | ||
|
|
43f7409de0 | ||
|
|
448ea7719f | ||
|
|
72b70e3e9e | ||
|
|
e8697327fa | ||
|
|
0bfd4ca780 | ||
|
|
12e3a562c4 | ||
|
|
ab54dbdb8b | ||
|
|
ac3771447a | ||
|
|
daa0c9b5be | ||
|
|
c3393c8213 | ||
|
|
03d933d10b | ||
|
|
579b4cd9aa | ||
|
|
f9436d5673 | ||
|
|
8ae5331d97 | ||
|
|
4d47fbdf41 | ||
|
|
e980f1164e | ||
|
|
e2f6db5cae | ||
|
|
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 | ||
|
|
032266a76a | ||
|
|
33cc6c8bae | ||
|
|
5638ab8594 |
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -1,5 +1,9 @@
|
|||||||
name: CI
|
name: CI
|
||||||
on: [push, pull_request]
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
jobs:
|
jobs:
|
||||||
tests:
|
tests:
|
||||||
strategy:
|
strategy:
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ COPY Gemfile Gemfile.lock mrsk.gemspec ./
|
|||||||
COPY lib/mrsk/version.rb /mrsk/lib/mrsk/version.rb
|
COPY lib/mrsk/version.rb /mrsk/lib/mrsk/version.rb
|
||||||
|
|
||||||
# Install system dependencies
|
# Install system dependencies
|
||||||
RUN apk add --no-cache --update build-base git docker openrc \
|
RUN apk add --no-cache --update build-base git docker openrc openssh-client-default \
|
||||||
&& rc-update add docker boot \
|
&& rc-update add docker boot \
|
||||||
&& gem install bundler --version=2.4.3 \
|
&& gem install bundler --version=2.4.3 \
|
||||||
&& bundle install
|
&& bundle install
|
||||||
@@ -31,6 +31,10 @@ RUN gem build mrsk.gemspec && \
|
|||||||
# Set the working directory to /workdir
|
# Set the working directory to /workdir
|
||||||
WORKDIR /workdir
|
WORKDIR /workdir
|
||||||
|
|
||||||
|
# Tell git it's safe to access /workdir/.git even if
|
||||||
|
# the directory is owned by a different user
|
||||||
|
RUN git config --global --add safe.directory /workdir
|
||||||
|
|
||||||
# Set the entrypoint to run the installed binary in /workdir
|
# Set the entrypoint to run the installed binary in /workdir
|
||||||
# Example: docker run -it -v "$PWD:/workdir" mrsk init
|
# Example: docker run -it -v "$PWD:/workdir" mrsk init
|
||||||
ENTRYPOINT ["mrsk"]
|
ENTRYPOINT ["mrsk"]
|
||||||
|
|||||||
14
Gemfile.lock
14
Gemfile.lock
@@ -1,11 +1,12 @@
|
|||||||
PATH
|
PATH
|
||||||
remote: .
|
remote: .
|
||||||
specs:
|
specs:
|
||||||
mrsk (0.10.0)
|
mrsk (0.13.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)
|
||||||
|
|||||||
242
README.md
242
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
|
||||||
@@ -42,24 +44,24 @@ Then edit your `.env` file to add your registry password as `MRSK_REGISTRY_PASSW
|
|||||||
Now you're ready to deploy to the servers:
|
Now you're ready to deploy to the servers:
|
||||||
|
|
||||||
```
|
```
|
||||||
mrsk deploy
|
mrsk setup
|
||||||
```
|
```
|
||||||
|
|
||||||
This will:
|
This will:
|
||||||
|
|
||||||
1. Connect to the servers over SSH (using root by default, authenticated by your ssh key)
|
1. Connect to the servers over SSH (using root by default, authenticated by your ssh key)
|
||||||
2. Install Docker on any server that might be missing it (using apt-get): root access is needed via ssh for this.
|
2. Install Docker and curl on any server that might be missing it (using apt-get): root access is needed via ssh for this.
|
||||||
3. Log into the registry both locally and remotely
|
3. Log into the registry both locally and remotely
|
||||||
4. Build the image using the standard Dockerfile in the root of the application.
|
4. Build the image using the standard Dockerfile in the root of the application.
|
||||||
5. Push the image to the registry.
|
5. Push the image to the registry.
|
||||||
6. Pull the image from the registry onto the servers.
|
6. Pull the image from the registry onto the servers.
|
||||||
7. Ensure Traefik is running and accepting traffic on port 80.
|
7. Ensure Traefik is running and accepting traffic on port 80.
|
||||||
8. Ensure your app responds with `200 OK` to `GET /up`.
|
8. Ensure your app responds with `200 OK` to `GET /up` (you must have curl installed inside your app image!).
|
||||||
9. Start a new container with the version of the app that matches the current git version hash.
|
9. Start a new container with the version of the app that matches the current git version hash.
|
||||||
10. Stop the old container running the previous version of the app.
|
10. Stop the old container running the previous version of the app.
|
||||||
11. Prune unused images and stopped containers to ensure servers don't fill up.
|
11. Prune unused images and stopped containers to ensure servers don't fill up.
|
||||||
|
|
||||||
Voila! All the servers are now serving the app on port 80. If you're just running a single server, you're ready to go. If you're running multiple servers, you need to put a load balancer in front of them.
|
Voila! All the servers are now serving the app on port 80. If you're just running a single server, you're ready to go. If you're running multiple servers, you need to put a load balancer in front of them. For subsequent deploys, or if your servers already have Docker and curl installed, you can just run `mrsk deploy`.
|
||||||
|
|
||||||
## Vision
|
## Vision
|
||||||
|
|
||||||
@@ -182,6 +184,19 @@ registry:
|
|||||||
|
|
||||||
A reference to secret `DOCKER_REGISTRY_TOKEN` will look for `ENV["DOCKER_REGISTRY_TOKEN"]` on the machine running MRSK.
|
A reference to secret `DOCKER_REGISTRY_TOKEN` will look for `ENV["DOCKER_REGISTRY_TOKEN"]` on the machine running MRSK.
|
||||||
|
|
||||||
|
#### Using AWS ECR as the container registry
|
||||||
|
|
||||||
|
AWS ECR's access token is only valid for 12hrs. In order to not have to manually regenerate the token every time, you can use ERB in the `deploy.yml` file to shell out to the `aws` cli command, and obtain the token:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
registry:
|
||||||
|
server: <your aws account id>.dkr.ecr.<your aws region id>.amazonaws.com
|
||||||
|
username: AWS
|
||||||
|
password: <%= %x(aws ecr get-login-password) %>
|
||||||
|
```
|
||||||
|
|
||||||
|
You will need to have the `aws` CLI installed locally for this to work.
|
||||||
|
|
||||||
### Using a different SSH user than root
|
### Using a different SSH user than root
|
||||||
|
|
||||||
The default SSH user is root, but you can change it using `ssh/user`:
|
The default SSH user is root, but you can change it using `ssh/user`:
|
||||||
@@ -191,6 +206,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 +231,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 +319,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-staging.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!
|
||||||
|
|
||||||
@@ -312,6 +344,21 @@ servers:
|
|||||||
my-label: "50"
|
my-label: "50"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Using shell expansion
|
||||||
|
|
||||||
|
You can use shell expansion to interpolate values from the host machine into labels and env variables with the `${}` syntax.
|
||||||
|
Anything within the curly braces will be executed on the host machine and the result will be interpolated into the label or env variable.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
labels:
|
||||||
|
host-machine: "${cat /etc/hostname}"
|
||||||
|
|
||||||
|
env:
|
||||||
|
HOST_DEPLOYMENT_DIR: "${PWD}"
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: Any other occurrence of `$` will be escaped to prevent unwanted shell expansion!
|
||||||
|
|
||||||
### Using container options
|
### Using container options
|
||||||
|
|
||||||
You can specialize the options used to start containers using the `options` definitions:
|
You can specialize the options used to start containers using the `options` definitions:
|
||||||
@@ -439,9 +486,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 +497,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:
|
volume:
|
||||||
- /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 labels
|
||||||
|
|
||||||
### Configure alternate entrypoints for traefik
|
Add labels to Traefik Docker container.
|
||||||
|
|
||||||
You can configure multiple entrypoints for traefik like so:
|
```yaml
|
||||||
|
traefik:
|
||||||
|
labels:
|
||||||
|
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 +620,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:
|
||||||
@@ -588,40 +668,46 @@ servers:
|
|||||||
|
|
||||||
This assumes the Cron settings are stored in `config/crontab`.
|
This assumes the Cron settings are stored in `config/crontab`.
|
||||||
|
|
||||||
### Using audit broadcasts
|
### Healthcheck
|
||||||
|
|
||||||
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:
|
MRSK uses Docker healtchecks to check the health of your application during deployment. Traefik uses this same healthcheck status to determine when a container is ready to receive traffic.
|
||||||
|
|
||||||
```yaml
|
The healthcheck defaults to testing the HTTP response to the path `/up` on port 3000, up to 7 times. You can tailor this behaviour with the `healthcheck` setting:
|
||||||
audit_broadcast_cmd:
|
|
||||||
bin/audit_broadcast
|
|
||||||
```
|
|
||||||
|
|
||||||
The broadcast command could look something like:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
curl -q -d content="[My App] ${1}" https://3.basecamp.com/XXXXX/integrations/XXXXX/buckets/XXXXX/chats/XXXXX/lines
|
|
||||||
```
|
|
||||||
|
|
||||||
That'll post a line like follows to a preconfigured chatbot in Basecamp:
|
|
||||||
|
|
||||||
```
|
|
||||||
[My App] [dhh] Rolled back to version d264c4e92470ad1bd18590f04466787262f605de
|
|
||||||
```
|
|
||||||
|
|
||||||
### Using custom healthcheck path or port
|
|
||||||
|
|
||||||
MRSK defaults to checking the health of your application again `/up` on port 3000. You can tailor both with the `healthcheck` setting:
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
healthcheck:
|
healthcheck:
|
||||||
path: /healthz
|
path: /healthz
|
||||||
port: 4000
|
port: 4000
|
||||||
|
max_attempts: 7
|
||||||
|
interval: 20s
|
||||||
```
|
```
|
||||||
|
|
||||||
This will ensure your application is configured with a traefik label for the healthcheck against `/healthz` and that the pre-deploy healthcheck that MRSK performs is done against the same path on port 4000.
|
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.
|
||||||
|
|
||||||
|
You can also specify a custom healthcheck command, which is useful for non-HTTP services:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
healthcheck:
|
||||||
|
cmd: /bin/check_health
|
||||||
|
```
|
||||||
|
|
||||||
|
The top-level healthcheck configuration applies to all services that use
|
||||||
|
Traefik, by default. You can also specialize the configuration at the role
|
||||||
|
level:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
servers:
|
||||||
|
job:
|
||||||
|
hosts: ...
|
||||||
|
cmd: bin/jobs
|
||||||
|
healthcheck:
|
||||||
|
cmd: bin/check
|
||||||
|
```
|
||||||
|
|
||||||
|
The healthcheck 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.
|
||||||
|
|
||||||
|
Note: The HTTP health checks assume that the `curl` command is available inside the container. If that's not the case, use the healthcheck's `cmd` option to specify an alternative check that the container supports.
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
### Running commands on servers
|
### Running commands on servers
|
||||||
@@ -761,6 +847,74 @@ mrsk lock acquire -m "Doing maintanence"
|
|||||||
mrsk lock release
|
mrsk lock release
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Rolling deployments
|
||||||
|
|
||||||
|
When deploying to large numbers of hosts, you might prefer not to restart your services on every host at the same time.
|
||||||
|
|
||||||
|
MRSK's default is to boot new containers on all hosts in parallel. But you can control this by configuring `boot/limit` and `boot/wait` as options:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
service: myservice
|
||||||
|
|
||||||
|
boot:
|
||||||
|
limit: 10 # Can also specify as a percentage of total hosts, such as "25%"
|
||||||
|
wait: 2
|
||||||
|
```
|
||||||
|
|
||||||
|
When `limit` is specified, containers will be booted on, at most, `limit` hosts at once. MRSK will pause for `wait` seconds between batches.
|
||||||
|
|
||||||
|
These settings only apply when booting containers (using `mrsk deploy`, or `mrsk app boot`). For other commands, MRSK continues to run commands in parallel across all hosts.
|
||||||
|
|
||||||
|
## Hooks
|
||||||
|
|
||||||
|
You can run custom scripts at specific points with hooks.
|
||||||
|
|
||||||
|
Hooks should be stored in the .mrsk/hooks folder. Running mrsk init will build that folder and add some sample scripts.
|
||||||
|
|
||||||
|
You can change their location by setting `hooks_path` in the configuration file.
|
||||||
|
|
||||||
|
If the script returns a non-zero exit code the command will be aborted.
|
||||||
|
|
||||||
|
`MRSK_*` environment variables are available to the hooks command for
|
||||||
|
fine-grained audit reporting, e.g. for triggering deployment reports or
|
||||||
|
firing a JSON webhook. These variables include:
|
||||||
|
- `MRSK_RECORDED_AT` - UTC timestamp in ISO 8601 format, e.g. `2023-04-14T17:07:31Z`
|
||||||
|
- `MRSK_PERFORMER` - the local user performing the command (from `whoami`)
|
||||||
|
- `MRSK_SERVICE_VERSION` - an abbreviated service and version for use in messages, e.g. app@150b24f
|
||||||
|
- `MRSK_VERSION` - an full version being deployed
|
||||||
|
- `MRSK_DESTINATION` - optional: destination, e.g. "staging"
|
||||||
|
- `MRSK_HOSTS` - a comma separated list of the hosts targeted by the command
|
||||||
|
- `MRSK_ROLE` - optional: role targeted, e.g. "web"
|
||||||
|
|
||||||
|
There are three hooks:
|
||||||
|
|
||||||
|
1. pre-connect
|
||||||
|
Called before taking the deploy lock. For checks that need to run before connecting to remote hosts - e.g. DNS warming.
|
||||||
|
|
||||||
|
2. pre-build
|
||||||
|
Used for pre-build checks - e.g. there are no uncommitted changes or that CI has passed.
|
||||||
|
|
||||||
|
3. post-deploy - run after a deploy, redeploy or rollback
|
||||||
|
|
||||||
|
This hook is also passed a `MRSK_RUNTIME` env variable.
|
||||||
|
|
||||||
|
This could be used to broadcast a deployment message, or register the new version with an APM.
|
||||||
|
|
||||||
|
The command could look something like:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
curl -q -d content="[My App] ${MRSK_PERFORMER} Rolled back to version ${MRSK_VERSION}" https://3.basecamp.com/XXXXX/integrations/XXXXX/buckets/XXXXX/chats/XXXXX/lines
|
||||||
|
```
|
||||||
|
|
||||||
|
That'll post a line like follows to a preconfigured chatbot in Basecamp:
|
||||||
|
|
||||||
|
```
|
||||||
|
[My App] [dhh] Rolled back to version d264c4e92470ad1bd18590f04466787262f605de
|
||||||
|
```
|
||||||
|
|
||||||
|
Set `--skip_hooks` to avoid running the hooks.
|
||||||
|
|
||||||
## Stage of development
|
## Stage of development
|
||||||
|
|
||||||
This is beta software. Commands may still move around. But we're live in production at [37signals](https://37signals.com).
|
This is beta software. Commands may still move around. But we're live in production at [37signals](https://37signals.com).
|
||||||
|
|||||||
4
bin/mrsk
4
bin/mrsk
@@ -8,9 +8,11 @@ require "mrsk"
|
|||||||
begin
|
begin
|
||||||
Mrsk::Cli::Main.start(ARGV)
|
Mrsk::Cli::Main.start(ARGV)
|
||||||
rescue SSHKit::Runner::ExecuteError => e
|
rescue SSHKit::Runner::ExecuteError => e
|
||||||
puts " \e[31mERROR (#{e.cause.class}): #{e.cause.message}\e[0m"
|
puts " \e[31mERROR (#{e.cause.class}): #{e.message}\e[0m"
|
||||||
puts e.cause.backtrace if ENV["VERBOSE"]
|
puts e.cause.backtrace if ENV["VERBOSE"]
|
||||||
|
exit 1
|
||||||
rescue => e
|
rescue => e
|
||||||
puts " \e[31mERROR (#{e.class}): #{e.message}\e[0m"
|
puts " \e[31mERROR (#{e.class}): #{e.message}\e[0m"
|
||||||
puts e.backtrace if ENV["VERBOSE"]
|
puts e.backtrace if ENV["VERBOSE"]
|
||||||
|
exit 1
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
module Mrsk::Cli
|
module Mrsk::Cli
|
||||||
|
class LockError < StandardError; end
|
||||||
|
class HookError < 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
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
|
|||||||
execute *MRSK.auditor.record("Booted #{name} accessory"), verbosity: :debug
|
execute *MRSK.auditor.record("Booted #{name} accessory"), verbosity: :debug
|
||||||
execute *accessory.run
|
execute *accessory.run
|
||||||
end
|
end
|
||||||
|
|
||||||
audit_broadcast "Booted accessory #{name}" unless options[:skip_broadcast]
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -2,30 +2,38 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
|
|||||||
desc "boot", "Boot app on servers (or reboot app if already running)"
|
desc "boot", "Boot app on servers (or reboot app if already running)"
|
||||||
def boot
|
def boot
|
||||||
with_lock do
|
with_lock do
|
||||||
say "Get most recent version available as an image...", :magenta unless options[:version]
|
hold_lock_on_error do
|
||||||
using_version(version_or_latest) do |version|
|
say "Get most recent version available as an image...", :magenta unless options[:version]
|
||||||
say "Start container with version #{version} using a #{MRSK.config.readiness_delay}s readiness delay (or reboot if already running)...", :magenta
|
using_version(version_or_latest) do |version|
|
||||||
|
say "Start container with version #{version} using a #{MRSK.config.readiness_delay}s readiness delay (or reboot if already running)...", :magenta
|
||||||
|
|
||||||
cli = self
|
on(MRSK.hosts) do
|
||||||
|
execute *MRSK.auditor.record("Tagging #{MRSK.config.absolute_image} as the latest image"), verbosity: :debug
|
||||||
|
execute *MRSK.app.tag_current_as_latest
|
||||||
|
end
|
||||||
|
|
||||||
on(MRSK.hosts) do |host|
|
on(MRSK.hosts, **MRSK.boot_strategy) do |host|
|
||||||
roles = MRSK.roles_on(host)
|
roles = MRSK.roles_on(host)
|
||||||
|
|
||||||
roles.each do |role|
|
roles.each do |role|
|
||||||
execute *MRSK.auditor(role: role).record("Booted app version #{version}"), verbosity: :debug
|
app = MRSK.app(role: role)
|
||||||
|
auditor = MRSK.auditor(role: role)
|
||||||
|
|
||||||
begin
|
if capture_with_info(*app.container_id_for_version(version, only_running: true), raise_on_non_zero_exit: false).present?
|
||||||
if capture_with_info(*MRSK.app(role: role).container_id_for_version(version)).present?
|
tmp_version = "#{version}_replaced_#{SecureRandom.hex(8)}"
|
||||||
tmp_version = "#{version}_#{SecureRandom.hex(8)}"
|
|
||||||
info "Renaming container #{version} to #{tmp_version} as already deployed on #{host}"
|
info "Renaming container #{version} to #{tmp_version} as already deployed on #{host}"
|
||||||
execute *MRSK.auditor(role: role).record("Renaming container #{version} to #{tmp_version}"), verbosity: :debug
|
execute *auditor.record("Renaming container #{version} to #{tmp_version}"), verbosity: :debug
|
||||||
execute *MRSK.app(role: role).rename_container(version: version, new_version: tmp_version)
|
execute *app.rename_container(version: version, new_version: tmp_version)
|
||||||
end
|
end
|
||||||
|
|
||||||
old_version = capture_with_info(*MRSK.app(role: role).current_running_version).strip
|
execute *auditor.record("Booted app version #{version}"), verbosity: :debug
|
||||||
execute *MRSK.app(role: role).run
|
|
||||||
sleep MRSK.config.readiness_delay
|
old_version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
|
||||||
execute *MRSK.app(role: role).stop(version: old_version), raise_on_non_zero_exit: false if old_version.present?
|
execute *app.start_or_run
|
||||||
|
|
||||||
|
Mrsk::Utils::HealthcheckPoller.wait_for_healthy(pause_after_ready: true) { capture_with_info(*app.status(version: version)) }
|
||||||
|
|
||||||
|
execute *app.stop(version: old_version), raise_on_non_zero_exit: false if old_version.present?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -54,7 +62,7 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
|
|||||||
roles = MRSK.roles_on(host)
|
roles = MRSK.roles_on(host)
|
||||||
|
|
||||||
roles.each do |role|
|
roles.each do |role|
|
||||||
execute *MRSK.auditor(role: role).record("Stopped app"), verbosity: :debug
|
execute *MRSK.auditor.record("Stopped app", role: role), verbosity: :debug
|
||||||
execute *MRSK.app(role: role).stop, raise_on_non_zero_exit: false
|
execute *MRSK.app(role: role).stop, raise_on_non_zero_exit: false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -101,7 +109,7 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
|
|||||||
roles = MRSK.roles_on(host)
|
roles = MRSK.roles_on(host)
|
||||||
|
|
||||||
roles.each do |role|
|
roles.each do |role|
|
||||||
execute *MRSK.auditor(role: role).record("Executed cmd '#{cmd}' on app version #{version}"), verbosity: :debug
|
execute *MRSK.auditor.record("Executed cmd '#{cmd}' on app version #{version}", role: role), verbosity: :debug
|
||||||
puts_by_host host, capture_with_info(*MRSK.app(role: role).execute_in_existing_container(cmd))
|
puts_by_host host, capture_with_info(*MRSK.app(role: role).execute_in_existing_container(cmd))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -124,6 +132,31 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
|
|||||||
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 "stale_containers", "Detect app stale containers"
|
||||||
|
option :stop, aliases: "-s", type: :boolean, default: false, desc: "Stop the stale containers found"
|
||||||
|
def stale_containers
|
||||||
|
with_lock do
|
||||||
|
stop = options[:stop]
|
||||||
|
|
||||||
|
cli = self
|
||||||
|
|
||||||
|
on(MRSK.hosts) do |host|
|
||||||
|
roles = MRSK.roles_on(host)
|
||||||
|
|
||||||
|
roles.each do |role|
|
||||||
|
cli.send(:stale_versions, host: host, role: role).each do |version|
|
||||||
|
if stop
|
||||||
|
puts_by_host host, "Stopping stale container for role #{role} with version #{version}"
|
||||||
|
execute *MRSK.app(role: role).stop(version: version), raise_on_non_zero_exit: false
|
||||||
|
else
|
||||||
|
puts_by_host host, "Detected stale container for role #{role} with version #{version} (use `mrsk app stale_containers --stop` to stop)"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
desc "images", "Show app images 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) }
|
||||||
@@ -183,7 +216,7 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
|
|||||||
roles = MRSK.roles_on(host)
|
roles = MRSK.roles_on(host)
|
||||||
|
|
||||||
roles.each do |role|
|
roles.each do |role|
|
||||||
execute *MRSK.auditor(role: role).record("Removed app container with version #{version}"), verbosity: :debug
|
execute *MRSK.auditor.record("Removed app container with version #{version}", role: role), verbosity: :debug
|
||||||
execute *MRSK.app(role: role).remove_container(version: version)
|
execute *MRSK.app(role: role).remove_container(version: version)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -197,7 +230,7 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
|
|||||||
roles = MRSK.roles_on(host)
|
roles = MRSK.roles_on(host)
|
||||||
|
|
||||||
roles.each do |role|
|
roles.each do |role|
|
||||||
execute *MRSK.auditor(role: role).record("Removed all app containers"), verbosity: :debug
|
execute *MRSK.auditor.record("Removed all app containers", role: role), verbosity: :debug
|
||||||
execute *MRSK.app(role: role).remove_containers
|
execute *MRSK.app(role: role).remove_containers
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -240,6 +273,17 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
|
|||||||
version.presence
|
version.presence
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def stale_versions(host:, role:)
|
||||||
|
versions = nil
|
||||||
|
on(host) do
|
||||||
|
versions = \
|
||||||
|
capture_with_info(*MRSK.app(role: role).list_versions, raise_on_non_zero_exit: false)
|
||||||
|
.split("\n")
|
||||||
|
.drop(1)
|
||||||
|
end
|
||||||
|
versions
|
||||||
|
end
|
||||||
|
|
||||||
def version_or_latest
|
def version_or_latest
|
||||||
options[:version] || "latest"
|
options[:version] || "latest"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -22,7 +20,7 @@ module Mrsk::Cli
|
|||||||
class_option :config_file, aliases: "-c", default: "config/deploy.yml", desc: "Path to config file"
|
class_option :config_file, aliases: "-c", default: "config/deploy.yml", desc: "Path to config file"
|
||||||
class_option :destination, aliases: "-d", desc: "Specify destination to be used for config file (staging -> deploy.staging.yml)"
|
class_option :destination, aliases: "-d", desc: "Specify destination to be used for config file (staging -> deploy.staging.yml)"
|
||||||
|
|
||||||
class_option :skip_broadcast, aliases: "-B", type: :boolean, default: false, desc: "Skip audit broadcasts"
|
class_option :skip_hooks, aliases: "-H", type: :boolean, default: false, desc: "Don't run hooks"
|
||||||
|
|
||||||
def initialize(*)
|
def initialize(*)
|
||||||
super
|
super
|
||||||
@@ -74,37 +72,75 @@ module Mrsk::Cli
|
|||||||
puts " Finished all in #{sprintf("%.1f seconds", runtime)}"
|
puts " Finished all in #{sprintf("%.1f seconds", runtime)}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def audit_broadcast(line)
|
|
||||||
run_locally { execute *MRSK.auditor.broadcast(line), verbosity: :debug }
|
|
||||||
end
|
|
||||||
|
|
||||||
def with_lock
|
def with_lock
|
||||||
acquire_lock
|
if MRSK.holding_lock?
|
||||||
|
yield
|
||||||
|
else
|
||||||
|
run_hook "pre-connect"
|
||||||
|
|
||||||
yield
|
acquire_lock
|
||||||
ensure
|
|
||||||
release_lock
|
begin
|
||||||
|
yield
|
||||||
|
rescue
|
||||||
|
if MRSK.hold_lock_on_error?
|
||||||
|
error " \e[31mDeploy lock was not released\e[0m"
|
||||||
|
else
|
||||||
|
release_lock
|
||||||
|
end
|
||||||
|
|
||||||
|
raise
|
||||||
|
end
|
||||||
|
|
||||||
|
release_lock
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def acquire_lock
|
def acquire_lock
|
||||||
if MRSK.lock_count == 0
|
raise_if_locked do
|
||||||
say "Acquiring the deploy lock"
|
say "Acquiring the deploy lock...", :magenta
|
||||||
on(MRSK.primary_host) { execute *MRSK.lock.acquire("Automatic deploy lock", MRSK.config.version) }
|
on(MRSK.primary_host) { execute *MRSK.lock.acquire("Automatic deploy lock", MRSK.config.version), verbosity: :debug }
|
||||||
end
|
|
||||||
MRSK.lock_count += 1
|
|
||||||
rescue SSHKit::Runner::ExecuteError => e
|
|
||||||
if e.message =~ /cannot create directory/
|
|
||||||
invoke "mrsk:cli:lock:status", []
|
|
||||||
end
|
end
|
||||||
|
|
||||||
raise LockError, "Deploy lock found"
|
MRSK.holding_lock = true
|
||||||
end
|
end
|
||||||
|
|
||||||
def release_lock
|
def release_lock
|
||||||
MRSK.lock_count -= 1
|
say "Releasing the deploy lock...", :magenta
|
||||||
if MRSK.lock_count == 0
|
on(MRSK.primary_host) { execute *MRSK.lock.release, verbosity: :debug }
|
||||||
say "Releasing the deploy lock"
|
|
||||||
on(MRSK.primary_host) { execute *MRSK.lock.release }
|
MRSK.holding_lock = false
|
||||||
|
end
|
||||||
|
|
||||||
|
def raise_if_locked
|
||||||
|
yield
|
||||||
|
rescue SSHKit::Runner::ExecuteError => e
|
||||||
|
if e.message =~ /cannot create directory/
|
||||||
|
on(MRSK.primary_host) { puts capture_with_debug(*MRSK.lock.status) }
|
||||||
|
raise LockError, "Deploy lock found"
|
||||||
|
else
|
||||||
|
raise e
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def hold_lock_on_error
|
||||||
|
if MRSK.hold_lock_on_error?
|
||||||
|
yield
|
||||||
|
else
|
||||||
|
MRSK.hold_lock_on_error = true
|
||||||
|
yield
|
||||||
|
MRSK.hold_lock_on_error = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def run_hook(hook, **details)
|
||||||
|
if !options[:skip_hooks] && MRSK.hook.hook_exists?(hook)
|
||||||
|
say "Running the #{hook} hook...", :magenta
|
||||||
|
run_locally do
|
||||||
|
MRSK.with_verbosity(:debug) { execute *MRSK.hook.run(hook, **details, hosts: MRSK.hosts.join(",")) }
|
||||||
|
rescue SSHKit::Command::Failed
|
||||||
|
raise HookError.new("Hook `#{hook}` failed")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
class Mrsk::Cli::Build < Mrsk::Cli::Base
|
class Mrsk::Cli::Build < Mrsk::Cli::Base
|
||||||
|
class BuildError < StandardError; end
|
||||||
|
|
||||||
desc "deliver", "Build app and push app image to registry then pull image on servers"
|
desc "deliver", "Build app and push app image to registry then pull image on servers"
|
||||||
def deliver
|
def deliver
|
||||||
with_lock do
|
with_lock do
|
||||||
@@ -12,6 +14,9 @@ class Mrsk::Cli::Build < Mrsk::Cli::Base
|
|||||||
with_lock do
|
with_lock do
|
||||||
cli = self
|
cli = self
|
||||||
|
|
||||||
|
verify_local_dependencies
|
||||||
|
run_hook "pre-build"
|
||||||
|
|
||||||
run_locally do
|
run_locally do
|
||||||
begin
|
begin
|
||||||
MRSK.with_verbosity(:debug) { execute *MRSK.builder.push }
|
MRSK.with_verbosity(:debug) { execute *MRSK.builder.push }
|
||||||
@@ -77,4 +82,19 @@ class Mrsk::Cli::Build < Mrsk::Cli::Base
|
|||||||
puts capture(*MRSK.builder.info)
|
puts capture(*MRSK.builder.info)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def verify_local_dependencies
|
||||||
|
run_locally do
|
||||||
|
begin
|
||||||
|
execute *MRSK.builder.ensure_local_dependencies_installed
|
||||||
|
rescue SSHKit::Command::Failed => e
|
||||||
|
build_error = e.message =~ /command not found/ ?
|
||||||
|
"Docker is not installed locally" :
|
||||||
|
"Docker buildx plugin is not installed locally"
|
||||||
|
|
||||||
|
raise BuildError, build_error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
class Mrsk::Cli::Healthcheck < Mrsk::Cli::Base
|
class Mrsk::Cli::Healthcheck < Mrsk::Cli::Base
|
||||||
MAX_ATTEMPTS = 7
|
|
||||||
|
|
||||||
class HealthcheckError < StandardError; end
|
|
||||||
|
|
||||||
default_command :perform
|
default_command :perform
|
||||||
|
|
||||||
desc "perform", "Health check current app version"
|
desc "perform", "Health check current app version"
|
||||||
@@ -10,37 +6,11 @@ class Mrsk::Cli::Healthcheck < Mrsk::Cli::Base
|
|||||||
on(MRSK.primary_host) do
|
on(MRSK.primary_host) do
|
||||||
begin
|
begin
|
||||||
execute *MRSK.healthcheck.run
|
execute *MRSK.healthcheck.run
|
||||||
|
Mrsk::Utils::HealthcheckPoller.wait_for_healthy { capture_with_info(*MRSK.healthcheck.status) }
|
||||||
target = "Health check against #{MRSK.config.healthcheck["path"]}"
|
rescue Mrsk::Utils::HealthcheckPoller::HealthcheckError => e
|
||||||
attempt = 1
|
|
||||||
|
|
||||||
begin
|
|
||||||
status = capture_with_info(*MRSK.healthcheck.curl)
|
|
||||||
|
|
||||||
if status == "200"
|
|
||||||
info "#{target} succeeded with 200 OK!"
|
|
||||||
else
|
|
||||||
raise HealthcheckError, "#{target} failed with status #{status}"
|
|
||||||
end
|
|
||||||
rescue SSHKit::Command::Failed
|
|
||||||
if attempt <= MAX_ATTEMPTS
|
|
||||||
info "#{target} failed to respond, retrying in #{attempt}s..."
|
|
||||||
sleep attempt
|
|
||||||
attempt += 1
|
|
||||||
|
|
||||||
retry
|
|
||||||
else
|
|
||||||
raise
|
|
||||||
end
|
|
||||||
end
|
|
||||||
rescue SSHKit::Command::Failed, HealthcheckError => e
|
|
||||||
error capture_with_info(*MRSK.healthcheck.logs)
|
error capture_with_info(*MRSK.healthcheck.logs)
|
||||||
|
error capture_with_pretty_json(*MRSK.healthcheck.container_health_log)
|
||||||
if e.message =~ /curl/
|
raise
|
||||||
raise SSHKit::Command::Failed, "#{target} failed to return 200 OK!"
|
|
||||||
else
|
|
||||||
raise
|
|
||||||
end
|
|
||||||
ensure
|
ensure
|
||||||
execute *MRSK.healthcheck.stop, raise_on_non_zero_exit: false
|
execute *MRSK.healthcheck.stop, raise_on_non_zero_exit: false
|
||||||
execute *MRSK.healthcheck.remove, raise_on_non_zero_exit: false
|
execute *MRSK.healthcheck.remove, raise_on_non_zero_exit: false
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ class Mrsk::Cli::Lock < Mrsk::Cli::Base
|
|||||||
desc "status", "Report lock status"
|
desc "status", "Report lock status"
|
||||||
def status
|
def status
|
||||||
handle_missing_lock do
|
handle_missing_lock do
|
||||||
on(MRSK.primary_host) { puts capture_with_info(*MRSK.lock.status) }
|
on(MRSK.primary_host) { puts capture_with_debug(*MRSK.lock.status) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -10,17 +10,17 @@ class Mrsk::Cli::Lock < Mrsk::Cli::Base
|
|||||||
option :message, aliases: "-m", type: :string, desc: "A lock mesasge", required: true
|
option :message, aliases: "-m", type: :string, desc: "A lock mesasge", required: true
|
||||||
def acquire
|
def acquire
|
||||||
message = options[:message]
|
message = options[:message]
|
||||||
handle_missing_lock do
|
raise_if_locked 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), verbosity: :debug }
|
||||||
say "Set the deploy lock"
|
say "Acquired the deploy lock"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
desc "release", "Release the deploy lock"
|
desc "release", "Release the deploy lock"
|
||||||
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, verbosity: :debug }
|
||||||
say "Removed the deploy lock"
|
say "Released the deploy lock"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
class Mrsk::Cli::Main < Mrsk::Cli::Base
|
class Mrsk::Cli::Main < Mrsk::Cli::Base
|
||||||
desc "setup", "Setup all accessories and deploy app to servers"
|
desc "setup", "Setup all accessories and deploy app to servers"
|
||||||
def setup
|
def setup
|
||||||
with_lock do
|
print_runtime do
|
||||||
print_runtime do
|
with_lock do
|
||||||
invoke "mrsk:cli:server:bootstrap"
|
invoke "mrsk:cli:server:bootstrap"
|
||||||
invoke "mrsk:cli:accessory:boot", [ "all" ]
|
invoke "mrsk:cli:accessory:boot", [ "all" ]
|
||||||
deploy
|
deploy
|
||||||
@@ -13,12 +13,9 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
|
|||||||
desc "deploy", "Deploy app to servers"
|
desc "deploy", "Deploy app to servers"
|
||||||
option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip image build and push"
|
option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip image build and push"
|
||||||
def deploy
|
def deploy
|
||||||
with_lock do
|
runtime = print_runtime do
|
||||||
invoke_options = deploy_options
|
with_lock do
|
||||||
|
invoke_options = deploy_options
|
||||||
runtime = print_runtime do
|
|
||||||
say "Ensure curl and Docker are installed...", :magenta
|
|
||||||
invoke "mrsk:cli:server:bootstrap", [], invoke_options
|
|
||||||
|
|
||||||
say "Log into image registry...", :magenta
|
say "Log into image registry...", :magenta
|
||||||
invoke "mrsk:cli:registry:login", [], invoke_options
|
invoke "mrsk:cli:registry:login", [], invoke_options
|
||||||
@@ -37,23 +34,26 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
|
|||||||
say "Ensure app can pass healthcheck...", :magenta
|
say "Ensure app can pass healthcheck...", :magenta
|
||||||
invoke "mrsk:cli:healthcheck:perform", [], invoke_options
|
invoke "mrsk:cli:healthcheck:perform", [], invoke_options
|
||||||
|
|
||||||
|
say "Detect stale containers...", :magenta
|
||||||
|
invoke "mrsk:cli:app:stale_containers", [], invoke_options
|
||||||
|
|
||||||
invoke "mrsk:cli:app:boot", [], invoke_options
|
invoke "mrsk:cli:app:boot", [], invoke_options
|
||||||
|
|
||||||
say "Prune old containers and images...", :magenta
|
say "Prune old containers and images...", :magenta
|
||||||
invoke "mrsk:cli:prune:all", [], invoke_options
|
invoke "mrsk:cli:prune:all", [], invoke_options
|
||||||
end
|
end
|
||||||
|
|
||||||
audit_broadcast "Deployed #{service_version} in #{runtime.round} seconds" unless options[:skip_broadcast]
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
run_hook "post-deploy", runtime: runtime.round
|
||||||
end
|
end
|
||||||
|
|
||||||
desc "redeploy", "Deploy 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"
|
||||||
option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip image build and push"
|
option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip image build and push"
|
||||||
def redeploy
|
def redeploy
|
||||||
with_lock do
|
runtime = print_runtime do
|
||||||
invoke_options = deploy_options
|
with_lock do
|
||||||
|
invoke_options = deploy_options
|
||||||
|
|
||||||
runtime = print_runtime do
|
|
||||||
if options[:skip_push]
|
if options[:skip_push]
|
||||||
say "Pull app image...", :magenta
|
say "Pull app image...", :magenta
|
||||||
invoke "mrsk:cli:build:pull", [], invoke_options
|
invoke "mrsk:cli:build:pull", [], invoke_options
|
||||||
@@ -65,41 +65,36 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
|
|||||||
say "Ensure app can pass healthcheck...", :magenta
|
say "Ensure app can pass healthcheck...", :magenta
|
||||||
invoke "mrsk:cli:healthcheck:perform", [], invoke_options
|
invoke "mrsk:cli:healthcheck:perform", [], invoke_options
|
||||||
|
|
||||||
|
say "Detect stale containers...", :magenta
|
||||||
|
invoke "mrsk:cli:app:stale_containers", [], invoke_options
|
||||||
|
|
||||||
invoke "mrsk:cli:app:boot", [], invoke_options
|
invoke "mrsk:cli:app:boot", [], invoke_options
|
||||||
end
|
end
|
||||||
|
|
||||||
audit_broadcast "Redeployed #{service_version} in #{runtime.round} seconds" unless options[:skip_broadcast]
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
run_hook "post-deploy", runtime: runtime.round
|
||||||
end
|
end
|
||||||
|
|
||||||
desc "rollback [VERSION]", "Rollback app to VERSION"
|
desc "rollback [VERSION]", "Rollback app to VERSION"
|
||||||
def rollback(version)
|
def rollback(version)
|
||||||
with_lock do
|
rolled_back = false
|
||||||
MRSK.config.version = version
|
runtime = print_runtime do
|
||||||
|
with_lock do
|
||||||
|
invoke_options = deploy_options
|
||||||
|
|
||||||
if container_name_available?(MRSK.config.service_with_version)
|
MRSK.config.version = version
|
||||||
say "Start version #{version}, then wait #{MRSK.config.readiness_delay}s for app to boot before stopping the old version...", :magenta
|
|
||||||
|
|
||||||
cli = self
|
|
||||||
old_version = nil
|
old_version = nil
|
||||||
|
|
||||||
on(MRSK.hosts) do |host|
|
if container_available?(version)
|
||||||
old_version = capture_with_info(*MRSK.app.current_running_version).strip.presence
|
invoke "mrsk:cli:app:boot", [], invoke_options.merge(version: version)
|
||||||
|
rolled_back = true
|
||||||
execute *MRSK.app.start
|
else
|
||||||
|
say "The app version '#{version}' is not available as a container (use 'mrsk app containers' for available versions)", :red
|
||||||
if old_version
|
|
||||||
sleep MRSK.config.readiness_delay
|
|
||||||
|
|
||||||
execute *MRSK.app.stop(version: old_version), raise_on_non_zero_exit: false
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
audit_broadcast "Rolled back #{service_version(Mrsk::Utils.abbreviate_version(old_version))} to #{service_version}" unless options[:skip_broadcast]
|
|
||||||
else
|
|
||||||
say "The app version '#{version}' is not available as a container (use 'mrsk app containers' for available versions)", :red
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
run_hook "post-deploy", runtime: runtime.round if rolled_back
|
||||||
end
|
end
|
||||||
|
|
||||||
desc "details", "Show details about all containers"
|
desc "details", "Show details about all containers"
|
||||||
@@ -119,7 +114,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
|
||||||
|
|
||||||
@@ -141,6 +136,14 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
|
|||||||
puts "Created .env file"
|
puts "Created .env file"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
unless (hooks_dir = Pathname.new(File.expand_path(".mrsk/hooks"))).exist?
|
||||||
|
hooks_dir.mkpath
|
||||||
|
Pathname.new(File.expand_path("templates/sample_hooks", __dir__)).each_child do |sample_hook|
|
||||||
|
FileUtils.cp sample_hook, hooks_dir, preserve: true
|
||||||
|
end
|
||||||
|
puts "Created sample hooks in .mrsk/hooks"
|
||||||
|
end
|
||||||
|
|
||||||
if options[:bundle]
|
if options[:bundle]
|
||||||
if (binstub = Pathname.new(File.expand_path("bin/mrsk"))).exist?
|
if (binstub = Pathname.new(File.expand_path("bin/mrsk"))).exist?
|
||||||
puts "Binstub already exists in bin/mrsk (remove first to create a new one)"
|
puts "Binstub already exists in bin/mrsk (remove first to create a new one)"
|
||||||
@@ -198,6 +201,9 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
|
|||||||
desc "healthcheck", "Healthcheck application"
|
desc "healthcheck", "Healthcheck application"
|
||||||
subcommand "healthcheck", Mrsk::Cli::Healthcheck
|
subcommand "healthcheck", Mrsk::Cli::Healthcheck
|
||||||
|
|
||||||
|
desc "lock", "Manage the deploy lock"
|
||||||
|
subcommand "lock", Mrsk::Cli::Lock
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
@@ -210,21 +216,28 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
|
|||||||
desc "traefik", "Manage Traefik load balancer"
|
desc "traefik", "Manage Traefik load balancer"
|
||||||
subcommand "traefik", Mrsk::Cli::Traefik
|
subcommand "traefik", Mrsk::Cli::Traefik
|
||||||
|
|
||||||
desc "lock", "Manage the deploy lock"
|
|
||||||
subcommand "lock", Mrsk::Cli::Lock
|
|
||||||
|
|
||||||
private
|
private
|
||||||
def container_name_available?(container_name, host: MRSK.primary_host)
|
def container_available?(version)
|
||||||
container_names = nil
|
begin
|
||||||
on(host) { container_names = capture_with_info(*MRSK.app.list_container_names).split("\n") }
|
on(MRSK.hosts) do
|
||||||
Array(container_names).include?(container_name)
|
MRSK.roles_on(host).each do |role|
|
||||||
|
container_id = capture_with_info(*MRSK.app(role: role).container_id_for_version(version))
|
||||||
|
raise "Container not found" unless container_id.present?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
rescue SSHKit::Runner::ExecuteError => e
|
||||||
|
if e.message =~ /Container not found/
|
||||||
|
say "Error looking for container version #{version}: #{e.message}"
|
||||||
|
return false
|
||||||
|
else
|
||||||
|
raise
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
def deploy_options
|
def deploy_options
|
||||||
{ "version" => MRSK.config.version }.merge(options.without("skip_push"))
|
{ "version" => MRSK.config.version }.merge(options.without("skip_push"))
|
||||||
end
|
end
|
||||||
|
|
||||||
def service_version(version = MRSK.config.abbreviated_version)
|
|
||||||
[ MRSK.config.service, version ].compact.join("@")
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ class Mrsk::Cli::Prune < Mrsk::Cli::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
desc "images", "Prune unused images older than 7 days"
|
desc "images", "Prune dangling images"
|
||||||
def images
|
def images
|
||||||
with_lock do
|
with_lock do
|
||||||
on(MRSK.hosts) do
|
on(MRSK.hosts) do
|
||||||
@@ -17,7 +17,7 @@ class Mrsk::Cli::Prune < Mrsk::Cli::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
desc "containers", "Prune stopped containers older than 3 days"
|
desc "containers", "Prune all stopped containers, except the last 5"
|
||||||
def containers
|
def containers
|
||||||
with_lock do
|
with_lock do
|
||||||
on(MRSK.hosts) do
|
on(MRSK.hosts) do
|
||||||
|
|||||||
@@ -1,17 +1,21 @@
|
|||||||
class Mrsk::Cli::Server < Mrsk::Cli::Base
|
class Mrsk::Cli::Server < Mrsk::Cli::Base
|
||||||
desc "bootstrap", "Ensure curl and Docker are installed on servers"
|
desc "bootstrap", "Set up Docker to run MRSK apps"
|
||||||
def bootstrap
|
def bootstrap
|
||||||
with_lock do
|
missing = []
|
||||||
on(MRSK.hosts + MRSK.accessory_hosts) do
|
|
||||||
dependencies_to_install = Array.new.tap do |dependencies|
|
|
||||||
dependencies << "curl" unless execute "which curl", raise_on_non_zero_exit: false
|
|
||||||
dependencies << "docker.io" unless execute "which docker", raise_on_non_zero_exit: false
|
|
||||||
end
|
|
||||||
|
|
||||||
if dependencies_to_install.any?
|
on(MRSK.hosts | MRSK.accessory_hosts) do |host|
|
||||||
execute "apt-get update -y && apt-get install #{dependencies_to_install.join(" ")} -y"
|
unless execute(*MRSK.docker.installed?, raise_on_non_zero_exit: false)
|
||||||
|
if execute(*MRSK.docker.superuser?, raise_on_non_zero_exit: false)
|
||||||
|
info "Missing Docker on #{host}. Installing…"
|
||||||
|
execute *MRSK.docker.install
|
||||||
|
else
|
||||||
|
missing << host
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if missing.any?
|
||||||
|
raise "Docker is not installed on #{missing.join(", ")} and can't be automatically installed without having root access and the `curl` command available. Install Docker manually: https://docs.docker.com/engine/install/"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -25,10 +25,6 @@ registry:
|
|||||||
# secret:
|
# secret:
|
||||||
# - RAILS_MASTER_KEY
|
# - RAILS_MASTER_KEY
|
||||||
|
|
||||||
# Call a broadcast command on deploys.
|
|
||||||
# audit_broadcast_cmd:
|
|
||||||
# bin/broadcast_to_bc
|
|
||||||
|
|
||||||
# Use a different ssh user than root
|
# Use a different ssh user than root
|
||||||
# ssh:
|
# ssh:
|
||||||
# user: app
|
# user: app
|
||||||
|
|||||||
14
lib/mrsk/cli/templates/sample_hooks/post-deploy.sample
Executable file
14
lib/mrsk/cli/templates/sample_hooks/post-deploy.sample
Executable file
@@ -0,0 +1,14 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# A sample post-deploy hook
|
||||||
|
#
|
||||||
|
# These environment variables are available:
|
||||||
|
# MRSK_RECORDED_AT
|
||||||
|
# MRSK_PERFORMER
|
||||||
|
# MRSK_VERSION
|
||||||
|
# MRSK_HOSTS
|
||||||
|
# MRSK_ROLE (if set)
|
||||||
|
# MRSK_DESTINATION (if set)
|
||||||
|
# MRSK_RUNTIME
|
||||||
|
|
||||||
|
echo "$MRSK_PERFORMER deployed $MRSK_VERSION to $MRSK_DESTINATION in $MRSK_RUNTIME seconds"
|
||||||
51
lib/mrsk/cli/templates/sample_hooks/pre-build.sample
Executable file
51
lib/mrsk/cli/templates/sample_hooks/pre-build.sample
Executable file
@@ -0,0 +1,51 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# A sample pre-build hook
|
||||||
|
#
|
||||||
|
# Checks:
|
||||||
|
# 1. We have a clean checkout
|
||||||
|
# 2. A remote is configured
|
||||||
|
# 3. The branch has been pushed to the remote
|
||||||
|
# 4. The version we are deploying matches the remote
|
||||||
|
#
|
||||||
|
# These environment variables are available:
|
||||||
|
# MRSK_RECORDED_AT
|
||||||
|
# MRSK_PERFORMER
|
||||||
|
# MRSK_VERSION
|
||||||
|
# MRSK_HOSTS
|
||||||
|
# MRSK_ROLE (if set)
|
||||||
|
# MRSK_DESTINATION (if set)
|
||||||
|
|
||||||
|
if [ -n "$(git status --porcelain)" ]; then
|
||||||
|
echo "Git checkout is not clean, aborting..." >&2
|
||||||
|
git status --porcelain >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
first_remote=$(git remote)
|
||||||
|
|
||||||
|
if [ -z "$first_remote" ]; then
|
||||||
|
echo "No git remote set, aborting..." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
current_branch=$(git branch --show-current)
|
||||||
|
|
||||||
|
if [ -z "$current_branch" ]; then
|
||||||
|
echo "No git remote set, aborting..." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
remote_head=$(git ls-remote $first_remote --tags $current_branch | cut -f1)
|
||||||
|
|
||||||
|
if [ -z "$remote_head" ]; then
|
||||||
|
echo "Branch not pushed to remote, aborting..." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$MRSK_VERSION" != "$remote_head" ]; then
|
||||||
|
echo "Version ($MRSK_VERSION) does not match remote HEAD ($remote_head), aborting..." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
47
lib/mrsk/cli/templates/sample_hooks/pre-connect.sample
Executable file
47
lib/mrsk/cli/templates/sample_hooks/pre-connect.sample
Executable file
@@ -0,0 +1,47 @@
|
|||||||
|
#!/usr/bin/env ruby
|
||||||
|
|
||||||
|
# A sample pre-connect check
|
||||||
|
#
|
||||||
|
# Warms DNS before connecting to hosts in parallel
|
||||||
|
#
|
||||||
|
# These environment variables are available:
|
||||||
|
# MRSK_RECORDED_AT
|
||||||
|
# MRSK_PERFORMER
|
||||||
|
# MRSK_VERSION
|
||||||
|
# MRSK_HOSTS
|
||||||
|
# MRSK_ROLE (if set)
|
||||||
|
# MRSK_DESTINATION (if set)
|
||||||
|
# MRSK_RUNTIME
|
||||||
|
|
||||||
|
hosts = ENV["MRSK_HOSTS"].split(",")
|
||||||
|
results = nil
|
||||||
|
max = 3
|
||||||
|
|
||||||
|
elapsed = Benchmark.realtime do
|
||||||
|
results = hosts.map do |host|
|
||||||
|
Thread.new do
|
||||||
|
tries = 1
|
||||||
|
|
||||||
|
begin
|
||||||
|
Socket.getaddrinfo(host, 0, Socket::AF_UNSPEC, Socket::SOCK_STREAM, nil, Socket::AI_CANONNAME)
|
||||||
|
rescue SocketError
|
||||||
|
if tries < max
|
||||||
|
puts "Retrying DNS warmup: #{host}"
|
||||||
|
tries += 1
|
||||||
|
sleep rand
|
||||||
|
retry
|
||||||
|
else
|
||||||
|
puts "DNS warmup failed: #{host}"
|
||||||
|
host
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
tries
|
||||||
|
end
|
||||||
|
end.map(&:value)
|
||||||
|
end
|
||||||
|
|
||||||
|
retries = results.sum - hosts.size
|
||||||
|
nopes = results.count { |r| r == max }
|
||||||
|
|
||||||
|
puts "Prewarmed %d DNS lookups in %.2f sec: %d retries, %d failures" % [ hosts.size, elapsed, retries, nopes ]
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -91,7 +94,7 @@ class Mrsk::Cli::Traefik < Mrsk::Cli::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
desc "remove_container", "Remove Traefik image from servers", hide: true
|
desc "remove_image", "Remove Traefik image from servers", hide: true
|
||||||
def remove_image
|
def remove_image
|
||||||
with_lock do
|
with_lock do
|
||||||
on(MRSK.traefik_hosts) do
|
on(MRSK.traefik_hosts) do
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ require "active_support/core_ext/enumerable"
|
|||||||
require "active_support/core_ext/module/delegation"
|
require "active_support/core_ext/module/delegation"
|
||||||
|
|
||||||
class Mrsk::Commander
|
class Mrsk::Commander
|
||||||
attr_accessor :verbosity, :lock_count
|
attr_accessor :verbosity, :holding_lock, :hold_lock_on_error
|
||||||
|
|
||||||
def initialize
|
def initialize
|
||||||
self.verbosity = :info
|
self.verbosity = :info
|
||||||
self.lock_count = 0
|
self.holding_lock = false
|
||||||
|
self.hold_lock_on_error = false
|
||||||
end
|
end
|
||||||
|
|
||||||
def config
|
def config
|
||||||
@@ -35,7 +36,7 @@ class Mrsk::Commander
|
|||||||
end
|
end
|
||||||
|
|
||||||
def primary_host
|
def primary_host
|
||||||
specific_hosts&.first || config.primary_web_host
|
specific_hosts&.first || specific_roles&.first&.primary_host || config.primary_web_host
|
||||||
end
|
end
|
||||||
|
|
||||||
def roles
|
def roles
|
||||||
@@ -50,6 +51,14 @@ class Mrsk::Commander
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def boot_strategy
|
||||||
|
if config.boot.limit.present?
|
||||||
|
{ in: :groups, limit: config.boot.limit, wait: config.boot.wait }
|
||||||
|
else
|
||||||
|
{}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def roles_on(host)
|
def roles_on(host)
|
||||||
roles.select { |role| role.hosts.include?(host.to_s) }.map(&:name)
|
roles.select { |role| role.hosts.include?(host.to_s) }.map(&:name)
|
||||||
end
|
end
|
||||||
@@ -75,18 +84,30 @@ class Mrsk::Commander
|
|||||||
Mrsk::Commands::Accessory.new(config, name: name)
|
Mrsk::Commands::Accessory.new(config, name: name)
|
||||||
end
|
end
|
||||||
|
|
||||||
def auditor(role: nil)
|
def auditor(**details)
|
||||||
Mrsk::Commands::Auditor.new(config, role: role)
|
Mrsk::Commands::Auditor.new(config, **details)
|
||||||
end
|
end
|
||||||
|
|
||||||
def builder
|
def builder
|
||||||
@builder ||= Mrsk::Commands::Builder.new(config)
|
@builder ||= Mrsk::Commands::Builder.new(config)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def docker
|
||||||
|
@docker ||= Mrsk::Commands::Docker.new(config)
|
||||||
|
end
|
||||||
|
|
||||||
def healthcheck
|
def healthcheck
|
||||||
@healthcheck ||= Mrsk::Commands::Healthcheck.new(config)
|
@healthcheck ||= Mrsk::Commands::Healthcheck.new(config)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def hook
|
||||||
|
@hook ||= Mrsk::Commands::Hook.new(config)
|
||||||
|
end
|
||||||
|
|
||||||
|
def lock
|
||||||
|
@lock ||= Mrsk::Commands::Lock.new(config)
|
||||||
|
end
|
||||||
|
|
||||||
def prune
|
def prune
|
||||||
@prune ||= Mrsk::Commands::Prune.new(config)
|
@prune ||= Mrsk::Commands::Prune.new(config)
|
||||||
end
|
end
|
||||||
@@ -99,10 +120,6 @@ class Mrsk::Commander
|
|||||||
@traefik ||= Mrsk::Commands::Traefik.new(config)
|
@traefik ||= Mrsk::Commands::Traefik.new(config)
|
||||||
end
|
end
|
||||||
|
|
||||||
def lock
|
|
||||||
@lock ||= Mrsk::Commands::Lock.new(config)
|
|
||||||
end
|
|
||||||
|
|
||||||
def with_verbosity(level)
|
def with_verbosity(level)
|
||||||
old_level = self.verbosity
|
old_level = self.verbosity
|
||||||
|
|
||||||
@@ -115,6 +132,14 @@ class Mrsk::Commander
|
|||||||
SSHKit.config.output_verbosity = old_level
|
SSHKit.config.output_verbosity = old_level
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def holding_lock?
|
||||||
|
self.holding_lock
|
||||||
|
end
|
||||||
|
|
||||||
|
def hold_lock_on_error?
|
||||||
|
self.hold_lock_on_error
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
# Lazy setup of SSHKit
|
# Lazy setup of SSHKit
|
||||||
def configure_sshkit_with(config)
|
def configure_sshkit_with(config)
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
|
|||||||
@role = role
|
@role = role
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def start_or_run
|
||||||
|
combine start, run, by: "||"
|
||||||
|
end
|
||||||
|
|
||||||
def run
|
def run
|
||||||
role = config.role(self.role)
|
role = config.role(self.role)
|
||||||
|
|
||||||
@@ -15,6 +19,7 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
|
|||||||
"--name", container_name,
|
"--name", container_name,
|
||||||
"-e", "MRSK_CONTAINER_NAME=\"#{container_name}\"",
|
"-e", "MRSK_CONTAINER_NAME=\"#{container_name}\"",
|
||||||
*role.env_args,
|
*role.env_args,
|
||||||
|
*role.health_check_args,
|
||||||
*config.logging_args,
|
*config.logging_args,
|
||||||
*config.volume_args,
|
*config.volume_args,
|
||||||
*role.label_args,
|
*role.label_args,
|
||||||
@@ -27,9 +32,13 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
|
|||||||
docker :start, container_name
|
docker :start, container_name
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def status(version:)
|
||||||
|
pipe container_id_for_version(version), xargs(docker(:inspect, "--format", DOCKER_HEALTH_STATUS_FORMAT))
|
||||||
|
end
|
||||||
|
|
||||||
def stop(version: nil)
|
def stop(version: nil)
|
||||||
pipe \
|
pipe \
|
||||||
version ? container_id_for_version(version) : current_container_id,
|
version ? container_id_for_version(version) : current_running_container_id,
|
||||||
xargs(config.stop_wait_time ? docker(:stop, "-t", config.stop_wait_time) : docker(:stop))
|
xargs(config.stop_wait_time ? docker(:stop, "-t", config.stop_wait_time) : docker(:stop))
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -40,7 +49,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_running_container_id,
|
||||||
"xargs docker logs#{" --since #{since}" if since}#{" --tail #{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
|
||||||
@@ -48,7 +57,7 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
|
|||||||
def follow_logs(host:, grep: nil)
|
def follow_logs(host:, grep: nil)
|
||||||
run_over_ssh \
|
run_over_ssh \
|
||||||
pipe(
|
pipe(
|
||||||
current_container_id,
|
current_running_container_id,
|
||||||
"xargs docker logs --timestamps --tail 10 --follow 2>&1",
|
"xargs docker logs --timestamps --tail 10 --follow 2>&1",
|
||||||
(%(grep "#{grep}") if grep)
|
(%(grep "#{grep}") if grep)
|
||||||
),
|
),
|
||||||
@@ -82,20 +91,23 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
def current_container_id
|
def current_running_container_id
|
||||||
docker :ps, "--quiet", *filter_args
|
docker :ps, "--quiet", *filter_args(status: :running), "--latest"
|
||||||
end
|
end
|
||||||
|
|
||||||
def container_id_for_version(version)
|
def container_id_for_version(version, only_running: false)
|
||||||
container_id_for(container_name: container_name(version))
|
container_id_for(container_name: container_name(version), only_running: only_running)
|
||||||
end
|
end
|
||||||
|
|
||||||
def current_running_version
|
def current_running_version
|
||||||
# FIXME: Find more graceful way to extract the version from "app-version" than using sed and tail!
|
list_versions("--latest", status: :running)
|
||||||
|
end
|
||||||
|
|
||||||
|
def list_versions(*docker_args, status: nil)
|
||||||
pipe \
|
pipe \
|
||||||
docker(:ps, *filter_args, "--format", '"{{.Names}}"'),
|
docker(:ps, *filter_args(status: status), *docker_args, "--format", '"{{.Names}}"'),
|
||||||
%(sed 's/-/\\n/g'),
|
%(grep -oE "\\-[^-]+$"), # Extract SHA from "service-role-dest-SHA"
|
||||||
"tail -n 1"
|
%(cut -c 2-)
|
||||||
end
|
end
|
||||||
|
|
||||||
def list_containers
|
def list_containers
|
||||||
@@ -128,20 +140,25 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
|
|||||||
docker :image, :prune, "--all", "--force", *filter_args
|
docker :image, :prune, "--all", "--force", *filter_args
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def tag_current_as_latest
|
||||||
|
docker :tag, config.absolute_image, config.latest_image
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
private
|
private
|
||||||
def container_name(version = nil)
|
def container_name(version = nil)
|
||||||
[ config.service, role, config.destination, version || config.version ].compact.join("-")
|
[ config.service, role, config.destination, version || config.version ].compact.join("-")
|
||||||
end
|
end
|
||||||
|
|
||||||
def filter_args
|
def filter_args(status: nil)
|
||||||
argumentize "--filter", filters
|
argumentize "--filter", filters(status: status)
|
||||||
end
|
end
|
||||||
|
|
||||||
def filters
|
def filters(status: nil)
|
||||||
[ "label=service=#{config.service}" ].tap do |filters|
|
[ "label=service=#{config.service}" ].tap do |filters|
|
||||||
filters << "label=destination=#{config.destination}" if config.destination
|
filters << "label=destination=#{config.destination}" if config.destination
|
||||||
filters << "label=role=#{role}" if role
|
filters << "label=role=#{role}" if role
|
||||||
|
filters << "status=#{status}" if status
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,27 +1,18 @@
|
|||||||
require "active_support/core_ext/time/conversions"
|
|
||||||
|
|
||||||
class Mrsk::Commands::Auditor < Mrsk::Commands::Base
|
class Mrsk::Commands::Auditor < Mrsk::Commands::Base
|
||||||
attr_reader :role
|
attr_reader :details
|
||||||
|
|
||||||
def initialize(config, role: nil)
|
def initialize(config, **details)
|
||||||
super(config)
|
super(config)
|
||||||
@role = role
|
@details = details
|
||||||
end
|
end
|
||||||
|
|
||||||
# Runs remotely
|
# Runs remotely
|
||||||
def record(line)
|
def record(line, **details)
|
||||||
append \
|
append \
|
||||||
[ :echo, tagged_record_line(line) ],
|
[ :echo, audit_tags(**details).except(:version, :service_version).to_s, line ],
|
||||||
audit_log_file
|
audit_log_file
|
||||||
end
|
end
|
||||||
|
|
||||||
# Runs locally
|
|
||||||
def broadcast(line)
|
|
||||||
if broadcast_cmd = config.audit_broadcast_cmd
|
|
||||||
[ broadcast_cmd, tagged_broadcast_line(line) ]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def reveal
|
def reveal
|
||||||
[ :tail, "-n", 50, audit_log_file ]
|
[ :tail, "-n", 50, audit_log_file ]
|
||||||
end
|
end
|
||||||
@@ -31,27 +22,7 @@ class Mrsk::Commands::Auditor < Mrsk::Commands::Base
|
|||||||
[ "mrsk", config.service, config.destination, "audit.log" ].compact.join("-")
|
[ "mrsk", config.service, config.destination, "audit.log" ].compact.join("-")
|
||||||
end
|
end
|
||||||
|
|
||||||
def tagged_record_line(line)
|
def audit_tags(**details)
|
||||||
tagged_line recorded_at_tag, performer_tag, role_tag, line
|
tags(**self.details, **details)
|
||||||
end
|
|
||||||
|
|
||||||
def tagged_broadcast_line(line)
|
|
||||||
tagged_line performer_tag, role_tag, line
|
|
||||||
end
|
|
||||||
|
|
||||||
def tagged_line(*tags_and_line)
|
|
||||||
"'#{tags_and_line.compact.join(" ")}'"
|
|
||||||
end
|
|
||||||
|
|
||||||
def recorded_at_tag
|
|
||||||
"[#{Time.now.to_fs(:db)}]"
|
|
||||||
end
|
|
||||||
|
|
||||||
def performer_tag
|
|
||||||
"[#{`whoami`.strip}]"
|
|
||||||
end
|
|
||||||
|
|
||||||
def role_tag
|
|
||||||
"[#{role}]" if role
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
module Mrsk::Commands
|
module Mrsk::Commands
|
||||||
class Base
|
class Base
|
||||||
delegate :redact, :argumentize, to: Mrsk::Utils
|
delegate :sensitive, :argumentize, to: Mrsk::Utils
|
||||||
|
|
||||||
|
DOCKER_HEALTH_STATUS_FORMAT = "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'"
|
||||||
|
DOCKER_HEALTH_LOG_FORMAT = "'{{json .State.Health}}'"
|
||||||
|
|
||||||
attr_accessor :config
|
attr_accessor :config
|
||||||
|
|
||||||
@@ -15,8 +18,8 @@ module Mrsk::Commands
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def container_id_for(container_name:)
|
def container_id_for(container_name:, only_running: false)
|
||||||
docker :container, :ls, "--all", "--filter", "name=^#{container_name}$", "--quiet"
|
docker :container, :ls, *("--all" unless only_running), "--filter", "name=^#{container_name}$", "--quiet"
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@@ -50,5 +53,9 @@ module Mrsk::Commands
|
|||||||
def docker(*args)
|
def docker(*args)
|
||||||
args.compact.unshift :docker
|
args.compact.unshift :docker
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def tags(**details)
|
||||||
|
Mrsk::Tags.from_config(config, **details)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ class Mrsk::Commands::Builder < Mrsk::Commands::Base
|
|||||||
delegate :create, :remove, :push, :clean, :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.inquiry
|
||||||
end
|
end
|
||||||
|
|
||||||
def target
|
def target
|
||||||
@@ -33,4 +33,24 @@ class Mrsk::Commands::Builder < Mrsk::Commands::Base
|
|||||||
def multiarch_remote
|
def multiarch_remote
|
||||||
@multiarch_remote ||= Mrsk::Commands::Builder::Multiarch::Remote.new(config)
|
@multiarch_remote ||= Mrsk::Commands::Builder::Multiarch::Remote.new(config)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_local_dependencies_installed
|
||||||
|
if name.native?
|
||||||
|
ensure_local_docker_installed
|
||||||
|
else
|
||||||
|
combine \
|
||||||
|
ensure_local_docker_installed,
|
||||||
|
ensure_local_buildx_installed
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def ensure_local_docker_installed
|
||||||
|
docker "--version"
|
||||||
|
end
|
||||||
|
|
||||||
|
def ensure_local_buildx_installed
|
||||||
|
docker :buildx, "version"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
|
|
||||||
class Mrsk::Commands::Builder::Base < Mrsk::Commands::Base
|
class Mrsk::Commands::Builder::Base < Mrsk::Commands::Base
|
||||||
|
class BuilderError < StandardError; end
|
||||||
|
|
||||||
delegate :argumentize, to: Mrsk::Utils
|
delegate :argumentize, to: Mrsk::Utils
|
||||||
|
|
||||||
def clean
|
def clean
|
||||||
@@ -7,7 +10,6 @@ class Mrsk::Commands::Builder::Base < Mrsk::Commands::Base
|
|||||||
|
|
||||||
def pull
|
def pull
|
||||||
docker :pull, config.absolute_image
|
docker :pull, config.absolute_image
|
||||||
docker :pull, config.latest_image
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_options
|
def build_options
|
||||||
@@ -18,6 +20,7 @@ class Mrsk::Commands::Builder::Base < Mrsk::Commands::Base
|
|||||||
context
|
context
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
private
|
private
|
||||||
def build_tags
|
def build_tags
|
||||||
[ "-t", config.absolute_image, "-t", config.latest_image ]
|
[ "-t", config.absolute_image, "-t", config.latest_image ]
|
||||||
@@ -28,7 +31,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
|
||||||
@@ -36,7 +39,11 @@ class Mrsk::Commands::Builder::Base < Mrsk::Commands::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def build_dockerfile
|
def build_dockerfile
|
||||||
argumentize "--file", dockerfile
|
if Pathname.new(File.expand_path(dockerfile)).exist?
|
||||||
|
argumentize "--file", dockerfile
|
||||||
|
else
|
||||||
|
raise BuilderError, "Missing #{dockerfile}"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def args
|
def args
|
||||||
|
|||||||
21
lib/mrsk/commands/docker.rb
Normal file
21
lib/mrsk/commands/docker.rb
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
class Mrsk::Commands::Docker < Mrsk::Commands::Base
|
||||||
|
# Install Docker using the https://github.com/docker/docker-install convenience script.
|
||||||
|
def install
|
||||||
|
pipe [ :curl, "-fsSL", "https://get.docker.com" ], :sh
|
||||||
|
end
|
||||||
|
|
||||||
|
# Checks the Docker client version. Fails if Docker is not installed.
|
||||||
|
def installed?
|
||||||
|
docker "-v"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Checks the Docker server version. Fails if Docker is not running.
|
||||||
|
def running?
|
||||||
|
docker :version
|
||||||
|
end
|
||||||
|
|
||||||
|
# Do we have superuser access to install Docker and start system services?
|
||||||
|
def superuser?
|
||||||
|
[ '[ "${EUID:-$(id -u)}" -eq 0 ]' ]
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -11,14 +11,19 @@ class Mrsk::Commands::Healthcheck < Mrsk::Commands::Base
|
|||||||
"--label", "service=#{container_name}",
|
"--label", "service=#{container_name}",
|
||||||
"-e", "MRSK_CONTAINER_NAME=\"#{container_name}\"",
|
"-e", "MRSK_CONTAINER_NAME=\"#{container_name}\"",
|
||||||
*web.env_args,
|
*web.env_args,
|
||||||
|
*web.health_check_args,
|
||||||
*config.volume_args,
|
*config.volume_args,
|
||||||
*web.option_args,
|
*web.option_args,
|
||||||
config.absolute_image,
|
config.absolute_image,
|
||||||
web.cmd
|
web.cmd
|
||||||
end
|
end
|
||||||
|
|
||||||
def curl
|
def status
|
||||||
[ :curl, "--silent", "--output", "/dev/null", "--write-out", "'%{http_code}'", "--max-time", "2", health_url ]
|
pipe container_id, xargs(docker(:inspect, "--format", DOCKER_HEALTH_STATUS_FORMAT))
|
||||||
|
end
|
||||||
|
|
||||||
|
def container_health_log
|
||||||
|
pipe container_id, xargs(docker(:inspect, "--format", DOCKER_HEALTH_LOG_FORMAT))
|
||||||
end
|
end
|
||||||
|
|
||||||
def logs
|
def logs
|
||||||
|
|||||||
14
lib/mrsk/commands/hook.rb
Normal file
14
lib/mrsk/commands/hook.rb
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
class Mrsk::Commands::Hook < Mrsk::Commands::Base
|
||||||
|
def run(hook, **details)
|
||||||
|
[ hook_file(hook), env: tags(**details).env ]
|
||||||
|
end
|
||||||
|
|
||||||
|
def hook_exists?(hook)
|
||||||
|
Pathname.new(hook_file(hook)).exist?
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def hook_file(hook)
|
||||||
|
"#{config.hooks_path}/#{hook}"
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
require "active_support/duration"
|
require "active_support/duration"
|
||||||
require "active_support/core_ext/numeric/time"
|
require "time"
|
||||||
|
|
||||||
class Mrsk::Commands::Lock < Mrsk::Commands::Base
|
class Mrsk::Commands::Lock < Mrsk::Commands::Base
|
||||||
def acquire(message, version)
|
def acquire(message, version)
|
||||||
@@ -49,7 +49,7 @@ class Mrsk::Commands::Lock < Mrsk::Commands::Base
|
|||||||
|
|
||||||
def lock_details(message, version)
|
def lock_details(message, version)
|
||||||
<<~DETAILS.strip
|
<<~DETAILS.strip
|
||||||
Locked by: #{locked_by} at #{Time.now.gmtime}
|
Locked by: #{locked_by} at #{Time.now.utc.iso8601}
|
||||||
Version: #{version}
|
Version: #{version}
|
||||||
Message: #{message}
|
Message: #{message}
|
||||||
DETAILS
|
DETAILS
|
||||||
|
|||||||
@@ -2,11 +2,19 @@ require "active_support/duration"
|
|||||||
require "active_support/core_ext/numeric/time"
|
require "active_support/core_ext/numeric/time"
|
||||||
|
|
||||||
class Mrsk::Commands::Prune < Mrsk::Commands::Base
|
class Mrsk::Commands::Prune < Mrsk::Commands::Base
|
||||||
def images(until_hours: 7.days.in_hours.to_i)
|
def images
|
||||||
docker :image, :prune, "--all", "--force", "--filter", "label=service=#{config.service}", "--filter", "until=#{until_hours}h"
|
docker :image, :prune, "--force", "--filter", "label=service=#{config.service}", "--filter", "dangling=true"
|
||||||
end
|
end
|
||||||
|
|
||||||
def containers(until_hours: 3.days.in_hours.to_i)
|
def containers(keep_last: 5)
|
||||||
docker :container, :prune, "--force", "--filter", "label=service=#{config.service}", "--filter", "until=#{until_hours}h"
|
pipe \
|
||||||
|
docker(:ps, "-q", "-a", "--filter", "label=service=#{config.service}", *stopped_containers_filters),
|
||||||
|
"tail -n +#{keep_last + 1}",
|
||||||
|
"while read container_id; do docker rm $container_id; done"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def stopped_containers_filters
|
||||||
|
[ "created", "exited", "dead" ].flat_map { |status| ["--filter", "status=#{status}"] }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -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, :argumentize_env_with_secrets, :optionize, to: Mrsk::Utils
|
||||||
|
|
||||||
IMAGE = "traefik:v2.9.9"
|
DEFAULT_IMAGE = "traefik:v2.9"
|
||||||
CONTAINER_PORT = 80
|
CONTAINER_PORT = 80
|
||||||
|
|
||||||
def run
|
def run
|
||||||
@@ -10,9 +10,11 @@ class Mrsk::Commands::Traefik < Mrsk::Commands::Base
|
|||||||
"--restart", "unless-stopped",
|
"--restart", "unless-stopped",
|
||||||
"--publish", port,
|
"--publish", port,
|
||||||
"--volume", "/var/run/docker.sock:/var/run/docker.sock",
|
"--volume", "/var/run/docker.sock:/var/run/docker.sock",
|
||||||
|
*env_args,
|
||||||
*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 +58,28 @@ class Mrsk::Commands::Traefik < Mrsk::Commands::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
def label_args
|
||||||
|
argumentize "--label", labels
|
||||||
|
end
|
||||||
|
|
||||||
|
def env_args
|
||||||
|
env_config = config.traefik["env"] || {}
|
||||||
|
|
||||||
|
if env_config.present?
|
||||||
|
argumentize_env_with_secrets(env_config)
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def labels
|
||||||
|
config.traefik["labels"] || []
|
||||||
|
end
|
||||||
|
|
||||||
|
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
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ require "erb"
|
|||||||
require "net/ssh/proxy/jump"
|
require "net/ssh/proxy/jump"
|
||||||
|
|
||||||
class Mrsk::Configuration
|
class Mrsk::Configuration
|
||||||
delegate :service, :image, :servers, :env, :labels, :registry, :builder, :stop_wait_time, to: :raw_config, allow_nil: true
|
delegate :service, :image, :servers, :env, :labels, :registry, :builder, :stop_wait_time, :hooks_path, to: :raw_config, allow_nil: true
|
||||||
delegate :argumentize, :argumentize_env_with_secrets, :optionize, to: Mrsk::Utils
|
delegate :argumentize, :argumentize_env_with_secrets, :optionize, to: Mrsk::Utils
|
||||||
|
|
||||||
attr_accessor :destination
|
attr_accessor :destination
|
||||||
@@ -50,7 +50,7 @@ class Mrsk::Configuration
|
|||||||
end
|
end
|
||||||
|
|
||||||
def version
|
def version
|
||||||
@declared_version.presence || ENV["VERSION"] || current_commit_hash
|
@declared_version.presence || ENV["VERSION"] || git_version
|
||||||
end
|
end
|
||||||
|
|
||||||
def abbreviated_version
|
def abbreviated_version
|
||||||
@@ -87,6 +87,10 @@ class Mrsk::Configuration
|
|||||||
roles.select(&:running_traefik?).flat_map(&:hosts).uniq
|
roles.select(&:running_traefik?).flat_map(&:hosts).uniq
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def boot
|
||||||
|
Mrsk::Configuration::Boot.new(config: self)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
def repository
|
def repository
|
||||||
[ raw_config.registry["server"], image ].compact.join("/")
|
[ raw_config.registry["server"], image ].compact.join("/")
|
||||||
@@ -143,6 +147,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
|
||||||
|
|
||||||
@@ -151,12 +157,8 @@ class Mrsk::Configuration
|
|||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
def audit_broadcast_cmd
|
|
||||||
raw_config.audit_broadcast_cmd
|
|
||||||
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
|
||||||
@@ -191,6 +193,10 @@ class Mrsk::Configuration
|
|||||||
raw_config.traefik || {}
|
raw_config.traefik || {}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def hooks_path
|
||||||
|
raw_config.hooks_path || ".mrsk/hooks"
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
# Will raise ArgumentError if any required config keys are missing
|
# Will raise ArgumentError if any required config keys are missing
|
||||||
def ensure_required_keys_present
|
def ensure_required_keys_present
|
||||||
@@ -227,10 +233,12 @@ class Mrsk::Configuration
|
|||||||
raw_config.servers.is_a?(Array) ? [ "web" ] : raw_config.servers.keys.sort
|
raw_config.servers.is_a?(Array) ? [ "web" ] : raw_config.servers.keys.sort
|
||||||
end
|
end
|
||||||
|
|
||||||
def current_commit_hash
|
def git_version
|
||||||
@current_commit_hash ||=
|
@git_version ||=
|
||||||
if system("git rev-parse")
|
if system("git rev-parse")
|
||||||
`git rev-parse HEAD`.strip
|
uncommitted_suffix = `git status --porcelain`.strip.present? ? "_uncommitted_#{SecureRandom.hex(8)}" : ""
|
||||||
|
|
||||||
|
"#{`git rev-parse HEAD`.strip}#{uncommitted_suffix}"
|
||||||
else
|
else
|
||||||
raise "Can't use commit hash as version, no git repository found in #{Dir.pwd}"
|
raise "Can't use commit hash as version, no git repository found in #{Dir.pwd}"
|
||||||
end
|
end
|
||||||
|
|||||||
20
lib/mrsk/configuration/boot.rb
Normal file
20
lib/mrsk/configuration/boot.rb
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
class Mrsk::Configuration::Boot
|
||||||
|
def initialize(config:)
|
||||||
|
@options = config.raw_config.boot || {}
|
||||||
|
@host_count = config.all_hosts.count
|
||||||
|
end
|
||||||
|
|
||||||
|
def limit
|
||||||
|
limit = @options["limit"]
|
||||||
|
|
||||||
|
if limit.to_s.end_with?("%")
|
||||||
|
@host_count * limit.to_i / 100
|
||||||
|
else
|
||||||
|
limit
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def wait
|
||||||
|
@options["wait"]
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -35,6 +35,28 @@ class Mrsk::Configuration::Role
|
|||||||
argumentize_env_with_secrets env
|
argumentize_env_with_secrets env
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def health_check_args
|
||||||
|
if health_check_cmd.present?
|
||||||
|
optionize({ "health-cmd" => health_check_cmd, "health-interval" => health_check_interval })
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def health_check_cmd
|
||||||
|
options = specializations["healthcheck"] || {}
|
||||||
|
options = config.healthcheck.merge(options) if running_traefik?
|
||||||
|
|
||||||
|
options["cmd"] || http_health_check(port: options["port"], path: options["path"])
|
||||||
|
end
|
||||||
|
|
||||||
|
def health_check_interval
|
||||||
|
options = specializations["healthcheck"] || {}
|
||||||
|
options = config.healthcheck.merge(options) if running_traefik?
|
||||||
|
|
||||||
|
options["interval"] || "1s"
|
||||||
|
end
|
||||||
|
|
||||||
def cmd
|
def cmd
|
||||||
specializations["cmd"]
|
specializations["cmd"]
|
||||||
end
|
end
|
||||||
@@ -74,18 +96,23 @@ class Mrsk::Configuration::Role
|
|||||||
def traefik_labels
|
def traefik_labels
|
||||||
if running_traefik?
|
if running_traefik?
|
||||||
{
|
{
|
||||||
"traefik.http.routers.#{config.service}.rule" => "PathPrefix(`/`)",
|
# Setting a service property ensures that the generated service name will be consistent between versions
|
||||||
"traefik.http.services.#{config.service}.loadbalancer.healthcheck.path" => config.healthcheck["path"],
|
"traefik.http.services.#{traefik_service}.loadbalancer.server.scheme" => "http",
|
||||||
"traefik.http.services.#{config.service}.loadbalancer.healthcheck.interval" => "1s",
|
|
||||||
"traefik.http.middlewares.#{config.service}-retry.retry.attempts" => "5",
|
"traefik.http.routers.#{traefik_service}.rule" => "PathPrefix(`/`)",
|
||||||
"traefik.http.middlewares.#{config.service}-retry.retry.initialinterval" => "500ms",
|
"traefik.http.middlewares.#{traefik_service}-retry.retry.attempts" => "5",
|
||||||
"traefik.http.routers.#{config.service}.middlewares" => "#{config.service}-retry@docker"
|
"traefik.http.middlewares.#{traefik_service}-retry.retry.initialinterval" => "500ms",
|
||||||
|
"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?
|
||||||
@@ -121,4 +148,8 @@ class Mrsk::Configuration::Role
|
|||||||
new_env["clear"] = (clear_app_env + clear_role_env).uniq
|
new_env["clear"] = (clear_app_env + clear_role_env).uniq
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def http_health_check(port:, path:)
|
||||||
|
"curl -f #{URI.join("http://localhost:#{port}", path)} || exit 1" if path.present? || port.present?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,12 +1,56 @@
|
|||||||
require "sshkit"
|
require "sshkit"
|
||||||
require "sshkit/dsl"
|
require "sshkit/dsl"
|
||||||
|
require "active_support/core_ext/hash/deep_merge"
|
||||||
|
require "json"
|
||||||
|
|
||||||
class SSHKit::Backend::Abstract
|
class SSHKit::Backend::Abstract
|
||||||
def capture_with_info(*args)
|
def capture_with_info(*args, **kwargs)
|
||||||
capture(*args, verbosity: Logger::INFO)
|
capture(*args, **kwargs, verbosity: Logger::INFO)
|
||||||
|
end
|
||||||
|
|
||||||
|
def capture_with_debug(*args, **kwargs)
|
||||||
|
capture(*args, **kwargs, verbosity: Logger::DEBUG)
|
||||||
|
end
|
||||||
|
|
||||||
|
def capture_with_pretty_json(*args, **kwargs)
|
||||||
|
JSON.pretty_generate(JSON.parse(capture(*args, **kwargs)))
|
||||||
end
|
end
|
||||||
|
|
||||||
def puts_by_host(host, output, type: "App")
|
def puts_by_host(host, output, type: "App")
|
||||||
puts "#{type} Host: #{host}\n#{output}\n\n"
|
puts "#{type} Host: #{host}\n#{output}\n\n"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Our execution pattern is for the CLI execute args lists returned
|
||||||
|
# from commands, but this doesn't support returning execution options
|
||||||
|
# from the command.
|
||||||
|
#
|
||||||
|
# Support this by using kwargs for CLI options and merging with the
|
||||||
|
# args-extracted options.
|
||||||
|
module CommandEnvMerge
|
||||||
|
private
|
||||||
|
|
||||||
|
# Override to merge options returned by commands in the args list with
|
||||||
|
# options passed by the CLI and pass them along as kwargs.
|
||||||
|
def command(args, options)
|
||||||
|
more_options, args = args.partition { |a| a.is_a? Hash }
|
||||||
|
more_options << options
|
||||||
|
|
||||||
|
build_command(args, **more_options.reduce(:deep_merge))
|
||||||
|
end
|
||||||
|
|
||||||
|
# Destructure options to pluck out env for merge
|
||||||
|
def build_command(args, env: nil, **options)
|
||||||
|
# Rely on native Ruby kwargs precedence rather than explicit Hash merges
|
||||||
|
SSHKit::Command.new(*args, **default_command_options, **options, env: env_for(env))
|
||||||
|
end
|
||||||
|
|
||||||
|
def default_command_options
|
||||||
|
{ in: pwd_path, host: @host, user: @user, group: @group }
|
||||||
|
end
|
||||||
|
|
||||||
|
def env_for(env)
|
||||||
|
@env.to_h.merge(env.to_h)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
prepend CommandEnvMerge
|
||||||
end
|
end
|
||||||
|
|||||||
39
lib/mrsk/tags.rb
Normal file
39
lib/mrsk/tags.rb
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
require "time"
|
||||||
|
|
||||||
|
class Mrsk::Tags
|
||||||
|
attr_reader :config, :tags
|
||||||
|
|
||||||
|
class << self
|
||||||
|
def from_config(config, **extra)
|
||||||
|
new(**default_tags(config), **extra)
|
||||||
|
end
|
||||||
|
|
||||||
|
def default_tags(config)
|
||||||
|
{ recorded_at: Time.now.utc.iso8601,
|
||||||
|
performer: `whoami`.chomp,
|
||||||
|
destination: config.destination,
|
||||||
|
version: config.version,
|
||||||
|
service_version: service_version(config) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def service_version(config)
|
||||||
|
[ config.service, config.abbreviated_version ].compact.join("@")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(**tags)
|
||||||
|
@tags = tags.compact
|
||||||
|
end
|
||||||
|
|
||||||
|
def env
|
||||||
|
tags.transform_keys { |detail| "MRSK_#{detail.upcase}" }
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_s
|
||||||
|
tags.values.map { |value| "[#{value}]" }.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
def except(*tags)
|
||||||
|
self.class.new(**self.tags.except(*tags))
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,12 +1,15 @@
|
|||||||
module Mrsk::Utils
|
module Mrsk::Utils
|
||||||
extend self
|
extend self
|
||||||
|
|
||||||
|
DOLLAR_SIGN_WITHOUT_SHELL_EXPANSION_REGEX = /\$(?!{[^\}]*\})/
|
||||||
|
|
||||||
# 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 +20,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,18 +42,55 @@ 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.
|
||||||
def escape_shell_value(value)
|
def escape_shell_value(value)
|
||||||
value.to_s.dump.gsub(/`/, '\\\\`')
|
value.to_s.dump
|
||||||
|
.gsub(/`/, '\\\\`')
|
||||||
|
.gsub(DOLLAR_SIGN_WITHOUT_SHELL_EXPANSION_REGEX, '\$')
|
||||||
end
|
end
|
||||||
|
|
||||||
# Abbreviate a git revhash for concise display
|
# Abbreviate a git revhash for concise display
|
||||||
def abbreviate_version(version)
|
def abbreviate_version(version)
|
||||||
version[0...7] if version
|
if version
|
||||||
|
# Don't abbreviate <sha>_uncommitted_<etc>
|
||||||
|
if version.include?("_")
|
||||||
|
version
|
||||||
|
else
|
||||||
|
version[0...7]
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
39
lib/mrsk/utils/healthcheck_poller.rb
Normal file
39
lib/mrsk/utils/healthcheck_poller.rb
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
class Mrsk::Utils::HealthcheckPoller
|
||||||
|
TRAEFIK_HEALTHY_DELAY = 2
|
||||||
|
|
||||||
|
class HealthcheckError < StandardError; end
|
||||||
|
|
||||||
|
class << self
|
||||||
|
def wait_for_healthy(pause_after_ready: false, &block)
|
||||||
|
attempt = 1
|
||||||
|
max_attempts = MRSK.config.healthcheck["max_attempts"]
|
||||||
|
|
||||||
|
begin
|
||||||
|
case status = block.call
|
||||||
|
when "healthy"
|
||||||
|
sleep TRAEFIK_HEALTHY_DELAY if pause_after_ready
|
||||||
|
when "running" # No health check configured
|
||||||
|
sleep MRSK.config.readiness_delay if pause_after_ready
|
||||||
|
else
|
||||||
|
raise HealthcheckError, "container not ready (#{status})"
|
||||||
|
end
|
||||||
|
rescue HealthcheckError => e
|
||||||
|
if attempt <= max_attempts
|
||||||
|
info "#{e.message}, retrying in #{attempt}s (attempt #{attempt}/#{max_attempts})..."
|
||||||
|
sleep attempt
|
||||||
|
attempt += 1
|
||||||
|
retry
|
||||||
|
else
|
||||||
|
raise
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
info "Container is healthy!"
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def info(message)
|
||||||
|
SSHKit.config.output.info(message)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
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.0"
|
VERSION = "0.13.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"
|
||||||
|
|||||||
@@ -2,10 +2,9 @@ require_relative "cli_test_case"
|
|||||||
|
|
||||||
class CliAppTest < CliTestCase
|
class CliAppTest < CliTestCase
|
||||||
test "boot" do
|
test "boot" do
|
||||||
# Stub current version fetch
|
stub_running
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:capture).returns("123") # old version
|
|
||||||
|
|
||||||
run_command("boot").tap do |output|
|
run_command("boot").tap do |output|
|
||||||
|
assert_match "docker tag dhh/app:latest dhh/app:latest", output
|
||||||
assert_match "docker run --detach --restart unless-stopped", output
|
assert_match "docker run --detach --restart unless-stopped", output
|
||||||
assert_match "docker container ls --all --filter name=^app-web-123$ --quiet | xargs docker stop", output
|
assert_match "docker container ls --all --filter name=^app-web-123$ --quiet | xargs docker stop", output
|
||||||
end
|
end
|
||||||
@@ -15,16 +14,20 @@ class CliAppTest < CliTestCase
|
|||||||
run_command("details") # Preheat MRSK const
|
run_command("details") # Preheat MRSK const
|
||||||
|
|
||||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet")
|
.with(:docker, :container, :ls, "--filter", "name=^app-web-latest$", "--quiet", raise_on_non_zero_exit: false)
|
||||||
.returns("12345678") # running version
|
.returns("12345678") # running version
|
||||||
|
|
||||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
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")
|
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'")
|
||||||
|
.returns("running") # health check
|
||||||
|
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
|
.with(:docker, :ps, "--filter", "label=service=app", "--filter", "label=role=web", "--filter", "status=running", "--latest", "--format", "\"{{.Names}}\"", "|", "grep -oE \"\\-[^-]+$\"", "|", "cut -c 2-", raise_on_non_zero_exit: false)
|
||||||
.returns("123") # old version
|
.returns("123") # old version
|
||||||
|
|
||||||
run_command("boot").tap do |output|
|
run_command("boot").tap do |output|
|
||||||
assert_match /Renaming container .* to .* as already deployed on 1.1.1.1/, output # Rename
|
assert_match /Renaming container .* to .* as already deployed on 1.1.1.1/, output # Rename
|
||||||
assert_match /docker rename .* .*/, output
|
assert_match /docker rename app-web-latest app-web-latest_replaced_[0-9a-f]{16}/, output
|
||||||
assert_match "docker run --detach --restart unless-stopped", output
|
assert_match "docker run --detach --restart unless-stopped", output
|
||||||
assert_match "docker container ls --all --filter name=^app-web-123$ --quiet | xargs docker stop", output
|
assert_match "docker container ls --all --filter name=^app-web-123$ --quiet | xargs docker stop", output
|
||||||
end
|
end
|
||||||
@@ -32,6 +35,28 @@ class CliAppTest < CliTestCase
|
|||||||
Thread.report_on_exception = true
|
Thread.report_on_exception = true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "boot uses group strategy when specified" do
|
||||||
|
Mrsk::Cli::App.any_instance.stubs(:on).with("1.1.1.1").twice # acquire & release lock
|
||||||
|
Mrsk::Cli::App.any_instance.stubs(:on).with([ "1.1.1.1" ]) # tag container
|
||||||
|
|
||||||
|
# Strategy is used when booting the containers
|
||||||
|
Mrsk::Cli::App.any_instance.expects(:on).with([ "1.1.1.1" ], in: :groups, limit: 3, wait: 2).with_block_given
|
||||||
|
|
||||||
|
run_command("boot", config: :with_boot_strategy)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "boot errors leave lock in place" do
|
||||||
|
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "version" => "999" }
|
||||||
|
|
||||||
|
Mrsk::Cli::App.any_instance.expects(:using_version).raises(RuntimeError)
|
||||||
|
|
||||||
|
assert !MRSK.holding_lock?
|
||||||
|
assert_raises(RuntimeError) do
|
||||||
|
stderred { run_command("boot") }
|
||||||
|
end
|
||||||
|
assert MRSK.holding_lock?
|
||||||
|
end
|
||||||
|
|
||||||
test "start" do
|
test "start" do
|
||||||
run_command("start").tap do |output|
|
run_command("start").tap do |output|
|
||||||
assert_match "docker start app-web-999", output
|
assert_match "docker start app-web-999", output
|
||||||
@@ -40,7 +65,28 @@ 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 --quiet --filter label=service=app --filter label=role=web | xargs docker stop", output
|
assert_match "docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --latest | xargs docker stop", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "stale_containers" do
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
|
.with(:docker, :ps, "--filter", "label=service=app", "--filter", "label=role=web", "--format", "\"{{.Names}}\"", "|", "grep -oE \"\\-[^-]+$\"", "|", "cut -c 2-", raise_on_non_zero_exit: false)
|
||||||
|
.returns("12345678\n87654321")
|
||||||
|
|
||||||
|
run_command("stale_containers").tap do |output|
|
||||||
|
assert_match /Detected stale container for role web with version 87654321/, output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "stop stale_containers" do
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
|
.with(:docker, :ps, "--filter", "label=service=app", "--filter", "label=role=web", "--format", "\"{{.Names}}\"", "|", "grep -oE \"\\-[^-]+$\"", "|", "cut -c 2-", raise_on_non_zero_exit: false)
|
||||||
|
.returns("12345678\n87654321")
|
||||||
|
|
||||||
|
run_command("stale_containers", "--stop").tap do |output|
|
||||||
|
assert_match /Stopping stale container for role web with version 87654321/, output
|
||||||
|
assert_match /#{Regexp.escape("docker container ls --all --filter name=^app-web-87654321$ --quiet | xargs docker stop")}/, output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -52,7 +98,7 @@ class CliAppTest < CliTestCase
|
|||||||
|
|
||||||
test "remove" do
|
test "remove" do
|
||||||
run_command("remove").tap do |output|
|
run_command("remove").tap do |output|
|
||||||
assert_match /#{Regexp.escape("docker ps --quiet --filter label=service=app --filter label=role=web | xargs docker stop")}/, output
|
assert_match /#{Regexp.escape("docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --latest | xargs docker stop")}/, output
|
||||||
assert_match /#{Regexp.escape("docker container prune --force --filter label=service=app")}/, output
|
assert_match /#{Regexp.escape("docker container prune --force --filter label=service=app")}/, output
|
||||||
assert_match /#{Regexp.escape("docker image prune --all --force --filter label=service=app")}/, output
|
assert_match /#{Regexp.escape("docker image prune --all --force --filter label=service=app")}/, output
|
||||||
end
|
end
|
||||||
@@ -84,7 +130,7 @@ class CliAppTest < CliTestCase
|
|||||||
|
|
||||||
test "exec with reuse" do
|
test "exec with reuse" do
|
||||||
run_command("exec", "--reuse", "ruby -v").tap do |output|
|
run_command("exec", "--reuse", "ruby -v").tap do |output|
|
||||||
assert_match "docker ps --filter label=service=app --format \"{{.Names}}\" | sed 's/-/\\n/g' | tail -n 1", output # Get current version
|
assert_match "docker ps --filter label=service=app --filter status=running --latest --format \"{{.Names}}\" | grep -oE \"\\-[^-]+$\" | cut -c 2-", output # Get current version
|
||||||
assert_match "docker exec app-web-999 ruby -v", output
|
assert_match "docker exec app-web-999 ruby -v", output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -103,33 +149,41 @@ class CliAppTest < CliTestCase
|
|||||||
|
|
||||||
test "logs" do
|
test "logs" do
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:exec)
|
SSHKit::Backend::Abstract.any_instance.stubs(:exec)
|
||||||
.with("ssh -t root@1.1.1.1 'docker ps --quiet --filter label=service=app --filter label=role=web | xargs docker logs --timestamps --tail 10 2>&1'")
|
.with("ssh -t root@1.1.1.1 'docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --latest| xargs docker logs --timestamps --tail 10 2>&1'")
|
||||||
|
|
||||||
assert_match "docker ps --quiet --filter label=service=app --filter label=role=web | xargs docker logs --tail 100 2>&1", run_command("logs")
|
assert_match "docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --latest | xargs docker logs --tail 100 2>&1", run_command("logs")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "logs with follow" do
|
test "logs with follow" do
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:exec)
|
SSHKit::Backend::Abstract.any_instance.stubs(:exec)
|
||||||
.with("ssh -t root@1.1.1.1 'docker ps --quiet --filter label=service=app --filter label=role=web | xargs docker logs --timestamps --tail 10 --follow 2>&1'")
|
.with("ssh -t root@1.1.1.1 'docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --latest | xargs docker logs --timestamps --tail 10 --follow 2>&1'")
|
||||||
|
|
||||||
assert_match "docker ps --quiet --filter label=service=app --filter label=role=web | xargs docker logs --timestamps --tail 10 --follow 2>&1", run_command("logs", "--follow")
|
assert_match "docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --latest | xargs docker logs --timestamps --tail 10 --follow 2>&1", run_command("logs", "--follow")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "version" do
|
test "version" do
|
||||||
run_command("version").tap do |output|
|
run_command("version").tap do |output|
|
||||||
assert_match "docker ps --filter label=service=app --format \"{{.Names}}\" | sed 's/-/\\n/g' | tail -n 1", output
|
assert_match "docker ps --filter label=service=app --filter status=running --latest --format \"{{.Names}}\" | grep -oE \"\\-[^-]+$\" | cut -c 2-", output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
test "version through main" do
|
test "version through main" do
|
||||||
stdouted { Mrsk::Cli::Main.start(["app", "version", "-c", "test/fixtures/deploy_with_accessories.yml", "--hosts", "1.1.1.1"]) }.tap do |output|
|
stdouted { Mrsk::Cli::Main.start(["app", "version", "-c", "test/fixtures/deploy_with_accessories.yml", "--hosts", "1.1.1.1"]) }.tap do |output|
|
||||||
assert_match "docker ps --filter label=service=app --format \"{{.Names}}\" | sed 's/-/\\n/g' | tail -n 1", output
|
assert_match "docker ps --filter label=service=app --filter status=running --latest --format \"{{.Names}}\" | grep -oE \"\\-[^-]+$\" | cut -c 2-", output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def run_command(*command)
|
def run_command(*command, config: :with_accessories)
|
||||||
stdouted { Mrsk::Cli::App.start([*command, "-c", "test/fixtures/deploy_with_accessories.yml", "--hosts", "1.1.1.1"]) }
|
stdouted { Mrsk::Cli::App.start([*command, "-c", "test/fixtures/deploy_#{config}.yml", "--hosts", "1.1.1.1"]) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def stub_running
|
||||||
|
SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info).returns("123") # old version
|
||||||
|
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
|
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'")
|
||||||
|
.returns("running") # health check
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ class CliBuildTest < CliTestCase
|
|||||||
|
|
||||||
test "push" do
|
test "push" do
|
||||||
run_command("push").tap do |output|
|
run_command("push").tap do |output|
|
||||||
|
assert_match /docker --version && docker buildx version/, output
|
||||||
assert_match /docker buildx build --push --platform linux\/amd64,linux\/arm64 --builder mrsk-app-multiarch -t dhh\/app:999 -t dhh\/app:latest --label service="app" --file Dockerfile \. as .*@localhost/, output
|
assert_match /docker buildx build --push --platform linux\/amd64,linux\/arm64 --builder mrsk-app-multiarch -t dhh\/app:999 -t dhh\/app:latest --label service="app" --file Dockerfile \. as .*@localhost/, output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -17,7 +18,10 @@ class CliBuildTest < CliTestCase
|
|||||||
test "push without builder" do
|
test "push without builder" do
|
||||||
stub_locking
|
stub_locking
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||||
.with { |arg| arg == :docker }
|
.with(:docker, "--version", "&&", :docker, :buildx, "version")
|
||||||
|
|
||||||
|
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||||
|
.with { |*args| args[0..1] == [:docker, :buildx] }
|
||||||
.raises(SSHKit::Command::Failed.new("no builder"))
|
.raises(SSHKit::Command::Failed.new("no builder"))
|
||||||
.then
|
.then
|
||||||
.returns(true)
|
.returns(true)
|
||||||
@@ -27,10 +31,28 @@ class CliBuildTest < CliTestCase
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "push with no buildx plugin" do
|
||||||
|
stub_locking
|
||||||
|
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||||
|
.with(:docker, "--version", "&&", :docker, :buildx, "version")
|
||||||
|
.raises(SSHKit::Command::Failed.new("no buildx"))
|
||||||
|
|
||||||
|
Mrsk::Commands::Builder.any_instance.stubs(:native_and_local?).returns(false)
|
||||||
|
assert_raises(Mrsk::Cli::Build::BuildError) { run_command("push") }
|
||||||
|
end
|
||||||
|
|
||||||
|
test "push pre-build hook failure" do
|
||||||
|
fail_hook("pre-build")
|
||||||
|
|
||||||
|
assert_raises(Mrsk::Cli::HookError) { run_command("push") }
|
||||||
|
|
||||||
|
assert @executions.none? { |args| args[0..2] == [:docker, :buildx, :build] }
|
||||||
|
end
|
||||||
|
|
||||||
test "pull" do
|
test "pull" do
|
||||||
run_command("pull").tap do |output|
|
run_command("pull").tap do |output|
|
||||||
assert_match /docker image rm --force dhh\/app:999/, output
|
assert_match /docker image rm --force dhh\/app:999/, output
|
||||||
assert_match /docker pull dhh\/app:latest/, output
|
assert_match /docker pull dhh\/app:999/, output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -73,10 +95,10 @@ class CliBuildTest < CliTestCase
|
|||||||
stdouted { Mrsk::Cli::Build.start([*command, "-c", "test/fixtures/deploy_with_accessories.yml"]) }
|
stdouted { Mrsk::Cli::Build.start([*command, "-c", "test/fixtures/deploy_with_accessories.yml"]) }
|
||||||
end
|
end
|
||||||
|
|
||||||
def stub_locking
|
def stub_dependency_checks
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||||
.with { |arg1, arg2| arg1 == :mkdir && arg2 == :mrsk_lock }
|
.with(:docker, "--version", "&&", :docker, :buildx, "version")
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||||
.with { |arg1, arg2| arg1 == :rm && arg2 == "mrsk_lock/details" }
|
.with { |*args| args[0..1] == [:docker, :buildx] }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
require "test_helper"
|
require "test_helper"
|
||||||
require "active_support/testing/stream"
|
|
||||||
|
|
||||||
class CliTestCase < ActiveSupport::TestCase
|
class CliTestCase < ActiveSupport::TestCase
|
||||||
include ActiveSupport::Testing::Stream
|
|
||||||
|
|
||||||
setup do
|
setup do
|
||||||
ENV["VERSION"] = "999"
|
ENV["VERSION"] = "999"
|
||||||
ENV["RAILS_MASTER_KEY"] = "123"
|
ENV["RAILS_MASTER_KEY"] = "123"
|
||||||
@@ -19,7 +16,30 @@ class CliTestCase < ActiveSupport::TestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def stdouted
|
def fail_hook(hook)
|
||||||
capture(:stdout) { yield }.strip
|
@executions = []
|
||||||
|
Mrsk::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)
|
||||||
|
|
||||||
|
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||||
|
.with { |*args| @executions << args; args != [".mrsk/hooks/#{hook}"] }
|
||||||
|
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||||
|
.with { |*args| args.first == ".mrsk/hooks/#{hook}" }
|
||||||
|
.raises(SSHKit::Command::Failed.new("failed"))
|
||||||
|
end
|
||||||
|
|
||||||
|
def ensure_hook_runs(hook)
|
||||||
|
Mrsk::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)
|
||||||
|
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||||
|
.with { |*args| args != [".mrsk/hooks/#{hook}"] }
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:execute)
|
||||||
|
.with { |*args| args.first == ".mrsk/hooks/#{hook}" }
|
||||||
|
.once
|
||||||
|
end
|
||||||
|
|
||||||
|
def stub_locking
|
||||||
|
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||||
|
.with { |arg1, arg2| arg1 == :mkdir && arg2 == :mrsk_lock }
|
||||||
|
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||||
|
.with { |arg1, arg2| arg1 == :rm && arg2 == "mrsk_lock/details" }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,62 +5,63 @@ class CliHealthcheckTest < 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
|
||||||
|
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:sleep) # No sleeping when retrying
|
Mrsk::Utils::HealthcheckPoller.stubs(:sleep) # No sleeping when retrying
|
||||||
|
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||||
.with(:docker, :container, :ls, "--all", "--filter", "name=^healthcheck-app-999$", "--quiet", "|", :xargs, :docker, :stop, raise_on_non_zero_exit: false)
|
.with(:docker, :container, :ls, "--all", "--filter", "name=^healthcheck-app-999$", "--quiet", "|", :xargs, :docker, :stop, raise_on_non_zero_exit: false)
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||||
.with(:docker, :run, "--detach", "--name", "healthcheck-app-999", "--publish", "3999:3000", "--label", "service=healthcheck-app", "-e", "MRSK_CONTAINER_NAME=\"healthcheck-app\"", "dhh/app:999")
|
.with(:docker, :run, "--detach", "--name", "healthcheck-app-999", "--publish", "3999:3000", "--label", "service=healthcheck-app", "-e", "MRSK_CONTAINER_NAME=\"healthcheck-app\"", "--health-cmd", "\"curl -f http://localhost:3000/up || exit 1\"", "--health-interval", "\"1s\"", "dhh/app:999")
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||||
.with(:docker, :container, :ls, "--all", "--filter", "name=^healthcheck-app-999$", "--quiet", "|", :xargs, :docker, :container, :rm, raise_on_non_zero_exit: false)
|
.with(:docker, :container, :ls, "--all", "--filter", "name=^healthcheck-app-999$", "--quiet", "|", :xargs, :docker, :container, :rm, raise_on_non_zero_exit: false)
|
||||||
|
|
||||||
# Fail twice to test retry logic
|
# Fail twice to test retry logic
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info)
|
SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info)
|
||||||
.with(:curl, "--silent", "--output", "/dev/null", "--write-out", "'%{http_code}'", "--max-time", "2", "http://localhost:3999/up")
|
.with(:docker, :container, :ls, "--all", "--filter", "name=^healthcheck-app-999$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'")
|
||||||
.raises(SSHKit::Command::Failed)
|
.returns("starting")
|
||||||
.then
|
.then
|
||||||
.raises(SSHKit::Command::Failed)
|
.returns("unhealthy")
|
||||||
.then
|
.then
|
||||||
.returns("200")
|
.returns("healthy")
|
||||||
|
|
||||||
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 "container not ready (starting), retrying in 1s (attempt 1/7)...", output
|
||||||
assert_match "Health check against /up failed to respond, retrying in 2s...", output
|
assert_match "container not ready (unhealthy), retrying in 2s (attempt 2/7)...", output
|
||||||
assert_match "Health check against /up succeeded with 200 OK!", output
|
assert_match "Container is healthy!", output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "perform failing because of curl" do
|
test "perform failing to become healthy" do
|
||||||
# Prevent expected failures from outputting to terminal
|
# Prevent expected failures from outputting to terminal
|
||||||
Thread.report_on_exception = false
|
Thread.report_on_exception = false
|
||||||
|
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute) # No need to execute anything here
|
Mrsk::Utils::HealthcheckPoller.stubs(:sleep) # No sleeping when retrying
|
||||||
|
|
||||||
|
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||||
|
.with(:docker, :container, :ls, "--all", "--filter", "name=^healthcheck-app-999$", "--quiet", "|", :xargs, :docker, :stop, raise_on_non_zero_exit: false)
|
||||||
|
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||||
|
.with(:docker, :run, "--detach", "--name", "healthcheck-app-999", "--publish", "3999:3000", "--label", "service=healthcheck-app", "-e", "MRSK_CONTAINER_NAME=\"healthcheck-app\"", "--health-cmd", "\"curl -f http://localhost:3000/up || exit 1\"", "--health-interval", "\"1s\"", "dhh/app:999")
|
||||||
|
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||||
|
.with(:docker, :container, :ls, "--all", "--filter", "name=^healthcheck-app-999$", "--quiet", "|", :xargs, :docker, :container, :rm, raise_on_non_zero_exit: false)
|
||||||
|
|
||||||
|
# Continually report unhealthy
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info)
|
SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info)
|
||||||
.with(:curl, "--silent", "--output", "/dev/null", "--write-out", "'%{http_code}'", "--max-time", "2", "http://localhost:3999/up")
|
.with(:docker, :container, :ls, "--all", "--filter", "name=^healthcheck-app-999$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'")
|
||||||
.returns("curl: command not found")
|
.returns("unhealthy")
|
||||||
|
|
||||||
|
# Capture logs when failing
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info)
|
SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info)
|
||||||
.with(:docker, :container, :ls, "--all", "--filter", "name=^healthcheck-app-999$", "--quiet", "|", :xargs, :docker, :logs, "--tail", 50, "2>&1")
|
.with(:docker, :container, :ls, "--all", "--filter", "name=^healthcheck-app-999$", "--quiet", "|", :xargs, :docker, :logs, "--tail", 50, "2>&1")
|
||||||
|
.returns("some log output")
|
||||||
|
|
||||||
exception = assert_raises SSHKit::Runner::ExecuteError do
|
# Capture container health log when failing
|
||||||
run_command("perform")
|
SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_pretty_json)
|
||||||
end
|
.with(:docker, :container, :ls, "--all", "--filter", "name=^healthcheck-app-999$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{json .State.Health}}'")
|
||||||
assert_match "Health check against /up failed to return 200 OK!", exception.message
|
.returns('{"Status":"unhealthy","Log":[{"ExitCode": 1,"Output": "/bin/sh: 1: curl: not found\n"}]}"')
|
||||||
end
|
|
||||||
|
|
||||||
test "perform failing for unknown reason" do
|
|
||||||
# Prevent expected failures from outputting to terminal
|
|
||||||
Thread.report_on_exception = false
|
|
||||||
|
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute) # No need to execute anything here
|
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info)
|
|
||||||
.with(:curl, "--silent", "--output", "/dev/null", "--write-out", "'%{http_code}'", "--max-time", "2", "http://localhost:3999/up")
|
|
||||||
.returns("500")
|
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info)
|
|
||||||
.with(:docker, :container, :ls, "--all", "--filter", "name=^healthcheck-app-999$", "--quiet", "|", :xargs, :docker, :logs, "--tail", 50, "2>&1")
|
|
||||||
|
|
||||||
exception = assert_raises do
|
exception = assert_raises do
|
||||||
run_command("perform")
|
run_command("perform")
|
||||||
end
|
end
|
||||||
assert_match "Health check against /up failed with status 500", exception.message
|
assert_match "container not ready (unhealthy)", exception.message
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@@ -10,67 +10,133 @@ class CliMainTest < CliTestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "deploy" do
|
test "deploy" do
|
||||||
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "skip_broadcast" => false, "version" => "999" }
|
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "version" => "999", "skip_hooks" => false }
|
||||||
|
|
||||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:server:bootstrap", [], invoke_options)
|
|
||||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:registry:login", [], invoke_options)
|
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:registry:login", [], invoke_options)
|
||||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:build:deliver", [], invoke_options)
|
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:build:deliver", [], invoke_options)
|
||||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:traefik:boot", [], invoke_options)
|
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:traefik:boot", [], invoke_options)
|
||||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:healthcheck:perform", [], invoke_options)
|
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:healthcheck:perform", [], invoke_options)
|
||||||
|
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:app:stale_containers", [], invoke_options)
|
||||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:app:boot", [], invoke_options)
|
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:app:boot", [], invoke_options)
|
||||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:prune:all", [], invoke_options)
|
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:prune:all", [], invoke_options)
|
||||||
|
|
||||||
|
Mrsk::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)
|
||||||
|
|
||||||
run_command("deploy").tap do |output|
|
run_command("deploy").tap do |output|
|
||||||
assert_match /Ensure curl and Docker are installed/, output
|
assert_match /Running the pre-connect hook.../, output
|
||||||
assert_match /Log into image registry/, output
|
assert_match /Log into image registry/, output
|
||||||
assert_match /Build and push app image/, output
|
assert_match /Build and push app image/, output
|
||||||
assert_match /Ensure Traefik is running/, output
|
assert_match /Ensure Traefik is running/, output
|
||||||
assert_match /Ensure app can pass healthcheck/, output
|
assert_match /Ensure app can pass healthcheck/, output
|
||||||
|
assert_match /Detect stale containers/, output
|
||||||
assert_match /Prune old containers and images/, output
|
assert_match /Prune old containers and images/, output
|
||||||
|
assert_match /Running the post-deploy hook.../, output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "deploy with skip_push" do
|
test "deploy with skip_push" do
|
||||||
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "skip_broadcast" => false, "version" => "999" }
|
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "version" => "999", "skip_hooks" => false }
|
||||||
|
|
||||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:server:bootstrap", [], invoke_options)
|
|
||||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:registry:login", [], invoke_options)
|
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:registry:login", [], invoke_options)
|
||||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:build:pull", [], invoke_options)
|
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:build:pull", [], invoke_options)
|
||||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:traefik:boot", [], invoke_options)
|
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:traefik:boot", [], invoke_options)
|
||||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:healthcheck:perform", [], invoke_options)
|
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:healthcheck:perform", [], invoke_options)
|
||||||
|
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:app:stale_containers", [], invoke_options)
|
||||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:app:boot", [], invoke_options)
|
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:app:boot", [], invoke_options)
|
||||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:prune:all", [], invoke_options)
|
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:prune:all", [], invoke_options)
|
||||||
|
|
||||||
run_command("deploy", "--skip_push").tap do |output|
|
run_command("deploy", "--skip_push").tap do |output|
|
||||||
assert_match /Acquiring the deploy lock/, output
|
assert_match /Acquiring the deploy lock/, output
|
||||||
assert_match /Ensure curl and Docker are installed/, output
|
|
||||||
assert_match /Log into image registry/, output
|
assert_match /Log into image registry/, output
|
||||||
assert_match /Pull app image/, output
|
assert_match /Pull app image/, output
|
||||||
assert_match /Ensure Traefik is running/, output
|
assert_match /Ensure Traefik is running/, output
|
||||||
assert_match /Ensure app can pass healthcheck/, output
|
assert_match /Ensure app can pass healthcheck/, output
|
||||||
|
assert_match /Detect stale containers/, output
|
||||||
assert_match /Prune old containers and images/, output
|
assert_match /Prune old containers and images/, output
|
||||||
assert_match /Releasing the deploy lock/, output
|
assert_match /Releasing the deploy lock/, output
|
||||||
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")
|
||||||
|
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_debug)
|
||||||
|
.with(:stat, :mrsk_lock, ">", "/dev/null", "&&", :cat, "mrsk_lock/details", "|", :base64, "-d")
|
||||||
|
|
||||||
|
assert_raises(Mrsk::Cli::LockError) do
|
||||||
|
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 during outside section leave remove lock" do
|
||||||
|
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "version" => "999", "skip_hooks" => false }
|
||||||
|
|
||||||
|
Mrsk::Cli::Main.any_instance.expects(:invoke)
|
||||||
|
.with("mrsk:cli:registry:login", [], invoke_options)
|
||||||
|
.raises(RuntimeError)
|
||||||
|
|
||||||
|
assert !MRSK.holding_lock?
|
||||||
|
assert_raises(RuntimeError) do
|
||||||
|
stderred { run_command("deploy") }
|
||||||
|
end
|
||||||
|
assert !MRSK.holding_lock?
|
||||||
|
end
|
||||||
|
|
||||||
|
test "deploy with skipped hooks" do
|
||||||
|
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "version" => "999", "skip_hooks" => true }
|
||||||
|
|
||||||
|
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:registry:login", [], invoke_options)
|
||||||
|
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:build:deliver", [], invoke_options)
|
||||||
|
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:traefik:boot", [], invoke_options)
|
||||||
|
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:healthcheck:perform", [], invoke_options)
|
||||||
|
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:app:stale_containers", [], invoke_options)
|
||||||
|
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:app:boot", [], invoke_options)
|
||||||
|
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:prune:all", [], invoke_options)
|
||||||
|
|
||||||
|
run_command("deploy", "--skip_hooks") do
|
||||||
|
refute_match /Running the post-deploy hook.../, output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
test "redeploy" do
|
test "redeploy" do
|
||||||
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "skip_broadcast" => false, "version" => "999" }
|
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "version" => "999", "skip_hooks" => false }
|
||||||
|
|
||||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:build:deliver", [], invoke_options)
|
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:build:deliver", [], invoke_options)
|
||||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:healthcheck:perform", [], invoke_options)
|
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:healthcheck:perform", [], invoke_options)
|
||||||
|
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:app:stale_containers", [], invoke_options)
|
||||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:app:boot", [], invoke_options)
|
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:app:boot", [], invoke_options)
|
||||||
|
|
||||||
|
Mrsk::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)
|
||||||
|
|
||||||
run_command("redeploy").tap do |output|
|
run_command("redeploy").tap do |output|
|
||||||
assert_match /Build and push app image/, output
|
assert_match /Build and push app image/, output
|
||||||
assert_match /Ensure app can pass healthcheck/, output
|
assert_match /Ensure app can pass healthcheck/, output
|
||||||
|
assert_match /Running the post-deploy hook.../, output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "redeploy with skip_push" do
|
test "redeploy with skip_push" do
|
||||||
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "skip_broadcast" => false, "version" => "999" }
|
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "version" => "999", "skip_hooks" => false }
|
||||||
|
|
||||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:build:pull", [], invoke_options)
|
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:build:pull", [], invoke_options)
|
||||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:healthcheck:perform", [], invoke_options)
|
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:healthcheck:perform", [], invoke_options)
|
||||||
|
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:app:stale_containers", [], invoke_options)
|
||||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:app:boot", [], invoke_options)
|
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:app:boot", [], invoke_options)
|
||||||
|
|
||||||
run_command("redeploy", "--skip_push").tap do |output|
|
run_command("redeploy", "--skip_push").tap do |output|
|
||||||
@@ -80,32 +146,61 @@ class CliMainTest < CliTestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "rollback bad version" do
|
test "rollback bad version" do
|
||||||
|
Thread.report_on_exception = 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)
|
[ "web", "workers" ].each do |role|
|
||||||
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, :container, :ls, "--filter", "name=^app-#{role}-123$", "--quiet", raise_on_non_zero_exit: false)
|
||||||
|
.returns("").at_least_once
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
|
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-#{role}-123$", "--quiet")
|
||||||
|
.returns("version-to-rollback\n").at_least_once
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
|
.with(:docker, :ps, "--filter", "label=service=app", "--filter", "label=role=#{role}", "--filter", "status=running", "--latest", "--format", "\"{{.Names}}\"", "|", "grep -oE \"\\-[^-]+$\"", "|", "cut -c 2-", raise_on_non_zero_exit: false)
|
||||||
|
.returns("version-to-rollback\n").at_least_once
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
|
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-#{role}-123$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'")
|
||||||
|
.returns("running").at_least_once # health check
|
||||||
|
end
|
||||||
|
|
||||||
|
Mrsk::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)
|
||||||
|
|
||||||
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 container with version 123", output
|
||||||
assert_match "docker start app-123", output
|
assert_match "docker tag dhh/app:123 dhh/app:latest", 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 start app-web-123", output
|
||||||
|
assert_match "docker container ls --all --filter name=^app-web-version-to-rollback$ --quiet | xargs docker stop", output, "Should stop the container that was previously running"
|
||||||
|
assert_match "Running the post-deploy hook...", output
|
||||||
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
|
|
||||||
|
Mrsk::Utils::HealthcheckPoller.stubs(:sleep)
|
||||||
|
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
|
.with(:docker, :container, :ls, "--filter", "name=^app-web-123$", "--quiet", raise_on_non_zero_exit: false)
|
||||||
|
.returns("").at_least_once
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
|
.with(:docker, :ps, "--filter", "label=service=app", "--filter", "label=role=web", "--filter", "status=running", "--latest", "--format", "\"{{.Names}}\"", "|", "grep -oE \"\\-[^-]+$\"", "|", "cut -c 2-", raise_on_non_zero_exit: false)
|
||||||
|
.returns("").at_least_once
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
|
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-123$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'")
|
||||||
|
.returns("running").at_least_once # health check
|
||||||
|
|
||||||
run_command("rollback", "123").tap do |output|
|
run_command("rollback", "123").tap do |output|
|
||||||
assert_match "Start version 123", output
|
assert_match "Start container with version 123", output
|
||||||
assert_match "docker start app-123", output
|
assert_match "docker start app-web-123 || docker run --detach --restart unless-stopped --name app-web-123", output
|
||||||
assert_no_match "docker stop", output
|
assert_no_match "docker stop", output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -165,9 +260,11 @@ class CliMainTest < CliTestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "init" do
|
test "init" do
|
||||||
Pathname.any_instance.expects(:exist?).returns(false).twice
|
Pathname.any_instance.expects(:exist?).returns(false).times(3)
|
||||||
|
Pathname.any_instance.stubs(:mkpath)
|
||||||
FileUtils.stubs(:mkdir_p)
|
FileUtils.stubs(:mkdir_p)
|
||||||
FileUtils.stubs(:cp_r)
|
FileUtils.stubs(:cp_r)
|
||||||
|
FileUtils.stubs(:cp)
|
||||||
|
|
||||||
run_command("init").tap do |output|
|
run_command("init").tap do |output|
|
||||||
assert_match /Created configuration file in config\/deploy.yml/, output
|
assert_match /Created configuration file in config\/deploy.yml/, output
|
||||||
@@ -176,7 +273,7 @@ class CliMainTest < CliTestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "init with existing config" do
|
test "init with existing config" do
|
||||||
Pathname.any_instance.expects(:exist?).returns(true).twice
|
Pathname.any_instance.expects(:exist?).returns(true).times(3)
|
||||||
|
|
||||||
run_command("init").tap do |output|
|
run_command("init").tap do |output|
|
||||||
assert_match /Config file already exists in config\/deploy.yml \(remove first to create a new one\)/, output
|
assert_match /Config file already exists in config\/deploy.yml \(remove first to create a new one\)/, output
|
||||||
@@ -184,9 +281,11 @@ class CliMainTest < CliTestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "init with bundle option" do
|
test "init with bundle option" do
|
||||||
Pathname.any_instance.expects(:exist?).returns(false).times(3)
|
Pathname.any_instance.expects(:exist?).returns(false).times(4)
|
||||||
|
Pathname.any_instance.stubs(:mkpath)
|
||||||
FileUtils.stubs(:mkdir_p)
|
FileUtils.stubs(:mkdir_p)
|
||||||
FileUtils.stubs(:cp_r)
|
FileUtils.stubs(:cp_r)
|
||||||
|
FileUtils.stubs(:cp)
|
||||||
|
|
||||||
run_command("init", "--bundle").tap do |output|
|
run_command("init", "--bundle").tap do |output|
|
||||||
assert_match /Created configuration file in config\/deploy.yml/, output
|
assert_match /Created configuration file in config\/deploy.yml/, output
|
||||||
@@ -199,9 +298,11 @@ class CliMainTest < CliTestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "init with bundle option and existing binstub" do
|
test "init with bundle option and existing binstub" do
|
||||||
Pathname.any_instance.expects(:exist?).returns(true).times(3)
|
Pathname.any_instance.expects(:exist?).returns(true).times(4)
|
||||||
|
Pathname.any_instance.stubs(:mkpath)
|
||||||
FileUtils.stubs(:mkdir_p)
|
FileUtils.stubs(:mkdir_p)
|
||||||
FileUtils.stubs(:cp_r)
|
FileUtils.stubs(:cp_r)
|
||||||
|
FileUtils.stubs(:cp)
|
||||||
|
|
||||||
run_command("init", "--bundle").tap do |output|
|
run_command("init", "--bundle").tap do |output|
|
||||||
assert_match /Config file already exists in config\/deploy.yml \(remove first to create a new one\)/, output
|
assert_match /Config file already exists in config\/deploy.yml \(remove first to create a new one\)/, output
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ class CliPruneTest < CliTestCase
|
|||||||
|
|
||||||
test "images" do
|
test "images" do
|
||||||
run_command("images").tap do |output|
|
run_command("images").tap do |output|
|
||||||
assert_match /docker image prune --all --force --filter label=service=app --filter until=168h on 1.1.1.\d/, output
|
assert_match /docker image prune --force --filter label=service=app --filter dangling=true on 1.1.1.\d/, output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "containers" do
|
test "containers" do
|
||||||
run_command("containers").tap do |output|
|
run_command("containers").tap do |output|
|
||||||
assert_match /docker container prune --force --filter label=service=app --filter until=72h on 1.1.1.\d/, output
|
assert_match /docker ps -q -a --filter label=service=app --filter status=created --filter status=exited --filter status=dead | tail -n +6 | while read container_id; do docker rm $container_id; done on 1.1.1.\d/, output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,30 @@
|
|||||||
require_relative "cli_test_case"
|
require_relative "cli_test_case"
|
||||||
|
|
||||||
class CliServerTest < CliTestCase
|
class CliServerTest < CliTestCase
|
||||||
test "bootstrap" do
|
test "bootstrap already installed" do
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:docker, "-v", raise_on_non_zero_exit: false).returns(true).at_least_once
|
||||||
|
|
||||||
|
assert_equal "", run_command("bootstrap")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "bootstrap install as non-root user" do
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:docker, "-v", raise_on_non_zero_exit: false).returns(false).at_least_once
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:execute).with('[ "${EUID:-$(id -u)}" -eq 0 ]', raise_on_non_zero_exit: false).returns(false).at_least_once
|
||||||
|
|
||||||
|
assert_raise RuntimeError, "Docker is not installed on 1.1.1.1, 1.1.1.3, 1.1.1.4, 1.1.1.2 and can't be automatically intalled without having root access and the `curl` command available. Install Docker manually: https://docs.docker.com/engine/install/" do
|
||||||
|
run_command("bootstrap")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "bootstrap install as root user" do
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:docker, "-v", raise_on_non_zero_exit: false).returns(false).at_least_once
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:execute).with('[ "${EUID:-$(id -u)}" -eq 0 ]', raise_on_non_zero_exit: false).returns(true).at_least_once
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:curl, "-fsSL", "https://get.docker.com", "|", :sh).at_least_once
|
||||||
|
|
||||||
run_command("bootstrap").tap do |output|
|
run_command("bootstrap").tap do |output|
|
||||||
assert_match /which curl/, output
|
("1.1.1.1".."1.1.1.4").map do |host|
|
||||||
assert_match /which docker/, output
|
assert_match "Missing Docker on #{host}. Installing…", output
|
||||||
assert_match /apt-get update -y && apt-get install curl docker.io -y/, output
|
end
|
||||||
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
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,7 @@ require "test_helper"
|
|||||||
|
|
||||||
class CommanderTest < ActiveSupport::TestCase
|
class CommanderTest < ActiveSupport::TestCase
|
||||||
setup do
|
setup do
|
||||||
@mrsk = Mrsk::Commander.new.tap do |mrsk|
|
configure_with(:deploy_with_roles)
|
||||||
mrsk.configure config_file: Pathname.new(File.expand_path("fixtures/deploy_with_roles.yml", __dir__))
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "lazy configuration" do
|
test "lazy configuration" do
|
||||||
@@ -47,12 +45,35 @@ class CommanderTest < ActiveSupport::TestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "primary_host with specific hosts via role" do
|
test "primary_host with specific hosts via role" do
|
||||||
@mrsk.specific_roles = "web"
|
@mrsk.specific_roles = "workers"
|
||||||
assert_equal "1.1.1.1", @mrsk.primary_host
|
assert_equal "1.1.1.3", @mrsk.primary_host
|
||||||
end
|
end
|
||||||
|
|
||||||
test "roles_on" do
|
test "roles_on" do
|
||||||
assert_equal [ "web" ], @mrsk.roles_on("1.1.1.1")
|
assert_equal [ "web" ], @mrsk.roles_on("1.1.1.1")
|
||||||
assert_equal [ "workers" ], @mrsk.roles_on("1.1.1.3")
|
assert_equal [ "workers" ], @mrsk.roles_on("1.1.1.3")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "default group strategy" do
|
||||||
|
assert_empty @mrsk.boot_strategy
|
||||||
|
end
|
||||||
|
|
||||||
|
test "specific limit group strategy" do
|
||||||
|
configure_with(:deploy_with_boot_strategy)
|
||||||
|
|
||||||
|
assert_equal({ in: :groups, limit: 3, wait: 2 }, @mrsk.boot_strategy)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "percentage-based group strategy" do
|
||||||
|
configure_with(:deploy_with_precentage_boot_strategy)
|
||||||
|
|
||||||
|
assert_equal({ in: :groups, limit: 1, wait: 2 }, @mrsk.boot_strategy)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def configure_with(variant)
|
||||||
|
@mrsk = Mrsk::Commander.new.tap do |mrsk|
|
||||||
|
mrsk.configure config_file: Pathname.new(File.expand_path("fixtures/#{variant}.yml", __dir__))
|
||||||
|
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\" --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999",
|
||||||
new_command.run.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\" --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" --log-opt max-size=\"10m\" --volume /local/path:/container/path --label service=\"app\" --label role=\"web\" --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999",
|
||||||
new_command.run.join(" ")
|
new_command.run.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -29,7 +29,23 @@ 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\" --health-cmd \"curl -f http://localhost:3000/healthz || exit 1\" --health-interval \"1s\" --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999",
|
||||||
|
new_command.run.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "run with custom healthcheck command" do
|
||||||
|
@config[:healthcheck] = { "cmd" => "/bin/up" }
|
||||||
|
|
||||||
|
assert_equal \
|
||||||
|
"docker run --detach --restart unless-stopped --name app-web-999 -e MRSK_CONTAINER_NAME=\"app-web-999\" -e RAILS_MASTER_KEY=\"456\" --health-cmd \"/bin/up\" --health-interval \"1s\" --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999",
|
||||||
|
new_command.run.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "run with role-specific healthcheck options" do
|
||||||
|
@config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "healthcheck" => { "cmd" => "/bin/healthy" } } }
|
||||||
|
|
||||||
|
assert_equal \
|
||||||
|
"docker run --detach --restart unless-stopped --name app-web-999 -e MRSK_CONTAINER_NAME=\"app-web-999\" -e RAILS_MASTER_KEY=\"456\" --health-cmd \"/bin/healthy\" --health-interval \"1s\" --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999",
|
||||||
new_command.run.join(" ")
|
new_command.run.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -44,7 +60,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\" --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --label service=\"app\" --label role=\"web\" --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999",
|
||||||
new_command.run.join(" ")
|
new_command.run.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -63,14 +79,14 @@ class CommandsAppTest < ActiveSupport::TestCase
|
|||||||
|
|
||||||
test "stop" do
|
test "stop" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker ps --quiet --filter label=service=app --filter label=role=web | xargs docker stop",
|
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --latest | xargs docker stop",
|
||||||
new_command.stop.join(" ")
|
new_command.stop.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "stop with custom stop wait time" do
|
test "stop with custom stop wait time" do
|
||||||
@config[:stop_wait_time] = 30
|
@config[:stop_wait_time] = 30
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker ps --quiet --filter label=service=app --filter label=role=web | xargs docker stop -t 30",
|
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --latest | xargs docker stop -t 30",
|
||||||
new_command.stop.join(" ")
|
new_command.stop.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -96,37 +112,37 @@ class CommandsAppTest < ActiveSupport::TestCase
|
|||||||
|
|
||||||
test "logs" do
|
test "logs" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker ps --quiet --filter label=service=app --filter label=role=web | xargs docker logs 2>&1",
|
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --latest | xargs docker logs 2>&1",
|
||||||
new_command.logs.join(" ")
|
new_command.logs.join(" ")
|
||||||
|
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker ps --quiet --filter label=service=app --filter label=role=web | xargs docker logs --since 5m 2>&1",
|
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --latest | xargs docker logs --since 5m 2>&1",
|
||||||
new_command.logs(since: "5m").join(" ")
|
new_command.logs(since: "5m").join(" ")
|
||||||
|
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker ps --quiet --filter label=service=app --filter label=role=web | xargs docker logs --tail 100 2>&1",
|
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --latest | xargs docker logs --tail 100 2>&1",
|
||||||
new_command.logs(lines: "100").join(" ")
|
new_command.logs(lines: "100").join(" ")
|
||||||
|
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker ps --quiet --filter label=service=app --filter label=role=web | xargs docker logs --since 5m --tail 100 2>&1",
|
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --latest | xargs docker logs --since 5m --tail 100 2>&1",
|
||||||
new_command.logs(since: "5m", lines: "100").join(" ")
|
new_command.logs(since: "5m", lines: "100").join(" ")
|
||||||
|
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker ps --quiet --filter label=service=app --filter label=role=web | xargs docker logs 2>&1 | grep 'my-id'",
|
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --latest | xargs docker logs 2>&1 | grep 'my-id'",
|
||||||
new_command.logs(grep: "my-id").join(" ")
|
new_command.logs(grep: "my-id").join(" ")
|
||||||
|
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker ps --quiet --filter label=service=app --filter label=role=web | xargs docker logs --since 5m 2>&1 | grep 'my-id'",
|
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --latest | xargs docker logs --since 5m 2>&1 | grep 'my-id'",
|
||||||
new_command.logs(since: "5m", grep: "my-id").join(" ")
|
new_command.logs(since: "5m", grep: "my-id").join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "follow logs" do
|
test "follow logs" do
|
||||||
assert_match \
|
assert_match \
|
||||||
"docker ps --quiet --filter label=service=app --filter label=role=web | xargs docker logs --timestamps --tail 10 --follow 2>&1",
|
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --latest | xargs docker logs --timestamps --tail 10 --follow 2>&1",
|
||||||
new_command.follow_logs(host: "app-1")
|
new_command.follow_logs(host: "app-1")
|
||||||
|
|
||||||
assert_match \
|
assert_match \
|
||||||
"docker ps --quiet --filter label=service=app --filter label=role=web | xargs docker logs --timestamps --tail 10 --follow 2>&1 | grep \"Completed\"",
|
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --latest | xargs docker logs --timestamps --tail 10 --follow 2>&1 | grep \"Completed\"",
|
||||||
new_command.follow_logs(host: "app-1", grep: "Completed")
|
new_command.follow_logs(host: "app-1", grep: "Completed")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -178,17 +194,17 @@ class CommandsAppTest < ActiveSupport::TestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
test "current_container_id" do
|
test "current_running_container_id" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker ps --quiet --filter label=service=app --filter label=role=web",
|
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --latest",
|
||||||
new_command.current_container_id.join(" ")
|
new_command.current_running_container_id.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "current_container_id with destination" do
|
test "current_running_container_id with destination" do
|
||||||
@destination = "staging"
|
@destination = "staging"
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker ps --quiet --filter label=service=app --filter label=destination=staging --filter label=role=web",
|
"docker ps --quiet --filter label=service=app --filter label=destination=staging --filter label=role=web --filter status=running --latest",
|
||||||
new_command.current_container_id.join(" ")
|
new_command.current_running_container_id.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "container_id_for" do
|
test "container_id_for" do
|
||||||
@@ -199,10 +215,20 @@ class CommandsAppTest < ActiveSupport::TestCase
|
|||||||
|
|
||||||
test "current_running_version" do
|
test "current_running_version" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker ps --filter label=service=app --filter label=role=web --format \"{{.Names}}\" | sed 's/-/\\n/g' | tail -n 1",
|
"docker ps --filter label=service=app --filter label=role=web --filter status=running --latest --format \"{{.Names}}\" | grep -oE \"\\-[^-]+$\" | cut -c 2-",
|
||||||
new_command.current_running_version.join(" ")
|
new_command.current_running_version.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "list_versions" do
|
||||||
|
assert_equal \
|
||||||
|
"docker ps --filter label=service=app --filter label=role=web --format \"{{.Names}}\" | grep -oE \"\\-[^-]+$\" | cut -c 2-",
|
||||||
|
new_command.list_versions.join(" ")
|
||||||
|
|
||||||
|
assert_equal \
|
||||||
|
"docker ps --filter label=service=app --filter label=role=web --filter status=running --latest --format \"{{.Names}}\" | grep -oE \"\\-[^-]+$\" | cut -c 2-",
|
||||||
|
new_command.list_versions("--latest", status: :running).join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
test "list_containers" do
|
test "list_containers" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker container ls --all --filter label=service=app --filter label=role=web",
|
"docker container ls --all --filter label=service=app --filter label=role=web",
|
||||||
@@ -267,6 +293,12 @@ class CommandsAppTest < ActiveSupport::TestCase
|
|||||||
new_command.remove_images.join(" ")
|
new_command.remove_images.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "tag_current_as_latest" do
|
||||||
|
assert_equal \
|
||||||
|
"docker tag dhh/app:999 dhh/app:latest",
|
||||||
|
new_command.tag_current_as_latest.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def new_command(role: "web")
|
def new_command(role: "web")
|
||||||
Mrsk::Commands::App.new(Mrsk::Configuration.new(@config, destination: @destination, version: "999"), role: role)
|
Mrsk::Commands::App.new(Mrsk::Configuration.new(@config, destination: @destination, version: "999"), role: role)
|
||||||
|
|||||||
@@ -1,43 +1,64 @@
|
|||||||
require "test_helper"
|
require "test_helper"
|
||||||
|
require "active_support/testing/time_helpers"
|
||||||
|
|
||||||
class CommandsAuditorTest < ActiveSupport::TestCase
|
class CommandsAuditorTest < ActiveSupport::TestCase
|
||||||
|
include ActiveSupport::Testing::TimeHelpers
|
||||||
|
|
||||||
setup do
|
setup do
|
||||||
|
freeze_time
|
||||||
|
|
||||||
@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" ]
|
||||||
audit_broadcast_cmd: "bin/audit_broadcast"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@auditor = new_command
|
||||||
|
@performer = `whoami`.strip
|
||||||
|
@recorded_at = Time.now.utc.iso8601
|
||||||
end
|
end
|
||||||
|
|
||||||
test "record" do
|
test "record" do
|
||||||
assert_match \
|
assert_equal [
|
||||||
/echo '.* app removed container' >> mrsk-app-audit.log/,
|
:echo,
|
||||||
new_command.record("app removed container").join(" ")
|
"[#{@recorded_at}] [#{@performer}]",
|
||||||
|
"app removed container",
|
||||||
|
">>", "mrsk-app-audit.log"
|
||||||
|
], @auditor.record("app removed container")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "record with destination" do
|
test "record with destination" do
|
||||||
@destination = "staging"
|
new_command(destination: "staging").tap do |auditor|
|
||||||
|
assert_equal [
|
||||||
assert_match \
|
:echo,
|
||||||
/echo '.* app removed container' >> mrsk-app-staging-audit.log/,
|
"[#{@recorded_at}] [#{@performer}] [staging]",
|
||||||
new_command.record("app removed container").join(" ")
|
"app removed container",
|
||||||
|
">>", "mrsk-app-staging-audit.log"
|
||||||
|
], auditor.record("app removed container")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "record with role" do
|
test "record with command details" do
|
||||||
@role = "web"
|
new_command(role: "web").tap do |auditor|
|
||||||
|
assert_equal [
|
||||||
assert_match \
|
:echo,
|
||||||
/echo '.* \[web\] app removed container' >> mrsk-app-audit.log/,
|
"[#{@recorded_at}] [#{@performer}] [web]",
|
||||||
new_command.record("app removed container").join(" ")
|
"app removed container",
|
||||||
|
">>", "mrsk-app-audit.log"
|
||||||
|
], auditor.record("app removed container")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "broadcast" do
|
test "record with arg details" do
|
||||||
assert_match \
|
assert_equal [
|
||||||
/bin\/audit_broadcast '\[.*\] app removed container'/,
|
:echo,
|
||||||
new_command.broadcast("app removed container").join(" ")
|
"[#{@recorded_at}] [#{@performer}] [value]",
|
||||||
|
"app removed container",
|
||||||
|
">>", "mrsk-app-audit.log"
|
||||||
|
], @auditor.record("app removed container", detail: "value")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
private
|
private
|
||||||
def new_command
|
def new_command(destination: nil, **details)
|
||||||
Mrsk::Commands::Auditor.new(Mrsk::Configuration.new(@config, destination: @destination, version: "123"), role: @role)
|
Mrsk::Commands::Auditor.new(Mrsk::Configuration.new(@config, destination: destination, version: "123"), **details)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -52,12 +52,21 @@ class CommandsBuilderTest < ActiveSupport::TestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "build dockerfile" do
|
test "build dockerfile" do
|
||||||
|
Pathname.any_instance.expects(:exist?).returns(true).once
|
||||||
builder = new_builder_command(builder: { "dockerfile" => "Dockerfile.xyz" })
|
builder = new_builder_command(builder: { "dockerfile" => "Dockerfile.xyz" })
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"-t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile.xyz",
|
"-t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile.xyz",
|
||||||
builder.target.build_options.join(" ")
|
builder.target.build_options.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "missing dockerfile" do
|
||||||
|
Pathname.any_instance.expects(:exist?).returns(false).once
|
||||||
|
builder = new_builder_command(builder: { "dockerfile" => "Dockerfile.xyz" })
|
||||||
|
assert_raises(Mrsk::Commands::Builder::Base::BuilderError) do
|
||||||
|
builder.target.build_options.join(" ")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
test "build context" do
|
test "build context" do
|
||||||
builder = new_builder_command(builder: { "context" => ".." })
|
builder = new_builder_command(builder: { "context" => ".." })
|
||||||
assert_equal \
|
assert_equal \
|
||||||
|
|||||||
26
test/commands/docker_test.rb
Normal file
26
test/commands/docker_test.rb
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class CommandsDockerTest < ActiveSupport::TestCase
|
||||||
|
setup do
|
||||||
|
@config = {
|
||||||
|
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ]
|
||||||
|
}
|
||||||
|
@docker = Mrsk::Commands::Docker.new(Mrsk::Configuration.new(@config))
|
||||||
|
end
|
||||||
|
|
||||||
|
test "install" do
|
||||||
|
assert_equal "curl -fsSL https://get.docker.com | sh", @docker.install.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "installed?" do
|
||||||
|
assert_equal "docker -v", @docker.installed?.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "running?" do
|
||||||
|
assert_equal "docker version", @docker.running?.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "superuser?" do
|
||||||
|
assert_equal '[ "${EUID:-$(id -u)}" -eq 0 ]', @docker.superuser?.join(" ")
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -10,7 +10,7 @@ class CommandsHealthcheckTest < ActiveSupport::TestCase
|
|||||||
|
|
||||||
test "run" do
|
test "run" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker run --detach --name healthcheck-app-123 --publish 3999:3000 --label service=healthcheck-app -e MRSK_CONTAINER_NAME=\"healthcheck-app\" dhh/app:123",
|
"docker run --detach --name healthcheck-app-123 --publish 3999:3000 --label service=healthcheck-app -e MRSK_CONTAINER_NAME=\"healthcheck-app\" --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" dhh/app:123",
|
||||||
new_command.run.join(" ")
|
new_command.run.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ class CommandsHealthcheckTest < ActiveSupport::TestCase
|
|||||||
@config[:healthcheck] = { "port" => 3001 }
|
@config[:healthcheck] = { "port" => 3001 }
|
||||||
|
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker run --detach --name healthcheck-app-123 --publish 3999:3001 --label service=healthcheck-app -e MRSK_CONTAINER_NAME=\"healthcheck-app\" dhh/app:123",
|
"docker run --detach --name healthcheck-app-123 --publish 3999:3001 --label service=healthcheck-app -e MRSK_CONTAINER_NAME=\"healthcheck-app\" --health-cmd \"curl -f http://localhost:3001/up || exit 1\" --health-interval \"1s\" dhh/app:123",
|
||||||
new_command.run.join(" ")
|
new_command.run.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -26,29 +26,35 @@ class CommandsHealthcheckTest < ActiveSupport::TestCase
|
|||||||
@destination = "staging"
|
@destination = "staging"
|
||||||
|
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker run --detach --name healthcheck-app-staging-123 --publish 3999:3000 --label service=healthcheck-app-staging -e MRSK_CONTAINER_NAME=\"healthcheck-app-staging\" dhh/app:123",
|
"docker run --detach --name healthcheck-app-staging-123 --publish 3999:3000 --label service=healthcheck-app-staging -e MRSK_CONTAINER_NAME=\"healthcheck-app-staging\" --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" dhh/app:123",
|
||||||
|
new_command.run.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "run with custom healthcheck" do
|
||||||
|
@config[:healthcheck] = { "cmd" => "/bin/up" }
|
||||||
|
|
||||||
|
assert_equal \
|
||||||
|
"docker run --detach --name healthcheck-app-123 --publish 3999:3000 --label service=healthcheck-app -e MRSK_CONTAINER_NAME=\"healthcheck-app\" --health-cmd \"/bin/up\" --health-interval \"1s\" dhh/app:123",
|
||||||
new_command.run.join(" ")
|
new_command.run.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "run with custom options" do
|
test "run with custom options" do
|
||||||
@config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "options" => { "mount" => "somewhere" } } }
|
@config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "options" => { "mount" => "somewhere" } } }
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker run --detach --name healthcheck-app-123 --publish 3999:3000 --label service=healthcheck-app -e MRSK_CONTAINER_NAME=\"healthcheck-app\" --mount \"somewhere\" dhh/app:123",
|
"docker run --detach --name healthcheck-app-123 --publish 3999:3000 --label service=healthcheck-app -e MRSK_CONTAINER_NAME=\"healthcheck-app\" --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" --mount \"somewhere\" dhh/app:123",
|
||||||
new_command.run.join(" ")
|
new_command.run.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "curl" do
|
test "status" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"curl --silent --output /dev/null --write-out '%{http_code}' --max-time 2 http://localhost:3999/up",
|
"docker container ls --all --filter name=^healthcheck-app-123$ --quiet | xargs docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'",
|
||||||
new_command.curl.join(" ")
|
new_command.status.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "curl with custom path" do
|
test "container_health_log" do
|
||||||
@config[:healthcheck] = { "path" => "/healthz" }
|
|
||||||
|
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"curl --silent --output /dev/null --write-out '%{http_code}' --max-time 2 http://localhost:3999/healthz",
|
"docker container ls --all --filter name=^healthcheck-app-123$ --quiet | xargs docker inspect --format '{{json .State.Health}}'",
|
||||||
new_command.curl.join(" ")
|
new_command.container_health_log.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "stop" do
|
test "stop" do
|
||||||
|
|||||||
44
test/commands/hook_test.rb
Normal file
44
test/commands/hook_test.rb
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class CommandsHookTest < ActiveSupport::TestCase
|
||||||
|
include ActiveSupport::Testing::TimeHelpers
|
||||||
|
|
||||||
|
setup do
|
||||||
|
freeze_time
|
||||||
|
|
||||||
|
@config = {
|
||||||
|
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ],
|
||||||
|
traefik: { "args" => { "accesslog.format" => "json", "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } }
|
||||||
|
}
|
||||||
|
|
||||||
|
@performer = `whoami`.strip
|
||||||
|
@recorded_at = Time.now.utc.iso8601
|
||||||
|
end
|
||||||
|
|
||||||
|
test "run" do
|
||||||
|
assert_equal [
|
||||||
|
".mrsk/hooks/foo",
|
||||||
|
{ env: {
|
||||||
|
"MRSK_RECORDED_AT" => @recorded_at,
|
||||||
|
"MRSK_PERFORMER" => @performer,
|
||||||
|
"MRSK_VERSION" => "123",
|
||||||
|
"MRSK_SERVICE_VERSION" => "app@123" } }
|
||||||
|
], new_command.run("foo")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "run with custom hooks_path" do
|
||||||
|
assert_equal [
|
||||||
|
"custom/hooks/path/foo",
|
||||||
|
{ env: {
|
||||||
|
"MRSK_RECORDED_AT" => @recorded_at,
|
||||||
|
"MRSK_PERFORMER" => @performer,
|
||||||
|
"MRSK_VERSION" => "123",
|
||||||
|
"MRSK_SERVICE_VERSION" => "app@123" } }
|
||||||
|
], new_command(hooks_path: "custom/hooks/path").run("foo")
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def new_command(**extra_config)
|
||||||
|
Mrsk::Commands::Hook.new(Mrsk::Configuration.new(@config.merge(**extra_config), version: "123"))
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -10,13 +10,13 @@ class CommandsPruneTest < ActiveSupport::TestCase
|
|||||||
|
|
||||||
test "images" do
|
test "images" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker image prune --all --force --filter label=service=app --filter until=168h",
|
"docker image prune --force --filter label=service=app --filter dangling=true",
|
||||||
new_command.images.join(" ")
|
new_command.images.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "containers" do
|
test "containers" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker container prune --force --filter label=service=app --filter until=72h",
|
"docker ps -q -a --filter label=service=app --filter status=created --filter status=exited --filter status=dead | tail -n +6 | while read container_id; do docker rm $container_id; done",
|
||||||
new_command.containers.join(" ")
|
new_command.containers.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -2,53 +2,83 @@ 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" } }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ENV["EXAMPLE_API_KEY"] = "456"
|
||||||
|
end
|
||||||
|
|
||||||
|
teardown do
|
||||||
|
ENV.delete("EXAMPLE_API_KEY")
|
||||||
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(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "run with env configured" do
|
||||||
|
assert_equal \
|
||||||
|
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --log-opt max-size=\"10m\" #{@image} --providers.docker --log.level=DEBUG --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
|
||||||
|
new_command.run.join(" ")
|
||||||
|
|
||||||
|
@config[:traefik]["env"] = { "secret" => %w[EXAMPLE_API_KEY] }
|
||||||
|
assert_equal \
|
||||||
|
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock -e EXAMPLE_API_KEY=\"456\" --log-opt max-size=\"10m\" #{@image} --providers.docker --log.level=DEBUG --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
|
||||||
new_command.run.join(" ")
|
new_command.run.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -56,7 +86,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 +94,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.services.app-web.loadbalancer.server.scheme=\"http\"", "--label", "traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\"", "--label", "traefik.http.middlewares.app-web-retry.retry.attempts=\"5\"", "--label", "traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\"", "--label", "traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\"" ], @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-web.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-web.rule"]
|
||||||
end
|
end
|
||||||
|
|
||||||
test "default traefik label on non-web role" do
|
test "default traefik label on non-web role" do
|
||||||
@@ -66,7 +66,7 @@ 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.services.app-beta.loadbalancer.server.scheme=\"http\"", "--label", "traefik.http.routers.app-beta.rule=\"PathPrefix(\\`/\\`)\"", "--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
|
end
|
||||||
|
|
||||||
test "env overwritten by role" do
|
test "env overwritten by role" do
|
||||||
@@ -97,7 +97,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 +119,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 +139,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
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class ConfigurationTest < ActiveSupport::TestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "role" do
|
test "role" do
|
||||||
assert_equal "web", @config.role(:web).name
|
assert @config.role(:web).name.web?
|
||||||
assert_equal "workers", @config_with_roles.role(:workers).name
|
assert_equal "workers", @config_with_roles.role(:workers).name
|
||||||
assert_nil @config.role(:missing)
|
assert_nil @config.role(:missing)
|
||||||
end
|
end
|
||||||
@@ -72,19 +72,36 @@ class ConfigurationTest < ActiveSupport::TestCase
|
|||||||
assert_equal [ "1.1.1.1", "1.1.1.2", "1.1.1.3" ], config.traefik_hosts
|
assert_equal [ "1.1.1.1", "1.1.1.2", "1.1.1.3" ], config.traefik_hosts
|
||||||
end
|
end
|
||||||
|
|
||||||
test "version" do
|
test "version no git repo" do
|
||||||
ENV.delete("VERSION")
|
ENV.delete("VERSION")
|
||||||
|
|
||||||
@config.expects(:system).with("git rev-parse").returns(nil)
|
@config.expects(:system).with("git rev-parse").returns(nil)
|
||||||
error = assert_raises(RuntimeError) { @config.version}
|
error = assert_raises(RuntimeError) { @config.version}
|
||||||
assert_match /no git repository found/, error.message
|
assert_match /no git repository found/, error.message
|
||||||
|
end
|
||||||
|
|
||||||
@config.expects(:current_commit_hash).returns("git-version")
|
test "version from git committed" do
|
||||||
|
ENV.delete("VERSION")
|
||||||
|
|
||||||
|
@config.expects(:`).with("git rev-parse HEAD").returns("git-version")
|
||||||
|
@config.expects(:`).with("git status --porcelain").returns("")
|
||||||
assert_equal "git-version", @config.version
|
assert_equal "git-version", @config.version
|
||||||
|
end
|
||||||
|
|
||||||
|
test "version from git uncommitted" do
|
||||||
|
ENV.delete("VERSION")
|
||||||
|
|
||||||
|
@config.expects(:`).with("git rev-parse HEAD").returns("git-version")
|
||||||
|
@config.expects(:`).with("git status --porcelain").returns("M file\n")
|
||||||
|
assert_match /^git-version_uncommitted_[0-9a-f]{16}$/, @config.version
|
||||||
|
end
|
||||||
|
|
||||||
|
test "version from env" do
|
||||||
ENV["VERSION"] = "env-version"
|
ENV["VERSION"] = "env-version"
|
||||||
assert_equal "env-version", @config.version
|
assert_equal "env-version", @config.version
|
||||||
|
end
|
||||||
|
|
||||||
|
test "version from arg" do
|
||||||
@config.version = "arg-version"
|
@config.version = "arg-version"
|
||||||
assert_equal "arg-version", @config.version
|
assert_equal "arg-version", @config.version
|
||||||
end
|
end
|
||||||
@@ -113,12 +130,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 +151,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 +266,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
|
||||||
|
|||||||
17
test/fixtures/deploy_with_boot_strategy.yml
vendored
Normal file
17
test/fixtures/deploy_with_boot_strategy.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
service: app
|
||||||
|
image: dhh/app
|
||||||
|
servers:
|
||||||
|
web:
|
||||||
|
- "1.1.1.1"
|
||||||
|
- "1.1.1.2"
|
||||||
|
workers:
|
||||||
|
- "1.1.1.3"
|
||||||
|
- "1.1.1.4"
|
||||||
|
|
||||||
|
registry:
|
||||||
|
username: user
|
||||||
|
password: pw
|
||||||
|
|
||||||
|
boot:
|
||||||
|
limit: 3
|
||||||
|
wait: 2
|
||||||
17
test/fixtures/deploy_with_precentage_boot_strategy.yml
vendored
Normal file
17
test/fixtures/deploy_with_precentage_boot_strategy.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
service: app
|
||||||
|
image: dhh/app
|
||||||
|
servers:
|
||||||
|
web:
|
||||||
|
- "1.1.1.1"
|
||||||
|
- "1.1.1.2"
|
||||||
|
workers:
|
||||||
|
- "1.1.1.3"
|
||||||
|
- "1.1.1.4"
|
||||||
|
|
||||||
|
registry:
|
||||||
|
username: user
|
||||||
|
password: pw
|
||||||
|
|
||||||
|
boot:
|
||||||
|
limit: 25%
|
||||||
|
wait: 2
|
||||||
36
test/integration/accessory_test.rb
Normal file
36
test/integration/accessory_test.rb
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
require_relative "integration_test"
|
||||||
|
|
||||||
|
class AccessoryTest < IntegrationTest
|
||||||
|
test "boot, stop, start, restart, logs, remove" do
|
||||||
|
mrsk :accessory, :boot, :busybox
|
||||||
|
assert_accessory_running :busybox
|
||||||
|
|
||||||
|
mrsk :accessory, :stop, :busybox
|
||||||
|
assert_accessory_not_running :busybox
|
||||||
|
|
||||||
|
mrsk :accessory, :start, :busybox
|
||||||
|
assert_accessory_running :busybox
|
||||||
|
|
||||||
|
mrsk :accessory, :restart, :busybox
|
||||||
|
assert_accessory_running :busybox
|
||||||
|
|
||||||
|
logs = mrsk :accessory, :logs, :busybox, capture: true
|
||||||
|
assert_match /Starting busybox.../, logs
|
||||||
|
|
||||||
|
mrsk :accessory, :remove, :busybox, "-y"
|
||||||
|
assert_accessory_not_running :busybox
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def assert_accessory_running(name)
|
||||||
|
assert_match /registry:4443\/busybox:1.36.0 "sh -c 'echo \\"Start/, accessory_details(name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def assert_accessory_not_running(name)
|
||||||
|
refute_match /registry:4443\/busybox:1.36.0 "sh -c 'echo \\"Start/, accessory_details(name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def accessory_details(name)
|
||||||
|
mrsk :accessory, :details, name, capture: true
|
||||||
|
end
|
||||||
|
end
|
||||||
55
test/integration/app_test.rb
Normal file
55
test/integration/app_test.rb
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
require_relative "integration_test"
|
||||||
|
|
||||||
|
class AppTest < IntegrationTest
|
||||||
|
test "stop, start, boot, logs, images, containers, exec, remove" do
|
||||||
|
mrsk :deploy
|
||||||
|
|
||||||
|
assert_app_is_up
|
||||||
|
|
||||||
|
mrsk :app, :stop
|
||||||
|
|
||||||
|
# traefik is up and returns 404s when it can't match a route
|
||||||
|
assert_app_not_found
|
||||||
|
|
||||||
|
mrsk :app, :start
|
||||||
|
|
||||||
|
# mrsk app start does not wait
|
||||||
|
wait_for_app_to_be_up
|
||||||
|
|
||||||
|
mrsk :app, :boot
|
||||||
|
|
||||||
|
wait_for_app_to_be_up
|
||||||
|
|
||||||
|
logs = mrsk :app, :logs, capture: true
|
||||||
|
assert_match /App Host: vm1/, logs
|
||||||
|
assert_match /App Host: vm2/, logs
|
||||||
|
assert_match /GET \/ HTTP\/1.1/, logs
|
||||||
|
|
||||||
|
images = mrsk :app, :images, capture: true
|
||||||
|
assert_match /App Host: vm1/, images
|
||||||
|
assert_match /App Host: vm2/, images
|
||||||
|
assert_match /registry:4443\/app\s+#{latest_app_version}/, images
|
||||||
|
assert_match /registry:4443\/app\s+latest/, images
|
||||||
|
|
||||||
|
containers = mrsk :app, :containers, capture: true
|
||||||
|
assert_match /App Host: vm1/, containers
|
||||||
|
assert_match /App Host: vm2/, containers
|
||||||
|
assert_match /registry:4443\/app:#{latest_app_version}/, containers
|
||||||
|
assert_match /registry:4443\/app:latest/, containers
|
||||||
|
|
||||||
|
exec_output = mrsk :app, :exec, :ps, capture: true
|
||||||
|
assert_match /App Host: vm1/, exec_output
|
||||||
|
assert_match /App Host: vm2/, exec_output
|
||||||
|
assert_match /1 root 0:\d\d ps/, exec_output
|
||||||
|
|
||||||
|
exec_output = mrsk :app, :exec, "--reuse", :ps, capture: true
|
||||||
|
assert_match /App Host: vm1/, exec_output
|
||||||
|
assert_match /App Host: vm2/, exec_output
|
||||||
|
assert_match /1 root 0:\d\d nginx/, exec_output
|
||||||
|
|
||||||
|
mrsk :app, :remove
|
||||||
|
|
||||||
|
# traefik is up and returns 404s when it can't match a route
|
||||||
|
assert_app_not_found
|
||||||
|
end
|
||||||
|
end
|
||||||
57
test/integration/docker-compose.yml
Normal file
57
test/integration/docker-compose.yml
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
version: "3.7"
|
||||||
|
name: "mrsk-test"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
shared:
|
||||||
|
registry:
|
||||||
|
deployer_bundle:
|
||||||
|
|
||||||
|
services:
|
||||||
|
shared:
|
||||||
|
build:
|
||||||
|
context: docker/shared
|
||||||
|
volumes:
|
||||||
|
- shared:/shared
|
||||||
|
|
||||||
|
deployer:
|
||||||
|
privileged: true
|
||||||
|
build:
|
||||||
|
context: docker/deployer
|
||||||
|
environment:
|
||||||
|
- TEST_ID=${TEST_ID}
|
||||||
|
volumes:
|
||||||
|
- ../..:/mrsk
|
||||||
|
- shared:/shared
|
||||||
|
- registry:/registry
|
||||||
|
- deployer_bundle:/usr/local/bundle/
|
||||||
|
|
||||||
|
registry:
|
||||||
|
build:
|
||||||
|
context: docker/registry
|
||||||
|
environment:
|
||||||
|
- REGISTRY_HTTP_ADDR=0.0.0.0:4443
|
||||||
|
- REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt
|
||||||
|
- REGISTRY_HTTP_TLS_KEY=/certs/domain.key
|
||||||
|
volumes:
|
||||||
|
- shared:/shared
|
||||||
|
- registry:/var/lib/registry/
|
||||||
|
|
||||||
|
vm1:
|
||||||
|
privileged: true
|
||||||
|
build:
|
||||||
|
context: docker/vm
|
||||||
|
volumes:
|
||||||
|
- shared:/shared
|
||||||
|
|
||||||
|
vm2:
|
||||||
|
privileged: true
|
||||||
|
build:
|
||||||
|
context: docker/vm
|
||||||
|
volumes:
|
||||||
|
- shared:/shared
|
||||||
|
|
||||||
|
load_balancer:
|
||||||
|
build:
|
||||||
|
context: docker/load_balancer
|
||||||
|
ports:
|
||||||
|
- "12345:80"
|
||||||
29
test/integration/docker/deployer/Dockerfile
Normal file
29
test/integration/docker/deployer/Dockerfile
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
FROM ruby:3.2
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN apt-get update --fix-missing && apt-get install -y ca-certificates openssh-client curl gnupg docker.io
|
||||||
|
|
||||||
|
RUN install -m 0755 -d /etc/apt/keyrings
|
||||||
|
RUN curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
||||||
|
RUN chmod a+r /etc/apt/keyrings/docker.gpg
|
||||||
|
RUN echo \
|
||||||
|
"deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian \
|
||||||
|
"$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | \
|
||||||
|
tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||||
|
|
||||||
|
RUN apt-get update --fix-missing && apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||||
|
|
||||||
|
COPY *.sh .
|
||||||
|
COPY app/ .
|
||||||
|
|
||||||
|
RUN ln -s /shared/ssh /root/.ssh
|
||||||
|
RUN mkdir -p /etc/docker/certs.d/registry:4443 && ln -s /shared/certs/domain.crt /etc/docker/certs.d/registry:4443/ca.crt
|
||||||
|
|
||||||
|
RUN git config --global user.email "deployer@example.com"
|
||||||
|
RUN git config --global user.name "Deployer"
|
||||||
|
RUN git init && git add . && git commit -am "Initial version"
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=1s CMD pgrep sleep
|
||||||
|
|
||||||
|
CMD ["./boot.sh"]
|
||||||
1
test/integration/docker/deployer/app/.env.erb
Normal file
1
test/integration/docker/deployer/app/.env.erb
Normal file
@@ -0,0 +1 @@
|
|||||||
|
SECRET_TOKEN=1234
|
||||||
3
test/integration/docker/deployer/app/.mrsk/hooks/post-deploy
Executable file
3
test/integration/docker/deployer/app/.mrsk/hooks/post-deploy
Executable file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
echo "Deployed!"
|
||||||
|
mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/post-deploy
|
||||||
3
test/integration/docker/deployer/app/.mrsk/hooks/pre-build
Executable file
3
test/integration/docker/deployer/app/.mrsk/hooks/pre-build
Executable file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
echo "About to build and push..."
|
||||||
|
mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/pre-build
|
||||||
8
test/integration/docker/deployer/app/.mrsk/hooks/pre-connect
Executable file
8
test/integration/docker/deployer/app/.mrsk/hooks/pre-connect
Executable file
@@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
echo "About to lock..."
|
||||||
|
if [ "$MRSK_HOSTS" != "vm1,vm2" ]; then
|
||||||
|
echo "Expected hosts to be 'vm1,vm2', got $MRSK_HOSTS"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/pre-connect
|
||||||
7
test/integration/docker/deployer/app/Dockerfile
Normal file
7
test/integration/docker/deployer/app/Dockerfile
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
FROM registry:4443/nginx:1-alpine-slim
|
||||||
|
|
||||||
|
COPY default.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
ARG COMMIT_SHA
|
||||||
|
RUN echo $COMMIT_SHA > /usr/share/nginx/html/version
|
||||||
|
|
||||||
27
test/integration/docker/deployer/app/config/deploy.yml
Normal file
27
test/integration/docker/deployer/app/config/deploy.yml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
service: app
|
||||||
|
image: app
|
||||||
|
servers:
|
||||||
|
- vm1
|
||||||
|
- vm2
|
||||||
|
registry:
|
||||||
|
server: registry:4443
|
||||||
|
username: root
|
||||||
|
password: root
|
||||||
|
builder:
|
||||||
|
multiarch: false
|
||||||
|
args:
|
||||||
|
COMMIT_SHA: <%= `git rev-parse HEAD` %>
|
||||||
|
healthcheck:
|
||||||
|
cmd: wget -qO- http://localhost > /dev/null
|
||||||
|
traefik:
|
||||||
|
args:
|
||||||
|
accesslog: true
|
||||||
|
accesslog.format: json
|
||||||
|
image: registry:4443/traefik:v2.9
|
||||||
|
accessories:
|
||||||
|
busybox:
|
||||||
|
image: registry:4443/busybox:1.36.0
|
||||||
|
cmd: sh -c 'echo "Starting busybox..."; trap exit term; while true; do sleep 1; done'
|
||||||
|
roles:
|
||||||
|
- web
|
||||||
|
stop_wait_time: 1
|
||||||
17
test/integration/docker/deployer/app/default.conf
Normal file
17
test/integration/docker/deployer/app/default.conf
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html index.htm;
|
||||||
|
}
|
||||||
|
|
||||||
|
# redirect server error pages to the static page /50x.html
|
||||||
|
#
|
||||||
|
error_page 500 502 503 504 /50x.html;
|
||||||
|
location = /50x.html {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
}
|
||||||
|
}
|
||||||
5
test/integration/docker/deployer/boot.sh
Executable file
5
test/integration/docker/deployer/boot.sh
Executable file
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
dockerd --max-concurrent-downloads 1 &
|
||||||
|
|
||||||
|
exec sleep infinity
|
||||||
23
test/integration/docker/deployer/setup.sh
Executable file
23
test/integration/docker/deployer/setup.sh
Executable file
@@ -0,0 +1,23 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
install_mrsk() {
|
||||||
|
cd /mrsk && gem build mrsk.gemspec -o /tmp/mrsk.gem && gem install /tmp/mrsk.gem
|
||||||
|
}
|
||||||
|
|
||||||
|
# Push the images to a persistent volume on the registry container
|
||||||
|
# This is to work around docker hub rate limits
|
||||||
|
push_image_to_registry_4443() {
|
||||||
|
# Check if the image is in the registry without having to pull it
|
||||||
|
if ! stat /registry/docker/registry/v2/repositories/$1/_manifests/tags/$2/current/link > /dev/null; then
|
||||||
|
hub_tag=$1:$2
|
||||||
|
registry_4443_tag=registry:4443/$1:$2
|
||||||
|
docker pull $hub_tag
|
||||||
|
docker tag $hub_tag $registry_4443_tag
|
||||||
|
docker push $registry_4443_tag
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
install_mrsk
|
||||||
|
push_image_to_registry_4443 nginx 1-alpine-slim
|
||||||
|
push_image_to_registry_4443 traefik v2.9
|
||||||
|
push_image_to_registry_4443 busybox 1.36.0
|
||||||
3
test/integration/docker/deployer/update_app_rev.sh
Executable file
3
test/integration/docker/deployer/update_app_rev.sh
Executable file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
git commit -am 'Update rev' --amend
|
||||||
5
test/integration/docker/load_balancer/Dockerfile
Normal file
5
test/integration/docker/load_balancer/Dockerfile
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
FROM nginx:1-alpine-slim
|
||||||
|
|
||||||
|
COPY default.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=1s CMD pgrep nginx
|
||||||
16
test/integration/docker/load_balancer/default.conf
Normal file
16
test/integration/docker/load_balancer/default.conf
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
upstream loadbalancer {
|
||||||
|
server vm1:80;
|
||||||
|
server vm2:80;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://loadbalancer;
|
||||||
|
proxy_connect_timeout 5;
|
||||||
|
proxy_send_timeout 5;
|
||||||
|
proxy_read_timeout 5;
|
||||||
|
send_timeout 5;
|
||||||
|
}
|
||||||
|
}
|
||||||
9
test/integration/docker/registry/Dockerfile
Normal file
9
test/integration/docker/registry/Dockerfile
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
FROM registry
|
||||||
|
|
||||||
|
COPY boot.sh .
|
||||||
|
|
||||||
|
RUN ln -s /shared/certs /certs
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=1s CMD pgrep registry
|
||||||
|
|
||||||
|
ENTRYPOINT ["./boot.sh"]
|
||||||
5
test/integration/docker/registry/boot.sh
Executable file
5
test/integration/docker/registry/boot.sh
Executable file
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
while [ ! -f /certs/domain.crt ]; do sleep 1; done
|
||||||
|
|
||||||
|
exec /entrypoint.sh /etc/docker/registry/config.yml
|
||||||
17
test/integration/docker/shared/Dockerfile
Normal file
17
test/integration/docker/shared/Dockerfile
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
FROM ubuntu:22.10
|
||||||
|
|
||||||
|
WORKDIR /work
|
||||||
|
|
||||||
|
RUN apt-get update --fix-missing && apt-get -y install openssh-client openssl
|
||||||
|
|
||||||
|
RUN mkdir ssh && \
|
||||||
|
ssh-keygen -t rsa -f ssh/id_rsa -N ""
|
||||||
|
|
||||||
|
COPY registry-dns.conf .
|
||||||
|
COPY boot.sh .
|
||||||
|
|
||||||
|
RUN mkdir certs && openssl req -newkey rsa:4096 -nodes -sha256 -keyout certs/domain.key -x509 -days 365 -out certs/domain.crt -subj '/CN=registry' -extensions EXT -config registry-dns.conf
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=1s CMD pgrep sleep
|
||||||
|
|
||||||
|
CMD ["./boot.sh"]
|
||||||
5
test/integration/docker/shared/boot.sh
Executable file
5
test/integration/docker/shared/boot.sh
Executable file
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
cp -r * /shared
|
||||||
|
|
||||||
|
exec sleep infinity
|
||||||
7
test/integration/docker/shared/registry-dns.conf
Normal file
7
test/integration/docker/shared/registry-dns.conf
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
[dn]
|
||||||
|
CN=registry
|
||||||
|
[req]
|
||||||
|
distinguished_name = dn
|
||||||
|
[EXT]
|
||||||
|
subjectAltName=DNS:registry
|
||||||
|
keyUsage=digitalSignature
|
||||||
14
test/integration/docker/vm/Dockerfile
Normal file
14
test/integration/docker/vm/Dockerfile
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
FROM ubuntu:22.10
|
||||||
|
|
||||||
|
WORKDIR /work
|
||||||
|
|
||||||
|
RUN apt-get update --fix-missing && apt-get -y install openssh-client openssh-server docker.io
|
||||||
|
|
||||||
|
RUN mkdir /root/.ssh && ln -s /shared/ssh/id_rsa.pub /root/.ssh/authorized_keys
|
||||||
|
RUN mkdir -p /etc/docker/certs.d/registry:4443 && ln -s /shared/certs/domain.crt /etc/docker/certs.d/registry:4443/ca.crt
|
||||||
|
|
||||||
|
COPY boot.sh .
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=1s CMD pgrep dockerd
|
||||||
|
|
||||||
|
CMD ["./boot.sh"]
|
||||||
9
test/integration/docker/vm/boot.sh
Executable file
9
test/integration/docker/vm/boot.sh
Executable file
@@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
while [ ! -f /root/.ssh/authorized_keys ]; do echo "Waiting for ssh keys"; sleep 1; done
|
||||||
|
|
||||||
|
service ssh restart
|
||||||
|
|
||||||
|
dockerd --max-concurrent-downloads 1 &
|
||||||
|
|
||||||
|
exec sleep infinity
|
||||||
131
test/integration/integration_test.rb
Normal file
131
test/integration/integration_test.rb
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
require "net/http"
|
||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class IntegrationTest < ActiveSupport::TestCase
|
||||||
|
setup do
|
||||||
|
ENV["TEST_ID"] = SecureRandom.hex
|
||||||
|
docker_compose "up --build -d"
|
||||||
|
wait_for_healthy
|
||||||
|
setup_deployer
|
||||||
|
end
|
||||||
|
|
||||||
|
teardown do
|
||||||
|
docker_compose "down -t 1"
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def docker_compose(*commands, capture: false, raise_on_error: true)
|
||||||
|
command = "TEST_ID=#{ENV["TEST_ID"]} docker compose #{commands.join(" ")}"
|
||||||
|
succeeded = false
|
||||||
|
if capture
|
||||||
|
result = stdouted { succeeded = system("cd test/integration && #{command}") }
|
||||||
|
else
|
||||||
|
succeeded = system("cd test/integration && #{command}")
|
||||||
|
end
|
||||||
|
|
||||||
|
raise "Command `#{command}` failed with error code `#{$?}`" if !succeeded && raise_on_error
|
||||||
|
result
|
||||||
|
end
|
||||||
|
|
||||||
|
def deployer_exec(*commands, **options)
|
||||||
|
docker_compose("exec deployer #{commands.join(" ")}", **options)
|
||||||
|
end
|
||||||
|
|
||||||
|
def mrsk(*commands, **options)
|
||||||
|
deployer_exec(:mrsk, *commands, **options)
|
||||||
|
end
|
||||||
|
|
||||||
|
def assert_app_is_down
|
||||||
|
response = app_response
|
||||||
|
debug_response_code(response, "502")
|
||||||
|
assert_equal "502", response.code
|
||||||
|
end
|
||||||
|
|
||||||
|
def assert_app_is_up(version: nil)
|
||||||
|
response = app_response
|
||||||
|
debug_response_code(response, "200")
|
||||||
|
assert_equal "200", response.code
|
||||||
|
assert_app_version(version, response) if version
|
||||||
|
end
|
||||||
|
|
||||||
|
def assert_app_not_found
|
||||||
|
response = app_response
|
||||||
|
debug_response_code(response, "404")
|
||||||
|
assert_equal "404", response.code
|
||||||
|
end
|
||||||
|
|
||||||
|
def wait_for_app_to_be_up(timeout: 10, up_count: 3)
|
||||||
|
timeout_at = Time.now + timeout
|
||||||
|
up_times = 0
|
||||||
|
response = app_response
|
||||||
|
while up_times < up_count && timeout_at > Time.now
|
||||||
|
sleep 0.1
|
||||||
|
up_times += 1 if response.code == "200"
|
||||||
|
response = app_response
|
||||||
|
end
|
||||||
|
assert_equal up_times, up_count
|
||||||
|
end
|
||||||
|
|
||||||
|
def app_response
|
||||||
|
Net::HTTP.get_response(URI.parse("http://localhost:12345/version"))
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_app_rev
|
||||||
|
deployer_exec "./update_app_rev.sh"
|
||||||
|
latest_app_version
|
||||||
|
end
|
||||||
|
|
||||||
|
def latest_app_version
|
||||||
|
deployer_exec("git rev-parse HEAD", capture: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
def assert_app_version(version, response)
|
||||||
|
assert_equal version, response.body.strip
|
||||||
|
end
|
||||||
|
|
||||||
|
def assert_hooks_ran(*hooks)
|
||||||
|
hooks.each do |hook|
|
||||||
|
file = "/tmp/#{ENV["TEST_ID"]}/#{hook}"
|
||||||
|
assert_equal "removed '#{file}'", deployer_exec("rm -v #{file}", capture: true).strip
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def assert_200(response)
|
||||||
|
code = response.code
|
||||||
|
if code != "200"
|
||||||
|
puts "Got response code #{code}, here are the traefik logs:"
|
||||||
|
mrsk :traefik, :logs
|
||||||
|
puts "And here are the load balancer logs"
|
||||||
|
docker_compose :logs, :load_balancer
|
||||||
|
puts "Tried to get the response code again and got #{app_response.code}"
|
||||||
|
end
|
||||||
|
assert_equal "200", code
|
||||||
|
end
|
||||||
|
|
||||||
|
def wait_for_healthy(timeout: 20)
|
||||||
|
timeout_at = Time.now + timeout
|
||||||
|
while docker_compose("ps -a | tail -n +2 | grep -v '(healthy)' | wc -l", capture: true) != "0"
|
||||||
|
if timeout_at < Time.now
|
||||||
|
docker_compose("ps -a | tail -n +2 | grep -v '(healthy)'")
|
||||||
|
raise "Container not healthy after #{timeout} seconds" if timeout_at < Time.now
|
||||||
|
end
|
||||||
|
sleep 0.1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def setup_deployer
|
||||||
|
deployer_exec("./setup.sh") unless $DEPLOYER_SETUP
|
||||||
|
$DEPLOYER_SETUP = true
|
||||||
|
end
|
||||||
|
|
||||||
|
def debug_response_code(app_response, expected_code)
|
||||||
|
code = app_response.code
|
||||||
|
if code != expected_code
|
||||||
|
puts "Got response code #{code}, here are the traefik logs:"
|
||||||
|
mrsk :traefik, :logs
|
||||||
|
puts "And here are the load balancer logs"
|
||||||
|
docker_compose :logs, :load_balancer
|
||||||
|
puts "Tried to get the response code again and got #{app_response.code}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
18
test/integration/lock_test.rb
Normal file
18
test/integration/lock_test.rb
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
require_relative "integration_test"
|
||||||
|
|
||||||
|
class LockTest < IntegrationTest
|
||||||
|
test "acquire, release, status" do
|
||||||
|
mrsk :lock, :acquire, "-m 'Integration Tests'"
|
||||||
|
|
||||||
|
status = mrsk :lock, :status, capture: true
|
||||||
|
assert_match /Locked by: Deployer at .*\nVersion: #{latest_app_version}\nMessage: Integration Tests/m, status
|
||||||
|
|
||||||
|
error = mrsk :deploy, capture: true, raise_on_error: false
|
||||||
|
assert_match /Deploy lock found/m, error
|
||||||
|
|
||||||
|
mrsk :lock, :release
|
||||||
|
|
||||||
|
status = mrsk :lock, :status, capture: true
|
||||||
|
assert_match /There is no deploy lock/m, status
|
||||||
|
end
|
||||||
|
end
|
||||||
59
test/integration/main_test.rb
Normal file
59
test/integration/main_test.rb
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
require_relative "integration_test"
|
||||||
|
|
||||||
|
class MainTest < IntegrationTest
|
||||||
|
test "deploy, redeploy, rollback, details and audit" do
|
||||||
|
first_version = latest_app_version
|
||||||
|
|
||||||
|
assert_app_is_down
|
||||||
|
|
||||||
|
mrsk :deploy
|
||||||
|
assert_app_is_up version: first_version
|
||||||
|
assert_hooks_ran "pre-connect", "pre-build", "post-deploy"
|
||||||
|
|
||||||
|
second_version = update_app_rev
|
||||||
|
|
||||||
|
mrsk :redeploy
|
||||||
|
assert_app_is_up version: second_version
|
||||||
|
assert_hooks_ran "pre-connect", "pre-build", "post-deploy"
|
||||||
|
|
||||||
|
mrsk :rollback, first_version
|
||||||
|
assert_hooks_ran "pre-connect", "post-deploy"
|
||||||
|
assert_app_is_up version: first_version
|
||||||
|
|
||||||
|
details = mrsk :details, capture: true
|
||||||
|
assert_match /Traefik Host: vm1/, details
|
||||||
|
assert_match /Traefik Host: vm2/, details
|
||||||
|
assert_match /App Host: vm1/, details
|
||||||
|
assert_match /App Host: vm2/, details
|
||||||
|
assert_match /traefik:v2.9/, details
|
||||||
|
assert_match /registry:4443\/app:#{first_version}/, details
|
||||||
|
|
||||||
|
audit = mrsk :audit, capture: true
|
||||||
|
assert_match /Booted app version #{first_version}.*Booted app version #{second_version}.*Booted app version #{first_version}.*/m, audit
|
||||||
|
end
|
||||||
|
|
||||||
|
test "envify" do
|
||||||
|
mrsk :envify
|
||||||
|
|
||||||
|
assert_equal "SECRET_TOKEN=1234", deployer_exec("cat .env", capture: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "config" do
|
||||||
|
config = YAML.load(mrsk(:config, capture: true))
|
||||||
|
version = latest_app_version
|
||||||
|
|
||||||
|
assert_equal [ "web" ], config[:roles]
|
||||||
|
assert_equal [ "vm1", "vm2" ], config[:hosts]
|
||||||
|
assert_equal "vm1", config[:primary_host]
|
||||||
|
assert_equal version, config[:version]
|
||||||
|
assert_equal "registry:4443/app", config[:repository]
|
||||||
|
assert_equal "registry:4443/app:#{version}", config[:absolute_image]
|
||||||
|
assert_equal "app-#{version}", config[:service_with_version]
|
||||||
|
assert_equal [], config[:env_args]
|
||||||
|
assert_equal [], config[:volume_args]
|
||||||
|
assert_equal({ user: "root", auth_methods: [ "publickey" ] }, config[:ssh_options])
|
||||||
|
assert_equal({ "multiarch" => false, "args" => { "COMMIT_SHA" => version } }, config[:builder])
|
||||||
|
assert_equal [ "--log-opt", "max-size=\"10m\"" ], config[:logging]
|
||||||
|
assert_equal({ "path" => "/up", "port" => 3000, "max_attempts" => 7, "cmd" => "wget -qO- http://localhost > /dev/null" }, config[:healthcheck])
|
||||||
|
end
|
||||||
|
end
|
||||||
36
test/integration/traefik_test.rb
Normal file
36
test/integration/traefik_test.rb
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
require_relative "integration_test"
|
||||||
|
|
||||||
|
class TraefikTest < IntegrationTest
|
||||||
|
test "boot, stop, start, restart, logs, remove" do
|
||||||
|
mrsk :traefik, :boot
|
||||||
|
assert_traefik_running
|
||||||
|
|
||||||
|
mrsk :traefik, :stop
|
||||||
|
assert_traefik_not_running
|
||||||
|
|
||||||
|
mrsk :traefik, :start
|
||||||
|
assert_traefik_running
|
||||||
|
|
||||||
|
mrsk :traefik, :restart
|
||||||
|
assert_traefik_running
|
||||||
|
|
||||||
|
logs = mrsk :traefik, :logs, capture: true
|
||||||
|
assert_match /Traefik version [\d.]+ built on/, logs
|
||||||
|
|
||||||
|
mrsk :traefik, :remove
|
||||||
|
assert_traefik_not_running
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def assert_traefik_running
|
||||||
|
assert_match /traefik:v2.9 "\/entrypoint.sh/, traefik_details
|
||||||
|
end
|
||||||
|
|
||||||
|
def assert_traefik_not_running
|
||||||
|
refute_match /traefik:v2.9 "\/entrypoint.sh/, traefik_details
|
||||||
|
end
|
||||||
|
|
||||||
|
def traefik_details
|
||||||
|
mrsk :traefik, :details, capture: true
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
require "bundler/setup"
|
require "bundler/setup"
|
||||||
require "active_support/test_case"
|
require "active_support/test_case"
|
||||||
require "active_support/testing/autorun"
|
require "active_support/testing/autorun"
|
||||||
|
require "active_support/testing/stream"
|
||||||
require "debug"
|
require "debug"
|
||||||
require "mocha/minitest" # using #stubs that can alter returns
|
require "mocha/minitest" # using #stubs that can alter returns
|
||||||
require "minitest/autorun" # using #stub that take args
|
require "minitest/autorun" # using #stub that take args
|
||||||
@@ -23,4 +24,14 @@ module SSHKit
|
|||||||
end
|
end
|
||||||
|
|
||||||
class ActiveSupport::TestCase
|
class ActiveSupport::TestCase
|
||||||
|
include ActiveSupport::Testing::Stream
|
||||||
|
|
||||||
|
private
|
||||||
|
def stdouted
|
||||||
|
capture(:stdout) { yield }.strip
|
||||||
|
end
|
||||||
|
|
||||||
|
def stderred
|
||||||
|
capture(:stderr) { yield }.strip
|
||||||
|
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,13 +30,35 @@ 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
|
||||||
assert_equal "\"foo\"", Mrsk::Utils.escape_shell_value("foo")
|
assert_equal "\"foo\"", Mrsk::Utils.escape_shell_value("foo")
|
||||||
assert_equal "\"\\`foo\\`\"", Mrsk::Utils.escape_shell_value("`foo`")
|
assert_equal "\"\\`foo\\`\"", Mrsk::Utils.escape_shell_value("`foo`")
|
||||||
|
|
||||||
|
assert_equal "\"${PWD}\"", Mrsk::Utils.escape_shell_value("${PWD}")
|
||||||
|
assert_equal "\"${cat /etc/hostname}\"", Mrsk::Utils.escape_shell_value("${cat /etc/hostname}")
|
||||||
|
assert_equal "\"\\${PWD]\"", Mrsk::Utils.escape_shell_value("${PWD]")
|
||||||
|
assert_equal "\"\\$(PWD)\"", Mrsk::Utils.escape_shell_value("$(PWD)")
|
||||||
|
assert_equal "\"\\$PWD\"", Mrsk::Utils.escape_shell_value("$PWD")
|
||||||
|
|
||||||
|
assert_equal "\"^(https?://)www.example.com/(.*)\\$\"",
|
||||||
|
Mrsk::Utils.escape_shell_value("^(https?://)www.example.com/(.*)$")
|
||||||
|
assert_equal "\"https://example.com/\\$2\"",
|
||||||
|
Mrsk::Utils.escape_shell_value("https://example.com/$2")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user