mrsk 0.9.0 → 0.10.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.
@@ -1,13 +1,21 @@
1
1
  class Mrsk::Commands::App < Mrsk::Commands::Base
2
- def run(role: :web)
3
- role = config.role(role)
2
+ attr_reader :role
3
+
4
+ def initialize(config, role: nil)
5
+ super(config)
6
+ @role = role
7
+ end
8
+
9
+ def run
10
+ role = config.role(self.role)
4
11
 
5
12
  docker :run,
6
13
  "--detach",
7
14
  "--restart unless-stopped",
8
- "--log-opt", "max-size=#{MAX_LOG_SIZE}",
9
- "--name", service_with_version,
15
+ "--name", container_name,
16
+ "-e", "MRSK_CONTAINER_NAME=\"#{container_name}\"",
10
17
  *role.env_args,
18
+ *config.logging_args,
11
19
  *config.volume_args,
12
20
  *role.label_args,
13
21
  *role.option_args,
@@ -16,17 +24,17 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
16
24
  end
17
25
 
18
26
  def start
19
- docker :start, service_with_version
27
+ docker :start, container_name
20
28
  end
21
29
 
22
30
  def stop(version: nil)
23
31
  pipe \
24
32
  version ? container_id_for_version(version) : current_container_id,
25
- xargs(docker(:stop))
33
+ xargs(config.stop_wait_time ? docker(:stop, "-t", config.stop_wait_time) : docker(:stop))
26
34
  end
27
35
 
28
36
  def info
29
- docker :ps, *service_filter
37
+ docker :ps, *filter_args
30
38
  end
31
39
 
32
40
 
@@ -51,7 +59,7 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
51
59
  def execute_in_existing_container(*command, interactive: false)
52
60
  docker :exec,
53
61
  ("-it" if interactive),
54
- config.service_with_version,
62
+ container_name,
55
63
  *command
56
64
  end
57
65
 
@@ -75,32 +83,23 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
75
83
 
76
84
 
77
85
  def current_container_id
78
- docker :ps, "--quiet", *service_filter
86
+ docker :ps, "--quiet", *filter_args
87
+ end
88
+
89
+ def container_id_for_version(version)
90
+ container_id_for(container_name: container_name(version))
79
91
  end
80
92
 
81
93
  def current_running_version
82
94
  # FIXME: Find more graceful way to extract the version from "app-version" than using sed and tail!
83
95
  pipe \
84
- docker(:ps, "--filter", "label=service=#{config.service}", "--format", '"{{.Names}}"'),
96
+ docker(:ps, *filter_args, "--format", '"{{.Names}}"'),
85
97
  %(sed 's/-/\\n/g'),
86
98
  "tail -n 1"
87
99
  end
88
100
 
89
- def most_recent_version_from_available_images
90
- pipe \
91
- docker(:image, :ls, "--format", '"{{.Tag}}"', config.repository),
92
- "head -n 1"
93
- end
94
-
95
- def all_versions_from_available_containers
96
- pipe \
97
- docker(:image, :ls, "--format", '"{{.Tag}}"', config.repository),
98
- "head -n 1"
99
- end
100
-
101
-
102
101
  def list_containers
103
- docker :container, :ls, "--all", *service_filter
102
+ docker :container, :ls, "--all", *filter_args
104
103
  end
105
104
 
106
105
  def list_container_names
@@ -109,12 +108,16 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
109
108
 
110
109
  def remove_container(version:)
111
110
  pipe \
112
- container_id_for(container_name: service_with_version(version)),
111
+ container_id_for(container_name: container_name(version)),
113
112
  xargs(docker(:container, :rm))
114
113
  end
115
114
 
115
+ def rename_container(version:, new_version:)
116
+ docker :rename, container_name(version), container_name(new_version)
117
+ end
118
+
116
119
  def remove_containers
117
- docker :container, :prune, "--force", *service_filter
120
+ docker :container, :prune, "--force", *filter_args
118
121
  end
119
122
 
120
123
  def list_images
@@ -122,24 +125,23 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
122
125
  end
123
126
 
124
127
  def remove_images
