Move hosts/roles specification to cli args instead of ENV

This commit is contained in:
David Heinemeier Hansson
2023-01-20 16:57:25 +01:00
parent 0388495819
commit 79b5ed179e
13 changed files with 87 additions and 66 deletions

View File

@@ -23,20 +23,20 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
option :version, desc: "Defaults to the most recent git-hash in local repository" option :version, desc: "Defaults to the most recent git-hash in local repository"
def start def start
if (version = options[:version]).present? if (version = options[:version]).present?
on(MRSK.config.hosts) { execute *MRSK.app.start(version: version) } on(MRSK.hosts) { execute *MRSK.app.start(version: version) }
else else
on(MRSK.config.hosts) { execute *MRSK.app.start, raise_on_non_zero_exit: false } on(MRSK.hosts) { execute *MRSK.app.start, raise_on_non_zero_exit: false }
end end
end end
desc "stop", "Stop app on servers" desc "stop", "Stop app on servers"
def stop def stop
on(MRSK.config.hosts) { execute *MRSK.app.stop, raise_on_non_zero_exit: false } on(MRSK.hosts) { execute *MRSK.app.stop, raise_on_non_zero_exit: false }
end end
desc "details", "Display details about app containers" desc "details", "Display details about app containers"
def details def details
on(MRSK.config.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.info) } on(MRSK.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.info) }
end end
desc "exec [CMD]", "Execute a custom task on servers passed in as CMD='bin/rake some:task'" desc "exec [CMD]", "Execute a custom task on servers passed in as CMD='bin/rake some:task'"
@@ -48,7 +48,7 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
if options[:once] if options[:once]
on(MRSK.config.primary_host) { puts capture_with_info(*MRSK.app.send(runner, cmd)) } on(MRSK.config.primary_host) { puts capture_with_info(*MRSK.app.send(runner, cmd)) }
else else
on(MRSK.config.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.send(runner, cmd)) } on(MRSK.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.send(runner, cmd)) }
end end
end end
@@ -80,18 +80,18 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
if options[:once] if options[:once]
on(MRSK.config.primary_host) { puts capture_with_info(*MRSK.app.exec("bin/rails", "runner", "'#{expression}'")) } on(MRSK.config.primary_host) { puts capture_with_info(*MRSK.app.exec("bin/rails", "runner", "'#{expression}'")) }
else else
on(MRSK.config.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.exec("bin/rails", "runner", "'#{expression}'")) } on(MRSK.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.exec("bin/rails", "runner", "'#{expression}'")) }
end end
end end
desc "containers", "List all the app containers currently on servers" desc "containers", "List all the app containers currently on servers"
def containers def containers
on(MRSK.config.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.list_containers) } on(MRSK.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.list_containers) }
end end
desc "current", "Return the current running container ID" desc "current", "Return the current running container ID"
def current def current
on(MRSK.config.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.current_container_id) } on(MRSK.hosts) { |host| puts_by_host host, capture_with_info(*MRSK.app.current_container_id) }
end end
desc "logs", "Show last 100 log lines from app on servers" desc "logs", "Show last 100 log lines from app on servers"
@@ -105,7 +105,7 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
lines = options[:lines] lines = options[:lines]
grep = options[:grep] grep = options[:grep]
on(MRSK.config.hosts) do |host| on(MRSK.hosts) do |host|
begin begin
puts_by_host host, capture_with_info(*MRSK.app.logs(since: since, lines: lines, grep: grep)) puts_by_host host, capture_with_info(*MRSK.app.logs(since: since, lines: lines, grep: grep))
rescue SSHKit::Command::Failed rescue SSHKit::Command::Failed
@@ -119,12 +119,12 @@ class Mrsk::Cli::App < Mrsk::Cli::Base
def remove def remove
case options[:only] case options[:only]
when "containers" when "containers"
on(MRSK.config.hosts) { execute *MRSK.app.remove_containers } on(MRSK.hosts) { execute *MRSK.app.remove_containers }
when "images" when "images"
on(MRSK.config.hosts) { execute *MRSK.app.remove_images } on(MRSK.hosts) { execute *MRSK.app.remove_images }
else else
on(MRSK.config.hosts) { execute *MRSK.app.remove_containers } on(MRSK.hosts) { execute *MRSK.app.remove_containers }
on(MRSK.config.hosts) { execute *MRSK.app.remove_images } on(MRSK.hosts) { execute *MRSK.app.remove_images }
end end
end end
end end

