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,4 +1,6 @@
1
1
  class Mrsk::Commands::App < Mrsk::Commands::Base
2
+ ACTIVE_DOCKER_STATUSES = [ :running, :restarting ]
3
+
2
4
  attr_reader :role
3
5
 
4
6
  def initialize(config, role: nil)
@@ -6,15 +8,21 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
6
8
  @role = role
7
9
  end
8
10
 
9
- def run
11
+ def start_or_run(hostname: nil)
12
+ combine start, run(hostname: hostname), by: "||"
13
+ end
14
+
15
+ def run(hostname: nil)
10
16
  role = config.role(self.role)
11
17
 
12
18
  docker :run,
13
19
  "--detach",
14
20
  "--restart unless-stopped",
15
21
  "--name", container_name,
22
+ *(["--hostname", hostname] if hostname),
16
23
  "-e", "MRSK_CONTAINER_NAME=\"#{container_name}\"",
17
24
  *role.env_args,
25
+ *role.health_check_args,
18
26
  *config.logging_args,
19
27
  *config.volume_args,
20
28
  *role.label_args,
@@ -27,9 +35,13 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
27
35
  docker :start, container_name
28
36
  end
29
37
 
38
+ def status(version:)
39
+ pipe container_id_for_version(version), xargs(docker(:inspect, "--format", DOCKER_HEALTH_STATUS_FORMAT))
40
+ end
41
+
30
42
  def stop(version: nil)
31
43
  pipe \
32
- version ? container_id_for_version(version) : current_container_id,
44
+ version ? container_id_for_version(version) : current_running_container_id,
33
45
  xargs(config.stop_wait_time ? docker(:stop, "-t", config.stop_wait_time) : docker(:stop))
34
46
  end
35
47
 
@@ -40,7 +52,7 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
40
52
 
41
53
  def logs(since: nil, lines: nil, grep: nil)
42
54
  pipe \
43
- current_container_id,
55
+ current_running_container_id,
44
56
  "xargs docker logs#{" --since #{since}" if since}#{" --tail #{lines}" if lines} 2>&1",
45
57
  ("grep '#{grep}'" if grep)
46
58
  end
@@ -48,7 +60,7 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
48
60
  def follow_logs(host:, grep: nil)
49
61
  run_over_ssh \
50
62
  pipe(
51
- current_container_id,
63
+ current_running_container_id,
52
64
  "xargs docker logs --timestamps --tail 10 --follow 2>&1",
53
65
  (%(grep "#{grep}") if grep)
54
66
  ),
@@ -82,20 +94,23 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
82
94
  end
83
95
 
84
96
 
85
- def current_container_id
86
- docker :ps, "--quiet", *filter_args
97
+ def current_running_container_id
98
+ docker :ps, "--quiet", *filter_args(statuses: ACTIVE_DOCKER_STATUSES), "--latest"
87
99
  end
88
100
 
89
- def container_id_for_version(version)
90
- container_id_for(container_name: container_name(version))
101
+ def container_id_for_version(version, only_running: false)
102
+ container_id_for(container_name: container_name(version), only_running: only_running)
91
103
  end
92
104
 
93
105
  def current_running_version
94
- # FIXME: Find more graceful way to extract the version from "app-version" than using sed and tail!
106
+ list_versions("--latest", statuses: ACTIVE_DOCKER_STATUSES)
107
+ end
108
+
109
+ def list_versions(*docker_args, statuses: nil)
95
110
  pipe \
96
- docker(:ps, *filter_args, "--format", '"{{.Names}}"'),
97
- %(sed 's/-/\\n/g'),
98
- "tail -n 1"
111
+ docker(:ps, *filter_args(statuses: statuses), *docker_args, "--format", '"{{.Names}}"'),
112
+ %(grep -oE "\\-[^-]+$"), # Extract SHA from "service-role-dest-SHA"
113
+ %(cut -c 2-)
99
114
  end
