mrsk 0.1.0 → 0.2.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.
@@ -2,22 +2,21 @@ require "mrsk/commands/base"
2
2
 
3
3
  class Mrsk::Commands::Builder < Mrsk::Commands::Base
4
4
  delegate :create, :remove, :push, :pull, :info, to: :target
5
- delegate :native?, :multiarch?, :remote?, to: :name
6
5
 
7
6
  def name
8
- target.class.to_s.demodulize.downcase.inquiry
7
+ target.class.to_s.remove("Mrsk::Commands::Builder::").underscore
9
8
  end
10
9
 
11
10
  def target
12
11
  case
13
- when config.builder.nil?
14
- multiarch
15
- when config.builder["multiarch"] == false
12
+ when config.builder && config.builder["multiarch"] == false
16
13
  native
17
- when config.builder["local"] && config.builder["local"]
14
+ when config.builder && config.builder["local"] && config.builder["remote"]
18
15
  multiarch_remote
16
+ when config.builder && config.builder["remote"]
17
+ native_remote
19
18
  else
20
- raise ArgumentError, "Builder configuration incorrect: #{config.builder.inspect}"
19
+ multiarch
21
20
  end
22
21
  end
23
22
 
@@ -25,6 +24,10 @@ class Mrsk::Commands::Builder < Mrsk::Commands::Base
25
24
  @native ||= Mrsk::Commands::Builder::Native.new(config)
26
25
  end
27
26
 
27
+ def native_remote
28
+ @native ||= Mrsk::Commands::Builder::Native::Remote.new(config)
29
+ end
30
+
28
31
  def multiarch
29
32
  @multiarch ||= Mrsk::Commands::Builder::Multiarch.new(config)
30
33
  end
@@ -35,5 +38,6 @@ class Mrsk::Commands::Builder < Mrsk::Commands::Base
35
38
  end
36
39
 
37
40
  require "mrsk/commands/builder/native"
41
+ require "mrsk/commands/builder/native/remote"
38
42
  require "mrsk/commands/builder/multiarch"
39
43
  require "mrsk/commands/builder/multiarch/remote"
@@ -8,7 +8,8 @@ class Mrsk::Commands::Traefik < Mrsk::Commands::Base
8
8
  "-p 80:80",
9
9
  "-v /var/run/docker.sock:/var/run/docker.sock",
10
10
  "traefik",
11
- "--providers.docker"
11
+ "--providers.docker",
12
+ "--log.level=DEBUG"
12
13
  end
13
14
 
14
15
  def start
@@ -23,8 +24,17 @@ class Mrsk::Commands::Traefik < Mrsk::Commands::Base
23
24
  docker :ps, "--filter", "name=traefik"
24
25
  end
25
26
 
26
- def logs
27
- docker :logs, "traefik", "-n", "100", "-t"
27
+ def logs(since: nil, lines: nil, grep: nil)
28
+ pipe \
29
+ docker(:logs, "traefik", (" --since #{since}" if since), (" -n #{lines}" if lines), "-t", "2>&1"),
30
+ ("grep '#{grep}'" if grep)
31
+ end
32
+
33
+ def follow_logs(host:, grep: nil)
34
+ run_over_ssh pipe(
35
+ docker(:logs, "traefik", "-t", "-n", "10", "-f", "2>&1"),
36
+ ("grep '#{grep}'" if grep)
37
+ ).join(" "), host: host
28
38
  end
29
39
 
30
40
  def remove_container
