mrsk 0.1.0 → 0.3.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.
@@ -8,8 +8,9 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
8
8
  "-d",
9
9
  "--restart unless-stopped",
10
10
  "--name", config.service_with_version,
11
- "-e", redact("RAILS_MASTER_KEY=#{config.master_key}"),
12
- *config.env_args,
11
+ *rails_master_key_arg,
12
+ *role.env_args,
13
+ *config.volume_args,
13
14
  *role.label_args,
14
15
  config.absolute_image,
15
16
  role.cmd
@@ -19,29 +20,64 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
19
20
  docker :start, "#{config.service}-#{version}"
20
21
  end
21
22
 
23
+ def current_container_id
24
+ docker :ps, "-q", *service_filter
25
+ end
26
+
22
27
  def stop
23
- [ "docker ps -q #{service_filter.join(" ")} | xargs docker stop" ]
28
+ pipe current_container_id, "xargs docker stop"
24
29
  end
25
30
 
26
31
  def info
27
32
  docker :ps, *service_filter
28
33
  end
29
34
 
30
- def logs
31
- [ "docker ps -q #{service_filter.join(" ")} | xargs docker logs -n 100 -t" ]
35
+ def logs(since: nil, lines: nil, grep: nil)
36
+ pipe \
37
+ current_container_id,
38
+ "xargs docker logs#{" --since #{since}" if since}#{" -n #{lines}" if lines} -t 2>&1",
39
+ ("grep '#{grep}'" if grep)
32
40
  end
33
41
 
34
42
  def exec(*command, interactive: false)
35
43
  docker :exec,
36
44
  ("-it" if interactive),
37
- "-e", redact("RAILS_MASTER_KEY=#{config.master_key}"),
45
+ *rails_master_key_arg,
38
46
  *config.env_args,
47
+ *config.volume_args,
39
48
  config.service_with_version,
40
49
  *command
41
50
  end
42
51
 
43
- def console(host: config.primary_host)
44
- "ssh -t #{config.ssh_user}@#{host} '#{exec("bin/rails", "c", interactive: true).join(" ")}'"
52
+ def run_exec(*command, interactive: false)
53
+ docker :run,
54
+ ("-it" if interactive),
55
+ "--rm",
56
+ *rails_master_key_arg,
57
+ *config.env_args,
58
+ *config.volume_args,
59
+ config.absolute_image,
60
+ *command
61
+ end
62
+
63
+ def exec_over_ssh(*command, host:)
64
+ run_over_ssh run_exec(*command, interactive: true).join(" "), host: host
65
+ end
66
+
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
73
+ end
74
+
75
+ def console(host:)
76
+ exec_over_ssh "bin/rails", "c", host: host
77
+ end
78
+
79
+ def bash(host:)
80
+ exec_over_ssh "bash", host: host
45
81
  end
46
82
 
47
83
  def list_containers
@@ -60,4 +96,12 @@ class Mrsk::Commands::App < Mrsk::Commands::Base
60
96
  def service_filter
61
97
  [ "--filter", "label=service=#{config.service}" ]
62
98
  end
99
+
100
+ def rails_master_key_arg
101
+ if master_key = config.master_key
102
+ [ "-e", redact("RAILS_MASTER_KEY=#{master_key}") ]
103
+ else
104
+ []
105
+ end
106
+ end
63
107
  end
@@ -1,7 +1,7 @@
1
- require "sshkit"
2
-
3
1
  module Mrsk::Commands
4
2
  class Base
3
+ delegate :redact, to: Mrsk::Utils
4
+
5
5
  attr_accessor :config
6
6
 
7
7
  def initialize(config)
@@ -9,19 +9,27 @@ module Mrsk::Commands
9
9
  end
10
10
 
11
11
  private
12
- def combine(*commands)
12
+ def combine(*commands, by: "&&")
13
13
  commands
14
- .collect { |command| command + [ "&&" ] }.flatten # Join commands with &&
15
- .tap { |commands| commands.pop } # Remove trailing &&
14
+ .compact
15
+ .collect { |command| Array(command) + [ by ] }.flatten # Join commands
16
+ .tap { |commands| commands.pop } # Remove trailing combiner
17
+ end
18
+
19
+ def chain(*commands)
20
+ combine *commands, by: ";"
21
+ end
22
+
23
+ def pipe(*commands)
24
+ combine *commands, by: "|"
16
25
  end
17
26
 
18
27
  def docker(*args)
19
28
  args.compact.unshift :docker
20
29
  end
21
30
 
