mrsk 0.7.1 → 0.8.0

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: ba5c8ba9da8547dd3ca0e986f41304de9155100135392e23028c7d820d593e2c
4
- data.tar.gz: 677ac4a60d8a494bb8ef20abb0993750a846eb5ea86c7ca2060b042dcdda846a
3
+ metadata.gz: bea897e88c4f7c2220fe37bc9ab2ee98b9b7e3a6e321eebfa75bcb6238cce4bf
4
+ data.tar.gz: c481c5c2b8a9d4bf8f2e38902f6f60eb01af78173bb49ee1e2a08c41b5a2e96c
5
5
  SHA512:
6
- metadata.gz: 88a8e0b9ac488cc4b3e5dd634b7a474d522b2f26c2e751998026b077676a8867f7f2837327a3b864328440c1a852929f81691aafd301f6a887b299a8b4d133f1
7
- data.tar.gz: 65508caaa6955884c408494f9c9ac42df5921b8fc183301406854a411088a7fbfefef3572b74cf109d213fa4b4985d5b878ce9ef66bda6eb346c80c5e404a95c
6
+ metadata.gz: b25f7b82c28809194794ed573f23895067a996e4f5511f17dc813a35066f7e0795d245ac63a920f4c960b75bf247c57638af2b50ecc109cd5591c25525de4142
7
+ data.tar.gz: 96f1b53a8b72c47a81d55274b4a739ec605d8a67e95811ab3b288c202bf4b756583d4b449ee26c4d3b5014bd3abcd410ffea45934ec86e57fd530b124b786d34
data/README.md CHANGED
@@ -14,7 +14,8 @@ servers:
14
14
  - 192.168.0.2
15
15
  registry:
16
16
  username: registry-user-name
17
- password: <%= ENV.fetch("MRSK_REGISTRY_PASSWORD") %>
17
+ password:
18
+ - MRSK_REGISTRY_PASSWORD
18
19
  env:
19
20
  secret:
20
21
  - RAILS_MASTER_KEY
@@ -348,7 +349,7 @@ If you need separate env variables for different destinations, you can set them
348
349
 
349
350
  ### Using audit broadcasts
350
351
 
351
- 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 reads the audit line from STDIN, and then does whatever with it:
352
+ 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:
352
353
 
353
354
  ```yaml
354
355
  audit_broadcast_cmd:
@@ -359,14 +360,13 @@ The broadcast command could look something like:
359
360
 
360
361
  ```bash
361
362
  #!/usr/bin/env bash
362
- read
363
- curl -q -d content="[My app] ${REPLY}" https://3.basecamp.com/XXXXX/integrations/XXXXX/buckets/XXXXX/chats/XXXXX/lines
363
+ curl -q -d content="[My App] ${1}" https://3.basecamp.com/XXXXX/integrations/XXXXX/buckets/XXXXX/chats/XXXXX/lines
364
364
  ```
365
365
 
366
366
  That'll post a line like follows to a preconfigured chatbot in Basecamp:
367
367
 
368
368
  ```
369
- [My App] [2023-02-18 11:29:52] [dhh] Rolled back to version d264c4e92470ad1bd18590f04466787262f605de
369
+ [My App] [dhh] Rolled back to version d264c4e92470ad1bd18590f04466787262f605de
370
370
  ```
371
371
 
372
372
  ### Using custom healthcheck path or port
@@ -448,7 +448,7 @@ mrsk app exec -i 'bin/rails console'
448
448
  ```
449
449
 
450
450
 
451
- ### Running details to see state of containers
451
+ ### Running details to show state of containers
452
452
 
453
453
  You can see the state of your servers by running `mrsk details`:
454
454
 
@@ -1,5 +1,5 @@
1
1
  class Mrsk::Cli::Accessory < Mrsk::Cli::Base
2
- desc "boot [NAME]", "Boot accessory service on host (use NAME=all to boot all accessories)"
2
+ desc "boot [NAME]", "Boot new accessory service on host (use NAME=all to boot all accessories)"
3
3
  def boot(name)
4
4
  if name == "all"
5
5
  MRSK.accessory_names.each { |accessory_name| boot(accessory_name) }
@@ -18,7 +18,7 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
18
18
  end
19
19
  end
20
20
 
21
- desc "upload [NAME]", "Upload accessory files to host"
21
+ desc "upload [NAME]", "Upload accessory files to host", hide: true
22
22
  def upload(name)
23
23
  with_accessory(name) do |accessory|
24
24
  on(accessory.host) do
@@ -33,7 +33,7 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
33
33
  end
34
34
  end
35
35
 
36
- desc "directories [NAME]", "Create accessory directories on host"
36
+ desc "directories [NAME]", "Create accessory directories on host", hide: true
37
37
  def directories(name)
38
38
  with_accessory(name) do |accessory|
39
39
  on(accessory.host) do
@@ -44,7 +44,7 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
44
44
  end
45
45
  end
46
46
 
47
- desc "reboot [NAME]", "Reboot accessory on host (stop container, remove container, start new container)"
47
+ desc "reboot [NAME]", "Reboot existing accessory on host (stop container, remove container, start new container)"
48
48
  def reboot(name)
49
49
  with_accessory(name) do |accessory|
50
50
  stop(name)
@@ -53,7 +53,7 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
53
53
  end
54
54
  end
55
55
 
56
- desc "start [NAME]", "Start existing accessory on host"
56
+ desc "start [NAME]", "Start existing accessory container on host"
57
57
  def start(name)
58
58
  with_accessory(name) do |accessory|
59
59
  on(accessory.host) do
@@ -63,7 +63,7 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
63
63
  end
64
64
  end
65
65
 
66
- desc "stop [NAME]", "Stop accessory on host"
66
+ desc "stop [NAME]", "Stop existing accessory container on host"
67
67
  def stop(name)