125
- docker :image, :prune, "--all", "--force", *service_filter
128
+ docker :image, :prune, "--all", "--force", *filter_args
126
129
  end
127
130
 
128
131
 
129
132
  private
130
- def service_with_version(version = nil)
131
- if version
132
- "#{config.service}-#{version}"
133
- else
134
- config.service_with_version
135
- end
133
+ def container_name(version = nil)
134
+ [ config.service, role, config.destination, version || config.version ].compact.join("-")
136
135
  end
137
136
 
138
- def container_id_for_version(version)
139
- container_id_for(container_name: service_with_version(version))
137
+ def filter_args
138
+ argumentize "--filter", filters
140
139
  end
141
140
 
142
- def service_filter
143
- [ "--filter", "label=service=#{config.service}" ]
141
+ def filters
142
+ [ "label=service=#{config.service}" ].tap do |filters|
143
+ filters << "label=destination=#{config.destination}" if config.destination
144
+ filters << "label=role=#{role}" if role
145
+ end
144
146
  end
145
147
  end
@@ -1,6 +1,13 @@
1
1
  require "active_support/core_ext/time/conversions"
2
2
 
3
3
  class Mrsk::Commands::Auditor < Mrsk::Commands::Base
4
+ attr_reader :role
5
+
6
+ def initialize(config, role: nil)
7
+ super(config)
8
+ @role = role
9
+ end
10
+
4
11
  # Runs remotely
5
12
  def record(line)
6
13
  append \
@@ -21,22 +28,30 @@ class Mrsk::Commands::Auditor < Mrsk::Commands::Base
21
28
 
22
29
  private
23
30
  def audit_log_file
24
- "mrsk-#{config.service}-audit.log"
31
+ [ "mrsk", config.service, config.destination, "audit.log" ].compact.join("-")
25
32
  end
26
33
 
27
34
  def tagged_record_line(line)
28
- "'#{recorded_at_tag} #{performer_tag} #{line}'"
35
+ tagged_line recorded_at_tag, performer_tag, role_tag, line
29
36
  end
30
37
 
31
38
  def tagged_broadcast_line(line)
32
- "'#{performer_tag} #{line}'"
39
+ tagged_line performer_tag, role_tag, line
33
40
  end
34
41
 
35
- def performer_tag
36
- "[#{`whoami`.strip}]"
42
+ def tagged_line(*tags_and_line)
43
+ "'#{tags_and_line.compact.join(" ")}'"
37
44
  end
38
45
 
39
46
  def recorded_at_tag
40
47
  "[#{Time.now.to_fs(:db)}]"
41
48
  end
49
+
50
+ def performer_tag
51
+ "[#{`whoami`.strip}]"
52
+ end
53
+
54
+ def role_tag
55
+ "[#{role}]" if role
56
+ end
42
57
  end
@@ -1,8 +1,6 @@
1
1
  module Mrsk::Commands
2
2
  class Base
3
- delegate :redact, to: Mrsk::Utils
4
-
5
- MAX_LOG_SIZE = "10m"
3
+ delegate :redact, :argumentize, to: Mrsk::Utils
6
4
 
7
5
  attr_accessor :config
8
6
 
@@ -18,7 +16,7 @@ module Mrsk::Commands
18
16
  end
19
17
 
20
18
  def container_id_for(container_name:)
21
- docker :container, :ls, "--all", "--filter", "name=#{container_name}", "--quiet"
19
+ docker :container, :ls, "--all", "--filter", "name=^#{container_name}$", "--quiet"
22
20
  end
23
21
 
24
22
  private
@@ -41,6 +39,10 @@ module Mrsk::Commands
41
39
  combine *commands, by: ">>"
42
40
  end
43
41
 
42
+ def write(*commands)
43
+ combine *commands, by: ">"
44
+ end
45
+
44
46
  def xargs(command)
45
47
  [ :xargs, command ].flatten
46
48
  end
@@ -7,6 +7,7 @@ class Mrsk::Commands::Builder::Base < Mrsk::Commands::Base
7
7
 
8
8
  def pull
9
9
  docker :pull, config.absolute_image
10
+ docker :pull, config.latest_image
10
11
  end
