Compare commits

..

41 Commits

Author SHA1 Message Date
Donal McBreen
8c32e6af07 Bump version for 2.0.0 2024-09-26 15:34:24 -04:00
Donal McBreen
a765c501a3 Bump version for 2.0.0.rc4 2024-09-26 07:06:51 -04:00
Donal McBreen
ae990efd02 Merge pull request #978 from basecamp/ignore-ssl-false
Handle ssl: false in proxy config
2024-09-26 11:56:00 +01:00
Donal McBreen
b3a6921118 Handle ssl: false in proxy config
Fixes: https://github.com/basecamp/kamal/issues/956
2024-09-26 06:17:45 -04:00
Donal McBreen
325bf9a797 Merge pull request #975 from basecamp/kamal-proxy-0.6.0
Bump to latest version of kamal-proxy
2024-09-25 23:03:25 +01:00
Donal McBreen
7bdf6cd2e8 Bump to latest version of kamal-proxy 2024-09-25 17:54:38 -04:00
Donal McBreen
7633fe0293 Merge pull request #974 from basecamp/proxy-boot-config
Proxy boot config
2024-09-25 20:28:24 +01:00
Donal McBreen
f6851048a6 Proxy boot config
Add commands for managing proxy boot config. Since the proxy can be
shared by multiple applications, the configuration doesn't belong in
`config/deploy.yml`.

Instead you can set the config with:

```
Usage:
  kamal proxy boot_config <set|get|clear>

Options:
      [--publish], [--no-publish], [--skip-publish]   # Publish the proxy ports on the host
                                                      # Default: true
      [--http-port=N]                                 # HTTP port to publish on the host
                                                      # Default: 80
      [--https-port=N]                                # HTTPS port to publish on the host
                                                      # Default: 443
      [--docker-options=option=value option2=value2]  # Docker options to pass to the proxy container
```

By default we boot the proxy with `--publish 80:80 --publish 443:443`.

You can stop it from publishing ports, specify different ports and pass
other docker options.

The config is stored in `.kamal/proxy/options` as arguments to be passed
verbatim to docker run.

Where someone wants to set the options in their application they can do
that by calling `kamal proxy boot_config set` in a pre-deploy hook.

There's an example in the integration tests showing how to use this to
front kamal-proxy with Traefik, using an accessory.
2024-09-25 15:15:26 -04:00
Donal McBreen
f0d7f786fa Traefik should be kamal-proxy in reboot hooks 2024-09-25 14:51:22 -04:00
Donal McBreen
4d8387b1c9 Merge pull request #973 from eroluysal/main
Fix adapter names
2024-09-25 19:48:40 +01:00
eroluysal
0258ac4297 Fix adapter names. 2024-09-25 21:22:59 +03:00
David Heinemeier Hansson
4a13803119 Bump version for 2.0.0.rc3 2024-09-23 16:48:07 -07:00
David Heinemeier Hansson
bda252835b Merge pull request #966 from basecamp/cleanup-default-templates
Bring default templates up to par with what Rails generates
2024-09-24 01:46:09 +02:00
David Heinemeier Hansson
0f5dfa204f Rearrange one last time 2024-09-23 16:44:54 -07:00
David Heinemeier Hansson
9dde204480 Rearange 2024-09-23 16:30:16 -07:00
David Heinemeier Hansson
b6cd4f8070 Bring default templates up to par with what Rails generates 2024-09-23 14:41:31 -07:00
David Heinemeier Hansson
e71bfcbadd Bump version for 2.0.0.rc2 2024-09-20 15:41:26 -07:00
David Heinemeier Hansson
567309596a Make the skip of timestamps a boolean 2024-09-20 12:50:46 -07:00
David Heinemeier Hansson
b89ec2bf63 Bump version for 2.0.0.rc1 2024-09-20 11:08:45 -07:00
David Heinemeier Hansson
3172adca30 Merge pull request #958 from basecamp/optional-timestamps
Add option to skip timestamps on logging output
2024-09-20 18:01:14 +02:00
David Heinemeier Hansson
04d21f45bb Fix test 2024-09-20 08:45:40 -07:00
David Heinemeier Hansson
eabd57350c Fix tests 2024-09-20 08:33:14 -07:00
David Heinemeier Hansson
487f6f5f53 Fix excess spacing 2024-09-20 08:31:56 -07:00
David Heinemeier Hansson
d98500982d Update tests 2024-09-20 08:19:38 -07:00
David Heinemeier Hansson
8693e968c1 Timestamps now default on for app logs too 2024-09-20 08:17:19 -07:00
David Heinemeier Hansson
6ab5fc9459 Allow timestamps on/off for app logging too 2024-09-20 08:04:28 -07:00
David Heinemeier Hansson
6fc2915884 Merge branch 'main' into optional-timestamps 2024-09-20 07:58:49 -07:00
David Heinemeier Hansson
afa6898a82 Fix pipe 2024-09-20 07:58:38 -07:00
David Heinemeier Hansson
384b36d158 Add option to skip timestamps on logging output
So it is easier to follow live when you are doing debugging, especially
early days app setup when you are the only user.
2024-09-20 07:42:31 -07:00
Donal McBreen
6df169a4fb Doc updates 2024-09-20 15:27:10 +01:00
Donal McBreen
ab109afc52 Merge pull request #957 from basecamp/numeric-timeouts
Response timeout should be a number
2024-09-20 09:50:05 +01:00
Donal McBreen
a6a48c456c Response timeout should be a number
Kamal will append the `s` for the duration when talking to kamal-proxy
so no need to have it in the config.
2024-09-20 09:26:06 +01:00
David Heinemeier Hansson
a4e5dbe5d4 Bump version for 2.0.0.beta2 2024-09-19 11:37:22 -07:00
Donal McBreen
56e90906b1 Merge pull request #954 from basecamp/two-app-integration-test
Integration test two apps
2024-09-19 16:36:45 +01:00
Donal McBreen
6e65968bdc Integration test two apps
Use localhost for app_with_roles and 127.0.0.1 for app. Confirm we can
deploy both and the respond to requests. Ensure the proxy is removed
once both have been removed.
2024-09-19 16:25:09 +01:00
Donal McBreen
85f1e14b97 Merge pull request #953 from basecamp/inherit-env-for-secrets
Avoid setting env via SSHKit
2024-09-19 15:18:31 +01:00
Donal McBreen
2c829a4824 Avoid setting env via SSHKit
SSHKit puts the env in the command, so leaks them in process listings.
2024-09-19 15:09:17 +01:00
Donal McBreen
45a58f7e15 Merge pull request #952 from basecamp/app-exec-in-kamal-network
Run app exec in the kamal network
2024-09-19 14:46:58 +01:00
Donal McBreen
834b343ded Run app exec in the kamal network
All other containers run in the kamal network, so let's add app exec-ed
containers as well.
2024-09-19 14:29:33 +01:00
Donal McBreen
9fe1821cae Merge pull request #951 from basecamp/proxy-config-ownership
Fix /home/kamal-proxy/.config/kamal-proxy ownership
2024-09-19 12:57:10 +01:00
Donal McBreen
1d7c9fec1d Fix /home/kamal-proxy/.config/kamal-proxy ownership
1. Update to kamal-proxy 0.4.0 which creates and chowns
/home/kamal-proxy/.config/kamal-proxy to kamal-proxy
2. Use a docker volume rather than mapping in a directory, so docker
keeps it owned by the correct user
2024-09-19 12:25:57 +01:00
49 changed files with 518 additions and 266 deletions

View File

@@ -1,7 +1,7 @@
PATH
remote: .
specs:
kamal (2.0.0.beta1)
kamal (2.0.0)
activesupport (>= 7.0)
base64 (~> 0.2)
bcrypt_pbkdf (~> 1.0)

View File

@@ -147,23 +147,25 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)"
option :grep_options, aliases: "-o", desc: "Additional options supplied to grep"
option :follow, aliases: "-f", desc: "Follow logs on primary server (or specific host set by --hosts)"
option :skip_timestamps, type: :boolean, aliases: "-T", desc: "Skip appending timestamps to logging output"
def logs(name)
with_accessory(name) do |accessory, hosts|
grep = options[:grep]
grep_options = options[:grep_options]
timestamps = !options[:skip_timestamps]
if options[:follow]
run_locally do
info "Following logs on #{hosts}..."
info accessory.follow_logs(grep: grep, grep_options: grep_options)
exec accessory.follow_logs(grep: grep, grep_options: grep_options)
info accessory.follow_logs(timestamps: timestamps, grep: grep, grep_options: grep_options)
exec accessory.follow_logs(timestamps: timestamps, grep: grep, grep_options: grep_options)
end
else
since = options[:since]
lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set
on(hosts) do
puts capture_with_info(*accessory.logs(since: since, lines: lines, grep: grep, grep_options: grep_options))
puts capture_with_info(*accessory.logs(timestamps: timestamps, since: since, lines: lines, grep: grep, grep_options: grep_options))
end
end
end