68
68
  with_accessory(name) do |accessory|
69
69
  on(accessory.host) do
@@ -73,7 +73,7 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
73
73
  end
74
74
  end
75
75
 
76
- desc "restart [NAME]", "Restart accessory on host"
76
+ desc "restart [NAME]", "Restart existing accessory container on host"
77
77
  def restart(name)
78
78
  with_accessory(name) do
79
79
  stop(name)
@@ -81,7 +81,7 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
81
81
  end
82
82
  end
83
83
 
84
- desc "details [NAME]", "Display details about accessory on host (use NAME=all to boot all accessories)"
84
+ desc "details [NAME]", "Show details about accessory on host (use NAME=all to show all accessories)"
85
85
  def details(name)
86
86
  if name == "all"
87
87
  MRSK.accessory_names.each { |accessory_name| details(accessory_name) }
@@ -92,7 +92,7 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
92
92
  end
93
93
  end
94
94
 
95
- desc "exec [NAME] [CMD]", "Execute a custom command on servers"
95
+ desc "exec [NAME] [CMD]", "Execute a custom command on servers (use --help to show options)"
96
96
  option :interactive, aliases: "-i", type: :boolean, default: false, desc: "Execute command over ssh for an interactive shell (use for console/bash)"
97
97
  option :reuse, type: :boolean, default: false, desc: "Reuse currently running container instead of starting a new one"
98
98
  def exec(name, cmd)
@@ -123,7 +123,7 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
123
123
  end
124
124
  end
125
125
 
126
- desc "logs [NAME]", "Show log lines from accessory on host"
126
+ desc "logs [NAME]", "Show log lines from accessory on host (use --help to show options)"
127
127
  option :since, aliases: "-s", desc: "Show logs since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)"
128
128
  option :lines, type: :numeric, aliases: "-n", desc: "Number of log lines to pull from each server"
129
129
  option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)"
@@ -149,21 +149,26 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
149
149
  end
150
150
  end
151
151
 
152
- desc "remove [NAME]", "Remove accessory container and image from host (use NAME=all to boot all accessories)"
152
+ desc "remove [NAME]", "Remove accessory container and image from host (use NAME=all to remove all accessories)"
153
+ option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
153
154
  def remove(name)
154
155
  if name == "all"
155
- MRSK.accessory_names.each { |accessory_name| remove(accessory_name) }
156
+ if options[:confirmed] || ask("This will remove all containers and images for all accessories. Are you sure?", limited_to: %w( y N ), default: "N") == "y"
157
+ MRSK.accessory_names.each { |accessory_name| remove(accessory_name) }
158
+ end
156
159
  else
157
- with_accessory(name) do
158
- stop(name)
159
- remove_container(name)
160
- remove_image(name)
161
- remove_service_directory(name)
160
+ if options[:confirmed] || ask("This will remove all containers and images for #{name}. Are you sure?", limited_to: %w( y N ), default: "N") == "y"
161
+ with_accessory(name) do
162
+ stop(name)
163
+ remove_container(name)
164
+ remove_image(name)
165
+ remove_service_directory(name)
166
+ end
162
167
  end
163
168
  end
164
169
  end
165
170
 
166
- desc "remove_container [NAME]", "Remove accessory container from host"
171
+ desc "remove_container [NAME]", "Remove accessory container from host", hide: true
167
172
  def remove_container(name)
168
173
  with_accessory(name) do |accessory|
169
174
  on(accessory.host) do
@@ -173,7 +178,7 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
173
178
  end
174
179
  end
175
180
 
176
- desc "remove_image [NAME]", "Remove accessory image from host"
181
+ desc "remove_image [NAME]", "Remove accessory image from host", hide: true
177
182
  def remove_image(name)
178
183
  with_accessory(name) do |accessory|
179
184
  on(accessory.host) do
@@ -183,7 +188,7 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
183
188
  end
184
189
  end
185
190
 
186
- desc "remove_service_directory [NAME]", "Remove accessory directory used for uploaded files and data directories from host"
191
+ desc "remove_service_directory [NAME]", "Remove accessory directory used for uploaded files and data directories from host", hide: true
187
192
  def remove_service_directory(name)
188
193
  with_accessory(name) do |accessory|
189
194
  on(accessory.host) do
data/lib/mrsk/cli/app.rb CHANGED
@@ -28,7 +28,7 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
28
28
  end
29
29
  end
30
30
 
31
- desc "start", "Start existing app on servers (use --version=<git-hash> to designate specific version)"
31
+ desc "start", "Start existing app container on servers"
32
32
  def start
33
33
  on(MRSK.hosts) do
34
34
  execute *MRSK.auditor.record("Started app version #{MRSK.version}"), verbosity: :debug
@@ -36,7 +36,7 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
36
36
  end
37
37
  end
38
38
 
39
- desc "stop", "Stop app on servers"
39
+ desc "stop", "Stop app container on servers"
40
40
  def stop
41
41
  on(MRSK.hosts) do
42
42
  execute *MRSK.auditor.record("Stopped app"), verbosity: :debug
@@ -44,12 +44,13 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
44
44
  end
45
45
  end
46
46
 
47
- desc "details", "Display details about app containers"
47
+ # FIXME: Drop in favor of just containers?
48
+ desc "details", "Show details about app containers"
48
49
  def details
49
50
  on(MRSK.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.info) }
50
51
  end
51
52
 
52
- desc "exec [CMD]", "Execute a custom command on servers"
53
+ desc "exec [CMD]", "Execute a custom command on servers (use --help to show options)"
53
54
  option :interactive, aliases: "-i", type: :boolean, default: false, desc: "Execute command over ssh for an interactive shell (use for console/bash)"
54
55
  option :reuse, type: :boolean, default: false, desc: "Reuse currently running container instead of starting a new one"
55
56
  def exec(cmd)