11
12
 
12
13
  def build_options
@@ -10,7 +10,8 @@ class Mrsk::Commands::Builder::Native < Mrsk::Commands::Builder::Base
10
10
  def push
11
11
  combine \
12
12
  docker(:build, *build_options, build_context),
13
- docker(:push, config.absolute_image)
13
+ docker(:push, config.absolute_image),
14
+ docker(:push, config.latest_image)
14
15
  end
15
16
 
16
17
  def info
@@ -9,8 +9,10 @@ class Mrsk::Commands::Healthcheck < Mrsk::Commands::Base
9
9
  "--name", container_name_with_version,
10
10
  "--publish", "#{EXPOSED_PORT}:#{config.healthcheck["port"]}",
11
11
  "--label", "service=#{container_name}",
12
+ "-e", "MRSK_CONTAINER_NAME=\"#{container_name}\"",
12
13
  *web.env_args,
13
14
  *config.volume_args,
15
+ *web.option_args,
14
16
  config.absolute_image,
15
17
  web.cmd
16
18
  end
@@ -33,15 +35,15 @@ class Mrsk::Commands::Healthcheck < Mrsk::Commands::Base
33
35
 
34
36
  private
35
37
  def container_name
36
- "healthcheck-#{config.service}"
38
+ [ "healthcheck", config.service, config.destination ].compact.join("-")
37
39
  end
38
40
 
39
41
  def container_name_with_version
40
- "healthcheck-#{config.service_with_version}"
42
+ "#{container_name}-#{config.version}"
41
43
  end
42
44
 
43
45
  def container_id
44
- container_id_for(container_name: container_name)
46
+ container_id_for(container_name: container_name_with_version)
45
47
  end
46
48
 
47
49
  def health_url
@@ -0,0 +1,63 @@
1
+ require "active_support/duration"
2
+ require "active_support/core_ext/numeric/time"
3
+
4
+ class Mrsk::Commands::Lock < Mrsk::Commands::Base
5
+ def acquire(message, version)
6
+ combine \
7
+ [:mkdir, lock_dir],
8
+ write_lock_details(message, version)
9
+ end
10
+
11
+ def release
12
+ combine \
13
+ [:rm, lock_details_file],
14
+ [:rm, "-r", lock_dir]
15
+ end
16
+
17
+ def status
18
+ combine \
19
+ stat_lock_dir,
20
+ read_lock_details
21
+ end
22
+
23
+ private
24
+ def write_lock_details(message, version)
25
+ write \
26
+ [:echo, "\"#{Base64.encode64(lock_details(message, version))}\""],
27
+ lock_details_file
28
+ end
29
+
30
+ def read_lock_details
31
+ pipe \
32
+ [:cat, lock_details_file],
33
+ [:base64, "-d"]
34
+ end
35
+
36
+ def stat_lock_dir
37
+ write \
38
+ [:stat, lock_dir],
39
+ "/dev/null"
40
+ end
41
+
42
+ def lock_dir
43
+ :mrsk_lock
44
+ end
45
+
46
+ def lock_details_file
47
+ [lock_dir, :details].join("/")
48
+ end
49
+
50
+ def lock_details(message, version)
51
+ <<~DETAILS.strip
52
+ Locked by: #{locked_by} at #{Time.now.gmtime}
53
+ Version: #{version}
54
+ Message: #{message}
55
+ DETAILS
56
+ end
57
+
58
+ def locked_by
59
+ `git config user.name`.strip
60
+ rescue Errno::ENOENT
61
+ "Unknown"
62
+ end
63
+ end
@@ -1,16 +1,18 @@
1
1
  class Mrsk::Commands::Traefik < Mrsk::Commands::Base
2
2
  delegate :optionize, to: Mrsk::Utils
3
3
 
4
+ IMAGE = "traefik:v2.9.9"
4
5
  CONTAINER_PORT = 80
5
6
 
6
7
  def run
7
8
  docker :run, "--name traefik",
8
9
  "--detach",
9
10
  "--restart", "unless-stopped",
10
- "--log-opt", "max-size=#{MAX_LOG_SIZE}",
11
11
  "--publish", port,
