mrsk 0.3.1 → 0.5.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: 7d7eec74db21b02c791939440655365ce5d24b9a75c6a79ffad1db2c6bb44ee8
4
- data.tar.gz: bbacf7f546dd7136b2d2412535a466f44b9000d74ed74cf3b1ddd0b3750728a1
3
+ metadata.gz: d7f288f243a0192ea977464282b13b71f4ba585db01db7e7d7ae805e5509cffe
4
+ data.tar.gz: e96ababf967b92aa6236339d7eb24382207789a3b2c2b64d95caff08fabda32c
5
5
  SHA512:
6
- metadata.gz: 919de205161fba4e0565b79fa67caf5439bfeefcac5fea4b36e48aac45c827922daf3c9ea2dba42cdfd55a954e9a33b58c46b087bb9f2e59b10071088f643b83
7
- data.tar.gz: 7af61fe9154261ade34923c0aeaf8f095f6e60cb8bf57f9aabea2488502b1c10b35ff3f2be4e5821aa5e31e59ec0b0d132949e8c9e85ee689e8465c85ae8fa05
6
+ metadata.gz: b1e33f7d1598785b107a4c7610df678c341c2d26899b2c6e67739991ed573f9fcfeb511ac422f78b1447c5952f8e90d4c581795729f3503bc18f89b6334c3686
7
+ data.tar.gz: 1d3db7364c2574a3222ec1ab5c03c91ad1c1d55c83d0d397075b9d86fd41d2dc3e9d67068b619a3d5495036357301b7d2d2be618e58e2b8e40c972534da95c89
data/README.md CHANGED
@@ -46,6 +46,15 @@ Kubernetes is a beast. Running it yourself on your own hardware is not for the f
46
46
 
47
47
  ## Configuration
48
48
 