@@ -91,21 +92,21 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
91
92
  end
92
93
  end
93
94
 
94
- desc "containers", "List all the app containers currently on servers"
95
+ desc "containers", "Show app containers on servers"
95
96
  def containers
96
97
  on(MRSK.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.list_containers) }
97
98
  end
98
99
 
99
- desc "images", "List all the app images currently on servers"
100
+ desc "images", "Show app images on servers"
100
101
  def images
101
102
  on(MRSK.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.list_images) }
102
103
  end
103
104
 
104
- desc "logs", "Show lines from app on servers"
105
- option :since, aliases: "-s", desc: "Show logs since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)"
106
- option :lines, type: :numeric, aliases: "-n", desc: "Number of log lines to pull from each server"
105
+ desc "logs", "Show log lines from app on servers (use --help to show options)"
106
+ option :since, aliases: "-s", desc: "Show lines since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)"
107
+ option :lines, type: :numeric, aliases: "-n", desc: "Number of lines to show from each server"
107
108
  option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)"
108
- option :follow, aliases: "-f", desc: "Follow logs on primary server (or specific host set by --hosts)"
109
+ option :follow, aliases: "-f", desc: "Follow log on primary server (or specific host set by --hosts)"
109
110
  def logs
110
111
  # FIXME: Catch when app containers aren't running
111
112
 
@@ -133,11 +134,12 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
133
134
 
134
135
  desc "remove", "Remove app containers and images from servers"
135
136
  def remove
137
+ stop
136
138
  remove_containers
137
139
  remove_images
138
140
  end
139
141
 
140
- desc "remove_container [VERSION]", "Remove app container with given version from servers"
142
+ desc "remove_container [VERSION]", "Remove app container with given version from servers", hide: true
141
143
  def remove_container(version)
142
144
  on(MRSK.hosts) do
143
145
  execute *MRSK.auditor.record("Removed app container with version #{version}"), verbosity: :debug
@@ -145,7 +147,7 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
145
147
  end
146
148
  end
147
149
 
148
- desc "remove_containers", "Remove all app containers from servers"
150
+ desc "remove_containers", "Remove all app containers from servers", hide: true
149
151
  def remove_containers
150
152
  on(MRSK.hosts) do
151
153
  execute *MRSK.auditor.record("Removed all app containers"), verbosity: :debug
@@ -153,7 +155,7 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
153
155
  end
154
156
  end
155
157
 
156
- desc "remove_images", "Remove all app images from servers"
158
+ desc "remove_images", "Remove all app images from servers", hide: true
157
159
  def remove_images
158
160
  on(MRSK.hosts) do
159
161
  execute *MRSK.auditor.record("Removed all app images"), verbosity: :debug
@@ -161,8 +163,8 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
161
163
  end
162
164
  end
163
165
 
164
- desc "current_version", "Shows the version currently running"
165
- def current_version
166
+ desc "version", "Show app version currently running on servers"
167
+ def version
166
168
  on(MRSK.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.current_running_version).strip }
167
169
  end
168
170
 
@@ -184,7 +186,12 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
184
186
  def most_recent_version_available(host: MRSK.primary_host)
185
187
  version = nil
186
188
  on(host) { version = capture_with_info(*MRSK.app.most_recent_version_from_available_images).strip }
187
- version.presence
189
+
190
+ if version == "<none>"
191
+ raise "Most recent image available was not tagged with a version (returned <none>)"
192
+ else
193
+ version.presence
194
+ end
188
195
  end
189
196
 
190
197
  def current_running_version(host: MRSK.primary_host)
data/lib/mrsk/cli/base.rb CHANGED
@@ -17,8 +17,8 @@ module Mrsk::Cli
17
17
  class_option :hosts, aliases: "-h", desc: "Run commands on these hosts instead of all (separate by comma)"
18
18
  class_option :roles, aliases: "-r", desc: "Run commands on these roles instead of all (separate by comma)"
19
19
 
20
- class_option :config_file, aliases: "-c", default: "config/deploy.yml", desc: "Path to config file (default: config/deploy.yml)"
21
- class_option :destination, aliases: "-d", desc: "Specify destination to be used for config file (west -> deploy.west.yml)"
20
+ class_option :config_file, aliases: "-c", default: "config/deploy.yml", desc: "Path to config file"
21
+ class_option :destination, aliases: "-d", desc: "Specify destination to be used for config file (staging -> deploy.staging.yml)"
22
22
 
23
23
  def initialize(*)
24
24
  super
@@ -1,11 +1,11 @@
1
1
  class Mrsk::Cli::Build < Mrsk::Cli::Base
2
- desc "deliver", "Deliver a newly built app image to servers"
2
+ desc "deliver", "Build app and push app image to registry then pull image on servers"
3
3
  def deliver
4
- invoke :push
5
- invoke :pull
4
+ push
5
+ pull
6
6
  end
7
7
 
8
- desc "push", "Build locally and push app image to registry"
8
+ desc "push", "Build and push app image to registry"
9
9
  def push
10
10
  cli = self
11
11
 
@@ -26,15 +26,16 @@ class Mrsk::Cli::Build < Mrsk::Cli::Base
26
26
  end
27
27
  end
28
28
 
29
- desc "pull", "Pull app image from the registry onto servers"
29
+ desc "pull", "Pull app image from registry onto servers"
30
30
  def pull
31
31
  on(MRSK.hosts) do
32
32
  execute *MRSK.auditor.record("Pulled image with version #{MRSK.version}"), verbosity: :debug
33
+ execute *MRSK.builder.clean, raise_on_non_zero_exit: false
33
34
  execute *MRSK.builder.pull
34
35
  end
35
36
  end
36
37
 
37
- desc "create", "Create a local build setup"
38
+ desc "create", "Create a build setup"
38
39
  def create
39
40
  run_locally do
40
41
  begin
