Compare commits
151 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1547089da0 | ||
|
|
ae7a4f3411 | ||
|
|
77c202ebaf | ||
|
|
063dfd9edd | ||
|
|
3e4a190173 | ||
|
|
d9c25533e4 | ||
|
|
d5ec0e6ad2 | ||
|
|
725da6aa68 | ||
|
|
84a874e63b | ||
|
|
ba567e0474 | ||
|
|
e464177349 | ||
|
|
68e6f82b30 | ||
|
|
55983c6431 | ||
|
|
b2cf3f33a7 | ||
|
|
16fb3adacb | ||
|
|
407c8b834e | ||
|
|
3468b45014 | ||
|
|
495b3cd95f | ||
|
|
b04e8cd8d7 | ||
|
|
f5391d7fe4 | ||
|
|
2386c903ca | ||
|
|
fbc4515888 | ||
|
|
99829092b3 | ||
|
|
084d1d4a1d | ||
|
|
a1596af815 | ||
|
|
69867e2650 | ||
|
|
eee47d10ee | ||
|
|
8a7843cb35 | ||
|
|
1cc5406b00 | ||
|
|
e31b98539c | ||
|
|
f367ca8ea5 | ||
|
|
14068b32b1 | ||
|
|
f52826b2d6 | ||
|
|
9204624752 | ||
|
|
006fa0de17 | ||
|
|
4d8241ebab | ||
|
|
86657b0172 | ||
|
|
aa2906086a | ||
|
|
f4b7c886fb | ||
|
|
4c778de2d9 | ||
|
|
70d2c71734 | ||
|
|
ac90ee068f | ||
|
|
75b44cd328 | ||
|
|
183fe9e06e | ||
|
|
1da882bb01 | ||
|
|
c662b8d578 | ||
|
|
dbe0c3a7f8 | ||
|
|
b9804a07aa | ||
|
|
f4d98bb67a | ||
|
|
42c3425411 | ||
|
|
57e48a33bb | ||
|
|
4acb78fff6 | ||
|
|
1a86b3ae6e | ||
|
|
a4ab34d8d9 | ||
|
|
24d03fd60e | ||
|
|
83fd2a051d | ||
|
|
72f30774ba | ||
|
|
3fa9cd5a41 | ||
|
|
c970ceebe3 | ||
|
|
8dd864af89 | ||
|
|
e4ab2a0d24 | ||
|
|
3069552315 | ||
|
|
77cd29f5ad | ||
|
|
d0d9dfcba9 | ||
|
|
b4d395cec9 | ||
|
|
e266945413 | ||
|
|
c9fff3cb40 | ||
|
|
cef1e53f84 | ||
|
|
9cf8da64c4 | ||
|
|
e9ba92386c | ||
|
|
685312c9f8 | ||
|
|
ca5e53404b | ||
|
|
2c14f48300 | ||
|
|
cd4e183213 | ||
|
|
7e8a8eb6e5 | ||
|
|
2465681408 | ||
|
|
b917d7cd40 | ||
|
|
1980a79e73 | ||
|
|
347eb69350 | ||
|
|
9a8a45015b | ||
|
|
8d0f4903ae | ||
|
|
57d582e3bc | ||
|
|
bf8779cef4 | ||
|
|
7142534e77 | ||
|
|
0f97e0b056 | ||
|
|
bd8c35b194 | ||
|
|
35075e2e4d | ||
|
|
53dad5f54f | ||
|
|
66f6e8b576 | ||
|
|
a3f5830728 | ||
|
|
a3e5505bb2 | ||
|
|
fdf8ef1343 | ||
|
|
3ee45d7b30 | ||
|
|
6856742eca | ||
|
|
c320343bb2 | ||
|
|
74a06b0ccd | ||
|
|
c0ca5e6dbb | ||
|
|
6f08750c3e | ||
|
|
e362b0106a | ||
|
|
8cec17dd05 | ||
|
|
0f3786781b | ||
|
|
844e3acf50 | ||
|
|
607368121e | ||
|
|
0f16ba1995 | ||
|
|
f3b8a59133 | ||
|
|
b4df51b8b4 | ||
|
|
bf79c7192f | ||
|
|
cb82767d0f | ||
|
|
3c91a83942 | ||
|
|
5cb9fb787b | ||
|
|
493c5690f1 | ||
|
|
5de55a22ff | ||
|
|
a1e40f9fec | ||
|
|
7ddf3bcb02 | ||
|
|
3654a7e1be | ||
|
|
6a7783c979 | ||
|
|
7dc2609b77 | ||
|
|
74960499c0 | ||
|
|
69b13ebc6a | ||
|
|
da2a543cbc | ||
|
|
08dacd2745 | ||
|
|
b6a10df56a | ||
|
|
c917dd82cf | ||
|
|
f04cae529a | ||
|
|
50c96e36c0 | ||
|
|
7b48648bf2 | ||
|
|
91df935d05 | ||
|
|
bbfcbfa94b | ||
|
|
440044b900 | ||
|
|
06419f8749 | ||
|
|
8d6d7ffed0 | ||
|
|
67ce1912f7 | ||
|
|
f45c754e53 | ||
|
|
d40057286d | ||
|
|
0840fdf0dd | ||
|
|
a434b10bfd | ||
|
|
e34031f70c | ||
|
|
23898a5197 | ||
|
|
1e9c9e9103 | ||
|
|
4b2c9cdc72 | ||
|
|
80191588c2 | ||
|
|
5ca806f4d3 | ||
|
|
1d04a6644f | ||
|
|
950624d667 | ||
|
|
6d1d7a4c82 | ||
|
|
ccf32c2c1f | ||
|
|
0ff1450a74 | ||
|
|
256933f6f3 | ||
|
|
92d82dd1a7 | ||
|
|
c17bdba61c | ||
|
|
13328687d1 |
7
.github/workflows/ci.yml
vendored
7
.github/workflows/ci.yml
vendored
@@ -4,6 +4,7 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
pull_request:
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
jobs:
|
jobs:
|
||||||
rubocop:
|
rubocop:
|
||||||
name: RuboCop
|
name: RuboCop
|
||||||
@@ -27,6 +28,7 @@ jobs:
|
|||||||
- "3.1"
|
- "3.1"
|
||||||
- "3.2"
|
- "3.2"
|
||||||
- "3.3"
|
- "3.3"
|
||||||
|
- "3.4.0-preview2"
|
||||||
gemfile:
|
gemfile:
|
||||||
- Gemfile
|
- Gemfile
|
||||||
- gemfiles/rails_edge.gemfile
|
- gemfiles/rails_edge.gemfile
|
||||||
@@ -41,6 +43,9 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Remove gemfile.lock
|
||||||
|
run: rm Gemfile.lock
|
||||||
|
|
||||||
- name: Install Ruby
|
- name: Install Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
@@ -49,3 +54,5 @@ jobs:
|
|||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: bin/test
|
run: bin/test
|
||||||
|
env:
|
||||||
|
RUBYOPT: ${{ startsWith(matrix.ruby-version, '3.4.') && '--enable=frozen-string-literal' || '' }}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
# Use the official Ruby 3.2.0 Alpine image as the base image
|
FROM ruby:3.3-alpine
|
||||||
FROM ruby:3.2.0-alpine
|
|
||||||
|
|
||||||
# Install docker/buildx-bin
|
# Install docker/buildx-bin
|
||||||
COPY --from=docker/buildx-bin /buildx /usr/libexec/docker/cli-plugins/docker-buildx
|
COPY --from=docker/buildx-bin /buildx /usr/libexec/docker/cli-plugins/docker-buildx
|
||||||
|
|||||||
129
Gemfile.lock
129
Gemfile.lock
@@ -1,152 +1,155 @@
|
|||||||
PATH
|
PATH
|
||||||
remote: .
|
remote: .
|
||||||
specs:
|
specs:
|
||||||
kamal (2.1.1)
|
kamal (2.4.0)
|
||||||
activesupport (>= 7.0)
|
activesupport (>= 7.0)
|
||||||
base64 (~> 0.2)
|
base64 (~> 0.2)
|
||||||
bcrypt_pbkdf (~> 1.0)
|
bcrypt_pbkdf (~> 1.0)
|
||||||
concurrent-ruby (~> 1.2)
|
concurrent-ruby (~> 1.2)
|
||||||
dotenv (~> 3.1)
|
dotenv (~> 3.1)
|
||||||
ed25519 (~> 1.2)
|
ed25519 (~> 1.2)
|
||||||
net-ssh (~> 7.0)
|
net-ssh (~> 7.3)
|
||||||
sshkit (>= 1.23.0, < 2.0)
|
sshkit (>= 1.23.0, < 2.0)
|
||||||
thor (~> 1.3)
|
thor (~> 1.3)
|
||||||
zeitwerk (~> 2.5)
|
zeitwerk (>= 2.6.18, < 3.0)
|
||||||
|
|
||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
actionpack (7.1.3.4)
|
actionpack (8.0.0.1)
|
||||||
actionview (= 7.1.3.4)
|
actionview (= 8.0.0.1)
|
||||||
activesupport (= 7.1.3.4)
|
activesupport (= 8.0.0.1)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
racc
|
|
||||||
rack (>= 2.2.4)
|
rack (>= 2.2.4)
|
||||||
rack-session (>= 1.0.1)
|
rack-session (>= 1.0.1)
|
||||||
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.3.4)
|
useragent (~> 0.16)
|
||||||
activesupport (= 7.1.3.4)
|
actionview (8.0.0.1)
|
||||||
|
activesupport (= 8.0.0.1)
|
||||||
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.3.4)
|
activesupport (8.0.0.1)
|
||||||
base64
|
base64
|
||||||
|
benchmark (>= 0.3)
|
||||||
bigdecimal
|
bigdecimal
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.3.1)
|
||||||
connection_pool (>= 2.2.5)
|
connection_pool (>= 2.2.5)
|
||||||
drb
|
drb
|
||||||
i18n (>= 1.6, < 2)
|
i18n (>= 1.6, < 2)
|
||||||
|
logger (>= 1.4.2)
|
||||||
minitest (>= 5.1)
|
minitest (>= 5.1)
|
||||||
mutex_m
|
securerandom (>= 0.3)
|
||||||
tzinfo (~> 2.0)
|
tzinfo (~> 2.0, >= 2.0.5)
|
||||||
|
uri (>= 0.13.1)
|
||||||
ast (2.4.2)
|
ast (2.4.2)
|
||||||
base64 (0.2.0)
|
base64 (0.2.0)
|
||||||
bcrypt_pbkdf (1.1.1)
|
bcrypt_pbkdf (1.1.1)
|
||||||
bcrypt_pbkdf (1.1.1-arm64-darwin)
|
bcrypt_pbkdf (1.1.1-arm64-darwin)
|
||||||
bcrypt_pbkdf (1.1.1-x86_64-darwin)
|
bcrypt_pbkdf (1.1.1-x86_64-darwin)
|
||||||
|
benchmark (0.4.0)
|
||||||
bigdecimal (3.1.8)
|
bigdecimal (3.1.8)
|
||||||
builder (3.3.0)
|
builder (3.3.0)
|
||||||
concurrent-ruby (1.3.3)
|
concurrent-ruby (1.3.4)
|
||||||
connection_pool (2.4.1)
|
connection_pool (2.4.1)
|
||||||
crass (1.0.6)
|
crass (1.0.6)
|
||||||
|
date (3.4.1)
|
||||||
debug (1.9.2)
|
debug (1.9.2)
|
||||||
irb (~> 1.10)
|
irb (~> 1.10)
|
||||||
reline (>= 0.3.8)
|
reline (>= 0.3.8)
|
||||||
dotenv (3.1.2)
|
dotenv (3.1.5)
|
||||||
drb (2.2.1)
|
drb (2.2.1)
|
||||||
ed25519 (1.3.0)
|
ed25519 (1.3.0)
|
||||||
erubi (1.13.0)
|
erubi (1.13.0)
|
||||||
i18n (1.14.5)
|
i18n (1.14.6)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
io-console (0.7.2)
|
io-console (0.8.0)
|
||||||
irb (1.14.0)
|
irb (1.14.2)
|
||||||
rdoc (>= 4.0.0)
|
rdoc (>= 4.0.0)
|
||||||
reline (>= 0.4.2)
|
reline (>= 0.4.2)
|
||||||
json (2.7.2)
|
json (2.9.0)
|
||||||
language_server-protocol (3.17.0.3)
|
language_server-protocol (3.17.0.3)
|
||||||
loofah (2.22.0)
|
logger (1.6.3)
|
||||||
|
loofah (2.23.1)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.12.0)
|
nokogiri (>= 1.12.0)
|
||||||
minitest (5.24.1)
|
minitest (5.25.4)
|
||||||
mocha (2.4.5)
|
mocha (2.7.1)
|
||||||
ruby2_keywords (>= 0.0.5)
|
ruby2_keywords (>= 0.0.5)
|
||||||
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.3)
|
net-ssh (7.3.0)
|
||||||
nokogiri (1.16.7-arm64-darwin)
|
nokogiri (1.17.2-arm64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.16.7-x86_64-darwin)
|
nokogiri (1.17.2-x86_64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.16.7-x86_64-linux)
|
nokogiri (1.17.2-x86_64-linux)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
parallel (1.25.1)
|
ostruct (0.6.1)
|
||||||
parser (3.3.4.0)
|
parallel (1.26.3)
|
||||||
|
parser (3.3.6.0)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
racc
|
racc
|
||||||
psych (5.1.2)
|
psych (5.2.1)
|
||||||
|
date
|
||||||
stringio
|
stringio
|
||||||
racc (1.8.1)
|
racc (1.8.1)
|
||||||
rack (3.1.7)
|
rack (3.1.8)
|
||||||
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)
|
||||||
rack (>= 1.3)
|
rack (>= 1.3)
|
||||||
rackup (2.1.0)
|
rackup (2.2.1)
|
||||||
rack (>= 3)
|
rack (>= 3)
|
||||||
webrick (~> 1.8)
|
|
||||||
rails-dom-testing (2.2.0)
|
rails-dom-testing (2.2.0)
|
||||||
activesupport (>= 5.0.0)
|
activesupport (>= 5.0.0)
|
||||||
minitest
|
minitest
|
||||||
nokogiri (>= 1.6)
|
nokogiri (>= 1.6)
|
||||||
rails-html-sanitizer (1.6.0)
|
rails-html-sanitizer (1.6.2)
|
||||||
loofah (~> 2.21)
|
loofah (~> 2.21)
|
||||||
nokogiri (~> 1.14)
|
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
|
||||||
railties (7.1.3.4)
|
railties (8.0.0.1)
|
||||||
actionpack (= 7.1.3.4)
|
actionpack (= 8.0.0.1)
|
||||||
activesupport (= 7.1.3.4)
|
activesupport (= 8.0.0.1)
|
||||||
irb
|
irb (~> 1.13)
|
||||||
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.2.1)
|
rake (13.2.1)
|
||||||
rdoc (6.7.0)
|
rdoc (6.8.1)
|
||||||
psych (>= 4.0.0)
|
psych (>= 4.0.0)
|
||||||
regexp_parser (2.9.2)
|
regexp_parser (2.9.3)
|
||||||
reline (0.5.9)
|
reline (0.5.12)
|
||||||
io-console (~> 0.5)
|
io-console (~> 0.5)
|
||||||
rexml (3.3.4)
|
rubocop (1.69.2)
|
||||||
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 (>= 2.4, < 3.0)
|
regexp_parser (>= 2.9.3, < 3.0)
|
||||||
rexml (>= 3.2.5, < 4.0)
|
rubocop-ast (>= 1.36.2, < 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, < 4.0)
|
||||||
rubocop-ast (1.32.0)
|
rubocop-ast (1.36.2)
|
||||||
parser (>= 3.3.1.0)
|
parser (>= 3.3.1.0)
|
||||||
rubocop-minitest (0.35.1)
|
rubocop-minitest (0.36.0)
|
||||||
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.21.1)
|
rubocop-performance (1.23.0)
|
||||||
rubocop (>= 1.48.1, < 2.0)
|
rubocop (>= 1.48.1, < 2.0)
|
||||||
rubocop-ast (>= 1.31.1, < 2.0)
|
rubocop-ast (>= 1.31.1, < 2.0)
|
||||||
rubocop-rails (2.25.1)
|
rubocop-rails (2.27.0)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
rack (>= 1.1)
|
rack (>= 1.1)
|
||||||
rubocop (>= 1.33.0, < 2.0)
|
rubocop (>= 1.52.0, < 2.0)
|
||||||
rubocop-ast (>= 1.31.1, < 2.0)
|
rubocop-ast (>= 1.31.1, < 2.0)
|
||||||
rubocop-rails-omakase (1.0.0)
|
rubocop-rails-omakase (1.0.0)
|
||||||
rubocop
|
rubocop
|
||||||
@@ -155,19 +158,23 @@ GEM
|
|||||||
rubocop-rails
|
rubocop-rails
|
||||||
ruby-progressbar (1.13.0)
|
ruby-progressbar (1.13.0)
|
||||||
ruby2_keywords (0.0.5)
|
ruby2_keywords (0.0.5)
|
||||||
sshkit (1.23.0)
|
securerandom (0.4.0)
|
||||||
|
sshkit (1.23.2)
|
||||||
base64
|
base64
|
||||||
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.1)
|
ostruct
|
||||||
strscan (3.1.0)
|
stringio (3.1.2)
|
||||||
thor (1.3.1)
|
thor (1.3.2)
|
||||||
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 (3.1.2)
|
||||||
webrick (1.8.1)
|
unicode-emoji (~> 4.0, >= 4.0.4)
|
||||||
zeitwerk (2.6.17)
|
unicode-emoji (4.0.4)
|
||||||
|
uri (1.0.2)
|
||||||
|
useragent (0.16.11)
|
||||||
|
zeitwerk (2.7.1)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
arm64-darwin
|
arm64-darwin
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ Gem::Specification.new do |spec|
|
|||||||
|
|
||||||
spec.add_dependency "activesupport", ">= 7.0"
|
spec.add_dependency "activesupport", ">= 7.0"
|
||||||
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.3"
|
||||||
spec.add_dependency "thor", "~> 1.3"
|
spec.add_dependency "thor", "~> 1.3"
|
||||||
spec.add_dependency "dotenv", "~> 3.1"
|
spec.add_dependency "dotenv", "~> 3.1"
|
||||||
spec.add_dependency "zeitwerk", "~> 2.5"
|
spec.add_dependency "zeitwerk", ">= 2.6.18", "< 3.0"
|
||||||
spec.add_dependency "ed25519", "~> 1.2"
|
spec.add_dependency "ed25519", "~> 1.2"
|
||||||
spec.add_dependency "bcrypt_pbkdf", "~> 1.0"
|
spec.add_dependency "bcrypt_pbkdf", "~> 1.0"
|
||||||
spec.add_dependency "concurrent-ruby", "~> 1.2"
|
spec.add_dependency "concurrent-ruby", "~> 1.2"
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
require "active_support/core_ext/array/conversions"
|
||||||
|
|
||||||
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, prepare: true)
|
def boot(name, prepare: true)
|
||||||
@@ -16,6 +18,11 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
|
|||||||
execute *accessory.ensure_env_directory
|
execute *accessory.ensure_env_directory
|
||||||
upload! accessory.secrets_io, accessory.secrets_path, mode: "0600"
|
upload! accessory.secrets_io, accessory.secrets_path, mode: "0600"
|
||||||
execute *accessory.run
|
execute *accessory.run
|
||||||
|
|
||||||
|
if accessory.running_proxy?
|
||||||
|
target = capture_with_info(*accessory.container_id_for(container_name: accessory.service_name, only_running: true)).strip
|
||||||
|
execute *accessory.deploy(target: target)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -73,6 +80,10 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
|
|||||||
on(hosts) do
|
on(hosts) do
|
||||||
execute *KAMAL.auditor.record("Started #{name} accessory"), verbosity: :debug
|
execute *KAMAL.auditor.record("Started #{name} accessory"), verbosity: :debug
|
||||||
execute *accessory.start
|
execute *accessory.start
|
||||||
|
if accessory.running_proxy?
|
||||||
|
target = capture_with_info(*accessory.container_id_for(container_name: accessory.service_name, only_running: true)).strip
|
||||||
|
execute *accessory.deploy(target: target)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -85,6 +96,11 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
|
|||||||
on(hosts) do
|
on(hosts) do
|
||||||
execute *KAMAL.auditor.record("Stopped #{name} accessory"), verbosity: :debug
|
execute *KAMAL.auditor.record("Stopped #{name} accessory"), verbosity: :debug
|
||||||
execute *accessory.stop, raise_on_non_zero_exit: false
|
execute *accessory.stop, raise_on_non_zero_exit: false
|
||||||
|
|
||||||
|
if accessory.running_proxy?
|
||||||
|
target = capture_with_info(*accessory.container_id_for(container_name: accessory.service_name, only_running: true)).strip
|
||||||
|
execute *accessory.remove if target
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -110,14 +126,15 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
desc "exec [NAME] [CMD]", "Execute a custom command on servers (use --help to show options)"
|
desc "exec [NAME] [CMD...]", "Execute a custom command on servers within the accessory container (use --help to show options)"
|
||||||
option :interactive, aliases: "-i", type: :boolean, default: false, desc: "Execute command over ssh for an interactive shell (use for console/bash)"
|
option :interactive, aliases: "-i", type: :boolean, default: false, desc: "Execute command over ssh for an interactive shell (use for console/bash)"
|
||||||
option :reuse, type: :boolean, default: false, desc: "Reuse currently running container instead of starting a new one"
|
option :reuse, type: :boolean, default: false, desc: "Reuse currently running container instead of starting a new one"
|
||||||
def exec(name, cmd)
|
def exec(name, *cmd)
|
||||||
|
cmd = Kamal::Utils.join_commands(cmd)
|
||||||
with_accessory(name) do |accessory, hosts|
|
with_accessory(name) do |accessory, hosts|
|
||||||
case
|
case
|
||||||
when options[:interactive] && options[:reuse]
|
when options[:interactive] && options[:reuse]
|
||||||
say "Launching interactive command with via SSH from existing container...", :magenta
|
say "Launching interactive command via SSH from existing container...", :magenta
|
||||||
run_locally { exec accessory.execute_in_existing_container_over_ssh(cmd) }
|
run_locally { exec accessory.execute_in_existing_container_over_ssh(cmd) }
|
||||||
|
|
||||||
when options[:interactive]
|
when options[:interactive]
|
||||||
@@ -126,16 +143,16 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
|
|||||||
|
|
||||||
when options[:reuse]
|
when options[:reuse]
|
||||||
say "Launching command from existing container...", :magenta
|
say "Launching command from existing container...", :magenta
|
||||||
on(hosts) do
|
on(hosts) do |host|
|
||||||
execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on #{name} accessory"), verbosity: :debug
|
execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on #{name} accessory"), verbosity: :debug
|
||||||
capture_with_info(*accessory.execute_in_existing_container(cmd))
|
puts_by_host host, capture_with_info(*accessory.execute_in_existing_container(cmd))
|
||||||
end
|
end
|
||||||
|
|
||||||
else
|
else
|
||||||
say "Launching command from new container...", :magenta
|
say "Launching command from new container...", :magenta
|
||||||
on(hosts) do
|
on(hosts) do |host|
|
||||||
execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on #{name} accessory"), verbosity: :debug
|
execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on #{name} accessory"), verbosity: :debug
|
||||||
capture_with_info(*accessory.execute_in_new_container(cmd))
|
puts_by_host host, capture_with_info(*accessory.execute_in_new_container(cmd))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -145,7 +162,7 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
|
|||||||
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 :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 :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 :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)"
|
||||||
option :grep_options, aliases: "-o", desc: "Additional options supplied to grep"
|
option :grep_options, 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)"
|
||||||
option :skip_timestamps, type: :boolean, aliases: "-T", desc: "Skip appending timestamps to logging output"
|
option :skip_timestamps, type: :boolean, aliases: "-T", desc: "Skip appending timestamps to logging output"
|
||||||
def logs(name)
|
def logs(name)
|
||||||
|
|||||||
@@ -94,9 +94,15 @@ class Kamal::Cli::App < Kamal::Cli::Base
|
|||||||
option :interactive, aliases: "-i", type: :boolean, default: false, desc: "Execute command over ssh for an interactive shell (use for console/bash)"
|
option :interactive, aliases: "-i", type: :boolean, default: false, desc: "Execute command over ssh for an interactive shell (use for console/bash)"
|
||||||
option :reuse, type: :boolean, default: false, desc: "Reuse currently running container instead of starting a new one"
|
option :reuse, type: :boolean, default: false, desc: "Reuse currently running container instead of starting a new one"
|
||||||
option :env, aliases: "-e", type: :hash, desc: "Set environment variables for the command"
|
option :env, aliases: "-e", type: :hash, desc: "Set environment variables for the command"
|
||||||
|
option :detach, type: :boolean, default: false, desc: "Execute command in a detached container"
|
||||||
def exec(*cmd)
|
def exec(*cmd)
|
||||||
|
if (incompatible_options = [ :interactive, :reuse ].select { |key| options[:detach] && options[key] }.presence)
|
||||||
|
raise ArgumentError, "Detach is not compatible with #{incompatible_options.join(" or ")}"
|
||||||
|
end
|
||||||
|
|
||||||
cmd = Kamal::Utils.join_commands(cmd)
|
cmd = Kamal::Utils.join_commands(cmd)
|
||||||
env = options[:env]
|
env = options[:env]
|
||||||
|
detach = options[:detach]
|
||||||
case
|
case
|
||||||
when options[:interactive] && options[:reuse]
|
when options[:interactive] && options[:reuse]
|
||||||
say "Get current version of running container...", :magenta unless options[:version]
|
say "Get current version of running container...", :magenta unless options[:version]
|
||||||
@@ -138,7 +144,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
|
|||||||
|
|
||||||
roles.each do |role|
|
roles.each do |role|
|
||||||
execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on app version #{version}"), verbosity: :debug
|
execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on app version #{version}"), verbosity: :debug
|
||||||
puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).execute_in_new_container(cmd, env: env))
|
puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).execute_in_new_container(cmd, env: env, detach: detach))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -186,15 +192,17 @@ class Kamal::Cli::App < Kamal::Cli::Base
|
|||||||
option :since, aliases: "-s", desc: "Show lines since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)"
|
option :since, aliases: "-s", desc: "Show lines 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 lines to show from each server"
|
option :lines, type: :numeric, aliases: "-n", desc: "Number of lines to show from each server"
|
||||||
option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)"
|
option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)"
|
||||||
option :grep_options, aliases: "-o", desc: "Additional options supplied to grep"
|
option :grep_options, desc: "Additional options supplied to grep"
|
||||||
option :follow, aliases: "-f", desc: "Follow log on primary server (or specific host set by --hosts)"
|
option :follow, aliases: "-f", desc: "Follow log on primary server (or specific host set by --hosts)"
|
||||||
option :skip_timestamps, type: :boolean, aliases: "-T", desc: "Skip appending timestamps to logging output"
|
option :skip_timestamps, type: :boolean, aliases: "-T", desc: "Skip appending timestamps to logging output"
|
||||||
|
option :container_id, desc: "Docker container ID to fetch logs"
|
||||||
def logs
|
def logs
|
||||||
# FIXME: Catch when app containers aren't running
|
# FIXME: Catch when app containers aren't running
|
||||||
|
|
||||||
grep = options[:grep]
|
grep = options[:grep]
|
||||||
grep_options = options[:grep_options]
|
grep_options = options[:grep_options]
|
||||||
since = options[:since]
|
since = options[:since]
|
||||||
|
container_id = options[:container_id]
|
||||||
timestamps = !options[:skip_timestamps]
|
timestamps = !options[:skip_timestamps]
|
||||||
|
|
||||||
if options[:follow]
|
if options[:follow]
|
||||||
@@ -207,8 +215,8 @@ class Kamal::Cli::App < Kamal::Cli::Base
|
|||||||
role = KAMAL.roles_on(KAMAL.primary_host).first
|
role = KAMAL.roles_on(KAMAL.primary_host).first
|
||||||
|
|
||||||
app = KAMAL.app(role: role, host: host)
|
app = KAMAL.app(role: role, host: host)
|
||||||
info app.follow_logs(host: KAMAL.primary_host, timestamps: timestamps, lines: lines, grep: grep, grep_options: grep_options)
|
info app.follow_logs(host: KAMAL.primary_host, container_id: container_id, timestamps: timestamps, lines: lines, grep: grep, grep_options: grep_options)
|
||||||
exec app.follow_logs(host: KAMAL.primary_host, timestamps: timestamps, lines: lines, grep: grep, grep_options: grep_options)
|
exec app.follow_logs(host: KAMAL.primary_host, container_id: container_id, timestamps: timestamps, lines: lines, grep: grep, grep_options: grep_options)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set
|
lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set
|
||||||
@@ -218,7 +226,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
|
|||||||
|
|
||||||
roles.each do |role|
|
roles.each do |role|
|
||||||
begin
|
begin
|
||||||
puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).logs(timestamps: timestamps, since: since, lines: lines, grep: grep, grep_options: grep_options))
|
puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).logs(container_id: container_id, timestamps: timestamps, since: since, lines: lines, grep: grep, grep_options: grep_options))
|
||||||
rescue SSHKit::Command::Failed
|
rescue SSHKit::Command::Failed
|
||||||
puts_by_host host, "Nothing found"
|
puts_by_host host, "Nothing found"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class Kamal::Cli::App::Boot
|
|||||||
|
|
||||||
def start_new_version
|
def start_new_version
|
||||||
audit "Booted app version #{version}"
|
audit "Booted app version #{version}"
|
||||||
hostname = "#{host.to_s[0...51].gsub(/\.+$/, '')}-#{SecureRandom.hex(6)}"
|
hostname = "#{host.to_s[0...51].chomp(".")}-#{SecureRandom.hex(6)}"
|
||||||
|
|
||||||
execute *app.ensure_env_directory
|
execute *app.ensure_env_directory
|
||||||
upload! role.secrets_io(host), role.secrets_path, mode: "0600"
|
upload! role.secrets_io(host), role.secrets_path, mode: "0600"
|
||||||
@@ -91,7 +91,7 @@ class Kamal::Cli::App::Boot
|
|||||||
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
|
begin
|
||||||
error capture_with_info(*app.logs(version: version))
|
error capture_with_info(*app.logs(container_id: app.container_id_for_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
|
rescue SSHKit::Command::Failed
|
||||||
error "Could not fetch logs for #{version}"
|
error "Could not fetch logs for #{version}"
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base
|
|||||||
puts "No documentation found for #{section}"
|
puts "No documentation found for #{section}"
|
||||||
end
|
end
|
||||||
|
|
||||||
desc "init", "Create config stub in config/deploy.yml and env stub in .env"
|
desc "init", "Create config stub in config/deploy.yml and secrets stub in .kamal"
|
||||||
option :bundle, type: :boolean, default: false, desc: "Add Kamal to the Gemfile and create a bin/kamal binstub"
|
option :bundle, type: :boolean, default: false, desc: "Add Kamal to the Gemfile and create a bin/kamal binstub"
|
||||||
def init
|
def init
|
||||||
require "fileutils"
|
require "fileutils"
|
||||||
|
|||||||
@@ -14,23 +14,25 @@ class Kamal::Cli::Proxy < Kamal::Cli::Base
|
|||||||
version = capture_with_info(*KAMAL.proxy.version).strip.presence
|
version = capture_with_info(*KAMAL.proxy.version).strip.presence
|
||||||
|
|
||||||
if version && Kamal::Utils.older_version?(version, Kamal::Configuration::PROXY_MINIMUM_VERSION)
|
if version && Kamal::Utils.older_version?(version, Kamal::Configuration::PROXY_MINIMUM_VERSION)
|
||||||
raise "kamal-proxy version #{version} is too old, please reboot to update to at least #{Kamal::Configuration::PROXY_MINIMUM_VERSION}"
|
raise "kamal-proxy version #{version} is too old, run `kamal proxy reboot` in order to update to at least #{Kamal::Configuration::PROXY_MINIMUM_VERSION}"
|
||||||
end
|
end
|
||||||
execute *KAMAL.proxy.start_or_run
|
execute *KAMAL.proxy.start_or_run
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
desc "boot_config <set|get|clear>", "Mange kamal-proxy boot configuration"
|
desc "boot_config <set|get|reset>", "Manage kamal-proxy boot configuration"
|
||||||
option :publish, type: :boolean, default: true, desc: "Publish the proxy ports on the host"
|
option :publish, type: :boolean, default: true, desc: "Publish the proxy ports on the host"
|
||||||
option :http_port, type: :numeric, default: Kamal::Configuration::PROXY_HTTP_PORT, desc: "HTTP port to publish on the host"
|
option :http_port, type: :numeric, default: Kamal::Configuration::PROXY_HTTP_PORT, desc: "HTTP port to publish on the host"
|
||||||
option :https_port, type: :numeric, default: Kamal::Configuration::PROXY_HTTPS_PORT, desc: "HTTPS port to publish on the host"
|
option :https_port, type: :numeric, default: Kamal::Configuration::PROXY_HTTPS_PORT, desc: "HTTPS port to publish on the host"
|
||||||
|
option :log_max_size, type: :string, default: Kamal::Configuration::PROXY_LOG_MAX_SIZE, desc: "Max size of proxy logs"
|
||||||
option :docker_options, type: :array, default: [], desc: "Docker options to pass to the proxy container", banner: "option=value option2=value2"
|
option :docker_options, type: :array, default: [], desc: "Docker options to pass to the proxy container", banner: "option=value option2=value2"
|
||||||
def boot_config(subcommand)
|
def boot_config(subcommand)
|
||||||
case subcommand
|
case subcommand
|
||||||
when "set"
|
when "set"
|
||||||
boot_options = [
|
boot_options = [
|
||||||
*(KAMAL.config.proxy_publish_args(options[:http_port], options[:https_port]) if options[:publish]),
|
*(KAMAL.config.proxy_publish_args(options[:http_port], options[:https_port]) if options[:publish]),
|
||||||
|
*(KAMAL.config.proxy_logging_args(options[:log_max_size])),
|
||||||
*options[:docker_options].map { |option| "--#{option}" }
|
*options[:docker_options].map { |option| "--#{option}" }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
class Kamal::Cli::Secrets < Kamal::Cli::Base
|
class Kamal::Cli::Secrets < Kamal::Cli::Base
|
||||||
desc "fetch [SECRETS...]", "Fetch secrets from a vault"
|
desc "fetch [SECRETS...]", "Fetch secrets from a vault"
|
||||||
option :adapter, type: :string, aliases: "-a", required: true, desc: "Which vault adapter to use"
|
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 :account, type: :string, required: false, desc: "The account identifier or username"
|
||||||
option :from, type: :string, required: false, desc: "A vault or folder to fetch the secrets from"
|
option :from, type: :string, required: false, desc: "A vault or folder to fetch the secrets from"
|
||||||
option :inline, type: :boolean, required: false, hidden: true
|
option :inline, type: :boolean, required: false, hidden: true
|
||||||
def fetch(*secrets)
|
def fetch(*secrets)
|
||||||
results = adapter(options[:adapter]).fetch(secrets, **options.slice(:account, :from).symbolize_keys)
|
adapter = initialize_adapter(options[:adapter])
|
||||||
|
|
||||||
|
if adapter.requires_account? && options[:account].blank?
|
||||||
|
return puts "No value provided for required options '--account'"
|
||||||
|
end
|
||||||
|
|
||||||
|
results = adapter.fetch(secrets, **options.slice(:account, :from).symbolize_keys)
|
||||||
|
|
||||||
return_or_puts JSON.dump(results).shellescape, inline: options[:inline]
|
return_or_puts JSON.dump(results).shellescape, inline: options[:inline]
|
||||||
end
|
end
|
||||||
@@ -29,7 +35,7 @@ class Kamal::Cli::Secrets < Kamal::Cli::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def adapter(adapter)
|
def initialize_adapter(adapter)
|
||||||
Kamal::Secrets::Adapters.lookup(adapter)
|
Kamal::Secrets::Adapters.lookup(adapter)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -13,12 +13,14 @@ servers:
|
|||||||
# - 192.168.0.1
|
# - 192.168.0.1
|
||||||
# cmd: bin/jobs
|
# cmd: bin/jobs
|
||||||
|
|
||||||
# Enable SSL auto certification via Let's Encrypt (and allow for multiple apps on one server).
|
# Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server.
|
||||||
# Set ssl: false if using something like Cloudflare to terminate SSL (but keep host!).
|
# Remove this section when using multiple web servers and ensure you terminate SSL at your load balancer.
|
||||||
|
#
|
||||||
|
# Note: If using Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption.
|
||||||
proxy:
|
proxy:
|
||||||
ssl: true
|
ssl: true
|
||||||
host: app.example.com
|
host: app.example.com
|
||||||
# kamal-proxy connects to your container over port 80, use `app_port` to specify a different port.
|
# Proxy connects to your container on port 80 by default.
|
||||||
# app_port: 3000
|
# app_port: 3000
|
||||||
|
|
||||||
# Credentials for your image host.
|
# Credentials for your image host.
|
||||||
@@ -34,6 +36,9 @@ registry:
|
|||||||
# Configure builder setup.
|
# Configure builder setup.
|
||||||
builder:
|
builder:
|
||||||
arch: amd64
|
arch: amd64
|
||||||
|
# Pass in additional build args needed for your Dockerfile.
|
||||||
|
# args:
|
||||||
|
# RUBY_VERSION: <%= File.read('.ruby-version').strip %>
|
||||||
|
|
||||||
# Inject ENV variables into containers (secrets come from .kamal/secrets).
|
# Inject ENV variables into containers (secrets come from .kamal/secrets).
|
||||||
#
|
#
|
||||||
@@ -89,7 +94,7 @@ builder:
|
|||||||
# directories:
|
# directories:
|
||||||
# - data:/var/lib/mysql
|
# - data:/var/lib/mysql
|
||||||
# redis:
|
# redis:
|
||||||
# image: redis:7.0
|
# image: valkey/valkey:8
|
||||||
# host: 192.168.0.2
|
# host: 192.168.0.2
|
||||||
# port: 6379
|
# port: 6379
|
||||||
# directories:
|
# directories:
|
||||||
|
|||||||
@@ -1,13 +1,3 @@
|
|||||||
#!/usr/bin/env ruby
|
#!/bin/sh
|
||||||
|
|
||||||
# A sample docker-setup hook
|
echo "Docker set up on $KAMAL_HOSTS..."
|
||||||
#
|
|
||||||
# Sets up a Docker network on defined hosts which can then be used by the application’s containers
|
|
||||||
|
|
||||||
hosts = ENV["KAMAL_HOSTS"].split(",")
|
|
||||||
|
|
||||||
hosts.each do |ip|
|
|
||||||
destination = "root@#{ip}"
|
|
||||||
puts "Creating a Docker network \"kamal\" on #{destination}"
|
|
||||||
`ssh #{destination} docker network create kamal`
|
|
||||||
end
|
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
class Kamal::Commands::Accessory < Kamal::Commands::Base
|
class Kamal::Commands::Accessory < Kamal::Commands::Base
|
||||||
|
include Proxy
|
||||||
|
|
||||||
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,
|
:network_args, :publish_args, :env_args, :volume_args, :label_args, :option_args,
|
||||||
:secrets_io, :secrets_path, :env_directory,
|
:secrets_io, :secrets_path, :env_directory, :proxy, :running_proxy?,
|
||||||
to: :accessory_config
|
to: :accessory_config
|
||||||
|
delegate :proxy_container_name, to: :config
|
||||||
|
|
||||||
|
|
||||||
def initialize(config, name:)
|
def initialize(config, name:)
|
||||||
super(config)
|
super(config)
|
||||||
@@ -15,7 +19,7 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base
|
|||||||
"--name", service_name,
|
"--name", service_name,
|
||||||
"--detach",
|
"--detach",
|
||||||
"--restart", "unless-stopped",
|
"--restart", "unless-stopped",
|
||||||
"--network", "kamal",
|
*network_args,
|
||||||
*config.logging_args,
|
*config.logging_args,
|
||||||
*publish_args,
|
*publish_args,
|
||||||
*env_args,
|
*env_args,
|
||||||
@@ -64,7 +68,7 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base
|
|||||||
docker :run,
|
docker :run,
|
||||||
("-it" if interactive),
|
("-it" if interactive),
|
||||||
"--rm",
|
"--rm",
|
||||||
"--network", "kamal",
|
*network_args,
|
||||||
*env_args,
|
*env_args,
|
||||||
*volume_args,
|
*volume_args,
|
||||||
image,
|
image,
|
||||||
|
|||||||
16
lib/kamal/commands/accessory/proxy.rb
Normal file
16
lib/kamal/commands/accessory/proxy.rb
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
module Kamal::Commands::Accessory::Proxy
|
||||||
|
delegate :proxy_container_name, to: :config
|
||||||
|
|
||||||
|
def deploy(target:)
|
||||||
|
proxy_exec :deploy, service_name, *proxy.deploy_command_args(target: target)
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove
|
||||||
|
proxy_exec :remove, service_name
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def proxy_exec(*command)
|
||||||
|
docker :exec, proxy_container_name, "kamal-proxy", *command
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -47,7 +47,7 @@ class Kamal::Commands::App < Kamal::Commands::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def info
|
def info
|
||||||
docker :ps, *filter_args
|
docker :ps, *container_filter_args
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ class Kamal::Commands::App < Kamal::Commands::Base
|
|||||||
|
|
||||||
def list_versions(*docker_args, statuses: nil)
|
def list_versions(*docker_args, statuses: nil)
|
||||||
pipe \
|
pipe \
|
||||||
docker(:ps, *filter_args(statuses: statuses), *docker_args, "--format", '"{{.Names}}"'),
|
docker(:ps, *container_filter_args(statuses: statuses), *docker_args, "--format", '"{{.Names}}"'),
|
||||||
extract_version_from_name
|
extract_version_from_name
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -91,11 +91,15 @@ class Kamal::Commands::App < Kamal::Commands::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def latest_container(format:, filters: nil)
|
def latest_container(format:, filters: nil)
|
||||||
docker :ps, "--latest", *format, *filter_args(statuses: ACTIVE_DOCKER_STATUSES), argumentize("--filter", filters)
|
docker :ps, "--latest", *format, *container_filter_args(statuses: ACTIVE_DOCKER_STATUSES), argumentize("--filter", filters)
|
||||||
end
|
end
|
||||||
|
|
||||||
def filter_args(statuses: nil)
|
def container_filter_args(statuses: nil)
|
||||||
argumentize "--filter", filters(statuses: statuses)
|
argumentize "--filter", container_filters(statuses: statuses)
|
||||||
|
end
|
||||||
|
|
||||||
|
def image_filter_args
|
||||||
|
argumentize "--filter", image_filters
|
||||||
end
|
end
|
||||||
|
|
||||||
def extract_version_from_name
|
def extract_version_from_name
|
||||||
@@ -103,13 +107,17 @@ class Kamal::Commands::App < Kamal::Commands::Base
|
|||||||
%(while read line; do echo ${line##{role.container_prefix}-}; done)
|
%(while read line; do echo ${line##{role.container_prefix}-}; done)
|
||||||
end
|
end
|
||||||
|
|
||||||
def filters(statuses: nil)
|
def container_filters(statuses: nil)
|
||||||
[ "label=service=#{config.service}" ].tap do |filters|
|
[ "label=service=#{config.service}" ].tap do |filters|
|
||||||
filters << "label=destination=#{config.destination}" if config.destination
|
filters << "label=destination=#{config.destination}"
|
||||||
filters << "label=role=#{role}" if role
|
filters << "label=role=#{role}" if role
|
||||||
statuses&.each do |status|
|
statuses&.each do |status|
|
||||||
filters << "status=#{status}"
|
filters << "status=#{status}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def image_filters
|
||||||
|
[ "label=service=#{config.service}" ]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ module Kamal::Commands::App::Containers
|
|||||||
DOCKER_HEALTH_LOG_FORMAT = "'{{json .State.Health}}'"
|
DOCKER_HEALTH_LOG_FORMAT = "'{{json .State.Health}}'"
|
||||||
|
|
||||||
def list_containers
|
def list_containers
|
||||||
docker :container, :ls, "--all", *filter_args
|
docker :container, :ls, "--all", *container_filter_args
|
||||||
end
|
end
|
||||||
|
|
||||||
def list_container_names
|
def list_container_names
|
||||||
@@ -20,7 +20,7 @@ module Kamal::Commands::App::Containers
|
|||||||
end
|
end
|
||||||
|
|
||||||
def remove_containers
|
def remove_containers
|
||||||
docker :container, :prune, "--force", *filter_args
|
docker :container, :prune, "--force", *container_filter_args
|
||||||
end
|
end
|
||||||
|
|
||||||
def container_health_log(version:)
|
def container_health_log(version:)
|
||||||
|
|||||||
@@ -7,13 +7,15 @@ module Kamal::Commands::App::Execution
|
|||||||
*command
|
*command
|
||||||
end
|
end
|
||||||
|
|
||||||
def execute_in_new_container(*command, interactive: false, env:)
|
def execute_in_new_container(*command, interactive: false, detach: false, env:)
|
||||||
docker :run,
|
docker :run,
|
||||||
("-it" if interactive),
|
("-it" if interactive),
|
||||||
"--rm",
|
("--detach" if detach),
|
||||||
|
("--rm" unless detach),
|
||||||
"--network", "kamal",
|
"--network", "kamal",
|
||||||
*role&.env_args(host),
|
*role&.env_args(host),
|
||||||
*argumentize("--env", env),
|
*argumentize("--env", env),
|
||||||
|
*role.logging_args,
|
||||||
*config.volume_args,
|
*config.volume_args,
|
||||||
*role&.option_args,
|
*role&.option_args,
|
||||||
config.absolute_image,
|
config.absolute_image,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ module Kamal::Commands::App::Images
|
|||||||
end
|
end
|
||||||
|
|
||||||
def remove_images
|
def remove_images
|
||||||
docker :image, :prune, "--all", "--force", *filter_args
|
docker :image, :prune, "--all", "--force", *image_filter_args
|
||||||
end
|
end
|
||||||
|
|
||||||
def tag_latest_image
|
def tag_latest_image
|
||||||
|
|||||||
@@ -1,18 +1,28 @@
|
|||||||
module Kamal::Commands::App::Logging
|
module Kamal::Commands::App::Logging
|
||||||
def logs(version: nil, timestamps: true, since: nil, lines: nil, grep: nil, grep_options: nil)
|
def logs(container_id: nil, timestamps: true, since: nil, lines: nil, grep: nil, grep_options: nil)
|
||||||
pipe \
|
pipe \
|
||||||
version ? container_id_for_version(version) : current_running_container_id,
|
container_id_command(container_id),
|
||||||
"xargs docker logs#{" --timestamps" if timestamps}#{" --since #{since}" if since}#{" --tail #{lines}" if lines} 2>&1",
|
"xargs docker logs#{" --timestamps" if timestamps}#{" --since #{since}" if since}#{" --tail #{lines}" if lines} 2>&1",
|
||||||
("grep '#{grep}'#{" #{grep_options}" if grep_options}" if grep)
|
("grep '#{grep}'#{" #{grep_options}" if grep_options}" if grep)
|
||||||
end
|
end
|
||||||
|
|
||||||
def follow_logs(host:, timestamps: true, lines: nil, grep: nil, grep_options: nil)
|
def follow_logs(host:, container_id: nil, timestamps: true, lines: nil, grep: nil, grep_options: nil)
|
||||||
run_over_ssh \
|
run_over_ssh \
|
||||||
pipe(
|
pipe(
|
||||||
current_running_container_id,
|
container_id_command(container_id),
|
||||||
"xargs docker logs#{" --timestamps" if timestamps}#{" --tail #{lines}" if lines} --follow 2>&1",
|
"xargs docker logs#{" --timestamps" if timestamps}#{" --tail #{lines}" if lines} --follow 2>&1",
|
||||||
(%(grep "#{grep}"#{" #{grep_options}" if grep_options}) if grep)
|
(%(grep "#{grep}"#{" #{grep_options}" if grep_options}) if grep)
|
||||||
),
|
),
|
||||||
host: host
|
host: host
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def container_id_command(container_id)
|
||||||
|
case container_id
|
||||||
|
when Array then container_id
|
||||||
|
when String, Symbol then "echo #{container_id}"
|
||||||
|
else current_running_container_id
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -11,14 +11,7 @@ module Kamal::Commands
|
|||||||
end
|
end
|
||||||
|
|
||||||
def run_over_ssh(*command, host:)
|
def run_over_ssh(*command, host:)
|
||||||
"ssh".tap do |cmd|
|
"ssh#{ssh_proxy_args}#{ssh_keys_args} -t #{config.ssh.user}@#{host} -p #{config.ssh.port} '#{command.join(" ").gsub("'", "'\\\\''")}'"
|
||||||
if config.ssh.proxy && config.ssh.proxy.is_a?(Net::SSH::Proxy::Jump)
|
|
||||||
cmd << " -J #{config.ssh.proxy.jump_proxies}"
|
|
||||||
elsif config.ssh.proxy && config.ssh.proxy.is_a?(Net::SSH::Proxy::Command)
|
|
||||||
cmd << " -o ProxyCommand='#{config.ssh.proxy.command_line_template}'"
|
|
||||||
end
|
|
||||||
cmd << " -t #{config.ssh.user}@#{host} -p #{config.ssh.port} '#{command.join(" ").gsub("'", "'\\\\''")}'"
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def container_id_for(container_name:, only_running: false)
|
def container_id_for(container_name:, only_running: false)
|
||||||
@@ -92,5 +85,24 @@ module Kamal::Commands
|
|||||||
def tags(**details)
|
def tags(**details)
|
||||||
Kamal::Tags.from_config(config, **details)
|
Kamal::Tags.from_config(config, **details)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def ssh_proxy_args
|
||||||
|
case config.ssh.proxy
|
||||||
|
when Net::SSH::Proxy::Jump
|
||||||
|
" -J #{config.ssh.proxy.jump_proxies}"
|
||||||
|
when Net::SSH::Proxy::Command
|
||||||
|
" -o ProxyCommand='#{config.ssh.proxy.command_line_template}'"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def ssh_keys_args
|
||||||
|
"#{ ssh_keys.join("") if ssh_keys}" + "#{" -o IdentitiesOnly=yes" if config.ssh&.keys_only}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def ssh_keys
|
||||||
|
config.ssh.keys&.map do |key|
|
||||||
|
" -i #{key}"
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
|
|||||||
delegate :argumentize, to: Kamal::Utils
|
delegate :argumentize, to: Kamal::Utils
|
||||||
delegate \
|
delegate \
|
||||||
:args, :secrets, :dockerfile, :target, :arches, :local_arches, :remote_arches, :remote,
|
:args, :secrets, :dockerfile, :target, :arches, :local_arches, :remote_arches, :remote,
|
||||||
:cache_from, :cache_to, :ssh, :driver, :docker_driver?,
|
:cache_from, :cache_to, :ssh, :provenance, :sbom, :driver, :docker_driver?,
|
||||||
to: :builder_config
|
to: :builder_config
|
||||||
|
|
||||||
def clean
|
def clean
|
||||||
@@ -37,7 +37,7 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def build_options
|
def build_options
|
||||||
[ *build_tags, *build_cache, *build_labels, *build_args, *build_secrets, *build_dockerfile, *build_target, *build_ssh ]
|
[ *build_tags, *build_cache, *build_labels, *build_args, *build_secrets, *build_dockerfile, *build_target, *build_ssh, *builder_provenance, *builder_sbom ]
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_context
|
def build_context
|
||||||
@@ -97,6 +97,14 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
|
|||||||
argumentize "--ssh", ssh if ssh.present?
|
argumentize "--ssh", ssh if ssh.present?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def builder_provenance
|
||||||
|
argumentize "--provenance", provenance unless provenance.nil?
|
||||||
|
end
|
||||||
|
|
||||||
|
def builder_sbom
|
||||||
|
argumentize "--sbom", sbom unless sbom.nil?
|
||||||
|
end
|
||||||
|
|
||||||
def builder_config
|
def builder_config
|
||||||
config.builder
|
config.builder
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,29 +1,31 @@
|
|||||||
module Kamal::Commands::Builder::Clone
|
module Kamal::Commands::Builder::Clone
|
||||||
extend ActiveSupport::Concern
|
|
||||||
|
|
||||||
included do
|
|
||||||
delegate :clone_directory, :build_directory, to: :"config.builder"
|
|
||||||
end
|
|
||||||
|
|
||||||
def clone
|
def clone
|
||||||
git :clone, Kamal::Git.root, "--recurse-submodules", path: clone_directory
|
git :clone, escaped_root, "--recurse-submodules", path: config.builder.clone_directory.shellescape
|
||||||
end
|
end
|
||||||
|
|
||||||
def clone_reset_steps
|
def clone_reset_steps
|
||||||
[
|
[
|
||||||
git(:remote, "set-url", :origin, Kamal::Git.root, path: build_directory),
|
git(:remote, "set-url", :origin, escaped_root, path: escaped_build_directory),
|
||||||
git(:fetch, :origin, path: build_directory),
|
git(:fetch, :origin, path: escaped_build_directory),
|
||||||
git(:reset, "--hard", Kamal::Git.revision, path: build_directory),
|
git(:reset, "--hard", Kamal::Git.revision, path: escaped_build_directory),
|
||||||
git(:clean, "-fdx", path: build_directory),
|
git(:clean, "-fdx", path: escaped_build_directory),
|
||||||
git(:submodule, :update, "--init", path: build_directory)
|
git(:submodule, :update, "--init", path: escaped_build_directory)
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
def clone_status
|
def clone_status
|
||||||
git :status, "--porcelain", path: build_directory
|
git :status, "--porcelain", path: escaped_build_directory
|
||||||
end
|
end
|
||||||
|
|
||||||
def clone_revision
|
def clone_revision
|
||||||
git :"rev-parse", :HEAD, path: build_directory
|
git :"rev-parse", :HEAD, path: escaped_build_directory
|
||||||
|
end
|
||||||
|
|
||||||
|
def escaped_root
|
||||||
|
Kamal::Git.root.shellescape
|
||||||
|
end
|
||||||
|
|
||||||
|
def escaped_build_directory
|
||||||
|
config.builder.build_directory.shellescape
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -14,9 +14,10 @@ class Kamal::Configuration
|
|||||||
|
|
||||||
include Validation
|
include Validation
|
||||||
|
|
||||||
PROXY_MINIMUM_VERSION = "v0.7.0"
|
PROXY_MINIMUM_VERSION = "v0.8.4"
|
||||||
PROXY_HTTP_PORT = 80
|
PROXY_HTTP_PORT = 80
|
||||||
PROXY_HTTPS_PORT = 443
|
PROXY_HTTPS_PORT = 443
|
||||||
|
PROXY_LOG_MAX_SIZE = "10m"
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
def create_from(config_file:, destination: nil, version: nil)
|
def create_from(config_file:, destination: nil, version: nil)
|
||||||
@@ -36,7 +37,7 @@ class Kamal::Configuration
|
|||||||
if file.exist?
|
if file.exist?
|
||||||
# Newer Psych doesn't load aliases by default
|
# Newer Psych doesn't load aliases by default
|
||||||
load_method = YAML.respond_to?(:unsafe_load) ? :unsafe_load : :load
|
load_method = YAML.respond_to?(:unsafe_load) ? :unsafe_load : :load
|
||||||
YAML.send(load_method, ERB.new(IO.read(file)).result).symbolize_keys
|
YAML.send(load_method, ERB.new(File.read(file)).result).symbolize_keys
|
||||||
else
|
else
|
||||||
raise "Configuration file not found in #{file}"
|
raise "Configuration file not found in #{file}"
|
||||||
end
|
end
|
||||||
@@ -252,8 +253,12 @@ class Kamal::Configuration
|
|||||||
argumentize "--publish", [ "#{http_port}:#{PROXY_HTTP_PORT}", "#{https_port}:#{PROXY_HTTPS_PORT}" ]
|
argumentize "--publish", [ "#{http_port}:#{PROXY_HTTP_PORT}", "#{https_port}:#{PROXY_HTTPS_PORT}" ]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def proxy_logging_args(max_size)
|
||||||
|
argumentize "--log-opt", "max-size=#{max_size}" if max_size.present?
|
||||||
|
end
|
||||||
|
|
||||||
def proxy_options_default
|
def proxy_options_default
|
||||||
proxy_publish_args PROXY_HTTP_PORT, PROXY_HTTPS_PORT
|
[ *proxy_publish_args(PROXY_HTTP_PORT, PROXY_HTTPS_PORT), *proxy_logging_args(PROXY_LOG_MAX_SIZE) ]
|
||||||
end
|
end
|
||||||
|
|
||||||
def proxy_image
|
def proxy_image
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
class Kamal::Configuration::Accessory
|
class Kamal::Configuration::Accessory
|
||||||
include Kamal::Configuration::Validation
|
include Kamal::Configuration::Validation
|
||||||
|
|
||||||
|
DEFAULT_NETWORK = "kamal"
|
||||||
|
|
||||||
delegate :argumentize, :optionize, to: Kamal::Utils
|
delegate :argumentize, :optionize, to: Kamal::Utils
|
||||||
|
|
||||||
attr_reader :name, :accessory_config, :env
|
attr_reader :name, :accessory_config, :env, :proxy
|
||||||
|
|
||||||
def initialize(name, config:)
|
def initialize(name, config:)
|
||||||
@name, @config, @accessory_config = name.inquiry, config, config.raw_config["accessories"][name]
|
@name, @config, @accessory_config = name.inquiry, config, config.raw_config["accessories"][name]
|
||||||
@@ -18,6 +20,8 @@ class Kamal::Configuration::Accessory
|
|||||||
config: accessory_config.fetch("env", {}),
|
config: accessory_config.fetch("env", {}),
|
||||||
secrets: config.secrets,
|
secrets: config.secrets,
|
||||||
context: "accessories/#{name}/env"
|
context: "accessories/#{name}/env"
|
||||||
|
|
||||||
|
initialize_proxy if running_proxy?
|
||||||
end
|
end
|
||||||
|
|
||||||
def service_name
|
def service_name
|
||||||
@@ -38,6 +42,10 @@ class Kamal::Configuration::Accessory
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def network_args
|
||||||
|
argumentize "--network", network
|
||||||
|
end
|
||||||
|
|
||||||
def publish_args
|
def publish_args
|
||||||
argumentize "--publish", port if port
|
argumentize "--publish", port if port
|
||||||
end
|
end
|
||||||
@@ -100,6 +108,17 @@ class Kamal::Configuration::Accessory
|
|||||||
accessory_config["cmd"]
|
accessory_config["cmd"]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def running_proxy?
|
||||||
|
@accessory_config["proxy"].present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize_proxy
|
||||||
|
@proxy = Kamal::Configuration::Proxy.new \
|
||||||
|
config: config,
|
||||||
|
proxy_config: accessory_config["proxy"],
|
||||||
|
context: "accessories/#{name}/proxy"
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
attr_accessor :config
|
attr_accessor :config
|
||||||
|
|
||||||
@@ -123,7 +142,7 @@ class Kamal::Configuration::Accessory
|
|||||||
end
|
end
|
||||||
|
|
||||||
def read_dynamic_file(local_file)
|
def read_dynamic_file(local_file)
|
||||||
StringIO.new(ERB.new(IO.read(local_file)).result)
|
StringIO.new(ERB.new(File.read(local_file)).result)
|
||||||
end
|
end
|
||||||
|
|
||||||
def expand_remote_file(remote_file)
|
def expand_remote_file(remote_file)
|
||||||
@@ -170,7 +189,13 @@ class Kamal::Configuration::Accessory
|
|||||||
|
|
||||||
def hosts_from_roles
|
def hosts_from_roles
|
||||||
if accessory_config.key?("roles")
|
if accessory_config.key?("roles")
|
||||||
accessory_config["roles"].flat_map { |role| config.role(role).hosts }
|
accessory_config["roles"].flat_map do |role|
|
||||||
|
config.role(role)&.hosts || raise(Kamal::ConfigurationError, "Unknown role in accessories config: '#{role}'")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def network
|
||||||
|
accessory_config["network"] || DEFAULT_NETWORK
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|||||||
@@ -111,6 +111,14 @@ class Kamal::Configuration::Builder
|
|||||||
builder_config["ssh"]
|
builder_config["ssh"]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def provenance
|
||||||
|
builder_config["provenance"]
|
||||||
|
end
|
||||||
|
|
||||||
|
def sbom
|
||||||
|
builder_config["sbom"]
|
||||||
|
end
|
||||||
|
|
||||||
def git_clone?
|
def git_clone?
|
||||||
Kamal::Git.used? && builder_config["context"].nil?
|
Kamal::Git.used? && builder_config["context"].nil?
|
||||||
end
|
end
|
||||||
@@ -166,7 +174,7 @@ class Kamal::Configuration::Builder
|
|||||||
end
|
end
|
||||||
|
|
||||||
def cache_to_config_for_registry
|
def cache_to_config_for_registry
|
||||||
[ "type=registry", builder_config["cache"]&.fetch("options", nil), "ref=#{cache_image_ref}" ].compact.join(",")
|
[ "type=registry", "ref=#{cache_image_ref}", builder_config["cache"]&.fetch("options", nil) ].compact.join(",")
|
||||||
end
|
end
|
||||||
|
|
||||||
def repo_basename
|
def repo_basename
|
||||||
|
|||||||
@@ -43,8 +43,8 @@ accessories:
|
|||||||
|
|
||||||
# Port mappings
|
# Port mappings
|
||||||
#
|
#
|
||||||
# See https://docs.docker.com/network/, and especially note the warning about the security
|
# See [https://docs.docker.com/network/](https://docs.docker.com/network/), and
|
||||||
# implications of exposing ports publicly.
|
# especially note the warning about the security implications of exposing ports publicly.
|
||||||
port: "127.0.0.1:3306:3306"
|
port: "127.0.0.1:3306:3306"
|
||||||
|
|
||||||
# Labels
|
# Labels
|
||||||
@@ -90,3 +90,15 @@ accessories:
|
|||||||
# They are not created or copied before mounting:
|
# They are not created or copied before mounting:
|
||||||
volumes:
|
volumes:
|
||||||
- /path/to/mysql-logs:/var/log/mysql
|
- /path/to/mysql-logs:/var/log/mysql
|
||||||
|
|
||||||
|
# Network
|
||||||
|
#
|
||||||
|
# The network the accessory will be attached to.
|
||||||
|
#
|
||||||
|
# Defaults to kamal:
|
||||||
|
network: custom
|
||||||
|
|
||||||
|
# Proxy
|
||||||
|
#
|
||||||
|
proxy:
|
||||||
|
...
|
||||||
|
|||||||
@@ -5,12 +5,12 @@
|
|||||||
# For example, for a Rails app, you might open a console with:
|
# For example, for a Rails app, you might open a console with:
|
||||||
#
|
#
|
||||||
# ```shell
|
# ```shell
|
||||||
# kamal app exec -i -r console "rails console"
|
# kamal app exec -i --reuse "bin/rails console"
|
||||||
# ```
|
# ```
|
||||||
#
|
#
|
||||||
# By defining an alias, like this:
|
# By defining an alias, like this:
|
||||||
aliases:
|
aliases:
|
||||||
console: app exec -r console -i "rails console"
|
console: app exec -i --reuse "bin/rails console"
|
||||||
# You can now open the console with:
|
# You can now open the console with:
|
||||||
#
|
#
|
||||||
# ```shell
|
# ```shell
|
||||||
|
|||||||
@@ -102,3 +102,15 @@ builder:
|
|||||||
#
|
#
|
||||||
# The build driver to use, defaults to `docker-container`:
|
# The build driver to use, defaults to `docker-container`:
|
||||||
driver: docker
|
driver: docker
|
||||||
|
|
||||||
|
# Provenance
|
||||||
|
#
|
||||||
|
# It is used to configure provenance attestations for the build result.
|
||||||
|
# The value can also be a boolean to enable or disable provenance attestations.
|
||||||
|
provenance: mode=max
|
||||||
|
|
||||||
|
# SBOM (Software Bill of Materials)
|
||||||
|
#
|
||||||
|
# It is used to configure SBOM generation for the build result.
|
||||||
|
# The value can also be a boolean to enable or disable SBOM generation.
|
||||||
|
sbom: true
|
||||||
|
|||||||
@@ -46,9 +46,22 @@ proxy:
|
|||||||
# The host value must point to the server we are deploying to, and port 443 must be
|
# The host value must point to the server we are deploying to, and port 443 must be
|
||||||
# open for the Let's Encrypt challenge to succeed.
|
# open for the Let's Encrypt challenge to succeed.
|
||||||
#
|
#
|
||||||
|
# If you set `ssl` to `true`, `kamal-proxy` will stop forwarding headers to your app,
|
||||||
|
# unless you explicitly set `forward_headers: true`
|
||||||
|
#
|
||||||
# Defaults to `false`:
|
# Defaults to `false`:
|
||||||
ssl: true
|
ssl: true
|
||||||
|
|
||||||
|
# Forward headers
|
||||||
|
#
|
||||||
|
# Whether to forward the `X-Forwarded-For` and `X-Forwarded-Proto` headers.
|
||||||
|
#
|
||||||
|
# If you are behind a trusted proxy, you can set this to `true` to forward the headers.
|
||||||
|
#
|
||||||
|
# By default, kamal-proxy will not forward the headers if the `ssl` option is set to `true`, and
|
||||||
|
# will forward them if it is set to `false`.
|
||||||
|
forward_headers: true
|
||||||
|
|
||||||
# Response timeout
|
# Response timeout
|
||||||
#
|
#
|
||||||
# How long to wait for requests to complete before timing out, defaults to 30 seconds:
|
# How long to wait for requests to complete before timing out, defaults to 30 seconds:
|
||||||
@@ -93,13 +106,3 @@ proxy:
|
|||||||
response_headers:
|
response_headers:
|
||||||
- X-Request-ID
|
- X-Request-ID
|
||||||
- X-Request-Start
|
- X-Request-Start
|
||||||
|
|
||||||
# Forward headers
|
|
||||||
#
|
|
||||||
# Whether to forward the `X-Forwarded-For` and `X-Forwarded-Proto` headers.
|
|
||||||
#
|
|
||||||
# If you are behind a trusted proxy, you can set this to `true` to forward the headers.
|
|
||||||
#
|
|
||||||
# By default, kamal-proxy will not forward the headers if the `ssl` option is set to `true`, and
|
|
||||||
# will forward them if it is set to `false`.
|
|
||||||
forward_headers: true
|
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
#
|
#
|
||||||
# The default registry is Docker Hub, but you can change it using `registry/server`.
|
# The default registry is Docker Hub, but you can change it using `registry/server`.
|
||||||
#
|
#
|
||||||
|
# By default, Docker Hub creates public repositories. To avoid making your images public,
|
||||||
|
# set up a private repository before deploying, or change the default repository privacy
|
||||||
|
# settings to private in your [Docker Hub settings](https://hub.docker.com/repository-settings/default-privacy).
|
||||||
|
#
|
||||||
# A reference to a secret (in this case, `DOCKER_REGISTRY_TOKEN`) will look up the secret
|
# A reference to a secret (in this case, `DOCKER_REGISTRY_TOKEN`) will look up the secret
|
||||||
# in the local environment:
|
# in the local environment:
|
||||||
registry:
|
registry:
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class Kamal::Configuration::Proxy
|
|||||||
def deploy_options
|
def deploy_options
|
||||||
{
|
{
|
||||||
host: hosts,
|
host: hosts,
|
||||||
tls: proxy_config["ssl"],
|
tls: proxy_config["ssl"].presence,
|
||||||
"deploy-timeout": seconds_duration(config.deploy_timeout),
|
"deploy-timeout": seconds_duration(config.deploy_timeout),
|
||||||
"drain-timeout": seconds_duration(config.drain_timeout),
|
"drain-timeout": seconds_duration(config.drain_timeout),
|
||||||
"health-check-interval": seconds_duration(proxy_config.dig("healthcheck", "interval")),
|
"health-check-interval": seconds_duration(proxy_config.dig("healthcheck", "interval")),
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ class Kamal::EnvFile
|
|||||||
def escape_docker_env_file_ascii_value(value)
|
def escape_docker_env_file_ascii_value(value)
|
||||||
# Doublequotes are treated literally in docker env files
|
# Doublequotes are treated literally in docker env files
|
||||||
# so remove leading and trailing ones and unescape any others
|
# so remove leading and trailing ones and unescape any others
|
||||||
value.to_s.dump[1..-2].gsub(/\\"/, "\"")
|
value.to_s.dump[1..-2]
|
||||||
|
.gsub(/\\"/, "\"")
|
||||||
|
.gsub(/\\#/, "#")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
require "dotenv"
|
require "dotenv"
|
||||||
|
|
||||||
class Kamal::Secrets
|
class Kamal::Secrets
|
||||||
attr_reader :secrets_files
|
|
||||||
|
|
||||||
Kamal::Secrets::Dotenv::InlineCommandSubstitution.install!
|
Kamal::Secrets::Dotenv::InlineCommandSubstitution.install!
|
||||||
|
|
||||||
def initialize(destination: nil)
|
def initialize(destination: nil)
|
||||||
@secrets_files = \
|
@destination = destination
|
||||||
[ ".kamal/secrets-common", ".kamal/secrets#{(".#{destination}" if destination)}" ].select { |f| File.exist?(f) }
|
|
||||||
@mutex = Mutex.new
|
@mutex = Mutex.new
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -17,10 +14,10 @@ class Kamal::Secrets
|
|||||||
secrets.fetch(key)
|
secrets.fetch(key)
|
||||||
end
|
end
|
||||||
rescue KeyError
|
rescue KeyError
|
||||||
if secrets_files
|
if secrets_files.present?
|
||||||
raise Kamal::ConfigurationError, "Secret '#{key}' not found in #{secrets_files.join(", ")}"
|
raise Kamal::ConfigurationError, "Secret '#{key}' not found in #{secrets_files.join(", ")}"
|
||||||
else
|
else
|
||||||
raise Kamal::ConfigurationError, "Secret '#{key}' not found, no secret files provided"
|
raise Kamal::ConfigurationError, "Secret '#{key}' not found, no secret files (#{secrets_filenames.join(", ")}) provided"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -28,10 +25,18 @@ class Kamal::Secrets
|
|||||||
secrets
|
secrets
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def secrets_files
|
||||||
|
@secrets_files ||= secrets_filenames.select { |f| File.exist?(f) }
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def secrets
|
def secrets
|
||||||
@secrets ||= secrets_files.inject({}) do |secrets, secrets_file|
|
@secrets ||= secrets_files.inject({}) do |secrets, secrets_file|
|
||||||
secrets.merge!(::Dotenv.parse(secrets_file))
|
secrets.merge!(::Dotenv.parse(secrets_file, overwrite: true))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def secrets_filenames
|
||||||
|
[ ".kamal/secrets-common", ".kamal/secrets#{(".#{@destination}" if @destination)}" ]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
42
lib/kamal/secrets/adapters/aws_secrets_manager.rb
Normal file
42
lib/kamal/secrets/adapters/aws_secrets_manager.rb
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
class Kamal::Secrets::Adapters::AwsSecretsManager < Kamal::Secrets::Adapters::Base
|
||||||
|
private
|
||||||
|
def login(_account)
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_secrets(secrets, account:, session:)
|
||||||
|
{}.tap do |results|
|
||||||
|
get_from_secrets_manager(secrets, account: account).each do |secret|
|
||||||
|
secret_name = secret["Name"]
|
||||||
|
secret_string = JSON.parse(secret["SecretString"])
|
||||||
|
|
||||||
|
secret_string.each do |key, value|
|
||||||
|
results["#{secret_name}/#{key}"] = value
|
||||||
|
end
|
||||||
|
rescue JSON::ParserError
|
||||||
|
results["#{secret_name}"] = secret["SecretString"]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_from_secrets_manager(secrets, account:)
|
||||||
|
`aws secretsmanager batch-get-secret-value --secret-id-list #{secrets.map(&:shellescape).join(" ")} --profile #{account.shellescape}`.tap do |secrets|
|
||||||
|
raise RuntimeError, "Could not read #{secrets} from AWS Secrets Manager" unless $?.success?
|
||||||
|
|
||||||
|
secrets = JSON.parse(secrets)
|
||||||
|
|
||||||
|
return secrets["SecretValues"] unless secrets["Errors"].present?
|
||||||
|
|
||||||
|
raise RuntimeError, secrets["Errors"].map { |error| "#{error['SecretId']}: #{error['Message']}" }.join(" ")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_dependencies!
|
||||||
|
raise RuntimeError, "AWS CLI is not installed" unless cli_installed?
|
||||||
|
end
|
||||||
|
|
||||||
|
def cli_installed?
|
||||||
|
`aws --version 2> /dev/null`
|
||||||
|
$?.success?
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,12 +1,20 @@
|
|||||||
class Kamal::Secrets::Adapters::Base
|
class Kamal::Secrets::Adapters::Base
|
||||||
delegate :optionize, to: Kamal::Utils
|
delegate :optionize, to: Kamal::Utils
|
||||||
|
|
||||||
def fetch(secrets, account:, from: nil)
|
def fetch(secrets, account: nil, from: nil)
|
||||||
|
raise RuntimeError, "Missing required option '--account'" if requires_account? && account.blank?
|
||||||
|
|
||||||
|
check_dependencies!
|
||||||
|
|
||||||
session = login(account)
|
session = login(account)
|
||||||
full_secrets = secrets.map { |secret| [ from, secret ].compact.join("/") }
|
full_secrets = secrets.map { |secret| [ from, secret ].compact.join("/") }
|
||||||
fetch_secrets(full_secrets, account: account, session: session)
|
fetch_secrets(full_secrets, account: account, session: session)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def requires_account?
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def login(...)
|
def login(...)
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
@@ -15,4 +23,8 @@ class Kamal::Secrets::Adapters::Base
|
|||||||
def fetch_secrets(...)
|
def fetch_secrets(...)
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def check_dependencies!
|
||||||
|
raise NotImplementedError
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -25,18 +25,15 @@ class Kamal::Secrets::Adapters::Bitwarden < Kamal::Secrets::Adapters::Base
|
|||||||
{}.tap do |results|
|
{}.tap do |results|
|
||||||
items_fields(secrets).each do |item, fields|
|
items_fields(secrets).each do |item, fields|
|
||||||
item_json = run_command("get item #{item.shellescape}", session: session, raw: true)
|
item_json = run_command("get item #{item.shellescape}", session: session, raw: true)
|
||||||
raise RuntimeError, "Could not read #{secret} from Bitwarden" unless $?.success?
|
raise RuntimeError, "Could not read #{item} from Bitwarden" unless $?.success?
|
||||||
item_json = JSON.parse(item_json)
|
item_json = JSON.parse(item_json)
|
||||||
|
|
||||||
if fields.any?
|
if fields.any?
|
||||||
fields.each do |field|
|
results.merge! fetch_secrets_from_fields(fields, item, item_json)
|
||||||
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
|
|
||||||
elsif item_json.dig("login", "password")
|
elsif item_json.dig("login", "password")
|
||||||
results[item] = item_json.dig("login", "password")
|
results[item] = item_json.dig("login", "password")
|
||||||
|
elsif item_json["fields"]&.any?
|
||||||
|
fields = item_json["fields"].pluck("name")
|
||||||
|
results.merge! fetch_secrets_from_fields(fields, item, item_json)
|
||||||
else
|
else
|
||||||
raise RuntimeError, "Item #{item} is not a login type item and no fields were specified"
|
raise RuntimeError, "Item #{item} is not a login type item and no fields were specified"
|
||||||
end
|
end
|
||||||
@@ -44,6 +41,15 @@ class Kamal::Secrets::Adapters::Bitwarden < Kamal::Secrets::Adapters::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def fetch_secrets_from_fields(fields, item, item_json)
|
||||||
|
fields.to_h do |field|
|
||||||
|
item_field = item_json["fields"].find { |f| f["name"] == field }
|
||||||
|
raise RuntimeError, "Could not find field #{field} in item #{item} in Bitwarden" unless item_field
|
||||||
|
value = item_field["value"]
|
||||||
|
[ "#{item}/#{field}", value ]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def items_fields(secrets)
|
def items_fields(secrets)
|
||||||
{}.tap do |items|
|
{}.tap do |items|
|
||||||
secrets.each do |secret|
|
secrets.each do |secret|
|
||||||
@@ -63,4 +69,13 @@ class Kamal::Secrets::Adapters::Bitwarden < Kamal::Secrets::Adapters::Base
|
|||||||
result = `#{full_command}`.strip
|
result = `#{full_command}`.strip
|
||||||
raw ? result : JSON.parse(result)
|
raw ? result : JSON.parse(result)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def check_dependencies!
|
||||||
|
raise RuntimeError, "Bitwarden CLI is not installed" unless cli_installed?
|
||||||
|
end
|
||||||
|
|
||||||
|
def cli_installed?
|
||||||
|
`bw --version 2> /dev/null`
|
||||||
|
$?.success?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
53
lib/kamal/secrets/adapters/doppler.rb
Normal file
53
lib/kamal/secrets/adapters/doppler.rb
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
class Kamal::Secrets::Adapters::Doppler < Kamal::Secrets::Adapters::Base
|
||||||
|
def requires_account?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def login(*)
|
||||||
|
unless loggedin?
|
||||||
|
`doppler login -y`
|
||||||
|
raise RuntimeError, "Failed to login to Doppler" unless $?.success?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def loggedin?
|
||||||
|
`doppler me --json 2> /dev/null`
|
||||||
|
$?.success?
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_secrets(secrets, **)
|
||||||
|
project_and_config_flags = ""
|
||||||
|
unless service_token_set?
|
||||||
|
project, config, _ = secrets.first.split("/")
|
||||||
|
|
||||||
|
unless project && config
|
||||||
|
raise RuntimeError, "Missing project or config from '--from=project/config' option"
|
||||||
|
end
|
||||||
|
|
||||||
|
project_and_config_flags = "-p #{project.shellescape} -c #{config.shellescape}"
|
||||||
|
end
|
||||||
|
|
||||||
|
secret_names = secrets.collect { |s| s.split("/").last }
|
||||||
|
|
||||||
|
items = `doppler secrets get #{secret_names.map(&:shellescape).join(" ")} --json #{project_and_config_flags}`
|
||||||
|
raise RuntimeError, "Could not read #{secrets} from Doppler" unless $?.success?
|
||||||
|
|
||||||
|
items = JSON.parse(items)
|
||||||
|
|
||||||
|
items.transform_values { |value| value["computed"] }
|
||||||
|
end
|
||||||
|
|
||||||
|
def service_token_set?
|
||||||
|
ENV["DOPPLER_TOKEN"] && ENV["DOPPLER_TOKEN"][0, 5] == "dp.st"
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_dependencies!
|
||||||
|
raise RuntimeError, "Doppler CLI is not installed" unless cli_installed?
|
||||||
|
end
|
||||||
|
|
||||||
|
def cli_installed?
|
||||||
|
`doppler --version 2> /dev/null`
|
||||||
|
$?.success?
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -27,4 +27,13 @@ class Kamal::Secrets::Adapters::LastPass < Kamal::Secrets::Adapters::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def check_dependencies!
|
||||||
|
raise RuntimeError, "LastPass CLI is not installed" unless cli_installed?
|
||||||
|
end
|
||||||
|
|
||||||
|
def cli_installed?
|
||||||
|
`lpass --version 2> /dev/null`
|
||||||
|
$?.success?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -58,4 +58,13 @@ class Kamal::Secrets::Adapters::OnePassword < Kamal::Secrets::Adapters::Base
|
|||||||
raise RuntimeError, "Could not read #{fields.join(", ")} from #{item} in the #{vault} 1Password vault" unless $?.success?
|
raise RuntimeError, "Could not read #{fields.join(", ")} from #{item} in the #{vault} 1Password vault" unless $?.success?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def check_dependencies!
|
||||||
|
raise RuntimeError, "1Password CLI is not installed" unless cli_installed?
|
||||||
|
end
|
||||||
|
|
||||||
|
def cli_installed?
|
||||||
|
`op --version 2> /dev/null`
|
||||||
|
$?.success?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -7,4 +7,8 @@ class Kamal::Secrets::Adapters::Test < Kamal::Secrets::Adapters::Base
|
|||||||
def fetch_secrets(secrets, account:, session:)
|
def fetch_secrets(secrets, account:, session:)
|
||||||
secrets.to_h { |secret| [ secret, secret.reverse ] }
|
secrets.to_h { |secret| [ secret, secret.reverse ] }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def check_dependencies!
|
||||||
|
# no op
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
5
lib/kamal/secrets/adapters/test_optional_account.rb
Normal file
5
lib/kamal/secrets/adapters/test_optional_account.rb
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
class Kamal::Secrets::Adapters::TestOptionalAccount < Kamal::Secrets::Adapters::Test
|
||||||
|
def requires_account?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -12,6 +12,8 @@ module Kamal::Utils
|
|||||||
attr = "#{key}=#{escape_shell_value(value)}"
|
attr = "#{key}=#{escape_shell_value(value)}"
|
||||||
attr = self.sensitive(attr, redaction: "#{key}=[REDACTED]") if sensitive
|
attr = self.sensitive(attr, redaction: "#{key}=[REDACTED]") if sensitive
|
||||||
[ argument, attr ]
|
[ argument, attr ]
|
||||||
|
elsif value == false
|
||||||
|
[ argument, "#{key}=false" ]
|
||||||
else
|
else
|
||||||
[ argument, key ]
|
[ argument, key ]
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
module Kamal
|
module Kamal
|
||||||
VERSION = "2.1.1"
|
VERSION = "2.4.0"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class CliAppTest < CliTestCase
|
|||||||
.returns("12345678") # running version
|
.returns("12345678") # running version
|
||||||
|
|
||||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
.with(:sh, "-c", "'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'", "|", :head, "-1", "|", "while read line; do echo ${line#app-web-}; done", raise_on_non_zero_exit: false)
|
.with(:sh, "-c", "'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting'", "|", :head, "-1", "|", "while read line; do echo ${line#app-web-}; done", raise_on_non_zero_exit: false)
|
||||||
.returns("123") # old version
|
.returns("123") # old version
|
||||||
|
|
||||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
@@ -63,7 +63,7 @@ class CliAppTest < CliTestCase
|
|||||||
.returns("12345678") # running version
|
.returns("12345678") # running version
|
||||||
|
|
||||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
.with(:sh, "-c", "'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'", "|", :head, "-1", "|", "while read line; do echo ${line#app-web-}; done", raise_on_non_zero_exit: false)
|
.with(:sh, "-c", "'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting'", "|", :head, "-1", "|", "while read line; do echo ${line#app-web-}; done", raise_on_non_zero_exit: false)
|
||||||
.returns("123").twice # old version
|
.returns("123").twice # old version
|
||||||
|
|
||||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
@@ -92,7 +92,7 @@ class CliAppTest < CliTestCase
|
|||||||
.returns("12345678") # running version
|
.returns("12345678") # running version
|
||||||
|
|
||||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
.with(:sh, "-c", "'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'", "|", :head, "-1", "|", "while read line; do echo ${line#app-web-}; done", raise_on_non_zero_exit: false)
|
.with(:sh, "-c", "'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting'", "|", :head, "-1", "|", "while read line; do echo ${line#app-web-}; done", raise_on_non_zero_exit: false)
|
||||||
.returns("123") # old version
|
.returns("123") # old version
|
||||||
|
|
||||||
run_command("boot", config: :with_env_tags).tap do |output|
|
run_command("boot", config: :with_env_tags).tap do |output|
|
||||||
@@ -196,17 +196,17 @@ class CliAppTest < CliTestCase
|
|||||||
|
|
||||||
test "stop" do
|
test "stop" do
|
||||||
run_command("stop").tap do |output|
|
run_command("stop").tap do |output|
|
||||||
assert_match "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker stop", output
|
assert_match "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker stop", output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "stale_containers" do
|
test "stale_containers" do
|
||||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
.with(:docker, :ps, "--filter", "label=service=app", "--filter", "label=role=web", "--format", "\"{{.Names}}\"", "|", "while read line; do echo ${line#app-web-}; done", raise_on_non_zero_exit: false)
|
.with(:docker, :ps, "--filter", "label=service=app", "--filter", "label=destination=", "--filter", "label=role=web", "--format", "\"{{.Names}}\"", "|", "while read line; do echo ${line#app-web-}; done", raise_on_non_zero_exit: false)
|
||||||
.returns("12345678\n87654321\n")
|
.returns("12345678\n87654321\n")
|
||||||
|
|
||||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
.with(:sh, "-c", "'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'", "|", :head, "-1", "|", "while read line; do echo ${line#app-web-}; done", raise_on_non_zero_exit: false)
|
.with(:sh, "-c", "'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting'", "|", :head, "-1", "|", "while read line; do echo ${line#app-web-}; done", raise_on_non_zero_exit: false)
|
||||||
.returns("12345678\n")
|
.returns("12345678\n")
|
||||||
|
|
||||||
run_command("stale_containers").tap do |output|
|
run_command("stale_containers").tap do |output|
|
||||||
@@ -216,11 +216,11 @@ class CliAppTest < CliTestCase
|
|||||||
|
|
||||||
test "stop stale_containers" do
|
test "stop stale_containers" do
|
||||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
.with(:docker, :ps, "--filter", "label=service=app", "--filter", "label=role=web", "--format", "\"{{.Names}}\"", "|", "while read line; do echo ${line#app-web-}; done", raise_on_non_zero_exit: false)
|
.with(:docker, :ps, "--filter", "label=service=app", "--filter", "label=destination=", "--filter", "label=role=web", "--format", "\"{{.Names}}\"", "|", "while read line; do echo ${line#app-web-}; done", raise_on_non_zero_exit: false)
|
||||||
.returns("12345678\n87654321\n")
|
.returns("12345678\n87654321\n")
|
||||||
|
|
||||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
.with(:sh, "-c", "'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'", "|", :head, "-1", "|", "while read line; do echo ${line#app-web-}; done", raise_on_non_zero_exit: false)
|
.with(:sh, "-c", "'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting'", "|", :head, "-1", "|", "while read line; do echo ${line#app-web-}; done", raise_on_non_zero_exit: false)
|
||||||
.returns("12345678\n")
|
.returns("12345678\n")
|
||||||
|
|
||||||
run_command("stale_containers", "--stop").tap do |output|
|
run_command("stale_containers", "--stop").tap do |output|
|
||||||
@@ -231,13 +231,13 @@ class CliAppTest < CliTestCase
|
|||||||
|
|
||||||
test "details" do
|
test "details" do
|
||||||
run_command("details").tap do |output|
|
run_command("details").tap do |output|
|
||||||
assert_match "docker ps --filter label=service=app --filter label=role=web", output
|
assert_match "docker ps --filter label=service=app --filter label=destination= --filter label=role=web", output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "remove" do
|
test "remove" do
|
||||||
run_command("remove").tap do |output|
|
run_command("remove").tap do |output|
|
||||||
assert_match /#{Regexp.escape("sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker stop")}/, output
|
assert_match /#{Regexp.escape("sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker stop")}/, output
|
||||||
assert_match /#{Regexp.escape("docker container prune --force --filter label=service=app")}/, output
|
assert_match /#{Regexp.escape("docker container prune --force --filter label=service=app")}/, output
|
||||||
assert_match /#{Regexp.escape("docker image prune --all --force --filter label=service=app")}/, output
|
assert_match /#{Regexp.escape("docker image prune --all --force --filter label=service=app")}/, output
|
||||||
end
|
end
|
||||||
@@ -263,26 +263,50 @@ class CliAppTest < CliTestCase
|
|||||||
|
|
||||||
test "exec" do
|
test "exec" do
|
||||||
run_command("exec", "ruby -v").tap do |output|
|
run_command("exec", "ruby -v").tap do |output|
|
||||||
assert_match "docker run --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env dhh/app:latest ruby -v", output
|
assert_match "docker run --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" dhh/app:latest ruby -v", output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "exec separate arguments" do
|
test "exec separate arguments" do
|
||||||
run_command("exec", "ruby", " -v").tap do |output|
|
run_command("exec", "ruby", " -v").tap do |output|
|
||||||
assert_match "docker run --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env dhh/app:latest ruby -v", output
|
assert_match "docker run --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" dhh/app:latest ruby -v", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "exec detach" do
|
||||||
|
run_command("exec", "--detach", "ruby -v").tap do |output|
|
||||||
|
assert_match "docker run --detach --network kamal --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" dhh/app:latest ruby -v", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "exec detach with reuse" do
|
||||||
|
assert_raises(ArgumentError, "Detach is not compatible with reuse") do
|
||||||
|
run_command("exec", "--detach", "--reuse", "ruby -v")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "exec detach with interactive" do
|
||||||
|
assert_raises(ArgumentError, "Detach is not compatible with interactive") do
|
||||||
|
run_command("exec", "--interactive", "--detach", "ruby -v")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "exec detach with interactive and reuse" do
|
||||||
|
assert_raises(ArgumentError, "Detach is not compatible with interactive or reuse") do
|
||||||
|
run_command("exec", "--interactive", "--detach", "--reuse", "ruby -v")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "exec with reuse" do
|
test "exec with reuse" do
|
||||||
run_command("exec", "--reuse", "ruby -v").tap do |output|
|
run_command("exec", "--reuse", "ruby -v").tap do |output|
|
||||||
assert_match "sh -c 'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | while read line; do echo ${line#app-web-}; done", output # Get current version
|
assert_match "sh -c 'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | while read line; do echo ${line#app-web-}; done", output # Get current version
|
||||||
assert_match "docker exec app-web-999 ruby -v", output
|
assert_match "docker exec app-web-999 ruby -v", output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "exec interactive" do
|
test "exec interactive" do
|
||||||
SSHKit::Backend::Abstract.any_instance.expects(:exec)
|
SSHKit::Backend::Abstract.any_instance.expects(:exec)
|
||||||
.with("ssh -t root@1.1.1.1 -p 22 'docker run -it --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env dhh/app:latest ruby -v'")
|
.with("ssh -t root@1.1.1.1 -p 22 'docker run -it --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" dhh/app:latest ruby -v'")
|
||||||
run_command("exec", "-i", "ruby -v").tap do |output|
|
run_command("exec", "-i", "ruby -v").tap do |output|
|
||||||
assert_match "Get most recent version available as an image...", output
|
assert_match "Get most recent version available as an image...", output
|
||||||
assert_match "Launching interactive command with version latest via SSH from new container on 1.1.1.1...", output
|
assert_match "Launching interactive command with version latest via SSH from new container on 1.1.1.1...", output
|
||||||
@@ -294,7 +318,7 @@ class CliAppTest < CliTestCase
|
|||||||
.with("ssh -t root@1.1.1.1 -p 22 'docker exec -it app-web-999 ruby -v'")
|
.with("ssh -t root@1.1.1.1 -p 22 'docker exec -it app-web-999 ruby -v'")
|
||||||
run_command("exec", "-i", "--reuse", "ruby -v").tap do |output|
|
run_command("exec", "-i", "--reuse", "ruby -v").tap do |output|
|
||||||
assert_match "Get current version of running container...", output
|
assert_match "Get current version of running container...", output
|
||||||
assert_match "Running /usr/bin/env sh -c 'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | while read line; do echo ${line#app-web-}; done on 1.1.1.1", output
|
assert_match "Running /usr/bin/env sh -c 'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | while read line; do echo ${line#app-web-}; done on 1.1.1.1", output
|
||||||
assert_match "Launching interactive command with version 999 via SSH from existing container on 1.1.1.1...", output
|
assert_match "Launching interactive command with version 999 via SSH from existing container on 1.1.1.1...", output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -313,46 +337,53 @@ class CliAppTest < CliTestCase
|
|||||||
|
|
||||||
test "logs" do
|
test "logs" do
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:exec)
|
SSHKit::Backend::Abstract.any_instance.stubs(:exec)
|
||||||
.with("ssh -t root@1.1.1.1 'sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1| xargs docker logs --timestamps --tail 10 2>&1'")
|
.with("ssh -t root@1.1.1.1 'sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1| xargs docker logs --timestamps --tail 10 2>&1'")
|
||||||
|
|
||||||
assert_match "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --tail 100 2>&1", run_command("logs")
|
assert_match "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --tail 100 2>&1", run_command("logs")
|
||||||
|
|
||||||
assert_match "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps 2>&1 | grep 'hey'", run_command("logs", "--grep", "hey")
|
assert_match "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps 2>&1 | grep 'hey'", run_command("logs", "--grep", "hey")
|
||||||
|
|
||||||
assert_match "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps 2>&1 | grep 'hey' -C 2", run_command("logs", "--grep", "hey", "--grep-options", "-C 2")
|
assert_match "sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps 2>&1 | grep 'hey' -C 2", run_command("logs", "--grep", "hey", "--grep-options", "-C 2")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "logs with follow" do
|
test "logs with follow" do
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:exec)
|
SSHKit::Backend::Abstract.any_instance.stubs(:exec)
|
||||||
.with("ssh -t root@1.1.1.1 -p 22 'sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --tail 10 --follow 2>&1'")
|
.with("ssh -t root@1.1.1.1 -p 22 'sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --tail 10 --follow 2>&1'")
|
||||||
|
|
||||||
assert_match "sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --tail 10 --follow 2>&1", run_command("logs", "--follow")
|
assert_match "sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --tail 10 --follow 2>&1", run_command("logs", "--follow")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "logs with follow and container_id" do
|
||||||
|
SSHKit::Backend::Abstract.any_instance.stubs(:exec)
|
||||||
|
.with("ssh -t root@1.1.1.1 -p 22 'echo ID123 | xargs docker logs --timestamps --tail 10 --follow 2>&1'")
|
||||||
|
|
||||||
|
assert_match "echo ID123 | xargs docker logs --timestamps --tail 10 --follow 2>&1", run_command("logs", "--follow", "--container-id", "ID123")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "logs with follow and grep" do
|
test "logs with follow and grep" do
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:exec)
|
SSHKit::Backend::Abstract.any_instance.stubs(:exec)
|
||||||
.with("ssh -t root@1.1.1.1 -p 22 'sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --follow 2>&1 | grep \"hey\"'")
|
.with("ssh -t root@1.1.1.1 -p 22 'sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --follow 2>&1 | grep \"hey\"'")
|
||||||
|
|
||||||
assert_match "sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --follow 2>&1 | grep \"hey\"", run_command("logs", "--follow", "--grep", "hey")
|
assert_match "sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --follow 2>&1 | grep \"hey\"", run_command("logs", "--follow", "--grep", "hey")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "logs with follow, grep and grep options" do
|
test "logs with follow, grep and grep options" do
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:exec)
|
SSHKit::Backend::Abstract.any_instance.stubs(:exec)
|
||||||
.with("ssh -t root@1.1.1.1 -p 22 'sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --follow 2>&1 | grep \"hey\" -C 2'")
|
.with("ssh -t root@1.1.1.1 -p 22 'sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --follow 2>&1 | grep \"hey\" -C 2'")
|
||||||
|
|
||||||
assert_match "sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --follow 2>&1 | grep \"hey\" -C 2", run_command("logs", "--follow", "--grep", "hey", "--grep-options", "-C 2")
|
assert_match "sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --follow 2>&1 | grep \"hey\" -C 2", run_command("logs", "--follow", "--grep", "hey", "--grep-options", "-C 2")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "version" do
|
test "version" do
|
||||||
run_command("version").tap do |output|
|
run_command("version").tap do |output|
|
||||||
assert_match "sh -c 'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | while read line; do echo ${line#app-web-}; done", output
|
assert_match "sh -c 'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | while read line; do echo ${line#app-web-}; done", output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
test "version through main" do
|
test "version through main" do
|
||||||
stdouted { Kamal::Cli::Main.start([ "app", "version", "-c", "test/fixtures/deploy_with_accessories.yml", "--hosts", "1.1.1.1" ]) }.tap do |output|
|
stdouted { Kamal::Cli::Main.start([ "app", "version", "-c", "test/fixtures/deploy_with_accessories.yml", "--hosts", "1.1.1.1" ]) }.tap do |output|
|
||||||
assert_match "sh -c 'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | while read line; do echo ${line#app-web-}; done", output
|
assert_match "sh -c 'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | while read line; do echo ${line#app-web-}; done", output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -250,7 +250,7 @@ class CliMainTest < CliTestCase
|
|||||||
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-#{role}-123$", "--quiet")
|
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-#{role}-123$", "--quiet")
|
||||||
.returns("version-to-rollback\n").at_least_once
|
.returns("version-to-rollback\n").at_least_once
|
||||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
.with(:sh, "-c", "'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=#{role} --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=#{role} --filter status=running --filter status=restarting'", "|", :head, "-1", "|", "while read line; do echo ${line#app-#{role}-}; done", raise_on_non_zero_exit: false)
|
.with(:sh, "-c", "'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --filter label=role=#{role} --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --filter label=role=#{role} --filter status=running --filter status=restarting'", "|", :head, "-1", "|", "while read line; do echo ${line#app-#{role}-}; done", raise_on_non_zero_exit: false)
|
||||||
.returns("version-to-rollback\n").at_least_once
|
.returns("version-to-rollback\n").at_least_once
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -280,7 +280,7 @@ class CliMainTest < CliTestCase
|
|||||||
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-123$", "--quiet")
|
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-123$", "--quiet")
|
||||||
.returns("123").at_least_once
|
.returns("123").at_least_once
|
||||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
.with(:sh, "-c", "'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'", "|", :head, "-1", "|", "while read line; do echo ${line#app-web-}; done", raise_on_non_zero_exit: false)
|
.with(:sh, "-c", "'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting'", "|", :head, "-1", "|", "while read line; do echo ${line#app-web-}; done", raise_on_non_zero_exit: false)
|
||||||
.returns("").at_least_once
|
.returns("").at_least_once
|
||||||
|
|
||||||
run_command("rollback", "123").tap do |output|
|
run_command("rollback", "123").tap do |output|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ class CliProxyTest < CliTestCase
|
|||||||
test "boot" do
|
test "boot" do
|
||||||
run_command("boot").tap do |output|
|
run_command("boot").tap do |output|
|
||||||
assert_match "docker login", output
|
assert_match "docker login", output
|
||||||
assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443\") #{KAMAL.config.proxy_image}", output
|
assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") #{KAMAL.config.proxy_image}", output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -18,11 +18,11 @@ class CliProxyTest < CliTestCase
|
|||||||
exception = assert_raises do
|
exception = assert_raises do
|
||||||
run_command("boot").tap do |output|
|
run_command("boot").tap do |output|
|
||||||
assert_match "docker login", output
|
assert_match "docker login", output
|
||||||
assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443\") #{KAMAL.config.proxy_image}", output
|
assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") #{KAMAL.config.proxy_image}", output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
assert_includes exception.message, "kamal-proxy version v0.0.1 is too old, please reboot to update to at least #{Kamal::Configuration::PROXY_MINIMUM_VERSION}"
|
assert_includes exception.message, "kamal-proxy version v0.0.1 is too old, run `kamal proxy reboot` in order to update to at least #{Kamal::Configuration::PROXY_MINIMUM_VERSION}"
|
||||||
ensure
|
ensure
|
||||||
Thread.report_on_exception = false
|
Thread.report_on_exception = false
|
||||||
end
|
end
|
||||||
@@ -36,7 +36,7 @@ class CliProxyTest < CliTestCase
|
|||||||
|
|
||||||
run_command("boot").tap do |output|
|
run_command("boot").tap do |output|
|
||||||
assert_match "docker login", output
|
assert_match "docker login", output
|
||||||
assert_match "docker container start kamal-proxy || docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443\") #{KAMAL.config.proxy_image}", output
|
assert_match "docker container start kamal-proxy || docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") #{KAMAL.config.proxy_image}", output
|
||||||
end
|
end
|
||||||
ensure
|
ensure
|
||||||
Thread.report_on_exception = false
|
Thread.report_on_exception = false
|
||||||
@@ -57,13 +57,13 @@ class CliProxyTest < CliTestCase
|
|||||||
assert_match "docker container stop kamal-proxy on 1.1.1.1", output
|
assert_match "docker container stop kamal-proxy on 1.1.1.1", output
|
||||||
assert_match "Running docker container stop traefik ; docker container prune --force --filter label=org.opencontainers.image.title=Traefik && docker image prune --all --force --filter label=org.opencontainers.image.title=Traefik on 1.1.1.1", output
|
assert_match "Running docker container stop traefik ; docker container prune --force --filter label=org.opencontainers.image.title=Traefik && docker image prune --all --force --filter label=org.opencontainers.image.title=Traefik on 1.1.1.1", output
|
||||||
assert_match "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy on 1.1.1.1", output
|
assert_match "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy on 1.1.1.1", output
|
||||||
assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443\") #{KAMAL.config.proxy_image} on 1.1.1.1", output
|
assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") #{KAMAL.config.proxy_image} on 1.1.1.1", output
|
||||||
assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target=\"abcdefabcdef:80\" --deploy-timeout=\"6s\" --drain-timeout=\"30s\" --buffer-requests --buffer-responses --log-request-header=\"Cache-Control\" --log-request-header=\"Last-Modified\" --log-request-header=\"User-Agent\" on 1.1.1.1", output
|
assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target=\"abcdefabcdef:80\" --deploy-timeout=\"6s\" --drain-timeout=\"30s\" --buffer-requests --buffer-responses --log-request-header=\"Cache-Control\" --log-request-header=\"Last-Modified\" --log-request-header=\"User-Agent\" on 1.1.1.1", output
|
||||||
|
|
||||||
assert_match "docker container stop kamal-proxy on 1.1.1.2", output
|
assert_match "docker container stop kamal-proxy on 1.1.1.2", output
|
||||||
assert_match "Running docker container stop traefik ; docker container prune --force --filter label=org.opencontainers.image.title=Traefik && docker image prune --all --force --filter label=org.opencontainers.image.title=Traefik on 1.1.1.2", output
|
assert_match "Running docker container stop traefik ; docker container prune --force --filter label=org.opencontainers.image.title=Traefik && docker image prune --all --force --filter label=org.opencontainers.image.title=Traefik on 1.1.1.2", output
|
||||||
assert_match "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy on 1.1.1.2", output
|
assert_match "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy on 1.1.1.2", output
|
||||||
assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443\") #{KAMAL.config.proxy_image} on 1.1.1.2", output
|
assert_match "docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") #{KAMAL.config.proxy_image} on 1.1.1.2", output
|
||||||
assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target=\"abcdefabcdef:80\" --deploy-timeout=\"6s\" --drain-timeout=\"30s\" --buffer-requests --buffer-responses --log-request-header=\"Cache-Control\" --log-request-header=\"Last-Modified\" --log-request-header=\"User-Agent\" on 1.1.1.2", output
|
assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target=\"abcdefabcdef:80\" --deploy-timeout=\"6s\" --drain-timeout=\"30s\" --buffer-requests --buffer-responses --log-request-header=\"Cache-Control\" --log-request-header=\"Last-Modified\" --log-request-header=\"User-Agent\" on 1.1.1.2", output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -198,7 +198,7 @@ class CliProxyTest < CliTestCase
|
|||||||
assert_match "/usr/bin/env mkdir -p .kamal", output
|
assert_match "/usr/bin/env mkdir -p .kamal", output
|
||||||
assert_match "docker network create kamal", output
|
assert_match "docker network create kamal", output
|
||||||
assert_match "docker login -u [REDACTED] -p [REDACTED]", output
|
assert_match "docker login -u [REDACTED] -p [REDACTED]", output
|
||||||
assert_match "docker container start kamal-proxy || docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443\") basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}", output
|
assert_match "docker container start kamal-proxy || docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}", output
|
||||||
assert_match "/usr/bin/env mkdir -p .kamal", output
|
assert_match "/usr/bin/env mkdir -p .kamal", output
|
||||||
assert_match %r{docker rename app-web-latest app-web-latest_replaced_.*}, output
|
assert_match %r{docker rename app-web-latest app-web-latest_replaced_.*}, output
|
||||||
assert_match "/usr/bin/env mkdir -p .kamal/apps/app/env/roles", output
|
assert_match "/usr/bin/env mkdir -p .kamal/apps/app/env/roles", output
|
||||||
@@ -240,7 +240,7 @@ class CliProxyTest < CliTestCase
|
|||||||
run_command("boot_config", "set").tap do |output|
|
run_command("boot_config", "set").tap do |output|
|
||||||
%w[ 1.1.1.1 1.1.1.2 ].each do |host|
|
%w[ 1.1.1.1 1.1.1.2 ].each do |host|
|
||||||
assert_match "Running /usr/bin/env mkdir -p .kamal/proxy on #{host}", output
|
assert_match "Running /usr/bin/env mkdir -p .kamal/proxy on #{host}", output
|
||||||
assert_match "Uploading \"--publish 80:80 --publish 443:443\" to .kamal/proxy/options on #{host}", output
|
assert_match "Uploading \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\" to .kamal/proxy/options on #{host}", output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -249,7 +249,25 @@ class CliProxyTest < CliTestCase
|
|||||||
run_command("boot_config", "set", "--publish", "false").tap do |output|
|
run_command("boot_config", "set", "--publish", "false").tap do |output|
|
||||||
%w[ 1.1.1.1 1.1.1.2 ].each do |host|
|
%w[ 1.1.1.1 1.1.1.2 ].each do |host|
|
||||||
assert_match "Running /usr/bin/env mkdir -p .kamal/proxy on #{host}", output
|
assert_match "Running /usr/bin/env mkdir -p .kamal/proxy on #{host}", output
|
||||||
assert_match "Uploading \"\" to .kamal/proxy/options on #{host}", output
|
assert_match "Uploading \"--log-opt max-size=10m\" to .kamal/proxy/options on #{host}", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "boot_config set custom max_size" do
|
||||||
|
run_command("boot_config", "set", "--log-max-size", "100m").tap do |output|
|
||||||
|
%w[ 1.1.1.1 1.1.1.2 ].each do |host|
|
||||||
|
assert_match "Running /usr/bin/env mkdir -p .kamal/proxy on #{host}", output
|
||||||
|
assert_match "Uploading \"--publish 80:80 --publish 443:443 --log-opt max-size=100m\" to .kamal/proxy/options on #{host}", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "boot_config set no log max size" do
|
||||||
|
run_command("boot_config", "set", "--log-max-size=").tap do |output|
|
||||||
|
%w[ 1.1.1.1 1.1.1.2 ].each do |host|
|
||||||
|
assert_match "Running /usr/bin/env mkdir -p .kamal/proxy on #{host}", output
|
||||||
|
assert_match "Uploading \"--publish 80:80 --publish 443:443\" to .kamal/proxy/options on #{host}", output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -258,7 +276,7 @@ class CliProxyTest < CliTestCase
|
|||||||
run_command("boot_config", "set", "--http-port", "8080", "--https-port", "8443").tap do |output|
|
run_command("boot_config", "set", "--http-port", "8080", "--https-port", "8443").tap do |output|
|
||||||
%w[ 1.1.1.1 1.1.1.2 ].each do |host|
|
%w[ 1.1.1.1 1.1.1.2 ].each do |host|
|
||||||
assert_match "Running /usr/bin/env mkdir -p .kamal/proxy on #{host}", output
|
assert_match "Running /usr/bin/env mkdir -p .kamal/proxy on #{host}", output
|
||||||
assert_match "Uploading \"--publish 8080:80 --publish 8443:443\" to .kamal/proxy/options on #{host}", output
|
assert_match "Uploading \"--publish 8080:80 --publish 8443:443 --log-opt max-size=10m\" to .kamal/proxy/options on #{host}", output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -267,14 +285,14 @@ class CliProxyTest < CliTestCase
|
|||||||
run_command("boot_config", "set", "--docker_options", "label=foo=bar", "add_host=thishost:thathost").tap do |output|
|
run_command("boot_config", "set", "--docker_options", "label=foo=bar", "add_host=thishost:thathost").tap do |output|
|
||||||
%w[ 1.1.1.1 1.1.1.2 ].each do |host|
|
%w[ 1.1.1.1 1.1.1.2 ].each do |host|
|
||||||
assert_match "Running /usr/bin/env mkdir -p .kamal/proxy on #{host}", output
|
assert_match "Running /usr/bin/env mkdir -p .kamal/proxy on #{host}", output
|
||||||
assert_match "Uploading \"--publish 80:80 --publish 443:443 --label=foo=bar --add_host=thishost:thathost\" to .kamal/proxy/options on #{host}", output
|
assert_match "Uploading \"--publish 80:80 --publish 443:443 --log-opt max-size=10m --label=foo=bar --add_host=thishost:thathost\" to .kamal/proxy/options on #{host}", output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "boot_config get" do
|
test "boot_config get" do
|
||||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
.with(:cat, ".kamal/proxy/options", "||", :echo, "\"--publish 80:80 --publish 443:443\"")
|
.with(:cat, ".kamal/proxy/options", "||", :echo, "\"--publish 80:80 --publish 443:443 --log-opt max-size=10m\"")
|
||||||
.returns("--publish 80:80 --publish 8443:443 --label=foo=bar")
|
.returns("--publish 80:80 --publish 8443:443 --label=foo=bar")
|
||||||
.twice
|
.twice
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,18 @@ class CliSecretsTest < CliTestCase
|
|||||||
run_command("fetch", "foo", "bar", "baz", "--account", "myaccount", "--adapter", "test")
|
run_command("fetch", "foo", "bar", "baz", "--account", "myaccount", "--adapter", "test")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "fetch missing --acount" do
|
||||||
|
assert_equal \
|
||||||
|
"No value provided for required options '--account'",
|
||||||
|
run_command("fetch", "foo", "bar", "baz", "--adapter", "test")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "fetch without required --account" do
|
||||||
|
assert_equal \
|
||||||
|
"\\{\\\"foo\\\":\\\"oof\\\",\\\"bar\\\":\\\"rab\\\",\\\"baz\\\":\\\"zab\\\"\\}",
|
||||||
|
run_command("fetch", "foo", "bar", "baz", "--adapter", "test_optional_account")
|
||||||
|
end
|
||||||
|
|
||||||
test "extract" do
|
test "extract" do
|
||||||
assert_equal "oof", run_command("extract", "foo", "{\"foo\":\"oof\", \"bar\":\"rab\", \"baz\":\"zab\"}")
|
assert_equal "oof", run_command("extract", "foo", "{\"foo\":\"oof\", \"bar\":\"rab\", \"baz\":\"zab\"}")
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -39,7 +39,10 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
|
|||||||
"busybox" => {
|
"busybox" => {
|
||||||
"service" => "custom-busybox",
|
"service" => "custom-busybox",
|
||||||
"image" => "busybox:latest",
|
"image" => "busybox:latest",
|
||||||
"host" => "1.1.1.7"
|
"host" => "1.1.1.7",
|
||||||
|
"proxy" => {
|
||||||
|
"host" => "busybox.example.com"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,6 +74,14 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
|
|||||||
new_command(:busybox).run.join(" ")
|
new_command(:busybox).run.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "run in custom network" do
|
||||||
|
@config[:accessories]["mysql"]["network"] = "custom"
|
||||||
|
|
||||||
|
assert_equal \
|
||||||
|
"docker run --name app-mysql --detach --restart unless-stopped --network custom --log-opt max-size=\"10m\" --publish 3306:3306 --env MYSQL_ROOT_HOST=\"%\" --env-file .kamal/apps/app/env/accessories/mysql.env --label service=\"app-mysql\" private.registry/mysql:8.0",
|
||||||
|
new_command(:mysql).run.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
test "start" do
|
test "start" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker container start app-mysql",
|
"docker container start app-mysql",
|
||||||
@@ -158,6 +169,18 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
|
|||||||
new_command(:mysql).remove_image.join(" ")
|
new_command(:mysql).remove_image.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "deploy" do
|
||||||
|
assert_equal \
|
||||||
|
"docker exec kamal-proxy kamal-proxy deploy custom-busybox --target=\"172.1.0.2:80\" --host=\"busybox.example.com\" --deploy-timeout=\"30s\" --drain-timeout=\"30s\" --buffer-requests --buffer-responses --log-request-header=\"Cache-Control\" --log-request-header=\"Last-Modified\" --log-request-header=\"User-Agent\"",
|
||||||
|
new_command(:busybox).deploy(target: "172.1.0.2").join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "remove" do
|
||||||
|
assert_equal \
|
||||||
|
"docker exec kamal-proxy kamal-proxy remove custom-busybox",
|
||||||
|
new_command(:busybox).remove.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)
|
||||||
|
|||||||
@@ -79,18 +79,18 @@ class CommandsAppTest < ActiveSupport::TestCase
|
|||||||
|
|
||||||
test "stop" do
|
test "stop" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker stop",
|
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker stop",
|
||||||
new_command.stop.join(" ")
|
new_command.stop.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "stop with custom drain timeout" do
|
test "stop with custom drain timeout" do
|
||||||
@config[:drain_timeout] = 20
|
@config[:drain_timeout] = 20
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker stop",
|
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker stop",
|
||||||
new_command.stop.join(" ")
|
new_command.stop.join(" ")
|
||||||
|
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=workers --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=workers --filter status=running --filter status=restarting' | head -1 | xargs docker stop -t 20",
|
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=workers --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=workers --filter status=running --filter status=restarting' | head -1 | xargs docker stop -t 20",
|
||||||
new_command(role: "workers").stop.join(" ")
|
new_command(role: "workers").stop.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -102,7 +102,7 @@ class CommandsAppTest < ActiveSupport::TestCase
|
|||||||
|
|
||||||
test "info" do
|
test "info" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker ps --filter label=service=app --filter label=role=web",
|
"docker ps --filter label=service=app --filter label=destination= --filter label=role=web",
|
||||||
new_command.info.join(" ")
|
new_command.info.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -135,6 +135,14 @@ class CommandsAppTest < ActiveSupport::TestCase
|
|||||||
new_command.deploy(target: "172.1.0.2").join(" ")
|
new_command.deploy(target: "172.1.0.2").join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "deploy with SSL false" do
|
||||||
|
@config[:proxy] = { "ssl" => false }
|
||||||
|
|
||||||
|
assert_equal \
|
||||||
|
"docker exec kamal-proxy kamal-proxy deploy app-web --target=\"172.1.0.2:80\" --deploy-timeout=\"30s\" --drain-timeout=\"30s\" --buffer-requests --buffer-responses --log-request-header=\"Cache-Control\" --log-request-header=\"Last-Modified\" --log-request-header=\"User-Agent\"",
|
||||||
|
new_command.deploy(target: "172.1.0.2").join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
test "remove" do
|
test "remove" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker exec kamal-proxy kamal-proxy remove app-web",
|
"docker exec kamal-proxy kamal-proxy remove app-web",
|
||||||
@@ -145,100 +153,124 @@ class CommandsAppTest < ActiveSupport::TestCase
|
|||||||
|
|
||||||
test "logs" do
|
test "logs" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps 2>&1",
|
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps 2>&1",
|
||||||
new_command.logs.join(" ")
|
new_command.logs.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "logs with container_id" do
|
||||||
|
assert_equal \
|
||||||
|
"echo C137 | xargs docker logs --timestamps 2>&1",
|
||||||
|
new_command.logs(container_id: "C137").join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
test "logs with since" do
|
test "logs with since" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --since 5m 2>&1",
|
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --since 5m 2>&1",
|
||||||
new_command.logs(since: "5m").join(" ")
|
new_command.logs(since: "5m").join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "logs with lines" do
|
test "logs with lines" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --tail 100 2>&1",
|
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --tail 100 2>&1",
|
||||||
new_command.logs(lines: "100").join(" ")
|
new_command.logs(lines: "100").join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "logs with since and lines" do
|
test "logs with since and lines" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --since 5m --tail 100 2>&1",
|
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --since 5m --tail 100 2>&1",
|
||||||
new_command.logs(since: "5m", lines: "100").join(" ")
|
new_command.logs(since: "5m", lines: "100").join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "logs with grep" do
|
test "logs with grep" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps 2>&1 | grep 'my-id'",
|
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps 2>&1 | grep 'my-id'",
|
||||||
new_command.logs(grep: "my-id").join(" ")
|
new_command.logs(grep: "my-id").join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "logs with grep and grep options" do
|
test "logs with grep and grep options" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps 2>&1 | grep 'my-id' -C 2",
|
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps 2>&1 | grep 'my-id' -C 2",
|
||||||
new_command.logs(grep: "my-id", grep_options: "-C 2").join(" ")
|
new_command.logs(grep: "my-id", grep_options: "-C 2").join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "logs with since, grep and grep options" do
|
test "logs with since, grep and grep options" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --since 5m 2>&1 | grep 'my-id' -C 2",
|
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --since 5m 2>&1 | grep 'my-id' -C 2",
|
||||||
new_command.logs(since: "5m", grep: "my-id", grep_options: "-C 2").join(" ")
|
new_command.logs(since: "5m", grep: "my-id", grep_options: "-C 2").join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "logs with since and grep" do
|
test "logs with since and grep" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --since 5m 2>&1 | grep 'my-id'",
|
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | xargs docker logs --timestamps --since 5m 2>&1 | grep 'my-id'",
|
||||||
new_command.logs(since: "5m", grep: "my-id").join(" ")
|
new_command.logs(since: "5m", grep: "my-id").join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "follow logs" do
|
test "follow logs" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"ssh -t root@app-1 -p 22 'sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --follow 2>&1'",
|
"ssh -t root@app-1 -p 22 'sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --follow 2>&1'",
|
||||||
new_command.follow_logs(host: "app-1")
|
new_command.follow_logs(host: "app-1")
|
||||||
|
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"ssh -t root@app-1 -p 22 'sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --follow 2>&1 | grep \"Completed\"'",
|
"ssh -t root@app-1 -p 22 'sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --follow 2>&1 | grep \"Completed\"'",
|
||||||
new_command.follow_logs(host: "app-1", grep: "Completed")
|
new_command.follow_logs(host: "app-1", grep: "Completed")
|
||||||
|
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"ssh -t root@app-1 -p 22 'sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --tail 123 --follow 2>&1'",
|
"ssh -t root@app-1 -p 22 'echo ID321 | xargs docker logs --timestamps --follow 2>&1'",
|
||||||
|
new_command.follow_logs(host: "app-1", container_id: "ID321")
|
||||||
|
|
||||||
|
assert_equal \
|
||||||
|
"ssh -t root@app-1 -p 22 'sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --tail 123 --follow 2>&1'",
|
||||||
new_command.follow_logs(host: "app-1", lines: 123)
|
new_command.follow_logs(host: "app-1", lines: 123)
|
||||||
|
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"ssh -t root@app-1 -p 22 'sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --tail 123 --follow 2>&1 | grep \"Completed\"'",
|
"ssh -t root@app-1 -p 22 'sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --timestamps --tail 123 --follow 2>&1 | grep \"Completed\"'",
|
||||||
new_command.follow_logs(host: "app-1", lines: 123, grep: "Completed")
|
new_command.follow_logs(host: "app-1", lines: 123, grep: "Completed")
|
||||||
|
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"ssh -t root@app-1 -p 22 'sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --tail 123 --follow 2>&1 | grep \"Completed\"'",
|
"ssh -t root@app-1 -p 22 'sh -c '\\''docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''\\'\\'''\\''{{.ID}}'\\''\\'\\'''\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting'\\'' | head -1 | xargs docker logs --tail 123 --follow 2>&1 | grep \"Completed\"'",
|
||||||
new_command.follow_logs(host: "app-1", timestamps: false, lines: 123, grep: "Completed")
|
new_command.follow_logs(host: "app-1", timestamps: false, lines: 123, grep: "Completed")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
test "execute in new container" do
|
test "execute in new container" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker run --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env dhh/app:999 bin/rails db:setup",
|
"docker run --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" dhh/app:999 bin/rails db:setup",
|
||||||
|
new_command.execute_in_new_container("bin/rails", "db:setup", env: {}).join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "execute in new container with logging" do
|
||||||
|
@config[:logging] = { "driver" => "local", "options" => { "max-size" => "100m", "max-file" => "3" } }
|
||||||
|
|
||||||
|
assert_equal \
|
||||||
|
"docker run --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" 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
|
||||||
|
|
||||||
test "execute in new container with env" do
|
test "execute in new container with env" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker run --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --env foo=\"bar\" dhh/app:999 bin/rails db:setup",
|
"docker run --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --env foo=\"bar\" --log-opt max-size=\"10m\" dhh/app:999 bin/rails db:setup",
|
||||||
new_command.execute_in_new_container("bin/rails", "db:setup", env: { "foo" => "bar" }).join(" ")
|
new_command.execute_in_new_container("bin/rails", "db:setup", env: { "foo" => "bar" }).join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "execute in new detached container" do
|
||||||
|
assert_equal \
|
||||||
|
"docker run --detach --network kamal --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" dhh/app:999 bin/rails db:setup",
|
||||||
|
new_command.execute_in_new_container("bin/rails", "db:setup", detach: true, env: {}).join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
test "execute in new container with tags" do
|
test "execute in new container with tags" do
|
||||||
@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 \
|
assert_equal \
|
||||||
"docker run --rm --network kamal --env ENV1=\"value1\" --env-file .kamal/apps/app/env/roles/web.env dhh/app:999 bin/rails db:setup",
|
"docker run --rm --network kamal --env ENV1=\"value1\" --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" 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
|
||||||
|
|
||||||
test "execute in new container with custom options" do
|
test "execute in new container with custom options" do
|
||||||
@config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "options" => { "mount" => "somewhere", "cap-add" => true } } }
|
@config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "options" => { "mount" => "somewhere", "cap-add" => true } } }
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker run --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --mount \"somewhere\" --cap-add dhh/app:999 bin/rails db:setup",
|
"docker run --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" --mount \"somewhere\" --cap-add dhh/app:999 bin/rails 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
|
||||||
|
|
||||||
@@ -255,7 +287,7 @@ class CommandsAppTest < ActiveSupport::TestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "execute in new container over ssh" do
|
test "execute in new container over ssh" do
|
||||||
assert_match %r{docker run -it --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env dhh/app:999 bin/rails c},
|
assert_match %r{docker run -it --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size="10m" dhh/app:999 bin/rails c},
|
||||||
new_command.execute_in_new_container_over_ssh("bin/rails", "c", env: {})
|
new_command.execute_in_new_container_over_ssh("bin/rails", "c", env: {})
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -263,13 +295,13 @@ 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 --network kamal --env ENV1=\"value1\" --env-file .kamal/apps/app/env/roles/web.env dhh/app:999 bin/rails c'",
|
assert_equal "ssh -t root@1.1.1.1 -p 22 'docker run -it --rm --network kamal --env ENV1=\"value1\" --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" 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
|
||||||
|
|
||||||
test "execute in new container with custom options over ssh" do
|
test "execute in new container with custom options over ssh" do
|
||||||
@config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "options" => { "mount" => "somewhere", "cap-add" => true } } }
|
@config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "options" => { "mount" => "somewhere", "cap-add" => true } } }
|
||||||
assert_match %r{docker run -it --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --mount \"somewhere\" --cap-add dhh/app:999 bin/rails c},
|
assert_match %r{docker run -it --rm --network kamal --env-file .kamal/apps/app/env/roles/web.env --log-opt max-size=\"10m\" --mount \"somewhere\" --cap-add dhh/app:999 bin/rails c},
|
||||||
new_command.execute_in_new_container_over_ssh("bin/rails", "c", env: {})
|
new_command.execute_in_new_container_over_ssh("bin/rails", "c", env: {})
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -307,6 +339,16 @@ class CommandsAppTest < ActiveSupport::TestCase
|
|||||||
assert_equal "ssh -J root@2.2.2.2 -t app@1.1.1.1 -p 22 'ls'", new_command.run_over_ssh("ls", host: "1.1.1.1")
|
assert_equal "ssh -J root@2.2.2.2 -t app@1.1.1.1 -p 22 'ls'", new_command.run_over_ssh("ls", host: "1.1.1.1")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "run over ssh with keys config" do
|
||||||
|
@config[:ssh] = { "keys" => [ "path_to_key.pem" ] }
|
||||||
|
assert_equal "ssh -i path_to_key.pem -t root@1.1.1.1 -p 22 'ls'", new_command.run_over_ssh("ls", host: "1.1.1.1")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "run over ssh with keys config with keys_only" do
|
||||||
|
@config[:ssh] = { "keys" => [ "path_to_key.pem" ], "keys_only" => true }
|
||||||
|
assert_equal "ssh -i path_to_key.pem -o IdentitiesOnly=yes -t root@1.1.1.1 -p 22 'ls'", new_command.run_over_ssh("ls", host: "1.1.1.1")
|
||||||
|
end
|
||||||
|
|
||||||
test "run over ssh with proxy_command" do
|
test "run over ssh with proxy_command" do
|
||||||
@config[:ssh] = { "proxy_command" => "ssh -W %h:%p user@proxy-server" }
|
@config[:ssh] = { "proxy_command" => "ssh -W %h:%p user@proxy-server" }
|
||||||
assert_equal "ssh -o ProxyCommand='ssh -W %h:%p user@proxy-server' -t root@1.1.1.1 -p 22 'ls'", new_command.run_over_ssh("ls", host: "1.1.1.1")
|
assert_equal "ssh -o ProxyCommand='ssh -W %h:%p user@proxy-server' -t root@1.1.1.1 -p 22 'ls'", new_command.run_over_ssh("ls", host: "1.1.1.1")
|
||||||
@@ -314,7 +356,7 @@ class CommandsAppTest < ActiveSupport::TestCase
|
|||||||
|
|
||||||
test "current_running_container_id" do
|
test "current_running_container_id" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1",
|
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1",
|
||||||
new_command.current_running_container_id.join(" ")
|
new_command.current_running_container_id.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -333,23 +375,23 @@ class CommandsAppTest < ActiveSupport::TestCase
|
|||||||
|
|
||||||
test "current_running_version" do
|
test "current_running_version" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"sh -c 'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | while read line; do echo ${line#app-web-}; done",
|
"sh -c 'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting' | head -1 | while read line; do echo ${line#app-web-}; done",
|
||||||
new_command.current_running_version.join(" ")
|
new_command.current_running_version.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "list_versions" do
|
test "list_versions" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker ps --filter label=service=app --filter label=role=web --format \"{{.Names}}\" | while read line; do echo ${line#app-web-}; done",
|
"docker ps --filter label=service=app --filter label=destination= --filter label=role=web --format \"{{.Names}}\" | while read line; do echo ${line#app-web-}; done",
|
||||||
new_command.list_versions.join(" ")
|
new_command.list_versions.join(" ")
|
||||||
|
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker ps --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest --format \"{{.Names}}\" | while read line; do echo ${line#app-web-}; done",
|
"docker ps --filter label=service=app --filter label=destination= --filter label=role=web --filter status=running --filter status=restarting --latest --format \"{{.Names}}\" | while read line; do echo ${line#app-web-}; done",
|
||||||
new_command.list_versions("--latest", statuses: [ :running, :restarting ]).join(" ")
|
new_command.list_versions("--latest", statuses: [ :running, :restarting ]).join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "list_containers" do
|
test "list_containers" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker container ls --all --filter label=service=app --filter label=role=web",
|
"docker container ls --all --filter label=service=app --filter label=destination= --filter label=role=web",
|
||||||
new_command.list_containers.join(" ")
|
new_command.list_containers.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -362,7 +404,7 @@ class CommandsAppTest < ActiveSupport::TestCase
|
|||||||
|
|
||||||
test "list_container_names" do
|
test "list_container_names" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker container ls --all --filter label=service=app --filter label=role=web --format '{{ .Names }}'",
|
"docker container ls --all --filter label=service=app --filter label=destination= --filter label=role=web --format '{{ .Names }}'",
|
||||||
new_command.list_container_names.join(" ")
|
new_command.list_container_names.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -381,7 +423,7 @@ class CommandsAppTest < ActiveSupport::TestCase
|
|||||||
|
|
||||||
test "remove_containers" do
|
test "remove_containers" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker container prune --force --filter label=service=app --filter label=role=web",
|
"docker container prune --force --filter label=service=app --filter label=destination= --filter label=role=web",
|
||||||
new_command.remove_containers.join(" ")
|
new_command.remove_containers.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -400,14 +442,14 @@ class CommandsAppTest < ActiveSupport::TestCase
|
|||||||
|
|
||||||
test "remove_images" do
|
test "remove_images" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker image prune --all --force --filter label=service=app --filter label=role=web",
|
"docker image prune --all --force --filter label=service=app",
|
||||||
new_command.remove_images.join(" ")
|
new_command.remove_images.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "remove_images with destination" do
|
test "remove_images with destination" do
|
||||||
@destination = "staging"
|
@destination = "staging"
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker image prune --all --force --filter label=service=app --filter label=destination=staging --filter label=role=web",
|
"docker image prune --all --force --filter label=service=app",
|
||||||
new_command.remove_images.join(" ")
|
new_command.remove_images.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -144,20 +144,59 @@ class CommandsBuilderTest < ActiveSupport::TestCase
|
|||||||
builder.push.join(" ")
|
builder.push.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "push with provenance" do
|
||||||
|
builder = new_builder_command(builder: { "provenance" => "mode=max" })
|
||||||
|
assert_equal \
|
||||||
|
"docker buildx build --push --platform linux/amd64 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile --provenance mode=max .",
|
||||||
|
builder.push.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "push with provenance false" do
|
||||||
|
builder = new_builder_command(builder: { "provenance" => false })
|
||||||
|
assert_equal \
|
||||||
|
"docker buildx build --push --platform linux/amd64 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile --provenance false .",
|
||||||
|
builder.push.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "push with sbom" do
|
||||||
|
builder = new_builder_command(builder: { "sbom" => true })
|
||||||
|
assert_equal \
|
||||||
|
"docker buildx build --push --platform linux/amd64 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile --sbom true .",
|
||||||
|
builder.push.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "push with sbom false" do
|
||||||
|
builder = new_builder_command(builder: { "sbom" => false })
|
||||||
|
assert_equal \
|
||||||
|
"docker buildx build --push --platform linux/amd64 --builder kamal-local-docker-container -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile --sbom false .",
|
||||||
|
builder.push.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
test "mirror count" do
|
test "mirror count" do
|
||||||
command = new_builder_command
|
command = new_builder_command
|
||||||
assert_equal "docker info --format '{{index .RegistryConfig.Mirrors 0}}'", command.first_mirror.join(" ")
|
assert_equal "docker info --format '{{index .RegistryConfig.Mirrors 0}}'", command.first_mirror.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "clone path with spaces" do
|
||||||
|
command = new_builder_command
|
||||||
|
Kamal::Git.stubs(:root).returns("/absolute/path with spaces")
|
||||||
|
clone_command = command.clone.join(" ")
|
||||||
|
clone_reset_commands = command.clone_reset_steps.map { |a| a.join(" ") }
|
||||||
|
|
||||||
|
assert_match(%r{path\\ with\\ space}, clone_command)
|
||||||
|
assert_no_match(%r{path with spaces}, clone_command)
|
||||||
|
|
||||||
|
clone_reset_commands.each do |command|
|
||||||
|
assert_match(%r{path\\ with\\ space}, command)
|
||||||
|
assert_no_match(%r{path with spaces}, command)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def new_builder_command(additional_config = {})
|
def new_builder_command(additional_config = {})
|
||||||
Kamal::Commands::Builder.new(Kamal::Configuration.new(@config.deep_merge(additional_config), version: "123"))
|
Kamal::Commands::Builder.new(Kamal::Configuration.new(@config.deep_merge(additional_config), version: "123"))
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_directory
|
|
||||||
"#{Dir.tmpdir}/kamal-clones/app/kamal/"
|
|
||||||
end
|
|
||||||
|
|
||||||
def local_arch
|
def local_arch
|
||||||
Kamal::Utils.docker_arch
|
Kamal::Utils.docker_arch
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class CommandsProxyTest < ActiveSupport::TestCase
|
|||||||
|
|
||||||
test "run" do
|
test "run" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443\") basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}",
|
"docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}",
|
||||||
new_command.run.join(" ")
|
new_command.run.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ class CommandsProxyTest < ActiveSupport::TestCase
|
|||||||
@config.delete(:proxy)
|
@config.delete(:proxy)
|
||||||
|
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443\") basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}",
|
"docker run --name kamal-proxy --network kamal --detach --restart unless-stopped --volume kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy $(cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\") basecamp/kamal-proxy:#{Kamal::Configuration::PROXY_MINIMUM_VERSION}",
|
||||||
new_command.run.join(" ")
|
new_command.run.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -113,7 +113,7 @@ class CommandsProxyTest < ActiveSupport::TestCase
|
|||||||
|
|
||||||
test "get_boot_options" do
|
test "get_boot_options" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443\"",
|
"cat .kamal/proxy/options || echo \"--publish 80:80 --publish 443:443 --log-opt max-size=10m\"",
|
||||||
new_command.get_boot_options.join(" ")
|
new_command.get_boot_options.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,9 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase
|
|||||||
"options" => {
|
"options" => {
|
||||||
"cpus" => "4",
|
"cpus" => "4",
|
||||||
"memory" => "2GB"
|
"memory" => "2GB"
|
||||||
|
},
|
||||||
|
"proxy" => {
|
||||||
|
"host" => "monitoring.example.com"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -152,4 +155,18 @@ class ConfigurationAccessoryTest < ActiveSupport::TestCase
|
|||||||
test "options" do
|
test "options" do
|
||||||
assert_equal [ "--cpus", "\"4\"", "--memory", "\"2GB\"" ], @config.accessory(:redis).option_args
|
assert_equal [ "--cpus", "\"4\"", "--memory", "\"2GB\"" ], @config.accessory(:redis).option_args
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "network_args default" do
|
||||||
|
assert_equal [ "--network", "kamal" ], @config.accessory(:mysql).network_args
|
||||||
|
end
|
||||||
|
|
||||||
|
test "network_args with configured options" do
|
||||||
|
@deploy[:accessories]["mysql"]["network"] = "database"
|
||||||
|
assert_equal [ "--network", "database" ], @config.accessory(:mysql).network_args
|
||||||
|
end
|
||||||
|
|
||||||
|
test "proxy" do
|
||||||
|
assert @config.accessory(:monitoring).running_proxy?
|
||||||
|
assert_equal [ "monitoring.example.com" ], @config.accessory(:monitoring).proxy.hosts
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ class ConfigurationBuilderTest < ActiveSupport::TestCase
|
|||||||
@deploy[:builder] = { "arch" => "amd64", "cache" => { "type" => "registry", "options" => "mode=max,image-manifest=true,oci-mediatypes=true" } }
|
@deploy[:builder] = { "arch" => "amd64", "cache" => { "type" => "registry", "options" => "mode=max,image-manifest=true,oci-mediatypes=true" } }
|
||||||
|
|
||||||
assert_equal "type=registry,ref=dhh/app-build-cache", config.builder.cache_from
|
assert_equal "type=registry,ref=dhh/app-build-cache", config.builder.cache_from
|
||||||
assert_equal "type=registry,mode=max,image-manifest=true,oci-mediatypes=true,ref=dhh/app-build-cache", config.builder.cache_to
|
assert_equal "type=registry,ref=dhh/app-build-cache,mode=max,image-manifest=true,oci-mediatypes=true", config.builder.cache_to
|
||||||
end
|
end
|
||||||
|
|
||||||
test "setting registry cache when using a custom registry" do
|
test "setting registry cache when using a custom registry" do
|
||||||
@@ -72,14 +72,14 @@ class ConfigurationBuilderTest < ActiveSupport::TestCase
|
|||||||
@deploy[:builder] = { "arch" => "amd64", "cache" => { "type" => "registry", "options" => "mode=max,image-manifest=true,oci-mediatypes=true" } }
|
@deploy[:builder] = { "arch" => "amd64", "cache" => { "type" => "registry", "options" => "mode=max,image-manifest=true,oci-mediatypes=true" } }
|
||||||
|
|
||||||
assert_equal "type=registry,ref=registry.example.com/dhh/app-build-cache", config.builder.cache_from
|
assert_equal "type=registry,ref=registry.example.com/dhh/app-build-cache", config.builder.cache_from
|
||||||
assert_equal "type=registry,mode=max,image-manifest=true,oci-mediatypes=true,ref=registry.example.com/dhh/app-build-cache", config.builder.cache_to
|
assert_equal "type=registry,ref=registry.example.com/dhh/app-build-cache,mode=max,image-manifest=true,oci-mediatypes=true", config.builder.cache_to
|
||||||
end
|
end
|
||||||
|
|
||||||
test "setting registry cache with image" do
|
test "setting registry cache with image" do
|
||||||
@deploy[:builder] = { "arch" => "amd64", "cache" => { "type" => "registry", "image" => "kamal", "options" => "mode=max" } }
|
@deploy[:builder] = { "arch" => "amd64", "cache" => { "type" => "registry", "image" => "kamal", "options" => "mode=max" } }
|
||||||
|
|
||||||
assert_equal "type=registry,ref=kamal", config.builder.cache_from
|
assert_equal "type=registry,ref=kamal", config.builder.cache_from
|
||||||
assert_equal "type=registry,mode=max,ref=kamal", config.builder.cache_to
|
assert_equal "type=registry,ref=kamal,mode=max", config.builder.cache_to
|
||||||
end
|
end
|
||||||
|
|
||||||
test "args" do
|
test "args" do
|
||||||
@@ -134,6 +134,26 @@ 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 "provenance" do
|
||||||
|
assert_nil config.builder.provenance
|
||||||
|
end
|
||||||
|
|
||||||
|
test "setting provenance" do
|
||||||
|
@deploy[:builder]["provenance"] = "mode=max"
|
||||||
|
|
||||||
|
assert_equal "mode=max", config.builder.provenance
|
||||||
|
end
|
||||||
|
|
||||||
|
test "sbom" do
|
||||||
|
assert_nil config.builder.sbom
|
||||||
|
end
|
||||||
|
|
||||||
|
test "setting sbom" do
|
||||||
|
@deploy[:builder]["sbom"] = true
|
||||||
|
|
||||||
|
assert_equal true, config.builder.sbom
|
||||||
|
end
|
||||||
|
|
||||||
test "local disabled but no remote set" do
|
test "local disabled but no remote set" do
|
||||||
@deploy[:builder]["local"] = false
|
@deploy[:builder]["local"] = false
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,16 @@ class EnvFileTest < ActiveSupport::TestCase
|
|||||||
Kamal::EnvFile.new(env).to_s
|
Kamal::EnvFile.new(env).to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "to_s won't escape '#'" do
|
||||||
|
env = {
|
||||||
|
"foo" => '#$foo',
|
||||||
|
"bar" => '#{bar}'
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_equal "foo=\#$foo\nbar=\#{bar}\n", \
|
||||||
|
Kamal::EnvFile.new(env).to_s
|
||||||
|
end
|
||||||
|
|
||||||
test "to_str won't escape chinese characters" do
|
test "to_str won't escape chinese characters" do
|
||||||
env = {
|
env = {
|
||||||
"foo" => '你好 means hello, "欢迎" means welcome, that\'s simple! 😃 {smile}'
|
"foo" => '你好 means hello, "欢迎" means welcome, that\'s simple! 😃 {smile}'
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ COPY *.sh .
|
|||||||
COPY app/ app/
|
COPY app/ app/
|
||||||
COPY app_with_roles/ app_with_roles/
|
COPY app_with_roles/ app_with_roles/
|
||||||
COPY app_with_traefik/ app_with_traefik/
|
COPY app_with_traefik/ app_with_traefik/
|
||||||
|
COPY app_with_proxied_accessory/ app_with_proxied_accessory/
|
||||||
|
|
||||||
RUN rm -rf /root/.ssh
|
RUN rm -rf /root/.ssh
|
||||||
RUN ln -s /shared/ssh /root/.ssh
|
RUN ln -s /shared/ssh /root/.ssh
|
||||||
@@ -30,6 +31,7 @@ RUN git config --global user.name "Deployer"
|
|||||||
RUN cd app && git init && 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 && git add . && git commit -am "Initial version"
|
RUN cd app_with_roles && git init && git add . && git commit -am "Initial version"
|
||||||
RUN cd app_with_traefik && git init && git add . && git commit -am "Initial version"
|
RUN cd app_with_traefik && git init && git add . && git commit -am "Initial version"
|
||||||
|
RUN cd app_with_proxied_accessory && git init && git add . && git commit -am "Initial version"
|
||||||
|
|
||||||
HEALTHCHECK --interval=1s CMD pgrep sleep
|
HEALTHCHECK --interval=1s CMD pgrep sleep
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
FROM registry:4443/nginx:1-alpine-slim
|
||||||
|
|
||||||
|
COPY default.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
ARG COMMIT_SHA
|
||||||
|
RUN echo $COMMIT_SHA > /usr/share/nginx/html/version
|
||||||
|
RUN mkdir -p /usr/share/nginx/html/versions && echo "version" > /usr/share/nginx/html/versions/$COMMIT_SHA
|
||||||
|
RUN mkdir -p /usr/share/nginx/html/versions && echo "hidden" > /usr/share/nginx/html/versions/.hidden
|
||||||
|
RUN echo "Up!" > /usr/share/nginx/html/up
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
service: app_with_proxied_accessory
|
||||||
|
image: app_with_proxied_accessory
|
||||||
|
servers:
|
||||||
|
- vm1
|
||||||
|
env:
|
||||||
|
clear:
|
||||||
|
CLEAR_TOKEN: 4321
|
||||||
|
CLEAR_TAG: ""
|
||||||
|
HOST_TOKEN: "${HOST_TOKEN}"
|
||||||
|
asset_path: /usr/share/nginx/html/versions
|
||||||
|
proxy:
|
||||||
|
host: 127.0.0.1
|
||||||
|
registry:
|
||||||
|
server: registry:4443
|
||||||
|
username: root
|
||||||
|
password: root
|
||||||
|
builder:
|
||||||
|
driver: docker
|
||||||
|
arch: <%= Kamal::Utils.docker_arch %>
|
||||||
|
args:
|
||||||
|
COMMIT_SHA: <%= `git rev-parse HEAD` %>
|
||||||
|
accessories:
|
||||||
|
busybox:
|
||||||
|
service: custom-busybox
|
||||||
|
image: registry:4443/busybox:1.36.0
|
||||||
|
cmd: sh -c 'echo "Starting busybox..."; trap exit term; while true; do sleep 1; done'
|
||||||
|
roles:
|
||||||
|
- web
|
||||||
|
netcat:
|
||||||
|
service: netcat
|
||||||
|
image: registry:4443/busybox:1.36.0
|
||||||
|
cmd: >
|
||||||
|
sh -c 'echo "Starting netcat..."; while true; do echo -e "HTTP/1.1 200 OK\r\nContent-Length: 11\r\n\r\nHello Ruby" | nc -l -p 80; done'
|
||||||
|
roles:
|
||||||
|
- web
|
||||||
|
port: 12345:80
|
||||||
|
proxy:
|
||||||
|
host: netcat
|
||||||
|
ssl: false
|
||||||
|
healthcheck:
|
||||||
|
interval: 1
|
||||||
|
timeout: 1
|
||||||
|
path: "/"
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html index.htm;
|
||||||
|
}
|
||||||
|
|
||||||
|
# redirect server error pages to the static page /50x.html
|
||||||
|
#
|
||||||
|
error_page 500 502 503 504 /50x.html;
|
||||||
|
location = /50x.html {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
}
|
||||||
|
}
|
||||||
63
test/integration/proxied_accessory_test.rb
Normal file
63
test/integration/proxied_accessory_test.rb
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
require_relative "integration_test"
|
||||||
|
|
||||||
|
class ProxiedAccessoryTest < IntegrationTest
|
||||||
|
test "boot, stop, start, restart, logs, remove" do
|
||||||
|
@app = "app_with_proxied_accessory"
|
||||||
|
|
||||||
|
kamal :deploy
|
||||||
|
|
||||||
|
kamal :accessory, :boot, :netcat
|
||||||
|
assert_accessory_running :netcat
|
||||||
|
assert_netcat_is_up
|
||||||
|
|
||||||
|
kamal :accessory, :stop, :netcat
|
||||||
|
assert_accessory_not_running :netcat
|
||||||
|
assert_netcat_not_found
|
||||||
|
|
||||||
|
kamal :accessory, :start, :netcat
|
||||||
|
assert_accessory_running :netcat
|
||||||
|
assert_netcat_is_up
|
||||||
|
|
||||||
|
kamal :accessory, :restart, :netcat
|
||||||
|
assert_accessory_running :netcat
|
||||||
|
assert_netcat_is_up
|
||||||
|
|
||||||
|
kamal :accessory, :remove, :netcat, "-y"
|
||||||
|
assert_accessory_not_running :netcat
|
||||||
|
assert_netcat_not_found
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def assert_accessory_running(name)
|
||||||
|
assert_match /registry:4443\/busybox:1.36.0 "sh -c 'echo \\"Start/, accessory_details(name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def assert_accessory_not_running(name)
|
||||||
|
assert_no_match /registry:4443\/busybox:1.36.0 "sh -c 'echo \\"Start/, accessory_details(name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def accessory_details(name)
|
||||||
|
kamal :accessory, :details, name, capture: true
|
||||||
|
end
|
||||||
|
|
||||||
|
def assert_netcat_is_up
|
||||||
|
response = netcat_response
|
||||||
|
debug_response_code(response, "200")
|
||||||
|
assert_equal "200", response.code
|
||||||
|
end
|
||||||
|
|
||||||
|
def assert_netcat_not_found
|
||||||
|
response = netcat_response
|
||||||
|
debug_response_code(response, "404")
|
||||||
|
assert_equal "404", response.code
|
||||||
|
end
|
||||||
|
|
||||||
|
def netcat_response
|
||||||
|
uri = URI.parse("http://127.0.0.1:12345/up")
|
||||||
|
http = Net::HTTP.new(uri.host, uri.port)
|
||||||
|
request = Net::HTTP::Get.new(uri)
|
||||||
|
request["Host"] = "netcat"
|
||||||
|
|
||||||
|
http.request(request)
|
||||||
|
end
|
||||||
|
end
|
||||||
169
test/secrets/aws_secrets_manager_adapter_test.rb
Normal file
169
test/secrets/aws_secrets_manager_adapter_test.rb
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class AwsSecretsManagerAdapterTest < SecretAdapterTestCase
|
||||||
|
test "fails when errors are present" do
|
||||||
|
stub_ticks.with("aws --version 2> /dev/null")
|
||||||
|
stub_ticks
|
||||||
|
.with("aws secretsmanager batch-get-secret-value --secret-id-list unknown1 unknown2 --profile default")
|
||||||
|
.returns(<<~JSON)
|
||||||
|
{
|
||||||
|
"SecretValues": [],
|
||||||
|
"Errors": [
|
||||||
|
{
|
||||||
|
"SecretId": "unknown1",
|
||||||
|
"ErrorCode": "ResourceNotFoundException",
|
||||||
|
"Message": "Secrets Manager can't find the specified secret."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"SecretId": "unknown2",
|
||||||
|
"ErrorCode": "ResourceNotFoundException",
|
||||||
|
"Message": "Secrets Manager can't find the specified secret."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
JSON
|
||||||
|
|
||||||
|
error = assert_raises RuntimeError do
|
||||||
|
JSON.parse(shellunescape(run_command("fetch", "unknown1", "unknown2")))
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_equal [ "unknown1: Secrets Manager can't find the specified secret.", "unknown2: Secrets Manager can't find the specified secret." ].join(" "), error.message
|
||||||
|
end
|
||||||
|
|
||||||
|
test "fetch" do
|
||||||
|
stub_ticks.with("aws --version 2> /dev/null")
|
||||||
|
stub_ticks
|
||||||
|
.with("aws secretsmanager batch-get-secret-value --secret-id-list secret/KEY1 secret/KEY2 secret2/KEY3 --profile default")
|
||||||
|
.returns(<<~JSON)
|
||||||
|
{
|
||||||
|
"SecretValues": [
|
||||||
|
{
|
||||||
|
"ARN": "arn:aws:secretsmanager:us-east-1:aaaaaaaaaaaa:secret:secret",
|
||||||
|
"Name": "secret",
|
||||||
|
"VersionId": "vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv",
|
||||||
|
"SecretString": "{\\"KEY1\\":\\"VALUE1\\", \\"KEY2\\":\\"VALUE2\\"}",
|
||||||
|
"VersionStages": [
|
||||||
|
"AWSCURRENT"
|
||||||
|
],
|
||||||
|
"CreatedDate": "2024-01-01T00:00:00.000000"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ARN": "arn:aws:secretsmanager:us-east-1:aaaaaaaaaaaa:secret:secret2",
|
||||||
|
"Name": "secret2",
|
||||||
|
"VersionId": "vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv",
|
||||||
|
"SecretString": "{\\"KEY3\\":\\"VALUE3\\"}",
|
||||||
|
"VersionStages": [
|
||||||
|
"AWSCURRENT"
|
||||||
|
],
|
||||||
|
"CreatedDate": "2024-01-01T00:00:00.000000"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Errors": []
|
||||||
|
}
|
||||||
|
JSON
|
||||||
|
|
||||||
|
json = JSON.parse(shellunescape(run_command("fetch", "secret/KEY1", "secret/KEY2", "secret2/KEY3")))
|
||||||
|
|
||||||
|
expected_json = {
|
||||||
|
"secret/KEY1"=>"VALUE1",
|
||||||
|
"secret/KEY2"=>"VALUE2",
|
||||||
|
"secret2/KEY3"=>"VALUE3"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_equal expected_json, json
|
||||||
|
end
|
||||||
|
|
||||||
|
test "fetch with string value" do
|
||||||
|
stub_ticks.with("aws --version 2> /dev/null")
|
||||||
|
stub_ticks
|
||||||
|
.with("aws secretsmanager batch-get-secret-value --secret-id-list secret secret2/KEY1 --profile default")
|
||||||
|
.returns(<<~JSON)
|
||||||
|
{
|
||||||
|
"SecretValues": [
|
||||||
|
{
|
||||||
|
"ARN": "arn:aws:secretsmanager:us-east-1:aaaaaaaaaaaa:secret:secret",
|
||||||
|
"Name": "secret",
|
||||||
|
"VersionId": "vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv",
|
||||||
|
"SecretString": "a-string-secret",
|
||||||
|
"VersionStages": [
|
||||||
|
"AWSCURRENT"
|
||||||
|
],
|
||||||
|
"CreatedDate": "2024-01-01T00:00:00.000000"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ARN": "arn:aws:secretsmanager:us-east-1:aaaaaaaaaaaa:secret:secret2",
|
||||||
|
"Name": "secret2",
|
||||||
|
"VersionId": "vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv",
|
||||||
|
"SecretString": "{\\"KEY2\\":\\"VALUE2\\"}",
|
||||||
|
"VersionStages": [
|
||||||
|
"AWSCURRENT"
|
||||||
|
],
|
||||||
|
"CreatedDate": "2024-01-01T00:00:00.000000"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Errors": []
|
||||||
|
}
|
||||||
|
JSON
|
||||||
|
|
||||||
|
json = JSON.parse(shellunescape(run_command("fetch", "secret", "secret2/KEY1")))
|
||||||
|
|
||||||
|
expected_json = {
|
||||||
|
"secret"=>"a-string-secret",
|
||||||
|
"secret2/KEY2"=>"VALUE2"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_equal expected_json, json
|
||||||
|
end
|
||||||
|
|
||||||
|
test "fetch with secret names" do
|
||||||
|
stub_ticks.with("aws --version 2> /dev/null")
|
||||||
|
stub_ticks
|
||||||
|
.with("aws secretsmanager batch-get-secret-value --secret-id-list secret/KEY1 secret/KEY2 --profile default")
|
||||||
|
.returns(<<~JSON)
|
||||||
|
{
|
||||||
|
"SecretValues": [
|
||||||
|
{
|
||||||
|
"ARN": "arn:aws:secretsmanager:us-east-1:aaaaaaaaaaaa:secret:secret",
|
||||||
|
"Name": "secret",
|
||||||
|
"VersionId": "vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv",
|
||||||
|
"SecretString": "{\\"KEY1\\":\\"VALUE1\\", \\"KEY2\\":\\"VALUE2\\"}",
|
||||||
|
"VersionStages": [
|
||||||
|
"AWSCURRENT"
|
||||||
|
],
|
||||||
|
"CreatedDate": "2024-01-01T00:00:00.000000"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Errors": []
|
||||||
|
}
|
||||||
|
JSON
|
||||||
|
|
||||||
|
json = JSON.parse(shellunescape(run_command("fetch", "--from", "secret", "KEY1", "KEY2")))
|
||||||
|
|
||||||
|
expected_json = {
|
||||||
|
"secret/KEY1"=>"VALUE1",
|
||||||
|
"secret/KEY2"=>"VALUE2"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_equal expected_json, json
|
||||||
|
end
|
||||||
|
|
||||||
|
test "fetch without CLI installed" do
|
||||||
|
stub_ticks_with("aws --version 2> /dev/null", succeed: false)
|
||||||
|
|
||||||
|
error = assert_raises RuntimeError do
|
||||||
|
JSON.parse(shellunescape(run_command("fetch", "SECRET1")))
|
||||||
|
end
|
||||||
|
assert_equal "AWS CLI is not installed", error.message
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def run_command(*command)
|
||||||
|
stdouted do
|
||||||
|
Kamal::Cli::Secrets.start \
|
||||||
|
[ *command,
|
||||||
|
"-c", "test/fixtures/deploy_with_accessories.yml",
|
||||||
|
"--adapter", "aws_secrets_manager",
|
||||||
|
"--account", "default" ]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -2,6 +2,8 @@ require "test_helper"
|
|||||||
|
|
||||||
class BitwardenAdapterTest < SecretAdapterTestCase
|
class BitwardenAdapterTest < SecretAdapterTestCase
|
||||||
test "fetch" do
|
test "fetch" do
|
||||||
|
stub_ticks.with("bw --version 2> /dev/null")
|
||||||
|
|
||||||
stub_unlocked
|
stub_unlocked
|
||||||
stub_ticks.with("bw sync").returns("")
|
stub_ticks.with("bw sync").returns("")
|
||||||
stub_mypassword
|
stub_mypassword
|
||||||
@@ -14,6 +16,8 @@ class BitwardenAdapterTest < SecretAdapterTestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "fetch with no login" do
|
test "fetch with no login" do
|
||||||
|
stub_ticks.with("bw --version 2> /dev/null")
|
||||||
|
|
||||||
stub_unlocked
|
stub_unlocked
|
||||||
stub_ticks.with("bw sync").returns("")
|
stub_ticks.with("bw sync").returns("")
|
||||||
stub_noteitem
|
stub_noteitem
|
||||||
@@ -25,6 +29,8 @@ class BitwardenAdapterTest < SecretAdapterTestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "fetch with from" do
|
test "fetch with from" do
|
||||||
|
stub_ticks.with("bw --version 2> /dev/null")
|
||||||
|
|
||||||
stub_unlocked
|
stub_unlocked
|
||||||
stub_ticks.with("bw sync").returns("")
|
stub_ticks.with("bw sync").returns("")
|
||||||
stub_myitem
|
stub_myitem
|
||||||
@@ -38,7 +44,26 @@ class BitwardenAdapterTest < SecretAdapterTestCase
|
|||||||
assert_equal expected_json, json
|
assert_equal expected_json, json
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "fetch all with from" do
|
||||||
|
stub_ticks.with("bw --version 2> /dev/null")
|
||||||
|
|
||||||
|
stub_unlocked
|
||||||
|
stub_ticks.with("bw sync").returns("")
|
||||||
|
stub_noteitem_with_fields
|
||||||
|
|
||||||
|
json = JSON.parse(shellunescape(run_command("fetch", "mynotefields")))
|
||||||
|
|
||||||
|
expected_json = {
|
||||||
|
"mynotefields/field1"=>"secret1", "mynotefields/field2"=>"blam", "mynotefields/field3"=>"fewgrwjgk",
|
||||||
|
"mynotefields/field4"=>"auto"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_equal expected_json, json
|
||||||
|
end
|
||||||
|
|
||||||
test "fetch with multiple items" do
|
test "fetch with multiple items" do
|
||||||
|
stub_ticks.with("bw --version 2> /dev/null")
|
||||||
|
|
||||||
stub_unlocked
|
stub_unlocked
|
||||||
|
|
||||||
stub_ticks.with("bw sync").returns("")
|
stub_ticks.with("bw sync").returns("")
|
||||||
@@ -80,6 +105,8 @@ class BitwardenAdapterTest < SecretAdapterTestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "fetch unauthenticated" do
|
test "fetch unauthenticated" do
|
||||||
|
stub_ticks.with("bw --version 2> /dev/null")
|
||||||
|
|
||||||
stub_ticks
|
stub_ticks
|
||||||
.with("bw status")
|
.with("bw status")
|
||||||
.returns(
|
.returns(
|
||||||
@@ -101,6 +128,8 @@ class BitwardenAdapterTest < SecretAdapterTestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "fetch locked" do
|
test "fetch locked" do
|
||||||
|
stub_ticks.with("bw --version 2> /dev/null")
|
||||||
|
|
||||||
stub_ticks
|
stub_ticks
|
||||||
.with("bw status")
|
.with("bw status")
|
||||||
.returns(
|
.returns(
|
||||||
@@ -126,6 +155,8 @@ class BitwardenAdapterTest < SecretAdapterTestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "fetch locked with session" do
|
test "fetch locked with session" do
|
||||||
|
stub_ticks.with("bw --version 2> /dev/null")
|
||||||
|
|
||||||
stub_ticks
|
stub_ticks
|
||||||
.with("bw status")
|
.with("bw status")
|
||||||
.returns(
|
.returns(
|
||||||
@@ -150,6 +181,15 @@ class BitwardenAdapterTest < SecretAdapterTestCase
|
|||||||
assert_equal expected_json, json
|
assert_equal expected_json, json
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "fetch without CLI installed" do
|
||||||
|
stub_ticks_with("bw --version 2> /dev/null", succeed: false)
|
||||||
|
|
||||||
|
error = assert_raises RuntimeError do
|
||||||
|
JSON.parse(shellunescape(run_command("fetch", "mynote")))
|
||||||
|
end
|
||||||
|
assert_equal "Bitwarden CLI is not installed", error.message
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def run_command(*command)
|
def run_command(*command)
|
||||||
stdouted do
|
stdouted do
|
||||||
@@ -216,6 +256,36 @@ class BitwardenAdapterTest < SecretAdapterTestCase
|
|||||||
JSON
|
JSON
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def stub_noteitem_with_fields(session: nil)
|
||||||
|
stub_ticks
|
||||||
|
.with("#{"BW_SESSION=#{session} " if session}bw get item mynotefields")
|
||||||
|
.returns(<<~JSON)
|
||||||
|
{
|
||||||
|
"passwordHistory":null,
|
||||||
|
"revisionDate":"2024-09-28T09:07:27.461Z",
|
||||||
|
"creationDate":"2024-09-28T09:07:00.740Z",
|
||||||
|
"deletedDate":null,
|
||||||
|
"object":"item",
|
||||||
|
"id":"aaaaaaaa-cccc-eeee-0000-222222222222",
|
||||||
|
"organizationId":null,
|
||||||
|
"folderId":null,
|
||||||
|
"type":2,
|
||||||
|
"reprompt":0,
|
||||||
|
"name":"noteitem",
|
||||||
|
"notes":"NOTES",
|
||||||
|
"favorite":false,
|
||||||
|
"fields":[
|
||||||
|
{"name":"field1","value":"secret1","type":1,"linkedId":null},
|
||||||
|
{"name":"field2","value":"blam","type":1,"linkedId":null},
|
||||||
|
{"name":"field3","value":"fewgrwjgk","type":1,"linkedId":null},
|
||||||
|
{"name":"field4","value":"auto","type":1,"linkedId":null}
|
||||||
|
],
|
||||||
|
"secureNote":{"type":0},
|
||||||
|
"collectionIds":[]
|
||||||
|
}
|
||||||
|
JSON
|
||||||
|
end
|
||||||
|
|
||||||
def stub_myitem
|
def stub_myitem
|
||||||
stub_ticks
|
stub_ticks
|
||||||
.with("bw get item myitem")
|
.with("bw get item myitem")
|
||||||
@@ -237,7 +307,8 @@ class BitwardenAdapterTest < SecretAdapterTestCase
|
|||||||
"fields":[
|
"fields":[
|
||||||
{"name":"field1","value":"secret1","type":1,"linkedId":null},
|
{"name":"field1","value":"secret1","type":1,"linkedId":null},
|
||||||
{"name":"field2","value":"blam","type":1,"linkedId":null},
|
{"name":"field2","value":"blam","type":1,"linkedId":null},
|
||||||
{"name":"field3","value":"fewgrwjgk","type":1,"linkedId":null}
|
{"name":"field3","value":"fewgrwjgk","type":1,"linkedId":null},
|
||||||
|
{"name":"field4","value":"auto","type":1,"linkedId":null}
|
||||||
],
|
],
|
||||||
"login":{"fido2Credentials":[],"uris":[],"username":null,"password":null,"totp":null,"passwordRevisionDate":null},"collectionIds":[]
|
"login":{"fido2Credentials":[],"uris":[],"username":null,"password":null,"totp":null,"passwordRevisionDate":null},"collectionIds":[]
|
||||||
}
|
}
|
||||||
|
|||||||
186
test/secrets/doppler_adapter_test.rb
Normal file
186
test/secrets/doppler_adapter_test.rb
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class DopplerAdapterTest < SecretAdapterTestCase
|
||||||
|
setup do
|
||||||
|
`true` # Ensure $? is 0
|
||||||
|
end
|
||||||
|
|
||||||
|
test "fetch" do
|
||||||
|
stub_ticks_with("doppler --version 2> /dev/null", succeed: true)
|
||||||
|
stub_ticks.with("doppler me --json 2> /dev/null")
|
||||||
|
|
||||||
|
stub_ticks
|
||||||
|
.with("doppler secrets get SECRET1 FSECRET1 FSECRET2 --json -p my-project -c prd")
|
||||||
|
.returns(<<~JSON)
|
||||||
|
{
|
||||||
|
"SECRET1": {
|
||||||
|
"computed":"secret1",
|
||||||
|
"computedVisibility":"unmasked",
|
||||||
|
"note":""
|
||||||
|
},
|
||||||
|
"FSECRET1": {
|
||||||
|
"computed":"fsecret1",
|
||||||
|
"computedVisibility":"unmasked",
|
||||||
|
"note":""
|
||||||
|
},
|
||||||
|
"FSECRET2": {
|
||||||
|
"computed":"fsecret2",
|
||||||
|
"computedVisibility":"unmasked",
|
||||||
|
"note":""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
JSON
|
||||||
|
|
||||||
|
json = JSON.parse(
|
||||||
|
shellunescape run_command("fetch", "--from", "my-project/prd", "SECRET1", "FSECRET1", "FSECRET2")
|
||||||
|
)
|
||||||
|
|
||||||
|
expected_json = {
|
||||||
|
"SECRET1"=>"secret1",
|
||||||
|
"FSECRET1"=>"fsecret1",
|
||||||
|
"FSECRET2"=>"fsecret2"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_equal expected_json, json
|
||||||
|
end
|
||||||
|
|
||||||
|
test "fetch having DOPPLER_TOKEN" do
|
||||||
|
ENV["DOPPLER_TOKEN"] = "dp.st.xxxxxxxxxxxxxxxxxxxxxx"
|
||||||
|
|
||||||
|
stub_ticks_with("doppler --version 2> /dev/null", succeed: true)
|
||||||
|
stub_ticks.with("doppler me --json 2> /dev/null")
|
||||||
|
|
||||||
|
stub_ticks
|
||||||
|
.with("doppler secrets get SECRET1 FSECRET1 FSECRET2 --json ")
|
||||||
|
.returns(<<~JSON)
|
||||||
|
{
|
||||||
|
"SECRET1": {
|
||||||
|
"computed":"secret1",
|
||||||
|
"computedVisibility":"unmasked",
|
||||||
|
"note":""
|
||||||
|
},
|
||||||
|
"FSECRET1": {
|
||||||
|
"computed":"fsecret1",
|
||||||
|
"computedVisibility":"unmasked",
|
||||||
|
"note":""
|
||||||
|
},
|
||||||
|
"FSECRET2": {
|
||||||
|
"computed":"fsecret2",
|
||||||
|
"computedVisibility":"unmasked",
|
||||||
|
"note":""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
JSON
|
||||||
|
|
||||||
|
json = JSON.parse(
|
||||||
|
shellunescape run_command("fetch", "SECRET1", "FSECRET1", "FSECRET2")
|
||||||
|
)
|
||||||
|
|
||||||
|
expected_json = {
|
||||||
|
"SECRET1"=>"secret1",
|
||||||
|
"FSECRET1"=>"fsecret1",
|
||||||
|
"FSECRET2"=>"fsecret2"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_equal expected_json, json
|
||||||
|
|
||||||
|
ENV.delete("DOPPLER_TOKEN")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "fetch with folder in secret" do
|
||||||
|
stub_ticks_with("doppler --version 2> /dev/null", succeed: true)
|
||||||
|
stub_ticks.with("doppler me --json 2> /dev/null")
|
||||||
|
|
||||||
|
stub_ticks
|
||||||
|
.with("doppler secrets get SECRET1 FSECRET1 FSECRET2 --json -p my-project -c prd")
|
||||||
|
.returns(<<~JSON)
|
||||||
|
{
|
||||||
|
"SECRET1": {
|
||||||
|
"computed":"secret1",
|
||||||
|
"computedVisibility":"unmasked",
|
||||||
|
"note":""
|
||||||
|
},
|
||||||
|
"FSECRET1": {
|
||||||
|
"computed":"fsecret1",
|
||||||
|
"computedVisibility":"unmasked",
|
||||||
|
"note":""
|
||||||
|
},
|
||||||
|
"FSECRET2": {
|
||||||
|
"computed":"fsecret2",
|
||||||
|
"computedVisibility":"unmasked",
|
||||||
|
"note":""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
JSON
|
||||||
|
|
||||||
|
json = JSON.parse(
|
||||||
|
shellunescape run_command("fetch", "my-project/prd/SECRET1", "my-project/prd/FSECRET1", "my-project/prd/FSECRET2")
|
||||||
|
)
|
||||||
|
|
||||||
|
expected_json = {
|
||||||
|
"SECRET1"=>"secret1",
|
||||||
|
"FSECRET1"=>"fsecret1",
|
||||||
|
"FSECRET2"=>"fsecret2"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_equal expected_json, json
|
||||||
|
end
|
||||||
|
|
||||||
|
test "fetch without --from" do
|
||||||
|
stub_ticks_with("doppler --version 2> /dev/null", succeed: true)
|
||||||
|
stub_ticks.with("doppler me --json 2> /dev/null")
|
||||||
|
|
||||||
|
error = assert_raises RuntimeError do
|
||||||
|
run_command("fetch", "FSECRET1", "FSECRET2")
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_equal "Missing project or config from '--from=project/config' option", error.message
|
||||||
|
end
|
||||||
|
|
||||||
|
test "fetch with signin" do
|
||||||
|
stub_ticks_with("doppler --version 2> /dev/null", succeed: true)
|
||||||
|
stub_ticks_with("doppler me --json 2> /dev/null", succeed: false)
|
||||||
|
stub_ticks_with("doppler login -y", succeed: true).returns("")
|
||||||
|
stub_ticks.with("doppler secrets get SECRET1 --json -p my-project -c prd").returns(single_item_json)
|
||||||
|
|
||||||
|
json = JSON.parse(shellunescape(run_command("fetch", "--from", "my-project/prd", "SECRET1")))
|
||||||
|
|
||||||
|
expected_json = {
|
||||||
|
"SECRET1"=>"secret1"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_equal expected_json, json
|
||||||
|
end
|
||||||
|
|
||||||
|
test "fetch without CLI installed" do
|
||||||
|
stub_ticks_with("doppler --version 2> /dev/null", succeed: false)
|
||||||
|
|
||||||
|
error = assert_raises RuntimeError do
|
||||||
|
JSON.parse(shellunescape(run_command("fetch", "HOST", "PORT")))
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_equal "Doppler CLI is not installed", error.message
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def run_command(*command)
|
||||||
|
stdouted do
|
||||||
|
Kamal::Cli::Secrets.start \
|
||||||
|
[ *command,
|
||||||
|
"-c", "test/fixtures/deploy_with_accessories.yml",
|
||||||
|
"--adapter", "doppler" ]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def single_item_json
|
||||||
|
<<~JSON
|
||||||
|
{
|
||||||
|
"SECRET1": {
|
||||||
|
"computed":"secret1",
|
||||||
|
"computedVisibility":"unmasked",
|
||||||
|
"note":""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
JSON
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -6,6 +6,7 @@ class LastPassAdapterTest < SecretAdapterTestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "fetch" do
|
test "fetch" do
|
||||||
|
stub_ticks.with("lpass --version 2> /dev/null")
|
||||||
stub_ticks.with("lpass status --color never").returns("Logged in as email@example.com.")
|
stub_ticks.with("lpass status --color never").returns("Logged in as email@example.com.")
|
||||||
|
|
||||||
stub_ticks
|
stub_ticks
|
||||||
@@ -63,6 +64,7 @@ class LastPassAdapterTest < SecretAdapterTestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "fetch with from" do
|
test "fetch with from" do
|
||||||
|
stub_ticks.with("lpass --version 2> /dev/null")
|
||||||
stub_ticks.with("lpass status --color never").returns("Logged in as email@example.com.")
|
stub_ticks.with("lpass status --color never").returns("Logged in as email@example.com.")
|
||||||
|
|
||||||
stub_ticks
|
stub_ticks
|
||||||
@@ -107,6 +109,8 @@ class LastPassAdapterTest < SecretAdapterTestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "fetch with signin" do
|
test "fetch with signin" do
|
||||||
|
stub_ticks.with("lpass --version 2> /dev/null")
|
||||||
|
|
||||||
stub_ticks_with("lpass status --color never", succeed: false).returns("Not logged in.")
|
stub_ticks_with("lpass status --color never", succeed: false).returns("Not logged in.")
|
||||||
stub_ticks_with("lpass login email@example.com", succeed: true).returns("")
|
stub_ticks_with("lpass login email@example.com", succeed: true).returns("")
|
||||||
stub_ticks.with("lpass show SECRET1 --json").returns(single_item_json)
|
stub_ticks.with("lpass show SECRET1 --json").returns(single_item_json)
|
||||||
@@ -120,6 +124,15 @@ class LastPassAdapterTest < SecretAdapterTestCase
|
|||||||
assert_equal expected_json, json
|
assert_equal expected_json, json
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "fetch without CLI installed" do
|
||||||
|
stub_ticks_with("lpass --version 2> /dev/null", succeed: false)
|
||||||
|
|
||||||
|
error = assert_raises RuntimeError do
|
||||||
|
JSON.parse(shellunescape(run_command("fetch", "SECRET1", "FOLDER1/FSECRET1", "FOLDER1/FSECRET2")))
|
||||||
|
end
|
||||||
|
assert_equal "LastPass CLI is not installed", error.message
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def run_command(*command)
|
def run_command(*command)
|
||||||
stdouted do
|
stdouted do
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ require "test_helper"
|
|||||||
|
|
||||||
class SecretsOnePasswordAdapterTest < SecretAdapterTestCase
|
class SecretsOnePasswordAdapterTest < SecretAdapterTestCase
|
||||||
test "fetch" do
|
test "fetch" do
|
||||||
|
stub_ticks.with("op --version 2> /dev/null")
|
||||||
stub_ticks.with("op account get --account myaccount 2> /dev/null")
|
stub_ticks.with("op account get --account myaccount 2> /dev/null")
|
||||||
|
|
||||||
stub_ticks
|
stub_ticks
|
||||||
@@ -56,6 +57,7 @@ class SecretsOnePasswordAdapterTest < SecretAdapterTestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "fetch with multiple items" do
|
test "fetch with multiple items" do
|
||||||
|
stub_ticks.with("op --version 2> /dev/null")
|
||||||
stub_ticks.with("op account get --account myaccount 2> /dev/null")
|
stub_ticks.with("op account get --account myaccount 2> /dev/null")
|
||||||
|
|
||||||
stub_ticks
|
stub_ticks
|
||||||
@@ -115,6 +117,8 @@ class SecretsOnePasswordAdapterTest < SecretAdapterTestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "fetch with signin, no session" do
|
test "fetch with signin, no session" do
|
||||||
|
stub_ticks.with("op --version 2> /dev/null")
|
||||||
|
|
||||||
stub_ticks_with("op account get --account myaccount 2> /dev/null", succeed: false)
|
stub_ticks_with("op account get --account myaccount 2> /dev/null", succeed: false)
|
||||||
stub_ticks_with("op signin --account \"myaccount\" --force --raw", succeed: true).returns("")
|
stub_ticks_with("op signin --account \"myaccount\" --force --raw", succeed: true).returns("")
|
||||||
|
|
||||||
@@ -132,6 +136,8 @@ class SecretsOnePasswordAdapterTest < SecretAdapterTestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "fetch with signin and session" do
|
test "fetch with signin and session" do
|
||||||
|
stub_ticks.with("op --version 2> /dev/null")
|
||||||
|
|
||||||
stub_ticks_with("op account get --account myaccount 2> /dev/null", succeed: false)
|
stub_ticks_with("op account get --account myaccount 2> /dev/null", succeed: false)
|
||||||
stub_ticks_with("op signin --account \"myaccount\" --force --raw", succeed: true).returns("1234567890")
|
stub_ticks_with("op signin --account \"myaccount\" --force --raw", succeed: true).returns("1234567890")
|
||||||
|
|
||||||
@@ -148,6 +154,15 @@ class SecretsOnePasswordAdapterTest < SecretAdapterTestCase
|
|||||||
assert_equal expected_json, json
|
assert_equal expected_json, json
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "fetch without CLI installed" do
|
||||||
|
stub_ticks_with("op --version 2> /dev/null", succeed: false)
|
||||||
|
|
||||||
|
error = assert_raises RuntimeError do
|
||||||
|
JSON.parse(shellunescape(run_command("fetch", "--from", "op://myvault/myitem", "section/SECRET1", "section/SECRET2", "section2/SECRET3")))
|
||||||
|
end
|
||||||
|
assert_equal "1Password CLI is not installed", error.message
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def run_command(*command)
|
def run_command(*command)
|
||||||
stdouted do
|
stdouted do
|
||||||
|
|||||||
@@ -31,4 +31,18 @@ class SecretsTest < ActiveSupport::TestCase
|
|||||||
assert_equal "JKL", Kamal::Secrets.new(destination: "nodest")["SECRET2"]
|
assert_equal "JKL", Kamal::Secrets.new(destination: "nodest")["SECRET2"]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "no secrets files" do
|
||||||
|
with_test_secrets do
|
||||||
|
error = assert_raises(Kamal::ConfigurationError) do
|
||||||
|
Kamal::Secrets.new["SECRET"]
|
||||||
|
end
|
||||||
|
assert_equal "Secret 'SECRET' not found, no secret files (.kamal/secrets-common, .kamal/secrets) provided", error.message
|
||||||
|
|
||||||
|
error = assert_raises(Kamal::ConfigurationError) do
|
||||||
|
Kamal::Secrets.new(destination: "dest")["SECRET"]
|
||||||
|
end
|
||||||
|
assert_equal "Secret 'SECRET' not found, no secret files (.kamal/secrets-common, .kamal/secrets.dest) provided", error.message
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ require "bundler/setup"
|
|||||||
require "active_support/test_case"
|
require "active_support/test_case"
|
||||||
require "active_support/testing/autorun"
|
require "active_support/testing/autorun"
|
||||||
require "active_support/testing/stream"
|
require "active_support/testing/stream"
|
||||||
|
require "rails/test_unit/line_filtering"
|
||||||
require "debug"
|
require "debug"
|
||||||
require "mocha/minitest" # using #stubs that can alter returns
|
require "mocha/minitest" # using #stubs that can alter returns
|
||||||
require "minitest/autorun" # using #stub that take args
|
require "minitest/autorun" # using #stub that take args
|
||||||
@@ -32,6 +33,7 @@ end
|
|||||||
|
|
||||||
class ActiveSupport::TestCase
|
class ActiveSupport::TestCase
|
||||||
include ActiveSupport::Testing::Stream
|
include ActiveSupport::Testing::Stream
|
||||||
|
extend Rails::LineFiltering
|
||||||
|
|
||||||
private
|
private
|
||||||
def stdouted
|
def stdouted
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ require "test_helper"
|
|||||||
|
|
||||||
class UtilsTest < ActiveSupport::TestCase
|
class UtilsTest < ActiveSupport::TestCase
|
||||||
test "argumentize" do
|
test "argumentize" do
|
||||||
assert_equal [ "--label", "foo=\"\\`bar\\`\"", "--label", "baz=\"qux\"", "--label", :quux ], \
|
assert_equal [ "--label", "foo=\"\\`bar\\`\"", "--label", "baz=\"qux\"", "--label", :quux, "--label", "quuz=false" ], \
|
||||||
Kamal::Utils.argumentize("--label", { foo: "`bar`", baz: "qux", quux: nil })
|
Kamal::Utils.argumentize("--label", { foo: "`bar`", baz: "qux", quux: nil, quuz: false })
|
||||||
end
|
end
|
||||||
|
|
||||||
test "argumentize with redacted" do
|
test "argumentize with redacted" do
|
||||||
|
|||||||
Reference in New Issue
Block a user