mrsk 0.0.1 → 0.0.3

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,17 +1,37 @@
1
+ require "mrsk/commands/base"
2
+
1
3
  class Mrsk::Commands::Traefik < Mrsk::Commands::Base
4
+ def run
5
+ docker :run, "--name traefik",
6
+ "-d",
7
+ "--restart unless-stopped",
8
+ "-p 80:80",
9
+ "-v /var/run/docker.sock:/var/run/docker.sock",
10
+ "traefik",
11
+ "--providers.docker"
12
+ end
13
+
2
14
  def start
3
- "docker run --name traefik " +
4
- "--rm -d " +
5
- "-p 80:80 " +
6
- "-v /var/run/docker.sock:/var/run/docker.sock " +
7
- "traefik --providers.docker"
15
+ docker :container, :start, "traefik"
8
16
  end
9
17
 
10
18
  def stop
11
- "docker container stop traefik"
19
+ docker :container, :stop, "traefik"
12
20
  end
13
21
 
14
22
  def info
15
- "docker ps --filter name=traefik"
23
+ docker :ps, "--filter", "name=traefik"
24
+ end
25
+
26
+ def logs
27
+ docker :logs, "traefik", "-n", "100", "-t"
28
+ end
29
+
30
+ def remove_container
31
+ docker :container, :prune, "-f", "--filter", "label=org.opencontainers.image.title=Traefik"
32
+ end
33
+
34
+ def remove_image
35
+ docker :image, :prune, "-a", "-f", "--filter", "label=org.opencontainers.image.title=Traefik"
16
36
  end
17
37
  end
data/lib/mrsk/commands.rb CHANGED
@@ -1,13 +1,2 @@
1
1
  module Mrsk::Commands
2
- class Base
3
- attr_accessor :config
4
-
5
- def initialize(config)
6
- @config = config
7
- end
8
- end
9
2
  end
10
-
11
- require "mrsk/commands/app"
12
- require "mrsk/commands/traefik"
13
- require "mrsk/commands/registry"
@@ -0,0 +1,70 @@
1
+ class Mrsk::Configuration::Role
2
+ delegate :argumentize, to: Mrsk::Configuration
3
+
4
+ attr_accessor :name
5
+
6
+ def initialize(name, config:)
7
+ @name, @config = name.inquiry, config
8
+ end
9
+
10
+ def hosts
11
+ @hosts ||= extract_hosts_from_config
12
+ end
13
+
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
20
+ end
21
+
22
+ def label_args
23
+ argumentize "--label", labels
24
+ end
25
+
26
+ def cmd
27
+ specializations["cmd"]
28
+ end
29
+
30
+ private
31
+ attr_accessor :config
32
+
33
+ def extract_hosts_from_config
34
+ if config.servers.is_a?(Array)
35
+ config.servers
36
+ else
37
+ servers = config.servers[name]
38
+ servers.is_a?(Array) ? servers : servers["hosts"]
39
+ end
40
+ end
41
+
42
+ def default_labels
43
+ { "service" => config.service, "role" => name }
44
+ end
45
+
46
+ 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
+ }
54
+ end
55
+
56
+ def custom_labels
57
+ Hash.new.tap do |labels|
58
+ labels.merge!(config.labels) if config.labels.present?
59
+ labels.merge!(specializations["labels"]) if specializations["labels"].present?
60
+ end
61
+ end
62
+
63
+ def specializations
64
+ if config.servers.is_a?(Array) || config.servers[name].is_a?(Array)
65
+ { }
66
+ else
67
+ config.servers[name].without("hosts")
68
+ end
69
+ end
70
+ end
@@ -1,60 +1,100 @@
1
1
  require "active_support/ordered_options"
2
+ require "active_support/core_ext/string/inquiry"
3
+ require "erb"
2
4
 
3
5
  class Mrsk::Configuration