View File

@@ -188,12 +188,14 @@ class Kamal::Cli::App < Kamal::Cli::Base
option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)"
option :grep_options, aliases: "-o", desc: "Additional options supplied to grep"
option :follow, aliases: "-f", desc: "Follow log on primary server (or specific host set by --hosts)"
option :skip_timestamps, type: :boolean, aliases: "-T", desc: "Skip appending timestamps to logging output"
def logs
# FIXME: Catch when app containers aren't running
grep = options[:grep]
grep_options = options[:grep_options]
since = options[:since]
timestamps = !options[:skip_timestamps]
if options[:follow]
lines = options[:lines].presence || ((since || grep) ? nil : 10) # Default to 10 lines if since or grep isn't set
@@ -205,8 +207,8 @@ class Kamal::Cli::App < Kamal::Cli::Base
role = KAMAL.roles_on(KAMAL.primary_host).first
app = KAMAL.app(role: role, host: host)
info app.follow_logs(host: KAMAL.primary_host, lines: lines, grep: grep, grep_options: grep_options)
exec app.follow_logs(host: KAMAL.primary_host, lines: lines, grep: grep, grep_options: grep_options)
info app.follow_logs(host: KAMAL.primary_host, timestamps: timestamps, lines: lines, grep: grep, grep_options: grep_options)
exec app.follow_logs(host: KAMAL.primary_host, timestamps: timestamps, lines: lines, grep: grep, grep_options: grep_options)
end
else
lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set
@@ -216,7 +218,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
roles.each do |role|
begin
puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).logs(since: since, lines: lines, grep: grep, grep_options: grep_options))
puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).logs(timestamps: timestamps, since: since, lines: lines, grep: grep, grep_options: grep_options))
rescue SSHKit::Command::Failed
puts_by_host host, "Nothing found"
end

View File

@@ -135,8 +135,10 @@ module Kamal::Cli
details = { hosts: KAMAL.hosts.join(","), command: command, subcommand: subcommand }
say "Running the #{hook} hook...", :magenta
run_locally do
execute *KAMAL.hook.run(hook, **details, **extra_details)
with_env KAMAL.hook.env(**details, **extra_details) do
run_locally do
execute *KAMAL.hook.run(hook)
end
rescue SSHKit::Command::Failed => e
raise HookError.new("Hook `#{hook}` failed:\n#{e.message}")
end
@@ -183,5 +185,14 @@ module Kamal::Cli
execute(*KAMAL.server.ensure_run_directory)
end
end
def with_env(env)
current_env = ENV.to_h.dup
ENV.update(env)
yield
ensure
ENV.clear
ENV.update(current_env)
end
end
end

View File

@@ -30,28 +30,30 @@ class Kamal::Cli::Build < Kamal::Cli::Base
say "Building with uncommitted changes:\n #{uncommitted_changes}", :yellow
end
run_locally do
begin
execute *KAMAL.builder.inspect_builder
rescue SSHKit::Command::Failed => e
if e.message =~ /(context not found|no builder|no compatible builder|does not exist)/
warn "Missing compatible builder, so creating a new one first"
begin
cli.remove
rescue SSHKit::Command::Failed
raise unless e.message =~ /(context not found|no builder|does not exist)/
with_env(KAMAL.config.builder.secrets) do
run_locally do
begin
execute *KAMAL.builder.inspect_builder
rescue SSHKit::Command::Failed => e
if e.message =~ /(context not found|no builder|no compatible builder|does not exist)/
warn "Missing compatible builder, so creating a new one first"
begin
cli.remove
rescue SSHKit::Command::Failed
raise unless e.message =~ /(context not found|no builder|does not exist)/
end
cli.create
else
raise
end
cli.create
else
raise
end
end
# Get the command here to ensure the Dir.chdir doesn't interfere with it
push = KAMAL.builder.push
# Get the command here to ensure the Dir.chdir doesn't interfere with it
push = KAMAL.builder.push
KAMAL.with_verbosity(:debug) do
Dir.chdir(KAMAL.config.builder.build_directory) { execute *push, env: KAMAL.config.builder.secrets }
KAMAL.with_verbosity(:debug) do
Dir.chdir(KAMAL.config.builder.build_directory) { execute *push }
end
end
end
end

View File

@@ -48,7 +48,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base
end
end
run_hook "post-deploy", secrets: true, runtime: runtime.round
run_hook "post-deploy", secrets: true, runtime: runtime.round.to_s
end
desc "redeploy", "Deploy app to servers without bootstrapping servers, starting kamal-proxy, pruning, and registry login"
@@ -75,7 +75,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base
end
end
run_hook "post-deploy", secrets: true, runtime: runtime.round
run_hook "post-deploy", secrets: true, runtime: runtime.round.to_s
end
desc "rollback [VERSION]", "Rollback app to VERSION"
@@ -99,7 +99,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base
end
end
run_hook "post-deploy", secrets: true, runtime: runtime.round if rolled_back
run_hook "post-deploy", secrets: true, runtime: runtime.round.to_s if rolled_back
end
desc "details", "Show details about all containers"

View File

@@ -21,6 +21,36 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
end
end
desc "boot_config <set|get|clear>", "Mange kamal-proxy boot configuration"
option :publish, type: :boolean, default: true, desc: "Publish the proxy ports on the host"
option :http_port, type: :numeric, default: Kamal::Configuration::PROXY_HTTP_PORT, desc: "HTTP port to publish on the host"
option :https_port, type: :numeric, default: Kamal::Configuration::PROXY_HTTPS_PORT, desc: "HTTPS port to publish on the host"
option :docker_options, type: :array, default: [], desc: "Docker options to pass to the proxy container", banner: "option=value option2=value2"
def boot_config(subcommand)
case subcommand
when "set"
boot_options = [
*(KAMAL.config.proxy_publish_args(options[:http_port], options[:https_port]) if options[:publish]),
*options[:docker_options].map { |option| "--#{option}" }
]
on(KAMAL.proxy_hosts) do |host|
execute(*KAMAL.proxy.ensure_proxy_directory)
upload! StringIO.new(boot_options.join(" ")), KAMAL.config.proxy_options_file
end
when "get"
on(KAMAL.proxy_hosts) do |host|
puts "Host #{host}: #{capture_with_info(*KAMAL.proxy.get_boot_options)}"
end
when "reset"
on(KAMAL.proxy_hosts) do |host|
execute *KAMAL.proxy.reset_boot_options
end
else
raise ArgumentError, "Unknown boot_config subcommand #{subcommand}"
end
end
desc "reboot", "Reboot proxy on servers (stop container, remove container, start new container)"
option :rolling, type: :boolean, default: false, desc: "Reboot proxy on hosts in sequence, rather than in parallel"
option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
@@ -140,21 +170,23 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
option :lines, type: :numeric, aliases: "-n", desc: "Number of log lines to pull from each server"
option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)"
option :follow, aliases: "-f", desc: "Follow logs on primary server (or specific host set by --hosts)"
option :skip_timestamps, type: :boolean, aliases: "-T", desc: "Skip appending timestamps to logging output"
def logs
grep = options[:grep]
timestamps = !options[:skip_timestamps]
if options[:follow]
run_locally do
info "Following logs on #{KAMAL.primary_host}..."
info KAMAL.proxy.follow_logs(host: KAMAL.primary_host, grep: grep)
exec KAMAL.proxy.follow_logs(host: KAMAL.primary_host, grep: grep)
info KAMAL.proxy.follow_logs(host: KAMAL.primary_host, timestamps: timestamps, grep: grep)
exec KAMAL.proxy.follow_logs(host: KAMAL.primary_host, timestamps: timestamps, grep: grep)
end
else
since = options[:since]
lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set
on(KAMAL.proxy_hosts) do |host|
puts_by_host host, capture(*KAMAL.proxy.logs(since: since, lines: lines, grep: grep)), type: "Proxy"
puts_by_host host, capture(*KAMAL.proxy.logs(timestamps: timestamps, since: since, lines: lines, grep: grep)), type: "Proxy"
end
end
end
@@ -167,7 +199,7 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
stop
remove_container
remove_image
remove_host_directory
remove_proxy_directory
end
end
end
@@ -192,12 +224,11 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
end
end
desc "remove_host_directory", "Remove proxy directory from servers", hide: true
def remove_host_directory
desc "remove_proxy_directory", "Remove the proxy directory from servers", hide: true
def remove_proxy_directory
with_lock do
on(KAMAL.proxy_hosts) do
execute *KAMAL.auditor.record("Removed #{KAMAL.config.proxy_directory}"), verbosity: :debug
execute *KAMAL.proxy.remove_host_directory, raise_on_non_zero_exit: false
execute *KAMAL.proxy.remove_proxy_directory, raise_on_non_zero_exit: false
end
end
end

