mrsk 0.11.0 → 0.13.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +141 -37
- data/bin/mrsk +1 -1
- data/lib/mrsk/cli/accessory.rb +0 -2
- data/lib/mrsk/cli/app.rb +66 -22
- data/lib/mrsk/cli/base.rb +80 -22
- data/lib/mrsk/cli/build.rb +20 -0
- data/lib/mrsk/cli/healthcheck.rb +4 -34
- data/lib/mrsk/cli/lock.rb +5 -5
- data/lib/mrsk/cli/main.rb +62 -53
- data/lib/mrsk/cli/prune.rb +4 -3
- data/lib/mrsk/cli/server.rb +13 -9
- data/lib/mrsk/cli/templates/deploy.yml +0 -4
- data/lib/mrsk/cli/templates/sample_hooks/post-deploy.sample +14 -0
- data/lib/mrsk/cli/templates/sample_hooks/pre-build.sample +51 -0
- data/lib/mrsk/cli/templates/sample_hooks/pre-connect.sample +47 -0
- data/lib/mrsk/cli/templates/sample_hooks/pre-deploy.sample +82 -0
- data/lib/mrsk/cli/traefik.rb +1 -1
- data/lib/mrsk/cli.rb +1 -0
- data/lib/mrsk/commander.rb +34 -9
- data/lib/mrsk/commands/app.rb +37 -15
- data/lib/mrsk/commands/auditor.rb +7 -36
- data/lib/mrsk/commands/base.rb +9 -2
- data/lib/mrsk/commands/builder/base.rb +9 -2
- data/lib/mrsk/commands/builder.rb +21 -1
- data/lib/mrsk/commands/docker.rb +21 -0
- data/lib/mrsk/commands/healthcheck.rb +7 -2
- data/lib/mrsk/commands/hook.rb +14 -0
- data/lib/mrsk/commands/lock.rb +2 -2
- data/lib/mrsk/commands/prune.rb +30 -4
- data/lib/mrsk/commands/traefik.rb +12 -1
- data/lib/mrsk/configuration/boot.rb +20 -0
- data/lib/mrsk/configuration/role.rb +29 -2
- data/lib/mrsk/configuration.rb +15 -9
- data/lib/mrsk/sshkit_with_ext.rb +46 -2
- data/lib/mrsk/tags.rb +39 -0
- data/lib/mrsk/utils/healthcheck_poller.rb +39 -0
- data/lib/mrsk/utils.rb +13 -2
- data/lib/mrsk/version.rb +1 -1
- metadata +11 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3a752ca316aea9ce851cc9b4ac96cd7fde613f94dd1b63e238c3320b10f82e8c
|
|
4
|
+
data.tar.gz: 689bba95ccc72b27b9cc85572b6e4d995f6bfed3d09be30c2076a81ed1eb07f6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cdbb9e9b58364ba933778bb18bf4576ee3c611fcbb3be9b341c2e21d53d08d37bd524c694e058be4456f0d7da837b488d20c009973017fa39f6c57fb5835d0ce
|
|
7
|
+
data.tar.gz: eb58c8ac236172f91719582f7b9606d2255c122be41c7f341a6a54e6b1680b4715f55ad99492ee47a57c38b74626b129d92bd77ed0d864042a7c457343aade13
|
data/README.md
CHANGED
|
@@ -44,30 +44,30 @@ Then edit your `.env` file to add your registry password as `MRSK_REGISTRY_PASSW
|
|
|
44
44
|
Now you're ready to deploy to the servers:
|
|
45
45
|
|
|
46
46
|
```
|
|
47
|
-
mrsk
|
|
47
|
+
mrsk setup
|
|
48
48
|
```
|
|
49
49
|
|
|
50
50
|
This will:
|
|
51
51
|
|
|
52
52
|
1. Connect to the servers over SSH (using root by default, authenticated by your ssh key)
|
|
53
|
-
2. Install Docker on any server that might be missing it (using apt-get): root access is needed via ssh for this.
|
|
53
|
+
2. Install Docker and curl on any server that might be missing it (using apt-get): root access is needed via ssh for this.
|
|
54
54
|
3. Log into the registry both locally and remotely
|
|
55
55
|
4. Build the image using the standard Dockerfile in the root of the application.
|
|
56
56
|
5. Push the image to the registry.
|
|
57
57
|
6. Pull the image from the registry onto the servers.
|
|
58
58
|
7. Ensure Traefik is running and accepting traffic on port 80.
|
|
59
|
-
8. Ensure your app responds with `200 OK` to `GET /up
|
|
59
|
+
8. Ensure your app responds with `200 OK` to `GET /up` (you must have curl installed inside your app image!).
|
|
60
60
|
9. Start a new container with the version of the app that matches the current git version hash.
|
|
61
61
|
10. Stop the old container running the previous version of the app.
|
|
62
62
|
11. Prune unused images and stopped containers to ensure servers don't fill up.
|
|
63
63
|
|
|
64
|
-
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.
|
|
64
|
+
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`.
|
|
65
65
|
|
|
66
66
|
## Vision
|
|
67
67
|
|
|
68
68
|
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!
|
|
69
69
|
|
|
70
|
-
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
|
|
70
|
+
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.
|
|
71
71
|
|
|
72
72
|
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.
|
|
73
73
|
|
|
@@ -184,6 +184,19 @@ registry:
|
|
|
184
184
|
|
|
185
185
|
A reference to secret `DOCKER_REGISTRY_TOKEN` will look for `ENV["DOCKER_REGISTRY_TOKEN"]` on the machine running MRSK.
|
|
186
186
|
|
|
187
|
+
#### Using AWS ECR as the container registry
|
|
188
|
+
|
|
189
|
+
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:
|
|
190
|
+
|
|
191
|
+
```yaml
|
|
192
|
+
registry:
|
|
193
|
+
server: <your aws account id>.dkr.ecr.<your aws region id>.amazonaws.com
|
|
194
|
+
username: AWS
|
|
195
|
+
password: <%= %x(aws ecr get-login-password) %>
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
You will need to have the `aws` CLI installed locally for this to work.
|
|
199
|
+
|
|
187
200
|
### Using a different SSH user than root
|
|
188
201
|
|
|
189
202
|
The default SSH user is root, but you can change it using `ssh/user`:
|
|
@@ -308,7 +321,7 @@ You can specialize the default Traefik rules by setting labels on the containers
|
|
|
308
321
|
labels:
|
|
309
322
|
traefik.http.routers.hey-web.rule: Host(`app.hey.com`)
|
|
310
323
|
```
|
|
311
|
-
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.
|
|
324
|
+
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.
|
|
312
325
|
|
|
313
326
|
Note: The backticks are needed to ensure the rule is passed in correctly and not treated as command substitution by Bash!
|
|
314
327
|
|
|
@@ -331,6 +344,21 @@ servers:
|
|
|
331
344
|
my-label: "50"
|
|
332
345
|
```
|
|
333
346
|
|
|
347
|
+
### Using shell expansion
|
|
348
|
+
|
|
349
|
+
You can use shell expansion to interpolate values from the host machine into labels and env variables with the `${}` syntax.
|
|
350
|
+
Anything within the curly braces will be executed on the host machine and the result will be interpolated into the label or env variable.
|
|
351
|
+
|
|
352
|
+
```yaml
|
|
353
|
+
labels:
|
|
354
|
+
host-machine: "${cat /etc/hostname}"
|
|
355
|
+
|
|
356
|
+
env:
|
|
357
|
+
HOST_DEPLOYMENT_DIR: "${PWD}"
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
Note: Any other occurrence of `$` will be escaped to prevent unwanted shell expansion!
|
|
361
|
+
|
|
334
362
|
### Using container options
|
|
335
363
|
|
|
336
364
|
You can specialize the options used to start containers using the `options` definitions:
|
|
@@ -507,25 +535,25 @@ traefik:
|
|
|
507
535
|
options:
|
|
508
536
|
publish:
|
|
509
537
|
- 8080:8080
|
|
510
|
-
|
|
538
|
+
volume:
|
|
511
539
|
- /tmp/example.json:/tmp/example.json
|
|
512
540
|
memory: 512m
|
|
513
541
|
```
|
|
514
542
|
|
|
515
543
|
This starts the Traefik container with `--volume /tmp/example.json:/tmp/example.json --publish 8080:8080 --memory 512m` arguments to `docker run`.
|
|
516
544
|
|
|
517
|
-
### Traefik container
|
|
545
|
+
### Traefik container labels
|
|
518
546
|
|
|
519
547
|
Add labels to Traefik Docker container.
|
|
520
548
|
|
|
521
549
|
```yaml
|
|
522
550
|
traefik:
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
551
|
+
labels:
|
|
552
|
+
traefik.enable: true
|
|
553
|
+
traefik.http.routers.dashboard.rule: Host(`traefik.example.com`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))
|
|
554
|
+
traefik.http.routers.dashboard.service: api@internal
|
|
555
|
+
traefik.http.routers.dashboard.middlewares: auth
|
|
556
|
+
traefik.http.middlewares.auth.basicauth.users: test:$2y$05$H2o72tMaO.TwY1wNQUV1K.fhjRgLHRDWohFvUZOJHBEtUXNKrqUKi # test:password
|
|
529
557
|
```
|
|
530
558
|
|
|
531
559
|
This labels Traefik container with `--label traefik.http.routers.dashboard.middlewares=\"auth\"` and so on.
|
|
@@ -640,42 +668,45 @@ servers:
|
|
|
640
668
|
|
|
641
669
|
This assumes the Cron settings are stored in `config/crontab`.
|
|
642
670
|
|
|
643
|
-
###
|
|
671
|
+
### Healthcheck
|
|
644
672
|
|
|
645
|
-
|
|
673
|
+
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.
|
|
674
|
+
|
|
675
|
+
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:
|
|
646
676
|
|
|
647
677
|
```yaml
|
|
648
|
-
|
|
649
|
-
|
|
678
|
+
healthcheck:
|
|
679
|
+
path: /healthz
|
|
680
|
+
port: 4000
|
|
681
|
+
max_attempts: 7
|
|
682
|
+
interval: 20s
|
|
650
683
|
```
|
|
651
684
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
```bash
|
|
655
|
-
#!/usr/bin/env bash
|
|
656
|
-
curl -q -d content="[My App] ${1}" https://3.basecamp.com/XXXXX/integrations/XXXXX/buckets/XXXXX/chats/XXXXX/lines
|
|
657
|
-
```
|
|
685
|
+
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.
|
|
658
686
|
|
|
659
|
-
|
|
687
|
+
You can also specify a custom healthcheck command, which is useful for non-HTTP services:
|
|
660
688
|
|
|
661
|
-
```
|
|
662
|
-
|
|
689
|
+
```yaml
|
|
690
|
+
healthcheck:
|
|
691
|
+
cmd: /bin/check_health
|
|
663
692
|
```
|
|
664
693
|
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
694
|
+
The top-level healthcheck configuration applies to all services that use
|
|
695
|
+
Traefik, by default. You can also specialize the configuration at the role
|
|
696
|
+
level:
|
|
668
697
|
|
|
669
698
|
```yaml
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
699
|
+
servers:
|
|
700
|
+
job:
|
|
701
|
+
hosts: ...
|
|
702
|
+
cmd: bin/jobs
|
|
703
|
+
healthcheck:
|
|
704
|
+
cmd: bin/check
|
|
674
705
|
```
|
|
675
706
|
|
|
676
|
-
|
|
707
|
+
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.
|
|
677
708
|
|
|
678
|
-
The
|
|
709
|
+
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.
|
|
679
710
|
|
|
680
711
|
## Commands
|
|
681
712
|
|
|
@@ -809,13 +840,86 @@ Message: Automatic deploy lock
|
|
|
809
840
|
You can also manually acquire and release the lock
|
|
810
841
|
|
|
811
842
|
```
|
|
812
|
-
mrsk lock acquire -m "Doing
|
|
843
|
+
mrsk lock acquire -m "Doing maintenance"
|
|
813
844
|
```
|
|
814
845
|
|
|
815
846
|
```
|
|
816
847
|
mrsk lock release
|
|
817
848
|
```
|
|
818
849
|
|
|
850
|
+
## Rolling deployments
|
|
851
|
+
|
|
852
|
+
When deploying to large numbers of hosts, you might prefer not to restart your services on every host at the same time.
|
|
853
|
+
|
|
854
|
+
MRSK's default is to boot new containers on all hosts in parallel. But you can control this by configuring `boot/limit` and `boot/wait` as options:
|
|
855
|
+
|
|
856
|
+
```yaml
|
|
857
|
+
service: myservice
|
|
858
|
+
|
|
859
|
+
boot:
|
|
860
|
+
limit: 10 # Can also specify as a percentage of total hosts, such as "25%"
|
|
861
|
+
wait: 2
|
|
862
|
+
```
|
|
863
|
+
|
|
864
|
+
When `limit` is specified, containers will be booted on, at most, `limit` hosts at once. MRSK will pause for `wait` seconds between batches.
|
|
865
|
+
|
|
866
|
+
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.
|
|
867
|
+
|
|
868
|
+
## Hooks
|
|
869
|
+
|
|
870
|
+
You can run custom scripts at specific points with hooks.
|
|
871
|
+
|
|
872
|
+
Hooks should be stored in the .mrsk/hooks folder. Running mrsk init will build that folder and add some sample scripts.
|
|
873
|
+
|
|
874
|
+
You can change their location by setting `hooks_path` in the configuration file.
|
|
875
|
+
|
|
876
|
+
If the script returns a non-zero exit code the command will be aborted.
|
|
877
|
+
|
|
878
|
+
`MRSK_*` environment variables are available to the hooks command for
|
|
879
|
+
fine-grained audit reporting, e.g. for triggering deployment reports or
|
|
880
|
+
firing a JSON webhook. These variables include:
|
|
881
|
+
- `MRSK_RECORDED_AT` - UTC timestamp in ISO 8601 format, e.g. `2023-04-14T17:07:31Z`
|
|
882
|
+
- `MRSK_PERFORMER` - the local user performing the command (from `whoami`)
|
|
883
|
+
- `MRSK_SERVICE_VERSION` - an abbreviated service and version for use in messages, e.g. app@150b24f
|
|
884
|
+
- `MRSK_VERSION` - an full version being deployed
|
|
885
|
+
- `MRSK_HOSTS` - a comma separated list of the hosts targeted by the command
|
|
886
|
+
- `MRSK_COMMAND` - The command we are running
|
|
887
|
+
- `MRSK_SUBCOMMAND` - optional: The subcommand we are running
|
|
888
|
+
- `MRSK_DESTINATION` - optional: destination, e.g. "staging"
|
|
889
|
+
- `MRSK_ROLE` - optional: role targeted, e.g. "web"
|
|
890
|
+
|
|
891
|
+
There are four hooks:
|
|
892
|
+
|
|
893
|
+
1. pre-connect
|
|
894
|
+
Called before taking the deploy lock. For checks that need to run before connecting to remote hosts - e.g. DNS warming.
|
|
895
|
+
|
|
896
|
+
2. pre-build
|
|
897
|
+
Used for pre-build checks - e.g. there are no uncommitted changes or that CI has passed.
|
|
898
|
+
|
|
899
|
+
3. pre-deploy
|
|
900
|
+
For final checks before deploying, e.g. checking CI completed
|
|
901
|
+
|
|
902
|
+
3. post-deploy - run after a deploy, redeploy or rollback
|
|
903
|
+
|
|
904
|
+
This hook is also passed a `MRSK_RUNTIME` env variable.
|
|
905
|
+
|
|
906
|
+
This could be used to broadcast a deployment message, or register the new version with an APM.
|
|
907
|
+
|
|
908
|
+
The command could look something like:
|
|
909
|
+
|
|
910
|
+
```bash
|
|
911
|
+
#!/usr/bin/env bash
|
|
912
|
+
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
|
|
913
|
+
```
|
|
914
|
+
|
|
915
|
+
That'll post a line like follows to a preconfigured chatbot in Basecamp:
|
|
916
|
+
|
|
917
|
+
```
|
|
918
|
+
[My App] [dhh] Rolled back to version d264c4e92470ad1bd18590f04466787262f605de
|
|
919
|
+
```
|
|
920
|
+
|
|
921
|
+
Set `--skip_hooks` to avoid running the hooks.
|
|
922
|
+
|
|
819
923
|
## Stage of development
|
|
820
924
|
|
|
821
925
|
This is beta software. Commands may still move around. But we're live in production at [37signals](https://37signals.com).
|
data/bin/mrsk
CHANGED
|
@@ -8,7 +8,7 @@ require "mrsk"
|
|
|
8
8
|
begin
|
|
9
9
|
Mrsk::Cli::Main.start(ARGV)
|
|
10
10
|
rescue SSHKit::Runner::ExecuteError => e
|
|
11
|
-
puts " \e[31mERROR (#{e.cause.class}): #{e.
|
|
11
|
+
puts " \e[31mERROR (#{e.cause.class}): #{e.message}\e[0m"
|
|
12
12
|
puts e.cause.backtrace if ENV["VERBOSE"]
|
|
13
13
|
exit 1
|
|
14
14
|
rescue => e
|
data/lib/mrsk/cli/accessory.rb
CHANGED
data/lib/mrsk/cli/app.rb
CHANGED
|
@@ -2,30 +2,38 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
|
|
|
2
2
|
desc "boot", "Boot app on servers (or reboot app if already running)"
|
|
3
3
|
def boot
|
|
4
4
|
with_lock do
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
hold_lock_on_error do
|
|
6
|
+
say "Get most recent version available as an image...", :magenta unless options[:version]
|
|
7
|
+
using_version(version_or_latest) do |version|
|
|
8
|
+
say "Start container with version #{version} using a #{MRSK.config.readiness_delay}s readiness delay (or reboot if already running)...", :magenta
|
|
9
|
+
|
|
10
|
+
on(MRSK.hosts) do
|
|
11
|
+
execute *MRSK.auditor.record("Tagging #{MRSK.config.absolute_image} as the latest image"), verbosity: :debug
|
|
12
|
+
execute *MRSK.app.tag_current_as_latest
|
|
13
|
+
end
|
|
8
14
|
|
|
9
|
-
|
|
15
|
+
on(MRSK.hosts, **MRSK.boot_strategy) do |host|
|
|
16
|
+
roles = MRSK.roles_on(host)
|
|
10
17
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
roles.each do |role|
|
|
15
|
-
execute *MRSK.auditor(role: role).record("Booted app version #{version}"), verbosity: :debug
|
|
18
|
+
roles.each do |role|
|
|
19
|
+
app = MRSK.app(role: role)
|
|
20
|
+
auditor = MRSK.auditor(role: role)
|
|
16
21
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
tmp_version = "#{version}_#{SecureRandom.hex(8)}"
|
|
22
|
+
if capture_with_info(*app.container_id_for_version(version, only_running: true), raise_on_non_zero_exit: false).present?
|
|
23
|
+
tmp_version = "#{version}_replaced_#{SecureRandom.hex(8)}"
|
|
20
24
|
info "Renaming container #{version} to #{tmp_version} as already deployed on #{host}"
|
|
21
|
-
execute *
|
|
22
|
-
execute *
|
|
25
|
+
execute *auditor.record("Renaming container #{version} to #{tmp_version}"), verbosity: :debug
|
|
26
|
+
execute *app.rename_container(version: version, new_version: tmp_version)
|
|
23
27
|
end
|
|
24
28
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
execute *
|
|
29
|
+
execute *auditor.record("Booted app version #{version}"), verbosity: :debug
|
|
30
|
+
|
|
31
|
+
old_version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
|
|
32
|
+
execute *app.start_or_run(hostname: "#{host}-#{SecureRandom.hex(6)}")
|
|
33
|
+
|
|
34
|
+
Mrsk::Utils::HealthcheckPoller.wait_for_healthy(pause_after_ready: true) { capture_with_info(*app.status(version: version)) }
|
|
35
|
+
|
|
36
|
+
execute *app.stop(version: old_version), raise_on_non_zero_exit: false if old_version.present?
|
|
29
37
|
end
|
|
30
38
|
end
|
|
31
39
|
end
|
|
@@ -54,7 +62,7 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
|
|
|
54
62
|
roles = MRSK.roles_on(host)
|
|
55
63
|
|
|
56
64
|
roles.each do |role|
|
|
57
|
-
execute *MRSK.auditor
|
|
65
|
+
execute *MRSK.auditor.record("Stopped app", role: role), verbosity: :debug
|
|
58
66
|
execute *MRSK.app(role: role).stop, raise_on_non_zero_exit: false
|
|
59
67
|
end
|
|
60
68
|
end
|
|
@@ -101,7 +109,7 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
|
|
|
101
109
|
roles = MRSK.roles_on(host)
|
|
102
110
|
|
|
103
111
|
roles.each do |role|
|
|
104
|
-
execute *MRSK.auditor
|
|
112
|
+
execute *MRSK.auditor.record("Executed cmd '#{cmd}' on app version #{version}", role: role), verbosity: :debug
|
|
105
113
|
puts_by_host host, capture_with_info(*MRSK.app(role: role).execute_in_existing_container(cmd))
|
|
106
114
|
end
|
|
107
115
|
end
|
|
@@ -124,6 +132,31 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
|
|
|
124
132
|
on(MRSK.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.list_containers) }
|
|
125
133
|
end
|
|
126
134
|
|
|
135
|
+
desc "stale_containers", "Detect app stale containers"
|
|
136
|
+
option :stop, aliases: "-s", type: :boolean, default: false, desc: "Stop the stale containers found"
|
|
137
|
+
def stale_containers
|
|
138
|
+
with_lock do
|
|
139
|
+
stop = options[:stop]
|
|
140
|
+
|
|
141
|
+
cli = self
|
|
142
|
+
|
|
143
|
+
on(MRSK.hosts) do |host|
|
|
144
|
+
roles = MRSK.roles_on(host)
|
|
145
|
+
|
|
146
|
+
roles.each do |role|
|
|
147
|
+
cli.send(:stale_versions, host: host, role: role).each do |version|
|
|
148
|
+
if stop
|
|
149
|
+
puts_by_host host, "Stopping stale container for role #{role} with version #{version}"
|
|
150
|
+
execute *MRSK.app(role: role).stop(version: version), raise_on_non_zero_exit: false
|
|
151
|
+
else
|
|
152
|
+
puts_by_host host, "Detected stale container for role #{role} with version #{version} (use `mrsk app stale_containers --stop` to stop)"
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
127
160
|
desc "images", "Show app images on servers"
|
|
128
161
|
def images
|
|
129
162
|
on(MRSK.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.list_images) }
|
|
@@ -183,7 +216,7 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
|
|
|
183
216
|
roles = MRSK.roles_on(host)
|
|
184
217
|
|
|
185
218
|
roles.each do |role|
|
|
186
|
-
execute *MRSK.auditor
|
|
219
|
+
execute *MRSK.auditor.record("Removed app container with version #{version}", role: role), verbosity: :debug
|
|
187
220
|
execute *MRSK.app(role: role).remove_container(version: version)
|
|
188
221
|
end
|
|
189
222
|
end
|
|
@@ -197,7 +230,7 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
|
|
|
197
230
|
roles = MRSK.roles_on(host)
|
|
198
231
|
|
|
199
232
|
roles.each do |role|
|
|
200
|
-
execute *MRSK.auditor
|
|
233
|
+
execute *MRSK.auditor.record("Removed all app containers", role: role), verbosity: :debug
|
|
201
234
|
execute *MRSK.app(role: role).remove_containers
|
|
202
235
|
end
|
|
203
236
|
end
|
|
@@ -240,6 +273,17 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
|
|
|
240
273
|
version.presence
|
|
241
274
|
end
|
|
242
275
|
|
|
276
|
+
def stale_versions(host:, role:)
|
|
277
|
+
versions = nil
|
|
278
|
+
on(host) do
|
|
279
|
+
versions = \
|
|
280
|
+
capture_with_info(*MRSK.app(role: role).list_versions, raise_on_non_zero_exit: false)
|
|
281
|
+
.split("\n")
|
|
282
|
+
.drop(1)
|
|
283
|
+
end
|
|
284
|
+
versions
|
|
285
|
+
end
|
|
286
|
+
|
|
243
287
|
def version_or_latest
|
|
244
288
|
options[:version] || "latest"
|
|
245
289
|
end
|
data/lib/mrsk/cli/base.rb
CHANGED
|
@@ -20,7 +20,7 @@ module Mrsk::Cli
|
|
|
20
20
|
class_option :config_file, aliases: "-c", default: "config/deploy.yml", desc: "Path to config file"
|
|
21
21
|
class_option :destination, aliases: "-d", desc: "Specify destination to be used for config file (staging -> deploy.staging.yml)"
|
|
22
22
|
|
|
23
|
-
class_option :
|
|
23
|
+
class_option :skip_hooks, aliases: "-H", type: :boolean, default: false, desc: "Don't run hooks"
|
|
24
24
|
|
|
25
25
|
def initialize(*)
|
|
26
26
|
super
|
|
@@ -72,42 +72,100 @@ module Mrsk::Cli
|
|
|
72
72
|
puts " Finished all in #{sprintf("%.1f seconds", runtime)}"
|
|
73
73
|
end
|
|
74
74
|
|
|
75
|
-
def audit_broadcast(line)
|
|
76
|
-
run_locally { execute *MRSK.auditor.broadcast(line), verbosity: :debug }
|
|
77
|
-
end
|
|
78
|
-
|
|
79
75
|
def with_lock
|
|
80
|
-
|
|
76
|
+
if MRSK.holding_lock?
|
|
77
|
+
yield
|
|
78
|
+
else
|
|
79
|
+
run_hook "pre-connect"
|
|
81
80
|
|
|
82
|
-
|
|
81
|
+
acquire_lock
|
|
82
|
+
|
|
83
|
+
begin
|
|
84
|
+
yield
|
|
85
|
+
rescue
|
|
86
|
+
if MRSK.hold_lock_on_error?
|
|
87
|
+
error " \e[31mDeploy lock was not released\e[0m"
|
|
88
|
+
else
|
|
89
|
+
release_lock
|
|
90
|
+
end
|
|
83
91
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
92
|
+
raise
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
release_lock
|
|
96
|
+
end
|
|
88
97
|
end
|
|
89
98
|
|
|
90
99
|
def acquire_lock
|
|
91
|
-
|
|
92
|
-
say "Acquiring the deploy lock"
|
|
93
|
-
on(MRSK.primary_host) { execute *MRSK.lock.acquire("Automatic deploy lock", MRSK.config.version) }
|
|
100
|
+
raise_if_locked do
|
|
101
|
+
say "Acquiring the deploy lock...", :magenta
|
|
102
|
+
on(MRSK.primary_host) { execute *MRSK.lock.acquire("Automatic deploy lock", MRSK.config.version), verbosity: :debug }
|
|
94
103
|
end
|
|
95
|
-
|
|
104
|
+
|
|
105
|
+
MRSK.holding_lock = true
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def release_lock
|
|
109
|
+
say "Releasing the deploy lock...", :magenta
|
|
110
|
+
on(MRSK.primary_host) { execute *MRSK.lock.release, verbosity: :debug }
|
|
111
|
+
|
|
112
|
+
MRSK.holding_lock = false
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def raise_if_locked
|
|
116
|
+
yield
|
|
96
117
|
rescue SSHKit::Runner::ExecuteError => e
|
|
97
118
|
if e.message =~ /cannot create directory/
|
|
98
|
-
|
|
119
|
+
on(MRSK.primary_host) { puts capture_with_debug(*MRSK.lock.status) }
|
|
99
120
|
raise LockError, "Deploy lock found"
|
|
100
121
|
else
|
|
101
122
|
raise e
|
|
102
123
|
end
|
|
103
124
|
end
|
|
104
125
|
|
|
105
|
-
def
|
|
106
|
-
MRSK.
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
126
|
+
def hold_lock_on_error
|
|
127
|
+
if MRSK.hold_lock_on_error?
|
|
128
|
+
yield
|
|
129
|
+
else
|
|
130
|
+
MRSK.hold_lock_on_error = true
|
|
131
|
+
yield
|
|
132
|
+
MRSK.hold_lock_on_error = false
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def run_hook(hook, **extra_details)
|
|
137
|
+
if !options[:skip_hooks] && MRSK.hook.hook_exists?(hook)
|
|
138
|
+
details = { hosts: MRSK.hosts.join(","), command: command, subcommand: subcommand }
|
|
139
|
+
|
|
140
|
+
say "Running the #{hook} hook...", :magenta
|
|
141
|
+
run_locally do
|
|
142
|
+
MRSK.with_verbosity(:debug) { execute *MRSK.hook.run(hook, **details, **extra_details) }
|
|
143
|
+
rescue SSHKit::Command::Failed
|
|
144
|
+
raise HookError.new("Hook `#{hook}` failed")
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def command
|
|
150
|
+
@mrsk_command ||= begin
|
|
151
|
+
invocation_class, invocation_commands = *first_invocation
|
|
152
|
+
if invocation_class == Mrsk::Cli::Main
|
|
153
|
+
invocation_commands[0]
|
|
154
|
+
else
|
|
155
|
+
Mrsk::Cli::Main.subcommand_classes.find { |command, clazz| clazz == invocation_class }[0]
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def subcommand
|
|
161
|
+
@mrsk_subcommand ||= begin
|
|
162
|
+
invocation_class, invocation_commands = *first_invocation
|
|
163
|
+
invocation_commands[0] if invocation_class != Mrsk::Cli::Main
|
|
110
164
|
end
|
|
111
165
|
end
|
|
112
|
-
|
|
166
|
+
|
|
167
|
+
def first_invocation
|
|
168
|
+
instance_variable_get("@_invocations").first
|
|
169
|
+
end
|
|
170
|
+
end
|
|
113
171
|
end
|
data/lib/mrsk/cli/build.rb
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
class Mrsk::Cli::Build < Mrsk::Cli::Base
|
|
2
|
+
class BuildError < StandardError; end
|
|
3
|
+
|
|
2
4
|
desc "deliver", "Build app and push app image to registry then pull image on servers"
|
|
3
5
|
def deliver
|
|
4
6
|
with_lock do
|
|
@@ -12,6 +14,9 @@ class Mrsk::Cli::Build < Mrsk::Cli::Base
|
|
|
12
14
|
with_lock do
|
|
13
15
|
cli = self
|
|
14
16
|
|
|
17
|
+
verify_local_dependencies
|
|
18
|
+
run_hook "pre-build"
|
|
19
|
+
|
|
15
20
|
run_locally do
|
|
16
21
|
begin
|
|
17
22
|
MRSK.with_verbosity(:debug) { execute *MRSK.builder.push }
|
|
@@ -77,4 +82,19 @@ class Mrsk::Cli::Build < Mrsk::Cli::Base
|
|
|
77
82
|
puts capture(*MRSK.builder.info)
|
|
78
83
|
end
|
|
79
84
|
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
def verify_local_dependencies
|
|
88
|
+
run_locally do
|
|
89
|
+
begin
|
|
90
|
+
execute *MRSK.builder.ensure_local_dependencies_installed
|
|
91
|
+
rescue SSHKit::Command::Failed => e
|
|
92
|
+
build_error = e.message =~ /command not found/ ?
|
|
93
|
+
"Docker is not installed locally" :
|
|
94
|
+
"Docker buildx plugin is not installed locally"
|
|
95
|
+
|
|
96
|
+
raise BuildError, build_error
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
80
100
|
end
|