4
- delegate :service, :image, :env, :registry, :ssh_user, to: :config, allow_nil: true
6
+ delegate :service, :image, :servers, :env, :labels, :registry, :builder, to: :config, allow_nil: true
7
+
8
+ class << self
9
+ def load_file(file)
10
+ if file.exist?
11
+ new YAML.load(ERB.new(IO.read(file)).result).symbolize_keys
12
+ else
13
+ raise "Configuration file not found in #{file}"
14
+ end
15
+ end
5
16
 
6
- def self.load_file(file)
7
- if file.exist?
8
- new YAML.load_file(file).symbolize_keys
9
- else
10
- raise "Configuration file not found in #{file}"
17
+ def argumentize(argument, attributes)
18
+ attributes.flat_map { |k, v| [ argument, "#{k}=#{v}" ] }
11
19
  end
12
20
  end
13
21
 
14
- def initialize(config)
22
+ def initialize(config, validate: true)
15
23
  @config = ActiveSupport::InheritableOptions.new(config)
16
- ensure_required_keys_present
24
+ ensure_required_keys_present if validate
25
+ end
26
+
27
+
28
+ def roles
29
+ @roles ||= role_names.collect { |role_name| Role.new(role_name, config: self) }
30
+ end
31
+
32
+ def role(name)
33
+ roles.detect { |r| r.name == name.to_s }
34
+ end
35
+
36
+ def hosts
37
+ hosts =
38
+ case
39
+ when ENV["HOSTS"]
40
+ ENV["HOSTS"].split(",")
41
+ when ENV["ROLES"]
42
+ role_names = ENV["ROLES"].split(",")
43
+ roles.select { |r| role_names.include?(r.name) }.flat_map(&:hosts)
44
+ else
45
+ roles.flat_map(&:hosts)
46
+ end
47
+
48
+ if hosts.any?
49
+ hosts
50
+ else
51
+ raise ArgumentError, "No hosts found"
52
+ end
17
53
  end
18
54
 
19
- def servers
20
- ENV["SERVERS"] || config.servers
55
+ def primary_host
56
+ role(:web).hosts.first
21
57
  end
22
58
 
59
+
23
60
  def version
24
61
  @version ||= ENV["VERSION"] || `git rev-parse HEAD`.strip
25
62
  end
26
63
 
27
- def absolute_image
28
- [ config.registry["server"], image_with_version ].compact.join("/")
64
+ def repository
65
+ [ config.registry["server"], image ].compact.join("/")
29
66
  end
30
67
 
31
- def image_with_version
32
- "#{image}:#{version}"
68
+ def absolute_image
69
+ "#{repository}:#{version}"
33
70
  end
34
71
 
35
72
  def service_with_version
36
73
  "#{service}-#{version}"
37
74
  end
38
75
 
39
- def envs
40
- parameterize "-e", \
41
- { "RAILS_MASTER_KEY" => master_key }.merge(env || {})
76
+
77
+ def env_args
78
+ if config.env.present?
79
+ self.class.argumentize "-e", config.env
80
+ else
81
+ []
82
+ end
42
83
  end
43
84
 
44
- def labels
45
- parameterize "--label", \
46
- "service" => service,
47
- "traefik.http.routers.#{service}.rule" => "'PathPrefix(`/`)'",
48
- "traefik.http.services.#{service}.loadbalancer.healthcheck.path" => "/up",
49
- "traefik.http.services.#{service}.loadbalancer.healthcheck.interval" => "1s",
50
- "traefik.http.middlewares.#{service}.retry.attempts" => "3",
51
- "traefik.http.middlewares.#{service}.retry.initialinterval" => "500ms"
85
+ def ssh_user
86
+ config.ssh_user || "root"
52
87
  end
53
88
 
54
89
  def ssh_options
55
- { user: config.ssh_user || "root", auth_methods: [ "publickey" ] }
90
+ { user: ssh_user, auth_methods: [ "publickey" ] }
56
91
  end
57
92
 
93
+ def master_key
94
+ ENV["RAILS_MASTER_KEY"] || File.read(Rails.root.join("config/master.key"))
95
+ end
96
+
97
+
58
98
  private
59
99
  attr_accessor :config
60
100
 