@@ -51,7 +52,7 @@ class Mrsk::Cli::Build < Mrsk::Cli::Base
51
52
  end
52
53
  end
53
54
 
54
- desc "remove", "Remove local build setup"
55
+ desc "remove", "Remove build setup"
55
56
  def remove
56
57
  run_locally do
57
58
  debug "Using builder: #{MRSK.builder.name}"
@@ -59,7 +60,7 @@ class Mrsk::Cli::Build < Mrsk::Cli::Base
59
60
  end
60
61
  end
61
62
 
62
- desc "details", "Show the name of the configured builder"
63
+ desc "details", "Show build setup"
63
64
  def details
64
65
  run_locally do
65
66
  puts "Builder: #{MRSK.builder.name}"
@@ -1,21 +1,40 @@
1
1
  class Mrsk::Cli::Healthcheck < Mrsk::Cli::Base
2
- desc "perform", "Health check the current version of the app"
2
+ MAX_ATTEMPTS = 5
3
+
4
+ default_command :perform
5
+
6
+ desc "perform", "Health check current app version"
3
7
  def perform
4
8
  on(MRSK.primary_host) do
5
9
  begin
6
10
  execute *MRSK.healthcheck.run
7
11
 
8
12
  target = "Health check against #{MRSK.config.healthcheck["path"]}"
13
+ attempt = 1
9
14
 
10
- if capture_with_info(*MRSK.healthcheck.curl) == "200"
11
- info "#{target} succeeded with 200 OK!"
12
- else
13
- # Catches 1xx, 2xx, 3xx
14
- raise SSHKit::Command::Failed, "#{target} failed to return 200 OK!"
15
+ begin
16
+ status = capture_with_info(*MRSK.healthcheck.curl)
17
+
18
+ if status == "200"
19
+ info "#{target} succeeded with 200 OK!"
20
+ else
21
+ raise "#{target} failed with status #{status}"
22
+ end
23
+ rescue SSHKit::Command::Failed
24
+ if attempt <= MAX_ATTEMPTS
25
+ info "#{target} failed to respond, retrying in #{attempt}s..."
26
+ sleep attempt
27
+ attempt += 1
28
+
29
+ retry
30
+ else
31
+ raise
32
+ end
15
33
  end
16
34
  rescue SSHKit::Command::Failed => e
35
+ error capture_with_info(*MRSK.healthcheck.logs)
36
+
17
37
  if e.message =~ /curl/
18
- # Catches 4xx, 5xx
19
38
  raise SSHKit::Command::Failed, "#{target} failed to return 200 OK!"
20
39
  else
21
40
  raise
data/lib/mrsk/cli/main.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  class Mrsk::Cli::Main < Mrsk::Cli::Base
2
- desc "setup", "Setup all accessories and deploy the app to servers"
2
+ desc "setup", "Setup all accessories and deploy app to servers"
3
3
  def setup
4
4
  print_runtime do
5
5
  invoke "mrsk:cli:server:bootstrap"
@@ -8,7 +8,7 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
8
8
  end
9
9
  end
10
10
 
11
- desc "deploy", "Deploy the app to servers"
11
+ desc "deploy", "Deploy app to servers"
12
12
  def deploy
13
13
  runtime = print_runtime do
14
14
  say "Ensure Docker is installed...", :magenta
@@ -35,7 +35,7 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
35
35
  audit_broadcast "Deployed app in #{runtime.to_i} seconds"
36
36
  end
37
37
 
38
- desc "redeploy", "Deploy new version of the app to servers (without bootstrapping servers, starting Traefik, pruning, and registry login)"
38
+ desc "redeploy", "Deploy app to servers without bootstrapping servers, starting Traefik, pruning, and registry login"
39
39
  def redeploy
40
40
  runtime = print_runtime do
41
41
  say "Build and push app image...", :magenta
@@ -50,7 +50,7 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
50
50
  audit_broadcast "Redeployed app in #{runtime.to_i} seconds"
51
51
  end
52
52
 
53
- desc "rollback [VERSION]", "Rollback the app to VERSION"
53
+ desc "rollback [VERSION]", "Rollback app to VERSION"
54
54
  def rollback(version)
55
55
  MRSK.version = version
56
56
 
@@ -68,7 +68,7 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
68
68
  end
69
69
  end
70
70
 
71
- desc "details", "Display details about Traefik and app containers"
71
+ desc "details", "Show details about all containers"
72
72
  def details
73
73
  invoke "mrsk:cli:traefik:details"
74
74
  invoke "mrsk:cli:app:details"
@@ -82,7 +82,7 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
82
82
  end
83
83
  end
84
84
 
85
- desc "config", "Show combined config"
85
+ desc "config", "Show combined config (including secrets!)"
86
86
  def config
87
87
  run_locally do
88
88
  puts MRSK.config.to_h.to_yaml
@@ -132,40 +132,44 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
132
132
  File.write(env_path, ERB.new(File.read(env_template_path)).result, perm: 0600)
133
133
  end
134
134
 
135
- desc "remove", "Remove Traefik, app, and registry session from servers"
135
+ desc "remove", "Remove Traefik, app, accessories, and registry session from servers"
136
+ option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
136
137
  def remove
137
- invoke "mrsk:cli:traefik:remove"
138
- invoke "mrsk:cli:app:remove"
139
- invoke "mrsk:cli:registry:logout"
138
+ if options[:confirmed] || ask(remove_confirmation_question, limited_to: %w( y N ), default: "N") == "y"
139
+ invoke "mrsk:cli:traefik:remove", [], options.without(:confirmed)
140
+ invoke "mrsk:cli:app:remove", [], options.without(:confirmed)
141
+ invoke "mrsk:cli:accessory:remove", [ "all" ]
142
+ invoke "mrsk:cli:registry:logout", [], options.without(:confirmed)
143
+ end
140
144
  end