View File

@@ -2,11 +2,22 @@
service: my-app
# Name of the container image.
image: user/my-app
image: my-user/my-app
# Deploy to these servers.
servers:
- 192.168.0.1
web:
- 192.168.0.1
# job:
# hosts:
# - 192.168.0.1
# cmd: bin/jobs
# Enable SSL auto certification via Let's Encrypt (and allow for multiple apps on one server).
# Set ssl: false if using something like Cloudflare to terminate SSL (but keep host!).
proxy:
ssl: true
host: app.example.com
# Credentials for your image host.
registry:
@@ -14,7 +25,7 @@ registry:
# server: registry.digitalocean.com / ghcr.io / ...
username: my-user
# Always use an access token rather than real password when possible.
# Always use an access token rather than real password (pulled from .kamal/secrets).
password:
- KAMAL_REGISTRY_PASSWORD
@@ -22,19 +33,44 @@ registry:
builder:
arch: amd64
# Inject ENV variables into containers (secrets come from .env).
# Remember to run `kamal env push` after making changes!
# Inject ENV variables into containers (secrets come from .kamal/secrets).
#
# env:
# clear:
# DB_HOST: 192.168.0.2
# secret:
# - RAILS_MASTER_KEY
# Aliases are triggered with "bin/kamal <alias>". You can overwrite arguments on invocation:
# "bin/kamal logs -r job" will tail logs from the first server in the job section.
#
# aliases:
# shell: app exec --interactive --reuse "bash"
# Use a different ssh user than root
#
# ssh:
# user: app
# Use accessory services (secrets come from .env).
# Use a persistent storage volume.
#
# volumes:
# - "app_storage:/app/storage"
# Bridge fingerprinted assets, like JS and CSS, between versions to avoid
# hitting 404 on in-flight requests. Combines all files from new and old
# version inside the asset_path.
#
# asset_path: /app/public/assets
# Configure rolling deploys by setting a wait time between batches of restarts.
#
# boot:
# limit: 10 # Can also specify as a percentage of total hosts, such as "25%"
# wait: 2
# Use accessory services (secrets come from .kamal/secrets).
#
# accessories:
# db:
# image: mysql:8.0
@@ -56,29 +92,3 @@ builder:
# port: 6379
# directories:
# - data:/data
# Bridge fingerprinted assets, like JS and CSS, between versions to avoid
# hitting 404 on in-flight requests. Combines all files from new and old
# version inside the asset_path.
#
# If your app is using the Sprockets gem, ensure it sets `config.assets.manifest`.
# See https://github.com/basecamp/kamal/issues/626 for details
#
# asset_path: /rails/public/assets
# Configure rolling deploys by setting a wait time between batches of restarts.
# boot:
# limit: 10 # Can also specify as a percentage of total hosts, such as "25%"
# wait: 2
# Configure the role used to determine the primary_host. This host takes
# deploy locks, runs health checks during the deploy, and follow logs, etc.
#
# Caution: there's no support for role renaming yet, so be careful to cleanup
# the previous role on the deployed hosts.
# primary_role: web
# Controls if we abort when see a role with no hosts. Disabling this may be
# useful for more complex deploy configurations.
#
# allow_empty_roles: false

View File

@@ -1,3 +1,3 @@
#!/bin/sh
echo "Rebooting Traefik on $KAMAL_HOSTS..."
echo "Rebooting kamal-proxy on $KAMAL_HOSTS..."

View File

@@ -1,5 +1,6 @@
# WARNING: Avoid adding secrets directly to this file
# If you must, then add `.kamal/secrets*` to your .gitignore file
# Secrets defined here are available for reference under registry/password, env/secret, builder/secrets,
# and accessories/*/env/secret in config/deploy.yml. All secrets should be pulled from either
# password manager, ENV, or a file. DO NOT ENTER RAW CREDENTIALS HERE! This file needs to be safe for git.
# Option 1: Read secrets from the environment
KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD

View File