22
- # Copied from SSHKit::Backend::Abstract#redact to be available inside Commands classes
23
- def redact(arg) # Used in execute_command to hide redact() args a user passes in
24
- arg.to_s.extend(SSHKit::Redaction) # to_s due to our inability to extend Integer, etc
25
- end
31
+ def run_over_ssh(command, host:)
32
+ "ssh -t #{config.ssh_user}@#{host} '#{command}'"
33
+ end
26
34
  end
27
35
  end
@@ -0,0 +1,26 @@
1
+ require "mrsk/commands/base"
2
+
3
+ class Mrsk::Commands::Builder::Base < Mrsk::Commands::Base
4
+ delegate :argumentize, to: Mrsk::Utils
5
+
6
+ def pull
7
+ docker :pull, config.absolute_image
8
+ end
9
+
10
+ def build_args
11
+ argumentize "--build-arg", args, redacted: true
12
+ end
13
+
14
+ def build_secrets
15
+ argumentize "--secret", secrets.collect { |secret| [ "id", secret ] }
16
+ end
17
+
18
+ private
19
+ def args
20
+ (config.builder && config.builder["args"]) || {}
21
+ end
22
+
23
+ def secrets
24
+ (config.builder && config.builder["secrets"]) || []
25
+ end
26
+ end
@@ -15,8 +15,16 @@ class Mrsk::Commands::Builder::Multiarch::Remote < Mrsk::Commands::Builder::Mult
15
15
  end
16
16
 
17
17
  private
18
+ def builder_name
19
+ super + "-remote"
20
+ end
21
+
22
+ def builder_name_with_arch(arch)
23
+ "#{builder_name}-#{arch}"
24
+ end
25
+
18
26
  def create_local_buildx
19
- docker :buildx, :create, "--use", "--name", builder_name, builder_name_with_arch(local["arch"]), "--platform", "linux/#{local["arch"]}"
27
+ docker :buildx, :create, "--name", builder_name, builder_name_with_arch(local["arch"]), "--platform", "linux/#{local["arch"]}"
20
28
  end
21
29
 
22
30
  def append_remote_buildx
@@ -50,9 +58,4 @@ class Mrsk::Commands::Builder::Multiarch::Remote < Mrsk::Commands::Builder::Mult
50
58
  def remote
51
59
  config.builder["remote"]
52
60
  end
53
-
54
- private
55
- def builder_name_with_arch(arch)
56
- "#{builder_name}-#{arch}"
57
- end
58
61
  end
@@ -1,6 +1,6 @@
1
- require "mrsk/commands/base"
1
+ require "mrsk/commands/builder/base"
2
2
 
3
- class Mrsk::Commands::Builder::Multiarch < Mrsk::Commands::Base
3
+ class Mrsk::Commands::Builder::Multiarch < Mrsk::Commands::Builder::Base
4
4
  def create
5
5
  docker :buildx, :create, "--use", "--name", builder_name
6
6
  end
@@ -10,11 +10,14 @@ class Mrsk::Commands::Builder::Multiarch < Mrsk::Commands::Base
10
10
  end
11
11
 
12
12
  def push
13
- docker :buildx, :build, "--push", "--platform linux/amd64,linux/arm64", "-t", config.absolute_image, "."
14
- end
15
-
16
- def pull
17
- docker :pull, config.absolute_image
13
+ docker :buildx, :build,
14
+ "--push",
15
+ "--platform", "linux/amd64,linux/arm64",
16
+ "--builder", builder_name,
17
+ "-t", config.absolute_image,
18
+ *build_args,
19
+ *build_secrets,
20
+ "."
18
21
  end
19
22
 
20
23
  def info
@@ -25,6 +28,6 @@ class Mrsk::Commands::Builder::Multiarch < Mrsk::Commands::Base
25
28
 
26
29
  private
27
30
  def builder_name
28
- "mrsk-#{config.service}"
31
+ "mrsk-#{config.service}-multiarch"
29
32
  end
30
33
  end