49
+ ### Using .env file to load required environment variables
50
+
51
+ MRSK uses [dotenv](https://github.com/bkeepers/dotenv) to automatically load environment variables set in the `.env` file present in the application root. This file can be used to set variables like `MRSK_REGISTRY_PASSWORD` or database passwords. But for this reason you must ensure that .env files are not checked into Git or included in your Dockerfile! The format is just key-value like:
52
+
53
+ ```bash
54
+ MRSK_REGISTRY_PASSWORD=pw
55
+ DB_PASSWORD=secret123
56
+ ```
57
+
49
58
  ### Using another registry than Docker Hub
50
59
 
51
60
  The default registry is Docker Hub, but you can change it using `registry/server`:
@@ -226,6 +235,18 @@ RUN --mount=type=secret,id=GITHUB_TOKEN \
226
235
  bundle install
227
236
  ```
228
237
 
238
+ ### Using command arguments for Traefik
239
+
240
+ You can customize the traefik command line:
241
+
242
+ ```yaml
243
+ traefik:
244
+ accesslog: true
245
+ accesslog.format: json
246
+ metrics.prometheus: true
247
+ metrics.prometheus.buckets: 0.1,0.3,1.2,5.0
248
+ ```
249
+
229
250
  ### Configuring build args for new images
230
251
 
231
252
  Build arguments that aren't secret can also be configured:
@@ -281,9 +302,9 @@ Now run `mrsk accessory start mysql` to start the MySQL server on 1.1.1.3. See `
281
302
 
282
303
  ## Commands
283
304
 
284
- ### Running remote execution and runners
305
+ ### Running commands on servers
285
306
 
286
- If you need to execute commands inside the Rails containers, you can use `mrsk app exec` and `mrsk app runner`. Examples:
307
+ You can execute one-off commands on the servers:
287
308
 
288
309
  ```bash
289
310
  # Runs command on all servers
@@ -326,13 +347,25 @@ Database adapter sqlite3
326
347
  Database schema version 20221231233303
327
348
 
328
349
  # Run Rails runner on primary server
329
- mrsk app runner -p 'puts Rails.application.config.time_zone'
350
+ mrsk app exec -p 'bin/rails runner "puts Rails.application.config.time_zone"'
330
351
  UTC
331
352
  ```
332
353
 
333
- ### Running a Rails console
354
+ ### Running interactive commands over SSH
355
+
356
+ You can run interactive commands, like a Rails console or a bash session, on a server (default is primary, use `--hosts` to connect to another):
357
+
358
+ ```bash
359
+ # Starts a bash session in a new container made from the most recent app image
360
+ mrsk app exec -i bash
361
+
362
+ # Starts a bash session in the currently running container for the app
363
+ mrsk app exec -i --reuse bash
364
+
365
+ # Starts a Rails console in a new container made from the most recent app image
366
+ mrsk app exec -i 'bin/rails console'
367
+ ```
334
368
 
335
- If you need to interact with the production console for the app, you can use `mrsk app console`, which will start a Rails console session on the primary host. You can start the console on a different host using `mrsk app console --host 192.168.0.2`. Be mindful that this is a live wire! Any changes made to the production database will take effect immeditately.
336
369
 
337
370
  ### Running details to see state of containers
338
371
 
data/bin/mrsk CHANGED
@@ -3,6 +3,7 @@
3
3
  # Prevent failures from being reported twice.
4
4
  Thread.report_on_exception = false
5
5
 
6
+ require "dotenv/load"
6
7
  require "mrsk/cli"
7
8
 
8
9
  begin
@@ -10,4 +11,7 @@ begin
10
11
  rescue SSHKit::Runner::ExecuteError => e
11
12
  puts " \e[31mERROR (#{e.cause.class}): #{e.cause.message}\e[0m"
12
13
  puts e.cause.backtrace if ENV["VERBOSE"]
14
+ rescue => e
15
+ puts " \e[31mERROR (#{e.class}): #{e.message}\e[0m"
16
+ puts e.backtrace if ENV["VERBOSE"]
13
17
  end
@@ -82,21 +82,27 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
82
82
  end
83
83
  end
84
84
 
85
- desc "exec [NAME] [CMD]", "Execute a custom command on accessory host"
86
- option :run, type: :boolean, default: false, desc: "Start a new container to run the command rather than reusing existing"
85
+ desc "exec [NAME] [CMD]", "Execute a custom command on servers"
86
+ option :interactive, aliases: "-i", type: :boolean, default: false, desc: "Execute command over ssh for an interactive shell (use for console/bash)"
87
+ option :reuse, type: :boolean, default: false, desc: "Reuse currently running container instead of starting a new one"
87
88
  def exec(name, cmd)
88
89
  with_accessory(name) do |accessory|
89
- runner = options[:run] ? :run_exec : :exec
90
- on(accessory.host) { |host| puts_by_host host, capture_with_info(*accessory.send(runner, cmd)) }
91
- end
92
- end
90
+ case
91
+ when options[:interactive] && options[:reuse]
92
+ say "Launching interactive command with via SSH from existing container...", :magenta
93
+ run_locally { exec accessory.execute_in_existing_container_over_ssh(cmd) }
93
94
 
94
- desc "bash [NAME]", "Start a bash session on primary host (or specific host set by --hosts)"
95
- def bash(name)
96
- with_accessory(name) do |accessory|
97
- run_locally do
98
- info "Launching bash session on #{accessory.host}"
99
- exec accessory.bash(host: accessory.host)
95
+ when options[:interactive]
96
+ say "Launching interactive command via SSH from new container...", :magenta
97
+ run_locally { exec accessory.execute_in_new_container_over_ssh(cmd) }
98
+
99
+ when options[:reuse]
100
+ say "Launching command from existing container...", :magenta
101
+ on(accessory.host) { capture_with_info(*accessory.execute_in_existing_container(cmd)) }
102
+
103
+ else
104
+ say "Launching command from new container...", :magenta
105
+ on(accessory.host) { capture_with_info(*accessory.execute_in_new_container(cmd)) }
100
106
  end
101
107
  end
102
108
  end
@@ -118,7 +124,7 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
118
124
  end
119
125
  else
120
126
  since = options[:since]
121
- lines = options[:lines]
127
+ lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set
122
128
 
123
129
  on(accessory.host) do
124
130
  puts capture_with_info(*accessory.logs(since: since, lines: lines, grep: grep))
@@ -148,7 +154,7 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base
148
154
  end
149
155
  end
150
156
 
151
- desc "remove_container [NAME]", "Remove accessory image from host"
157
+ desc "remove_image [NAME]", "Remove accessory image from host"
152
158
  def remove_image(name)
153
159
  with_accessory(name) do |accessory|
154
160
  on(accessory.host) { execute *accessory.remove_image }
data/lib/mrsk/cli/app.rb CHANGED
@@ -1,32 +1,39 @@
1
1
  require "mrsk/cli/base"
2
2
 
3
3
  class Mrsk::Cli::App < Mrsk::Cli::Base
4
- desc "boot", "Boot app on servers (or start them if they've already been booted)"
4
+ desc "boot", "Boot app on servers (or reboot app if already running)"
5
5
  def boot
6
- MRSK.config.roles.each do |role|
7
- on(role.hosts) do |host|
8
- begin
9
- execute *MRSK.app.run(role: role.name)
10
- rescue SSHKit::Command::Failed => e
11
- if e.message =~ /already in use/
12
- error "Container with same version already deployed on #{host}, starting that instead"
13
- execute *MRSK.app.start, host: host
14
- else
15
- raise
6
+ cli = self
7
+
8
+ say "Ensure no other version of the app is running...", :magenta
9
+ stop
10
+
11
+ say "Get most recent version available as an image...", :magenta unless options[:version]
12
+ using_version(options[:version] || most_recent_version_available) do |version|
13
+ say "Start container with version #{version} (or reboot if already running)...", :magenta
14
+
15
+ MRSK.config.roles.each do |role|
16
+ on(role.hosts) do |host|
17
+ begin
18
+ execute *MRSK.app.run(role: role.name)
19
+ rescue SSHKit::Command::Failed => e
20
+ if e.message =~ /already in use/
21
+ error "Rebooting container with same version already deployed on #{host}"
22
+
23
+ cli.remove_container version
24
+ execute *MRSK.app.run(role: role.name)
25
+ else
26
+ raise
27
+ end
16
28
  end
17
29
  end
18
30
  end
19
31
  end
20
32
  end
21
-
33
+
22
34
  desc "start", "Start existing app on servers (use --version=<git-hash> to designate specific version)"
23
- option :version, desc: "Defaults to the most recent git-hash in local repository"
24
35
  def start
25
- if (version = options[:version]).present?
26
- on(MRSK.hosts) { execute *MRSK.app.start(version: version) }
27
- else
28
- on(MRSK.hosts) { execute *MRSK.app.start, raise_on_non_zero_exit: false }
29
- end
36
+ on(MRSK.hosts) { execute *MRSK.app.start, raise_on_non_zero_exit: false }
30
37
  end
31
38
 
32
39
  desc "stop", "Stop app on servers"
@@ -40,45 +47,38 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
40
47
  end
41
48
 
42
49
  desc "exec [CMD]", "Execute a custom command on servers"
43
- option :method, aliases: "-m", default: "exec", desc: "Execution method: [exec] perform inside app container / [run] perform in new container / [ssh] perform over ssh"
50
+ option :interactive, aliases: "-i", type: :boolean, default: false, desc: "Execute command over ssh for an interactive shell (use for console/bash)"
51
+ option :reuse, type: :boolean, default: false, desc: "Reuse currently running container instead of starting a new one"
44
52
  def exec(cmd)
45
- runner = \
46
- case options[:method]
47
- when "exec" then "exec"
48
- when "run" then "run_exec"
49
- when "ssh" then "exec_over_ssh"
50
- else raise "Unknown method: #{options[:method]}"
51
- end.inquiry
52
-
53
- if runner.exec_over_ssh?
54
- run_locally do
55
- info "Launching command on #{MRSK.primary_host}"
56
- exec MRSK.app.exec_over_ssh(cmd, host: MRSK.primary_host)
53
+ case
54
+ when options[:interactive] && options[:reuse]
55
+ say "Get current version of running container...", :magenta unless options[:version]
56
+ using_version(options[:version] || current_running_version) do |version|
57
+ say "Launching interactive command with version #{version} via SSH from existing container on #{MRSK.primary_host}...", :magenta
58
+ run_locally { exec MRSK.app.execute_in_existing_container_over_ssh(cmd, host: MRSK.primary_host) }
57
59
  end
58
- else
59
- on(MRSK.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.send(runner, cmd)) }
60
- end
61
- end
62
60
 
