mrsk 0.12.0 → 0.12.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8a09720b016119167213b3c1aab02e56c5e3de0eaa09dc39996d55931048e3e5
4
- data.tar.gz: 7b435e7ff711c7267cb3f1771d56791fe76c1fb62950daeeacfe923288c314e8
3
+ metadata.gz: '099a4dc2dc59df4e0c5301b85a579970dbc6c46a3c1e2a634f4461c5cff1f241'
4
+ data.tar.gz: 0a0356837992a9847b7b6bc51956c26a4fb0e8fbb9809753a5bdb373bacd3aee
5
5
  SHA512:
6
- metadata.gz: de31f5f7e47e1f93446603e48a9a1760fca96bc41ee6dfec0660030fb0fbcdcb3fb4145d4b81fda9c3f4eef5de0a205f54e67e7bb27c91d7ebd0c93c3f5d0c57
7
- data.tar.gz: 48d36ec263b4ccfd3729059eb85f340717b17406f88e8281955e948fcfe9c30ffe081bad01977e6f9836ecd480361cf94f15cdfe79aa7477c18a6fb6ad9b97bb
6
+ metadata.gz: 06f1365b5f8a7cc2064f2bd184224dba12b1c0ebf57cc502c9ac423a9d0ae96a601161fe681764f9a660b8a6214f2a86b79d1c4d46d9ff13819c52159db14318
7
+ data.tar.gz: 7acf9c5d2e26709b391a2c9915300c569fd6cc7d841b55cd3a497122d2d2317b8d2b3df4e634f14c38acd571fbc8aac51096a221f10097f8ce68d6c07dbce038
data/README.md CHANGED
@@ -308,7 +308,7 @@ You can specialize the default Traefik rules by setting labels on the containers
308
308
  labels:
309
309
  traefik.http.routers.hey-web.rule: Host(`app.hey.com`)
310
310
  ```
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.
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-staging.rule" if it was for the "staging" destination.
312
312
 
313
313
  Note: The backticks are needed to ensure the rule is passed in correctly and not treated as command substitution by Bash!
314
314
 
@@ -677,6 +677,21 @@ That'll post a line like follows to a preconfigured chatbot in Basecamp:
677
677
  [My App] [dhh] Rolled back to version d264c4e92470ad1bd18590f04466787262f605de
678
678
  ```
679
679
 
680
+ `MRSK_*` environment variables are available to the broadcast command for
681
+ fine-grained audit reporting, e.g. for triggering deployment reports or
682
+ firing a JSON webhook. These variables include:
683
+ - `MRSK_RECORDED_AT` - UTC timestamp in ISO 8601 format, e.g. `2023-04-14T17:07:31Z`
684
+ - `MRSK_PERFORMER` - the local user performing the command (from `whoami`)
685
+ - `MRSK_MESSAGE` - the full audit message, e.g. "Deployed app@150b24f"
686
+ - `MRSK_DESTINATION` - optional: destination, e.g. "staging"
687
+ - `MRSK_ROLE` - optional: role targeted, e.g. "web"
688
+
689
+ Use `mrsk broadcast` to test and troubleshoot your broadcast command:
690
+
691
+ ```bash
692
+ mrsk broadcast -m "test audit message"
693
+ ```
694
+
680
695
  ### Healthcheck
681
696
 
682
697
  MRSK uses Docker healtchecks to check the health of your application during deployment. Traefik uses this same healthcheck status to determine when a container is ready to receive traffic.
@@ -714,7 +729,7 @@ servers:
714
729
 
715
730
  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.
716
731
 
717
- Note that the HTTP health checks assume that the `curl` command is avilable inside the container. If that's not the case, use the healthcheck's `cmd` option to specify an alternative check that the container supports.
732
+ 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.
718
733
 
719
734
  ## Commands
720
735
 
data/lib/mrsk/cli/app.rb CHANGED
@@ -60,7 +60,7 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
60
60
  roles = MRSK.roles_on(host)
61
61
 
62
62
  roles.each do |role|
63
- execute *MRSK.auditor(role: role).record("Stopped app"), verbosity: :debug
63
+ execute *MRSK.auditor.record("Stopped app", role: role), verbosity: :debug
64
64
  execute *MRSK.app(role: role).stop, raise_on_non_zero_exit: false
65
65
  end
66
66
  end
@@ -107,7 +107,7 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
107
107
  roles = MRSK.roles_on(host)
108
108
 
109
109
  roles.each do |role|
110
- execute *MRSK.auditor(role: role).record("Executed cmd '#{cmd}' on app version #{version}"), verbosity: :debug
110
+ execute *MRSK.auditor.record("Executed cmd '#{cmd}' on app version #{version}", role: role), verbosity: :debug
111
111
  puts_by_host host, capture_with_info(*MRSK.app(role: role).execute_in_existing_container(cmd))