12
12
  "--volume", "/var/run/docker.sock:/var/run/docker.sock",
13
- "traefik",
13
+ *config.logging_args,
14
+ *docker_options_args,
15
+ IMAGE,
14
16
  "--providers.docker",
15
17
  "--log.level=DEBUG",
16
18
  *cmd_option_args
@@ -25,7 +27,7 @@ class Mrsk::Commands::Traefik < Mrsk::Commands::Base
25
27
  end
26
28
 
27
29
  def info
28
- docker :ps, "--filter", "name=traefik"
30
+ docker :ps, "--filter", "name=^traefik$"
29
31
  end
30
32
 
31
33
  def logs(since: nil, lines: nil, grep: nil)
@@ -49,20 +51,24 @@ class Mrsk::Commands::Traefik < Mrsk::Commands::Base
49
51
  docker :image, :prune, "--all", "--force", "--filter", "label=org.opencontainers.image.title=Traefik"
50
52
  end
51
53
 
52
- def port
54
+ def port
53
55
  "#{host_port}:#{CONTAINER_PORT}"
54
56
  end
55
57
 
56
58
  private
59
+ def docker_options_args
60
+ optionize(config.traefik["options"] || {})
61
+ end
62
+
57
63
  def cmd_option_args
58
- if args = config.raw_config.dig(:traefik, "args")
59
- optionize args
64
+ if args = config.traefik["args"]
65
+ optionize args, with: "="
60
66
  else
61
67
  []
62
68
  end
63
69
  end
64
70
 
65
71
  def host_port
66
- config.raw_config.dig(:traefik, "host_port") || CONTAINER_PORT
72
+ config.traefik["host_port"] || CONTAINER_PORT
67
73
  end
68
74
  end
@@ -1,5 +1,5 @@
1
1
  class Mrsk::Configuration::Accessory
2
- delegate :argumentize, :argumentize_env_with_secrets, to: Mrsk::Utils
2
+ delegate :argumentize, :argumentize_env_with_secrets, :optionize, to: Mrsk::Utils
3
3
 
4
4
  attr_accessor :name, :specifics
5
5
 
@@ -15,18 +15,24 @@ class Mrsk::Configuration::Accessory
15
15
  specifics["image"]
16
16
  end
17
17
 
18
- def host
19
- specifics["host"] || raise(ArgumentError, "Missing host for accessory")
18
+ def hosts
19
+ if (specifics.keys & ["host", "hosts", "roles"]).size != 1
20
+ raise ArgumentError, "Specify one of `host`, `hosts` or `roles` for accessory `#{name}`"
21
+ end
22
+
23
+ hosts_from_host || hosts_from_hosts || hosts_from_roles
20
24
  end
21
25
 
22
26
  def port
23
- if specifics["port"].to_s.include?(":")
24
- specifics["port"]
25
- else
26
- "#{specifics["port"]}:#{specifics["port"]}"
27
+ if port = specifics["port"]&.to_s
28
+ port.include?(":") ? port : "#{port}:#{port}"
27
29
  end
28
30
  end
29
31
 
32
+ def publish_args
33
+ argumentize "--publish", port if port
34
+ end
35
+
30
36
  def labels
31
37
  default_labels.merge(specifics["labels"] || {})
32
38
  end
@@ -65,6 +71,18 @@ class Mrsk::Configuration::Accessory
65
71
  argumentize "--volume", volumes
66
72
  end
67
73
 
74
+ def option_args
75
+ if args = specifics["options"]
76
+ optionize args
77
+ else
78
+ []
79
+ end
80
+ end
81
+
82
+ def cmd
83
+ specifics["cmd"]
84
+ end
85
+
68
86
  private
69
87
  attr_accessor :config
70
88
 
@@ -120,4 +138,32 @@ class Mrsk::Configuration::Accessory
120
138
  def service_data_directory
121
139
  "$PWD/#{service_name}"
122
140
  end