100
115
 
101
116
  def list_containers
@@ -128,20 +143,27 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
128
143
  docker :image, :prune, "--all", "--force", *filter_args
129
144
  end
130
145
 
146
+ def tag_current_as_latest
147
+ docker :tag, config.absolute_image, config.latest_image
148
+ end
149
+
131
150
 
132
151
  private
133
152
  def container_name(version = nil)
134
153
  [ config.service, role, config.destination, version || config.version ].compact.join("-")
135
154
  end
136
155
 
137
- def filter_args
138
- argumentize "--filter", filters
156
+ def filter_args(statuses: nil)
157
+ argumentize "--filter", filters(statuses: statuses)
139
158
  end
140
159
 
141
- def filters
160
+ def filters(statuses: nil)
142
161
  [ "label=service=#{config.service}" ].tap do |filters|
143
162
  filters << "label=destination=#{config.destination}" if config.destination
144
163
  filters << "label=role=#{role}" if role
164
+ statuses&.each do |status|
165
+ filters << "status=#{status}"
166
+ end
145
167
  end
146
168
  end
147
169
  end
@@ -1,27 +1,18 @@
1
- require "active_support/core_ext/time/conversions"
2
-
3
1
  class Mrsk::Commands::Auditor < Mrsk::Commands::Base
4
- attr_reader :role
2
+ attr_reader :details
5
3
 
6
- def initialize(config, role: nil)
4
+ def initialize(config, **details)
7
5
  super(config)
8
- @role = role
6
+ @details = details
9
7
  end
10
8
 
11
9
  # Runs remotely
12
- def record(line)
10
+ def record(line, **details)
13
11
  append \
14
- [ :echo, tagged_record_line(line) ],
12
+ [ :echo, audit_tags(**details).except(:version, :service_version).to_s, line ],
15
13
  audit_log_file
16
14
  end
17
15
 
18
- # Runs locally
19
- def broadcast(line)
20
- if broadcast_cmd = config.audit_broadcast_cmd
21
- [ broadcast_cmd, tagged_broadcast_line(line) ]
22
- end
23
- end
24
-
25
16
  def reveal
26
17
  [ :tail, "-n", 50, audit_log_file ]
27
18
  end
@@ -31,27 +22,7 @@ class Mrsk::Commands::Auditor < Mrsk::Commands::Base
31
22
  [ "mrsk", config.service, config.destination, "audit.log" ].compact.join("-")
32
23
  end
33
24
 
34
- def tagged_record_line(line)
35
- tagged_line recorded_at_tag, performer_tag, role_tag, line
36
- end
37
-
38
- def tagged_broadcast_line(line)
39
- tagged_line performer_tag, role_tag, line
40
- end
41
-
42
- def tagged_line(*tags_and_line)
43
- "'#{tags_and_line.compact.join(" ")}'"
44
- end
45
-
46
- def recorded_at_tag
47
- "[#{Time.now.to_fs(:db)}]"
48
- end
49
-
50
- def performer_tag
51
- "[#{`whoami`.strip}]"
52
- end
53
-
54
- def role_tag
55
- "[#{role}]" if role
25
+ def audit_tags(**details)
26
+ tags(**self.details, **details)
56
27
  end
57
28
  end
@@ -2,6 +2,9 @@ module Mrsk::Commands
2
2
  class Base
3
3
  delegate :sensitive, :argumentize, to: Mrsk::Utils
4
4
 
5
+ DOCKER_HEALTH_STATUS_FORMAT = "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'"
6
+ DOCKER_HEALTH_LOG_FORMAT = "'{{json .State.Health}}'"
7
+
5
8
  attr_accessor :config
6
9
 
7
10
  def initialize(config)
@@ -15,8 +18,8 @@ module Mrsk::Commands
15
18
  end
16
19
  end
17
20
 