63
- desc "console", "Start Rails Console on primary host (or specific host set by --hosts)"
64
- def console
65
- run_locally do
66
- info "Launching Rails console on #{MRSK.primary_host}"
67
- exec MRSK.app.console(host: MRSK.primary_host)
68
- end
69
- end
61
+ when options[:interactive]
62
+ say "Get most recent version available as an image...", :magenta unless options[:version]
63
+ using_version(options[:version] || most_recent_version_available) do |version|
64
+ say "Launching interactive command with version #{version} via SSH from new container on #{MRSK.primary_host}...", :magenta
65
+ run_locally { exec MRSK.app.execute_in_new_container_over_ssh(cmd, host: MRSK.primary_host) }
66
+ end
70
67
 
71
- desc "bash", "Start a bash session on primary host (or specific host set by --hosts)"
72
- def bash
73
- run_locally do
74
- info "Launching bash session on #{MRSK.primary_host}"
75
- exec MRSK.app.bash(host: MRSK.primary_host)
76
- end
77
- end
68
+ when options[:reuse]
69
+ say "Get current version of running container...", :magenta unless options[:version]
70
+ using_version(options[:version] || current_running_version) do |version|
71
+ say "Launching command with version #{version} from existing container on #{MRSK.primary_host}...", :magenta
72
+ on(MRSK.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.execute_in_existing_container(cmd)) }
73
+ end
78
74
 
79
- desc "runner [EXPRESSION]", "Execute Rails runner with given expression"
80
- def runner(expression)
81
- on(MRSK.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.exec("bin/rails", "runner", "'#{expression}'")) }
75
+ else
76
+ say "Get most recent version available as an image...", :magenta unless options[:version]
77
+ using_version(options[:version] || most_recent_version_available) do |version|
78
+ say "Launching command with version #{version} from new container on #{MRSK.primary_host}...", :magenta
79
+ on(MRSK.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.execute_in_new_container(cmd)) }
80
+ end
81
+ end
82
82
  end
83
83
 
84
84
  desc "containers", "List all the app containers currently on servers"
@@ -86,6 +86,11 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
86
86
  on(MRSK.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.list_containers) }
87
87
  end
88
88
 
89
+ desc "images", "List all the app images currently on servers"
90
+ def images
91
+ on(MRSK.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.list_images) }
92
+ end
93
+
89
94
  desc "current", "Return the current running container ID"
90
95
  def current
91
96
  on(MRSK.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.current_container_id) }