@@ -0,0 +1,60 @@
1
+ class Mrsk::Configuration::Assessory
2
+ delegate :argumentize, :argumentize_env_with_secrets, to: Mrsk::Utils
3
+
4
+ attr_accessor :name, :specifics
5
+
6
+ def initialize(name, config:)
7
+ @name, @config, @specifics = name.inquiry, config, config.raw_config["accessories"][name]
8
+ end
9
+
10
+ def service_name
11
+ "#{config.service}-#{name}"
12
+ end
13
+
14
+ def image
15
+ specifics["image"]
16
+ end
17
+
18
+ def host
19
+ specifics["host"] || raise(ArgumentError, "Missing host for accessory")
20
+ end
21
+
22
+ def port
23
+ if specifics["port"].to_s.include?(":")
24
+ specifics["port"]
25
+ else
26
+ "#{specifics["port"]}:#{specifics["port"]}"
27
+ end
28
+ end
29
+
30
+ def labels
31
+ default_labels.merge(specifics["labels"] || {})
32
+ end
33
+
34
+ def label_args
35
+ argumentize "--label", labels
36
+ end
37
+
38
+ def env
39
+ specifics["env"] || {}
40
+ end
41
+
42
+ def env_args
43
+ argumentize_env_with_secrets env
44
+ end
45
+
46
+ def volumes
47
+ specifics["volumes"] || []
48
+ end
49
+
50
+ def volume_args
51
+ argumentize "--volume", volumes
52
+ end
53
+
54
+ private
55
+ attr_accessor :config
56
+
57
+ def default_labels
58
+ { "service" => service_name }
59
+ end
60
+ end
@@ -1,5 +1,5 @@
1
1
  class Mrsk::Configuration::Role
2
- delegate :argumentize, to: Mrsk::Configuration
2
+ delegate :argumentize, :argumentize_env_with_secrets, to: Mrsk::Utils
3
3
 
4
4
  attr_accessor :name
5
5
 
@@ -12,21 +12,33 @@ class Mrsk::Configuration::Role
12
12
  end
13
13
 
14
14
  def labels
15
- if name.web?
16
- default_labels.merge(traefik_labels).merge(custom_labels)
17
- else
18
- default_labels.merge(custom_labels)
19
- end
15
+ default_labels.merge(traefik_labels).merge(custom_labels)
20
16
  end
21
17
 
22
18
  def label_args
23
19
  argumentize "--label", labels
24
20
  end
25
21
 
22
+ def env
23
+ if config.env && config.env["secret"]
24
+ merged_env_with_secrets
25
+ else
26
+ merged_env
27
+ end
28
+ end
29
+
30
+ def env_args
31
+ argumentize_env_with_secrets env
32
+ end
33
+
26
34
  def cmd
27
35
  specializations["cmd"]
28
36
  end
29
37
 
38
+ def running_traefik?
39
+ name.web? || specializations["traefik"]
40
+ end
41
+
30
42
  private
31
43
  attr_accessor :config
32
44
 
@@ -44,13 +56,17 @@ class Mrsk::Configuration::Role
44
56
  end
45
57
 
46
58
  def traefik_labels
47
- {
48
- "traefik.http.routers.#{config.service}.rule" => "'PathPrefix(`/`)'",
49
- "traefik.http.services.#{config.service}.loadbalancer.healthcheck.path" => "/up",
50
- "traefik.http.services.#{config.service}.loadbalancer.healthcheck.interval" => "1s",
51
- "traefik.http.middlewares.#{config.service}.retry.attempts" => "3",
52
- "traefik.http.middlewares.#{config.service}.retry.initialinterval" => "500ms"
53
- }
59
+ if running_traefik?
60
+ {
61
+ "traefik.http.routers.#{config.service}.rule" => "'PathPrefix(`/`)'",
62
+ "traefik.http.services.#{config.service}.loadbalancer.healthcheck.path" => "/up",
63
+ "traefik.http.services.#{config.service}.loadbalancer.healthcheck.interval" => "1s",
64
+ "traefik.http.middlewares.#{config.service}.retry.attempts" => "3",
65
+ "traefik.http.middlewares.#{config.service}.retry.initialinterval" => "500ms"
66
+ }
67
+ else
68
+ {}
69
+ end
54
70
  end
55
71
 
56
72
  def custom_labels