18
- def container_id_for(container_name:)
19
- docker :container, :ls, "--all", "--filter", "name=^#{container_name}$", "--quiet"
21
+ def container_id_for(container_name:, only_running: false)
22
+ docker :container, :ls, *("--all" unless only_running), "--filter", "name=^#{container_name}$", "--quiet"
20
23
  end
21
24
 
22
25
  private
@@ -50,5 +53,9 @@ module Mrsk::Commands
50
53
  def docker(*args)
51
54
  args.compact.unshift :docker
52
55
  end
56
+
57
+ def tags(**details)
58
+ Mrsk::Tags.from_config(config, **details)
59
+ end
53
60
  end
54
61
  end
@@ -1,4 +1,7 @@
1
+
1
2
  class Mrsk::Commands::Builder::Base < Mrsk::Commands::Base
3
+ class BuilderError < StandardError; end
4
+
2
5
  delegate :argumentize, to: Mrsk::Utils
3
6
 
4
7
  def clean
@@ -7,7 +10,6 @@ class Mrsk::Commands::Builder::Base < Mrsk::Commands::Base
7
10
 
8
11
  def pull
9
12
  docker :pull, config.absolute_image
10
- docker :pull, config.latest_image
11
13
  end
12
14
 
13
15
  def build_options
@@ -18,6 +20,7 @@ class Mrsk::Commands::Builder::Base < Mrsk::Commands::Base
18
20
  context
19
21
  end
20
22
 
23
+
21
24
  private
22
25
  def build_tags
23
26
  [ "-t", config.absolute_image, "-t", config.latest_image ]
@@ -36,7 +39,11 @@ class Mrsk::Commands::Builder::Base < Mrsk::Commands::Base
36
39
  end
37
40
 
38
41
  def build_dockerfile
39
- argumentize "--file", dockerfile
42
+ if Pathname.new(File.expand_path(dockerfile)).exist?
43
+ argumentize "--file", dockerfile
44
+ else
45
+ raise BuilderError, "Missing #{dockerfile}"
46
+ end
40
47
  end
41
48
 
42
49
  def args
@@ -2,7 +2,7 @@ class Mrsk::Commands::Builder < Mrsk::Commands::Base
2
2
  delegate :create, :remove, :push, :clean, :pull, :info, to: :target
3
3
 
4
4
  def name
5
- target.class.to_s.remove("Mrsk::Commands::Builder::").underscore
5
+ target.class.to_s.remove("Mrsk::Commands::Builder::").underscore.inquiry
6
6
  end
7
7
 
8
8
  def target
@@ -33,4 +33,24 @@ class Mrsk::Commands::Builder < Mrsk::Commands::Base
33
33
  def multiarch_remote
34
34
  @multiarch_remote ||= Mrsk::Commands::Builder::Multiarch::Remote.new(config)
35
35
  end
36
+
37
+
38
+ def ensure_local_dependencies_installed
39
+ if name.native?
40
+ ensure_local_docker_installed
41
+ else
42
+ combine \
43
+ ensure_local_docker_installed,
44
+ ensure_local_buildx_installed
45
+ end
46
+ end
47
+
48
+ private
49
+ def ensure_local_docker_installed
50
+ docker "--version"
51
+ end
52
+
53
+ def ensure_local_buildx_installed
54
+ docker :buildx, "version"
55
+ end
36
56
  end
@@ -0,0 +1,21 @@
1
+ class Mrsk::Commands::Docker < Mrsk::Commands::Base
2
+ # Install Docker using the https://github.com/docker/docker-install convenience script.
3
+ def install
4
+ pipe [ :curl, "-fsSL", "https://get.docker.com" ], :sh
5
+ end
6
+
7
+ # Checks the Docker client version. Fails if Docker is not installed.
8
+ def installed?
9
+ docker "-v"
10
+ end
11
+
12
+ # Checks the Docker server version. Fails if Docker is not running.
13
+ def running?
14
+ docker :version
15
+ end
16
+
17
+ # Do we have superuser access to install Docker and start system services?
18
+ def superuser?
19
+ [ '[ "${EUID:-$(id -u)}" -eq 0 ]' ]
20
+ end
21
+ end
@@ -11,14 +11,19 @@ class Mrsk::Commands::Healthcheck < Mrsk::Commands::Base
11
11
  "--label", "service=#{container_name}",