@@ -109,7 +114,7 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
109
114
  end
110
115
  else
111
116
  since = options[:since]
112
- lines = options[:lines]
117
+ lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set
113
118
 
114
119
  on(MRSK.hosts) do |host|
115
120
  begin
@@ -122,16 +127,55 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
122
127
  end
123
128
 
124
129
  desc "remove", "Remove app containers and images from servers"
125
- option :only, default: "", desc: "Use 'containers' or 'images'"
126
130
  def remove
127
- case options[:only]
128
- when "containers"
129
- on(MRSK.hosts) { execute *MRSK.app.remove_containers }
130
- when "images"
131
- on(MRSK.hosts) { execute *MRSK.app.remove_images }
132
- else
133
- on(MRSK.hosts) { execute *MRSK.app.remove_containers }
134
- on(MRSK.hosts) { execute *MRSK.app.remove_images }
135
- end
131
+ remove_containers
132
+ remove_images
133
+ end
134
+
135
+ desc "remove_container [VERSION]", "Remove app container with given version from servers"
136
+ def remove_container(version)
137
+ on(MRSK.hosts) { execute *MRSK.app.remove_container(version: version) }
136
138
  end
139
+
140
+ desc "remove_containers", "Remove all app containers from servers"
141
+ def remove_containers
142
+ on(MRSK.hosts) { execute *MRSK.app.remove_containers }
143
+ end
144
+
145
+ desc "remove_images", "Remove all app images from servers"
146
+ def remove_images
147
+ on(MRSK.hosts) { execute *MRSK.app.remove_images }
148
+ end
149
+
150
+ desc "current_version", "Shows the version currently running"
151
+ def current_version
152
+ on(MRSK.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.current_running_version).strip }
153
+ end
154
+
155
+ private
156
+ def using_version(new_version)
157
+ if new_version
158
+ begin
159
+ old_version = MRSK.config.version
160
+ MRSK.config.version = new_version
161
+ yield new_version
162
+ ensure
163
+ MRSK.config.version = old_version
164
+ end
165
+ else
166
+ yield MRSK.config.version
167
+ end
168
+ end
169
+
170
+ def most_recent_version_available(host: MRSK.primary_host)
171
+ version = nil
172
+ on(host) { version = capture_with_info(*MRSK.app.most_recent_version_from_available_images).strip }
173
+ version.presence
174
+ end
175
+
176
+ def current_running_version(host: MRSK.primary_host)
177
+ version = nil
178
+ on(host) { version = capture_with_info(*MRSK.app.current_running_version).strip }
179
+ version.presence
180
+ end
137
181
  end
data/lib/mrsk/cli/base.rb CHANGED
@@ -8,6 +8,7 @@ module Mrsk::Cli
8
8
  def self.exit_on_failure?() true end
9
9
 
10
10
  class_option :verbose, type: :boolean, aliases: "-v", desc: "Detailed logging"
11
+ class_option :quiet, type: :boolean, aliases: "-q", desc: "Minimal logging"
11
12
 
12
13
  class_option :version, desc: "Run commands against a specific app version"
13
14
 
@@ -28,12 +29,20 @@ module Mrsk::Cli
28
29
  MRSK.tap do |commander|
29
30
  commander.config_file = Pathname.new(File.expand_path(options[:config_file]))
30
31
  commander.destination = options[:destination]
31
- commander.verbose = options[:verbose]
32
32
  commander.version = options[:version]
33
33
 
34
34
  commander.specific_hosts = options[:hosts]&.split(",")
35
35
  commander.specific_roles = options[:roles]&.split(",")
36
36
  commander.specific_primary! if options[:primary]
37
+
38
+ if options[:verbose]
39
+ ENV["VERBOSE"] = "1" # For backtraces via cli/start
40
+ commander.verbosity = :debug
41
+ end
42
+
43
+ if options[:quiet]
44
+ commander.verbosity = :error
45
+ end
37
46
  end
38
47
  end
39
48
 
@@ -9,18 +9,17 @@ class Mrsk::Cli::Build < Mrsk::Cli::Base
9
9
 
10
10
  desc "push", "Build locally and push app image to registry"
11
11
  def push
12
- verbose = options[:verbose]
13
12
  cli = self
14
13
 
15
14
  run_locally do
16
15
  begin
17
- MRSK.verbosity(:debug) { execute *MRSK.builder.push }
16
+ MRSK.with_verbosity(:debug) { execute *MRSK.builder.push }
18
17
  rescue SSHKit::Command::Failed => e
19
18
  if e.message =~ /(no builder)|(no such file or directory)/
20
19
  error "Missing compatible builder, so creating a new one first"
21
20
 
22
21
  if cli.create
23
- MRSK.verbosity(:debug) { execute *MRSK.builder.push }
22
+ MRSK.with_verbosity(:debug) { execute *MRSK.builder.push }
24
23
  end
25
24
  else
26
25
  raise
data/lib/mrsk/cli/main.rb CHANGED
@@ -21,12 +21,21 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
21
21
  desc "deploy", "Deploy the app to servers"