@@ -64,7 +80,23 @@ class Mrsk::Configuration::Role
64
80
  if config.servers.is_a?(Array) || config.servers[name].is_a?(Array)
65
81
  { }
66
82
  else
67
- config.servers[name].without("hosts")
83
+ config.servers[name].except("hosts")
84
+ end
85
+ end
86
+
87
+ def specialized_env
88
+ specializations["env"] || {}
89
+ end
90
+
91
+ def merged_env
92
+ config.env&.merge(specialized_env) || {}
93
+ end
94
+
95
+ # Secrets are stored in an array, which won't merge by default, so have to do it by hand.
96
+ def merged_env_with_secrets
97
+ merged_env.tap do |new_env|
98
+ new_env["secret"] = Array(config.env["secret"]) + Array(specialized_env["secret"])
99
+ new_env["clear"] = (Array(config.env["clear"] || config.env) + Array(specialized_env["clear"] || specialized_env)).uniq
68
100
  end
69
101
  end
70
102
  end
@@ -3,26 +3,42 @@ require "active_support/core_ext/string/inquiry"
3
3
  require "active_support/core_ext/module/delegation"
4
4
  require "pathname"
5
5
  require "erb"
6
+ require "mrsk/utils"
6
7
 
7
8
  class Mrsk::Configuration
8
- delegate :service, :image, :servers, :env, :labels, :registry, :builder, to: :config, allow_nil: true
9
+ delegate :service, :image, :servers, :env, :labels, :registry, :builder, to: :raw_config, allow_nil: true
10
+ delegate :argumentize, :argumentize_env_with_secrets, to: Mrsk::Utils
11
+
12
+ attr_accessor :raw_config
9
13
 
10
14
  class << self
11
- def load_file(file)
12
- if file.exist?
13
- new YAML.load(ERB.new(IO.read(file)).result).symbolize_keys
14
- else
15
- raise "Configuration file not found in #{file}"
16
- end
15
+ def create_from(base_config_file, destination: nil, version: "missing")
16
+ new(load_config_file(base_config_file).tap do |config|
17
+ if destination
18
+ config.merge! \
19
+ load_config_file destination_config_file(base_config_file, destination)
20
+ end
21
+ end, version: version)
17
22
  end
18
23
 
19
- def argumentize(argument, attributes)
20
- attributes.flat_map { |k, v| [ argument, "#{k}=#{v}" ] }
21
- end
24
+ private
25
+ def load_config_file(file)
26
+ if file.exist?
27
+ YAML.load(ERB.new(IO.read(file)).result).symbolize_keys
28
+ else
29
+ raise "Configuration file not found in #{file}"
30
+ end
31
+ end
32
+
33
+ def destination_config_file(base_config_file, destination)
34
+ dir, basename = base_config_file.split
35
+ dir.join basename.to_s.remove(".yml") + ".#{destination}.yml"
36
+ end
22
37
  end
23
38
 
24
- def initialize(config, validate: true)
25
- @config = ActiveSupport::InheritableOptions.new(config)
39
+ def initialize(raw_config, version: "missing", validate: true)
40
+ @raw_config = ActiveSupport::InheritableOptions.new(raw_config)
41
+ @version = version
26
42
  ensure_required_keys_present if validate
27
43
  end
28
44
 
@@ -35,36 +51,34 @@ class Mrsk::Configuration
35
51
  roles.detect { |r| r.name == name.to_s }
36
52
  end
37
53
 
38
- def hosts
39
- hosts =
40
- case
41
- when ENV["HOSTS"]
42
- ENV["HOSTS"].split(",")
43
- when ENV["ROLES"]
44
- role_names = ENV["ROLES"].split(",")
45
- roles.select { |r| role_names.include?(r.name) }.flat_map(&:hosts)
46
- else
47
- roles.flat_map(&:hosts)
48
- end
54
+ def accessories
55
+ @accessories ||= raw_config.accessories.keys.collect { |name| Mrsk::Configuration::Assessory.new(name, config: self) }
56
+ end
49
57
 