112
112
  end
113
113
  end
@@ -214,7 +214,7 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
214
214
  roles = MRSK.roles_on(host)
215
215
 
216
216
  roles.each do |role|
217
- execute *MRSK.auditor(role: role).record("Removed app container with version #{version}"), verbosity: :debug
217
+ execute *MRSK.auditor.record("Removed app container with version #{version}", role: role), verbosity: :debug
218
218
  execute *MRSK.app(role: role).remove_container(version: version)
219
219
  end
220
220
  end
@@ -228,7 +228,7 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
228
228
  roles = MRSK.roles_on(host)
229
229
 
230
230
  roles.each do |role|
231
- execute *MRSK.auditor(role: role).record("Removed all app containers"), verbosity: :debug
231
+ execute *MRSK.auditor.record("Removed all app containers", role: role), verbosity: :debug
232
232
  execute *MRSK.app(role: role).remove_containers
233
233
  end
234
234
  end
@@ -9,6 +9,7 @@ class Mrsk::Cli::Healthcheck < Mrsk::Cli::Base
9
9
  Mrsk::Utils::HealthcheckPoller.wait_for_healthy { capture_with_info(*MRSK.healthcheck.status) }
10
10
  rescue Mrsk::Utils::HealthcheckPoller::HealthcheckError => e
11
11
  error capture_with_info(*MRSK.healthcheck.logs)
12
+ error capture_with_pretty_json(*MRSK.healthcheck.container_health_log)
12
13
  raise
13
14
  ensure
14
15
  execute *MRSK.healthcheck.stop, raise_on_non_zero_exit: false
data/lib/mrsk/cli/main.rb CHANGED
@@ -200,6 +200,13 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
200
200
  end
201
201
  end
202
202
 
203
+ desc "broadcast", "Broadcast an audit message"
204
+ option :message, aliases: "-m", type: :string, desc: "Audit mesasge", required: true
205
+ def broadcast
206
+ say "Broadcast: #{options[:message]}", :magenta
207
+ audit_broadcast options[:message]
208
+ end
209
+
203
210
  desc "version", "Show MRSK version"
204
211
  def version
205
212
  puts Mrsk::VERSION
@@ -84,8 +84,8 @@ class Mrsk::Commander
84
84
  Mrsk::Commands::Accessory.new(config, name: name)
85
85
  end
86
86
 
87
- def auditor(role: nil)
88
- Mrsk::Commands::Auditor.new(config, role: role)
87
+ def auditor(**details)
88
+ Mrsk::Commands::Auditor.new(config, **details)
89
89
  end
90
90
 
91
91
  def builder
@@ -1,24 +1,24 @@
1
- require "active_support/core_ext/time/conversions"
1
+ require "time"
2
2
 
3
3
  class Mrsk::Commands::Auditor < Mrsk::Commands::Base
4
- attr_reader :role
4
+ attr_reader :details
5
5
 
6
- def initialize(config, role: nil)
6
+ def initialize(config, **details)
7
7
  super(config)
8
- @role = role
8
+ @details = default_details.merge(details)
9
9
  end
10
10
 
11
11
  # Runs remotely
12
- def record(line)
12
+ def record(line, **details)
13
13
  append \
14
- [ :echo, tagged_record_line(line) ],
14
+ [ :echo, *audit_tags(**details), line ],
15
15
  audit_log_file
16
16
  end
17
17
 
18
18
  # Runs locally
19
- def broadcast(line)
19
+ def broadcast(line, **details)
20
20
  if broadcast_cmd = config.audit_broadcast_cmd
21
- [ broadcast_cmd, tagged_broadcast_line(line) ]
21
+ [ broadcast_cmd, *broadcast_args(line, **details), env: env_for(event: line, **details) ]
22
22
  end
23
23
  end
24
24
 
@@ -31,27 +31,29 @@ class Mrsk::Commands::Auditor < Mrsk::Commands::Base
31
31
  [ "mrsk", config.service, config.destination, "audit.log" ].compact.join("-")
32
32
  end
33
33
 
34
- def tagged_record_line(line)
35
- tagged_line recorded_at_tag, performer_tag, role_tag, line
34
+ def default_details
35
+ { recorded_at: Time.now.utc.iso8601,
36
+ performer: `whoami`.chomp,
37
+ destination: config.destination }
36
38
  end
37
39
 
38
- def tagged_broadcast_line(line)
39
- tagged_line performer_tag, role_tag, line
40
+ def audit_tags(**details)
41
+ tags_for **self.details.merge(details)
40
42
  end
