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
@@ -1,7 +1,4 @@
1
1
  class Mrsk::Cli::Healthcheck < Mrsk::Cli::Base
2
-
3
- class HealthcheckError < StandardError; end
4
-
5
2
  default_command :perform
6
3
 
7
4
  desc "perform", "Health check current app version"
@@ -9,38 +6,11 @@ class Mrsk::Cli::Healthcheck < Mrsk::Cli::Base
9
6
  on(MRSK.primary_host) do
10
7
  begin
11
8
  execute *MRSK.healthcheck.run
12
-
13
- target = "Health check against #{MRSK.config.healthcheck["path"]}"
14
- attempt = 1
15
- max_attempts = MRSK.config.healthcheck["max_attempts"]
16
-
17
- begin
18
- status = capture_with_info(*MRSK.healthcheck.curl)
19
-
20
- if status == "200"
21
- info "#{target} succeeded with 200 OK!"
22
- else
23
- raise HealthcheckError, "#{target} failed with status #{status}"
24
- end
25
- rescue SSHKit::Command::Failed
26
- if attempt <= max_attempts
27
- info "#{target} failed to respond, retrying in #{attempt}s (attempt #{attempt}/#{max_attempts})..."
28
- sleep attempt
29
- attempt += 1
30
-
31
- retry
32
- else
33
- raise
34
- end
35
- end
36
- rescue SSHKit::Command::Failed, HealthcheckError => e
9
+ Mrsk::Utils::HealthcheckPoller.wait_for_healthy { capture_with_info(*MRSK.healthcheck.status) }
10
+ rescue Mrsk::Utils::HealthcheckPoller::HealthcheckError => e
37
11
  error capture_with_info(*MRSK.healthcheck.logs)
38
-
39
- if e.message =~ /curl/
40
- raise SSHKit::Command::Failed, "#{target} failed to return 200 OK!"
41
- else
42
- raise
43
- end
12
+ error capture_with_pretty_json(*MRSK.healthcheck.container_health_log)
13
+ raise
44
14
  ensure
45
15
  execute *MRSK.healthcheck.stop, raise_on_non_zero_exit: false
46
16
  execute *MRSK.healthcheck.remove, raise_on_non_zero_exit: false
data/lib/mrsk/cli/lock.rb CHANGED
@@ -2,16 +2,16 @@ class Mrsk::Cli::Lock < Mrsk::Cli::Base
2
2
  desc "status", "Report lock status"
3
3
  def status
4
4
  handle_missing_lock do
5
- on(MRSK.primary_host) { puts capture_with_info(*MRSK.lock.status) }
5
+ on(MRSK.primary_host) { puts capture_with_debug(*MRSK.lock.status) }
6
6
  end
7
7
  end
8
8
 
9
9
  desc "acquire", "Acquire the deploy lock"
10
- option :message, aliases: "-m", type: :string, desc: "A lock mesasge", required: true
10
+ option :message, aliases: "-m", type: :string, desc: "A lock message", required: true
11
11
  def acquire
12
12
  message = options[:message]
13
- handle_missing_lock do
14
- on(MRSK.primary_host) { execute *MRSK.lock.acquire(message, MRSK.config.version) }
13
+ raise_if_locked do
14
+ on(MRSK.primary_host) { execute *MRSK.lock.acquire(message, MRSK.config.version), verbosity: :debug }
15
15
  say "Acquired the deploy lock"
16
16
  end
17
17
  end
@@ -19,7 +19,7 @@ class Mrsk::Cli::Lock < Mrsk::Cli::Base
19
19
  desc "release", "Release the deploy lock"
20
20
  def release
21
21
  handle_missing_lock do
22
- on(MRSK.primary_host) { execute *MRSK.lock.release }
22
+ on(MRSK.primary_host) { execute *MRSK.lock.release, verbosity: :debug }
23
23
  say "Released the deploy lock"
24
24
  end
25
25
  end
data/lib/mrsk/cli/main.rb CHANGED
@@ -1,8 +1,8 @@
1
1
  class Mrsk::Cli::Main < Mrsk::Cli::Base
2
2
  desc "setup", "Setup all accessories and deploy app to servers"
3
3
  def setup
4
- with_lock do
5
- print_runtime do
4
+ print_runtime do
5
+ with_lock do
6
6
  invoke "mrsk:cli:server:bootstrap"
7
7
  invoke "mrsk:cli:accessory:boot", [ "all" ]
8
8
  deploy