@@ -39,16 +39,16 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base
end
def logs(since: nil, lines: nil, grep: nil, grep_options: nil)
def logs(timestamps: true, since: nil, lines: nil, grep: nil, grep_options: nil)
pipe \
docker(:logs, service_name, (" --since #{since}" if since), (" --tail #{lines}" if lines), "--timestamps", "2>&1"),
docker(:logs, service_name, (" --since #{since}" if since), (" --tail #{lines}" if lines), ("--timestamps" if timestamps), "2>&1"),
("grep '#{grep}'#{" #{grep_options}" if grep_options}" if grep)
end
def follow_logs(grep: nil, grep_options: nil)
def follow_logs(timestamps: true, grep: nil, grep_options: nil)
run_over_ssh \
pipe \
docker(:logs, service_name, "--timestamps", "--tail", "10", "--follow", "2>&1"),
docker(:logs, service_name, ("--timestamps" if timestamps), "--tail", "10", "--follow", "2>&1"),
(%(grep "#{grep}"#{" #{grep_options}" if grep_options}) if grep)
end

View File

@@ -11,6 +11,7 @@ module Kamal::Commands::App::Execution
docker :run,
("-it" if interactive),
"--rm",
"--network", "kamal",
*role&.env_args(host),
*argumentize("--env", env),
*config.volume_args,

View File

@@ -1,16 +1,16 @@
module Kamal::Commands::App::Logging
def logs(version: nil, since: nil, lines: nil, grep: nil, grep_options: nil)
def logs(version: nil, timestamps: true, since: nil, lines: nil, grep: nil, grep_options: nil)
pipe \
version ? container_id_for_version(version) : current_running_container_id,
"xargs docker logs#{" --since #{since}" if since}#{" --tail #{lines}" if lines} 2>&1",
"xargs docker logs#{" --timestamps" if timestamps}#{" --since #{since}" if since}#{" --tail #{lines}" if lines} 2>&1",
("grep '#{grep}'#{" #{grep_options}" if grep_options}" if grep)
end
def follow_logs(host:, lines: nil, grep: nil, grep_options: nil)
def follow_logs(host:, timestamps: true, lines: nil, grep: nil, grep_options: nil)
run_over_ssh \
pipe(
current_running_container_id,
"xargs docker logs --timestamps#{" --tail #{lines}" if lines} --follow 2>&1",
"xargs docker logs#{" --timestamps" if timestamps}#{" --tail #{lines}" if lines} --follow 2>&1",
(%(grep "#{grep}"#{" #{grep_options}" if grep_options}) if grep)
),
host: host

View File

@@ -1,9 +1,12 @@
class Kamal::Commands::Hook < Kamal::Commands::Base
def run(hook, secrets: false, **details)
env = tags(**details).env
env.merge!(config.secrets.to_h) if secrets
def run(hook)
[ hook_file(hook) ]
end
[ hook_file(hook), env: env ]
def env(secrets: false, **details)
tags(**details).env.tap do |env|
env.merge!(config.secrets.to_h) if secrets
end
end
def hook_exists?(hook)

View File

@@ -7,10 +7,8 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base
"--network", "kamal",
"--detach",
"--restart", "unless-stopped",
*config.proxy_publish_args,
"--volume", "/var/run/docker.sock:/var/run/docker.sock",
*config.proxy_config_volume.docker_args,
*config.logging_args,
"--volume", "kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy",
"\$\(#{get_boot_options.join(" ")}\)",
config.proxy_image
end
@@ -36,15 +34,15 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base
[ :cut, "-d:", "-f2" ]
end
def logs(since: nil, lines: nil, grep: nil, grep_options: nil)
def logs(timestamps: true, since: nil, lines: nil, grep: nil, grep_options: nil)
pipe \
docker(:logs, container_name, (" --since #{since}" if since), (" --tail #{lines}" if lines), "--timestamps", "2>&1"),
docker(:logs, container_name, ("--since #{since}" if since), ("--tail #{lines}" if lines), ("--timestamps" if timestamps), "2>&1"),
("grep '#{grep}'#{" #{grep_options}" if grep_options}" if grep)
end
def follow_logs(host:, grep: nil, grep_options: nil)
def follow_logs(host:, timestamps: true, grep: nil, grep_options: nil)
run_over_ssh pipe(
docker(:logs, container_name, "--timestamps", "--tail", "10", "--follow", "2>&1"),
docker(:logs, container_name, ("--timestamps" if timestamps), "--tail", "10", "--follow", "2>&1"),
(%(grep "#{grep}"#{" #{grep_options}" if grep_options}) if grep)
).join(" "), host: host
end
@@ -57,10 +55,6 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base
docker :image, :prune, "--all", "--force", "--filter", "label=org.opencontainers.image.title=kamal-proxy"
end
def remove_host_directory
remove_directory config.proxy_directory
end
def cleanup_traefik
chain \
docker(:container, :stop, "traefik"),
@@ -70,6 +64,22 @@ class Kamal::Commands::Proxy < Kamal::Commands::Base
)
end
def ensure_proxy_directory
make_directory config.proxy_directory
end
def remove_proxy_directory
remove_directory config.proxy_directory
end
def get_boot_options
combine [ :cat, config.proxy_options_file ], [ :echo, "\"#{config.proxy_options_default.join(" ")}\"" ], by: "||"
end
def reset_boot_options
remove_file config.proxy_options_file
end
private
def container_name
config.proxy_container_name

View File

@@ -14,7 +14,7 @@ class Kamal::Configuration
include Validation
PROXY_MINIMUM_VERSION = "v0.3.0"
PROXY_MINIMUM_VERSION = "v0.6.0"
PROXY_HTTP_PORT = 80
PROXY_HTTPS_PORT = 443
@@ -216,10 +216,6 @@ class Kamal::Configuration
File.join apps_directory, [ service, destination ].compact.join("-")
end
def proxy_directory
File.join run_directory, "proxy"
end
def env_directory
File.join app_directory, "env"
end
@@ -250,8 +246,12 @@ class Kamal::Configuration
env_tags.detect { |t| t.name == name.to_s }
end
def proxy_publish_args
argumentize "--publish", [ "#{PROXY_HTTP_PORT}:#{PROXY_HTTP_PORT}", "#{PROXY_HTTPS_PORT}:#{PROXY_HTTPS_PORT}" ]
def proxy_publish_args(http_port, https_port)
argumentize "--publish", [ "#{http_port}:#{PROXY_HTTP_PORT}", "#{https_port}:#{PROXY_HTTPS_PORT}" ]
end
def proxy_options_default
proxy_publish_args PROXY_HTTP_PORT, PROXY_HTTPS_PORT
end
def proxy_image
@@ -262,10 +262,12 @@ class Kamal::Configuration
"kamal-proxy"
end
def proxy_config_volume
Kamal::Configuration::Volume.new \
host_path: File.join(proxy_directory, "config"),
container_path: "/home/kamal-proxy/.config/kamal-proxy"
def proxy_directory
File.join run_directory, "proxy"
end
def proxy_options_file
File.join proxy_directory, "options"
end

View File

@@ -2,10 +2,6 @@
#
# The builder configuration controls how the application is built with `docker build`
#
# If no configuration is specified, Kamal will:
# 1. Create a buildx context called `kamal-local-docker-container`, using the docker-container driver
# 2. Use `docker build` to build a multiarch image for linux/amd64,linux/arm64 with that context
#
# See https://kamal-deploy.org/docs/configuration/builder-examples/ for more information
# Builder options
@@ -78,7 +74,7 @@ builder:
# Build secrets
#
# Values are read from the .kamal/secrets.
# Values are read from .kamal/secrets.
#
secrets:
- SECRET1

View File

@@ -36,6 +36,8 @@ image: my-image
labels:
my-label: my-value
# Volumes
#
# Additional volumes to mount into the container
volumes:
- /path/on/host:/path/in/container:ro
@@ -58,7 +60,7 @@ servers:
env:
...
# Asset Bridging
# Asset Path
#
# Used for asset bridging across deployments, default to `nil`
#
@@ -74,6 +76,8 @@ env:
# To configure this, set the path to the assets:
asset_path: /path/to/assets
# Hooks path
#
# Path to hooks, defaults to `.kamal/hooks`
# See https://kamal-deploy.org/docs/hooks for more information
hooks_path: /user_home/kamal/hooks
@@ -83,7 +87,7 @@ hooks_path: /user_home/kamal/hooks
# Whether deployments require a destination to be specified, defaults to `false`
require_destination: true
# The primary role
# Primary role
#
# This defaults to `web`, but if you have no web role, you can change this
primary_role: workers

View File

@@ -12,11 +12,16 @@ env:
DATABASE_HOST: mysql-db1
DATABASE_PORT: 3306
# Using .kamal/secrets file to load required environment variables
# Secrets
#
# Kamal uses dotenv to automatically load environment variables set in the .kamal/secrets file.
# Kamal uses dotenv to automatically load environment variables set in the `.kamal/secrets` file.
#
# This file can be used to set variables like KAMAL_REGISTRY_PASSWORD or database passwords.
# If you are using destinations, secrets will instead be read from `.kamal/secrets-<DESTINATION>` if
# it exists.
#
# Common secrets across all destinations can be set in `.kamal/secrets-common`.
#
# This file can be used to set variables like `KAMAL_REGISTRY_PASSWORD` or database passwords.
# You can use variable or command substitution in the secrets file.
#
# ```
@@ -24,6 +29,14 @@ env:
# RAILS_MASTER_KEY=$(cat config/master.key)
# ```
#
# You can also use [secret helpers](../commands/secrets) for some common password managers.
# ```
# SECRETS=$(kamal secrets fetch ...)
#
# REGISTRY_PASSWORD=$(kamal secrets extract REGISTRY_PASSWORD $SECRETS)
# DB_PASSWORD=$(kamal secrets extract DB_PASSWORD $SECRETS)
# ```
#
# If you store secrets directly in .kamal/secrets, ensure that it is not checked into version control.
#
# To pass the secrets you should list them under the `secret` key. When you do this the

View File

@@ -47,7 +47,7 @@ proxy:
# Response timeout
#
# How long to wait for requests to complete before timing out, defaults to 30 seconds
response_timeout: 10s
response_timeout: 10
# Healthcheck
#
@@ -91,7 +91,7 @@ proxy:
# Forward headers
#
# Whether to forward the X-Forwarded-For and X-Forwarded-Proto headers (defaults to false)
# Whether to forward the X-Forwarded-For and X-Forwarded-Proto headers.
#
# If you are behind a trusted proxy, you can set this to true to forward the headers.
#

View File

@@ -26,8 +26,12 @@ servers:
#
# When there are other options to set, the list of hosts goes under the `hosts` key
#
# By default only the primary role uses a proxy, but you can set `proxy` to change
# it.
# By default only the primary role uses a proxy.
#
# For other roles, you can set it to `proxy: true` enable it and inherit the root proxy
# configuration or provide a map of options to override the root configuration.
#
# For the primary role, you can set `proxy: false` to disable the proxy.
#
# You can also set a custom cmd to run in the container, and overwrite other settings
# from the root configuration.

View File

@@ -29,7 +29,7 @@ class Kamal::Configuration::Proxy
def deploy_options
{
host: proxy_config["host"],
tls: proxy_config["ssl"],
tls: proxy_config["ssl"] ? true : nil,
"deploy-timeout": seconds_duration(config.deploy_timeout),
"drain-timeout": seconds_duration(config.drain_timeout),
"health-check-interval": seconds_duration(proxy_config.dig("healthcheck", "interval")),

View File

@@ -3,7 +3,7 @@ class Kamal::Secrets::Adapters::LastPass < Kamal::Secrets::Adapters::Base
def login(account)
unless loggedin?(account)
`lpass login #{account.shellescape}`
raise RuntimeError, "Failed to login to 1Password" unless $?.success?
raise RuntimeError, "Failed to login to LastPass" unless $?.success?
end
end
@@ -13,7 +13,7 @@ class Kamal::Secrets::Adapters::LastPass < Kamal::Secrets::Adapters::Base
def fetch_secrets(secrets, account:, session:)
items = `lpass show #{secrets.map(&:shellescape).join(" ")} --json`
raise RuntimeError, "Could not read #{secrets} from 1Password" unless $?.success?
raise RuntimeError, "Could not read #{secrets} from LastPass" unless $?.success?
items = JSON.parse(items)

View File

@@ -1,3 +1,3 @@
module Kamal
VERSION = "2.0.0.beta1"
VERSION = "2.0.0"
end

View File

@@ -41,7 +41,7 @@ class CliAccessoryTest < CliTestCase
test "upload" do
run_command("upload", "mysql").tap do |output|
assert_match "mkdir -p app-mysql/etc/mysql", output
assert_match "test/fixtures/files/my.cnf app-mysql/etc/mysql/my.cnf", output
assert_match "test/fixtures/files/my.cnf to app-mysql/etc/mysql/my.cnf", output
assert_match "chmod 755 app-mysql/etc/mysql/my.cnf", output
end
end

View File

@@ -263,13 +263,13 @@ class CliAppTest < CliTestCase
test "exec" do
run_command("exec", "ruby -v").tap do |output|
assert_match "docker run --rm --env-file .kamal/apps/app/env/roles/web.env dhh/app:latest ruby -v", output
assert_match "docker run --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env dhh/app:latest ruby -v", output
end
end
test "exec separate arguments" do
run_command("exec", "ruby", " -v").tap do |output|
assert_match "docker run --rm --env-file .kamal/apps/app/env/roles/web.env dhh/app:latest ruby -v", output
assert_match "docker run --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env dhh/app:latest ruby -v", output
end
end
@@ -282,7 +282,7 @@ class CliAppTest < CliTestCase
test "exec interactive" do
SSHKit::Backend::Abstract.any_instance.expects(:exec)
.with("ssh -t root@1.1.1.1 -p 22 'docker run -it --rm --env-file .kamal/apps/app/env/roles/web.env dhh/app:latest ruby -v'")
.with("ssh -t root@1.1.1.1 -p 22 'docker run -it --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env dhh/app:latest ruby -v'")
run_command("exec", "-i", "ruby -v").tap do |output|
assert_match "Get most recent version available as an image...", output
assert_match "Launching interactive command with version latest via SSH from new container on 1.1.1.1...", output
@@ -315,11 +315,11 @@ class CliAppTest < CliTestCase
SSHKit::Backend::Abstract.any_instance.stubs(:exec)
.with("ssh -t root@1.1.1.1 'sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1| xargs docker logs --timestamps --tail 10 2>&1'")
assert_match "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --tail 100 2>&1", run_command("logs")
assert_match "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --tail 100 2>&1", run_command("logs")
assert_match "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs 2>&1 | grep 'hey'", run_command("logs", "--grep", "hey")
assert_match "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps 2>&1 | grep 'hey'", run_command("logs", "--grep", "hey")
assert_match "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs 2>&1 | grep 'hey' -C 2", run_command("logs", "--grep", "hey", "--grep-options", "-C 2")
assert_match "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps 2>&1 | grep 'hey' -C 2", run_command("logs", "--grep", "hey", "--grep-options", "-C 2")
end
test "logs with follow" do

View File

@@ -49,7 +49,7 @@ class CliBuildTest < CliTestCase
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:git, "-C", build_directory, :submodule, :update, "--init")
SSHKit::Backend::Abstract.any_instance.expects(:execute)
.with(:docker, :buildx, :build, "--push", "--platform", "linux/amd64", "--builder", "kamal-local-docker-container", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".", env: {})
.with(:docker, :buildx, :build, "--push", "--platform", "linux/amd64", "--builder", "kamal-local-docker-container", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".")
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:git, "-C", anything, :"rev-parse", :HEAD)
@@ -140,7 +140,7 @@ class CliBuildTest < CliTestCase
.returns("")
SSHKit::Backend::Abstract.any_instance.expects(:execute)
.with(:docker, :buildx, :build, "--push", "--platform", "linux/amd64", "--builder", "kamal-local-docker-container", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".", env: {})
.with(:docker, :buildx, :build, "--push", "--platform", "linux/amd64", "--builder", "kamal-local-docker-container", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".")
run_command("push").tap do |output|
assert_match /WARN Missing compatible builder, so creating a new one first/, output

View File

@@ -41,27 +41,7 @@ class CliTestCase < ActiveSupport::TestCase
end
def assert_hook_ran(hook, output, version:, service_version:, hosts:, command:, subcommand: nil, runtime: false, secrets: false)
whoami = `whoami`.chomp
performer = Kamal::Git.email.presence || whoami
service = service_version.split("@").first
assert_match "Running the #{hook} hook...\n", output
expected = %r{Running\s/usr/bin/env\s\.kamal/hooks/#{hook}\sas\s#{whoami}@localhost\n\s
DEBUG\s\[[0-9a-f]*\]\sCommand:\s\(\sexport\s
KAMAL_RECORDED_AT=\"\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\dZ\"\s
KAMAL_PERFORMER=\"#{performer}\"\s
KAMAL_VERSION=\"#{version}\"\s
KAMAL_SERVICE_VERSION=\"#{service_version}\"\s
KAMAL_SERVICE=\"#{service}\"\s
KAMAL_HOSTS=\"#{hosts}\"\s
KAMAL_COMMAND=\"#{command}\"\s
#{"KAMAL_SUBCOMMAND=\\\"#{subcommand}\\\"\\s" if subcommand}
#{"KAMAL_RUNTIME=\\\"\\d+\\\"\\s" if runtime}
#{"DB_PASSWORD=\"secret\"\\s" if secrets}
;\s/usr/bin/env\s\.kamal/hooks/#{hook} }x
assert_match expected, output
assert_match %r{usr/bin/env\s\.kamal/hooks/#{hook}}, output
end
def with_argv(*argv)

View File

@@ -4,7 +4,7 @@ class CliProxyTest < CliTestCase
test "boot" do
run_command("boot").tap do |output|
assert_match "docker login", output
assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/proxy/config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" #{KAMAL.config.proxy_image}", output
assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443\") #{KAMAL.config.proxy_image}", output
end
end
@@ -18,7 +18,7 @@ class CliProxyTest < CliTestCase
exception = assert_raises do
run_command("boot").tap do |output|
assert_match "docker login", output
assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/proxy/config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" #{KAMAL.config.proxy_image}", output
assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443\") #{KAMAL.config.proxy_image}", output
end
end
@@ -36,7 +36,7 @@ class CliProxyTest < CliTestCase
run_command("boot").tap do |output|
assert_match "docker login", output
assert_match "docker container start kamal-proxy || docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/proxy/config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" #{KAMAL.config.proxy_image}", output
assert_match "docker container start kamal-proxy || docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443\") #{KAMAL.config.proxy_image}", output
end
ensure
Thread.report_on_exception = false
@@ -57,13 +57,13 @@ class CliProxyTest < CliTestCase
assert_match "docker container stop kamal-proxy on 1.1.1.1", output
assert_match "Running docker container stop traefik ; docker container prune --force --filter label=org.opencontainers.image.title=Traefik && docker image prune --all --force --filter label=org.opencontainers.image.title=Traefik on 1.1.1.1", output
assert_match "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy on 1.1.1.1", output
assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/proxy/config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" #{KAMAL.config.proxy_image} on 1.1.1.1", output
assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443\") #{KAMAL.config.proxy_image} on 1.1.1.1", output
assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"abcdefabcdef:80\" --deploy-timeout \"6s\" --drain-timeout \"30s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" --log-request-header \"User-Agent\" on 1.1.1.1", output
assert_match "docker container stop kamal-proxy on 1.1.1.2", output
assert_match "Running docker container stop traefik ; docker container prune --force --filter label=org.opencontainers.image.title=Traefik && docker image prune --all --force --filter label=org.opencontainers.image.title=Traefik on 1.1.1.2", output
assert_match "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy on 1.1.1.2", output
assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/proxy/config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" #{KAMAL.config.proxy_image} on 1.1.1.2", output
assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443\") #{KAMAL.config.proxy_image} on 1.1.1.2", output
assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"abcdefabcdef:80\" --deploy-timeout \"6s\" --drain-timeout \"30s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" --log-request-header \"User-Agent\" on 1.1.1.2", output
end
end
@@ -111,11 +111,11 @@ class CliProxyTest < CliTestCase
test "logs" do
SSHKit::Backend::Abstract.any_instance.stubs(:capture)
.with(:docker, :logs, "kamal-proxy", " --tail 100", "--timestamps", "2>&1")
.with(:docker, :logs, "kamal-proxy", "--tail 100", "--timestamps", "2>&1")
.returns("Log entry")
SSHKit::Backend::Abstract.any_instance.stubs(:capture)
.with(:docker, :logs, "proxy", " --tail 100", "--timestamps", "2>&1")
.with(:docker, :logs, "proxy", "--tail 100", "--timestamps", "2>&1")
.returns("Log entry")
run_command("logs").tap do |output|
@@ -136,7 +136,6 @@ class CliProxyTest < CliTestCase
assert_match "/usr/bin/env ls .kamal/apps | wc -l", output
assert_match "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy", output
assert_match "docker image prune --all --force --filter label=org.opencontainers.image.title=kamal-proxy", output
assert_match "/usr/bin/env rm -r .kamal/proxy", output
end
end
@@ -176,12 +175,6 @@ class CliProxyTest < CliTestCase
end
end
test "remove_host_directory" do
run_command("remove_host_directory").tap do |output|
assert_match "/usr/bin/env rm -r .kamal/proxy", output
end
end
test "upgrade" do
Object.any_instance.stubs(:sleep)
@@ -205,11 +198,11 @@ class CliProxyTest < CliTestCase
assert_match "/usr/bin/env mkdir -p .kamal", output
assert_match "docker network create kamal", output
assert_match "docker login -u [REDACTED] -p [REDACTED]", output
assert_match "docker container start kamal-proxy || docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/proxy/config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}", output
assert_match "docker container start kamal-proxy || docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443\") basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}", output
assert_match "/usr/bin/env mkdir -p .kamal", output
assert_match %r{docker rename app-web-latest app-web-latest_replaced_.*}, output
assert_match "/usr/bin/env mkdir -p .kamal/apps/app/env/roles", output
assert_match %r{/usr/bin/env .* .kamal/apps/app/env/roles/web.env}, output
assert_match "Uploading \"\\n\" to .kamal/apps/app/env/roles/web.env", output
assert_match %r{docker run --detach --restart unless-stopped --name app-web-latest --network kamal --hostname 1.1.1.1-.* -e KAMAL_CONTAINER_NAME="app-web-latest" -e KAMAL_VERSION="latest" --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size="10m" --label service="app" --label role="web" --label destination dhh/app:latest}, output
assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"12345678:80\" --deploy-timeout \"6s\" --drain-timeout \"30s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" --log-request-header \"User-Agent\"", output
assert_match "docker container ls --all --filter name=^app-web-12345678$ --quiet | xargs docker stop", output
@@ -243,6 +236,62 @@ class CliProxyTest < CliTestCase
end
end
test "boot_config set" do
run_command("boot_config", "set").tap do |output|
%w[ 1.1.1.1 1.1.1.2 ].each do |host|
assert_match "Running /usr/bin/env mkdir -p .kamal/proxy on #{host}", output
assert_match "Uploading \"--publish 80:80 --publish 443:443\" to .kamal/proxy/options on #{host}", output
end
end
end
test "boot_config set no publish" do
run_command("boot_config", "set", "--publish", "false").tap do |output|
%w[ 1.1.1.1 1.1.1.2 ].each do |host|
assert_match "Running /usr/bin/env mkdir -p .kamal/proxy on #{host}", output
assert_match "Uploading \"\" to .kamal/proxy/options on #{host}", output
end
end
end
test "boot_config set custom ports" do
run_command("boot_config", "set", "--http-port", "8080", "--https-port", "8443").tap do |output|
%w[ 1.1.1.1 1.1.1.2 ].each do |host|
assert_match "Running /usr/bin/env mkdir -p .kamal/proxy on #{host}", output
assert_match "Uploading \"--publish 8080:80 --publish 8443:443\" to .kamal/proxy/options on #{host}", output
end
end
end
test "boot_config set docker options" do
run_command("boot_config", "set", "--docker_options", "label=foo=bar", "add_host=thishost:thathost").tap do |output|
%w[ 1.1.1.1 1.1.1.2 ].each do |host|
assert_match "Running /usr/bin/env mkdir -p .kamal/proxy on #{host}", output
assert_match "Uploading \"--publish 80:80 --publish 443:443 --label=foo=bar --add_host=thishost:thathost\" to .kamal/proxy/options on #{host}", output
end
end
end
test "boot_config get" do
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
.with(:cat, ".kamal/proxy/options", "||", :echo, "\"--publish 80:80 --publish 443:443\"")
.returns("--publish 80:80 --publish 8443:443 --label=foo=bar")
.twice
run_command("boot_config", "get").tap do |output|
assert_match "Host 1.1.1.1: --publish 80:80 --publish 8443:443 --label=foo=bar", output
assert_match "Host 1.1.1.2: --publish 80:80 --publish 8443:443 --label=foo=bar", output
end
end
test "boot_config reset" do
run_command("boot_config", "reset").tap do |output|
%w[ 1.1.1.1 1.1.1.2 ].each do |host|
assert_match "rm .kamal/proxy/options on #{host}", output
end
end
end
private
def run_command(*command, fixture: :with_proxy)
stdouted { Kamal::Cli::Proxy.start([ *command, "-c", "test/fixtures/deploy_#{fixture}.yml" ]) }

View File

@@ -130,12 +130,20 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
assert_equal \
"docker logs app-mysql --since 5m --tail 100 --timestamps 2>&1 | grep 'thing' -C 2",
new_command(:mysql).logs(since: "5m", lines: 100, grep: "thing", grep_options: "-C 2").join(" ")
assert_equal \
"docker logs app-mysql --since 5m --tail 100 2>&1 | grep 'thing' -C 2",
new_command(:mysql).logs(timestamps: false, since: "5m", lines: 100, grep: "thing", grep_options: "-C 2").join(" ")
end
test "follow logs" do
assert_equal \
"ssh -t root@1.1.1.5 -p 22 'docker logs app-mysql --timestamps --tail 10 --follow 2>&1'",
new_command(:mysql).follow_logs
assert_equal \
"ssh -t root@1.1.1.5 -p 22 'docker logs app-mysql --tail 10 --follow 2>&1'",
new_command(:mysql).follow_logs(timestamps: false)
end
test "remove container" do

View File

@@ -129,49 +129,49 @@ class CommandsAppTest < ActiveSupport::TestCase
test "logs" do
assert_equal \
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs 2>&1",
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps 2>&1",
new_command.logs.join(" ")
end
test "logs with since" do
assert_equal \
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --since 5m 2>&1",
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --since 5m 2>&1",
new_command.logs(since: "5m").join(" ")
end
test "logs with lines" do
assert_equal \
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --tail 100 2>&1",
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --tail 100 2>&1",
new_command.logs(lines: "100").join(" ")
end
test "logs with since and lines" do
assert_equal \
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --since 5m --tail 100 2>&1",
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --since 5m --tail 100 2>&1",
new_command.logs(since: "5m", lines: "100").join(" ")
end
test "logs with grep" do
assert_equal \
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs 2>&1 | grep 'my-id'",
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps 2>&1 | grep 'my-id'",
new_command.logs(grep: "my-id").join(" ")
end
test "logs with grep and grep options" do
assert_equal \
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs 2>&1 | grep 'my-id' -C 2",
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps 2>&1 | grep 'my-id' -C 2",
new_command.logs(grep: "my-id", grep_options: "-C 2").join(" ")
end
test "logs with since, grep and grep options" do
assert_equal \
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --since 5m 2>&1 | grep 'my-id' -C 2",
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --since 5m 2>&1 | grep 'my-id' -C 2",
new_command.logs(since: "5m", grep: "my-id", grep_options: "-C 2").join(" ")
end
test "logs with since and grep" do
assert_equal \
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --since 5m 2>&1 | grep 'my-id'",
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --since 5m 2>&1 | grep 'my-id'",
new_command.logs(since: "5m", grep: "my-id").join(" ")
end
@@ -191,18 +191,22 @@ class CommandsAppTest < ActiveSupport::TestCase
assert_equal \
"ssh -t root@app-1 -p 22 'sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --tail 123 --follow 2>&1 | grep \"Completed\"'",
new_command.follow_logs(host: "app-1", lines: 123, grep: "Completed")
assert_equal \
"ssh -t root@app-1 -p 22 'sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --tail 123 --follow 2>&1 | grep \"Completed\"'",
new_command.follow_logs(host: "app-1", timestamps: false, lines: 123, grep: "Completed")
end
test "execute in new container" do
assert_equal \
"docker run --rm --env-file .kamal/apps/app/env/roles/web.env dhh/app:999 bin/rails db:setup",
"docker run --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env dhh/app:999 bin/rails db:setup",
new_command.execute_in_new_container("bin/rails", "db:setup", env: {}).join(" ")
end
test "execute in new container with env" do
assert_equal \
"docker run --rm --env-file .kamal/apps/app/env/roles/web.env --env foo=\"bar\" dhh/app:999 bin/rails db:setup",
"docker run --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --env foo=\"bar\" dhh/app:999 bin/rails db:setup",
new_command.execute_in_new_container("bin/rails", "db:setup", env: { "foo" => "bar" }).join(" ")
end
@@ -211,14 +215,14 @@ class CommandsAppTest < ActiveSupport::TestCase
@config[:env]["tags"] = { "tag1" => { "ENV1" => "value1" } }
assert_equal \
"docker run --rm --env ENV1=\"value1\" --env-file .kamal/apps/app/env/roles/web.env dhh/app:999 bin/rails db:setup",
"docker run --rm --network kamal --env ENV1=\"value1\" --env-file .kamal/apps/app/env/roles/web.env dhh/app:999 bin/rails db:setup",
new_command.execute_in_new_container("bin/rails", "db:setup", env: {}).join(" ")
end
test "execute in new container with custom options" do
@config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "options" => { "mount" => "somewhere", "cap-add" => true } } }
assert_equal \
"docker run --rm --env-file .kamal/apps/app/env/roles/web.env --mount \"somewhere\" --cap-add dhh/app:999 bin/rails db:setup",
"docker run --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --mount \"somewhere\" --cap-add dhh/app:999 bin/rails db:setup",
new_command.execute_in_new_container("bin/rails", "db:setup", env: {}).join(" ")
end
@@ -235,7 +239,7 @@ class CommandsAppTest < ActiveSupport::TestCase
end
test "execute in new container over ssh" do
assert_match %r{docker run -it --rm --env-file .kamal/apps/app/env/roles/web.env dhh/app:999 bin/rails c},
assert_match %r{docker run -it --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env dhh/app:999 bin/rails c},
new_command.execute_in_new_container_over_ssh("bin/rails", "c", env: {})
end
@@ -243,13 +247,13 @@ class CommandsAppTest < ActiveSupport::TestCase
@config[:servers] = [ { "1.1.1.1" => "tag1" } ]
@config[:env]["tags"] = { "tag1" => { "ENV1" => "value1" } }
assert_equal "ssh -t root@1.1.1.1 -p 22 'docker run -it --rm --env ENV1=\"value1\" --env-file .kamal/apps/app/env/roles/web.env dhh/app:999 bin/rails c'",
assert_equal "ssh -t root@1.1.1.1 -p 22 'docker run -it --rm --network kamal --env ENV1=\"value1\" --env-file .kamal/apps/app/env/roles/web.env dhh/app:999 bin/rails c'",
new_command.execute_in_new_container_over_ssh("bin/rails", "c", env: {})
end
test "execute in new container with custom options over ssh" do
@config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "options" => { "mount" => "somewhere", "cap-add" => true } } }
assert_match %r{docker run -it --rm --env-file .kamal/apps/app/env/roles/web.env --mount \"somewhere\" --cap-add dhh/app:999 bin/rails c},
assert_match %r{docker run -it --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --mount \"somewhere\" --cap-add dhh/app:999 bin/rails c},
new_command.execute_in_new_container_over_ssh("bin/rails", "c", env: {})
end

View File

@@ -16,41 +16,34 @@ class CommandsHookTest < ActiveSupport::TestCase
end
test "run" do
assert_equal [
".kamal/hooks/foo",
{ env: {
"KAMAL_RECORDED_AT" => @recorded_at,
"KAMAL_PERFORMER" => @performer,
"KAMAL_VERSION" => "123",
"KAMAL_SERVICE_VERSION" => "app@123",
"KAMAL_SERVICE" => "app" } }
], new_command.run("foo")
assert_equal [ ".kamal/hooks/foo" ], new_command.run("foo")
end
test "env" do
assert_equal ({
"KAMAL_RECORDED_AT" => @recorded_at,
"KAMAL_PERFORMER" => @performer,
"KAMAL_VERSION" => "123",
"KAMAL_SERVICE_VERSION" => "app@123",
"KAMAL_SERVICE" => "app"
}), new_command.env
end
test "run with custom hooks_path" do
assert_equal [
"custom/hooks/path/foo",
{ env: {
"KAMAL_RECORDED_AT" => @recorded_at,
"KAMAL_PERFORMER" => @performer,
"KAMAL_VERSION" => "123",
"KAMAL_SERVICE_VERSION" => "app@123",
"KAMAL_SERVICE" => "app" } }
], new_command(hooks_path: "custom/hooks/path").run("foo")
assert_equal [ "custom/hooks/path/foo" ], new_command(hooks_path: "custom/hooks/path").run("foo")
end
test "hook with secrets" do
test "env with secrets" do
with_test_secrets("secrets" => "DB_PASSWORD=secret") do
assert_equal [
".kamal/hooks/foo",
{ env: {
assert_equal (
{
"KAMAL_RECORDED_AT" => @recorded_at,
"KAMAL_PERFORMER" => @performer,
"KAMAL_VERSION" => "123",
"KAMAL_SERVICE_VERSION" => "app@123",
"KAMAL_SERVICE" => "app",
"DB_PASSWORD" => "secret" } }
], new_command(env: { "secret" => [ "DB_PASSWORD" ] }).run("foo", secrets: true)
"DB_PASSWORD" => "secret" }
), new_command.env(secrets: true)
end
end

View File

@@ -15,13 +15,7 @@ class CommandsProxyTest < ActiveSupport::TestCase
test "run" do
assert_equal \
"docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/proxy/config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" #{KAMAL.config.proxy_image}",
new_command.run.join(" ")
end
test "run with ports configured" do
assert_equal \
"docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/proxy/config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" #{KAMAL.config.proxy_image}",
"docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443\") basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}",
new_command.run.join(" ")
end
@@ -29,15 +23,7 @@ class CommandsProxyTest < ActiveSupport::TestCase
@config.delete(:proxy)
assert_equal \
"docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/proxy/config:/home/kamal-proxy/.config/kamal-proxy --log-opt max-size=\"10m\" #{KAMAL.config.proxy_image}",
new_command.run.join(" ")
end
test "run with logging config" do
@config[:logging] = { "driver" => "local", "options" => { "max-size" => "100m", "max-file" => "3" } }
assert_equal \
"docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/proxy/config:/home/kamal-proxy/.config/kamal-proxy --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" #{KAMAL.config.proxy_image}",
"docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443\") basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}",
new_command.run.join(" ")
end
@@ -67,16 +53,22 @@ class CommandsProxyTest < ActiveSupport::TestCase
test "proxy logs since 2h" do
assert_equal \
"docker logs kamal-proxy --since 2h --timestamps 2>&1",
"docker logs kamal-proxy --since 2h --timestamps 2>&1",
new_command.logs(since: "2h").join(" ")
end
test "proxy logs last 10 lines" do
assert_equal \
"docker logs kamal-proxy --tail 10 --timestamps 2>&1",
"docker logs kamal-proxy --tail 10 --timestamps 2>&1",
new_command.logs(lines: 10).join(" ")
end
test "proxy logs without timestamps" do
assert_equal \
"docker logs kamal-proxy 2>&1",
new_command.logs(timestamps: false).join(" ")
end
test "proxy logs with grep hello!" do
assert_equal \
"docker logs kamal-proxy --timestamps 2>&1 | grep 'hello!'",
@@ -113,6 +105,24 @@ class CommandsProxyTest < ActiveSupport::TestCase
new_command.version.join(" ")
end
test "ensure_proxy_directory" do
assert_equal \
"mkdir -p .kamal/proxy",
new_command.ensure_proxy_directory.join(" ")
end
test "get_boot_options" do
assert_equal \
"cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443\"",
new_command.get_boot_options.join(" ")
end
test "reset_boot_options" do
assert_equal \
"rm .kamal/proxy/options",
new_command.reset_boot_options.join(" ")
end
private
def new_command
Kamal::Commands::Proxy.new(Kamal::Configuration.new(@config, version: "123"))

View File

@@ -1,6 +1,6 @@
require "test_helper"
class ConfigurationEnvTest < ActiveSupport::TestCase
class ConfigurationProxyTest < ActiveSupport::TestCase
setup do
@deploy = {
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" },
@@ -18,6 +18,12 @@ class ConfigurationEnvTest < ActiveSupport::TestCase
assert_raises(Kamal::ConfigurationError) { config.proxy.ssl? }
end
test "ssl false" do
@deploy[:proxy] = { "ssl" => false }
assert_not config.proxy.ssl?
assert_not config.proxy.deploy_options.has_key?(:tls)
end
private
def config
Kamal::Configuration.new(@deploy)

View File

@@ -9,7 +9,7 @@ class BrokenDeployTest < IntegrationTest
kamal :deploy
assert_app_is_up version: first_version
assert_container_running host: :vm3, name: "app-workers-#{first_version}"
assert_container_running host: :vm3, name: "app_with_roles-workers-#{first_version}"
second_version = break_app
@@ -17,8 +17,8 @@ class BrokenDeployTest < IntegrationTest
assert_failed_deploy output
assert_app_is_up version: first_version
assert_container_running host: :vm3, name: "app-workers-#{first_version}"
assert_container_not_running host: :vm3, name: "app-workers-#{second_version}"
assert_container_running host: :vm3, name: "app_with_roles-workers-#{first_version}"
assert_container_not_running host: :vm3, name: "app_with_roles-workers-#{second_version}"
end
private

View File

@@ -19,6 +19,7 @@ RUN apt-get update --fix-missing && apt-get install -y docker-ce docker-ce-cli c
COPY *.sh .
COPY app/ app/
COPY app_with_roles/ app_with_roles/
COPY app_with_traefik/ app_with_traefik/
RUN rm -rf /root/.ssh
RUN ln -s /shared/ssh /root/.ssh
@@ -28,6 +29,7 @@ RUN git config --global user.email "deployer@example.com"
RUN git config --global user.name "Deployer"
RUN cd app && git init && git add . && git commit -am "Initial version"
RUN cd app_with_roles && git init && git add . && git commit -am "Initial version"
RUN cd app_with_traefik && git init && git add . && git commit -am "Initial version"
HEALTHCHECK --interval=1s CMD pgrep sleep

View File

@@ -1,3 +1,3 @@
#!/bin/sh
echo "Rebooting Traefik on ${KAMAL_HOSTS}..."
echo "Rebooting kamal-proxy on ${KAMAL_HOSTS}..."
mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/pre-proxy-reboot

View File

@@ -23,7 +23,8 @@ asset_path: /usr/share/nginx/html/versions
deploy_timeout: 2
drain_timeout: 2
readiness_delay: 0
proxy:
host: 127.0.0.1
registry:
server: registry:4443
username: root

View File

@@ -1,5 +1,5 @@
service: app
image: app
service: app_with_roles
image: app_with_roles
servers:
web:
hosts:
@@ -14,6 +14,7 @@ drain_timeout: 2
readiness_delay: 0
proxy:
host: localhost
healthcheck:
interval: 1
timeout: 1

View File

@@ -0,0 +1,3 @@
kamal proxy boot_config set --publish false \
--docker_options label=traefik.http.services.kamal_proxy.loadbalancer.server.scheme=http \
label=traefik.http.routers.kamal_proxy.rule=PathPrefix\(\`/\`\)

View File

@@ -0,0 +1 @@
SECRET_TOKEN='1234 with "中文"'

View File

@@ -0,0 +1,9 @@
FROM registry:4443/nginx:1-alpine-slim
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
RUN mkdir -p /usr/share/nginx/html/versions && echo "hidden" > /usr/share/nginx/html/versions/.hidden
RUN echo "Up!" > /usr/share/nginx/html/up

View File

@@ -0,0 +1,29 @@
service: app_with_traefik
image: app_with_traefik
servers:
- vm1
- vm2
deploy_timeout: 2
drain_timeout: 2
readiness_delay: 0
registry:
server: registry:4443
username: root
password: root
builder:
driver: docker
arch: <%= Kamal::Utils.docker_arch %>
args:
COMMIT_SHA: <%= `git rev-parse HEAD` %>
accessories:
traefik:
service: traefik
image: traefik:v2.10
port: 80
cmd: "--providers.docker"
options:
volume:
- "/var/run/docker.sock:/var/run/docker.sock"
roles:
- web

View File

@@ -0,0 +1,17 @@
server {
listen 80;
listen [::]:80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}

View File

@@ -8,6 +8,8 @@ server {
location / {
proxy_pass http://loadbalancer;
proxy_set_header Host $host;
proxy_connect_timeout 10;
proxy_send_timeout 10;
proxy_read_timeout 10;

View File

@@ -50,8 +50,8 @@ class IntegrationTest < ActiveSupport::TestCase
assert_equal "502", response.code
end
def assert_app_is_up(version: nil)
response = app_response
def assert_app_is_up(version: nil, app: @app)
response = app_response(app: app)
debug_response_code(response, "200")
assert_equal "200", response.code
assert_app_version(version, response) if version
@@ -69,8 +69,8 @@ class IntegrationTest < ActiveSupport::TestCase
assert_equal up_times, up_count
end
def app_response
Net::HTTP.get_response(URI.parse("http://localhost:12345/version"))
def app_response(app: @app)
Net::HTTP.get_response(URI.parse("http://#{app_host(app)}:12345/version"))
end
def update_app_rev
@@ -153,11 +153,24 @@ class IntegrationTest < ActiveSupport::TestCase
assert_directory_removed("./kamal/apps/#{@app}")
end
def assert_proxy_directory_removed
assert_directory_removed("./kamal/proxy")
end
def assert_directory_removed(directory)
assert docker_compose("exec vm1 ls #{directory} | wc -l", capture: true).strip == "0"
end
def assert_proxy_running
assert_container_running(host: "vm1", name: "kamal-proxy")
end
def assert_proxy_not_running
assert_container_not_running(host: "vm1", name: "kamal-proxy")
end
def app_host(app = @app)
case app
when "app"
"127.0.0.1"
else
"localhost"
end
end
end

View File

@@ -46,13 +46,13 @@ class MainTest < IntegrationTest
assert_app_is_up version: version
assert_hooks_ran "pre-connect", "pre-build", "pre-deploy", "post-deploy"
assert_container_running host: :vm3, name: "app-workers-#{version}"
assert_container_running host: :vm3, name: "app_with_roles-workers-#{version}"
second_version = update_app_rev
kamal :redeploy
assert_app_is_up version: second_version
assert_container_running host: :vm3, name: "app-workers-#{second_version}"
assert_container_running host: :vm3, name: "app_with_roles-workers-#{second_version}"
end
test "config" do
@@ -88,6 +88,14 @@ class MainTest < IntegrationTest
end
test "setup and remove" do
@app = "app_with_roles"
kamal :proxy, :set_config,
"--publish=false",
"--options=label=traefik.http.services.kamal_proxy.loadbalancer.server.scheme=http",
"label=traefik.http.routers.kamal_proxy.rule=PathPrefix\\\(\\\`/\\\`\\\)",
"label=traefik.http.routers.kamal_proxy.priority=2"
# Check remove completes when nothing has been setup yet
kamal :remove, "-y"
assert_no_images_or_containers
@@ -98,7 +106,38 @@ class MainTest < IntegrationTest
kamal :remove, "-y"
assert_no_images_or_containers
assert_app_directory_removed
assert_proxy_directory_removed
end
test "two apps" do
@app = "app"
kamal :deploy
app1_version = latest_app_version
@app = "app_with_roles"
kamal :deploy
app2_version = latest_app_version
assert_app_is_up version: app1_version, app: "app"
assert_app_is_up version: app2_version, app: "app_with_roles"
@app = "app"
kamal :remove, "-y"
assert_app_directory_removed
assert_proxy_running
@app = "app_with_roles"
kamal :remove, "-y"
assert_app_directory_removed
assert_proxy_not_running
end
test "deploy with traefik" do
@app = "app_with_traefik"
first_version = latest_app_version
kamal :setup
assert_app_is_up version: first_version
end
private
@@ -116,21 +155,21 @@ class MainTest < IntegrationTest
end
def assert_env(key, value, vm:, version:)
assert_equal "#{key}=#{value}", docker_compose("exec #{vm} docker exec app-web-#{version} env | grep #{key}", capture: true)
assert_equal "#{key}=#{value}", docker_compose("exec #{vm} docker exec #{@app}-web-#{version} env | grep #{key}", capture: true)
end
def assert_no_env(key, vm:, version:)
assert_raises(RuntimeError, /exit 1/) do
docker_compose("exec #{vm} docker exec app-web-#{version} env | grep #{key}", capture: true)
docker_compose("exec #{vm} docker exec #{@app}-web-#{version} env | grep #{key}", capture: true)
end
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
assert_equal "200", Net::HTTP.get_response(URI.parse("http://#{app_host}:12345/versions/#{version}")).code
end
assert_equal "200", Net::HTTP.get_response(URI.parse("http://localhost:12345/versions/.hidden")).code
assert_equal "200", Net::HTTP.get_response(URI.parse("http://#{app_host}:12345/versions/.hidden")).code
end
def vm1_image_ids

View File

@@ -48,19 +48,5 @@ class ProxyTest < IntegrationTest
kamal :proxy, :remove
assert_proxy_not_running
assert_proxy_directory_removed
end
private
def assert_proxy_running
assert_match /basecamp\/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION} \"kamal-proxy run\"/, proxy_details
end
def assert_proxy_not_running
assert_no_match /basecamp\/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION} \"kamal-proxy run\"/, proxy_details
end
def proxy_details
kamal :proxy, :details, capture: true
end
end

View File

@@ -13,6 +13,13 @@ ActiveSupport::LogSubscriber.logger = ActiveSupport::Logger.new(STDOUT) if ENV["
# Applies to remote commands only.
SSHKit.config.backend = SSHKit::Backend::Printer
class SSHKit::Backend::Printer
def upload!(local, location, **kwargs)
local = local.string.inspect if local.respond_to?(:string)
puts "Uploading #{local} to #{location} on #{host}"
end
end
# Ensure local commands use the printer backend too.
# See https://github.com/capistrano/sshkit/blob/master/lib/sshkit/dsl.rb#L9
module SSHKit