View File

@@ -9,6 +9,9 @@ module Mrsk::Cli
class_option :verbose, type: :boolean, aliases: "-v", desc: "Detailed logging" class_option :verbose, type: :boolean, aliases: "-v", desc: "Detailed logging"
class_option :hosts, aliases: "-h", desc: "Run commands on these hosts instead of all (separate by comma)"
class_option :roles, aliases: "-r", desc: "Run commands on these roles instead of all (separate by comma)"
class_option :config_file, aliases: "-c", default: "config/deploy.yml", desc: "Path to config file (default: config/deploy.yml)" class_option :config_file, aliases: "-c", default: "config/deploy.yml", desc: "Path to config file (default: config/deploy.yml)"
class_option :destination, aliases: "-d", desc: "Specify destination to be used for config file (west -> deploy.west.yml)" class_option :destination, aliases: "-d", desc: "Specify destination to be used for config file (west -> deploy.west.yml)"
@@ -22,6 +25,8 @@ module Mrsk::Cli
MRSK.tap do |commander| MRSK.tap do |commander|
commander.config_file = Pathname.new(File.expand_path(options[:config_file])) commander.config_file = Pathname.new(File.expand_path(options[:config_file]))
commander.destination = options[:destination] commander.destination = options[:destination]
commander.hosts = options[:hosts]&.split(",")
commander.roles = options[:roles]&.split(",")
commander.verbose = options[:verbose] commander.verbose = options[:verbose]
end end
end end

View File

@@ -26,7 +26,7 @@ class Mrsk::Cli::Build < Mrsk::Cli::Base
desc "pull", "Pull app image from the registry onto servers" desc "pull", "Pull app image from the registry onto servers"
def pull def pull
on(MRSK.config.hosts) { execute *MRSK.builder.pull } on(MRSK.hosts) { execute *MRSK.builder.pull }
end end
desc "create", "Create a local build setup" desc "create", "Create a local build setup"

View File

@@ -32,7 +32,7 @@ class Mrsk::Cli::Main < Mrsk::Cli::Base
desc "rollback [VERSION]", "Rollback the app to VERSION (that must already be on servers)" desc "rollback [VERSION]", "Rollback the app to VERSION (that must already be on servers)"
def rollback(version) def rollback(version)
on(MRSK.config.hosts) do on(MRSK.hosts) do
execute *MRSK.app.stop, raise_on_non_zero_exit: false execute *MRSK.app.stop, raise_on_non_zero_exit: false
execute *MRSK.app.start(version: version) execute *MRSK.app.start(version: version)
end end

View File

@@ -9,11 +9,11 @@ class Mrsk::Cli::Prune < Mrsk::Cli::Base
desc "images", "Prune unused images older than 30 days" desc "images", "Prune unused images older than 30 days"
def images def images
on(MRSK.config.hosts) { execute *MRSK.prune.images } on(MRSK.hosts) { execute *MRSK.prune.images }
end end
desc "containers", "Prune stopped containers for the service older than 3 days" desc "containers", "Prune stopped containers for the service older than 3 days"
def containers def containers
on(MRSK.config.hosts) { execute *MRSK.prune.containers } on(MRSK.hosts) { execute *MRSK.prune.containers }
end end
end end

View File

@@ -4,14 +4,14 @@ class Mrsk::Cli::Registry < Mrsk::Cli::Base
desc "login", "Login to the registry locally and remotely" desc "login", "Login to the registry locally and remotely"
def login def login
run_locally { execute *MRSK.registry.login } run_locally { execute *MRSK.registry.login }
on(MRSK.config.hosts) { execute *MRSK.registry.login } on(MRSK.hosts) { execute *MRSK.registry.login }
rescue ArgumentError => e rescue ArgumentError => e
puts e.message puts e.message
end end
desc "logout", "Logout of the registry remotely" desc "logout", "Logout of the registry remotely"
def logout def logout
on(MRSK.config.hosts) { execute *MRSK.registry.logout } on(MRSK.hosts) { execute *MRSK.registry.logout }
rescue ArgumentError => e rescue ArgumentError => e
puts e.message puts e.message
end end

View File

