mrsk 0.1.0 → 0.3.0

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