@@ -68,11 +108,9 @@ class Mrsk::Configuration
68
108
  end
69
109
  end
70
110
 
71
- def parameterize(param, hash)
72
- hash.collect { |k, v| "#{param} #{k}=#{v}" }.join(" ")
73
- end
74
-
75
- def master_key
76
- ENV["RAILS_MASTER_KEY"] || File.read(Rails.root.join("config/master.key"))
111
+ def role_names
112
+ config.servers.is_a?(Array) ? [ "web" ] : config.servers.keys.sort
77
113
  end
78
114
  end
115
+
116
+ require "mrsk/configuration/role"
data/lib/mrsk/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Mrsk
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.3"
3
3
  end
data/lib/mrsk.rb CHANGED
@@ -3,6 +3,4 @@ end
3
3
 
4
4
  require "mrsk/version"
5
5
  require "mrsk/engine"
6
-
7
- require "mrsk/configuration"
8
- require "mrsk/commands"
6
+ require "mrsk/commander"
@@ -1,31 +1,97 @@
1
1
  require_relative "setup"
2
2
 
3
- app = Mrsk::Commands::App.new(MRSK_CONFIG)
4
-
5
3
  namespace :mrsk do
6
4
  namespace :app do
7
- desc "Build and push app image to servers"
8
- task :push do
9
- run_locally { execute app.push }
10
- on(MRSK_CONFIG.servers) { execute app.pull }
5
+ desc "Run app on servers (or start them if they've already been run)"
6
+ task :run do
7
+ MRSK.config.roles.each do |role|
8
+ on(role.hosts) do |host|
9
+ begin
10
+ execute *MRSK.app.run(role: role.name)
11
+ rescue SSHKit::Command::Failed => e
12
+ if e.message =~ /already in use/
13
+ error "Container with same version already deployed on #{host}, starting that instead"
14
+ execute *MRSK.app.start, host: host
15
+ else
16
+ raise
17
+ end
18
+ end
19
+ end
20
+ end
11
21
  end
12
22
 
13
- desc "Start app on servers"
23
+ desc "Start existing app on servers (use VERSION=<git-hash> to designate which version)"
14
24
  task :start do
15
- on(MRSK_CONFIG.servers) { execute app.start }
25
+ on(MRSK.config.hosts) { execute *MRSK.app.start, raise_on_non_zero_exit: false }
16
26
  end
17
27
 
18
28
  desc "Stop app on servers"
19
29
  task :stop do
20
- on(MRSK_CONFIG.servers) { execute app.stop, raise_on_non_zero_exit: false }
30
+ on(MRSK.config.hosts) { execute *MRSK.app.stop, raise_on_non_zero_exit: false }
21
31
  end
22
32
 
23
- desc "Restart app on servers"
33
+ desc "Start app on servers (use VERSION=<git-hash> to designate which version)"
24
34
  task restart: %i[ stop start ]
25
35
 
26
36
  desc "Display information about app containers"
27
37
  task :info do