@@ -13,12 +13,9 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
13
13
  desc "deploy", "Deploy app to servers"
14
14
  option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip image build and push"
15
15
  def deploy
16
- with_lock do
17
- invoke_options = deploy_options
18
-
19
- runtime = print_runtime do
20
- say "Ensure curl and Docker are installed...", :magenta
21
- invoke "mrsk:cli:server:bootstrap", [], invoke_options
16
+ runtime = print_runtime do
17
+ with_lock do
18
+ invoke_options = deploy_options
22
19
 
23
20
  say "Log into image registry...", :magenta
24
21
  invoke "mrsk:cli:registry:login", [], invoke_options
@@ -31,29 +28,34 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
31
28
  invoke "mrsk:cli:build:deliver", [], invoke_options
32
29
  end
33
30
 
31
+ run_hook "pre-deploy"
32
+
34
33
  say "Ensure Traefik is running...", :magenta
35
34
  invoke "mrsk:cli:traefik:boot", [], invoke_options
36
35
 
37
36
  say "Ensure app can pass healthcheck...", :magenta
38
37
  invoke "mrsk:cli:healthcheck:perform", [], invoke_options
39
38
 
39
+ say "Detect stale containers...", :magenta
40
+ invoke "mrsk:cli:app:stale_containers", [], invoke_options
41
+
40
42
  invoke "mrsk:cli:app:boot", [], invoke_options
41
43
 
42
44
  say "Prune old containers and images...", :magenta
43
45
  invoke "mrsk:cli:prune:all", [], invoke_options
44
46
  end
45
-
46
- audit_broadcast "Deployed #{service_version} in #{runtime.round} seconds" unless options[:skip_broadcast]
47
47
  end
48
+
49
+ run_hook "post-deploy", runtime: runtime.round
48
50
  end
49
51
 
50
52
  desc "redeploy", "Deploy app to servers without bootstrapping servers, starting Traefik, pruning, and registry login"
51
53
  option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip image build and push"
52
54
  def redeploy
53
- with_lock do
54
- invoke_options = deploy_options
55
+ runtime = print_runtime do
56
+ with_lock do
57
+ invoke_options = deploy_options
55
58
 
56
- runtime = print_runtime do
57
59
  if options[:skip_push]
58
60
  say "Pull app image...", :magenta
59
61
  invoke "mrsk:cli:build:pull", [], invoke_options
@@ -62,49 +64,43 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
62
64
  invoke "mrsk:cli:build:deliver", [], invoke_options
63
65
  end
64
66
 
67
+ run_hook "pre-deploy"
68
+
65
69
  say "Ensure app can pass healthcheck...", :magenta
66
70
  invoke "mrsk:cli:healthcheck:perform", [], invoke_options
67
71
 
72
+ say "Detect stale containers...", :magenta
73
+ invoke "mrsk:cli:app:stale_containers", [], invoke_options
74
+
68
75
  invoke "mrsk:cli:app:boot", [], invoke_options
69
76
  end
70
-
71
- audit_broadcast "Redeployed #{service_version} in #{runtime.round} seconds" unless options[:skip_broadcast]
72
77
  end
78
+
79
+ run_hook "post-deploy", runtime: runtime.round
73
80
  end
74
81
 
75
82
  desc "rollback [VERSION]", "Rollback app to VERSION"
76
83
  def rollback(version)
77
- with_lock do
78
- MRSK.config.version = version
84
+ rolled_back = false
85
+ runtime = print_runtime do
86
+ with_lock do
87
+ invoke_options = deploy_options
79
88
 
80
- if container_available?(version)
81
- say "Start version #{version}, then wait #{MRSK.config.readiness_delay}s for app to boot before stopping the old version...", :magenta
82
-
83
- cli = self
89
+ MRSK.config.version = version
84
90
  old_version = nil
85
91
 
86
- on(MRSK.hosts) do |host|
87
- roles = MRSK.roles_on(host)
88
-
89
- roles.each do |role|
90
- app = MRSK.app(role: role)
91
- old_version = capture_with_info(*app.current_running_version).strip.presence
92
+ if container_available?(version)
93
+ run_hook "pre-deploy"
92
94
 
93
- execute *app.start
94
-
95
- if old_version
96
- sleep MRSK.config.readiness_delay
97
-
98
- execute *app.stop(version: old_version), raise_on_non_zero_exit: false
99
- end
100
- end
95
+ invoke "mrsk:cli:app:boot", [], invoke_options.merge(version: version)
96
+ rolled_back = true
97
+ else
98
+ say "The app version '#{version}' is not available as a container (use 'mrsk app containers' for available versions)", :red
101
99
  end
