Compare commits
116 Commits
command-li
...
v2.3.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
685312c9f8 | ||
|
|
ca5e53404b | ||
|
|
2c14f48300 | ||
|
|
cd4e183213 | ||
|
|
2465681408 | ||
|
|
b917d7cd40 | ||
|
|
1980a79e73 | ||
|
|
347eb69350 | ||
|
|
9a8a45015b | ||
|
|
8d0f4903ae | ||
|
|
57d582e3bc | ||
|
|
bf8779cef4 | ||
|
|
7142534e77 | ||
|
|
0f97e0b056 | ||
|
|
bd8c35b194 | ||
|
|
35075e2e4d | ||
|
|
53dad5f54f | ||
|
|
66f6e8b576 | ||
|
|
a3f5830728 | ||
|
|
a3e5505bb2 | ||
|
|
fdf8ef1343 | ||
|
|
3ee45d7b30 | ||
|
|
c320343bb2 | ||
|
|
74a06b0ccd | ||
|
|
c0ca5e6dbb | ||
|
|
6f08750c3e | ||
|
|
e362b0106a | ||
|
|
8cec17dd05 | ||
|
|
0f3786781b | ||
|
|
844e3acf50 | ||
|
|
607368121e | ||
|
|
0f16ba1995 | ||
|
|
f3b8a59133 | ||
|
|
bf79c7192f | ||
|
|
cb82767d0f | ||
|
|
5cb9fb787b | ||
|
|
493c5690f1 | ||
|
|
5de55a22ff | ||
|
|
a1e40f9fec | ||
|
|
7ddf3bcb02 | ||
|
|
3654a7e1be | ||
|
|
6a7783c979 | ||
|
|
7dc2609b77 | ||
|
|
74960499c0 | ||
|
|
69b13ebc6a | ||
|
|
da2a543cbc | ||
|
|
08dacd2745 | ||
|
|
b6a10df56a | ||
|
|
c917dd82cf | ||
|
|
f04cae529a | ||
|
|
50c96e36c0 | ||
|
|
7b48648bf2 | ||
|
|
91df935d05 | ||
|
|
bbfcbfa94b | ||
|
|
440044b900 | ||
|
|
06419f8749 | ||
|
|
8d6d7ffed0 | ||
|
|
67ce1912f7 | ||
|
|
f45c754e53 | ||
|
|
d40057286d | ||
|
|
0840fdf0dd | ||
|
|
a434b10bfd | ||
|
|
e34031f70c | ||
|
|
23898a5197 | ||
|
|
1e9c9e9103 | ||
|
|
4b2c9cdc72 | ||
|
|
80191588c2 | ||
|
|
5ca806f4d3 | ||
|
|
1d04a6644f | ||
|
|
950624d667 | ||
|
|
81f3508507 | ||
|
|
9a16873f21 | ||
|
|
6d1d7a4c82 | ||
|
|
ccf32c2c1f | ||
|
|
e5ca53db6e | ||
|
|
82a436fa02 | ||
|
|
7be2e7e0ba | ||
|
|
4f7ebd73a3 | ||
|
|
279bda2770 | ||
|
|
aa15fa532a | ||
|
|
276b469c2b | ||
|
|
c10b3fb07a | ||
|
|
0ff1450a74 | ||
|
|
f47fd13e5b | ||
|
|
256933f6f3 | ||
|
|
1d8c40f5d2 | ||
|
|
73c78079bc | ||
|
|
cd12f95a97 | ||
|
|
641e9056b3 | ||
|
|
b4bcf35f78 | ||
|
|
7f6095c9eb | ||
|
|
ef1271df47 | ||
|
|
df1232d90f | ||
|
|
e75365c8c6 | ||
|
|
e441399255 | ||
|
|
af992ce755 | ||
|
|
32caf4b148 | ||
|
|
28a02262df | ||
|
|
b11fb93a6c | ||
|
|
67ad7662ab | ||
|
|
c63ec39f07 | ||
|
|
8df7d7d92d | ||
|
|
1d48a0fb0a | ||
|
|
0f815e17e4 | ||
|
|
a310aa8fef | ||
|
|
29b02f5c30 | ||
|
|
6d63c4e9c6 | ||
|
|
472d163cc7 | ||
|
|
dadac999d7 | ||
|
|
92d82dd1a7 | ||
|
|
c17bdba61c | ||
|
|
13328687d1 | ||
|
|
2b0810d063 | ||
|
|
098f1855e2 | ||
|
|
88351312bf | ||
|
|
190f4fba28 |
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -27,6 +27,7 @@ jobs:
|
||||
- "3.1"
|
||||
- "3.2"
|
||||
- "3.3"
|
||||
- "3.4.0-preview2"
|
||||
gemfile:
|
||||
- Gemfile
|
||||
- gemfiles/rails_edge.gemfile
|
||||
@@ -41,6 +42,9 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Remove gemfile.lock
|
||||
run: rm Gemfile.lock
|
||||
|
||||
- name: Install Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
@@ -49,3 +53,5 @@ jobs:
|
||||
|
||||
- name: Run tests
|
||||
run: bin/test
|
||||
env:
|
||||
RUBYOPT: ${{ startsWith(matrix.ruby-version, '3.4.') && '--enable=frozen-string-literal' || '' }}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
# Use the official Ruby 3.2.0 Alpine image as the base image
|
||||
FROM ruby:3.2.0-alpine
|
||||
FROM ruby:3.3-alpine
|
||||
|
||||
# Install docker/buildx-bin
|
||||
COPY --from=docker/buildx-bin /buildx /usr/libexec/docker/cli-plugins/docker-buildx
|
||||
@@ -33,7 +32,7 @@ WORKDIR /workdir
|
||||
|
||||
# Tell git it's safe to access /workdir/.git even if
|
||||
# the directory is owned by a different user
|
||||
RUN git config --global --add safe.directory /workdir
|
||||
RUN git config --global --add safe.directory '*'
|
||||
|
||||
# Set the entrypoint to run the installed binary in /workdir
|
||||
# Example: docker run -it -v "$PWD:/workdir" kamal init
|
||||
|
||||
42
Gemfile.lock
42
Gemfile.lock
@@ -1,24 +1,24 @@
|
||||
PATH
|
||||
remote: .
|
||||
specs:
|
||||
kamal (2.0.0)
|
||||
kamal (2.3.0)
|
||||
activesupport (>= 7.0)
|
||||
base64 (~> 0.2)
|
||||
bcrypt_pbkdf (~> 1.0)
|
||||
concurrent-ruby (~> 1.2)
|
||||
dotenv (~> 3.1)
|
||||
ed25519 (~> 1.2)
|
||||
net-ssh (~> 7.0)
|
||||
net-ssh (~> 7.3)
|
||||
sshkit (>= 1.23.0, < 2.0)
|
||||
thor (~> 1.3)
|
||||
zeitwerk (~> 2.5)
|
||||
zeitwerk (>= 2.6.18, < 3.0)
|
||||
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actionpack (7.1.3.4)
|
||||
actionview (= 7.1.3.4)
|
||||
activesupport (= 7.1.3.4)
|
||||
actionpack (7.1.4.1)
|
||||
actionview (= 7.1.4.1)
|
||||
activesupport (= 7.1.4.1)
|
||||
nokogiri (>= 1.8.5)
|
||||
racc
|
||||
rack (>= 2.2.4)
|
||||
@@ -26,13 +26,13 @@ GEM
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
actionview (7.1.3.4)
|
||||
activesupport (= 7.1.3.4)
|
||||
actionview (7.1.4.1)
|
||||
activesupport (= 7.1.4.1)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.11)
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
activesupport (7.1.3.4)
|
||||
activesupport (7.1.4.1)
|
||||
base64
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
@@ -49,7 +49,7 @@ GEM
|
||||
bcrypt_pbkdf (1.1.1-x86_64-darwin)
|
||||
bigdecimal (3.1.8)
|
||||
builder (3.3.0)
|
||||
concurrent-ruby (1.3.3)
|
||||
concurrent-ruby (1.3.4)
|
||||
connection_pool (2.4.1)
|
||||
crass (1.0.6)
|
||||
debug (1.9.2)
|
||||
@@ -59,7 +59,7 @@ GEM
|
||||
drb (2.2.1)
|
||||
ed25519 (1.3.0)
|
||||
erubi (1.13.0)
|
||||
i18n (1.14.5)
|
||||
i18n (1.14.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
io-console (0.7.2)
|
||||
irb (1.14.0)
|
||||
@@ -70,7 +70,7 @@ GEM
|
||||
loofah (2.22.0)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.12.0)
|
||||
minitest (5.24.1)
|
||||
minitest (5.25.1)
|
||||
mocha (2.4.5)
|
||||
ruby2_keywords (>= 0.0.5)
|
||||
mutex_m (0.2.0)
|
||||
@@ -78,7 +78,7 @@ GEM
|
||||
net-ssh (>= 2.6.5, < 8.0.0)
|
||||
net-sftp (4.0.0)
|
||||
net-ssh (>= 5.0.0, < 8.0.0)
|
||||
net-ssh (7.2.3)
|
||||
net-ssh (7.3.0)
|
||||
nokogiri (1.16.7-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.7-x86_64-darwin)
|
||||
@@ -92,7 +92,7 @@ GEM
|
||||
psych (5.1.2)
|
||||
stringio
|
||||
racc (1.8.1)
|
||||
rack (3.1.7)
|
||||
rack (3.1.8)
|
||||
rack-session (2.0.0)
|
||||
rack (>= 3.0.0)
|
||||
rack-test (2.1.0)
|
||||
@@ -107,9 +107,9 @@ GEM
|
||||
rails-html-sanitizer (1.6.0)
|
||||
loofah (~> 2.21)
|
||||
nokogiri (~> 1.14)
|
||||
railties (7.1.3.4)
|
||||
actionpack (= 7.1.3.4)
|
||||
activesupport (= 7.1.3.4)
|
||||
railties (7.1.4.1)
|
||||
actionpack (= 7.1.4.1)
|
||||
activesupport (= 7.1.4.1)
|
||||
irb
|
||||
rackup (>= 1.0.0)
|
||||
rake (>= 12.2)
|
||||
@@ -122,8 +122,7 @@ GEM
|
||||
regexp_parser (2.9.2)
|
||||
reline (0.5.9)
|
||||
io-console (~> 0.5)
|
||||
rexml (3.3.4)
|
||||
strscan
|
||||
rexml (3.3.9)
|
||||
rubocop (1.65.1)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (>= 3.17.0)
|
||||
@@ -161,13 +160,12 @@ GEM
|
||||
net-sftp (>= 2.1.2)
|
||||
net-ssh (>= 2.8.0)
|
||||
stringio (3.1.1)
|
||||
strscan (3.1.0)
|
||||
thor (1.3.1)
|
||||
tzinfo (2.0.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
unicode-display_width (2.5.0)
|
||||
webrick (1.8.1)
|
||||
zeitwerk (2.6.17)
|
||||
webrick (1.8.2)
|
||||
zeitwerk (2.7.1)
|
||||
|
||||
PLATFORMS
|
||||
arm64-darwin
|
||||
|
||||
@@ -13,10 +13,10 @@ Gem::Specification.new do |spec|
|
||||
|
||||
spec.add_dependency "activesupport", ">= 7.0"
|
||||
spec.add_dependency "sshkit", ">= 1.23.0", "< 2.0"
|
||||
spec.add_dependency "net-ssh", "~> 7.0"
|
||||
spec.add_dependency "net-ssh", "~> 7.3"
|
||||
spec.add_dependency "thor", "~> 1.3"
|
||||
spec.add_dependency "dotenv", "~> 3.1"
|
||||
spec.add_dependency "zeitwerk", "~> 2.5"
|
||||
spec.add_dependency "zeitwerk", ">= 2.6.18", "< 3.0"
|
||||
spec.add_dependency "ed25519", "~> 1.2"
|
||||
spec.add_dependency "bcrypt_pbkdf", "~> 1.0"
|
||||
spec.add_dependency "concurrent-ruby", "~> 1.2"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
require "active_support/core_ext/array/conversions"
|
||||
|
||||
class Kamal::Cli::Accessory < Kamal::Cli::Base
|
||||
desc "boot [NAME]", "Boot new accessory service on host (use NAME=all to boot all accessories)"
|
||||
def boot(name, prepare: true)
|
||||
|
||||
@@ -68,7 +68,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
|
||||
version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
|
||||
endpoint = capture_with_info(*app.container_id_for_version(version)).strip
|
||||
if endpoint.present?
|
||||
execute *app.remove(target: endpoint), raise_on_non_zero_exit: false
|
||||
execute *app.remove, raise_on_non_zero_exit: false
|
||||
end
|
||||
end
|
||||
|
||||
@@ -203,7 +203,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
|
||||
run_locally do
|
||||
info "Following logs on #{KAMAL.primary_host}..."
|
||||
|
||||
KAMAL.specific_roles ||= [ "web" ]
|
||||
KAMAL.specific_roles ||= [ KAMAL.primary_role.name ]
|
||||
role = KAMAL.roles_on(KAMAL.primary_host).first
|
||||
|
||||
app = KAMAL.app(role: role, host: host)
|
||||
|
||||
@@ -135,7 +135,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base
|
||||
puts "No documentation found for #{section}"
|
||||
end
|
||||
|
||||
desc "init", "Create config stub in config/deploy.yml and env stub in .env"
|
||||
desc "init", "Create config stub in config/deploy.yml and secrets stub in .kamal"
|
||||
option :bundle, type: :boolean, default: false, desc: "Add Kamal to the Gemfile and create a bin/kamal binstub"
|
||||
def init
|
||||
require "fileutils"
|
||||
|
||||
@@ -14,23 +14,25 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
|
||||
version = capture_with_info(*KAMAL.proxy.version).strip.presence
|
||||
|
||||
if version && Kamal::Utils.older_version?(version, Kamal::Configuration::PROXY_MINIMUM_VERSION)
|
||||
raise "kamal-proxy version #{version} is too old, please reboot to update to at least #{Kamal::Configuration::PROXY_MINIMUM_VERSION}"
|
||||
raise "kamal-proxy version #{version} is too old, run `kamal proxy reboot` in order to update to at least #{Kamal::Configuration::PROXY_MINIMUM_VERSION}"
|
||||
end
|
||||
execute *KAMAL.proxy.start_or_run
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
desc "boot_config <set|get|clear>", "Mange kamal-proxy boot configuration"
|
||||
desc "boot_config <set|get|reset>", "Manage kamal-proxy boot configuration"
|
||||
option :publish, type: :boolean, default: true, desc: "Publish the proxy ports on the host"
|
||||
option :http_port, type: :numeric, default: Kamal::Configuration::PROXY_HTTP_PORT, desc: "HTTP port to publish on the host"
|
||||
option :https_port, type: :numeric, default: Kamal::Configuration::PROXY_HTTPS_PORT, desc: "HTTPS port to publish on the host"
|
||||
option :log_max_size, type: :string, default: Kamal::Configuration::PROXY_LOG_MAX_SIZE, desc: "Max size of proxy logs"
|
||||
option :docker_options, type: :array, default: [], desc: "Docker options to pass to the proxy container", banner: "option=value option2=value2"
|
||||
def boot_config(subcommand)
|
||||
case subcommand
|
||||
when "set"
|
||||
boot_options = [
|
||||
*(KAMAL.config.proxy_publish_args(options[:http_port], options[:https_port]) if options[:publish]),
|
||||
*(KAMAL.config.proxy_logging_args(options[:log_max_size])),
|
||||
*options[:docker_options].map { |option| "--#{option}" }
|
||||
]
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ class Kamal::Cli::Secrets < Kamal::Cli::Base
|
||||
def fetch(*secrets)
|
||||
results = adapter(options[:adapter]).fetch(secrets, **options.slice(:account, :from).symbolize_keys)
|
||||
|
||||
return_or_puts JSON.dump(results), inline: options[:inline]
|
||||
return_or_puts JSON.dump(results).shellescape, inline: options[:inline]
|
||||
end
|
||||
|
||||
desc "extract", "Extract a single secret from the results of a fetch call"
|
||||
@@ -21,6 +21,13 @@ class Kamal::Cli::Secrets < Kamal::Cli::Base
|
||||
return_or_puts value, inline: options[:inline]
|
||||
end
|
||||
|
||||
desc "print", "Print the secrets (for debugging)"
|
||||
def print
|
||||
KAMAL.config.secrets.to_h.each do |key, value|
|
||||
puts "#{key}=#{value}"
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def adapter(adapter)
|
||||
Kamal::Secrets::Adapters.lookup(adapter)
|
||||
|
||||
@@ -13,11 +13,15 @@ servers:
|
||||
# - 192.168.0.1
|
||||
# cmd: bin/jobs
|
||||
|
||||
# Enable SSL auto certification via Let's Encrypt (and allow for multiple apps on one server).
|
||||
# Set ssl: false if using something like Cloudflare to terminate SSL (but keep host!).
|
||||
proxy:
|
||||
# Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server.
|
||||
# Remove this section when using multiple web servers and ensure you terminate SSL at your load balancer.
|
||||
#
|
||||
# Note: If using Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption.
|
||||
proxy:
|
||||
ssl: true
|
||||
host: app.example.com
|
||||
# Proxy connects to your container on port 80 by default.
|
||||
# app_port: 3000
|
||||
|
||||
# Credentials for your image host.
|
||||
registry:
|
||||
@@ -87,7 +91,7 @@ builder:
|
||||
# directories:
|
||||
# - data:/var/lib/mysql
|
||||
# redis:
|
||||
# image: redis:7.0
|
||||
# image: valkey/valkey:8
|
||||
# host: 192.168.0.2
|
||||
# port: 6379
|
||||
# directories:
|
||||
|
||||
@@ -1,13 +1,3 @@
|
||||
#!/usr/bin/env ruby
|
||||
#!/bin/sh
|
||||
|
||||
# A sample docker-setup hook
|
||||
#
|
||||
# Sets up a Docker network on defined hosts which can then be used by the application’s containers
|
||||
|
||||
hosts = ENV["KAMAL_HOSTS"].split(",")
|
||||
|
||||
hosts.each do |ip|
|
||||
destination = "root@#{ip}"
|
||||
puts "Creating a Docker network \"kamal\" on #{destination}"
|
||||
`ssh #{destination} docker network create kamal`
|
||||
end
|
||||
echo "Docker set up on $KAMAL_HOSTS..."
|
||||
|
||||
@@ -43,7 +43,12 @@ class Kamal::Commander::Specifics
|
||||
end
|
||||
|
||||
def specified_hosts
|
||||
(specific_hosts || config.all_hosts) \
|
||||
.select { |host| (specific_roles || config.roles).flat_map(&:hosts).include?(host) }
|
||||
specified_hosts = specific_hosts || config.all_hosts
|
||||
|
||||
if (specific_role_hosts = specific_roles&.flat_map(&:hosts)).present?
|
||||
specified_hosts.select { |host| specific_role_hosts.include?(host) }
|
||||
else
|
||||
specified_hosts
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
class Kamal::Commands::Accessory < Kamal::Commands::Base
|
||||
attr_reader :accessory_config
|
||||
delegate :service_name, :image, :hosts, :port, :files, :directories, :cmd,
|
||||
:publish_args, :env_args, :volume_args, :label_args, :option_args,
|
||||
:network_args, :publish_args, :env_args, :volume_args, :label_args, :option_args,
|
||||
:secrets_io, :secrets_path, :env_directory,
|
||||
to: :accessory_config
|
||||
|
||||
@@ -15,7 +15,7 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base
|
||||
"--name", service_name,
|
||||
"--detach",
|
||||
"--restart", "unless-stopped",
|
||||
"--network", "kamal",
|
||||
*network_args,
|
||||
*config.logging_args,
|
||||
*publish_args,
|
||||
*env_args,
|
||||
@@ -64,7 +64,7 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base
|
||||
docker :run,
|
||||
("-it" if interactive),
|
||||
"--rm",
|
||||
"--network", "kamal",
|
||||
*network_args,
|
||||
*env_args,
|
||||
*volume_args,
|
||||
image,
|
||||
|
||||
@@ -5,8 +5,8 @@ module Kamal::Commands::App::Proxy
|
||||
proxy_exec :deploy, role.container_prefix, *role.proxy.deploy_command_args(target: target)
|
||||
end
|
||||
|
||||
def remove(target:)
|
||||
proxy_exec :remove, role.container_prefix, *role.proxy.remove_command_args(target: target)
|
||||
def remove
|
||||
proxy_exec :remove, role.container_prefix
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -11,14 +11,7 @@ module Kamal::Commands
|
||||
end
|
||||
|
||||
def run_over_ssh(*command, host:)
|
||||
"ssh".tap do |cmd|
|
||||
if config.ssh.proxy && config.ssh.proxy.is_a?(Net::SSH::Proxy::Jump)
|
||||
cmd << " -J #{config.ssh.proxy.jump_proxies}"
|
||||
elsif config.ssh.proxy && config.ssh.proxy.is_a?(Net::SSH::Proxy::Command)
|
||||
cmd << " -o ProxyCommand='#{config.ssh.proxy.command_line_template}'"
|
||||
end
|
||||
cmd << " -t #{config.ssh.user}@#{host} -p #{config.ssh.port} '#{command.join(" ").gsub("'", "'\\\\''")}'"
|
||||
end
|
||||
"ssh#{ssh_proxy_args} -t #{config.ssh.user}@#{host} -p #{config.ssh.port} '#{command.join(" ").gsub("'", "'\\\\''")}'"
|
||||
end
|
||||
|
||||
def container_id_for(container_name:, only_running: false)
|
||||
@@ -92,5 +85,14 @@ module Kamal::Commands
|
||||
def tags(**details)
|
||||
Kamal::Tags.from_config(config, **details)
|
||||
end
|
||||
|
||||
def ssh_proxy_args
|
||||
case config.ssh.proxy
|
||||
when Net::SSH::Proxy::Jump
|
||||
" -J #{config.ssh.proxy.jump_proxies}"
|
||||
when Net::SSH::Proxy::Command
|
||||
" -o ProxyCommand='#{config.ssh.proxy.command_line_template}'"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,7 +6,7 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
|
||||
delegate :argumentize, to: Kamal::Utils
|
||||
delegate \
|
||||
:args, :secrets, :dockerfile, :target, :arches, :local_arches, :remote_arches, :remote,
|
||||
:cache_from, :cache_to, :ssh, :driver, :docker_driver?,
|
||||
:cache_from, :cache_to, :ssh, :provenance, :driver, :docker_driver?,
|
||||
to: :builder_config
|
||||
|
||||
def clean
|
||||
@@ -37,7 +37,7 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
|
||||
end
|
||||
|
||||
def build_options
|
||||
[ *build_tags, *build_cache, *build_labels, *build_args, *build_secrets, *build_dockerfile, *build_target, *build_ssh ]
|
||||
[ *build_tags, *build_cache, *build_labels, *build_args, *build_secrets, *build_dockerfile, *build_target, *build_ssh, *builder_provenance ]
|
||||
end
|
||||
|
||||
def build_context
|
||||
@@ -97,6 +97,10 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
|
||||
argumentize "--ssh", ssh if ssh.present?
|
||||
end
|
||||
|
||||
def builder_provenance
|
||||
argumentize "--provenance", provenance unless provenance.nil?
|
||||
end
|
||||
|
||||
def builder_config
|
||||
config.builder
|
||||
end
|
||||
|
||||
@@ -1,29 +1,31 @@
|
||||
module Kamal::Commands::Builder::Clone
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
delegate :clone_directory, :build_directory, to: :"config.builder"
|
||||
end
|
||||
|
||||
def clone
|
||||
git :clone, Kamal::Git.root, "--recurse-submodules", path: clone_directory
|
||||
git :clone, escaped_root, "--recurse-submodules", path: config.builder.clone_directory.shellescape
|
||||
end
|
||||
|
||||
def clone_reset_steps
|
||||
[
|
||||
git(:remote, "set-url", :origin, Kamal::Git.root, path: build_directory),
|
||||
git(:fetch, :origin, path: build_directory),
|
||||
git(:reset, "--hard", Kamal::Git.revision, path: build_directory),
|
||||
git(:clean, "-fdx", path: build_directory),
|
||||
git(:submodule, :update, "--init", path: build_directory)
|
||||
git(:remote, "set-url", :origin, escaped_root, path: escaped_build_directory),
|
||||
git(:fetch, :origin, path: escaped_build_directory),
|
||||
git(:reset, "--hard", Kamal::Git.revision, path: escaped_build_directory),
|
||||
git(:clean, "-fdx", path: escaped_build_directory),
|
||||
git(:submodule, :update, "--init", path: escaped_build_directory)
|
||||
]
|
||||
end
|
||||
|
||||
def clone_status
|
||||
git :status, "--porcelain", path: build_directory
|
||||
git :status, "--porcelain", path: escaped_build_directory
|
||||
end
|
||||
|
||||
def clone_revision
|
||||
git :"rev-parse", :HEAD, path: build_directory
|
||||
git :"rev-parse", :HEAD, path: escaped_build_directory
|
||||
end
|
||||
|
||||
def escaped_root
|
||||
Kamal::Git.root.shellescape
|
||||
end
|
||||
|
||||
def escaped_build_directory
|
||||
config.builder.build_directory.shellescape
|
||||
end
|
||||
end
|
||||
|
||||
@@ -14,12 +14,15 @@ class Kamal::Configuration
|
||||
|
||||
include Validation
|
||||
|
||||
PROXY_MINIMUM_VERSION = "v0.6.0"
|
||||
PROXY_MINIMUM_VERSION = "v0.8.2"
|
||||
PROXY_HTTP_PORT = 80
|
||||
PROXY_HTTPS_PORT = 443
|
||||
PROXY_LOG_MAX_SIZE = "10m"
|
||||
|
||||
class << self
|
||||
def create_from(config_file:, destination: nil, version: nil)
|
||||
ENV["KAMAL_DESTINATION"] = destination
|
||||
|
||||
raw_config = load_config_files(config_file, *destination_config_file(config_file, destination))
|
||||
|
||||
new raw_config, destination: destination, version: version
|
||||
@@ -250,8 +253,12 @@ class Kamal::Configuration
|
||||
argumentize "--publish", [ "#{http_port}:#{PROXY_HTTP_PORT}", "#{https_port}:#{PROXY_HTTPS_PORT}" ]
|
||||
end
|
||||
|
||||
def proxy_logging_args(max_size)
|
||||
argumentize "--log-opt", "max-size=#{max_size}" if max_size.present?
|
||||
end
|
||||
|
||||
def proxy_options_default
|
||||
proxy_publish_args PROXY_HTTP_PORT, PROXY_HTTPS_PORT
|
||||
[ *proxy_publish_args(PROXY_HTTP_PORT, PROXY_HTTPS_PORT), *proxy_logging_args(PROXY_LOG_MAX_SIZE) ]
|
||||
end
|
||||
|
||||
def proxy_image
|
||||
@@ -360,7 +367,7 @@ class Kamal::Configuration
|
||||
end
|
||||
|
||||
def ensure_unique_hosts_for_ssl_roles
|
||||
hosts = roles.select(&:ssl?).map { |role| role.proxy.host }
|
||||
hosts = roles.select(&:ssl?).flat_map { |role| role.proxy.hosts }
|
||||
duplicates = hosts.tally.filter_map { |host, count| host if count > 1 }
|
||||
|
||||
raise Kamal::ConfigurationError, "Different roles can't share the same host for SSL: #{duplicates.join(", ")}" if duplicates.any?
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
class Kamal::Configuration::Accessory
|
||||
include Kamal::Configuration::Validation
|
||||
|
||||
DEFAULT_NETWORK = "kamal"
|
||||
|
||||
delegate :argumentize, :optionize, to: Kamal::Utils
|
||||
|
||||
attr_reader :name, :accessory_config, :env
|
||||
@@ -38,6 +40,10 @@ class Kamal::Configuration::Accessory
|
||||
end
|
||||
end
|
||||
|
||||
def network_args
|
||||
argumentize "--network", network
|
||||
end
|
||||
|
||||
def publish_args
|
||||
argumentize "--publish", port if port
|
||||
end
|
||||
@@ -173,4 +179,8 @@ class Kamal::Configuration::Accessory
|
||||
accessory_config["roles"].flat_map { |role| config.role(role).hosts }
|
||||
end
|
||||
end
|
||||
|
||||
def network
|
||||
accessory_config["network"] || DEFAULT_NETWORK
|
||||
end
|
||||
end
|
||||
|
||||
@@ -111,6 +111,10 @@ class Kamal::Configuration::Builder
|
||||
builder_config["ssh"]
|
||||
end
|
||||
|
||||
def provenance
|
||||
builder_config["provenance"]
|
||||
end
|
||||
|
||||
def git_clone?
|
||||
Kamal::Git.used? && builder_config["context"].nil?
|
||||
end
|
||||
@@ -166,7 +170,7 @@ class Kamal::Configuration::Builder
|
||||
end
|
||||
|
||||
def cache_to_config_for_registry
|
||||
[ "type=registry", builder_config["cache"]&.fetch("options", nil), "ref=#{cache_image_ref}" ].compact.join(",")
|
||||
[ "type=registry", "ref=#{cache_image_ref}", builder_config["cache"]&.fetch("options", nil) ].compact.join(",")
|
||||
end
|
||||
|
||||
def repo_basename
|
||||
|
||||
@@ -90,3 +90,11 @@ accessories:
|
||||
# They are not created or copied before mounting:
|
||||
volumes:
|
||||
- /path/to/mysql-logs:/var/log/mysql
|
||||
|
||||
# Network
|
||||
#
|
||||
# The network the accessory will be attached to.
|
||||
#
|
||||
# Defaults to kamal:
|
||||
network: custom
|
||||
|
||||
|
||||
@@ -102,3 +102,9 @@ builder:
|
||||
#
|
||||
# The build driver to use, defaults to `docker-container`:
|
||||
driver: docker
|
||||
|
||||
# Provenance
|
||||
#
|
||||
# It is used to configure provenance attestations for the build result.
|
||||
# The value can also be a boolean to enable or disable provenance attestations.
|
||||
provenance: mode=max
|
||||
|
||||
@@ -17,16 +17,19 @@
|
||||
# `proxy: true` or providing a proxy configuration.
|
||||
proxy:
|
||||
|
||||
# Host
|
||||
# Hosts
|
||||
#
|
||||
# The hosts that will be used to serve the app. The proxy will only route requests
|
||||
# to this host to your app.
|
||||
#
|
||||
# If no hosts are set, then all requests will be forwarded, except for matching
|
||||
# requests for other apps deployed on that server that do have a host set.
|
||||
#
|
||||
# Specify one of `host` or `hosts`.
|
||||
host: foo.example.com
|
||||
# If multiple hosts are needed, these can be specified by comma-separating the hosts.
|
||||
host: foo.example.com,bar.example.com
|
||||
hosts:
|
||||
- foo.example.com
|
||||
- bar.example.com
|
||||
|
||||
# App port
|
||||
#
|
||||
|
||||
@@ -61,3 +61,10 @@ ssh:
|
||||
# An array of strings, with each element of the array being
|
||||
# a raw private key in PEM format.
|
||||
key_data: [ "-----BEGIN OPENSSH PRIVATE KEY-----" ]
|
||||
|
||||
# Config
|
||||
#
|
||||
# Set to true to load the default OpenSSH config files (~/.ssh/config,
|
||||
# /etc/ssh_config), to false ignore config files, or to a file path
|
||||
# (or array of paths) to load specific configuration. Defaults to true.
|
||||
config: true
|
||||
|
||||
@@ -22,14 +22,14 @@ class Kamal::Configuration::Proxy
|
||||
proxy_config.fetch("ssl", false)
|
||||
end
|
||||
|
||||
def host
|
||||
proxy_config["host"]
|
||||
def hosts
|
||||
proxy_config["hosts"] || proxy_config["host"]&.split(",") || []
|
||||
end
|
||||
|
||||
def deploy_options
|
||||
{
|
||||
host: proxy_config["host"],
|
||||
tls: proxy_config["ssl"] ? true : nil,
|
||||
host: hosts,
|
||||
tls: proxy_config["ssl"].presence,
|
||||
"deploy-timeout": seconds_duration(config.deploy_timeout),
|
||||
"drain-timeout": seconds_duration(config.drain_timeout),
|
||||
"health-check-interval": seconds_duration(proxy_config.dig("healthcheck", "interval")),
|
||||
@@ -48,11 +48,7 @@ class Kamal::Configuration::Proxy
|
||||
end
|
||||
|
||||
def deploy_command_args(target:)
|
||||
optionize ({ target: "#{target}:#{app_port}" }).merge(deploy_options)
|
||||
end
|
||||
|
||||
def remove_command_args(target:)
|
||||
optionize({ target: "#{target}:#{app_port}" })
|
||||
optionize ({ target: "#{target}:#{app_port}" }).merge(deploy_options), with: "="
|
||||
end
|
||||
|
||||
def merge(other)
|
||||
|
||||
@@ -3,9 +3,13 @@ class Kamal::Configuration::Validator::Proxy < Kamal::Configuration::Validator
|
||||
unless config.nil?
|
||||
super
|
||||
|
||||
if config["host"].blank? && config["ssl"]
|
||||
if config["host"].blank? && config["hosts"].blank? && config["ssl"]
|
||||
error "Must set a host to enable automatic SSL"
|
||||
end
|
||||
|
||||
if (config.keys & [ "host", "hosts" ]).size > 1
|
||||
error "Specify one of 'host' or 'hosts', not both"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -37,6 +37,8 @@ class Kamal::EnvFile
|
||||
def escape_docker_env_file_ascii_value(value)
|
||||
# Doublequotes are treated literally in docker env files
|
||||
# so remove leading and trailing ones and unescape any others
|
||||
value.to_s.dump[1..-2].gsub(/\\"/, "\"")
|
||||
value.to_s.dump[1..-2]
|
||||
.gsub(/\\"/, "\"")
|
||||
.gsub(/\\#/, "#")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
require "dotenv"
|
||||
|
||||
class Kamal::Secrets
|
||||
attr_reader :secrets_files
|
||||
|
||||
Kamal::Secrets::Dotenv::InlineCommandSubstitution.install!
|
||||
|
||||
def initialize(destination: nil)
|
||||
@secrets_files = \
|
||||
[ ".kamal/secrets-common", ".kamal/secrets#{(".#{destination}" if destination)}" ].select { |f| File.exist?(f) }
|
||||
@destination = destination
|
||||
@mutex = Mutex.new
|
||||
end
|
||||
|
||||
@@ -17,10 +14,10 @@ class Kamal::Secrets
|
||||
secrets.fetch(key)
|
||||
end
|
||||
rescue KeyError
|
||||
if secrets_files
|
||||
if secrets_files.present?
|
||||
raise Kamal::ConfigurationError, "Secret '#{key}' not found in #{secrets_files.join(", ")}"
|
||||
else
|
||||
raise Kamal::ConfigurationError, "Secret '#{key}' not found, no secret files provided"
|
||||
raise Kamal::ConfigurationError, "Secret '#{key}' not found, no secret files (#{secrets_filenames.join(", ")}) provided"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -28,10 +25,18 @@ class Kamal::Secrets
|
||||
secrets
|
||||
end
|
||||
|
||||
def secrets_files
|
||||
@secrets_files ||= secrets_filenames.select { |f| File.exist?(f) }
|
||||
end
|
||||
|
||||
private
|
||||
def secrets
|
||||
@secrets ||= secrets_files.inject({}) do |secrets, secrets_file|
|
||||
secrets.merge!(::Dotenv.parse(secrets_file))
|
||||
end
|
||||
end
|
||||
|
||||
def secrets_filenames
|
||||
[ ".kamal/secrets-common", ".kamal/secrets#{(".#{@destination}" if @destination)}" ]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,6 +2,7 @@ class Kamal::Secrets::Adapters::Base
|
||||
delegate :optionize, to: Kamal::Utils
|
||||
|
||||
def fetch(secrets, account:, from: nil)
|
||||
check_dependencies!
|
||||
session = login(account)
|
||||
full_secrets = secrets.map { |secret| [ from, secret ].compact.join("/") }
|
||||
fetch_secrets(full_secrets, account: account, session: session)
|
||||
@@ -15,4 +16,8 @@ class Kamal::Secrets::Adapters::Base
|
||||
def fetch_secrets(...)
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def check_dependencies!
|
||||
raise NotImplementedError
|
||||
end
|
||||
end
|
||||
|
||||
@@ -25,23 +25,31 @@ class Kamal::Secrets::Adapters::Bitwarden < Kamal::Secrets::Adapters::Base
|
||||
{}.tap do |results|
|
||||
items_fields(secrets).each do |item, fields|
|
||||
item_json = run_command("get item #{item.shellescape}", session: session, raw: true)
|
||||
raise RuntimeError, "Could not read #{secret} from Bitwarden" unless $?.success?
|
||||
raise RuntimeError, "Could not read #{item} from Bitwarden" unless $?.success?
|
||||
item_json = JSON.parse(item_json)
|
||||
|
||||
if fields.any?
|
||||
fields.each do |field|
|
||||
item_field = item_json["fields"].find { |f| f["name"] == field }
|
||||
raise RuntimeError, "Could not find field #{field} in item #{item} in Bitwarden" unless item_field
|
||||
value = item_field["value"]
|
||||
results["#{item}/#{field}"] = value
|
||||
end
|
||||
results.merge! fetch_secrets_from_fields(fields, item, item_json)
|
||||
elsif item_json.dig("login", "password")
|
||||
results[item] = item_json.dig("login", "password")
|
||||
elsif item_json["fields"]&.any?
|
||||
fields = item_json["fields"].pluck("name")
|
||||
results.merge! fetch_secrets_from_fields(fields, item, item_json)
|
||||
else
|
||||
results[item] = item_json["login"]["password"]
|
||||
raise RuntimeError, "Item #{item} is not a login type item and no fields were specified"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_secrets_from_fields(fields, item, item_json)
|
||||
fields.to_h do |field|
|
||||
item_field = item_json["fields"].find { |f| f["name"] == field }
|
||||
raise RuntimeError, "Could not find field #{field} in item #{item} in Bitwarden" unless item_field
|
||||
value = item_field["value"]
|
||||
[ "#{item}/#{field}", value ]
|
||||
end
|
||||
end
|
||||
|
||||
def items_fields(secrets)
|
||||
{}.tap do |items|
|
||||
secrets.each do |secret|
|
||||
@@ -61,4 +69,13 @@ class Kamal::Secrets::Adapters::Bitwarden < Kamal::Secrets::Adapters::Base
|
||||
result = `#{full_command}`.strip
|
||||
raw ? result : JSON.parse(result)
|
||||
end
|
||||
|
||||
def check_dependencies!
|
||||
raise RuntimeError, "Bitwarden CLI is not installed" unless cli_installed?
|
||||
end
|
||||
|
||||
def cli_installed?
|
||||
`bw --version 2> /dev/null`
|
||||
$?.success?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -27,4 +27,13 @@ class Kamal::Secrets::Adapters::LastPass < Kamal::Secrets::Adapters::Base
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def check_dependencies!
|
||||
raise RuntimeError, "LastPass CLI is not installed" unless cli_installed?
|
||||
end
|
||||
|
||||
def cli_installed?
|
||||
`lpass --version 2> /dev/null`
|
||||
$?.success?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -58,4 +58,13 @@ class Kamal::Secrets::Adapters::OnePassword < Kamal::Secrets::Adapters::Base
|
||||
raise RuntimeError, "Could not read #{fields.join(", ")} from #{item} in the #{vault} 1Password vault" unless $?.success?
|
||||
end
|
||||
end
|
||||
|
||||
def check_dependencies!
|
||||
raise RuntimeError, "1Password CLI is not installed" unless cli_installed?
|
||||
end
|
||||
|
||||
def cli_installed?
|
||||
`op --version 2> /dev/null`
|
||||
$?.success?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,4 +7,8 @@ class Kamal::Secrets::Adapters::Test < Kamal::Secrets::Adapters::Base
|
||||
def fetch_secrets(secrets, account:, session:)
|
||||
secrets.to_h { |secret| [ secret, secret.reverse ] }
|
||||
end
|
||||
|
||||
def check_dependencies!
|
||||
# no op
|
||||
end
|
||||
end
|
||||
|
||||
@@ -16,7 +16,7 @@ class Kamal::Secrets::Dotenv::InlineCommandSubstitution
|
||||
else
|
||||
if command =~ /\A\s*kamal\s*secrets\s+/
|
||||
# Inline the command
|
||||
inline_secrets_command(command).shellescape
|
||||
inline_secrets_command(command)
|
||||
else
|
||||
# Execute the command and return the value
|
||||
`#{command}`.chomp
|
||||
|
||||
@@ -12,6 +12,8 @@ module Kamal::Utils
|
||||
attr = "#{key}=#{escape_shell_value(value)}"
|
||||
attr = self.sensitive(attr, redaction: "#{key}=[REDACTED]") if sensitive
|
||||
[ argument, attr ]
|
||||
elsif value == false
|
||||
[ argument, "#{key}=false" ]
|
||||
else
|
||||
[ argument, key ]
|
||||
end
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
module Kamal
|
||||
VERSION = "2.0.0"
|
||||
VERSION = "2.3.0"
|
||||
end
|
||||
|
||||
@@ -130,7 +130,7 @@ class CliAppTest < CliTestCase
|
||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", "|", :xargs, :docker, :stop, raise_on_non_zero_exit: false)
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:execute)
|
||||
.with(:docker, :exec, "kamal-proxy", "kamal-proxy", :deploy, "app-web", "--target", "\"123:80\"", "--deploy-timeout", "\"1s\"", "--drain-timeout", "\"30s\"", "--buffer-requests", "--buffer-responses", "--log-request-header", "\"Cache-Control\"", "--log-request-header", "\"Last-Modified\"", "--log-request-header", "\"User-Agent\"").raises(SSHKit::Command::Failed.new("Failed to deploy"))
|
||||
.with(:docker, :exec, "kamal-proxy", "kamal-proxy", :deploy, "app-web", "--target=\"123:80\"", "--deploy-timeout=\"1s\"", "--drain-timeout=\"30s\"", "--buffer-requests", "--buffer-responses", "--log-request-header=\"Cache-Control\"", "--log-request-header=\"Last-Modified\"", "--log-request-header=\"User-Agent\"").raises(SSHKit::Command::Failed.new("Failed to deploy"))
|
||||
|
||||
stderred do
|
||||
run_command("boot", config: :with_roles, host: nil, allow_execute_error: true).tap do |output|
|
||||
@@ -190,7 +190,7 @@ class CliAppTest < CliTestCase
|
||||
|
||||
run_command("start").tap do |output|
|
||||
assert_match "docker start app-web-999", output
|
||||
assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"999:80\" --deploy-timeout \"30s\" --drain-timeout \"30s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\"", output
|
||||
assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target=\"999:80\" --deploy-timeout=\"30s\" --drain-timeout=\"30s\" --buffer-requests --buffer-responses --log-request-header=\"Cache-Control\" --log-request-header=\"Last-Modified\"", output
|
||||
end
|
||||
end
|
||||
|
||||
@@ -383,7 +383,7 @@ class CliAppTest < CliTestCase
|
||||
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 --network kamal --hostname 1.1.1.1-[0-9a-f]{12} -e KAMAL_CONTAINER_NAME="app-web-latest" -e KAMAL_VERSION="latest" --env-file .kamal\/apps\/app\/env\/roles\/web.env --log-opt max-size="10m" --label service="app" --label role="web" --label destination dhh\/app:latest/, output
|
||||
assert_match /docker exec kamal-proxy kamal-proxy deploy app-web --target "123:80"/, output
|
||||
assert_match /docker exec kamal-proxy kamal-proxy deploy app-web --target="123:80"/, output
|
||||
assert_match "docker container ls --all --filter name=^app-web-123$ --quiet | xargs docker stop", output
|
||||
end
|
||||
end
|
||||
@@ -392,8 +392,8 @@ class CliAppTest < CliTestCase
|
||||
SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info).returns("123") # old version
|
||||
|
||||
run_command("boot", config: :with_proxy_roles, host: nil).tap do |output|
|
||||
assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"123:80\" --deploy-timeout \"6s\" --drain-timeout \"30s\" --target-timeout \"10s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" --log-request-header \"User-Agent\"", output
|
||||
assert_match "docker exec kamal-proxy kamal-proxy deploy app-web2 --target \"123:80\" --deploy-timeout \"6s\" --drain-timeout \"30s\" --target-timeout \"15s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" --log-request-header \"User-Agent\"", output
|
||||
assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target=\"123:80\" --deploy-timeout=\"6s\" --drain-timeout=\"30s\" --target-timeout=\"10s\" --buffer-requests --buffer-responses --log-request-header=\"Cache-Control\" --log-request-header=\"Last-Modified\" --log-request-header=\"User-Agent\"", output
|
||||
assert_match "docker exec kamal-proxy kamal-proxy deploy app-web2 --target=\"123:80\" --deploy-timeout=\"6s\" --drain-timeout=\"30s\" --target-timeout=\"15s\" --buffer-requests --buffer-responses --log-request-header=\"Cache-Control\" --log-request-header=\"Last-Modified\" --log-request-header=\"User-Agent\"", output
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ class CliProxyTest < CliTestCase
|
||||
test "boot" do
|
||||
run_command("boot").tap do |output|
|
||||
assert_match "docker login", output
|
||||
assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443\") #{KAMAL.config.proxy_image}", output
|
||||
assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") #{KAMAL.config.proxy_image}", output
|
||||
end
|
||||
end
|
||||
|
||||
@@ -18,11 +18,11 @@ class CliProxyTest < CliTestCase
|
||||
exception = assert_raises do
|
||||
run_command("boot").tap do |output|
|
||||
assert_match "docker login", output
|
||||
assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443\") #{KAMAL.config.proxy_image}", output
|
||||
assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") #{KAMAL.config.proxy_image}", output
|
||||
end
|
||||
end
|
||||
|
||||
assert_includes exception.message, "kamal-proxy version v0.0.1 is too old, please reboot to update to at least #{Kamal::Configuration::PROXY_MINIMUM_VERSION}"
|
||||
assert_includes exception.message, "kamal-proxy version v0.0.1 is too old, run `kamal proxy reboot` in order to update to at least #{Kamal::Configuration::PROXY_MINIMUM_VERSION}"
|
||||
ensure
|
||||
Thread.report_on_exception = false
|
||||
end
|
||||
@@ -36,7 +36,7 @@ class CliProxyTest < CliTestCase
|
||||
|
||||
run_command("boot").tap do |output|
|
||||
assert_match "docker login", output
|
||||
assert_match "docker container start kamal-proxy || docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443\") #{KAMAL.config.proxy_image}", output
|
||||
assert_match "docker container start kamal-proxy || docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") #{KAMAL.config.proxy_image}", output
|
||||
end
|
||||
ensure
|
||||
Thread.report_on_exception = false
|
||||
@@ -57,14 +57,14 @@ class CliProxyTest < CliTestCase
|
||||
assert_match "docker container stop kamal-proxy on 1.1.1.1", output
|
||||
assert_match "Running docker container stop traefik ; docker container prune --force --filter label=org.opencontainers.image.title=Traefik && docker image prune --all --force --filter label=org.opencontainers.image.title=Traefik on 1.1.1.1", output
|
||||
assert_match "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy on 1.1.1.1", output
|
||||
assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443\") #{KAMAL.config.proxy_image} on 1.1.1.1", output
|
||||
assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"abcdefabcdef:80\" --deploy-timeout \"6s\" --drain-timeout \"30s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" --log-request-header \"User-Agent\" on 1.1.1.1", output
|
||||
assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") #{KAMAL.config.proxy_image} on 1.1.1.1", output
|
||||
assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target=\"abcdefabcdef:80\" --deploy-timeout=\"6s\" --drain-timeout=\"30s\" --buffer-requests --buffer-responses --log-request-header=\"Cache-Control\" --log-request-header=\"Last-Modified\" --log-request-header=\"User-Agent\" on 1.1.1.1", output
|
||||
|
||||
assert_match "docker container stop kamal-proxy on 1.1.1.2", output
|
||||
assert_match "Running docker container stop traefik ; docker container prune --force --filter label=org.opencontainers.image.title=Traefik && docker image prune --all --force --filter label=org.opencontainers.image.title=Traefik on 1.1.1.2", output
|
||||
assert_match "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy on 1.1.1.2", output
|
||||
assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443\") #{KAMAL.config.proxy_image} on 1.1.1.2", output
|
||||
assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"abcdefabcdef:80\" --deploy-timeout \"6s\" --drain-timeout \"30s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" --log-request-header \"User-Agent\" on 1.1.1.2", output
|
||||
assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") #{KAMAL.config.proxy_image} on 1.1.1.2", output
|
||||
assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target=\"abcdefabcdef:80\" --deploy-timeout=\"6s\" --drain-timeout=\"30s\" --buffer-requests --buffer-responses --log-request-header=\"Cache-Control\" --log-request-header=\"Last-Modified\" --log-request-header=\"User-Agent\" on 1.1.1.2", output
|
||||
end
|
||||
end
|
||||
|
||||
@@ -198,13 +198,13 @@ class CliProxyTest < CliTestCase
|
||||
assert_match "/usr/bin/env mkdir -p .kamal", output
|
||||
assert_match "docker network create kamal", output
|
||||
assert_match "docker login -u [REDACTED] -p [REDACTED]", output
|
||||
assert_match "docker container start kamal-proxy || docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443\") basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}", output
|
||||
assert_match "docker container start kamal-proxy || docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}", output
|
||||
assert_match "/usr/bin/env mkdir -p .kamal", output
|
||||
assert_match %r{docker rename app-web-latest app-web-latest_replaced_.*}, output
|
||||
assert_match "/usr/bin/env mkdir -p .kamal/apps/app/env/roles", output
|
||||
assert_match "Uploading \"\\n\" to .kamal/apps/app/env/roles/web.env", output
|
||||
assert_match %r{docker run --detach --restart unless-stopped --name app-web-latest --network kamal --hostname 1.1.1.1-.* -e KAMAL_CONTAINER_NAME="app-web-latest" -e KAMAL_VERSION="latest" --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size="10m" --label service="app" --label role="web" --label destination dhh/app:latest}, output
|
||||
assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"12345678:80\" --deploy-timeout \"6s\" --drain-timeout \"30s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" --log-request-header \"User-Agent\"", output
|
||||
assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target=\"12345678:80\" --deploy-timeout=\"6s\" --drain-timeout=\"30s\" --buffer-requests --buffer-responses --log-request-header=\"Cache-Control\" --log-request-header=\"Last-Modified\" --log-request-header=\"User-Agent\"", output
|
||||
assert_match "docker container ls --all --filter name=^app-web-12345678$ --quiet | xargs docker stop", output
|
||||
assert_match "docker tag dhh/app:latest dhh/app:latest", output
|
||||
assert_match "/usr/bin/env mkdir -p .kamal", output
|
||||
@@ -240,7 +240,7 @@ class CliProxyTest < CliTestCase
|
||||
run_command("boot_config", "set").tap do |output|
|
||||
%w[ 1.1.1.1 1.1.1.2 ].each do |host|
|
||||
assert_match "Running /usr/bin/env mkdir -p .kamal/proxy on #{host}", output
|
||||
assert_match "Uploading \"--publish 80:80 --publish 443:443\" to .kamal/proxy/options on #{host}", output
|
||||
assert_match "Uploading \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\" to .kamal/proxy/options on #{host}", output
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -249,7 +249,25 @@ class CliProxyTest < CliTestCase
|
||||
run_command("boot_config", "set", "--publish", "false").tap do |output|
|
||||
%w[ 1.1.1.1 1.1.1.2 ].each do |host|
|
||||
assert_match "Running /usr/bin/env mkdir -p .kamal/proxy on #{host}", output
|
||||
assert_match "Uploading \"\" to .kamal/proxy/options on #{host}", output
|
||||
assert_match "Uploading \"--log-opt max-size=10m\" to .kamal/proxy/options on #{host}", output
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
test "boot_config set custom max_size" do
|
||||
run_command("boot_config", "set", "--log-max-size", "100m").tap do |output|
|
||||
%w[ 1.1.1.1 1.1.1.2 ].each do |host|
|
||||
assert_match "Running /usr/bin/env mkdir -p .kamal/proxy on #{host}", output
|
||||
assert_match "Uploading \"--publish 80:80 --publish 443:443 --log-opt max-size=100m\" to .kamal/proxy/options on #{host}", output
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
test "boot_config set no log max size" do
|
||||
run_command("boot_config", "set", "--log-max-size=").tap do |output|
|
||||
%w[ 1.1.1.1 1.1.1.2 ].each do |host|
|
||||
assert_match "Running /usr/bin/env mkdir -p .kamal/proxy on #{host}", output
|
||||
assert_match "Uploading \"--publish 80:80 --publish 443:443\" to .kamal/proxy/options on #{host}", output
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -258,7 +276,7 @@ class CliProxyTest < CliTestCase
|
||||
run_command("boot_config", "set", "--http-port", "8080", "--https-port", "8443").tap do |output|
|
||||
%w[ 1.1.1.1 1.1.1.2 ].each do |host|
|
||||
assert_match "Running /usr/bin/env mkdir -p .kamal/proxy on #{host}", output
|
||||
assert_match "Uploading \"--publish 8080:80 --publish 8443:443\" to .kamal/proxy/options on #{host}", output
|
||||
assert_match "Uploading \"--publish 8080:80 --publish 8443:443 --log-opt max-size=10m\" to .kamal/proxy/options on #{host}", output
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -267,14 +285,14 @@ class CliProxyTest < CliTestCase
|
||||
run_command("boot_config", "set", "--docker_options", "label=foo=bar", "add_host=thishost:thathost").tap do |output|
|
||||
%w[ 1.1.1.1 1.1.1.2 ].each do |host|
|
||||
assert_match "Running /usr/bin/env mkdir -p .kamal/proxy on #{host}", output
|
||||
assert_match "Uploading \"--publish 80:80 --publish 443:443 --label=foo=bar --add_host=thishost:thathost\" to .kamal/proxy/options on #{host}", output
|
||||
assert_match "Uploading \"--publish 80:80 --publish 443:443 --log-opt max-size=10m --label=foo=bar --add_host=thishost:thathost\" to .kamal/proxy/options on #{host}", output
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
test "boot_config get" do
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||
.with(:cat, ".kamal/proxy/options", "||", :echo, "\"--publish 80:80 --publish 443:443\"")
|
||||
.with(:cat, ".kamal/proxy/options", "||", :echo, "\"--publish 80:80 --publish 443:443 --log-opt max-size=10m\"")
|
||||
.returns("--publish 80:80 --publish 8443:443 --label=foo=bar")
|
||||
.twice
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ require_relative "cli_test_case"
|
||||
class CliSecretsTest < CliTestCase
|
||||
test "fetch" do
|
||||
assert_equal \
|
||||
"{\"foo\":\"oof\",\"bar\":\"rab\",\"baz\":\"zab\"}",
|
||||
"\\{\\\"foo\\\":\\\"oof\\\",\\\"bar\\\":\\\"rab\\\",\\\"baz\\\":\\\"zab\\\"\\}",
|
||||
run_command("fetch", "foo", "bar", "baz", "--account", "myaccount", "--adapter", "test")
|
||||
end
|
||||
|
||||
@@ -15,6 +15,12 @@ class CliSecretsTest < CliTestCase
|
||||
assert_equal "oof", run_command("extract", "foo", "{\"abc/foo\":\"oof\", \"bar\":\"rab\", \"baz\":\"zab\"}")
|
||||
end
|
||||
|
||||
test "print" do
|
||||
with_test_secrets("secrets" => "SECRET1=ABC\nSECRET2=${SECRET1}DEF\n") do
|
||||
assert_equal "SECRET1=ABC\nSECRET2=ABCDEF", run_command("print")
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def run_command(*command)
|
||||
stdouted { Kamal::Cli::Secrets.start([ *command, "-c", "test/fixtures/deploy_with_accessories.yml" ]) }
|
||||
|
||||
@@ -150,6 +150,27 @@ class CommanderTest < ActiveSupport::TestCase
|
||||
assert_equal [ "1.1.1.2" ], @kamal.proxy_hosts
|
||||
end
|
||||
|
||||
test "accessory hosts without filtering" do
|
||||
configure_with(:deploy_with_single_accessory)
|
||||
assert_equal [ "1.1.1.5" ], @kamal.accessory_hosts
|
||||
|
||||
configure_with(:deploy_with_accessories_on_independent_server)
|
||||
assert_equal [ "1.1.1.5", "1.1.1.1", "1.1.1.2" ], @kamal.accessory_hosts
|
||||
end
|
||||
|
||||
test "accessory hosts with role filtering" do
|
||||
configure_with(:deploy_with_single_accessory)
|
||||
@kamal.specific_roles = [ "web" ]
|
||||
assert_equal [], @kamal.accessory_hosts
|
||||
|
||||
configure_with(:deploy_with_accessories_on_independent_server)
|
||||
@kamal.specific_roles = [ "web" ]
|
||||
assert_equal [ "1.1.1.1", "1.1.1.2" ], @kamal.accessory_hosts
|
||||
|
||||
@kamal.specific_roles = [ "workers" ]
|
||||
assert_equal [], @kamal.accessory_hosts
|
||||
end
|
||||
|
||||
private
|
||||
def configure_with(variant)
|
||||
@kamal = Kamal::Commander.new.tap do |kamal|
|
||||
|
||||
@@ -71,6 +71,14 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
|
||||
new_command(:busybox).run.join(" ")
|
||||
end
|
||||
|
||||
test "run in custom network" do
|
||||
@config[:accessories]["mysql"]["network"] = "custom"
|
||||
|
||||
assert_equal \
|
||||
"docker run --name app-mysql --detach --restart unless-stopped --network custom --log-opt max-size=\"10m\" --publish 3306:3306 --env MYSQL_ROOT_HOST=\"%\" --env-file .kamal/apps/app/env/accessories/mysql.env --label service=\"app-mysql\" private.registry/mysql:8.0",
|
||||
new_command(:mysql).run.join(" ")
|
||||
end
|
||||
|
||||
test "start" do
|
||||
assert_equal \
|
||||
"docker container start app-mysql",
|
||||
|
||||
@@ -115,14 +115,38 @@ class CommandsAppTest < ActiveSupport::TestCase
|
||||
|
||||
test "deploy" do
|
||||
assert_equal \
|
||||
"docker exec kamal-proxy kamal-proxy deploy app-web --target \"172.1.0.2:80\" --deploy-timeout \"30s\" --drain-timeout \"30s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" --log-request-header \"User-Agent\"",
|
||||
"docker exec kamal-proxy kamal-proxy deploy app-web --target=\"172.1.0.2:80\" --deploy-timeout=\"30s\" --drain-timeout=\"30s\" --buffer-requests --buffer-responses --log-request-header=\"Cache-Control\" --log-request-header=\"Last-Modified\" --log-request-header=\"User-Agent\"",
|
||||
new_command.deploy(target: "172.1.0.2").join(" ")
|
||||
end
|
||||
|
||||
test "deploy with SSL" do
|
||||
@config[:proxy] = { "ssl" => true, "host" => "example.com" }
|
||||
|
||||
assert_equal \
|
||||
"docker exec kamal-proxy kamal-proxy deploy app-web --target=\"172.1.0.2:80\" --host=\"example.com\" --tls --deploy-timeout=\"30s\" --drain-timeout=\"30s\" --buffer-requests --buffer-responses --log-request-header=\"Cache-Control\" --log-request-header=\"Last-Modified\" --log-request-header=\"User-Agent\"",
|
||||
new_command.deploy(target: "172.1.0.2").join(" ")
|
||||
end
|
||||
|
||||
test "deploy with SSL targeting multiple hosts" do
|
||||
@config[:proxy] = { "ssl" => true, "hosts" => [ "example.com", "anotherexample.com" ] }
|
||||
|
||||
assert_equal \
|
||||
"docker exec kamal-proxy kamal-proxy deploy app-web --target=\"172.1.0.2:80\" --host=\"example.com\" --host=\"anotherexample.com\" --tls --deploy-timeout=\"30s\" --drain-timeout=\"30s\" --buffer-requests --buffer-responses --log-request-header=\"Cache-Control\" --log-request-header=\"Last-Modified\" --log-request-header=\"User-Agent\"",
|
||||
new_command.deploy(target: "172.1.0.2").join(" ")
|
||||
end
|
||||
|
||||
test "deploy with SSL false" do
|
||||
@config[:proxy] = { "ssl" => false }
|
||||
|
||||
assert_equal \
|
||||
"docker exec kamal-proxy kamal-proxy deploy app-web --target=\"172.1.0.2:80\" --deploy-timeout=\"30s\" --drain-timeout=\"30s\" --buffer-requests --buffer-responses --log-request-header=\"Cache-Control\" --log-request-header=\"Last-Modified\" --log-request-header=\"User-Agent\"",
|
||||
new_command.deploy(target: "172.1.0.2").join(" ")
|
||||
end
|
||||
|
||||
test "remove" do
|
||||
assert_equal \
|
||||
"docker exec kamal-proxy kamal-proxy remove app-web --target \"172.1.0.2:80\"",
|
||||
new_command.remove(target: "172.1.0.2").join(" ")
|
||||
"docker exec kamal-proxy kamal-proxy remove app-web",
|
||||
new_command.remove.join(" ")
|
||||
end
|
||||
|
||||
|
||||
|
||||
@@ -144,20 +144,45 @@ class CommandsBuilderTest < ActiveSupport::TestCase
|
||||
builder.push.join(" ")
|
||||
end
|
||||
|
||||
test "push with provenance" do
|
||||
builder = new_builder_command(builder: { "provenance" => "mode=max" })
|
||||
assert_equal \
|
||||
"docker buildx build --push --platform linux/amd64 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile --provenance mode=max .",
|
||||
builder.push.join(" ")
|
||||
end
|
||||
|
||||
test "push with provenance false" do
|
||||
builder = new_builder_command(builder: { "provenance" => false })
|
||||
assert_equal \
|
||||
"docker buildx build --push --platform linux/amd64 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile --provenance false .",
|
||||
builder.push.join(" ")
|
||||
end
|
||||
|
||||
test "mirror count" do
|
||||
command = new_builder_command
|
||||
assert_equal "docker info --format '{{index .RegistryConfig.Mirrors 0}}'", command.first_mirror.join(" ")
|
||||
end
|
||||
|
||||
test "clone path with spaces" do
|
||||
command = new_builder_command
|
||||
Kamal::Git.stubs(:root).returns("/absolute/path with spaces")
|
||||
clone_command = command.clone.join(" ")
|
||||
clone_reset_commands = command.clone_reset_steps.map { |a| a.join(" ") }
|
||||
|
||||
assert_match(%r{path\\ with\\ space}, clone_command)
|
||||
assert_no_match(%r{path with spaces}, clone_command)
|
||||
|
||||
clone_reset_commands.each do |command|
|
||||
assert_match(%r{path\\ with\\ space}, command)
|
||||
assert_no_match(%r{path with spaces}, command)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def new_builder_command(additional_config = {})
|
||||
Kamal::Commands::Builder.new(Kamal::Configuration.new(@config.deep_merge(additional_config), version: "123"))
|
||||
end
|
||||
|
||||
def build_directory
|
||||
"#{Dir.tmpdir}/kamal-clones/app/kamal/"
|
||||
end
|
||||
|
||||
def local_arch
|
||||
Kamal::Utils.docker_arch
|
||||
end
|
||||
|
||||
@@ -15,7 +15,7 @@ class CommandsProxyTest < ActiveSupport::TestCase
|
||||
|
||||
test "run" do
|
||||
assert_equal \
|
||||
"docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443\") basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}",
|
||||
"docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}",
|
||||
new_command.run.join(" ")
|
||||
end
|
||||
|
||||
@@ -23,7 +23,7 @@ class CommandsProxyTest < ActiveSupport::TestCase
|
||||
@config.delete(:proxy)
|
||||
|
||||
assert_equal \
|
||||
"docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443\") basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}",
|
||||
"docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}",
|
||||
new_command.run.join(" ")
|
||||
end
|
||||
|
||||
@@ -113,7 +113,7 @@ class CommandsProxyTest < ActiveSupport::TestCase
|
||||
|
||||
test "get_boot_options" do
|
||||
assert_equal \
|
||||
"cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443\"",
|
||||
"cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\"",
|
||||
new_command.get_boot_options.join(" ")
|
||||
end
|
||||
|
||||
|
||||
@@ -152,4 +152,13 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase
|
||||
test "options" do
|
||||
assert_equal [ "--cpus", "\"4\"", "--memory", "\"2GB\"" ], @config.accessory(:redis).option_args
|
||||
end
|
||||
|
||||
test "network_args default" do
|
||||
assert_equal [ "--network", "kamal" ], @config.accessory(:mysql).network_args
|
||||
end
|
||||
|
||||
test "network_args with configured options" do
|
||||
@deploy[:accessories]["mysql"]["network"] = "database"
|
||||
assert_equal [ "--network", "database" ], @config.accessory(:mysql).network_args
|
||||
end
|
||||
end
|
||||
|
||||
@@ -64,7 +64,7 @@ class ConfigurationBuilderTest < ActiveSupport::TestCase
|
||||
@deploy[:builder] = { "arch" => "amd64", "cache" => { "type" => "registry", "options" => "mode=max,image-manifest=true,oci-mediatypes=true" } }
|
||||
|
||||
assert_equal "type=registry,ref=dhh/app-build-cache", config.builder.cache_from
|
||||
assert_equal "type=registry,mode=max,image-manifest=true,oci-mediatypes=true,ref=dhh/app-build-cache", config.builder.cache_to
|
||||
assert_equal "type=registry,ref=dhh/app-build-cache,mode=max,image-manifest=true,oci-mediatypes=true", config.builder.cache_to
|
||||
end
|
||||
|
||||
test "setting registry cache when using a custom registry" do
|
||||
@@ -72,14 +72,14 @@ class ConfigurationBuilderTest < ActiveSupport::TestCase
|
||||
@deploy[:builder] = { "arch" => "amd64", "cache" => { "type" => "registry", "options" => "mode=max,image-manifest=true,oci-mediatypes=true" } }
|
||||
|
||||
assert_equal "type=registry,ref=registry.example.com/dhh/app-build-cache", config.builder.cache_from
|
||||
assert_equal "type=registry,mode=max,image-manifest=true,oci-mediatypes=true,ref=registry.example.com/dhh/app-build-cache", config.builder.cache_to
|
||||
assert_equal "type=registry,ref=registry.example.com/dhh/app-build-cache,mode=max,image-manifest=true,oci-mediatypes=true", config.builder.cache_to
|
||||
end
|
||||
|
||||
test "setting registry cache with image" do
|
||||
@deploy[:builder] = { "arch" => "amd64", "cache" => { "type" => "registry", "image" => "kamal", "options" => "mode=max" } }
|
||||
|
||||
assert_equal "type=registry,ref=kamal", config.builder.cache_from
|
||||
assert_equal "type=registry,mode=max,ref=kamal", config.builder.cache_to
|
||||
assert_equal "type=registry,ref=kamal,mode=max", config.builder.cache_to
|
||||
end
|
||||
|
||||
test "args" do
|
||||
@@ -134,6 +134,16 @@ class ConfigurationBuilderTest < ActiveSupport::TestCase
|
||||
assert_equal "default=$SSH_AUTH_SOCK", config.builder.ssh
|
||||
end
|
||||
|
||||
test "provenance" do
|
||||
assert_nil config.builder.provenance
|
||||
end
|
||||
|
||||
test "setting provenance" do
|
||||
@deploy[:builder]["provenance"] = "mode=max"
|
||||
|
||||
assert_equal "mode=max", config.builder.provenance
|
||||
end
|
||||
|
||||
test "local disabled but no remote set" do
|
||||
@deploy[:builder]["local"] = false
|
||||
|
||||
|
||||
@@ -13,15 +13,29 @@ class ConfigurationProxyTest < ActiveSupport::TestCase
|
||||
assert_equal true, config.proxy.ssl?
|
||||
end
|
||||
|
||||
test "ssl with multiple hosts passed via host" do
|
||||
@deploy[:proxy] = { "ssl" => true, "host" => "example.com,anotherexample.com" }
|
||||
assert_equal true, config.proxy.ssl?
|
||||
end
|
||||
|
||||
test "ssl with multiple hosts passed via hosts" do
|
||||
@deploy[:proxy] = { "ssl" => true, "hosts" => [ "example.com", "anotherexample.com" ] }
|
||||
assert_equal true, config.proxy.ssl?
|
||||
end
|
||||
|
||||
test "ssl with no host" do
|
||||
@deploy[:proxy] = { "ssl" => true }
|
||||
assert_raises(Kamal::ConfigurationError) { config.proxy.ssl? }
|
||||
end
|
||||
|
||||
test "ssl with both host and hosts" do
|
||||
@deploy[:proxy] = { "ssl" => true, host: "example.com", hosts: [ "anotherexample.com" ] }
|
||||
assert_raises(Kamal::ConfigurationError) { config.proxy.ssl? }
|
||||
end
|
||||
|
||||
test "ssl false" do
|
||||
@deploy[:proxy] = { "ssl" => false }
|
||||
assert_not config.proxy.ssl?
|
||||
assert_not config.proxy.deploy_options.has_key?(:tls)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -222,6 +222,13 @@ class ConfigurationTest < ActiveSupport::TestCase
|
||||
assert_equal "my-user", config.registry.username
|
||||
end
|
||||
|
||||
test "destination is loaded into env" do
|
||||
dest_config_file = Pathname.new(File.expand_path("fixtures/deploy_for_dest.yml", __dir__))
|
||||
|
||||
config = Kamal::Configuration.create_from config_file: dest_config_file, destination: "world"
|
||||
assert_equal ENV["KAMAL_DESTINATION"], "world"
|
||||
end
|
||||
|
||||
test "destination yml config merge" do
|
||||
dest_config_file = Pathname.new(File.expand_path("fixtures/deploy_for_dest.yml", __dir__))
|
||||
|
||||
@@ -377,4 +384,15 @@ class ConfigurationTest < ActiveSupport::TestCase
|
||||
|
||||
assert_equal "Different roles can't share the same host for SSL: foo.example.com", exception.message
|
||||
end
|
||||
|
||||
test "two proxy ssl roles with same host in a hosts array" do
|
||||
@deploy_with_roles[:servers]["web"] = { "hosts" => [ "1.1.1.1" ], "proxy" => { "ssl" => true, "hosts" => [ "foo.example.com", "bar.example.com" ] } }
|
||||
@deploy_with_roles[:servers]["workers"] = { "hosts" => [ "1.1.1.1" ], "proxy" => { "ssl" => true, "hosts" => [ "www.example.com", "foo.example.com" ] } }
|
||||
|
||||
exception = assert_raises(Kamal::ConfigurationError) do
|
||||
Kamal::Configuration.new(@deploy_with_roles)
|
||||
end
|
||||
|
||||
assert_equal "Different roles can't share the same host for SSL: foo.example.com", exception.message
|
||||
end
|
||||
end
|
||||
|
||||
@@ -11,6 +11,16 @@ class EnvFileTest < ActiveSupport::TestCase
|
||||
Kamal::EnvFile.new(env).to_s
|
||||
end
|
||||
|
||||
test "to_s won't escape '#'" do
|
||||
env = {
|
||||
"foo" => '#$foo',
|
||||
"bar" => '#{bar}'
|
||||
}
|
||||
|
||||
assert_equal "foo=\#$foo\nbar=\#{bar}\n", \
|
||||
Kamal::EnvFile.new(env).to_s
|
||||
end
|
||||
|
||||
test "to_str won't escape chinese characters" do
|
||||
env = {
|
||||
"foo" => '你好 means hello, "欢迎" means welcome, that\'s simple! 😃 {smile}'
|
||||
|
||||
38
test/fixtures/deploy_with_accessories_on_independent_server.yml
vendored
Normal file
38
test/fixtures/deploy_with_accessories_on_independent_server.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
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
|
||||
builder:
|
||||
arch: amd64
|
||||
|
||||
accessories:
|
||||
mysql:
|
||||
image: mysql:5.7
|
||||
host: 1.1.1.5
|
||||
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
test/fixtures/deploy_with_single_accessory.yml
vendored
Normal file
29
test/fixtures/deploy_with_single_accessory.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
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
|
||||
builder:
|
||||
arch: amd64
|
||||
|
||||
accessories:
|
||||
mysql:
|
||||
image: mysql:5.7
|
||||
host: 1.1.1.5
|
||||
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
|
||||
@@ -8,7 +8,7 @@ class AppTest < IntegrationTest
|
||||
|
||||
kamal :app, :stop
|
||||
|
||||
assert_app_is_down
|
||||
assert_app_not_found
|
||||
|
||||
kamal :app, :start
|
||||
|
||||
@@ -48,7 +48,7 @@ class AppTest < IntegrationTest
|
||||
|
||||
kamal :app, :remove
|
||||
|
||||
assert_app_is_down
|
||||
assert_app_not_found
|
||||
assert_app_directory_removed
|
||||
end
|
||||
end
|
||||
|
||||
@@ -15,14 +15,15 @@ readiness_delay: 0
|
||||
|
||||
proxy:
|
||||
host: localhost
|
||||
ssl: false
|
||||
healthcheck:
|
||||
interval: 1
|
||||
timeout: 1
|
||||
path: "/up"
|
||||
response_timeout: 2
|
||||
buffering:
|
||||
requests: true
|
||||
responses: true
|
||||
requests: false
|
||||
responses: false
|
||||
memory: 400_000
|
||||
max_request_body: 40_000_000
|
||||
max_response_body: 40_000_000
|
||||
|
||||
@@ -50,6 +50,12 @@ class IntegrationTest < ActiveSupport::TestCase
|
||||
assert_equal "502", response.code
|
||||
end
|
||||
|
||||
def assert_app_not_found
|
||||
response = app_response
|
||||
debug_response_code(response, "404")
|
||||
assert_equal "404", response.code
|
||||
end
|
||||
|
||||
def assert_app_is_up(version: nil, app: @app)
|
||||
response = app_response(app: app)
|
||||
debug_response_code(response, "200")
|
||||
|
||||
@@ -2,23 +2,40 @@ require "test_helper"
|
||||
|
||||
class BitwardenAdapterTest < SecretAdapterTestCase
|
||||
test "fetch" do
|
||||
stub_ticks.with("bw --version 2> /dev/null")
|
||||
|
||||
stub_unlocked
|
||||
stub_ticks.with("bw sync").returns("")
|
||||
stub_mypassword
|
||||
|
||||
json = JSON.parse(run_command("fetch", "mypassword"))
|
||||
json = JSON.parse(shellunescape(run_command("fetch", "mypassword")))
|
||||
|
||||
expected_json = { "mypassword"=>"secret123" }
|
||||
|
||||
assert_equal expected_json, json
|
||||
end
|
||||
|
||||
test "fetch with no login" do
|
||||
stub_ticks.with("bw --version 2> /dev/null")
|
||||
|
||||
stub_unlocked
|
||||
stub_ticks.with("bw sync").returns("")
|
||||
stub_noteitem
|
||||
|
||||
error = assert_raises RuntimeError do
|
||||
JSON.parse(shellunescape(run_command("fetch", "mynote")))
|
||||
end
|
||||
assert_match(/not a login type item/, error.message)
|
||||
end
|
||||
|
||||
test "fetch with from" do
|
||||
stub_ticks.with("bw --version 2> /dev/null")
|
||||
|
||||
stub_unlocked
|
||||
stub_ticks.with("bw sync").returns("")
|
||||
stub_myitem
|
||||
|
||||
json = JSON.parse(run_command("fetch", "--from", "myitem", "field1", "field2", "field3"))
|
||||
json = JSON.parse(shellunescape(run_command("fetch", "--from", "myitem", "field1", "field2", "field3")))
|
||||
|
||||
expected_json = {
|
||||
"myitem/field1"=>"secret1", "myitem/field2"=>"blam", "myitem/field3"=>"fewgrwjgk"
|
||||
@@ -27,7 +44,26 @@ class BitwardenAdapterTest < SecretAdapterTestCase
|
||||
assert_equal expected_json, json
|
||||
end
|
||||
|
||||
test "fetch all with from" do
|
||||
stub_ticks.with("bw --version 2> /dev/null")
|
||||
|
||||
stub_unlocked
|
||||
stub_ticks.with("bw sync").returns("")
|
||||
stub_noteitem_with_fields
|
||||
|
||||
json = JSON.parse(shellunescape(run_command("fetch", "mynotefields")))
|
||||
|
||||
expected_json = {
|
||||
"mynotefields/field1"=>"secret1", "mynotefields/field2"=>"blam", "mynotefields/field3"=>"fewgrwjgk",
|
||||
"mynotefields/field4"=>"auto"
|
||||
}
|
||||
|
||||
assert_equal expected_json, json
|
||||
end
|
||||
|
||||
test "fetch with multiple items" do
|
||||
stub_ticks.with("bw --version 2> /dev/null")
|
||||
|
||||
stub_unlocked
|
||||
|
||||
stub_ticks.with("bw sync").returns("")
|
||||
@@ -59,7 +95,7 @@ class BitwardenAdapterTest < SecretAdapterTestCase
|
||||
JSON
|
||||
|
||||
|
||||
json = JSON.parse(run_command("fetch", "mypassword", "myitem/field1", "myitem/field2", "myitem2/field3"))
|
||||
json = JSON.parse(shellunescape(run_command("fetch", "mypassword", "myitem/field1", "myitem/field2", "myitem2/field3")))
|
||||
|
||||
expected_json = {
|
||||
"mypassword"=>"secret123", "myitem/field1"=>"secret1", "myitem/field2"=>"blam", "myitem2/field3"=>"fewgrwjgk"
|
||||
@@ -69,6 +105,8 @@ class BitwardenAdapterTest < SecretAdapterTestCase
|
||||
end
|
||||
|
||||
test "fetch unauthenticated" do
|
||||
stub_ticks.with("bw --version 2> /dev/null")
|
||||
|
||||
stub_ticks
|
||||
.with("bw status")
|
||||
.returns(
|
||||
@@ -82,7 +120,7 @@ class BitwardenAdapterTest < SecretAdapterTestCase
|
||||
stub_ticks.with("bw sync").returns("")
|
||||
stub_mypassword
|
||||
|
||||
json = JSON.parse(run_command("fetch", "mypassword"))
|
||||
json = JSON.parse(shellunescape(run_command("fetch", "mypassword")))
|
||||
|
||||
expected_json = { "mypassword"=>"secret123" }
|
||||
|
||||
@@ -90,6 +128,8 @@ class BitwardenAdapterTest < SecretAdapterTestCase
|
||||
end
|
||||
|
||||
test "fetch locked" do
|
||||
stub_ticks.with("bw --version 2> /dev/null")
|
||||
|
||||
stub_ticks
|
||||
.with("bw status")
|
||||
.returns(
|
||||
@@ -107,7 +147,7 @@ class BitwardenAdapterTest < SecretAdapterTestCase
|
||||
stub_ticks.with("bw sync").returns("")
|
||||
stub_mypassword
|
||||
|
||||
json = JSON.parse(run_command("fetch", "mypassword"))
|
||||
json = JSON.parse(shellunescape(run_command("fetch", "mypassword")))
|
||||
|
||||
expected_json = { "mypassword"=>"secret123" }
|
||||
|
||||
@@ -115,6 +155,8 @@ class BitwardenAdapterTest < SecretAdapterTestCase
|
||||
end
|
||||
|
||||
test "fetch locked with session" do
|
||||
stub_ticks.with("bw --version 2> /dev/null")
|
||||
|
||||
stub_ticks
|
||||
.with("bw status")
|
||||
.returns(
|
||||
@@ -132,13 +174,22 @@ class BitwardenAdapterTest < SecretAdapterTestCase
|
||||
stub_ticks.with("BW_SESSION=0987654321 bw sync").returns("")
|
||||
stub_mypassword(session: "0987654321")
|
||||
|
||||
json = JSON.parse(run_command("fetch", "mypassword"))
|
||||
json = JSON.parse(shellunescape(run_command("fetch", "mypassword")))
|
||||
|
||||
expected_json = { "mypassword"=>"secret123" }
|
||||
|
||||
assert_equal expected_json, json
|
||||
end
|
||||
|
||||
test "fetch without CLI installed" do
|
||||
stub_ticks_with("bw --version 2> /dev/null", succeed: false)
|
||||
|
||||
error = assert_raises RuntimeError do
|
||||
JSON.parse(shellunescape(run_command("fetch", "mynote")))
|
||||
end
|
||||
assert_equal "Bitwarden CLI is not installed", error.message
|
||||
end
|
||||
|
||||
private
|
||||
def run_command(*command)
|
||||
stdouted do
|
||||
@@ -181,6 +232,60 @@ class BitwardenAdapterTest < SecretAdapterTestCase
|
||||
JSON
|
||||
end
|
||||
|
||||
def stub_noteitem(session: nil)
|
||||
stub_ticks
|
||||
.with("#{"BW_SESSION=#{session} " if session}bw get item mynote")
|
||||
.returns(<<~JSON)
|
||||
{
|
||||
"passwordHistory":null,
|
||||
"revisionDate":"2024-09-28T09:07:27.461Z",
|
||||
"creationDate":"2024-09-28T09:07:00.740Z",
|
||||
"deletedDate":null,
|
||||
"object":"item",
|
||||
"id":"aaaaaaaa-cccc-eeee-0000-222222222222",
|
||||
"organizationId":null,
|
||||
"folderId":null,
|
||||
"type":2,
|
||||
"reprompt":0,
|
||||
"name":"noteitem",
|
||||
"notes":"NOTES",
|
||||
"favorite":false,
|
||||
"secureNote":{"type":0},
|
||||
"collectionIds":[]
|
||||
}
|
||||
JSON
|
||||
end
|
||||
|
||||
def stub_noteitem_with_fields(session: nil)
|
||||
stub_ticks
|
||||
.with("#{"BW_SESSION=#{session} " if session}bw get item mynotefields")
|
||||
.returns(<<~JSON)
|
||||
{
|
||||
"passwordHistory":null,
|
||||
"revisionDate":"2024-09-28T09:07:27.461Z",
|
||||
"creationDate":"2024-09-28T09:07:00.740Z",
|
||||
"deletedDate":null,
|
||||
"object":"item",
|
||||
"id":"aaaaaaaa-cccc-eeee-0000-222222222222",
|
||||
"organizationId":null,
|
||||
"folderId":null,
|
||||
"type":2,
|
||||
"reprompt":0,
|
||||
"name":"noteitem",
|
||||
"notes":"NOTES",
|
||||
"favorite":false,
|
||||
"fields":[
|
||||
{"name":"field1","value":"secret1","type":1,"linkedId":null},
|
||||
{"name":"field2","value":"blam","type":1,"linkedId":null},
|
||||
{"name":"field3","value":"fewgrwjgk","type":1,"linkedId":null},
|
||||
{"name":"field4","value":"auto","type":1,"linkedId":null}
|
||||
],
|
||||
"secureNote":{"type":0},
|
||||
"collectionIds":[]
|
||||
}
|
||||
JSON
|
||||
end
|
||||
|
||||
def stub_myitem
|
||||
stub_ticks
|
||||
.with("bw get item myitem")
|
||||
@@ -202,7 +307,8 @@ class BitwardenAdapterTest < SecretAdapterTestCase
|
||||
"fields":[
|
||||
{"name":"field1","value":"secret1","type":1,"linkedId":null},
|
||||
{"name":"field2","value":"blam","type":1,"linkedId":null},
|
||||
{"name":"field3","value":"fewgrwjgk","type":1,"linkedId":null}
|
||||
{"name":"field3","value":"fewgrwjgk","type":1,"linkedId":null},
|
||||
{"name":"field4","value":"auto","type":1,"linkedId":null}
|
||||
],
|
||||
"login":{"fido2Credentials":[],"uris":[],"username":null,"password":null,"totp":null,"passwordRevisionDate":null},"collectionIds":[]
|
||||
}
|
||||
|
||||
@@ -12,10 +12,4 @@ class SecretsInlineCommandSubstitution < SecretAdapterTestCase
|
||||
substituted = Kamal::Secrets::Dotenv::InlineCommandSubstitution.call("FOO=$(blah)", nil, overwrite: false)
|
||||
assert_equal "FOO=results", substituted
|
||||
end
|
||||
|
||||
test "escapes correctly" do
|
||||
Kamal::Cli::Main.expects(:start).with { |command| command == [ "secrets", "fetch", "...", "--inline" ] }.returns("{ \"foo\" : \"bar\" }")
|
||||
substituted = Kamal::Secrets::Dotenv::InlineCommandSubstitution.call("SECRETS=$(kamal secrets fetch ...)", nil, overwrite: false)
|
||||
assert_equal "SECRETS=\\{\\ \\\"foo\\\"\\ :\\ \\\"bar\\\"\\ \\}", substituted
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,6 +6,7 @@ class LastPassAdapterTest < SecretAdapterTestCase
|
||||
end
|
||||
|
||||
test "fetch" do
|
||||
stub_ticks.with("lpass --version 2> /dev/null")
|
||||
stub_ticks.with("lpass status --color never").returns("Logged in as email@example.com.")
|
||||
|
||||
stub_ticks
|
||||
@@ -51,7 +52,7 @@ class LastPassAdapterTest < SecretAdapterTestCase
|
||||
]
|
||||
JSON
|
||||
|
||||
json = JSON.parse(run_command("fetch", "SECRET1", "FOLDER1/FSECRET1", "FOLDER1/FSECRET2"))
|
||||
json = JSON.parse(shellunescape(run_command("fetch", "SECRET1", "FOLDER1/FSECRET1", "FOLDER1/FSECRET2")))
|
||||
|
||||
expected_json = {
|
||||
"SECRET1"=>"secret1",
|
||||
@@ -63,6 +64,7 @@ class LastPassAdapterTest < SecretAdapterTestCase
|
||||
end
|
||||
|
||||
test "fetch with from" do
|
||||
stub_ticks.with("lpass --version 2> /dev/null")
|
||||
stub_ticks.with("lpass status --color never").returns("Logged in as email@example.com.")
|
||||
|
||||
stub_ticks
|
||||
@@ -96,7 +98,7 @@ class LastPassAdapterTest < SecretAdapterTestCase
|
||||
]
|
||||
JSON
|
||||
|
||||
json = JSON.parse(run_command("fetch", "--from", "FOLDER1", "FSECRET1", "FSECRET2"))
|
||||
json = JSON.parse(shellunescape(run_command("fetch", "--from", "FOLDER1", "FSECRET1", "FSECRET2")))
|
||||
|
||||
expected_json = {
|
||||
"FOLDER1/FSECRET1"=>"fsecret1",
|
||||
@@ -107,11 +109,13 @@ class LastPassAdapterTest < SecretAdapterTestCase
|
||||
end
|
||||
|
||||
test "fetch with signin" do
|
||||
stub_ticks.with("lpass --version 2> /dev/null")
|
||||
|
||||
stub_ticks_with("lpass status --color never", succeed: false).returns("Not logged in.")
|
||||
stub_ticks_with("lpass login email@example.com", succeed: true).returns("")
|
||||
stub_ticks.with("lpass show SECRET1 --json").returns(single_item_json)
|
||||
|
||||
json = JSON.parse(run_command("fetch", "SECRET1"))
|
||||
json = JSON.parse(shellunescape(run_command("fetch", "SECRET1")))
|
||||
|
||||
expected_json = {
|
||||
"SECRET1"=>"secret1"
|
||||
@@ -120,6 +124,15 @@ class LastPassAdapterTest < SecretAdapterTestCase
|
||||
assert_equal expected_json, json
|
||||
end
|
||||
|
||||
test "fetch without CLI installed" do
|
||||
stub_ticks_with("lpass --version 2> /dev/null", succeed: false)
|
||||
|
||||
error = assert_raises RuntimeError do
|
||||
JSON.parse(shellunescape(run_command("fetch", "SECRET1", "FOLDER1/FSECRET1", "FOLDER1/FSECRET2")))
|
||||
end
|
||||
assert_equal "LastPass CLI is not installed", error.message
|
||||
end
|
||||
|
||||
private
|
||||
def run_command(*command)
|
||||
stdouted do
|
||||
|
||||
@@ -2,6 +2,7 @@ require "test_helper"
|
||||
|
||||
class SecretsOnePasswordAdapterTest < SecretAdapterTestCase
|
||||
test "fetch" do
|
||||
stub_ticks.with("op --version 2> /dev/null")
|
||||
stub_ticks.with("op account get --account myaccount 2> /dev/null")
|
||||
|
||||
stub_ticks
|
||||
@@ -44,7 +45,7 @@ class SecretsOnePasswordAdapterTest < SecretAdapterTestCase
|
||||
]
|
||||
JSON
|
||||
|
||||
json = JSON.parse(run_command("fetch", "--from", "op://myvault/myitem", "section/SECRET1", "section/SECRET2", "section2/SECRET3"))
|
||||
json = JSON.parse(shellunescape(run_command("fetch", "--from", "op://myvault/myitem", "section/SECRET1", "section/SECRET2", "section2/SECRET3")))
|
||||
|
||||
expected_json = {
|
||||
"myvault/myitem/section/SECRET1"=>"VALUE1",
|
||||
@@ -56,6 +57,7 @@ class SecretsOnePasswordAdapterTest < SecretAdapterTestCase
|
||||
end
|
||||
|
||||
test "fetch with multiple items" do
|
||||
stub_ticks.with("op --version 2> /dev/null")
|
||||
stub_ticks.with("op account get --account myaccount 2> /dev/null")
|
||||
|
||||
stub_ticks
|
||||
@@ -103,7 +105,7 @@ class SecretsOnePasswordAdapterTest < SecretAdapterTestCase
|
||||
}
|
||||
JSON
|
||||
|
||||
json = JSON.parse(run_command("fetch", "--from", "op://myvault", "myitem/section/SECRET1", "myitem/section/SECRET2", "myitem2/section2/SECRET3"))
|
||||
json = JSON.parse(shellunescape(run_command("fetch", "--from", "op://myvault", "myitem/section/SECRET1", "myitem/section/SECRET2", "myitem2/section2/SECRET3")))
|
||||
|
||||
expected_json = {
|
||||
"myvault/myitem/section/SECRET1"=>"VALUE1",
|
||||
@@ -115,6 +117,8 @@ class SecretsOnePasswordAdapterTest < SecretAdapterTestCase
|
||||
end
|
||||
|
||||
test "fetch with signin, no session" do
|
||||
stub_ticks.with("op --version 2> /dev/null")
|
||||
|
||||
stub_ticks_with("op account get --account myaccount 2> /dev/null", succeed: false)
|
||||
stub_ticks_with("op signin --account \"myaccount\" --force --raw", succeed: true).returns("")
|
||||
|
||||
@@ -122,7 +126,7 @@ class SecretsOnePasswordAdapterTest < SecretAdapterTestCase
|
||||
.with("op item get myitem --vault \"myvault\" --fields \"label=section.SECRET1\" --format \"json\" --account \"myaccount\"")
|
||||
.returns(single_item_json)
|
||||
|
||||
json = JSON.parse(run_command("fetch", "--from", "op://myvault/myitem", "section/SECRET1"))
|
||||
json = JSON.parse(shellunescape(run_command("fetch", "--from", "op://myvault/myitem", "section/SECRET1")))
|
||||
|
||||
expected_json = {
|
||||
"myvault/myitem/section/SECRET1"=>"VALUE1"
|
||||
@@ -132,6 +136,8 @@ class SecretsOnePasswordAdapterTest < SecretAdapterTestCase
|
||||
end
|
||||
|
||||
test "fetch with signin and session" do
|
||||
stub_ticks.with("op --version 2> /dev/null")
|
||||
|
||||
stub_ticks_with("op account get --account myaccount 2> /dev/null", succeed: false)
|
||||
stub_ticks_with("op signin --account \"myaccount\" --force --raw", succeed: true).returns("1234567890")
|
||||
|
||||
@@ -139,7 +145,7 @@ class SecretsOnePasswordAdapterTest < SecretAdapterTestCase
|
||||
.with("op item get myitem --vault \"myvault\" --fields \"label=section.SECRET1\" --format \"json\" --account \"myaccount\" --session \"1234567890\"")
|
||||
.returns(single_item_json)
|
||||
|
||||
json = JSON.parse(run_command("fetch", "--from", "op://myvault/myitem", "section/SECRET1"))
|
||||
json = JSON.parse(shellunescape(run_command("fetch", "--from", "op://myvault/myitem", "section/SECRET1")))
|
||||
|
||||
expected_json = {
|
||||
"myvault/myitem/section/SECRET1"=>"VALUE1"
|
||||
@@ -148,6 +154,15 @@ class SecretsOnePasswordAdapterTest < SecretAdapterTestCase
|
||||
assert_equal expected_json, json
|
||||
end
|
||||
|
||||
test "fetch without CLI installed" do
|
||||
stub_ticks_with("op --version 2> /dev/null", succeed: false)
|
||||
|
||||
error = assert_raises RuntimeError do
|
||||
JSON.parse(shellunescape(run_command("fetch", "--from", "op://myvault/myitem", "section/SECRET1", "section/SECRET2", "section2/SECRET3")))
|
||||
end
|
||||
assert_equal "1Password CLI is not installed", error.message
|
||||
end
|
||||
|
||||
private
|
||||
def run_command(*command)
|
||||
stdouted do
|
||||
|
||||
@@ -31,4 +31,18 @@ class SecretsTest < ActiveSupport::TestCase
|
||||
assert_equal "JKL", Kamal::Secrets.new(destination: "nodest")["SECRET2"]
|
||||
end
|
||||
end
|
||||
|
||||
test "no secrets files" do
|
||||
with_test_secrets do
|
||||
error = assert_raises(Kamal::ConfigurationError) do
|
||||
Kamal::Secrets.new["SECRET"]
|
||||
end
|
||||
assert_equal "Secret 'SECRET' not found, no secret files (.kamal/secrets-common, .kamal/secrets) provided", error.message
|
||||
|
||||
error = assert_raises(Kamal::ConfigurationError) do
|
||||
Kamal::Secrets.new(destination: "dest")["SECRET"]
|
||||
end
|
||||
assert_equal "Secret 'SECRET' not found, no secret files (.kamal/secrets-common, .kamal/secrets.dest) provided", error.message
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -86,4 +86,8 @@ class SecretAdapterTestCase < ActiveSupport::TestCase
|
||||
stub_ticks.with { |c| c == command && (succeed ? `true` : `false`) }
|
||||
Kamal::Secrets::Adapters::Base.any_instance.stubs(:`)
|
||||
end
|
||||
|
||||
def shellunescape(string)
|
||||
"\"#{string}\"".undump.gsub(/\\([{}])/, "\\1")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,8 +2,8 @@ require "test_helper"
|
||||
|
||||
class UtilsTest < ActiveSupport::TestCase
|
||||
test "argumentize" do
|
||||
assert_equal [ "--label", "foo=\"\\`bar\\`\"", "--label", "baz=\"qux\"", "--label", :quux ], \
|
||||
Kamal::Utils.argumentize("--label", { foo: "`bar`", baz: "qux", quux: nil })
|
||||
assert_equal [ "--label", "foo=\"\\`bar\\`\"", "--label", "baz=\"qux\"", "--label", :quux, "--label", "quuz=false" ], \
|
||||
Kamal::Utils.argumentize("--label", { foo: "`bar`", baz: "qux", quux: nil, quuz: false })
|
||||
end
|
||||
|
||||
test "argumentize with redacted" do
|
||||
|
||||
Reference in New Issue
Block a user