28
- on(MRSK_CONFIG.servers) { |host| puts "Host: #{host}\n" + capture(app.info) + "\n\n" }
38
+ on(MRSK.config.hosts) { |host| puts "App Host: #{host}\n" + capture(*MRSK.app.info) + "\n\n" }
39
+ end
40
+
41
+ desc "Execute a custom task on servers passed in as CMD='bin/rake some:task'"
42
+ task :exec do
43
+ on(MRSK.config.hosts) { |host| puts "App Host: #{host}\n" + capture(*MRSK.app.exec(ENV["CMD"])) + "\n\n" }
44
+ end
45
+
46
+ desc "Start Rails Console on primary host"
47
+ task :console do
48
+ puts "Launching Rails console on #{MRSK.config.primary_host}..."
49
+ exec app.console
50
+ end
51
+
52
+ namespace :exec do
53
+ desc "Execute Rails command on servers, like CMD='runner \"puts %(Hello World)\""
54
+ task :rails do
55
+ on(MRSK.config.hosts) { |host| puts "App Host: #{host}\n" + capture(*MRSK.app.exec("bin/rails", ENV["CMD"])) + "\n\n" }
56
+ end
57
+
58
+ desc "Execute a custom task on the first defined server"
59
+ task :once do
60
+ on(MRSK.config.primary_host) { puts capture(*MRSK.app.exec(ENV["CMD"])) }
61
+ end
62
+
63
+ namespace :once do
64
+ desc "Execute Rails command on the first defined server, like CMD='runner \"puts %(Hello World)\""
65
+ task :rails do
66
+ on(MRSK.config.primary_host) { puts capture(*MRSK.app.exec("bin/rails", ENV["CMD"])) }
67
+ end
68
+ end
69
+ end
70
+
71
+ desc "List all the app containers currently on servers"
72
+ task :containers do
73
+ on(MRSK.config.hosts) { |host| puts "App Host: #{host}\n" + capture(*MRSK.app.list_containers) + "\n\n" }
74
+ end
75
+
76
+ desc "Show last 100 log lines from app on servers"
77
+ task :logs do
78
+ # FIXME: Catch when app containers aren't running
79
+ on(MRSK.config.hosts) { |host| puts "App Host: #{host}\n" + capture(*MRSK.app.logs) + "\n\n" }
80
+ end
81
+
82
+ desc "Remove app containers and images from servers"
83
+ task remove: %w[ remove:containers remove:images ]
84
+
85
+ namespace :remove do
86
+ desc "Remove app containers from servers"
87
+ task :containers do
88
+ on(MRSK.config.hosts) { execute *MRSK.app.remove_containers }
89
+ end
90
+
91
+ desc "Remove app images from servers"
92
+ task :images do
93
+ on(MRSK.config.hosts) { execute *MRSK.app.remove_images }
94
+ end
29
95
  end
30
96
  end
31
97
  end
@@ -0,0 +1,52 @@
1
+ require_relative "setup"
2
+
3
+ namespace :mrsk do
4
+ namespace :build do
5
+ desc "Deliver a newly built app image to servers"
6
+ task deliver: %i[ push pull ]
7
+
8
+ desc "Build locally and push app image to registry"
9
+ task :push do
10
+ run_locally do
11
+ begin
12
+ debug "Using builder: #{MRSK.builder.name}"
13
+ info "Building image may take a while (run with VERBOSE=1 for progress logging)"
14
+ execute *MRSK.builder.push
15
+ rescue SSHKit::Command::Failed => e
16
+ error "Missing compatible builder, so creating a new one first"
17
+ execute *MRSK.builder.create
18
+ execute *MRSK.builder.push
19
+ end
20
+ end unless ENV["VERSION"]
21
+ end
22
+
23
+ desc "Pull app image from the registry onto servers"
24
+ task :pull do
25
+ on(MRSK.config.hosts) { execute *MRSK.builder.pull }
26
+ end
27
+
28
+ desc "Create a local build setup"
29
+ task :create do
30
+ run_locally do
31
+ debug "Using builder: #{MRSK.builder.name}"
32
+ execute *MRSK.builder.create
33
+ end
34
+ end
35
+
36
+ desc "Remove local build setup"
37
+ task :remove do
38
+ run_locally do
39
+ debug "Using builder: #{MRSK.builder.name}"
40
+ execute *MRSK.builder.remove
41
+ end
42
+ end
43
+
44
+ desc "Show the name of the configured builder"
45
+ task :info do
46
+ run_locally do
47
+ puts "Builder: #{MRSK.builder.name} (#{MRSK.builder.target.class.name})"
48
+ puts capture(*MRSK.builder.info)
49
+ end
50
+ end
51
+ end
52
+ end
@@ -1,12 +1,37 @@
1
+ require_relative "setup"
2
+
1
3
  namespace :mrsk do
4
+ desc "Deploy app for the first time to a fresh server"
5
+ task fresh: %w[ server:bootstrap registry:login build:deliver traefik:run app:stop app:run ]
6
+
2
7
  desc "Push the latest version of the app, ensure Traefik is running, then restart app"
