Compare commits
100 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa2ceaa92a | ||
|
|
c3e7721da5 | ||
|
|
0656e02375 | ||
|
|
aed77a78fb | ||
|
|
9244247389 | ||
|
|
6e517665e8 | ||
|
|
4b0afdf42b | ||
|
|
5aa3f7bd4c | ||
|
|
ccbcbbc8c5 | ||
|
|
8a7260d1e9 | ||
|
|
89c56910c9 | ||
|
|
52e06c1351 | ||
|
|
9bcc953cd6 | ||
|
|
e2015b47f9 | ||
|
|
23f2bf71f9 | ||
|
|
054a85d3c0 | ||
|
|
5a0da160b4 | ||
|
|
72d9fcbaaa | ||
|
|
a201a6ca68 | ||
|
|
1d81d9ec15 | ||
|
|
aa67564dc5 | ||
|
|
fd6ac4f84b | ||
|
|
c8f232b64f | ||
|
|
7f3dd59a73 | ||
|
|
6672e3e77d | ||
|
|
b164d50ff1 | ||
|
|
1d88281fee | ||
|
|
a004232ffc | ||
|
|
487aa306c9 | ||
|
|
cbf94fa7f5 | ||
|
|
344e2d7995 | ||
|
|
b387df0e4f | ||
|
|
9c8a44eec4 | ||
|
|
99f763d742 | ||
|
|
4bd1f0536c | ||
|
|
e217332cde | ||
|
|
30d630ce4d | ||
|
|
22e7243b10 | ||
|
|
259a018d5a | ||
|
|
a82e88d5c9 | ||
|
|
d6459e869a | ||
|
|
ad21c7e984 | ||
|
|
87965281a3 | ||
|
|
dca96eafaa | ||
|
|
7b1439c3c6 | ||
|
|
b9e5ce7ca7 | ||
|
|
f62c1a50c4 | ||
|
|
2c1d6ed891 | ||
|
|
1331e7b9c7 | ||
|
|
c5e5f5d7cc | ||
|
|
6a573c19a6 | ||
|
|
0ab0649d07 | ||
|
|
d62c35e63e | ||
|
|
9a14fbb048 | ||
|
|
092ca425d7 | ||
|
|
68404e2673 | ||
|
|
681439f122 | ||
|
|
a1c6ac41d0 | ||
|
|
9219b87630 | ||
|
|
1f847299c0 | ||
|
|
a525d45b4d | ||
|
|
045410368d | ||
|
|
045da87219 | ||
|
|
fc67cdea33 | ||
|
|
38cfc4488b | ||
|
|
0e453a02de | ||
|
|
aa12dc1d12 | ||
|
|
8acd35c4b7 | ||
|
|
104914bf14 | ||
|
|
913f07bbf2 | ||
|
|
9b63ad5cb8 | ||
|
|
8c17b1ebc6 | ||
|
|
f8f7c6ec57 | ||
|
|
da26457d52 | ||
|
|
95b606a427 | ||
|
|
d249b9a431 | ||
|
|
9f6660dfbf | ||
|
|
9ac3d57b29 | ||
|
|
8354fbee06 | ||
|
|
cde5c7abbf | ||
|
|
1ebc8b8daa | ||
|
|
145b73c4f0 | ||
|
|
d538447973 | ||
|
|
4822a9d950 | ||
|
|
1d55c5941b | ||
|
|
89b44153bb | ||
|
|
5482052e19 | ||
|
|
dda8efe39a | ||
|
|
c60124188f | ||
|
|
f7147e07d4 | ||
|
|
71741742ff | ||
|
|
e252004eef | ||
|
|
85a5a09aac | ||
|
|
548452aa12 | ||
|
|
2c5f2a7ce0 | ||
|
|
ae68193f99 | ||
|
|
24f4308372 | ||
|
|
d0ffb850da | ||
|
|
826308aabd | ||
|
|
897b3b4e46 |
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -26,16 +26,12 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
ruby-version:
|
||||
- "3.1"
|
||||
- "3.2"
|
||||
- "3.3"
|
||||
- "3.4"
|
||||
gemfile:
|
||||
- Gemfile
|
||||
- gemfiles/rails_edge.gemfile
|
||||
exclude:
|
||||
- ruby-version: "3.1"
|
||||
gemfile: gemfiles/rails_edge.gemfile
|
||||
name: ${{ format('Tests (Ruby {0})', matrix.ruby-version) }}
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
|
||||
@@ -13,8 +13,7 @@ COPY Gemfile Gemfile.lock kamal.gemspec ./
|
||||
COPY lib/kamal/version.rb /kamal/lib/kamal/version.rb
|
||||
|
||||
# Install system dependencies
|
||||
RUN apk add --no-cache build-base git docker openrc openssh-client-default yaml-dev \
|
||||
&& rc-update add docker boot \
|
||||
RUN apk add --no-cache build-base git docker-cli openssh-client-default yaml-dev \
|
||||
&& gem install bundler --version=2.6.5 \
|
||||
&& bundle install
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
PATH
|
||||
remote: .
|
||||
specs:
|
||||
kamal (2.6.0)
|
||||
kamal (2.7.0)
|
||||
activesupport (>= 7.0)
|
||||
base64 (~> 0.2)
|
||||
bcrypt_pbkdf (~> 1.0)
|
||||
@@ -101,8 +101,9 @@ GEM
|
||||
date
|
||||
stringio
|
||||
racc (1.8.1)
|
||||
rack (3.1.12)
|
||||
rack-session (2.0.0)
|
||||
rack (3.1.16)
|
||||
rack-session (2.1.1)
|
||||
base64 (>= 0.1.0)
|
||||
rack (>= 3.0.0)
|
||||
rack-test (2.1.0)
|
||||
rack (>= 1.3)
|
||||
|
||||
@@ -24,11 +24,11 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
|
||||
directories(name)
|
||||
upload(name)
|
||||
|
||||
on(hosts) do
|
||||
on(hosts) do |host|
|
||||
execute *KAMAL.auditor.record("Booted #{name} accessory"), verbosity: :debug
|
||||
execute *accessory.ensure_env_directory
|
||||
upload! accessory.secrets_io, accessory.secrets_path, mode: "0600"
|
||||
execute *accessory.run
|
||||
execute *accessory.run(host: host)
|
||||
|
||||
if accessory.running_proxy?
|
||||
target = capture_with_info(*accessory.container_id_for(container_name: accessory.service_name, only_running: true)).strip
|
||||
|
||||
@@ -12,6 +12,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
|
||||
|
||||
KAMAL.roles_on(host).each do |role|
|
||||
Kamal::Cli::App::Assets.new(host, role, self).run
|
||||
Kamal::Cli::App::SslCertificates.new(host, role, self).run
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
28
lib/kamal/cli/app/ssl_certificates.rb
Normal file
28
lib/kamal/cli/app/ssl_certificates.rb
Normal file
@@ -0,0 +1,28 @@
|
||||
class Kamal::Cli::App::SslCertificates
|
||||
attr_reader :host, :role, :sshkit
|
||||
delegate :execute, :info, :upload!, to: :sshkit
|
||||
|
||||
def initialize(host, role, sshkit)
|
||||
@host = host
|
||||
@role = role
|
||||
@sshkit = sshkit
|
||||
end
|
||||
|
||||
def run
|
||||
if role.running_proxy? && role.proxy.custom_ssl_certificate?
|
||||
info "Writing SSL certificates for #{role.name} on #{host}"
|
||||
execute *app.create_ssl_directory
|
||||
if cert_content = role.proxy.certificate_pem_content
|
||||
upload!(StringIO.new(cert_content), role.proxy.host_tls_cert, mode: "0644")
|
||||
end
|
||||
if key_content = role.proxy.private_key_pem_content
|
||||
upload!(StringIO.new(key_content), role.proxy.host_tls_key, mode: "0644")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def app
|
||||
@app ||= KAMAL.app(role: role, host: host)
|
||||
end
|
||||
end
|
||||
@@ -14,6 +14,10 @@ class Kamal::Cli::Build < Kamal::Cli::Base
|
||||
def push
|
||||
cli = self
|
||||
|
||||
# Ensure pre-connect hooks run before the build, they may needed for a remote builder
|
||||
# or the pre-build hooks.
|
||||
pre_connect_if_required
|
||||
|
||||
ensure_docker_installed
|
||||
login_to_registry_locally
|
||||
|
||||
|
||||
@@ -120,18 +120,6 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
|
||||
execute *KAMAL.proxy.ensure_apps_config_directory
|
||||
|
||||
execute *KAMAL.proxy.run
|
||||
|
||||
KAMAL.roles_on(host).select(&:running_proxy?).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_id_for_version(version)).strip
|
||||
|
||||
if endpoint.present?
|
||||
info "Deploying #{endpoint} for role `#{role}` on #{host}..."
|
||||
execute *app.deploy(target: endpoint)
|
||||
end
|
||||
end
|
||||
end
|
||||
run_hook "post-proxy-reboot", hosts: host_list
|
||||
end
|
||||
|
||||
@@ -43,7 +43,7 @@ class GithubStatusChecks
|
||||
attr_reader :remote_url, :git_sha, :github_client, :combined_status
|
||||
|
||||
def initialize
|
||||
@remote_url = `git config --get remote.origin.url`.strip.delete_prefix("https://github.com/")
|
||||
@remote_url = github_repo_from_remote_url
|
||||
@git_sha = `git rev-parse HEAD`.strip
|
||||
@github_client = Octokit::Client.new(access_token: ENV["GITHUB_TOKEN"])
|
||||
refresh!
|
||||
@@ -77,6 +77,18 @@ class GithubStatusChecks
|
||||
"Build not started..."
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def github_repo_from_remote_url
|
||||
url = `git config --get remote.origin.url`.strip.delete_suffix(".git")
|
||||
if url.start_with?("https://github.com/")
|
||||
url.delete_prefix("https://github.com/")
|
||||
elsif url.start_with?("git@github.com:")
|
||||
url.delete_prefix("git@github.com:")
|
||||
else
|
||||
url
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ class Kamal::Commander::Specifics
|
||||
@primary_role = primary_or_first_role(roles_on(primary_host))
|
||||
|
||||
stable_sort!(roles) { |role| role == primary_role ? 0 : 1 }
|
||||
stable_sort!(hosts) { |host| roles_on(host).any? { |role| role == primary_role } ? 0 : 1 }
|
||||
sort_primary_role_hosts_first!(hosts)
|
||||
end
|
||||
|
||||
def roles_on(host)
|
||||
@@ -19,7 +19,7 @@ class Kamal::Commander::Specifics
|
||||
end
|
||||
|
||||
def app_hosts
|
||||
config.app_hosts & specified_hosts
|
||||
@app_hosts ||= sort_primary_role_hosts_first!(config.app_hosts & specified_hosts)
|
||||
end
|
||||
|
||||
def proxy_hosts
|
||||
@@ -55,4 +55,8 @@ class Kamal::Commander::Specifics
|
||||
specified_hosts
|
||||
end
|
||||
end
|
||||
|
||||
def sort_primary_role_hosts_first!(hosts)
|
||||
stable_sort!(hosts) { |host| roles_on(host).any? { |role| role == primary_role } ? 0 : 1 }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -12,7 +12,7 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base
|
||||
@accessory_config = config.accessory(name)
|
||||
end
|
||||
|
||||
def run
|
||||
def run(host: nil)
|
||||
docker :run,
|
||||
"--name", service_name,
|
||||
"--detach",
|
||||
@@ -20,6 +20,7 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base
|
||||
*network_args,
|
||||
*config.logging_args,
|
||||
*publish_args,
|
||||
*([ "--env", "KAMAL_HOST=\"#{host}\"" ] if host),
|
||||
*env_args,
|
||||
*volume_args,
|
||||
*label_args,
|
||||
@@ -55,14 +56,14 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base
|
||||
|
||||
def execute_in_existing_container(*command, interactive: false)
|
||||
docker :exec,
|
||||
("-it" if interactive),
|
||||
(docker_interactive_args if interactive),
|
||||
service_name,
|
||||
*command
|
||||
end
|
||||
|
||||
def execute_in_new_container(*command, interactive: false)
|
||||
docker :run,
|
||||
("-it" if interactive),
|
||||
(docker_interactive_args if interactive),
|
||||
"--rm",
|
||||
*network_args,
|
||||
*env_args,
|
||||
|
||||
@@ -20,8 +20,9 @@ class Kamal::Commands::App < Kamal::Commands::Base
|
||||
"--name", container_name,
|
||||
"--network", "kamal",
|
||||
*([ "--hostname", hostname ] if hostname),
|
||||
"-e", "KAMAL_CONTAINER_NAME=\"#{container_name}\"",
|
||||
"-e", "KAMAL_VERSION=\"#{config.version}\"",
|
||||
"--env", "KAMAL_CONTAINER_NAME=\"#{container_name}\"",
|
||||
"--env", "KAMAL_VERSION=\"#{config.version}\"",
|
||||
"--env", "KAMAL_HOST=\"#{host}\"",
|
||||
*role.env_args(host),
|
||||
*role.logging_args,
|
||||
*config.volume_args,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
module Kamal::Commands::App::Execution
|
||||
def execute_in_existing_container(*command, interactive: false, env:)
|
||||
docker :exec,
|
||||
("-it" if interactive),
|
||||
(docker_interactive_args if interactive),
|
||||
*argumentize("--env", env),
|
||||
container_name,
|
||||
*command
|
||||
@@ -9,7 +9,7 @@ module Kamal::Commands::App::Execution
|
||||
|
||||
def execute_in_new_container(*command, interactive: false, detach: false, env:)
|
||||
docker :run,
|
||||
("-it" if interactive),
|
||||
(docker_interactive_args if interactive),
|
||||
("--detach" if detach),
|
||||
("--rm" unless detach),
|
||||
"--network", "kamal",
|
||||
|
||||
@@ -21,6 +21,10 @@ module Kamal::Commands::App::Proxy
|
||||
remove_directory config.proxy_boot.app_directory
|
||||
end
|
||||
|
||||
def create_ssl_directory
|
||||
make_directory(File.join(config.proxy_boot.tls_directory, role.name))
|
||||
end
|
||||
|
||||
private
|
||||
def proxy_exec(*command)
|
||||
docker :exec, proxy_container_name, "kamal-proxy", *command
|
||||
|
||||
@@ -84,6 +84,10 @@ module Kamal::Commands
|
||||
args.compact.unshift :docker
|
||||
end
|
||||
|
||||
def pack(*args)
|
||||
args.compact.unshift :pack
|
||||
end
|
||||
|
||||
def git(*args, path: nil)
|
||||
[ :git, *([ "-C", path ] if path), *args.compact ]
|
||||
end
|
||||
@@ -122,5 +126,9 @@ module Kamal::Commands
|
||||
def ensure_local_buildx_installed
|
||||
docker :buildx, "version"
|
||||
end
|
||||
|
||||
def docker_interactive_args
|
||||
STDIN.isatty ? "-it" : "-i"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,7 +2,7 @@ require "active_support/core_ext/string/filters"
|
||||
|
||||
class Kamal::Commands::Builder < Kamal::Commands::Base
|
||||
delegate :create, :remove, :dev, :push, :clean, :pull, :info, :inspect_builder, :validate_image, :first_mirror, to: :target
|
||||
delegate :local?, :remote?, :cloud?, to: "config.builder"
|
||||
delegate :local?, :remote?, :pack?, :cloud?, to: "config.builder"
|
||||
|
||||
include Clone
|
||||
|
||||
@@ -17,6 +17,8 @@ class Kamal::Commands::Builder < Kamal::Commands::Base
|
||||
else
|
||||
remote
|
||||
end
|
||||
elsif pack?
|
||||
pack
|
||||
elsif cloud?
|
||||
cloud
|
||||
else
|
||||
@@ -36,6 +38,10 @@ class Kamal::Commands::Builder < Kamal::Commands::Base
|
||||
@hybrid ||= Kamal::Commands::Builder::Hybrid.new(config)
|
||||
end
|
||||
|
||||
def pack
|
||||
@pack ||= Kamal::Commands::Builder::Pack.new(config)
|
||||
end
|
||||
|
||||
def cloud
|
||||
@cloud ||= Kamal::Commands::Builder::Cloud.new(config)
|
||||
end
|
||||
|
||||
@@ -6,6 +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,
|
||||
:pack?, :pack_builder, :pack_buildpacks,
|
||||
:cache_from, :cache_to, :ssh, :provenance, :sbom, :driver, :docker_driver?,
|
||||
to: :builder_config
|
||||
|
||||
|
||||
46
lib/kamal/commands/builder/pack.rb
Normal file
46
lib/kamal/commands/builder/pack.rb
Normal file
@@ -0,0 +1,46 @@
|
||||
class Kamal::Commands::Builder::Pack < Kamal::Commands::Builder::Base
|
||||
def push(export_action = "registry")
|
||||
combine \
|
||||
build,
|
||||
export(export_action)
|
||||
end
|
||||
|
||||
def remove;end
|
||||
|
||||
def info
|
||||
pack :builder, :inspect, pack_builder
|
||||
end
|
||||
alias_method :inspect_builder, :info
|
||||
|
||||
private
|
||||
def build
|
||||
pack(:build,
|
||||
config.repository,
|
||||
"--platform", platform,
|
||||
"--creation-time", "now",
|
||||
"--builder", pack_builder,
|
||||
buildpacks,
|
||||
"-t", config.absolute_image,
|
||||
"-t", config.latest_image,
|
||||
"--env", "BP_IMAGE_LABELS=service=#{config.service}",
|
||||
*argumentize("--env", args),
|
||||
*argumentize("--env", secrets, sensitive: true),
|
||||
"--path", build_context)
|
||||
end
|
||||
|
||||
def export(export_action)
|
||||
return unless export_action == "registry"
|
||||
|
||||
combine \
|
||||
docker(:push, config.absolute_image),
|
||||
docker(:push, config.latest_image)
|
||||
end
|
||||
|
||||
def platform
|
||||
"linux/#{local_arches.first}"
|
||||
end
|
||||
|
||||
def buildpacks
|
||||
(pack_buildpacks << "paketo-buildpacks/image-labels").map { |buildpack| [ "--buildpack", buildpack ] }
|
||||
end
|
||||
end
|
||||
@@ -63,7 +63,7 @@ class Kamal::Configuration
|
||||
@env = Env.new(config: @raw_config.env || {}, secrets: secrets)
|
||||
|
||||
@logging = Logging.new(logging_config: @raw_config.logging)
|
||||
@proxy = Proxy.new(config: self, proxy_config: @raw_config.key?(:proxy) ? @raw_config.proxy : {})
|
||||
@proxy = Proxy.new(config: self, proxy_config: @raw_config.proxy, secrets: secrets)
|
||||
@proxy_boot = Proxy::Boot.new(config: self)
|
||||
@ssh = Ssh.new(config: self)
|
||||
@sshkit = Sshkit.new(config: self)
|
||||
|
||||
@@ -125,7 +125,8 @@ class Kamal::Configuration::Accessory
|
||||
Kamal::Configuration::Proxy.new \
|
||||
config: config,
|
||||
proxy_config: accessory_config["proxy"],
|
||||
context: "accessories/#{name}/proxy"
|
||||
context: "accessories/#{name}/proxy",
|
||||
secrets: config.secrets
|
||||
end
|
||||
|
||||
def initialize_registry
|
||||
|
||||
@@ -61,6 +61,10 @@ class Kamal::Configuration::Builder
|
||||
!!builder_config["cache"]
|
||||
end
|
||||
|
||||
def pack?
|
||||
!!builder_config["pack"]
|
||||
end
|
||||
|
||||
def args
|
||||
builder_config["args"] || {}
|
||||
end
|
||||
@@ -85,6 +89,14 @@ class Kamal::Configuration::Builder
|
||||
builder_config.fetch("driver", "docker-container")
|
||||
end
|
||||
|
||||
def pack_builder
|
||||
builder_config["pack"]["builder"] if pack?
|
||||
end
|
||||
|
||||
def pack_buildpacks
|
||||
builder_config["pack"]["buildpacks"] if pack?
|
||||
end
|
||||
|
||||
def local_disabled?
|
||||
builder_config["local"] == false
|
||||
end
|
||||
|
||||
@@ -31,6 +31,19 @@ builder:
|
||||
# Defaults to true:
|
||||
local: true
|
||||
|
||||
# Buildpack configuration
|
||||
#
|
||||
# The build configuration for using pack to build a Cloud Native Buildpack image.
|
||||
#
|
||||
# For additional buildpack customization options you can create a project descriptor
|
||||
# file(project.toml) that the Pack CLI will automatically use.
|
||||
# See https://buildpacks.io/docs/for-app-developers/how-to/build-inputs/use-project-toml/ for more information.
|
||||
pack:
|
||||
builder: heroku/builder:24
|
||||
buildpacks:
|
||||
- heroku/ruby
|
||||
- heroku/procfile
|
||||
|
||||
# Builder cache
|
||||
#
|
||||
# The type must be either 'gha' or 'registry'.
|
||||
|
||||
@@ -10,11 +10,6 @@
|
||||
# They are application-specific, so they are not shared when multiple applications
|
||||
# run on the same proxy.
|
||||
#
|
||||
# The proxy is enabled by default on the primary role but can be disabled by
|
||||
# setting `proxy: false`.
|
||||
#
|
||||
# It is disabled by default on all other roles but can be enabled by setting
|
||||
# `proxy: true` or providing a proxy configuration.
|
||||
proxy:
|
||||
|
||||
# Hosts
|
||||
@@ -50,7 +45,27 @@ proxy:
|
||||
# unless you explicitly set `forward_headers: true`
|
||||
#
|
||||
# Defaults to `false`:
|
||||
ssl: true
|
||||
ssl: ...
|
||||
|
||||
# Custom SSL certificate
|
||||
#
|
||||
# In some cases, using Let's Encrypt for automatic certificate management is not an
|
||||
# option, for example if you are running from host than one host. Or you may already
|
||||
# have SSL certificates issued by a different Certificate Authority (CA).
|
||||
# Kamal supports loading custom SSL certificates
|
||||
# directly from secrets.
|
||||
#
|
||||
# Examples:
|
||||
# ssl: true # Enable SSL with Let's Encrypt
|
||||
# ssl: false # Disable SSL
|
||||
# ssl: # Enable custom SSL
|
||||
# certificate_pem: CERTIFICATE_PEM
|
||||
# private_key_pem: PRIVATE_KEY_PEM
|
||||
#
|
||||
# ### Notes
|
||||
# - If the certificate or key is missing or invalid, kamal-proxy will fail to start.
|
||||
# - Always handle SSL certificates and private keys securely. Avoid hard-coding them in deploy.yml files or source control.
|
||||
# - For automated certificate management, consider using the built-in Let's Encrypt integration instead.
|
||||
|
||||
# SSL redirect
|
||||
#
|
||||
@@ -74,6 +89,17 @@ proxy:
|
||||
# How long to wait for requests to complete before timing out, defaults to 30 seconds:
|
||||
response_timeout: 10
|
||||
|
||||
# Path-based routing
|
||||
#
|
||||
# For applications that split their traffic to different services based on the request path,
|
||||
# you can use path-based routing to mount services under different path prefixes.
|
||||
path_prefix: '/api'
|
||||
# By default, the path prefix will be stripped from the request before it is forwarded upstream.
|
||||
# So in the example above, a request to /api/users/123 will be forwarded to web-1 as /users/123.
|
||||
# To instead forward the request with the original path (including the prefix),
|
||||
# specify --strip-path-prefix=false
|
||||
strip_path_prefix: false
|
||||
|
||||
# Healthcheck
|
||||
#
|
||||
# When deploying, the proxy will by default hit `/up` once every second until we hit
|
||||
@@ -113,3 +139,30 @@ proxy:
|
||||
response_headers:
|
||||
- X-Request-ID
|
||||
- X-Request-Start
|
||||
|
||||
# Enabling/disabling the proxy on roles
|
||||
#
|
||||
# The proxy is enabled by default on the primary role but can be disabled by
|
||||
# setting `proxy: false` in the primary role's configuration.
|
||||
#
|
||||
# ```yaml
|
||||
# servers:
|
||||
# web:
|
||||
# hosts:
|
||||
# - ...
|
||||
# proxy: false
|
||||
# ```
|
||||
#
|
||||
# It is disabled by default on all other roles but can be enabled by setting
|
||||
# `proxy: true` or providing a proxy configuration for that role.
|
||||
#
|
||||
# ```yaml
|
||||
# servers:
|
||||
# web:
|
||||
# hosts:
|
||||
# - ...
|
||||
# web2:
|
||||
# hosts:
|
||||
# - ...
|
||||
# proxy: true
|
||||
# ```
|
||||
|
||||
@@ -6,11 +6,14 @@ class Kamal::Configuration::Proxy
|
||||
|
||||
delegate :argumentize, :optionize, to: Kamal::Utils
|
||||
|
||||
attr_reader :config, :proxy_config
|
||||
attr_reader :config, :proxy_config, :role_name, :secrets
|
||||
|
||||
def initialize(config:, proxy_config:, context: "proxy")
|
||||
def initialize(config:, proxy_config:, role_name: nil, secrets:, context: "proxy")
|
||||
@config = config
|
||||
@proxy_config = proxy_config
|
||||
@proxy_config = {} if @proxy_config.nil?
|
||||
@role_name = role_name
|
||||
@secrets = secrets
|
||||
validate! @proxy_config, with: Kamal::Configuration::Validator::Proxy, context: context
|
||||
end
|
||||
|
||||
@@ -26,10 +29,46 @@ class Kamal::Configuration::Proxy
|
||||
proxy_config["hosts"] || proxy_config["host"]&.split(",") || []
|
||||
end
|
||||
|
||||
def custom_ssl_certificate?
|
||||
ssl = proxy_config["ssl"]
|
||||
return false unless ssl.is_a?(Hash)
|
||||
ssl["certificate_pem"].present? && ssl["private_key_pem"].present?
|
||||
end
|
||||
|
||||
def certificate_pem_content
|
||||
ssl = proxy_config["ssl"]
|
||||
return nil unless ssl.is_a?(Hash)
|
||||
secrets[ssl["certificate_pem"]]
|
||||
end
|
||||
|
||||
def private_key_pem_content
|
||||
ssl = proxy_config["ssl"]
|
||||
return nil unless ssl.is_a?(Hash)
|
||||
secrets[ssl["private_key_pem"]]
|
||||
end
|
||||
|
||||
def host_tls_cert
|
||||
tls_path(config.proxy_boot.tls_directory, "cert.pem")
|
||||
end
|
||||
|
||||
def host_tls_key
|
||||
tls_path(config.proxy_boot.tls_directory, "key.pem")
|
||||
end
|
||||
|
||||
def container_tls_cert
|
||||
tls_path(config.proxy_boot.tls_container_directory, "cert.pem")
|
||||
end
|
||||
|
||||
def container_tls_key
|
||||
tls_path(config.proxy_boot.tls_container_directory, "key.pem") if custom_ssl_certificate?
|
||||
end
|
||||
|
||||
def deploy_options
|
||||
{
|
||||
host: hosts,
|
||||
tls: proxy_config["ssl"].presence,
|
||||
tls: ssl? ? true : nil,
|
||||
"tls-certificate-path": container_tls_cert,
|
||||
"tls-private-key-path": container_tls_key,
|
||||
"deploy-timeout": seconds_duration(config.deploy_timeout),
|
||||
"drain-timeout": seconds_duration(config.drain_timeout),
|
||||
"health-check-interval": seconds_duration(proxy_config.dig("healthcheck", "interval")),
|
||||
@@ -41,6 +80,8 @@ class Kamal::Configuration::Proxy
|
||||
"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"),
|
||||
"path-prefix": proxy_config.dig("path_prefix"),
|
||||
"strip-path-prefix": proxy_config.dig("strip_path_prefix"),
|
||||
"forward-headers": proxy_config.dig("forward_headers"),
|
||||
"tls-redirect": proxy_config.dig("ssl_redirect"),
|
||||
"log-request-header": proxy_config.dig("logging", "request_headers") || DEFAULT_LOG_REQUEST_HEADERS,
|
||||
@@ -65,10 +106,14 @@ class Kamal::Configuration::Proxy
|
||||
end
|
||||
|
||||
def merge(other)
|
||||
self.class.new config: config, proxy_config: proxy_config.deep_merge(other.proxy_config)
|
||||
self.class.new config: config, proxy_config: other.proxy_config.deep_merge(proxy_config), role_name: role_name, secrets: secrets
|
||||
end
|
||||
|
||||
private
|
||||
def tls_path(directory, filename)
|
||||
File.join([ directory, role_name, filename ].compact) if custom_ssl_certificate?
|
||||
end
|
||||
|
||||
def seconds_duration(value)
|
||||
value ? "#{value}s" : nil
|
||||
end
|
||||
|
||||
@@ -100,6 +100,14 @@ class Kamal::Configuration::Proxy::Boot
|
||||
File.join app_container_directory, "error_pages"
|
||||
end
|
||||
|
||||
def tls_directory
|
||||
File.join app_directory, "tls"
|
||||
end
|
||||
|
||||
def tls_container_directory
|
||||
File.join app_container_directory, "tls"
|
||||
end
|
||||
|
||||
private
|
||||
def ensure_valid_bind_ips(bind_ips)
|
||||
bind_ips.present? && bind_ips.each do |ip|
|
||||
|
||||
@@ -68,7 +68,7 @@ class Kamal::Configuration::Role
|
||||
end
|
||||
|
||||
def proxy
|
||||
@proxy ||= config.proxy.merge(specialized_proxy) if running_proxy?
|
||||
@proxy ||= specialized_proxy.merge(config.proxy) if running_proxy?
|
||||
end
|
||||
|
||||
def running_proxy?
|
||||
@@ -150,8 +150,8 @@ class Kamal::Configuration::Role
|
||||
end
|
||||
|
||||
def ensure_one_host_for_ssl
|
||||
if running_proxy? && proxy.ssl? && hosts.size > 1
|
||||
raise Kamal::ConfigurationError, "SSL is only supported on a single server, found #{hosts.size} servers for role #{name}"
|
||||
if running_proxy? && proxy.ssl? && hosts.size > 1 && !proxy.custom_ssl_certificate?
|
||||
raise Kamal::ConfigurationError, "SSL is only supported on a single server unless you provide custom certificates, found #{hosts.size} servers for role #{name}"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -173,6 +173,8 @@ class Kamal::Configuration::Role
|
||||
@specialized_proxy = Kamal::Configuration::Proxy.new \
|
||||
config: config,
|
||||
proxy_config: proxy_config,
|
||||
secrets: config.secrets,
|
||||
role_name: name,
|
||||
context: "servers/#{name}/proxy"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -24,7 +24,9 @@ class Kamal::Configuration::Validator
|
||||
example_value = example[key]
|
||||
|
||||
if example_value == "..."
|
||||
unless key.to_s == "proxy" && boolean?(value.class)
|
||||
if key.to_s == "ssl"
|
||||
validate_type! value, TrueClass, FalseClass, Hash
|
||||
elsif key.to_s != "proxy" || !boolean?(value.class)
|
||||
validate_type! value, *(Array if key == :servers), Hash
|
||||
end
|
||||
elsif key == "hosts"
|
||||
@@ -169,6 +171,18 @@ class Kamal::Configuration::Validator
|
||||
unknown_keys_error unknown_keys if unknown_keys.present?
|
||||
end
|
||||
|
||||
def validate_labels!(labels)
|
||||
return true if labels.blank?
|
||||
|
||||
with_context("labels") do
|
||||
labels.each do |key, _|
|
||||
with_context(key) do
|
||||
error "invalid label. destination, role, and service are reserved labels" if %w[destination role service].include?(key)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def validate_docker_options!(options)
|
||||
if options
|
||||
error "Cannot set restart policy in docker options, unless-stopped is required" if options["restart"]
|
||||
|
||||
@@ -6,6 +6,8 @@ class Kamal::Configuration::Validator::Accessory < Kamal::Configuration::Validat
|
||||
error "specify one of `host`, `hosts`, `role`, `roles`, `tag` or `tags`"
|
||||
end
|
||||
|
||||
validate_labels!(config["labels"])
|
||||
|
||||
validate_docker_options!(config["options"])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -8,6 +8,8 @@ class Kamal::Configuration::Validator::Builder < Kamal::Configuration::Validator
|
||||
|
||||
error "Builder arch not set" unless config["arch"].present?
|
||||
|
||||
error "buildpacks only support building for one arch" if config["pack"] && config["arch"].is_a?(Array) && config["arch"].size > 1
|
||||
|
||||
error "Cannot disable local builds, no remote is set" if config["local"] == false && config["remote"].blank?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -10,6 +10,16 @@ class Kamal::Configuration::Validator::Proxy < Kamal::Configuration::Validator
|
||||
if (config.keys & [ "host", "hosts" ]).size > 1
|
||||
error "Specify one of 'host' or 'hosts', not both"
|
||||
end
|
||||
|
||||
if config["ssl"].is_a?(Hash)
|
||||
if config["ssl"]["certificate_pem"].present? && config["ssl"]["private_key_pem"].blank?
|
||||
error "Missing private_key_pem setting (required when certificate_pem is present)"
|
||||
end
|
||||
|
||||
if config["ssl"]["private_key_pem"].present? && config["ssl"]["certificate_pem"].blank?
|
||||
error "Missing certificate_pem setting (required when private_key_pem is present)"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,6 +6,7 @@ class Kamal::Configuration::Validator::Role < Kamal::Configuration::Validator
|
||||
validate_servers!(config)
|
||||
else
|
||||
super
|
||||
validate_labels!(config["labels"])
|
||||
validate_docker_options!(config["options"])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,8 +6,8 @@ class Kamal::Secrets::Adapters::BitwardenSecretsManager < Kamal::Secrets::Adapte
|
||||
private
|
||||
LIST_ALL_SELECTOR = "all"
|
||||
LIST_ALL_FROM_PROJECT_SUFFIX = "/all"
|
||||
LIST_COMMAND = "secret list -o env"
|
||||
GET_COMMAND = "secret get -o env"
|
||||
LIST_COMMAND = "secret list"
|
||||
GET_COMMAND = "secret get"
|
||||
|
||||
def fetch_secrets(secrets, from:, account:, session:)
|
||||
raise RuntimeError, "You must specify what to retrieve from Bitwarden Secrets Manager" if secrets.length == 0
|
||||
@@ -18,17 +18,17 @@ class Kamal::Secrets::Adapters::BitwardenSecretsManager < Kamal::Secrets::Adapte
|
||||
{}.tap do |results|
|
||||
if command.nil?
|
||||
secrets.each do |secret_uuid|
|
||||
secret = run_command("#{GET_COMMAND} #{secret_uuid.shellescape}")
|
||||
item_json = run_command("#{GET_COMMAND} #{secret_uuid.shellescape}")
|
||||
raise RuntimeError, "Could not read #{secret_uuid} from Bitwarden Secrets Manager" unless $?.success?
|
||||
key, value = parse_secret(secret)
|
||||
results[key] = value
|
||||
item_json = JSON.parse(item_json)
|
||||
results[item_json["key"]] = item_json["value"]
|
||||
end
|
||||
else
|
||||
secrets = run_command(command)
|
||||
items_json = run_command(command)
|
||||
raise RuntimeError, "Could not read secrets from Bitwarden Secrets Manager" unless $?.success?
|
||||
secrets.split("\n").each do |secret|
|
||||
key, value = parse_secret(secret)
|
||||
results[key] = value
|
||||
|
||||
JSON.parse(items_json).each do |item_json|
|
||||
results[item_json["key"]] = item_json["value"]
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -45,19 +45,13 @@ class Kamal::Secrets::Adapters::BitwardenSecretsManager < Kamal::Secrets::Adapte
|
||||
end
|
||||
end
|
||||
|
||||
def parse_secret(secret)
|
||||
key, value = secret.split("=", 2)
|
||||
value = value.gsub(/^"|"$/, "")
|
||||
[ key, value ]
|
||||
end
|
||||
|
||||
def run_command(command, session: nil)
|
||||
full_command = [ "bws", command ].join(" ")
|
||||
`#{full_command}`
|
||||
end
|
||||
|
||||
def login(account)
|
||||
run_command("run 'echo OK'")
|
||||
run_command("project list")
|
||||
raise RuntimeError, "Could not authenticate to Bitwarden Secrets Manager. Did you set a valid access token?" unless $?.success?
|
||||
end
|
||||
|
||||
|
||||
@@ -16,17 +16,33 @@ class Kamal::Secrets::Adapters::OnePassword < Kamal::Secrets::Adapters::Base
|
||||
end
|
||||
|
||||
def fetch_secrets(secrets, from:, account:, session:)
|
||||
if secrets.blank?
|
||||
fetch_all_secrets(from: from, account: account, session: session) if secrets.blank?
|
||||
else
|
||||
fetch_specified_secrets(secrets, from: from, account: account, session: session)
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_specified_secrets(secrets, from:, account:, session:)
|
||||
{}.tap do |results|
|
||||
vaults_items_fields(prefixed_secrets(secrets, from: from)).map do |vault, items|
|
||||
items.each do |item, fields|
|
||||
fields_json = JSON.parse(op_item_get(vault, item, fields, account: account, session: session))
|
||||
fields_json = JSON.parse(op_item_get(vault, item, fields: fields, account: account, session: session))
|
||||
fields_json = [ fields_json ] if fields.one?
|
||||
|
||||
fields_json.each do |field_json|
|
||||
# The reference is in the form `op://vault/item/field[/field]`
|
||||
field = field_json["reference"].delete_prefix("op://").delete_suffix("/password")
|
||||
results[field] = field_json["value"]
|
||||
end
|
||||
results.merge!(fields_map(fields_json))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_all_secrets(from:, account:, session:)
|
||||
{}.tap do |results|
|
||||
vault_items(from).each do |vault, items|
|
||||
items.each do |item|
|
||||
fields_json = JSON.parse(op_item_get(vault, item, account: account, session: session)).fetch("fields")
|
||||
|
||||
results.merge!(fields_map(fields_json))
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -50,12 +66,30 @@ class Kamal::Secrets::Adapters::OnePassword < Kamal::Secrets::Adapters::Base
|
||||
end
|
||||
end
|
||||
|
||||
def op_item_get(vault, item, fields, account:, session:)
|
||||
labels = fields.map { |field| "label=#{field}" }.join(",")
|
||||
options = to_options(vault: vault, fields: labels, format: "json", account: account, session: session.presence)
|
||||
def vault_items(from)
|
||||
from = from.delete_prefix("op://")
|
||||
vault, item = from.split("/")
|
||||
{ vault => [ item ] }
|
||||
end
|
||||
|
||||
`op item get #{item.shellescape} #{options}`.tap do
|
||||
raise RuntimeError, "Could not read #{fields.join(", ")} from #{item} in the #{vault} 1Password vault" unless $?.success?
|
||||
def fields_map(fields_json)
|
||||
fields_json.to_h do |field_json|
|
||||
# The reference is in the form `op://vault/item/field[/field]`
|
||||
field = field_json["reference"].delete_prefix("op://").delete_suffix("/password")
|
||||
[ field, field_json["value"] ]
|
||||
end
|
||||
end
|
||||
|
||||
def op_item_get(vault, item, fields: nil, account:, session:)
|
||||
options = { vault: vault, format: "json", account: account, session: session.presence }
|
||||
|
||||
if fields.present?
|
||||
labels = fields.map { |field| "label=#{field}" }.join(",")
|
||||
options.merge!(fields: labels)
|
||||
end
|
||||
|
||||
`op item get #{item.shellescape} #{to_options(**options)}`.tap do
|
||||
raise RuntimeError, "Could not read #{"#{fields.join(", ")} " if fields.present?}from #{item} in the #{vault} 1Password vault" unless $?.success?
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
130
lib/kamal/secrets/adapters/passbolt.rb
Normal file
130
lib/kamal/secrets/adapters/passbolt.rb
Normal file
@@ -0,0 +1,130 @@
|
||||
class Kamal::Secrets::Adapters::Passbolt < Kamal::Secrets::Adapters::Base
|
||||
def requires_account?
|
||||
false
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def login(*)
|
||||
`passbolt verify`
|
||||
raise RuntimeError, "Failed to login to Passbolt" unless $?.success?
|
||||
end
|
||||
|
||||
def fetch_secrets(secrets, from:, **)
|
||||
secrets = prefixed_secrets(secrets, from: from)
|
||||
raise ArgumentError, "No secrets given to fetch" if secrets.empty?
|
||||
|
||||
secret_names = secrets.collect { |s| s.split("/").last }
|
||||
folders = secrets_get_folders(secrets)
|
||||
|
||||
# build filter conditions for each secret with its corresponding folder
|
||||
filter_conditions = []
|
||||
secrets.each do |secret|
|
||||
parts = secret.split("/")
|
||||
secret_name = parts.last
|
||||
|
||||
if parts.size > 1
|
||||
# get the folder path without the secret name
|
||||
folder_path = parts[0..-2]
|
||||
|
||||
# find the most nested folder for this path
|
||||
current_folder = nil
|
||||
current_path = []
|
||||
|
||||
folder_path.each do |folder_name|
|
||||
current_path << folder_name
|
||||
matching_folders = folders.select { |f| get_folder_path(f, folders) == current_path.join("/") }
|
||||
current_folder = matching_folders.first if matching_folders.any?
|
||||
end
|
||||
|
||||
if current_folder
|
||||
filter_conditions << "(Name == #{secret_name.shellescape.inspect} && FolderParentID == #{current_folder["id"].shellescape.inspect})"
|
||||
end
|
||||
else
|
||||
# for root level secrets (no folders)
|
||||
filter_conditions << "Name == #{secret_name.shellescape.inspect}"
|
||||
end
|
||||
end
|
||||
|
||||
filter_condition = filter_conditions.any? ? "--filter '#{filter_conditions.join(" || ")}'" : ""
|
||||
items = `passbolt list resources #{filter_condition} #{folders.map { |item| "--folder #{item["id"]}" }.join(" ")} --json`
|
||||
raise RuntimeError, "Could not read #{secrets} from Passbolt" unless $?.success?
|
||||
|
||||
items = JSON.parse(items)
|
||||
found_names = items.map { |item| item["name"] }
|
||||
missing_secrets = secret_names - found_names
|
||||
raise RuntimeError, "Could not find the following secrets in Passbolt: #{missing_secrets.join(", ")}" if missing_secrets.any?
|
||||
|
||||
items.to_h { |item| [ item["name"], item["password"] ] }
|
||||
end
|
||||
|
||||
def secrets_get_folders(secrets)
|
||||
# extract all folder paths (both parent and nested)
|
||||
folder_paths = secrets
|
||||
.select { |s| s.include?("/") }
|
||||
.map { |s| s.split("/")[0..-2] } # get all parts except the secret name
|
||||
.uniq
|
||||
|
||||
return [] if folder_paths.empty?
|
||||
|
||||
all_folders = []
|
||||
|
||||
# first get all top-level folders
|
||||
parent_folders = folder_paths.map(&:first).uniq
|
||||
filter_condition = "--filter '#{parent_folders.map { |name| "Name == #{name.shellescape.inspect}" }.join(" || ")}'"
|
||||
fetch_folders = `passbolt list folders #{filter_condition} --json`
|
||||
raise RuntimeError, "Could not read folders from Passbolt" unless $?.success?
|
||||
|
||||
parent_folder_items = JSON.parse(fetch_folders)
|
||||
all_folders.concat(parent_folder_items)
|
||||
|
||||
# get nested folders for each parent
|
||||
folder_paths.each do |path|
|
||||
next if path.size <= 1 # skip non-nested folders
|
||||
|
||||
parent = path[0]
|
||||
parent_folder = parent_folder_items.find { |f| f["name"] == parent }
|
||||
next unless parent_folder
|
||||
|
||||
# for each nested level, get the folders using the parent's ID
|
||||
current_parent = parent_folder
|
||||
path[1..-1].each do |folder_name|
|
||||
filter_condition = "--filter 'Name == #{folder_name.shellescape.inspect} && FolderParentID == #{current_parent["id"].shellescape.inspect}'"
|
||||
fetch_nested = `passbolt list folders #{filter_condition} --json`
|
||||
next unless $?.success?
|
||||
|
||||
nested_folders = JSON.parse(fetch_nested)
|
||||
break if nested_folders.empty?
|
||||
|
||||
all_folders.concat(nested_folders)
|
||||
current_parent = nested_folders.first
|
||||
end
|
||||
end
|
||||
|
||||
# check if we found all required folders
|
||||
found_paths = all_folders.map { |f| get_folder_path(f, all_folders) }
|
||||
missing_paths = folder_paths.map { |path| path.join("/") } - found_paths
|
||||
raise RuntimeError, "Could not find the following folders in Passbolt: #{missing_paths.join(", ")}" if missing_paths.any?
|
||||
|
||||
all_folders
|
||||
end
|
||||
|
||||
def get_folder_path(folder, all_folders, path = [])
|
||||
path.unshift(folder["name"])
|
||||
return path.join("/") if folder["folder_parent_id"].to_s.empty?
|
||||
|
||||
parent = all_folders.find { |f| f["id"] == folder["folder_parent_id"] }
|
||||
return path.join("/") unless parent
|
||||
|
||||
get_folder_path(parent, all_folders, path)
|
||||
end
|
||||
|
||||
def check_dependencies!
|
||||
raise RuntimeError, "Passbolt CLI is not installed" unless cli_installed?
|
||||
end
|
||||
|
||||
def cli_installed?
|
||||
`passbolt --version 2> /dev/null`
|
||||
$?.success?
|
||||
end
|
||||
end
|
||||
@@ -1,3 +1,3 @@
|
||||
module Kamal
|
||||
VERSION = "2.6.0"
|
||||
VERSION = "2.7.0"
|
||||
end
|
||||
|
||||
@@ -15,7 +15,7 @@ class CliAccessoryTest < CliTestCase
|
||||
|
||||
run_command("boot", "mysql").tap do |output|
|
||||
assert_match "docker login private.registry -u [REDACTED] -p [REDACTED] on 1.1.1.3", output
|
||||
assert_match "docker run --name app-mysql --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 3306:3306 --env MYSQL_ROOT_HOST=\"%\" --env-file .kamal/apps/app/env/accessories/mysql.env --volume $PWD/app-mysql/etc/mysql/my.cnf:/etc/mysql/my.cnf --volume $PWD/app-mysql/data:/var/lib/mysql --label service=\"app-mysql\" private.registry/mysql:5.7 on 1.1.1.3", output
|
||||
assert_match "docker run --name app-mysql --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 3306:3306 --env KAMAL_HOST=\"1.1.1.3\" --env MYSQL_ROOT_HOST=\"%\" --env-file .kamal/apps/app/env/accessories/mysql.env --volume $PWD/app-mysql/etc/mysql/my.cnf:/etc/mysql/my.cnf --volume $PWD/app-mysql/data:/var/lib/mysql --label service=\"app-mysql\" private.registry/mysql:5.7 on 1.1.1.3", output
|
||||
end
|
||||
end
|
||||
|
||||
@@ -35,10 +35,10 @@ class CliAccessoryTest < CliTestCase
|
||||
assert_match /docker network create kamal.*on 1.1.1.1/, output
|
||||
assert_match /docker network create kamal.*on 1.1.1.2/, output
|
||||
assert_match /docker network create kamal.*on 1.1.1.3/, output
|
||||
assert_match "docker run --name app-mysql --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 3306:3306 --env MYSQL_ROOT_HOST=\"%\" --env-file .kamal/apps/app/env/accessories/mysql.env --volume $PWD/app-mysql/etc/mysql/my.cnf:/etc/mysql/my.cnf --volume $PWD/app-mysql/data:/var/lib/mysql --label service=\"app-mysql\" private.registry/mysql:5.7 on 1.1.1.3", output
|
||||
assert_match "docker run --name app-redis --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/apps/app/env/accessories/redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.1", output
|
||||
assert_match "docker run --name app-redis --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/apps/app/env/accessories/redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.2", output
|
||||
assert_match "docker run --name custom-box --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --env-file .kamal/apps/app/env/accessories/busybox.env --label service=\"custom-box\" other.registry/busybox:latest on 1.1.1.3", output
|
||||
assert_match "docker run --name app-mysql --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 3306:3306 --env KAMAL_HOST=\"1.1.1.3\" --env MYSQL_ROOT_HOST=\"%\" --env-file .kamal/apps/app/env/accessories/mysql.env --volume $PWD/app-mysql/etc/mysql/my.cnf:/etc/mysql/my.cnf --volume $PWD/app-mysql/data:/var/lib/mysql --label service=\"app-mysql\" private.registry/mysql:5.7 on 1.1.1.3", output
|
||||
assert_match "docker run --name app-redis --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 6379:6379 --env KAMAL_HOST=\"1.1.1.1\" --env-file .kamal/apps/app/env/accessories/redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.1", output
|
||||
assert_match "docker run --name app-redis --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 6379:6379 --env KAMAL_HOST=\"1.1.1.2\" --env-file .kamal/apps/app/env/accessories/redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.2", output
|
||||
assert_match "docker run --name custom-box --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --env KAMAL_HOST=\"1.1.1.3\" --env-file .kamal/apps/app/env/accessories/busybox.env --label service=\"custom-box\" other.registry/busybox:latest on 1.1.1.3", output
|
||||
end
|
||||
end
|
||||
|
||||
@@ -215,8 +215,8 @@ class CliAccessoryTest < CliTestCase
|
||||
run_command("boot", "redis", "--hosts", "1.1.1.1").tap do |output|
|
||||
assert_match "docker login private.registry -u [REDACTED] -p [REDACTED] on 1.1.1.1", output
|
||||
assert_no_match "docker login private.registry -u [REDACTED] -p [REDACTED] on 1.1.1.2", output
|
||||
assert_match "docker run --name app-redis --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/apps/app/env/accessories/redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.1", output
|
||||
assert_no_match "docker run --name app-redis --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/apps/app/env/accessories/redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.2", output
|
||||
assert_match "docker run --name app-redis --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 6379:6379 --env KAMAL_HOST=\"1.1.1.1\" --env-file .kamal/apps/app/env/accessories/redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.1", output
|
||||
assert_no_match /docker run --name app-redis .* on 1.1.1.2/, output
|
||||
end
|
||||
end
|
||||
|
||||
@@ -227,8 +227,8 @@ class CliAccessoryTest < CliTestCase
|
||||
run_command("boot", "redis", "--hosts", "1.1.1.1,1.1.1.3").tap do |output|
|
||||
assert_match "docker login private.registry -u [REDACTED] -p [REDACTED] on 1.1.1.1", output
|
||||
assert_no_match "docker login private.registry -u [REDACTED] -p [REDACTED] on 1.1.1.3", output
|
||||
assert_match "docker run --name app-redis --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/apps/app/env/accessories/redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.1", output
|
||||
assert_no_match "docker run --name app-redis --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/apps/app/env/accessories/redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.3", output
|
||||
assert_match "docker run --name app-redis --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 6379:6379 --env KAMAL_HOST=\"1.1.1.1\" --env-file .kamal/apps/app/env/accessories/redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.1", output
|
||||
assert_no_match /docker run --name app-redis .* on 1.1.1.3/, output
|
||||
end
|
||||
end
|
||||
|
||||
@@ -237,7 +237,7 @@ class CliAccessoryTest < CliTestCase
|
||||
assert_match "Upgrading all accessories on 1.1.1.3,1.1.1.1,1.1.1.2...", output
|
||||
assert_match "docker network create kamal on 1.1.1.3", output
|
||||
assert_match "docker container stop app-mysql on 1.1.1.3", output
|
||||
assert_match "docker run --name app-mysql --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 3306:3306 --env MYSQL_ROOT_HOST="%" --env-file .kamal/apps/app/env/accessories/mysql.env --volume $PWD/app-mysql/etc/mysql/my.cnf:/etc/mysql/my.cnf --volume $PWD/app-mysql/data:/var/lib/mysql --label service=\"app-mysql\" private.registry/mysql:5.7 on 1.1.1.3", output
|
||||
assert_match "docker run --name app-mysql --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 3306:3306 --env KAMAL_HOST=\"1.1.1.3\" --env MYSQL_ROOT_HOST="%" --env-file .kamal/apps/app/env/accessories/mysql.env --volume $PWD/app-mysql/etc/mysql/my.cnf:/etc/mysql/my.cnf --volume $PWD/app-mysql/data:/var/lib/mysql --label service=\"app-mysql\" private.registry/mysql:5.7 on 1.1.1.3", output
|
||||
assert_match "Upgraded all accessories on 1.1.1.3,1.1.1.1,1.1.1.2...", output
|
||||
end
|
||||
end
|
||||
@@ -247,15 +247,15 @@ class CliAccessoryTest < CliTestCase
|
||||
assert_match "Upgrading all accessories on 1.1.1.3...", output
|
||||
assert_match "docker network create kamal on 1.1.1.3", output
|
||||
assert_match "docker container stop app-mysql on 1.1.1.3", output
|
||||
assert_match "docker run --name app-mysql --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 3306:3306 --env MYSQL_ROOT_HOST="%" --env-file .kamal/apps/app/env/accessories/mysql.env --volume $PWD/app-mysql/etc/mysql/my.cnf:/etc/mysql/my.cnf --volume $PWD/app-mysql/data:/var/lib/mysql --label service=\"app-mysql\" private.registry/mysql:5.7 on 1.1.1.3", output
|
||||
assert_match "docker run --name app-mysql --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 3306:3306 --env KAMAL_HOST=\"1.1.1.3\" --env MYSQL_ROOT_HOST="%" --env-file .kamal/apps/app/env/accessories/mysql.env --volume $PWD/app-mysql/etc/mysql/my.cnf:/etc/mysql/my.cnf --volume $PWD/app-mysql/data:/var/lib/mysql --label service=\"app-mysql\" private.registry/mysql:5.7 on 1.1.1.3", output
|
||||
assert_match "Upgraded all accessories on 1.1.1.3", output
|
||||
end
|
||||
end
|
||||
|
||||
test "boot with web role filter" do
|
||||
run_command("boot", "redis", "-r", "web").tap do |output|
|
||||
assert_match "docker run --name app-redis --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/apps/app/env/accessories/redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.1", output
|
||||
assert_match "docker run --name app-redis --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/apps/app/env/accessories/redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.2", output
|
||||
assert_match "docker run --name app-redis --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 6379:6379 --env KAMAL_HOST=\"1.1.1.1\" --env-file .kamal/apps/app/env/accessories/redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.1", output
|
||||
assert_match "docker run --name app-redis --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 6379:6379 --env KAMAL_HOST=\"1.1.1.2\" --env-file .kamal/apps/app/env/accessories/redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.2", output
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -104,7 +104,7 @@ class CliAppTest < CliTestCase
|
||||
|
||||
run_command("boot", config: :with_env_tags).tap do |output|
|
||||
assert_match "docker tag dhh/app:latest dhh/app:latest", output
|
||||
assert_match %r{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 TEST="root" --env EXPERIMENT="disabled" --env SITE="site1"}, output
|
||||
assert_match %r{docker run --detach --restart unless-stopped --name app-web-latest --network kamal --hostname 1.1.1.1-[0-9a-f]{12} --env KAMAL_CONTAINER_NAME="app-web-latest" --env KAMAL_VERSION="latest" --env KAMAL_HOST="1.1.1.1" --env TEST="root" --env EXPERIMENT="disabled" --env SITE="site1"}, output
|
||||
assert_match "docker container ls --all --filter name=^app-web-123$ --quiet | xargs docker stop", output
|
||||
end
|
||||
end
|
||||
@@ -220,6 +220,21 @@ class CliAppTest < CliTestCase
|
||||
end
|
||||
end
|
||||
|
||||
test "boot with custom ssl certificate" do
|
||||
Kamal::Configuration::Proxy.any_instance.stubs(:custom_ssl_certificate?).returns(true)
|
||||
Kamal::Configuration::Proxy.any_instance.stubs(:certificate_pem_content).returns("CERTIFICATE CONTENT")
|
||||
Kamal::Configuration::Proxy.any_instance.stubs(:private_key_pem_content).returns("PRIVATE KEY CONTENT")
|
||||
|
||||
stub_running
|
||||
run_command("boot", config: :with_proxy).tap do |output|
|
||||
assert_match "Writing SSL certificates for web on 1.1.1.1", output
|
||||
assert_match "mkdir -p .kamal/proxy/apps-config/app/tls", output
|
||||
assert_match "Uploading \"CERTIFICATE CONTENT\" to .kamal/proxy/apps-config/app/tls/web/cert.pem", output
|
||||
assert_match "--tls-certificate-path=\"/home/kamal-proxy/.apps-config/app/tls/web/cert.pem\"", output
|
||||
assert_match "--tls-private-key-path=\"/home/kamal-proxy/.apps-config/app/tls/web/key.pem\"", output
|
||||
end
|
||||
end
|
||||
|
||||
test "start" do
|
||||
SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info).returns("999") # old version
|
||||
|
||||
@@ -361,11 +376,13 @@ class CliAppTest < CliTestCase
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:exec)
|
||||
.with("ssh -t root@1.1.1.1 -p 22 'docker run -it --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" dhh/app:latest ruby -v'")
|
||||
|
||||
run_command("exec", "-i", "ruby -v").tap do |output|
|
||||
assert_hook_ran "pre-connect", output
|
||||
assert_match "docker login -u [REDACTED] -p [REDACTED]", output
|
||||
assert_match "Get most recent version available as an image...", output
|
||||
assert_match "Launching interactive command with version latest via SSH from new container on 1.1.1.1...", output
|
||||
stub_stdin_tty do
|
||||
run_command("exec", "-i", "ruby -v").tap do |output|
|
||||
assert_hook_ran "pre-connect", output
|
||||
assert_match "docker login -u [REDACTED] -p [REDACTED]", output
|
||||
assert_match "Get most recent version available as an image...", output
|
||||
assert_match "Launching interactive command with version latest via SSH from new container on 1.1.1.1...", output
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -374,11 +391,26 @@ class CliAppTest < CliTestCase
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:exec)
|
||||
.with("ssh -t root@1.1.1.1 -p 22 'docker exec -it app-web-999 ruby -v'")
|
||||
|
||||
run_command("exec", "-i", "--reuse", "ruby -v").tap do |output|
|
||||
assert_hook_ran "pre-connect", output
|
||||
assert_match "Get current version of running container...", output
|
||||
assert_match "Running /usr/bin/env sh -c 'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --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=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | while read line; do echo ${line#app-web-}; done on 1.1.1.1", output
|
||||
assert_match "Launching interactive command with version 999 via SSH from existing container on 1.1.1.1...", output
|
||||
stub_stdin_tty do
|
||||
run_command("exec", "-i", "--reuse", "ruby -v").tap do |output|
|
||||
assert_hook_ran "pre-connect", output
|
||||
assert_match "Get current version of running container...", output
|
||||
assert_match "Running /usr/bin/env sh -c 'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --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=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | while read line; do echo ${line#app-web-}; done on 1.1.1.1", output
|
||||
assert_match "Launching interactive command with version 999 via SSH from existing container on 1.1.1.1...", output
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
test "exec interactive with pipe on STDIN" do
|
||||
Kamal::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:exec)
|
||||
.with("ssh -t root@1.1.1.1 -p 22 'docker exec -i app-web-999 ruby -v'")
|
||||
|
||||
stub_stdin_file do
|
||||
run_command("exec", "-i", "--reuse", "ruby -v").tap do |output|
|
||||
assert_hook_ran "pre-connect", output
|
||||
assert_match "Launching interactive command with version 999 via SSH from existing container on 1.1.1.1...", output
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -474,7 +506,7 @@ class CliAppTest < CliTestCase
|
||||
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 --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 run --detach --restart unless-stopped --name app-web-latest --network kamal --hostname 1.1.1.1-[0-9a-f]{12} --env KAMAL_CONTAINER_NAME="app-web-latest" --env KAMAL_VERSION="latest" --env KAMAL_HOST="1.1.1.1" --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 container ls --all --filter name=^app-web-123$ --quiet | xargs docker stop", output
|
||||
end
|
||||
|
||||
@@ -21,6 +21,7 @@ class CliBuildTest < CliTestCase
|
||||
.returns("")
|
||||
|
||||
run_command("push", "--verbose").tap do |output|
|
||||
assert_hook_ran "pre-connect", output
|
||||
assert_hook_ran "pre-build", output
|
||||
assert_match /Cloning repo into build directory/, output
|
||||
assert_match /git -C #{Dir.tmpdir}\/kamal-clones\/app-#{pwd_sha} clone #{Dir.pwd}/, output
|
||||
|
||||
@@ -44,42 +44,20 @@ class CliProxyTest < CliTestCase
|
||||
end
|
||||
|
||||
test "reboot" do
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-123$", "--quiet")
|
||||
.returns("abcdefabcdef")
|
||||
.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 prune --force --filter label=org.opencontainers.image.title=kamal-proxy on 1.1.1.1", output
|
||||
assert_match "mkdir -p .kamal/proxy/apps-config on 1.1.1.1", output
|
||||
assert_match "echo $(cat .kamal/proxy/options 2> /dev/null || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") $(cat .kamal/proxy/image 2> /dev/null || echo \"basecamp/kamal-proxy\"):$(cat .kamal/proxy/image_version 2> /dev/null || echo \"#{Kamal::Configuration::Proxy::Boot::MINIMUM_VERSION}\") $(cat .kamal/proxy/run_command 2> /dev/null || echo \"\") | xargs docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy --volume $(pwd)/.kamal/proxy/apps-config:/home/kamal-proxy/.apps-config 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 "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy on 1.1.1.2", output
|
||||
assert_match "mkdir -p .kamal/proxy/apps-config on 1.1.1.1", output
|
||||
assert_match "echo $(cat .kamal/proxy/options 2> /dev/null || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") $(cat .kamal/proxy/image 2> /dev/null || echo \"basecamp/kamal-proxy\"):$(cat .kamal/proxy/image_version 2> /dev/null || echo \"#{Kamal::Configuration::Proxy::Boot::MINIMUM_VERSION}\") $(cat .kamal/proxy/run_command 2> /dev/null || echo \"\") | xargs docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy --volume $(pwd)/.kamal/proxy/apps-config:/home/kamal-proxy/.apps-config 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
|
||||
|
||||
test "reboot --rolling" do
|
||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-123$", "--quiet")
|
||||
.returns("abcdefabcdef")
|
||||
.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
|
||||
@@ -204,7 +182,7 @@ class CliProxyTest < CliTestCase
|
||||
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 %r{docker run --detach --restart unless-stopped --name app-web-latest --network kamal --hostname 1.1.1.1-.* --env KAMAL_CONTAINER_NAME="app-web-latest" --env KAMAL_VERSION="latest" --env KAMAL_HOST="1.1.1.1" --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size="10m" --label service="app" --label role="web" --label destination dhh/app:latest}, output
|
||||
assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target=\"12345678:80\" --deploy-timeout=\"6s\" --drain-timeout=\"30s\" --buffer-requests --buffer-responses --log-request-header=\"Cache-Control\" --log-request-header=\"Last-Modified\" --log-request-header=\"User-Agent\"", output
|
||||
assert_match "docker container ls --all --filter name=^app-web-12345678$ --quiet | xargs docker stop", output
|
||||
assert_match "docker tag dhh/app:latest dhh/app:latest", output
|
||||
|
||||
@@ -149,6 +149,12 @@ class CommanderTest < ActiveSupport::TestCase
|
||||
assert_equal [], @kamal.accessory_hosts
|
||||
end
|
||||
|
||||
test "primary role hosts are first" do
|
||||
configure_with(:deploy_with_roles_workers_primary)
|
||||
assert_equal [ "1.1.1.1", "1.1.1.2", "1.1.1.3", "1.1.1.4" ], @kamal.hosts
|
||||
assert_equal [ "1.1.1.1", "1.1.1.2", "1.1.1.3", "1.1.1.4" ], @kamal.app_hosts
|
||||
end
|
||||
|
||||
private
|
||||
def configure_with(variant)
|
||||
@kamal = Kamal::Commander.new.tap do |kamal|
|
||||
|
||||
@@ -118,14 +118,21 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
|
||||
test "execute in new container over ssh" do
|
||||
new_command(:mysql).stub(:run_over_ssh, ->(cmd) { cmd.join(" ") }) do
|
||||
assert_match %r{docker run -it --rm --network kamal --env MYSQL_ROOT_HOST=\"%\" --env-file .kamal/apps/app/env/accessories/mysql.env private.registry/mysql:8.0 mysql -u root},
|
||||
new_command(:mysql).execute_in_new_container_over_ssh("mysql", "-u", "root")
|
||||
stub_stdin_tty { new_command(:mysql).execute_in_new_container_over_ssh("mysql", "-u", "root") }
|
||||
end
|
||||
end
|
||||
|
||||
test "execute in existing container over ssh" do
|
||||
new_command(:mysql).stub(:run_over_ssh, ->(cmd) { cmd.join(" ") }) do
|
||||
assert_match %r{docker exec -it app-mysql mysql -u root},
|
||||
new_command(:mysql).execute_in_existing_container_over_ssh("mysql", "-u", "root")
|
||||
stub_stdin_tty { new_command(:mysql).execute_in_existing_container_over_ssh("mysql", "-u", "root") }
|
||||
end
|
||||
end
|
||||
|
||||
test "execute in existing container with piped input over ssh" do
|
||||
new_command(:mysql).stub(:run_over_ssh, ->(cmd) { cmd.join(" ") }) do
|
||||
assert_match %r{docker exec -i app-mysql mysql -u root},
|
||||
stub_stdin_file { new_command(:mysql).execute_in_existing_container_over_ssh("mysql", "-u", "root") }
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -13,13 +13,13 @@ class CommandsAppTest < ActiveSupport::TestCase
|
||||
|
||||
test "run" do
|
||||
assert_equal \
|
||||
"docker run --detach --restart unless-stopped --name app-web-999 --network kamal -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label destination dhh/app:999",
|
||||
"docker run --detach --restart unless-stopped --name app-web-999 --network kamal --env KAMAL_CONTAINER_NAME=\"app-web-999\" --env KAMAL_VERSION=\"999\" --env KAMAL_HOST=\"1.1.1.1\" --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label destination dhh/app:999",
|
||||
new_command.run.join(" ")
|
||||
end
|
||||
|
||||
test "run with hostname" do
|
||||
assert_equal \
|
||||
"docker run --detach --restart unless-stopped --name app-web-999 --network kamal --hostname myhost -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label destination dhh/app:999",
|
||||
"docker run --detach --restart unless-stopped --name app-web-999 --network kamal --hostname myhost --env KAMAL_CONTAINER_NAME=\"app-web-999\" --env KAMAL_VERSION=\"999\" --env KAMAL_HOST=\"1.1.1.1\" --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label destination dhh/app:999",
|
||||
new_command.run(hostname: "myhost").join(" ")
|
||||
end
|
||||
|
||||
@@ -27,14 +27,14 @@ class CommandsAppTest < ActiveSupport::TestCase
|
||||
@config[:volumes] = [ "/local/path:/container/path" ]
|
||||
|
||||
assert_equal \
|
||||
"docker run --detach --restart unless-stopped --name app-web-999 --network kamal -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" --volume /local/path:/container/path --label service=\"app\" --label role=\"web\" --label destination dhh/app:999",
|
||||
"docker run --detach --restart unless-stopped --name app-web-999 --network kamal --env KAMAL_CONTAINER_NAME=\"app-web-999\" --env KAMAL_VERSION=\"999\" --env KAMAL_HOST=\"1.1.1.1\" --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" --volume /local/path:/container/path --label service=\"app\" --label role=\"web\" --label destination dhh/app:999",
|
||||
new_command.run.join(" ")
|
||||
end
|
||||
|
||||
test "run with custom options" do
|
||||
@config[:servers] = { "web" => [ "1.1.1.1" ], "jobs" => { "hosts" => [ "1.1.1.2" ], "cmd" => "bin/jobs", "options" => { "mount" => "somewhere", "cap-add" => true } } }
|
||||
assert_equal \
|
||||
"docker run --detach --restart unless-stopped --name app-jobs-999 --network kamal -e KAMAL_CONTAINER_NAME=\"app-jobs-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/apps/app/env/roles/jobs.env --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"jobs\" --label destination --mount \"somewhere\" --cap-add dhh/app:999 bin/jobs",
|
||||
"docker run --detach --restart unless-stopped --name app-jobs-999 --network kamal --env KAMAL_CONTAINER_NAME=\"app-jobs-999\" --env KAMAL_VERSION=\"999\" --env KAMAL_HOST=\"1.1.1.2\" --env-file .kamal/apps/app/env/roles/jobs.env --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"jobs\" --label destination --mount \"somewhere\" --cap-add dhh/app:999 bin/jobs",
|
||||
new_command(role: "jobs", host: "1.1.1.2").run.join(" ")
|
||||
end
|
||||
|
||||
@@ -42,7 +42,7 @@ class CommandsAppTest < ActiveSupport::TestCase
|
||||
@config[:logging] = { "driver" => "local", "options" => { "max-size" => "100m", "max-file" => "3" } }
|
||||
|
||||
assert_equal \
|
||||
"docker run --detach --restart unless-stopped --name app-web-999 --network kamal -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/apps/app/env/roles/web.env --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --label service=\"app\" --label role=\"web\" --label destination dhh/app:999",
|
||||
"docker run --detach --restart unless-stopped --name app-web-999 --network kamal --env KAMAL_CONTAINER_NAME=\"app-web-999\" --env KAMAL_VERSION=\"999\" --env KAMAL_HOST=\"1.1.1.1\" --env-file .kamal/apps/app/env/roles/web.env --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --label service=\"app\" --label role=\"web\" --label destination dhh/app:999",
|
||||
new_command.run.join(" ")
|
||||
end
|
||||
|
||||
@@ -51,7 +51,7 @@ class CommandsAppTest < ActiveSupport::TestCase
|
||||
@config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "logging" => { "driver" => "local", "options" => { "max-size" => "100m" } } } }
|
||||
|
||||
assert_equal \
|
||||
"docker run --detach --restart unless-stopped --name app-web-999 --network kamal -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/apps/app/env/roles/web.env --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --label service=\"app\" --label role=\"web\" --label destination dhh/app:999",
|
||||
"docker run --detach --restart unless-stopped --name app-web-999 --network kamal --env KAMAL_CONTAINER_NAME=\"app-web-999\" --env KAMAL_VERSION=\"999\" --env KAMAL_HOST=\"1.1.1.1\" --env-file .kamal/apps/app/env/roles/web.env --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --label service=\"app\" --label role=\"web\" --label destination dhh/app:999",
|
||||
new_command.run.join(" ")
|
||||
end
|
||||
|
||||
@@ -60,7 +60,7 @@ class CommandsAppTest < ActiveSupport::TestCase
|
||||
@config[:env]["tags"] = { "tag1" => { "ENV1" => "value1" } }
|
||||
|
||||
assert_equal \
|
||||
"docker run --detach --restart unless-stopped --name app-web-999 --network kamal -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env ENV1=\"value1\" --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label destination dhh/app:999",
|
||||
"docker run --detach --restart unless-stopped --name app-web-999 --network kamal --env KAMAL_CONTAINER_NAME=\"app-web-999\" --env KAMAL_VERSION=\"999\" --env KAMAL_HOST=\"1.1.1.1\" --env ENV1=\"value1\" --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label destination dhh/app:999",
|
||||
new_command.run.join(" ")
|
||||
end
|
||||
|
||||
@@ -149,8 +149,6 @@ class CommandsAppTest < ActiveSupport::TestCase
|
||||
new_command.remove.join(" ")
|
||||
end
|
||||
|
||||
|
||||
|
||||
test "logs" do
|
||||
assert_equal \
|
||||
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps 2>&1",
|
||||
@@ -288,7 +286,7 @@ class CommandsAppTest < ActiveSupport::TestCase
|
||||
|
||||
test "execute in new container over ssh" do
|
||||
assert_match %r{docker run -it --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size="10m" dhh/app:999 bin/rails c},
|
||||
new_command.execute_in_new_container_over_ssh("bin/rails", "c", env: {})
|
||||
stub_stdin_tty { new_command.execute_in_new_container_over_ssh("bin/rails", "c", env: {}) }
|
||||
end
|
||||
|
||||
test "execute in new container over ssh with tags" do
|
||||
@@ -296,18 +294,23 @@ class CommandsAppTest < ActiveSupport::TestCase
|
||||
@config[:env]["tags"] = { "tag1" => { "ENV1" => "value1" } }
|
||||
|
||||
assert_equal "ssh -t root@1.1.1.1 -p 22 'docker run -it --rm --network kamal --env ENV1=\"value1\" --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" dhh/app:999 bin/rails c'",
|
||||
new_command.execute_in_new_container_over_ssh("bin/rails", "c", env: {})
|
||||
stub_stdin_tty { new_command.execute_in_new_container_over_ssh("bin/rails", "c", env: {}) }
|
||||
end
|
||||
|
||||
test "execute in new container with custom options over ssh" do
|
||||
@config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "options" => { "mount" => "somewhere", "cap-add" => true } } }
|
||||
assert_match %r{docker run -it --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" --mount \"somewhere\" --cap-add dhh/app:999 bin/rails c},
|
||||
new_command.execute_in_new_container_over_ssh("bin/rails", "c", env: {})
|
||||
stub_stdin_tty { new_command.execute_in_new_container_over_ssh("bin/rails", "c", env: {}) }
|
||||
end
|
||||
|
||||
test "execute in existing container over ssh" do
|
||||
assert_match %r{docker exec -it app-web-999 bin/rails c},
|
||||
new_command.execute_in_existing_container_over_ssh("bin/rails", "c", env: {})
|
||||
stub_stdin_tty { new_command.execute_in_existing_container_over_ssh("bin/rails", "c", env: {}) }
|
||||
end
|
||||
|
||||
test "execute in existing container with piped input over ssh" do
|
||||
assert_match %r{docker exec -i app-web-999 bin/rails c},
|
||||
stub_stdin_file { new_command.execute_in_existing_container_over_ssh("bin/rails", "c", env: {}) }
|
||||
end
|
||||
|
||||
test "run over ssh" do
|
||||
|
||||
@@ -61,6 +61,32 @@ class CommandsBuilderTest < ActiveSupport::TestCase
|
||||
builder.push.join(" ")
|
||||
end
|
||||
|
||||
test "target pack when pack is set" do
|
||||
builder = new_builder_command(image: "dhh/app", builder: { "arch" => "amd64", "pack" => { "builder" => "heroku/builder:24", "buildpacks" => [ "heroku/ruby", "heroku/procfile" ] } })
|
||||
assert_equal "pack", builder.name
|
||||
assert_equal \
|
||||
"pack build dhh/app --platform linux/amd64 --creation-time now --builder heroku/builder:24 --buildpack heroku/ruby --buildpack heroku/procfile --buildpack paketo-buildpacks/image-labels -t dhh/app:123 -t dhh/app:latest --env BP_IMAGE_LABELS=service=app --path . && docker push dhh/app:123 && docker push dhh/app:latest",
|
||||
builder.push.join(" ")
|
||||
end
|
||||
|
||||
test "pack build args passed as env" do
|
||||
builder = new_builder_command(image: "dhh/app", builder: { "args" => { "a" => 1, "b" => 2 }, "arch" => "amd64", "pack" => { "builder" => "heroku/builder:24", "buildpacks" => [ "heroku/ruby", "heroku/procfile" ] } })
|
||||
|
||||
assert_equal \
|
||||
"pack build dhh/app --platform linux/amd64 --creation-time now --builder heroku/builder:24 --buildpack heroku/ruby --buildpack heroku/procfile --buildpack paketo-buildpacks/image-labels -t dhh/app:123 -t dhh/app:latest --env BP_IMAGE_LABELS=service=app --env a=\"1\" --env b=\"2\" --path . && docker push dhh/app:123 && docker push dhh/app:latest",
|
||||
builder.push.join(" ")
|
||||
end
|
||||
|
||||
test "pack build secrets as env" do
|
||||
with_test_secrets("secrets" => "token_a=foo\ntoken_b=bar") do
|
||||
builder = new_builder_command(image: "dhh/app", builder: { "secrets" => [ "token_a", "token_b" ], "arch" => "amd64", "pack" => { "builder" => "heroku/builder:24", "buildpacks" => [ "heroku/ruby", "heroku/procfile" ] } })
|
||||
|
||||
assert_equal \
|
||||
"pack build dhh/app --platform linux/amd64 --creation-time now --builder heroku/builder:24 --buildpack heroku/ruby --buildpack heroku/procfile --buildpack paketo-buildpacks/image-labels -t dhh/app:123 -t dhh/app:latest --env BP_IMAGE_LABELS=service=app --env token_a=\"foo\" --env token_b=\"bar\" --path . && docker push dhh/app:123 && docker push dhh/app:latest",
|
||||
builder.push.join(" ")
|
||||
end
|
||||
end
|
||||
|
||||
test "cloud builder" do
|
||||
builder = new_builder_command(builder: { "arch" => [ "#{local_arch}" ], "driver" => "cloud docker-org-name/builder-name" })
|
||||
assert_equal "cloud", builder.name
|
||||
|
||||
@@ -16,6 +16,23 @@ class ConfigurationBuilderTest < ActiveSupport::TestCase
|
||||
assert_equal false, config.builder.remote?
|
||||
end
|
||||
|
||||
test "pack?" do
|
||||
assert_not config.builder.pack?
|
||||
end
|
||||
|
||||
test "pack? with pack builder" do
|
||||
@deploy[:builder] = { "arch" => "arm64", "pack" => { "builder" => "heroku/builder:24" } }
|
||||
|
||||
assert config.builder.pack?
|
||||
end
|
||||
|
||||
test "pack details" do
|
||||
@deploy[:builder] = { "arch" => "amd64", "pack" => { "builder" => "heroku/builder:24", "buildpacks" => [ "heroku/ruby", "heroku/procfile" ] } }
|
||||
|
||||
assert_equal "heroku/builder:24", config.builder.pack_builder
|
||||
assert_equal [ "heroku/ruby", "heroku/procfile" ], config.builder.pack_buildpacks
|
||||
end
|
||||
|
||||
test "remote" do
|
||||
assert_nil config.builder.remote
|
||||
end
|
||||
|
||||
@@ -25,5 +25,7 @@ class ConfigurationProxyBootTest < ActiveSupport::TestCase
|
||||
assert_equal "/home/kamal-proxy/.apps-config/app", @proxy_boot_config.app_container_directory
|
||||
assert_equal ".kamal/proxy/apps-config/app/error_pages", @proxy_boot_config.error_pages_directory
|
||||
assert_equal "/home/kamal-proxy/.apps-config/app/error_pages", @proxy_boot_config.error_pages_container_directory
|
||||
assert_equal ".kamal/proxy/apps-config/app/tls", @proxy_boot_config.tls_directory
|
||||
assert_equal "/home/kamal-proxy/.apps-config/app/tls", @proxy_boot_config.tls_container_directory
|
||||
end
|
||||
end
|
||||
|
||||
@@ -45,6 +45,66 @@ class ConfigurationProxyTest < ActiveSupport::TestCase
|
||||
end
|
||||
end
|
||||
|
||||
test "ssl with certificate and private key from secrets" do
|
||||
with_test_secrets("secrets" => "CERT_PEM=certificate\nKEY_PEM=private_key") do
|
||||
@deploy[:proxy] = {
|
||||
"ssl" => {
|
||||
"certificate_pem" => "CERT_PEM",
|
||||
"private_key_pem" => "KEY_PEM"
|
||||
},
|
||||
"host" => "example.com"
|
||||
}
|
||||
|
||||
proxy = config.proxy
|
||||
assert_equal ".kamal/proxy/apps-config/app/tls/cert.pem", proxy.host_tls_cert
|
||||
assert_equal ".kamal/proxy/apps-config/app/tls/key.pem", proxy.host_tls_key
|
||||
assert_equal "/home/kamal-proxy/.apps-config/app/tls/cert.pem", proxy.container_tls_cert
|
||||
assert_equal "/home/kamal-proxy/.apps-config/app/tls/key.pem", proxy.container_tls_key
|
||||
end
|
||||
end
|
||||
|
||||
test "deploy options with custom ssl certificates" do
|
||||
with_test_secrets("secrets" => "CERT_PEM=certificate\nKEY_PEM=private_key") do
|
||||
@deploy[:proxy] = {
|
||||
"ssl" => {
|
||||
"certificate_pem" => "CERT_PEM",
|
||||
"private_key_pem" => "KEY_PEM"
|
||||
},
|
||||
"host" => "example.com"
|
||||
}
|
||||
|
||||
proxy = config.proxy
|
||||
options = proxy.deploy_options
|
||||
assert_equal true, options[:tls]
|
||||
assert_equal "/home/kamal-proxy/.apps-config/app/tls/cert.pem", options[:"tls-certificate-path"]
|
||||
assert_equal "/home/kamal-proxy/.apps-config/app/tls/key.pem", options[:"tls-private-key-path"]
|
||||
end
|
||||
end
|
||||
|
||||
test "ssl with certificate and no private key" do
|
||||
with_test_secrets("secrets" => "CERT_PEM=certificate") do
|
||||
@deploy[:proxy] = {
|
||||
"ssl" => {
|
||||
"certificate_pem" => "CERT_PEM"
|
||||
},
|
||||
"host" => "example.com"
|
||||
}
|
||||
assert_raises(Kamal::ConfigurationError) { config.proxy.ssl? }
|
||||
end
|
||||
end
|
||||
|
||||
test "ssl with private key and no certificate" do
|
||||
with_test_secrets("secrets" => "KEY_PEM=private_key") do
|
||||
@deploy[:proxy] = {
|
||||
"ssl" => {
|
||||
"private_key_pem" => "KEY_PEM"
|
||||
},
|
||||
"host" => "example.com"
|
||||
}
|
||||
assert_raises(Kamal::ConfigurationError) { config.proxy.ssl? }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def config
|
||||
Kamal::Configuration.new(@deploy)
|
||||
|
||||
@@ -42,6 +42,7 @@ class ConfigurationValidationTest < ActiveSupport::TestCase
|
||||
assert_error "servers/web/options: should be a hash", servers: { "web" => { "options" => "" } }
|
||||
assert_error "servers/web/logging/options: should be a hash", servers: { "web" => { "logging" => { "options" => "" } } }
|
||||
assert_error "servers/web/logging/driver: should be a string", servers: { "web" => { "logging" => { "driver" => [] } } }
|
||||
assert_error "servers/web/labels/service: invalid label. destination, role, and service are reserved labels", servers: { "web" => { "labels" => { "service" => "foo" } } }
|
||||
assert_error "servers/web/labels: should be a hash", servers: { "web" => { "labels" => [] } }
|
||||
assert_error "servers/web/env: should be a hash", servers: { "web" => { "env" => [] } }
|
||||
assert_error "servers/web/env: tags are only allowed in the root env", servers: { "web" => { "hosts" => [ "1.1.1.1" ], "env" => { "tags" => {} } } }
|
||||
@@ -58,6 +59,7 @@ class ConfigurationValidationTest < ActiveSupport::TestCase
|
||||
assert_error "accessories/accessory1: should be a hash", accessories: { "accessory1" => [] }
|
||||
assert_error "accessories/accessory1: unknown key: unknown", accessories: { "accessory1" => { "unknown" => "baz" } }
|
||||
assert_error "accessories/accessory1/options: should be a hash", accessories: { "accessory1" => { "options" => [] } }
|
||||
assert_error "accessories/accessory1/labels/destination: invalid label. destination, role, and service are reserved labels", accessories: { "accessory1" => { "host" => "host", "labels" => { "destination" => "foo" } } }
|
||||
assert_error "accessories/accessory1/host: should be a string", accessories: { "accessory1" => { "host" => [] } }
|
||||
assert_error "accessories/accessory1/env: should be a hash", accessories: { "accessory1" => { "env" => [] } }
|
||||
assert_error "accessories/accessory1/env: tags are only allowed in the root env", accessories: { "accessory1" => { "host" => "host", "env" => { "tags" => {} } } }
|
||||
@@ -94,6 +96,7 @@ class ConfigurationValidationTest < ActiveSupport::TestCase
|
||||
assert_error "builder/arch: should be an array or a string", builder: { "arch" => {} }
|
||||
assert_error "builder/args: should be a hash", builder: { "args" => [ "foo" ] }
|
||||
assert_error "builder/cache/options: should be a string", builder: { "cache" => { "options" => [] } }
|
||||
assert_error "builder: buildpacks only support building for one arch", builder: { "arch" => [ "amd64", "arm64" ], "pack" => { "builder" => "heroku/builder:24" } }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -386,7 +386,7 @@ class ConfigurationTest < ActiveSupport::TestCase
|
||||
Kamal::Configuration.new(@deploy_with_roles)
|
||||
end
|
||||
|
||||
assert_equal "SSL is only supported on a single server, found 2 servers for role workers", exception.message
|
||||
assert_equal "SSL is only supported on a single server unless you provide custom certificates, found 2 servers for role workers", exception.message
|
||||
end
|
||||
|
||||
test "two proxy ssl roles with same host" do
|
||||
|
||||
19
test/fixtures/deploy_with_roles_workers_primary.yml
vendored
Normal file
19
test/fixtures/deploy_with_roles_workers_primary.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
service: app
|
||||
image: dhh/app
|
||||
servers:
|
||||
workers:
|
||||
- 1.1.1.1
|
||||
- 1.1.1.2
|
||||
web:
|
||||
- 1.1.1.3
|
||||
- 1.1.1.4
|
||||
env:
|
||||
REDIS_URL: redis://x/y
|
||||
registry:
|
||||
server: registry.digitalocean.com
|
||||
username: user
|
||||
password: pw
|
||||
builder:
|
||||
arch: amd64
|
||||
deploy_timeout: 1
|
||||
primary_role: workers
|
||||
@@ -41,6 +41,8 @@ services:
|
||||
context: docker/vm
|
||||
volumes:
|
||||
- shared:/shared
|
||||
ports:
|
||||
- "22443:443"
|
||||
|
||||
vm2:
|
||||
privileged: true
|
||||
@@ -61,6 +63,7 @@ services:
|
||||
context: docker/load_balancer
|
||||
ports:
|
||||
- "12345:80"
|
||||
- "12443:443"
|
||||
depends_on:
|
||||
- vm1
|
||||
- vm2
|
||||
|
||||
@@ -18,6 +18,7 @@ RUN apt-get update --fix-missing && apt-get install -y docker-ce docker-ce-cli c
|
||||
|
||||
COPY *.sh .
|
||||
COPY app/ app/
|
||||
COPY app_with_custom_certificate/ app_with_custom_certificate/
|
||||
COPY app_with_roles/ app_with_roles/
|
||||
COPY app_with_traefik/ app_with_traefik/
|
||||
COPY app_with_proxied_accessory/ app_with_proxied_accessory/
|
||||
@@ -29,6 +30,7 @@ RUN mkdir -p /etc/docker/certs.d/registry:4443 && ln -s /shared/certs/domain.crt
|
||||
RUN git config --global user.email "deployer@example.com"
|
||||
RUN git config --global user.name "Deployer"
|
||||
RUN cd app && git init && git add . && git commit -am "Initial version"
|
||||
RUN cd app_with_custom_certificate && git init && git add . && git commit -am "Initial version"
|
||||
RUN cd app_with_roles && git init && git add . && git commit -am "Initial version"
|
||||
RUN cd app_with_traefik && git init && git add . && git commit -am "Initial version"
|
||||
RUN cd app_with_proxied_accessory && git init && git add . && git commit -am "Initial version"
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
CUSTOM_CERT=$(cat certs/cert.pem)
|
||||
CUSTOM_KEY=$(cat certs/key.pem)
|
||||
@@ -0,0 +1,10 @@
|
||||
FROM registry:4443/nginx:1-alpine-slim
|
||||
|
||||
COPY default.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
ARG COMMIT_SHA
|
||||
|
||||
RUN echo $COMMIT_SHA > /usr/share/nginx/html/version
|
||||
RUN mkdir -p /usr/share/nginx/html/versions && echo "version" > /usr/share/nginx/html/versions/$COMMIT_SHA
|
||||
RUN mkdir -p /usr/share/nginx/html/versions && echo "hidden" > /usr/share/nginx/html/versions/.hidden
|
||||
RUN echo "Up!" > /usr/share/nginx/html/up
|
||||
@@ -0,0 +1,19 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDCzCCAfOgAwIBAgIUJHOADjhddzCAdXFfZvhXAsVMwhowDQYJKoZIhvcNAQEL
|
||||
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTI1MDYxNzA5MDYxOVoYDzIxMjUw
|
||||
NTI0MDkwNjE5WjAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEB
|
||||
AQUAA4IBDwAwggEKAoIBAQDQaLWwoLZ3/cZdiW/m4pqOe228wCx/CRU9/E2AT9NS
|
||||
ofuJNtUaxw7QAAFEWIrnf9y3M09lZeox1CNmXe2GADnnx/n906zSGX18SdDmWrxa
|
||||
L/1t5OZiXl3we5PM3UNvbFPSq1MCnOtvo6jTPM7shIpJ/5/KuuqovyrO31VCnc2+
|
||||
ycEzJ2BOcKFUFAeyT/8bk9lAI+1971PLqC6ut9dfy8PVHSPyGrxGiQCpStU7NiQj
|
||||
LUkqte7x9GcIKTJUjMkWIsvGke9oGoGgEl5gEfqxFAs3ZkA1aYkiHhwFtrUkGOOf
|
||||
O1C6sqfwnnAhtG8LnULGlFYi3GoKALF2XSIagGpaQM5HAgMBAAGjUzBRMB0GA1Ud
|
||||
DgQWBBQg2m871YSI220bQEG5APeGzeaz4zAfBgNVHSMEGDAWgBQg2m871YSI220b
|
||||
QEG5APeGzeaz4zAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBc
|
||||
yQvjLV+Uym+SI/bmKNKafW7ioWSkWAfTl/bvCB8xCX2OJsSqh1vjiKhkcJ6t0Tcj
|
||||
cEiYs7Q+2NVC+s+0ztrN1y4Ve8iX9K9D6o/09bD23zTKpftxCMv8NqoBicNVJ7O9
|
||||
sINcTqzrIPb+jawE47ogNvlorsU1hi1GTmDHtIqVJPQwiNCIWd8frBLf+WfCHCCK
|
||||
xRJb4hh5wR05v94L0/QdfKQ8qqCRG0VLyoGGcUyQgC8PLLlHRIWIYuwo3xhUK9nN
|
||||
Gn8WNiACY4ry1wRauqIp54N3fM1a5sgzpgPKc8++KLVBpxhDy8nRoFAD0k6y1iM0
|
||||
2EoVLhbMvwhYwHOHkktp
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDQaLWwoLZ3/cZd
|
||||
iW/m4pqOe228wCx/CRU9/E2AT9NSofuJNtUaxw7QAAFEWIrnf9y3M09lZeox1CNm
|
||||
Xe2GADnnx/n906zSGX18SdDmWrxaL/1t5OZiXl3we5PM3UNvbFPSq1MCnOtvo6jT
|
||||
PM7shIpJ/5/KuuqovyrO31VCnc2+ycEzJ2BOcKFUFAeyT/8bk9lAI+1971PLqC6u
|
||||
t9dfy8PVHSPyGrxGiQCpStU7NiQjLUkqte7x9GcIKTJUjMkWIsvGke9oGoGgEl5g
|
||||
EfqxFAs3ZkA1aYkiHhwFtrUkGOOfO1C6sqfwnnAhtG8LnULGlFYi3GoKALF2XSIa
|
||||
gGpaQM5HAgMBAAECggEAM2dIPRb+uozU8vg1qhCFR5RpBi+uKe0vGJlU8kt+F3kN
|
||||
hhQIrvCfFi2SIm3mYOAYK/WTZTKkd4LX8mVDcxQ2NBWOcw1VKIMSAOhiBpclsub4
|
||||
TrUxH90ftXN9in+epOpmqGUKdfAHYANRXjy22v5773GF06aTv2hbYigSqvoqJ57A
|
||||
PCdpw9q9sTwJqR9reU3f9fHsUyIwLCQpbtFyQc8aU9LHqgs4SAkaogY+4mPmlCrl
|
||||
pQ5wGljTXmK5g1o/v+mu1WdeGNOzd5//xp0YImkGtyiqh8Ab891MI1wPgivNP5Lo
|
||||
Ru1wKhegj89XamT/LUCtn6NCcokE/9pqEXrKK7JeVQKBgQD98kGUkdAm+zHjRZsr
|
||||
KTeQQ/wszFrNcbP9irE5MqnASWskIXcAhGVJrqbtinLPLIeT22BTsJkCUjVJdfX2
|
||||
MObjiJP0LMrMVpGQC0b+i4boS8W/lY5T4fM97B+ILc3Y1OYiUedg0gVsFspSR4ef
|
||||
luNfbKbmdzYYqFz6a/q5vExqBQKBgQDSGC2MJXYAewRJ9Mk3fNvll/6yz73rGCct
|
||||
tljwNXUgC7y2nEabDverPd74olSxojQwus/kA8JrMVa2IkXo+lKAwLV+nyj3PGHw
|
||||
3szTeAVWrGIRveWuW6IQ5zOP2IGkX5Jm+XSPVihnMz7SZA6k6qCtWVVywfBubSpi
|
||||
1dMNWAhs2wKBgBvMVw1yYLzDppRgXDn/SwvJxWMKA66VkcRhWEEQoLBh2Q6dcy9l
|
||||
TskgCznZe/PdxgGTdBn1LOqqIRcniIMomz2xB7Ek7hYsK8b+1QisMVpgYQc10dyw
|
||||
0TWoEVOQ4AWqWH7NRGy+0MUiQYd8OQZpN/6MIED+L7fHRlZLV6jZSewZAoGBAJwo
|
||||
bHJmxbbFuQJfd9BOdgPJXf76emdrpHNNvf2NPml7T+FLdw95qI0Xh8u2nM0Li09N
|
||||
C4inYrLaEWF/SAdLSFd65WwgUQqzTvkCIaxs4UrzBlG5nCZk5ak6sBCTFIlgoCj5
|
||||
8bE4kP9kD6XByUC7RIKUi/aoQFVTvtWHqT+Z12lRAoGAAVoZVxE+xPAfzVyAatpH
|
||||
M8WwgB23r07thNDiJCUMOQUT8LRFKg/Hyj6jB2W7gj669G/Bvoar++nXJVw7QCiv
|
||||
MlOk1pfaKuW82rCPnTeUzJwf2KQ8Jg2avasD4GFWZBJVvlHN1ONySViIpb67hhAK
|
||||
1OcbfGutFiGWhUwXNVkVc4U=
|
||||
-----END PRIVATE KEY-----
|
||||
@@ -0,0 +1,36 @@
|
||||
service: app_with_custom_certificate
|
||||
image: app_with_custom_certificate
|
||||
servers:
|
||||
web:
|
||||
hosts:
|
||||
- vm1
|
||||
- vm2
|
||||
workers:
|
||||
hosts:
|
||||
- vm3
|
||||
cmd: sleep infinity
|
||||
deploy_timeout: 2
|
||||
drain_timeout: 2
|
||||
readiness_delay: 0
|
||||
|
||||
proxy:
|
||||
host: localhost
|
||||
ssl:
|
||||
certificate_pem: CUSTOM_CERT
|
||||
private_key_pem: CUSTOM_KEY
|
||||
healthcheck:
|
||||
interval: 1
|
||||
timeout: 1
|
||||
path: "/up"
|
||||
|
||||
asset_path: /usr/share/nginx/html/versions
|
||||
|
||||
registry:
|
||||
server: registry:4443
|
||||
username: root
|
||||
password: root
|
||||
builder:
|
||||
driver: docker
|
||||
arch: <%= Kamal::Utils.docker_arch %>
|
||||
args:
|
||||
COMMIT_SHA: <%= `git rev-parse HEAD` %>
|
||||
@@ -0,0 +1,17 @@
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name localhost;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
}
|
||||
|
||||
# redirect server error pages to the static page /50x.html
|
||||
#
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
}
|
||||
@@ -63,8 +63,8 @@ class IntegrationTest < ActiveSupport::TestCase
|
||||
assert_match message, response.body.strip if message
|
||||
end
|
||||
|
||||
def assert_app_is_up(version: nil, app: @app)
|
||||
response = app_response(app: app)
|
||||
def assert_app_is_up(version: nil, app: @app, cert: nil)
|
||||
response = app_response(app: app, cert: cert)
|
||||
debug_response_code(response, "200")
|
||||
assert_equal "200", response.code
|
||||
assert_app_version(version, response) if version
|
||||
@@ -82,8 +82,14 @@ class IntegrationTest < ActiveSupport::TestCase
|
||||
assert_equal up_times, up_count
|
||||
end
|
||||
|
||||
def app_response(app: @app)
|
||||
Net::HTTP.get_response(URI.parse("http://#{app_host(app)}:12345/version"))
|
||||
def app_response(app: @app, cert: nil)
|
||||
uri = cert ? URI.parse("https://#{app_host(app)}:22443/version") : URI.parse("http://#{app_host(app)}:12345/version")
|
||||
|
||||
if cert
|
||||
https_response_with_cert(uri, cert)
|
||||
else
|
||||
Net::HTTP.get_response(uri)
|
||||
end
|
||||
end
|
||||
|
||||
def update_app_rev
|
||||
@@ -186,4 +192,19 @@ class IntegrationTest < ActiveSupport::TestCase
|
||||
"localhost"
|
||||
end
|
||||
end
|
||||
|
||||
def https_response_with_cert(uri, cert)
|
||||
host = uri.host
|
||||
port = uri.port
|
||||
|
||||
http = Net::HTTP.new(uri.host, uri.port)
|
||||
http.use_ssl = true
|
||||
|
||||
store = OpenSSL::X509::Store.new
|
||||
store.add_cert(OpenSSL::X509::Certificate.new(File.read(cert)))
|
||||
http.cert_store = store
|
||||
|
||||
request = Net::HTTP::Get.new(uri)
|
||||
http.request(request)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -142,8 +142,19 @@ class MainTest < IntegrationTest
|
||||
assert_app_is_up version: first_version
|
||||
end
|
||||
|
||||
test "deploy with a custom certificate" do
|
||||
@app = "app_with_custom_certificate"
|
||||
|
||||
first_version = latest_app_version
|
||||
|
||||
kamal :setup
|
||||
|
||||
assert_app_is_up version: first_version, cert: "test/integration/docker/deployer/app_with_custom_certificate/certs/cert.pem"
|
||||
end
|
||||
|
||||
private
|
||||
def assert_envs(version:)
|
||||
assert_env :KAMAL_HOST, "vm1", version: version, vm: :vm1
|
||||
assert_env :CLEAR_TOKEN, "4321", version: version, vm: :vm1
|
||||
assert_env :HOST_TOKEN, "abcd", version: version, vm: :vm1
|
||||
assert_env :SECRET_TOKEN, "1234 with \"中文\"", version: version, vm: :vm1
|
||||
|
||||
@@ -15,57 +15,111 @@ class BitwardenSecretsManagerAdapterTest < SecretAdapterTestCase
|
||||
stub_ticks.with("bws --version 2> /dev/null")
|
||||
stub_login
|
||||
stub_ticks
|
||||
.with("bws secret list -o env")
|
||||
.returns("KAMAL_REGISTRY_PASSWORD=\"some_password\"\nMY_OTHER_SECRET=\"my=weird\"secret\"")
|
||||
.with("bws secret list")
|
||||
.returns(<<~JSON)
|
||||
[
|
||||
{
|
||||
"key": "KAMAL_REGISTRY_PASSWORD",
|
||||
"value": "some_password"
|
||||
},
|
||||
{
|
||||
"key": "MY_OTHER_SECRET",
|
||||
"value": "my=wierd\\"secret"
|
||||
}
|
||||
]
|
||||
JSON
|
||||
|
||||
expected = '{"KAMAL_REGISTRY_PASSWORD":"some_password","MY_OTHER_SECRET":"my\=weird\"secret"}'
|
||||
actual = shellunescape(run_command("fetch", "all"))
|
||||
assert_equal expected, actual
|
||||
json = JSON.parse(shellunescape(run_command("fetch", "all")))
|
||||
|
||||
expected_json = {
|
||||
"KAMAL_REGISTRY_PASSWORD"=>"some_password",
|
||||
"MY_OTHER_SECRET"=>"my=wierd\"secret"
|
||||
}
|
||||
|
||||
assert_equal expected_json, json
|
||||
end
|
||||
|
||||
test "fetch all with from" do
|
||||
stub_ticks.with("bws --version 2> /dev/null")
|
||||
stub_login
|
||||
stub_ticks
|
||||
.with("bws secret list -o env 82aeb5bd-6958-4a89-8197-eacab758acce")
|
||||
.returns("KAMAL_REGISTRY_PASSWORD=\"some_password\"\nMY_OTHER_SECRET=\"my=weird\"secret\"")
|
||||
.with("bws secret list 82aeb5bd-6958-4a89-8197-eacab758acce")
|
||||
.returns(<<~JSON)
|
||||
[
|
||||
{
|
||||
"key": "KAMAL_REGISTRY_PASSWORD",
|
||||
"value": "some_password"
|
||||
},
|
||||
{
|
||||
"key": "MY_OTHER_SECRET",
|
||||
"value": "my=wierd\\"secret"
|
||||
}
|
||||
]
|
||||
JSON
|
||||
|
||||
expected = '{"KAMAL_REGISTRY_PASSWORD":"some_password","MY_OTHER_SECRET":"my\=weird\"secret"}'
|
||||
actual = shellunescape(run_command("fetch", "all", "--from", "82aeb5bd-6958-4a89-8197-eacab758acce"))
|
||||
assert_equal expected, actual
|
||||
json = JSON.parse(shellunescape(run_command("fetch", "all", "--from", "82aeb5bd-6958-4a89-8197-eacab758acce")))
|
||||
|
||||
expected_json = {
|
||||
"KAMAL_REGISTRY_PASSWORD"=>"some_password",
|
||||
"MY_OTHER_SECRET"=>"my=wierd\"secret"
|
||||
}
|
||||
|
||||
assert_equal expected_json, json
|
||||
end
|
||||
|
||||
test "fetch item" do
|
||||
stub_ticks.with("bws --version 2> /dev/null")
|
||||
stub_login
|
||||
stub_ticks
|
||||
.with("bws secret get -o env 82aeb5bd-6958-4a89-8197-eacab758acce")
|
||||
.returns("KAMAL_REGISTRY_PASSWORD=\"some_password\"")
|
||||
.with("bws secret get 82aeb5bd-6958-4a89-8197-eacab758acce")
|
||||
.returns(<<~JSON)
|
||||
{
|
||||
"key": "KAMAL_REGISTRY_PASSWORD",
|
||||
"value": "some_password"
|
||||
}
|
||||
JSON
|
||||
|
||||
expected = '{"KAMAL_REGISTRY_PASSWORD":"some_password"}'
|
||||
actual = shellunescape(run_command("fetch", "82aeb5bd-6958-4a89-8197-eacab758acce"))
|
||||
assert_equal expected, actual
|
||||
json = JSON.parse(shellunescape(run_command("fetch", "82aeb5bd-6958-4a89-8197-eacab758acce")))
|
||||
expected_json = {
|
||||
"KAMAL_REGISTRY_PASSWORD"=>"some_password"
|
||||
}
|
||||
|
||||
assert_equal expected_json, json
|
||||
end
|
||||
|
||||
test "fetch with multiple items" do
|
||||
stub_ticks.with("bws --version 2> /dev/null")
|
||||
stub_login
|
||||
stub_ticks
|
||||
.with("bws secret get -o env 82aeb5bd-6958-4a89-8197-eacab758acce")
|
||||
.returns("KAMAL_REGISTRY_PASSWORD=\"some_password\"")
|
||||
.with("bws secret get 82aeb5bd-6958-4a89-8197-eacab758acce")
|
||||
.returns(<<~JSON)
|
||||
{
|
||||
"key": "KAMAL_REGISTRY_PASSWORD",
|
||||
"value": "some_password"
|
||||
}
|
||||
JSON
|
||||
stub_ticks
|
||||
.with("bws secret get -o env 6f8cdf27-de2b-4c77-a35d-07df8050e332")
|
||||
.returns("MY_OTHER_SECRET=\"my=weird\"secret\"")
|
||||
.with("bws secret get 6f8cdf27-de2b-4c77-a35d-07df8050e332")
|
||||
.returns(<<~JSON)
|
||||
{
|
||||
"key": "MY_OTHER_SECRET",
|
||||
"value": "my=wierd\\"secret"
|
||||
}
|
||||
JSON
|
||||
|
||||
expected = '{"KAMAL_REGISTRY_PASSWORD":"some_password","MY_OTHER_SECRET":"my\=weird\"secret"}'
|
||||
actual = shellunescape(run_command("fetch", "82aeb5bd-6958-4a89-8197-eacab758acce", "6f8cdf27-de2b-4c77-a35d-07df8050e332"))
|
||||
assert_equal expected, actual
|
||||
json = JSON.parse(shellunescape(run_command("fetch", "82aeb5bd-6958-4a89-8197-eacab758acce", "6f8cdf27-de2b-4c77-a35d-07df8050e332")))
|
||||
expected_json = {
|
||||
"KAMAL_REGISTRY_PASSWORD"=>"some_password",
|
||||
"MY_OTHER_SECRET"=>"my=wierd\"secret"
|
||||
}
|
||||
|
||||
assert_equal expected_json, json
|
||||
end
|
||||
|
||||
test "fetch all empty" do
|
||||
stub_ticks.with("bws --version 2> /dev/null")
|
||||
stub_login
|
||||
stub_ticks_with("bws secret list -o env", succeed: false).returns("Error:\n0: Received error message from server")
|
||||
stub_ticks_with("bws secret list", succeed: false).returns("Error:\n0: Received error message from server")
|
||||
|
||||
error = assert_raises RuntimeError do
|
||||
(shellunescape(run_command("fetch", "all")))
|
||||
@@ -76,8 +130,8 @@ class BitwardenSecretsManagerAdapterTest < SecretAdapterTestCase
|
||||
test "fetch nonexistent item" do
|
||||
stub_ticks.with("bws --version 2> /dev/null")
|
||||
stub_login
|
||||
stub_ticks_with("bws secret get -o env 82aeb5bd-6958-4a89-8197-eacab758acce", succeed: false)
|
||||
.returns("ERROR (RuntimeError): Could not read 82aeb5bd-6958-4a89-8197-eacab758acce from Bitwarden Secrets Manager")
|
||||
stub_ticks_with("bws secret get 82aeb5bd-6958-4a89-8197-eacab758acce", succeed: false)
|
||||
.returns("Error:\n0: Received error message from server")
|
||||
|
||||
error = assert_raises RuntimeError do
|
||||
(shellunescape(run_command("fetch", "82aeb5bd-6958-4a89-8197-eacab758acce")))
|
||||
@@ -85,9 +139,29 @@ class BitwardenSecretsManagerAdapterTest < SecretAdapterTestCase
|
||||
assert_equal("Could not read 82aeb5bd-6958-4a89-8197-eacab758acce from Bitwarden Secrets Manager", error.message)
|
||||
end
|
||||
|
||||
test "fetch item with linebreak in value" do
|
||||
stub_ticks.with("bws --version 2> /dev/null")
|
||||
stub_login
|
||||
stub_ticks
|
||||
.with("bws secret get 82aeb5bd-6958-4a89-8197-eacab758acce")
|
||||
.returns(<<~JSON)
|
||||
{
|
||||
"key": "SSH_PRIVATE_KEY",
|
||||
"value": "some_key\\nwith_linebreak"
|
||||
}
|
||||
JSON
|
||||
|
||||
json = JSON.parse(shellunescape(run_command("fetch", "82aeb5bd-6958-4a89-8197-eacab758acce")))
|
||||
expected_json = {
|
||||
"SSH_PRIVATE_KEY"=>"some_key\nwith_linebreak"
|
||||
}
|
||||
|
||||
assert_equal expected_json, json
|
||||
end
|
||||
|
||||
test "fetch with no access token" do
|
||||
stub_ticks.with("bws --version 2> /dev/null")
|
||||
stub_ticks_with("bws run 'echo OK'", succeed: false)
|
||||
stub_ticks_with("bws project list", succeed: false)
|
||||
|
||||
error = assert_raises RuntimeError do
|
||||
(shellunescape(run_command("fetch", "all")))
|
||||
@@ -106,7 +180,7 @@ class BitwardenSecretsManagerAdapterTest < SecretAdapterTestCase
|
||||
|
||||
private
|
||||
def stub_login
|
||||
stub_ticks.with("bws run 'echo OK'").returns("OK")
|
||||
stub_ticks.with("bws project list").returns("OK")
|
||||
end
|
||||
|
||||
def run_command(*command)
|
||||
|
||||
@@ -6,7 +6,7 @@ class SecretsOnePasswordAdapterTest < SecretAdapterTestCase
|
||||
stub_ticks.with("op account get --account myaccount 2> /dev/null")
|
||||
|
||||
stub_ticks
|
||||
.with("op item get myitem --vault \"myvault\" --fields \"label=section.SECRET1,label=section.SECRET2,label=section2.SECRET3\" --format \"json\" --account \"myaccount\"")
|
||||
.with("op item get myitem --vault \"myvault\" --format \"json\" --account \"myaccount\" --fields \"label=section.SECRET1,label=section.SECRET2,label=section2.SECRET3\"")
|
||||
.returns(<<~JSON)
|
||||
[
|
||||
{
|
||||
@@ -61,7 +61,7 @@ class SecretsOnePasswordAdapterTest < SecretAdapterTestCase
|
||||
stub_ticks.with("op account get --account myaccount 2> /dev/null")
|
||||
|
||||
stub_ticks
|
||||
.with("op item get myitem --vault \"myvault\" --fields \"label=section.SECRET1,label=section.SECRET2\" --format \"json\" --account \"myaccount\"")
|
||||
.with("op item get myitem --vault \"myvault\" --format \"json\" --account \"myaccount\" --fields \"label=section.SECRET1,label=section.SECRET2\"")
|
||||
.returns(<<~JSON)
|
||||
[
|
||||
{
|
||||
@@ -90,7 +90,7 @@ class SecretsOnePasswordAdapterTest < SecretAdapterTestCase
|
||||
JSON
|
||||
|
||||
stub_ticks
|
||||
.with("op item get myitem2 --vault \"myvault\" --fields \"label=section2.SECRET3\" --format \"json\" --account \"myaccount\"")
|
||||
.with("op item get myitem2 --vault \"myvault\" --format \"json\" --account \"myaccount\" --fields \"label=section2.SECRET3\"")
|
||||
.returns(<<~JSON)
|
||||
{
|
||||
"id": "aaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
@@ -116,6 +116,63 @@ class SecretsOnePasswordAdapterTest < SecretAdapterTestCase
|
||||
assert_equal expected_json, json
|
||||
end
|
||||
|
||||
test "fetch all fields" do
|
||||
stub_ticks.with("op --version 2> /dev/null")
|
||||
stub_ticks.with("op account get --account myaccount 2> /dev/null")
|
||||
|
||||
stub_ticks
|
||||
.with("op item get myitem --vault \"myvault\" --format \"json\" --account \"myaccount\"")
|
||||
.returns(<<~JSON)
|
||||
{
|
||||
"id": "ucbtiii777",
|
||||
"title": "A title",
|
||||
"version": 45,
|
||||
"vault": {
|
||||
"id": "vu7ki98do",
|
||||
"name": "Vault"
|
||||
},
|
||||
"category": "LOGIN",
|
||||
"last_edited_by": "ABCT3684BC",
|
||||
"created_at": "2025-05-22T06:47:01Z",
|
||||
"updated_at": "2025-05-22T00:36:48.02598-07:00",
|
||||
"additional_information": "—",
|
||||
"fields": [
|
||||
{
|
||||
"id": "aaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"section": {
|
||||
"id": "cccccccccccccccccccccccccc",
|
||||
"label": "section"
|
||||
},
|
||||
"type": "CONCEALED",
|
||||
"label": "SECRET1",
|
||||
"value": "VALUE1",
|
||||
"reference": "op://myvault/myitem/section/SECRET1"
|
||||
},
|
||||
{
|
||||
"id": "bbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
"section": {
|
||||
"id": "cccccccccccccccccccccccccc",
|
||||
"label": "section"
|
||||
},
|
||||
"type": "CONCEALED",
|
||||
"label": "SECRET2",
|
||||
"value": "VALUE2",
|
||||
"reference": "op://myvault/myitem/section/SECRET2"
|
||||
}
|
||||
]
|
||||
}
|
||||
JSON
|
||||
|
||||
json = JSON.parse(shellunescape(run_command("fetch", "--from", "op://myvault/myitem")))
|
||||
|
||||
expected_json = {
|
||||
"myvault/myitem/section/SECRET1"=>"VALUE1",
|
||||
"myvault/myitem/section/SECRET2"=>"VALUE2"
|
||||
}
|
||||
|
||||
assert_equal expected_json, json
|
||||
end
|
||||
|
||||
test "fetch with signin, no session" do
|
||||
stub_ticks.with("op --version 2> /dev/null")
|
||||
|
||||
@@ -123,7 +180,7 @@ class SecretsOnePasswordAdapterTest < SecretAdapterTestCase
|
||||
stub_ticks_with("op signin --account \"myaccount\" --force --raw", succeed: true).returns("")
|
||||
|
||||
stub_ticks
|
||||
.with("op item get myitem --vault \"myvault\" --fields \"label=section.SECRET1\" --format \"json\" --account \"myaccount\"")
|
||||
.with("op item get myitem --vault \"myvault\" --format \"json\" --account \"myaccount\" --fields \"label=section.SECRET1\"")
|
||||
.returns(single_item_json)
|
||||
|
||||
json = JSON.parse(shellunescape(run_command("fetch", "--from", "op://myvault/myitem", "section/SECRET1")))
|
||||
@@ -142,7 +199,7 @@ class SecretsOnePasswordAdapterTest < SecretAdapterTestCase
|
||||
stub_ticks_with("op signin --account \"myaccount\" --force --raw", succeed: true).returns("1234567890")
|
||||
|
||||
stub_ticks
|
||||
.with("op item get myitem --vault \"myvault\" --fields \"label=section.SECRET1\" --format \"json\" --account \"myaccount\" --session \"1234567890\"")
|
||||
.with("op item get myitem --vault \"myvault\" --format \"json\" --account \"myaccount\" --session \"1234567890\" --fields \"label=section.SECRET1\"")
|
||||
.returns(single_item_json)
|
||||
|
||||
json = JSON.parse(shellunescape(run_command("fetch", "--from", "op://myvault/myitem", "section/SECRET1")))
|
||||
|
||||
474
test/secrets/passbolt_adapter.rb
Normal file
474
test/secrets/passbolt_adapter.rb
Normal file
@@ -0,0 +1,474 @@
|
||||
require "test_helper"
|
||||
|
||||
class PassboltAdapterTest < SecretAdapterTestCase
|
||||
setup do
|
||||
`true` # Ensure $? is 0
|
||||
end
|
||||
|
||||
test "fetch" do
|
||||
stub_ticks_with("passbolt --version 2> /dev/null", succeed: true)
|
||||
stub_ticks.with("passbolt verify 2> /dev/null", succeed: true)
|
||||
|
||||
stub_ticks
|
||||
.with("passbolt list resources --filter 'Name == \"SECRET1\" || Name == \"FSECRET1\" || Name == \"FSECRET2\"' --json")
|
||||
.returns(<<~JSON)
|
||||
[
|
||||
{
|
||||
"id": "4c116996-f6d0-4342-9572-0d676f75b3ac",
|
||||
"folder_parent_id": "",
|
||||
"name": "FSECRET1",
|
||||
"username": "",
|
||||
"uri": "",
|
||||
"password": "fsecret1",
|
||||
"description": "",
|
||||
"created_timestamp": "2025-02-21T06:04:29Z",
|
||||
"modified_timestamp": "2025-02-21T06:04:29Z"
|
||||
},
|
||||
{
|
||||
"id": "62949b26-4957-43fe-9523-294d66861499",
|
||||
"folder_parent_id": "",
|
||||
"name": "FSECRET2",
|
||||
"username": "",
|
||||
"uri": "",
|
||||
"password": "fsecret2",
|
||||
"description": "",
|
||||
"created_timestamp": "2025-02-21T06:04:34Z",
|
||||
"modified_timestamp": "2025-02-21T06:04:34Z"
|
||||
},
|
||||
{
|
||||
"id": "dd32963c-0db5-4303-a6fc-22c5229dabef",
|
||||
"folder_parent_id": "",
|
||||
"name": "SECRET1",
|
||||
"username": "",
|
||||
"uri": "",
|
||||
"password": "secret1",
|
||||
"description": "",
|
||||
"created_timestamp": "2025-02-21T06:04:23Z",
|
||||
"modified_timestamp": "2025-02-21T06:04:23Z"
|
||||
}
|
||||
]
|
||||
JSON
|
||||
|
||||
json = JSON.parse(
|
||||
shellunescape run_command("fetch", "SECRET1", "FSECRET1", "FSECRET2")
|
||||
)
|
||||
|
||||
expected_json = {
|
||||
"SECRET1"=>"secret1",
|
||||
"FSECRET1"=>"fsecret1",
|
||||
"FSECRET2"=>"fsecret2"
|
||||
}
|
||||
|
||||
assert_equal expected_json, json
|
||||
end
|
||||
|
||||
test "fetch with --from" do
|
||||
stub_ticks_with("passbolt --version 2> /dev/null", succeed: true)
|
||||
stub_ticks.with("passbolt verify 2> /dev/null", succeed: true)
|
||||
|
||||
stub_ticks
|
||||
.with("passbolt list folders --filter 'Name == \"my-project\"' --json")
|
||||
.returns(<<~JSON)
|
||||
[
|
||||
{
|
||||
"id": "dcbe0e39-42d8-42db-9637-8256b9f2f8e3",
|
||||
"folder_parent_id": "",
|
||||
"name": "my-project",
|
||||
"created_timestamp": "2025-02-21T19:52:50Z",
|
||||
"modified_timestamp": "2025-02-21T19:52:50Z"
|
||||
}
|
||||
]
|
||||
JSON
|
||||
|
||||
stub_ticks
|
||||
.with("passbolt list resources --filter '(Name == \"SECRET1\" && FolderParentID == \"dcbe0e39-42d8-42db-9637-8256b9f2f8e3\") || (Name == \"FSECRET1\" && FolderParentID == \"dcbe0e39-42d8-42db-9637-8256b9f2f8e3\") || (Name == \"FSECRET2\" && FolderParentID == \"dcbe0e39-42d8-42db-9637-8256b9f2f8e3\")' --folder dcbe0e39-42d8-42db-9637-8256b9f2f8e3 --json")
|
||||
.returns(<<~JSON)
|
||||
[
|
||||
{
|
||||
"id": "4c116996-f6d0-4342-9572-0d676f75b3ac",
|
||||
"folder_parent_id": "dcbe0e39-42d8-42db-9637-8256b9f2f8e3",
|
||||
"name": "FSECRET1",
|
||||
"username": "",
|
||||
"uri": "",
|
||||
"password": "fsecret1",
|
||||
"description": "",
|
||||
"created_timestamp": "2025-02-21T06:04:29Z",
|
||||
"modified_timestamp": "2025-02-21T06:04:29Z"
|
||||
},
|
||||
{
|
||||
"id": "62949b26-4957-43fe-9523-294d66861499",
|
||||
"folder_parent_id": "dcbe0e39-42d8-42db-9637-8256b9f2f8e3",
|
||||
"name": "FSECRET2",
|
||||
"username": "",
|
||||
"uri": "",
|
||||
"password": "fsecret2",
|
||||
"description": "",
|
||||
"created_timestamp": "2025-02-21T06:04:34Z",
|
||||
"modified_timestamp": "2025-02-21T06:04:34Z"
|
||||
},
|
||||
{
|
||||
"id": "dd32963c-0db5-4303-a6fc-22c5229dabef",
|
||||
"folder_parent_id": "dcbe0e39-42d8-42db-9637-8256b9f2f8e3",
|
||||
"name": "SECRET1",
|
||||
"username": "",
|
||||
"uri": "",
|
||||
"password": "secret1",
|
||||
"description": "",
|
||||
"created_timestamp": "2025-02-21T06:04:23Z",
|
||||
"modified_timestamp": "2025-02-21T06:04:23Z"
|
||||
}
|
||||
]
|
||||
JSON
|
||||
|
||||
json = JSON.parse(
|
||||
shellunescape run_command("fetch", "--from", "my-project", "SECRET1", "FSECRET1", "FSECRET2")
|
||||
)
|
||||
|
||||
expected_json = {
|
||||
"SECRET1"=>"secret1",
|
||||
"FSECRET1"=>"fsecret1",
|
||||
"FSECRET2"=>"fsecret2"
|
||||
}
|
||||
|
||||
assert_equal expected_json, json
|
||||
end
|
||||
|
||||
test "fetch with folder in secret" do
|
||||
stub_ticks_with("passbolt --version 2> /dev/null", succeed: true)
|
||||
stub_ticks.with("passbolt verify 2> /dev/null", succeed: true)
|
||||
|
||||
stub_ticks
|
||||
.with("passbolt list folders --filter 'Name == \"my-project\"' --json")
|
||||
.returns(<<~JSON)
|
||||
[
|
||||
{
|
||||
"id": "dcbe0e39-42d8-42db-9637-8256b9f2f8e3",
|
||||
"folder_parent_id": "",
|
||||
"name": "my-project",
|
||||
"created_timestamp": "2025-02-21T19:52:50Z",
|
||||
"modified_timestamp": "2025-02-21T19:52:50Z"
|
||||
}
|
||||
]
|
||||
JSON
|
||||
|
||||
stub_ticks
|
||||
.with("passbolt list resources --filter '(Name == \"SECRET1\" && FolderParentID == \"dcbe0e39-42d8-42db-9637-8256b9f2f8e3\") || (Name == \"FSECRET1\" && FolderParentID == \"dcbe0e39-42d8-42db-9637-8256b9f2f8e3\") || (Name == \"FSECRET2\" && FolderParentID == \"dcbe0e39-42d8-42db-9637-8256b9f2f8e3\")' --folder dcbe0e39-42d8-42db-9637-8256b9f2f8e3 --json")
|
||||
.returns(<<~JSON)
|
||||
[
|
||||
{
|
||||
"id": "4c116996-f6d0-4342-9572-0d676f75b3ac",
|
||||
"folder_parent_id": "dcbe0e39-42d8-42db-9637-8256b9f2f8e3",
|
||||
"name": "FSECRET1",
|
||||
"username": "",
|
||||
"uri": "",
|
||||
"password": "fsecret1",
|
||||
"description": "",
|
||||
"created_timestamp": "2025-02-21T06:04:29Z",
|
||||
"modified_timestamp": "2025-02-21T06:04:29Z"
|
||||
},
|
||||
{
|
||||
"id": "62949b26-4957-43fe-9523-294d66861499",
|
||||
"folder_parent_id": "dcbe0e39-42d8-42db-9637-8256b9f2f8e3",
|
||||
"name": "FSECRET2",
|
||||
"username": "",
|
||||
"uri": "",
|
||||
"password": "fsecret2",
|
||||
"description": "",
|
||||
"created_timestamp": "2025-02-21T06:04:34Z",
|
||||
"modified_timestamp": "2025-02-21T06:04:34Z"
|
||||
},
|
||||
{
|
||||
"id": "dd32963c-0db5-4303-a6fc-22c5229dabef",
|
||||
"folder_parent_id": "dcbe0e39-42d8-42db-9637-8256b9f2f8e3",
|
||||
"name": "SECRET1",
|
||||
"username": "",
|
||||
"uri": "",
|
||||
"password": "secret1",
|
||||
"description": "",
|
||||
"created_timestamp": "2025-02-21T06:04:23Z",
|
||||
"modified_timestamp": "2025-02-21T06:04:23Z"
|
||||
}
|
||||
]
|
||||
JSON
|
||||
|
||||
json = JSON.parse(
|
||||
shellunescape run_command("fetch", "my-project/SECRET1", "my-project/FSECRET1", "my-project/FSECRET2")
|
||||
)
|
||||
|
||||
expected_json = {
|
||||
"SECRET1"=>"secret1",
|
||||
"FSECRET1"=>"fsecret1",
|
||||
"FSECRET2"=>"fsecret2"
|
||||
}
|
||||
|
||||
assert_equal expected_json, json
|
||||
end
|
||||
|
||||
test "fetch from multiple folders" do
|
||||
stub_ticks_with("passbolt --version 2> /dev/null", succeed: true)
|
||||
stub_ticks.with("passbolt verify 2> /dev/null", succeed: true)
|
||||
|
||||
stub_ticks
|
||||
.with("passbolt list folders --filter 'Name == \"my-project\" || Name == \"other-project\"' --json")
|
||||
.returns(<<~JSON)
|
||||
[
|
||||
{
|
||||
"id": "dcbe0e39-42d8-42db-9637-8256b9f2f8e3",
|
||||
"folder_parent_id": "",
|
||||
"name": "my-project",
|
||||
"created_timestamp": "2025-02-21T19:52:50Z",
|
||||
"modified_timestamp": "2025-02-21T19:52:50Z"
|
||||
},
|
||||
{
|
||||
"id": "14e11dd8-b279-4689-8bd9-fa33ebb527da",
|
||||
"folder_parent_id": "",
|
||||
"name": "other-project",
|
||||
"created_timestamp": "2025-02-21T20:00:29Z",
|
||||
"modified_timestamp": "2025-02-21T20:00:29Z"
|
||||
}
|
||||
]
|
||||
JSON
|
||||
|
||||
stub_ticks
|
||||
.with("passbolt list resources --filter '(Name == \"SECRET1\" && FolderParentID == \"dcbe0e39-42d8-42db-9637-8256b9f2f8e3\") || (Name == \"FSECRET1\" && FolderParentID == \"dcbe0e39-42d8-42db-9637-8256b9f2f8e3\") || (Name == \"FSECRET2\" && FolderParentID == \"14e11dd8-b279-4689-8bd9-fa33ebb527da\")' --folder dcbe0e39-42d8-42db-9637-8256b9f2f8e3 --folder 14e11dd8-b279-4689-8bd9-fa33ebb527da --json")
|
||||
.returns(<<~JSON)
|
||||
[
|
||||
{
|
||||
"id": "4c116996-f6d0-4342-9572-0d676f75b3ac",
|
||||
"folder_parent_id": "dcbe0e39-42d8-42db-9637-8256b9f2f8e3",
|
||||
"name": "FSECRET1",
|
||||
"username": "",
|
||||
"uri": "",
|
||||
"password": "fsecret1",
|
||||
"description": "",
|
||||
"created_timestamp": "2025-02-21T06:04:29Z",
|
||||
"modified_timestamp": "2025-02-21T06:04:29Z"
|
||||
},
|
||||
{
|
||||
"id": "62949b26-4957-43fe-9523-294d66861499",
|
||||
"folder_parent_id": "14e11dd8-b279-4689-8bd9-fa33ebb527da",
|
||||
"name": "FSECRET2",
|
||||
"username": "",
|
||||
"uri": "",
|
||||
"password": "fsecret2",
|
||||
"description": "",
|
||||
"created_timestamp": "2025-02-21T06:04:34Z",
|
||||
"modified_timestamp": "2025-02-21T06:04:34Z"
|
||||
},
|
||||
{
|
||||
"id": "dd32963c-0db5-4303-a6fc-22c5229dabef",
|
||||
"folder_parent_id": "dcbe0e39-42d8-42db-9637-8256b9f2f8e3",
|
||||
"name": "SECRET1",
|
||||
"username": "",
|
||||
"uri": "",
|
||||
"password": "secret1",
|
||||
"description": "",
|
||||
"created_timestamp": "2025-02-21T06:04:23Z",
|
||||
"modified_timestamp": "2025-02-21T06:04:23Z"
|
||||
}
|
||||
]
|
||||
JSON
|
||||
|
||||
json = JSON.parse(
|
||||
shellunescape run_command("fetch", "my-project/SECRET1", "my-project/FSECRET1", "other-project/FSECRET2")
|
||||
)
|
||||
|
||||
expected_json = {
|
||||
"SECRET1"=>"secret1",
|
||||
"FSECRET1"=>"fsecret1",
|
||||
"FSECRET2"=>"fsecret2"
|
||||
}
|
||||
|
||||
assert_equal expected_json, json
|
||||
end
|
||||
|
||||
test "fetch from nested folder" do
|
||||
stub_ticks_with("passbolt --version 2> /dev/null", succeed: true)
|
||||
stub_ticks.with("passbolt verify 2> /dev/null", succeed: true)
|
||||
|
||||
stub_ticks
|
||||
.with("passbolt list folders --filter 'Name == \"my-project\"' --json")
|
||||
.returns(<<~JSON)
|
||||
[
|
||||
{
|
||||
"id": "dcbe0e39-42d8-42db-9637-8256b9f2f8e3",
|
||||
"folder_parent_id": "",
|
||||
"name": "my-project",
|
||||
"created_timestamp": "2025-02-21T19:52:50Z",
|
||||
"modified_timestamp": "2025-02-21T19:52:50Z"
|
||||
}
|
||||
]
|
||||
JSON
|
||||
|
||||
stub_ticks
|
||||
.with("passbolt list folders --filter 'Name == \"subfolder\" && FolderParentID == \"dcbe0e39-42d8-42db-9637-8256b9f2f8e3\"' --json")
|
||||
.returns(<<~JSON)
|
||||
[
|
||||
{
|
||||
"id": "6a3f21fc-aa40-4ba9-852c-7477fdd0310d",
|
||||
"folder_parent_id": "dcbe0e39-42d8-42db-9637-8256b9f2f8e3",
|
||||
"name": "subfolder",
|
||||
"created_timestamp": "2025-02-21T19:52:50Z",
|
||||
"modified_timestamp": "2025-02-21T19:52:50Z"
|
||||
}
|
||||
]
|
||||
JSON
|
||||
|
||||
stub_ticks
|
||||
.with("passbolt list resources --filter '(Name == \"SECRET1\" && FolderParentID == \"6a3f21fc-aa40-4ba9-852c-7477fdd0310d\") || (Name == \"FSECRET1\" && FolderParentID == \"6a3f21fc-aa40-4ba9-852c-7477fdd0310d\") || (Name == \"FSECRET2\" && FolderParentID == \"6a3f21fc-aa40-4ba9-852c-7477fdd0310d\")' --folder dcbe0e39-42d8-42db-9637-8256b9f2f8e3 --folder 6a3f21fc-aa40-4ba9-852c-7477fdd0310d --json")
|
||||
.returns(<<~JSON)
|
||||
[
|
||||
{
|
||||
"id": "4c116996-f6d0-4342-9572-0d676f75b3ac",
|
||||
"folder_parent_id": "6a3f21fc-aa40-4ba9-852c-7477fdd0310d",
|
||||
"name": "FSECRET1",
|
||||
"username": "",
|
||||
"uri": "",
|
||||
"password": "fsecret1",
|
||||
"description": "",
|
||||
"created_timestamp": "2025-02-21T06:04:29Z",
|
||||
"modified_timestamp": "2025-02-21T06:04:29Z"
|
||||
},
|
||||
{
|
||||
"id": "62949b26-4957-43fe-9523-294d66861499",
|
||||
"folder_parent_id": "6a3f21fc-aa40-4ba9-852c-7477fdd0310d",
|
||||
"name": "FSECRET2",
|
||||
"username": "",
|
||||
"uri": "",
|
||||
"password": "fsecret2",
|
||||
"description": "",
|
||||
"created_timestamp": "2025-02-21T06:04:34Z",
|
||||
"modified_timestamp": "2025-02-21T06:04:34Z"
|
||||
},
|
||||
{
|
||||
"id": "dd32963c-0db5-4303-a6fc-22c5229dabef",
|
||||
"folder_parent_id": "6a3f21fc-aa40-4ba9-852c-7477fdd0310d",
|
||||
"name": "SECRET1",
|
||||
"username": "",
|
||||
"uri": "",
|
||||
"password": "secret1",
|
||||
"description": "",
|
||||
"created_timestamp": "2025-02-21T06:04:23Z",
|
||||
"modified_timestamp": "2025-02-21T06:04:23Z"
|
||||
}
|
||||
]
|
||||
JSON
|
||||
|
||||
json = JSON.parse(
|
||||
shellunescape run_command("fetch", "--from", "my-project/subfolder", "SECRET1", "FSECRET1", "FSECRET2")
|
||||
)
|
||||
|
||||
expected_json = {
|
||||
"SECRET1"=>"secret1",
|
||||
"FSECRET1"=>"fsecret1",
|
||||
"FSECRET2"=>"fsecret2"
|
||||
}
|
||||
|
||||
assert_equal expected_json, json
|
||||
end
|
||||
|
||||
test "fetch from nested folder in secret" do
|
||||
stub_ticks_with("passbolt --version 2> /dev/null", succeed: true)
|
||||
stub_ticks.with("passbolt verify 2> /dev/null", succeed: true)
|
||||
|
||||
stub_ticks
|
||||
.with("passbolt list folders --filter 'Name == \"my-project\"' --json")
|
||||
.returns(<<~JSON)
|
||||
[
|
||||
{
|
||||
"id": "dcbe0e39-42d8-42db-9637-8256b9f2f8e3",
|
||||
"folder_parent_id": "",
|
||||
"name": "my-project",
|
||||
"created_timestamp": "2025-02-21T19:52:50Z",
|
||||
"modified_timestamp": "2025-02-21T19:52:50Z"
|
||||
}
|
||||
]
|
||||
JSON
|
||||
|
||||
stub_ticks
|
||||
.with("passbolt list folders --filter 'Name == \"subfolder\" && FolderParentID == \"dcbe0e39-42d8-42db-9637-8256b9f2f8e3\"' --json")
|
||||
.returns(<<~JSON)
|
||||
[
|
||||
{
|
||||
"id": "6a3f21fc-aa40-4ba9-852c-7477fdd0310d",
|
||||
"folder_parent_id": "dcbe0e39-42d8-42db-9637-8256b9f2f8e3",
|
||||
"name": "subfolder",
|
||||
"created_timestamp": "2025-02-21T19:52:50Z",
|
||||
"modified_timestamp": "2025-02-21T19:52:50Z"
|
||||
}
|
||||
]
|
||||
JSON
|
||||
|
||||
stub_ticks
|
||||
.with("passbolt list resources --filter '(Name == \"SECRET1\" && FolderParentID == \"6a3f21fc-aa40-4ba9-852c-7477fdd0310d\") || (Name == \"FSECRET1\" && FolderParentID == \"6a3f21fc-aa40-4ba9-852c-7477fdd0310d\") || (Name == \"FSECRET2\" && FolderParentID == \"6a3f21fc-aa40-4ba9-852c-7477fdd0310d\")' --folder dcbe0e39-42d8-42db-9637-8256b9f2f8e3 --folder 6a3f21fc-aa40-4ba9-852c-7477fdd0310d --json")
|
||||
.returns(<<~JSON)
|
||||
[
|
||||
{
|
||||
"id": "4c116996-f6d0-4342-9572-0d676f75b3ac",
|
||||
"folder_parent_id": "6a3f21fc-aa40-4ba9-852c-7477fdd0310d",
|
||||
"name": "FSECRET1",
|
||||
"username": "",
|
||||
"uri": "",
|
||||
"password": "fsecret1",
|
||||
"description": "",
|
||||
"created_timestamp": "2025-02-21T06:04:29Z",
|
||||
"modified_timestamp": "2025-02-21T06:04:29Z"
|
||||
},
|
||||
{
|
||||
"id": "62949b26-4957-43fe-9523-294d66861499",
|
||||
"folder_parent_id": "6a3f21fc-aa40-4ba9-852c-7477fdd0310d",
|
||||
"name": "FSECRET2",
|
||||
"username": "",
|
||||
"uri": "",
|
||||
"password": "fsecret2",
|
||||
"description": "",
|
||||
"created_timestamp": "2025-02-21T06:04:34Z",
|
||||
"modified_timestamp": "2025-02-21T06:04:34Z"
|
||||
},
|
||||
{
|
||||
"id": "dd32963c-0db5-4303-a6fc-22c5229dabef",
|
||||
"folder_parent_id": "6a3f21fc-aa40-4ba9-852c-7477fdd0310d",
|
||||
"name": "SECRET1",
|
||||
"username": "",
|
||||
"uri": "",
|
||||
"password": "secret1",
|
||||
"description": "",
|
||||
"created_timestamp": "2025-02-21T06:04:23Z",
|
||||
"modified_timestamp": "2025-02-21T06:04:23Z"
|
||||
}
|
||||
]
|
||||
JSON
|
||||
|
||||
json = JSON.parse(
|
||||
shellunescape run_command("fetch", "my-project/subfolder/SECRET1", "my-project/subfolder/FSECRET1", "my-project/subfolder/FSECRET2")
|
||||
)
|
||||
|
||||
expected_json = {
|
||||
"SECRET1"=>"secret1",
|
||||
"FSECRET1"=>"fsecret1",
|
||||
"FSECRET2"=>"fsecret2"
|
||||
}
|
||||
|
||||
assert_equal expected_json, json
|
||||
end
|
||||
|
||||
test "fetch without CLI installed" do
|
||||
stub_ticks_with("passbolt --version 2> /dev/null", succeed: false)
|
||||
|
||||
error = assert_raises RuntimeError do
|
||||
JSON.parse(shellunescape(run_command("fetch", "HOST", "PORT")))
|
||||
end
|
||||
|
||||
assert_equal "Passbolt CLI is not installed", error.message
|
||||
end
|
||||
|
||||
private
|
||||
def run_command(*command)
|
||||
stdouted do
|
||||
Kamal::Cli::Secrets.start \
|
||||
[ *command,
|
||||
"-c", "test/fixtures/deploy_with_accessories.yml",
|
||||
"--adapter", "passbolt" ]
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -3,6 +3,7 @@ require "active_support/test_case"
|
||||
require "active_support/testing/autorun"
|
||||
require "active_support/testing/stream"
|
||||
require "rails/test_unit/line_filtering"
|
||||
require "pty"
|
||||
require "debug"
|
||||
require "mocha/minitest" # using #stubs that can alter returns
|
||||
require "minitest/autorun" # using #stub that take args
|
||||
@@ -48,6 +49,27 @@ class ActiveSupport::TestCase
|
||||
capture(:stderr) { yield }.strip
|
||||
end
|
||||
|
||||
def stub_stdin_tty
|
||||
PTY.open do |master, slave|
|
||||
stub_stdin(master) { yield }
|
||||
end
|
||||
end
|
||||
|
||||
def stub_stdin_file
|
||||
File.open("/dev/null", "r") do |file|
|
||||
stub_stdin(file) { yield }
|
||||
end
|
||||
end
|
||||
|
||||
def stub_stdin(io)
|
||||
original_stdin = STDIN.dup
|
||||
STDIN.reopen(io)
|
||||
yield
|
||||
ensure
|
||||
STDIN.reopen(original_stdin)
|
||||
original_stdin.close
|
||||
end
|
||||
|
||||
def with_test_secrets(**files)
|
||||
setup_test_secrets(**files)
|
||||
yield
|
||||
|
||||
Reference in New Issue
Block a user