@@ -0,0 +1,71 @@
1
+ require "mrsk/commands/builder/native"
2
+
3
+ class Mrsk::Commands::Builder::Native::Remote < Mrsk::Commands::Builder::Native
4
+ def create
5
+ chain \
6
+ create_context,
7
+ create_buildx
8
+ end
9
+
10
+ def remove
11
+ chain \
12
+ remove_context,
13
+ remove_buildx
14
+ end
15
+
16
+ def push
17
+ docker :buildx, :build,
18
+ "--push",
19
+ "--platform", platform,
20
+ "--builder", builder_name,
21
+ "-t", config.absolute_image,
22
+ *build_args,
23
+ *build_secrets,
24
+ "."
25
+ end
26
+
27
+ def info
28
+ chain \
29
+ docker(:context, :ls),
30
+ docker(:buildx, :ls)
31
+ end
32
+
33
+
34
+ private
35
+ def arch
36
+ config.builder["remote"]["arch"]
37
+ end
38
+
39
+ def host
40
+ config.builder["remote"]["host"]
41
+ end
42
+
43
+ def builder_name
44
+ "mrsk-#{config.service}-native-remote"
45
+ end
46
+
47
+ def builder_name_with_arch
48
+ "#{builder_name}-#{arch}"
49
+ end
50
+
51
+ def platform
52
+ "linux/#{arch}"
53
+ end
54
+
55
+ def create_context
56
+ docker :context, :create,
57
+ builder_name_with_arch, "--description", "'#{builder_name} #{arch} native host'", "--docker", "'host=#{host}'"
58
+ end
59
+
60
+ def remove_context
61
+ docker :context, :rm, builder_name_with_arch
62
+ end
63
+
64
+ def create_buildx
65
+ docker :buildx, :create, "--name", builder_name, builder_name_with_arch, "--platform", platform
66
+ end
67
+
68
+ def remove_buildx
69
+ docker :buildx, :rm, builder_name
70
+ end
71
+ end
@@ -1,6 +1,6 @@
1
- require "mrsk/commands/base"
1
+ require "mrsk/commands/builder/base"
2
2
 
3
- class Mrsk::Commands::Builder::Native < Mrsk::Commands::Base
3
+ class Mrsk::Commands::Builder::Native < Mrsk::Commands::Builder::Base
4
4
  def create
5
5
  # No-op on native
6
6
  end
@@ -11,14 +11,10 @@ class Mrsk::Commands::Builder::Native < Mrsk::Commands::Base
11
11
 
12
12
  def push
13
13
  combine \
14
- docker(:build, "-t", config.absolute_image, "."),
14
+ docker(:build, "-t", *build_args, *build_secrets, config.absolute_image, "."),
15
15
  docker(:push, config.absolute_image)
16
16
  end
17
17
 
18
- def pull
19
- docker :pull, config.absolute_image
20
- end
21
-
22
18
  def info
23
19
  # No-op on native
24
20
  end
@@ -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,116 @@
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 files
47
+ specifics["files"]&.to_h do |local_to_remote_mapping|
48
+ local_file, remote_file = local_to_remote_mapping.split(":")
49
+ [ expand_local_file(local_file), expand_remote_file(remote_file) ]
50
+ end || {}
51
+ end
52
+
53
+ def directories
54
+ specifics["directories"]&.to_h do |host_to_container_mapping|
55
+ host_relative_path, container_path = host_to_container_mapping.split(":")
56
+ [ expand_host_path(host_relative_path), container_path ]
57
+ end || {}
58
+ end
59
+
60
+ def volumes
61
+ specific_volumes + remote_files_as_volumes + remote_directories_as_volumes
62
+ end
63
+
64
+ def volume_args
65
+ argumentize "--volume", volumes
66
+ end
67
+
68
+ private
69
+ attr_accessor :config
70
+
71
+ def default_labels
72
+ { "service" => service_name }
73
+ end
74
+
75
+ def expand_local_file(local_file)
76
+ if local_file.end_with?("erb")
77
+ read_dynamic_file(local_file)
78
+ else
79
+ Pathname.new(File.expand_path(local_file)).to_s
80
+ end
81
+ end
82
+
83
+ def read_dynamic_file(local_file)
84
+ StringIO.new(ERB.new(IO.read(local_file)).result)
85
+ end
86
+
87
+ def expand_remote_file(remote_file)
88
+ service_name + remote_file
89
+ end
90
+
91
+ def specific_volumes
92
+ specifics["volumes"] || []
93
+ end
94
+
95
+ def remote_files_as_volumes
96
+ specifics["files"]&.collect do |local_to_remote_mapping|
97
+ _, remote_file = local_to_remote_mapping.split(":")
98
+ "#{service_data_directory + remote_file}:#{remote_file}"
99
+ end || []
100
+ end
101
+
102
+ def remote_directories_as_volumes
103
+ specifics["directories"]&.collect do |host_to_container_mapping|
104
+ host_relative_path, container_path = host_to_container_mapping.split(":")
105
+ [ expand_host_path(host_relative_path), container_path ].join(":")
106
+ end || []
107
+ end
108
+
109
+ def expand_host_path(host_relative_path)
110
+ "#{service_data_directory}/#{host_relative_path}"
111
+ end
112
+
113
+ def service_data_directory
114
+ "$PWD/#{service_name}"
115
+ end
116
+ 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