102
-
103
- audit_broadcast "Rolled back #{service_version(Mrsk::Utils.abbreviate_version(old_version))} to #{service_version}" unless options[:skip_broadcast]
104
- else
105
- say "The app version '#{version}' is not available as a container (use 'mrsk app containers' for available versions)", :red
106
100
  end
107
101
  end
102
+
103
+ run_hook "post-deploy", runtime: runtime.round if rolled_back
108
104
  end
109
105
 
110
106
  desc "details", "Show details about all containers"
@@ -146,6 +142,14 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
146
142
  puts "Created .env file"
147
143
  end
148
144
 
145
+ unless (hooks_dir = Pathname.new(File.expand_path(".mrsk/hooks"))).exist?
146
+ hooks_dir.mkpath
147
+ Pathname.new(File.expand_path("templates/sample_hooks", __dir__)).each_child do |sample_hook|
148
+ FileUtils.cp sample_hook, hooks_dir, preserve: true
149
+ end
150
+ puts "Created sample hooks in .mrsk/hooks"
151
+ end
152
+
149
153
  if options[:bundle]
150
154
  if (binstub = Pathname.new(File.expand_path("bin/mrsk"))).exist?
151
155
  puts "Binstub already exists in bin/mrsk (remove first to create a new one)"
@@ -203,6 +207,9 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
203
207
  desc "healthcheck", "Healthcheck application"
204
208
  subcommand "healthcheck", Mrsk::Cli::Healthcheck
205
209
 
210
+ desc "lock", "Manage the deploy lock"
211
+ subcommand "lock", Mrsk::Cli::Lock
212
+
206
213
  desc "prune", "Prune old application images and containers"
207
214
  subcommand "prune", Mrsk::Cli::Prune
208
215
 
@@ -215,26 +222,28 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
215
222
  desc "traefik", "Manage Traefik load balancer"
216
223
  subcommand "traefik", Mrsk::Cli::Traefik
217
224
 
218
- desc "lock", "Manage the deploy lock"
219
- subcommand "lock", Mrsk::Cli::Lock
220
-
221
225
  private
222
- def container_available?(version, host: MRSK.primary_host)
223
- available = nil
224
-
225
- on(host) do
226
- first_role = MRSK.roles_on(host).first
227
- available = capture_with_info(*MRSK.app(role: first_role).container_id_for_version(version)).present?
226
+ def container_available?(version)
227
+ begin
228
+ on(MRSK.hosts) do
229
+ MRSK.roles_on(host).each do |role|
230
+ container_id = capture_with_info(*MRSK.app(role: role).container_id_for_version(version))
231
+ raise "Container not found" unless container_id.present?
232
+ end
233
+ end
234
+ rescue SSHKit::Runner::ExecuteError => e
235
+ if e.message =~ /Container not found/
236
+ say "Error looking for container version #{version}: #{e.message}"
237
+ return false
238
+ else
239
+ raise
240
+ end
228
241
  end
229
242
 
230
- available
243
+ true
231
244
  end
232
245
 
233
246
  def deploy_options
234
247
  { "version" => MRSK.config.version }.merge(options.without("skip_push"))
235
248
  end
236
-
237
- def service_version(version = MRSK.config.abbreviated_version)
238
- [ MRSK.config.service, version ].compact.join("@")
239
- end
240
249
  end
@@ -7,17 +7,18 @@ class Mrsk::Cli::Prune < Mrsk::Cli::Base
7
7
  end
8
8
  end
9
9
 
10
- desc "images", "Prune unused images older than 7 days"
10
+ desc "images", "Prune dangling images"
11
11
  def images
12
12
  with_lock do
13
13
  on(MRSK.hosts) do
14
14
  execute *MRSK.auditor.record("Pruned images"), verbosity: :debug
15
- execute *MRSK.prune.images
15
+ execute *MRSK.prune.dangling_images
16
+ execute *MRSK.prune.tagged_images
16
17
  end
17
18
  end
18
19
  end
19
20
 
20
- desc "containers", "Prune stopped containers older than 3 days"
21
+ desc "containers", "Prune all stopped containers, except the last 5"
21
22
  def containers
22
23
  with_lock do
23
24
  on(MRSK.hosts) do