22
22
  def deploy
23
23
  print_runtime do
24
+ say "Ensure Docker is installed...", :magenta
24
25
  invoke "mrsk:cli:server:bootstrap"
26
+
27
+ say "Log into image registry...", :magenta
25
28
  invoke "mrsk:cli:registry:login"
29
+
30
+ say "Build and push app image...", :magenta
26
31
  invoke "mrsk:cli:build:deliver"
32
+
33
+ say "Ensure Traefik is running...", :magenta
27
34
  invoke "mrsk:cli:traefik:boot"
28
- invoke "mrsk:cli:app:stop"
35
+
29
36
  invoke "mrsk:cli:app:boot"
37
+
38
+ say "Prune old containers and images...", :magenta
30
39
  invoke "mrsk:cli:prune:all"
31
40
  end
32
41
  end
@@ -34,17 +43,23 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
34
43
  desc "redeploy", "Deploy new version of the app to servers (without bootstrapping servers, starting Traefik, pruning, and registry login)"
35
44
  def redeploy
36
45
  print_runtime do
46
+ say "Build and push app image...", :magenta
37
47
  invoke "mrsk:cli:build:deliver"
38
- invoke "mrsk:cli:app:stop"
48
+
39
49
  invoke "mrsk:cli:app:boot"
40
50
  end
41
51
  end
42
52
 
43
- desc "rollback [VERSION]", "Rollback the app to VERSION (that must already be on servers)"
53
+ desc "rollback [VERSION]", "Rollback the app to VERSION"
44
54
  def rollback(version)
55
+ MRSK.version = version
56
+
57
+ cli = self
58
+
59
+ cli.say "Stop current version, then start version #{version}...", :magenta
45
60
  on(MRSK.hosts) do
46
61
  execute *MRSK.app.stop, raise_on_non_zero_exit: false
47
- execute *MRSK.app.start(version: version)
62
+ execute *MRSK.app.start
48
63
  end
49
64
  end
50
65
 
@@ -50,7 +50,7 @@ class Mrsk::Cli::Traefik < Mrsk::Cli::Base
50
50
  end
51
51
  else
52
52
  since = options[:since]
53
- lines = options[:lines]
53
+ lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set
54
54
 
55
55
  on(MRSK.traefik_hosts) do |host|
56
56
  puts_by_host host, capture(*MRSK.traefik.logs(since: since, lines: lines, grep: grep)), type: "Traefik"
@@ -9,10 +9,10 @@ require "mrsk/commands/traefik"
9
9
  require "mrsk/commands/registry"
10
10
 
11
11
  class Mrsk::Commander
12
- attr_accessor :config_file, :destination, :verbose, :version
12
+ attr_accessor :config_file, :destination, :verbosity, :version
13
13
 
14
- def initialize(config_file: nil, destination: nil, verbose: false)
15
- @config_file, @destination, @verbose = config_file, destination, verbose
14
+ def initialize(config_file: nil, destination: nil, verbosity: :info)
15
+ @config_file, @destination, @verbosity = config_file, destination, verbosity
16
16
  end
17
17
 
18
18
  def config
@@ -78,7 +78,7 @@ class Mrsk::Commander
78
78
  end
79
79
 
80
80
 
81
- def verbosity(level)
81
+ def with_verbosity(level)
82
82
  old_level = SSHKit.config.output_verbosity
83
83
  SSHKit.config.output_verbosity = level
84
84
  yield
@@ -86,6 +86,13 @@ class Mrsk::Commander
86
86
  SSHKit.config.output_verbosity = old_level
87
87
  end
88
88
 
89
+ # Test-induced damage!
90
+ def reset
91
+ @config = @config_file = @destination = @version = nil
92
+ @app = @builder = @traefik = @registry = @prune = nil
93
+ @verbosity = :info
94
+ end
95
+
89
96
  private
90
97
  def cascading_version
91
98
  version.presence || ENV["VERSION"] || `git rev-parse HEAD`.strip
@@ -95,6 +102,6 @@ class Mrsk::Commander
95
102
  def configure_sshkit_with(config)
96
103
  SSHKit::Backend::Netssh.configure { |ssh| ssh.ssh_options = config.ssh_options }
97
104
  SSHKit.config.command_map[:docker] = "docker" # No need to use /usr/bin/env, just clogs up the logs
98
- SSHKit.config.output_verbosity = :debug if verbose
105
+ SSHKit.config.output_verbosity = verbosity
99
106
  end
100
107
  end
@@ -33,6 +33,7 @@ class Mrsk::Commands::Accessory < Mrsk::Commands::Base
33
33
  docker :ps, *service_filter
34
34
  end
35
35
 
36
+
36
37
  def logs(since: nil, lines: nil, grep: nil)
37
38
  pipe \
38
39
  docker(:logs, service_name, (" --since #{since}" if since), (" -n #{lines}" if lines), "-t", "2>&1"),
@@ -42,20 +43,19 @@ class Mrsk::Commands::Accessory < Mrsk::Commands::Base
42
43
  def follow_logs(grep: nil)