12
12
  "-e", "MRSK_CONTAINER_NAME=\"#{container_name}\"",
13
13
  *web.env_args,
14
+ *web.health_check_args,
14
15
  *config.volume_args,
15
16
  *web.option_args,
16
17
  config.absolute_image,
17
18
  web.cmd
18
19
  end
19
20
 
20
- def curl
21
- [ :curl, "--silent", "--output", "/dev/null", "--write-out", "'%{http_code}'", "--max-time", "2", health_url ]
21
+ def status
22
+ pipe container_id, xargs(docker(:inspect, "--format", DOCKER_HEALTH_STATUS_FORMAT))
23
+ end
24
+
25
+ def container_health_log
26
+ pipe container_id, xargs(docker(:inspect, "--format", DOCKER_HEALTH_LOG_FORMAT))
22
27
  end
23
28
 
24
29
  def logs
@@ -0,0 +1,14 @@
1
+ class Mrsk::Commands::Hook < Mrsk::Commands::Base
2
+ def run(hook, **details)
3
+ [ hook_file(hook), env: tags(**details).env ]
4
+ end
5
+
6
+ def hook_exists?(hook)
7
+ Pathname.new(hook_file(hook)).exist?
8
+ end
9
+
10
+ private
11
+ def hook_file(hook)
12
+ "#{config.hooks_path}/#{hook}"
13
+ end
14
+ end
@@ -1,5 +1,5 @@
1
1
  require "active_support/duration"
2
- require "active_support/core_ext/numeric/time"
2
+ require "time"
3
3
 
4
4
  class Mrsk::Commands::Lock < Mrsk::Commands::Base
5
5
  def acquire(message, version)
@@ -49,7 +49,7 @@ class Mrsk::Commands::Lock < Mrsk::Commands::Base
49
49
 
50
50
  def lock_details(message, version)
51
51
  <<~DETAILS.strip
52
- Locked by: #{locked_by} at #{Time.now.gmtime}
52
+ Locked by: #{locked_by} at #{Time.now.utc.iso8601}
53
53
  Version: #{version}
54
54
  Message: #{message}
55
55
  DETAILS
@@ -2,11 +2,37 @@ require "active_support/duration"
2
2
  require "active_support/core_ext/numeric/time"
3
3
 
4
4
  class Mrsk::Commands::Prune < Mrsk::Commands::Base
5
- def images(until_hours: 7.days.in_hours.to_i)
6
- docker :image, :prune, "--all", "--force", "--filter", "label=service=#{config.service}", "--filter", "until=#{until_hours}h"
5
+ def dangling_images
6
+ docker :image, :prune, "--force", "--filter", "label=service=#{config.service}", "--filter", "dangling=true"
7
7
  end
8
8
 
9
- def containers(until_hours: 3.days.in_hours.to_i)
10
- docker :container, :prune, "--force", "--filter", "label=service=#{config.service}", "--filter", "until=#{until_hours}h"
9
+ def tagged_images
10
+ pipe \
11
+ docker(:image, :ls, *service_filter, "--format", "'{{.ID}} {{.Repository}}:{{.Tag}}'"),
12
+ "grep -v -w \"#{active_image_list}\"",
13
+ "while read image tag; do docker rmi $tag; done"
11
14
  end