141
145
 
142
- desc "version", "Display the MRSK version"
146
+ desc "version", "Show MRSK version"
143
147
  def version
144
148
  puts Mrsk::VERSION
145
149
  end
146
150
 
147
- desc "accessory", "Manage the accessories"
151
+ desc "accessory", "Manage accessories (db/redis/search)"
148
152
  subcommand "accessory", Mrsk::Cli::Accessory
149
153
 
150
- desc "app", "Manage the application"
154
+ desc "app", "Manage application"
151
155
  subcommand "app", Mrsk::Cli::App
152
156
 
153
- desc "build", "Build the application image"
157
+ desc "build", "Build application image"
154
158
  subcommand "build", Mrsk::Cli::Build
155
159
 
156
- desc "healthcheck", "Healthcheck the application"
160
+ desc "healthcheck", "Healthcheck application"
157
161
  subcommand "healthcheck", Mrsk::Cli::Healthcheck
158
162
 
159
163
  desc "prune", "Prune old application images and containers"
160
164
  subcommand "prune", Mrsk::Cli::Prune
161
165
 
162
- desc "registry", "Login and out of the image registry"
166
+ desc "registry", "Login and -out of the image registry"
163
167
  subcommand "registry", Mrsk::Cli::Registry
164
168
 
165
169
  desc "server", "Bootstrap servers with Docker"
166
170
  subcommand "server", Mrsk::Cli::Server
167
171
 
168
- desc "traefik", "Manage the Traefik load balancer"
172
+ desc "traefik", "Manage Traefik load balancer"
169
173
  subcommand "traefik", Mrsk::Cli::Traefik
170
174
 
171
175
  private
@@ -174,4 +178,10 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
174
178
  on(host) { container_names = capture_with_info(*MRSK.app.list_container_names).split("\n") }
175
179
  Array(container_names).include?(container_name)
176
180
  end
181
+
182
+ def remove_confirmation_question
183
+ "This will remove all containers and images. " +
184
+ (MRSK.config.accessories.any? ? "Including #{MRSK.config.accessories.collect(&:name).to_sentence}. " : "") +
185
+ "Are you sure?"
186
+ end
177
187
  end
@@ -1,8 +1,8 @@
1
1
  class Mrsk::Cli::Prune < Mrsk::Cli::Base
2
2
  desc "all", "Prune unused images and stopped containers"
3
3
  def all
4
- invoke :containers
5
- invoke :images
4
+ containers
5
+ images
6
6
  end
7
7
 
8
8
  desc "images", "Prune unused images older than 7 days"
@@ -13,7 +13,7 @@ class Mrsk::Cli::Prune < Mrsk::Cli::Base
13
13
  end
14
14
  end
15
15
 
16
- desc "containers", "Prune stopped containers for the service older than 3 days"
16
+ desc "containers", "Prune stopped containers older than 3 days"
17
17
  def containers
18
18
  on(MRSK.hosts) do
19
19
  execute *MRSK.auditor.record("Pruned containers"), verbosity: :debug
@@ -1,15 +1,17 @@
1
1
  class Mrsk::Cli::Registry < Mrsk::Cli::Base
2
- desc "login", "Login to the registry locally and remotely"
2
+ desc "login", "Log in to registry locally and remotely"
3
3
  def login
4
4
  run_locally { execute *MRSK.registry.login }
5
5
  on(MRSK.hosts) { execute *MRSK.registry.login }
6
+ # FIXME: This rescue needed?
6
7
  rescue ArgumentError => e
7
8
  puts e.message
8
9
  end
9
10
 
10
- desc "logout", "Logout of the registry remotely"
11
+ desc "logout", "Log out of registry remotely"
11
12
  def logout
12
13
  on(MRSK.hosts) { execute *MRSK.registry.logout }
14
+ # FIXME: This rescue needed?
13
15
  rescue ArgumentError => e
14
16
  puts e.message
15
17
  end
@@ -1,5 +1,5 @@
1
1
  class Mrsk::Cli::Server < Mrsk::Cli::Base
2
- desc "bootstrap", "Ensure Docker is installed on the servers"
2
+ desc "bootstrap", "Ensure Docker is installed on servers"
3
3
  def bootstrap
4
4
  on(MRSK.hosts + MRSK.accessory_hosts) { execute "which docker || (apt-get update -y && apt-get install docker.io -y)" }
5
5
  end
@@ -1,5 +1,4 @@
1
- # Name of your application. Used to uniquely configuring Traefik and app containers.
2
- # Your Dockerfile should set LABEL service=the-same-value to ensure image pruning works.
1
+ # Name of your application. Used to uniquely configure containers.
3
2
  service: my-app
4
3
 
5
4
  # Name of the container image.
@@ -14,4 +13,64 @@ registry:
14
13
  # Specify the registry server, if you're not using Docker Hub
15
14
  # server: registry.digitalocean.com / ghcr.io / ...
16
15
  username: my-user