141
+
142
+ def hosts_from_host
143
+ if specifics.key?("host")
144
+ host = specifics["host"]
145
+ if host
146
+ [host]
147
+ else
148
+ raise ArgumentError, "Missing host for accessory `#{name}`"
149
+ end
150
+ end
151
+ end
152
+
153
+ def hosts_from_hosts
154
+ if specifics.key?("hosts")
155
+ hosts = specifics["hosts"]
156
+ if hosts.is_a?(Array)
157
+ hosts
158
+ else
159
+ raise ArgumentError, "Hosts should be an Array for accessory `#{name}`"
160
+ end
161
+ end
162
+ end
163
+
164
+ def hosts_from_roles
165
+ if specifics.key?("roles")
166
+ specifics["roles"].flat_map { |role| config.role(role).hosts }
167
+ end
168
+ end
123
169
  end
@@ -7,6 +7,10 @@ class Mrsk::Configuration::Role
7
7
  @name, @config = name.inquiry, config
8
8
  end
9
9
 
10
+ def primary_host
11
+ hosts.first
12
+ end
13
+
10
14
  def hosts
11
15
  @hosts ||= extract_hosts_from_config
12
16
  end
@@ -55,12 +59,16 @@ class Mrsk::Configuration::Role
55
59
  config.servers
56
60
  else
57
61
  servers = config.servers[name]
58
- servers.is_a?(Array) ? servers : servers["hosts"]
62
+ servers.is_a?(Array) ? servers : Array(servers["hosts"])
59
63
  end
60
64
  end
61
65
 
62
66
  def default_labels
63
- { "service" => config.service, "role" => name }
67
+ if config.destination
68
+ { "service" => config.service, "role" => name, "destination" => config.destination }
69
+ else
70
+ { "service" => config.service, "role" => name }
71
+ end
64
72
  end
65
73
 
66
74
  def traefik_labels
@@ -69,8 +77,9 @@ class Mrsk::Configuration::Role
69
77
  "traefik.http.routers.#{config.service}.rule" => "PathPrefix(`/`)",
70
78
  "traefik.http.services.#{config.service}.loadbalancer.healthcheck.path" => config.healthcheck["path"],
71
79
  "traefik.http.services.#{config.service}.loadbalancer.healthcheck.interval" => "1s",
72
- "traefik.http.middlewares.#{config.service}.retry.attempts" => "5",
73
- "traefik.http.middlewares.#{config.service}.retry.initialinterval" => "500ms"
80
+ "traefik.http.middlewares.#{config.service}-retry.retry.attempts" => "5",
81
+ "traefik.http.middlewares.#{config.service}-retry.retry.initialinterval" => "500ms",
82
+ "traefik.http.routers.#{config.service}.middlewares" => "#{config.service}-retry@docker"
74
83
  }
75
84
  else
76
85
  {}
@@ -6,23 +6,24 @@ require "erb"
6
6
  require "net/ssh/proxy/jump"
7
7
 
8
8
  class Mrsk::Configuration
9
- delegate :service, :image, :servers, :env, :labels, :registry, :builder, to: :raw_config, allow_nil: true
10
- delegate :argumentize, :argumentize_env_with_secrets, to: Mrsk::Utils
9
+ delegate :service, :image, :servers, :env, :labels, :registry, :builder, :stop_wait_time, to: :raw_config, allow_nil: true
10
+ delegate :argumentize, :argumentize_env_with_secrets, :optionize, to: Mrsk::Utils
11
11
 
12
- attr_accessor :version
12
+ attr_accessor :destination
13
13
  attr_accessor :raw_config
14
14
 
15
15
  class << self
16
- def create_from(base_config_file, destination: nil, version: "missing")
17
- new(load_config_file(base_config_file).tap do |config|
18
- if destination
19
- config.deep_merge! \
20
- load_config_file destination_config_file(base_config_file, destination)
21
- end
22
- end, version: version)
16
+ def create_from(config_file:, destination: nil, version: nil)
17
+ raw_config = load_config_files(config_file, *destination_config_file(config_file, destination))
18
+
19
+ new raw_config, destination: destination, version: version
23
20
  end
24
21
 
25
22
  private
23
+ def load_config_files(*files)
24
+ files.inject({}) { |config, file| config.deep_merge! load_config_file(file) }
25
+ end
26
+
26
27
  def load_config_file(file)