@@ -1,17 +1,21 @@
1
1
  class Mrsk::Cli::Server < Mrsk::Cli::Base
2
- desc "bootstrap", "Ensure curl and Docker are installed on servers"
2
+ desc "bootstrap", "Set up Docker to run MRSK apps"
3
3
  def bootstrap
4
- with_lock do
5
- on(MRSK.hosts + MRSK.accessory_hosts) do
6
- dependencies_to_install = Array.new.tap do |dependencies|
7
- dependencies << "curl" unless execute "which curl", raise_on_non_zero_exit: false
8
- dependencies << "docker.io" unless execute "which docker", raise_on_non_zero_exit: false
9
- end
4
+ missing = []
10
5
 
11
- if dependencies_to_install.any?
12
- execute "apt-get update -y && apt-get install #{dependencies_to_install.join(" ")} -y"
6
+ on(MRSK.hosts | MRSK.accessory_hosts) do |host|
7
+ unless execute(*MRSK.docker.installed?, raise_on_non_zero_exit: false)
8
+ if execute(*MRSK.docker.superuser?, raise_on_non_zero_exit: false)
9
+ info "Missing Docker on #{host}. Installing…"
10
+ execute *MRSK.docker.install
11
+ else
12
+ missing << host
13
13
  end
14
14
  end
15
15
  end
16
+
17
+ if missing.any?
18
+ raise "Docker is not installed on #{missing.join(", ")} and can't be automatically installed without having root access and the `curl` command available. Install Docker manually: https://docs.docker.com/engine/install/"
19
+ end
16
20
  end
17
21
  end
@@ -25,10 +25,6 @@ registry:
25
25
  # secret:
26
26
  # - RAILS_MASTER_KEY
27
27
 
28
- # Call a broadcast command on deploys.
29
- # audit_broadcast_cmd:
30
- # bin/broadcast_to_bc
31
-
32
28
  # Use a different ssh user than root
33
29
  # ssh:
34
30
  # user: app