17
- password: my-password-should-go-somewhere-safe
16
+ password:
17
+ - MRSK_REGISTRY_PASSWORD
18
+
19
+ # Inject ENV variables into containers (secrets come from .env).
20
+ # env:
21
+ # clear:
22
+ # DB_HOST: 192.168.0.2
23
+ # secret:
24
+ # - RAILS_MASTER_KEY
25
+
26
+ # Call a broadcast command on deploys.
27
+ # audit_broadcast_cmd:
28
+ # bin/broadcast_to_bc
29
+
30
+ # Use a different ssh user than root
31
+ # ssh:
32
+ # user: app
33
+
34
+ # Configure builder setup.
35
+ # builder:
36
+ # args:
37
+ # RUBY_VERSION: 3.2.0
38
+ # secrets:
39
+ # - GITHUB_TOKEN
40
+ # remote:
41
+ # arch: amd64
42
+ # host: ssh://app@192.168.0.1
43
+
44
+ # Use accessory services (secrets come from .env).
45
+ # accessories:
46
+ # db:
47
+ # image: mysql:8.0
48
+ # host: 192.168.0.2
49
+ # port: 3306
50
+ # env:
51
+ # clear:
52
+ # MYSQL_ROOT_HOST: '%'
53
+ # secret:
54
+ # - MYSQL_ROOT_PASSWORD
55
+ # files:
56
+ # - config/mysql/production.cnf:/etc/mysql/my.cnf
57
+ # - db/production.sql.erb:/docker-entrypoint-initdb.d/setup.sql
58
+ # directories:
59
+ # - data:/var/lib/mysql
60
+ # redis:
61
+ # image: redis:7.0
62
+ # host: 192.168.0.2
63
+ # port: 6379
64
+ # directories:
65
+ # - data:/data
66
+
67
+ # Configure custom arguments for Traefik
68
+ # traefik:
69
+ # args:
70
+ # accesslog: true
71
+ # accesslog.format: json
72
+
73
+ # Configure a custom healthcheck (default is /up on port 3000)
74
+ # healthcheck:
75
+ # path: /healthz
76
+ # port: 4000
@@ -6,12 +6,12 @@ class Mrsk::Cli::Traefik < Mrsk::Cli::Base
6
6
 
7
7
  desc "reboot", "Reboot Traefik on servers (stop container, remove container, start new container)"
8
8
  def reboot
9
- invoke :stop
10
- invoke :remove_container
11
- invoke :boot
9
+ stop
10
+ remove_container
11
+ boot
12
12
  end
13
13
 
14
- desc "start", "Start existing Traefik on servers"
14
+ desc "start", "Start existing Traefik container on servers"
15
15
  def start
16
16
  on(MRSK.traefik_hosts) do
17
17
  execute *MRSK.auditor.record("Started traefik"), verbosity: :debug
@@ -19,7 +19,7 @@ class Mrsk::Cli::Traefik < Mrsk::Cli::Base
19
19
  end
20
20
  end
21
21
 
22
- desc "stop", "Stop Traefik on servers"
22
+ desc "stop", "Stop existing Traefik container on servers"
23
23
  def stop
24
24
  on(MRSK.traefik_hosts) do
25
25
  execute *MRSK.auditor.record("Stopped traefik"), verbosity: :debug
@@ -27,13 +27,13 @@ class Mrsk::Cli::Traefik < Mrsk::Cli::Base
27
27
  end
28
28
  end
29
29
 
30
- desc "restart", "Restart Traefik on servers"
30
+ desc "restart", "Restart existing Traefik container on servers"
31
31
  def restart
32
- invoke :stop
33
- invoke :start
32
+ stop
33
+ start
34
34
  end
35
35
 
36
- desc "details", "Display details about Traefik containers from servers"
36
+ desc "details", "Show details about Traefik container from servers"
37
37
  def details
38
38
  on(MRSK.traefik_hosts) { |host| puts_by_host host, capture_with_info(*MRSK.traefik.info), type: "Traefik" }
39
39
  end
@@ -64,12 +64,12 @@ class Mrsk::Cli::Traefik < Mrsk::Cli::Base
64
64
 
65
65
  desc "remove", "Remove Traefik container and image from servers"
66
66
  def remove
67
- invoke :stop
68
- invoke :remove_container
69
- invoke :remove_image
67
+ stop
68
+ remove_container
69
+ remove_image
70
70
  end
71
71
 
72
- desc "remove_container", "Remove Traefik container from servers"
72
+ desc "remove_container", "Remove Traefik container from servers", hide: true
73
73
  def remove_container
74
74
  on(MRSK.traefik_hosts) do
75
75
  execute *MRSK.auditor.record("Removed traefik container"), verbosity: :debug
@@ -77,7 +77,7 @@ class Mrsk::Cli::Traefik < Mrsk::Cli::Base
77
77
  end
78
78
  end
79
79
 
80
- desc "remove_container", "Remove Traefik image from servers"
80
+ desc "remove_container", "Remove Traefik image from servers", hide: true
81
81
  def remove_image
82
82
  on(MRSK.traefik_hosts) do
83
83
  execute *MRSK.auditor.record("Removed traefik image"), verbosity: :debug
@@ -49,32 +49,32 @@ class Mrsk::Commander
49
49
  @app ||= Mrsk::Commands::App.new(config)
50
50
  end
51
51
 
52
- def builder
53
- @builder ||= Mrsk::Commands::Builder.new(config)
52
+ def accessory(name)
53
+ Mrsk::Commands::Accessory.new(config, name: name)
54
54
  end
55
55
 
56
- def traefik
57
- @traefik ||= Mrsk::Commands::Traefik.new(config)
56
+ def auditor
57
+ @auditor ||= Mrsk::Commands::Auditor.new(config)
58
58
  end
59
59
 
60
- def registry
61
- @registry ||= Mrsk::Commands::Registry.new(config)
60
+ def builder
61
+ @builder ||= Mrsk::Commands::Builder.new(config)
62
62
  end
63
63
 
64
- def prune
65
- @prune ||= Mrsk::Commands::Prune.new(config)
64
+ def healthcheck
65
+ @healthcheck ||= Mrsk::Commands::Healthcheck.new(config)
66
66
  end
67
67
 
68
- def accessory(name)
69
- Mrsk::Commands::Accessory.new(config, name: name)
68
+ def prune
69
+ @prune ||= Mrsk::Commands::Prune.new(config)
70
70
  end
71
71
 
72
- def auditor
73
- @auditor ||= Mrsk::Commands::Auditor.new(config)
72
+ def registry
73
+ @registry ||= Mrsk::Commands::Registry.new(config)
74
74
  end
75
75
 