15
+
16
+ def containers(keep_last: 5)
17
+ pipe \
18
+ docker(:ps, "-q", "-a", *service_filter, *stopped_containers_filters),
19
+ "tail -n +#{keep_last + 1}",
20
+ "while read container_id; do docker rm $container_id; done"
21
+ end
22
+
23
+ private
24
+ def stopped_containers_filters
25
+ [ "created", "exited", "dead" ].flat_map { |status| ["--filter", "status=#{status}"] }
26
+ end
27
+
28
+ def active_image_list
29
+ # Pull the images that are used by any containers
30
+ # Append repo:latest - to avoid deleting the latest tag
31
+ # Append repo:<none> - to avoid deleting dangling images that are in use. Unused dangling images are deleted separately
32
+ "$(docker container ls -a --format '{{.Image}}\\|' --filter label=service=#{config.service} | tr -d '\\n')#{config.latest_image}\\|#{config.repository}:<none>"
33
+ end
34
+
35
+ def service_filter
36
+ [ "--filter", "label=service=#{config.service}" ]
37
+ end
12
38
  end
@@ -1,5 +1,5 @@
1
1
  class Mrsk::Commands::Traefik < Mrsk::Commands::Base
2
- delegate :argumentize, :optionize, to: Mrsk::Utils
2
+ delegate :argumentize, :argumentize_env_with_secrets, :optionize, to: Mrsk::Utils
3
3
 
4
4
  DEFAULT_IMAGE = "traefik:v2.9"
5
5
  CONTAINER_PORT = 80
@@ -10,6 +10,7 @@ class Mrsk::Commands::Traefik < Mrsk::Commands::Base
10
10
  "--restart", "unless-stopped",
11
11
  "--publish", port,
12
12
  "--volume", "/var/run/docker.sock:/var/run/docker.sock",
13
+ *env_args,
13
14
  *config.logging_args,
14
15
  *label_args,
15
16
  *docker_options_args,
@@ -61,6 +62,16 @@ class Mrsk::Commands::Traefik < Mrsk::Commands::Base
61
62
  argumentize "--label", labels
62
63
  end
63
64
 
65
+ def env_args
66
+ env_config = config.traefik["env"] || {}
67
+
68
+ if env_config.present?
69
+ argumentize_env_with_secrets(env_config)
70
+ else
71
+ []
72
+ end
73
+ end
74
+
64
75
  def labels
65
76
  config.traefik["labels"] || []
66
77
  end
@@ -0,0 +1,20 @@
1
+ class Mrsk::Configuration::Boot
2
+ def initialize(config:)
3
+ @options = config.raw_config.boot || {}
4
+ @host_count = config.all_hosts.count
5
+ end
6
+
7
+ def limit
8
+ limit = @options["limit"]
9
+
10
+ if limit.to_s.end_with?("%")
11
+ @host_count * limit.to_i / 100
12
+ else
13
+ limit
14
+ end
15
+ end
16
+
17
+ def wait
18
+ @options["wait"]
19
+ end
20
+ end
@@ -35,6 +35,28 @@ class Mrsk::Configuration::Role
35
35
  argumentize_env_with_secrets env
36
36
  end
37
37
 
38
+ def health_check_args
39
+ if health_check_cmd.present?
40
+ optionize({ "health-cmd" => health_check_cmd, "health-interval" => health_check_interval })
41
+ else
42
+ []
43
+ end
44
+ end
45
+
46
+ def health_check_cmd
47
+ options = specializations["healthcheck"] || {}
48
+ options = config.healthcheck.merge(options) if running_traefik?
49
+
50
+ options["cmd"] || http_health_check(port: options["port"], path: options["path"])
51
+ end
52
+
53
+ def health_check_interval
54
+ options = specializations["healthcheck"] || {}
55
+ options = config.healthcheck.merge(options) if running_traefik?
56
+
57
+ options["interval"] || "1s"
58
+ end
59
+
38
60
  def cmd
39
61
  specializations["cmd"]
40
62
  end
@@ -74,9 +96,10 @@ class Mrsk::Configuration::Role
74
96
  def traefik_labels
75
97
  if running_traefik?