@@ -0,0 +1,14 @@
1
+ #!/bin/sh
2
+
3
+ # A sample post-deploy hook
4
+ #
5
+ # These environment variables are available:
6
+ # MRSK_RECORDED_AT
7
+ # MRSK_PERFORMER
8
+ # MRSK_VERSION
9
+ # MRSK_HOSTS
10
+ # MRSK_ROLE (if set)
11
+ # MRSK_DESTINATION (if set)
12
+ # MRSK_RUNTIME
13
+
14
+ echo "$MRSK_PERFORMER deployed $MRSK_VERSION to $MRSK_DESTINATION in $MRSK_RUNTIME seconds"
@@ -0,0 +1,51 @@
1
+ #!/bin/sh
2
+
3
+ # A sample pre-build hook
4
+ #
5
+ # Checks:
6
+ # 1. We have a clean checkout
7
+ # 2. A remote is configured
8
+ # 3. The branch has been pushed to the remote
9
+ # 4. The version we are deploying matches the remote
10
+ #
11
+ # These environment variables are available:
12
+ # MRSK_RECORDED_AT
13
+ # MRSK_PERFORMER
14
+ # MRSK_VERSION
15
+ # MRSK_HOSTS
16
+ # MRSK_ROLE (if set)
17
+ # MRSK_DESTINATION (if set)
18
+
19
+ if [ -n "$(git status --porcelain)" ]; then
20
+ echo "Git checkout is not clean, aborting..." >&2
21
+ git status --porcelain >&2
22
+ exit 1
23
+ fi
24
+
25
+ first_remote=$(git remote)
26
+
27
+ if [ -z "$first_remote" ]; then
28
+ echo "No git remote set, aborting..." >&2
29
+ exit 1
30
+ fi
31
+
32
+ current_branch=$(git branch --show-current)
33
+
34
+ if [ -z "$current_branch" ]; then
35
+ echo "No git remote set, aborting..." >&2
36
+ exit 1
37
+ fi
38
+
39
+ remote_head=$(git ls-remote $first_remote --tags $current_branch | cut -f1)
40
+
41
+ if [ -z "$remote_head" ]; then
42
+ echo "Branch not pushed to remote, aborting..." >&2
43
+ exit 1
44
+ fi
45
+
46
+ if [ "$MRSK_VERSION" != "$remote_head" ]; then
47
+ echo "Version ($MRSK_VERSION) does not match remote HEAD ($remote_head), aborting..." >&2
48
+ exit 1
49
+ fi
50
+
51
+ exit 0
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # A sample pre-connect check
4
+ #
5
+ # Warms DNS before connecting to hosts in parallel
6
+ #
7
+ # These environment variables are available:
8
+ # MRSK_RECORDED_AT
9
+ # MRSK_PERFORMER
10
+ # MRSK_VERSION
11
+ # MRSK_HOSTS
12
+ # MRSK_ROLE (if set)
13
+ # MRSK_DESTINATION (if set)
14
+ # MRSK_RUNTIME
15
+
16
+ hosts = ENV["MRSK_HOSTS"].split(",")
17
+ results = nil
18
+ max = 3
19
+
20
+ elapsed = Benchmark.realtime do
21
+ results = hosts.map do |host|
22
+ Thread.new do
23
+ tries = 1
24
+
25
+ begin
26
+ Socket.getaddrinfo(host, 0, Socket::AF_UNSPEC, Socket::SOCK_STREAM, nil, Socket::AI_CANONNAME)
27
+ rescue SocketError
28
+ if tries < max
29
+ puts "Retrying DNS warmup: #{host}"
30
+ tries += 1
31
+ sleep rand
32
+ retry
33
+ else
34
+ puts "DNS warmup failed: #{host}"
35
+ host
36
+ end
37
+ end
38
+
39
+ tries
40
+ end
41
+ end.map(&:value)
42
+ end
43
+
44
+ retries = results.sum - hosts.size
45
+ nopes = results.count { |r| r == max }
46
+
47
+ puts "Prewarmed %d DNS lookups in %.2f sec: %d retries, %d failures" % [ hosts.size, elapsed, retries, nopes ]
@@ -0,0 +1,82 @@
1
+ #!/bin/sh
2
+
3
+ # A sample pre-deploy hook
4
+ #
5
+ # Checks the Github status of the build, waiting for a pending build to complete for up to 720 seconds.
6
+ #
7
+ # Fails unless the combined status is "success"
8
+ #
9
+ # These environment variables are available:
10
+ # MRSK_RECORDED_AT
11
+ # MRSK_PERFORMER
12
+ # MRSK_VERSION
13
+ # MRSK_HOSTS
14
+ # MRSK_COMMAND
15
+ # MRSK_SUBCOMMAND
16
+ # MRSK_ROLE (if set)
17
+ # MRSK_DESTINATION (if set)
18
+
19
+ #!/usr/bin/env ruby
20
+
21
+ # Only check the build status for production deployments
22
+ if ENV["MRSK_COMMAND"] == "rollback" || ENV["MRSK_DESTINATION"] != "production"
23
+ exit 0
24
+ end
25
+
26
+ require "bundler/inline"
27
+
28
+ # true = install gems so this is fast on repeat invocations
29
+ gemfile(true, quiet: true) do
30
+ source "https://rubygems.org"
31
+
32
+ gem "octokit"
33
+ gem "faraday-retry"
34
+ end
35
+
36
+ MAX_ATTEMPTS = 72
37
+ ATTEMPTS_GAP = 10
38
+
39
+ def exit_with_error(message)
40
+ $stderr.puts message
41
+ exit 1
42
+ end
43
+
44
+ def first_status_url(combined_status, state)
45
+ first_status = combined_status[:statuses].find { |status| status[:state] == state }
46
+ first_status && first_status[:target_url]
47
+ end
48
+
49
+ remote_url = `git config --get remote.origin.url`.strip.delete_prefix("https://github.com/")
50
+ git_sha = `git rev-parse HEAD`.strip
51
+
52
+ repository = Octokit::Repository.from_url(remote_url)
53
+ github_client = Octokit::Client.new(access_token: ENV["GITHUB_TOKEN"])
54
+ attempts = 0
55
+
56
+ begin
57
+ loop do
58
+ combined_status = github_client.combined_status(remote_url, git_sha)
59
+ state = combined_status[:state]
60
+ first_status_url = first_status_url(combined_status, state)
61
+
62
+ case state
63
+ when "success"
64
+ puts "Build passed, see #{first_status_url}"
65
+ exit 0
66
+ when "failure"
67
+ exit_with_error "Build failed, see #{first_status_url}"
68
+ when "pending"
69
+ attempts += 1
70
+ end
71
+
72
+ puts "Waiting #{ATTEMPTS_GAP} more seconds for build to complete#{", see #{first_status_url}" if first_status_url}..."
73
+
74
+ if attempts == MAX_ATTEMPTS
75
+ exit_with_error "Build status is still pending, gave up after #{MAX_ATTEMPTS * ATTEMPTS_GAP} seconds"
76
+ end
77
+
78
+ sleep(ATTEMPTS_GAP)
79
+ end
80
+ rescue Octokit::NotFound
81
+ exit_with_error "Build status could not be found"
82
+ end
@@ -94,7 +94,7 @@ class Mrsk::Cli::Traefik < Mrsk::Cli::Base
94
94
  end
