mrsk 0.9.0 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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