76
98
  {
99
+ # Setting a service property ensures that the generated service name will be consistent between versions
100
+ "traefik.http.services.#{traefik_service}.loadbalancer.server.scheme" => "http",
101
+
77
102
  "traefik.http.routers.#{traefik_service}.rule" => "PathPrefix(`/`)",
78
- "traefik.http.services.#{traefik_service}.loadbalancer.healthcheck.path" => config.healthcheck["path"],
79
- "traefik.http.services.#{traefik_service}.loadbalancer.healthcheck.interval" => "1s",
80
103
  "traefik.http.middlewares.#{traefik_service}-retry.retry.attempts" => "5",
81
104
  "traefik.http.middlewares.#{traefik_service}-retry.retry.initialinterval" => "500ms",
82
105
  "traefik.http.routers.#{traefik_service}.middlewares" => "#{traefik_service}-retry@docker"
@@ -125,4 +148,8 @@ class Mrsk::Configuration::Role
125
148
  new_env["clear"] = (clear_app_env + clear_role_env).uniq
126
149
  end
127
150
  end
151
+
152
+ def http_health_check(port:, path:)
153
+ "curl -f #{URI.join("http://localhost:#{port}", path)} || exit 1" if path.present? || port.present?
154
+ end
128
155
  end
@@ -6,7 +6,7 @@ require "erb"
6
6
  require "net/ssh/proxy/jump"
7
7
 
8
8
  class Mrsk::Configuration
9
- delegate :service, :image, :servers, :env, :labels, :registry, :builder, :stop_wait_time, to: :raw_config, allow_nil: true
9
+ delegate :service, :image, :servers, :env, :labels, :registry, :builder, :stop_wait_time, :hooks_path, to: :raw_config, allow_nil: true
10
10
  delegate :argumentize, :argumentize_env_with_secrets, :optionize, to: Mrsk::Utils
11
11
 
12
12
  attr_accessor :destination
@@ -50,7 +50,7 @@ class Mrsk::Configuration
50
50
  end
51
51
 
52
52
  def version
53
- @declared_version.presence || ENV["VERSION"] || current_commit_hash
53
+ @declared_version.presence || ENV["VERSION"] || git_version
54
54
  end
55
55
 
56
56
  def abbreviated_version
@@ -87,6 +87,10 @@ class Mrsk::Configuration
87
87
  roles.select(&:running_traefik?).flat_map(&:hosts).uniq
88
88
  end
89
89
 
90
+ def boot
91
+ Mrsk::Configuration::Boot.new(config: self)
92
+ end
93
+
90
94
 
91
95
  def repository
92
96
  [ raw_config.registry["server"], image ].compact.join("/")
@@ -153,10 +157,6 @@ class Mrsk::Configuration
153
157
  end
154
158
 
155
159
 
156
- def audit_broadcast_cmd
157
- raw_config.audit_broadcast_cmd
158
- end
159
-
160
160
  def healthcheck
161
161
  { "path" => "/up", "port" => 3000, "max_attempts" => 7 }.merge(raw_config.healthcheck || {})
162
162
  end
@@ -193,6 +193,10 @@ class Mrsk::Configuration
193
193
  raw_config.traefik || {}
194
194
  end
195
195
 
196
+ def hooks_path
197
+ raw_config.hooks_path || ".mrsk/hooks"
198
+ end
199
+
196
200
  private
197
201
  # Will raise ArgumentError if any required config keys are missing
198
202
  def ensure_required_keys_present
@@ -229,10 +233,12 @@ class Mrsk::Configuration
229
233
  raw_config.servers.is_a?(Array) ? [ "web" ] : raw_config.servers.keys.sort
230
234
  end
231
235
 
232
- def current_commit_hash
233
- @current_commit_hash ||=
236
+ def git_version
237
+ @git_version ||=
234
238
  if system("git rev-parse")
235
- `git rev-parse HEAD`.strip
239
+ uncommitted_suffix = `git status --porcelain`.strip.present? ? "_uncommitted_#{SecureRandom.hex(8)}" : ""
240
+
241
+ "#{`git rev-parse HEAD`.strip}#{uncommitted_suffix}"
236
242
  else
237
243
  raise "Can't use commit hash as version, no git repository found in #{Dir.pwd}"
238
244
  end
@@ -1,12 +1,56 @@
1
1
  require "sshkit"
2
2
  require "sshkit/dsl"
3
+ require "active_support/core_ext/hash/deep_merge"
4
+ require "json"
3
5
 
4
6
  class SSHKit::Backend::Abstract
5
- def capture_with_info(*args)
6
- capture(*args, verbosity: Logger::INFO)
7
+ def capture_with_info(*args, **kwargs)
8
+ capture(*args, **kwargs, verbosity: Logger::INFO)
9
+ end
10
+
11
+ def capture_with_debug(*args, **kwargs)
12
+ capture(*args, **kwargs, verbosity: Logger::DEBUG)
13
+ end
14
+
15
+ def capture_with_pretty_json(*args, **kwargs)
16
+ JSON.pretty_generate(JSON.parse(capture(*args, **kwargs)))
7
17
  end
8
18
 
9
19
  def puts_by_host(host, output, type: "App")
10
20
  puts "#{type} Host: #{host}\n#{output}\n\n"
11
21
  end
22
+
23
+ # Our execution pattern is for the CLI execute args lists returned
24
+ # from commands, but this doesn't support returning execution options
25
+ # from the command.
26
+ #
27
+ # Support this by using kwargs for CLI options and merging with the
28
+ # args-extracted options.
29
+ module CommandEnvMerge
30
+ private
31
+
32
+ # Override to merge options returned by commands in the args list with
33
+ # options passed by the CLI and pass them along as kwargs.
34
+ def command(args, options)
35
+ more_options, args = args.partition { |a| a.is_a? Hash }
36
+ more_options << options
37
+
38
+ build_command(args, **more_options.reduce(:deep_merge))
39
+ end
40
+
41
+ # Destructure options to pluck out env for merge
42
+ def build_command(args, env: nil, **options)
43
+ # Rely on native Ruby kwargs precedence rather than explicit Hash merges
44
+ SSHKit::Command.new(*args, **default_command_options, **options, env: env_for(env))
45
+ end
46
+
47
+ def default_command_options
48
+ { in: pwd_path, host: @host, user: @user, group: @group }
49
+ end
50
+
51
+ def env_for(env)
52
+ @env.to_h.merge(env.to_h)
53
+ end
54
+ end
55
+ prepend CommandEnvMerge
12
56
  end
data/lib/mrsk/tags.rb ADDED
@@ -0,0 +1,39 @@
1
+ require "time"
2
+
3
+ class Mrsk::Tags
4
+ attr_reader :config, :tags
5
+
6
+ class << self
7
+ def from_config(config, **extra)
8
+ new(**default_tags(config), **extra)
9
+ end
10
+
11
+ def default_tags(config)
12
+ { recorded_at: Time.now.utc.iso8601,
13
+ performer: `whoami`.chomp,
14
+ destination: config.destination,
15
+ version: config.version,
16
+ service_version: service_version(config) }
17
+ end
18
+
19
+ def service_version(config)
20
+ [ config.service, config.abbreviated_version ].compact.join("@")
21
+ end
22
+ end
23
+
24
+ def initialize(**tags)
25
+ @tags = tags.compact
26
+ end
27
+
28
+ def env
29
+ tags.transform_keys { |detail| "MRSK_#{detail.upcase}" }
30
+ end
31
+
32
+ def to_s
33
+ tags.values.map { |value| "[#{value}]" }.join(" ")
34
+ end
35
+
36
+ def except(*tags)
37
+ self.class.new(**self.tags.except(*tags))
38
+ end
39
+ end