Compare commits
63 Commits
xiaohui-zh
...
proxy-expe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b21af0f5b3 | ||
|
|
459ba95bbf | ||
|
|
01d08738ff | ||
|
|
19cf94457f | ||
|
|
269a5ff3e6 | ||
|
|
c8adda1550 | ||
|
|
d891eb91e4 | ||
|
|
268ec1c6e0 | ||
|
|
42fdbd98cb | ||
|
|
6adf3c117f | ||
|
|
9f0b10425c | ||
|
|
5f2384f123 | ||
|
|
eab7d3adc5 | ||
|
|
d2d0223c37 | ||
|
|
56268d724d | ||
|
|
cffb6c3d7e | ||
|
|
bd1726f305 | ||
|
|
7ddb122a22 | ||
|
|
98c951bbdb | ||
|
|
374c117b79 | ||
|
|
d6a5cf3c78 | ||
|
|
2aeabda455 | ||
|
|
c048c097ed | ||
|
|
ed148628fb | ||
|
|
d48080c772 | ||
|
|
3f64338929 | ||
|
|
0ab838bc25 | ||
|
|
b7382ceeaf | ||
|
|
69367fbc6b | ||
|
|
2515bd705c | ||
|
|
579e169be2 | ||
|
|
d6f5da92be | ||
|
|
9ccfe20b10 | ||
|
|
e871d347d5 | ||
|
|
b8af719bb7 | ||
|
|
f48987aa03 | ||
|
|
ef051eca1b | ||
|
|
173d44ee0a | ||
|
|
4e811372f8 | ||
|
|
ec4aa45852 | ||
|
|
5e11a64181 | ||
|
|
57d9ce177a | ||
|
|
8a98949634 | ||
|
|
0eb9f48082 | ||
|
|
9db6fc0704 | ||
|
|
27fede3caa | ||
|
|
29c723f7ec | ||
|
|
2755582c47 | ||
|
|
fa73d722ea | ||
|
|
c535e4e44f | ||
|
|
0ea07b1760 | ||
|
|
03b531f179 | ||
|
|
d8570d1c2c | ||
|
|
3fe70b458d | ||
|
|
ade8b43599 | ||
|
|
d24fc3ca4e | ||
|
|
7c244bbb98 | ||
|
|
1369c46a83 | ||
|
|
deccf1cfaf | ||
|
|
44726ff65a | ||
|
|
fd0d4af21f | ||
|
|
13409ada5a | ||
|
|
e160852e4d |
13
.github/workflows/ci.yml
vendored
13
.github/workflows/ci.yml
vendored
@@ -24,25 +24,12 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
ruby-version:
|
ruby-version:
|
||||||
- "2.7"
|
|
||||||
- "3.1"
|
- "3.1"
|
||||||
- "3.2"
|
- "3.2"
|
||||||
- "3.3"
|
- "3.3"
|
||||||
gemfile:
|
gemfile:
|
||||||
- Gemfile
|
- Gemfile
|
||||||
- gemfiles/ruby_2.7.gemfile
|
|
||||||
- gemfiles/rails_edge.gemfile
|
- gemfiles/rails_edge.gemfile
|
||||||
exclude:
|
|
||||||
- ruby-version: "2.7"
|
|
||||||
gemfile: Gemfile
|
|
||||||
- ruby-version: "2.7"
|
|
||||||
gemfile: gemfiles/rails_edge.gemfile
|
|
||||||
- ruby-version: "3.1"
|
|
||||||
gemfile: gemfiles/ruby_2.7.gemfile
|
|
||||||
- ruby-version: "3.2"
|
|
||||||
gemfile: gemfiles/ruby_2.7.gemfile
|
|
||||||
- ruby-version: "3.3"
|
|
||||||
gemfile: gemfiles/ruby_2.7.gemfile
|
|
||||||
name: ${{ format('Tests (Ruby {0})', matrix.ruby-version) }}
|
name: ${{ format('Tests (Ruby {0})', matrix.ruby-version) }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Use the official Ruby 3.2.0 Alpine image as the base image
|
# Use the official Ruby 3.2.0 Alpine image as the base image
|
||||||
FROM ruby:3.2.0-alpine
|
FROM ruby:3.2.0-alpine
|
||||||
|
|
||||||
# Install docker/buildx-bin
|
# Install docker/buildx-bin
|
||||||
COPY --from=docker/buildx-bin /buildx /usr/libexec/docker/cli-plugins/docker-buildx
|
COPY --from=docker/buildx-bin /buildx /usr/libexec/docker/cli-plugins/docker-buildx
|
||||||
|
|
||||||
# Set the working directory to /kamal
|
# Set the working directory to /kamal
|
||||||
@@ -14,7 +14,7 @@ COPY Gemfile Gemfile.lock kamal.gemspec ./
|
|||||||
COPY lib/kamal/version.rb /kamal/lib/kamal/version.rb
|
COPY lib/kamal/version.rb /kamal/lib/kamal/version.rb
|
||||||
|
|
||||||
# Install system dependencies
|
# Install system dependencies
|
||||||
RUN apk add --no-cache --update build-base git docker openrc openssh-client-default \
|
RUN apk add --no-cache build-base git docker openrc openssh-client-default \
|
||||||
&& rc-update add docker boot \
|
&& rc-update add docker boot \
|
||||||
&& gem install bundler --version=2.4.3 \
|
&& gem install bundler --version=2.4.3 \
|
||||||
&& bundle install
|
&& bundle install
|
||||||
|
|||||||
11
Gemfile.lock
11
Gemfile.lock
@@ -1,7 +1,7 @@
|
|||||||
PATH
|
PATH
|
||||||
remote: .
|
remote: .
|
||||||
specs:
|
specs:
|
||||||
kamal (1.7.3)
|
kamal (2.0.0.alpha)
|
||||||
activesupport (>= 7.0)
|
activesupport (>= 7.0)
|
||||||
base64 (~> 0.2)
|
base64 (~> 0.2)
|
||||||
bcrypt_pbkdf (~> 1.0)
|
bcrypt_pbkdf (~> 1.0)
|
||||||
@@ -9,9 +9,8 @@ PATH
|
|||||||
dotenv (~> 2.8)
|
dotenv (~> 2.8)
|
||||||
ed25519 (~> 1.2)
|
ed25519 (~> 1.2)
|
||||||
net-ssh (~> 7.0)
|
net-ssh (~> 7.0)
|
||||||
sshkit (>= 1.22.2, < 2.0)
|
sshkit (>= 1.23.0, < 2.0)
|
||||||
thor (~> 1.2)
|
thor (~> 1.3)
|
||||||
x25519 (~> 1.0, >= 1.0.10)
|
|
||||||
zeitwerk (~> 2.5)
|
zeitwerk (~> 2.5)
|
||||||
|
|
||||||
GEM
|
GEM
|
||||||
@@ -154,9 +153,8 @@ GEM
|
|||||||
rubocop-rails
|
rubocop-rails
|
||||||
ruby-progressbar (1.13.0)
|
ruby-progressbar (1.13.0)
|
||||||
ruby2_keywords (0.0.5)
|
ruby2_keywords (0.0.5)
|
||||||
sshkit (1.22.2)
|
sshkit (1.23.0)
|
||||||
base64
|
base64
|
||||||
mutex_m
|
|
||||||
net-scp (>= 1.1.2)
|
net-scp (>= 1.1.2)
|
||||||
net-sftp (>= 2.1.2)
|
net-sftp (>= 2.1.2)
|
||||||
net-ssh (>= 2.8.0)
|
net-ssh (>= 2.8.0)
|
||||||
@@ -166,7 +164,6 @@ GEM
|
|||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
unicode-display_width (2.5.0)
|
unicode-display_width (2.5.0)
|
||||||
webrick (1.8.1)
|
webrick (1.8.1)
|
||||||
x25519 (1.0.10)
|
|
||||||
zeitwerk (2.6.12)
|
zeitwerk (2.6.12)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
|
|||||||
21
bin/docs
21
bin/docs
@@ -17,12 +17,14 @@ end
|
|||||||
|
|
||||||
DOCS = {
|
DOCS = {
|
||||||
"accessory" => "Accessories",
|
"accessory" => "Accessories",
|
||||||
|
"alias" => "Aliases",
|
||||||
"boot" => "Booting",
|
"boot" => "Booting",
|
||||||
"builder" => "Builders",
|
"builder" => "Builders",
|
||||||
"configuration" => "Configuration overview",
|
"configuration" => "Configuration overview",
|
||||||
"env" => "Environment variables",
|
"env" => "Environment variables",
|
||||||
"healthcheck" => "Healthchecks",
|
"healthcheck" => "Healthchecks",
|
||||||
"logging" => "Logging",
|
"logging" => "Logging",
|
||||||
|
"proxy" => "Proxy (Experimental)",
|
||||||
"registry" => "Docker Registry",
|
"registry" => "Docker Registry",
|
||||||
"role" => "Roles",
|
"role" => "Roles",
|
||||||
"servers" => "Servers",
|
"servers" => "Servers",
|
||||||
@@ -67,26 +69,27 @@ class DocWriter
|
|||||||
output.puts
|
output.puts
|
||||||
place = :new_section
|
place = :new_section
|
||||||
elsif line =~ /^ *#/
|
elsif line =~ /^ *#/
|
||||||
generate_line(line, place: place)
|
generate_line(line, heading: place == :new_section)
|
||||||
place = :in_section
|
place = :in_section
|
||||||
else
|
else
|
||||||
output.puts "```yaml"
|
output.puts "```yaml"
|
||||||
output.print line
|
output.puts line
|
||||||
place = :in_yaml
|
place = :in_yaml
|
||||||
end
|
end
|
||||||
when :in_yaml
|
when :in_yaml, :in_empty_line_yaml
|
||||||
if line =~ /^ *#/
|
if line =~ /^ *#/
|
||||||
output.puts "```"
|
output.puts "```"
|
||||||
generate_line(line, place: :new_section)
|
generate_line(line, heading: place == :in_empty_line_yaml)
|
||||||
place = :in_section
|
place = :in_section
|
||||||
|
elsif line.empty?
|
||||||
|
place = :in_empty_line_yaml
|
||||||
else
|
else
|
||||||
output.puts
|
output.puts line
|
||||||
output.print line
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
output.puts "\n```" if place == :in_yaml
|
output.puts "```" if place == :in_yaml
|
||||||
end
|
end
|
||||||
|
|
||||||
def generate_header
|
def generate_header
|
||||||
@@ -98,7 +101,7 @@ class DocWriter
|
|||||||
output.puts
|
output.puts
|
||||||
end
|
end
|
||||||
|
|
||||||
def generate_line(line, place: :in_section)
|
def generate_line(line, heading: false)
|
||||||
line = line.gsub(/^ *#\s?/, "")
|
line = line.gsub(/^ *#\s?/, "")
|
||||||
|
|
||||||
if line =~ /(.*)kamal docs ([a-z]*)(.*)/
|
if line =~ /(.*)kamal docs ([a-z]*)(.*)/
|
||||||
@@ -109,7 +112,7 @@ class DocWriter
|
|||||||
line = "#{$1}[#{titlify($2.split("/").last)}](#{$2})#{$3}"
|
line = "#{$1}[#{titlify($2.split("/").last)}](#{$2})#{$3}"
|
||||||
end
|
end
|
||||||
|
|
||||||
if place == :new_section
|
if heading
|
||||||
output.puts "## [#{line}](##{linkify(line)})"
|
output.puts "## [#{line}](##{linkify(line)})"
|
||||||
else
|
else
|
||||||
output.puts line
|
output.puts line
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
source 'https://rubygems.org'
|
|
||||||
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
|
|
||||||
|
|
||||||
gemspec path: "../"
|
|
||||||
|
|
||||||
gem "nokogiri", "~> 1.15.0"
|
|
||||||
@@ -12,13 +12,12 @@ Gem::Specification.new do |spec|
|
|||||||
spec.executables = %w[ kamal ]
|
spec.executables = %w[ kamal ]
|
||||||
|
|
||||||
spec.add_dependency "activesupport", ">= 7.0"
|
spec.add_dependency "activesupport", ">= 7.0"
|
||||||
spec.add_dependency "sshkit", ">= 1.22.2", "< 2.0"
|
spec.add_dependency "sshkit", ">= 1.23.0", "< 2.0"
|
||||||
spec.add_dependency "net-ssh", "~> 7.0"
|
spec.add_dependency "net-ssh", "~> 7.0"
|
||||||
spec.add_dependency "thor", "~> 1.2"
|
spec.add_dependency "thor", "~> 1.3"
|
||||||
spec.add_dependency "dotenv", "~> 2.8"
|
spec.add_dependency "dotenv", "~> 2.8"
|
||||||
spec.add_dependency "zeitwerk", "~> 2.5"
|
spec.add_dependency "zeitwerk", "~> 2.5"
|
||||||
spec.add_dependency "ed25519", "~> 1.2"
|
spec.add_dependency "ed25519", "~> 1.2"
|
||||||
spec.add_dependency "x25519", "~> 1.0", ">= 1.0.10"
|
|
||||||
spec.add_dependency "bcrypt_pbkdf", "~> 1.0"
|
spec.add_dependency "bcrypt_pbkdf", "~> 1.0"
|
||||||
spec.add_dependency "concurrent-ruby", "~> 1.2"
|
spec.add_dependency "concurrent-ruby", "~> 1.2"
|
||||||
spec.add_dependency "base64", "~> 0.2"
|
spec.add_dependency "base64", "~> 0.2"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
module Kamal::Cli
|
module Kamal::Cli
|
||||||
|
class BootError < StandardError; end
|
||||||
class HookError < StandardError; end
|
class HookError < StandardError; end
|
||||||
class LockError < StandardError; end
|
class LockError < StandardError; end
|
||||||
end
|
end
|
||||||
|
|||||||
9
lib/kamal/cli/alias/command.rb
Normal file
9
lib/kamal/cli/alias/command.rb
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
class Kamal::Cli::Alias::Command < Thor::DynamicCommand
|
||||||
|
def run(instance, args = [])
|
||||||
|
if (_alias = KAMAL.config.aliases[name])
|
||||||
|
Kamal::Cli::Main.start(Shellwords.split(_alias.command) + ARGV[1..-1])
|
||||||
|
else
|
||||||
|
super
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -38,8 +38,17 @@ class Kamal::Cli::App < Kamal::Cli::Base
|
|||||||
roles = KAMAL.roles_on(host)
|
roles = KAMAL.roles_on(host)
|
||||||
|
|
||||||
roles.each do |role|
|
roles.each do |role|
|
||||||
|
app = KAMAL.app(role: role, host: host)
|
||||||
execute *KAMAL.auditor.record("Started app version #{KAMAL.config.version}"), verbosity: :debug
|
execute *KAMAL.auditor.record("Started app version #{KAMAL.config.version}"), verbosity: :debug
|
||||||
execute *KAMAL.app(role: role, host: host).start, raise_on_non_zero_exit: false
|
execute *app.start, raise_on_non_zero_exit: false
|
||||||
|
|
||||||
|
if role.running_traefik? && KAMAL.proxy_host?(host)
|
||||||
|
version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
|
||||||
|
endpoint = capture_with_info(*app.container_endpoint(version: version)).strip
|
||||||
|
raise Kamal::Cli::BootError, "Failed to get endpoint for #{role} on #{host}, did the container boot?" if endpoint.empty?
|
||||||
|
|
||||||
|
execute *KAMAL.proxy.deploy(role.container_prefix, target: endpoint)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -52,8 +61,18 @@ class Kamal::Cli::App < Kamal::Cli::Base
|
|||||||
roles = KAMAL.roles_on(host)
|
roles = KAMAL.roles_on(host)
|
||||||
|
|
||||||
roles.each do |role|
|
roles.each do |role|
|
||||||
|
app = KAMAL.app(role: role, host: host)
|
||||||
execute *KAMAL.auditor.record("Stopped app", role: role), verbosity: :debug
|
execute *KAMAL.auditor.record("Stopped app", role: role), verbosity: :debug
|
||||||
execute *KAMAL.app(role: role, host: host).stop, raise_on_non_zero_exit: false
|
|
||||||
|
if role.running_traefik? && KAMAL.proxy_host?(host)
|
||||||
|
version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
|
||||||
|
endpoint = capture_with_info(*app.container_endpoint(version: version)).strip
|
||||||
|
if endpoint.present?
|
||||||
|
execute *KAMAL.proxy.remove(role.container_prefix, target: endpoint), raise_on_non_zero_exit: false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
execute *app.stop, raise_on_non_zero_exit: false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -71,11 +90,12 @@ class Kamal::Cli::App < Kamal::Cli::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
desc "exec [CMD]", "Execute a custom command on servers within the app container (use --help to show options)"
|
desc "exec [CMD...]", "Execute a custom command on servers within the app container (use --help to show options)"
|
||||||
option :interactive, aliases: "-i", type: :boolean, default: false, desc: "Execute command over ssh for an interactive shell (use for console/bash)"
|
option :interactive, aliases: "-i", type: :boolean, default: false, desc: "Execute command over ssh for an interactive shell (use for console/bash)"
|
||||||
option :reuse, type: :boolean, default: false, desc: "Reuse currently running container instead of starting a new one"
|
option :reuse, type: :boolean, default: false, desc: "Reuse currently running container instead of starting a new one"
|
||||||
option :env, aliases: "-e", type: :hash, desc: "Set environment variables for the command"
|
option :env, aliases: "-e", type: :hash, desc: "Set environment variables for the command"
|
||||||
def exec(cmd)
|
def exec(*cmd)
|
||||||
|
cmd = Kamal::Utils.join_commands(cmd)
|
||||||
env = options[:env]
|
env = options[:env]
|
||||||
case
|
case
|
||||||
when options[:interactive] && options[:reuse]
|
when options[:interactive] && options[:reuse]
|
||||||
|
|||||||
@@ -45,11 +45,22 @@ class Kamal::Cli::App::Boot
|
|||||||
|
|
||||||
def start_new_version
|
def start_new_version
|
||||||
audit "Booted app version #{version}"
|
audit "Booted app version #{version}"
|
||||||
|
|
||||||
execute *app.tie_cord(role.cord_host_file) if uses_cord?
|
|
||||||
hostname = "#{host.to_s[0...51].gsub(/\.+$/, '')}-#{SecureRandom.hex(6)}"
|
hostname = "#{host.to_s[0...51].gsub(/\.+$/, '')}-#{SecureRandom.hex(6)}"
|
||||||
execute *app.run(hostname: hostname)
|
|
||||||
Kamal::Cli::Healthcheck::Poller.wait_for_healthy(pause_after_ready: true) { capture_with_info(*app.status(version: version)) }
|
if proxy_host?
|
||||||
|
execute *app.run_for_proxy(hostname: hostname)
|
||||||
|
if running_traefik?
|
||||||
|
endpoint = capture_with_info(*app.container_endpoint(version: version)).strip
|
||||||
|
raise Kamal::Cli::BootError, "Failed to get endpoint for #{role} on #{host}, did the container boot?" if endpoint.empty?
|
||||||
|
execute *KAMAL.proxy.deploy(role.container_prefix, target: endpoint)
|
||||||
|
else
|
||||||
|
Kamal::Cli::Healthcheck::Poller.wait_for_healthy(pause_after_ready: true) { capture_with_info(*app.status(version: version)) }
|
||||||
|
end
|
||||||
|
else
|
||||||
|
execute *app.tie_cord(role.cord_host_file) if uses_cord?
|
||||||
|
execute *app.run(hostname: hostname)
|
||||||
|
Kamal::Cli::Healthcheck::Poller.wait_for_healthy(pause_after_ready: true) { capture_with_info(*app.status(version: version)) }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def stop_new_version
|
def stop_new_version
|
||||||
@@ -57,7 +68,7 @@ class Kamal::Cli::App::Boot
|
|||||||
end
|
end
|
||||||
|
|
||||||
def stop_old_version(version)
|
def stop_old_version(version)
|
||||||
if uses_cord?
|
if uses_cord? && !proxy_host?
|
||||||
cord = capture_with_info(*app.cord(version: version), raise_on_non_zero_exit: false).strip
|
cord = capture_with_info(*app.cord(version: version), raise_on_non_zero_exit: false).strip
|
||||||
if cord.present?
|
if cord.present?
|
||||||
execute *app.cut_cord(cord)
|
execute *app.cut_cord(cord)
|
||||||
@@ -116,4 +127,8 @@ class Kamal::Cli::App::Boot
|
|||||||
def queuer?
|
def queuer?
|
||||||
barrier && !barrier_role?
|
barrier && !barrier_role?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def proxy_host?
|
||||||
|
KAMAL.proxy_host?(host)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ module Kamal::Cli
|
|||||||
class Base < Thor
|
class Base < Thor
|
||||||
include SSHKit::DSL
|
include SSHKit::DSL
|
||||||
|
|
||||||
def self.exit_on_failure?() true end
|
def self.exit_on_failure?() false end
|
||||||
|
def self.dynamic_command_class() Kamal::Cli::Alias::Command end
|
||||||
|
|
||||||
class_option :verbose, type: :boolean, aliases: "-v", desc: "Detailed logging"
|
class_option :verbose, type: :boolean, aliases: "-v", desc: "Detailed logging"
|
||||||
class_option :quiet, type: :boolean, aliases: "-q", desc: "Minimal logging"
|
class_option :quiet, type: :boolean, aliases: "-q", desc: "Minimal logging"
|
||||||
@@ -22,15 +23,26 @@ module Kamal::Cli
|
|||||||
|
|
||||||
class_option :skip_hooks, aliases: "-H", type: :boolean, default: false, desc: "Don't run hooks"
|
class_option :skip_hooks, aliases: "-H", type: :boolean, default: false, desc: "Don't run hooks"
|
||||||
|
|
||||||
def initialize(*)
|
def initialize(args = [], local_options = {}, config = {})
|
||||||
super
|
if config[:current_command].is_a?(Kamal::Cli::Alias::Command)
|
||||||
|
# When Thor generates a dynamic command, it doesn't attempt to parse the arguments.
|
||||||
|
# For our purposes, it means the arguments are passed in args rather than local_options.
|
||||||
|
super([], args, config)
|
||||||
|
else
|
||||||
|
super
|
||||||
|
end
|
||||||
@original_env = ENV.to_h.dup
|
@original_env = ENV.to_h.dup
|
||||||
load_envs
|
load_env
|
||||||
initialize_commander(options_with_subcommand_class_options)
|
initialize_commander(options_with_subcommand_class_options)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def load_envs
|
def reload_env
|
||||||
|
reset_env
|
||||||
|
load_env
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_env
|
||||||
if destination = options[:destination]
|
if destination = options[:destination]
|
||||||
Dotenv.load(".env.#{destination}", ".env")
|
Dotenv.load(".env.#{destination}", ".env")
|
||||||
else
|
else
|
||||||
@@ -38,10 +50,27 @@ module Kamal::Cli
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def reload_envs
|
def reset_env
|
||||||
|
replace_env @original_env
|
||||||
|
end
|
||||||
|
|
||||||
|
def replace_env(env)
|
||||||
ENV.clear
|
ENV.clear
|
||||||
ENV.update(@original_env)
|
ENV.update(env)
|
||||||
load_envs
|
end
|
||||||
|
|
||||||
|
def with_original_env
|
||||||
|
keeping_current_env do
|
||||||
|
reset_env
|
||||||
|
yield
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def keeping_current_env
|
||||||
|
current_env = ENV.to_h.dup
|
||||||
|
yield
|
||||||
|
ensure
|
||||||
|
replace_env(current_env)
|
||||||
end
|
end
|
||||||
|
|
||||||
def options_with_subcommand_class_options
|
def options_with_subcommand_class_options
|
||||||
|
|||||||
@@ -59,11 +59,14 @@ class Kamal::Cli::Build < Kamal::Cli::Base
|
|||||||
|
|
||||||
desc "pull", "Pull app image from registry onto servers"
|
desc "pull", "Pull app image from registry onto servers"
|
||||||
def pull
|
def pull
|
||||||
on(KAMAL.hosts) do
|
if (first_hosts = mirror_hosts).any?
|
||||||
execute *KAMAL.auditor.record("Pulled image with version #{KAMAL.config.version}"), verbosity: :debug
|
# Pull on a single host per mirror first to seed them
|
||||||
execute *KAMAL.builder.clean, raise_on_non_zero_exit: false
|
say "Pulling image on #{first_hosts.join(", ")} to seed the #{"mirror".pluralize(first_hosts.count)}...", :magenta
|
||||||
execute *KAMAL.builder.pull
|
pull_on_hosts(first_hosts)
|
||||||
execute *KAMAL.builder.validate_image
|
say "Pulling image on remaining hosts...", :magenta
|
||||||
|
pull_on_hosts(KAMAL.hosts - first_hosts)
|
||||||
|
else
|
||||||
|
pull_on_hosts(KAMAL.hosts)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -131,4 +134,28 @@ class Kamal::Cli::Build < Kamal::Cli::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def mirror_hosts
|
||||||
|
if KAMAL.hosts.many?
|
||||||
|
mirror_hosts = Concurrent::Hash.new
|
||||||
|
on(KAMAL.hosts) do |host|
|
||||||
|
first_mirror = capture_with_info(*KAMAL.builder.first_mirror).strip.presence
|
||||||
|
mirror_hosts[first_mirror] ||= host.to_s if first_mirror
|
||||||
|
rescue SSHKit::Command::Failed => e
|
||||||
|
raise unless e.message =~ /error calling index: reflect: slice index out of range/
|
||||||
|
end
|
||||||
|
mirror_hosts.values
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def pull_on_hosts(hosts)
|
||||||
|
on(hosts) do
|
||||||
|
execute *KAMAL.auditor.record("Pulled image with version #{KAMAL.config.version}"), verbosity: :debug
|
||||||
|
execute *KAMAL.builder.clean, raise_on_non_zero_exit: false
|
||||||
|
execute *KAMAL.builder.pull
|
||||||
|
execute *KAMAL.builder.validate_image
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -39,8 +39,13 @@ class Kamal::Cli::Main < Kamal::Cli::Base
|
|||||||
with_lock do
|
with_lock do
|
||||||
run_hook "pre-deploy"
|
run_hook "pre-deploy"
|
||||||
|
|
||||||
say "Ensure Traefik is running...", :magenta
|
if KAMAL.config.proxy.enabled?
|
||||||
invoke "kamal:cli:traefik:boot", [], invoke_options
|
say "Ensure Traefik/kamal-proxy is running...", :magenta
|
||||||
|
invoke "kamal:cli:proxy:boot", [], invoke_options
|
||||||
|
else
|
||||||
|
say "Ensure Traefik is running...", :magenta
|
||||||
|
invoke "kamal:cli:traefik:boot", [], invoke_options
|
||||||
|
end
|
||||||
|
|
||||||
say "Detect stale containers...", :magenta
|
say "Detect stale containers...", :magenta
|
||||||
invoke "kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true)
|
invoke "kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true)
|
||||||
@@ -108,7 +113,11 @@ class Kamal::Cli::Main < Kamal::Cli::Base
|
|||||||
|
|
||||||
desc "details", "Show details about all containers"
|
desc "details", "Show details about all containers"
|
||||||
def details
|
def details
|
||||||
invoke "kamal:cli:traefik:details"
|
if KAMAL.config.proxy.enabled?
|
||||||
|
invoke "kamal:cli:proxy:details"
|
||||||
|
else
|
||||||
|
invoke "kamal:cli:traefik:details"
|
||||||
|
end
|
||||||
invoke "kamal:cli:app:details"
|
invoke "kamal:cli:app:details"
|
||||||
invoke "kamal:cli:accessory:details", [ "all" ]
|
invoke "kamal:cli:accessory:details", [ "all" ]
|
||||||
end
|
end
|
||||||
@@ -191,10 +200,12 @@ class Kamal::Cli::Main < Kamal::Cli::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
if Pathname.new(File.expand_path(env_template_path)).exist?
|
if Pathname.new(File.expand_path(env_template_path)).exist?
|
||||||
File.write(env_path, ERB.new(File.read(env_template_path), trim_mode: "-").result, perm: 0600)
|
# Ensure existing env doesn't pollute template evaluation
|
||||||
|
content = with_original_env { ERB.new(File.read(env_template_path), trim_mode: "-").result }
|
||||||
|
File.write(env_path, content, perm: 0600)
|
||||||
|
|
||||||
unless options[:skip_push]
|
unless options[:skip_push]
|
||||||
reload_envs
|
reload_env
|
||||||
invoke "kamal:cli:env:push", options
|
invoke "kamal:cli:env:push", options
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
@@ -207,7 +218,11 @@ class Kamal::Cli::Main < Kamal::Cli::Base
|
|||||||
def remove
|
def remove
|
||||||
confirming "This will remove all containers and images. Are you sure?" do
|
confirming "This will remove all containers and images. Are you sure?" do
|
||||||
with_lock do
|
with_lock do
|
||||||
invoke "kamal:cli:traefik:remove", [], options.without(:confirmed)
|
if KAMAL.config.proxy.enabled?
|
||||||
|
invoke "kamal:cli:proxy:remove", [], options.without(:confirmed)
|
||||||
|
else
|
||||||
|
invoke "kamal:cli:traefik:remove", [], options.without(:confirmed)
|
||||||
|
end
|
||||||
invoke "kamal:cli:app:remove", [], options.without(:confirmed)
|
invoke "kamal:cli:app:remove", [], options.without(:confirmed)
|
||||||
invoke "kamal:cli:accessory:remove", [ "all" ], options
|
invoke "kamal:cli:accessory:remove", [ "all" ], options
|
||||||
invoke "kamal:cli:registry:logout", [], options.without(:confirmed).merge(skip_local: true)
|
invoke "kamal:cli:registry:logout", [], options.without(:confirmed).merge(skip_local: true)
|
||||||
@@ -235,6 +250,9 @@ class Kamal::Cli::Main < Kamal::Cli::Base
|
|||||||
desc "lock", "Manage the deploy lock"
|
desc "lock", "Manage the deploy lock"
|
||||||
subcommand "lock", Kamal::Cli::Lock
|
subcommand "lock", Kamal::Cli::Lock
|
||||||
|
|
||||||
|
desc "proxy", "Prune old application images and containers"
|
||||||
|
subcommand "proxy", Kamal::Cli::Proxy
|
||||||
|
|
||||||
desc "prune", "Prune old application images and containers"
|
desc "prune", "Prune old application images and containers"
|
||||||
subcommand "prune", Kamal::Cli::Prune
|
subcommand "prune", Kamal::Cli::Prune
|
||||||
|
|
||||||
|
|||||||
160
lib/kamal/cli/proxy.rb
Normal file
160
lib/kamal/cli/proxy.rb
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
class Kamal::Cli::Proxy < Kamal::Cli::Base
|
||||||
|
desc "boot", "Boot proxy on servers"
|
||||||
|
def boot
|
||||||
|
raise_unless_kamal_proxy_enabled!
|
||||||
|
with_lock do
|
||||||
|
on(KAMAL.traefik_hosts) do |host|
|
||||||
|
execute *KAMAL.registry.login
|
||||||
|
execute *KAMAL.traefik_or_proxy(host).start_or_run
|
||||||
|
end
|
||||||
|
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"
|
||||||
|
def reboot
|
||||||
|
raise_unless_kamal_proxy_enabled!
|
||||||
|
confirming "This will cause a brief outage on each host. Are you sure?" do
|
||||||
|
with_lock do
|
||||||
|
host_groups = options[:rolling] ? KAMAL.traefik_hosts : [ KAMAL.traefik_hosts ]
|
||||||
|
host_groups.each do |hosts|
|
||||||
|
host_list = Array(hosts).join(",")
|
||||||
|
run_hook "pre-traefik-reboot", hosts: host_list
|
||||||
|
on(hosts) do |host|
|
||||||
|
execute *KAMAL.auditor.record("Rebooted proxy"), verbosity: :debug
|
||||||
|
execute *KAMAL.registry.login
|
||||||
|
|
||||||
|
"Stopping and removing Traefik on #{host}, if running..."
|
||||||
|
execute *KAMAL.traefik.stop, raise_on_non_zero_exit: false
|
||||||
|
execute *KAMAL.traefik.remove_container
|
||||||
|
|
||||||
|
"Stopping and removing kamal-proxy on #{host}, if running..."
|
||||||
|
execute *KAMAL.proxy.stop, raise_on_non_zero_exit: false
|
||||||
|
execute *KAMAL.proxy.remove_container
|
||||||
|
|
||||||
|
execute *KAMAL.traefik_or_proxy(host).run
|
||||||
|
|
||||||
|
if KAMAL.proxy_host?(host)
|
||||||
|
KAMAL.roles_on(host).select(&:running_traefik?).each do |role|
|
||||||
|
app = KAMAL.app(role: role, host: host)
|
||||||
|
|
||||||
|
version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
|
||||||
|
endpoint = capture_with_info(*app.container_endpoint(version: version)).strip
|
||||||
|
|
||||||
|
if endpoint.present?
|
||||||
|
info "Deploying #{endpoint} for role `#{role}` on #{host}..."
|
||||||
|
execute *KAMAL.proxy.deploy(role.container_prefix, target: endpoint)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
run_hook "post-traefik-reboot", hosts: host_list
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "start", "Start existing proxy container on servers"
|
||||||
|
def start
|
||||||
|
raise_unless_kamal_proxy_enabled!
|
||||||
|
with_lock do
|
||||||
|
on(KAMAL.traefik_hosts) do |host|
|
||||||
|
execute *KAMAL.auditor.record("Started proxy"), verbosity: :debug
|
||||||
|
execute *KAMAL.traefik_or_proxy(host).start
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "stop", "Stop existing proxy container on servers"
|
||||||
|
def stop
|
||||||
|
raise_unless_kamal_proxy_enabled!
|
||||||
|
with_lock do
|
||||||
|
on(KAMAL.traefik_hosts) do |host|
|
||||||
|
execute *KAMAL.auditor.record("Stopped proxy"), verbosity: :debug
|
||||||
|
execute *KAMAL.traefik_or_proxy(host).stop, raise_on_non_zero_exit: false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "restart", "Restart existing proxy container on servers"
|
||||||
|
def restart
|
||||||
|
raise_unless_kamal_proxy_enabled!
|
||||||
|
with_lock do
|
||||||
|
stop
|
||||||
|
start
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "details", "Show details about proxy container from servers"
|
||||||
|
def details
|
||||||
|
raise_unless_kamal_proxy_enabled!
|
||||||
|
on(KAMAL.traefik_hosts) { |host| puts_by_host host, capture_with_info(*KAMAL.traefik_or_proxy(host).info), type: "Proxy" }
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "logs", "Show log lines from proxy on servers"
|
||||||
|
option :since, aliases: "-s", desc: "Show logs since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)"
|
||||||
|
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)"
|
||||||
|
def logs
|
||||||
|
raise_unless_kamal_proxy_enabled!
|
||||||
|
grep = options[:grep]
|
||||||
|
|
||||||
|
if options[:follow]
|
||||||
|
run_locally do
|
||||||
|
info "Following logs on #{KAMAL.primary_host}..."
|
||||||
|
info KAMAL.traefik_or_proxy(KAMAL.primary_host).follow_logs(host: KAMAL.primary_host, grep: grep)
|
||||||
|
exec KAMAL.traefik_or_proxy(KAMAL.primary_host).follow_logs(host: KAMAL.primary_host, 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.traefik_hosts) do |host|
|
||||||
|
puts_by_host host, capture(*KAMAL.traefik_or_proxy(host).logs(since: since, lines: lines, grep: grep)), type: "Proxy"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "remove", "Remove proxy container and image from servers"
|
||||||
|
def remove
|
||||||
|
raise_unless_kamal_proxy_enabled!
|
||||||
|
with_lock do
|
||||||
|
stop
|
||||||
|
remove_container
|
||||||
|
remove_image
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "remove_container", "Remove proxy container from servers", hide: true
|
||||||
|
def remove_container
|
||||||
|
raise_unless_kamal_proxy_enabled!
|
||||||
|
with_lock do
|
||||||
|
on(KAMAL.traefik_hosts) do
|
||||||
|
execute *KAMAL.auditor.record("Removed proxy container"), verbosity: :debug
|
||||||
|
execute *KAMAL.proxy.remove_container
|
||||||
|
execute *KAMAL.traefik.remove_container
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "remove_image", "Remove proxy image from servers", hide: true
|
||||||
|
def remove_image
|
||||||
|
raise_unless_kamal_proxy_enabled!
|
||||||
|
with_lock do
|
||||||
|
on(KAMAL.traefik_hosts) do
|
||||||
|
execute *KAMAL.auditor.record("Removed proxy image"), verbosity: :debug
|
||||||
|
execute *KAMAL.proxy.remove_image
|
||||||
|
execute *KAMAL.traefik.remove_image
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def raise_unless_kamal_proxy_enabled!
|
||||||
|
unless KAMAL.config.proxy.enabled?
|
||||||
|
raise "kamal proxy commands are disabled unless experimental proxy support is enabled. Use `kamal traefik` commands instead."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
class Kamal::Cli::Server < Kamal::Cli::Base
|
class Kamal::Cli::Server < Kamal::Cli::Base
|
||||||
desc "exec", "Run a custom command on the server (use --help to show options)"
|
desc "exec", "Run a custom command on the server (use --help to show options)"
|
||||||
option :interactive, type: :boolean, aliases: "-i", default: false, desc: "Run the command interactively (use for console/bash)"
|
option :interactive, type: :boolean, aliases: "-i", default: false, desc: "Run the command interactively (use for console/bash)"
|
||||||
def exec(cmd)
|
def exec(*cmd)
|
||||||
|
cmd = Kamal::Utils.join_commands(cmd)
|
||||||
hosts = KAMAL.hosts | KAMAL.accessory_hosts
|
hosts = KAMAL.hosts | KAMAL.accessory_hosts
|
||||||
|
|
||||||
case
|
case
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
class Kamal::Cli::Traefik < Kamal::Cli::Base
|
class Kamal::Cli::Traefik < Kamal::Cli::Base
|
||||||
desc "boot", "Boot Traefik on servers"
|
desc "boot", "Boot Traefik on servers"
|
||||||
def boot
|
def boot
|
||||||
|
raise_if_kamal_proxy_enabled!
|
||||||
with_lock do
|
with_lock do
|
||||||
on(KAMAL.traefik_hosts) do
|
on(KAMAL.traefik_hosts) do
|
||||||
execute *KAMAL.registry.login
|
execute *KAMAL.registry.login
|
||||||
@@ -13,6 +14,7 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
|
|||||||
option :rolling, type: :boolean, default: false, desc: "Reboot traefik on hosts in sequence, rather than in parallel"
|
option :rolling, type: :boolean, default: false, desc: "Reboot traefik on hosts in sequence, rather than in parallel"
|
||||||
option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
|
option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
|
||||||
def reboot
|
def reboot
|
||||||
|
raise_if_kamal_proxy_enabled!
|
||||||
confirming "This will cause a brief outage on each host. Are you sure?" do
|
confirming "This will cause a brief outage on each host. Are you sure?" do
|
||||||
with_lock do
|
with_lock do
|
||||||
host_groups = options[:rolling] ? KAMAL.traefik_hosts : [ KAMAL.traefik_hosts ]
|
host_groups = options[:rolling] ? KAMAL.traefik_hosts : [ KAMAL.traefik_hosts ]
|
||||||
@@ -34,6 +36,7 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
|
|||||||
|
|
||||||
desc "start", "Start existing Traefik container on servers"
|
desc "start", "Start existing Traefik container on servers"
|
||||||
def start
|
def start
|
||||||
|
raise_if_kamal_proxy_enabled!
|
||||||
with_lock do
|
with_lock do
|
||||||
on(KAMAL.traefik_hosts) do
|
on(KAMAL.traefik_hosts) do
|
||||||
execute *KAMAL.auditor.record("Started traefik"), verbosity: :debug
|
execute *KAMAL.auditor.record("Started traefik"), verbosity: :debug
|
||||||
@@ -44,6 +47,7 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
|
|||||||
|
|
||||||
desc "stop", "Stop existing Traefik container on servers"
|
desc "stop", "Stop existing Traefik container on servers"
|
||||||
def stop
|
def stop
|
||||||
|
raise_if_kamal_proxy_enabled!
|
||||||
with_lock do
|
with_lock do
|
||||||
on(KAMAL.traefik_hosts) do
|
on(KAMAL.traefik_hosts) do
|
||||||
execute *KAMAL.auditor.record("Stopped traefik"), verbosity: :debug
|
execute *KAMAL.auditor.record("Stopped traefik"), verbosity: :debug
|
||||||
@@ -54,6 +58,7 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
|
|||||||
|
|
||||||
desc "restart", "Restart existing Traefik container on servers"
|
desc "restart", "Restart existing Traefik container on servers"
|
||||||
def restart
|
def restart
|
||||||
|
raise_if_kamal_proxy_enabled!
|
||||||
with_lock do
|
with_lock do
|
||||||
stop
|
stop
|
||||||
start
|
start
|
||||||
@@ -62,6 +67,7 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
|
|||||||
|
|
||||||
desc "details", "Show details about Traefik container from servers"
|
desc "details", "Show details about Traefik container from servers"
|
||||||
def details
|
def details
|
||||||
|
raise_if_kamal_proxy_enabled!
|
||||||
on(KAMAL.traefik_hosts) { |host| puts_by_host host, capture_with_info(*KAMAL.traefik.info), type: "Traefik" }
|
on(KAMAL.traefik_hosts) { |host| puts_by_host host, capture_with_info(*KAMAL.traefik.info), type: "Traefik" }
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -72,6 +78,7 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
|
|||||||
option :grep_options, aliases: "-o", desc: "Additional options supplied to grep"
|
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 :follow, aliases: "-f", desc: "Follow logs on primary server (or specific host set by --hosts)"
|
||||||
def logs
|
def logs
|
||||||
|
raise_if_kamal_proxy_enabled!
|
||||||
grep = options[:grep]
|
grep = options[:grep]
|
||||||
grep_options = options[:grep_options]
|
grep_options = options[:grep_options]
|
||||||
|
|
||||||
@@ -93,6 +100,7 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
|
|||||||
|
|
||||||
desc "remove", "Remove Traefik container and image from servers"
|
desc "remove", "Remove Traefik container and image from servers"
|
||||||
def remove
|
def remove
|
||||||
|
raise_if_kamal_proxy_enabled!
|
||||||
with_lock do
|
with_lock do
|
||||||
stop
|
stop
|
||||||
remove_container
|
remove_container
|
||||||
@@ -102,6 +110,7 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
|
|||||||
|
|
||||||
desc "remove_container", "Remove Traefik container from servers", hide: true
|
desc "remove_container", "Remove Traefik container from servers", hide: true
|
||||||
def remove_container
|
def remove_container
|
||||||
|
raise_if_kamal_proxy_enabled!
|
||||||
with_lock do
|
with_lock do
|
||||||
on(KAMAL.traefik_hosts) do
|
on(KAMAL.traefik_hosts) do
|
||||||
execute *KAMAL.auditor.record("Removed traefik container"), verbosity: :debug
|
execute *KAMAL.auditor.record("Removed traefik container"), verbosity: :debug
|
||||||
@@ -112,6 +121,7 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
|
|||||||
|
|
||||||
desc "remove_image", "Remove Traefik image from servers", hide: true
|
desc "remove_image", "Remove Traefik image from servers", hide: true
|
||||||
def remove_image
|
def remove_image
|
||||||
|
raise_if_kamal_proxy_enabled!
|
||||||
with_lock do
|
with_lock do
|
||||||
on(KAMAL.traefik_hosts) do
|
on(KAMAL.traefik_hosts) do
|
||||||
execute *KAMAL.auditor.record("Removed traefik image"), verbosity: :debug
|
execute *KAMAL.auditor.record("Removed traefik image"), verbosity: :debug
|
||||||
@@ -119,4 +129,11 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def raise_if_kamal_proxy_enabled!
|
||||||
|
if KAMAL.config.proxy.enabled?
|
||||||
|
raise "kamal traefik commands are disabled when experimental proxy support is enabled. Use `kamal proxy` commands instead."
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ require "active_support/core_ext/module/delegation"
|
|||||||
|
|
||||||
class Kamal::Commander
|
class Kamal::Commander
|
||||||
attr_accessor :verbosity, :holding_lock, :connected
|
attr_accessor :verbosity, :holding_lock, :connected
|
||||||
delegate :hosts, :roles, :primary_host, :primary_role, :roles_on, :traefik_hosts, :accessory_hosts, to: :specifics
|
delegate :hosts, :roles, :primary_host, :primary_role, :roles_on, :traefik_hosts, :proxy_hosts, :proxy_host?, :accessory_hosts, to: :specifics
|
||||||
|
|
||||||
def initialize
|
def initialize
|
||||||
self.verbosity = :info
|
self.verbosity = :info
|
||||||
@@ -27,7 +27,11 @@ class Kamal::Commander
|
|||||||
|
|
||||||
def specific_primary!
|
def specific_primary!
|
||||||
@specifics = nil
|
@specifics = nil
|
||||||
self.specific_hosts = [ config.primary_host ]
|
if specific_roles.present?
|
||||||
|
self.specific_hosts = [ specific_roles.first.primary_host ]
|
||||||
|
else
|
||||||
|
self.specific_hosts = [ config.primary_host ]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def specific_roles=(role_names)
|
def specific_roles=(role_names)
|
||||||
@@ -97,6 +101,10 @@ class Kamal::Commander
|
|||||||
@lock ||= Kamal::Commands::Lock.new(config)
|
@lock ||= Kamal::Commands::Lock.new(config)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def proxy
|
||||||
|
@proxy ||= Kamal::Commands::Proxy.new(config)
|
||||||
|
end
|
||||||
|
|
||||||
def prune
|
def prune
|
||||||
@prune ||= Kamal::Commands::Prune.new(config)
|
@prune ||= Kamal::Commands::Prune.new(config)
|
||||||
end
|
end
|
||||||
@@ -113,6 +121,14 @@ class Kamal::Commander
|
|||||||
@traefik ||= Kamal::Commands::Traefik.new(config)
|
@traefik ||= Kamal::Commands::Traefik.new(config)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def alias(name)
|
||||||
|
config.aliases[name]
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
def traefik_or_proxy(host)
|
||||||
|
proxy_host?(host) ? proxy : traefik
|
||||||
|
end
|
||||||
|
|
||||||
def with_verbosity(level)
|
def with_verbosity(level)
|
||||||
old_level = self.verbosity
|
old_level = self.verbosity
|
||||||
|
|||||||
@@ -22,6 +22,15 @@ class Kamal::Commander::Specifics
|
|||||||
config.traefik_hosts & specified_hosts
|
config.traefik_hosts & specified_hosts
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def proxy_hosts
|
||||||
|
traefik_hosts & config.proxy_hosts
|
||||||
|
end
|
||||||
|
|
||||||
|
def proxy_host?(host)
|
||||||
|
host = host.hostname if host.is_a?(SSHKit::Host)
|
||||||
|
proxy_hosts.include?(host)
|
||||||
|
end
|
||||||
|
|
||||||
def accessory_hosts
|
def accessory_hosts
|
||||||
specific_hosts || config.accessories.flat_map(&:hosts)
|
specific_hosts || config.accessories.flat_map(&:hosts)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -30,6 +30,24 @@ class Kamal::Commands::App < Kamal::Commands::Base
|
|||||||
role.cmd
|
role.cmd
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def run_for_proxy(hostname: nil)
|
||||||
|
docker :run,
|
||||||
|
"--detach",
|
||||||
|
"--restart unless-stopped",
|
||||||
|
"--name", container_name,
|
||||||
|
*([ "--hostname", hostname ] if hostname),
|
||||||
|
"-e", "KAMAL_CONTAINER_NAME=\"#{container_name}\"",
|
||||||
|
"-e", "KAMAL_VERSION=\"#{config.version}\"",
|
||||||
|
*role.env_args(host),
|
||||||
|
*role.logging_args,
|
||||||
|
*config.volume_args,
|
||||||
|
*role.asset_volume_args,
|
||||||
|
*role.label_args_for_proxy,
|
||||||
|
*role.option_args,
|
||||||
|
config.absolute_image,
|
||||||
|
role.cmd
|
||||||
|
end
|
||||||
|
|
||||||
def start
|
def start
|
||||||
docker :start, container_name
|
docker :start, container_name
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -28,4 +28,11 @@ module Kamal::Commands::App::Containers
|
|||||||
container_id_for(container_name: container_name(version)),
|
container_id_for(container_name: container_name(version)),
|
||||||
xargs(docker(:inspect, "--format", DOCKER_HEALTH_LOG_FORMAT))
|
xargs(docker(:inspect, "--format", DOCKER_HEALTH_LOG_FORMAT))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def container_endpoint(version:)
|
||||||
|
pipe \
|
||||||
|
container_id_for(container_name: container_name(version)),
|
||||||
|
xargs(docker(:inspect, "--format", "'{{.NetworkSettings.IPAddress}}{{range $k, $v := .NetworkSettings.Ports}}{{printf \":%s\" $k}}{{break}}{{end}}'")),
|
||||||
|
[ :sed, "-e", "'s/\\/tcp$//'" ]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ require "active_support/core_ext/string/filters"
|
|||||||
|
|
||||||
class Kamal::Commands::Builder < Kamal::Commands::Base
|
class Kamal::Commands::Builder < Kamal::Commands::Base
|
||||||
delegate :create, :remove, :push, :clean, :pull, :info, :context_hosts, :config_context_hosts, :validate_image,
|
delegate :create, :remove, :push, :clean, :pull, :info, :context_hosts, :config_context_hosts, :validate_image,
|
||||||
to: :target
|
:first_mirror, to: :target
|
||||||
|
|
||||||
include Clone
|
include Clone
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,10 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
|
|||||||
[]
|
[]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def first_mirror
|
||||||
|
docker(:info, "--format '{{index .RegistryConfig.Mirrors 0}}'")
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def build_tags
|
def build_tags
|
||||||
[ "-t", config.absolute_image, "-t", config.latest_image ]
|
[ "-t", config.absolute_image, "-t", config.latest_image ]
|
||||||
|
|||||||
68
lib/kamal/commands/proxy.rb
Normal file
68
lib/kamal/commands/proxy.rb
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
class Kamal::Commands::Proxy < Kamal::Commands::Base
|
||||||
|
delegate :argumentize, :optionize, to: Kamal::Utils
|
||||||
|
delegate :container_name, to: :proxy_config
|
||||||
|
|
||||||
|
attr_reader :proxy_config
|
||||||
|
|
||||||
|
def initialize(config)
|
||||||
|
super
|
||||||
|
@proxy_config = config.proxy
|
||||||
|
end
|
||||||
|
|
||||||
|
def run
|
||||||
|
docker :run,
|
||||||
|
"--name", container_name,
|
||||||
|
"--detach",
|
||||||
|
"--restart", "unless-stopped",
|
||||||
|
*proxy_config.publish_args,
|
||||||
|
"--volume", "/var/run/docker.sock:/var/run/docker.sock",
|
||||||
|
"--volume", "#{container_name}:/root/.config/kamal-proxy",
|
||||||
|
*config.logging_args,
|
||||||
|
proxy_config.image
|
||||||
|
end
|
||||||
|
|
||||||
|
def start
|
||||||
|
docker :container, :start, container_name
|
||||||
|
end
|
||||||
|
|
||||||
|
def stop(name: container_name)
|
||||||
|
docker :container, :stop, name
|
||||||
|
end
|
||||||
|
|
||||||
|
def start_or_run
|
||||||
|
combine start, run, by: "||"
|
||||||
|
end
|
||||||
|
|
||||||
|
def deploy(service, target:)
|
||||||
|
docker :exec, container_name, "kamal-proxy", :deploy, service, *optionize({ target: target }), *proxy_config.deploy_command_args
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove(service, target:)
|
||||||
|
docker :exec, container_name, "kamal-proxy", :remove, service, *optionize({ target: target })
|
||||||
|
end
|
||||||
|
|
||||||
|
def info
|
||||||
|
docker :ps, "--filter", "name=^#{container_name}$"
|
||||||
|
end
|
||||||
|
|
||||||
|
def logs(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"),
|
||||||
|
("grep '#{grep}'#{" #{grep_options}" if grep_options}" if grep)
|
||||||
|
end
|
||||||
|
|
||||||
|
def follow_logs(host:, grep: nil, grep_options: nil)
|
||||||
|
run_over_ssh pipe(
|
||||||
|
docker(:logs, container_name, "--timestamps", "--tail", "10", "--follow", "2>&1"),
|
||||||
|
(%(grep "#{grep}"#{" #{grep_options}" if grep_options}) if grep)
|
||||||
|
).join(" "), host: host
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_container
|
||||||
|
docker :container, :prune, "--force", "--filter", "label=org.opencontainers.image.title=kamal-proxy"
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_image
|
||||||
|
docker :image, :prune, "--all", "--force", "--filter", "label=org.opencontainers.image.title=kamal-proxy"
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -11,7 +11,7 @@ class Kamal::Configuration
|
|||||||
delegate :argumentize, :optionize, to: Kamal::Utils
|
delegate :argumentize, :optionize, to: Kamal::Utils
|
||||||
|
|
||||||
attr_reader :destination, :raw_config
|
attr_reader :destination, :raw_config
|
||||||
attr_reader :accessories, :boot, :builder, :env, :healthcheck, :logging, :traefik, :servers, :ssh, :sshkit, :registry
|
attr_reader :accessories, :aliases, :boot, :builder, :env, :healthcheck, :logging, :proxy, :traefik, :servers, :ssh, :sshkit, :registry
|
||||||
|
|
||||||
include Validation
|
include Validation
|
||||||
|
|
||||||
@@ -47,19 +47,21 @@ class Kamal::Configuration
|
|||||||
@destination = destination
|
@destination = destination
|
||||||
@declared_version = version
|
@declared_version = version
|
||||||
|
|
||||||
validate! raw_config, example: validation_yml.symbolize_keys, context: ""
|
validate! raw_config, example: validation_yml.symbolize_keys, context: "", with: Kamal::Configuration::Validator::Configuration
|
||||||
|
|
||||||
# Eager load config to validate it, these are first as they have dependencies later on
|
# Eager load config to validate it, these are first as they have dependencies later on
|
||||||
@servers = Servers.new(config: self)
|
@servers = Servers.new(config: self)
|
||||||
@registry = Registry.new(config: self)
|
@registry = Registry.new(config: self)
|
||||||
|
|
||||||
@accessories = @raw_config.accessories&.keys&.collect { |name| Accessory.new(name, config: self) } || []
|
@accessories = @raw_config.accessories&.keys&.collect { |name| Accessory.new(name, config: self) } || []
|
||||||
|
@aliases = @raw_config.aliases&.keys&.to_h { |name| [ name, Alias.new(name, config: self) ] } || {}
|
||||||
@boot = Boot.new(config: self)
|
@boot = Boot.new(config: self)
|
||||||
@builder = Builder.new(config: self)
|
@builder = Builder.new(config: self)
|
||||||
@env = Env.new(config: @raw_config.env || {})
|
@env = Env.new(config: @raw_config.env || {})
|
||||||
|
|
||||||
@healthcheck = Healthcheck.new(healthcheck_config: @raw_config.healthcheck)
|
@healthcheck = Healthcheck.new(healthcheck_config: @raw_config.healthcheck)
|
||||||
@logging = Logging.new(logging_config: @raw_config.logging)
|
@logging = Logging.new(logging_config: @raw_config.logging)
|
||||||
|
@proxy = Proxy.new(config: self)
|
||||||
@traefik = Traefik.new(config: self)
|
@traefik = Traefik.new(config: self)
|
||||||
@ssh = Ssh.new(config: self)
|
@ssh = Ssh.new(config: self)
|
||||||
@sshkit = Sshkit.new(config: self)
|
@sshkit = Sshkit.new(config: self)
|
||||||
@@ -141,6 +143,10 @@ class Kamal::Configuration
|
|||||||
traefik_roles.flat_map(&:hosts).uniq
|
traefik_roles.flat_map(&:hosts).uniq
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def proxy_hosts
|
||||||
|
proxy.hosts
|
||||||
|
end
|
||||||
|
|
||||||
def repository
|
def repository
|
||||||
[ registry.server, image ].compact.join("/")
|
[ registry.server, image ].compact.join("/")
|
||||||
end
|
end
|
||||||
|
|||||||
15
lib/kamal/configuration/alias.rb
Normal file
15
lib/kamal/configuration/alias.rb
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
class Kamal::Configuration::Alias
|
||||||
|
include Kamal::Configuration::Validation
|
||||||
|
|
||||||
|
attr_reader :name, :command
|
||||||
|
|
||||||
|
def initialize(name, config:)
|
||||||
|
@name, @command = name.inquiry, config.raw_config["aliases"][name]
|
||||||
|
|
||||||
|
validate! \
|
||||||
|
command,
|
||||||
|
example: validation_yml["aliases"]["uname"],
|
||||||
|
context: "aliases/#{name}",
|
||||||
|
with: Kamal::Configuration::Validator::Alias
|
||||||
|
end
|
||||||
|
end
|
||||||
26
lib/kamal/configuration/docs/alias.yml
Normal file
26
lib/kamal/configuration/docs/alias.yml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Aliases
|
||||||
|
#
|
||||||
|
# Aliases are shortcuts for Kamal commands.
|
||||||
|
#
|
||||||
|
# For example, for a Rails app, you might open a console with:
|
||||||
|
#
|
||||||
|
# ```shell
|
||||||
|
# kamal app exec -i -r console "rails console"
|
||||||
|
# ```
|
||||||
|
#
|
||||||
|
# By defining an alias, like this:
|
||||||
|
aliases:
|
||||||
|
console: app exec -r console -i "rails console"
|
||||||
|
# You can now open the console with:
|
||||||
|
# ```shell
|
||||||
|
# kamal console
|
||||||
|
# ```
|
||||||
|
|
||||||
|
# Configuring aliases
|
||||||
|
#
|
||||||
|
# Aliases are defined in the root config under the alias key
|
||||||
|
#
|
||||||
|
# Each alias is named and can only contain lowercase letters, numbers, dashes and underscores.
|
||||||
|
|
||||||
|
aliases:
|
||||||
|
uname: app exec -p -q -r web "uname -a"
|
||||||
@@ -2,13 +2,24 @@
|
|||||||
#
|
#
|
||||||
# Configuration is read from the `config/deploy.yml`
|
# Configuration is read from the `config/deploy.yml`
|
||||||
#
|
#
|
||||||
|
|
||||||
|
# Destinations
|
||||||
|
#
|
||||||
# When running commands, you can specify a destination with the `-d` flag,
|
# When running commands, you can specify a destination with the `-d` flag,
|
||||||
# e.g. `kamal deploy -d staging`
|
# e.g. `kamal deploy -d staging`
|
||||||
#
|
#
|
||||||
# In this case the configuration will also be read from `config/deploy.staging.yml`
|
# In this case the configuration will also be read from `config/deploy.staging.yml`
|
||||||
# and merged with the base configuration.
|
# and merged with the base configuration.
|
||||||
|
|
||||||
|
# Extensions
|
||||||
#
|
#
|
||||||
# The available configuration options are explained below.
|
# Kamal will not accept unrecognized keys in the configuration file.
|
||||||
|
#
|
||||||
|
# However, you might want to declare a configuration block using YAML anchors
|
||||||
|
# and aliases to avoid repetition.
|
||||||
|
#
|
||||||
|
# You can use prefix a configuration section with `x-` to indicate that it is an
|
||||||
|
# extension. Kamal will ignore the extension and not raise an error.
|
||||||
|
|
||||||
# The service name
|
# The service name
|
||||||
# This is a required value. It is used as the container name prefix.
|
# This is a required value. It is used as the container name prefix.
|
||||||
@@ -132,6 +143,12 @@ accessories:
|
|||||||
traefik:
|
traefik:
|
||||||
...
|
...
|
||||||
|
|
||||||
|
# Proxy
|
||||||
|
#
|
||||||
|
# **Experimental** Configuration for kamal-proxy the replacement for Traefik, see kamal docs proxy
|
||||||
|
proxy:
|
||||||
|
...
|
||||||
|
|
||||||
# SSHKit
|
# SSHKit
|
||||||
#
|
#
|
||||||
# See kamal docs sshkit
|
# See kamal docs sshkit
|
||||||
@@ -155,3 +172,9 @@ healthcheck:
|
|||||||
# Docker logging configuration, see kamal docs logging
|
# Docker logging configuration, see kamal docs logging
|
||||||
logging:
|
logging:
|
||||||
...
|
...
|
||||||
|
|
||||||
|
# Aliases
|
||||||
|
#
|
||||||
|
# Alias configuration, see kamal docs alias
|
||||||
|
aliases:
|
||||||
|
...
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ env:
|
|||||||
# To pass the secrets you should list them under the `secret` key. When you do this the
|
# To pass the secrets you should list them under the `secret` key. When you do this the
|
||||||
# other variables need to be moved under the `clear` key.
|
# other variables need to be moved under the `clear` key.
|
||||||
#
|
#
|
||||||
# Unlike clear valies, secrets are not passed directly to the container,
|
# Unlike clear values, secrets are not passed directly to the container,
|
||||||
# but are stored in an env file on the host
|
# but are stored in an env file on the host
|
||||||
# The file is not updated when deploying, only when running `kamal envify` or `kamal env push`.
|
# The file is not updated when deploying, only when running `kamal envify` or `kamal env push`.
|
||||||
env:
|
env:
|
||||||
|
|||||||
114
lib/kamal/configuration/docs/proxy.yml
Normal file
114
lib/kamal/configuration/docs/proxy.yml
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
# Proxy
|
||||||
|
#
|
||||||
|
# **Experimental** [kamal-proxy](http://github.com/basecamp/kamal-proxy) is a
|
||||||
|
# custom built specifically for Kamal. It will replace Traefik in Kamal v2.0,
|
||||||
|
# but currently is available as an experimental feature.
|
||||||
|
#
|
||||||
|
# When this is enabled, the proxy will be started on the hosts listed under the hosts key.
|
||||||
|
# In addition, the kamal traefik command will be disabled and replaced by kamal proxy.
|
||||||
|
#
|
||||||
|
# The kamal proxy command works identically to kamal traefik on hosts that have not
|
||||||
|
# been included. It will also handle switching between Traefik and kamal-proxy when you
|
||||||
|
# run kamal proxy reboot.
|
||||||
|
|
||||||
|
# Limitations
|
||||||
|
#
|
||||||
|
# Currently the proxy will run on ports 80 and 443 and will bind to those
|
||||||
|
# ports on the host.
|
||||||
|
#
|
||||||
|
# There is no way to set custom options for `docker run` when booting the proxy.
|
||||||
|
#
|
||||||
|
# If you have custom Traefik configuration via labels or boot arguments they may
|
||||||
|
# not have an equivalent in kamal-proxy.
|
||||||
|
|
||||||
|
# Proxy settings
|
||||||
|
#
|
||||||
|
# The proxy is configured in the root configuration under `traefik`. These are
|
||||||
|
# options that are set when deploying the application, not when booting the proxy
|
||||||
|
#
|
||||||
|
# They are application specific, so are not shared when multiple applications
|
||||||
|
# with the same proxy.
|
||||||
|
proxy:
|
||||||
|
|
||||||
|
# Enabled
|
||||||
|
#
|
||||||
|
# Whether to enable experimental proxy support. Defaults to false
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
# Hosts
|
||||||
|
#
|
||||||
|
# The hosts to run the proxy on, instead of Traefik
|
||||||
|
# This is a temporary setting and will be removed when we full switch to kamal-proxy
|
||||||
|
#
|
||||||
|
# If you run `kamal traefik reboot`, then the proxy will be started on these hosts
|
||||||
|
# in place of traefik.
|
||||||
|
hosts:
|
||||||
|
- 10.0.0.1
|
||||||
|
- 10.0.0.2
|
||||||
|
|
||||||
|
# Host
|
||||||
|
#
|
||||||
|
# This is the host that will be used to serve the app. By setting this you can run
|
||||||
|
# multiple apps on the same server sharing the same instance of the proxy.
|
||||||
|
#
|
||||||
|
# If this is set only requests that match this host will be forwarded by the proxy.
|
||||||
|
# if this is not set, then all requests will be forwarded, except for matching
|
||||||
|
# requests for other apps that do have a host set.
|
||||||
|
host: foo.example.com
|
||||||
|
|
||||||
|
# Deploy timeout
|
||||||
|
#
|
||||||
|
# How long to wait for the app to boot when deploying, defaults to 30 seconds
|
||||||
|
deploy_timeout: 10s
|
||||||
|
|
||||||
|
# Response timeout
|
||||||
|
#
|
||||||
|
# How long to wait for requests to complete before timing out, defaults to 30 seconds
|
||||||
|
response_timeout: 10
|
||||||
|
|
||||||
|
# Healthcheck
|
||||||
|
#
|
||||||
|
# When deploying, the proxy will by default hit /up once every second until we hit
|
||||||
|
# the deploy timeout, with a 5 second timeout for each request.
|
||||||
|
#
|
||||||
|
# Once the app is up, the proxy will stop hitting the healthcheck endpoint.
|
||||||
|
healthcheck:
|
||||||
|
interval: 3
|
||||||
|
path: /health
|
||||||
|
timeout: 3
|
||||||
|
|
||||||
|
# Buffering
|
||||||
|
#
|
||||||
|
# Whether to buffer request and response bodies in the proxy
|
||||||
|
#
|
||||||
|
# By default buffering is enabled with a max request body size of 1GB and no limit
|
||||||
|
# for response size.
|
||||||
|
#
|
||||||
|
# You can also set the memory limit for buffering, which defaults to 1MB, anything
|
||||||
|
# larger than that is written to disk.
|
||||||
|
buffering:
|
||||||
|
requests: true
|
||||||
|
responses: true
|
||||||
|
max_request_body: 40_000_000
|
||||||
|
max_response_body: 0
|
||||||
|
memory: 2_000_000
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
#
|
||||||
|
# Configure request logging for the proxy
|
||||||
|
# You can specify request and response headers to log.
|
||||||
|
# By default, Cache-Control and Last-Modified request headers are logged
|
||||||
|
logging:
|
||||||
|
request_headers:
|
||||||
|
- Cache-Control
|
||||||
|
- X-Forwarded-Proto
|
||||||
|
response_headers:
|
||||||
|
- X-Request-ID
|
||||||
|
- X-Request-Start
|
||||||
|
|
||||||
|
# Forward headers
|
||||||
|
#
|
||||||
|
# Whether to forward the X-Forwarded-For and X-Forwarded-Proto headers (defaults to false)
|
||||||
|
#
|
||||||
|
# If you are behind a trusted proxy, you can set this to true to forward the headers.
|
||||||
|
forward_headers: true
|
||||||
@@ -44,3 +44,23 @@ ssh:
|
|||||||
# Defaults to `fatal`. Set this to debug if you are having
|
# Defaults to `fatal`. Set this to debug if you are having
|
||||||
# SSH connection issues.
|
# SSH connection issues.
|
||||||
log_level: debug
|
log_level: debug
|
||||||
|
|
||||||
|
# Keys Only
|
||||||
|
#
|
||||||
|
# Set to true to use only private keys from keys and key_data parameters,
|
||||||
|
# even if ssh-agent offers more identities. This option is intended for
|
||||||
|
# situations where ssh-agent offers many different identites or you have
|
||||||
|
# a need to overwrite all identites and force a single one.
|
||||||
|
keys_only: false
|
||||||
|
|
||||||
|
# Keys
|
||||||
|
#
|
||||||
|
# An array of file names of private keys to use for publickey
|
||||||
|
# and hostbased authentication
|
||||||
|
keys: [ "~/.ssh/id.pem" ]
|
||||||
|
|
||||||
|
# Key Data
|
||||||
|
#
|
||||||
|
# An array of strings, with each element of the array being
|
||||||
|
# a raw private key in PEM format.
|
||||||
|
key_data: [ "-----BEGIN OPENSSH PRIVATE KEY-----" ]
|
||||||
|
|||||||
66
lib/kamal/configuration/proxy.rb
Normal file
66
lib/kamal/configuration/proxy.rb
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
class Kamal::Configuration::Proxy
|
||||||
|
include Kamal::Configuration::Validation
|
||||||
|
|
||||||
|
DEFAULT_HTTP_PORT = 80
|
||||||
|
DEFAULT_HTTPS_PORT = 443
|
||||||
|
DEFAULT_IMAGE = "basecamp/kamal-proxy:latest"
|
||||||
|
DEFAULT_LOG_REQUEST_HEADERS = [ "Cache-Control", "Last-Modified" ]
|
||||||
|
|
||||||
|
delegate :argumentize, :optionize, to: Kamal::Utils
|
||||||
|
|
||||||
|
def initialize(config:)
|
||||||
|
@proxy_config = config.raw_config.proxy || {}
|
||||||
|
validate! proxy_config
|
||||||
|
end
|
||||||
|
|
||||||
|
def enabled?
|
||||||
|
!!proxy_config.fetch("enabled", false)
|
||||||
|
end
|
||||||
|
|
||||||
|
def hosts
|
||||||
|
if enabled?
|
||||||
|
proxy_config.fetch("hosts", [])
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def image
|
||||||
|
proxy_config.fetch("image", DEFAULT_IMAGE)
|
||||||
|
end
|
||||||
|
|
||||||
|
def container_name
|
||||||
|
"kamal-proxy"
|
||||||
|
end
|
||||||
|
|
||||||
|
def publish_args
|
||||||
|
argumentize "--publish", [ "#{DEFAULT_HTTP_PORT}:#{DEFAULT_HTTP_PORT}", "#{DEFAULT_HTTPS_PORT}:#{DEFAULT_HTTPS_PORT}" ]
|
||||||
|
end
|
||||||
|
|
||||||
|
def deploy_options
|
||||||
|
{
|
||||||
|
host: proxy_config["host"],
|
||||||
|
"deploy-timeout": proxy_config["deploy_timeout"],
|
||||||
|
"drain-timeout": proxy_config["drain_timeout"],
|
||||||
|
"health-check-interval": proxy_config.dig("health_check", "interval"),
|
||||||
|
"health-check-timeout": proxy_config.dig("health_check", "timeout"),
|
||||||
|
"health-check-path": proxy_config.dig("health_check", "path"),
|
||||||
|
"target-timeout": proxy_config["response_timeout"],
|
||||||
|
"buffer-requests": proxy_config.fetch("buffering", { "requests": true }).fetch("requests", true),
|
||||||
|
"buffer-responses": proxy_config.fetch("buffering", { "responses": true }).fetch("responses", true),
|
||||||
|
"buffer-memory": proxy_config.dig("buffering", "memory"),
|
||||||
|
"max-request-body": proxy_config.dig("buffering", "max_request_body"),
|
||||||
|
"max-response-body": proxy_config.dig("buffering", "max_response_body"),
|
||||||
|
"forward-headers": proxy_config.dig("forward_headers"),
|
||||||
|
"log-request-header": proxy_config.dig("logging", "request_headers") || DEFAULT_LOG_REQUEST_HEADERS,
|
||||||
|
"log-response-header": proxy_config.dig("logging", "response_headers")
|
||||||
|
}.compact
|
||||||
|
end
|
||||||
|
|
||||||
|
def deploy_command_args
|
||||||
|
optionize deploy_options
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
attr_accessor :proxy_config
|
||||||
|
end
|
||||||
@@ -58,10 +58,18 @@ class Kamal::Configuration::Role
|
|||||||
default_labels.merge(traefik_labels).merge(custom_labels)
|
default_labels.merge(traefik_labels).merge(custom_labels)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def labels_for_proxy
|
||||||
|
default_labels.merge(custom_labels)
|
||||||
|
end
|
||||||
|
|
||||||
def label_args
|
def label_args
|
||||||
argumentize "--label", labels
|
argumentize "--label", labels
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def label_args_for_proxy
|
||||||
|
argumentize "--label", labels_for_proxy
|
||||||
|
end
|
||||||
|
|
||||||
def logging_args
|
def logging_args
|
||||||
logging.args
|
logging.args
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -26,8 +26,20 @@ class Kamal::Configuration::Ssh
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def keys_only
|
||||||
|
ssh_config["keys_only"]
|
||||||
|
end
|
||||||
|
|
||||||
|
def keys
|
||||||
|
ssh_config["keys"]
|
||||||
|
end
|
||||||
|
|
||||||
|
def key_data
|
||||||
|
ssh_config["key_data"]
|
||||||
|
end
|
||||||
|
|
||||||
def options
|
def options
|
||||||
{ user: user, port: port, proxy: proxy, logger: logger, keepalive: true, keepalive_interval: 30 }.compact
|
{ user: user, port: port, proxy: proxy, logger: logger, keepalive: true, keepalive_interval: 30, keys_only: keys_only, keys: keys, key_data: key_data }.compact
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_h
|
def to_h
|
||||||
|
|||||||
@@ -13,33 +13,34 @@ class Kamal::Configuration::Validator
|
|||||||
|
|
||||||
private
|
private
|
||||||
def validate_against_example!(validation_config, example)
|
def validate_against_example!(validation_config, example)
|
||||||
validate_type! validation_config, Hash
|
validate_type! validation_config, example.class
|
||||||
|
|
||||||
if (unknown_keys = validation_config.keys - example.keys).any?
|
if example.class == Hash
|
||||||
unknown_keys_error unknown_keys
|
check_unknown_keys! validation_config, example
|
||||||
end
|
|
||||||
|
|
||||||
validation_config.each do |key, value|
|
validation_config.each do |key, value|
|
||||||
with_context(key) do
|
next if extension?(key)
|
||||||
example_value = example[key]
|
with_context(key) do
|
||||||
|
example_value = example[key]
|
||||||
|
|
||||||
if example_value == "..."
|
if example_value == "..."
|
||||||
validate_type! value, *(Array if key == :servers), Hash
|
validate_type! value, *(Array if key == :servers), Hash
|
||||||
elsif key == "hosts"
|
elsif key == "hosts"
|
||||||
validate_servers! value
|
validate_servers! value
|
||||||
elsif example_value.is_a?(Array)
|
elsif example_value.is_a?(Array)
|
||||||
validate_array_of! value, example_value.first.class
|
validate_array_of! value, example_value.first.class
|
||||||
elsif example_value.is_a?(Hash)
|
elsif example_value.is_a?(Hash)
|
||||||
case key.to_s
|
case key.to_s
|
||||||
when "options", "args"
|
when "options", "args"
|
||||||
validate_type! value, Hash
|
validate_type! value, Hash
|
||||||
when "labels"
|
when "labels"
|
||||||
validate_hash_of! value, example_value.first[1].class
|
validate_hash_of! value, example_value.first[1].class
|
||||||
|
else
|
||||||
|
validate_against_example! value, example_value
|
||||||
|
end
|
||||||
else
|
else
|
||||||
validate_against_example! value, example_value
|
validate_type! value, example_value.class
|
||||||
end
|
end
|
||||||
else
|
|
||||||
validate_type! value, example_value.class
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -137,4 +138,18 @@ class Kamal::Configuration::Validator
|
|||||||
ensure
|
ensure
|
||||||
@context = old_context
|
@context = old_context
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def allow_extensions?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def extension?(key)
|
||||||
|
key.to_s.start_with?("x-")
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_unknown_keys!(config, example)
|
||||||
|
unknown_keys = config.keys - example.keys
|
||||||
|
unknown_keys.reject! { |key| extension?(key) } if allow_extensions?
|
||||||
|
unknown_keys_error unknown_keys if unknown_keys.present?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
15
lib/kamal/configuration/validator/alias.rb
Normal file
15
lib/kamal/configuration/validator/alias.rb
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
class Kamal::Configuration::Validator::Alias < Kamal::Configuration::Validator
|
||||||
|
def validate!
|
||||||
|
super
|
||||||
|
|
||||||
|
name = context.delete_prefix("aliases/")
|
||||||
|
|
||||||
|
if name !~ /\A[a-z0-9_-]+\z/
|
||||||
|
error "Invalid alias name: '#{name}'. Must only contain lowercase letters, alphanumeric, hyphens and underscores."
|
||||||
|
end
|
||||||
|
|
||||||
|
if Kamal::Cli::Main.commands.include?(name)
|
||||||
|
error "Alias '#{name}' conflicts with a built-in command."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
6
lib/kamal/configuration/validator/configuration.rb
Normal file
6
lib/kamal/configuration/validator/configuration.rb
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
class Kamal::Configuration::Validator::Configuration < Kamal::Configuration::Validator
|
||||||
|
private
|
||||||
|
def allow_extensions?
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -9,6 +9,10 @@ module Kamal::Git
|
|||||||
`git config user.name`.strip
|
`git config user.name`.strip
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def email
|
||||||
|
`git config user.email`.strip
|
||||||
|
end
|
||||||
|
|
||||||
def revision
|
def revision
|
||||||
`git rev-parse HEAD`.strip
|
`git rev-parse HEAD`.strip
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ class Kamal::Tags
|
|||||||
|
|
||||||
def default_tags(config)
|
def default_tags(config)
|
||||||
{ recorded_at: Time.now.utc.iso8601,
|
{ recorded_at: Time.now.utc.iso8601,
|
||||||
performer: `whoami`.chomp,
|
performer: Kamal::Git.email.presence || `whoami`.chomp,
|
||||||
destination: config.destination,
|
destination: config.destination,
|
||||||
version: config.version,
|
version: config.version,
|
||||||
service_version: service_version(config),
|
service_version: service_version(config),
|
||||||
|
|||||||
@@ -77,4 +77,8 @@ module Kamal::Utils
|
|||||||
def stable_sort!(elements, &block)
|
def stable_sort!(elements, &block)
|
||||||
elements.sort_by!.with_index { |element, index| [ block.call(element), index ] }
|
elements.sort_by!.with_index { |element, index| [ block.call(element), index ] }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def join_commands(commands)
|
||||||
|
commands.map(&:strip).join(" ")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
module Kamal
|
module Kamal
|
||||||
VERSION = "1.7.3"
|
VERSION = "2.0.0.alpha"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -247,6 +247,12 @@ class CliAppTest < CliTestCase
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "exec separate arguments" do
|
||||||
|
run_command("exec", "ruby", " -v").tap do |output|
|
||||||
|
assert_match "docker run --rm --env-file .kamal/env/roles/app-web.env dhh/app:latest ruby -v", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
test "exec with reuse" do
|
test "exec with reuse" do
|
||||||
run_command("exec", "--reuse", "ruby -v").tap do |output|
|
run_command("exec", "--reuse", "ruby -v").tap do |output|
|
||||||
assert_match "sh -c 'docker ps --latest --format '\\''{{.Names}}'\\'' --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 --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | while read line; do echo ${line#app-web-}; done", output # Get current version
|
assert_match "sh -c 'docker ps --latest --format '\\''{{.Names}}'\\'' --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 --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | while read line; do echo ${line#app-web-}; done", output # Get current version
|
||||||
@@ -350,6 +356,18 @@ class CliAppTest < CliTestCase
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "boot proxy" do
|
||||||
|
SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info).returns("123") # old version
|
||||||
|
|
||||||
|
run_command("boot", config: :with_proxy).tap do |output|
|
||||||
|
assert_match /Renaming container .* to .* as already deployed on 1.1.1.1/, output # Rename
|
||||||
|
assert_match /docker rename app-web-latest app-web-latest_replaced_[0-9a-f]{16}/, output
|
||||||
|
assert_match /docker run --detach --restart unless-stopped --name app-web-latest --hostname 1.1.1.1-[0-9a-f]{12} -e KAMAL_CONTAINER_NAME="app-web-latest" -e KAMAL_VERSION="latest" --env-file .kamal\/env\/roles\/app-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 "123"/, output
|
||||||
|
assert_match "docker container ls --all --filter name=^app-web-123$ --quiet | xargs docker stop", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def run_command(*command, config: :with_accessories, host: "1.1.1.1", allow_execute_error: false)
|
def run_command(*command, config: :with_accessories, host: "1.1.1.1", allow_execute_error: false)
|
||||||
stdouted do
|
stdouted do
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ class CliBuildTest < CliTestCase
|
|||||||
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:docker, "--version", "&&", :docker, :buildx, "version")
|
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:docker, "--version", "&&", :docker, :buildx, "version")
|
||||||
|
|
||||||
SSHKit::Backend::Abstract.any_instance.expects(:execute)
|
SSHKit::Backend::Abstract.any_instance.expects(:execute)
|
||||||
.with(:git, "-C", "#{Dir.tmpdir}/kamal-clones/app-#{pwd_sha}", :clone, Dir.pwd)
|
.with(:git, "-C", "#{Dir.tmpdir}/kamal-clones/app-#{pwd_sha}", :clone, Dir.pwd, "--recurse-submodules")
|
||||||
.raises(SSHKit::Command::Failed.new("fatal: destination path 'kamal' already exists and is not an empty directory"))
|
.raises(SSHKit::Command::Failed.new("fatal: destination path 'kamal' already exists and is not an empty directory"))
|
||||||
.then
|
.then
|
||||||
.returns(true)
|
.returns(true)
|
||||||
@@ -50,6 +50,7 @@ class CliBuildTest < CliTestCase
|
|||||||
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:git, "-C", build_directory, :fetch, :origin)
|
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:git, "-C", build_directory, :fetch, :origin)
|
||||||
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:git, "-C", build_directory, :reset, "--hard", Kamal::Git.revision)
|
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:git, "-C", build_directory, :reset, "--hard", Kamal::Git.revision)
|
||||||
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:git, "-C", build_directory, :clean, "-fdx")
|
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:git, "-C", build_directory, :clean, "-fdx")
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:git, "-C", build_directory, :submodule, :update, "--init")
|
||||||
|
|
||||||
SSHKit::Backend::Abstract.any_instance.expects(:execute)
|
SSHKit::Backend::Abstract.any_instance.expects(:execute)
|
||||||
.with(:docker, :buildx, :build, "--push", "--platform", "linux/amd64,linux/arm64", "--builder", "kamal-app-multiarch", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".")
|
.with(:docker, :buildx, :build, "--push", "--platform", "linux/amd64,linux/arm64", "--builder", "kamal-app-multiarch", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".")
|
||||||
@@ -88,7 +89,7 @@ class CliBuildTest < CliTestCase
|
|||||||
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:docker, "--version", "&&", :docker, :buildx, "version")
|
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:docker, "--version", "&&", :docker, :buildx, "version")
|
||||||
|
|
||||||
SSHKit::Backend::Abstract.any_instance.expects(:execute)
|
SSHKit::Backend::Abstract.any_instance.expects(:execute)
|
||||||
.with(:git, "-C", "#{Dir.tmpdir}/kamal-clones/app-#{pwd_sha}", :clone, Dir.pwd)
|
.with(:git, "-C", "#{Dir.tmpdir}/kamal-clones/app-#{pwd_sha}", :clone, Dir.pwd, "--recurse-submodules")
|
||||||
.raises(SSHKit::Command::Failed.new("fatal: destination path 'kamal' already exists and is not an empty directory"))
|
.raises(SSHKit::Command::Failed.new("fatal: destination path 'kamal' already exists and is not an empty directory"))
|
||||||
.then
|
.then
|
||||||
.returns(true)
|
.returns(true)
|
||||||
@@ -169,12 +170,41 @@ class CliBuildTest < CliTestCase
|
|||||||
|
|
||||||
test "pull" do
|
test "pull" do
|
||||||
run_command("pull").tap do |output|
|
run_command("pull").tap do |output|
|
||||||
|
assert_match /docker info --format '{{index .RegistryConfig.Mirrors 0}}'/, output
|
||||||
assert_match /docker image rm --force dhh\/app:999/, output
|
assert_match /docker image rm --force dhh\/app:999/, output
|
||||||
assert_match /docker pull dhh\/app:999/, output
|
assert_match /docker pull dhh\/app:999/, output
|
||||||
assert_match "docker inspect -f '{{ .Config.Labels.service }}' dhh/app:999 | grep -x app || (echo \"Image dhh/app:999 is missing the 'service' label\" && exit 1)", output
|
assert_match "docker inspect -f '{{ .Config.Labels.service }}' dhh/app:999 | grep -x app || (echo \"Image dhh/app:999 is missing the 'service' label\" && exit 1)", output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "pull with mirror" do
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
|
.with(:docker, :info, "--format '{{index .RegistryConfig.Mirrors 0}}'")
|
||||||
|
.returns("registry-mirror.example.com")
|
||||||
|
.at_least_once
|
||||||
|
|
||||||
|
run_command("pull").tap do |output|
|
||||||
|
assert_match /Pulling image on 1\.1\.1\.\d to seed the mirror\.\.\./, output
|
||||||
|
assert_match "Pulling image on remaining hosts...", output
|
||||||
|
assert_equal 4, output.scan(/docker pull dhh\/app:999/).size, output
|
||||||
|
assert_match "docker inspect -f '{{ .Config.Labels.service }}' dhh/app:999 | grep -x app || (echo \"Image dhh/app:999 is missing the 'service' label\" && exit 1)", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "pull with mirrors" do
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
|
.with(:docker, :info, "--format '{{index .RegistryConfig.Mirrors 0}}'")
|
||||||
|
.returns("registry-mirror.example.com", "registry-mirror2.example.com")
|
||||||
|
.at_least_once
|
||||||
|
|
||||||
|
run_command("pull").tap do |output|
|
||||||
|
assert_match /Pulling image on 1\.1\.1\.\d, 1\.1\.1\.\d to seed the mirrors\.\.\./, output
|
||||||
|
assert_match "Pulling image on remaining hosts...", output
|
||||||
|
assert_equal 4, output.scan(/docker pull dhh\/app:999/).size, output
|
||||||
|
assert_match "docker inspect -f '{{ .Config.Labels.service }}' dhh/app:999 | grep -x app || (echo \"Image dhh/app:999 is missing the 'service' label\" && exit 1)", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
test "create" do
|
test "create" do
|
||||||
run_command("create").tap do |output|
|
run_command("create").tap do |output|
|
||||||
assert_match /docker buildx create --use --name kamal-app-multiarch/, output
|
assert_match /docker buildx create --use --name kamal-app-multiarch/, output
|
||||||
|
|||||||
@@ -42,12 +42,13 @@ class CliTestCase < ActiveSupport::TestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
def assert_hook_ran(hook, output, version:, service_version:, hosts:, command:, subcommand: nil, runtime: false)
|
def assert_hook_ran(hook, output, version:, service_version:, hosts:, command:, subcommand: nil, runtime: false)
|
||||||
performer = `whoami`.strip
|
whoami = `whoami`.chomp
|
||||||
|
performer = Kamal::Git.email.presence || whoami
|
||||||
service = service_version.split("@").first
|
service = service_version.split("@").first
|
||||||
|
|
||||||
assert_match "Running the #{hook} hook...\n", output
|
assert_match "Running the #{hook} hook...\n", output
|
||||||
|
|
||||||
expected = %r{Running\s/usr/bin/env\s\.kamal/hooks/#{hook}\sas\s#{performer}@localhost\n\s
|
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
|
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_RECORDED_AT=\"\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\dZ\"\s
|
||||||
KAMAL_PERFORMER=\"#{performer}\"\s
|
KAMAL_PERFORMER=\"#{performer}\"\s
|
||||||
@@ -62,4 +63,12 @@ class CliTestCase < ActiveSupport::TestCase
|
|||||||
|
|
||||||
assert_match expected, output
|
assert_match expected, output
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def with_argv(*argv)
|
||||||
|
old_argv = ARGV
|
||||||
|
ARGV.replace(*argv)
|
||||||
|
yield
|
||||||
|
ensure
|
||||||
|
ARGV.replace(old_argv)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
require_relative "cli_test_case"
|
require_relative "cli_test_case"
|
||||||
|
|
||||||
class CliMainTest < CliTestCase
|
class CliMainTest < CliTestCase
|
||||||
|
setup { @original_env = ENV.to_h.dup }
|
||||||
|
teardown { ENV.clear; ENV.update @original_env }
|
||||||
|
|
||||||
test "setup" do
|
test "setup" do
|
||||||
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "version" => "999", "skip_hooks" => false }
|
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "version" => "999", "skip_hooks" => false }
|
||||||
|
|
||||||
@@ -122,6 +125,11 @@ class CliMainTest < CliTestCase
|
|||||||
.with(:docker, :buildx, :inspect, "kamal-app-multiarch", "> /dev/null")
|
.with(:docker, :buildx, :inspect, "kamal-app-multiarch", "> /dev/null")
|
||||||
.returns("")
|
.returns("")
|
||||||
|
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
|
.with(:docker, :info, "--format '{{index .RegistryConfig.Mirrors 0}}'")
|
||||||
|
.returns("")
|
||||||
|
.at_least_once
|
||||||
|
|
||||||
assert_raises(Kamal::Cli::LockError) do
|
assert_raises(Kamal::Cli::LockError) do
|
||||||
run_command("deploy")
|
run_command("deploy")
|
||||||
end
|
end
|
||||||
@@ -155,6 +163,11 @@ class CliMainTest < CliTestCase
|
|||||||
.with(:docker, :buildx, :inspect, "kamal-app-multiarch", "> /dev/null")
|
.with(:docker, :buildx, :inspect, "kamal-app-multiarch", "> /dev/null")
|
||||||
.returns("")
|
.returns("")
|
||||||
|
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
|
.with(:docker, :info, "--format '{{index .RegistryConfig.Mirrors 0}}'")
|
||||||
|
.returns("")
|
||||||
|
.at_least_once
|
||||||
|
|
||||||
assert_raises(SSHKit::Runner::ExecuteError) do
|
assert_raises(SSHKit::Runner::ExecuteError) do
|
||||||
run_command("deploy")
|
run_command("deploy")
|
||||||
end
|
end
|
||||||
@@ -434,7 +447,7 @@ class CliMainTest < CliTestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "envify" do
|
test "envify" do
|
||||||
with_test_dot_env_erb(contents: "HELLO=<%= 'world' %>") do
|
with_test_dotenv(".env.erb": "HELLO=<%= 'world' %>") do
|
||||||
run_command("envify")
|
run_command("envify")
|
||||||
assert_equal("HELLO=world", File.read(".env"))
|
assert_equal("HELLO=world", File.read(".env"))
|
||||||
end
|
end
|
||||||
@@ -448,14 +461,14 @@ class CliMainTest < CliTestCase
|
|||||||
<% end -%>
|
<% end -%>
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
with_test_dot_env_erb(contents: file) do
|
with_test_dotenv(".env.erb": file) do
|
||||||
run_command("envify")
|
run_command("envify")
|
||||||
assert_equal("HELLO=world\nKEY=value\n", File.read(".env"))
|
assert_equal("HELLO=world\nKEY=value\n", File.read(".env"))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "envify with destination" do
|
test "envify with destination" do
|
||||||
with_test_dot_env_erb(contents: "HELLO=<%= 'world' %>", file: ".env.world.erb") do
|
with_test_dotenv(".env.world.erb": "HELLO=<%= 'world' %>") do
|
||||||
run_command("envify", "-d", "world", config_file: "deploy_for_dest")
|
run_command("envify", "-d", "world", config_file: "deploy_for_dest")
|
||||||
assert_equal "HELLO=world", File.read(".env.world")
|
assert_equal "HELLO=world", File.read(".env.world")
|
||||||
end
|
end
|
||||||
@@ -470,6 +483,13 @@ class CliMainTest < CliTestCase
|
|||||||
run_command("envify", "--skip-push")
|
run_command("envify", "--skip-push")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "envify with clean env" do
|
||||||
|
with_test_dotenv(".env": "HELLO=already", ".env.erb": "HELLO=<%= ENV.fetch 'HELLO', 'never' %>") do
|
||||||
|
run_command("envify", "--skip-push")
|
||||||
|
assert_equal "HELLO=never", File.read(".env")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
test "remove with confirmation" do
|
test "remove with confirmation" do
|
||||||
run_command("remove", "-y", config_file: "deploy_with_accessories").tap do |output|
|
run_command("remove", "-y", config_file: "deploy_with_accessories").tap do |output|
|
||||||
assert_match /docker container stop traefik/, output
|
assert_match /docker container stop traefik/, output
|
||||||
@@ -517,19 +537,59 @@ class CliMainTest < CliTestCase
|
|||||||
assert_equal Kamal::VERSION, version
|
assert_equal Kamal::VERSION, version
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "run an alias for details" do
|
||||||
|
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:traefik:details")
|
||||||
|
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:details")
|
||||||
|
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:accessory:details", [ "all" ])
|
||||||
|
|
||||||
|
run_command("info", config_file: "deploy_with_aliases")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "run an alias for a console" do
|
||||||
|
run_command("console", config_file: "deploy_with_aliases").tap do |output|
|
||||||
|
assert_match "docker exec app-console-999 bin/console on 1.1.1.5", output
|
||||||
|
assert_match "App Host: 1.1.1.5", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "run an alias for a console overriding role" do
|
||||||
|
run_command("console", "-r", "workers", config_file: "deploy_with_aliases").tap do |output|
|
||||||
|
assert_match "docker exec app-workers-999 bin/console on 1.1.1.3", output
|
||||||
|
assert_match "App Host: 1.1.1.3", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "run an alias for a console passing command" do
|
||||||
|
run_command("exec", "bin/job", config_file: "deploy_with_aliases").tap do |output|
|
||||||
|
assert_match "docker exec app-console-999 bin/job on 1.1.1.5", output
|
||||||
|
assert_match "App Host: 1.1.1.5", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "append to command with an alias" do
|
||||||
|
run_command("rails", "db:migrate:status", config_file: "deploy_with_aliases").tap do |output|
|
||||||
|
assert_match "docker exec app-console-999 rails db:migrate:status on 1.1.1.5", output
|
||||||
|
assert_match "App Host: 1.1.1.5", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def run_command(*command, config_file: "deploy_simple")
|
def run_command(*command, config_file: "deploy_simple")
|
||||||
stdouted { Kamal::Cli::Main.start([ *command, "-c", "test/fixtures/#{config_file}.yml" ]) }
|
with_argv([ *command, "-c", "test/fixtures/#{config_file}.yml" ]) do
|
||||||
|
stdouted { Kamal::Cli::Main.start }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def with_test_dot_env_erb(contents:, file: ".env.erb")
|
def with_test_dotenv(**files)
|
||||||
Dir.mktmpdir do |dir|
|
Dir.mktmpdir do |dir|
|
||||||
fixtures_dup = File.join(dir, "test")
|
fixtures_dup = File.join(dir, "test")
|
||||||
FileUtils.mkdir_p(fixtures_dup)
|
FileUtils.mkdir_p(fixtures_dup)
|
||||||
FileUtils.cp_r("test/fixtures/", fixtures_dup)
|
FileUtils.cp_r("test/fixtures/", fixtures_dup)
|
||||||
|
|
||||||
Dir.chdir(dir) do
|
Dir.chdir(dir) do
|
||||||
File.write(file, contents)
|
files.each do |filename, contents|
|
||||||
|
File.binwrite(filename.to_s, contents)
|
||||||
|
end
|
||||||
yield
|
yield
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
141
test/cli/proxy_test.rb
Normal file
141
test/cli/proxy_test.rb
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
require_relative "cli_test_case"
|
||||||
|
|
||||||
|
class CliProxyTest < CliTestCase
|
||||||
|
test "boot" do
|
||||||
|
run_command("boot").tap do |output|
|
||||||
|
assert_match "docker login", output
|
||||||
|
assert_match "docker run --name kamal-proxy --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume kamal-proxy:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "reboot" do
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
|
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-123$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{.NetworkSettings.IPAddress}}{{range $k, $v := .NetworkSettings.Ports}}{{printf \":%s\" $k}}{{break}}{{end}}'", "|", :sed, "-e", "'s/\\/tcp$//'")
|
||||||
|
.returns("172.1.0.2:80")
|
||||||
|
.at_least_once
|
||||||
|
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
|
.with { |*args| args[0..1] == [ :sh, "-c" ] }
|
||||||
|
.returns("123")
|
||||||
|
.at_least_once
|
||||||
|
|
||||||
|
run_command("reboot", "-y").tap do |output|
|
||||||
|
assert_match "docker container stop kamal-proxy on 1.1.1.1", output
|
||||||
|
assert_match "docker container stop 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 container prune --force --filter label=org.opencontainers.image.title=Traefik on 1.1.1.1", output
|
||||||
|
assert_match "docker run --name kamal-proxy --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume kamal-proxy:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE} on 1.1.1.1", output
|
||||||
|
assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"172.1.0.2:80\" --deploy-timeout \"6s\" on 1.1.1.1", output
|
||||||
|
|
||||||
|
assert_match "docker container stop kamal-proxy on 1.1.1.2", output
|
||||||
|
assert_match "docker container stop 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 container prune --force --filter label=org.opencontainers.image.title=Traefik on 1.1.1.2", output
|
||||||
|
assert_match "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" traefik:v2.10 --providers.docker --log.level=\"DEBUG\" on 1.1.1.2", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "reboot --rolling" do
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
|
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-123$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{.NetworkSettings.IPAddress}}{{range $k, $v := .NetworkSettings.Ports}}{{printf \":%s\" $k}}{{break}}{{end}}'", "|", :sed, "-e", "'s/\\/tcp$//'")
|
||||||
|
.returns("172.1.0.2:80")
|
||||||
|
.at_least_once
|
||||||
|
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
|
.with { |*args| args[0..1] == [ :sh, "-c" ] }
|
||||||
|
.returns("123")
|
||||||
|
.at_least_once
|
||||||
|
|
||||||
|
run_command("reboot", "--rolling", "-y").tap do |output|
|
||||||
|
assert_match "Running docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy on 1.1.1.1", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "start" do
|
||||||
|
run_command("start").tap do |output|
|
||||||
|
assert_match "docker container start kamal-proxy", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "stop" do
|
||||||
|
run_command("stop").tap do |output|
|
||||||
|
assert_match "docker container stop kamal-proxy", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "restart" do
|
||||||
|
Kamal::Cli::Proxy.any_instance.expects(:stop)
|
||||||
|
Kamal::Cli::Proxy.any_instance.expects(:start)
|
||||||
|
|
||||||
|
run_command("restart")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "details" do
|
||||||
|
run_command("details").tap do |output|
|
||||||
|
assert_match "docker ps --filter name=^kamal-proxy$", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "logs" do
|
||||||
|
SSHKit::Backend::Abstract.any_instance.stubs(:capture)
|
||||||
|
.with(:docker, :logs, "kamal-proxy", " --tail 100", "--timestamps", "2>&1")
|
||||||
|
.returns("Log entry")
|
||||||
|
|
||||||
|
SSHKit::Backend::Abstract.any_instance.stubs(:capture)
|
||||||
|
.with(:docker, :logs, "traefik", " --tail 100", "--timestamps", "2>&1")
|
||||||
|
.returns("Log entry")
|
||||||
|
|
||||||
|
run_command("logs").tap do |output|
|
||||||
|
assert_match "Proxy Host: 1.1.1.1", output
|
||||||
|
assert_match "Log entry", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "logs with follow" do
|
||||||
|
SSHKit::Backend::Abstract.any_instance.stubs(:exec)
|
||||||
|
.with("ssh -t root@1.1.1.1 -p 22 'docker logs kamal-proxy --timestamps --tail 10 --follow 2>&1'")
|
||||||
|
|
||||||
|
assert_match "docker logs kamal-proxy --timestamps --tail 10 --follow", run_command("logs", "--follow")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "remove" do
|
||||||
|
Kamal::Cli::Proxy.any_instance.expects(:stop)
|
||||||
|
Kamal::Cli::Proxy.any_instance.expects(:remove_container)
|
||||||
|
Kamal::Cli::Proxy.any_instance.expects(:remove_image)
|
||||||
|
|
||||||
|
run_command("remove")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "remove_container" do
|
||||||
|
run_command("remove_container").tap do |output|
|
||||||
|
assert_match "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "remove_image" do
|
||||||
|
run_command("remove_image").tap do |output|
|
||||||
|
assert_match "docker image prune --all --force --filter label=org.opencontainers.image.title=kamal-proxy", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "commands disallowed when proxy is disabled" do
|
||||||
|
assert_raises_when_disabled "boot"
|
||||||
|
assert_raises_when_disabled "reboot"
|
||||||
|
assert_raises_when_disabled "start"
|
||||||
|
assert_raises_when_disabled "stop"
|
||||||
|
assert_raises_when_disabled "details"
|
||||||
|
assert_raises_when_disabled "logs"
|
||||||
|
assert_raises_when_disabled "remove"
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def run_command(*command, fixture: :with_proxy)
|
||||||
|
stdouted { Kamal::Cli::Proxy.start([ *command, "-c", "test/fixtures/deploy_#{fixture}.yml" ]) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def assert_raises_when_disabled(command)
|
||||||
|
assert_raises "kamal proxy commands are disabled unless experimental proxy support is enabled. Use `kamal traefik` commands instead." do
|
||||||
|
run_command(command, fixture: :with_accessories)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -3,8 +3,8 @@ require_relative "cli_test_case"
|
|||||||
class CliServerTest < CliTestCase
|
class CliServerTest < CliTestCase
|
||||||
test "running a command with exec" do
|
test "running a command with exec" do
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:capture)
|
SSHKit::Backend::Abstract.any_instance.stubs(:capture)
|
||||||
.with("date", verbosity: 1)
|
.with("date", verbosity: 1)
|
||||||
.returns("Today")
|
.returns("Today")
|
||||||
|
|
||||||
hosts = "1.1.1.1".."1.1.1.4"
|
hosts = "1.1.1.1".."1.1.1.4"
|
||||||
run_command("exec", "date").tap do |output|
|
run_command("exec", "date").tap do |output|
|
||||||
@@ -15,6 +15,20 @@ class CliServerTest < CliTestCase
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "running a command with exec multiple arguments" do
|
||||||
|
SSHKit::Backend::Abstract.any_instance.stubs(:capture)
|
||||||
|
.with("date -j", verbosity: 1)
|
||||||
|
.returns("Today")
|
||||||
|
|
||||||
|
hosts = "1.1.1.1".."1.1.1.4"
|
||||||
|
run_command("exec", "date", "-j").tap do |output|
|
||||||
|
hosts.map do |host|
|
||||||
|
assert_match "Running 'date -j' on #{hosts.to_a.join(', ')}...", output
|
||||||
|
assert_match "App Host: #{host}\nToday", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
test "bootstrap already installed" do
|
test "bootstrap already installed" do
|
||||||
stub_setup
|
stub_setup
|
||||||
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:docker, "-v", raise_on_non_zero_exit: false).returns(true).at_least_once
|
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:docker, "-v", raise_on_non_zero_exit: false).returns(true).at_least_once
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class CommandsAuditorTest < ActiveSupport::TestCase
|
|||||||
}
|
}
|
||||||
|
|
||||||
@auditor = new_command
|
@auditor = new_command
|
||||||
@performer = `whoami`.strip
|
@performer = Kamal::Git.email.presence || `whoami`.chomp
|
||||||
@recorded_at = Time.now.utc.iso8601
|
@recorded_at = Time.now.utc.iso8601
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -200,6 +200,11 @@ class CommandsBuilderTest < ActiveSupport::TestCase
|
|||||||
assert_equal [ "unix:///var/run/docker.sock", "ssh://host" ], command.config_context_hosts
|
assert_equal [ "unix:///var/run/docker.sock", "ssh://host" ], command.config_context_hosts
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "mirror count" do
|
||||||
|
command = new_builder_command
|
||||||
|
assert_equal "docker info --format '{{index .RegistryConfig.Mirrors 0}}'", command.first_mirror.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def new_builder_command(additional_config = {})
|
def new_builder_command(additional_config = {})
|
||||||
Kamal::Commands::Builder.new(Kamal::Configuration.new(@config.merge(additional_config), version: "123"))
|
Kamal::Commands::Builder.new(Kamal::Configuration.new(@config.merge(additional_config), version: "123"))
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class CommandsHookTest < ActiveSupport::TestCase
|
|||||||
traefik: { "args" => { "accesslog.format" => "json", "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } }
|
traefik: { "args" => { "accesslog.format" => "json", "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } }
|
||||||
}
|
}
|
||||||
|
|
||||||
@performer = `whoami`.strip
|
@performer = Kamal::Git.email.presence || `whoami`.chomp
|
||||||
@recorded_at = Time.now.utc.iso8601
|
@recorded_at = Time.now.utc.iso8601
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
126
test/commands/proxy_test.rb
Normal file
126
test/commands/proxy_test.rb
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class CommandsProxyTest < ActiveSupport::TestCase
|
||||||
|
setup do
|
||||||
|
@config = {
|
||||||
|
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
ENV["EXAMPLE_API_KEY"] = "456"
|
||||||
|
end
|
||||||
|
|
||||||
|
teardown do
|
||||||
|
ENV.delete("EXAMPLE_API_KEY")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "run" do
|
||||||
|
assert_equal \
|
||||||
|
"docker run --name kamal-proxy --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume kamal-proxy:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}",
|
||||||
|
new_command.run.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "run with ports configured" do
|
||||||
|
assert_equal \
|
||||||
|
"docker run --name kamal-proxy --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume kamal-proxy:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}",
|
||||||
|
new_command.run.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "run without configuration" do
|
||||||
|
@config.delete(:proxy)
|
||||||
|
|
||||||
|
assert_equal \
|
||||||
|
"docker run --name kamal-proxy --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume kamal-proxy:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_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 --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume kamal-proxy:/root/.config/kamal-proxy --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}",
|
||||||
|
new_command.run.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "proxy start" do
|
||||||
|
assert_equal \
|
||||||
|
"docker container start kamal-proxy",
|
||||||
|
new_command.start.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "proxy stop" do
|
||||||
|
assert_equal \
|
||||||
|
"docker container stop kamal-proxy",
|
||||||
|
new_command.stop.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "proxy info" do
|
||||||
|
assert_equal \
|
||||||
|
"docker ps --filter name=^kamal-proxy$",
|
||||||
|
new_command.info.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "proxy logs" do
|
||||||
|
assert_equal \
|
||||||
|
"docker logs kamal-proxy --timestamps 2>&1",
|
||||||
|
new_command.logs.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "proxy logs since 2h" do
|
||||||
|
assert_equal \
|
||||||
|
"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",
|
||||||
|
new_command.logs(lines: 10).join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "proxy logs with grep hello!" do
|
||||||
|
assert_equal \
|
||||||
|
"docker logs kamal-proxy --timestamps 2>&1 | grep 'hello!'",
|
||||||
|
new_command.logs(grep: "hello!").join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "proxy remove container" do
|
||||||
|
assert_equal \
|
||||||
|
"docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy",
|
||||||
|
new_command.remove_container.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "proxy remove image" do
|
||||||
|
assert_equal \
|
||||||
|
"docker image prune --all --force --filter label=org.opencontainers.image.title=kamal-proxy",
|
||||||
|
new_command.remove_image.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "proxy follow logs" do
|
||||||
|
assert_equal \
|
||||||
|
"ssh -t root@1.1.1.1 -p 22 'docker logs kamal-proxy --timestamps --tail 10 --follow 2>&1'",
|
||||||
|
new_command.follow_logs(host: @config[:servers].first)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "proxy follow logs with grep hello!" do
|
||||||
|
assert_equal \
|
||||||
|
"ssh -t root@1.1.1.1 -p 22 'docker logs kamal-proxy --timestamps --tail 10 --follow 2>&1 | grep \"hello!\"'",
|
||||||
|
new_command.follow_logs(host: @config[:servers].first, grep: "hello!")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "deploy" do
|
||||||
|
assert_equal \
|
||||||
|
"docker exec kamal-proxy kamal-proxy deploy service --target \"172.1.0.2:80\"",
|
||||||
|
new_command.deploy("service", target: "172.1.0.2:80").join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "remove" do
|
||||||
|
assert_equal \
|
||||||
|
"docker exec kamal-proxy kamal-proxy remove service --target \"172.1.0.2:80\"",
|
||||||
|
new_command.remove("service", target: "172.1.0.2:80").join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def new_command
|
||||||
|
Kamal::Commands::Proxy.new(Kamal::Configuration.new(@config, version: "123"))
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -344,4 +344,12 @@ class ConfigurationTest < ActiveSupport::TestCase
|
|||||||
|
|
||||||
assert_raises(Kamal::ConfigurationError) { Kamal::Configuration.new(@deploy_with_roles.merge(retain_containers: 0)) }
|
assert_raises(Kamal::ConfigurationError) { Kamal::Configuration.new(@deploy_with_roles.merge(retain_containers: 0)) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "extensions" do
|
||||||
|
dest_config_file = Pathname.new(File.expand_path("fixtures/deploy_with_extensions.yml", __dir__))
|
||||||
|
|
||||||
|
config = Kamal::Configuration.create_from config_file: dest_config_file
|
||||||
|
assert_equal config.role(:web_tokyo).running_traefik?, true
|
||||||
|
assert_equal config.role(:web_chicago).running_traefik?, true
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
21
test/fixtures/deploy_with_aliases.yml
vendored
Normal file
21
test/fixtures/deploy_with_aliases.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
service: app
|
||||||
|
image: dhh/app
|
||||||
|
servers:
|
||||||
|
web:
|
||||||
|
- 1.1.1.1
|
||||||
|
- 1.1.1.2
|
||||||
|
workers:
|
||||||
|
hosts:
|
||||||
|
- 1.1.1.3
|
||||||
|
- 1.1.1.4
|
||||||
|
console:
|
||||||
|
hosts:
|
||||||
|
- 1.1.1.5
|
||||||
|
registry:
|
||||||
|
username: user
|
||||||
|
password: pw
|
||||||
|
aliases:
|
||||||
|
info: details
|
||||||
|
console: app exec --reuse -p -r console "bin/console"
|
||||||
|
exec: app exec --reuse -p -r console
|
||||||
|
rails: app exec --reuse -p -r console rails
|
||||||
24
test/fixtures/deploy_with_extensions.yml
vendored
Normal file
24
test/fixtures/deploy_with_extensions.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
|
||||||
|
x-web: &web
|
||||||
|
traefik: true
|
||||||
|
|
||||||
|
service: app
|
||||||
|
image: dhh/app
|
||||||
|
servers:
|
||||||
|
web_chicago:
|
||||||
|
<<: *web
|
||||||
|
hosts:
|
||||||
|
- 1.1.1.1
|
||||||
|
- 1.1.1.2
|
||||||
|
web_tokyo:
|
||||||
|
<<: *web
|
||||||
|
hosts:
|
||||||
|
- 1.1.1.3
|
||||||
|
- 1.1.1.4
|
||||||
|
env:
|
||||||
|
REDIS_URL: redis://x/y
|
||||||
|
registry:
|
||||||
|
server: registry.digitalocean.com
|
||||||
|
username: user
|
||||||
|
password: pw
|
||||||
|
primary_role: web_tokyo
|
||||||
42
test/fixtures/deploy_with_proxy.yml
vendored
Normal file
42
test/fixtures/deploy_with_proxy.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
service: app
|
||||||
|
image: dhh/app
|
||||||
|
servers:
|
||||||
|
web:
|
||||||
|
- "1.1.1.1"
|
||||||
|
- "1.1.1.2"
|
||||||
|
workers:
|
||||||
|
- "1.1.1.3"
|
||||||
|
- "1.1.1.4"
|
||||||
|
registry:
|
||||||
|
username: user
|
||||||
|
password: pw
|
||||||
|
|
||||||
|
proxy:
|
||||||
|
enabled: true
|
||||||
|
hosts:
|
||||||
|
- "1.1.1.1"
|
||||||
|
deploy_timeout: 6s
|
||||||
|
|
||||||
|
accessories:
|
||||||
|
mysql:
|
||||||
|
image: mysql:5.7
|
||||||
|
host: 1.1.1.3
|
||||||
|
port: 3306
|
||||||
|
env:
|
||||||
|
clear:
|
||||||
|
MYSQL_ROOT_HOST: '%'
|
||||||
|
secret:
|
||||||
|
- MYSQL_ROOT_PASSWORD
|
||||||
|
files:
|
||||||
|
- test/fixtures/files/my.cnf:/etc/mysql/my.cnf
|
||||||
|
directories:
|
||||||
|
- data:/var/lib/mysql
|
||||||
|
redis:
|
||||||
|
image: redis:latest
|
||||||
|
roles:
|
||||||
|
- web
|
||||||
|
port: 6379
|
||||||
|
directories:
|
||||||
|
- data:/data
|
||||||
|
|
||||||
|
readiness_delay: 0
|
||||||
@@ -29,6 +29,5 @@ class BrokenDeployTest < IntegrationTest
|
|||||||
assert_match /First web container is unhealthy on vm[12], not booting any other roles/, output
|
assert_match /First web container is unhealthy on vm[12], not booting any other roles/, output
|
||||||
assert_match "First web container is unhealthy, not booting workers on vm3", output
|
assert_match "First web container is unhealthy, not booting workers on vm3", output
|
||||||
assert_match "nginx: [emerg] unexpected end of file, expecting \";\" or \"}\" in /etc/nginx/conf.d/default.conf:2", output
|
assert_match "nginx: [emerg] unexpected end of file, expecting \";\" or \"}\" in /etc/nginx/conf.d/default.conf:2", output
|
||||||
assert_match 'ERROR {"Status":"unhealthy","FailingStreak":0,"Log":[]}', output
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -29,8 +29,6 @@ services:
|
|||||||
context: docker/registry
|
context: docker/registry
|
||||||
environment:
|
environment:
|
||||||
- REGISTRY_HTTP_ADDR=0.0.0.0:4443
|
- REGISTRY_HTTP_ADDR=0.0.0.0:4443
|
||||||
- REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt
|
|
||||||
- REGISTRY_HTTP_TLS_KEY=/certs/domain.key
|
|
||||||
volumes:
|
volumes:
|
||||||
- shared:/shared
|
- shared:/shared
|
||||||
- registry:/var/lib/registry/
|
- registry:/var/lib/registry/
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ COPY app_with_roles/ app_with_roles/
|
|||||||
|
|
||||||
RUN rm -rf /root/.ssh
|
RUN rm -rf /root/.ssh
|
||||||
RUN ln -s /shared/ssh /root/.ssh
|
RUN ln -s /shared/ssh /root/.ssh
|
||||||
RUN mkdir -p /etc/docker/certs.d/registry:4443 && ln -s /shared/certs/domain.crt /etc/docker/certs.d/registry:4443/ca.crt
|
|
||||||
|
|
||||||
RUN git config --global user.email "deployer@example.com"
|
RUN git config --global user.email "deployer@example.com"
|
||||||
RUN git config --global user.name "Deployer"
|
RUN git config --global user.name "Deployer"
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
echo "About to lock..."
|
echo "About to lock..."
|
||||||
if [ "$KAMAL_HOSTS" != "vm1,vm2" ]; then
|
|
||||||
echo "Expected hosts to be 'vm1,vm2', got $KAMAL_HOSTS"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/pre-connect
|
mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/pre-connect
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ ARG COMMIT_SHA
|
|||||||
RUN echo $COMMIT_SHA > /usr/share/nginx/html/version
|
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 "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 mkdir -p /usr/share/nginx/html/versions && echo "hidden" > /usr/share/nginx/html/versions/.hidden
|
||||||
|
RUN echo "Up!" > /usr/share/nginx/html/up
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
echo "About to lock..."
|
echo "About to lock..."
|
||||||
if [ "$KAMAL_HOSTS" != "vm1,vm2,vm3" ]; then
|
|
||||||
echo "Expected hosts to be 'vm1,vm2,vm3', got $KAMAL_HOSTS"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/pre-connect
|
mkdir -p /tmp/${TEST_ID} && touch /tmp/${TEST_ID}/pre-connect
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ ARG COMMIT_SHA
|
|||||||
RUN echo $COMMIT_SHA > /usr/share/nginx/html/version
|
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 "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 mkdir -p /usr/share/nginx/html/versions && echo "hidden" > /usr/share/nginx/html/versions/.hidden
|
||||||
|
RUN echo "Up!" > /usr/share/nginx/html/up
|
||||||
|
|||||||
@@ -9,6 +9,11 @@ servers:
|
|||||||
hosts:
|
hosts:
|
||||||
- vm3
|
- vm3
|
||||||
cmd: sleep infinity
|
cmd: sleep infinity
|
||||||
|
proxy:
|
||||||
|
enabled: true
|
||||||
|
hosts:
|
||||||
|
- vm2
|
||||||
|
deploy_timeout: 2s
|
||||||
|
|
||||||
asset_path: /usr/share/nginx/html/versions
|
asset_path: /usr/share/nginx/html/versions
|
||||||
|
|
||||||
@@ -37,3 +42,7 @@ accessories:
|
|||||||
- web
|
- web
|
||||||
stop_wait_time: 1
|
stop_wait_time: 1
|
||||||
readiness_delay: 0
|
readiness_delay: 0
|
||||||
|
aliases:
|
||||||
|
whome: version
|
||||||
|
worker_hostname: app exec -r workers -q --reuse hostname
|
||||||
|
uname: server exec -q -p uname
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
dockerd --max-concurrent-downloads 1 &
|
dockerd --max-concurrent-downloads 1 --insecure-registry registry:4443 &
|
||||||
|
|
||||||
exec sleep infinity
|
exec sleep infinity
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
while [ ! -f /certs/domain.crt ]; do sleep 1; done
|
|
||||||
|
|
||||||
exec /entrypoint.sh /etc/docker/registry/config.yml
|
exec /entrypoint.sh /etc/docker/registry/config.yml
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ RUN mkdir ssh && \
|
|||||||
COPY registry-dns.conf .
|
COPY registry-dns.conf .
|
||||||
COPY boot.sh .
|
COPY boot.sh .
|
||||||
|
|
||||||
RUN mkdir certs && openssl req -newkey rsa:4096 -nodes -sha256 -keyout certs/domain.key -x509 -days 365 -out certs/domain.crt -subj '/CN=registry' -extensions EXT -config registry-dns.conf
|
|
||||||
|
|
||||||
HEALTHCHECK --interval=1s CMD pgrep sleep
|
HEALTHCHECK --interval=1s CMD pgrep sleep
|
||||||
|
|
||||||
CMD ["./boot.sh"]
|
CMD ["./boot.sh"]
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ WORKDIR /work
|
|||||||
RUN apt-get update --fix-missing && apt-get -y install openssh-client openssh-server docker.io
|
RUN apt-get update --fix-missing && apt-get -y install openssh-client openssh-server docker.io
|
||||||
|
|
||||||
RUN mkdir /root/.ssh && ln -s /shared/ssh/id_rsa.pub /root/.ssh/authorized_keys
|
RUN mkdir /root/.ssh && ln -s /shared/ssh/id_rsa.pub /root/.ssh/authorized_keys
|
||||||
RUN mkdir -p /etc/docker/certs.d/registry:4443 && ln -s /shared/certs/domain.crt /etc/docker/certs.d/registry:4443/ca.crt
|
|
||||||
|
|
||||||
RUN echo "HOST_TOKEN=abcd" >> /etc/environment
|
RUN echo "HOST_TOKEN=abcd" >> /etc/environment
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,6 @@ while [ ! -f /root/.ssh/authorized_keys ]; do echo "Waiting for ssh keys"; sleep
|
|||||||
|
|
||||||
service ssh restart
|
service ssh restart
|
||||||
|
|
||||||
dockerd --max-concurrent-downloads 1 &
|
dockerd --max-concurrent-downloads 1 --insecure-registry registry:4443 &
|
||||||
|
|
||||||
exec sleep infinity
|
exec sleep infinity
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class IntegrationTest < ActiveSupport::TestCase
|
|||||||
succeeded = system("cd test/integration && #{command}")
|
succeeded = system("cd test/integration && #{command}")
|
||||||
end
|
end
|
||||||
|
|
||||||
raise "Command `#{command}` failed with error code `#{$?}`" if !succeeded && raise_on_error
|
raise "Command `#{command}` failed with error code `#{$?}`, and output:\n#{result}" if !succeeded && raise_on_error
|
||||||
result
|
result
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -82,6 +82,22 @@ class MainTest < IntegrationTest
|
|||||||
assert_equal({ "cmd"=>"wget -qO- http://localhost > /dev/null || exit 1", "interval"=>"1s", "max_attempts"=>3, "port"=>3000, "path"=>"/up", "cord"=>"/tmp/kamal-cord", "log_lines"=>50 }, config[:healthcheck])
|
assert_equal({ "cmd"=>"wget -qO- http://localhost > /dev/null || exit 1", "interval"=>"1s", "max_attempts"=>3, "port"=>3000, "path"=>"/up", "cord"=>"/tmp/kamal-cord", "log_lines"=>50 }, config[:healthcheck])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "aliases" do
|
||||||
|
@app = "app_with_roles"
|
||||||
|
|
||||||
|
kamal :envify
|
||||||
|
kamal :deploy
|
||||||
|
|
||||||
|
output = kamal :whome, capture: true
|
||||||
|
assert_equal Kamal::VERSION, output
|
||||||
|
|
||||||
|
output = kamal :worker_hostname, capture: true
|
||||||
|
assert_match /App Host: vm3\nvm3-[0-9a-f]{12}$/, output
|
||||||
|
|
||||||
|
output = kamal :uname, "-o", capture: true
|
||||||
|
assert_match "App Host: vm1\nGNU/Linux", output
|
||||||
|
end
|
||||||
|
|
||||||
test "setup and remove" do
|
test "setup and remove" do
|
||||||
# Check remove completes when nothing has been setup yet
|
# Check remove completes when nothing has been setup yet
|
||||||
kamal :remove, "-y"
|
kamal :remove, "-y"
|
||||||
|
|||||||
84
test/integration/proxy_test.rb
Normal file
84
test/integration/proxy_test.rb
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
require_relative "integration_test"
|
||||||
|
|
||||||
|
class ProxyTest < IntegrationTest
|
||||||
|
setup do
|
||||||
|
@app = "app_with_roles"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "boot, reboot, stop, start, restart, logs, remove" do
|
||||||
|
kamal :envify
|
||||||
|
|
||||||
|
kamal :proxy, :boot
|
||||||
|
assert_proxy_running
|
||||||
|
|
||||||
|
output = kamal :proxy, :reboot, "-y", "--verbose", capture: true
|
||||||
|
assert_proxy_running
|
||||||
|
assert_hooks_ran "pre-traefik-reboot", "post-traefik-reboot"
|
||||||
|
assert_match /Rebooting Traefik on vm1,vm2.../, output
|
||||||
|
assert_match /Rebooted Traefik on vm1,vm2/, output
|
||||||
|
|
||||||
|
output = kamal :proxy, :reboot, "--rolling", "-y", "--verbose", capture: true
|
||||||
|
assert_proxy_running
|
||||||
|
assert_hooks_ran "pre-traefik-reboot", "post-traefik-reboot"
|
||||||
|
assert_match /Rebooting Traefik on vm1.../, output
|
||||||
|
assert_match /Rebooted Traefik on vm1/, output
|
||||||
|
assert_match /Rebooting Traefik on vm2.../, output
|
||||||
|
assert_match /Rebooted Traefik on vm2/, output
|
||||||
|
|
||||||
|
kamal :proxy, :boot
|
||||||
|
assert_proxy_running
|
||||||
|
assert_traefik_running
|
||||||
|
|
||||||
|
# Check booting when booted doesn't raise an error
|
||||||
|
kamal :proxy, :stop
|
||||||
|
assert_proxy_not_running
|
||||||
|
assert_traefik_not_running
|
||||||
|
|
||||||
|
# Check booting when stopped works
|
||||||
|
kamal :proxy, :boot
|
||||||
|
assert_proxy_running
|
||||||
|
assert_traefik_running
|
||||||
|
|
||||||
|
kamal :proxy, :stop
|
||||||
|
assert_proxy_not_running
|
||||||
|
assert_traefik_not_running
|
||||||
|
|
||||||
|
kamal :proxy, :start
|
||||||
|
assert_proxy_running
|
||||||
|
assert_traefik_running
|
||||||
|
|
||||||
|
kamal :proxy, :restart
|
||||||
|
assert_proxy_running
|
||||||
|
assert_traefik_running
|
||||||
|
|
||||||
|
logs = kamal :proxy, :logs, capture: true
|
||||||
|
assert_match /Traefik version [\d.]+ built on/, logs
|
||||||
|
|
||||||
|
kamal :proxy, :remove
|
||||||
|
assert_proxy_not_running
|
||||||
|
assert_traefik_not_running
|
||||||
|
|
||||||
|
kamal :env, :delete
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def assert_proxy_running
|
||||||
|
assert_match /basecamp\/kamal-proxy:latest \"kamal-proxy run\"/, proxy_details
|
||||||
|
end
|
||||||
|
|
||||||
|
def assert_proxy_not_running
|
||||||
|
assert_no_match /basecamp\/kamal-proxy:latest \"kamal-proxy run\"/, proxy_details
|
||||||
|
end
|
||||||
|
|
||||||
|
def assert_traefik_running
|
||||||
|
assert_match /traefik:v2.10 "\/entrypoint.sh/, proxy_details
|
||||||
|
end
|
||||||
|
|
||||||
|
def assert_traefik_not_running
|
||||||
|
assert_no_match /traefik:v2.10 "\/entrypoint.sh/, proxy_details
|
||||||
|
end
|
||||||
|
|
||||||
|
def proxy_details
|
||||||
|
kamal :proxy, :details, capture: true
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user