@@ -3,6 +3,6 @@ require "mrsk/cli/base"
class Mrsk::Cli::Server < Mrsk::Cli::Base class Mrsk::Cli::Server < Mrsk::Cli::Base
desc "bootstrap", "Ensure Docker is installed on the servers" desc "bootstrap", "Ensure Docker is installed on the servers"
def bootstrap def bootstrap
on(MRSK.config.hosts) { execute "which docker || (apt-get update -y && apt-get install docker.io -y)" } on(MRSK.hosts) { execute "which docker || (apt-get update -y && apt-get install docker.io -y)" }
end end
end end

View File

@@ -3,17 +3,17 @@ require "mrsk/cli/base"
class Mrsk::Cli::Traefik < Mrsk::Cli::Base class Mrsk::Cli::Traefik < Mrsk::Cli::Base
desc "boot", "Boot Traefik on servers" desc "boot", "Boot Traefik on servers"
def boot def boot
on(MRSK.config.traefik_hosts) { execute *MRSK.traefik.run, raise_on_non_zero_exit: false } on(MRSK.traefik_hosts) { execute *MRSK.traefik.run, raise_on_non_zero_exit: false }
end end
desc "start", "Start existing Traefik on servers" desc "start", "Start existing Traefik on servers"
def start def start
on(MRSK.config.traefik_hosts) { execute *MRSK.traefik.start, raise_on_non_zero_exit: false } on(MRSK.traefik_hosts) { execute *MRSK.traefik.start, raise_on_non_zero_exit: false }
end end
desc "stop", "Stop Traefik on servers" desc "stop", "Stop Traefik on servers"
def stop def stop
on(MRSK.config.traefik_hosts) { execute *MRSK.traefik.stop, raise_on_non_zero_exit: false } on(MRSK.traefik_hosts) { execute *MRSK.traefik.stop, raise_on_non_zero_exit: false }
end end
desc "restart", "Restart Traefik on servers" desc "restart", "Restart Traefik on servers"
@@ -24,19 +24,19 @@ class Mrsk::Cli::Traefik < Mrsk::Cli::Base
desc "details", "Display details about Traefik containers from servers" desc "details", "Display details about Traefik containers from servers"
def details def details
on(MRSK.config.traefik_hosts) { |host| puts_by_host host, capture_with_info(*MRSK.traefik.info), type: "Traefik" } on(MRSK.traefik_hosts) { |host| puts_by_host host, capture_with_info(*MRSK.traefik.info), type: "Traefik" }
end end
desc "logs", "Show last 100 log lines from Traefik on servers" desc "logs", "Show last 100 log lines from Traefik on servers"
def logs def logs
on(MRSK.config.hosts) { |host| puts_by_host host, capture(*MRSK.traefik.logs), type: "Traefik" } on(MRSK.hosts) { |host| puts_by_host host, capture(*MRSK.traefik.logs), type: "Traefik" }
end end
desc "remove", "Remove Traefik container and image from servers" desc "remove", "Remove Traefik container and image from servers"
def remove def remove
invoke :stop invoke :stop
on(MRSK.config.traefik_hosts) do on(MRSK.traefik_hosts) do
execute *MRSK.traefik.remove_container execute *MRSK.traefik.remove_container
execute *MRSK.traefik.remove_image execute *MRSK.traefik.remove_image
end end

View File

@@ -16,6 +16,22 @@ class Mrsk::Commander
@config ||= Mrsk::Configuration.create_from(config_file, destination: destination).tap { |config| setup_with(config) } @config ||= Mrsk::Configuration.create_from(config_file, destination: destination).tap { |config| setup_with(config) }
end end
def hosts=(hosts)
@hosts = hosts if hosts.present?
end
def roles=(role_names)
@hosts = config.roles.select { |r| role_names.include?(r.name) }.flat_map(&:hosts) if role_names.present?
end
def hosts
@hosts || config.all_hosts
end
def traefik_hosts
@hosts || config.traefik_hosts
end
def app def app
@app ||= Mrsk::Commands::App.new(config) @app ||= Mrsk::Commands::App.new(config)

View File

@@ -48,25 +48,10 @@ class Mrsk::Configuration
roles.detect { |r| r.name == name.to_s } roles.detect { |r| r.name == name.to_s }
end end
def hosts def all_hosts
hosts =
case
when ENV["HOSTS"]
ENV["HOSTS"].split(",")
when ENV["ROLES"]
role_names = ENV["ROLES"].split(",")
roles.select { |r| role_names.include?(r.name) }.flat_map(&:hosts)
else
roles.flat_map(&:hosts) roles.flat_map(&:hosts)
end end
if hosts.any?
hosts
else
raise ArgumentError, "No hosts found"
end
end
def primary_host def primary_host
role(:web).hosts.first role(:web).hosts.first
end end