3
- task deploy: [ "app:push", "traefik:start", "app:restart" ]
8
+ task deploy: %w[ registry:login build:deliver traefik:run app:stop app:run prune ]
9
+
10
+ desc "Rollback to VERSION=x that was already run as a container on servers"
11
+ task rollback: %w[ app:restart ]
4
12
 
5
13
  desc "Display information about Traefik and app containers"
6
- task info: [ "traefik:info", "app:info" ]
14
+ task info: %w[ traefik:info app:info ]
7
15
 
8
- desc "Create config stub"
16
+ desc "Create config stub in config/deploy.yml"
9
17
  task :init do
10
- Rails.root.join("config/deploy.yml")
18
+ require "fileutils"
19
+
20
+ if (deploy_file = Rails.root.join("config/deploy.yml")).exist?
21
+ puts "Config file already exists in config/deploy.yml (remove first to create a new one)"
22
+ else
23
+ FileUtils.cp_r Pathname.new(File.expand_path("templates/deploy.yml", __dir__)), deploy_file
24
+ puts "Created configuration file in config/deploy.yml"
25
+ end
26
+
27
+ if (binstub = Rails.root.join("bin/mrsk")).exist?
28
+ puts "Binstub already exists in bin/mrsk (remove first to create a new one)"
29
+ else
30
+ FileUtils.cp_r Pathname.new(File.expand_path("templates/mrsk", __dir__)), binstub
31
+ puts "Created binstub file in bin/mrsk"
32
+ end
11
33
  end
34
+
35
+ desc "Remove Traefik, app, and registry session from servers"
36
+ task remove: %w[ traefik:remove app:remove registry:logout ]
12
37
  end
@@ -0,0 +1,18 @@
1
+ require_relative "setup"
2
+
3
+ namespace :mrsk do
4
+ desc "Prune unused images and stopped containers"
5
+ task prune: %w[ prune:containers prune:images ]
6
+
7
+ namespace :prune do
8
+ desc "Prune unused images older than 30 days"
9
+ task :images do
10
+ on(MRSK.config.hosts) { MRSK.verbosity(:debug) { execute *MRSK.prune.images } }
11
+ end
12
+
13
+ desc "Prune stopped containers for the service older than 3 days"
14
+ task :containers do
15
+ on(MRSK.config.hosts) { MRSK.verbosity(:debug) { execute *MRSK.prune.containers } }
16
+ end
17
+ end
18
+ end
@@ -1,13 +1,16 @@
1
1
  require_relative "setup"
2
2
 
3
- registry = Mrsk::Commands::Registry.new(MRSK_CONFIG)
4
-
5
3
  namespace :mrsk do
6
4
  namespace :registry do
7
5
  desc "Login to the registry locally and remotely"
8
6
  task :login do
9
- run_locally { execute registry.login }
10
- on(MRSK_CONFIG.servers) { execute registry.login }
7
+ run_locally { execute *MRSK.registry.login }
8
+ on(MRSK.config.hosts) { execute *MRSK.registry.login }
9
+ end
10
+
11
+ desc "Logout of the registry remotely"
12
+ task :logout do
13
+ on(MRSK.config.hosts) { execute *MRSK.registry.logout }
11
14
  end
12
15
  end
13
16
  end
@@ -0,0 +1,11 @@
1
+ require_relative "setup"
2
+
3
+ namespace :mrsk do
4
+ namespace :server do
5
+ desc "Setup Docker on the remote servers"
6
+ task :bootstrap do
7
+ # FIXME: Detect when apt-get is not available and use the appropriate alternative
8
+ on(MRSK.config.hosts) { execute "apt-get install docker.io -y" }
9
+ end
10
+ end
11
+ end
@@ -3,6 +3,4 @@ require "sshkit/dsl"
3
3
 
4
4
  include SSHKit::DSL
5
5
 