76
- def healthcheck
77
- @healthcheck ||= Mrsk::Commands::Healthcheck.new(config)
76
+ def traefik
77
+ @traefik ||= Mrsk::Commands::Traefik.new(config)
78
78
  end
79
79
 
80
80
 
@@ -10,10 +10,10 @@ class Mrsk::Commands::Accessory < Mrsk::Commands::Base
10
10
  def run
11
11
  docker :run,
12
12
  "--name", service_name,
13
- "-d",
13
+ "--detach",
14
14
  "--restart", "unless-stopped",
15
15
  "--log-opt", "max-size=#{MAX_LOG_SIZE}",
16
- "-p", port,
16
+ "--publish", port,
17
17
  *env_args,
18
18
  *volume_args,
19
19
  *label_args,
@@ -35,14 +35,14 @@ class Mrsk::Commands::Accessory < Mrsk::Commands::Base
35
35
 
36
36
  def logs(since: nil, lines: nil, grep: nil)
37
37
  pipe \
38
- docker(:logs, service_name, (" --since #{since}" if since), (" -n #{lines}" if lines), "-t", "2>&1"),
38
+ docker(:logs, service_name, (" --since #{since}" if since), (" --tail #{lines}" if lines), "--timestamps", "2>&1"),
39
39
  ("grep '#{grep}'" if grep)
40
40
  end
41
41
 
42
42
  def follow_logs(grep: nil)
43
43
  run_over_ssh \
44
44
  pipe \
45
- docker(:logs, service_name, "-t", "-n", "10", "-f", "2>&1"),
45
+ docker(:logs, service_name, "--timestamps", "--tail", "10", "--follow", "2>&1"),
46
46
  (%(grep "#{grep}") if grep)
47
47
  end
48
48
 
@@ -96,11 +96,11 @@ class Mrsk::Commands::Accessory < Mrsk::Commands::Base
96
96
  end
97
97
 
98
98
  def remove_container
99
- docker :container, :prune, "-f", *service_filter
99
+ docker :container, :prune, "--force", *service_filter
100
100
  end
101
101
 
102
102
  def remove_image
103
- docker :image, :prune, "-a", "-f", *service_filter
103
+ docker :image, :prune, "--all", "--force", *service_filter
104
104
  end
105
105
 
106
106
  private
@@ -3,7 +3,7 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
3
3
  role = config.role(role)
4
4
 
5
5
  docker :run,
6
- "-d",
6
+ "--detach",
7
7
  "--restart unless-stopped",
8
8
  "--log-opt", "max-size=#{MAX_LOG_SIZE}",
9
9
  "--name", service_with_version,
@@ -30,7 +30,7 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
30
30
  def logs(since: nil, lines: nil, grep: nil)
31
31
  pipe \
32
32
  current_container_id,
33
- "xargs docker logs#{" --since #{since}" if since}#{" -n #{lines}" if lines} 2>&1",
33
+ "xargs docker logs#{" --since #{since}" if since}#{" --tail #{lines}" if lines} 2>&1",
34
34
  ("grep '#{grep}'" if grep)
35
35
  end
36
36
 
@@ -38,7 +38,7 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
38
38
  run_over_ssh \
39
39
  pipe(
40
40
  current_container_id,
41
- "xargs docker logs -t -n 10 -f 2>&1",
41
+ "xargs docker logs --timestamps --tail 10 --follow 2>&1",
42
42
  (%(grep "#{grep}") if grep)
43
43
  ),
44
44
  host: host
@@ -72,7 +72,7 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
72
72
 
73
73
 
74
74
  def current_container_id
75
- docker :ps, "-q", *service_filter
75
+ docker :ps, "--quiet", *service_filter
76
76
  end
77
77
 
78
78
  def current_running_version
@@ -97,7 +97,7 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
97
97
 
98
98
 
99
99
  def list_containers
100
- docker :container, :ls, "-a", *service_filter
100
+ docker :container, :ls, "--all", *service_filter
101
101
  end
102
102
 
103
103
  def list_container_names
@@ -111,7 +111,7 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
111
111
  end
112
112
 
113
113
  def remove_containers
114
- docker :container, :prune, "-f", *service_filter
114
+ docker :container, :prune, "--force", *service_filter
115
115
  end
116
116
 
117
117
  def list_images
@@ -119,7 +119,7 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
119
119
  end
120
120
 
121
121
  def remove_images
122
- docker :image, :prune, "-a", "-f", *service_filter
122
+ docker :image, :prune, "--all", "--force", *service_filter
123
123
  end
124
124
 
125
125
 
@@ -11,9 +11,7 @@ class Mrsk::Commands::Auditor < Mrsk::Commands::Base
11
11
  # Runs locally
12
12
  def broadcast(line)
13
13
  if broadcast_cmd = config.audit_broadcast_cmd
14
- pipe \
15
- [ :echo, tagged_broadcast_line(line) ],
16
- broadcast_cmd
14
+ [ broadcast_cmd, tagged_broadcast_line(line) ]
17
15
  end
18
16
  end
19
17
 
@@ -18,7 +18,7 @@ module Mrsk::Commands
18
18
  end
19
19
 
20
20
  def container_id_for(container_name:)
21
- docker :container, :ls, "-a", "-f", "name=#{container_name}", "-q"
21
+ docker :container, :ls, "--all", "--filter", "name=#{container_name}", "--quiet"
22
22
  end
23
23
 
24
24
  private
@@ -1,6 +1,10 @@
1
1
  class Mrsk::Commands::Builder::Base < Mrsk::Commands::Base
2
2
  delegate :argumentize, to: Mrsk::Utils
3
3
 
4
+ def clean
5
+ docker :image, :rm, "--force", config.absolute_image
6
+ end
7
+
4
8
  def pull
5
9
  docker :pull, config.absolute_image
6
10
  end