41
43
 
42
- def tagged_line(*tags_and_line)
43
- "'#{tags_and_line.compact.join(" ")}'"
44
+ def broadcast_args(line, **details)
45
+ "'#{broadcast_tags(**details).join(" ")} #{line}'"
44
46
  end
45
47
 
46
- def recorded_at_tag
47
- "[#{Time.now.to_fs(:db)}]"
48
+ def broadcast_tags(**details)
49
+ tags_for **self.details.merge(details).except(:recorded_at)
48
50
  end
49
51
 
50
- def performer_tag
51
- "[#{`whoami`.strip}]"
52
+ def tags_for(**details)
53
+ details.compact.values.map { |value| "[#{value}]" }
52
54
  end
53
55
 
54
- def role_tag
55
- "[#{role}]" if role
56
+ def env_for(**details)
57
+ self.details.merge(details).compact.transform_keys { |detail| "MRSK_#{detail.upcase}" }
56
58
  end
57
59
  end
@@ -3,6 +3,7 @@ module Mrsk::Commands
3
3
  delegate :sensitive, :argumentize, to: Mrsk::Utils
4
4
 
5
5
  DOCKER_HEALTH_STATUS_FORMAT = "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'"
6
+ DOCKER_HEALTH_LOG_FORMAT = "'{{json .State.Health}}'"
6
7
 
7
8
  attr_accessor :config
8
9
 
@@ -22,6 +22,10 @@ class Mrsk::Commands::Healthcheck < Mrsk::Commands::Base
22
22
  pipe container_id, xargs(docker(:inspect, "--format", DOCKER_HEALTH_STATUS_FORMAT))
23
23
  end
24
24
 
25
+ def container_health_log
26
+ pipe container_id, xargs(docker(:inspect, "--format", DOCKER_HEALTH_LOG_FORMAT))
27
+ end
28
+
25
29
  def logs
26
30
  pipe container_id, xargs(docker(:logs, "--tail", 50, "2>&1"))
27
31
  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
@@ -3,7 +3,7 @@ require "active_support/core_ext/numeric/time"
3
3
 
4
4
  class Mrsk::Commands::Prune < Mrsk::Commands::Base
5
5
  def images
6
- docker :image, :prune, "--all", "--force", "--filter", "label=service=#{config.service}", "--filter", "dangling=true"
6
+ docker :image, :prune, "--force", "--filter", "label=service=#{config.service}", "--filter", "dangling=true"
7
7
  end
8
8
 
9
9
  def containers(keep_last: 5)
@@ -1,12 +1,52 @@
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
7
  def capture_with_info(*args, **kwargs)
6
8
  capture(*args, **kwargs, verbosity: Logger::INFO)
7
9
  end
8
10
 
11
+ def capture_with_pretty_json(*args, **kwargs)
12
+ JSON.pretty_generate(JSON.parse(capture(*args, **kwargs)))
13
+ end
14
+
9
15
  def puts_by_host(host, output, type: "App")
10
16
  puts "#{type} Host: #{host}\n#{output}\n\n"
11
17
  end
18
+
19
+ # Our execution pattern is for the CLI execute args lists returned
20
+ # from commands, but this doesn't support returning execution options
21
+ # from the command.
22
+ #
23
+ # Support this by using kwargs for CLI options and merging with the
24
+ # args-extracted options.
25
+ module CommandEnvMerge
26
+ private
27
+
28
+ # Override to merge options returned by commands in the args list with
29
+ # options passed by the CLI and pass them along as kwargs.
30
+ def command(args, options)
31
+ more_options, args = args.partition { |a| a.is_a? Hash }
32
+ more_options << options
33
+
34
+ build_command(args, **more_options.reduce(:deep_merge))
35
+ end
36
+
37
+ # Destructure options to pluck out env for merge
38
+ def build_command(args, env: nil, **options)
39
+ # Rely on native Ruby kwargs precedence rather than explicit Hash merges
40
+ SSHKit::Command.new(*args, **default_command_options, **options, env: env_for(env))
41
+ end
42
+
43
+ def default_command_options
44
+ { in: pwd_path, host: @host, user: @user, group: @group }
45
+ end
46
+
47
+ def env_for(env)
48
+ @env.to_h.merge(env.to_h)
49
+ end
50
+ end
51
+ prepend CommandEnvMerge
12
52
  end
data/lib/mrsk/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Mrsk
2
- VERSION = "0.12.0"
2
+ VERSION = "0.12.1"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mrsk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.0
4
+ version: 0.12.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Heinemeier Hansson
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-05-02 00:00:00.000000000 Z
11
+ date: 2023-05-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport