diff --git a/lib/kamal/cli/app/boot.rb b/lib/kamal/cli/app/boot.rb index ed7e2ed6..a3dec523 100644 --- a/lib/kamal/cli/app/boot.rb +++ b/lib/kamal/cli/app/boot.rb @@ -11,12 +11,12 @@ class Kamal::Cli::App::Boot end def run - old_version = old_version_renamed_if_clashing + old_version, old_app = old_version_renamed_if_clashing start_new_version if old_version - stop_old_version(old_version) + stop_old_version(old_version, old_app) end end @@ -41,7 +41,13 @@ class Kamal::Cli::App::Boot execute *app.rename_container(version: version, new_version: renamed_version) end - capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip.presence + [ role, *role.previous_roles ].each do |old_role| + old_app = KAMAL.app(role: old_role) + old_version = capture_with_info(*old_app.current_running_version, raise_on_non_zero_exit: false).strip.presence + return [ old_version, old_app ] if old_version + end + + nil end def start_new_version @@ -51,7 +57,7 @@ class Kamal::Cli::App::Boot Kamal::Cli::Healthcheck::Poller.wait_for_healthy(pause_after_ready: true) { capture_with_info(*app.status(version: version)) } end - def stop_old_version(version) + def stop_old_version(version, app) if uses_cord? cord = capture_with_info(*app.cord(version: version), raise_on_non_zero_exit: false).strip if cord.present? diff --git a/lib/kamal/commands/app.rb b/lib/kamal/commands/app.rb index 15992640..7f421dfd 100644 --- a/lib/kamal/commands/app.rb +++ b/lib/kamal/commands/app.rb @@ -3,7 +3,7 @@ class Kamal::Commands::App < Kamal::Commands::Base ACTIVE_DOCKER_STATUSES = [ :running, :restarting ] - attr_reader :role, :role + attr_reader :role def initialize(config, role: nil) super(config) diff --git a/lib/kamal/configuration.rb b/lib/kamal/configuration.rb index ec2c3f66..64c512c1 100644 --- a/lib/kamal/configuration.rb +++ b/lib/kamal/configuration.rb @@ -71,7 +71,9 @@ class Kamal::Configuration def roles - @roles ||= role_names.collect { |role_name| Role.new(role_name, config: self) } + @roles ||= role_names.collect do |role_name| + Role.new(role_name, config: self, specializations: role_specializations(role_name), primary: role_name == primary_role_name) + end end def role(name) @@ -325,6 +327,14 @@ class Kamal::Configuration raw_config.servers.is_a?(Array) ? [ "web" ] : raw_config.servers.keys.sort end + def role_specializations(name) + if servers.is_a?(Array) || servers[name].is_a?(Array) + {} + else + servers[name].except("hosts") + end + end + def git_version @git_version ||= if Kamal::Git.used? diff --git a/lib/kamal/configuration/role.rb b/lib/kamal/configuration/role.rb index c726c7b8..52405916 100644 --- a/lib/kamal/configuration/role.rb +++ b/lib/kamal/configuration/role.rb @@ -2,11 +2,11 @@ class Kamal::Configuration::Role CORD_FILE = "cord" delegate :argumentize, :optionize, to: Kamal::Utils - attr_accessor :name + attr_reader :name alias to_s name - def initialize(name, config:) - @name, @config = name.inquiry, config + def initialize(name, config:, specializations:, primary:) + @name, @config, @specializations, @primary = name.inquiry, config, specializations, primary end def primary_host @@ -98,7 +98,7 @@ class Kamal::Configuration::Role end def primary? - self == @config.primary_role + @primary end @@ -163,8 +163,14 @@ class Kamal::Configuration::Role File.join config.run_directory, "assets", "volumes", container_name(version) end + def previous_roles + previous_role_names.map do |role_name| + Kamal::Configuration::Role.new(role_name, config: config, specializations: specializations, primary: primary?) + end + end + private - attr_accessor :config + attr_reader :config, :specializations def extract_hosts_from_config if config.servers.is_a?(Array) @@ -207,14 +213,6 @@ class Kamal::Configuration::Role end end - def specializations - if config.servers.is_a?(Array) || config.servers[name].is_a?(Array) - {} - else - config.servers[name].except("hosts") - end - end - def specialized_env Kamal::Configuration::Env.from_config config: specializations.fetch("env", {}) end @@ -237,4 +235,8 @@ class Kamal::Configuration::Role options end end + + def previous_role_names + specializations.fetch("previously", []) + end end diff --git a/test/configuration/role_test.rb b/test/configuration/role_test.rb index 0952f1e8..a49ffe83 100644 --- a/test/configuration/role_test.rb +++ b/test/configuration/role_test.rb @@ -53,7 +53,8 @@ class ConfigurationRoleTest < ActiveSupport::TestCase test "custom labels via role specialization" do @deploy_with_roles[:labels] = { "my.custom.label" => "50" } @deploy_with_roles[:servers]["workers"]["labels"] = { "my.custom.label" => "70" } - assert_equal "70", @config_with_roles.role(:workers).labels["my.custom.label"] + config_with_roles = Kamal::Configuration.new(@deploy_with_roles) + assert_equal "70", config_with_roles.role(:workers).labels["my.custom.label"] end test "overwriting default traefik label" do @@ -109,6 +110,8 @@ class ConfigurationRoleTest < ActiveSupport::TestCase ] } + config_with_roles = Kamal::Configuration.new(@deploy_with_roles) + ENV["REDIS_PASSWORD"] = "secret456" ENV["DB_PASSWORD"] = "secret&\"123" @@ -117,8 +120,8 @@ class ConfigurationRoleTest < ActiveSupport::TestCase DB_PASSWORD=secret&\"123 ENV - assert_equal expected_secrets_file, @config_with_roles.role(:workers).env.secrets_io.string - assert_equal [ "--env-file", ".kamal/env/roles/app-workers.env", "--env", "REDIS_URL=\"redis://a/b\"", "--env", "WEB_CONCURRENCY=\"4\"" ], @config_with_roles.role(:workers).env_args + assert_equal expected_secrets_file, config_with_roles.role(:workers).env.secrets_io.string + assert_equal [ "--env-file", ".kamal/env/roles/app-workers.env", "--env", "REDIS_URL=\"redis://a/b\"", "--env", "WEB_CONCURRENCY=\"4\"" ], config_with_roles.role(:workers).env_args ensure ENV["REDIS_PASSWORD"] = nil ENV["DB_PASSWORD"] = nil @@ -135,14 +138,16 @@ class ConfigurationRoleTest < ActiveSupport::TestCase ] } + config_with_roles = Kamal::Configuration.new(@deploy_with_roles) + ENV["DB_PASSWORD"] = "secret123" expected_secrets_file = <<~ENV DB_PASSWORD=secret123 ENV - assert_equal expected_secrets_file, @config_with_roles.role(:workers).env.secrets_io.string - assert_equal [ "--env-file", ".kamal/env/roles/app-workers.env", "--env", "REDIS_URL=\"redis://a/b\"", "--env", "WEB_CONCURRENCY=\"4\"" ], @config_with_roles.role(:workers).env_args + assert_equal expected_secrets_file, config_with_roles.role(:workers).env.secrets_io.string + assert_equal [ "--env-file", ".kamal/env/roles/app-workers.env", "--env", "REDIS_URL=\"redis://a/b\"", "--env", "WEB_CONCURRENCY=\"4\"" ], config_with_roles.role(:workers).env_args ensure ENV["DB_PASSWORD"] = nil end @@ -185,14 +190,16 @@ class ConfigurationRoleTest < ActiveSupport::TestCase } } + config_with_roles = Kamal::Configuration.new(@deploy_with_roles) + ENV["REDIS_PASSWORD"] = "secret456" expected_secrets_file = <<~ENV REDIS_PASSWORD=secret456 ENV - assert_equal expected_secrets_file, @config_with_roles.role(:workers).env.secrets_io.string - assert_equal [ "--env-file", ".kamal/env/roles/app-workers.env", "--env", "REDIS_URL=\"redis://c/d\"" ], @config_with_roles.role(:workers).env_args + assert_equal expected_secrets_file, config_with_roles.role(:workers).env.secrets_io.string + assert_equal [ "--env-file", ".kamal/env/roles/app-workers.env", "--env", "REDIS_URL=\"redis://c/d\"" ], config_with_roles.role(:workers).env_args ensure ENV["REDIS_PASSWORD"] = nil end diff --git a/test/integration/docker/deployer/app_with_roles/config/deploy_renamed_roles.yml b/test/integration/docker/deployer/app_with_roles/config/deploy_renamed_roles.yml new file mode 100644 index 00000000..bb8e3524 --- /dev/null +++ b/test/integration/docker/deployer/app_with_roles/config/deploy_renamed_roles.yml @@ -0,0 +1,42 @@ +service: app +image: app +primary_role: app +servers: + app: + previously: + - web + hosts: + - vm1 + - vm2 + jobs: + previously: + - workers + hosts: + - vm3 + cmd: sleep infinity + +asset_path: /usr/share/nginx/html/versions + +registry: + server: registry:4443 + username: root + password: root +builder: + multiarch: false + args: + COMMIT_SHA: <%= `git rev-parse HEAD` %> +healthcheck: + cmd: wget -qO- http://localhost > /dev/null || exit 1 +traefik: + args: + accesslog: true + accesslog.format: json + image: registry:4443/traefik:v2.10 +accessories: + busybox: + service: custom-busybox + image: registry:4443/busybox:1.36.0 + cmd: sh -c 'echo "Starting busybox..."; trap exit term; while true; do sleep 1; done' + roles: + - app +stop_wait_time: 1 diff --git a/test/integration/docker/deployer/rename_roles.sh b/test/integration/docker/deployer/rename_roles.sh new file mode 100755 index 00000000..e6b1f56d --- /dev/null +++ b/test/integration/docker/deployer/rename_roles.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +cd $1 && cp -f config/deploy_renamed_roles.yml config/deploy.yml && git commit -am 'Rename roles' diff --git a/test/integration/integration_test.rb b/test/integration/integration_test.rb index bd62c474..25cca0e0 100644 --- a/test/integration/integration_test.rb +++ b/test/integration/integration_test.rb @@ -131,4 +131,16 @@ class IntegrationTest < ActiveSupport::TestCase puts "Tried to get the response code again and got #{app_response.code}" end end + + def assert_container_running(host:, name:) + assert container_running?(host: host, name: name) + end + + def assert_container_not_running(host:, name:) + assert_not container_running?(host: host, name: name) + end + + def container_running?(host:, name:) + docker_compose("exec #{host} docker ps --filter=name=#{name} | tail -n+2", capture: true).strip.present? + end end diff --git a/test/integration/main_test.rb b/test/integration/main_test.rb index ac549b97..8d05fcb1 100644 --- a/test/integration/main_test.rb +++ b/test/integration/main_test.rb @@ -92,6 +92,33 @@ class MainTest < IntegrationTest assert_no_images_or_containers end + test "rename roles" do + @app = "app_with_roles" + + kamal :envify + kamal :deploy + + first_version = latest_app_version + + assert_container_running host: :vm1, name: "app-web-#{first_version}" + assert_container_running host: :vm2, name: "app-web-#{first_version}" + assert_container_running host: :vm3, name: "app-workers-#{first_version}" + + rename_roles + + kamal :envify + kamal :deploy + + second_version = latest_app_version + + assert_container_running host: :vm1, name: "app-app-#{second_version}" + assert_container_running host: :vm2, name: "app-app-#{second_version}" + assert_container_running host: :vm3, name: "app-jobs-#{second_version}" + assert_container_not_running host: :vm1, name: "app-web-#{first_version}" + assert_container_not_running host: :vm2, name: "app-web-#{first_version}" + assert_container_not_running host: :vm3, name: "app-workers-#{first_version}" + end + private def assert_local_env_file(contents) assert_equal contents, deployer_exec("cat .env", capture: true) @@ -139,7 +166,7 @@ class MainTest < IntegrationTest assert vm1_container_ids.any? end - def assert_container_running(host:, name:) - assert docker_compose("exec #{host} docker ps --filter=name=#{name} -q", capture: true).strip.present? + def rename_roles + deployer_exec "./rename_roles.sh #{@app}", workdir: "/" end end