@@ -1,5 +1,5 @@
1
1
  class Mrsk::Commands::Builder < Mrsk::Commands::Base
2
- delegate :create, :remove, :push, :pull, :info, to: :target
2
+ delegate :create, :remove, :push, :clean, :pull, :info, to: :target
3
3
 
4
4
  def name
5
5
  target.class.to_s.remove("Mrsk::Commands::Builder::").underscore
@@ -5,9 +5,9 @@ class Mrsk::Commands::Healthcheck < Mrsk::Commands::Base
5
5
  web = config.role(:web)
6
6
 
7
7
  docker :run,
8
- "-d",
8
+ "--detach",
9
9
  "--name", container_name_with_version,
10
- "-p", "#{EXPOSED_PORT}:#{config.healthcheck["port"]}",
10
+ "--publish", "#{EXPOSED_PORT}:#{config.healthcheck["port"]}",
11
11
  "--label", "service=#{container_name}",
12
12
  *web.env_args,
13
13
  *config.volume_args,
@@ -16,19 +16,19 @@ class Mrsk::Commands::Healthcheck < Mrsk::Commands::Base
16
16
  end
17
17
 
18
18
  def curl
19
- [ :curl, "--silent", "--output", "/dev/null", "--write-out", "'%{http_code}'", health_url ]
19
+ [ :curl, "--silent", "--output", "/dev/null", "--write-out", "'%{http_code}'", "--max-time", "2", health_url ]
20
+ end
21
+
22
+ def logs
23
+ pipe container_id, xargs(docker(:logs, "--tail", 50, "2>&1"))
20
24
  end
21
25
 
22
26
  def stop
23
- pipe \
24
- container_id_for(container_name: container_name),
25
- xargs(docker(:stop))
27
+ pipe container_id, xargs(docker(:stop))
26
28
  end
27
29
 
28
30
  def remove
29
- pipe \
30
- container_id_for(container_name: container_name),
31
- xargs(docker(:container, :rm))
31
+ pipe container_id, xargs(docker(:container, :rm))
32
32
  end
33
33
 
34
34
  private
@@ -40,6 +40,10 @@ class Mrsk::Commands::Healthcheck < Mrsk::Commands::Base
40
40
  "healthcheck-#{config.service_with_version}"
41
41
  end
42
42
 
43
+ def container_id
44
+ container_id_for(container_name: container_name)
45
+ end
46
+
43
47
  def health_url
44
48
  "http://localhost:#{EXPOSED_PORT}#{config.healthcheck["path"]}"
45
49
  end
@@ -2,10 +2,19 @@ class Mrsk::Commands::Registry < Mrsk::Commands::Base
2
2
  delegate :registry, to: :config
3
3
 
4
4
  def login
5
- docker :login, registry["server"], "-u", redact(registry["username"]), "-p", redact(registry["password"])
5
+ docker :login, registry["server"], "-u", redact(registry["username"]), "-p", redact(lookup_password)
6
6
  end
7
7
 
8
8
  def logout
9
9
  docker :logout, registry["server"]
10
10
  end
11
+
12
+ private
13
+ def lookup_password
14
+ if registry["password"].is_a?(Array)
15
+ ENV.fetch(registry["password"].first).dup
16
+ else
17
+ registry["password"]
18
+ end
19
+ end
11
20
  end
@@ -1,11 +1,11 @@
1
1
  class Mrsk::Commands::Traefik < Mrsk::Commands::Base
2
2
  def run
3
3
  docker :run, "--name traefik",
4
- "-d",
4
+ "--detach",
5
5
  "--restart", "unless-stopped",
6
6
  "--log-opt", "max-size=#{MAX_LOG_SIZE}",
7
- "-p 80:80",
8
- "-v /var/run/docker.sock:/var/run/docker.sock",
7
+ "--publish", "80:80",
8
+ "--volume", "/var/run/docker.sock:/var/run/docker.sock",
9
9
  "traefik",
10
10
  "--providers.docker",
11
11
  "--log.level=DEBUG",
@@ -26,23 +26,23 @@ class Mrsk::Commands::Traefik < Mrsk::Commands::Base
26
26
 
27
27
  def logs(since: nil, lines: nil, grep: nil)
28
28
  pipe \
29
- docker(:logs, "traefik", (" --since #{since}" if since), (" -n #{lines}" if lines), "-t", "2>&1"),
29
+ docker(:logs, "traefik", (" --since #{since}" if since), (" --tail #{lines}" if lines), "--timestamps", "2>&1"),
30
30
  ("grep '#{grep}'" if grep)
31
31
  end
32
32
 
33
33
  def follow_logs(host:, grep: nil)
34
34
  run_over_ssh pipe(
35
- docker(:logs, "traefik", "-t", "-n", "10", "-f", "2>&1"),
35
+ docker(:logs, "traefik", "--timestamps", "--tail", "10", "--follow", "2>&1"),
36
36
  (%(grep "#{grep}") if grep)
37
37
  ).join(" "), host: host
38
38
  end
39
39
 
40
40
  def remove_container
41
- docker :container, :prune, "-f", "--filter", "label=org.opencontainers.image.title=Traefik"
41
+ docker :container, :prune, "--force", "--filter", "label=org.opencontainers.image.title=Traefik"
42
42
  end
43
43
 
44
44
  def remove_image
45
- docker :image, :prune, "-a", "-f", "--filter", "label=org.opencontainers.image.title=Traefik"
45
+ docker :image, :prune, "--all", "--force", "--filter", "label=org.opencontainers.image.title=Traefik"
46
46
  end
47
47
 
48
48
  private
data/lib/mrsk/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Mrsk
2
- VERSION = "0.7.1"
2
+ VERSION = "0.8.0"
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.7.1
4
+ version: 0.8.0
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-02-18 00:00:00.000000000 Z
11
+ date: 2023-02-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport