Compare commits
70 Commits
revert-905
...
proxy-expe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0f5997aef | ||
|
|
85f62ebc22 | ||
|
|
e0df051756 | ||
|
|
0b22fea8a9 | ||
|
|
f088e0cb64 | ||
|
|
9b673c2114 | ||
|
|
e0d336dc11 | ||
|
|
1422ecaeb8 | ||
|
|
f8de2898c9 | ||
|
|
0cd1a4fb07 | ||
|
|
4381f3bc5f | ||
|
|
35de0891c0 | ||
|
|
fb9c8f16f1 | ||
|
|
d92b3628f4 | ||
|
|
4732543eca | ||
|
|
e58d33b389 | ||
|
|
97b842fcee | ||
|
|
98eb38f11c | ||
|
|
805fc1554e | ||
|
|
61715e0a4b | ||
|
|
debdf00cca | ||
|
|
9089c41f30 | ||
|
|
c9946808b1 | ||
|
|
deb2a6d298 | ||
|
|
0cb69a84f5 | ||
|
|
aa630f156a | ||
|
|
63d0b5ddfa | ||
|
|
06f4caa866 | ||
|
|
5aa3d1aeb0 | ||
|
|
a4d668cd39 | ||
|
|
7156c80f34 | ||
|
|
aed2ef99d0 | ||
|
|
57cbf7cdb5 | ||
|
|
b99c044327 | ||
|
|
8ad6a0ed16 | ||
|
|
8b62e2694a | ||
|
|
be1df4356a | ||
|
|
8210e8e768 | ||
|
|
9b96ef2412 | ||
|
|
1522d94ac9 | ||
|
|
a68294c384 | ||
|
|
31a347c285 | ||
|
|
3d502ab12d | ||
|
|
5226d52f8a | ||
|
|
9deb8af4a0 | ||
|
|
068aaa0bd0 | ||
|
|
a726a86a17 | ||
|
|
b2e1a4d4c1 | ||
|
|
9ade79fc84 | ||
|
|
79731da619 | ||
|
|
0ae8046905 | ||
|
|
d5ecca0fd4 | ||
|
|
0c6a593554 | ||
|
|
3f37fea7c3 | ||
|
|
7daaabd4d4 | ||
|
|
fcdef5fa06 | ||
|
|
5480b40ba3 | ||
|
|
1d0e81b00a | ||
|
|
5910249d02 | ||
|
|
b464c4fd4a | ||
|
|
56754fe40c | ||
|
|
6a06efc9d9 | ||
|
|
5c4c33e0a8 | ||
|
|
0b5506f6f2 | ||
|
|
a2549b1f60 | ||
|
|
9b9e60ec7f | ||
|
|
e557eea79c | ||
|
|
d7e785cd36 | ||
|
|
5cda3086c4 | ||
|
|
362f5d00f6 |
105
Gemfile.lock
105
Gemfile.lock
@@ -6,7 +6,7 @@ PATH
|
|||||||
base64 (~> 0.2)
|
base64 (~> 0.2)
|
||||||
bcrypt_pbkdf (~> 1.0)
|
bcrypt_pbkdf (~> 1.0)
|
||||||
concurrent-ruby (~> 1.2)
|
concurrent-ruby (~> 1.2)
|
||||||
dotenv (~> 2.8)
|
dotenv (~> 3.1)
|
||||||
ed25519 (~> 1.2)
|
ed25519 (~> 1.2)
|
||||||
net-ssh (~> 7.0)
|
net-ssh (~> 7.0)
|
||||||
sshkit (>= 1.23.0, < 2.0)
|
sshkit (>= 1.23.0, < 2.0)
|
||||||
@@ -16,9 +16,9 @@ PATH
|
|||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
actionpack (7.1.2)
|
actionpack (7.1.3.4)
|
||||||
actionview (= 7.1.2)
|
actionview (= 7.1.3.4)
|
||||||
activesupport (= 7.1.2)
|
activesupport (= 7.1.3.4)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
racc
|
racc
|
||||||
rack (>= 2.2.4)
|
rack (>= 2.2.4)
|
||||||
@@ -26,13 +26,13 @@ GEM
|
|||||||
rack-test (>= 0.6.3)
|
rack-test (>= 0.6.3)
|
||||||
rails-dom-testing (~> 2.2)
|
rails-dom-testing (~> 2.2)
|
||||||
rails-html-sanitizer (~> 1.6)
|
rails-html-sanitizer (~> 1.6)
|
||||||
actionview (7.1.2)
|
actionview (7.1.3.4)
|
||||||
activesupport (= 7.1.2)
|
activesupport (= 7.1.3.4)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
erubi (~> 1.11)
|
erubi (~> 1.11)
|
||||||
rails-dom-testing (~> 2.2)
|
rails-dom-testing (~> 2.2)
|
||||||
rails-html-sanitizer (~> 1.6)
|
rails-html-sanitizer (~> 1.6)
|
||||||
activesupport (7.1.2)
|
activesupport (7.1.3.4)
|
||||||
base64
|
base64
|
||||||
bigdecimal
|
bigdecimal
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
@@ -44,54 +44,55 @@ GEM
|
|||||||
tzinfo (~> 2.0)
|
tzinfo (~> 2.0)
|
||||||
ast (2.4.2)
|
ast (2.4.2)
|
||||||
base64 (0.2.0)
|
base64 (0.2.0)
|
||||||
bcrypt_pbkdf (1.1.0)
|
bcrypt_pbkdf (1.1.1)
|
||||||
bigdecimal (3.1.5)
|
bcrypt_pbkdf (1.1.1-arm64-darwin)
|
||||||
builder (3.2.4)
|
bcrypt_pbkdf (1.1.1-x86_64-darwin)
|
||||||
concurrent-ruby (1.2.2)
|
bigdecimal (3.1.8)
|
||||||
|
builder (3.3.0)
|
||||||
|
concurrent-ruby (1.3.3)
|
||||||
connection_pool (2.4.1)
|
connection_pool (2.4.1)
|
||||||
crass (1.0.6)
|
crass (1.0.6)
|
||||||
debug (1.9.1)
|
debug (1.9.2)
|
||||||
irb (~> 1.10)
|
irb (~> 1.10)
|
||||||
reline (>= 0.3.8)
|
reline (>= 0.3.8)
|
||||||
dotenv (2.8.1)
|
dotenv (3.1.2)
|
||||||
drb (2.2.0)
|
drb (2.2.1)
|
||||||
ruby2_keywords
|
|
||||||
ed25519 (1.3.0)
|
ed25519 (1.3.0)
|
||||||
erubi (1.12.0)
|
erubi (1.13.0)
|
||||||
i18n (1.14.1)
|
i18n (1.14.5)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
io-console (0.7.1)
|
io-console (0.7.2)
|
||||||
irb (1.11.0)
|
irb (1.14.0)
|
||||||
rdoc
|
rdoc (>= 4.0.0)
|
||||||
reline (>= 0.3.8)
|
reline (>= 0.4.2)
|
||||||
json (2.7.1)
|
json (2.7.2)
|
||||||
language_server-protocol (3.17.0.3)
|
language_server-protocol (3.17.0.3)
|
||||||
loofah (2.22.0)
|
loofah (2.22.0)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.12.0)
|
nokogiri (>= 1.12.0)
|
||||||
minitest (5.20.0)
|
minitest (5.24.1)
|
||||||
mocha (2.1.0)
|
mocha (2.4.5)
|
||||||
ruby2_keywords (>= 0.0.5)
|
ruby2_keywords (>= 0.0.5)
|
||||||
mutex_m (0.2.0)
|
mutex_m (0.2.0)
|
||||||
net-scp (4.0.0)
|
net-scp (4.0.0)
|
||||||
net-ssh (>= 2.6.5, < 8.0.0)
|
net-ssh (>= 2.6.5, < 8.0.0)
|
||||||
net-sftp (4.0.0)
|
net-sftp (4.0.0)
|
||||||
net-ssh (>= 5.0.0, < 8.0.0)
|
net-ssh (>= 5.0.0, < 8.0.0)
|
||||||
net-ssh (7.2.1)
|
net-ssh (7.2.3)
|
||||||
nokogiri (1.16.0-arm64-darwin)
|
nokogiri (1.16.7-arm64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.16.0-x86_64-darwin)
|
nokogiri (1.16.7-x86_64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.16.0-x86_64-linux)
|
nokogiri (1.16.7-x86_64-linux)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
parallel (1.24.0)
|
parallel (1.25.1)
|
||||||
parser (3.3.0.5)
|
parser (3.3.4.0)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
racc
|
racc
|
||||||
psych (5.1.2)
|
psych (5.1.2)
|
||||||
stringio
|
stringio
|
||||||
racc (1.7.3)
|
racc (1.8.1)
|
||||||
rack (3.0.8)
|
rack (3.1.7)
|
||||||
rack-session (2.0.0)
|
rack-session (2.0.0)
|
||||||
rack (>= 3.0.0)
|
rack (>= 3.0.0)
|
||||||
rack-test (2.1.0)
|
rack-test (2.1.0)
|
||||||
@@ -106,42 +107,43 @@ GEM
|
|||||||
rails-html-sanitizer (1.6.0)
|
rails-html-sanitizer (1.6.0)
|
||||||
loofah (~> 2.21)
|
loofah (~> 2.21)
|
||||||
nokogiri (~> 1.14)
|
nokogiri (~> 1.14)
|
||||||
railties (7.1.2)
|
railties (7.1.3.4)
|
||||||
actionpack (= 7.1.2)
|
actionpack (= 7.1.3.4)
|
||||||
activesupport (= 7.1.2)
|
activesupport (= 7.1.3.4)
|
||||||
irb
|
irb
|
||||||
rackup (>= 1.0.0)
|
rackup (>= 1.0.0)
|
||||||
rake (>= 12.2)
|
rake (>= 12.2)
|
||||||
thor (~> 1.0, >= 1.2.2)
|
thor (~> 1.0, >= 1.2.2)
|
||||||
zeitwerk (~> 2.6)
|
zeitwerk (~> 2.6)
|
||||||
rainbow (3.1.1)
|
rainbow (3.1.1)
|
||||||
rake (13.1.0)
|
rake (13.2.1)
|
||||||
rdoc (6.6.2)
|
rdoc (6.7.0)
|
||||||
psych (>= 4.0.0)
|
psych (>= 4.0.0)
|
||||||
regexp_parser (2.9.0)
|
regexp_parser (2.9.2)
|
||||||
reline (0.4.2)
|
reline (0.5.9)
|
||||||
io-console (~> 0.5)
|
io-console (~> 0.5)
|
||||||
rexml (3.2.6)
|
rexml (3.3.4)
|
||||||
rubocop (1.62.1)
|
strscan
|
||||||
|
rubocop (1.65.1)
|
||||||
json (~> 2.3)
|
json (~> 2.3)
|
||||||
language_server-protocol (>= 3.17.0)
|
language_server-protocol (>= 3.17.0)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
parser (>= 3.3.0.2)
|
parser (>= 3.3.0.2)
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
regexp_parser (>= 1.8, < 3.0)
|
regexp_parser (>= 2.4, < 3.0)
|
||||||
rexml (>= 3.2.5, < 4.0)
|
rexml (>= 3.2.5, < 4.0)
|
||||||
rubocop-ast (>= 1.31.1, < 2.0)
|
rubocop-ast (>= 1.31.1, < 2.0)
|
||||||
ruby-progressbar (~> 1.7)
|
ruby-progressbar (~> 1.7)
|
||||||
unicode-display_width (>= 2.4.0, < 3.0)
|
unicode-display_width (>= 2.4.0, < 3.0)
|
||||||
rubocop-ast (1.31.2)
|
rubocop-ast (1.32.0)
|
||||||
parser (>= 3.3.0.4)
|
parser (>= 3.3.1.0)
|
||||||
rubocop-minitest (0.35.0)
|
rubocop-minitest (0.35.1)
|
||||||
rubocop (>= 1.61, < 2.0)
|
rubocop (>= 1.61, < 2.0)
|
||||||
rubocop-ast (>= 1.31.1, < 2.0)
|
rubocop-ast (>= 1.31.1, < 2.0)
|
||||||
rubocop-performance (1.20.2)
|
rubocop-performance (1.21.1)
|
||||||
rubocop (>= 1.48.1, < 2.0)
|
rubocop (>= 1.48.1, < 2.0)
|
||||||
rubocop-ast (>= 1.30.0, < 2.0)
|
rubocop-ast (>= 1.31.1, < 2.0)
|
||||||
rubocop-rails (2.24.0)
|
rubocop-rails (2.25.1)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
rack (>= 1.1)
|
rack (>= 1.1)
|
||||||
rubocop (>= 1.33.0, < 2.0)
|
rubocop (>= 1.33.0, < 2.0)
|
||||||
@@ -158,13 +160,14 @@ GEM
|
|||||||
net-scp (>= 1.1.2)
|
net-scp (>= 1.1.2)
|
||||||
net-sftp (>= 2.1.2)
|
net-sftp (>= 2.1.2)
|
||||||
net-ssh (>= 2.8.0)
|
net-ssh (>= 2.8.0)
|
||||||
stringio (3.1.0)
|
stringio (3.1.1)
|
||||||
thor (1.3.0)
|
strscan (3.1.0)
|
||||||
|
thor (1.3.1)
|
||||||
tzinfo (2.0.6)
|
tzinfo (2.0.6)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
unicode-display_width (2.5.0)
|
unicode-display_width (2.5.0)
|
||||||
webrick (1.8.1)
|
webrick (1.8.1)
|
||||||
zeitwerk (2.6.12)
|
zeitwerk (2.6.17)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
arm64-darwin
|
arm64-darwin
|
||||||
|
|||||||
1
bin/docs
1
bin/docs
@@ -24,6 +24,7 @@ DOCS = {
|
|||||||
"env" => "Environment variables",
|
"env" => "Environment variables",
|
||||||
"healthcheck" => "Healthchecks",
|
"healthcheck" => "Healthchecks",
|
||||||
"logging" => "Logging",
|
"logging" => "Logging",
|
||||||
|
"proxy" => "Proxy (Experimental)",
|
||||||
"registry" => "Docker Registry",
|
"registry" => "Docker Registry",
|
||||||
"role" => "Roles",
|
"role" => "Roles",
|
||||||
"servers" => "Servers",
|
"servers" => "Servers",
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ Gem::Specification.new do |spec|
|
|||||||
spec.add_dependency "sshkit", ">= 1.23.0", "< 2.0"
|
spec.add_dependency "sshkit", ">= 1.23.0", "< 2.0"
|
||||||
spec.add_dependency "net-ssh", "~> 7.0"
|
spec.add_dependency "net-ssh", "~> 7.0"
|
||||||
spec.add_dependency "thor", "~> 1.3"
|
spec.add_dependency "thor", "~> 1.3"
|
||||||
spec.add_dependency "dotenv", "~> 2.8"
|
spec.add_dependency "dotenv", "~> 3.1"
|
||||||
spec.add_dependency "zeitwerk", "~> 2.5"
|
spec.add_dependency "zeitwerk", "~> 2.5"
|
||||||
spec.add_dependency "ed25519", "~> 1.2"
|
spec.add_dependency "ed25519", "~> 1.2"
|
||||||
spec.add_dependency "bcrypt_pbkdf", "~> 1.0"
|
spec.add_dependency "bcrypt_pbkdf", "~> 1.0"
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ end
|
|||||||
require "active_support"
|
require "active_support"
|
||||||
require "zeitwerk"
|
require "zeitwerk"
|
||||||
require "yaml"
|
require "yaml"
|
||||||
|
require "tmpdir"
|
||||||
|
require "pathname"
|
||||||
|
|
||||||
loader = Zeitwerk::Loader.for_gem
|
loader = Zeitwerk::Loader.for_gem
|
||||||
loader.ignore(File.join(__dir__, "kamal", "sshkit_with_ext.rb"))
|
loader.ignore(File.join(__dir__, "kamal", "sshkit_with_ext.rb"))
|
||||||
loader.setup
|
loader.setup
|
||||||
loader.eager_load # We need all commands loaded.
|
loader.eager_load_namespace(Kamal::Cli) # We need all commands loaded.
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
module Kamal::Cli
|
module Kamal::Cli
|
||||||
|
class BootError < StandardError; end
|
||||||
class HookError < StandardError; end
|
class HookError < StandardError; end
|
||||||
class LockError < StandardError; end
|
class LockError < StandardError; end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
class Kamal::Cli::Accessory < Kamal::Cli::Base
|
class Kamal::Cli::Accessory < Kamal::Cli::Base
|
||||||
desc "boot [NAME]", "Boot new accessory service on host (use NAME=all to boot all accessories)"
|
desc "boot [NAME]", "Boot new accessory service on host (use NAME=all to boot all accessories)"
|
||||||
def boot(name, login: true)
|
def boot(name, prepare: true)
|
||||||
with_lock do
|
with_lock do
|
||||||
if name == "all"
|
if name == "all"
|
||||||
KAMAL.accessory_names.each { |accessory_name| boot(accessory_name) }
|
KAMAL.accessory_names.each { |accessory_name| boot(accessory_name) }
|
||||||
else
|
else
|
||||||
|
prepare(name) if prepare
|
||||||
|
|
||||||
with_accessory(name) do |accessory, hosts|
|
with_accessory(name) do |accessory, hosts|
|
||||||
directories(name)
|
directories(name)
|
||||||
upload(name)
|
upload(name)
|
||||||
|
|
||||||
on(hosts) do
|
on(hosts) do
|
||||||
execute *KAMAL.registry.login if login
|
|
||||||
execute *KAMAL.auditor.record("Booted #{name} accessory"), verbosity: :debug
|
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
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -55,15 +58,10 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
|
|||||||
if name == "all"
|
if name == "all"
|
||||||
KAMAL.accessory_names.each { |accessory_name| reboot(accessory_name) }
|
KAMAL.accessory_names.each { |accessory_name| reboot(accessory_name) }
|
||||||
else
|
else
|
||||||
with_accessory(name) do |accessory, hosts|
|
prepare(name)
|
||||||
on(hosts) do
|
|
||||||
execute *KAMAL.registry.login
|
|
||||||
end
|
|
||||||
|
|
||||||
stop(name)
|
stop(name)
|
||||||
remove_container(name)
|
remove_container(name)
|
||||||
boot(name, login: false)
|
boot(name, prepare: false)
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -95,12 +93,10 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
|
|||||||
desc "restart [NAME]", "Restart existing accessory container on host"
|
desc "restart [NAME]", "Restart existing accessory container on host"
|
||||||
def restart(name)
|
def restart(name)
|
||||||
with_lock do
|
with_lock do
|
||||||
with_accessory(name) do
|
|
||||||
stop(name)
|
stop(name)
|
||||||
start(name)
|
start(name)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
desc "details [NAME]", "Show details about accessory on host (use NAME=all to show all accessories)"
|
desc "details [NAME]", "Show details about accessory on host (use NAME=all to show all accessories)"
|
||||||
def details(name)
|
def details(name)
|
||||||
@@ -249,11 +245,20 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def remove_accessory(name)
|
def remove_accessory(name)
|
||||||
with_accessory(name) do
|
|
||||||
stop(name)
|
stop(name)
|
||||||
remove_container(name)
|
remove_container(name)
|
||||||
remove_image(name)
|
remove_image(name)
|
||||||
remove_service_directory(name)
|
remove_service_directory(name)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def prepare(name)
|
||||||
|
with_accessory(name) do |accessory, hosts|
|
||||||
|
on(hosts) do
|
||||||
|
execute *KAMAL.registry.login
|
||||||
|
execute *KAMAL.docker.create_network
|
||||||
|
rescue SSHKit::Command::Failed => e
|
||||||
|
raise unless e.message.include?("already exists")
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -38,8 +38,17 @@ class Kamal::Cli::App < Kamal::Cli::Base
|
|||||||
roles = KAMAL.roles_on(host)
|
roles = KAMAL.roles_on(host)
|
||||||
|
|
||||||
roles.each do |role|
|
roles.each do |role|
|
||||||
|
app = KAMAL.app(role: role, host: host)
|
||||||
execute *KAMAL.auditor.record("Started app version #{KAMAL.config.version}"), verbosity: :debug
|
execute *KAMAL.auditor.record("Started app version #{KAMAL.config.version}"), verbosity: :debug
|
||||||
execute *KAMAL.app(role: role, host: host).start, raise_on_non_zero_exit: false
|
execute *app.start, raise_on_non_zero_exit: false
|
||||||
|
|
||||||
|
if role.running_traefik? && KAMAL.proxy_host?(host)
|
||||||
|
version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
|
||||||
|
endpoint = capture_with_info(*app.container_id_for_version(version)).strip
|
||||||
|
raise Kamal::Cli::BootError, "Failed to get endpoint for #{role} on #{host}, did the container boot?" if endpoint.empty?
|
||||||
|
|
||||||
|
execute *KAMAL.proxy.deploy(role.container_prefix, target: endpoint)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -52,8 +61,18 @@ class Kamal::Cli::App < Kamal::Cli::Base
|
|||||||
roles = KAMAL.roles_on(host)
|
roles = KAMAL.roles_on(host)
|
||||||
|
|
||||||
roles.each do |role|
|
roles.each do |role|
|
||||||
|
app = KAMAL.app(role: role, host: host)
|
||||||
execute *KAMAL.auditor.record("Stopped app", role: role), verbosity: :debug
|
execute *KAMAL.auditor.record("Stopped app", role: role), verbosity: :debug
|
||||||
execute *KAMAL.app(role: role, host: host).stop, raise_on_non_zero_exit: false
|
|
||||||
|
if role.running_traefik? && KAMAL.proxy_host?(host)
|
||||||
|
version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
|
||||||
|
endpoint = capture_with_info(*app.container_id_for_version(version)).strip
|
||||||
|
if endpoint.present?
|
||||||
|
execute *KAMAL.proxy.remove(role.container_prefix, target: endpoint), raise_on_non_zero_exit: false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
execute *app.stop, raise_on_non_zero_exit: false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
class Kamal::Cli::App::Boot
|
class Kamal::Cli::App::Boot
|
||||||
attr_reader :host, :role, :version, :barrier, :sshkit
|
attr_reader :host, :role, :version, :barrier, :sshkit
|
||||||
delegate :execute, :capture_with_info, :capture_with_pretty_json, :info, :error, to: :sshkit
|
delegate :execute, :capture_with_info, :capture_with_pretty_json, :info, :error, :upload!, to: :sshkit
|
||||||
delegate :uses_cord?, :assets?, :running_traefik?, to: :role
|
delegate :uses_cord?, :assets?, :running_traefik?, to: :role
|
||||||
|
|
||||||
def initialize(host, role, sshkit, version, barrier)
|
def initialize(host, role, sshkit, version, barrier)
|
||||||
@@ -45,19 +45,33 @@ class Kamal::Cli::App::Boot
|
|||||||
|
|
||||||
def start_new_version
|
def start_new_version
|
||||||
audit "Booted app version #{version}"
|
audit "Booted app version #{version}"
|
||||||
|
|
||||||
execute *app.tie_cord(role.cord_host_file) if uses_cord?
|
|
||||||
hostname = "#{host.to_s[0...51].gsub(/\.+$/, '')}-#{SecureRandom.hex(6)}"
|
hostname = "#{host.to_s[0...51].gsub(/\.+$/, '')}-#{SecureRandom.hex(6)}"
|
||||||
|
|
||||||
|
execute *app.ensure_env_directory
|
||||||
|
upload! role.secrets_io(host), role.secrets_path, mode: "0600"
|
||||||
|
|
||||||
|
if proxy_host?
|
||||||
|
execute *app.run_for_proxy(hostname: hostname)
|
||||||
|
if running_traefik?
|
||||||
|
endpoint = capture_with_info(*app.container_id_for_version(version)).strip
|
||||||
|
raise Kamal::Cli::BootError, "Failed to get endpoint for #{role} on #{host}, did the container boot?" if endpoint.empty?
|
||||||
|
execute *KAMAL.proxy.deploy(role.container_prefix, target: endpoint)
|
||||||
|
else
|
||||||
|
Kamal::Cli::Healthcheck::Poller.wait_for_healthy(pause_after_ready: true) { capture_with_info(*app.status(version: version)) }
|
||||||
|
end
|
||||||
|
else
|
||||||
|
execute *app.tie_cord(role.cord_host_file) if uses_cord?
|
||||||
execute *app.run(hostname: hostname)
|
execute *app.run(hostname: hostname)
|
||||||
Kamal::Cli::Healthcheck::Poller.wait_for_healthy(pause_after_ready: true) { capture_with_info(*app.status(version: version)) }
|
Kamal::Cli::Healthcheck::Poller.wait_for_healthy(pause_after_ready: true) { capture_with_info(*app.status(version: version)) }
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def stop_new_version
|
def stop_new_version
|
||||||
execute *app.stop(version: version), raise_on_non_zero_exit: false
|
execute *app.stop(version: version), raise_on_non_zero_exit: false
|
||||||
end
|
end
|
||||||
|
|
||||||
def stop_old_version(version)
|
def stop_old_version(version)
|
||||||
if uses_cord?
|
if uses_cord? && !proxy_host?
|
||||||
cord = capture_with_info(*app.cord(version: version), raise_on_non_zero_exit: false).strip
|
cord = capture_with_info(*app.cord(version: version), raise_on_non_zero_exit: false).strip
|
||||||
if cord.present?
|
if cord.present?
|
||||||
execute *app.cut_cord(cord)
|
execute *app.cut_cord(cord)
|
||||||
@@ -88,8 +102,12 @@ class Kamal::Cli::App::Boot
|
|||||||
def close_barrier
|
def close_barrier
|
||||||
if barrier.close
|
if barrier.close
|
||||||
info "First #{KAMAL.primary_role} container is unhealthy on #{host}, not booting any other roles"
|
info "First #{KAMAL.primary_role} container is unhealthy on #{host}, not booting any other roles"
|
||||||
|
begin
|
||||||
error capture_with_info(*app.logs(version: version))
|
error capture_with_info(*app.logs(version: version))
|
||||||
error capture_with_info(*app.container_health_log(version: version))
|
error capture_with_info(*app.container_health_log(version: version))
|
||||||
|
rescue SSHKit::Command::Failed
|
||||||
|
error "Could not fetch logs for #{version}"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -116,4 +134,8 @@ class Kamal::Cli::App::Boot
|
|||||||
def queuer?
|
def queuer?
|
||||||
barrier && !barrier_role?
|
barrier && !barrier_role?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def proxy_host?
|
||||||
|
KAMAL.proxy_host?(host)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
require "thor"
|
require "thor"
|
||||||
require "dotenv"
|
|
||||||
require "kamal/sshkit_with_ext"
|
require "kamal/sshkit_with_ext"
|
||||||
|
|
||||||
module Kamal::Cli
|
module Kamal::Cli
|
||||||
@@ -31,53 +30,15 @@ module Kamal::Cli
|
|||||||
else
|
else
|
||||||
super
|
super
|
||||||
end
|
end
|
||||||
@original_env = ENV.to_h.dup
|
initialize_commander unless KAMAL.configured?
|
||||||
load_env
|
|
||||||
initialize_commander(options_with_subcommand_class_options)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def reload_env
|
|
||||||
reset_env
|
|
||||||
load_env
|
|
||||||
end
|
|
||||||
|
|
||||||
def load_env
|
|
||||||
if destination = options[:destination]
|
|
||||||
Dotenv.load(".env.#{destination}", ".env")
|
|
||||||
else
|
|
||||||
Dotenv.load(".env")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def reset_env
|
|
||||||
replace_env @original_env
|
|
||||||
end
|
|
||||||
|
|
||||||
def replace_env(env)
|
|
||||||
ENV.clear
|
|
||||||
ENV.update(env)
|
|
||||||
end
|
|
||||||
|
|
||||||
def with_original_env
|
|
||||||
keeping_current_env do
|
|
||||||
reset_env
|
|
||||||
yield
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def keeping_current_env
|
|
||||||
current_env = ENV.to_h.dup
|
|
||||||
yield
|
|
||||||
ensure
|
|
||||||
replace_env(current_env)
|
|
||||||
end
|
|
||||||
|
|
||||||
def options_with_subcommand_class_options
|
def options_with_subcommand_class_options
|
||||||
options.merge(@_initializer.last[:class_options] || {})
|
options.merge(@_initializer.last[:class_options] || {})
|
||||||
end
|
end
|
||||||
|
|
||||||
def initialize_commander(options)
|
def initialize_commander
|
||||||
KAMAL.tap do |commander|
|
KAMAL.tap do |commander|
|
||||||
if options[:verbose]
|
if options[:verbose]
|
||||||
ENV["VERBOSE"] = "1" # For backtraces via cli/start
|
ENV["VERBOSE"] = "1" # For backtraces via cli/start
|
||||||
@@ -112,8 +73,6 @@ module Kamal::Cli
|
|||||||
if KAMAL.holding_lock?
|
if KAMAL.holding_lock?
|
||||||
yield
|
yield
|
||||||
else
|
else
|
||||||
ensure_run_and_locks_directory
|
|
||||||
|
|
||||||
acquire_lock
|
acquire_lock
|
||||||
|
|
||||||
begin
|
begin
|
||||||
@@ -142,6 +101,8 @@ module Kamal::Cli
|
|||||||
end
|
end
|
||||||
|
|
||||||
def acquire_lock
|
def acquire_lock
|
||||||
|
ensure_run_and_locks_directory
|
||||||
|
|
||||||
raise_if_locked do
|
raise_if_locked do
|
||||||
say "Acquiring the deploy lock...", :magenta
|
say "Acquiring the deploy lock...", :magenta
|
||||||
on(KAMAL.primary_host) { execute *KAMAL.lock.acquire("Automatic deploy lock", KAMAL.config.version), verbosity: :debug }
|
on(KAMAL.primary_host) { execute *KAMAL.lock.acquire("Automatic deploy lock", KAMAL.config.version), verbosity: :debug }
|
||||||
|
|||||||
@@ -32,10 +32,15 @@ class Kamal::Cli::Build < Kamal::Cli::Base
|
|||||||
|
|
||||||
run_locally do
|
run_locally do
|
||||||
begin
|
begin
|
||||||
execute *KAMAL.builder.buildx_inspect
|
execute *KAMAL.builder.inspect_builder
|
||||||
rescue SSHKit::Command::Failed => e
|
rescue SSHKit::Command::Failed => e
|
||||||
if e.message =~ /(context not found|no builder|does not exist)/
|
if e.message =~ /(context not found|no builder|no compatible builder|does not exist)/
|
||||||
warn "Missing compatible builder, so creating a new one first"
|
warn "Missing compatible builder, so creating a new one first"
|
||||||
|
begin
|
||||||
|
cli.remove
|
||||||
|
rescue SSHKit::Command::Failed
|
||||||
|
raise unless e.message =~ /(context not found|no builder|does not exist)/
|
||||||
|
end
|
||||||
cli.create
|
cli.create
|
||||||
else
|
else
|
||||||
raise
|
raise
|
||||||
@@ -46,7 +51,7 @@ class Kamal::Cli::Build < Kamal::Cli::Base
|
|||||||
push = KAMAL.builder.push
|
push = KAMAL.builder.push
|
||||||
|
|
||||||
KAMAL.with_verbosity(:debug) do
|
KAMAL.with_verbosity(:debug) do
|
||||||
Dir.chdir(KAMAL.config.builder.build_directory) { execute *push }
|
Dir.chdir(KAMAL.config.builder.build_directory) { execute *push, env: KAMAL.config.builder.secrets }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
require "tempfile"
|
|
||||||
|
|
||||||
class Kamal::Cli::Env < Kamal::Cli::Base
|
|
||||||
desc "push", "Push the env file to the remote hosts"
|
|
||||||
def push
|
|
||||||
with_lock do
|
|
||||||
on(KAMAL.hosts) do
|
|
||||||
execute *KAMAL.auditor.record("Pushed env files"), verbosity: :debug
|
|
||||||
|
|
||||||
KAMAL.roles_on(host).each do |role|
|
|
||||||
execute *KAMAL.app(role: role, host: host).make_env_directory
|
|
||||||
upload! role.env(host).secrets_io, role.env(host).secrets_file, mode: 400
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
on(KAMAL.traefik_hosts) do
|
|
||||||
execute *KAMAL.traefik.make_env_directory
|
|
||||||
upload! KAMAL.traefik.env.secrets_io, KAMAL.traefik.env.secrets_file, mode: 400
|
|
||||||
end
|
|
||||||
|
|
||||||
on(KAMAL.accessory_hosts) do
|
|
||||||
KAMAL.accessories_on(host).each do |accessory|
|
|
||||||
accessory_config = KAMAL.config.accessory(accessory)
|
|
||||||
execute *KAMAL.accessory(accessory).make_env_directory
|
|
||||||
upload! accessory_config.env.secrets_io, accessory_config.env.secrets_file, mode: 400
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
desc "delete", "Delete the env file from the remote hosts"
|
|
||||||
def delete
|
|
||||||
with_lock do
|
|
||||||
on(KAMAL.hosts) do
|
|
||||||
execute *KAMAL.auditor.record("Deleted env files"), verbosity: :debug
|
|
||||||
|
|
||||||
KAMAL.roles_on(host).each do |role|
|
|
||||||
execute *KAMAL.app(role: role, host: host).remove_env_file
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
on(KAMAL.traefik_hosts) do
|
|
||||||
execute *KAMAL.traefik.remove_env_file
|
|
||||||
end
|
|
||||||
|
|
||||||
on(KAMAL.accessory_hosts) do
|
|
||||||
KAMAL.accessories_on(host).each do |accessory|
|
|
||||||
accessory_config = KAMAL.config.accessory(accessory)
|
|
||||||
execute *KAMAL.accessory(accessory).remove_env_file
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
require "concurrent/ivar"
|
||||||
|
|
||||||
class Kamal::Cli::Healthcheck::Barrier
|
class Kamal::Cli::Healthcheck::Barrier
|
||||||
def initialize
|
def initialize
|
||||||
@ivar = Concurrent::IVar.new
|
@ivar = Concurrent::IVar.new
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ class Kamal::Cli::Lock < Kamal::Cli::Base
|
|||||||
def status
|
def status
|
||||||
handle_missing_lock do
|
handle_missing_lock do
|
||||||
on(KAMAL.primary_host) do
|
on(KAMAL.primary_host) do
|
||||||
execute *KAMAL.server.ensure_run_directory
|
|
||||||
puts capture_with_debug(*KAMAL.lock.status)
|
puts capture_with_debug(*KAMAL.lock.status)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -13,9 +12,10 @@ class Kamal::Cli::Lock < Kamal::Cli::Base
|
|||||||
option :message, aliases: "-m", type: :string, desc: "A lock message", required: true
|
option :message, aliases: "-m", type: :string, desc: "A lock message", required: true
|
||||||
def acquire
|
def acquire
|
||||||
message = options[:message]
|
message = options[:message]
|
||||||
|
ensure_run_and_locks_directory
|
||||||
|
|
||||||
raise_if_locked do
|
raise_if_locked do
|
||||||
on(KAMAL.primary_host) do
|
on(KAMAL.primary_host) do
|
||||||
execute *KAMAL.server.ensure_run_directory
|
|
||||||
execute *KAMAL.lock.acquire(message, KAMAL.config.version), verbosity: :debug
|
execute *KAMAL.lock.acquire(message, KAMAL.config.version), verbosity: :debug
|
||||||
end
|
end
|
||||||
say "Acquired the deploy lock"
|
say "Acquired the deploy lock"
|
||||||
@@ -26,7 +26,6 @@ class Kamal::Cli::Lock < Kamal::Cli::Base
|
|||||||
def release
|
def release
|
||||||
handle_missing_lock do
|
handle_missing_lock do
|
||||||
on(KAMAL.primary_host) do
|
on(KAMAL.primary_host) do
|
||||||
execute *KAMAL.server.ensure_run_directory
|
|
||||||
execute *KAMAL.lock.release, verbosity: :debug
|
execute *KAMAL.lock.release, verbosity: :debug
|
||||||
end
|
end
|
||||||
say "Released the deploy lock"
|
say "Released the deploy lock"
|
||||||
|
|||||||
@@ -9,10 +9,6 @@ class Kamal::Cli::Main < Kamal::Cli::Base
|
|||||||
say "Ensure Docker is installed...", :magenta
|
say "Ensure Docker is installed...", :magenta
|
||||||
invoke "kamal:cli:server:bootstrap", [], invoke_options
|
invoke "kamal:cli:server:bootstrap", [], invoke_options
|
||||||
|
|
||||||
say "Evaluate and push env files...", :magenta
|
|
||||||
invoke "kamal:cli:main:envify", [], invoke_options
|
|
||||||
invoke "kamal:cli:env:push", [], invoke_options
|
|
||||||
|
|
||||||
invoke "kamal:cli:accessory:boot", [ "all" ], invoke_options
|
invoke "kamal:cli:accessory:boot", [ "all" ], invoke_options
|
||||||
deploy
|
deploy
|
||||||
end
|
end
|
||||||
@@ -37,10 +33,15 @@ class Kamal::Cli::Main < Kamal::Cli::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
with_lock do
|
with_lock do
|
||||||
run_hook "pre-deploy"
|
run_hook "pre-deploy", secrets: true
|
||||||
|
|
||||||
|
if KAMAL.config.proxy.enabled?
|
||||||
|
say "Ensure Traefik/kamal-proxy is running...", :magenta
|
||||||
|
invoke "kamal:cli:proxy:boot", [], invoke_options
|
||||||
|
else
|
||||||
say "Ensure Traefik is running...", :magenta
|
say "Ensure Traefik is running...", :magenta
|
||||||
invoke "kamal:cli:traefik:boot", [], invoke_options
|
invoke "kamal:cli:traefik:boot", [], invoke_options
|
||||||
|
end
|
||||||
|
|
||||||
say "Detect stale containers...", :magenta
|
say "Detect stale containers...", :magenta
|
||||||
invoke "kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true)
|
invoke "kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true)
|
||||||
@@ -52,7 +53,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
run_hook "post-deploy", runtime: runtime.round
|
run_hook "post-deploy", secrets: true, runtime: runtime.round
|
||||||
end
|
end
|
||||||
|
|
||||||
desc "redeploy", "Deploy app to servers without bootstrapping servers, starting Traefik, pruning, and registry login"
|
desc "redeploy", "Deploy app to servers without bootstrapping servers, starting Traefik, pruning, and registry login"
|
||||||
@@ -70,7 +71,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
with_lock do
|
with_lock do
|
||||||
run_hook "pre-deploy"
|
run_hook "pre-deploy", secrets: true
|
||||||
|
|
||||||
say "Detect stale containers...", :magenta
|
say "Detect stale containers...", :magenta
|
||||||
invoke "kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true)
|
invoke "kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true)
|
||||||
@@ -79,7 +80,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
run_hook "post-deploy", runtime: runtime.round
|
run_hook "post-deploy", secrets: true, runtime: runtime.round
|
||||||
end
|
end
|
||||||
|
|
||||||
desc "rollback [VERSION]", "Rollback app to VERSION"
|
desc "rollback [VERSION]", "Rollback app to VERSION"
|
||||||
@@ -93,7 +94,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base
|
|||||||
old_version = nil
|
old_version = nil
|
||||||
|
|
||||||
if container_available?(version)
|
if container_available?(version)
|
||||||
run_hook "pre-deploy"
|
run_hook "pre-deploy", secrets: true
|
||||||
|
|
||||||
invoke "kamal:cli:app:boot", [], invoke_options.merge(version: version)
|
invoke "kamal:cli:app:boot", [], invoke_options.merge(version: version)
|
||||||
rolled_back = true
|
rolled_back = true
|
||||||
@@ -103,12 +104,16 @@ class Kamal::Cli::Main < Kamal::Cli::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
run_hook "post-deploy", runtime: runtime.round if rolled_back
|
run_hook "post-deploy", secrets: true, runtime: runtime.round if rolled_back
|
||||||
end
|
end
|
||||||
|
|
||||||
desc "details", "Show details about all containers"
|
desc "details", "Show details about all containers"
|
||||||
def details
|
def details
|
||||||
|
if KAMAL.config.proxy.enabled?
|
||||||
|
invoke "kamal:cli:proxy:details"
|
||||||
|
else
|
||||||
invoke "kamal:cli:traefik:details"
|
invoke "kamal:cli:traefik:details"
|
||||||
|
end
|
||||||
invoke "kamal:cli:app:details"
|
invoke "kamal:cli:app:details"
|
||||||
invoke "kamal:cli:accessory:details", [ "all" ]
|
invoke "kamal:cli:accessory:details", [ "all" ]
|
||||||
end
|
end
|
||||||
@@ -152,9 +157,10 @@ class Kamal::Cli::Main < Kamal::Cli::Base
|
|||||||
puts "Created configuration file in config/deploy.yml"
|
puts "Created configuration file in config/deploy.yml"
|
||||||
end
|
end
|
||||||
|
|
||||||
unless (deploy_file = Pathname.new(File.expand_path(".env"))).exist?
|
unless (secrets_file = Pathname.new(File.expand_path(".kamal/secrets"))).exist?
|
||||||
FileUtils.cp_r Pathname.new(File.expand_path("templates/template.env", __dir__)), deploy_file
|
FileUtils.mkdir_p secrets_file.dirname
|
||||||
puts "Created .env file"
|
FileUtils.cp_r Pathname.new(File.expand_path("templates/secrets", __dir__)), secrets_file
|
||||||
|
puts "Created .kamal/secrets file"
|
||||||
end
|
end
|
||||||
|
|
||||||
unless (hooks_dir = Pathname.new(File.expand_path(".kamal/hooks"))).exist?
|
unless (hooks_dir = Pathname.new(File.expand_path(".kamal/hooks"))).exist?
|
||||||
@@ -179,37 +185,16 @@ class Kamal::Cli::Main < Kamal::Cli::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
desc "envify", "Create .env by evaluating .env.erb (or .env.staging.erb -> .env.staging when using -d staging)"
|
|
||||||
option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip .env file push"
|
|
||||||
def envify
|
|
||||||
if destination = options[:destination]
|
|
||||||
env_template_path = ".env.#{destination}.erb"
|
|
||||||
env_path = ".env.#{destination}"
|
|
||||||
else
|
|
||||||
env_template_path = ".env.erb"
|
|
||||||
env_path = ".env"
|
|
||||||
end
|
|
||||||
|
|
||||||
if Pathname.new(File.expand_path(env_template_path)).exist?
|
|
||||||
# Ensure existing env doesn't pollute template evaluation
|
|
||||||
content = with_original_env { ERB.new(File.read(env_template_path), trim_mode: "-").result }
|
|
||||||
File.write(env_path, content, perm: 0600)
|
|
||||||
|
|
||||||
unless options[:skip_push]
|
|
||||||
reload_env
|
|
||||||
invoke "kamal:cli:env:push", options
|
|
||||||
end
|
|
||||||
else
|
|
||||||
puts "Skipping envify (no #{env_template_path} exist)"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
desc "remove", "Remove Traefik, app, accessories, and registry session from servers"
|
desc "remove", "Remove Traefik, app, accessories, and registry session from servers"
|
||||||
option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
|
option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
|
||||||
def remove
|
def remove
|
||||||
confirming "This will remove all containers and images. Are you sure?" do
|
confirming "This will remove all containers and images. Are you sure?" do
|
||||||
with_lock do
|
with_lock do
|
||||||
|
if KAMAL.config.proxy.enabled?
|
||||||
|
invoke "kamal:cli:proxy:remove", [], options.without(:confirmed)
|
||||||
|
else
|
||||||
invoke "kamal:cli:traefik:remove", [], options.without(:confirmed)
|
invoke "kamal:cli:traefik:remove", [], options.without(:confirmed)
|
||||||
|
end
|
||||||
invoke "kamal:cli:app:remove", [], options.without(:confirmed)
|
invoke "kamal:cli:app:remove", [], options.without(:confirmed)
|
||||||
invoke "kamal:cli:accessory:remove", [ "all" ], options
|
invoke "kamal:cli:accessory:remove", [ "all" ], options
|
||||||
invoke "kamal:cli:registry:logout", [], options.without(:confirmed).merge(skip_local: true)
|
invoke "kamal:cli:registry:logout", [], options.without(:confirmed).merge(skip_local: true)
|
||||||
@@ -231,18 +216,21 @@ class Kamal::Cli::Main < Kamal::Cli::Base
|
|||||||
desc "build", "Build application image"
|
desc "build", "Build application image"
|
||||||
subcommand "build", Kamal::Cli::Build
|
subcommand "build", Kamal::Cli::Build
|
||||||
|
|
||||||
desc "env", "Manage environment files"
|
|
||||||
subcommand "env", Kamal::Cli::Env
|
|
||||||
|
|
||||||
desc "lock", "Manage the deploy lock"
|
desc "lock", "Manage the deploy lock"
|
||||||
subcommand "lock", Kamal::Cli::Lock
|
subcommand "lock", Kamal::Cli::Lock
|
||||||
|
|
||||||
|
desc "proxy", "Prune old application images and containers"
|
||||||
|
subcommand "proxy", Kamal::Cli::Proxy
|
||||||
|
|
||||||
desc "prune", "Prune old application images and containers"
|
desc "prune", "Prune old application images and containers"
|
||||||
subcommand "prune", Kamal::Cli::Prune
|
subcommand "prune", Kamal::Cli::Prune
|
||||||
|
|
||||||
desc "registry", "Login and -out of the image registry"
|
desc "registry", "Login and -out of the image registry"
|
||||||
subcommand "registry", Kamal::Cli::Registry
|
subcommand "registry", Kamal::Cli::Registry
|
||||||
|
|
||||||
|
desc "secrets", "Helpers for extracting secrets"
|
||||||
|
subcommand "secrets", Kamal::Cli::Secrets
|
||||||
|
|
||||||
desc "server", "Bootstrap servers with curl and Docker"
|
desc "server", "Bootstrap servers with curl and Docker"
|
||||||
subcommand "server", Kamal::Cli::Server
|
subcommand "server", Kamal::Cli::Server
|
||||||
|
|
||||||
|
|||||||
213
lib/kamal/cli/proxy.rb
Normal file
213
lib/kamal/cli/proxy.rb
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
class Kamal::Cli::Proxy < Kamal::Cli::Base
|
||||||
|
desc "boot", "Boot proxy on servers"
|
||||||
|
def boot
|
||||||
|
raise_unless_kamal_proxy_enabled!
|
||||||
|
with_lock do
|
||||||
|
on(KAMAL.hosts) do |host|
|
||||||
|
execute *KAMAL.docker.create_network
|
||||||
|
rescue SSHKit::Command::Failed => e
|
||||||
|
raise unless e.message.include?("already exists")
|
||||||
|
end
|
||||||
|
|
||||||
|
on(KAMAL.traefik_hosts) do |host|
|
||||||
|
execute *KAMAL.registry.login
|
||||||
|
if KAMAL.proxy_host?(host)
|
||||||
|
execute *KAMAL.proxy.start_or_run
|
||||||
|
else
|
||||||
|
execute *KAMAL.traefik.ensure_env_directory
|
||||||
|
upload! KAMAL.traefik.secrets_io, KAMAL.traefik.secrets_path, mode: "0600"
|
||||||
|
execute *KAMAL.traefik.start_or_run
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "reboot", "Reboot proxy on servers (stop container, remove container, start new container)"
|
||||||
|
option :rolling, type: :boolean, default: false, desc: "Reboot proxy on hosts in sequence, rather than in parallel"
|
||||||
|
option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
|
||||||
|
def reboot
|
||||||
|
raise_unless_kamal_proxy_enabled!
|
||||||
|
confirming "This will cause a brief outage on each host. Are you sure?" do
|
||||||
|
with_lock do
|
||||||
|
host_groups = options[:rolling] ? KAMAL.traefik_hosts : [ KAMAL.traefik_hosts ]
|
||||||
|
host_groups.each do |hosts|
|
||||||
|
host_list = Array(hosts).join(",")
|
||||||
|
run_hook "pre-traefik-reboot", hosts: host_list
|
||||||
|
on(hosts) do |host|
|
||||||
|
execute *KAMAL.auditor.record("Rebooted proxy"), verbosity: :debug
|
||||||
|
execute *KAMAL.registry.login
|
||||||
|
|
||||||
|
"Stopping and removing Traefik on #{host}, if running..."
|
||||||
|
execute *KAMAL.traefik.stop, raise_on_non_zero_exit: false
|
||||||
|
execute *KAMAL.traefik.remove_container
|
||||||
|
|
||||||
|
"Stopping and removing kamal-proxy on #{host}, if running..."
|
||||||
|
execute *KAMAL.proxy.stop, raise_on_non_zero_exit: false
|
||||||
|
execute *KAMAL.proxy.remove_container
|
||||||
|
|
||||||
|
execute *KAMAL.traefik_or_proxy(host).run
|
||||||
|
|
||||||
|
if KAMAL.proxy_host?(host)
|
||||||
|
KAMAL.roles_on(host).select(&:running_traefik?).each do |role|
|
||||||
|
app = KAMAL.app(role: role, host: host)
|
||||||
|
|
||||||
|
version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
|
||||||
|
endpoint = capture_with_info(*app.container_id_for_version(version)).strip
|
||||||
|
|
||||||
|
if endpoint.present?
|
||||||
|
info "Deploying #{endpoint} for role `#{role}` on #{host}..."
|
||||||
|
execute *KAMAL.proxy.deploy(role.container_prefix, target: endpoint)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
run_hook "post-traefik-reboot", hosts: host_list
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "upgrade", "Upgrade to correct proxy on servers (stop container, remove container, start new container)"
|
||||||
|
option :rolling, type: :boolean, default: false, desc: "Reboot proxy on hosts in sequence, rather than in parallel"
|
||||||
|
option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
|
||||||
|
def upgrade
|
||||||
|
invoke_options = { "version" => KAMAL.config.version }.merge(options)
|
||||||
|
|
||||||
|
raise_unless_kamal_proxy_enabled!
|
||||||
|
confirming "This will cause a brief outage on each host. Are you sure?" do
|
||||||
|
host_groups = options[:rolling] ? KAMAL.hosts : [ KAMAL.hosts ]
|
||||||
|
host_groups.each do |hosts|
|
||||||
|
host_list = Array(hosts).join(",")
|
||||||
|
run_hook "pre-traefik-reboot", hosts: host_list
|
||||||
|
on(hosts) do |host|
|
||||||
|
execute *KAMAL.auditor.record("Rebooted proxy"), verbosity: :debug
|
||||||
|
execute *KAMAL.registry.login
|
||||||
|
|
||||||
|
"Stopping and removing Traefik on #{host}, if running..."
|
||||||
|
execute *KAMAL.traefik.stop, raise_on_non_zero_exit: false
|
||||||
|
execute *KAMAL.traefik.remove_container
|
||||||
|
|
||||||
|
"Stopping and removing kamal-proxy on #{host}, if running..."
|
||||||
|
execute *KAMAL.proxy.stop, raise_on_non_zero_exit: false
|
||||||
|
execute *KAMAL.proxy.remove_container
|
||||||
|
end
|
||||||
|
|
||||||
|
invoke "kamal:cli:proxy:boot", [], invoke_options.merge("hosts" => host_list)
|
||||||
|
reset_invocation(Kamal::Cli::Proxy)
|
||||||
|
invoke "kamal:cli:app:boot", [], invoke_options.merge("hosts" => host_list, version: KAMAL.config.latest_tag)
|
||||||
|
reset_invocation(Kamal::Cli::App)
|
||||||
|
invoke "kamal:cli:prune:all", [], invoke_options.merge("hosts" => host_list)
|
||||||
|
reset_invocation(Kamal::Cli::Prune)
|
||||||
|
|
||||||
|
run_hook "post-traefik-reboot", hosts: host_list
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "start", "Start existing proxy container on servers"
|
||||||
|
def start
|
||||||
|
raise_unless_kamal_proxy_enabled!
|
||||||
|
with_lock do
|
||||||
|
on(KAMAL.traefik_hosts) do |host|
|
||||||
|
execute *KAMAL.auditor.record("Started proxy"), verbosity: :debug
|
||||||
|
execute *KAMAL.traefik_or_proxy(host).start
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "stop", "Stop existing proxy container on servers"
|
||||||
|
def stop
|
||||||
|
raise_unless_kamal_proxy_enabled!
|
||||||
|
with_lock do
|
||||||
|
on(KAMAL.traefik_hosts) do |host|
|
||||||
|
execute *KAMAL.auditor.record("Stopped proxy"), verbosity: :debug
|
||||||
|
execute *KAMAL.traefik_or_proxy(host).stop, raise_on_non_zero_exit: false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "restart", "Restart existing proxy container on servers"
|
||||||
|
def restart
|
||||||
|
raise_unless_kamal_proxy_enabled!
|
||||||
|
with_lock do
|
||||||
|
stop
|
||||||
|
start
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "details", "Show details about proxy container from servers"
|
||||||
|
def details
|
||||||
|
raise_unless_kamal_proxy_enabled!
|
||||||
|
on(KAMAL.traefik_hosts) { |host| puts_by_host host, capture_with_info(*KAMAL.traefik_or_proxy(host).info), type: "Proxy" }
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "logs", "Show log lines from proxy on servers"
|
||||||
|
option :since, aliases: "-s", desc: "Show logs since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)"
|
||||||
|
option :lines, type: :numeric, aliases: "-n", desc: "Number of log lines to pull from each server"
|
||||||
|
option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)"
|
||||||
|
option :follow, aliases: "-f", desc: "Follow logs on primary server (or specific host set by --hosts)"
|
||||||
|
def logs
|
||||||
|
raise_unless_kamal_proxy_enabled!
|
||||||
|
grep = options[:grep]
|
||||||
|
|
||||||
|
if options[:follow]
|
||||||
|
run_locally do
|
||||||
|
info "Following logs on #{KAMAL.primary_host}..."
|
||||||
|
info KAMAL.traefik_or_proxy(KAMAL.primary_host).follow_logs(host: KAMAL.primary_host, grep: grep)
|
||||||
|
exec KAMAL.traefik_or_proxy(KAMAL.primary_host).follow_logs(host: KAMAL.primary_host, grep: grep)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
since = options[:since]
|
||||||
|
lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set
|
||||||
|
|
||||||
|
on(KAMAL.traefik_hosts) do |host|
|
||||||
|
puts_by_host host, capture(*KAMAL.traefik_or_proxy(host).logs(since: since, lines: lines, grep: grep)), type: "Proxy"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "remove", "Remove proxy container and image from servers"
|
||||||
|
def remove
|
||||||
|
raise_unless_kamal_proxy_enabled!
|
||||||
|
with_lock do
|
||||||
|
stop
|
||||||
|
remove_container
|
||||||
|
remove_image
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "remove_container", "Remove proxy container from servers", hide: true
|
||||||
|
def remove_container
|
||||||
|
raise_unless_kamal_proxy_enabled!
|
||||||
|
with_lock do
|
||||||
|
on(KAMAL.traefik_hosts) do
|
||||||
|
execute *KAMAL.auditor.record("Removed proxy container"), verbosity: :debug
|
||||||
|
execute *KAMAL.proxy.remove_container
|
||||||
|
execute *KAMAL.traefik.remove_container
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "remove_image", "Remove proxy image from servers", hide: true
|
||||||
|
def remove_image
|
||||||
|
raise_unless_kamal_proxy_enabled!
|
||||||
|
with_lock do
|
||||||
|
on(KAMAL.traefik_hosts) do
|
||||||
|
execute *KAMAL.auditor.record("Removed proxy image"), verbosity: :debug
|
||||||
|
execute *KAMAL.proxy.remove_image
|
||||||
|
execute *KAMAL.traefik.remove_image
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def raise_unless_kamal_proxy_enabled!
|
||||||
|
unless KAMAL.config.proxy.enabled?
|
||||||
|
raise "kamal proxy commands are disabled unless experimental proxy support is enabled. Use `kamal traefik` commands instead."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def reset_invocation(cli_class)
|
||||||
|
instance_variable_get("@_invocations")[cli_class].pop
|
||||||
|
end
|
||||||
|
end
|
||||||
47
lib/kamal/cli/secrets.rb
Normal file
47
lib/kamal/cli/secrets.rb
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
class Kamal::Cli::Secrets < Kamal::Cli::Base
|
||||||
|
desc "fetch [SECRETS...]", "Fetch secrets from a vault"
|
||||||
|
option :adapter, type: :string, aliases: "-a", required: true, desc: "Which vault adapter to use"
|
||||||
|
option :account, type: :string, required: true, desc: "The account identifier or username"
|
||||||
|
option :from, type: :string, required: false, desc: "A vault or folder to fetch the secrets from"
|
||||||
|
option :inline, type: :boolean, required: false, hidden: true
|
||||||
|
def fetch(*secrets)
|
||||||
|
handle_output(inline: options[:inline]) do
|
||||||
|
results = adapter(options[:adapter]).fetch(secrets, **options.slice(:account, :from).symbolize_keys)
|
||||||
|
JSON.dump(results).shellescape
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "extract", "Extract a single secret from the results of a fetch call"
|
||||||
|
option :inline, type: :boolean, required: false, hidden: true
|
||||||
|
def extract(name, secrets)
|
||||||
|
handle_output(inline: options[:inline]) do
|
||||||
|
parsed_secrets = JSON.parse(secrets)
|
||||||
|
|
||||||
|
value = parsed_secrets[name] || parsed_secrets.find { |k, v| k.end_with?("/#{name}") }&.last
|
||||||
|
|
||||||
|
raise "Could not find secret #{name}" if value.nil?
|
||||||
|
|
||||||
|
value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def adapter(adapter)
|
||||||
|
Kamal::Secrets::Adapters.lookup(adapter)
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_output(inline: nil)
|
||||||
|
yield.tap do |output|
|
||||||
|
puts output unless inline
|
||||||
|
end
|
||||||
|
rescue => e
|
||||||
|
handle_error(e)
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_error(e)
|
||||||
|
$stderr.puts " \e[31mERROR (#{e.class}): #{e.message}\e[0m"
|
||||||
|
$stderr.puts e.backtrace if ENV["VERBOSE"]
|
||||||
|
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
end
|
||||||
16
lib/kamal/cli/templates/secrets
Normal file
16
lib/kamal/cli/templates/secrets
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# WARNING: Avoid adding secrets directly to this file
|
||||||
|
# If you must, then add `.kamal/secrets*` to your .gitignore file
|
||||||
|
|
||||||
|
# Option 1: Read secrets from the environment
|
||||||
|
KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD
|
||||||
|
|
||||||
|
# Option 2: Read secrets via a command
|
||||||
|
# RAILS_MASTER_KEY=$(cat config/master.key)
|
||||||
|
|
||||||
|
# Option 3: Read secrets via kamal secrets helpers
|
||||||
|
# These will handle logging in and fetching the secrets in as few calls as possible
|
||||||
|
# There are adapters for 1Password, LastPass + Bitwarden
|
||||||
|
#
|
||||||
|
# SECRETS=$(kamal secrets fetch --adapter 1password --account my-account --from MyVault/MyItem KAMAL_REGISTRY_PASSWORD RAILS_MASTER_KEY)
|
||||||
|
# KAMAL_REGISTRY_PASSWORD=$(kamal secrets extract KAMAL_REGISTRY_PASSWORD $SECRETS)
|
||||||
|
# RAILS_MASTER_KEY=$(kamal secrets extract RAILS_MASTER_KEY $SECRETS)
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
KAMAL_REGISTRY_PASSWORD=change-this
|
|
||||||
RAILS_MASTER_KEY=another-env
|
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
class Kamal::Cli::Traefik < Kamal::Cli::Base
|
class Kamal::Cli::Traefik < Kamal::Cli::Base
|
||||||
desc "boot", "Boot Traefik on servers"
|
desc "boot", "Boot Traefik on servers"
|
||||||
def boot
|
def boot
|
||||||
|
raise_if_kamal_proxy_enabled!
|
||||||
with_lock do
|
with_lock do
|
||||||
on(KAMAL.traefik_hosts) do
|
on(KAMAL.traefik_hosts) do
|
||||||
execute *KAMAL.registry.login
|
execute *KAMAL.registry.login
|
||||||
|
execute *KAMAL.traefik.ensure_env_directory
|
||||||
|
upload! KAMAL.traefik.secrets_io, KAMAL.traefik.secrets_path, mode: "0600"
|
||||||
execute *KAMAL.traefik.start_or_run
|
execute *KAMAL.traefik.start_or_run
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -13,6 +16,7 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
|
|||||||
option :rolling, type: :boolean, default: false, desc: "Reboot traefik on hosts in sequence, rather than in parallel"
|
option :rolling, type: :boolean, default: false, desc: "Reboot traefik on hosts in sequence, rather than in parallel"
|
||||||
option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
|
option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
|
||||||
def reboot
|
def reboot
|
||||||
|
raise_if_kamal_proxy_enabled!
|
||||||
confirming "This will cause a brief outage on each host. Are you sure?" do
|
confirming "This will cause a brief outage on each host. Are you sure?" do
|
||||||
with_lock do
|
with_lock do
|
||||||
host_groups = options[:rolling] ? KAMAL.traefik_hosts : [ KAMAL.traefik_hosts ]
|
host_groups = options[:rolling] ? KAMAL.traefik_hosts : [ KAMAL.traefik_hosts ]
|
||||||
@@ -34,6 +38,7 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
|
|||||||
|
|
||||||
desc "start", "Start existing Traefik container on servers"
|
desc "start", "Start existing Traefik container on servers"
|
||||||
def start
|
def start
|
||||||
|
raise_if_kamal_proxy_enabled!
|
||||||
with_lock do
|
with_lock do
|
||||||
on(KAMAL.traefik_hosts) do
|
on(KAMAL.traefik_hosts) do
|
||||||
execute *KAMAL.auditor.record("Started traefik"), verbosity: :debug
|
execute *KAMAL.auditor.record("Started traefik"), verbosity: :debug
|
||||||
@@ -44,6 +49,7 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
|
|||||||
|
|
||||||
desc "stop", "Stop existing Traefik container on servers"
|
desc "stop", "Stop existing Traefik container on servers"
|
||||||
def stop
|
def stop
|
||||||
|
raise_if_kamal_proxy_enabled!
|
||||||
with_lock do
|
with_lock do
|
||||||
on(KAMAL.traefik_hosts) do
|
on(KAMAL.traefik_hosts) do
|
||||||
execute *KAMAL.auditor.record("Stopped traefik"), verbosity: :debug
|
execute *KAMAL.auditor.record("Stopped traefik"), verbosity: :debug
|
||||||
@@ -54,6 +60,7 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
|
|||||||
|
|
||||||
desc "restart", "Restart existing Traefik container on servers"
|
desc "restart", "Restart existing Traefik container on servers"
|
||||||
def restart
|
def restart
|
||||||
|
raise_if_kamal_proxy_enabled!
|
||||||
with_lock do
|
with_lock do
|
||||||
stop
|
stop
|
||||||
start
|
start
|
||||||
@@ -62,6 +69,7 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
|
|||||||
|
|
||||||
desc "details", "Show details about Traefik container from servers"
|
desc "details", "Show details about Traefik container from servers"
|
||||||
def details
|
def details
|
||||||
|
raise_if_kamal_proxy_enabled!
|
||||||
on(KAMAL.traefik_hosts) { |host| puts_by_host host, capture_with_info(*KAMAL.traefik.info), type: "Traefik" }
|
on(KAMAL.traefik_hosts) { |host| puts_by_host host, capture_with_info(*KAMAL.traefik.info), type: "Traefik" }
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -72,6 +80,7 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
|
|||||||
option :grep_options, aliases: "-o", desc: "Additional options supplied to grep"
|
option :grep_options, aliases: "-o", desc: "Additional options supplied to grep"
|
||||||
option :follow, aliases: "-f", desc: "Follow logs on primary server (or specific host set by --hosts)"
|
option :follow, aliases: "-f", desc: "Follow logs on primary server (or specific host set by --hosts)"
|
||||||
def logs
|
def logs
|
||||||
|
raise_if_kamal_proxy_enabled!
|
||||||
grep = options[:grep]
|
grep = options[:grep]
|
||||||
grep_options = options[:grep_options]
|
grep_options = options[:grep_options]
|
||||||
|
|
||||||
@@ -93,6 +102,7 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
|
|||||||
|
|
||||||
desc "remove", "Remove Traefik container and image from servers"
|
desc "remove", "Remove Traefik container and image from servers"
|
||||||
def remove
|
def remove
|
||||||
|
raise_if_kamal_proxy_enabled!
|
||||||
with_lock do
|
with_lock do
|
||||||
stop
|
stop
|
||||||
remove_container
|
remove_container
|
||||||
@@ -102,6 +112,7 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
|
|||||||
|
|
||||||
desc "remove_container", "Remove Traefik container from servers", hide: true
|
desc "remove_container", "Remove Traefik container from servers", hide: true
|
||||||
def remove_container
|
def remove_container
|
||||||
|
raise_if_kamal_proxy_enabled!
|
||||||
with_lock do
|
with_lock do
|
||||||
on(KAMAL.traefik_hosts) do
|
on(KAMAL.traefik_hosts) do
|
||||||
execute *KAMAL.auditor.record("Removed traefik container"), verbosity: :debug
|
execute *KAMAL.auditor.record("Removed traefik container"), verbosity: :debug
|
||||||
@@ -112,6 +123,7 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
|
|||||||
|
|
||||||
desc "remove_image", "Remove Traefik image from servers", hide: true
|
desc "remove_image", "Remove Traefik image from servers", hide: true
|
||||||
def remove_image
|
def remove_image
|
||||||
|
raise_if_kamal_proxy_enabled!
|
||||||
with_lock do
|
with_lock do
|
||||||
on(KAMAL.traefik_hosts) do
|
on(KAMAL.traefik_hosts) do
|
||||||
execute *KAMAL.auditor.record("Removed traefik image"), verbosity: :debug
|
execute *KAMAL.auditor.record("Removed traefik image"), verbosity: :debug
|
||||||
@@ -119,4 +131,11 @@ class Kamal::Cli::Traefik < Kamal::Cli::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def raise_if_kamal_proxy_enabled!
|
||||||
|
if KAMAL.config.proxy.enabled?
|
||||||
|
raise "kamal traefik commands are disabled when experimental proxy support is enabled. Use `kamal proxy` commands instead."
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
require "active_support/core_ext/enumerable"
|
require "active_support/core_ext/enumerable"
|
||||||
require "active_support/core_ext/module/delegation"
|
require "active_support/core_ext/module/delegation"
|
||||||
|
require "active_support/core_ext/object/blank"
|
||||||
|
|
||||||
class Kamal::Commander
|
class Kamal::Commander
|
||||||
attr_accessor :verbosity, :holding_lock, :connected
|
attr_accessor :verbosity, :holding_lock, :connected
|
||||||
delegate :hosts, :roles, :primary_host, :primary_role, :roles_on, :traefik_hosts, :accessory_hosts, to: :specifics
|
delegate :hosts, :roles, :primary_host, :primary_role, :roles_on, :traefik_hosts, :proxy_hosts, :proxy_host?, :accessory_hosts, to: :specifics
|
||||||
|
|
||||||
def initialize
|
def initialize
|
||||||
self.verbosity = :info
|
self.verbosity = :info
|
||||||
@@ -23,6 +24,10 @@ class Kamal::Commander
|
|||||||
@config, @config_kwargs = nil, kwargs
|
@config, @config_kwargs = nil, kwargs
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def configured?
|
||||||
|
@config || @config_kwargs
|
||||||
|
end
|
||||||
|
|
||||||
attr_reader :specific_roles, :specific_hosts
|
attr_reader :specific_roles, :specific_hosts
|
||||||
|
|
||||||
def specific_primary!
|
def specific_primary!
|
||||||
@@ -101,6 +106,10 @@ class Kamal::Commander
|
|||||||
@lock ||= Kamal::Commands::Lock.new(config)
|
@lock ||= Kamal::Commands::Lock.new(config)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def proxy
|
||||||
|
@proxy ||= Kamal::Commands::Proxy.new(config)
|
||||||
|
end
|
||||||
|
|
||||||
def prune
|
def prune
|
||||||
@prune ||= Kamal::Commands::Prune.new(config)
|
@prune ||= Kamal::Commands::Prune.new(config)
|
||||||
end
|
end
|
||||||
@@ -122,6 +131,10 @@ class Kamal::Commander
|
|||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
def traefik_or_proxy(host)
|
||||||
|
proxy_host?(host) ? proxy : traefik
|
||||||
|
end
|
||||||
|
|
||||||
def with_verbosity(level)
|
def with_verbosity(level)
|
||||||
old_level = self.verbosity
|
old_level = self.verbosity
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,15 @@ class Kamal::Commander::Specifics
|
|||||||
config.traefik_hosts & specified_hosts
|
config.traefik_hosts & specified_hosts
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def proxy_hosts
|
||||||
|
config.proxy_hosts
|
||||||
|
end
|
||||||
|
|
||||||
|
def proxy_host?(host)
|
||||||
|
host = host.hostname if host.is_a?(SSHKit::Host)
|
||||||
|
proxy_hosts.include?(host)
|
||||||
|
end
|
||||||
|
|
||||||
def accessory_hosts
|
def accessory_hosts
|
||||||
specific_hosts || config.accessories.flat_map(&:hosts)
|
specific_hosts || config.accessories.flat_map(&:hosts)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
class Kamal::Commands::Accessory < Kamal::Commands::Base
|
class Kamal::Commands::Accessory < Kamal::Commands::Base
|
||||||
attr_reader :accessory_config
|
attr_reader :accessory_config
|
||||||
delegate :service_name, :image, :hosts, :port, :files, :directories, :cmd,
|
delegate :service_name, :image, :hosts, :port, :files, :directories, :cmd,
|
||||||
:publish_args, :env_args, :volume_args, :label_args, :option_args, to: :accessory_config
|
:publish_args, :env_args, :volume_args, :label_args, :option_args,
|
||||||
|
:secrets_io, :secrets_path, :env_directory,
|
||||||
|
to: :accessory_config
|
||||||
|
|
||||||
def initialize(config, name:)
|
def initialize(config, name:)
|
||||||
super(config)
|
super(config)
|
||||||
@@ -13,6 +15,7 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base
|
|||||||
"--name", service_name,
|
"--name", service_name,
|
||||||
"--detach",
|
"--detach",
|
||||||
"--restart", "unless-stopped",
|
"--restart", "unless-stopped",
|
||||||
|
"--network", "kamal",
|
||||||
*config.logging_args,
|
*config.logging_args,
|
||||||
*publish_args,
|
*publish_args,
|
||||||
*env_args,
|
*env_args,
|
||||||
@@ -61,6 +64,7 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base
|
|||||||
docker :run,
|
docker :run,
|
||||||
("-it" if interactive),
|
("-it" if interactive),
|
||||||
"--rm",
|
"--rm",
|
||||||
|
"--network", "kamal",
|
||||||
*env_args,
|
*env_args,
|
||||||
*volume_args,
|
*volume_args,
|
||||||
image,
|
image,
|
||||||
@@ -98,12 +102,8 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base
|
|||||||
docker :image, :rm, "--force", image
|
docker :image, :rm, "--force", image
|
||||||
end
|
end
|
||||||
|
|
||||||
def make_env_directory
|
def ensure_env_directory
|
||||||
make_directory accessory_config.env.secrets_directory
|
make_directory env_directory
|
||||||
end
|
|
||||||
|
|
||||||
def remove_env_file
|
|
||||||
[ :rm, "-f", accessory_config.env.secrets_file ]
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ class Kamal::Commands::App < Kamal::Commands::Base
|
|||||||
|
|
||||||
attr_reader :role, :host
|
attr_reader :role, :host
|
||||||
|
|
||||||
|
delegate :container_name, to: :role
|
||||||
|
|
||||||
def initialize(config, role: nil, host: nil)
|
def initialize(config, role: nil, host: nil)
|
||||||
super(config)
|
super(config)
|
||||||
@role = role
|
@role = role
|
||||||
@@ -30,6 +32,25 @@ class Kamal::Commands::App < Kamal::Commands::Base
|
|||||||
role.cmd
|
role.cmd
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def run_for_proxy(hostname: nil)
|
||||||
|
docker :run,
|
||||||
|
"--detach",
|
||||||
|
"--restart unless-stopped",
|
||||||
|
"--name", container_name,
|
||||||
|
"--network", "kamal",
|
||||||
|
*([ "--hostname", hostname ] if hostname),
|
||||||
|
"-e", "KAMAL_CONTAINER_NAME=\"#{container_name}\"",
|
||||||
|
"-e", "KAMAL_VERSION=\"#{config.version}\"",
|
||||||
|
*role.env_args(host),
|
||||||
|
*role.logging_args,
|
||||||
|
*config.volume_args,
|
||||||
|
*role.asset_volume_args,
|
||||||
|
*role.label_args_for_proxy,
|
||||||
|
*role.option_args,
|
||||||
|
config.absolute_image,
|
||||||
|
role.cmd
|
||||||
|
end
|
||||||
|
|
||||||
def start
|
def start
|
||||||
docker :start, container_name
|
docker :start, container_name
|
||||||
end
|
end
|
||||||
@@ -69,21 +90,11 @@ class Kamal::Commands::App < Kamal::Commands::Base
|
|||||||
extract_version_from_name
|
extract_version_from_name
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def ensure_env_directory
|
||||||
def make_env_directory
|
make_directory role.env_directory
|
||||||
make_directory role.env(host).secrets_directory
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def remove_env_file
|
|
||||||
[ :rm, "-f", role.env(host).secrets_file ]
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
private
|
private
|
||||||
def container_name(version = nil)
|
|
||||||
[ role.container_prefix, version || config.version ].compact.join("-")
|
|
||||||
end
|
|
||||||
|
|
||||||
def latest_image_id
|
def latest_image_id
|
||||||
docker :image, :ls, *argumentize("--filter", "reference=#{config.latest_image}"), "--format", "'{{.ID}}'"
|
docker :image, :ls, *argumentize("--filter", "reference=#{config.latest_image}"), "--format", "'{{.ID}}'"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -8,9 +8,12 @@ class Kamal::Commands::Auditor < Kamal::Commands::Base
|
|||||||
|
|
||||||
# Runs remotely
|
# Runs remotely
|
||||||
def record(line, **details)
|
def record(line, **details)
|
||||||
append \
|
combine \
|
||||||
|
[ :mkdir, "-p", config.run_directory ],
|
||||||
|
append(
|
||||||
[ :echo, audit_tags(**details).except(:version, :service_version, :service).to_s, line ],
|
[ :echo, audit_tags(**details).except(:version, :service_version, :service).to_s, line ],
|
||||||
audit_log_file
|
audit_log_file
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def reveal
|
def reveal
|
||||||
|
|||||||
@@ -37,6 +37,10 @@ module Kamal::Commands
|
|||||||
[ :rm, "-r", path ]
|
[ :rm, "-r", path ]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def remove_file(path)
|
||||||
|
[ :rm, path ]
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def combine(*commands, by: "&&")
|
def combine(*commands, by: "&&")
|
||||||
commands
|
commands
|
||||||
@@ -81,6 +85,10 @@ module Kamal::Commands
|
|||||||
[ :git, *([ "-C", path ] if path), *args.compact ]
|
[ :git, *([ "-C", path ] if path), *args.compact ]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def grep(*args)
|
||||||
|
args.compact.unshift :grep
|
||||||
|
end
|
||||||
|
|
||||||
def tags(**details)
|
def tags(**details)
|
||||||
Kamal::Tags.from_config(config, **details)
|
Kamal::Tags.from_config(config, **details)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
require "active_support/core_ext/string/filters"
|
require "active_support/core_ext/string/filters"
|
||||||
|
|
||||||
class Kamal::Commands::Builder < Kamal::Commands::Base
|
class Kamal::Commands::Builder < Kamal::Commands::Base
|
||||||
delegate :create, :remove, :push, :clean, :pull, :info, :buildx_inspect, :validate_image, :first_mirror, to: :target
|
delegate :create, :remove, :push, :clean, :pull, :info, :inspect_builder, :validate_image, :first_mirror, to: :target
|
||||||
delegate :local?, :remote?, to: "config.builder"
|
delegate :local?, :remote?, to: "config.builder"
|
||||||
|
|
||||||
include Clone
|
include Clone
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
|
|||||||
docker(:buildx, :ls)
|
docker(:buildx, :ls)
|
||||||
end
|
end
|
||||||
|
|
||||||
def buildx_inspect
|
def inspect_builder
|
||||||
docker :buildx, :inspect, builder_name unless docker_driver?
|
docker :buildx, :inspect, builder_name unless docker_driver?
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def build_secrets
|
def build_secrets
|
||||||
argumentize "--secret", secrets.collect { |secret| [ "id", secret ] }
|
argumentize "--secret", secrets.keys.collect { |secret| [ "id", secret ] }
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_dockerfile
|
def build_dockerfile
|
||||||
@@ -101,10 +101,6 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
|
|||||||
config.builder
|
config.builder
|
||||||
end
|
end
|
||||||
|
|
||||||
def context_host(builder_name)
|
|
||||||
docker :context, :inspect, builder_name, "--format", ENDPOINT_DOCKER_HOST_INSPECT
|
|
||||||
end
|
|
||||||
|
|
||||||
def platform_options(arches)
|
def platform_options(arches)
|
||||||
argumentize "--platform", arches.map { |arch| "linux/#{arch}" }.join(",") if arches.any?
|
argumentize "--platform", arches.map { |arch| "linux/#{arch}" }.join(",") if arches.any?
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -16,6 +16,6 @@ class Kamal::Commands::Builder::Hybrid < Kamal::Commands::Builder::Remote
|
|||||||
end
|
end
|
||||||
|
|
||||||
def append_remote_buildx
|
def append_remote_buildx
|
||||||
docker :buildx, :create, *platform_options(remote_arches), "--append", "--name", builder_name, builder_name
|
docker :buildx, :create, *platform_options(remote_arches), "--append", "--name", builder_name, remote_context_name
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -17,21 +17,44 @@ class Kamal::Commands::Builder::Remote < Kamal::Commands::Builder::Base
|
|||||||
docker(:buildx, :ls)
|
docker(:buildx, :ls)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def inspect_builder
|
||||||
|
combine \
|
||||||
|
combine inspect_buildx, inspect_remote_context,
|
||||||
|
[ "(echo no compatible builder && exit 1)" ],
|
||||||
|
by: "||"
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def builder_name
|
def builder_name
|
||||||
"kamal-remote-#{driver}-#{remote.gsub(/[^a-z0-9_-]/, "-")}"
|
"kamal-remote-#{remote.gsub(/[^a-z0-9_-]/, "-")}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def remote_context_name
|
||||||
|
"#{builder_name}-context"
|
||||||
|
end
|
||||||
|
|
||||||
|
def inspect_buildx
|
||||||
|
pipe \
|
||||||
|
docker(:buildx, :inspect, builder_name),
|
||||||
|
grep("-q", "Endpoint:.*#{remote_context_name}")
|
||||||
|
end
|
||||||
|
|
||||||
|
def inspect_remote_context
|
||||||
|
pipe \
|
||||||
|
docker(:context, :inspect, remote_context_name, "--format", ENDPOINT_DOCKER_HOST_INSPECT),
|
||||||
|
grep("-xq", remote)
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_remote_context
|
def create_remote_context
|
||||||
docker :context, :create, builder_name, "--description", "'#{builder_name} host'", "--docker", "'host=#{remote}'"
|
docker :context, :create, remote_context_name, "--description", "'#{builder_name} host'", "--docker", "'host=#{remote}'"
|
||||||
end
|
end
|
||||||
|
|
||||||
def remove_remote_context
|
def remove_remote_context
|
||||||
docker :context, :rm, builder_name
|
docker :context, :rm, remote_context_name
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_buildx
|
def create_buildx
|
||||||
docker :buildx, :create, "--name", builder_name, builder_name
|
docker :buildx, :create, "--name", builder_name, remote_context_name
|
||||||
end
|
end
|
||||||
|
|
||||||
def remove_buildx
|
def remove_buildx
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ class Kamal::Commands::Docker < Kamal::Commands::Base
|
|||||||
[ '[ "${EUID:-$(id -u)}" -eq 0 ] || command -v sudo >/dev/null || command -v su >/dev/null' ]
|
[ '[ "${EUID:-$(id -u)}" -eq 0 ] || command -v sudo >/dev/null || command -v su >/dev/null' ]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def create_network
|
||||||
|
docker :network, :create, :kamal
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def get_docker
|
def get_docker
|
||||||
shell \
|
shell \
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
class Kamal::Commands::Hook < Kamal::Commands::Base
|
class Kamal::Commands::Hook < Kamal::Commands::Base
|
||||||
def run(hook, **details)
|
def run(hook, secrets: false, **details)
|
||||||
[ hook_file(hook), env: tags(**details).env ]
|
env = tags(**details).env
|
||||||
|
env.merge!(config.secrets.to_h) if secrets
|
||||||
|
|
||||||
|
[ hook_file(hook), env: env ]
|
||||||
end
|
end
|
||||||
|
|
||||||
def hook_exists?(hook)
|
def hook_exists?(hook)
|
||||||
|
|||||||
69
lib/kamal/commands/proxy.rb
Normal file
69
lib/kamal/commands/proxy.rb
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
class Kamal::Commands::Proxy < Kamal::Commands::Base
|
||||||
|
delegate :argumentize, :optionize, to: Kamal::Utils
|
||||||
|
delegate :container_name, :app_port, to: :proxy_config
|
||||||
|
|
||||||
|
attr_reader :proxy_config
|
||||||
|
|
||||||
|
def initialize(config)
|
||||||
|
super
|
||||||
|
@proxy_config = config.proxy
|
||||||
|
end
|
||||||
|
|
||||||
|
def run
|
||||||
|
docker :run,
|
||||||
|
"--name", container_name,
|
||||||
|
"--network", "kamal",
|
||||||
|
"--detach",
|
||||||
|
"--restart", "unless-stopped",
|
||||||
|
*proxy_config.publish_args,
|
||||||
|
"--volume", "/var/run/docker.sock:/var/run/docker.sock",
|
||||||
|
"--volume", "#{proxy_config.config_directory_as_docker_volume}:/root/.config/kamal-proxy",
|
||||||
|
*config.logging_args,
|
||||||
|
proxy_config.image
|
||||||
|
end
|
||||||
|
|
||||||
|
def start
|
||||||
|
docker :container, :start, container_name
|
||||||
|
end
|
||||||
|
|
||||||
|
def stop(name: container_name)
|
||||||
|
docker :container, :stop, name
|
||||||
|
end
|
||||||
|
|
||||||
|
def start_or_run
|
||||||
|
combine start, run, by: "||"
|
||||||
|
end
|
||||||
|
|
||||||
|
def deploy(service, target:)
|
||||||
|
docker :exec, container_name, "kamal-proxy", :deploy, service, *optionize({ target: "#{target}:#{app_port}" }), *proxy_config.deploy_command_args
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove(service, target:)
|
||||||
|
docker :exec, container_name, "kamal-proxy", :remove, service, *optionize({ target: "#{target}:#{app_port}" })
|
||||||
|
end
|
||||||
|
|
||||||
|
def info
|
||||||
|
docker :ps, "--filter", "name=^#{container_name}$"
|
||||||
|
end
|
||||||
|
|
||||||
|
def logs(since: nil, lines: nil, grep: nil, grep_options: nil)
|
||||||
|
pipe \
|
||||||
|
docker(:logs, container_name, (" --since #{since}" if since), (" --tail #{lines}" if lines), "--timestamps", "2>&1"),
|
||||||
|
("grep '#{grep}'#{" #{grep_options}" if grep_options}" if grep)
|
||||||
|
end
|
||||||
|
|
||||||
|
def follow_logs(host:, grep: nil, grep_options: nil)
|
||||||
|
run_over_ssh pipe(
|
||||||
|
docker(:logs, container_name, "--timestamps", "--tail", "10", "--follow", "2>&1"),
|
||||||
|
(%(grep "#{grep}"#{" #{grep_options}" if grep_options}) if grep)
|
||||||
|
).join(" "), host: host
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_container
|
||||||
|
docker :container, :prune, "--force", "--filter", "label=org.opencontainers.image.title=kamal-proxy"
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_image
|
||||||
|
docker :image, :prune, "--all", "--force", "--filter", "label=org.opencontainers.image.title=kamal-proxy"
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -9,7 +9,7 @@ class Kamal::Commands::Prune < Kamal::Commands::Base
|
|||||||
def tagged_images
|
def tagged_images
|
||||||
pipe \
|
pipe \
|
||||||
docker(:image, :ls, *service_filter, "--format", "'{{.ID}} {{.Repository}}:{{.Tag}}'"),
|
docker(:image, :ls, *service_filter, "--format", "'{{.ID}} {{.Repository}}:{{.Tag}}'"),
|
||||||
"grep -v -w \"#{active_image_list}\"",
|
grep("-v -w \"#{active_image_list}\""),
|
||||||
"while read image tag; do docker rmi $tag; done"
|
"while read image tag; do docker rmi $tag; done"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
class Kamal::Commands::Traefik < Kamal::Commands::Base
|
class Kamal::Commands::Traefik < Kamal::Commands::Base
|
||||||
delegate :argumentize, :optionize, to: Kamal::Utils
|
delegate :argumentize, :optionize, to: Kamal::Utils
|
||||||
delegate :port, :publish?, :labels, :env, :image, :options, :args, to: :"config.traefik"
|
delegate :port, :publish?, :labels, :env, :image, :options, :args, :env_args, :secrets_io, :env_directory, :secrets_path, to: :"config.traefik"
|
||||||
|
|
||||||
def run
|
def run
|
||||||
docker :run, "--name traefik",
|
docker :run, "--name traefik",
|
||||||
@@ -54,12 +54,8 @@ class Kamal::Commands::Traefik < Kamal::Commands::Base
|
|||||||
docker :image, :prune, "--all", "--force", "--filter", "label=org.opencontainers.image.title=Traefik"
|
docker :image, :prune, "--all", "--force", "--filter", "label=org.opencontainers.image.title=Traefik"
|
||||||
end
|
end
|
||||||
|
|
||||||
def make_env_directory
|
def ensure_env_directory
|
||||||
make_directory(env.secrets_directory)
|
make_directory env_directory
|
||||||
end
|
|
||||||
|
|
||||||
def remove_env_file
|
|
||||||
[ :rm, "-f", env.secrets_file ]
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@@ -71,10 +67,6 @@ class Kamal::Commands::Traefik < Kamal::Commands::Base
|
|||||||
argumentize "--label", labels
|
argumentize "--label", labels
|
||||||
end
|
end
|
||||||
|
|
||||||
def env_args
|
|
||||||
env.args
|
|
||||||
end
|
|
||||||
|
|
||||||
def docker_options_args
|
def docker_options_args
|
||||||
optionize(options)
|
optionize(options)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ require "active_support/ordered_options"
|
|||||||
require "active_support/core_ext/string/inquiry"
|
require "active_support/core_ext/string/inquiry"
|
||||||
require "active_support/core_ext/module/delegation"
|
require "active_support/core_ext/module/delegation"
|
||||||
require "active_support/core_ext/hash/keys"
|
require "active_support/core_ext/hash/keys"
|
||||||
require "pathname"
|
|
||||||
require "erb"
|
require "erb"
|
||||||
require "net/ssh/proxy/jump"
|
require "net/ssh/proxy/jump"
|
||||||
|
|
||||||
@@ -11,7 +10,7 @@ class Kamal::Configuration
|
|||||||
delegate :argumentize, :optionize, to: Kamal::Utils
|
delegate :argumentize, :optionize, to: Kamal::Utils
|
||||||
|
|
||||||
attr_reader :destination, :raw_config
|
attr_reader :destination, :raw_config
|
||||||
attr_reader :accessories, :aliases, :boot, :builder, :env, :healthcheck, :logging, :traefik, :servers, :ssh, :sshkit, :registry
|
attr_reader :accessories, :aliases, :boot, :builder, :env, :healthcheck, :logging, :proxy, :traefik, :servers, :ssh, :sshkit, :registry
|
||||||
|
|
||||||
include Validation
|
include Validation
|
||||||
|
|
||||||
@@ -57,10 +56,11 @@ class Kamal::Configuration
|
|||||||
@aliases = @raw_config.aliases&.keys&.to_h { |name| [ name, Alias.new(name, config: self) ] } || {}
|
@aliases = @raw_config.aliases&.keys&.to_h { |name| [ name, Alias.new(name, config: self) ] } || {}
|
||||||
@boot = Boot.new(config: self)
|
@boot = Boot.new(config: self)
|
||||||
@builder = Builder.new(config: self)
|
@builder = Builder.new(config: self)
|
||||||
@env = Env.new(config: @raw_config.env || {})
|
@env = Env.new(config: @raw_config.env || {}, secrets: secrets)
|
||||||
|
|
||||||
@healthcheck = Healthcheck.new(healthcheck_config: @raw_config.healthcheck)
|
@healthcheck = Healthcheck.new(healthcheck_config: @raw_config.healthcheck)
|
||||||
@logging = Logging.new(logging_config: @raw_config.logging)
|
@logging = Logging.new(logging_config: @raw_config.logging)
|
||||||
|
@proxy = Proxy.new(config: self)
|
||||||
@traefik = Traefik.new(config: self)
|
@traefik = Traefik.new(config: self)
|
||||||
@ssh = Ssh.new(config: self)
|
@ssh = Ssh.new(config: self)
|
||||||
@sshkit = Sshkit.new(config: self)
|
@sshkit = Sshkit.new(config: self)
|
||||||
@@ -142,6 +142,10 @@ class Kamal::Configuration
|
|||||||
traefik_roles.flat_map(&:hosts).uniq
|
traefik_roles.flat_map(&:hosts).uniq
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def proxy_hosts
|
||||||
|
proxy.hosts
|
||||||
|
end
|
||||||
|
|
||||||
def repository
|
def repository
|
||||||
[ registry.server, image ].compact.join("/")
|
[ registry.server, image ].compact.join("/")
|
||||||
end
|
end
|
||||||
@@ -198,16 +202,12 @@ class Kamal::Configuration
|
|||||||
|
|
||||||
|
|
||||||
def run_directory
|
def run_directory
|
||||||
raw_config.run_directory || ".kamal"
|
".kamal"
|
||||||
end
|
end
|
||||||
|
|
||||||
def run_directory_as_docker_volume
|
def run_directory_as_docker_volume
|
||||||
if Pathname.new(run_directory).absolute?
|
|
||||||
run_directory
|
|
||||||
else
|
|
||||||
File.join "$(pwd)", run_directory
|
File.join "$(pwd)", run_directory
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
def hooks_path
|
def hooks_path
|
||||||
raw_config.hooks_path || ".kamal/hooks"
|
raw_config.hooks_path || ".kamal/hooks"
|
||||||
@@ -218,13 +218,13 @@ class Kamal::Configuration
|
|||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
def host_env_directory
|
def env_directory
|
||||||
File.join(run_directory, "env")
|
File.join(run_directory, "env")
|
||||||
end
|
end
|
||||||
|
|
||||||
def env_tags
|
def env_tags
|
||||||
@env_tags ||= if (tags = raw_config.env["tags"])
|
@env_tags ||= if (tags = raw_config.env["tags"])
|
||||||
tags.collect { |name, config| Env::Tag.new(name, config: config) }
|
tags.collect { |name, config| Env::Tag.new(name, config: config, secrets: secrets) }
|
||||||
else
|
else
|
||||||
[]
|
[]
|
||||||
end
|
end
|
||||||
@@ -254,6 +254,10 @@ class Kamal::Configuration
|
|||||||
}.compact
|
}.compact
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def secrets
|
||||||
|
@secrets ||= Kamal::Secrets.new(destination: destination)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
# Will raise ArgumentError if any required config keys are missing
|
# Will raise ArgumentError if any required config keys are missing
|
||||||
def ensure_destination_if_required
|
def ensure_destination_if_required
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class Kamal::Configuration::Accessory
|
|||||||
|
|
||||||
@env = Kamal::Configuration::Env.new \
|
@env = Kamal::Configuration::Env.new \
|
||||||
config: accessory_config.fetch("env", {}),
|
config: accessory_config.fetch("env", {}),
|
||||||
secrets_file: File.join(config.host_env_directory, "accessories", "#{service_name}.env"),
|
secrets: config.secrets,
|
||||||
context: "accessories/#{name}/env"
|
context: "accessories/#{name}/env"
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -51,7 +51,19 @@ class Kamal::Configuration::Accessory
|
|||||||
end
|
end
|
||||||
|
|
||||||
def env_args
|
def env_args
|
||||||
env.args
|
[ *env.clear_args, *argumentize("--env-file", secrets_path) ]
|
||||||
|
end
|
||||||
|
|
||||||
|
def env_directory
|
||||||
|
File.join(config.env_directory, "accessories")
|
||||||
|
end
|
||||||
|
|
||||||
|
def secrets_io
|
||||||
|
env.secrets_io
|
||||||
|
end
|
||||||
|
|
||||||
|
def secrets_path
|
||||||
|
File.join(config.env_directory, "accessories", "#{service_name}.env")
|
||||||
end
|
end
|
||||||
|
|
||||||
def files
|
def files
|
||||||
|
|||||||
@@ -28,7 +28,9 @@ class Kamal::Configuration::Builder
|
|||||||
end
|
end
|
||||||
|
|
||||||
def local_arches
|
def local_arches
|
||||||
@local_arches ||= if remote
|
@local_arches ||= if local_disabled?
|
||||||
|
[]
|
||||||
|
elsif remote
|
||||||
arches & [ Kamal::Utils.docker_arch ]
|
arches & [ Kamal::Utils.docker_arch ]
|
||||||
else
|
else
|
||||||
arches
|
arches
|
||||||
@@ -48,7 +50,7 @@ class Kamal::Configuration::Builder
|
|||||||
end
|
end
|
||||||
|
|
||||||
def local?
|
def local?
|
||||||
arches.empty? || local_arches.any?
|
!local_disabled? && (arches.empty? || local_arches.any?)
|
||||||
end
|
end
|
||||||
|
|
||||||
def cached?
|
def cached?
|
||||||
@@ -60,7 +62,7 @@ class Kamal::Configuration::Builder
|
|||||||
end
|
end
|
||||||
|
|
||||||
def secrets
|
def secrets
|
||||||
builder_config["secrets"] || []
|
(builder_config["secrets"] || []).to_h { |key| [ key, config.secrets[key] ] }
|
||||||
end
|
end
|
||||||
|
|
||||||
def dockerfile
|
def dockerfile
|
||||||
@@ -79,6 +81,10 @@ class Kamal::Configuration::Builder
|
|||||||
builder_config.fetch("driver", "docker-container")
|
builder_config.fetch("driver", "docker-container")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def local_disabled?
|
||||||
|
builder_config["local"] == false
|
||||||
|
end
|
||||||
|
|
||||||
def cache_from
|
def cache_from
|
||||||
if cached?
|
if cached?
|
||||||
case builder_config["cache"]["type"]
|
case builder_config["cache"]["type"]
|
||||||
|
|||||||
@@ -30,6 +30,11 @@ builder:
|
|||||||
# If you have a remote builder, you can configure it here
|
# If you have a remote builder, you can configure it here
|
||||||
remote: ssh://docker@docker-builder
|
remote: ssh://docker@docker-builder
|
||||||
|
|
||||||
|
# Whether to allow local builds
|
||||||
|
#
|
||||||
|
# Defaults to true
|
||||||
|
local: true
|
||||||
|
|
||||||
# Builder cache
|
# Builder cache
|
||||||
#
|
#
|
||||||
# The type must be either 'gha' or 'registry'
|
# The type must be either 'gha' or 'registry'
|
||||||
|
|||||||
@@ -143,6 +143,12 @@ accessories:
|
|||||||
traefik:
|
traefik:
|
||||||
...
|
...
|
||||||
|
|
||||||
|
# Proxy
|
||||||
|
#
|
||||||
|
# **Experimental** Configuration for kamal-proxy the replacement for Traefik, see kamal docs proxy
|
||||||
|
proxy:
|
||||||
|
...
|
||||||
|
|
||||||
# SSHKit
|
# SSHKit
|
||||||
#
|
#
|
||||||
# See kamal docs sshkit
|
# See kamal docs sshkit
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Environment variables
|
# Environment variables
|
||||||
#
|
#
|
||||||
# Environment variables can be set directory in the Kamal configuration or
|
# Environment variables can be set directly in the Kamal configuration or
|
||||||
# for loaded from a .env file, for secrets that should not be checked into Git.
|
# loaded from a .env file, for secrets that should not be checked into Git.
|
||||||
|
|
||||||
# Reading environment variables from the configuration
|
# Reading environment variables from the configuration
|
||||||
#
|
#
|
||||||
@@ -24,14 +24,12 @@ env:
|
|||||||
# KAMAL_REGISTRY_PASSWORD=pw
|
# KAMAL_REGISTRY_PASSWORD=pw
|
||||||
# DB_PASSWORD=secret123
|
# DB_PASSWORD=secret123
|
||||||
# ```
|
# ```
|
||||||
# See https://kamal-deploy.org/docs/commands/envify/ for how to use generated .env files.
|
|
||||||
#
|
#
|
||||||
# To pass the secrets you should list them under the `secret` key. When you do this the
|
# To pass the secrets you should list them under the `secret` key. When you do this the
|
||||||
# other variables need to be moved under the `clear` key.
|
# other variables need to be moved under the `clear` key.
|
||||||
#
|
#
|
||||||
# Unlike clear values, secrets are not passed directly to the container,
|
# Unlike clear values, secrets are not passed directly to the container,
|
||||||
# but are stored in an env file on the host
|
# but are stored in an env file on the host
|
||||||
# The file is not updated when deploying, only when running `kamal envify` or `kamal env push`.
|
|
||||||
env:
|
env:
|
||||||
clear:
|
clear:
|
||||||
DB_USER: app
|
DB_USER: app
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
# On roles that are running Traefik, Kamal will supply a default healthcheck to `docker run`.
|
# On roles that are running Traefik, Kamal will supply a default healthcheck to `docker run`.
|
||||||
# For other roles, by default no healthcheck is supplied.
|
# For other roles, by default no healthcheck is supplied.
|
||||||
#
|
#
|
||||||
# If no healthcheck is supplied and the image does not define one, they we wait for the container
|
# If no healthcheck is supplied and the image does not define one, then we wait for the container
|
||||||
# to reach a running state and then pause for the readiness delay.
|
# to reach a running state and then pause for the readiness delay.
|
||||||
#
|
#
|
||||||
# The default healthcheck is `curl -f http://localhost:<port>/<path>`, so it assumes that `curl`
|
# The default healthcheck is `curl -f http://localhost:<port>/<path>`, so it assumes that `curl`
|
||||||
|
|||||||
127
lib/kamal/configuration/docs/proxy.yml
Normal file
127
lib/kamal/configuration/docs/proxy.yml
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
# Proxy
|
||||||
|
#
|
||||||
|
# **Experimental** [kamal-proxy](http://github.com/basecamp/kamal-proxy) is a
|
||||||
|
# custom built specifically for Kamal. It will replace Traefik in Kamal v2.0,
|
||||||
|
# but currently is available as an experimental feature.
|
||||||
|
#
|
||||||
|
# When this is enabled, the proxy will be started on the hosts listed under the hosts key.
|
||||||
|
# In addition, the kamal traefik command will be disabled and replaced by kamal proxy.
|
||||||
|
#
|
||||||
|
# The kamal proxy command works identically to kamal traefik on hosts that have not
|
||||||
|
# been included. It will also handle switching between Traefik and kamal-proxy when you
|
||||||
|
# run kamal proxy reboot.
|
||||||
|
|
||||||
|
# Limitations
|
||||||
|
#
|
||||||
|
# Currently the proxy will run on ports 80 and 443 and will bind to those
|
||||||
|
# ports on the host.
|
||||||
|
#
|
||||||
|
# There is no way to set custom options for `docker run` when booting the proxy.
|
||||||
|
#
|
||||||
|
# If you have custom Traefik configuration via labels or boot arguments they may
|
||||||
|
# not have an equivalent in kamal-proxy.
|
||||||
|
|
||||||
|
# Proxy settings
|
||||||
|
#
|
||||||
|
# The proxy is configured in the root configuration under `traefik`. These are
|
||||||
|
# options that are set when deploying the application, not when booting the proxy
|
||||||
|
#
|
||||||
|
# They are application specific, so are not shared when multiple applications
|
||||||
|
# with the same proxy.
|
||||||
|
proxy:
|
||||||
|
|
||||||
|
# Enabled
|
||||||
|
#
|
||||||
|
# Whether to enable experimental proxy support. Defaults to false
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
# Hosts
|
||||||
|
#
|
||||||
|
# The hosts to run the proxy on, instead of Traefik
|
||||||
|
# This is a temporary setting and will be removed when we full switch to kamal-proxy
|
||||||
|
#
|
||||||
|
# If you run `kamal traefik reboot`, then the proxy will be started on these hosts
|
||||||
|
# in place of traefik.
|
||||||
|
hosts:
|
||||||
|
- 10.0.0.1
|
||||||
|
- 10.0.0.2
|
||||||
|
|
||||||
|
# Host
|
||||||
|
#
|
||||||
|
# This is the host that will be used to serve the app. By setting this you can run
|
||||||
|
# multiple apps on the same server sharing the same instance of the proxy.
|
||||||
|
#
|
||||||
|
# If this is set only requests that match this host will be forwarded by the proxy.
|
||||||
|
# if this is not set, then all requests will be forwarded, except for matching
|
||||||
|
# requests for other apps that do have a host set.
|
||||||
|
host: foo.example.com
|
||||||
|
|
||||||
|
# App port
|
||||||
|
#
|
||||||
|
# The port the application container is exposed on
|
||||||
|
# Defaults to 80
|
||||||
|
app_port: 3000
|
||||||
|
|
||||||
|
# SSL
|
||||||
|
#
|
||||||
|
# Kamal Proxy can automatically obtain and renew TLS certificates for your applications.
|
||||||
|
# To ensure this set, the ssl flag. This only works if we are deploying to one server and
|
||||||
|
# the host flag is set.
|
||||||
|
ssl: true
|
||||||
|
|
||||||
|
# Deploy timeout
|
||||||
|
#
|
||||||
|
# How long to wait for the app to boot when deploying, defaults to 30 seconds
|
||||||
|
deploy_timeout: 10s
|
||||||
|
|
||||||
|
# Response timeout
|
||||||
|
#
|
||||||
|
# How long to wait for requests to complete before timing out, defaults to 30 seconds
|
||||||
|
response_timeout: 10
|
||||||
|
|
||||||
|
# Healthcheck
|
||||||
|
#
|
||||||
|
# When deploying, the proxy will by default hit /up once every second until we hit
|
||||||
|
# the deploy timeout, with a 5 second timeout for each request.
|
||||||
|
#
|
||||||
|
# Once the app is up, the proxy will stop hitting the healthcheck endpoint.
|
||||||
|
healthcheck:
|
||||||
|
interval: 3
|
||||||
|
path: /health
|
||||||
|
timeout: 3
|
||||||
|
|
||||||
|
# Buffering
|
||||||
|
#
|
||||||
|
# Whether to buffer request and response bodies in the proxy
|
||||||
|
#
|
||||||
|
# By default buffering is enabled with a max request body size of 1GB and no limit
|
||||||
|
# for response size.
|
||||||
|
#
|
||||||
|
# You can also set the memory limit for buffering, which defaults to 1MB, anything
|
||||||
|
# larger than that is written to disk.
|
||||||
|
buffering:
|
||||||
|
requests: true
|
||||||
|
responses: true
|
||||||
|
max_request_body: 40_000_000
|
||||||
|
max_response_body: 0
|
||||||
|
memory: 2_000_000
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
#
|
||||||
|
# Configure request logging for the proxy
|
||||||
|
# You can specify request and response headers to log.
|
||||||
|
# By default, Cache-Control and Last-Modified request headers are logged
|
||||||
|
logging:
|
||||||
|
request_headers:
|
||||||
|
- Cache-Control
|
||||||
|
- X-Forwarded-Proto
|
||||||
|
response_headers:
|
||||||
|
- X-Request-ID
|
||||||
|
- X-Request-Start
|
||||||
|
|
||||||
|
# Forward headers
|
||||||
|
#
|
||||||
|
# Whether to forward the X-Forwarded-For and X-Forwarded-Proto headers (defaults to false)
|
||||||
|
#
|
||||||
|
# If you are behind a trusted proxy, you can set this to true to forward the headers.
|
||||||
|
forward_headers: true
|
||||||
@@ -1,36 +1,29 @@
|
|||||||
class Kamal::Configuration::Env
|
class Kamal::Configuration::Env
|
||||||
include Kamal::Configuration::Validation
|
include Kamal::Configuration::Validation
|
||||||
|
|
||||||
attr_reader :secrets_keys, :clear, :secrets_file, :context
|
attr_reader :context, :secrets
|
||||||
|
attr_reader :clear, :secret_keys
|
||||||
delegate :argumentize, to: Kamal::Utils
|
delegate :argumentize, to: Kamal::Utils
|
||||||
|
|
||||||
def initialize(config:, secrets_file: nil, context: "env")
|
def initialize(config:, secrets:, context: "env")
|
||||||
@clear = config.fetch("clear", config.key?("secret") || config.key?("tags") ? {} : config)
|
@clear = config.fetch("clear", config.key?("secret") || config.key?("tags") ? {} : config)
|
||||||
@secrets_keys = config.fetch("secret", [])
|
@secrets = secrets
|
||||||
@secrets_file = secrets_file
|
@secret_keys = config.fetch("secret", [])
|
||||||
@context = context
|
@context = context
|
||||||
validate! config, context: context, with: Kamal::Configuration::Validator::Env
|
validate! config, context: context, with: Kamal::Configuration::Validator::Env
|
||||||
end
|
end
|
||||||
|
|
||||||
def args
|
def clear_args
|
||||||
[ "--env-file", secrets_file, *argumentize("--env", clear) ]
|
argumentize("--env", clear)
|
||||||
end
|
end
|
||||||
|
|
||||||
def secrets_io
|
def secrets_io
|
||||||
StringIO.new(Kamal::EnvFile.new(secrets).to_s)
|
Kamal::EnvFile.new(secret_keys.to_h { |key| [ key, secrets[key] ] }).to_io
|
||||||
end
|
|
||||||
|
|
||||||
def secrets
|
|
||||||
@secrets ||= secrets_keys.to_h { |key| [ key, ENV.fetch(key) ] }
|
|
||||||
end
|
|
||||||
|
|
||||||
def secrets_directory
|
|
||||||
File.dirname(secrets_file)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def merge(other)
|
def merge(other)
|
||||||
self.class.new \
|
self.class.new \
|
||||||
config: { "clear" => clear.merge(other.clear), "secret" => secrets_keys | other.secrets_keys },
|
config: { "clear" => clear.merge(other.clear), "secret" => secret_keys | other.secret_keys },
|
||||||
secrets_file: secrets_file || other.secrets_file
|
secrets: secrets
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
7
lib/kamal/configuration/env/tag.rb
vendored
7
lib/kamal/configuration/env/tag.rb
vendored
@@ -1,12 +1,13 @@
|
|||||||
class Kamal::Configuration::Env::Tag
|
class Kamal::Configuration::Env::Tag
|
||||||
attr_reader :name, :config
|
attr_reader :name, :config, :secrets
|
||||||
|
|
||||||
def initialize(name, config:)
|
def initialize(name, config:, secrets:)
|
||||||
@name = name
|
@name = name
|
||||||
@config = config
|
@config = config
|
||||||
|
@secrets = secrets
|
||||||
end
|
end
|
||||||
|
|
||||||
def env
|
def env
|
||||||
Kamal::Configuration::Env.new(config: config)
|
Kamal::Configuration::Env.new(config: config, secrets: secrets)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
80
lib/kamal/configuration/proxy.rb
Normal file
80
lib/kamal/configuration/proxy.rb
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
class Kamal::Configuration::Proxy
|
||||||
|
include Kamal::Configuration::Validation
|
||||||
|
|
||||||
|
DEFAULT_HTTP_PORT = 80
|
||||||
|
DEFAULT_HTTPS_PORT = 443
|
||||||
|
DEFAULT_IMAGE = "basecamp/kamal-proxy:latest"
|
||||||
|
DEFAULT_LOG_REQUEST_HEADERS = [ "Cache-Control", "Last-Modified" ]
|
||||||
|
|
||||||
|
delegate :argumentize, :optionize, to: Kamal::Utils
|
||||||
|
|
||||||
|
def initialize(config:)
|
||||||
|
@config = config
|
||||||
|
@proxy_config = config.raw_config.proxy || {}
|
||||||
|
validate! proxy_config, with: Kamal::Configuration::Validator::Proxy
|
||||||
|
end
|
||||||
|
|
||||||
|
def enabled?
|
||||||
|
!!proxy_config.fetch("enabled", false)
|
||||||
|
end
|
||||||
|
|
||||||
|
def hosts
|
||||||
|
if enabled?
|
||||||
|
proxy_config.fetch("hosts", [])
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def app_port
|
||||||
|
proxy_config.fetch("app_port", 80)
|
||||||
|
end
|
||||||
|
|
||||||
|
def image
|
||||||
|
proxy_config.fetch("image", DEFAULT_IMAGE)
|
||||||
|
end
|
||||||
|
|
||||||
|
def container_name
|
||||||
|
"kamal-proxy"
|
||||||
|
end
|
||||||
|
|
||||||
|
def publish_args
|
||||||
|
argumentize "--publish", [ "#{DEFAULT_HTTP_PORT}:#{DEFAULT_HTTP_PORT}", "#{DEFAULT_HTTPS_PORT}:#{DEFAULT_HTTPS_PORT}" ]
|
||||||
|
end
|
||||||
|
|
||||||
|
def ssl?
|
||||||
|
proxy_config.fetch("ssl", false)
|
||||||
|
end
|
||||||
|
|
||||||
|
def deploy_options
|
||||||
|
{
|
||||||
|
host: proxy_config["host"],
|
||||||
|
tls: proxy_config["ssl"],
|
||||||
|
"deploy-timeout": proxy_config["deploy_timeout"],
|
||||||
|
"drain-timeout": proxy_config["drain_timeout"],
|
||||||
|
"health-check-interval": proxy_config.dig("health_check", "interval"),
|
||||||
|
"health-check-timeout": proxy_config.dig("health_check", "timeout"),
|
||||||
|
"health-check-path": proxy_config.dig("health_check", "path"),
|
||||||
|
"target-timeout": proxy_config["response_timeout"],
|
||||||
|
"buffer-requests": proxy_config.fetch("buffering", { "requests": true }).fetch("requests", true),
|
||||||
|
"buffer-responses": proxy_config.fetch("buffering", { "responses": true }).fetch("responses", true),
|
||||||
|
"buffer-memory": proxy_config.dig("buffering", "memory"),
|
||||||
|
"max-request-body": proxy_config.dig("buffering", "max_request_body"),
|
||||||
|
"max-response-body": proxy_config.dig("buffering", "max_response_body"),
|
||||||
|
"forward-headers": proxy_config.dig("forward_headers"),
|
||||||
|
"log-request-header": proxy_config.dig("logging", "request_headers") || DEFAULT_LOG_REQUEST_HEADERS,
|
||||||
|
"log-response-header": proxy_config.dig("logging", "response_headers")
|
||||||
|
}.compact
|
||||||
|
end
|
||||||
|
|
||||||
|
def deploy_command_args
|
||||||
|
optionize deploy_options
|
||||||
|
end
|
||||||
|
|
||||||
|
def config_directory_as_docker_volume
|
||||||
|
File.join config.run_directory_as_docker_volume, "proxy", "config"
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
attr_reader :config, :proxy_config
|
||||||
|
end
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
class Kamal::Configuration::Registry
|
class Kamal::Configuration::Registry
|
||||||
include Kamal::Configuration::Validation
|
include Kamal::Configuration::Validation
|
||||||
|
|
||||||
attr_reader :registry_config
|
attr_reader :registry_config, :secrets
|
||||||
|
|
||||||
def initialize(config:)
|
def initialize(config:)
|
||||||
@registry_config = config.raw_config.registry || {}
|
@registry_config = config.raw_config.registry || {}
|
||||||
|
@secrets = config.secrets
|
||||||
validate! registry_config, with: Kamal::Configuration::Validator::Registry
|
validate! registry_config, with: Kamal::Configuration::Validator::Registry
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -23,7 +24,7 @@ class Kamal::Configuration::Registry
|
|||||||
private
|
private
|
||||||
def lookup(key)
|
def lookup(key)
|
||||||
if registry_config[key].is_a?(Array)
|
if registry_config[key].is_a?(Array)
|
||||||
ENV.fetch(registry_config[key].first).dup
|
secrets[registry_config[key].first]
|
||||||
else
|
else
|
||||||
registry_config[key]
|
registry_config[key]
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class Kamal::Configuration::Role
|
|||||||
|
|
||||||
@specialized_env = Kamal::Configuration::Env.new \
|
@specialized_env = Kamal::Configuration::Env.new \
|
||||||
config: specializations.fetch("env", {}),
|
config: specializations.fetch("env", {}),
|
||||||
secrets_file: File.join(config.host_env_directory, "roles", "#{container_prefix}.env"),
|
secrets: config.secrets,
|
||||||
context: "servers/#{name}/env"
|
context: "servers/#{name}/env"
|
||||||
|
|
||||||
@specialized_logging = Kamal::Configuration::Logging.new \
|
@specialized_logging = Kamal::Configuration::Logging.new \
|
||||||
@@ -58,10 +58,18 @@ class Kamal::Configuration::Role
|
|||||||
default_labels.merge(traefik_labels).merge(custom_labels)
|
default_labels.merge(traefik_labels).merge(custom_labels)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def labels_for_proxy
|
||||||
|
default_labels.merge(custom_labels)
|
||||||
|
end
|
||||||
|
|
||||||
def label_args
|
def label_args
|
||||||
argumentize "--label", labels
|
argumentize "--label", labels
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def label_args_for_proxy
|
||||||
|
argumentize "--label", labels_for_proxy
|
||||||
|
end
|
||||||
|
|
||||||
def logging_args
|
def logging_args
|
||||||
logging.args
|
logging.args
|
||||||
end
|
end
|
||||||
@@ -77,7 +85,19 @@ class Kamal::Configuration::Role
|
|||||||
end
|
end
|
||||||
|
|
||||||
def env_args(host)
|
def env_args(host)
|
||||||
env(host).args
|
[ *env(host).clear_args, *argumentize("--env-file", secrets_path) ]
|
||||||
|
end
|
||||||
|
|
||||||
|
def env_directory
|
||||||
|
File.join(config.env_directory, "roles")
|
||||||
|
end
|
||||||
|
|
||||||
|
def secrets_io(host)
|
||||||
|
env(host).secrets_io
|
||||||
|
end
|
||||||
|
|
||||||
|
def secrets_path
|
||||||
|
File.join(config.env_directory, "roles", "#{container_prefix}.env")
|
||||||
end
|
end
|
||||||
|
|
||||||
def asset_volume_args
|
def asset_volume_args
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
class Kamal::Configuration::Traefik
|
class Kamal::Configuration::Traefik
|
||||||
|
delegate :argumentize, to: Kamal::Utils
|
||||||
|
|
||||||
DEFAULT_IMAGE = "traefik:v2.10"
|
DEFAULT_IMAGE = "traefik:v2.10"
|
||||||
CONTAINER_PORT = 80
|
CONTAINER_PORT = 80
|
||||||
DEFAULT_ARGS = {
|
DEFAULT_ARGS = {
|
||||||
@@ -34,7 +36,7 @@ class Kamal::Configuration::Traefik
|
|||||||
def env
|
def env
|
||||||
Kamal::Configuration::Env.new \
|
Kamal::Configuration::Env.new \
|
||||||
config: traefik_config.fetch("env", {}),
|
config: traefik_config.fetch("env", {}),
|
||||||
secrets_file: File.join(config.host_env_directory, "traefik", "traefik.env"),
|
secrets: config.secrets,
|
||||||
context: "traefik/env"
|
context: "traefik/env"
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -57,4 +59,20 @@ class Kamal::Configuration::Traefik
|
|||||||
def image
|
def image
|
||||||
traefik_config.fetch("image", DEFAULT_IMAGE)
|
traefik_config.fetch("image", DEFAULT_IMAGE)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def env_args
|
||||||
|
[ *env.clear_args, *argumentize("--env-file", secrets_path) ]
|
||||||
|
end
|
||||||
|
|
||||||
|
def env_directory
|
||||||
|
File.join(config.env_directory, "traefik")
|
||||||
|
end
|
||||||
|
|
||||||
|
def secrets_io
|
||||||
|
env.secrets_io
|
||||||
|
end
|
||||||
|
|
||||||
|
def secrets_path
|
||||||
|
File.join(config.env_directory, "traefik", "traefik.env")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -7,5 +7,7 @@ class Kamal::Configuration::Validator::Builder < Kamal::Configuration::Validator
|
|||||||
end
|
end
|
||||||
|
|
||||||
error "Builder arch not set" unless config["arch"].present?
|
error "Builder arch not set" unless config["arch"].present?
|
||||||
|
|
||||||
|
error "Cannot disable local builds, no remote is set" if config["local"] == false && config["remote"].blank?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
9
lib/kamal/configuration/validator/proxy.rb
Normal file
9
lib/kamal/configuration/validator/proxy.rb
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
class Kamal::Configuration::Validator::Proxy < Kamal::Configuration::Validator
|
||||||
|
def validate!
|
||||||
|
super
|
||||||
|
|
||||||
|
if config["host"].blank? && config["ssl"]
|
||||||
|
error "Must set a host to enable automatic SSL"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -15,6 +15,10 @@ class Kamal::EnvFile
|
|||||||
env_file.presence || "\n"
|
env_file.presence || "\n"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def to_io
|
||||||
|
StringIO.new(to_s)
|
||||||
|
end
|
||||||
|
|
||||||
alias to_str to_s
|
alias to_str to_s
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
33
lib/kamal/secrets.rb
Normal file
33
lib/kamal/secrets.rb
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
require "dotenv"
|
||||||
|
|
||||||
|
class Kamal::Secrets
|
||||||
|
attr_reader :secrets_files
|
||||||
|
|
||||||
|
Kamal::Secrets::Dotenv::InlineCommandSubstitution.install!
|
||||||
|
|
||||||
|
def initialize(destination: nil)
|
||||||
|
@secrets_files = \
|
||||||
|
[ ".kamal/secrets-common", ".kamal/secrets#{(".#{destination}" if destination)}" ].select { |f| File.exist?(f) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def [](key)
|
||||||
|
secrets.fetch(key)
|
||||||
|
rescue KeyError
|
||||||
|
if secrets_files
|
||||||
|
raise Kamal::ConfigurationError, "Secret '#{key}' not found in #{secrets_files.join(", ")}"
|
||||||
|
else
|
||||||
|
raise Kamal::ConfigurationError, "Secret '#{key}' not found, no secret files provided"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_h
|
||||||
|
secrets
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def secrets
|
||||||
|
@secrets ||= secrets_files.inject({}) do |secrets, secrets_file|
|
||||||
|
secrets.merge!(::Dotenv.parse(secrets_file))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
14
lib/kamal/secrets/adapters.rb
Normal file
14
lib/kamal/secrets/adapters.rb
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
require "active_support/core_ext/string/inflections"
|
||||||
|
module Kamal::Secrets::Adapters
|
||||||
|
def self.lookup(name)
|
||||||
|
name = "one_password" if name.downcase == "1password"
|
||||||
|
name = "last_pass" if name.downcase == "lastpass"
|
||||||
|
adapter_class(name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.adapter_class(name)
|
||||||
|
Object.const_get("Kamal::Secrets::Adapters::#{name.camelize}").new
|
||||||
|
rescue NameError => e
|
||||||
|
raise RuntimeError, "Unknown secrets adapter: #{name}"
|
||||||
|
end
|
||||||
|
end
|
||||||
18
lib/kamal/secrets/adapters/base.rb
Normal file
18
lib/kamal/secrets/adapters/base.rb
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
class Kamal::Secrets::Adapters::Base
|
||||||
|
delegate :optionize, to: Kamal::Utils
|
||||||
|
|
||||||
|
def fetch(secrets, account:, from: nil)
|
||||||
|
session = login(account)
|
||||||
|
full_secrets = secrets.map { |secret| [ from, secret ].compact.join("/") }
|
||||||
|
fetch_secrets(full_secrets, account: account, session: session)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def login(...)
|
||||||
|
raise NotImplementedError
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_secrets(...)
|
||||||
|
raise NotImplementedError
|
||||||
|
end
|
||||||
|
end
|
||||||
64
lib/kamal/secrets/adapters/bitwarden.rb
Normal file
64
lib/kamal/secrets/adapters/bitwarden.rb
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
class Kamal::Secrets::Adapters::Bitwarden < Kamal::Secrets::Adapters::Base
|
||||||
|
private
|
||||||
|
def login(account)
|
||||||
|
status = run_command("status")
|
||||||
|
|
||||||
|
if status["status"] == "unauthenticated"
|
||||||
|
run_command("login #{account.shellescape}", raw: true)
|
||||||
|
status = run_command("status")
|
||||||
|
end
|
||||||
|
|
||||||
|
if status["status"] == "locked"
|
||||||
|
session = run_command("unlock --raw", raw: true).presence
|
||||||
|
status = run_command("status", session: session)
|
||||||
|
end
|
||||||
|
|
||||||
|
raise RuntimeError, "Failed to login to and unlock Bitwarden" unless status["status"] == "unlocked"
|
||||||
|
|
||||||
|
run_command("sync", session: session, raw: true)
|
||||||
|
raise RuntimeError, "Failed to sync Bitwarden" unless $?.success?
|
||||||
|
|
||||||
|
session
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_secrets(secrets, account:, session:)
|
||||||
|
{}.tap do |results|
|
||||||
|
items_fields(secrets).each do |item, fields|
|
||||||
|
item_json = run_command("get item #{item.shellescape}", session: session, raw: true)
|
||||||
|
raise RuntimeError, "Could not read #{secret} from Bitwarden" unless $?.success?
|
||||||
|
item_json = JSON.parse(item_json)
|
||||||
|
|
||||||
|
if fields.any?
|
||||||
|
fields.each do |field|
|
||||||
|
item_field = item_json["fields"].find { |f| f["name"] == field }
|
||||||
|
raise RuntimeError, "Could not find field #{field} in item #{item} in Bitwarden" unless item_field
|
||||||
|
value = item_field["value"]
|
||||||
|
results["#{item}/#{field}"] = value
|
||||||
|
end
|
||||||
|
else
|
||||||
|
results[item] = item_json["login"]["password"]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def items_fields(secrets)
|
||||||
|
{}.tap do |items|
|
||||||
|
secrets.each do |secret|
|
||||||
|
item, field = secret.split("/")
|
||||||
|
items[item] ||= []
|
||||||
|
items[item] << field
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def signedin?(account)
|
||||||
|
run_command("status")["status"] != "unauthenticated"
|
||||||
|
end
|
||||||
|
|
||||||
|
def run_command(command, session: nil, raw: false)
|
||||||
|
full_command = [ *("BW_SESSION=#{session.shellescape}" if session), "bw", command ].join(" ")
|
||||||
|
result = `#{full_command}`.strip
|
||||||
|
raw ? result : JSON.parse(result)
|
||||||
|
end
|
||||||
|
end
|
||||||
30
lib/kamal/secrets/adapters/last_pass.rb
Normal file
30
lib/kamal/secrets/adapters/last_pass.rb
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
class Kamal::Secrets::Adapters::LastPass < Kamal::Secrets::Adapters::Base
|
||||||
|
private
|
||||||
|
def login(account)
|
||||||
|
unless loggedin?(account)
|
||||||
|
`lpass login #{account.shellescape}`
|
||||||
|
raise RuntimeError, "Failed to login to 1Password" unless $?.success?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def loggedin?(account)
|
||||||
|
`lpass status --color never`.strip == "Logged in as #{account}."
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_secrets(secrets, account:, session:)
|
||||||
|
items = `lpass show #{secrets.map(&:shellescape).join(" ")} --json`
|
||||||
|
raise RuntimeError, "Could not read #{secrets} from 1Password" unless $?.success?
|
||||||
|
|
||||||
|
items = JSON.parse(items)
|
||||||
|
|
||||||
|
{}.tap do |results|
|
||||||
|
items.each do |item|
|
||||||
|
results[item["fullname"]] = item["password"]
|
||||||
|
end
|
||||||
|
|
||||||
|
if (missing_items = secrets - results.keys).any?
|
||||||
|
raise RuntimeError, "Could not find #{missing_items.join(", ")} in LassPass"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
61
lib/kamal/secrets/adapters/one_password.rb
Normal file
61
lib/kamal/secrets/adapters/one_password.rb
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
class Kamal::Secrets::Adapters::OnePassword < Kamal::Secrets::Adapters::Base
|
||||||
|
delegate :optionize, to: Kamal::Utils
|
||||||
|
|
||||||
|
private
|
||||||
|
def login(account)
|
||||||
|
unless loggedin?(account)
|
||||||
|
`op signin #{to_options(account: account, force: true, raw: true)}`.tap do
|
||||||
|
raise RuntimeError, "Failed to login to 1Password" unless $?.success?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def loggedin?(account)
|
||||||
|
`op account get --account #{account.shellescape} 2> /dev/null`
|
||||||
|
$?.success?
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_secrets(secrets, account:, session:)
|
||||||
|
{}.tap do |results|
|
||||||
|
vaults_items_fields(secrets).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 = [ 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
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_options(**options)
|
||||||
|
optionize(options.compact).join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
def vaults_items_fields(secrets)
|
||||||
|
{}.tap do |vaults|
|
||||||
|
secrets.each do |secret|
|
||||||
|
secret = secret.delete_prefix("op://")
|
||||||
|
vault, item, *fields = secret.split("/")
|
||||||
|
fields << "password" if fields.empty?
|
||||||
|
|
||||||
|
vaults[vault] ||= {}
|
||||||
|
vaults[vault][item] ||= []
|
||||||
|
vaults[vault][item] << fields.join(".")
|
||||||
|
end
|
||||||
|
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)
|
||||||
|
|
||||||
|
`op item get #{item.shellescape} #{options}`.tap do
|
||||||
|
raise RuntimeError, "Could not read #{fields.join(", ")} from #{item} in the #{vault} 1Password vault" unless $?.success?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
10
lib/kamal/secrets/adapters/test.rb
Normal file
10
lib/kamal/secrets/adapters/test.rb
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
class Kamal::Secrets::Adapters::Test < Kamal::Secrets::Adapters::Base
|
||||||
|
private
|
||||||
|
def login(account)
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_secrets(secrets, account:, session:)
|
||||||
|
secrets.to_h { |secret| [ secret, secret.reverse ] }
|
||||||
|
end
|
||||||
|
end
|
||||||
32
lib/kamal/secrets/dotenv/inline_command_substitution.rb
Normal file
32
lib/kamal/secrets/dotenv/inline_command_substitution.rb
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
class Kamal::Secrets::Dotenv::InlineCommandSubstitution
|
||||||
|
class << self
|
||||||
|
def install!
|
||||||
|
::Dotenv::Parser.substitutions.map! { |sub| sub == ::Dotenv::Substitutions::Command ? self : sub }
|
||||||
|
end
|
||||||
|
|
||||||
|
def call(value, _env, overwrite: false)
|
||||||
|
# Process interpolated shell commands
|
||||||
|
value.gsub(Dotenv::Substitutions::Command.singleton_class::INTERPOLATED_SHELL_COMMAND) do |*|
|
||||||
|
# Eliminate opening and closing parentheses
|
||||||
|
command = $LAST_MATCH_INFO[:cmd][1..-2]
|
||||||
|
|
||||||
|
if $LAST_MATCH_INFO[:backslash]
|
||||||
|
# Command is escaped, don't replace it.
|
||||||
|
$LAST_MATCH_INFO[0][1..]
|
||||||
|
else
|
||||||
|
if command =~ /\A\s*kamal\s*secrets\s+/
|
||||||
|
# Inline the command
|
||||||
|
inline_secrets_command(command)
|
||||||
|
else
|
||||||
|
# Execute the command and return the value
|
||||||
|
`#{command}`.chomp
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def inline_secrets_command(command)
|
||||||
|
Kamal::Cli::Main.start(command.shellsplit[1..] + [ "--inline" ]).chomp
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -3,6 +3,7 @@ require "sshkit/dsl"
|
|||||||
require "net/scp"
|
require "net/scp"
|
||||||
require "active_support/core_ext/hash/deep_merge"
|
require "active_support/core_ext/hash/deep_merge"
|
||||||
require "json"
|
require "json"
|
||||||
|
require "concurrent/atomic/semaphore"
|
||||||
|
|
||||||
class SSHKit::Backend::Abstract
|
class SSHKit::Backend::Abstract
|
||||||
def capture_with_info(*args, **kwargs)
|
def capture_with_info(*args, **kwargs)
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
require "active_support/core_ext/object/try"
|
||||||
|
|
||||||
module Kamal::Utils
|
module Kamal::Utils
|
||||||
extend self
|
extend self
|
||||||
|
|
||||||
@@ -54,6 +56,12 @@ module Kamal::Utils
|
|||||||
|
|
||||||
# Escape a value to make it safe for shell use.
|
# Escape a value to make it safe for shell use.
|
||||||
def escape_shell_value(value)
|
def escape_shell_value(value)
|
||||||
|
value.to_s.scan(/[\x00-\x7F]+|[^\x00-\x7F]+/) \
|
||||||
|
.map { |part| part.ascii_only? ? escape_ascii_shell_value(part) : part }
|
||||||
|
.join
|
||||||
|
end
|
||||||
|
|
||||||
|
def escape_ascii_shell_value(value)
|
||||||
value.to_s.dump
|
value.to_s.dump
|
||||||
.gsub(/`/, '\\\\`')
|
.gsub(/`/, '\\\\`')
|
||||||
.gsub(DOLLAR_SIGN_WITHOUT_SHELL_EXPANSION_REGEX, '\$')
|
.gsub(DOLLAR_SIGN_WITHOUT_SHELL_EXPANSION_REGEX, '\$')
|
||||||
|
|||||||
@@ -1,13 +1,21 @@
|
|||||||
require_relative "cli_test_case"
|
require_relative "cli_test_case"
|
||||||
|
|
||||||
class CliAccessoryTest < CliTestCase
|
class CliAccessoryTest < CliTestCase
|
||||||
|
setup do
|
||||||
|
setup_test_secrets("secrets" => "MYSQL_ROOT_PASSWORD=secret")
|
||||||
|
end
|
||||||
|
|
||||||
|
teardown do
|
||||||
|
teardown_test_secrets
|
||||||
|
end
|
||||||
|
|
||||||
test "boot" do
|
test "boot" do
|
||||||
Kamal::Cli::Accessory.any_instance.expects(:directories).with("mysql")
|
Kamal::Cli::Accessory.any_instance.expects(:directories).with("mysql")
|
||||||
Kamal::Cli::Accessory.any_instance.expects(:upload).with("mysql")
|
Kamal::Cli::Accessory.any_instance.expects(:upload).with("mysql")
|
||||||
|
|
||||||
run_command("boot", "mysql").tap do |output|
|
run_command("boot", "mysql").tap do |output|
|
||||||
assert_match /docker login.*on 1.1.1.3/, output
|
assert_match /docker login.*on 1.1.1.3/, output
|
||||||
assert_match "docker run --name app-mysql --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 3306:3306 --env-file .kamal/env/accessories/app-mysql.env --env MYSQL_ROOT_HOST=\"%\" --volume $PWD/app-mysql/etc/mysql/my.cnf:/etc/mysql/my.cnf --volume $PWD/app-mysql/data:/var/lib/mysql --label service=\"app-mysql\" 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 MYSQL_ROOT_HOST=\"%\" --env-file .kamal/env/accessories/app-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\" mysql:5.7 on 1.1.1.3", output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -21,9 +29,12 @@ class CliAccessoryTest < CliTestCase
|
|||||||
assert_match /docker login.*on 1.1.1.3/, output
|
assert_match /docker login.*on 1.1.1.3/, output
|
||||||
assert_match /docker login.*on 1.1.1.1/, output
|
assert_match /docker login.*on 1.1.1.1/, output
|
||||||
assert_match /docker login.*on 1.1.1.2/, output
|
assert_match /docker login.*on 1.1.1.2/, output
|
||||||
assert_match "docker run --name app-mysql --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 3306:3306 --env-file .kamal/env/accessories/app-mysql.env --env MYSQL_ROOT_HOST=\"%\" --volume $PWD/app-mysql/etc/mysql/my.cnf:/etc/mysql/my.cnf --volume $PWD/app-mysql/data:/var/lib/mysql --label service=\"app-mysql\" mysql:5.7 on 1.1.1.3", output
|
assert_match /docker network create kamal.*on 1.1.1.1/, output
|
||||||
assert_match "docker run --name app-redis --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/env/accessories/app-redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.1", output
|
assert_match /docker network create kamal.*on 1.1.1.2/, output
|
||||||
assert_match "docker run --name app-redis --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/env/accessories/app-redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest 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/env/accessories/app-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\" 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/env/accessories/app-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/env/accessories/app-redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.2", output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -43,7 +54,7 @@ class CliAccessoryTest < CliTestCase
|
|||||||
Kamal::Commands::Registry.any_instance.expects(:login)
|
Kamal::Commands::Registry.any_instance.expects(:login)
|
||||||
Kamal::Cli::Accessory.any_instance.expects(:stop).with("mysql")
|
Kamal::Cli::Accessory.any_instance.expects(:stop).with("mysql")
|
||||||
Kamal::Cli::Accessory.any_instance.expects(:remove_container).with("mysql")
|
Kamal::Cli::Accessory.any_instance.expects(:remove_container).with("mysql")
|
||||||
Kamal::Cli::Accessory.any_instance.expects(:boot).with("mysql", login: false)
|
Kamal::Cli::Accessory.any_instance.expects(:boot).with("mysql", prepare: false)
|
||||||
|
|
||||||
run_command("reboot", "mysql")
|
run_command("reboot", "mysql")
|
||||||
end
|
end
|
||||||
@@ -52,10 +63,10 @@ class CliAccessoryTest < CliTestCase
|
|||||||
Kamal::Commands::Registry.any_instance.expects(:login).times(3)
|
Kamal::Commands::Registry.any_instance.expects(:login).times(3)
|
||||||
Kamal::Cli::Accessory.any_instance.expects(:stop).with("mysql")
|
Kamal::Cli::Accessory.any_instance.expects(:stop).with("mysql")
|
||||||
Kamal::Cli::Accessory.any_instance.expects(:remove_container).with("mysql")
|
Kamal::Cli::Accessory.any_instance.expects(:remove_container).with("mysql")
|
||||||
Kamal::Cli::Accessory.any_instance.expects(:boot).with("mysql", login: false)
|
Kamal::Cli::Accessory.any_instance.expects(:boot).with("mysql", prepare: false)
|
||||||
Kamal::Cli::Accessory.any_instance.expects(:stop).with("redis")
|
Kamal::Cli::Accessory.any_instance.expects(:stop).with("redis")
|
||||||
Kamal::Cli::Accessory.any_instance.expects(:remove_container).with("redis")
|
Kamal::Cli::Accessory.any_instance.expects(:remove_container).with("redis")
|
||||||
Kamal::Cli::Accessory.any_instance.expects(:boot).with("redis", login: false)
|
Kamal::Cli::Accessory.any_instance.expects(:boot).with("redis", prepare: false)
|
||||||
|
|
||||||
run_command("reboot", "all")
|
run_command("reboot", "all")
|
||||||
end
|
end
|
||||||
@@ -192,8 +203,8 @@ class CliAccessoryTest < CliTestCase
|
|||||||
run_command("boot", "redis", "--hosts", "1.1.1.1").tap do |output|
|
run_command("boot", "redis", "--hosts", "1.1.1.1").tap do |output|
|
||||||
assert_match /docker login.*on 1.1.1.1/, output
|
assert_match /docker login.*on 1.1.1.1/, output
|
||||||
assert_no_match /docker login.*on 1.1.1.2/, output
|
assert_no_match /docker login.*on 1.1.1.2/, output
|
||||||
assert_match "docker run --name app-redis --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/env/accessories/app-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/env/accessories/app-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 --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/env/accessories/app-redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.2", 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/env/accessories/app-redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.2", output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -204,8 +215,8 @@ class CliAccessoryTest < CliTestCase
|
|||||||
run_command("boot", "redis", "--hosts", "1.1.1.1,1.1.1.3").tap do |output|
|
run_command("boot", "redis", "--hosts", "1.1.1.1,1.1.1.3").tap do |output|
|
||||||
assert_match /docker login.*on 1.1.1.1/, output
|
assert_match /docker login.*on 1.1.1.1/, output
|
||||||
assert_no_match /docker login.*on 1.1.1.3/, output
|
assert_no_match /docker login.*on 1.1.1.3/, output
|
||||||
assert_match "docker run --name app-redis --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/env/accessories/app-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/env/accessories/app-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 --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/env/accessories/app-redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.3", 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/env/accessories/app-redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.3", output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ class CliAppTest < CliTestCase
|
|||||||
|
|
||||||
run_command("boot", config: :with_env_tags).tap do |output|
|
run_command("boot", config: :with_env_tags).tap do |output|
|
||||||
assert_match "docker tag dhh/app:latest dhh/app:latest", 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 --hostname 1.1.1.1-[0-9a-f]{12} -e KAMAL_CONTAINER_NAME="app-web-latest" -e KAMAL_VERSION="latest" --env-file .kamal/env/roles/app-web.env --env TEST="root" --env EXPERIMENT="disabled" --env SITE="site1"}, output
|
assert_match %r{docker run --detach --restart unless-stopped --name app-web-latest --hostname 1.1.1.1-[0-9a-f]{12} -e KAMAL_CONTAINER_NAME="app-web-latest" -e KAMAL_VERSION="latest" --env 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
|
assert_match "docker container ls --all --filter name=^app-web-123$ --quiet | xargs docker stop", output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -356,6 +356,18 @@ class CliAppTest < CliTestCase
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "boot proxy" do
|
||||||
|
SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info).returns("123") # old version
|
||||||
|
|
||||||
|
run_command("boot", config: :with_proxy).tap do |output|
|
||||||
|
assert_match /Renaming container .* to .* as already deployed on 1.1.1.1/, output # Rename
|
||||||
|
assert_match /docker rename app-web-latest app-web-latest_replaced_[0-9a-f]{16}/, output
|
||||||
|
assert_match /docker run --detach --restart unless-stopped --name app-web-latest --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\/env\/roles\/app-web.env --log-opt max-size="10m" --label service="app" --label role="web" --label destination dhh\/app:latest/, output
|
||||||
|
assert_match /docker exec kamal-proxy kamal-proxy deploy app-web --target "123:80"/, output
|
||||||
|
assert_match "docker container ls --all --filter name=^app-web-123$ --quiet | xargs docker stop", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def run_command(*command, config: :with_accessories, host: "1.1.1.1", allow_execute_error: false)
|
def run_command(*command, config: :with_accessories, host: "1.1.1.1", allow_execute_error: false)
|
||||||
stdouted do
|
stdouted do
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ class CliBuildTest < CliTestCase
|
|||||||
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:git, "-C", build_directory, :submodule, :update, "--init")
|
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:git, "-C", build_directory, :submodule, :update, "--init")
|
||||||
|
|
||||||
SSHKit::Backend::Abstract.any_instance.expects(:execute)
|
SSHKit::Backend::Abstract.any_instance.expects(:execute)
|
||||||
.with(:docker, :buildx, :build, "--push", "--platform", "linux/amd64", "--builder", "kamal-local-docker-container", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".")
|
.with(:docker, :buildx, :build, "--push", "--platform", "linux/amd64", "--builder", "kamal-local-docker-container", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".", env: {})
|
||||||
|
|
||||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
.with(:git, "-C", anything, :"rev-parse", :HEAD)
|
.with(:git, "-C", anything, :"rev-parse", :HEAD)
|
||||||
@@ -119,6 +119,9 @@ class CliBuildTest < CliTestCase
|
|||||||
SSHKit::Backend::Abstract.any_instance.expects(:execute)
|
SSHKit::Backend::Abstract.any_instance.expects(:execute)
|
||||||
.with(:docker, "--version", "&&", :docker, :buildx, "version")
|
.with(:docker, "--version", "&&", :docker, :buildx, "version")
|
||||||
|
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:execute)
|
||||||
|
.with(:docker, :buildx, :rm, "kamal-local-docker-container")
|
||||||
|
|
||||||
SSHKit::Backend::Abstract.any_instance.expects(:execute)
|
SSHKit::Backend::Abstract.any_instance.expects(:execute)
|
||||||
.with(:docker, :buildx, :create, "--name", "kamal-local-docker-container", "--driver=docker-container")
|
.with(:docker, :buildx, :create, "--name", "kamal-local-docker-container", "--driver=docker-container")
|
||||||
|
|
||||||
@@ -137,7 +140,7 @@ class CliBuildTest < CliTestCase
|
|||||||
.returns("")
|
.returns("")
|
||||||
|
|
||||||
SSHKit::Backend::Abstract.any_instance.expects(:execute)
|
SSHKit::Backend::Abstract.any_instance.expects(:execute)
|
||||||
.with(:docker, :buildx, :build, "--push", "--platform", "linux/amd64", "--builder", "kamal-local-docker-container", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".")
|
.with(:docker, :buildx, :build, "--push", "--platform", "linux/amd64", "--builder", "kamal-local-docker-container", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".", env: {})
|
||||||
|
|
||||||
run_command("push").tap do |output|
|
run_command("push").tap do |output|
|
||||||
assert_match /WARN Missing compatible builder, so creating a new one first/, output
|
assert_match /WARN Missing compatible builder, so creating a new one first/, output
|
||||||
@@ -210,16 +213,25 @@ class CliBuildTest < CliTestCase
|
|||||||
test "create remote" do
|
test "create remote" do
|
||||||
run_command("create", fixture: :with_remote_builder).tap do |output|
|
run_command("create", fixture: :with_remote_builder).tap do |output|
|
||||||
assert_match "Running /usr/bin/env true on 1.1.1.5", output
|
assert_match "Running /usr/bin/env true on 1.1.1.5", output
|
||||||
assert_match "docker context create kamal-remote-docker-container-ssh---app-1-1-1-5 --description 'kamal-remote-docker-container-ssh---app-1-1-1-5 host' --docker 'host=ssh://app@1.1.1.5'", output
|
assert_match "docker context create kamal-remote-ssh---app-1-1-1-5-context --description 'kamal-remote-ssh---app-1-1-1-5 host' --docker 'host=ssh://app@1.1.1.5'", output
|
||||||
assert_match "docker buildx create --name kamal-remote-docker-container-ssh---app-1-1-1-5 kamal-remote-docker-container-ssh---app-1-1-1-5", output
|
assert_match "docker buildx create --name kamal-remote-ssh---app-1-1-1-5 kamal-remote-ssh---app-1-1-1-5-context", output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "create remote with custom ports" do
|
test "create remote with custom ports" do
|
||||||
run_command("create", fixture: :with_remote_builder_and_custom_ports).tap do |output|
|
run_command("create", fixture: :with_remote_builder_and_custom_ports).tap do |output|
|
||||||
assert_match "Running /usr/bin/env true on 1.1.1.5", output
|
assert_match "Running /usr/bin/env true on 1.1.1.5", output
|
||||||
assert_match "docker context create kamal-remote-docker-container-ssh---app-1-1-1-5-2122 --description 'kamal-remote-docker-container-ssh---app-1-1-1-5-2122 host' --docker 'host=ssh://app@1.1.1.5:2122'", output
|
assert_match "docker context create kamal-remote-ssh---app-1-1-1-5-2122-context --description 'kamal-remote-ssh---app-1-1-1-5-2122 host' --docker 'host=ssh://app@1.1.1.5:2122'", output
|
||||||
assert_match "docker buildx create --name kamal-remote-docker-container-ssh---app-1-1-1-5-2122 kamal-remote-docker-container-ssh---app-1-1-1-5-2122", output
|
assert_match "docker buildx create --name kamal-remote-ssh---app-1-1-1-5-2122 kamal-remote-ssh---app-1-1-1-5-2122-context", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "create hybrid" do
|
||||||
|
run_command("create", fixture: :with_hybrid_builder).tap do |output|
|
||||||
|
assert_match "Running /usr/bin/env true on 1.1.1.5", output
|
||||||
|
assert_match "docker buildx create --platform linux/#{Kamal::Utils.docker_arch} --name kamal-hybrid-docker-container-ssh---app-1-1-1-5 --driver=docker-container", output
|
||||||
|
assert_match "docker context create kamal-hybrid-docker-container-ssh---app-1-1-1-5-context --description 'kamal-hybrid-docker-container-ssh---app-1-1-1-5 host' --docker 'host=ssh://app@1.1.1.5'", output
|
||||||
|
assert_match "docker buildx create --platform linux/#{Kamal::Utils.docker_arch == "amd64" ? "arm64" : "amd64"} --append --name kamal-hybrid-docker-container-ssh---app-1-1-1-5 kamal-hybrid-docker-container-ssh---app-1-1-1-5-context", output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class CliTestCase < ActiveSupport::TestCase
|
|||||||
.with(:docker, :buildx, :inspect, "kamal-local-docker-container")
|
.with(:docker, :buildx, :inspect, "kamal-local-docker-container")
|
||||||
end
|
end
|
||||||
|
|
||||||
def assert_hook_ran(hook, output, version:, service_version:, hosts:, command:, subcommand: nil, runtime: false)
|
def assert_hook_ran(hook, output, version:, service_version:, hosts:, command:, subcommand: nil, runtime: false, secrets: false)
|
||||||
whoami = `whoami`.chomp
|
whoami = `whoami`.chomp
|
||||||
performer = Kamal::Git.email.presence || whoami
|
performer = Kamal::Git.email.presence || whoami
|
||||||
service = service_version.split("@").first
|
service = service_version.split("@").first
|
||||||
@@ -58,6 +58,7 @@ class CliTestCase < ActiveSupport::TestCase
|
|||||||
KAMAL_COMMAND=\"#{command}\"\s
|
KAMAL_COMMAND=\"#{command}\"\s
|
||||||
#{"KAMAL_SUBCOMMAND=\\\"#{subcommand}\\\"\\s" if subcommand}
|
#{"KAMAL_SUBCOMMAND=\\\"#{subcommand}\\\"\\s" if subcommand}
|
||||||
#{"KAMAL_RUNTIME=\\\"\\d+\\\"\\s" if runtime}
|
#{"KAMAL_RUNTIME=\\\"\\d+\\\"\\s" if runtime}
|
||||||
|
#{"DB_PASSWORD=\"secret\"\\s" if secrets}
|
||||||
;\s/usr/bin/env\s\.kamal/hooks/#{hook} }x
|
;\s/usr/bin/env\s\.kamal/hooks/#{hook} }x
|
||||||
|
|
||||||
assert_match expected, output
|
assert_match expected, output
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
require_relative "cli_test_case"
|
|
||||||
|
|
||||||
class CliEnvTest < CliTestCase
|
|
||||||
test "push" do
|
|
||||||
run_command("push").tap do |output|
|
|
||||||
assert_match "Running /usr/bin/env mkdir -p .kamal/env/roles on 1.1.1.1", output
|
|
||||||
assert_match "Running /usr/bin/env mkdir -p .kamal/env/traefik on 1.1.1.1", output
|
|
||||||
assert_match "Running /usr/bin/env mkdir -p .kamal/env/accessories on 1.1.1.1", output
|
|
||||||
assert_match "Running /usr/bin/env mkdir -p .kamal/env/roles on 1.1.1.1", output
|
|
||||||
assert_match "Running /usr/bin/env mkdir -p .kamal/env/traefik on 1.1.1.2", output
|
|
||||||
assert_match "Running /usr/bin/env mkdir -p .kamal/env/accessories on 1.1.1.1", output
|
|
||||||
assert_match ".kamal/env/roles/app-web.env", output
|
|
||||||
assert_match ".kamal/env/roles/app-workers.env", output
|
|
||||||
assert_match ".kamal/env/traefik/traefik.env", output
|
|
||||||
assert_match ".kamal/env/accessories/app-redis.env", output
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
test "delete" do
|
|
||||||
run_command("delete").tap do |output|
|
|
||||||
assert_match "Running /usr/bin/env rm -f .kamal/env/roles/app-web.env on 1.1.1.1", output
|
|
||||||
assert_match "Running /usr/bin/env rm -f .kamal/env/roles/app-web.env on 1.1.1.2", output
|
|
||||||
assert_match "Running /usr/bin/env rm -f .kamal/env/roles/app-workers.env on 1.1.1.3", output
|
|
||||||
assert_match "Running /usr/bin/env rm -f .kamal/env/roles/app-workers.env on 1.1.1.4", output
|
|
||||||
assert_match "Running /usr/bin/env rm -f .kamal/env/traefik/traefik.env on 1.1.1.1", output
|
|
||||||
assert_match "Running /usr/bin/env rm -f .kamal/env/traefik/traefik.env on 1.1.1.2", output
|
|
||||||
assert_match "Running /usr/bin/env rm -f .kamal/env/accessories/app-redis.env on 1.1.1.1", output
|
|
||||||
assert_match "Running /usr/bin/env rm -f .kamal/env/accessories/app-redis.env on 1.1.1.2", output
|
|
||||||
assert_match "Running /usr/bin/env rm -f .kamal/env/accessories/app-mysql.env on 1.1.1.3", output
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
def run_command(*command)
|
|
||||||
stdouted { Kamal::Cli::Env.start([ *command, "-c", "test/fixtures/deploy_with_accessories.yml" ]) }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -8,14 +8,11 @@ class CliMainTest < CliTestCase
|
|||||||
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "version" => "999", "skip_hooks" => false }
|
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "version" => "999", "skip_hooks" => false }
|
||||||
|
|
||||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:server:bootstrap", [], invoke_options)
|
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:server:bootstrap", [], invoke_options)
|
||||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:main:envify", [], invoke_options)
|
|
||||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:env:push", [], invoke_options)
|
|
||||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:accessory:boot", [ "all" ], invoke_options)
|
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:accessory:boot", [ "all" ], invoke_options)
|
||||||
Kamal::Cli::Main.any_instance.expects(:deploy)
|
Kamal::Cli::Main.any_instance.expects(:deploy)
|
||||||
|
|
||||||
run_command("setup").tap do |output|
|
run_command("setup").tap do |output|
|
||||||
assert_match /Ensure Docker is installed.../, output
|
assert_match /Ensure Docker is installed.../, output
|
||||||
assert_match /Evaluate and push env files.../, output
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -23,8 +20,6 @@ class CliMainTest < CliTestCase
|
|||||||
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "version" => "999", "skip_hooks" => false }
|
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "version" => "999", "skip_hooks" => false }
|
||||||
|
|
||||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:server:bootstrap", [], invoke_options)
|
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:server:bootstrap", [], invoke_options)
|
||||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:env:push", [], invoke_options)
|
|
||||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:main:envify", [], invoke_options)
|
|
||||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:accessory:boot", [ "all" ], invoke_options)
|
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:accessory:boot", [ "all" ], invoke_options)
|
||||||
# deploy
|
# deploy
|
||||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:registry:login", [], invoke_options.merge(skip_local: true))
|
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:registry:login", [], invoke_options.merge(skip_local: true))
|
||||||
@@ -36,7 +31,6 @@ class CliMainTest < CliTestCase
|
|||||||
|
|
||||||
run_command("setup", "--skip_push").tap do |output|
|
run_command("setup", "--skip_push").tap do |output|
|
||||||
assert_match /Ensure Docker is installed.../, output
|
assert_match /Ensure Docker is installed.../, output
|
||||||
assert_match /Evaluate and push env files.../, output
|
|
||||||
# deploy
|
# deploy
|
||||||
assert_match /Acquiring the deploy lock/, output
|
assert_match /Acquiring the deploy lock/, output
|
||||||
assert_match /Log into image registry/, output
|
assert_match /Log into image registry/, output
|
||||||
@@ -49,6 +43,7 @@ class CliMainTest < CliTestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "deploy" do
|
test "deploy" do
|
||||||
|
with_test_secrets("secrets" => "DB_PASSWORD=secret") do
|
||||||
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "version" => "999", "skip_hooks" => false, "verbose" => true }
|
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "version" => "999", "skip_hooks" => false, "verbose" => true }
|
||||||
|
|
||||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:registry:login", [], invoke_options.merge(skip_local: false))
|
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:registry:login", [], invoke_options.merge(skip_local: false))
|
||||||
@@ -65,11 +60,12 @@ class CliMainTest < CliTestCase
|
|||||||
assert_hook_ran "pre-connect", output, **hook_variables
|
assert_hook_ran "pre-connect", output, **hook_variables
|
||||||
assert_match /Log into image registry/, output
|
assert_match /Log into image registry/, output
|
||||||
assert_match /Build and push app image/, output
|
assert_match /Build and push app image/, output
|
||||||
assert_hook_ran "pre-deploy", output, **hook_variables
|
assert_hook_ran "pre-deploy", output, **hook_variables, secrets: true
|
||||||
assert_match /Ensure Traefik is running/, output
|
assert_match /Ensure Traefik is running/, output
|
||||||
assert_match /Detect stale containers/, output
|
assert_match /Detect stale containers/, output
|
||||||
assert_match /Prune old containers and images/, output
|
assert_match /Prune old containers and images/, output
|
||||||
assert_hook_ran "post-deploy", output, **hook_variables, runtime: true
|
assert_hook_ran "post-deploy", output, **hook_variables, runtime: true, secrets: true
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -388,42 +384,40 @@ class CliMainTest < CliTestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "init" do
|
test "init" do
|
||||||
Pathname.any_instance.expects(:exist?).returns(false).times(3)
|
in_dummy_git_repo do
|
||||||
Pathname.any_instance.stubs(:mkpath)
|
|
||||||
FileUtils.stubs(:mkdir_p)
|
|
||||||
FileUtils.stubs(:cp_r)
|
|
||||||
FileUtils.stubs(:cp)
|
|
||||||
|
|
||||||
run_command("init").tap do |output|
|
run_command("init").tap do |output|
|
||||||
assert_match /Created configuration file in config\/deploy.yml/, output
|
assert_match "Created configuration file in config/deploy.yml", output
|
||||||
assert_match /Created \.env file/, output
|
assert_match "Created .kamal/secrets file", output
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_file "config/deploy.yml", "service: my-app"
|
||||||
|
assert_file ".kamal/secrets", "KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "init with existing config" do
|
test "init with existing config" do
|
||||||
Pathname.any_instance.expects(:exist?).returns(true).times(3)
|
in_dummy_git_repo do
|
||||||
|
run_command("init")
|
||||||
|
|
||||||
run_command("init").tap do |output|
|
run_command("init").tap do |output|
|
||||||
assert_match /Config file already exists in config\/deploy.yml \(remove first to create a new one\)/, output
|
assert_match /Config file already exists in config\/deploy.yml \(remove first to create a new one\)/, output
|
||||||
|
assert_no_match /Added .kamal\/secrets/, output
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "init with bundle option" do
|
test "init with bundle option" do
|
||||||
Pathname.any_instance.expects(:exist?).returns(false).times(4)
|
in_dummy_git_repo do
|
||||||
Pathname.any_instance.stubs(:mkpath)
|
|
||||||
FileUtils.stubs(:mkdir_p)
|
|
||||||
FileUtils.stubs(:cp_r)
|
|
||||||
FileUtils.stubs(:cp)
|
|
||||||
|
|
||||||
run_command("init", "--bundle").tap do |output|
|
run_command("init", "--bundle").tap do |output|
|
||||||
assert_match /Created configuration file in config\/deploy.yml/, output
|
assert_match "Created configuration file in config/deploy.yml", output
|
||||||
assert_match /Created \.env file/, output
|
assert_match "Created .kamal/secrets file", output
|
||||||
assert_match /Adding Kamal to Gemfile and bundle/, output
|
assert_match /Adding Kamal to Gemfile and bundle/, output
|
||||||
assert_match /bundle add kamal/, output
|
assert_match /bundle add kamal/, output
|
||||||
assert_match /bundle binstubs kamal/, output
|
assert_match /bundle binstubs kamal/, output
|
||||||
assert_match /Created binstub file in bin\/kamal/, output
|
assert_match /Created binstub file in bin\/kamal/, output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
test "init with bundle option and existing binstub" do
|
test "init with bundle option and existing binstub" do
|
||||||
Pathname.any_instance.expects(:exist?).returns(true).times(4)
|
Pathname.any_instance.expects(:exist?).returns(true).times(4)
|
||||||
@@ -438,50 +432,6 @@ class CliMainTest < CliTestCase
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "envify" do
|
|
||||||
with_test_dotenv(".env.erb": "HELLO=<%= 'world' %>") do
|
|
||||||
run_command("envify")
|
|
||||||
assert_equal("HELLO=world", File.read(".env"))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
test "envify with blank line trimming" do
|
|
||||||
file = <<~EOF
|
|
||||||
HELLO=<%= 'world' %>
|
|
||||||
<% if true -%>
|
|
||||||
KEY=value
|
|
||||||
<% end -%>
|
|
||||||
EOF
|
|
||||||
|
|
||||||
with_test_dotenv(".env.erb": file) do
|
|
||||||
run_command("envify")
|
|
||||||
assert_equal("HELLO=world\nKEY=value\n", File.read(".env"))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
test "envify with destination" do
|
|
||||||
with_test_dotenv(".env.world.erb": "HELLO=<%= 'world' %>") do
|
|
||||||
run_command("envify", "-d", "world", config_file: "deploy_for_dest")
|
|
||||||
assert_equal "HELLO=world", File.read(".env.world")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
test "envify with skip_push" do
|
|
||||||
Pathname.any_instance.expects(:exist?).returns(true).times(1)
|
|
||||||
File.expects(:read).with(".env.erb").returns("HELLO=<%= 'world' %>")
|
|
||||||
File.expects(:write).with(".env", "HELLO=world", perm: 0600)
|
|
||||||
|
|
||||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:env:push").never
|
|
||||||
run_command("envify", "--skip-push")
|
|
||||||
end
|
|
||||||
|
|
||||||
test "envify with clean env" do
|
|
||||||
with_test_dotenv(".env": "HELLO=already", ".env.erb": "HELLO=<%= ENV.fetch 'HELLO', 'never' %>") do
|
|
||||||
run_command("envify", "--skip-push")
|
|
||||||
assert_equal "HELLO=never", File.read(".env")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
test "remove with confirmation" do
|
test "remove with confirmation" do
|
||||||
run_command("remove", "-y", config_file: "deploy_with_accessories").tap do |output|
|
run_command("remove", "-y", config_file: "deploy_with_accessories").tap do |output|
|
||||||
assert_match /docker container stop traefik/, output
|
assert_match /docker container stop traefik/, output
|
||||||
@@ -572,18 +522,16 @@ class CliMainTest < CliTestCase
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def with_test_dotenv(**files)
|
def in_dummy_git_repo
|
||||||
Dir.mktmpdir do |dir|
|
Dir.mktmpdir do |tmpdir|
|
||||||
fixtures_dup = File.join(dir, "test")
|
Dir.chdir(tmpdir) do
|
||||||
FileUtils.mkdir_p(fixtures_dup)
|
`git init`
|
||||||
FileUtils.cp_r("test/fixtures/", fixtures_dup)
|
|
||||||
|
|
||||||
Dir.chdir(dir) do
|
|
||||||
files.each do |filename, contents|
|
|
||||||
File.binwrite(filename.to_s, contents)
|
|
||||||
end
|
|
||||||
yield
|
yield
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def assert_file(file, content)
|
||||||
|
assert_match content, File.read(file)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
141
test/cli/proxy_test.rb
Normal file
141
test/cli/proxy_test.rb
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
require_relative "cli_test_case"
|
||||||
|
|
||||||
|
class CliProxyTest < CliTestCase
|
||||||
|
test "boot" do
|
||||||
|
run_command("boot").tap do |output|
|
||||||
|
assert_match "docker login", output
|
||||||
|
assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/proxy/config:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "reboot" do
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
|
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-123$", "--quiet")
|
||||||
|
.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 stop traefik on 1.1.1.1", output
|
||||||
|
assert_match "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy on 1.1.1.1", output
|
||||||
|
assert_match "docker container prune --force --filter label=org.opencontainers.image.title=Traefik on 1.1.1.1", output
|
||||||
|
assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/proxy/config:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE} on 1.1.1.1", output
|
||||||
|
assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"abcdefabcdef:80\" --deploy-timeout \"6s\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\" on 1.1.1.1", output
|
||||||
|
|
||||||
|
assert_match "docker container stop kamal-proxy on 1.1.1.2", output
|
||||||
|
assert_match "docker container stop traefik on 1.1.1.2", output
|
||||||
|
assert_match "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy on 1.1.1.2", output
|
||||||
|
assert_match "docker container prune --force --filter label=org.opencontainers.image.title=Traefik on 1.1.1.2", output
|
||||||
|
assert_match "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" traefik:v2.10 --providers.docker --log.level=\"DEBUG\" on 1.1.1.2", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "reboot --rolling" do
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
|
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-123$", "--quiet")
|
||||||
|
.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
|
||||||
|
end
|
||||||
|
|
||||||
|
test "start" do
|
||||||
|
run_command("start").tap do |output|
|
||||||
|
assert_match "docker container start kamal-proxy", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "stop" do
|
||||||
|
run_command("stop").tap do |output|
|
||||||
|
assert_match "docker container stop kamal-proxy", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "restart" do
|
||||||
|
Kamal::Cli::Proxy.any_instance.expects(:stop)
|
||||||
|
Kamal::Cli::Proxy.any_instance.expects(:start)
|
||||||
|
|
||||||
|
run_command("restart")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "details" do
|
||||||
|
run_command("details").tap do |output|
|
||||||
|
assert_match "docker ps --filter name=^kamal-proxy$", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "logs" do
|
||||||
|
SSHKit::Backend::Abstract.any_instance.stubs(:capture)
|
||||||
|
.with(:docker, :logs, "kamal-proxy", " --tail 100", "--timestamps", "2>&1")
|
||||||
|
.returns("Log entry")
|
||||||
|
|
||||||
|
SSHKit::Backend::Abstract.any_instance.stubs(:capture)
|
||||||
|
.with(:docker, :logs, "traefik", " --tail 100", "--timestamps", "2>&1")
|
||||||
|
.returns("Log entry")
|
||||||
|
|
||||||
|
run_command("logs").tap do |output|
|
||||||
|
assert_match "Proxy Host: 1.1.1.1", output
|
||||||
|
assert_match "Log entry", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "logs with follow" do
|
||||||
|
SSHKit::Backend::Abstract.any_instance.stubs(:exec)
|
||||||
|
.with("ssh -t root@1.1.1.1 -p 22 'docker logs kamal-proxy --timestamps --tail 10 --follow 2>&1'")
|
||||||
|
|
||||||
|
assert_match "docker logs kamal-proxy --timestamps --tail 10 --follow", run_command("logs", "--follow")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "remove" do
|
||||||
|
Kamal::Cli::Proxy.any_instance.expects(:stop)
|
||||||
|
Kamal::Cli::Proxy.any_instance.expects(:remove_container)
|
||||||
|
Kamal::Cli::Proxy.any_instance.expects(:remove_image)
|
||||||
|
|
||||||
|
run_command("remove")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "remove_container" do
|
||||||
|
run_command("remove_container").tap do |output|
|
||||||
|
assert_match "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "remove_image" do
|
||||||
|
run_command("remove_image").tap do |output|
|
||||||
|
assert_match "docker image prune --all --force --filter label=org.opencontainers.image.title=kamal-proxy", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "commands disallowed when proxy is disabled" do
|
||||||
|
assert_raises_when_disabled "boot"
|
||||||
|
assert_raises_when_disabled "reboot"
|
||||||
|
assert_raises_when_disabled "start"
|
||||||
|
assert_raises_when_disabled "stop"
|
||||||
|
assert_raises_when_disabled "details"
|
||||||
|
assert_raises_when_disabled "logs"
|
||||||
|
assert_raises_when_disabled "remove"
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def run_command(*command, fixture: :with_proxy)
|
||||||
|
stdouted { Kamal::Cli::Proxy.start([ *command, "-c", "test/fixtures/deploy_#{fixture}.yml" ]) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def assert_raises_when_disabled(command)
|
||||||
|
assert_raises "kamal proxy commands are disabled unless experimental proxy support is enabled. Use `kamal traefik` commands instead." do
|
||||||
|
run_command(command, fixture: :with_accessories)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
22
test/cli/secrets_test.rb
Normal file
22
test/cli/secrets_test.rb
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
require_relative "cli_test_case"
|
||||||
|
|
||||||
|
class CliSecretsTest < CliTestCase
|
||||||
|
test "fetch" do
|
||||||
|
assert_equal \
|
||||||
|
"\\{\\\"foo\\\":\\\"oof\\\",\\\"bar\\\":\\\"rab\\\",\\\"baz\\\":\\\"zab\\\"\\}",
|
||||||
|
run_command("fetch", "foo", "bar", "baz", "--account", "myaccount", "--adapter", "test")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "extract" do
|
||||||
|
assert_equal "oof", run_command("extract", "foo", "{\"foo\":\"oof\", \"bar\":\"rab\", \"baz\":\"zab\"}")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "extract match from end" do
|
||||||
|
assert_equal "oof", run_command("extract", "foo", "{\"abc/foo\":\"oof\", \"bar\":\"rab\", \"baz\":\"zab\"}")
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def run_command(*command)
|
||||||
|
stdouted { Kamal::Cli::Secrets.start([ *command, "-c", "test/fixtures/deploy_with_accessories.yml" ]) }
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -2,6 +2,8 @@ require "test_helper"
|
|||||||
|
|
||||||
class CommandsAccessoryTest < ActiveSupport::TestCase
|
class CommandsAccessoryTest < ActiveSupport::TestCase
|
||||||
setup do
|
setup do
|
||||||
|
setup_test_secrets("secrets" => "MYSQL_ROOT_PASSWORD=secret123")
|
||||||
|
|
||||||
@config = {
|
@config = {
|
||||||
service: "app", image: "dhh/app", registry: { "server" => "private.registry", "username" => "dhh", "password" => "secret" },
|
service: "app", image: "dhh/app", registry: { "server" => "private.registry", "username" => "dhh", "password" => "secret" },
|
||||||
servers: [ "1.1.1.1" ],
|
servers: [ "1.1.1.1" ],
|
||||||
@@ -41,25 +43,23 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ENV["MYSQL_ROOT_PASSWORD"] = "secret123"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
teardown do
|
teardown do
|
||||||
ENV.delete("MYSQL_ROOT_PASSWORD")
|
teardown_test_secrets
|
||||||
end
|
end
|
||||||
|
|
||||||
test "run" do
|
test "run" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker run --name app-mysql --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 3306:3306 --env-file .kamal/env/accessories/app-mysql.env --env MYSQL_ROOT_HOST=\"%\" --label service=\"app-mysql\" private.registry/mysql:8.0",
|
"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/env/accessories/app-mysql.env --label service=\"app-mysql\" private.registry/mysql:8.0",
|
||||||
new_command(:mysql).run.join(" ")
|
new_command(:mysql).run.join(" ")
|
||||||
|
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker run --name app-redis --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/env/accessories/app-redis.env --env SOMETHING=\"else\" --volume /var/lib/redis:/data --label service=\"app-redis\" --label cache=\"true\" redis:latest",
|
"docker run --name app-redis --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --publish 6379:6379 --env SOMETHING=\"else\" --env-file .kamal/env/accessories/app-redis.env --volume /var/lib/redis:/data --label service=\"app-redis\" --label cache=\"true\" redis:latest",
|
||||||
new_command(:redis).run.join(" ")
|
new_command(:redis).run.join(" ")
|
||||||
|
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker run --name custom-busybox --detach --restart unless-stopped --log-opt max-size=\"10m\" --env-file .kamal/env/accessories/custom-busybox.env --label service=\"custom-busybox\" busybox:latest",
|
"docker run --name custom-busybox --detach --restart unless-stopped --network kamal --log-opt max-size=\"10m\" --env-file .kamal/env/accessories/custom-busybox.env --label service=\"custom-busybox\" busybox:latest",
|
||||||
new_command(:busybox).run.join(" ")
|
new_command(:busybox).run.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
|
|||||||
@config[:logging] = { "driver" => "local", "options" => { "max-size" => "100m", "max-file" => "3" } }
|
@config[:logging] = { "driver" => "local", "options" => { "max-size" => "100m", "max-file" => "3" } }
|
||||||
|
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker run --name custom-busybox --detach --restart unless-stopped --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --env-file .kamal/env/accessories/custom-busybox.env --label service=\"custom-busybox\" busybox:latest",
|
"docker run --name custom-busybox --detach --restart unless-stopped --network kamal --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --env-file .kamal/env/accessories/custom-busybox.env --label service=\"custom-busybox\" busybox:latest",
|
||||||
new_command(:busybox).run.join(" ")
|
new_command(:busybox).run.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -92,7 +92,7 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
|
|||||||
|
|
||||||
test "execute in new container" do
|
test "execute in new container" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker run --rm --env-file .kamal/env/accessories/app-mysql.env --env MYSQL_ROOT_HOST=\"%\" private.registry/mysql:8.0 mysql -u root",
|
"docker run --rm --network kamal --env MYSQL_ROOT_HOST=\"%\" --env-file .kamal/env/accessories/app-mysql.env private.registry/mysql:8.0 mysql -u root",
|
||||||
new_command(:mysql).execute_in_new_container("mysql", "-u", "root").join(" ")
|
new_command(:mysql).execute_in_new_container("mysql", "-u", "root").join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -104,7 +104,7 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
|
|||||||
|
|
||||||
test "execute in new container over ssh" do
|
test "execute in new container over ssh" do
|
||||||
new_command(:mysql).stub(:run_over_ssh, ->(cmd) { cmd.join(" ") }) do
|
new_command(:mysql).stub(:run_over_ssh, ->(cmd) { cmd.join(" ") }) do
|
||||||
assert_match %r{docker run -it --rm --env-file .kamal/env/accessories/app-mysql.env --env MYSQL_ROOT_HOST=\"%\" private.registry/mysql:8.0 mysql -u root},
|
assert_match %r{docker run -it --rm --network kamal --env MYSQL_ROOT_HOST=\"%\" --env-file .kamal/env/accessories/app-mysql.env private.registry/mysql:8.0 mysql -u root},
|
||||||
new_command(:mysql).execute_in_new_container_over_ssh("mysql", "-u", "root")
|
new_command(:mysql).execute_in_new_container_over_ssh("mysql", "-u", "root")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -150,14 +150,6 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
|
|||||||
new_command(:mysql).remove_image.join(" ")
|
new_command(:mysql).remove_image.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "make_env_directory" do
|
|
||||||
assert_equal "mkdir -p .kamal/env/accessories", new_command(:mysql).make_env_directory.join(" ")
|
|
||||||
end
|
|
||||||
|
|
||||||
test "remove_env_file" do
|
|
||||||
assert_equal "rm -f .kamal/env/accessories/app-mysql.env", new_command(:mysql).remove_env_file.join(" ")
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
def new_command(accessory)
|
def new_command(accessory)
|
||||||
Kamal::Commands::Accessory.new(Kamal::Configuration.new(@config), name: accessory)
|
Kamal::Commands::Accessory.new(Kamal::Configuration.new(@config), name: accessory)
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ require "test_helper"
|
|||||||
|
|
||||||
class CommandsAppTest < ActiveSupport::TestCase
|
class CommandsAppTest < ActiveSupport::TestCase
|
||||||
setup do
|
setup do
|
||||||
ENV["RAILS_MASTER_KEY"] = "456"
|
setup_test_secrets("secrets" => "RAILS_MASTER_KEY=456")
|
||||||
Kamal::Configuration.any_instance.stubs(:run_id).returns("12345678901234567890123456789012")
|
Kamal::Configuration.any_instance.stubs(:run_id).returns("12345678901234567890123456789012")
|
||||||
|
|
||||||
@config = { service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ], env: { "secret" => [ "RAILS_MASTER_KEY" ] }, builder: { "arch" => "amd64" } }
|
@config = { service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ], env: { "secret" => [ "RAILS_MASTER_KEY" ] }, builder: { "arch" => "amd64" } }
|
||||||
end
|
end
|
||||||
|
|
||||||
teardown do
|
teardown do
|
||||||
ENV.delete("RAILS_MASTER_KEY")
|
teardown_test_secrets
|
||||||
end
|
end
|
||||||
|
|
||||||
test "run" do
|
test "run" do
|
||||||
@@ -85,7 +85,7 @@ class CommandsAppTest < ActiveSupport::TestCase
|
|||||||
@config[:env]["tags"] = { "tag1" => { "ENV1" => "value1" } }
|
@config[:env]["tags"] = { "tag1" => { "ENV1" => "value1" } }
|
||||||
|
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-web.env --env ENV1=\"value1\" --health-cmd \"(curl -f http://localhost:3000/up || exit 1) && (stat /tmp/kamal-cord/cord > /dev/null || exit 1)\" --health-interval \"1s\" --volume $(pwd)/.kamal/cords/app-web-12345678901234567890123456789012:/tmp/kamal-cord --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label destination --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.app-web.priority=\"2\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999",
|
"docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env ENV1=\"value1\" --env-file .kamal/env/roles/app-web.env --health-cmd \"(curl -f http://localhost:3000/up || exit 1) && (stat /tmp/kamal-cord/cord > /dev/null || exit 1)\" --health-interval \"1s\" --volume $(pwd)/.kamal/cords/app-web-12345678901234567890123456789012:/tmp/kamal-cord --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label destination --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.app-web.priority=\"2\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999",
|
||||||
new_command.run.join(" ")
|
new_command.run.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -219,7 +219,7 @@ class CommandsAppTest < ActiveSupport::TestCase
|
|||||||
@config[:env]["tags"] = { "tag1" => { "ENV1" => "value1" } }
|
@config[:env]["tags"] = { "tag1" => { "ENV1" => "value1" } }
|
||||||
|
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker run --rm --env-file .kamal/env/roles/app-web.env --env ENV1=\"value1\" dhh/app:999 bin/rails db:setup",
|
"docker run --rm --env ENV1=\"value1\" --env-file .kamal/env/roles/app-web.env dhh/app:999 bin/rails db:setup",
|
||||||
new_command.execute_in_new_container("bin/rails", "db:setup", env: {}).join(" ")
|
new_command.execute_in_new_container("bin/rails", "db:setup", env: {}).join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -251,7 +251,7 @@ class CommandsAppTest < ActiveSupport::TestCase
|
|||||||
@config[:servers] = [ { "1.1.1.1" => "tag1" } ]
|
@config[:servers] = [ { "1.1.1.1" => "tag1" } ]
|
||||||
@config[:env]["tags"] = { "tag1" => { "ENV1" => "value1" } }
|
@config[:env]["tags"] = { "tag1" => { "ENV1" => "value1" } }
|
||||||
|
|
||||||
assert_equal "ssh -t root@1.1.1.1 -p 22 'docker run -it --rm --env-file .kamal/env/roles/app-web.env --env ENV1=\"value1\" dhh/app:999 bin/rails c'",
|
assert_equal "ssh -t root@1.1.1.1 -p 22 'docker run -it --rm --env ENV1=\"value1\" --env-file .kamal/env/roles/app-web.env dhh/app:999 bin/rails c'",
|
||||||
new_command.execute_in_new_container_over_ssh("bin/rails", "c", env: {})
|
new_command.execute_in_new_container_over_ssh("bin/rails", "c", env: {})
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -412,14 +412,6 @@ class CommandsAppTest < ActiveSupport::TestCase
|
|||||||
new_command.tag_latest_image.join(" ")
|
new_command.tag_latest_image.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "make_env_directory" do
|
|
||||||
assert_equal "mkdir -p .kamal/env/roles", new_command.make_env_directory.join(" ")
|
|
||||||
end
|
|
||||||
|
|
||||||
test "remove_env_file" do
|
|
||||||
assert_equal "rm -f .kamal/env/roles/app-web.env", new_command.remove_env_file.join(" ")
|
|
||||||
end
|
|
||||||
|
|
||||||
test "cord" do
|
test "cord" do
|
||||||
assert_equal "docker inspect -f '{{ range .Mounts }}{{printf \"%s %s\\n\" .Source .Destination}}{{ end }}' app-web-123 | awk '$2 == \"/tmp/kamal-cord\" {print $1}'", new_command.cord(version: 123).join(" ")
|
assert_equal "docker inspect -f '{{ range .Mounts }}{{printf \"%s %s\\n\" .Source .Destination}}{{ end }}' app-web-123 | awk '$2 == \"/tmp/kamal-cord\" {print $1}'", new_command.cord(version: 123).join(" ")
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class CommandsAuditorTest < ActiveSupport::TestCase
|
|||||||
|
|
||||||
test "record" do
|
test "record" do
|
||||||
assert_equal [
|
assert_equal [
|
||||||
|
:mkdir, "-p", ".kamal", "&&",
|
||||||
:echo,
|
:echo,
|
||||||
"[#{@recorded_at}] [#{@performer}]",
|
"[#{@recorded_at}] [#{@performer}]",
|
||||||
"app removed container",
|
"app removed container",
|
||||||
@@ -28,6 +29,7 @@ class CommandsAuditorTest < ActiveSupport::TestCase
|
|||||||
test "record with destination" do
|
test "record with destination" do
|
||||||
new_command(destination: "staging").tap do |auditor|
|
new_command(destination: "staging").tap do |auditor|
|
||||||
assert_equal [
|
assert_equal [
|
||||||
|
:mkdir, "-p", ".kamal", "&&",
|
||||||
:echo,
|
:echo,
|
||||||
"[#{@recorded_at}] [#{@performer}] [staging]",
|
"[#{@recorded_at}] [#{@performer}] [staging]",
|
||||||
"app removed container",
|
"app removed container",
|
||||||
@@ -39,6 +41,7 @@ class CommandsAuditorTest < ActiveSupport::TestCase
|
|||||||
test "record with command details" do
|
test "record with command details" do
|
||||||
new_command(role: "web").tap do |auditor|
|
new_command(role: "web").tap do |auditor|
|
||||||
assert_equal [
|
assert_equal [
|
||||||
|
:mkdir, "-p", ".kamal", "&&",
|
||||||
:echo,
|
:echo,
|
||||||
"[#{@recorded_at}] [#{@performer}] [web]",
|
"[#{@recorded_at}] [#{@performer}] [web]",
|
||||||
"app removed container",
|
"app removed container",
|
||||||
@@ -49,6 +52,7 @@ class CommandsAuditorTest < ActiveSupport::TestCase
|
|||||||
|
|
||||||
test "record with arg details" do
|
test "record with arg details" do
|
||||||
assert_equal [
|
assert_equal [
|
||||||
|
:mkdir, "-p", ".kamal", "&&",
|
||||||
:echo,
|
:echo,
|
||||||
"[#{@recorded_at}] [#{@performer}] [value]",
|
"[#{@recorded_at}] [#{@performer}] [value]",
|
||||||
"app removed container",
|
"app removed container",
|
||||||
|
|||||||
@@ -37,11 +37,19 @@ class CommandsBuilderTest < ActiveSupport::TestCase
|
|||||||
builder.push.join(" ")
|
builder.push.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "remote build if remote is set and local disabled" do
|
||||||
|
builder = new_builder_command(builder: { "arch" => [ "amd64", "arm64" ], "remote" => "ssh://app@127.0.0.1", "cache" => { "type" => "gha" }, "local" => false })
|
||||||
|
assert_equal "remote", builder.name
|
||||||
|
assert_equal \
|
||||||
|
"docker buildx build --push --platform linux/amd64,linux/arm64 --builder kamal-remote-ssh---app-127-0-0-1 -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile .",
|
||||||
|
builder.push.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
test "target remote when remote set and arch is non local" do
|
test "target remote when remote set and arch is non local" do
|
||||||
builder = new_builder_command(builder: { "arch" => [ "#{remote_arch}" ], "remote" => "ssh://app@host", "cache" => { "type" => "gha" } })
|
builder = new_builder_command(builder: { "arch" => [ "#{remote_arch}" ], "remote" => "ssh://app@host", "cache" => { "type" => "gha" } })
|
||||||
assert_equal "remote", builder.name
|
assert_equal "remote", builder.name
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker buildx build --push --platform linux/#{remote_arch} --builder kamal-remote-docker-container-ssh---app-host -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile .",
|
"docker buildx build --push --platform linux/#{remote_arch} --builder kamal-remote-ssh---app-host -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile .",
|
||||||
builder.push.join(" ")
|
builder.push.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -61,11 +69,14 @@ class CommandsBuilderTest < ActiveSupport::TestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "build secrets" do
|
test "build secrets" do
|
||||||
|
with_test_secrets("secrets" => "token_a=foo\ntoken_b=bar") do
|
||||||
|
FileUtils.touch("Dockerfile")
|
||||||
builder = new_builder_command(builder: { "secrets" => [ "token_a", "token_b" ] })
|
builder = new_builder_command(builder: { "secrets" => [ "token_a", "token_b" ] })
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"-t dhh/app:123 -t dhh/app:latest --label service=\"app\" --secret id=\"token_a\" --secret id=\"token_b\" --file Dockerfile",
|
"-t dhh/app:123 -t dhh/app:latest --label service=\"app\" --secret id=\"token_a\" --secret id=\"token_b\" --file Dockerfile",
|
||||||
builder.target.build_options.join(" ")
|
builder.target.build_options.join(" ")
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
test "build dockerfile" do
|
test "build dockerfile" do
|
||||||
Pathname.any_instance.expects(:exist?).returns(true).once
|
Pathname.any_instance.expects(:exist?).returns(true).once
|
||||||
@@ -105,11 +116,14 @@ class CommandsBuilderTest < ActiveSupport::TestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "push with build secrets" do
|
test "push with build secrets" do
|
||||||
|
with_test_secrets("secrets" => "a=foo\nb=bar") do
|
||||||
|
FileUtils.touch("Dockerfile")
|
||||||
builder = new_builder_command(builder: { "secrets" => [ "a", "b" ] })
|
builder = new_builder_command(builder: { "secrets" => [ "a", "b" ] })
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker buildx build --push --platform linux/amd64 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --secret id=\"a\" --secret id=\"b\" --file Dockerfile .",
|
"docker buildx build --push --platform linux/amd64 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --secret id=\"a\" --secret id=\"b\" --file Dockerfile .",
|
||||||
builder.push.join(" ")
|
builder.push.join(" ")
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
test "build with ssh agent socket" do
|
test "build with ssh agent socket" do
|
||||||
builder = new_builder_command(builder: { "ssh" => "default=$SSH_AUTH_SOCK" })
|
builder = new_builder_command(builder: { "ssh" => "default=$SSH_AUTH_SOCK" })
|
||||||
|
|||||||
@@ -39,6 +39,21 @@ class CommandsHookTest < ActiveSupport::TestCase
|
|||||||
], new_command(hooks_path: "custom/hooks/path").run("foo")
|
], new_command(hooks_path: "custom/hooks/path").run("foo")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "hook with secrets" do
|
||||||
|
with_test_secrets("secrets" => "DB_PASSWORD=secret") do
|
||||||
|
assert_equal [
|
||||||
|
".kamal/hooks/foo",
|
||||||
|
{ env: {
|
||||||
|
"KAMAL_RECORDED_AT" => @recorded_at,
|
||||||
|
"KAMAL_PERFORMER" => @performer,
|
||||||
|
"KAMAL_VERSION" => "123",
|
||||||
|
"KAMAL_SERVICE_VERSION" => "app@123",
|
||||||
|
"KAMAL_SERVICE" => "app",
|
||||||
|
"DB_PASSWORD" => "secret" } }
|
||||||
|
], new_command(env: { "secret" => [ "DB_PASSWORD" ] }).run("foo", secrets: true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def new_command(**extra_config)
|
def new_command(**extra_config)
|
||||||
Kamal::Commands::Hook.new(Kamal::Configuration.new(@config.merge(**extra_config), version: "123"))
|
Kamal::Commands::Hook.new(Kamal::Configuration.new(@config.merge(**extra_config), version: "123"))
|
||||||
|
|||||||
126
test/commands/proxy_test.rb
Normal file
126
test/commands/proxy_test.rb
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class CommandsProxyTest < ActiveSupport::TestCase
|
||||||
|
setup do
|
||||||
|
@config = {
|
||||||
|
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ], builder: { "arch" => "amd64" }
|
||||||
|
}
|
||||||
|
|
||||||
|
ENV["EXAMPLE_API_KEY"] = "456"
|
||||||
|
end
|
||||||
|
|
||||||
|
teardown do
|
||||||
|
ENV.delete("EXAMPLE_API_KEY")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "run" do
|
||||||
|
assert_equal \
|
||||||
|
"docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/proxy/config:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}",
|
||||||
|
new_command.run.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "run with ports configured" do
|
||||||
|
assert_equal \
|
||||||
|
"docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/proxy/config:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}",
|
||||||
|
new_command.run.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "run without configuration" do
|
||||||
|
@config.delete(:proxy)
|
||||||
|
|
||||||
|
assert_equal \
|
||||||
|
"docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/proxy/config:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}",
|
||||||
|
new_command.run.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "run with logging config" do
|
||||||
|
@config[:logging] = { "driver" => "local", "options" => { "max-size" => "100m", "max-file" => "3" } }
|
||||||
|
|
||||||
|
assert_equal \
|
||||||
|
"docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume $(pwd)/.kamal/proxy/config:/root/.config/kamal-proxy --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}",
|
||||||
|
new_command.run.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "proxy start" do
|
||||||
|
assert_equal \
|
||||||
|
"docker container start kamal-proxy",
|
||||||
|
new_command.start.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "proxy stop" do
|
||||||
|
assert_equal \
|
||||||
|
"docker container stop kamal-proxy",
|
||||||
|
new_command.stop.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "proxy info" do
|
||||||
|
assert_equal \
|
||||||
|
"docker ps --filter name=^kamal-proxy$",
|
||||||
|
new_command.info.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "proxy logs" do
|
||||||
|
assert_equal \
|
||||||
|
"docker logs kamal-proxy --timestamps 2>&1",
|
||||||
|
new_command.logs.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "proxy logs since 2h" do
|
||||||
|
assert_equal \
|
||||||
|
"docker logs kamal-proxy --since 2h --timestamps 2>&1",
|
||||||
|
new_command.logs(since: "2h").join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "proxy logs last 10 lines" do
|
||||||
|
assert_equal \
|
||||||
|
"docker logs kamal-proxy --tail 10 --timestamps 2>&1",
|
||||||
|
new_command.logs(lines: 10).join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "proxy logs with grep hello!" do
|
||||||
|
assert_equal \
|
||||||
|
"docker logs kamal-proxy --timestamps 2>&1 | grep 'hello!'",
|
||||||
|
new_command.logs(grep: "hello!").join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "proxy remove container" do
|
||||||
|
assert_equal \
|
||||||
|
"docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy",
|
||||||
|
new_command.remove_container.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "proxy remove image" do
|
||||||
|
assert_equal \
|
||||||
|
"docker image prune --all --force --filter label=org.opencontainers.image.title=kamal-proxy",
|
||||||
|
new_command.remove_image.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "proxy follow logs" do
|
||||||
|
assert_equal \
|
||||||
|
"ssh -t root@1.1.1.1 -p 22 'docker logs kamal-proxy --timestamps --tail 10 --follow 2>&1'",
|
||||||
|
new_command.follow_logs(host: @config[:servers].first)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "proxy follow logs with grep hello!" do
|
||||||
|
assert_equal \
|
||||||
|
"ssh -t root@1.1.1.1 -p 22 'docker logs kamal-proxy --timestamps --tail 10 --follow 2>&1 | grep \"hello!\"'",
|
||||||
|
new_command.follow_logs(host: @config[:servers].first, grep: "hello!")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "deploy" do
|
||||||
|
assert_equal \
|
||||||
|
"docker exec kamal-proxy kamal-proxy deploy service --target \"172.1.0.2:80\" --buffer-requests --buffer-responses --log-request-header \"Cache-Control\" --log-request-header \"Last-Modified\"",
|
||||||
|
new_command.deploy("service", target: "172.1.0.2").join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "remove" do
|
||||||
|
assert_equal \
|
||||||
|
"docker exec kamal-proxy kamal-proxy remove service --target \"172.1.0.2:80\"",
|
||||||
|
new_command.remove("service", target: "172.1.0.2").join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def new_command
|
||||||
|
Kamal::Commands::Proxy.new(Kamal::Configuration.new(@config, version: "123"))
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -11,51 +11,52 @@ class CommandsRegistryTest < ActiveSupport::TestCase
|
|||||||
builder: { "arch" => "amd64" },
|
builder: { "arch" => "amd64" },
|
||||||
servers: [ "1.1.1.1" ]
|
servers: [ "1.1.1.1" ]
|
||||||
}
|
}
|
||||||
@registry = Kamal::Commands::Registry.new Kamal::Configuration.new(@config)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "registry login" do
|
test "registry login" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker login hub.docker.com -u \"dhh\" -p \"secret\"",
|
"docker login hub.docker.com -u \"dhh\" -p \"secret\"",
|
||||||
@registry.login.join(" ")
|
registry.login.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "registry login with ENV password" do
|
test "registry login with ENV password" do
|
||||||
ENV["KAMAL_REGISTRY_PASSWORD"] = "more-secret"
|
with_test_secrets("secrets" => "KAMAL_REGISTRY_PASSWORD=more-secret") do
|
||||||
@config[:registry]["password"] = [ "KAMAL_REGISTRY_PASSWORD" ]
|
@config[:registry]["password"] = [ "KAMAL_REGISTRY_PASSWORD" ]
|
||||||
|
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker login hub.docker.com -u \"dhh\" -p \"more-secret\"",
|
"docker login hub.docker.com -u \"dhh\" -p \"more-secret\"",
|
||||||
@registry.login.join(" ")
|
registry.login.join(" ")
|
||||||
ensure
|
end
|
||||||
ENV.delete("KAMAL_REGISTRY_PASSWORD")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "registry login escape password" do
|
test "registry login escape password" do
|
||||||
ENV["KAMAL_REGISTRY_PASSWORD"] = "more-secret'\""
|
with_test_secrets("secrets" => "KAMAL_REGISTRY_PASSWORD=more-secret'\"") do
|
||||||
@config[:registry]["password"] = [ "KAMAL_REGISTRY_PASSWORD" ]
|
@config[:registry]["password"] = [ "KAMAL_REGISTRY_PASSWORD" ]
|
||||||
|
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker login hub.docker.com -u \"dhh\" -p \"more-secret'\\\"\"",
|
"docker login hub.docker.com -u \"dhh\" -p \"more-secret'\\\"\"",
|
||||||
@registry.login.join(" ")
|
registry.login.join(" ")
|
||||||
ensure
|
end
|
||||||
ENV.delete("KAMAL_REGISTRY_PASSWORD")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "registry login with ENV username" do
|
test "registry login with ENV username" do
|
||||||
ENV["KAMAL_REGISTRY_USERNAME"] = "also-secret"
|
with_test_secrets("secrets" => "KAMAL_REGISTRY_USERNAME=also-secret") do
|
||||||
@config[:registry]["username"] = [ "KAMAL_REGISTRY_USERNAME" ]
|
@config[:registry]["username"] = [ "KAMAL_REGISTRY_USERNAME" ]
|
||||||
|
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker login hub.docker.com -u \"also-secret\" -p \"secret\"",
|
"docker login hub.docker.com -u \"also-secret\" -p \"secret\"",
|
||||||
@registry.login.join(" ")
|
registry.login.join(" ")
|
||||||
ensure
|
end
|
||||||
ENV.delete("KAMAL_REGISTRY_USERNAME")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "registry logout" do
|
test "registry logout" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker logout hub.docker.com",
|
"docker logout hub.docker.com",
|
||||||
@registry.logout.join(" ")
|
registry.logout.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def registry
|
||||||
|
Kamal::Commands::Registry.new Kamal::Configuration.new(@config)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -12,10 +12,6 @@ class CommandsServerTest < ActiveSupport::TestCase
|
|||||||
assert_equal "mkdir -p .kamal", new_command.ensure_run_directory.join(" ")
|
assert_equal "mkdir -p .kamal", new_command.ensure_run_directory.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "ensure non default run directory" do
|
|
||||||
assert_equal "mkdir -p /var/run/kamal", new_command(run_directory: "/var/run/kamal").ensure_run_directory.join(" ")
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
def new_command(extra_config = {})
|
def new_command(extra_config = {})
|
||||||
Kamal::Commands::Server.new(Kamal::Configuration.new(@config.merge(extra_config)))
|
Kamal::Commands::Server.new(Kamal::Configuration.new(@config.merge(extra_config)))
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ class CommandsTraefikTest < ActiveSupport::TestCase
|
|||||||
traefik: { "image" => @image, "args" => { "accesslog.format" => "json", "api.insecure" => true, "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } }
|
traefik: { "image" => @image, "args" => { "accesslog.format" => "json", "api.insecure" => true, "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } }
|
||||||
}
|
}
|
||||||
|
|
||||||
ENV["EXAMPLE_API_KEY"] = "456"
|
setup_test_secrets("secrets" => "EXAMPLE_API_KEY=456")
|
||||||
end
|
end
|
||||||
|
|
||||||
teardown do
|
teardown do
|
||||||
ENV.delete("EXAMPLE_API_KEY")
|
teardown_test_secrets
|
||||||
end
|
end
|
||||||
|
|
||||||
test "run" do
|
test "run" do
|
||||||
@@ -81,9 +81,9 @@ class CommandsTraefikTest < ActiveSupport::TestCase
|
|||||||
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
|
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
|
||||||
new_command.run.join(" ")
|
new_command.run.join(" ")
|
||||||
|
|
||||||
@config[:traefik]["env"] = { "secret" => %w[EXAMPLE_API_KEY] }
|
@config[:traefik]["env"] = { "EXAMPLE_API_KEY" => "456" }
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
|
"docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --env EXAMPLE_API_KEY=\"456\" --env-file .kamal/env/traefik/traefik.env --log-opt max-size=\"10m\" --label traefik.http.routers.catchall.entryPoints=\"http\" --label traefik.http.routers.catchall.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.routers.catchall.service=\"unavailable\" --label traefik.http.routers.catchall.priority=\"1\" --label traefik.http.services.unavailable.loadbalancer.server.port=\"0\" #{@image} --providers.docker --log.level=\"DEBUG\" --accesslog.format=\"json\" --api.insecure --metrics.prometheus.buckets=\"0.1,0.3,1.2,5.0\"",
|
||||||
new_command.run.join(" ")
|
new_command.run.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -188,20 +188,6 @@ class CommandsTraefikTest < ActiveSupport::TestCase
|
|||||||
new_command.follow_logs(host: @config[:servers].first, grep: "hello!")
|
new_command.follow_logs(host: @config[:servers].first, grep: "hello!")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "secrets io" do
|
|
||||||
@config[:traefik]["env"] = { "secret" => %w[EXAMPLE_API_KEY] }
|
|
||||||
|
|
||||||
assert_equal "EXAMPLE_API_KEY=456\n", new_command.env.secrets_io.string
|
|
||||||
end
|
|
||||||
|
|
||||||
test "make_env_directory" do
|
|
||||||
assert_equal "mkdir -p .kamal/env/traefik", new_command.make_env_directory.join(" ")
|
|
||||||
end
|
|
||||||
|
|
||||||
test "remove_env_file" do
|
|
||||||
assert_equal "rm -f .kamal/env/traefik/traefik.env", new_command.remove_env_file.join(" ")
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
def new_command
|
def new_command
|
||||||
Kamal::Commands::Traefik.new(Kamal::Configuration.new(@config, version: "123"))
|
Kamal::Commands::Traefik.new(Kamal::Configuration.new(@config, version: "123"))
|
||||||
|
|||||||
@@ -116,25 +116,14 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "env args" do
|
test "env args" do
|
||||||
assert_equal [ "--env-file", ".kamal/env/accessories/app-mysql.env", "--env", "MYSQL_ROOT_HOST=\"%\"" ], @config.accessory(:mysql).env_args
|
with_test_secrets("secrets" => "MYSQL_ROOT_PASSWORD=secret123") do
|
||||||
assert_equal [ "--env-file", ".kamal/env/accessories/app-redis.env", "--env", "SOMETHING=\"else\"" ], @config.accessory(:redis).env_args
|
config = Kamal::Configuration.new(@deploy)
|
||||||
|
|
||||||
|
assert_equal [ "--env", "MYSQL_ROOT_HOST=\"%\"", "--env-file", ".kamal/env/accessories/app-mysql.env" ], config.accessory(:mysql).env_args.map(&:to_s)
|
||||||
|
assert_equal "MYSQL_ROOT_PASSWORD=secret123\n", config.accessory(:mysql).secrets_io.string
|
||||||
|
assert_equal [ "--env", "SOMETHING=\"else\"", "--env-file", ".kamal/env/accessories/app-redis.env" ], @config.accessory(:redis).env_args
|
||||||
|
assert_equal "\n", config.accessory(:redis).secrets_io.string
|
||||||
end
|
end
|
||||||
|
|
||||||
test "env with secrets" do
|
|
||||||
ENV["MYSQL_ROOT_PASSWORD"] = "secret123"
|
|
||||||
|
|
||||||
expected_secrets_file = <<~ENV
|
|
||||||
MYSQL_ROOT_PASSWORD=secret123
|
|
||||||
ENV
|
|
||||||
|
|
||||||
assert_equal expected_secrets_file, @config.accessory(:mysql).env.secrets_io.string
|
|
||||||
assert_equal [ "--env-file", ".kamal/env/accessories/app-mysql.env", "--env", "MYSQL_ROOT_HOST=\"%\"" ], @config.accessory(:mysql).env_args
|
|
||||||
ensure
|
|
||||||
ENV["MYSQL_ROOT_PASSWORD"] = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
test "env secrets path" do
|
|
||||||
assert_equal ".kamal/env/accessories/app-mysql.env", @config.accessory(:mysql).env.secrets_file
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "volume args" do
|
test "volume args" do
|
||||||
|
|||||||
@@ -93,13 +93,15 @@ class ConfigurationBuilderTest < ActiveSupport::TestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "secrets" do
|
test "secrets" do
|
||||||
assert_equal [], config.builder.secrets
|
assert_equal({}, config.builder.secrets)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "setting secrets" do
|
test "setting secrets" do
|
||||||
|
with_test_secrets("secrets" => "GITHUB_TOKEN=secret123") do
|
||||||
@deploy[:builder]["secrets"] = [ "GITHUB_TOKEN" ]
|
@deploy[:builder]["secrets"] = [ "GITHUB_TOKEN" ]
|
||||||
|
|
||||||
assert_equal [ "GITHUB_TOKEN" ], config.builder.secrets
|
assert_equal({ "GITHUB_TOKEN" => "secret123" }, config.builder.secrets)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "dockerfile" do
|
test "dockerfile" do
|
||||||
@@ -132,6 +134,23 @@ class ConfigurationBuilderTest < ActiveSupport::TestCase
|
|||||||
assert_equal "default=$SSH_AUTH_SOCK", config.builder.ssh
|
assert_equal "default=$SSH_AUTH_SOCK", config.builder.ssh
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "local disabled but no remote set" do
|
||||||
|
@deploy[:builder]["local"] = false
|
||||||
|
|
||||||
|
assert_raises(Kamal::ConfigurationError) do
|
||||||
|
config.builder
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "local disabled all arches are remote" do
|
||||||
|
@deploy[:builder]["local"] = false
|
||||||
|
@deploy[:builder]["remote"] = "ssh://root@192.168.0.1"
|
||||||
|
@deploy[:builder]["arch"] = [ "amd64", "arm64" ]
|
||||||
|
|
||||||
|
assert_equal [], config.builder.local_arches
|
||||||
|
assert_equal [ "amd64", "arm64" ], config.builder.remote_arches
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def config
|
def config
|
||||||
Kamal::Configuration.new(@deploy)
|
Kamal::Configuration.new(@deploy)
|
||||||
|
|||||||
6
test/configuration/env/tags_test.rb
vendored
6
test/configuration/env/tags_test.rb
vendored
@@ -79,8 +79,7 @@ class ConfigurationEnvTagsTest < ActiveSupport::TestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "tag secret env" do
|
test "tag secret env" do
|
||||||
ENV["PASSWORD"] = "hello"
|
with_test_secrets("secrets" => "PASSWORD=hello") do
|
||||||
|
|
||||||
deploy = {
|
deploy = {
|
||||||
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" },
|
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" },
|
||||||
servers: [ { "1.1.1.1" => "secrets" } ],
|
servers: [ { "1.1.1.1" => "secrets" } ],
|
||||||
@@ -94,8 +93,7 @@ class ConfigurationEnvTagsTest < ActiveSupport::TestCase
|
|||||||
|
|
||||||
config = Kamal::Configuration.new(deploy)
|
config = Kamal::Configuration.new(deploy)
|
||||||
assert_equal "hello", config.role("web").env("1.1.1.1").secrets["PASSWORD"]
|
assert_equal "hello", config.role("web").env("1.1.1.1").secrets["PASSWORD"]
|
||||||
ensure
|
end
|
||||||
ENV.delete "PASSWORD"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "tag clear env" do
|
test "tag clear env" do
|
||||||
|
|||||||
@@ -6,27 +6,21 @@ class ConfigurationEnvTest < ActiveSupport::TestCase
|
|||||||
test "simple" do
|
test "simple" do
|
||||||
assert_config \
|
assert_config \
|
||||||
config: { "foo" => "bar", "baz" => "haz" },
|
config: { "foo" => "bar", "baz" => "haz" },
|
||||||
clear: { "foo" => "bar", "baz" => "haz" },
|
clear: { "foo" => "bar", "baz" => "haz" }
|
||||||
secrets: {}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "clear" do
|
test "clear" do
|
||||||
assert_config \
|
assert_config \
|
||||||
config: { "clear" => { "foo" => "bar", "baz" => "haz" } },
|
config: { "clear" => { "foo" => "bar", "baz" => "haz" } },
|
||||||
clear: { "foo" => "bar", "baz" => "haz" },
|
clear: { "foo" => "bar", "baz" => "haz" }
|
||||||
secrets: {}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "secret" do
|
test "secret" do
|
||||||
ENV["PASSWORD"] = "hello"
|
with_test_secrets("secrets" => "PASSWORD=hello") do
|
||||||
env = Kamal::Configuration::Env.new config: { "secret" => [ "PASSWORD" ] }
|
|
||||||
|
|
||||||
assert_config \
|
assert_config \
|
||||||
config: { "secret" => [ "PASSWORD" ] },
|
config: { "secret" => [ "PASSWORD" ] },
|
||||||
clear: {},
|
|
||||||
secrets: { "PASSWORD" => "hello" }
|
secrets: { "PASSWORD" => "hello" }
|
||||||
ensure
|
end
|
||||||
ENV.delete "PASSWORD"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "missing secret" do
|
test "missing secret" do
|
||||||
@@ -34,11 +28,11 @@ class ConfigurationEnvTest < ActiveSupport::TestCase
|
|||||||
"secret" => [ "PASSWORD" ]
|
"secret" => [ "PASSWORD" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
assert_raises(KeyError) { Kamal::Configuration::Env.new(config: { "secret" => [ "PASSWORD" ] }).secrets }
|
assert_raises(Kamal::ConfigurationError) { Kamal::Configuration::Env.new(config: { "secret" => [ "PASSWORD" ] }, secrets: Kamal::Secrets.new).secrets_io }
|
||||||
end
|
end
|
||||||
|
|
||||||
test "secret and clear" do
|
test "secret and clear" do
|
||||||
ENV["PASSWORD"] = "hello"
|
with_test_secrets("secrets" => "PASSWORD=hello") do
|
||||||
config = {
|
config = {
|
||||||
"secret" => [ "PASSWORD" ],
|
"secret" => [ "PASSWORD" ],
|
||||||
"clear" => {
|
"clear" => {
|
||||||
@@ -51,24 +45,15 @@ class ConfigurationEnvTest < ActiveSupport::TestCase
|
|||||||
config: config,
|
config: config,
|
||||||
clear: { "foo" => "bar", "baz" => "haz" },
|
clear: { "foo" => "bar", "baz" => "haz" },
|
||||||
secrets: { "PASSWORD" => "hello" }
|
secrets: { "PASSWORD" => "hello" }
|
||||||
ensure
|
|
||||||
ENV.delete "PASSWORD"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "stringIO conversion" do
|
|
||||||
env = {
|
|
||||||
"foo" => "bar",
|
|
||||||
"baz" => "haz"
|
|
||||||
}
|
|
||||||
|
|
||||||
assert_equal "foo=bar\nbaz=haz\n", \
|
|
||||||
StringIO.new(Kamal::EnvFile.new(env)).read
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def assert_config(config:, clear:, secrets:)
|
def assert_config(config:, clear: {}, secrets: {})
|
||||||
env = Kamal::Configuration::Env.new config: config, secrets_file: "secrets.env"
|
env = Kamal::Configuration::Env.new config: config, secrets: Kamal::Secrets.new
|
||||||
assert_equal clear, env.clear
|
expected_clear_args = clear.to_a.flat_map { |key, value| [ "--env", "#{key}=\"#{value}\"" ] }
|
||||||
assert_equal secrets, env.secrets
|
assert_equal expected_clear_args, env.clear_args.map(&:to_s) # to_s removes the redactions
|
||||||
|
expected_secrets = secrets.to_a.flat_map { |key, value| "#{key}=#{value}" }.join("\n") + "\n"
|
||||||
|
assert_equal expected_secrets, env.secrets_io.string
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
25
test/configuration/proxy_test.rb
Normal file
25
test/configuration/proxy_test.rb
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class ConfigurationEnvTest < ActiveSupport::TestCase
|
||||||
|
setup do
|
||||||
|
@deploy = {
|
||||||
|
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" },
|
||||||
|
builder: { "arch" => "amd64" }, servers: [ "1.1.1.1" ]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "ssl with host" do
|
||||||
|
@deploy[:proxy] = { "ssl" => true, "host" => "example.com" }
|
||||||
|
assert_equal true, config.proxy.ssl?
|
||||||
|
end
|
||||||
|
|
||||||
|
test "ssl with no host" do
|
||||||
|
@deploy[:proxy] = { "ssl" => true }
|
||||||
|
assert_raises(Kamal::ConfigurationError) { config.proxy.ssl? }
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def config
|
||||||
|
Kamal::Configuration.new(@deploy)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -9,8 +9,6 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
|
|||||||
env: { "REDIS_URL" => "redis://x/y" }
|
env: { "REDIS_URL" => "redis://x/y" }
|
||||||
}
|
}
|
||||||
|
|
||||||
@config = Kamal::Configuration.new(@deploy)
|
|
||||||
|
|
||||||
@deploy_with_roles = @deploy.dup.merge({
|
@deploy_with_roles = @deploy.dup.merge({
|
||||||
servers: {
|
servers: {
|
||||||
"web" => [ "1.1.1.1", "1.1.1.2" ],
|
"web" => [ "1.1.1.1", "1.1.1.2" ],
|
||||||
@@ -24,31 +22,29 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@config_with_roles = Kamal::Configuration.new(@deploy_with_roles)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "hosts" do
|
test "hosts" do
|
||||||
assert_equal [ "1.1.1.1", "1.1.1.2" ], @config.role(:web).hosts
|
assert_equal [ "1.1.1.1", "1.1.1.2" ], config.role(:web).hosts
|
||||||
assert_equal [ "1.1.1.3", "1.1.1.4" ], @config_with_roles.role(:workers).hosts
|
assert_equal [ "1.1.1.3", "1.1.1.4" ], config_with_roles.role(:workers).hosts
|
||||||
end
|
end
|
||||||
|
|
||||||
test "cmd" do
|
test "cmd" do
|
||||||
assert_nil @config.role(:web).cmd
|
assert_nil config.role(:web).cmd
|
||||||
assert_equal "bin/jobs", @config_with_roles.role(:workers).cmd
|
assert_equal "bin/jobs", config_with_roles.role(:workers).cmd
|
||||||
end
|
end
|
||||||
|
|
||||||
test "label args" do
|
test "label args" do
|
||||||
assert_equal [ "--label", "service=\"app\"", "--label", "role=\"workers\"", "--label", "destination" ], @config_with_roles.role(:workers).label_args
|
assert_equal [ "--label", "service=\"app\"", "--label", "role=\"workers\"", "--label", "destination" ], config_with_roles.role(:workers).label_args
|
||||||
end
|
end
|
||||||
|
|
||||||
test "special label args for web" do
|
test "special label args for web" do
|
||||||
assert_equal [ "--label", "service=\"app\"", "--label", "role=\"web\"", "--label", "destination", "--label", "traefik.http.services.app-web.loadbalancer.server.scheme=\"http\"", "--label", "traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\"", "--label", "traefik.http.routers.app-web.priority=\"2\"", "--label", "traefik.http.middlewares.app-web-retry.retry.attempts=\"5\"", "--label", "traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\"", "--label", "traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\"" ], @config.role(:web).label_args
|
assert_equal [ "--label", "service=\"app\"", "--label", "role=\"web\"", "--label", "destination", "--label", "traefik.http.services.app-web.loadbalancer.server.scheme=\"http\"", "--label", "traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\"", "--label", "traefik.http.routers.app-web.priority=\"2\"", "--label", "traefik.http.middlewares.app-web-retry.retry.attempts=\"5\"", "--label", "traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\"", "--label", "traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\"" ], config.role(:web).label_args
|
||||||
end
|
end
|
||||||
|
|
||||||
test "custom labels" do
|
test "custom labels" do
|
||||||
@deploy[:labels] = { "my.custom.label" => "50" }
|
@deploy[:labels] = { "my.custom.label" => "50" }
|
||||||
assert_equal "50", @config.role(:web).labels["my.custom.label"]
|
assert_equal "50", config.role(:web).labels["my.custom.label"]
|
||||||
end
|
end
|
||||||
|
|
||||||
test "custom labels via role specialization" do
|
test "custom labels via role specialization" do
|
||||||
@@ -59,7 +55,7 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
|
|||||||
|
|
||||||
test "overwriting default traefik label" do
|
test "overwriting default traefik label" do
|
||||||
@deploy[:labels] = { "traefik.http.routers.app-web.rule" => "\"Host(\\`example.com\\`) || (Host(\\`example.org\\`) && Path(\\`/traefik\\`))\"" }
|
@deploy[:labels] = { "traefik.http.routers.app-web.rule" => "\"Host(\\`example.com\\`) || (Host(\\`example.org\\`) && Path(\\`/traefik\\`))\"" }
|
||||||
assert_equal "\"Host(\\`example.com\\`) || (Host(\\`example.org\\`) && Path(\\`/traefik\\`))\"", @config.role(:web).labels["traefik.http.routers.app-web.rule"]
|
assert_equal "\"Host(\\`example.com\\`) || (Host(\\`example.org\\`) && Path(\\`/traefik\\`))\"", config.role(:web).labels["traefik.http.routers.app-web.rule"]
|
||||||
end
|
end
|
||||||
|
|
||||||
test "default traefik label on non-web role" do
|
test "default traefik label on non-web role" do
|
||||||
@@ -71,26 +67,38 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "env overwritten by role" do
|
test "env overwritten by role" do
|
||||||
assert_equal "redis://a/b", @config_with_roles.role(:workers).env("1.1.1.3").clear["REDIS_URL"]
|
assert_equal "redis://a/b", config_with_roles.role(:workers).env("1.1.1.3").clear["REDIS_URL"]
|
||||||
|
|
||||||
assert_equal "\n", @config_with_roles.role(:workers).env("1.1.1.3").secrets_io.string
|
assert_equal \
|
||||||
assert_equal [ "--env-file", ".kamal/env/roles/app-workers.env", "--env", "REDIS_URL=\"redis://a/b\"", "--env", "WEB_CONCURRENCY=\"4\"" ], @config_with_roles.role(:workers).env_args("1.1.1.3")
|
[ "--env", "REDIS_URL=\"redis://a/b\"", "--env", "WEB_CONCURRENCY=\"4\"", "--env-file", ".kamal/env/roles/app-workers.env" ],
|
||||||
|
config_with_roles.role(:workers).env_args("1.1.1.3").map(&:to_s)
|
||||||
|
|
||||||
|
assert_equal \
|
||||||
|
"\n",
|
||||||
|
config_with_roles.role(:workers).secrets_io("1.1.1.3").read
|
||||||
end
|
end
|
||||||
|
|
||||||
test "container name" do
|
test "container name" do
|
||||||
ENV["VERSION"] = "12345"
|
ENV["VERSION"] = "12345"
|
||||||
|
|
||||||
assert_equal "app-workers-12345", @config_with_roles.role(:workers).container_name
|
assert_equal "app-workers-12345", config_with_roles.role(:workers).container_name
|
||||||
assert_equal "app-web-12345", @config_with_roles.role(:web).container_name
|
assert_equal "app-web-12345", config_with_roles.role(:web).container_name
|
||||||
ensure
|
ensure
|
||||||
ENV.delete("VERSION")
|
ENV.delete("VERSION")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "env args" do
|
test "env args" do
|
||||||
assert_equal [ "--env-file", ".kamal/env/roles/app-workers.env", "--env", "REDIS_URL=\"redis://a/b\"", "--env", "WEB_CONCURRENCY=\"4\"" ], @config_with_roles.role(:workers).env_args("1.1.1.3")
|
assert_equal \
|
||||||
|
[ "--env", "REDIS_URL=\"redis://a/b\"", "--env", "WEB_CONCURRENCY=\"4\"", "--env-file", ".kamal/env/roles/app-workers.env" ],
|
||||||
|
config_with_roles.role(:workers).env_args("1.1.1.3").map(&:to_s)
|
||||||
|
|
||||||
|
assert_equal \
|
||||||
|
"\n",
|
||||||
|
config_with_roles.role(:workers).secrets_io("1.1.1.3").read
|
||||||
end
|
end
|
||||||
|
|
||||||
test "env secret overwritten by role" do
|
test "env secret overwritten by role" do
|
||||||
|
with_test_secrets("secrets" => "REDIS_PASSWORD=secret456\nDB_PASSWORD=secret&\"123") do
|
||||||
@deploy_with_roles[:env] = {
|
@deploy_with_roles[:env] = {
|
||||||
"clear" => {
|
"clear" => {
|
||||||
"REDIS_URL" => "redis://a/b"
|
"REDIS_URL" => "redis://a/b"
|
||||||
@@ -110,22 +118,18 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
ENV["REDIS_PASSWORD"] = "secret456"
|
assert_equal \
|
||||||
ENV["DB_PASSWORD"] = "secret&\"123"
|
[ "--env", "REDIS_URL=\"redis://a/b\"", "--env", "WEB_CONCURRENCY=\"4\"", "--env-file", ".kamal/env/roles/app-workers.env" ],
|
||||||
|
config_with_roles.role(:workers).env_args("1.1.1.3").map(&:to_s)
|
||||||
|
|
||||||
expected_secrets_file = <<~ENV
|
assert_equal \
|
||||||
REDIS_PASSWORD=secret456
|
"REDIS_PASSWORD=secret456\nDB_PASSWORD=secret&\"123\n",
|
||||||
DB_PASSWORD=secret&\"123
|
config_with_roles.role(:workers).secrets_io("1.1.1.3").read
|
||||||
ENV
|
end
|
||||||
|
|
||||||
assert_equal expected_secrets_file, Kamal::Configuration.new(@deploy_with_roles).role(:workers).env("1.1.1.3").secrets_io.string
|
|
||||||
assert_equal [ "--env-file", ".kamal/env/roles/app-workers.env", "--env", "REDIS_URL=\"redis://a/b\"", "--env", "WEB_CONCURRENCY=\"4\"" ], @config_with_roles.role(:workers).env_args("1.1.1.3")
|
|
||||||
ensure
|
|
||||||
ENV["REDIS_PASSWORD"] = nil
|
|
||||||
ENV["DB_PASSWORD"] = nil
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "env secrets only in role" do
|
test "env secrets only in role" do
|
||||||
|
with_test_secrets("secrets" => "DB_PASSWORD=secret123") do
|
||||||
@deploy_with_roles[:servers]["workers"]["env"] = {
|
@deploy_with_roles[:servers]["workers"]["env"] = {
|
||||||
"clear" => {
|
"clear" => {
|
||||||
"REDIS_URL" => "redis://a/b",
|
"REDIS_URL" => "redis://a/b",
|
||||||
@@ -136,19 +140,18 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
ENV["DB_PASSWORD"] = "secret123"
|
assert_equal \
|
||||||
|
[ "--env", "REDIS_URL=\"redis://a/b\"", "--env", "WEB_CONCURRENCY=\"4\"", "--env-file", ".kamal/env/roles/app-workers.env" ],
|
||||||
|
config_with_roles.role(:workers).env_args("1.1.1.3").map(&:to_s)
|
||||||
|
|
||||||
expected_secrets_file = <<~ENV
|
assert_equal \
|
||||||
DB_PASSWORD=secret123
|
"DB_PASSWORD=secret123\n",
|
||||||
ENV
|
config_with_roles.role(:workers).secrets_io("1.1.1.3").read
|
||||||
|
end
|
||||||
assert_equal expected_secrets_file, Kamal::Configuration.new(@deploy_with_roles).role(:workers).env("1.1.1.3").secrets_io.string
|
|
||||||
assert_equal [ "--env-file", ".kamal/env/roles/app-workers.env", "--env", "REDIS_URL=\"redis://a/b\"", "--env", "WEB_CONCURRENCY=\"4\"" ], @config_with_roles.role(:workers).env_args("1.1.1.3")
|
|
||||||
ensure
|
|
||||||
ENV["DB_PASSWORD"] = nil
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "env secrets only at top level" do
|
test "env secrets only at top level" do
|
||||||
|
with_test_secrets("secrets" => "REDIS_PASSWORD=secret456") do
|
||||||
@deploy_with_roles[:env] = {
|
@deploy_with_roles[:env] = {
|
||||||
"clear" => {
|
"clear" => {
|
||||||
"REDIS_URL" => "redis://a/b"
|
"REDIS_URL" => "redis://a/b"
|
||||||
@@ -158,19 +161,18 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
ENV["REDIS_PASSWORD"] = "secret456"
|
assert_equal \
|
||||||
|
[ "--env", "REDIS_URL=\"redis://a/b\"", "--env", "WEB_CONCURRENCY=\"4\"", "--env-file", ".kamal/env/roles/app-workers.env" ],
|
||||||
|
config_with_roles.role(:workers).env_args("1.1.1.3").map(&:to_s)
|
||||||
|
|
||||||
expected_secrets_file = <<~ENV
|
assert_equal \
|
||||||
REDIS_PASSWORD=secret456
|
"REDIS_PASSWORD=secret456\n",
|
||||||
ENV
|
config_with_roles.role(:workers).secrets_io("1.1.1.3").read
|
||||||
|
end
|
||||||
assert_equal expected_secrets_file, Kamal::Configuration.new(@deploy_with_roles).role(:workers).env("1.1.1.3").secrets_io.string
|
|
||||||
assert_equal [ "--env-file", ".kamal/env/roles/app-workers.env", "--env", "REDIS_URL=\"redis://a/b\"", "--env", "WEB_CONCURRENCY=\"4\"" ], @config_with_roles.role(:workers).env_args("1.1.1.3")
|
|
||||||
ensure
|
|
||||||
ENV["REDIS_PASSWORD"] = nil
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "env overwritten by role with secrets" do
|
test "env overwritten by role with secrets" do
|
||||||
|
with_test_secrets("secrets" => "REDIS_PASSWORD=secret456") do
|
||||||
@deploy_with_roles[:env] = {
|
@deploy_with_roles[:env] = {
|
||||||
"clear" => {
|
"clear" => {
|
||||||
"REDIS_URL" => "redis://a/b"
|
"REDIS_URL" => "redis://a/b"
|
||||||
@@ -186,51 +188,44 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ENV["REDIS_PASSWORD"] = "secret456"
|
assert_equal \
|
||||||
|
[ "--env", "REDIS_URL=\"redis://c/d\"", "--env-file", ".kamal/env/roles/app-workers.env" ],
|
||||||
|
config_with_roles.role(:workers).env_args("1.1.1.3").map(&:to_s)
|
||||||
|
|
||||||
expected_secrets_file = <<~ENV
|
assert_equal \
|
||||||
REDIS_PASSWORD=secret456
|
"REDIS_PASSWORD=secret456\n",
|
||||||
ENV
|
config_with_roles.role(:workers).secrets_io("1.1.1.3").read
|
||||||
|
|
||||||
config = Kamal::Configuration.new(@deploy_with_roles)
|
|
||||||
assert_equal expected_secrets_file, config.role(:workers).env("1.1.1.3").secrets_io.string
|
|
||||||
assert_equal [ "--env-file", ".kamal/env/roles/app-workers.env", "--env", "REDIS_URL=\"redis://c/d\"" ], config.role(:workers).env_args("1.1.1.3")
|
|
||||||
ensure
|
|
||||||
ENV["REDIS_PASSWORD"] = nil
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "env secrets_file" do
|
|
||||||
assert_equal ".kamal/env/roles/app-workers.env", @config_with_roles.role(:workers).env("1.1.1.3").secrets_file
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "uses cord" do
|
test "uses cord" do
|
||||||
assert @config_with_roles.role(:web).uses_cord?
|
assert config_with_roles.role(:web).uses_cord?
|
||||||
assert_not @config_with_roles.role(:workers).uses_cord?
|
assert_not config_with_roles.role(:workers).uses_cord?
|
||||||
end
|
end
|
||||||
|
|
||||||
test "cord host file" do
|
test "cord host file" do
|
||||||
assert_match %r{.kamal/cords/app-web-[0-9a-f]{32}/cord}, @config_with_roles.role(:web).cord_host_file
|
assert_match %r{.kamal/cords/app-web-[0-9a-f]{32}/cord}, config_with_roles.role(:web).cord_host_file
|
||||||
end
|
end
|
||||||
|
|
||||||
test "cord volume" do
|
test "cord volume" do
|
||||||
assert_equal "/tmp/kamal-cord", @config_with_roles.role(:web).cord_volume.container_path
|
assert_equal "/tmp/kamal-cord", config_with_roles.role(:web).cord_volume.container_path
|
||||||
assert_match %r{.kamal/cords/app-web-[0-9a-f]{32}}, @config_with_roles.role(:web).cord_volume.host_path
|
assert_match %r{.kamal/cords/app-web-[0-9a-f]{32}}, config_with_roles.role(:web).cord_volume.host_path
|
||||||
assert_equal "--volume", @config_with_roles.role(:web).cord_volume.docker_args[0]
|
assert_equal "--volume", config_with_roles.role(:web).cord_volume.docker_args[0]
|
||||||
assert_match %r{\$\(pwd\)/.kamal/cords/app-web-[0-9a-f]{32}:/tmp/kamal-cord}, @config_with_roles.role(:web).cord_volume.docker_args[1]
|
assert_match %r{\$\(pwd\)/.kamal/cords/app-web-[0-9a-f]{32}:/tmp/kamal-cord}, config_with_roles.role(:web).cord_volume.docker_args[1]
|
||||||
end
|
end
|
||||||
|
|
||||||
test "cord container file" do
|
test "cord container file" do
|
||||||
assert_equal "/tmp/kamal-cord/cord", @config_with_roles.role(:web).cord_container_file
|
assert_equal "/tmp/kamal-cord/cord", config_with_roles.role(:web).cord_container_file
|
||||||
end
|
end
|
||||||
|
|
||||||
test "asset path and volume args" do
|
test "asset path and volume args" do
|
||||||
ENV["VERSION"] = "12345"
|
ENV["VERSION"] = "12345"
|
||||||
assert_nil @config_with_roles.role(:web).asset_volume_args
|
assert_nil config_with_roles.role(:web).asset_volume_args
|
||||||
assert_nil @config_with_roles.role(:workers).asset_volume_args
|
assert_nil config_with_roles.role(:workers).asset_volume_args
|
||||||
assert_nil @config_with_roles.role(:web).asset_path
|
assert_nil config_with_roles.role(:web).asset_path
|
||||||
assert_nil @config_with_roles.role(:workers).asset_path
|
assert_nil config_with_roles.role(:workers).asset_path
|
||||||
assert_not @config_with_roles.role(:web).assets?
|
assert_not config_with_roles.role(:web).assets?
|
||||||
assert_not @config_with_roles.role(:workers).assets?
|
assert_not config_with_roles.role(:workers).assets?
|
||||||
|
|
||||||
config_with_assets = Kamal::Configuration.new(@deploy_with_roles.dup.tap { |c|
|
config_with_assets = Kamal::Configuration.new(@deploy_with_roles.dup.tap { |c|
|
||||||
c[:asset_path] = "foo"
|
c[:asset_path] = "foo"
|
||||||
@@ -258,17 +253,26 @@ class ConfigurationRoleTest < ActiveSupport::TestCase
|
|||||||
|
|
||||||
test "asset extracted path" do
|
test "asset extracted path" do
|
||||||
ENV["VERSION"] = "12345"
|
ENV["VERSION"] = "12345"
|
||||||
assert_equal ".kamal/assets/extracted/app-web-12345", @config_with_roles.role(:web).asset_extracted_path
|
assert_equal ".kamal/assets/extracted/app-web-12345", config_with_roles.role(:web).asset_extracted_path
|
||||||
assert_equal ".kamal/assets/extracted/app-workers-12345", @config_with_roles.role(:workers).asset_extracted_path
|
assert_equal ".kamal/assets/extracted/app-workers-12345", config_with_roles.role(:workers).asset_extracted_path
|
||||||
ensure
|
ensure
|
||||||
ENV.delete("VERSION")
|
ENV.delete("VERSION")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "asset volume path" do
|
test "asset volume path" do
|
||||||
ENV["VERSION"] = "12345"
|
ENV["VERSION"] = "12345"
|
||||||
assert_equal ".kamal/assets/volumes/app-web-12345", @config_with_roles.role(:web).asset_volume_path
|
assert_equal ".kamal/assets/volumes/app-web-12345", config_with_roles.role(:web).asset_volume_path
|
||||||
assert_equal ".kamal/assets/volumes/app-workers-12345", @config_with_roles.role(:workers).asset_volume_path
|
assert_equal ".kamal/assets/volumes/app-workers-12345", config_with_roles.role(:workers).asset_volume_path
|
||||||
ensure
|
ensure
|
||||||
ENV.delete("VERSION")
|
ENV.delete("VERSION")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def config
|
||||||
|
Kamal::Configuration.new(@deploy)
|
||||||
|
end
|
||||||
|
|
||||||
|
def config_with_roles
|
||||||
|
Kamal::Configuration.new(@deploy_with_roles)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -294,17 +294,11 @@ class ConfigurationTest < ActiveSupport::TestCase
|
|||||||
test "run directory" do
|
test "run directory" do
|
||||||
config = Kamal::Configuration.new(@deploy)
|
config = Kamal::Configuration.new(@deploy)
|
||||||
assert_equal ".kamal", config.run_directory
|
assert_equal ".kamal", config.run_directory
|
||||||
|
|
||||||
config = Kamal::Configuration.new(@deploy.merge!(run_directory: "/root/kamal"))
|
|
||||||
assert_equal "/root/kamal", config.run_directory
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "run directory as docker volume" do
|
test "run directory as docker volume" do
|
||||||
config = Kamal::Configuration.new(@deploy)
|
config = Kamal::Configuration.new(@deploy)
|
||||||
assert_equal "$(pwd)/.kamal", config.run_directory_as_docker_volume
|
assert_equal "$(pwd)/.kamal", config.run_directory_as_docker_volume
|
||||||
|
|
||||||
config = Kamal::Configuration.new(@deploy.merge!(run_directory: "/root/kamal"))
|
|
||||||
assert_equal "/root/kamal", config.run_directory_as_docker_volume
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "run id" do
|
test "run id" do
|
||||||
|
|||||||
42
test/fixtures/deploy_with_hybrid_builder.yml
vendored
Normal file
42
test/fixtures/deploy_with_hybrid_builder.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
service: app
|
||||||
|
image: dhh/app
|
||||||
|
servers:
|
||||||
|
web:
|
||||||
|
- "1.1.1.1"
|
||||||
|
- "1.1.1.2"
|
||||||
|
workers:
|
||||||
|
- "1.1.1.3"
|
||||||
|
- "1.1.1.4"
|
||||||
|
registry:
|
||||||
|
username: user
|
||||||
|
password: pw
|
||||||
|
|
||||||
|
accessories:
|
||||||
|
mysql:
|
||||||
|
image: mysql:5.7
|
||||||
|
host: 1.1.1.3
|
||||||
|
port: 3306
|
||||||
|
env:
|
||||||
|
clear:
|
||||||
|
MYSQL_ROOT_HOST: '%'
|
||||||
|
secret:
|
||||||
|
- MYSQL_ROOT_PASSWORD
|
||||||
|
files:
|
||||||
|
- test/fixtures/files/my.cnf:/etc/mysql/my.cnf
|
||||||
|
directories:
|
||||||
|
- data:/var/lib/mysql
|
||||||
|
redis:
|
||||||
|
image: redis:latest
|
||||||
|
roles:
|
||||||
|
- web
|
||||||
|
port: 6379
|
||||||
|
directories:
|
||||||
|
- data:/data
|
||||||
|
|
||||||
|
readiness_delay: 0
|
||||||
|
|
||||||
|
builder:
|
||||||
|
arch:
|
||||||
|
- arm64
|
||||||
|
- amd64
|
||||||
|
remote: ssh://app@1.1.1.5
|
||||||
44
test/fixtures/deploy_with_proxy.yml
vendored
Normal file
44
test/fixtures/deploy_with_proxy.yml
vendored
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
service: app
|
||||||
|
image: dhh/app
|
||||||
|
servers:
|
||||||
|
web:
|
||||||
|
- "1.1.1.1"
|
||||||
|
- "1.1.1.2"
|
||||||
|
workers:
|
||||||
|
- "1.1.1.3"
|
||||||
|
- "1.1.1.4"
|
||||||
|
registry:
|
||||||
|
username: user
|
||||||
|
password: pw
|
||||||
|
builder:
|
||||||
|
arch: amd64
|
||||||
|
|
||||||
|
proxy:
|
||||||
|
enabled: true
|
||||||
|
hosts:
|
||||||
|
- "1.1.1.1"
|
||||||
|
deploy_timeout: 6s
|
||||||
|
|
||||||
|
accessories:
|
||||||
|
mysql:
|
||||||
|
image: mysql:5.7
|
||||||
|
host: 1.1.1.3
|
||||||
|
port: 3306
|
||||||
|
env:
|
||||||
|
clear:
|
||||||
|
MYSQL_ROOT_HOST: '%'
|
||||||
|
secret:
|
||||||
|
- MYSQL_ROOT_PASSWORD
|
||||||
|
files:
|
||||||
|
- test/fixtures/files/my.cnf:/etc/mysql/my.cnf
|
||||||
|
directories:
|
||||||
|
- data:/var/lib/mysql
|
||||||
|
redis:
|
||||||
|
image: redis:latest
|
||||||
|
roles:
|
||||||
|
- web
|
||||||
|
port: 6379
|
||||||
|
directories:
|
||||||
|
- data:/data
|
||||||
|
|
||||||
|
readiness_delay: 0
|
||||||
2
test/fixtures/deploy_with_remote_builder.yml
vendored
2
test/fixtures/deploy_with_remote_builder.yml
vendored
@@ -32,8 +32,6 @@ accessories:
|
|||||||
port: 6379
|
port: 6379
|
||||||
directories:
|
directories:
|
||||||
- data:/data
|
- data:/data
|
||||||
builder:
|
|
||||||
arch: amd64
|
|
||||||
|
|
||||||
readiness_delay: 0
|
readiness_delay: 0
|
||||||
|
|
||||||
|
|||||||
@@ -32,8 +32,6 @@ accessories:
|
|||||||
port: 6379
|
port: 6379
|
||||||
directories:
|
directories:
|
||||||
- data:/data
|
- data:/data
|
||||||
builder:
|
|
||||||
arch: amd64
|
|
||||||
|
|
||||||
readiness_delay: 0
|
readiness_delay: 0
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ require_relative "integration_test"
|
|||||||
|
|
||||||
class AccessoryTest < IntegrationTest
|
class AccessoryTest < IntegrationTest
|
||||||
test "boot, stop, start, restart, logs, remove" do
|
test "boot, stop, start, restart, logs, remove" do
|
||||||
kamal :envify
|
|
||||||
|
|
||||||
kamal :accessory, :boot, :busybox
|
kamal :accessory, :boot, :busybox
|
||||||
assert_accessory_running :busybox
|
assert_accessory_running :busybox
|
||||||
|
|
||||||
@@ -21,8 +19,6 @@ class AccessoryTest < IntegrationTest
|
|||||||
|
|
||||||
kamal :accessory, :remove, :busybox, "-y"
|
kamal :accessory, :remove, :busybox, "-y"
|
||||||
assert_accessory_not_running :busybox
|
assert_accessory_not_running :busybox
|
||||||
|
|
||||||
kamal :env, :delete
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ require_relative "integration_test"
|
|||||||
|
|
||||||
class AppTest < IntegrationTest
|
class AppTest < IntegrationTest
|
||||||
test "stop, start, boot, logs, images, containers, exec, remove" do
|
test "stop, start, boot, logs, images, containers, exec, remove" do
|
||||||
kamal :envify
|
|
||||||
|
|
||||||
kamal :deploy
|
kamal :deploy
|
||||||
|
|
||||||
assert_app_is_up
|
assert_app_is_up
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ class BrokenDeployTest < IntegrationTest
|
|||||||
test "deploying a bad image" do
|
test "deploying a bad image" do
|
||||||
@app = "app_with_roles"
|
@app = "app_with_roles"
|
||||||
|
|
||||||
kamal :envify
|
|
||||||
|
|
||||||
first_version = latest_app_version
|
first_version = latest_app_version
|
||||||
|
|
||||||
kamal :deploy
|
kamal :deploy
|
||||||
@@ -29,6 +27,5 @@ class BrokenDeployTest < IntegrationTest
|
|||||||
assert_match /First web container is unhealthy on vm[12], not booting any other roles/, output
|
assert_match /First web container is unhealthy on vm[12], not booting any other roles/, output
|
||||||
assert_match "First web container is unhealthy, not booting workers on vm3", output
|
assert_match "First web container is unhealthy, not booting workers on vm3", output
|
||||||
assert_match "nginx: [emerg] unexpected end of file, expecting \";\" or \"}\" in /etc/nginx/conf.d/default.conf:2", output
|
assert_match "nginx: [emerg] unexpected end of file, expecting \";\" or \"}\" in /etc/nginx/conf.d/default.conf:2", output
|
||||||
assert_match 'ERROR {"Status":"unhealthy","FailingStreak":0,"Log":[]}', output
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ services:
|
|||||||
context: docker/registry
|
context: docker/registry
|
||||||
environment:
|
environment:
|
||||||
- REGISTRY_HTTP_ADDR=0.0.0.0:4443
|
- REGISTRY_HTTP_ADDR=0.0.0.0:4443
|
||||||
|
- REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt
|
||||||
|
- REGISTRY_HTTP_TLS_KEY=/certs/domain.key
|
||||||
volumes:
|
volumes:
|
||||||
- shared:/shared
|
- shared:/shared
|
||||||
- registry:/var/lib/registry/
|
- registry:/var/lib/registry/
|
||||||
|
|||||||
@@ -22,11 +22,12 @@ COPY app_with_roles/ app_with_roles/
|
|||||||
|
|
||||||
RUN rm -rf /root/.ssh
|
RUN rm -rf /root/.ssh
|
||||||
RUN ln -s /shared/ssh /root/.ssh
|
RUN ln -s /shared/ssh /root/.ssh
|
||||||
|
RUN mkdir -p /etc/docker/certs.d/registry:4443 && ln -s /shared/certs/domain.crt /etc/docker/certs.d/registry:4443/ca.crt
|
||||||
|
|
||||||
RUN git config --global user.email "deployer@example.com"
|
RUN git config --global user.email "deployer@example.com"
|
||||||
RUN git config --global user.name "Deployer"
|
RUN git config --global user.name "Deployer"
|
||||||
RUN cd app && git init && echo ".env" >> .gitignore && git add . && git commit -am "Initial version"
|
RUN cd app && git init && git add . && git commit -am "Initial version"
|
||||||
RUN cd app_with_roles && git init && echo ".env" >> .gitignore && git add . && git commit -am "Initial version"
|
RUN cd app_with_roles && git init && git add . && git commit -am "Initial version"
|
||||||
|
|
||||||
HEALTHCHECK --interval=1s CMD pgrep sleep
|
HEALTHCHECK --interval=1s CMD pgrep sleep
|
||||||
|
|
||||||
|
|||||||
4
test/integration/docker/deployer/app/.kamal/secrets
Normal file
4
test/integration/docker/deployer/app/.kamal/secrets
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
SECRETS=$(kamal secrets fetch --adapter test --account test INTERPOLATED_SECRET1 INTERPOLATED_SECRET2 INTERPOLATED_中文)
|
||||||
|
INTERPOLATED_SECRET1=$(kamal secrets extract INTERPOLATED_SECRET1 ${SECRETS})
|
||||||
|
INTERPOLATED_SECRET2=$(kamal secrets extract INTERPOLATED_SECRET2 ${SECRETS})
|
||||||
|
INTERPOLATED_SECRET3=$(kamal secrets extract INTERPOLATED_中文 ${SECRETS})
|
||||||
@@ -6,4 +6,4 @@ ARG COMMIT_SHA
|
|||||||
RUN echo $COMMIT_SHA > /usr/share/nginx/html/version
|
RUN echo $COMMIT_SHA > /usr/share/nginx/html/version
|
||||||
RUN mkdir -p /usr/share/nginx/html/versions && echo "version" > /usr/share/nginx/html/versions/$COMMIT_SHA
|
RUN mkdir -p /usr/share/nginx/html/versions && echo "version" > /usr/share/nginx/html/versions/$COMMIT_SHA
|
||||||
RUN mkdir -p /usr/share/nginx/html/versions && echo "hidden" > /usr/share/nginx/html/versions/.hidden
|
RUN mkdir -p /usr/share/nginx/html/versions && echo "hidden" > /usr/share/nginx/html/versions/.hidden
|
||||||
|
RUN echo "Up!" > /usr/share/nginx/html/up
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ env:
|
|||||||
HOST_TOKEN: "${HOST_TOKEN}"
|
HOST_TOKEN: "${HOST_TOKEN}"
|
||||||
secret:
|
secret:
|
||||||
- SECRET_TOKEN
|
- SECRET_TOKEN
|
||||||
|
- INTERPOLATED_SECRET1
|
||||||
|
- INTERPOLATED_SECRET2
|
||||||
|
- INTERPOLATED_SECRET3
|
||||||
tags:
|
tags:
|
||||||
tag1:
|
tag1:
|
||||||
CLEAR_TAG: tagged
|
CLEAR_TAG: tagged
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ ARG COMMIT_SHA
|
|||||||
RUN echo $COMMIT_SHA > /usr/share/nginx/html/version
|
RUN echo $COMMIT_SHA > /usr/share/nginx/html/version
|
||||||
RUN mkdir -p /usr/share/nginx/html/versions && echo "version" > /usr/share/nginx/html/versions/$COMMIT_SHA
|
RUN mkdir -p /usr/share/nginx/html/versions && echo "version" > /usr/share/nginx/html/versions/$COMMIT_SHA
|
||||||
RUN mkdir -p /usr/share/nginx/html/versions && echo "hidden" > /usr/share/nginx/html/versions/.hidden
|
RUN mkdir -p /usr/share/nginx/html/versions && echo "hidden" > /usr/share/nginx/html/versions/.hidden
|
||||||
|
RUN echo "Up!" > /usr/share/nginx/html/up
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user