Asset paths
During deployments both the old and new containers will be active for a small period of time. There also may be lagging requests for older CSS and JS after the deployment. This can lead to 404s if a request for old assets hits a new container or visa-versa. This PR makes sure that both sets of assets are available throughout the deployment from before the new version of the app is booted. This can be configured by setting the asset path: ```yaml asset_path: "/rails/public/assets" ``` The process is: 1. We extract the assets out of the container, with docker run, docker cp, docker stop. Docker run sets the container command to "sleep" so this needs to be available in the container. 2. We create an asset volume directory on the host for the new version of the app on the host and copy the assets in there. 3. If there is a previous deployment we also copy the new assets into its asset volume and copy the older assets into the new asset volume. 4. We start the new container mapping the asset volume over the top of the container's asset path. This means the both the old and new versions have replaced the asset path with a volume containing both sets of assets and should be able to serve any request during the deployment. The older assets will continue to be available until the next deployment.
This commit is contained in:
@@ -55,8 +55,6 @@ class CliAppTest < CliTestCase
|
||||
end
|
||||
|
||||
test "boot errors leave lock in place" do
|
||||
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "version" => "999" }
|
||||
|
||||
Kamal::Cli::App.any_instance.expects(:using_version).raises(RuntimeError)
|
||||
|
||||
assert !KAMAL.holding_lock?
|
||||
@@ -66,6 +64,34 @@ class CliAppTest < CliTestCase
|
||||
assert KAMAL.holding_lock?
|
||||
end
|
||||
|
||||
test "boot with assets" do
|
||||
Object.any_instance.stubs(:sleep)
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", raise_on_non_zero_exit: false)
|
||||
.returns("12345678") # running version
|
||||
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'")
|
||||
.returns("running") # health check
|
||||
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||
.with(:docker, :ps, "--filter", "label=service=app", "--filter", "label=role=web", "--filter", "status=running", "--filter", "status=restarting", "--latest", "--format", "\"{{.Names}}\"", "|", "while read line; do echo ${line#app-web-}; done", raise_on_non_zero_exit: false)
|
||||
.returns("123").twice # old version
|
||||
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||
.with(:docker, :inspect, "-f '{{ range .Mounts }}{{printf \"%s %s\n\" .Source .Destination}}{{ end }}'", "app-web-123", "|", :awk, "'$2 == \"/tmp/kamal-cord\" {print $1}'", :raise_on_non_zero_exit => false)
|
||||
.returns("") # old version
|
||||
|
||||
run_command("boot", config: :with_assets).tap do |output|
|
||||
assert_match "docker tag dhh/app:latest dhh/app:latest", output
|
||||
assert_match "/usr/bin/env mkdir -p .kamal/assets/volumes/app-web-latest ; cp -rn .kamal/assets/extracted/app-web-latest/* .kamal/assets/volumes/app-web-latest ; cp -rn .kamal/assets/extracted/app-web-latest/* .kamal/assets/volumes/app-web-123 || true ; cp -rn .kamal/assets/extracted/app-web-123/* .kamal/assets/volumes/app-web-latest || true", output
|
||||
assert_match "/usr/bin/env mkdir -p .kamal/assets/extracted/app-web-latest && docker stop -t 1 app-web-assets 2> /dev/null || true && docker run --name app-web-assets --detach --rm dhh/app:latest sleep 1000000 && docker cp -L app-web-assets:/public/assets/. .kamal/assets/extracted/app-web-latest && docker stop -t 1 app-web-assets", output
|
||||
assert_match /docker run --detach --restart unless-stopped --name app-web-latest --hostname 1.1.1.1-[0-9a-f]{12} /, output
|
||||
assert_match "docker container ls --all --filter name=^app-web-123$ --quiet | xargs docker stop", output
|
||||
assert_match "/usr/bin/env find .kamal/assets/extracted -maxdepth 1 -name 'app-web-*' ! -name app-web-latest -exec rm -rf \"{}\" + ; find .kamal/assets/volumes -maxdepth 1 -name 'app-web-*' ! -name app-web-latest -exec rm -rf \"{}\" +", output
|
||||
end
|
||||
end
|
||||
|
||||
test "start" do
|
||||
run_command("start").tap do |output|
|
||||
assert_match "docker start app-web-999", output
|
||||
|
||||
@@ -345,8 +345,39 @@ class CommandsAppTest < ActiveSupport::TestCase
|
||||
assert_equal "rm -r corddir", new_command.cut_cord("corddir").join(" ")
|
||||
end
|
||||
|
||||
test "extract assets" do
|
||||
assert_equal [
|
||||
:mkdir, "-p", ".kamal/assets/extracted/app-web-999", "&&",
|
||||
:docker, :stop, "-t 1", "app-web-assets", "2> /dev/null", "|| true", "&&",
|
||||
:docker, :run, "--name", "app-web-assets", "--detach", "--rm", "dhh/app:latest", "sleep 1000000", "&&",
|
||||
:docker, :cp, "-L", "app-web-assets:/public/assets/.", ".kamal/assets/extracted/app-web-999", "&&",
|
||||
:docker, :stop, "-t 1", "app-web-assets"
|
||||
], new_command(asset_path: "/public/assets").extract_assets
|
||||
end
|
||||
|
||||
test "sync asset volumes" do
|
||||
assert_equal [
|
||||
:mkdir, "-p", ".kamal/assets/volumes/app-web-999", ";",
|
||||
:cp, "-rn", ".kamal/assets/extracted/app-web-999/*", ".kamal/assets/volumes/app-web-999"
|
||||
], new_command(asset_path: "/public/assets").sync_asset_volumes
|
||||
|
||||
assert_equal [
|
||||
:mkdir, "-p", ".kamal/assets/volumes/app-web-999", ";",
|
||||
:cp, "-rn", ".kamal/assets/extracted/app-web-999/*", ".kamal/assets/volumes/app-web-999", ";",
|
||||
:cp, "-rn", ".kamal/assets/extracted/app-web-999/*", ".kamal/assets/volumes/app-web-998", "|| true", ";",
|
||||
:cp, "-rn", ".kamal/assets/extracted/app-web-998/*", ".kamal/assets/volumes/app-web-999", "|| true",
|
||||
], new_command(asset_path: "/public/assets").sync_asset_volumes(old_version: 998)
|
||||
end
|
||||
|
||||
test "clean up assets" do
|
||||
assert_equal [
|
||||
:find, ".kamal/assets/extracted", "-maxdepth 1", "-name", "'app-web-*'", "!", "-name", "app-web-999", "-exec rm -rf \"{}\" +", ";",
|
||||
:find, ".kamal/assets/volumes", "-maxdepth 1", "-name", "'app-web-*'", "!", "-name", "app-web-999", "-exec rm -rf \"{}\" +"
|
||||
], new_command(asset_path: "/public/assets").clean_up_assets
|
||||
end
|
||||
|
||||
private
|
||||
def new_command(role: "web")
|
||||
Kamal::Commands::App.new(Kamal::Configuration.new(@config, destination: @destination, version: "999"), role: role)
|
||||
def new_command(role: "web", **additional_config)
|
||||
Kamal::Commands::App.new(Kamal::Configuration.new(@config.merge(additional_config), destination: @destination, version: "999"), role: role)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -80,6 +80,15 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
|
||||
assert_equal expected_env, @config_with_roles.role(:workers).env_file
|
||||
end
|
||||
|
||||
test "container name" do
|
||||
ENV["VERSION"] = "12345"
|
||||
|
||||
assert_equal "app-workers-12345", @config_with_roles.role(:workers).container_name
|
||||
assert_equal "app-web-12345", @config_with_roles.role(:web).container_name
|
||||
ensure
|
||||
ENV.delete("VERSION")
|
||||
end
|
||||
|
||||
test "env args" do
|
||||
assert_equal ["--env-file", ".kamal/env/roles/app-workers.env"], @config_with_roles.role(:workers).env_args
|
||||
end
|
||||
@@ -180,19 +189,67 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
|
||||
assert !@config_with_roles.role(:workers).uses_cord?
|
||||
end
|
||||
|
||||
test "cord host directory" do
|
||||
assert_match %r{\$\(pwd\)/.kamal/cords/app-web-[0-9a-f]{32}}, @config_with_roles.role(:web).cord_host_directory
|
||||
end
|
||||
|
||||
test "cord host file" do
|
||||
assert_match %r{\$\(pwd\)/.kamal/cords/app-web-[0-9a-f]{32}/cord}, @config_with_roles.role(:web).cord_host_file
|
||||
assert_match %r{.kamal/cords/app-web-[0-9a-f]{32}/cord}, @config_with_roles.role(:web).cord_host_file
|
||||
end
|
||||
|
||||
test "cord container directory" do
|
||||
assert_equal "/tmp/kamal-cord", @config_with_roles.role(:web).cord_container_directory
|
||||
test "cord volume" do
|
||||
assert_equal "/tmp/kamal-cord", @config_with_roles.role(:web).cord_volume.container_path
|
||||
assert_match %r{.kamal/cords/app-web-[0-9a-f]{32}}, @config_with_roles.role(:web).cord_volume.host_path
|
||||
assert_equal "--volume", @config_with_roles.role(:web).cord_volume.docker_args[0]
|
||||
assert_match %r{\$\(pwd\)/.kamal/cords/app-web-[0-9a-f]{32}:/tmp/kamal-cord}, @config_with_roles.role(:web).cord_volume.docker_args[1]
|
||||
end
|
||||
|
||||
test "cord container file" do
|
||||
assert_equal "/tmp/kamal-cord/cord", @config_with_roles.role(:web).cord_container_file
|
||||
end
|
||||
|
||||
test "asset path and volume args" do
|
||||
ENV["VERSION"] = "12345"
|
||||
assert_nil @config_with_roles.role(:web).asset_volume_args
|
||||
assert_nil @config_with_roles.role(:workers).asset_volume_args
|
||||
assert_nil @config_with_roles.role(:web).asset_path
|
||||
assert_nil @config_with_roles.role(:workers).asset_path
|
||||
assert !@config_with_roles.role(:web).assets?
|
||||
assert !@config_with_roles.role(:workers).assets?
|
||||
|
||||
config_with_assets = Kamal::Configuration.new(@deploy_with_roles.dup.tap { |c|
|
||||
c[:asset_path] = "foo"
|
||||
})
|
||||
assert_equal "foo", config_with_assets.role(:web).asset_path
|
||||
assert_equal "foo", config_with_assets.role(:workers).asset_path
|
||||
assert_equal ["--volume", "$(pwd)/.kamal/assets/volumes/app-web-12345:foo"], config_with_assets.role(:web).asset_volume_args
|
||||
assert_nil config_with_assets.role(:workers).asset_volume_args
|
||||
assert config_with_assets.role(:web).assets?
|
||||
assert !config_with_assets.role(:workers).assets?
|
||||
|
||||
config_with_assets = Kamal::Configuration.new(@deploy_with_roles.dup.tap { |c|
|
||||
c[:servers]["web"] = { "hosts" => [ "1.1.1.1", "1.1.1.2" ], "asset_path" => "bar" }
|
||||
})
|
||||
assert_equal "bar", config_with_assets.role(:web).asset_path
|
||||
assert_nil config_with_assets.role(:workers).asset_path
|
||||
assert_equal ["--volume", "$(pwd)/.kamal/assets/volumes/app-web-12345:bar"], config_with_assets.role(:web).asset_volume_args
|
||||
assert_nil config_with_assets.role(:workers).asset_volume_args
|
||||
assert config_with_assets.role(:web).assets?
|
||||
assert !config_with_assets.role(:workers).assets?
|
||||
|
||||
ensure
|
||||
ENV.delete("VERSION")
|
||||
end
|
||||
|
||||
test "asset extracted path" do
|
||||
ENV["VERSION"] = "12345"
|
||||
assert_equal ".kamal/assets/extracted/app-web-12345", @config_with_roles.role(:web).asset_extracted_path
|
||||
assert_equal ".kamal/assets/extracted/app-workers-12345", @config_with_roles.role(:workers).asset_extracted_path
|
||||
ensure
|
||||
ENV.delete("VERSION")
|
||||
end
|
||||
|
||||
test "asset volume path" do
|
||||
ENV["VERSION"] = "12345"
|
||||
assert_equal ".kamal/assets/volumes/app-web-12345", @config_with_roles.role(:web).asset_volume_path
|
||||
assert_equal ".kamal/assets/volumes/app-workers-12345", @config_with_roles.role(:workers).asset_volume_path
|
||||
ensure
|
||||
ENV.delete("VERSION")
|
||||
end
|
||||
end
|
||||
|
||||
13
test/configuration/volume_test.rb
Normal file
13
test/configuration/volume_test.rb
Normal file
@@ -0,0 +1,13 @@
|
||||
require "test_helper"
|
||||
|
||||
class ConfigurationVolumeTest < ActiveSupport::TestCase
|
||||
test "docker args absolute" do
|
||||
volume = Kamal::Configuration::Volume.new(host_path: "/root/foo/bar", container_path: "/assets")
|
||||
assert_equal ["--volume", "/root/foo/bar:/assets"], volume.docker_args
|
||||
end
|
||||
|
||||
test "docker args relative" do
|
||||
volume = Kamal::Configuration::Volume.new(host_path: "foo/bar", container_path: "/assets")
|
||||
assert_equal ["--volume", "$(pwd)/foo/bar:/assets"], volume.docker_args
|
||||
end
|
||||
end
|
||||
@@ -265,4 +265,9 @@ class ConfigurationTest < ActiveSupport::TestCase
|
||||
SecureRandom.expects(:hex).with(16).returns("09876543211234567890098765432112")
|
||||
assert_equal "09876543211234567890098765432112", @config.run_id
|
||||
end
|
||||
|
||||
test "asset path" do
|
||||
assert_nil @config.asset_path
|
||||
assert_equal "foo", Kamal::Configuration.new(@deploy.merge!(asset_path: "foo")).asset_path
|
||||
end
|
||||
end
|
||||
|
||||
9
test/fixtures/deploy_with_assets.yml
vendored
Normal file
9
test/fixtures/deploy_with_assets.yml
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
service: app
|
||||
image: dhh/app
|
||||
servers:
|
||||
- "1.1.1.1"
|
||||
- "1.1.1.2"
|
||||
registry:
|
||||
username: user
|
||||
password: pw
|
||||
asset_path: /public/assets
|
||||
@@ -4,4 +4,5 @@ COPY default.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
ARG COMMIT_SHA
|
||||
RUN echo $COMMIT_SHA > /usr/share/nginx/html/version
|
||||
RUN mkdir -p /usr/share/nginx/html/versions && echo "version" > /usr/share/nginx/html/versions/$COMMIT_SHA
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ env:
|
||||
CLEAR_TOKEN: '4321'
|
||||
secret:
|
||||
- SECRET_TOKEN
|
||||
asset_path: /usr/share/nginx/html/versions
|
||||
|
||||
registry:
|
||||
server: registry:4443
|
||||
|
||||
@@ -20,6 +20,8 @@ class MainTest < IntegrationTest
|
||||
assert_app_is_up version: second_version
|
||||
assert_hooks_ran "pre-connect", "pre-build", "pre-deploy", "post-deploy"
|
||||
|
||||
assert_accumulated_assets first_version, second_version
|
||||
|
||||
kamal :rollback, first_version
|
||||
assert_hooks_ran "pre-connect", "pre-deploy", "post-deploy"
|
||||
assert_app_is_up version: first_version
|
||||
@@ -69,4 +71,10 @@ class MainTest < IntegrationTest
|
||||
def assert_no_remote_env_file
|
||||
assert_equal "nofile", docker_compose("exec vm1 stat /root/.kamal/env/roles/app-web.env 2> /dev/null || echo nofile", capture: true)
|
||||
end
|
||||
|
||||
def assert_accumulated_assets(*versions)
|
||||
versions.each do |version|
|
||||
assert_equal "200", Net::HTTP.get_response(URI.parse("http://localhost:12345/versions/#{version}")).code
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user