50
- if hosts.any?
51
- hosts
52
- else
53
- raise ArgumentError, "No hosts found"
54
- end
58
+ def accessory(name)
59
+ accessories.detect { |a| a.name == name.to_s }
60
+ end
61
+
62
+
63
+ def all_hosts
64
+ roles.flat_map(&:hosts)
55
65
  end
56
66
 
57
- def primary_host
67
+ def primary_web_host
58
68
  role(:web).hosts.first
59
69
  end
60
70
 
71
+ def traefik_hosts
72
+ roles.select(&:running_traefik?).flat_map(&:hosts)
73
+ end
74
+
61
75
 
62
76
  def version
63
- @version ||= ENV["VERSION"] || `git rev-parse HEAD`.strip
77
+ @version
64
78
  end
65
79
 
66
80
  def repository
67
- [ config.registry["server"], image ].compact.join("/")
81
+ [ raw_config.registry["server"], image ].compact.join("/")
68
82
  end
69
83
 
70
84
  def absolute_image
@@ -77,15 +91,23 @@ class Mrsk::Configuration
77
91
 
78
92
 
79
93
  def env_args
80
- if config.env.present?
81
- self.class.argumentize "-e", config.env
94
+ if raw_config.env.present?
95
+ argumentize_env_with_secrets(raw_config.env)
96
+ else
97
+ []
98
+ end
99
+ end
100
+
101
+ def volume_args
102
+ if raw_config.volumes.present?
103
+ argumentize "--volume", raw_config.volumes
82
104
  else
83
105
  []
84
106
  end
85
107
  end
86
108
 
87
109
  def ssh_user
88
- config.ssh_user || "root"
110
+ raw_config.ssh_user || "root"
89
111
  end
90
112
 
91
113
  def ssh_options
@@ -96,23 +118,42 @@ class Mrsk::Configuration
96
118
  ENV["RAILS_MASTER_KEY"] || File.read(Pathname.new(File.expand_path("config/master.key")))
97
119
  end
98
120
 
121
+ def to_h
122
+ {
123
+ roles: role_names,
124
+ hosts: all_hosts,
125
+ primary_host: primary_web_host,
126
+ version: version,
127
+ repository: repository,
128
+ absolute_image: absolute_image,
129
+ service_with_version: service_with_version,
130
+ env_args: env_args,
131
+ volume_args: volume_args,
132
+ ssh_options: ssh_options,
133
+ builder: raw_config.builder
134
+ }.compact
135
+ end
99
136
 
100
- private
101
- attr_accessor :config
102
137
 
138
+ private
103
139
  def ensure_required_keys_present
104
- %i[ service image registry ].each do |key|
105
- raise ArgumentError, "Missing required configuration for #{key}" unless config[key].present?
140
+ %i[ service image registry servers ].each do |key|
141
+ raise ArgumentError, "Missing required configuration for #{key}" unless raw_config[key].present?
106
142
  end
107
143
 
108
- %w[ username password ].each do |key|
109
- raise ArgumentError, "Missing required configuration for registry/#{key}" unless config.registry[key].present?
110
- end
144
+ if raw_config.registry["username"].blank?
145
+ raise ArgumentError, "You must specify a username for the registry in config/deploy.yml"
146
+ end
147
+
148
+ if raw_config.registry["password"].blank?
149
+ raise ArgumentError, "You must specify a password for the registry in config/deploy.yml (or set the ENV variable if that's used)"
150
+ end
111
151
  end
112
152
 
113
153
  def role_names
114
- config.servers.is_a?(Array) ? [ "web" ] : config.servers.keys.sort
154
+ raw_config.servers.is_a?(Array) ? [ "web" ] : raw_config.servers.keys.sort
115
155
  end
116
156
  end
