Compare commits
138 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
edcfc77d95 | ||
|
|
a71e167a03 | ||
|
|
2daaf442fa | ||
|
|
61b7dc90f2 | ||
|
|
f6442513ae | ||
|
|
5e8df58e6b | ||
|
|
9d5a6d1321 | ||
|
|
ecfd258093 | ||
|
|
313f89a108 | ||
|
|
9ab448e186 | ||
|
|
e1433f3895 | ||
|
|
a29e188c90 | ||
|
|
95e3915991 | ||
|
|
30d342183d | ||
|
|
83f5f3f053 | ||
|
|
e6ca270537 | ||
|
|
cd88c49c42 | ||
|
|
d03195ce1c | ||
|
|
da1c049829 | ||
|
|
4095e1853d | ||
|
|
dbc9989730 | ||
|
|
e493369453 | ||
|
|
e760cfa457 | ||
|
|
f8d651af0d | ||
|
|
08172be375 | ||
|
|
a3cc2317e2 | ||
|
|
2746a48e88 | ||
|
|
9a501867b4 | ||
|
|
c5397ff51e | ||
|
|
4950f61a87 | ||
|
|
08d8790851 | ||
|
|
02256ac8fe | ||
|
|
dadd8225da | ||
|
|
aa28ee0f3e | ||
|
|
2007ab475e | ||
|
|
4df3389d09 | ||
|
|
21b13bf8d3 | ||
|
|
6e6f696717 | ||
|
|
98c12a254e | ||
|
|
f0301d2007 | ||
|
|
d3f5e9efe8 | ||
|
|
d9b3fac17a | ||
|
|
cd5c41ddbe | ||
|
|
a14c6141e5 | ||
|
|
95d6ee5031 | ||
|
|
80a4ca4f8a | ||
|
|
12ca865e71 | ||
|
|
66b4a0ea40 | ||
|
|
04b39ea798 | ||
|
|
ae55a7b5d8 | ||
|
|
601cfbd95e | ||
|
|
9fdc85c2e6 | ||
|
|
222eda6085 | ||
|
|
504a09ef1d | ||
|
|
5a25f073f7 | ||
|
|
c8f521c0e8 | ||
|
|
28d6a131a9 | ||
|
|
3a9075b8ba | ||
|
|
079d9538bb | ||
|
|
8e94c21729 | ||
|
|
b536fcfa43 | ||
|
|
85005be07f | ||
|
|
fc00392d68 | ||
|
|
fe9affa349 | ||
|
|
3ecb3a4bfc | ||
|
|
787812cdc2 | ||
|
|
91fb85d6b5 | ||
|
|
db0bf6bb16 | ||
|
|
de2de19434 | ||
|
|
f9fbebaa72 | ||
|
|
1e300f3798 | ||
|
|
0373f6c4de | ||
|
|
9037088f99 | ||
|
|
ff7a1e6726 | ||
|
|
602aa43496 | ||
|
|
e35334e5fe | ||
|
|
cedb8d900f | ||
|
|
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 | ||
|
|
88a7413b3e | ||
|
|
9cc73fed9a | ||
|
|
19527b4f65 | ||
|
|
aceabb3824 | ||
|
|
99fe31d4b4 | ||
|
|
828e56912e |
@@ -27,7 +27,7 @@ Please keep the following guidelines in mind when opening a pull request:
|
|||||||
- Add tests for your changes, if possible.
|
- Add tests for your changes, if possible.
|
||||||
- Ensure that your changes don't break existing functionality.
|
- Ensure that your changes don't break existing functionality.
|
||||||
|
|
||||||
#### Commit message guidline
|
#### Commit message guidelines
|
||||||
|
|
||||||
A good commit message should describe what changed and why.
|
A good commit message should describe what changed and why.
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ RUN apk add --no-cache --update build-base git docker openrc openssh-client-defa
|
|||||||
|
|
||||||
# Copy the rest of our application code into the container.
|
# Copy the rest of our application code into the container.
|
||||||
# We do this after bundle install, to avoid having to run bundle
|
# We do this after bundle install, to avoid having to run bundle
|
||||||
# everytime we do small fixes in the source code.
|
# every time we do small fixes in the source code.
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Install the gem locally from the project folder
|
# Install the gem locally from the project folder
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
PATH
|
PATH
|
||||||
remote: .
|
remote: .
|
||||||
specs:
|
specs:
|
||||||
mrsk (0.12.0)
|
mrsk (0.15.1)
|
||||||
activesupport (>= 7.0)
|
activesupport (>= 7.0)
|
||||||
bcrypt_pbkdf (~> 1.0)
|
bcrypt_pbkdf (~> 1.0)
|
||||||
dotenv (~> 2.8)
|
dotenv (~> 2.8)
|
||||||
|
|||||||
187
README.md
187
README.md
@@ -44,30 +44,48 @@ 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`.
|
||||||
|
|
||||||
|
### Rails <7 usage
|
||||||
|
|
||||||
|
MRSK is not needed to be in your application Gemfile to be used. However, if you want to guarantee specific MRSK version in your CI/CD workflows, you can create a separate Gemfile for MRSK, for example, `gemfile/mrsk.gemfile`:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
source 'https://rubygems.org'
|
||||||
|
|
||||||
|
gem 'mrsk', '~> 0.14'
|
||||||
|
```
|
||||||
|
|
||||||
|
Bundle with `BUNDLE_GEMFILE=gemfiles/mrsk.gemfile bundle`.
|
||||||
|
|
||||||
|
After this MRSK can be used for deployment:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
BUNDLE_GEMFILE=gemfiles/mrsk.gemfile bundle exec mrsk deploy
|
||||||
|
```
|
||||||
|
|
||||||
## Vision
|
## Vision
|
||||||
|
|
||||||
In the past decade+, there's been an explosion in commercial offerings that make deploying web apps easier. Heroku kicked it off with an incredible offering that stayed ahead of the competition seemingly forever. These days we have excellent alternatives like Fly.io and Render. And hosted Kubernetes is making things easier too on AWS, GCP, Digital Ocean, and elsewhere. But these are all offerings that have you renting computers in the cloud at a premium. If you want to run on your own hardware, or even just have a clear migration path to do so in the future, you need to carefully consider how locked in you get to these commercial platforms. Preferably before the bills swallow your business whole!
|
In the past decade+, there's been an explosion in commercial offerings that make deploying web apps easier. Heroku kicked it off with an incredible offering that stayed ahead of the competition seemingly forever. These days we have excellent alternatives like Fly.io and Render. And hosted Kubernetes is making things easier too on AWS, GCP, Digital Ocean, and elsewhere. But these are all offerings that have you renting computers in the cloud at a premium. If you want to run on your own hardware, or even just have a clear migration path to do so in the future, you need to carefully consider how locked in you get to these commercial platforms. Preferably before the bills swallow your business whole!
|
||||||
|
|
||||||
MRSK seeks to bring the advance in ergonomics pioneered by these commercial offerings to deploying web apps anywhere. Whether that's low-cost cloud options without the managed-service markup from the likes of Digital Ocean, Hetzner, OVH, etc, or it's your own colocated bare metal. To MRSK, it's all the same. Feed the config file a list of IP addresses with vanilla Ubuntu servers that have seen no prep beyond an added SSH key, and you'll be running in literally minutes.
|
MRSK seeks to bring the advance in ergonomics pioneered by these commercial offerings to deploying web apps anywhere. Whether that's low-cost cloud options without the managed-service markup from the likes of Digital Ocean, Hetzner, OVH, etc., or it's your own colocated bare metal. To MRSK, it's all the same. Feed the config file a list of IP addresses with vanilla Ubuntu servers that have seen no prep beyond an added SSH key, and you'll be running in literally minutes.
|
||||||
|
|
||||||
This approach gives you enormous portability. You can have your web app deployed on several clouds at ease like this. Or you can buy the baseline with your own hardware, then deploy to a cloud before a big seasonal spike to get more capacity. When you're not locked into a single provider from a tooling perspective, there are a lot of compelling options available.
|
This approach gives you enormous portability. You can have your web app deployed on several clouds at ease like this. Or you can buy the baseline with your own hardware, then deploy to a cloud before a big seasonal spike to get more capacity. When you're not locked into a single provider from a tooling perspective, there are a lot of compelling options available.
|
||||||
|
|
||||||
@@ -123,6 +141,8 @@ This template can safely be checked into git. Then everyone deploying the app ca
|
|||||||
|
|
||||||
If you need separate env variables for different destinations, you can set them with `.env.destination.erb` for the template, which will generate `.env.staging` when run with `mrsk envify -d staging`.
|
If you need separate env variables for different destinations, you can set them with `.env.destination.erb` for the template, which will generate `.env.staging` when run with `mrsk envify -d staging`.
|
||||||
|
|
||||||
|
Note: If you utilize biometrics with 1Password you can remove the `session_token` related parts in the example and just call `op read op://Vault/Docker Hub/password -n`.
|
||||||
|
|
||||||
#### Bitwarden as a secret store
|
#### Bitwarden as a secret store
|
||||||
|
|
||||||
If you are using open source secret store like bitwarden, you can create `.env.erb` as a template which looks up the secrets.
|
If you are using open source secret store like bitwarden, you can create `.env.erb` as a template which looks up the secrets.
|
||||||
@@ -184,6 +204,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`:
|
||||||
@@ -193,13 +226,13 @@ 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:
|
If you are using non-root user (`app` as above example), you need to bootstrap your servers manually, before using them with MRSK. On Ubuntu, you'd do:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo apt update
|
sudo apt update
|
||||||
sudo apt upgrade -y
|
sudo apt upgrade -y
|
||||||
sudo apt install -y docker.io curl git
|
sudo apt install -y docker.io curl git
|
||||||
sudo usermod -a -G docker ubuntu
|
sudo usermod -a -G docker app
|
||||||
```
|
```
|
||||||
|
|
||||||
### Using a proxy SSH host
|
### Using a proxy SSH host
|
||||||
@@ -308,7 +341,7 @@ You can specialize the default Traefik rules by setting labels on the containers
|
|||||||
labels:
|
labels:
|
||||||
traefik.http.routers.hey-web.rule: Host(`app.hey.com`)
|
traefik.http.routers.hey-web.rule: Host(`app.hey.com`)
|
||||||
```
|
```
|
||||||
Traefik rules are in the "service-role-destination" format. The default role will be `web` if no rule is specified. If the destination is not specified, it is not included. To give an example, the above rule would become "traefik.http.routers.hey-web.rule" if it was for the "staging" destination.
|
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!
|
||||||
|
|
||||||
@@ -367,6 +400,16 @@ servers:
|
|||||||
|
|
||||||
That'll start the job containers with `docker run ... --cap-add --cpu-count 4 ...`.
|
That'll start the job containers with `docker run ... --cap-add --cpu-count 4 ...`.
|
||||||
|
|
||||||
|
### Setting a minimum version
|
||||||
|
|
||||||
|
You can set the minimum MRSK version with:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
minimum_version: 0.13.3
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: versions <= 0.13.2 will ignore this setting.
|
||||||
|
|
||||||
### Configuring logging
|
### Configuring logging
|
||||||
|
|
||||||
You can configure the logging driver and options passed to Docker using `logging`:
|
You can configure the logging driver and options passed to Docker using `logging`:
|
||||||
@@ -450,6 +493,37 @@ builder:
|
|||||||
context: ".."
|
context: ".."
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Using multistage builder cache
|
||||||
|
|
||||||
|
Docker multistage build cache can singlehandedly speed up your builds by a lot. Currently MRSK only supports using the GHA cache or the Registry cache:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Using GHA cache
|
||||||
|
builder:
|
||||||
|
cache:
|
||||||
|
type: gha
|
||||||
|
|
||||||
|
# Using Registry cache
|
||||||
|
builder:
|
||||||
|
cache:
|
||||||
|
type: registry
|
||||||
|
|
||||||
|
# Using Registry cache with different cache image
|
||||||
|
builder:
|
||||||
|
cache:
|
||||||
|
type: registry
|
||||||
|
# default image name is <image>-build-cache
|
||||||
|
image: application-cache-image
|
||||||
|
|
||||||
|
# Using Registry cache with additinonal cache-to options
|
||||||
|
builder:
|
||||||
|
cache:
|
||||||
|
type: registry
|
||||||
|
options: mode=max,image-manifest=true,oci-mediatypes=true
|
||||||
|
```
|
||||||
|
|
||||||
|
For further insights into build cache optimization, check out documentation on Docker's official website: https://docs.docker.com/build/cache/.
|
||||||
|
|
||||||
### Using build secrets for new images
|
### Using build secrets for new images
|
||||||
|
|
||||||
Some images need a secret passed in during build time, like a GITHUB_TOKEN, to give access to private gem repositories. This can be done by having the secret in ENV, then referencing it in the builder configuration:
|
Some images need a secret passed in during build time, like a GITHUB_TOKEN, to give access to private gem repositories. This can be done by having the secret in ENV, then referencing it in the builder configuration:
|
||||||
@@ -522,7 +596,7 @@ 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
|
||||||
```
|
```
|
||||||
@@ -568,6 +642,16 @@ traefik:
|
|||||||
entrypoints.otherentrypoint.address: ':9000'
|
entrypoints.otherentrypoint.address: ':9000'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Rebooting Traefik
|
||||||
|
|
||||||
|
If you make changes to Traefik args or labels, you'll need to reboot with:
|
||||||
|
|
||||||
|
`mrsk traefik reboot`
|
||||||
|
|
||||||
|
In production, reboot the Traefik containers one by one with a slower but safer approach, using a rolling reboot:
|
||||||
|
|
||||||
|
`mrsk traefik reboot --rolling`
|
||||||
|
|
||||||
### Configuring build args for new images
|
### Configuring build args for new images
|
||||||
|
|
||||||
Build arguments that aren't secret can also be configured:
|
Build arguments that aren't secret can also be configured:
|
||||||
@@ -655,31 +739,9 @@ servers:
|
|||||||
|
|
||||||
This assumes the Cron settings are stored in `config/crontab`.
|
This assumes the Cron settings are stored in `config/crontab`.
|
||||||
|
|
||||||
### Using audit broadcasts
|
|
||||||
|
|
||||||
If you'd like to broadcast audits of deploys, rollbacks, etc to a chatroom or elsewhere, you can configure the `audit_broadcast_cmd` setting with the path to a bin file that will be passed the audit line as the first argument:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
audit_broadcast_cmd:
|
|
||||||
bin/audit_broadcast
|
|
||||||
```
|
|
||||||
|
|
||||||
The broadcast command could look something like:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
curl -q -d content="[My App] ${1}" https://3.basecamp.com/XXXXX/integrations/XXXXX/buckets/XXXXX/chats/XXXXX/lines
|
|
||||||
```
|
|
||||||
|
|
||||||
That'll post a line like follows to a preconfigured chatbot in Basecamp:
|
|
||||||
|
|
||||||
```
|
|
||||||
[My App] [dhh] Rolled back to version d264c4e92470ad1bd18590f04466787262f605de
|
|
||||||
```
|
|
||||||
|
|
||||||
### Healthcheck
|
### Healthcheck
|
||||||
|
|
||||||
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.
|
MRSK uses Docker healthchecks 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.
|
||||||
|
|
||||||
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:
|
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:
|
||||||
|
|
||||||
@@ -688,6 +750,7 @@ healthcheck:
|
|||||||
path: /healthz
|
path: /healthz
|
||||||
port: 4000
|
port: 4000
|
||||||
max_attempts: 7
|
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.
|
||||||
@@ -714,7 +777,7 @@ servers:
|
|||||||
|
|
||||||
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.
|
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 that the HTTP health checks assume that the `curl` command is avilable inside the container. If that's not the case, use the healthcheck's `cmd` option to specify an alternative check that the container supports.
|
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
|
||||||
|
|
||||||
@@ -833,7 +896,7 @@ If you wish to remove the entire application, including Traefik, containers, ima
|
|||||||
|
|
||||||
## Locking
|
## Locking
|
||||||
|
|
||||||
Commands that are unsafe to run concurrently will take a deploy lock while they run. The lock is the `mrsk_lock` directory on the primary server.
|
Commands that are unsafe to run concurrently will take a deploy lock while they run. The lock is the `mrsk_lock-<service>` directory on the primary server.
|
||||||
|
|
||||||
You can check the lock status with:
|
You can check the lock status with:
|
||||||
|
|
||||||
@@ -848,7 +911,7 @@ Message: Automatic deploy lock
|
|||||||
You can also manually acquire and release the lock
|
You can also manually acquire and release the lock
|
||||||
|
|
||||||
```
|
```
|
||||||
mrsk lock acquire -m "Doing maintanence"
|
mrsk lock acquire -m "Doing maintenance"
|
||||||
```
|
```
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -873,6 +936,60 @@ When `limit` is specified, containers will be booted on, at most, `limit` hosts
|
|||||||
|
|
||||||
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.
|
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` - the full version being deployed
|
||||||
|
- `MRSK_HOSTS` - a comma-separated list of the hosts targeted by the command
|
||||||
|
- `MRSK_COMMAND` - The command we are running
|
||||||
|
- `MRSK_SUBCOMMAND` - optional: The subcommand we are running
|
||||||
|
- `MRSK_DESTINATION` - optional: destination, e.g. "staging"
|
||||||
|
- `MRSK_ROLE` - optional: role targeted, e.g. "web"
|
||||||
|
|
||||||
|
There are four 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. pre-deploy
|
||||||
|
For final checks before deploying, e.g. checking CI completed
|
||||||
|
|
||||||
|
3. post-deploy - run after a deploy, redeploy or rollback.
|
||||||
|
This hook is also passed a `MRSK_RUNTIME` env variable set to the total seconds the deploy took.
|
||||||
|
|
||||||
|
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 the following 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).
|
||||||
|
|||||||
2
bin/mrsk
2
bin/mrsk
@@ -8,7 +8,7 @@ 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
|
exit 1
|
||||||
rescue => e
|
rescue => e
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
module Mrsk::Cli
|
module Mrsk::Cli
|
||||||
class LockError < StandardError; end
|
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
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
class Mrsk::Cli::Accessory < Mrsk::Cli::Base
|
class Mrsk::Cli::Accessory < Mrsk::Cli::Base
|
||||||
desc "boot [NAME]", "Boot new accessory service on host (use NAME=all to boot all accessories)"
|
desc "boot [NAME]", "Boot new accessory service on host (use NAME=all to boot all accessories)"
|
||||||
def boot(name)
|
def boot(name, login: true)
|
||||||
with_lock do
|
mutating do
|
||||||
if name == "all"
|
if name == "all"
|
||||||
MRSK.accessory_names.each { |accessory_name| boot(accessory_name) }
|
MRSK.accessory_names.each { |accessory_name| boot(accessory_name) }
|
||||||
else
|
else
|
||||||
@@ -10,12 +10,10 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
|
|||||||
upload(name)
|
upload(name)
|
||||||
|
|
||||||
on(accessory.hosts) do
|
on(accessory.hosts) do
|
||||||
execute *MRSK.registry.login
|
execute *MRSK.registry.login if login
|
||||||
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
|
||||||
@@ -23,7 +21,7 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
|
|||||||
|
|
||||||
desc "upload [NAME]", "Upload accessory files to host", hide: true
|
desc "upload [NAME]", "Upload accessory files to host", hide: true
|
||||||
def upload(name)
|
def upload(name)
|
||||||
with_lock do
|
mutating do
|
||||||
with_accessory(name) do |accessory|
|
with_accessory(name) do |accessory|
|
||||||
on(accessory.hosts) do
|
on(accessory.hosts) do
|
||||||
accessory.files.each do |(local, remote)|
|
accessory.files.each do |(local, remote)|
|
||||||
@@ -40,7 +38,7 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
|
|||||||
|
|
||||||
desc "directories [NAME]", "Create accessory directories on host", hide: true
|
desc "directories [NAME]", "Create accessory directories on host", hide: true
|
||||||
def directories(name)
|
def directories(name)
|
||||||
with_lock do
|
mutating do
|
||||||
with_accessory(name) do |accessory|
|
with_accessory(name) do |accessory|
|
||||||
on(accessory.hosts) do
|
on(accessory.hosts) do
|
||||||
accessory.directories.keys.each do |host_path|
|
accessory.directories.keys.each do |host_path|
|
||||||
@@ -53,18 +51,22 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
|
|||||||
|
|
||||||
desc "reboot [NAME]", "Reboot existing accessory on host (stop container, remove container, start new container)"
|
desc "reboot [NAME]", "Reboot existing accessory on host (stop container, remove container, start new container)"
|
||||||
def reboot(name)
|
def reboot(name)
|
||||||
with_lock do
|
mutating do
|
||||||
with_accessory(name) do |accessory|
|
with_accessory(name) do |accessory|
|
||||||
|
on(accessory.hosts) do
|
||||||
|
execute *MRSK.registry.login
|
||||||
|
end
|
||||||
|
|
||||||
stop(name)
|
stop(name)
|
||||||
remove_container(name)
|
remove_container(name)
|
||||||
boot(name)
|
boot(name, login: false)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
desc "start [NAME]", "Start existing accessory container on host"
|
desc "start [NAME]", "Start existing accessory container on host"
|
||||||
def start(name)
|
def start(name)
|
||||||
with_lock do
|
mutating do
|
||||||
with_accessory(name) do |accessory|
|
with_accessory(name) do |accessory|
|
||||||
on(accessory.hosts) do
|
on(accessory.hosts) do
|
||||||
execute *MRSK.auditor.record("Started #{name} accessory"), verbosity: :debug
|
execute *MRSK.auditor.record("Started #{name} accessory"), verbosity: :debug
|
||||||
@@ -76,7 +78,7 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
|
|||||||
|
|
||||||
desc "stop [NAME]", "Stop existing accessory container on host"
|
desc "stop [NAME]", "Stop existing accessory container on host"
|
||||||
def stop(name)
|
def stop(name)
|
||||||
with_lock do
|
mutating do
|
||||||
with_accessory(name) do |accessory|
|
with_accessory(name) do |accessory|
|
||||||
on(accessory.hosts) do
|
on(accessory.hosts) do
|
||||||
execute *MRSK.auditor.record("Stopped #{name} accessory"), verbosity: :debug
|
execute *MRSK.auditor.record("Stopped #{name} accessory"), verbosity: :debug
|
||||||
@@ -88,7 +90,7 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
|
|||||||
|
|
||||||
desc "restart [NAME]", "Restart existing accessory container on host"
|
desc "restart [NAME]", "Restart existing accessory container on host"
|
||||||
def restart(name)
|
def restart(name)
|
||||||
with_lock do
|
mutating do
|
||||||
with_accessory(name) do
|
with_accessory(name) do
|
||||||
stop(name)
|
stop(name)
|
||||||
start(name)
|
start(name)
|
||||||
@@ -167,7 +169,7 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
|
|||||||
desc "remove [NAME]", "Remove accessory container, image and data directory from host (use NAME=all to remove all accessories)"
|
desc "remove [NAME]", "Remove accessory container, image and data directory from host (use NAME=all to remove all accessories)"
|
||||||
option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
|
option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
|
||||||
def remove(name)
|
def remove(name)
|
||||||
with_lock do
|
mutating do
|
||||||
if name == "all"
|
if name == "all"
|
||||||
MRSK.accessory_names.each { |accessory_name| remove(accessory_name) }
|
MRSK.accessory_names.each { |accessory_name| remove(accessory_name) }
|
||||||
else
|
else
|
||||||
@@ -185,7 +187,7 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
|
|||||||
|
|
||||||
desc "remove_container [NAME]", "Remove accessory container from host", hide: true
|
desc "remove_container [NAME]", "Remove accessory container from host", hide: true
|
||||||
def remove_container(name)
|
def remove_container(name)
|
||||||
with_lock do
|
mutating do
|
||||||
with_accessory(name) do |accessory|
|
with_accessory(name) do |accessory|
|
||||||
on(accessory.hosts) do
|
on(accessory.hosts) do
|
||||||
execute *MRSK.auditor.record("Remove #{name} accessory container"), verbosity: :debug
|
execute *MRSK.auditor.record("Remove #{name} accessory container"), verbosity: :debug
|
||||||
@@ -197,7 +199,7 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
|
|||||||
|
|
||||||
desc "remove_image [NAME]", "Remove accessory image from host", hide: true
|
desc "remove_image [NAME]", "Remove accessory image from host", hide: true
|
||||||
def remove_image(name)
|
def remove_image(name)
|
||||||
with_lock do
|
mutating do
|
||||||
with_accessory(name) do |accessory|
|
with_accessory(name) do |accessory|
|
||||||
on(accessory.hosts) do
|
on(accessory.hosts) do
|
||||||
execute *MRSK.auditor.record("Removed #{name} accessory image"), verbosity: :debug
|
execute *MRSK.auditor.record("Removed #{name} accessory image"), verbosity: :debug
|
||||||
@@ -209,7 +211,7 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
|
|||||||
|
|
||||||
desc "remove_service_directory [NAME]", "Remove accessory directory used for uploaded files and data directories from host", hide: true
|
desc "remove_service_directory [NAME]", "Remove accessory directory used for uploaded files and data directories from host", hide: true
|
||||||
def remove_service_directory(name)
|
def remove_service_directory(name)
|
||||||
with_lock do
|
mutating do
|
||||||
with_accessory(name) do |accessory|
|
with_accessory(name) do |accessory|
|
||||||
on(accessory.hosts) do
|
on(accessory.hosts) do
|
||||||
execute *accessory.remove_service_directory
|
execute *accessory.remove_service_directory
|
||||||
|
|||||||
@@ -1,38 +1,40 @@
|
|||||||
class Mrsk::Cli::App < Mrsk::Cli::Base
|
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
|
mutating 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
|
||||||
|
|
||||||
on(MRSK.hosts) do
|
on(MRSK.hosts) do
|
||||||
execute *MRSK.auditor.record("Tagging #{MRSK.config.absolute_image} as the latest image"), verbosity: :debug
|
execute *MRSK.auditor.record("Tagging #{MRSK.config.absolute_image} as the latest image"), verbosity: :debug
|
||||||
execute *MRSK.app.tag_current_as_latest
|
execute *MRSK.app.tag_current_as_latest
|
||||||
end
|
end
|
||||||
|
|
||||||
on(MRSK.hosts, **MRSK.boot_strategy) 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|
|
||||||
app = MRSK.app(role: role)
|
app = MRSK.app(role: role)
|
||||||
auditor = MRSK.auditor(role: role)
|
auditor = MRSK.auditor(role: role)
|
||||||
|
|
||||||
execute *auditor.record("Booted app version #{version}"), verbosity: :debug
|
if capture_with_info(*app.container_id_for_version(version, only_running: true), raise_on_non_zero_exit: false).present?
|
||||||
|
tmp_version = "#{version}_replaced_#{SecureRandom.hex(8)}"
|
||||||
|
info "Renaming container #{version} to #{tmp_version} as already deployed on #{host}"
|
||||||
|
execute *auditor.record("Renaming container #{version} to #{tmp_version}"), verbosity: :debug
|
||||||
|
execute *app.rename_container(version: version, new_version: tmp_version)
|
||||||
|
end
|
||||||
|
|
||||||
if capture_with_info(*app.container_id_for_version(version), raise_on_non_zero_exit: false).present?
|
execute *auditor.record("Booted app version #{version}"), verbosity: :debug
|
||||||
tmp_version = "#{version}_#{SecureRandom.hex(8)}"
|
|
||||||
info "Renaming container #{version} to #{tmp_version} as already deployed on #{host}"
|
old_version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
|
||||||
execute *auditor.record("Renaming container #{version} to #{tmp_version}"), verbosity: :debug
|
execute *app.start_or_run(hostname: "#{host}-#{SecureRandom.hex(6)}")
|
||||||
execute *app.rename_container(version: version, new_version: tmp_version)
|
|
||||||
|
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
|
||||||
|
|
||||||
old_version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
|
|
||||||
execute *app.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
|
||||||
@@ -41,7 +43,7 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
|
|||||||
|
|
||||||
desc "start", "Start existing app container on servers"
|
desc "start", "Start existing app container on servers"
|
||||||
def start
|
def start
|
||||||
with_lock do
|
mutating do
|
||||||
on(MRSK.hosts) do |host|
|
on(MRSK.hosts) do |host|
|
||||||
roles = MRSK.roles_on(host)
|
roles = MRSK.roles_on(host)
|
||||||
|
|
||||||
@@ -55,12 +57,12 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
|
|||||||
|
|
||||||
desc "stop", "Stop app container on servers"
|
desc "stop", "Stop app container on servers"
|
||||||
def stop
|
def stop
|
||||||
with_lock do
|
mutating do
|
||||||
on(MRSK.hosts) do |host|
|
on(MRSK.hosts) 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("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
|
||||||
@@ -107,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
|
||||||
@@ -133,7 +135,7 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
|
|||||||
desc "stale_containers", "Detect app stale containers"
|
desc "stale_containers", "Detect app stale containers"
|
||||||
option :stop, aliases: "-s", type: :boolean, default: false, desc: "Stop the stale containers found"
|
option :stop, aliases: "-s", type: :boolean, default: false, desc: "Stop the stale containers found"
|
||||||
def stale_containers
|
def stale_containers
|
||||||
with_lock do
|
mutating do
|
||||||
stop = options[:stop]
|
stop = options[:stop]
|
||||||
|
|
||||||
cli = self
|
cli = self
|
||||||
@@ -200,7 +202,7 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
|
|||||||
|
|
||||||
desc "remove", "Remove app containers and images from servers"
|
desc "remove", "Remove app containers and images from servers"
|
||||||
def remove
|
def remove
|
||||||
with_lock do
|
mutating do
|
||||||
stop
|
stop
|
||||||
remove_containers
|
remove_containers
|
||||||
remove_images
|
remove_images
|
||||||
@@ -209,12 +211,12 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
|
|||||||
|
|
||||||
desc "remove_container [VERSION]", "Remove app container with given version from servers", hide: true
|
desc "remove_container [VERSION]", "Remove app container with given version from servers", hide: true
|
||||||
def remove_container(version)
|
def remove_container(version)
|
||||||
with_lock do
|
mutating do
|
||||||
on(MRSK.hosts) do |host|
|
on(MRSK.hosts) 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("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
|
||||||
@@ -223,12 +225,12 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
|
|||||||
|
|
||||||
desc "remove_containers", "Remove all app containers from servers", hide: true
|
desc "remove_containers", "Remove all app containers from servers", hide: true
|
||||||
def remove_containers
|
def remove_containers
|
||||||
with_lock do
|
mutating do
|
||||||
on(MRSK.hosts) do |host|
|
on(MRSK.hosts) 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("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
|
||||||
@@ -237,7 +239,7 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
|
|||||||
|
|
||||||
desc "remove_images", "Remove all app images from servers", hide: true
|
desc "remove_images", "Remove all app images from servers", hide: true
|
||||||
def remove_images
|
def remove_images
|
||||||
with_lock do
|
mutating do
|
||||||
on(MRSK.hosts) do
|
on(MRSK.hosts) do
|
||||||
execute *MRSK.auditor.record("Removed all app images"), verbosity: :debug
|
execute *MRSK.auditor.record("Removed all app images"), verbosity: :debug
|
||||||
execute *MRSK.app.remove_images
|
execute *MRSK.app.remove_images
|
||||||
|
|||||||
@@ -20,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
|
||||||
@@ -72,53 +72,57 @@ 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)
|
def mutating
|
||||||
run_locally { execute *MRSK.auditor.broadcast(line), verbosity: :debug }
|
return yield if MRSK.holding_lock?
|
||||||
end
|
|
||||||
|
|
||||||
def with_lock
|
MRSK.config.ensure_env_available
|
||||||
if MRSK.holding_lock?
|
|
||||||
|
run_hook "pre-connect"
|
||||||
|
|
||||||
|
acquire_lock
|
||||||
|
|
||||||
|
begin
|
||||||
yield
|
yield
|
||||||
else
|
rescue
|
||||||
acquire_lock
|
if MRSK.hold_lock_on_error?
|
||||||
|
error " \e[31mDeploy lock was not released\e[0m"
|
||||||
begin
|
else
|
||||||
yield
|
release_lock
|
||||||
rescue
|
|
||||||
if MRSK.hold_lock_on_error?
|
|
||||||
error " \e[31mDeploy lock was not released\e[0m"
|
|
||||||
else
|
|
||||||
release_lock
|
|
||||||
end
|
|
||||||
|
|
||||||
raise
|
|
||||||
end
|
end
|
||||||
|
|
||||||
release_lock
|
raise
|
||||||
end
|
end
|
||||||
|
|
||||||
|
release_lock
|
||||||
end
|
end
|
||||||
|
|
||||||
def acquire_lock
|
def acquire_lock
|
||||||
say "Acquiring the deploy lock"
|
raise_if_locked do
|
||||||
on(MRSK.primary_host) { execute *MRSK.lock.acquire("Automatic deploy lock", MRSK.config.version) }
|
say "Acquiring the deploy lock...", :magenta
|
||||||
|
on(MRSK.primary_host) { execute *MRSK.lock.acquire("Automatic deploy lock", MRSK.config.version), verbosity: :debug }
|
||||||
|
end
|
||||||
|
|
||||||
MRSK.holding_lock = true
|
MRSK.holding_lock = true
|
||||||
|
end
|
||||||
|
|
||||||
|
def release_lock
|
||||||
|
say "Releasing the deploy lock...", :magenta
|
||||||
|
on(MRSK.primary_host) { execute *MRSK.lock.release, verbosity: :debug }
|
||||||
|
|
||||||
|
MRSK.holding_lock = false
|
||||||
|
end
|
||||||
|
|
||||||
|
def raise_if_locked
|
||||||
|
yield
|
||||||
rescue SSHKit::Runner::ExecuteError => e
|
rescue SSHKit::Runner::ExecuteError => e
|
||||||
if e.message =~ /cannot create directory/
|
if e.message =~ /cannot create directory/
|
||||||
on(MRSK.primary_host) { execute *MRSK.lock.status }
|
on(MRSK.primary_host) { puts capture_with_debug(*MRSK.lock.status) }
|
||||||
raise LockError, "Deploy lock found"
|
raise LockError, "Deploy lock found"
|
||||||
else
|
else
|
||||||
raise e
|
raise e
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def release_lock
|
|
||||||
say "Releasing the deploy lock"
|
|
||||||
on(MRSK.primary_host) { execute *MRSK.lock.release }
|
|
||||||
|
|
||||||
MRSK.holding_lock = false
|
|
||||||
end
|
|
||||||
|
|
||||||
def hold_lock_on_error
|
def hold_lock_on_error
|
||||||
if MRSK.hold_lock_on_error?
|
if MRSK.hold_lock_on_error?
|
||||||
yield
|
yield
|
||||||
@@ -128,5 +132,40 @@ module Mrsk::Cli
|
|||||||
MRSK.hold_lock_on_error = false
|
MRSK.hold_lock_on_error = false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
def run_hook(hook, **extra_details)
|
||||||
|
if !options[:skip_hooks] && MRSK.hook.hook_exists?(hook)
|
||||||
|
details = { hosts: MRSK.hosts.join(","), command: command, subcommand: subcommand }
|
||||||
|
|
||||||
|
say "Running the #{hook} hook...", :magenta
|
||||||
|
run_locally do
|
||||||
|
MRSK.with_verbosity(:debug) { execute *MRSK.hook.run(hook, **details, **extra_details) }
|
||||||
|
rescue SSHKit::Command::Failed
|
||||||
|
raise HookError.new("Hook `#{hook}` failed")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def command
|
||||||
|
@mrsk_command ||= begin
|
||||||
|
invocation_class, invocation_commands = *first_invocation
|
||||||
|
if invocation_class == Mrsk::Cli::Main
|
||||||
|
invocation_commands[0]
|
||||||
|
else
|
||||||
|
Mrsk::Cli::Main.subcommand_classes.find { |command, clazz| clazz == invocation_class }[0]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def subcommand
|
||||||
|
@mrsk_subcommand ||= begin
|
||||||
|
invocation_class, invocation_commands = *first_invocation
|
||||||
|
invocation_commands[0] if invocation_class != Mrsk::Cli::Main
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def first_invocation
|
||||||
|
instance_variable_get("@_invocations").first
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ class Mrsk::Cli::Build < Mrsk::Cli::Base
|
|||||||
|
|
||||||
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
|
mutating do
|
||||||
push
|
push
|
||||||
pull
|
pull
|
||||||
end
|
end
|
||||||
@@ -11,14 +11,15 @@ class Mrsk::Cli::Build < Mrsk::Cli::Base
|
|||||||
|
|
||||||
desc "push", "Build and push app image to registry"
|
desc "push", "Build and push app image to registry"
|
||||||
def push
|
def push
|
||||||
with_lock do
|
mutating do
|
||||||
cli = self
|
cli = self
|
||||||
|
|
||||||
|
verify_local_dependencies
|
||||||
|
run_hook "pre-build"
|
||||||
|
|
||||||
run_locally do
|
run_locally do
|
||||||
begin
|
begin
|
||||||
if cli.verify_local_dependencies
|
MRSK.with_verbosity(:debug) { execute *MRSK.builder.push }
|
||||||
MRSK.with_verbosity(:debug) { execute *MRSK.builder.push }
|
|
||||||
end
|
|
||||||
rescue SSHKit::Command::Failed => e
|
rescue SSHKit::Command::Failed => e
|
||||||
if e.message =~ /(no builder)|(no such file or directory)/
|
if e.message =~ /(no builder)|(no such file or directory)/
|
||||||
error "Missing compatible builder, so creating a new one first"
|
error "Missing compatible builder, so creating a new one first"
|
||||||
@@ -36,7 +37,7 @@ class Mrsk::Cli::Build < Mrsk::Cli::Base
|
|||||||
|
|
||||||
desc "pull", "Pull app image from registry onto servers"
|
desc "pull", "Pull app image from registry onto servers"
|
||||||
def pull
|
def pull
|
||||||
with_lock do
|
mutating do
|
||||||
on(MRSK.hosts) do
|
on(MRSK.hosts) do
|
||||||
execute *MRSK.auditor.record("Pulled image with version #{MRSK.config.version}"), verbosity: :debug
|
execute *MRSK.auditor.record("Pulled image with version #{MRSK.config.version}"), verbosity: :debug
|
||||||
execute *MRSK.builder.clean, raise_on_non_zero_exit: false
|
execute *MRSK.builder.clean, raise_on_non_zero_exit: false
|
||||||
@@ -47,7 +48,7 @@ class Mrsk::Cli::Build < Mrsk::Cli::Base
|
|||||||
|
|
||||||
desc "create", "Create a build setup"
|
desc "create", "Create a build setup"
|
||||||
def create
|
def create
|
||||||
with_lock do
|
mutating do
|
||||||
run_locally do
|
run_locally do
|
||||||
begin
|
begin
|
||||||
debug "Using builder: #{MRSK.builder.name}"
|
debug "Using builder: #{MRSK.builder.name}"
|
||||||
@@ -66,7 +67,7 @@ class Mrsk::Cli::Build < Mrsk::Cli::Base
|
|||||||
|
|
||||||
desc "remove", "Remove build setup"
|
desc "remove", "Remove build setup"
|
||||||
def remove
|
def remove
|
||||||
with_lock do
|
mutating do
|
||||||
run_locally do
|
run_locally do
|
||||||
debug "Using builder: #{MRSK.builder.name}"
|
debug "Using builder: #{MRSK.builder.name}"
|
||||||
execute *MRSK.builder.remove
|
execute *MRSK.builder.remove
|
||||||
@@ -82,21 +83,18 @@ class Mrsk::Cli::Build < Mrsk::Cli::Base
|
|||||||
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"
|
||||||
|
|
||||||
desc "", "" # Really a private method, but needed to be invoked from #push
|
raise BuildError, build_error
|
||||||
def verify_local_dependencies
|
end
|
||||||
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
|
||||||
|
|
||||||
true
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ class Mrsk::Cli::Healthcheck < Mrsk::Cli::Base
|
|||||||
Mrsk::Utils::HealthcheckPoller.wait_for_healthy { capture_with_info(*MRSK.healthcheck.status) }
|
Mrsk::Utils::HealthcheckPoller.wait_for_healthy { capture_with_info(*MRSK.healthcheck.status) }
|
||||||
rescue Mrsk::Utils::HealthcheckPoller::HealthcheckError => e
|
rescue Mrsk::Utils::HealthcheckPoller::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)
|
||||||
raise
|
raise
|
||||||
ensure
|
ensure
|
||||||
execute *MRSK.healthcheck.stop, raise_on_non_zero_exit: false
|
execute *MRSK.healthcheck.stop, raise_on_non_zero_exit: false
|
||||||
|
|||||||
@@ -2,16 +2,16 @@ 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
|
||||||
|
|
||||||
desc "acquire", "Acquire the deploy lock"
|
desc "acquire", "Acquire the deploy lock"
|
||||||
option :message, aliases: "-m", type: :string, desc: "A lock mesasge", required: true
|
option :message, aliases: "-m", type: :string, desc: "A lock message", 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 "Acquired the deploy lock"
|
say "Acquired the deploy lock"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -19,7 +19,7 @@ class Mrsk::Cli::Lock < Mrsk::Cli::Base
|
|||||||
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 "Released 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
|
mutating 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,10 +13,10 @@ 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
|
mutating do
|
||||||
|
invoke_options = deploy_options
|
||||||
|
|
||||||
runtime = print_runtime do
|
|
||||||
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
|
||||||
|
|
||||||
@@ -28,6 +28,8 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
|
|||||||
invoke "mrsk:cli:build:deliver", [], invoke_options
|
invoke "mrsk:cli:build:deliver", [], invoke_options
|
||||||
end
|
end
|
||||||
|
|
||||||
|
run_hook "pre-deploy"
|
||||||
|
|
||||||
say "Ensure Traefik is running...", :magenta
|
say "Ensure Traefik is running...", :magenta
|
||||||
invoke "mrsk:cli:traefik:boot", [], invoke_options
|
invoke "mrsk:cli:traefik:boot", [], invoke_options
|
||||||
|
|
||||||
@@ -37,25 +39,23 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
|
|||||||
say "Detect stale containers...", :magenta
|
say "Detect stale containers...", :magenta
|
||||||
invoke "mrsk:cli:app:stale_containers", [], invoke_options
|
invoke "mrsk:cli:app:stale_containers", [], invoke_options
|
||||||
|
|
||||||
hold_lock_on_error do
|
invoke "mrsk:cli:app:boot", [], invoke_options
|
||||||
invoke "mrsk:cli:app:boot", [], invoke_options
|
|
||||||
end
|
|
||||||
|
|
||||||
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
|
mutating 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
|
||||||
@@ -64,61 +64,43 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
|
|||||||
invoke "mrsk:cli:build:deliver", [], invoke_options
|
invoke "mrsk:cli:build:deliver", [], invoke_options
|
||||||
end
|
end
|
||||||
|
|
||||||
|
run_hook "pre-deploy"
|
||||||
|
|
||||||
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
|
say "Detect stale containers...", :magenta
|
||||||
invoke "mrsk:cli:app:stale_containers", [], invoke_options
|
invoke "mrsk:cli:app:stale_containers", [], invoke_options
|
||||||
|
|
||||||
hold_lock_on_error do
|
invoke "mrsk:cli:app:boot", [], invoke_options
|
||||||
invoke "mrsk:cli:app:boot", [], invoke_options
|
|
||||||
end
|
|
||||||
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
|
||||||
invoke_options = deploy_options
|
runtime = print_runtime do
|
||||||
|
mutating do
|
||||||
|
invoke_options = deploy_options
|
||||||
|
|
||||||
hold_lock_on_error do
|
|
||||||
MRSK.config.version = version
|
MRSK.config.version = version
|
||||||
old_version = nil
|
old_version = nil
|
||||||
|
|
||||||
if container_available?(version)
|
if container_available?(version)
|
||||||
say "Start version #{version}, then wait #{MRSK.config.readiness_delay}s for app to boot before stopping the old version...", :magenta
|
run_hook "pre-deploy"
|
||||||
|
|
||||||
on(MRSK.hosts) do
|
invoke "mrsk:cli:app:boot", [], invoke_options.merge(version: version)
|
||||||
execute *MRSK.auditor.record("Tagging #{MRSK.config.absolute_image} as the latest image"), verbosity: :debug
|
rolled_back = true
|
||||||
execute *MRSK.app.tag_current_as_latest
|
|
||||||
end
|
|
||||||
|
|
||||||
on(MRSK.hosts) do |host|
|
|
||||||
roles = MRSK.roles_on(host)
|
|
||||||
|
|
||||||
roles.each do |role|
|
|
||||||
app = MRSK.app(role: role)
|
|
||||||
old_version = capture_with_info(*app.current_running_version).strip.presence
|
|
||||||
|
|
||||||
execute *app.start
|
|
||||||
|
|
||||||
if old_version
|
|
||||||
sleep MRSK.config.readiness_delay
|
|
||||||
|
|
||||||
execute *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
|
else
|
||||||
say "The app version '#{version}' is not available as a container (use 'mrsk app containers' for available versions)", :red
|
say "The app version '#{version}' is not available as a container (use 'mrsk app containers' for available versions)", :red
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
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"
|
||||||
@@ -160,6 +142,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)"
|
||||||
@@ -190,7 +180,7 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
|
|||||||
desc "remove", "Remove Traefik, app, accessories, and registry session from servers"
|
desc "remove", "Remove Traefik, app, accessories, and registry session from servers"
|
||||||
option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
|
option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
|
||||||
def remove
|
def remove
|
||||||
with_lock do
|
mutating do
|
||||||
if options[:confirmed] || ask("This will remove all containers and images. Are you sure?", limited_to: %w( y N ), default: "N") == "y"
|
if options[:confirmed] || ask("This will remove all containers and images. Are you sure?", limited_to: %w( y N ), default: "N") == "y"
|
||||||
invoke "mrsk:cli:traefik:remove", [], options.without(:confirmed)
|
invoke "mrsk:cli:traefik:remove", [], options.without(:confirmed)
|
||||||
invoke "mrsk:cli:app:remove", [], options.without(:confirmed)
|
invoke "mrsk:cli:app:remove", [], options.without(:confirmed)
|
||||||
@@ -217,6 +207,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
|
||||||
|
|
||||||
@@ -229,9 +222,6 @@ 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_available?(version)
|
def container_available?(version)
|
||||||
begin
|
begin
|
||||||
@@ -256,8 +246,4 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
|
|||||||
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
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
class Mrsk::Cli::Prune < Mrsk::Cli::Base
|
class Mrsk::Cli::Prune < Mrsk::Cli::Base
|
||||||
desc "all", "Prune unused images and stopped containers"
|
desc "all", "Prune unused images and stopped containers"
|
||||||
def all
|
def all
|
||||||
with_lock do
|
mutating do
|
||||||
containers
|
containers
|
||||||
images
|
images
|
||||||
end
|
end
|
||||||
@@ -9,17 +9,18 @@ class Mrsk::Cli::Prune < Mrsk::Cli::Base
|
|||||||
|
|
||||||
desc "images", "Prune dangling images"
|
desc "images", "Prune dangling images"
|
||||||
def images
|
def images
|
||||||
with_lock do
|
mutating do
|
||||||
on(MRSK.hosts) do
|
on(MRSK.hosts) do
|
||||||
execute *MRSK.auditor.record("Pruned images"), verbosity: :debug
|
execute *MRSK.auditor.record("Pruned images"), verbosity: :debug
|
||||||
execute *MRSK.prune.images
|
execute *MRSK.prune.dangling_images
|
||||||
|
execute *MRSK.prune.tagged_images
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
desc "containers", "Prune all stopped containers, except the last 5"
|
desc "containers", "Prune all stopped containers, except the last 5"
|
||||||
def containers
|
def containers
|
||||||
with_lock do
|
mutating do
|
||||||
on(MRSK.hosts) do
|
on(MRSK.hosts) do
|
||||||
execute *MRSK.auditor.record("Pruned containers"), verbosity: :debug
|
execute *MRSK.auditor.record("Pruned containers"), verbosity: :debug
|
||||||
execute *MRSK.prune.containers
|
execute *MRSK.prune.containers
|
||||||
|
|||||||
@@ -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 ]
|
||||||
109
lib/mrsk/cli/templates/sample_hooks/pre-deploy.sample
Executable file
109
lib/mrsk/cli/templates/sample_hooks/pre-deploy.sample
Executable file
@@ -0,0 +1,109 @@
|
|||||||
|
#!/usr/bin/env ruby
|
||||||
|
|
||||||
|
# A sample pre-deploy hook
|
||||||
|
#
|
||||||
|
# Checks the Github status of the build, waiting for a pending build to complete for up to 720 seconds.
|
||||||
|
#
|
||||||
|
# Fails unless the combined status is "success"
|
||||||
|
#
|
||||||
|
# These environment variables are available:
|
||||||
|
# MRSK_RECORDED_AT
|
||||||
|
# MRSK_PERFORMER
|
||||||
|
# MRSK_VERSION
|
||||||
|
# MRSK_HOSTS
|
||||||
|
# MRSK_COMMAND
|
||||||
|
# MRSK_SUBCOMMAND
|
||||||
|
# MRSK_ROLE (if set)
|
||||||
|
# MRSK_DESTINATION (if set)
|
||||||
|
|
||||||
|
# Only check the build status for production deployments
|
||||||
|
if ENV["MRSK_COMMAND"] == "rollback" || ENV["MRSK_DESTINATION"] != "production"
|
||||||
|
exit 0
|
||||||
|
end
|
||||||
|
|
||||||
|
require "bundler/inline"
|
||||||
|
|
||||||
|
# true = install gems so this is fast on repeat invocations
|
||||||
|
gemfile(true, quiet: true) do
|
||||||
|
source "https://rubygems.org"
|
||||||
|
|
||||||
|
gem "octokit"
|
||||||
|
gem "faraday-retry"
|
||||||
|
end
|
||||||
|
|
||||||
|
MAX_ATTEMPTS = 72
|
||||||
|
ATTEMPTS_GAP = 10
|
||||||
|
|
||||||
|
def exit_with_error(message)
|
||||||
|
$stderr.puts message
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
|
||||||
|
class GithubStatusChecks
|
||||||
|
attr_reader :remote_url, :git_sha, :github_client, :combined_status
|
||||||
|
|
||||||
|
def initialize
|
||||||
|
@remote_url = `git config --get remote.origin.url`.strip.delete_prefix("https://github.com/")
|
||||||
|
@git_sha = `git rev-parse HEAD`.strip
|
||||||
|
@github_client = Octokit::Client.new(access_token: ENV["GITHUB_TOKEN"])
|
||||||
|
refresh!
|
||||||
|
end
|
||||||
|
|
||||||
|
def refresh!
|
||||||
|
@combined_status = github_client.combined_status(remote_url, git_sha)
|
||||||
|
end
|
||||||
|
|
||||||
|
def state
|
||||||
|
combined_status[:state]
|
||||||
|
end
|
||||||
|
|
||||||
|
def first_status_url
|
||||||
|
first_status = combined_status[:statuses].find { |status| status[:state] == state }
|
||||||
|
first_status && first_status[:target_url]
|
||||||
|
end
|
||||||
|
|
||||||
|
def complete_count
|
||||||
|
combined_status[:statuses].count { |status| status[:state] != "pending"}
|
||||||
|
end
|
||||||
|
|
||||||
|
def total_count
|
||||||
|
combined_status[:statuses].count
|
||||||
|
end
|
||||||
|
|
||||||
|
def current_status
|
||||||
|
if total_count > 0
|
||||||
|
"Completed #{complete_count}/#{total_count} checks, see #{first_status_url} ..."
|
||||||
|
else
|
||||||
|
"Build not started..."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
$stdout.sync = true
|
||||||
|
|
||||||
|
puts "Checking build status..."
|
||||||
|
attempts = 0
|
||||||
|
checks = GithubStatusChecks.new
|
||||||
|
|
||||||
|
begin
|
||||||
|
loop do
|
||||||
|
case checks.state
|
||||||
|
when "success"
|
||||||
|
puts "Checks passed, see #{checks.first_status_url}"
|
||||||
|
exit 0
|
||||||
|
when "failure"
|
||||||
|
exit_with_error "Checks failed, see #{checks.first_status_url}"
|
||||||
|
when "pending"
|
||||||
|
attempts += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
exit_with_error "Checks are still pending, gave up after #{MAX_ATTEMPTS * ATTEMPTS_GAP} seconds" if attempts == MAX_ATTEMPTS
|
||||||
|
|
||||||
|
puts checks.current_status
|
||||||
|
sleep(ATTEMPTS_GAP)
|
||||||
|
checks.refresh!
|
||||||
|
end
|
||||||
|
rescue Octokit::NotFound
|
||||||
|
exit_with_error "Build status could not be found"
|
||||||
|
end
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
class Mrsk::Cli::Traefik < Mrsk::Cli::Base
|
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
|
mutating do
|
||||||
on(MRSK.traefik_hosts) do
|
on(MRSK.traefik_hosts) do
|
||||||
execute *MRSK.registry.login
|
execute *MRSK.registry.login
|
||||||
execute *MRSK.traefik.run, raise_on_non_zero_exit: false
|
execute *MRSK.traefik.run, raise_on_non_zero_exit: false
|
||||||
@@ -10,17 +10,22 @@ class Mrsk::Cli::Traefik < Mrsk::Cli::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
desc "reboot", "Reboot Traefik on servers (stop container, remove container, start new container)"
|
desc "reboot", "Reboot Traefik on servers (stop container, remove container, start new container)"
|
||||||
|
option :rolling, type: :boolean, default: false, desc: "Reboot traefik on hosts in sequence, rather than in parallel"
|
||||||
def reboot
|
def reboot
|
||||||
with_lock do
|
mutating do
|
||||||
stop
|
on(MRSK.traefik_hosts, in: options[:rolling] ? :sequence : :parallel) do
|
||||||
remove_container
|
execute *MRSK.auditor.record("Rebooted traefik"), verbosity: :debug
|
||||||
boot
|
execute *MRSK.registry.login
|
||||||
|
execute *MRSK.traefik.stop, raise_on_non_zero_exit: false
|
||||||
|
execute *MRSK.traefik.remove_container
|
||||||
|
execute *MRSK.traefik.run, raise_on_non_zero_exit: false
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
desc "start", "Start existing Traefik container on servers"
|
desc "start", "Start existing Traefik container on servers"
|
||||||
def start
|
def start
|
||||||
with_lock do
|
mutating do
|
||||||
on(MRSK.traefik_hosts) do
|
on(MRSK.traefik_hosts) do
|
||||||
execute *MRSK.auditor.record("Started traefik"), verbosity: :debug
|
execute *MRSK.auditor.record("Started traefik"), verbosity: :debug
|
||||||
execute *MRSK.traefik.start, raise_on_non_zero_exit: false
|
execute *MRSK.traefik.start, raise_on_non_zero_exit: false
|
||||||
@@ -30,7 +35,7 @@ class Mrsk::Cli::Traefik < Mrsk::Cli::Base
|
|||||||
|
|
||||||
desc "stop", "Stop existing Traefik container on servers"
|
desc "stop", "Stop existing Traefik container on servers"
|
||||||
def stop
|
def stop
|
||||||
with_lock do
|
mutating do
|
||||||
on(MRSK.traefik_hosts) do
|
on(MRSK.traefik_hosts) do
|
||||||
execute *MRSK.auditor.record("Stopped traefik"), verbosity: :debug
|
execute *MRSK.auditor.record("Stopped traefik"), verbosity: :debug
|
||||||
execute *MRSK.traefik.stop, raise_on_non_zero_exit: false
|
execute *MRSK.traefik.stop, raise_on_non_zero_exit: false
|
||||||
@@ -40,7 +45,7 @@ class Mrsk::Cli::Traefik < Mrsk::Cli::Base
|
|||||||
|
|
||||||
desc "restart", "Restart existing Traefik container on servers"
|
desc "restart", "Restart existing Traefik container on servers"
|
||||||
def restart
|
def restart
|
||||||
with_lock do
|
mutating do
|
||||||
stop
|
stop
|
||||||
start
|
start
|
||||||
end
|
end
|
||||||
@@ -77,7 +82,7 @@ class Mrsk::Cli::Traefik < Mrsk::Cli::Base
|
|||||||
|
|
||||||
desc "remove", "Remove Traefik container and image from servers"
|
desc "remove", "Remove Traefik container and image from servers"
|
||||||
def remove
|
def remove
|
||||||
with_lock do
|
mutating do
|
||||||
stop
|
stop
|
||||||
remove_container
|
remove_container
|
||||||
remove_image
|
remove_image
|
||||||
@@ -86,7 +91,7 @@ class Mrsk::Cli::Traefik < Mrsk::Cli::Base
|
|||||||
|
|
||||||
desc "remove_container", "Remove Traefik container from servers", hide: true
|
desc "remove_container", "Remove Traefik container from servers", hide: true
|
||||||
def remove_container
|
def remove_container
|
||||||
with_lock do
|
mutating do
|
||||||
on(MRSK.traefik_hosts) do
|
on(MRSK.traefik_hosts) do
|
||||||
execute *MRSK.auditor.record("Removed traefik container"), verbosity: :debug
|
execute *MRSK.auditor.record("Removed traefik container"), verbosity: :debug
|
||||||
execute *MRSK.traefik.remove_container
|
execute *MRSK.traefik.remove_container
|
||||||
@@ -96,7 +101,7 @@ class Mrsk::Cli::Traefik < Mrsk::Cli::Base
|
|||||||
|
|
||||||
desc "remove_image", "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
|
mutating do
|
||||||
on(MRSK.traefik_hosts) do
|
on(MRSK.traefik_hosts) do
|
||||||
execute *MRSK.auditor.record("Removed traefik image"), verbosity: :debug
|
execute *MRSK.auditor.record("Removed traefik image"), verbosity: :debug
|
||||||
execute *MRSK.traefik.remove_image
|
execute *MRSK.traefik.remove_image
|
||||||
|
|||||||
@@ -84,8 +84,8 @@ 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
|
||||||
@@ -100,6 +100,14 @@ class Mrsk::Commander
|
|||||||
@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
|
||||||
@@ -112,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
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
class Mrsk::Commands::App < Mrsk::Commands::Base
|
class Mrsk::Commands::App < Mrsk::Commands::Base
|
||||||
|
ACTIVE_DOCKER_STATUSES = [ :running, :restarting ]
|
||||||
|
|
||||||
attr_reader :role
|
attr_reader :role
|
||||||
|
|
||||||
def initialize(config, role: nil)
|
def initialize(config, role: nil)
|
||||||
@@ -6,13 +8,18 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
|
|||||||
@role = role
|
@role = role
|
||||||
end
|
end
|
||||||
|
|
||||||
def run
|
def start_or_run(hostname: nil)
|
||||||
|
combine start, run(hostname: hostname), by: "||"
|
||||||
|
end
|
||||||
|
|
||||||
|
def run(hostname: nil)
|
||||||
role = config.role(self.role)
|
role = config.role(self.role)
|
||||||
|
|
||||||
docker :run,
|
docker :run,
|
||||||
"--detach",
|
"--detach",
|
||||||
"--restart unless-stopped",
|
"--restart unless-stopped",
|
||||||
"--name", container_name,
|
"--name", container_name,
|
||||||
|
*(["--hostname", hostname] if hostname),
|
||||||
"-e", "MRSK_CONTAINER_NAME=\"#{container_name}\"",
|
"-e", "MRSK_CONTAINER_NAME=\"#{container_name}\"",
|
||||||
*role.env_args,
|
*role.env_args,
|
||||||
*role.health_check_args,
|
*role.health_check_args,
|
||||||
@@ -88,20 +95,20 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
|
|||||||
|
|
||||||
|
|
||||||
def current_running_container_id
|
def current_running_container_id
|
||||||
docker :ps, "--quiet", *filter_args(status: :running), "--latest"
|
docker :ps, "--quiet", *filter_args(statuses: ACTIVE_DOCKER_STATUSES), "--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
|
||||||
list_versions("--latest", status: :running)
|
list_versions("--latest", statuses: ACTIVE_DOCKER_STATUSES)
|
||||||
end
|
end
|
||||||
|
|
||||||
def list_versions(*docker_args, status: nil)
|
def list_versions(*docker_args, statuses: nil)
|
||||||
pipe \
|
pipe \
|
||||||
docker(:ps, *filter_args(status: status), *docker_args, "--format", '"{{.Names}}"'),
|
docker(:ps, *filter_args(statuses: statuses), *docker_args, "--format", '"{{.Names}}"'),
|
||||||
%(grep -oE "\\-[^-]+$"), # Extract SHA from "service-role-dest-SHA"
|
%(grep -oE "\\-[^-]+$"), # Extract SHA from "service-role-dest-SHA"
|
||||||
%(cut -c 2-)
|
%(cut -c 2-)
|
||||||
end
|
end
|
||||||
@@ -146,15 +153,17 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
|
|||||||
[ 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(status: nil)
|
def filter_args(statuses: nil)
|
||||||
argumentize "--filter", filters(status: status)
|
argumentize "--filter", filters(statuses: statuses)
|
||||||
end
|
end
|
||||||
|
|
||||||
def filters(status: nil)
|
def filters(statuses: 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
|
statuses&.each do |status|
|
||||||
|
filters << "status=#{status}"
|
||||||
|
end
|
||||||
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
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ module Mrsk::Commands
|
|||||||
delegate :sensitive, :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_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
|
||||||
|
|
||||||
@@ -12,13 +13,17 @@ module Mrsk::Commands
|
|||||||
|
|
||||||
def run_over_ssh(*command, host:)
|
def run_over_ssh(*command, host:)
|
||||||
"ssh".tap do |cmd|
|
"ssh".tap do |cmd|
|
||||||
cmd << " -J #{config.ssh_proxy.jump_proxies}" if config.ssh_proxy
|
if config.ssh_proxy && config.ssh_proxy.is_a?(Net::SSH::Proxy::Jump)
|
||||||
|
cmd << " -J #{config.ssh_proxy.jump_proxies}"
|
||||||
|
elsif config.ssh_proxy && config.ssh_proxy.is_a?(Net::SSH::Proxy::Command)
|
||||||
|
cmd << " -o ProxyCommand='#{config.ssh_proxy.command_line_template}'"
|
||||||
|
end
|
||||||
cmd << " -t #{config.ssh_user}@#{host} '#{command.join(" ")}'"
|
cmd << " -t #{config.ssh_user}@#{host} '#{command.join(" ")}'"
|
||||||
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
|
||||||
@@ -52,5 +57,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
|
||||||
|
|||||||
@@ -7,11 +7,13 @@ class Mrsk::Commands::Builder < Mrsk::Commands::Base
|
|||||||
|
|
||||||
def target
|
def target
|
||||||
case
|
case
|
||||||
when config.builder && config.builder["multiarch"] == false
|
when !config.builder.multiarch? && !config.builder.cached?
|
||||||
native
|
native
|
||||||
when config.builder && config.builder["local"] && config.builder["remote"]
|
when !config.builder.multiarch? && config.builder.cached?
|
||||||
|
native_cached
|
||||||
|
when config.builder.local? && config.builder.remote?
|
||||||
multiarch_remote
|
multiarch_remote
|
||||||
when config.builder && config.builder["remote"]
|
when config.builder.remote?
|
||||||
native_remote
|
native_remote
|
||||||
else
|
else
|
||||||
multiarch
|
multiarch
|
||||||
@@ -22,6 +24,10 @@ class Mrsk::Commands::Builder < Mrsk::Commands::Base
|
|||||||
@native ||= Mrsk::Commands::Builder::Native.new(config)
|
@native ||= Mrsk::Commands::Builder::Native.new(config)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def native_cached
|
||||||
|
@native ||= Mrsk::Commands::Builder::Native::Cached.new(config)
|
||||||
|
end
|
||||||
|
|
||||||
def native_remote
|
def native_remote
|
||||||
@native ||= Mrsk::Commands::Builder::Native::Remote.new(config)
|
@native ||= Mrsk::Commands::Builder::Native::Remote.new(config)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ class Mrsk::Commands::Builder::Base < Mrsk::Commands::Base
|
|||||||
class BuilderError < StandardError; end
|
class BuilderError < StandardError; end
|
||||||
|
|
||||||
delegate :argumentize, to: Mrsk::Utils
|
delegate :argumentize, to: Mrsk::Utils
|
||||||
|
delegate :args, :secrets, :dockerfile, :local_arch, :local_host, :remote_arch, :remote_host, :cache_from, :cache_to, to: :builder_config
|
||||||
|
|
||||||
def clean
|
def clean
|
||||||
docker :image, :rm, "--force", config.absolute_image
|
docker :image, :rm, "--force", config.absolute_image
|
||||||
@@ -13,11 +14,11 @@ class Mrsk::Commands::Builder::Base < Mrsk::Commands::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def build_options
|
def build_options
|
||||||
[ *build_tags, *build_labels, *build_args, *build_secrets, *build_dockerfile ]
|
[ *build_tags, *build_cache, *build_labels, *build_args, *build_secrets, *build_dockerfile ]
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_context
|
def build_context
|
||||||
context
|
config.builder.context
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
@@ -26,6 +27,13 @@ class Mrsk::Commands::Builder::Base < Mrsk::Commands::Base
|
|||||||
[ "-t", config.absolute_image, "-t", config.latest_image ]
|
[ "-t", config.absolute_image, "-t", config.latest_image ]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def build_cache
|
||||||
|
if cache_to && cache_from
|
||||||
|
["--cache-to", cache_to,
|
||||||
|
"--cache-from", cache_from]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def build_labels
|
def build_labels
|
||||||
argumentize "--label", { service: config.service }
|
argumentize "--label", { service: config.service }
|
||||||
end
|
end
|
||||||
@@ -46,19 +54,7 @@ class Mrsk::Commands::Builder::Base < Mrsk::Commands::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def args
|
def builder_config
|
||||||
(config.builder && config.builder["args"]) || {}
|
config.builder
|
||||||
end
|
|
||||||
|
|
||||||
def secrets
|
|
||||||
(config.builder && config.builder["secrets"]) || []
|
|
||||||
end
|
|
||||||
|
|
||||||
def dockerfile
|
|
||||||
(config.builder && config.builder["dockerfile"]) || "Dockerfile"
|
|
||||||
end
|
|
||||||
|
|
||||||
def context
|
|
||||||
(config.builder && config.builder["context"]) || "."
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -22,17 +22,17 @@ class Mrsk::Commands::Builder::Multiarch::Remote < Mrsk::Commands::Builder::Mult
|
|||||||
end
|
end
|
||||||
|
|
||||||
def create_local_buildx
|
def create_local_buildx
|
||||||
docker :buildx, :create, "--name", builder_name, builder_name_with_arch(local["arch"]), "--platform", "linux/#{local["arch"]}"
|
docker :buildx, :create, "--name", builder_name, builder_name_with_arch(local_arch), "--platform", "linux/#{local_arch}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def append_remote_buildx
|
def append_remote_buildx
|
||||||
docker :buildx, :create, "--append", "--name", builder_name, builder_name_with_arch(remote["arch"]), "--platform", "linux/#{remote["arch"]}"
|
docker :buildx, :create, "--append", "--name", builder_name, builder_name_with_arch(remote_arch), "--platform", "linux/#{remote_arch}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_contexts
|
def create_contexts
|
||||||
combine \
|
combine \
|
||||||
create_context(local["arch"], local["host"]),
|
create_context(local_arch, local_host),
|
||||||
create_context(remote["arch"], remote["host"])
|
create_context(remote_arch, remote_host)
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_context(arch, host)
|
def create_context(arch, host)
|
||||||
@@ -41,19 +41,11 @@ class Mrsk::Commands::Builder::Multiarch::Remote < Mrsk::Commands::Builder::Mult
|
|||||||
|
|
||||||
def remove_contexts
|
def remove_contexts
|
||||||
combine \
|
combine \
|
||||||
remove_context(local["arch"]),
|
remove_context(local_arch),
|
||||||
remove_context(remote["arch"])
|
remove_context(remote_arch)
|
||||||
end
|
end
|
||||||
|
|
||||||
def remove_context(arch)
|
def remove_context(arch)
|
||||||
docker :context, :rm, builder_name_with_arch(arch)
|
docker :context, :rm, builder_name_with_arch(arch)
|
||||||
end
|
end
|
||||||
|
|
||||||
def local
|
|
||||||
config.builder["local"]
|
|
||||||
end
|
|
||||||
|
|
||||||
def remote
|
|
||||||
config.builder["remote"]
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
class Mrsk::Commands::Builder::Native < Mrsk::Commands::Builder::Base
|
class Mrsk::Commands::Builder::Native < Mrsk::Commands::Builder::Base
|
||||||
def create
|
def create
|
||||||
# No-op on native
|
# No-op on native without cache
|
||||||
end
|
end
|
||||||
|
|
||||||
def remove
|
def remove
|
||||||
# No-op on native
|
# No-op on native without cache
|
||||||
end
|
end
|
||||||
|
|
||||||
def push
|
def push
|
||||||
|
|||||||
16
lib/mrsk/commands/builder/native/cached.rb
Normal file
16
lib/mrsk/commands/builder/native/cached.rb
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
class Mrsk::Commands::Builder::Native::Cached < Mrsk::Commands::Builder::Native
|
||||||
|
def create
|
||||||
|
docker :buildx, :create, "--use", "--driver=docker-container"
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove
|
||||||
|
docker :buildx, :rm, builder_name
|
||||||
|
end
|
||||||
|
|
||||||
|
def push
|
||||||
|
docker :buildx, :build,
|
||||||
|
"--push",
|
||||||
|
*build_options,
|
||||||
|
build_context
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -28,29 +28,21 @@ class Mrsk::Commands::Builder::Native::Remote < Mrsk::Commands::Builder::Native
|
|||||||
|
|
||||||
|
|
||||||
private
|
private
|
||||||
def arch
|
|
||||||
config.builder["remote"]["arch"]
|
|
||||||
end
|
|
||||||
|
|
||||||
def host
|
|
||||||
config.builder["remote"]["host"]
|
|
||||||
end
|
|
||||||
|
|
||||||
def builder_name
|
def builder_name
|
||||||
"mrsk-#{config.service}-native-remote"
|
"mrsk-#{config.service}-native-remote"
|
||||||
end
|
end
|
||||||
|
|
||||||
def builder_name_with_arch
|
def builder_name_with_arch
|
||||||
"#{builder_name}-#{arch}"
|
"#{builder_name}-#{remote_arch}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def platform
|
def platform
|
||||||
"linux/#{arch}"
|
"linux/#{remote_arch}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_context
|
def create_context
|
||||||
docker :context, :create,
|
docker :context, :create,
|
||||||
builder_name_with_arch, "--description", "'#{builder_name} #{arch} native host'", "--docker", "'host=#{host}'"
|
builder_name_with_arch, "--description", "'#{builder_name} #{remote_arch} native host'", "--docker", "'host=#{remote_host}'"
|
||||||
end
|
end
|
||||||
|
|
||||||
def remove_context
|
def remove_context
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ class Mrsk::Commands::Healthcheck < Mrsk::Commands::Base
|
|||||||
pipe container_id, xargs(docker(:inspect, "--format", DOCKER_HEALTH_STATUS_FORMAT))
|
pipe container_id, xargs(docker(:inspect, "--format", DOCKER_HEALTH_STATUS_FORMAT))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def container_health_log
|
||||||
|
pipe container_id, xargs(docker(:inspect, "--format", DOCKER_HEALTH_LOG_FORMAT))
|
||||||
|
end
|
||||||
|
|
||||||
def logs
|
def logs
|
||||||
pipe container_id, xargs(docker(:logs, "--tail", 50, "2>&1"))
|
pipe container_id, xargs(docker(:logs, "--tail", 50, "2>&1"))
|
||||||
end
|
end
|
||||||
|
|||||||
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)
|
||||||
@@ -40,7 +40,7 @@ class Mrsk::Commands::Lock < Mrsk::Commands::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def lock_dir
|
def lock_dir
|
||||||
:mrsk_lock
|
"mrsk_lock-#{config.service}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def lock_details_file
|
def lock_details_file
|
||||||
@@ -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,13 +2,20 @@ 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
|
def dangling_images
|
||||||
docker :image, :prune, "--all", "--force", "--filter", "label=service=#{config.service}", "--filter", "dangling=true"
|
docker :image, :prune, "--force", "--filter", "label=service=#{config.service}", "--filter", "dangling=true"
|
||||||
|
end
|
||||||
|
|
||||||
|
def tagged_images
|
||||||
|
pipe \
|
||||||
|
docker(:image, :ls, *service_filter, "--format", "'{{.ID}} {{.Repository}}:{{.Tag}}'"),
|
||||||
|
"grep -v -w \"#{active_image_list}\"",
|
||||||
|
"while read image tag; do docker rmi $tag; done"
|
||||||
end
|
end
|
||||||
|
|
||||||
def containers(keep_last: 5)
|
def containers(keep_last: 5)
|
||||||
pipe \
|
pipe \
|
||||||
docker(:ps, "-q", "-a", "--filter", "label=service=#{config.service}", *stopped_containers_filters),
|
docker(:ps, "-q", "-a", *service_filter, *stopped_containers_filters),
|
||||||
"tail -n +#{keep_last + 1}",
|
"tail -n +#{keep_last + 1}",
|
||||||
"while read container_id; do docker rm $container_id; done"
|
"while read container_id; do docker rm $container_id; done"
|
||||||
end
|
end
|
||||||
@@ -17,4 +24,15 @@ class Mrsk::Commands::Prune < Mrsk::Commands::Base
|
|||||||
def stopped_containers_filters
|
def stopped_containers_filters
|
||||||
[ "created", "exited", "dead" ].flat_map { |status| ["--filter", "status=#{status}"] }
|
[ "created", "exited", "dead" ].flat_map { |status| ["--filter", "status=#{status}"] }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def active_image_list
|
||||||
|
# Pull the images that are used by any containers
|
||||||
|
# Append repo:latest - to avoid deleting the latest tag
|
||||||
|
# Append repo:<none> - to avoid deleting dangling images that are in use. Unused dangling images are deleted separately
|
||||||
|
"$(docker container ls -a --format '{{.Image}}\\|' --filter label=service=#{config.service} | tr -d '\\n')#{config.latest_image}\\|#{config.repository}:<none>"
|
||||||
|
end
|
||||||
|
|
||||||
|
def service_filter
|
||||||
|
[ "--filter", "label=service=#{config.service}" ]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
class Mrsk::Commands::Traefik < Mrsk::Commands::Base
|
class Mrsk::Commands::Traefik < Mrsk::Commands::Base
|
||||||
delegate :argumentize, :optionize, to: Mrsk::Utils
|
delegate :argumentize, :argumentize_env_with_secrets, :optionize, to: Mrsk::Utils
|
||||||
|
|
||||||
DEFAULT_IMAGE = "traefik:v2.9"
|
DEFAULT_IMAGE = "traefik:v2.9"
|
||||||
CONTAINER_PORT = 80
|
CONTAINER_PORT = 80
|
||||||
|
DEFAULT_ARGS = {
|
||||||
|
'log.level' => 'DEBUG'
|
||||||
|
}
|
||||||
|
|
||||||
def run
|
def run
|
||||||
docker :run, "--name traefik",
|
docker :run, "--name traefik",
|
||||||
@@ -10,12 +13,12 @@ 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,
|
*label_args,
|
||||||
*docker_options_args,
|
*docker_options_args,
|
||||||
image,
|
image,
|
||||||
"--providers.docker",
|
"--providers.docker",
|
||||||
"--log.level=DEBUG",
|
|
||||||
*cmd_option_args
|
*cmd_option_args
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -61,6 +64,16 @@ class Mrsk::Commands::Traefik < Mrsk::Commands::Base
|
|||||||
argumentize "--label", labels
|
argumentize "--label", labels
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def env_args
|
||||||
|
env_config = config.traefik["env"] || {}
|
||||||
|
|
||||||
|
if env_config.present?
|
||||||
|
argumentize_env_with_secrets(env_config)
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def labels
|
def labels
|
||||||
config.traefik["labels"] || []
|
config.traefik["labels"] || []
|
||||||
end
|
end
|
||||||
@@ -75,9 +88,9 @@ class Mrsk::Commands::Traefik < Mrsk::Commands::Base
|
|||||||
|
|
||||||
def cmd_option_args
|
def cmd_option_args
|
||||||
if args = config.traefik["args"]
|
if args = config.traefik["args"]
|
||||||
optionize args, with: "="
|
optionize DEFAULT_ARGS.merge(args), with: "="
|
||||||
else
|
else
|
||||||
[]
|
optionize DEFAULT_ARGS, with: "="
|
||||||
end
|
end
|
||||||
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, :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
|
||||||
@@ -157,10 +157,6 @@ class Mrsk::Configuration
|
|||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
def audit_broadcast_cmd
|
|
||||||
raw_config.audit_broadcast_cmd
|
|
||||||
end
|
|
||||||
|
|
||||||
def healthcheck
|
def healthcheck
|
||||||
{ "path" => "/up", "port" => 3000, "max_attempts" => 7 }.merge(raw_config.healthcheck || {})
|
{ "path" => "/up", "port" => 3000, "max_attempts" => 7 }.merge(raw_config.healthcheck || {})
|
||||||
end
|
end
|
||||||
@@ -169,8 +165,12 @@ class Mrsk::Configuration
|
|||||||
raw_config.readiness_delay || 7
|
raw_config.readiness_delay || 7
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def minimum_version
|
||||||
|
raw_config.minimum_version
|
||||||
|
end
|
||||||
|
|
||||||
def valid?
|
def valid?
|
||||||
ensure_required_keys_present && ensure_env_available
|
ensure_required_keys_present && ensure_valid_mrsk_version
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
@@ -186,7 +186,7 @@ class Mrsk::Configuration
|
|||||||
env_args: env_args,
|
env_args: env_args,
|
||||||
volume_args: volume_args,
|
volume_args: volume_args,
|
||||||
ssh_options: ssh_options,
|
ssh_options: ssh_options,
|
||||||
builder: raw_config.builder,
|
builder: builder.to_h,
|
||||||
accessories: raw_config.accessories,
|
accessories: raw_config.accessories,
|
||||||
logging: logging_args,
|
logging: logging_args,
|
||||||
healthcheck: healthcheck
|
healthcheck: healthcheck
|
||||||
@@ -197,6 +197,22 @@ class Mrsk::Configuration
|
|||||||
raw_config.traefik || {}
|
raw_config.traefik || {}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def hooks_path
|
||||||
|
raw_config.hooks_path || ".mrsk/hooks"
|
||||||
|
end
|
||||||
|
|
||||||
|
def builder
|
||||||
|
Mrsk::Configuration::Builder.new(config: self)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Will raise KeyError if any secret ENVs are missing
|
||||||
|
def ensure_env_available
|
||||||
|
env_args
|
||||||
|
roles.each(&:env_args)
|
||||||
|
|
||||||
|
true
|
||||||
|
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
|
||||||
@@ -221,22 +237,25 @@ class Mrsk::Configuration
|
|||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
# Will raise KeyError if any secret ENVs are missing
|
def ensure_valid_mrsk_version
|
||||||
def ensure_env_available
|
if minimum_version && Gem::Version.new(minimum_version) > Gem::Version.new(Mrsk::VERSION)
|
||||||
env_args
|
raise ArgumentError, "Current version is #{Mrsk::VERSION}, minimum required is #{minimum_version}"
|
||||||
roles.each(&:env_args)
|
end
|
||||||
|
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
def role_names
|
def role_names
|
||||||
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
|
||||||
|
|||||||
114
lib/mrsk/configuration/builder.rb
Normal file
114
lib/mrsk/configuration/builder.rb
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
class Mrsk::Configuration::Builder
|
||||||
|
def initialize(config:)
|
||||||
|
@options = config.raw_config.builder || {}
|
||||||
|
@image = config.image
|
||||||
|
@server = config.registry["server"]
|
||||||
|
|
||||||
|
valid?
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_h
|
||||||
|
@options
|
||||||
|
end
|
||||||
|
|
||||||
|
def multiarch?
|
||||||
|
@options["multiarch"] != false
|
||||||
|
end
|
||||||
|
|
||||||
|
def local?
|
||||||
|
!!@options["local"]
|
||||||
|
end
|
||||||
|
|
||||||
|
def remote?
|
||||||
|
!!@options["remote"]
|
||||||
|
end
|
||||||
|
|
||||||
|
def cached?
|
||||||
|
!!@options["cache"]
|
||||||
|
end
|
||||||
|
|
||||||
|
def args
|
||||||
|
@options["args"] || {}
|
||||||
|
end
|
||||||
|
|
||||||
|
def secrets
|
||||||
|
@options["secrets"] || []
|
||||||
|
end
|
||||||
|
|
||||||
|
def dockerfile
|
||||||
|
@options["dockerfile"] || "Dockerfile"
|
||||||
|
end
|
||||||
|
|
||||||
|
def context
|
||||||
|
@options["context"] || "."
|
||||||
|
end
|
||||||
|
|
||||||
|
def local_arch
|
||||||
|
@options["local"]["arch"] if local?
|
||||||
|
end
|
||||||
|
|
||||||
|
def local_host
|
||||||
|
@options["local"]["host"] if local?
|
||||||
|
end
|
||||||
|
|
||||||
|
def remote_arch
|
||||||
|
@options["remote"]["arch"] if remote?
|
||||||
|
end
|
||||||
|
|
||||||
|
def remote_host
|
||||||
|
@options["remote"]["host"] if remote?
|
||||||
|
end
|
||||||
|
|
||||||
|
def cache_from
|
||||||
|
if cached?
|
||||||
|
case @options["cache"]["type"]
|
||||||
|
when "gha"
|
||||||
|
cache_from_config_for_gha
|
||||||
|
when "registry"
|
||||||
|
cache_from_config_for_registry
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def cache_to
|
||||||
|
if cached?
|
||||||
|
case @options["cache"]["type"]
|
||||||
|
when "gha"
|
||||||
|
cache_to_config_for_gha
|
||||||
|
when "registry"
|
||||||
|
cache_to_config_for_registry
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def valid?
|
||||||
|
if @options["local"] && !@options["remote"]
|
||||||
|
raise ArgumentError, "You must specify both local and remote builder config for remote multiarch builds"
|
||||||
|
end
|
||||||
|
|
||||||
|
if @options["cache"] && @options["cache"]["type"]
|
||||||
|
raise ArgumentError, "Invalid cache type: #{@options["cache"]["type"]}" unless ["gha", "registry"].include?(@options["cache"]["type"])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def cache_image
|
||||||
|
@options["cache"]&.fetch("image", nil) || "#{@image}-build-cache"
|
||||||
|
end
|
||||||
|
|
||||||
|
def cache_from_config_for_gha
|
||||||
|
"type=gha"
|
||||||
|
end
|
||||||
|
|
||||||
|
def cache_from_config_for_registry
|
||||||
|
[ "type=registry", "ref=#{@server}/#{cache_image}" ].compact.join(",")
|
||||||
|
end
|
||||||
|
|
||||||
|
def cache_to_config_for_gha
|
||||||
|
[ "type=gha", @options["cache"]&.fetch("options", nil)].compact.join(",")
|
||||||
|
end
|
||||||
|
|
||||||
|
def cache_to_config_for_registry
|
||||||
|
[ "type=registry", @options["cache"]&.fetch("options", nil), "ref=#{@server}/#{cache_image}" ].compact.join(",")
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -37,7 +37,7 @@ class Mrsk::Configuration::Role
|
|||||||
|
|
||||||
def health_check_args
|
def health_check_args
|
||||||
if health_check_cmd.present?
|
if health_check_cmd.present?
|
||||||
optionize({ "health-cmd" => health_check_cmd, "health-interval" => "1s" })
|
optionize({ "health-cmd" => health_check_cmd, "health-interval" => health_check_interval })
|
||||||
else
|
else
|
||||||
[]
|
[]
|
||||||
end
|
end
|
||||||
@@ -50,6 +50,13 @@ class Mrsk::Configuration::Role
|
|||||||
options["cmd"] || http_health_check(port: options["port"], path: options["path"])
|
options["cmd"] || http_health_check(port: options["port"], path: options["path"])
|
||||||
end
|
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
|
||||||
|
|||||||
@@ -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, **kwargs)
|
def capture_with_info(*args, **kwargs)
|
||||||
capture(*args, **kwargs, verbosity: Logger::INFO)
|
capture(*args, **kwargs, verbosity: Logger::INFO)
|
||||||
end
|
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
|
||||||
|
|
||||||
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
|
||||||
@@ -84,6 +84,13 @@ module Mrsk::Utils
|
|||||||
|
|
||||||
# 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
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
module Mrsk
|
module Mrsk
|
||||||
VERSION = "0.12.0"
|
VERSION = "0.15.1"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -40,9 +40,10 @@ class CliAccessoryTest < CliTestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "reboot" do
|
test "reboot" do
|
||||||
|
Mrsk::Commands::Registry.any_instance.expects(:login)
|
||||||
Mrsk::Cli::Accessory.any_instance.expects(:stop).with("mysql")
|
Mrsk::Cli::Accessory.any_instance.expects(:stop).with("mysql")
|
||||||
Mrsk::Cli::Accessory.any_instance.expects(:remove_container).with("mysql")
|
Mrsk::Cli::Accessory.any_instance.expects(:remove_container).with("mysql")
|
||||||
Mrsk::Cli::Accessory.any_instance.expects(:boot).with("mysql")
|
Mrsk::Cli::Accessory.any_instance.expects(:boot).with("mysql", login: false)
|
||||||
|
|
||||||
run_command("reboot", "mysql")
|
run_command("reboot", "mysql")
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -2,15 +2,10 @@ require_relative "cli_test_case"
|
|||||||
|
|
||||||
class CliAppTest < CliTestCase
|
class CliAppTest < CliTestCase
|
||||||
test "boot" do
|
test "boot" do
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info).returns("123") # old version
|
stub_running
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
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 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 --name app-web-latest --hostname 1.1.1.1-[0-9a-f]{12} /, 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
|
||||||
end
|
end
|
||||||
@@ -19,7 +14,7 @@ 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", raise_on_non_zero_exit: false)
|
.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)
|
||||||
@@ -27,13 +22,13 @@ class CliAppTest < CliTestCase
|
|||||||
.returns("running") # health check
|
.returns("running") # health check
|
||||||
|
|
||||||
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", "--filter", "status=running", "--latest", "--format", "\"{{.Names}}\"", "|", "grep -oE \"\\-[^-]+$\"", "|", "cut -c 2-", raise_on_non_zero_exit: false)
|
.with(:docker, :ps, "--filter", "label=service=app", "--filter", "label=role=web", "--filter", "status=running", "--filter", "status=restarting", "--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 --name app-web-latest --hostname 1.1.1.1-[0-9a-f]{12} /, 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
|
||||||
ensure
|
ensure
|
||||||
@@ -50,6 +45,18 @@ class CliAppTest < CliTestCase
|
|||||||
run_command("boot", config: :with_boot_strategy)
|
run_command("boot", config: :with_boot_strategy)
|
||||||
end
|
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
|
||||||
@@ -58,7 +65,7 @@ class CliAppTest < CliTestCase
|
|||||||
|
|
||||||
test "stop" do
|
test "stop" do
|
||||||
run_command("stop").tap do |output|
|
run_command("stop").tap do |output|
|
||||||
assert_match "docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --latest | xargs docker stop", output
|
assert_match "docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | xargs docker stop", output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -91,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 --filter status=running --latest | xargs docker stop")}/, output
|
assert_match /#{Regexp.escape("docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --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
|
||||||
@@ -123,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 --filter status=running --latest --format \"{{.Names}}\" | grep -oE \"\\-[^-]+$\" | cut -c 2-", output # Get current version
|
assert_match "docker ps --filter label=service=app --filter status=running --filter status=restarting --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
|
||||||
@@ -142,28 +149,28 @@ 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 --filter status=running --latest| 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 --filter status=restarting --latest| xargs docker logs --timestamps --tail 10 2>&1'")
|
||||||
|
|
||||||
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")
|
assert_match "docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --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 --filter status=running --latest | 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 --filter status=restarting --latest | xargs docker logs --timestamps --tail 10 --follow 2>&1'")
|
||||||
|
|
||||||
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")
|
assert_match "docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --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 --filter status=running --latest --format \"{{.Names}}\" | grep -oE \"\\-[^-]+$\" | cut -c 2-", output
|
assert_match "docker ps --filter label=service=app --filter status=running --filter status=restarting --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 --filter status=running --latest --format \"{{.Names}}\" | grep -oE \"\\-[^-]+$\" | cut -c 2-", output
|
assert_match "docker ps --filter label=service=app --filter status=running --filter status=restarting --latest --format \"{{.Names}}\" | grep -oE \"\\-[^-]+$\" | cut -c 2-", output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -171,4 +178,12 @@ class CliAppTest < CliTestCase
|
|||||||
def run_command(*command, config: :with_accessories)
|
def run_command(*command, config: :with_accessories)
|
||||||
stdouted { Mrsk::Cli::App.start([*command, "-c", "test/fixtures/deploy_#{config}.yml", "--hosts", "1.1.1.1"]) }
|
stdouted { Mrsk::Cli::App.start([*command, "-c", "test/fixtures/deploy_#{config}.yml", "--hosts", "1.1.1.1"]) }
|
||||||
end
|
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
|
||||||
|
|||||||
@@ -9,17 +9,23 @@ class CliBuildTest < CliTestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "push" do
|
test "push" do
|
||||||
Mrsk::Cli::Build.any_instance.stubs(:verify_local_dependencies).returns(true)
|
Mrsk::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)
|
||||||
|
hook_variables = { version: 999, service_version: "app@999", hosts: "1.1.1.1,1.1.1.2,1.1.1.3,1.1.1.4", command: "build", subcommand: "push" }
|
||||||
|
|
||||||
run_command("push").tap do |output|
|
run_command("push").tap do |output|
|
||||||
|
assert_hook_ran "pre-build", output, **hook_variables
|
||||||
|
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
|
||||||
|
|
||||||
test "push without builder" do
|
test "push without builder" do
|
||||||
stub_locking
|
stub_locking
|
||||||
Mrsk::Cli::Build.any_instance.stubs(:verify_local_dependencies).returns(true)
|
|
||||||
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)
|
||||||
@@ -29,6 +35,24 @@ 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
|
||||||
@@ -70,32 +94,15 @@ class CliBuildTest < CliTestCase
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "verify local dependencies" do
|
|
||||||
Mrsk::Commands::Builder.any_instance.stubs(:name).returns("remote".inquiry)
|
|
||||||
|
|
||||||
run_command("verify_local_dependencies").tap do |output|
|
|
||||||
assert_match /docker --version && docker buildx version/, output
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
test "verify local dependencies with no buildx plugin" do
|
|
||||||
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("verify_local_dependencies") }
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
def run_command(*command)
|
def run_command(*command)
|
||||||
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,8 +1,6 @@
|
|||||||
require "test_helper"
|
require "test_helper"
|
||||||
|
|
||||||
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"
|
||||||
@@ -16,4 +14,43 @@ class CliTestCase < ActiveSupport::TestCase
|
|||||||
ENV.delete("MYSQL_ROOT_PASSWORD")
|
ENV.delete("MYSQL_ROOT_PASSWORD")
|
||||||
ENV.delete("VERSION")
|
ENV.delete("VERSION")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def fail_hook(hook)
|
||||||
|
@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 stub_locking
|
||||||
|
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||||
|
.with { |arg1, arg2| arg1 == :mkdir && arg2 == "mrsk_lock-app" }
|
||||||
|
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||||
|
.with { |arg1, arg2| arg1 == :rm && arg2 == "mrsk_lock-app/details" }
|
||||||
|
end
|
||||||
|
|
||||||
|
def assert_hook_ran(hook, output, version:, service_version:, hosts:, command:, subcommand: nil, runtime: nil)
|
||||||
|
performer = `whoami`.strip
|
||||||
|
|
||||||
|
assert_match "Running the #{hook} hook...\n", output
|
||||||
|
|
||||||
|
expected = %r{Running\s/usr/bin/env\s\.mrsk/hooks/#{hook}\sas\s#{performer}@localhost\n\s
|
||||||
|
DEBUG\s\[[0-9a-f]*\]\sCommand:\s\(\sexport\s
|
||||||
|
MRSK_RECORDED_AT=\"\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\dZ\"\s
|
||||||
|
MRSK_PERFORMER=\"#{performer}\"\s
|
||||||
|
MRSK_VERSION=\"#{version}\"\s
|
||||||
|
MRSK_SERVICE_VERSION=\"#{service_version}\"\s
|
||||||
|
MRSK_HOSTS=\"#{hosts}\"\s
|
||||||
|
MRSK_COMMAND=\"#{command}\"\s
|
||||||
|
#{"MRSK_SUBCOMMAND=\\\"#{subcommand}\\\"\\s" if subcommand}
|
||||||
|
#{"MRSK_RUNTIME=\\\"#{runtime}\\\"\\s" if runtime}
|
||||||
|
;\s/usr/bin/env\s\.mrsk/hooks/#{hook} }x
|
||||||
|
|
||||||
|
assert_match expected, output
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -53,6 +53,11 @@ class CliHealthcheckTest < CliTestCase
|
|||||||
.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")
|
.returns("some log output")
|
||||||
|
|
||||||
|
# Capture container health log when failing
|
||||||
|
SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_pretty_json)
|
||||||
|
.with(:docker, :container, :ls, "--all", "--filter", "name=^healthcheck-app-999$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{json .State.Health}}'")
|
||||||
|
.returns('{"Status":"unhealthy","Log":[{"ExitCode": 1,"Output": "/bin/sh: 1: curl: not found\n"}]}"')
|
||||||
|
|
||||||
exception = assert_raises do
|
exception = assert_raises do
|
||||||
run_command("perform")
|
run_command("perform")
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ 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: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)
|
||||||
@@ -20,18 +20,24 @@ class CliMainTest < CliTestCase
|
|||||||
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)
|
||||||
|
hook_variables = { version: 999, service_version: "app@999", hosts: "1.1.1.1,1.1.1.2", command: "deploy" }
|
||||||
|
|
||||||
run_command("deploy").tap do |output|
|
run_command("deploy").tap do |output|
|
||||||
|
assert_hook_ran "pre-connect", output, **hook_variables
|
||||||
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_hook_ran "pre-deploy", output, **hook_variables
|
||||||
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 /Detect stale containers/, output
|
||||||
assert_match /Prune old containers and images/, output
|
assert_match /Prune old containers and images/, output
|
||||||
|
assert_hook_ran "post-deploy", output, **hook_variables, runtime: 0
|
||||||
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: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)
|
||||||
@@ -57,11 +63,11 @@ class CliMainTest < CliTestCase
|
|||||||
Thread.report_on_exception = false
|
Thread.report_on_exception = false
|
||||||
|
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||||
.with { |*arg| arg[0..1] == [:mkdir, :mrsk_lock] }
|
.with { |*arg| arg[0..1] == [:mkdir, 'mrsk_lock-app'] }
|
||||||
.raises(RuntimeError, "mkdir: cannot create directory ‘mrsk_lock’: File exists")
|
.raises(RuntimeError, "mkdir: cannot create directory ‘mrsk_lock-app’: File exists")
|
||||||
|
|
||||||
SSHKit::Backend::Abstract.any_instance.expects(:execute)
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_debug)
|
||||||
.with(:stat, :mrsk_lock, ">", "/dev/null", "&&", :cat, "mrsk_lock/details", "|", :base64, "-d")
|
.with(:stat, 'mrsk_lock-app', ">", "/dev/null", "&&", :cat, "mrsk_lock-app/details", "|", :base64, "-d")
|
||||||
|
|
||||||
assert_raises(Mrsk::Cli::LockError) do
|
assert_raises(Mrsk::Cli::LockError) do
|
||||||
run_command("deploy")
|
run_command("deploy")
|
||||||
@@ -72,7 +78,7 @@ class CliMainTest < CliTestCase
|
|||||||
Thread.report_on_exception = false
|
Thread.report_on_exception = false
|
||||||
|
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||||
.with { |*arg| arg[0..1] == [:mkdir, :mrsk_lock] }
|
.with { |*arg| arg[0..1] == [:mkdir, 'mrsk_lock-app'] }
|
||||||
.raises(SocketError, "getaddrinfo: nodename nor servname provided, or not known")
|
.raises(SocketError, "getaddrinfo: nodename nor servname provided, or not known")
|
||||||
|
|
||||||
assert_raises(SSHKit::Runner::ExecuteError) do
|
assert_raises(SSHKit::Runner::ExecuteError) do
|
||||||
@@ -80,25 +86,8 @@ class CliMainTest < CliTestCase
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "deploy errors during critical section leave lock in place" do
|
|
||||||
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "skip_broadcast" => false, "version" => "999" }
|
|
||||||
|
|
||||||
Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli: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:app:stale_containers", [], 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:boot", [], invoke_options).raises(RuntimeError)
|
|
||||||
|
|
||||||
assert !MRSK.holding_lock?
|
|
||||||
assert_raises(RuntimeError) do
|
|
||||||
stderred { run_command("deploy") }
|
|
||||||
end
|
|
||||||
assert MRSK.holding_lock?
|
|
||||||
end
|
|
||||||
|
|
||||||
test "deploy errors during outside section leave remove lock" do
|
test "deploy errors during outside section leave remove lock" 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)
|
Mrsk::Cli::Main.any_instance.expects(:invoke)
|
||||||
.with("mrsk:cli:registry:login", [], invoke_options)
|
.with("mrsk:cli:registry:login", [], invoke_options)
|
||||||
@@ -111,22 +100,52 @@ class CliMainTest < CliTestCase
|
|||||||
assert !MRSK.holding_lock?
|
assert !MRSK.holding_lock?
|
||||||
end
|
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 "deploy with missing secrets" do
|
||||||
|
assert_raises(KeyError) do
|
||||||
|
run_command("deploy", config_file: "deploy_with_secrets")
|
||||||
|
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: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)
|
||||||
|
|
||||||
|
hook_variables = { version: 999, service_version: "app@999", hosts: "1.1.1.1,1.1.1.2", command: "redeploy" }
|
||||||
|
|
||||||
run_command("redeploy").tap do |output|
|
run_command("redeploy").tap do |output|
|
||||||
|
assert_hook_ran "pre-connect", output, **hook_variables
|
||||||
assert_match /Build and push app image/, output
|
assert_match /Build and push app image/, output
|
||||||
|
assert_hook_ran "pre-deploy", output, **hook_variables
|
||||||
|
assert_match /Running the pre-deploy hook.../, output
|
||||||
assert_match /Ensure app can pass healthcheck/, output
|
assert_match /Ensure app can pass healthcheck/, output
|
||||||
|
assert_hook_ran "post-deploy", output, **hook_variables, runtime: "0"
|
||||||
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)
|
||||||
@@ -151,35 +170,52 @@ class CliMainTest < CliTestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "rollback good version" do
|
test "rollback good version" do
|
||||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
[ "web", "workers" ].each do |role|
|
||||||
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-123$", "--quiet")
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
.returns("version-to-rollback\n").at_least_once
|
.with(:docker, :container, :ls, "--filter", "name=^app-#{role}-123$", "--quiet", raise_on_non_zero_exit: false)
|
||||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
.returns("").at_least_once
|
||||||
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-workers-123$", "--quiet")
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
.returns("version-to-rollback\n").at_least_once
|
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-#{role}-123$", "--quiet")
|
||||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
.returns("version-to-rollback\n").at_least_once
|
||||||
.with(:docker, :ps, "--filter", "label=service=app", "--filter", "label=role=web", "--filter", "status=running", "--latest", "--format", "\"{{.Names}}\"", "|", "grep -oE \"\\-[^-]+$\"", "|", "cut -c 2-")
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
.returns("version-to-rollback\n").at_least_once
|
.with(:docker, :ps, "--filter", "label=service=app", "--filter", "label=role=#{role}", "--filter", "status=running", "--filter", "status=restarting", "--latest", "--format", "\"{{.Names}}\"", "|", "grep -oE \"\\-[^-]+$\"", "|", "cut -c 2-", raise_on_non_zero_exit: false)
|
||||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
.returns("version-to-rollback\n").at_least_once
|
||||||
.with(:docker, :ps, "--filter", "label=service=app", "--filter", "label=role=workers", "--filter", "status=running", "--latest", "--format", "\"{{.Names}}\"", "|", "grep -oE \"\\-[^-]+$\"", "|", "cut -c 2-")
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
.returns("version-to-rollback\n").at_least_once
|
.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)
|
||||||
|
hook_variables = { version: 123, service_version: "app@123", hosts: "1.1.1.1,1.1.1.2,1.1.1.3,1.1.1.4", command: "rollback" }
|
||||||
|
|
||||||
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_hook_ran "pre-deploy", output, **hook_variables
|
||||||
assert_match "docker tag dhh/app:123 dhh/app:latest", output
|
assert_match "docker tag dhh/app:123 dhh/app:latest", output
|
||||||
assert_match "docker start app-web-123", output
|
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 "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_hook_ran "post-deploy", output, **hook_variables, runtime: "0"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "rollback without old version" do
|
test "rollback without old version" do
|
||||||
Mrsk::Cli::Main.any_instance.stubs(:container_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", "--filter", "label=role=web", "--filter", "status=running", "--latest", "--format", "\"{{.Names}}\"", "|", "grep -oE \"\\-[^-]+$\"", "|", "cut -c 2-").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", "--filter", "status=restarting", "--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-web-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
|
||||||
@@ -239,9 +275,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
|
||||||
@@ -250,7 +288,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
|
||||||
@@ -258,9 +296,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
|
||||||
@@ -273,9 +313,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,7 +10,8 @@ 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 dangling=true on 1.1.1.\d/, output
|
assert_match "docker image prune --force --filter label=service=app --filter dangling=true on 1.1.1.", output
|
||||||
|
assert_match "docker image ls --filter label=service=app --format '{{.ID}} {{.Repository}}:{{.Tag}}' | grep -v -w \"$(docker container ls -a --format '{{.Image}}\\|' --filter label=service=app | tr -d '\\n')dhh/app:latest\\|dhh/app:<none>\" | while read image tag; do docker rmi $tag; done on 1.1.1.", output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class CliServerTest < CliTestCase
|
|||||||
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(: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
|
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
|
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 installed without having root access and the `curl` command available. Install Docker manually: https://docs.docker.com/engine/install/" do
|
||||||
run_command("bootstrap")
|
run_command("bootstrap")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,16 +4,24 @@ class CliTraefikTest < CliTestCase
|
|||||||
test "boot" do
|
test "boot" do
|
||||||
run_command("boot").tap do |output|
|
run_command("boot").tap do |output|
|
||||||
assert_match "docker login", 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
|
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
|
||||||
|
|
||||||
test "reboot" do
|
test "reboot" do
|
||||||
Mrsk::Cli::Traefik.any_instance.expects(:stop)
|
Mrsk::Commands::Registry.any_instance.expects(:login).twice
|
||||||
Mrsk::Cli::Traefik.any_instance.expects(:remove_container)
|
|
||||||
Mrsk::Cli::Traefik.any_instance.expects(:boot)
|
|
||||||
|
|
||||||
run_command("reboot")
|
run_command("reboot").tap do |output|
|
||||||
|
assert_match "docker container stop traefik", output
|
||||||
|
assert_match "docker container prune --force --filter label=org.opencontainers.image.title=Traefik", output
|
||||||
|
assert_match "docker 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
|
||||||
|
|
||||||
|
test "reboot --rolling" do
|
||||||
|
run_command("reboot", "--rolling").tap do |output|
|
||||||
|
assert_match "Running docker container prune --force --filter label=org.opencontainers.image.title=Traefik on 1.1.1.1", output.lines[3]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "start" do
|
test "start" do
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ class CommanderTest < ActiveSupport::TestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "percentage-based group strategy" do
|
test "percentage-based group strategy" do
|
||||||
configure_with(:deploy_with_precentage_boot_strategy)
|
configure_with(:deploy_with_percentage_boot_strategy)
|
||||||
|
|
||||||
assert_equal({ in: :groups, limit: 1, wait: 2 }, @mrsk.boot_strategy)
|
assert_equal({ in: :groups, limit: 1, wait: 2 }, @mrsk.boot_strategy)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -17,6 +17,12 @@ class CommandsAppTest < ActiveSupport::TestCase
|
|||||||
new_command.run.join(" ")
|
new_command.run.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "run with hostname" do
|
||||||
|
assert_equal \
|
||||||
|
"docker run --detach --restart unless-stopped --name app-web-999 --hostname myhost -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(hostname: "myhost").join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
test "run with volumes" do
|
test "run with volumes" do
|
||||||
@config[:volumes] = ["/local/path:/container/path" ]
|
@config[:volumes] = ["/local/path:/container/path" ]
|
||||||
|
|
||||||
@@ -77,16 +83,28 @@ class CommandsAppTest < ActiveSupport::TestCase
|
|||||||
new_command.start.join(" ")
|
new_command.start.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "start_or_run" do
|
||||||
|
assert_equal \
|
||||||
|
"docker start app-web-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.start_or_run.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "start_or_run with hostname" do
|
||||||
|
assert_equal \
|
||||||
|
"docker start app-web-999 || docker run --detach --restart unless-stopped --name app-web-999 --hostname myhost -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.start_or_run(hostname: "myhost").join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
test "stop" do
|
test "stop" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --latest | xargs docker stop",
|
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --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 --filter status=running --latest | xargs docker stop -t 30",
|
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | xargs docker stop -t 30",
|
||||||
new_command.stop.join(" ")
|
new_command.stop.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -112,37 +130,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 --filter status=running --latest | xargs docker logs 2>&1",
|
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --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 --filter status=running --latest | xargs docker logs --since 5m 2>&1",
|
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --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 --filter status=running --latest | xargs docker logs --tail 100 2>&1",
|
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --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 --filter status=running --latest | xargs docker logs --since 5m --tail 100 2>&1",
|
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --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 --filter status=running --latest | xargs docker logs 2>&1 | grep 'my-id'",
|
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --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 --filter status=running --latest | xargs docker logs --since 5m 2>&1 | grep 'my-id'",
|
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --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 --filter status=running --latest | xargs docker logs --timestamps --tail 10 --follow 2>&1",
|
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --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 --filter status=running --latest | 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 --filter status=restarting --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
|
||||||
|
|
||||||
@@ -193,17 +211,21 @@ class CommandsAppTest < ActiveSupport::TestCase
|
|||||||
assert_equal "ssh -J root@2.2.2.2 -t app@1.1.1.1 'ls'", new_command.run_over_ssh("ls", host: "1.1.1.1")
|
assert_equal "ssh -J root@2.2.2.2 -t app@1.1.1.1 'ls'", new_command.run_over_ssh("ls", host: "1.1.1.1")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "run over ssh with proxy_command" do
|
||||||
|
@config[:ssh] = { "proxy_command" => "ssh -W %h:%p user@proxy-server" }
|
||||||
|
assert_equal "ssh -o ProxyCommand='ssh -W %h:%p user@proxy-server' -t root@1.1.1.1 'ls'", new_command.run_over_ssh("ls", host: "1.1.1.1")
|
||||||
|
end
|
||||||
|
|
||||||
test "current_running_container_id" do
|
test "current_running_container_id" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --latest",
|
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest",
|
||||||
new_command.current_running_container_id.join(" ")
|
new_command.current_running_container_id.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "current_running_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 --filter status=running --latest",
|
"docker ps --quiet --filter label=service=app --filter label=destination=staging --filter label=role=web --filter status=running --filter status=restarting --latest",
|
||||||
new_command.current_running_container_id.join(" ")
|
new_command.current_running_container_id.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -215,7 +237,7 @@ 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 --filter status=running --latest --format \"{{.Names}}\" | grep -oE \"\\-[^-]+$\" | cut -c 2-",
|
"docker ps --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest --format \"{{.Names}}\" | grep -oE \"\\-[^-]+$\" | cut -c 2-",
|
||||||
new_command.current_running_version.join(" ")
|
new_command.current_running_version.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -225,8 +247,8 @@ class CommandsAppTest < ActiveSupport::TestCase
|
|||||||
new_command.list_versions.join(" ")
|
new_command.list_versions.join(" ")
|
||||||
|
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker ps --filter label=service=app --filter label=role=web --filter status=running --latest --format \"{{.Names}}\" | grep -oE \"\\-[^-]+$\" | cut -c 2-",
|
"docker ps --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest --format \"{{.Names}}\" | grep -oE \"\\-[^-]+$\" | cut -c 2-",
|
||||||
new_command.list_versions("--latest", status: :running).join(" ")
|
new_command.list_versions("--latest", statuses: [ :running, :restarting ]).join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "list_containers" do
|
test "list_containers" do
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ class CommandsBuilderTest < ActiveSupport::TestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "target multiarch by default" do
|
test "target multiarch by default" do
|
||||||
builder = new_builder_command
|
builder = new_builder_command(builder: { "cache" => { "type" => "gha" }})
|
||||||
assert_equal "multiarch", builder.name
|
assert_equal "multiarch", builder.name
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker buildx build --push --platform linux/amd64,linux/arm64 --builder mrsk-app-multiarch -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile .",
|
"docker buildx build --push --platform linux/amd64,linux/arm64 --builder mrsk-app-multiarch -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile .",
|
||||||
builder.push.join(" ")
|
builder.push.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -21,19 +21,27 @@ class CommandsBuilderTest < ActiveSupport::TestCase
|
|||||||
builder.push.join(" ")
|
builder.push.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "target native cached when multiarch is off and cache is set" do
|
||||||
|
builder = new_builder_command(builder: { "multiarch" => false, "cache" => { "type" => "gha" }})
|
||||||
|
assert_equal "native/cached", builder.name
|
||||||
|
assert_equal \
|
||||||
|
"docker buildx build --push -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile .",
|
||||||
|
builder.push.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
test "target multiarch remote when local and remote is set" do
|
test "target multiarch remote when local and remote is set" do
|
||||||
builder = new_builder_command(builder: { "local" => { }, "remote" => { } })
|
builder = new_builder_command(builder: { "local" => { }, "remote" => { }, "cache" => { "type" => "gha" } })
|
||||||
assert_equal "multiarch/remote", builder.name
|
assert_equal "multiarch/remote", builder.name
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker buildx build --push --platform linux/amd64,linux/arm64 --builder mrsk-app-multiarch-remote -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile .",
|
"docker buildx build --push --platform linux/amd64,linux/arm64 --builder mrsk-app-multiarch-remote -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile .",
|
||||||
builder.push.join(" ")
|
builder.push.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "target native remote when only remote is set" do
|
test "target native remote when only remote is set" do
|
||||||
builder = new_builder_command(builder: { "remote" => { "arch" => "amd64" } })
|
builder = new_builder_command(builder: { "remote" => { "arch" => "amd64" }, "cache" => { "type" => "gha" } })
|
||||||
assert_equal "native/remote", builder.name
|
assert_equal "native/remote", builder.name
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker buildx build --push --platform linux/amd64 --builder mrsk-app-native-remote -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile .",
|
"docker buildx build --push --platform linux/amd64 --builder mrsk-app-native-remote -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile .",
|
||||||
builder.push.join(" ")
|
builder.push.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -88,7 +96,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase
|
|||||||
builder.push.join(" ")
|
builder.push.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "native push with with build secrets" do
|
test "native push with build secrets" do
|
||||||
builder = new_builder_command(builder: { "multiarch" => false, "secrets" => [ "a", "b" ] })
|
builder = new_builder_command(builder: { "multiarch" => false, "secrets" => [ "a", "b" ] })
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker build -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --secret id=\"a\" --secret id=\"b\" --file Dockerfile . && docker push dhh/app:123 && docker push dhh/app:latest",
|
"docker build -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --secret id=\"a\" --secret id=\"b\" --file Dockerfile . && docker push dhh/app:123 && docker push dhh/app:latest",
|
||||||
|
|||||||
@@ -51,6 +51,12 @@ class CommandsHealthcheckTest < ActiveSupport::TestCase
|
|||||||
new_command.status.join(" ")
|
new_command.status.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "container_health_log" do
|
||||||
|
assert_equal \
|
||||||
|
"docker container ls --all --filter name=^healthcheck-app-123$ --quiet | xargs docker inspect --format '{{json .State.Health}}'",
|
||||||
|
new_command.container_health_log.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
test "stop" do
|
test "stop" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker container ls --all --filter name=^healthcheck-app-123$ --quiet | xargs docker stop",
|
"docker container ls --all --filter name=^healthcheck-app-123$ --quiet | xargs docker stop",
|
||||||
|
|||||||
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,19 +10,19 @@ class CommandsLockTest < ActiveSupport::TestCase
|
|||||||
|
|
||||||
test "status" do
|
test "status" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"stat mrsk_lock > /dev/null && cat mrsk_lock/details | base64 -d",
|
"stat mrsk_lock-app > /dev/null && cat mrsk_lock-app/details | base64 -d",
|
||||||
new_command.status.join(" ")
|
new_command.status.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "acquire" do
|
test "acquire" do
|
||||||
assert_match \
|
assert_match \
|
||||||
/mkdir mrsk_lock && echo ".*" > mrsk_lock\/details/m,
|
/mkdir mrsk_lock-app && echo ".*" > mrsk_lock-app\/details/m,
|
||||||
new_command.acquire("Hello", "123").join(" ")
|
new_command.acquire("Hello", "123").join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "release" do
|
test "release" do
|
||||||
assert_match \
|
assert_match \
|
||||||
"rm mrsk_lock/details && rm -r mrsk_lock",
|
"rm mrsk_lock-app/details && rm -r mrsk_lock-app",
|
||||||
new_command.release.join(" ")
|
new_command.release.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -8,10 +8,16 @@ class CommandsPruneTest < ActiveSupport::TestCase
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
test "images" do
|
test "dangling images" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker image prune --all --force --filter label=service=app --filter dangling=true",
|
"docker image prune --force --filter label=service=app --filter dangling=true",
|
||||||
new_command.images.join(" ")
|
new_command.dangling_images.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "tagged images" do
|
||||||
|
assert_equal \
|
||||||
|
"docker image ls --filter label=service=app --format '{{.ID}} {{.Repository}}:{{.Tag}}' | grep -v -w \"$(docker container ls -a --format '{{.Image}}\\|' --filter label=service=app | tr -d '\\n')dhh/app:latest\\|dhh/app:<none>\" | while read image tag; do docker rmi $tag; done",
|
||||||
|
new_command.tagged_images.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "containers" do
|
test "containers" do
|
||||||
|
|||||||
@@ -8,60 +8,77 @@ class CommandsTraefikTest < ActiveSupport::TestCase
|
|||||||
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: { "image" => @image, "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\" #{@image} --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\" #{@image} --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\" #{@image} --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\" #{@image} --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\" #{@image} --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\" #{@image} --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\" #{@image} --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\" #{@image} --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(" ")
|
new_command.run.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "run with labels configured" do
|
test "run with labels 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\" #{@image} --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]["labels"] = { "traefik.http.routers.dashboard.service" => "api@internal", "traefik.http.routers.dashboard.middlewares" => "auth" }
|
@config[:traefik]["labels"] = { "traefik.http.routers.dashboard.service" => "api@internal", "traefik.http.routers.dashboard.middlewares" => "auth" }
|
||||||
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\" --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\"",
|
"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
|
||||||
|
|
||||||
@@ -69,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\" #{Mrsk::Commands::Traefik::DEFAULT_IMAGE} --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
|
||||||
|
|
||||||
@@ -77,7 +94,15 @@ 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\" #{@image} --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(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "run with default args overriden" do
|
||||||
|
@config[:traefik]["args"]["log.level"] = "ERROR"
|
||||||
|
|
||||||
|
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=\"ERROR\" --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
|
||||||
|
|
||||||
|
|||||||
151
test/configuration/builder_test.rb
Normal file
151
test/configuration/builder_test.rb
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class ConfigurationBuilderTest < ActiveSupport::TestCase
|
||||||
|
setup do
|
||||||
|
@deploy = {
|
||||||
|
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" },
|
||||||
|
servers: [ "1.1.1.1" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@config = Mrsk::Configuration.new(@deploy)
|
||||||
|
|
||||||
|
@deploy_with_builder_option = {
|
||||||
|
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" },
|
||||||
|
servers: [ "1.1.1.1" ],
|
||||||
|
builder: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@config_with_builder_option = Mrsk::Configuration.new(@deploy_with_builder_option)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "multiarch?" do
|
||||||
|
assert_equal true, @config.builder.multiarch?
|
||||||
|
end
|
||||||
|
|
||||||
|
test "setting multiarch to false" do
|
||||||
|
@deploy_with_builder_option[:builder] = { "multiarch" => false }
|
||||||
|
|
||||||
|
assert_equal false, @config_with_builder_option.builder.multiarch?
|
||||||
|
end
|
||||||
|
|
||||||
|
test "local?" do
|
||||||
|
assert_equal false, @config.builder.local?
|
||||||
|
end
|
||||||
|
|
||||||
|
test "remote?" do
|
||||||
|
assert_equal false, @config.builder.remote?
|
||||||
|
end
|
||||||
|
|
||||||
|
test "remote_arch" do
|
||||||
|
assert_nil @config.builder.remote_arch
|
||||||
|
end
|
||||||
|
|
||||||
|
test "remote_host" do
|
||||||
|
assert_nil @config.builder.remote_host
|
||||||
|
end
|
||||||
|
|
||||||
|
test "remote config is missing when local is specified" do
|
||||||
|
@deploy_with_builder_option[:builder] = { "local" => { "arch" => "arm64", "host" => "unix:///Users/<%= `whoami`.strip %>/.docker/run/docker.sock" } }
|
||||||
|
|
||||||
|
assert_raises(ArgumentError) do
|
||||||
|
@config_with_builder_option.builder
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "setting both local and remote configs" do
|
||||||
|
@deploy_with_builder_option[:builder] = {
|
||||||
|
"local" => { "arch" => "arm64", "host" => "unix:///Users/<%= `whoami`.strip %>/.docker/run/docker.sock" },
|
||||||
|
"remote" => { "arch" => "amd64", "host" => "ssh://root@192.168.0.1" }
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_equal true, @config_with_builder_option.builder.local?
|
||||||
|
assert_equal true, @config_with_builder_option.builder.remote?
|
||||||
|
|
||||||
|
assert_equal "amd64", @config_with_builder_option.builder.remote_arch
|
||||||
|
assert_equal "ssh://root@192.168.0.1", @config_with_builder_option.builder.remote_host
|
||||||
|
|
||||||
|
assert_equal "arm64", @config_with_builder_option.builder.local_arch
|
||||||
|
assert_equal "unix:///Users/<%= `whoami`.strip %>/.docker/run/docker.sock", @config_with_builder_option.builder.local_host
|
||||||
|
end
|
||||||
|
|
||||||
|
test "cached?" do
|
||||||
|
assert_equal false, @config.builder.cached?
|
||||||
|
end
|
||||||
|
|
||||||
|
test "invalid cache type specified" do
|
||||||
|
@deploy_with_builder_option[:builder] = { "cache" => { "type" => "invalid" } }
|
||||||
|
|
||||||
|
assert_raises(ArgumentError) do
|
||||||
|
@config_with_builder_option.builder
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "cache_from" do
|
||||||
|
assert_nil @config.builder.cache_from
|
||||||
|
end
|
||||||
|
|
||||||
|
test "cache_to" do
|
||||||
|
assert_nil @config.builder.cache_to
|
||||||
|
end
|
||||||
|
|
||||||
|
test "setting gha cache" do
|
||||||
|
@deploy_with_builder_option[:builder] = { "cache" => { "type" => "gha", "options" => "mode=max" } }
|
||||||
|
|
||||||
|
assert_equal "type=gha", @config_with_builder_option.builder.cache_from
|
||||||
|
assert_equal "type=gha,mode=max", @config_with_builder_option.builder.cache_to
|
||||||
|
end
|
||||||
|
|
||||||
|
test "setting registry cache" do
|
||||||
|
@deploy_with_builder_option[:builder] = { "cache" => { "type" => "registry", "options" => "mode=max,image-manifest=true,oci-mediatypes=true" } }
|
||||||
|
|
||||||
|
assert_equal "type=registry,ref=/dhh/app-build-cache", @config_with_builder_option.builder.cache_from
|
||||||
|
assert_equal "type=registry,mode=max,image-manifest=true,oci-mediatypes=true,ref=/dhh/app-build-cache", @config_with_builder_option.builder.cache_to
|
||||||
|
end
|
||||||
|
|
||||||
|
test "setting registry cache with image" do
|
||||||
|
@deploy_with_builder_option[:builder] = { "cache" => { "type" => "registry", "image" => "mrsk", "options" => "mode=max" } }
|
||||||
|
|
||||||
|
assert_equal "type=registry,ref=/mrsk", @config_with_builder_option.builder.cache_from
|
||||||
|
assert_equal "type=registry,mode=max,ref=/mrsk", @config_with_builder_option.builder.cache_to
|
||||||
|
end
|
||||||
|
|
||||||
|
test "args" do
|
||||||
|
assert_equal({}, @config.builder.args)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "setting args" do
|
||||||
|
@deploy_with_builder_option[:builder] = { "args" => { "key" => "value" } }
|
||||||
|
|
||||||
|
assert_equal({ "key" => "value" }, @config_with_builder_option.builder.args)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "secrets" do
|
||||||
|
assert_equal [], @config.builder.secrets
|
||||||
|
end
|
||||||
|
|
||||||
|
test "setting secrets" do
|
||||||
|
@deploy_with_builder_option[:builder] = { "secrets" => ["GITHUB_TOKEN"] }
|
||||||
|
|
||||||
|
assert_equal ["GITHUB_TOKEN"], @config_with_builder_option.builder.secrets
|
||||||
|
end
|
||||||
|
|
||||||
|
test "dockerfile" do
|
||||||
|
assert_equal "Dockerfile", @config.builder.dockerfile
|
||||||
|
end
|
||||||
|
|
||||||
|
test "setting dockerfile" do
|
||||||
|
@deploy_with_builder_option[:builder] = { "dockerfile" => "Dockerfile.dev" }
|
||||||
|
|
||||||
|
assert_equal "Dockerfile.dev", @config_with_builder_option.builder.dockerfile
|
||||||
|
end
|
||||||
|
|
||||||
|
test "context" do
|
||||||
|
assert_equal ".", @config.builder.context
|
||||||
|
end
|
||||||
|
|
||||||
|
test "setting context" do
|
||||||
|
@deploy_with_builder_option[:builder] = { "context" => ".." }
|
||||||
|
|
||||||
|
assert_equal "..", @config_with_builder_option.builder.context
|
||||||
|
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
|
||||||
@@ -149,7 +166,7 @@ class ConfigurationTest < ActiveSupport::TestCase
|
|||||||
assert_raises(KeyError) do
|
assert_raises(KeyError) do
|
||||||
config = Mrsk::Configuration.new(@deploy.tap { |c| c.merge!({
|
config = Mrsk::Configuration.new(@deploy.tap { |c| c.merge!({
|
||||||
env: { "secret" => [ "PASSWORD" ] }
|
env: { "secret" => [ "PASSWORD" ] }
|
||||||
}) })
|
}) }).ensure_env_available
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -249,6 +266,22 @@ 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, "max_attempts" => 7 }}, @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"], :builder=>{}, :logging=>["--log-opt", "max-size=\"10m\""], :healthcheck=>{"path"=>"/up", "port"=>3000, "max_attempts" => 7 }}, @config.to_h)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "min version is lower" do
|
||||||
|
config = Mrsk::Configuration.new(@deploy.tap { |c| c.merge!(minimum_version: "0.0.1") })
|
||||||
|
assert_equal "0.0.1", config.minimum_version
|
||||||
|
end
|
||||||
|
|
||||||
|
test "min version is equal" do
|
||||||
|
config = Mrsk::Configuration.new(@deploy.tap { |c| c.merge!(minimum_version: Mrsk::VERSION) })
|
||||||
|
assert_equal Mrsk::VERSION, config.minimum_version
|
||||||
|
end
|
||||||
|
|
||||||
|
test "min version is higher" do
|
||||||
|
assert_raises(ArgumentError) do
|
||||||
|
Mrsk::Configuration.new(@deploy.tap { |c| c.merge!(minimum_version: "10000.0.0") })
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
11
test/fixtures/deploy_with_secrets.yml
vendored
Normal file
11
test/fixtures/deploy_with_secrets.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
service: app
|
||||||
|
image: dhh/app
|
||||||
|
servers:
|
||||||
|
- "1.1.1.1"
|
||||||
|
- "1.1.1.2"
|
||||||
|
registry:
|
||||||
|
username: user
|
||||||
|
password: pw
|
||||||
|
env:
|
||||||
|
secret:
|
||||||
|
- PASSWORD
|
||||||
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
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
require "net/http"
|
|
||||||
require "test_helper"
|
|
||||||
|
|
||||||
class DeployTest < ActiveSupport::TestCase
|
|
||||||
|
|
||||||
setup do
|
|
||||||
docker_compose "up --build --force-recreate -d"
|
|
||||||
wait_for_healthy
|
|
||||||
end
|
|
||||||
|
|
||||||
teardown do
|
|
||||||
docker_compose "down -v"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "deploy" do
|
|
||||||
assert_app_is_down
|
|
||||||
|
|
||||||
mrsk :deploy
|
|
||||||
|
|
||||||
assert_app_is_up
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
def docker_compose(*commands, capture: false)
|
|
||||||
command = "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 `#{$?}`" unless succeeded
|
|
||||||
result
|
|
||||||
end
|
|
||||||
|
|
||||||
def deployer_exec(*commands, capture: false)
|
|
||||||
if capture
|
|
||||||
stdouted { docker_compose("exec deployer #{commands.join(" ")}") }
|
|
||||||
else
|
|
||||||
docker_compose("exec deployer #{commands.join(" ")}", capture: capture)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def mrsk(*commands, capture: false)
|
|
||||||
deployer_exec(:mrsk, *commands, capture: capture)
|
|
||||||
end
|
|
||||||
|
|
||||||
def assert_app_is_down
|
|
||||||
assert_equal "502", app_response.code
|
|
||||||
end
|
|
||||||
|
|
||||||
def assert_app_is_up
|
|
||||||
code = app_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 app_response
|
|
||||||
Net::HTTP.get_response(URI.parse("http://localhost:12345"))
|
|
||||||
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
|
|
||||||
end
|
|
||||||
@@ -3,6 +3,8 @@ name: "mrsk-test"
|
|||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
shared:
|
shared:
|
||||||
|
registry:
|
||||||
|
deployer_bundle:
|
||||||
|
|
||||||
services:
|
services:
|
||||||
shared:
|
shared:
|
||||||
@@ -15,9 +17,13 @@ services:
|
|||||||
privileged: true
|
privileged: true
|
||||||
build:
|
build:
|
||||||
context: docker/deployer
|
context: docker/deployer
|
||||||
|
environment:
|
||||||
|
- TEST_ID=${TEST_ID}
|
||||||
volumes:
|
volumes:
|
||||||
- ../..:/mrsk
|
- ../..:/mrsk
|
||||||
- shared:/shared
|
- shared:/shared
|
||||||
|
- registry:/registry
|
||||||
|
- deployer_bundle:/usr/local/bundle/
|
||||||
|
|
||||||
registry:
|
registry:
|
||||||
build:
|
build:
|
||||||
@@ -28,6 +34,7 @@ services:
|
|||||||
- REGISTRY_HTTP_TLS_KEY=/certs/domain.key
|
- REGISTRY_HTTP_TLS_KEY=/certs/domain.key
|
||||||
volumes:
|
volumes:
|
||||||
- shared:/shared
|
- shared:/shared
|
||||||
|
- registry:/var/lib/registry/
|
||||||
|
|
||||||
vm1:
|
vm1:
|
||||||
privileged: true
|
privileged: true
|
||||||
|
|||||||
@@ -14,9 +14,10 @@ RUN echo \
|
|||||||
|
|
||||||
RUN apt-get update --fix-missing && apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
RUN apt-get update --fix-missing && apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||||
|
|
||||||
COPY boot.sh .
|
COPY *.sh .
|
||||||
COPY app/ .
|
COPY app/ .
|
||||||
|
|
||||||
|
RUN rm -rf /root/.ssh
|
||||||
RUN ln -s /shared/ssh /root/.ssh
|
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 mkdir -p /etc/docker/certs.d/registry:4443 && ln -s /shared/certs/domain.crt /etc/docker/certs.d/registry:4443/ca.crt
|
||||||
|
|
||||||
|
|||||||
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
|
||||||
3
test/integration/docker/deployer/app/.mrsk/hooks/pre-deploy
Executable file
3
test/integration/docker/deployer/app/.mrsk/hooks/pre-deploy
Executable file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
echo "Deployed!"
|
||||||
|
mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/pre-deploy
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
FROM nginx:1-alpine-slim
|
FROM registry:4443/nginx:1-alpine-slim
|
||||||
|
|
||||||
COPY default.conf /etc/nginx/conf.d/default.conf
|
COPY default.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
ARG COMMIT_SHA
|
||||||
|
RUN echo $COMMIT_SHA > /usr/share/nginx/html/version
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,19 @@ registry:
|
|||||||
password: root
|
password: root
|
||||||
builder:
|
builder:
|
||||||
multiarch: false
|
multiarch: false
|
||||||
|
args:
|
||||||
|
COMMIT_SHA: <%= `git rev-parse HEAD` %>
|
||||||
healthcheck:
|
healthcheck:
|
||||||
cmd: wget -qO- http://localhost > /dev/null
|
cmd: wget -qO- http://localhost > /dev/null
|
||||||
traefik:
|
traefik:
|
||||||
args:
|
args:
|
||||||
accesslog: true
|
accesslog: true
|
||||||
accesslog.format: json
|
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
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
cd /mrsk && gem build mrsk.gemspec -o /tmp/mrsk.gem && gem install /tmp/mrsk.gem
|
dockerd --max-concurrent-downloads 1 &
|
||||||
|
|
||||||
dockerd &
|
exec sleep infinity
|
||||||
|
|
||||||
trap "pkill -f sleep" term
|
|
||||||
|
|
||||||
sleep infinity & wait
|
|
||||||
|
|||||||
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
|
||||||
@@ -8,5 +8,9 @@ server {
|
|||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://loadbalancer;
|
proxy_pass http://loadbalancer;
|
||||||
|
proxy_connect_timeout 10;
|
||||||
|
proxy_send_timeout 10;
|
||||||
|
proxy_read_timeout 10;
|
||||||
|
send_timeout 10;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,4 @@
|
|||||||
|
|
||||||
while [ ! -f /certs/domain.crt ]; do sleep 1; done
|
while [ ! -f /certs/domain.crt ]; do sleep 1; done
|
||||||
|
|
||||||
trap "pkill -f registry" term
|
exec /entrypoint.sh /etc/docker/registry/config.yml
|
||||||
|
|
||||||
/entrypoint.sh /etc/docker/registry/config.yml & wait
|
|
||||||
|
|||||||
@@ -2,6 +2,4 @@
|
|||||||
|
|
||||||
cp -r * /shared
|
cp -r * /shared
|
||||||
|
|
||||||
trap "pkill -f sleep" term
|
exec sleep infinity
|
||||||
|
|
||||||
sleep infinity & wait
|
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ while [ ! -f /root/.ssh/authorized_keys ]; do echo "Waiting for ssh keys"; sleep
|
|||||||
|
|
||||||
service ssh restart
|
service ssh restart
|
||||||
|
|
||||||
dockerd &
|
dockerd --max-concurrent-downloads 1 &
|
||||||
|
|
||||||
trap "pkill -f sleep" term
|
exec sleep infinity
|
||||||
|
|
||||||
sleep infinity & wait
|
|
||||||
|
|||||||
138
test/integration/integration_test.rb
Normal file
138
test/integration/integration_test.rb
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
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
|
||||||
|
unless passed?
|
||||||
|
[:deployer, :vm1, :vm2, :shared, :load_balancer, :registry].each do |container|
|
||||||
|
puts
|
||||||
|
puts "Logs for #{container}:"
|
||||||
|
docker_compose :logs, container
|
||||||
|
end
|
||||||
|
end
|
||||||
|
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: 20, 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", "pre-deploy", "post-deploy"
|
||||||
|
|
||||||
|
second_version = update_app_rev
|
||||||
|
|
||||||
|
mrsk :redeploy
|
||||||
|
assert_app_is_up version: second_version
|
||||||
|
assert_hooks_ran "pre-connect", "pre-build", "pre-deploy", "post-deploy"
|
||||||
|
|
||||||
|
mrsk :rollback, first_version
|
||||||
|
assert_hooks_ran "pre-connect", "pre-deploy", "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
|
||||||
Reference in New Issue
Block a user