43
44
  run_over_ssh pipe(
44
45
  docker(:logs, service_name, "-t", "-n", "10", "-f", "2>&1"),
45
- ("grep '#{grep}'" if grep)
46
- ).join(" "), host: host
46
+ (%(grep "#{grep}") if grep)
47
+ ).join(" ")
47
48
  end
48
49
 
49
- def exec(*command, interactive: false)
50
+
51
+ def execute_in_existing_container(*command, interactive: false)
50
52
  docker :exec,
51
53
  ("-it" if interactive),
52
- *env_args,
53
- *volume_args,
54
54
  service_name,
55
55
  *command
56
56
  end
57
57
 
58
- def run_exec(*command, interactive: false)
58
+ def execute_in_new_container(*command, interactive: false)
59
59
  docker :run,
60
60
  ("-it" if interactive),
61
61
  "--rm",
@@ -65,10 +65,19 @@ class Mrsk::Commands::Accessory < Mrsk::Commands::Base
65
65
  *command
66
66
  end
67
67
 
68
- def bash(host:)
69
- exec_over_ssh "bash", host: host
68
+ def execute_in_existing_container_over_ssh(*command)
69
+ run_over_ssh execute_in_existing_container(*command, interactive: true).join(" ")
70
+ end
71
+
72
+ def execute_in_new_container_over_ssh(*command)
73
+ run_over_ssh execute_in_new_container(*command, interactive: true).join(" ")
74
+ end
75
+
76
+ def run_over_ssh(command)
77
+ super command, host: host
70
78
  end
71
79
 
80
+
72
81
  def ensure_local_file_present(local_file)
73
82
  if !local_file.is_a?(StringIO) && !Pathname.new(local_file).exist?
74
83
  raise "Missing file: #{local_file}"
@@ -96,10 +105,6 @@ class Mrsk::Commands::Accessory < Mrsk::Commands::Base
96
105
  end
97
106
 
98
107
  private
99
- def exec_over_ssh(*command, host:)
100
- run_over_ssh run_exec(*command, interactive: true).join(" "), host: host
101
- end
102
-
103
108
  def service_filter
104
109
  [ "--filter", "label=service=#{service_name}" ]
105
110
  end
@@ -7,7 +7,7 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
7
7
  docker :run,
8
8
  "-d",
9
9
  "--restart unless-stopped",
10
- "--name", config.service_with_version,
10
+ "--name", service_with_version,
11
11
  *rails_master_key_arg,
12
12
  *role.env_args,
13
13
  *config.volume_args,
@@ -16,40 +16,43 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
16
16
  role.cmd
17
17
  end
18
18
 
19
- def start(version: config.version)
20
- docker :start, "#{config.service}-#{version}"
21
- end
22
-
23
- def current_container_id
24
- docker :ps, "-q", *service_filter
19
+ def start
20
+ docker :start, service_with_version
25
21
  end
26
22
 
27
23
  def stop
28
- pipe current_container_id, "xargs docker stop"
24
+ pipe current_container_id, xargs(docker(:stop))
29
25
  end
30
26
 
31
27
  def info
32
28
  docker :ps, *service_filter
33
29
  end
34
30
 
31
+
35
32
  def logs(since: nil, lines: nil, grep: nil)
36
33
  pipe \
37
34
  current_container_id,
38
- "xargs docker logs#{" --since #{since}" if since}#{" -n #{lines}" if lines} -t 2>&1",
35
+ "xargs docker logs#{" --since #{since}" if since}#{" -n #{lines}" if lines} 2>&1",
39
36
  ("grep '#{grep}'" if grep)
40
37
  end
41
38
 
42
- def exec(*command, interactive: false)
39
+ def follow_logs(host:, grep: nil)
40
+ run_over_ssh pipe(
41
+ current_container_id,
42
+ "xargs docker logs -t -n 10 -f 2>&1",
43
+ (%(grep "#{grep}") if grep)
44
+ ).join(" "), host: host
45
+ end
46
+
47
+
48
+ def execute_in_existing_container(*command, interactive: false)
43
49
  docker :exec,
44
50
  ("-it" if interactive),
45
- *rails_master_key_arg,
46
- *config.env_args,
47
- *config.volume_args,
48
51
  config.service_with_version,
49
52
  *command
50
53
  end
51
54
 
52
- def run_exec(*command, interactive: false)
55
+ def execute_in_new_container(*command, interactive: false)
53
56
  docker :run,
54
57
  ("-it" if interactive),
55
58
  "--rm",
@@ -60,39 +63,70 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
60
63
  *command
61
64
  end
62
65
 
63
- def exec_over_ssh(*command, host:)
64
- run_over_ssh run_exec(*command, interactive: true).join(" "), host: host
66
+ def execute_in_existing_container_over_ssh(*command, host:)
67
+ run_over_ssh execute_in_existing_container(*command, interactive: true).join(" "), host: host
65
68
  end
66
69
 
