From c06585fef42a228f3e2c1b08db216a3c61fcec1b Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Tue, 28 Mar 2023 13:05:09 +0100 Subject: [PATCH] Daemon/host/role accessories Allow the hosts for accessories to be specified by host or role, or on all app hosts by setting `daemon: true`. ``` # Single host mysql: host: 1.1.1.1 # Multiple hosts redis: hosts: - 1.1.1.1 - 1.1.1.2 # By role monitoring: roles: - web - jobs ``` --- README.md | 21 ++++++- lib/mrsk/cli/accessory.rb | 68 +++++++++++++---------- lib/mrsk/commander.rb | 2 +- lib/mrsk/commands/accessory.rb | 9 +-- lib/mrsk/configuration/accessory.rb | 40 ++++++++++++- test/cli/accessory_test.rb | 6 +- test/cli/main_test.rb | 16 +++--- test/configuration/accessory_test.rb | 40 +++++++++++-- test/fixtures/deploy_simple.yml | 8 +++ test/fixtures/deploy_with_accessories.yml | 11 +++- 10 files changed, 165 insertions(+), 56 deletions(-) create mode 100644 test/fixtures/deploy_simple.yml diff --git a/README.md b/README.md index 3f4d15e7..032d4307 100644 --- a/README.md +++ b/README.md @@ -540,7 +540,8 @@ accessories: memory: "2GB" redis: image: redis:latest - host: 1.1.1.4 + role: + - web port: "36379:6379" volumes: - /var/lib/redis:/data @@ -550,6 +551,24 @@ accessories: port: 44444 ``` +The hosts that the accessories will run on can be specified by host, role or on all app hosts: + +```yaml + # Single host + mysql: + host: 1.1.1.1 + # Multiple hosts + redis: + hosts: + - 1.1.1.1 + - 1.1.1.2 + # By role + monitoring: + roles: + - web + - jobs +``` + Now run `mrsk accessory start mysql` to start the MySQL server on 1.1.1.3. See `mrsk accessory` for all the commands possible. Accessory images must be public or tagged in your private registry. diff --git a/lib/mrsk/cli/accessory.rb b/lib/mrsk/cli/accessory.rb index 258b4c8d..c34a3902 100644 --- a/lib/mrsk/cli/accessory.rb +++ b/lib/mrsk/cli/accessory.rb @@ -9,7 +9,7 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base directories(name) upload(name) - on(accessory.host) do + on(accessory.hosts) do execute *MRSK.registry.login execute *MRSK.auditor.record("Booted #{name} accessory"), verbosity: :debug execute *accessory.run @@ -25,7 +25,7 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base def upload(name) with_lock do with_accessory(name) do |accessory| - on(accessory.host) do + on(accessory.hosts) do accessory.files.each do |(local, remote)| accessory.ensure_local_file_present(local) @@ -42,7 +42,7 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base def directories(name) with_lock do with_accessory(name) do |accessory| - on(accessory.host) do + on(accessory.hosts) do accessory.directories.keys.each do |host_path| execute *accessory.make_directory(host_path) end @@ -66,7 +66,7 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base def start(name) with_lock do with_accessory(name) do |accessory| - on(accessory.host) do + on(accessory.hosts) do execute *MRSK.auditor.record("Started #{name} accessory"), verbosity: :debug execute *accessory.start end @@ -78,7 +78,7 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base def stop(name) with_lock do with_accessory(name) do |accessory| - on(accessory.host) do + on(accessory.hosts) do execute *MRSK.auditor.record("Stopped #{name} accessory"), verbosity: :debug execute *accessory.stop, raise_on_non_zero_exit: false end @@ -102,7 +102,7 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base MRSK.accessory_names.each { |accessory_name| details(accessory_name) } else with_accessory(name) do |accessory| - on(accessory.host) { puts capture_with_info(*accessory.info) } + on(accessory.hosts) { puts capture_with_info(*accessory.info) } end end end @@ -123,14 +123,14 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base when options[:reuse] say "Launching command from existing container...", :magenta - on(accessory.host) do + on(accessory.hosts) do execute *MRSK.auditor.record("Executed cmd '#{cmd}' on #{name} accessory"), verbosity: :debug capture_with_info(*accessory.execute_in_existing_container(cmd)) end else say "Launching command from new container...", :magenta - on(accessory.host) do + on(accessory.hosts) do execute *MRSK.auditor.record("Executed cmd '#{cmd}' on #{name} accessory"), verbosity: :debug capture_with_info(*accessory.execute_in_new_container(cmd)) end @@ -149,7 +149,7 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base if options[:follow] run_locally do - info "Following logs on #{accessory.host}..." + info "Following logs on #{accessory.hosts}..." info accessory.follow_logs(grep: grep) exec accessory.follow_logs(grep: grep) end @@ -157,7 +157,7 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base since = options[:since] lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set - on(accessory.host) do + on(accessory.hosts) do puts capture_with_info(*accessory.logs(since: since, lines: lines, grep: grep)) end end @@ -167,15 +167,17 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base desc "remove [NAME]", "Remove accessory container, image and data directory from host (use NAME=all to remove all accessories)" option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question" def remove(name) - if name == "all" - MRSK.accessory_names.each { |accessory_name| remove(accessory_name) } - else - if options[:confirmed] || ask("This will remove all containers, images and data directories for #{name}. Are you sure?", limited_to: %w( y N ), default: "N") == "y" - with_accessory(name) do - stop(name) - remove_container(name) - remove_image(name) - remove_service_directory(name) + with_lock do + if name == "all" + MRSK.accessory_names.each { |accessory_name| remove(accessory_name) } + else + if options[:confirmed] || ask("This will remove all containers, images and data directories for #{name}. Are you sure?", limited_to: %w( y N ), default: "N") == "y" + with_accessory(name) do + stop(name) + remove_container(name) + remove_image(name) + remove_service_directory(name) + end end end end @@ -183,29 +185,35 @@ class Mrsk::Cli::Accessory < Mrsk::Cli::Base desc "remove_container [NAME]", "Remove accessory container from host", hide: true def remove_container(name) - with_accessory(name) do |accessory| - on(accessory.host) do - execute *MRSK.auditor.record("Remove #{name} accessory container"), verbosity: :debug - execute *accessory.remove_container + with_lock do + with_accessory(name) do |accessory| + on(accessory.hosts) do + execute *MRSK.auditor.record("Remove #{name} accessory container"), verbosity: :debug + execute *accessory.remove_container + end end end end desc "remove_image [NAME]", "Remove accessory image from host", hide: true def remove_image(name) - with_accessory(name) do |accessory| - on(accessory.host) do - execute *MRSK.auditor.record("Removed #{name} accessory image"), verbosity: :debug - execute *accessory.remove_image + with_lock do + with_accessory(name) do |accessory| + on(accessory.hosts) do + execute *MRSK.auditor.record("Removed #{name} accessory image"), verbosity: :debug + execute *accessory.remove_image + end end end end desc "remove_service_directory [NAME]", "Remove accessory directory used for uploaded files and data directories from host", hide: true def remove_service_directory(name) - with_accessory(name) do |accessory| - on(accessory.host) do - execute *accessory.remove_service_directory + with_lock do + with_accessory(name) do |accessory| + on(accessory.hosts) do + execute *accessory.remove_service_directory + end end end end diff --git a/lib/mrsk/commander.rb b/lib/mrsk/commander.rb index b70357c6..9dc557af 100644 --- a/lib/mrsk/commander.rb +++ b/lib/mrsk/commander.rb @@ -59,7 +59,7 @@ class Mrsk::Commander end def accessory_hosts - specific_hosts || config.accessories.collect(&:host) + specific_hosts || config.accessories.flat_map(&:hosts) end def accessory_names diff --git a/lib/mrsk/commands/accessory.rb b/lib/mrsk/commands/accessory.rb index 529e6e32..9ea7c088 100644 --- a/lib/mrsk/commands/accessory.rb +++ b/lib/mrsk/commands/accessory.rb @@ -1,7 +1,7 @@ class Mrsk::Commands::Accessory < Mrsk::Commands::Base attr_reader :accessory_config - delegate :service_name, :image, :host, :port, :files, :directories, :publish_args, :env_args, :volume_args, - :label_args, :option_args, to: :accessory_config + delegate :service_name, :image, :hosts, :port, :files, :directories, :cmd, + :publish_args, :env_args, :volume_args, :label_args, :option_args, to: :accessory_config def initialize(config, name:) super(config) @@ -19,7 +19,8 @@ class Mrsk::Commands::Accessory < Mrsk::Commands::Base *volume_args, *label_args, *option_args, - image + image, + cmd end def start @@ -75,7 +76,7 @@ class Mrsk::Commands::Accessory < Mrsk::Commands::Base end def run_over_ssh(command) - super command, host: host + super command, host: hosts.first end diff --git a/lib/mrsk/configuration/accessory.rb b/lib/mrsk/configuration/accessory.rb index 73c8a7dc..595846a6 100644 --- a/lib/mrsk/configuration/accessory.rb +++ b/lib/mrsk/configuration/accessory.rb @@ -15,8 +15,12 @@ class Mrsk::Configuration::Accessory specifics["image"] end - def host - specifics["host"] || raise(ArgumentError, "Missing host for accessory") + def hosts + if (specifics.keys & ["host", "hosts", "roles"]).size != 1 + raise ArgumentError, "Specify one of `host`, `hosts` or `roles` for accessory `#{name}`" + end + + hosts_from_host || hosts_from_hosts || hosts_from_roles end def port @@ -75,6 +79,10 @@ class Mrsk::Configuration::Accessory end end + def cmd + specifics["cmd"] + end + private attr_accessor :config @@ -130,4 +138,32 @@ class Mrsk::Configuration::Accessory def service_data_directory "$PWD/#{service_name}" end + + def hosts_from_host + if specifics.key?("host") + host = specifics["host"] + if host + [host] + else + raise ArgumentError, "Missing host for accessory `#{name}`" + end + end + end + + def hosts_from_hosts + if specifics.key?("hosts") + hosts = specifics["hosts"] + if hosts.is_a?(Array) + hosts + else + raise ArgumentError, "Hosts should be an Array for accessory `#{name}`" + end + end + end + + def hosts_from_roles + if specifics.key?("roles") + specifics["roles"].flat_map { |role| config.role(role).hosts } + end + end end diff --git a/test/cli/accessory_test.rb b/test/cli/accessory_test.rb index a2e4d73d..28c4977f 100644 --- a/test/cli/accessory_test.rb +++ b/test/cli/accessory_test.rb @@ -19,9 +19,11 @@ class CliAccessoryTest < CliTestCase run_command("boot", "all").tap do |output| assert_match /docker login.*on 1.1.1.3/, output - assert_match /docker login.*on 1.1.1.4/, output + assert_match /docker login.*on 1.1.1.1/, output + assert_match /docker login.*on 1.1.1.2/, output assert_match "docker run --name app-mysql --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 3306:3306 -e [REDACTED] -e MYSQL_ROOT_HOST=\"%\" --volume $PWD/app-mysql/etc/mysql/my.cnf:/etc/mysql/my.cnf --volume $PWD/app-mysql/data:/var/lib/mysql --label service=\"app-mysql\" mysql:5.7 on 1.1.1.3", output - assert_match "docker run --name app-redis --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 6379:6379 --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.4", output + assert_match "docker run --name app-redis --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 6379:6379 --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.1", output + assert_match "docker run --name app-redis --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 6379:6379 --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.2", output end end diff --git a/test/cli/main_test.rb b/test/cli/main_test.rb index 3ebf9fb9..b27e5630 100644 --- a/test/cli/main_test.rb +++ b/test/cli/main_test.rb @@ -10,7 +10,7 @@ class CliMainTest < CliTestCase end test "deploy" do - invoke_options = { "config_file" => "test/fixtures/deploy_with_accessories.yml", "skip_broadcast" => false, "version" => "999" } + invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "skip_broadcast" => false, "version" => "999" } Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:server:bootstrap", [], invoke_options) Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:registry:login", [], invoke_options) @@ -31,7 +31,7 @@ class CliMainTest < CliTestCase end test "deploy with skip_push" do - invoke_options = { "config_file" => "test/fixtures/deploy_with_accessories.yml", "skip_broadcast" => false, "version" => "999" } + invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "skip_broadcast" => false, "version" => "999" } Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:server:bootstrap", [], invoke_options) Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:registry:login", [], invoke_options) @@ -54,7 +54,7 @@ class CliMainTest < CliTestCase end test "redeploy" do - invoke_options = { "config_file" => "test/fixtures/deploy_with_accessories.yml", "skip_broadcast" => false, "version" => "999" } + invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "skip_broadcast" => false, "version" => "999" } Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:build:deliver", [], invoke_options) Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:healthcheck:perform", [], invoke_options) @@ -67,7 +67,7 @@ class CliMainTest < CliTestCase end test "redeploy with skip_push" do - invoke_options = { "config_file" => "test/fixtures/deploy_with_accessories.yml", "skip_broadcast" => false, "version" => "999" } + invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "skip_broadcast" => false, "version" => "999" } Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:build:pull", [], invoke_options) Mrsk::Cli::Main.any_instance.expects(:invoke).with("mrsk:cli:healthcheck:perform", [], invoke_options) @@ -92,7 +92,7 @@ class CliMainTest < CliTestCase Mrsk::Cli::Main.any_instance.stubs(:container_name_available?).returns(true) SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info).with(:docker, :ps, "--filter", "label=service=app", "--format", "\"{{.Names}}\"", "|", "sed 's/-/\\n/g'", "|", "tail -n 1").returns("version-to-rollback\n").at_least_once - run_command("rollback", "123").tap do |output| + run_command("rollback", "123", config_file: "deploy_with_accessories").tap do |output| assert_match "Start version 123", output assert_match "docker start app-123", output assert_match "docker container ls --all --filter name=^app-version-to-rollback$ --quiet | xargs docker stop", output, "Should stop the container that was previously running" @@ -126,7 +126,7 @@ class CliMainTest < CliTestCase end test "config" do - run_command("config", config_file: "deploy_with_accessories").tap do |output| + run_command("config", config_file: "deploy_simple").tap do |output| config = YAML.load(output) assert_equal ["web"], config[:roles] @@ -224,7 +224,7 @@ class CliMainTest < CliTestCase end test "remove with confirmation" do - run_command("remove", "-y").tap do |output| + run_command("remove", "-y", config_file: "deploy_with_accessories").tap do |output| assert_match /docker container stop traefik/, output assert_match /docker container prune --force --filter label=org.opencontainers.image.title=Traefik/, output assert_match /docker image prune --all --force --filter label=org.opencontainers.image.title=Traefik/, output @@ -253,7 +253,7 @@ class CliMainTest < CliTestCase end private - def run_command(*command, config_file: "deploy_with_accessories") + def run_command(*command, config_file: "deploy_simple") stdouted { Mrsk::Cli::Main.start([*command, "-c", "test/fixtures/#{config_file}.yml"]) } end end diff --git a/test/configuration/accessory_test.rb b/test/configuration/accessory_test.rb index fd070949..205eebc6 100644 --- a/test/configuration/accessory_test.rb +++ b/test/configuration/accessory_test.rb @@ -4,7 +4,10 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase setup do @deploy = { service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, - servers: [ "1.1.1.1", "1.1.1.2" ], + 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" }, accessories: { "mysql" => { @@ -29,7 +32,7 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase }, "redis" => { "image" => "redis:latest", - "host" => "1.1.1.6", + "hosts" => [ "1.1.1.6", "1.1.1.7" ], "port" => "6379:6379", "labels" => { "cache" => true @@ -44,6 +47,21 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase "cpus" => 4, "memory" => "2GB" } + }, + "monitoring" => { + "image" => "monitoring:latest", + "roles" => [ "web" ], + "port" => "4321:4321", + "labels" => { + "cache" => true + }, + "env" => { + "STATSD_PORT" => "8126" + }, + "options" => { + "cpus" => 4, + "memory" => "2GB" + } } } } @@ -62,8 +80,9 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase end test "host" do - assert_equal "1.1.1.5", @config.accessory(:mysql).host - assert_equal "1.1.1.6", @config.accessory(:redis).host + assert_equal ["1.1.1.5"], @config.accessory(:mysql).hosts + assert_equal ["1.1.1.6", "1.1.1.7"], @config.accessory(:redis).hosts + assert_equal ["1.1.1.1", "1.1.1.2"], @config.accessory(:monitoring).hosts end test "missing host" do @@ -71,10 +90,21 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase @config = Mrsk::Configuration.new(@deploy) assert_raises(ArgumentError) do - @config.accessory(:mysql).host + @config.accessory(:mysql).hosts end end + test "setting host, hosts and roles" do + @deploy[:accessories]["mysql"]["hosts"] = true + @deploy[:accessories]["mysql"]["roles"] = true + @config = Mrsk::Configuration.new(@deploy) + + exception = assert_raises(ArgumentError) do + @config.accessory(:mysql).hosts + end + assert_equal "Specify one of `host`, `hosts` or `roles` for accessory `mysql`", exception.message + end + test "label args" do assert_equal ["--label", "service=\"app-mysql\""], @config.accessory(:mysql).label_args assert_equal ["--label", "service=\"app-redis\"", "--label", "cache=\"true\""], @config.accessory(:redis).label_args diff --git a/test/fixtures/deploy_simple.yml b/test/fixtures/deploy_simple.yml new file mode 100644 index 00000000..520c138e --- /dev/null +++ b/test/fixtures/deploy_simple.yml @@ -0,0 +1,8 @@ +service: app +image: dhh/app +servers: + - "1.1.1.1" + - "1.1.1.2" +registry: + username: user + password: pw diff --git a/test/fixtures/deploy_with_accessories.yml b/test/fixtures/deploy_with_accessories.yml index 4440a1f9..dd78e9f4 100644 --- a/test/fixtures/deploy_with_accessories.yml +++ b/test/fixtures/deploy_with_accessories.yml @@ -1,8 +1,12 @@ service: app image: dhh/app servers: - - 1.1.1.1 - - 1.1.1.2 + web: + - "1.1.1.1" + - "1.1.1.2" + workers: + - "1.1.1.3" + - "1.1.1.4" registry: username: user password: pw @@ -23,7 +27,8 @@ accessories: - data:/var/lib/mysql redis: image: redis:latest - host: 1.1.1.4 + roles: + - web port: 6379 directories: - data:/data