6
- MRSK_CONFIG = Mrsk::Configuration.load_file(Rails.root.join("config/deploy.yml"))
7
-
8
- SSHKit::Backend::Netssh.configure { |ssh| ssh.ssh_options = MRSK_CONFIG.ssh_options }
6
+ MRSK = Mrsk::Commander.new config_file: Rails.root.join("config/deploy.yml"), verbose: ENV["VERBOSE"]
@@ -0,0 +1,24 @@
1
+ # Name of your application will be used for uniquely configuring Traefik and app containers.
2
+ # Your Dockerfile should set LABEL service=the-same-value to ensure image pruning works.
3
+ service: my-app
4
+
5
+ # Name of the container image
6
+ image: user/my-app
7
+
8
+ # All the servers targeted for deploy. You can reference a single server for a command by using SERVERS=192.168.0.1
9
+ servers:
10
+ - 192.168.0.1
11
+
12
+ # The following envs are made available to the container when started
13
+ env:
14
+ # Remember never to put passwords or tokens directly into this file, use encrypted credentials
15
+ # REDIS_URL: redis://x/y
16
+
17
+ # Where your images will be hosted
18
+ registry:
19
+ # Specify the registry server, if you're not using Docker Hub
20
+ # server: registry.digitalocean.com / ghcr.io / ...
21
+
22
+ # Set credentials with bin/rails credentials:edit
23
+ username: my-user
24
+ password: my-password-should-go-in-credentials
@@ -0,0 +1,8 @@
1
+ #!/bin/bash
2
+
3
+ if [ "${*}" == "" ]; then
4
+ # Improve so list matches
5
+ exec bin/rake -T mrsk
6
+ else
7
+ exec bin/rake "mrsk:$@"
8
+ fi
@@ -1,25 +1,41 @@
1
1
  require_relative "setup"
2
2
 
3
- traefik = Mrsk::Commands::Traefik.new(MRSK_CONFIG)
4
-
5
3
  namespace :mrsk do
6
4
  namespace :traefik do
7
- desc "Start Traefik"
5
+ desc "Run Traefik on servers"
6
+ task :run do
7
+ on(MRSK.config.role(:web).hosts) { execute *MRSK.traefik.run, raise_on_non_zero_exit: false }
8
+ end
9
+
10
+ desc "Start existing Traefik on servers"
8
11
  task :start do
9
- on(MRSK_CONFIG.servers) { execute traefik.start, raise_on_non_zero_exit: false }
12
+ on(MRSK.config.role(:web).hosts) { execute *MRSK.traefik.start, raise_on_non_zero_exit: false }
10
13
  end
11
14
 
12
- desc "Stop Traefik"
15
+ desc "Stop Traefik on servers"
13
16
  task :stop do
14
- on(MRSK_CONFIG.servers) { execute traefik.stop, raise_on_non_zero_exit: false }
17
+ on(MRSK.config.role(:web).hosts) { execute *MRSK.traefik.stop, raise_on_non_zero_exit: false }
15
18
  end
16
19
 
17
- desc "Restart Traefik"
20
+ desc "Restart Traefik on servers"
18
21
  task restart: %i[ stop start ]
19
22
 
20
- desc "Display information about Traefik containers"
23
+ desc "Display information about Traefik containers from servers"
21
24
  task :info do
22
- on(MRSK_CONFIG.servers) { |host| puts "Host: #{host}\n" + capture(traefik.info) + "\n\n" }
25
+ on(MRSK.config.role(:web).hosts) { |host| puts "Traefik Host: #{host}\n" + capture(*MRSK.traefik.info) + "\n\n" }
26
+ end
27
+
28
+ desc "Show last 100 log lines from Traefik on servers"
29
+ task :logs do
30
+ on(MRSK.config.hosts) { |host| puts "Traefik Host: #{host}\n" + capture(*MRSK.traefik.logs) + "\n\n" }
31
+ end
32
+
33
+ desc "Remove Traefik container and image from servers"
34
+ task remove: %i[ stop ] do
35
+ on(MRSK.config.role(:web).hosts) do
36
+ execute *MRSK.traefik.remove_container
37
+ execute *MRSK.traefik.remove_image
38
+ end
23
39
  end
24
40
  end
25
41
  end