67
- def follow_logs(host:, grep: nil)
68
- run_over_ssh pipe(
69
- current_container_id,
70
- "xargs docker logs -t -n 10 -f 2>&1",
71
- ("grep '#{grep}'" if grep)
72
- ).join(" "), host: host
70
+ def execute_in_new_container_over_ssh(*command, host:)
71
+ run_over_ssh execute_in_new_container(*command, interactive: true).join(" "), host: host
73
72
  end
74
73
 
75
- def console(host:)
76
- exec_over_ssh "bin/rails", "c", host: host
74
+
75
+ def current_container_id
76
+ docker :ps, "-q", *service_filter
77
77
  end
78
78
 
79
- def bash(host:)
80
- exec_over_ssh "bash", host: host
79
+ def container_id_for(container_name:)
80
+ docker :container, :ls, "-a", "-f", "name=#{container_name}", "-q"
81
81
  end
82
82
 
83
+ def current_running_version
84
+ # FIXME: Find more graceful way to extract the version from "app-version" than using sed and tail!
85
+ pipe \
86
+ docker(:ps, "--filter", "label=service=#{config.service}", "--format", '"{{.Names}}"'),
87
+ %(sed 's/-/\\n/g'),
88
+ "tail -n 1"
89
+ end
90
+
91
+ def most_recent_version_from_available_images
92
+ pipe \
93
+ docker(:image, :ls, "--format", '"{{.Tag}}"', config.repository),
94
+ "head -n 1"
95
+ end
96
+
97
+
83
98
  def list_containers
84
99
  docker :container, :ls, "-a", *service_filter
85
100
  end
86
101
 
102
+ def remove_container(version:)
103
+ pipe \
104
+ container_id_for(container_name: service_with_version(version)),
105
+ xargs(docker(:container, :rm))
106
+ end
107
+
87
108
  def remove_containers
88
109
  docker :container, :prune, "-f", *service_filter
89
110
  end
90
111
 
112
+ def list_images
113
+ docker :image, :ls, config.repository
114
+ end
115
+
91
116
  def remove_images
92
117
  docker :image, :prune, "-a", "-f", *service_filter
93
118
  end
94
119
 
120
+
95
121
  private
122
+ def service_with_version(version = nil)
123
+ if version
124
+ "#{config.service}-#{version}"
125
+ else
126
+ config.service_with_version
127
+ end
128
+ end
129
+
96
130
  def service_filter
97
131
  [ "--filter", "label=service=#{config.service}" ]
98
132
  end
@@ -8,6 +8,10 @@ module Mrsk::Commands
8
8
  @config = config
9
9
  end
10
10
 
11
+ def run_over_ssh(command, host:)
12
+ "ssh -t #{config.ssh_user}@#{host} '#{command}'"
13
+ end
14
+
11
15
  private
12
16
  def combine(*commands, by: "&&")
13
17
  commands
@@ -24,12 +28,12 @@ module Mrsk::Commands
24
28
  combine *commands, by: "|"
25
29
  end
26
30
 
27
- def docker(*args)
28
- args.compact.unshift :docker
31
+ def xargs(command)
32
+ [ :xargs, command ].flatten
29
33
  end
30
34
 
31
- def run_over_ssh(command, host:)
32
- "ssh -t #{config.ssh_user}@#{host} '#{command}'"
35
+ def docker(*args)
36
+ args.compact.unshift :docker
33
37
  end
34
38
  end
35
39
  end
@@ -9,7 +9,8 @@ class Mrsk::Commands::Traefik < Mrsk::Commands::Base
9
9
  "-v /var/run/docker.sock:/var/run/docker.sock",
10
10
  "traefik",
11
11
  "--providers.docker",
12
- "--log.level=DEBUG"
12
+ "--log.level=DEBUG",
13
+ *cmd_args
13
14
  end
14
15
 
15
16
  def start
@@ -33,7 +34,7 @@ class Mrsk::Commands::Traefik < Mrsk::Commands::Base
33
34
  def follow_logs(host:, grep: nil)
34
35
  run_over_ssh pipe(
35
36
  docker(:logs, "traefik", "-t", "-n", "10", "-f", "2>&1"),
36
- ("grep '#{grep}'" if grep)
37
+ (%(grep "#{grep}") if grep)
37
38
  ).join(" "), host: host
38
39
  end
39
40
 
@@ -44,4 +45,9 @@ class Mrsk::Commands::Traefik < Mrsk::Commands::Base
44
45
  def remove_image
45
46
  docker :image, :prune, "-a", "-f", "--filter", "label=org.opencontainers.image.title=Traefik"
46
47
  end
48
+
49
+ private
50
+ def cmd_args
51
+ (config.raw_config.dig(:traefik, "args") || { }).collect { |(key, value)| [ "--#{key}", value ] }.flatten
52
+ end
47
53
  end
@@ -1,4 +1,4 @@
1
- class Mrsk::Configuration::Assessory
1
+ class Mrsk::Configuration::Accessory
2
2
  delegate :argumentize, :argumentize_env_with_secrets, to: Mrsk::Utils
3
3
 
