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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +141 -37
  3. data/bin/mrsk +1 -1
  4. data/lib/mrsk/cli/accessory.rb +0 -2
  5. data/lib/mrsk/cli/app.rb +66 -22
  6. data/lib/mrsk/cli/base.rb +80 -22
  7. data/lib/mrsk/cli/build.rb +20 -0
  8. data/lib/mrsk/cli/healthcheck.rb +4 -34
  9. data/lib/mrsk/cli/lock.rb +5 -5
  10. data/lib/mrsk/cli/main.rb +62 -53
  11. data/lib/mrsk/cli/prune.rb +4 -3
  12. data/lib/mrsk/cli/server.rb +13 -9
  13. data/lib/mrsk/cli/templates/deploy.yml +0 -4
  14. data/lib/mrsk/cli/templates/sample_hooks/post-deploy.sample +14 -0
  15. data/lib/mrsk/cli/templates/sample_hooks/pre-build.sample +51 -0
  16. data/lib/mrsk/cli/templates/sample_hooks/pre-connect.sample +47 -0
  17. data/lib/mrsk/cli/templates/sample_hooks/pre-deploy.sample +82 -0
  18. data/lib/mrsk/cli/traefik.rb +1 -1
  19. data/lib/mrsk/cli.rb +1 -0
  20. data/lib/mrsk/commander.rb +34 -9
  21. data/lib/mrsk/commands/app.rb +37 -15
  22. data/lib/mrsk/commands/auditor.rb +7 -36
  23. data/lib/mrsk/commands/base.rb +9 -2
  24. data/lib/mrsk/commands/builder/base.rb +9 -2
  25. data/lib/mrsk/commands/builder.rb +21 -1
  26. data/lib/mrsk/commands/docker.rb +21 -0
  27. data/lib/mrsk/commands/healthcheck.rb +7 -2
  28. data/lib/mrsk/commands/hook.rb +14 -0
  29. data/lib/mrsk/commands/lock.rb +2 -2
  30. data/lib/mrsk/commands/prune.rb +30 -4
  31. data/lib/mrsk/commands/traefik.rb +12 -1
  32. data/lib/mrsk/configuration/boot.rb +20 -0
  33. data/lib/mrsk/configuration/role.rb +29 -2
  34. data/lib/mrsk/configuration.rb +15 -9
  35. data/lib/mrsk/sshkit_with_ext.rb +46 -2
  36. data/lib/mrsk/tags.rb +39 -0
  37. data/lib/mrsk/utils/healthcheck_poller.rb +39 -0
  38. data/lib/mrsk/utils.rb +13 -2
  39. data/lib/mrsk/version.rb +1 -1
  40. metadata +11 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3a8c9aac5626518667d963bd8c0b8e757be3dfe2cfed585f1e7d0783e2e017a4
4
- data.tar.gz: 9e7c336b776e518d98a3a25a7a3c47858cf40f2d8398c881d8c3ec3f224d1222
3
+ metadata.gz: 3a752ca316aea9ce851cc9b4ac96cd7fde613f94dd1b63e238c3320b10f82e8c
4
+ data.tar.gz: 689bba95ccc72b27b9cc85572b6e4d995f6bfed3d09be30c2076a81ed1eb07f6
5
5
  SHA512:
6
- metadata.gz: 3889961797501b12da9ca958021b98a5a2b1167943813156241e5da44cc35b3d6ace84b56f4850b50e351cd995be099821d791e2136001f37ba700f8310e5128
7
- data.tar.gz: a15e1ad08dfc1c8471f0fa341d865ffc237d88aae229cd250b49feb1d4ca10e750ddc2dec85c392974017c13c6d7b2f583fb3af85e840d5dddcbed19c8f90712
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 deploy
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, 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.
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
- volumes:
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 lables
545
+ ### Traefik container labels
518
546
 
519
547
  Add labels to Traefik Docker container.
520
548
 
521
549
  ```yaml
522
550
  traefik:
523
- lables:
524
- - traefik.enable: true
525
- - traefik.http.routers.dashboard.rule: Host(`traefik.example.com`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))
526
- - traefik.http.routers.dashboard.service: api@internal
527
- - traefik.http.routers.dashboard.middlewares: auth
528
- - traefik.http.middlewares.auth.basicauth.users: test:$2y$05$H2o72tMaO.TwY1wNQUV1K.fhjRgLHRDWohFvUZOJHBEtUXNKrqUKi # test:password
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
- ### Using audit broadcasts
671
+ ### Healthcheck
644
672
 
645
- 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:
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
- audit_broadcast_cmd:
649
- bin/audit_broadcast
678
+ healthcheck:
679
+ path: /healthz
680
+ port: 4000
681
+ max_attempts: 7
682
+ interval: 20s
650
683
  ```