95
95
  end
96
96
 
97
- desc "remove_container", "Remove Traefik image from servers", hide: true
97
+ desc "remove_image", "Remove Traefik image from servers", hide: true
98
98
  def remove_image
99
99
  with_lock do
100
100
  on(MRSK.traefik_hosts) do
data/lib/mrsk/cli.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  module Mrsk::Cli
2
2
  class LockError < StandardError; end
3
+ class HookError < StandardError; end
3
4
  end
4
5
 
5
6
  # SSHKit uses instance eval, so we need a global const for ergonomics
@@ -2,11 +2,12 @@ require "active_support/core_ext/enumerable"
2
2
  require "active_support/core_ext/module/delegation"
3
3
 
4
4
  class Mrsk::Commander
5
- attr_accessor :verbosity, :lock_count
5
+ attr_accessor :verbosity, :holding_lock, :hold_lock_on_error
6
6
 
7
7
  def initialize
8
8
  self.verbosity = :info
9
- self.lock_count = 0
9
+ self.holding_lock = false
10
+ self.hold_lock_on_error = false
10
11
  end
11
12
 
12
13
  def config
@@ -35,7 +36,7 @@ class Mrsk::Commander
35
36
  end
36
37
 
37
38
  def primary_host
38
- specific_hosts&.first || config.primary_web_host
39
+ specific_hosts&.first || specific_roles&.first&.primary_host || config.primary_web_host
39
40
  end
40
41
 
41
42
  def roles
@@ -50,6 +51,14 @@ class Mrsk::Commander
50
51
  end
51
52
  end
52
53
 
54
+ def boot_strategy
55
+ if config.boot.limit.present?
56
+ { in: :groups, limit: config.boot.limit, wait: config.boot.wait }
57
+ else
58
+ {}
59
+ end
60
+ end
61
+
53
62
  def roles_on(host)
54
63
  roles.select { |role| role.hosts.include?(host.to_s) }.map(&:name)
55
64
  end
@@ -75,18 +84,30 @@ class Mrsk::Commander
75
84
  Mrsk::Commands::Accessory.new(config, name: name)
76
85
  end
77
86
 
78
- def auditor(role: nil)
79
- Mrsk::Commands::Auditor.new(config, role: role)
87
+ def auditor(**details)
88
+ Mrsk::Commands::Auditor.new(config, **details)
80
89
  end
81
90
 
82
91
  def builder
83
92
  @builder ||= Mrsk::Commands::Builder.new(config)
84
93
  end
85
94
 
95
+ def docker
96
+ @docker ||= Mrsk::Commands::Docker.new(config)
97
+ end
98
+
86
99
  def healthcheck
87
100
  @healthcheck ||= Mrsk::Commands::Healthcheck.new(config)
88
101
  end
89
102
 
103
+ def hook
104
+ @hook ||= Mrsk::Commands::Hook.new(config)
105
+ end
106
+
107
+ def lock
108
+ @lock ||= Mrsk::Commands::Lock.new(config)
109
+ end
110
+
90
111
  def prune
91
112
  @prune ||= Mrsk::Commands::Prune.new(config)
92
113
  end
@@ -99,10 +120,6 @@ class Mrsk::Commander
99
120
  @traefik ||= Mrsk::Commands::Traefik.new(config)
100
121
  end
101
122
 
102
- def lock
103
- @lock ||= Mrsk::Commands::Lock.new(config)
104
- end
105
-
106
123
  def with_verbosity(level)
107
124
  old_level = self.verbosity
108
125
 
@@ -115,6 +132,14 @@ class Mrsk::Commander
115
132
  SSHKit.config.output_verbosity = old_level
116
133
  end
117
134
 
135
+ def holding_lock?
136
+ self.holding_lock
137
+ end
138
+
139
+ def hold_lock_on_error?
140
+ self.hold_lock_on_error
141
+ end
142
+
118
143
  private
119
144
  # Lazy setup of SSHKit
120
145
  def configure_sshkit_with(config)