View File

@@ -3,10 +3,27 @@ require "mrsk/commander"
class CommanderTest < ActiveSupport::TestCase class CommanderTest < ActiveSupport::TestCase
setup do setup do
@mrsk = Mrsk::Commander.new config_file: Pathname.new(File.expand_path("fixtures/deploy.erb.yml", __dir__)) @mrsk = Mrsk::Commander.new config_file: Pathname.new(File.expand_path("fixtures/deploy_with_roles.yml", __dir__))
end end
test "lazy configuration" do test "lazy configuration" do
assert_equal Mrsk::Configuration, @mrsk.config.class assert_equal Mrsk::Configuration, @mrsk.config.class
end end
test "overwriting hosts" do
assert_equal [ "1.1.1.1", "1.1.1.2", "1.1.1.3", "1.1.1.4" ], @mrsk.hosts
@mrsk.hosts = [ "1.2.3.4", "1.2.3.5" ]
assert_equal [ "1.2.3.4", "1.2.3.5" ], @mrsk.hosts
end
test "overwriting hosts with roles" do
assert_equal [ "1.1.1.1", "1.1.1.2", "1.1.1.3", "1.1.1.4" ], @mrsk.hosts
@mrsk.roles = [ "workers", "web" ]
assert_equal [ "1.1.1.1", "1.1.1.2", "1.1.1.3", "1.1.1.4" ], @mrsk.hosts
@mrsk.roles = [ "workers" ]
assert_equal [ "1.1.1.3", "1.1.1.4" ], @mrsk.hosts
end
end end

View File

@@ -43,26 +43,9 @@ class ConfigurationTest < ActiveSupport::TestCase
assert_nil @config.role(:missing) assert_nil @config.role(:missing)
end end
test "hosts" do test "all hosts" do
assert_equal [ "1.1.1.1", "1.1.1.2"], @config.hosts assert_equal [ "1.1.1.1", "1.1.1.2"], @config.all_hosts
assert_equal [ "1.1.1.1", "1.1.1.2", "1.1.1.3", "1.1.1.4" ], @config_with_roles.hosts assert_equal [ "1.1.1.1", "1.1.1.2", "1.1.1.3", "1.1.1.4" ], @config_with_roles.all_hosts
end
test "hosts from ENV" do
ENV["HOSTS"] = "1.1.1.5,1.1.1.6"
assert_equal [ "1.1.1.5", "1.1.1.6"], @config.hosts
ensure
ENV["HOSTS"] = nil
end
test "hosts from ENV roles" do
ENV["ROLES"] = "web,workers"
assert_equal [ "1.1.1.1", "1.1.1.2", "1.1.1.3", "1.1.1.4" ], @config_with_roles.hosts
ENV["ROLES"] = "workers"
assert_equal [ "1.1.1.3", "1.1.1.4" ], @config_with_roles.hosts
ensure
ENV["ROLES"] = nil
end end
test "primary host" do test "primary host" do
@@ -127,10 +110,10 @@ class ConfigurationTest < ActiveSupport::TestCase
dest_config_file = Pathname.new(File.expand_path("fixtures/deploy_for_dest.yml", __dir__)) dest_config_file = Pathname.new(File.expand_path("fixtures/deploy_for_dest.yml", __dir__))
config = Mrsk::Configuration.create_from dest_config_file, destination: "world" config = Mrsk::Configuration.create_from dest_config_file, destination: "world"
assert_equal "1.1.1.1", config.hosts.first assert_equal "1.1.1.1", config.all_hosts.first
config = Mrsk::Configuration.create_from dest_config_file, destination: "mars" config = Mrsk::Configuration.create_from dest_config_file, destination: "mars"
assert_equal "1.1.1.3", config.hosts.first assert_equal "1.1.1.3", config.all_hosts.first
end end
test "destination yml config file missing" do test "destination yml config file missing" do

15
test/fixtures/deploy_with_roles.yml vendored Normal file
View File

@@ -0,0 +1,15 @@
service: app
image: dhh/app
servers:
web:
- 1.1.1.1
- 1.1.1.2
workers:
- 1.1.1.3
- 1.1.1.4
env:
REDIS_URL: redis://x/y
registry:
server: registry.digitalocean.com
username: user
password: pw