27
28
  if file.exist?
28
29
  YAML.load(ERB.new(IO.read(file)).result).symbolize_keys
@@ -32,18 +33,31 @@ class Mrsk::Configuration
32
33
  end
33
34
 
34
35
  def destination_config_file(base_config_file, destination)
35
- dir, basename = base_config_file.split
36
- dir.join basename.to_s.remove(".yml") + ".#{destination}.yml"
36
+ base_config_file.sub_ext(".#{destination}.yml") if destination
37
37
  end
38
38
  end
39
39
 
40
- def initialize(raw_config, version: "missing", validate: true)
40
+ def initialize(raw_config, destination: nil, version: nil, validate: true)
41
41
  @raw_config = ActiveSupport::InheritableOptions.new(raw_config)
42
- @version = version
42
+ @destination = destination
43
+ @declared_version = version
43
44
  valid? if validate
44
45
  end
45
46
 
46
47
 
48
+ def version=(version)
49
+ @declared_version = version
50
+ end
51
+
52
+ def version
53
+ @declared_version.presence || ENV["VERSION"] || current_commit_hash
54
+ end
55
+
56
+ def abbreviated_version
57
+ Mrsk::Utils.abbreviate_version(version)
58
+ end
59
+
60
+
47
61
  def roles
48
62
  @roles ||= role_names.collect { |role_name| Role.new(role_name, config: self) }
49
63
  end
@@ -62,15 +76,15 @@ class Mrsk::Configuration
62
76
 
63
77
 
64
78
  def all_hosts
65
- roles.flat_map(&:hosts)
79
+ roles.flat_map(&:hosts).uniq
66
80
  end
67
81
 
68
82
  def primary_web_host
69
- role(:web).hosts.first
83
+ role(:web).primary_host
70
84
  end
71
85
 
72
86
  def traefik_hosts
73
- roles.select(&:running_traefik?).flat_map(&:hosts)
87
+ roles.select(&:running_traefik?).flat_map(&:hosts).uniq
74
88
  end
75
89
 
76
90
 
@@ -107,6 +121,15 @@ class Mrsk::Configuration
107
121
  end
108
122
  end
109
123
 
124
+ def logging_args
125
+ if raw_config.logging.present?
126
+ optionize({ "log-driver" => raw_config.logging["driver"] }.compact) +
127
+ argumentize("--log-opt", raw_config.logging["options"])
128
+ else
129
+ argumentize("--log-opt", { "max-size" => "10m" })
130
+ end
131
+ end
132
+
110
133
 
111
134
  def ssh_user
112
135
  if raw_config.ssh.present?
@@ -159,10 +182,14 @@ class Mrsk::Configuration
159
182
  ssh_options: ssh_options,
160
183
  builder: raw_config.builder,
161
184
  accessories: raw_config.accessories,
185
+ logging: logging_args,
162
186
  healthcheck: healthcheck
163
187
  }.compact
164
188
  end
165
189
 
190
+ def traefik
191
+ raw_config.traefik || {}
192
+ end
166
193
 
167
194
  private
168
195
  # Will raise ArgumentError if any required config keys are missing
@@ -179,6 +206,12 @@ class Mrsk::Configuration
179
206
  raise ArgumentError, "You must specify a password for the registry in config/deploy.yml (or set the ENV variable if that's used)"
180
207
  end
181
208
 
209
+ roles.each do |role|
210
+ if role.hosts.empty?
211
+ raise ArgumentError, "No servers specified for the #{role.name} role"
212
+ end
213
+ end
214
+
182
215
  true
183
216
  end
184
217
 
@@ -193,4 +226,13 @@ class Mrsk::Configuration
193
226
  def role_names
194
227
  raw_config.servers.is_a?(Array) ? [ "web" ] : raw_config.servers.keys.sort
195
228
  end
229
+
230
+ def current_commit_hash
231
+ @current_commit_hash ||=
232
+ if system("git rev-parse")
233
+ `git rev-parse HEAD`.strip
234
+ else
235
+ raise "Can't use commit hash as version, no git repository found in #{Dir.pwd}"
236
+ end
237
+ end
196
238
  end