651
684
 
652
- The broadcast command could look something like:
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
- That'll post a line like follows to a preconfigured chatbot in Basecamp:
687
+ You can also specify a custom healthcheck command, which is useful for non-HTTP services:
660
688
 
661
- ```
662
- [My App] [dhh] Rolled back to version d264c4e92470ad1bd18590f04466787262f605de
689
+ ```yaml
690
+ healthcheck:
691
+ cmd: /bin/check_health
663
692
  ```
664
693
 
665
- ### Custom healthcheck
666
-
667
- MRSK defaults to checking the health of your application again `/up` on port 3000 up to 7 times. You can tailor the behaviour with the `healthcheck` setting:
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
- healthcheck:
671
- path: /healthz
672
- port: 4000
673
- max_attempts: 7
699
+ servers:
700
+ job:
701
+ hosts: ...
702
+ cmd: bin/jobs
703
+ healthcheck:
704
+ cmd: bin/check
674
705
  ```
675
706
 
676
- 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.
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 healthcheck also allows for an optional `max_attempts` setting, which will attempt the healthcheck up to the specified number of times before failing the deploy. This is useful for applications that take a while to start up. The default is 7.
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 maintanence"
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.cause.message}\e[0m"
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
@@ -14,8 +14,6 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
14
14
  execute *MRSK.auditor.record("Booted #{name} accessory"), verbosity: :debug
15
15
  execute *accessory.run
16
16
  end
17
-
18
- audit_broadcast "Booted accessory #{name}" unless options[:skip_broadcast]
19
17
  end
20
18
  end
21
19
  end
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
- say "Get most recent version available as an image...", :magenta unless options[:version]
6
- using_version(version_or_latest) do |version|
7
- say "Start container with version #{version} using a #{MRSK.config.readiness_delay}s readiness delay (or reboot if already running)...", :magenta
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
- cli = self
15
+ on(MRSK.hosts, **MRSK.boot_strategy) do |host|
16
+ roles = MRSK.roles_on(host)
10
17
 
11
- on(MRSK.hosts) do |host|
12
- roles = MRSK.roles_on(host)
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
- begin
18
- if capture_with_info(*MRSK.app(role: role).container_id_for_version(version)).present?
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 *MRSK.auditor(role: role).record("Renaming container #{version} to #{tmp_version}"), verbosity: :debug
22
- execute *MRSK.app(role: role).rename_container(version: version, new_version: tmp_version)
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
- old_version = capture_with_info(*MRSK.app(role: role).current_running_version).strip
26
- execute *MRSK.app(role: role).run
27
- sleep MRSK.config.readiness_delay
28
- execute *MRSK.app(role: role).stop(version: old_version), raise_on_non_zero_exit: false if old_version.present?
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(role: role).record("Stopped app"), verbosity: :debug
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(role: role).record("Executed cmd '#{cmd}' on app version #{version}"), verbosity: :debug
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(role: role).record("Removed app container with version #{version}"), verbosity: :debug
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(role: role).record("Removed all app containers"), verbosity: :debug
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 :skip_broadcast, aliases: "-B", type: :boolean, default: false, desc: "Skip audit broadcasts"
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
- acquire_lock
76
+ if MRSK.holding_lock?
77
+ yield
78
+ else
79
+ run_hook "pre-connect"
81
80
 
82
- yield
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
- release_lock
85
- rescue
86
- error " \e[31mDeploy lock was not released\e[0m" if MRSK.lock_count > 0
87
- raise
92
+ raise
93
+ end
94
+
95
+ release_lock
96
+ end
88
97
  end
89
98
 
90
99
  def acquire_lock
91
- if MRSK.lock_count == 0
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
- MRSK.lock_count += 1
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
- invoke "mrsk:cli:lock:status", []
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 release_lock
106
- MRSK.lock_count -= 1
107
- if MRSK.lock_count == 0
108
- say "Releasing the deploy lock"
109
- on(MRSK.primary_host) { execute *MRSK.lock.release }
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
- end
166
+
167
+ def first_invocation
168
+ instance_variable_get("@_invocations").first
169
+ end
170
+ end
113
171
  end
@@ -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