4
4
  attr_accessor :name, :specifics
@@ -74,12 +74,19 @@ class Mrsk::Configuration::Assessory
74
74
 
75
75
  def expand_local_file(local_file)
76
76
  if local_file.end_with?("erb")
77
- read_dynamic_file(local_file)
77
+ with_clear_env_loaded { read_dynamic_file(local_file) }
78
78
  else
79
79
  Pathname.new(File.expand_path(local_file)).to_s
80
80
  end
81
81
  end
82
82
 
83
+ def with_clear_env_loaded
84
+ (env["clear"] || env).each { |k, v| ENV[k] = v }
85
+ yield
86
+ ensure
87
+ (env["clear"] || env).each { |k, v| ENV.delete(k) }
88
+ end
89
+
83
90
  def read_dynamic_file(local_file)
84
91
  StringIO.new(ERB.new(IO.read(local_file)).result)
85
92
  end
@@ -9,6 +9,7 @@ class Mrsk::Configuration
9
9
  delegate :service, :image, :servers, :env, :labels, :registry, :builder, to: :raw_config, allow_nil: true
10
10
  delegate :argumentize, :argumentize_env_with_secrets, to: Mrsk::Utils
11
11
 
12
+ attr_accessor :version
12
13
  attr_accessor :raw_config
13
14
 
14
15
  class << self
@@ -39,7 +40,7 @@ class Mrsk::Configuration
39
40
  def initialize(raw_config, version: "missing", validate: true)
40
41
  @raw_config = ActiveSupport::InheritableOptions.new(raw_config)
41
42
  @version = version
42
- ensure_required_keys_present if validate
43
+ valid? if validate
43
44
  end
44
45
 
45
46
 
@@ -52,7 +53,7 @@ class Mrsk::Configuration
52
53
  end
53
54
 
54
55
  def accessories
55
- @accessories ||= raw_config.accessories&.keys&.collect { |name| Mrsk::Configuration::Assessory.new(name, config: self) } || []
56
+ @accessories ||= raw_config.accessories&.keys&.collect { |name| Mrsk::Configuration::Accessory.new(name, config: self) } || []
56
57
  end
57
58
 
58
59
  def accessory(name)
@@ -73,10 +74,6 @@ class Mrsk::Configuration
73
74
  end
74
75
 
75
76
 
76
- def version
77
- @version
78
- end
79
-
80
77
  def repository
81
78
  [ raw_config.registry["server"], image ].compact.join("/")
82
79
  end
@@ -120,6 +117,12 @@ class Mrsk::Configuration
120
117
  end
121
118
  end
122
119
 
120
+
121
+ def valid?
122
+ ensure_required_keys_present && ensure_env_available
123
+ end
124
+
125
+
123
126
  def to_h
124
127
  {
125
128
  roles: role_names,
@@ -139,6 +142,7 @@ class Mrsk::Configuration
139
142
 
140
143
 
141
144
  private
145
+ # Will raise ArgumentError if any required config keys are missing
142
146
  def ensure_required_keys_present
143
147
  %i[ service image registry servers ].each do |key|
144
148
  raise ArgumentError, "Missing required configuration for #{key}" unless raw_config[key].present?
@@ -151,6 +155,16 @@ class Mrsk::Configuration
151
155
  if raw_config.registry["password"].blank?
152
156
  raise ArgumentError, "You must specify a password for the registry in config/deploy.yml (or set the ENV variable if that's used)"
153
157
  end
158
+
159
+ true
160
+ end
161
+
162
+ # Will raise KeyError if any secret ENVs are missing
163
+ def ensure_env_available
164
+ env_args
165
+ roles.each(&:env_args)
166
+
167
+ true
154
168
  end
155
169
 
156
170
  def role_names
data/lib/mrsk/utils.rb CHANGED
@@ -18,7 +18,7 @@ module Mrsk::Utils
18
18
  if (secrets = env["secret"]).present?
19
19
  argumentize("-e", secrets.to_h { |key| [ key, ENV.fetch(key) ] }, redacted: true) + argumentize("-e", env["clear"])
20
20
  else
21
- argumentize "-e", env
21
+ argumentize "-e", env.fetch("clear", env)
22
22
  end
23
23
  end
24
24
 
data/lib/mrsk/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Mrsk
2
- VERSION = "0.3.1"
2
+ VERSION = "0.5.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.3.1
4
+ version: 0.5.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-01-27 00:00:00.000000000 Z
11
+ date: 2023-02-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '1.2'
55
+ - !ruby/object:Gem::Dependency
56
+ name: dotenv
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '2.8'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '2.8'
55
69
  description:
56
70
  email: dhh@hey.com
57
71
  executables:
@@ -113,7 +127,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
113
127
  - !ruby/object:Gem::Version
114
128
  version: '0'
115
129
  requirements: []
116
- rubygems_version: 3.4.5
130
+ rubygems_version: 3.4.6
117
131
  signing_key:
118
132
  specification_version: 4
119
133
  summary: Deploy Rails apps in containers to servers running Docker with zero downtime.