117
157
 
118
158
  require "mrsk/configuration/role"
159
+ require "mrsk/configuration/accessory"
@@ -0,0 +1,12 @@
1
+ require "sshkit"
2
+ require "sshkit/dsl"
3
+
4
+ class SSHKit::Backend::Abstract
5
+ def capture_with_info(*args)
6
+ capture(*args, verbosity: Logger::INFO)
7
+ end
8
+
9
+ def puts_by_host(host, output, type: "App")
10
+ puts "#{type} Host: #{host}\n#{output}\n\n"
11
+ end
12
+ end
data/lib/mrsk/utils.rb ADDED
@@ -0,0 +1,29 @@
1
+ module Mrsk::Utils
2
+ extend self
3
+
4
+ # Return a list of shell arguments using the same named argument against the passed attributes (hash or array).
5
+ def argumentize(argument, attributes, redacted: false)
6
+ Array(attributes).flat_map do |k, v|
7
+ if v.present?
8
+ [ argument, redacted ? redact("#{k}=#{v}") : "#{k}=#{v}" ]
9
+ else
10
+ [ argument, k ]
11
+ end
12
+ end
13
+ end
14
+
15
+ # Return a list of shell arguments using the same named argument against the passed attributes,
16
+ # but redacts and expands secrets.
17
+ def argumentize_env_with_secrets(env)
18
+ if (secrets = env["secret"]).present?
19
+ argumentize("-e", secrets.to_h { |key| [ key, ENV.fetch(key) ] }, redacted: true) + argumentize("-e", env["clear"])
20
+ else
21
+ argumentize "-e", env
22
+ end
23
+ end
24
+
25
+ # Copied from SSHKit::Backend::Abstract#redact to be available inside Commands classes
26
+ def redact(arg) # Used in execute_command to hide redact() args a user passes in
27
+ arg.to_s.extend(SSHKit::Redaction) # to_s due to our inability to extend Integer, etc
28
+ end
29
+ end
data/lib/mrsk/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Mrsk
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.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.1.0
4
+ version: 0.2.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-14 00:00:00.000000000 Z
11
+ date: 2023-01-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -64,6 +64,7 @@ files:
64
64
  - bin/mrsk
65
65
  - lib/mrsk.rb
66
66
  - lib/mrsk/cli.rb
67
+ - lib/mrsk/cli/accessory.rb
67
68
  - lib/mrsk/cli/app.rb
68
69
  - lib/mrsk/cli/base.rb
69
70
  - lib/mrsk/cli/build.rb
@@ -75,17 +76,23 @@ files:
75
76
  - lib/mrsk/cli/traefik.rb
76
77
  - lib/mrsk/commander.rb
77
78
  - lib/mrsk/commands.rb
79
+ - lib/mrsk/commands/accessory.rb
78
80
  - lib/mrsk/commands/app.rb
79
81
  - lib/mrsk/commands/base.rb
80
82
  - lib/mrsk/commands/builder.rb
83
+ - lib/mrsk/commands/builder/base.rb
81
84
  - lib/mrsk/commands/builder/multiarch.rb
82
85
  - lib/mrsk/commands/builder/multiarch/remote.rb
83
86
  - lib/mrsk/commands/builder/native.rb
87
+ - lib/mrsk/commands/builder/native/remote.rb
84
88
  - lib/mrsk/commands/prune.rb
85
89
  - lib/mrsk/commands/registry.rb
86
90
  - lib/mrsk/commands/traefik.rb
87
91
  - lib/mrsk/configuration.rb
92
+ - lib/mrsk/configuration/accessory.rb
88
93
  - lib/mrsk/configuration/role.rb
94
+ - lib/mrsk/sshkit_with_ext.rb
95
+ - lib/mrsk/utils.rb
89
96
  - lib/mrsk/version.rb
90
97
  homepage: https://github.com/rails/mrsk
91
98
  licenses: