Compare commits
365 Commits
v0.16.0
...
kamal-prox
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
015c5a6f90 | ||
|
|
6568cef868 | ||
|
|
90ecb6a12a | ||
|
|
2c2053558a | ||
|
|
10b8c826d8 | ||
|
|
187861fa60 | ||
|
|
5ff1203c80 | ||
|
|
0e73f02743 | ||
|
|
83d0078525 | ||
|
|
96ef0fbc4d | ||
|
|
b12654ccd0 | ||
|
|
64f5955444 | ||
|
|
d2a719998a | ||
|
|
6a7c90cf4d | ||
|
|
2c2d94c6d9 | ||
|
|
c62bd1dc31 | ||
|
|
a83df9e135 | ||
|
|
7b55f4734e | ||
|
|
1e296c4140 | ||
|
|
9700e2b3c4 | ||
|
|
706b82baa1 | ||
|
|
fa7e941648 | ||
|
|
78c0a0ba4b | ||
|
|
060e5d2027 | ||
|
|
8a4f7163bb | ||
|
|
ee758d951a | ||
|
|
bb2ca81d87 | ||
|
|
773ba3a5ab | ||
|
|
5be6fa3b4e | ||
|
|
07c5658396 | ||
|
|
0efb5ccfff | ||
|
|
990f1b4413 | ||
|
|
da9428f64d | ||
|
|
17dcaccb6a | ||
|
|
448349d0e5 | ||
|
|
b6dba57c7d | ||
|
|
0ea2a2c509 | ||
|
|
307750ff70 | ||
|
|
88947b6a7b | ||
|
|
f48c227768 | ||
|
|
f98380ef0c | ||
|
|
0bc27c10cc | ||
|
|
e58d2f67f2 | ||
|
|
938ac375a1 | ||
|
|
dc1f707a56 | ||
|
|
033f2a3401 | ||
|
|
7cac7e6fb0 | ||
|
|
fb58fc0ba6 | ||
|
|
12cad5458a | ||
|
|
f8b7f74543 | ||
|
|
489d6dbcbb | ||
|
|
6d062ce271 | ||
|
|
1e44cc2597 | ||
|
|
63c47eca4c | ||
|
|
3c8428504d | ||
|
|
8e71c48747 | ||
|
|
67a86e1068 | ||
|
|
b67f40bdf7 | ||
|
|
375f0283c4 | ||
|
|
947be0877f | ||
|
|
b8aaddb4c9 | ||
|
|
f48f528043 | ||
|
|
ec0a082542 | ||
|
|
6c638a8a77 | ||
|
|
1f5b936fa2 | ||
|
|
f785451cc7 | ||
|
|
d475e88dbe | ||
|
|
d551f044d6 | ||
|
|
2611179d5e | ||
|
|
1a013b8d4b | ||
|
|
2f912367ac | ||
|
|
9a9a0914cd | ||
|
|
12c518097f | ||
|
|
69f90387a8 | ||
|
|
e6d436f646 | ||
|
|
31669d4dce | ||
|
|
9d20c1466e | ||
|
|
ff1dabe7f8 | ||
|
|
69aa422890 | ||
|
|
f8b0883036 | ||
|
|
c8100d1f26 | ||
|
|
3628ecaa44 | ||
|
|
67a2d5e7ca | ||
|
|
5e492ecc4d | ||
|
|
77bad291a1 | ||
|
|
a0ce9f66c4 | ||
|
|
82962c375d | ||
|
|
8a6a51977f | ||
|
|
2562853ae3 | ||
|
|
ed90b99f0d | ||
|
|
ba7a13f895 | ||
|
|
05ac808f2a | ||
|
|
fb7d9077ff | ||
|
|
bade195e93 | ||
|
|
55dd2f49c1 | ||
|
|
511a182539 | ||
|
|
8bb596e216 | ||
|
|
699bcc0d27 | ||
|
|
6aacd1f9e2 | ||
|
|
20e71d91c0 | ||
|
|
866303a59b | ||
|
|
53bfefeb2f | ||
|
|
f3b7569032 | ||
|
|
e5457cf7b4 | ||
|
|
cee449c269 | ||
|
|
786454f2ee | ||
|
|
827e18480d | ||
|
|
9f9c9ccbde | ||
|
|
981d391d4d | ||
|
|
900041001a | ||
|
|
43672ec9a5 | ||
|
|
5481fbb973 | ||
|
|
49afdbb09a | ||
|
|
5f58575b62 | ||
|
|
cb49d7dada | ||
|
|
3d26fa8ddd | ||
|
|
ea9f8b488d | ||
|
|
83472af32c | ||
|
|
e99e1955b8 | ||
|
|
30e0c44396 | ||
|
|
20d6e5365e | ||
|
|
72ace2bf0b | ||
|
|
ba40d026d0 | ||
|
|
0f13600ba3 | ||
|
|
bbf952952d | ||
|
|
474b76cf47 | ||
|
|
3ecfb3744f | ||
|
|
c985fa33d1 | ||
|
|
e8b9f8907f | ||
|
|
4966d52919 | ||
|
|
52bb40add0 | ||
|
|
73a9276cdd | ||
|
|
8c0784ed4a | ||
|
|
089a2d3bba | ||
|
|
bd76d23916 | ||
|
|
fa37fcd10c | ||
|
|
f5dc0858b0 | ||
|
|
9dddb140b1 | ||
|
|
26b1d57c90 | ||
|
|
b94199415f | ||
|
|
f69c45b7ea | ||
|
|
32a2ae5b2c | ||
|
|
37544a6383 | ||
|
|
a1bc6d61af | ||
|
|
5c32be10f1 | ||
|
|
dc5af03593 | ||
|
|
1abd029ea0 | ||
|
|
c4d0d3e5eb | ||
|
|
46e7cf8e78 | ||
|
|
c7cfc074b6 | ||
|
|
c10f43e365 | ||
|
|
8e2184d65e | ||
|
|
2be397b679 | ||
|
|
cc8c508556 | ||
|
|
3b16e047c5 | ||
|
|
6563393d9a | ||
|
|
91f350fcce | ||
|
|
e4e9664049 | ||
|
|
1acef5221f | ||
|
|
788a57e85e | ||
|
|
f9a934a01f | ||
|
|
f286fdc374 | ||
|
|
828cca322b | ||
|
|
cb030e8751 | ||
|
|
6892abb4be | ||
|
|
bcfd0ca88a | ||
|
|
2e8071a5b3 | ||
|
|
200e2686fd | ||
|
|
db94789dc1 | ||
|
|
2bffc3bc74 | ||
|
|
064ace0598 | ||
|
|
a02af74dda | ||
|
|
5ef384d666 | ||
|
|
b94dfe193b | ||
|
|
bc6c027315 | ||
|
|
1c2a45817a | ||
|
|
b411356409 | ||
|
|
77e72e34ce | ||
|
|
ad04bb7556 | ||
|
|
1ec69d3764 | ||
|
|
2d1a0dc9ba | ||
|
|
c984db152f | ||
|
|
aea55480ad | ||
|
|
5a09aa12ba | ||
|
|
aca7796e9d | ||
|
|
8b6d8306d1 | ||
|
|
bb50546467 | ||
|
|
acc6b9ad71 | ||
|
|
9c681d4a38 | ||
|
|
2a8924b53c | ||
|
|
c5ae54d7d4 | ||
|
|
4b05068493 | ||
|
|
68eb549795 | ||
|
|
1a3dd52af4 | ||
|
|
0d709a3fdb | ||
|
|
414d29ae4e | ||
|
|
f8d8319c2f | ||
|
|
f6a9d54902 | ||
|
|
b2fd5744fb | ||
|
|
457f06da13 | ||
|
|
7fa53d90bd | ||
|
|
a155b7baab | ||
|
|
175e3bc159 | ||
|
|
e3d8a2aa82 | ||
|
|
0e067fb5e1 | ||
|
|
63babecba7 | ||
|
|
79baa598fa | ||
|
|
b1dc188841 | ||
|
|
635876bdb9 | ||
|
|
11521517fa | ||
|
|
610d9de3fd | ||
|
|
bf79df0f72 | ||
|
|
a0959b5afd | ||
|
|
7472e5dfa6 | ||
|
|
887b7dd46d | ||
|
|
77a79b299a | ||
|
|
efcb855db7 | ||
|
|
7137850354 | ||
|
|
8a85840a47 | ||
|
|
80cc0c23d8 | ||
|
|
14a9129410 | ||
|
|
60187cc3a4 | ||
|
|
87cb8c1f71 | ||
|
|
ed58ce6e61 | ||
|
|
263b4a4fb8 | ||
|
|
073f745677 | ||
|
|
a9cc7c73d2 | ||
|
|
6898e8789e | ||
|
|
d0ac6507e7 | ||
|
|
628a47ad88 | ||
|
|
47f8725cf3 | ||
|
|
5fd4a28bf7 | ||
|
|
97ba6b746b | ||
|
|
9e25d8a012 | ||
|
|
da161445fa | ||
|
|
f339626667 | ||
|
|
2d86d4f7cc | ||
|
|
792aa1dbdf | ||
|
|
24a2f51641 | ||
|
|
8f53104d00 | ||
|
|
2d22143a24 | ||
|
|
cbd99306eb | ||
|
|
78fc91f2ec | ||
|
|
dd748fac8c | ||
|
|
b732b2dd55 | ||
|
|
e3254b2aa8 | ||
|
|
e9269d2ee8 | ||
|
|
d2214b43b7 | ||
|
|
370481921e | ||
|
|
aa23f26330 | ||
|
|
f4933d83bf | ||
|
|
6c36c82153 | ||
|
|
8ca04032a1 | ||
|
|
2fb22c934b | ||
|
|
f96d071222 | ||
|
|
f6662c7a8f | ||
|
|
645f5ab72d | ||
|
|
8dca65f48f | ||
|
|
83a2d52ff4 | ||
|
|
1a2796a7d0 | ||
|
|
d80fdf8468 | ||
|
|
90fefc419f | ||
|
|
8671963719 | ||
|
|
a03ffd5b92 | ||
|
|
0861730e0e | ||
|
|
6b0f93a564 | ||
|
|
e6371faf4f | ||
|
|
e95a9b4fa2 | ||
|
|
e5886a1a8e | ||
|
|
ec8192b160 | ||
|
|
2da03a220d | ||
|
|
cfbfb37e23 | ||
|
|
ff4d025840 | ||
|
|
59ac59d351 | ||
|
|
3df87520db | ||
|
|
85ce65a4ce | ||
|
|
12a82a6c58 | ||
|
|
b2d2a254d7 | ||
|
|
62cdf31ae2 | ||
|
|
0dcebe7d34 | ||
|
|
32a5c157b9 | ||
|
|
97cea8950d | ||
|
|
873be0b76b | ||
|
|
3a8eb0cf7d | ||
|
|
e9ef13d06d | ||
|
|
f648fe6c3f | ||
|
|
46895d0b08 | ||
|
|
431ca9e809 | ||
|
|
6b5c5f0650 | ||
|
|
d303fcc621 | ||
|
|
3ae855ef28 | ||
|
|
76a3086569 | ||
|
|
07646bc020 | ||
|
|
880b8b267a | ||
|
|
37e5c48a27 | ||
|
|
deb67386fa | ||
|
|
81d74e4a9d | ||
|
|
39c13dcc18 | ||
|
|
e7314a0eea | ||
|
|
168c6e2da3 | ||
|
|
564765862b | ||
|
|
3c12d1799c | ||
|
|
60835d13a8 | ||
|
|
892cf0e66b | ||
|
|
8ddc484ce6 | ||
|
|
0e021e3c57 | ||
|
|
fb0aeec27e | ||
|
|
a367819a1c | ||
|
|
0afe289a20 | ||
|
|
bf6af46ac3 | ||
|
|
df2b76aee1 | ||
|
|
70a3c7195a | ||
|
|
c651de177f | ||
|
|
7b42daa9fb | ||
|
|
9d49b3e391 | ||
|
|
2c5ab054db | ||
|
|
66291a2aea | ||
|
|
d96e086945 | ||
|
|
8424458174 | ||
|
|
6a3b0249fe | ||
|
|
dfc2803714 | ||
|
|
ade90bc051 | ||
|
|
daa53f5831 | ||
|
|
50a4f83db6 | ||
|
|
00cb7d99d8 | ||
|
|
fb74910dc8 | ||
|
|
26dcd75423 | ||
|
|
afb9b0bbe2 | ||
|
|
718776eb72 | ||
|
|
9d35793287 | ||
|
|
0b439362da | ||
|
|
2962f545b9 | ||
|
|
cd02510d0f | ||
|
|
cccf79ed94 | ||
|
|
aa9999809c | ||
|
|
6263bf96ba | ||
|
|
9a539ffc86 | ||
|
|
8a41d15b69 | ||
|
|
94bf090657 | ||
|
|
adc7173cf2 | ||
|
|
fd6bf5324a | ||
|
|
c2b2f7ea33 | ||
|
|
bbcc90e4d1 | ||
|
|
84f78cd9f9 | ||
|
|
787688ea08 | ||
|
|
bcfa1d83e8 | ||
|
|
9363b6a464 | ||
|
|
338fd4e493 | ||
|
|
eb3cb81a79 | ||
|
|
556f7f5a37 | ||
|
|
c2ec04f8c1 | ||
|
|
519659b84c | ||
|
|
560d0698ac | ||
|
|
f40e8e9af1 | ||
|
|
1ab7405e36 | ||
|
|
aeadd7c11f | ||
|
|
d0fbf538d3 | ||
|
|
cfe77934e8 | ||
|
|
3f6ca1648e | ||
|
|
7c6d302baa | ||
|
|
b8eb50b982 | ||
|
|
d981c3c968 | ||
|
|
416860d9b0 | ||
|
|
33d5d7e9a2 | ||
|
|
99c1102a3a |
35
.github/workflows/ci.yml
vendored
35
.github/workflows/ci.yml
vendored
@@ -1,10 +1,25 @@
|
|||||||
name: CI
|
name: CI
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
pull_request:
|
pull_request:
|
||||||
jobs:
|
jobs:
|
||||||
|
rubocop:
|
||||||
|
name: RuboCop
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
BUNDLE_ONLY: rubocop
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Setup Ruby and install gems
|
||||||
|
uses: ruby/setup-ruby@v1
|
||||||
|
with:
|
||||||
|
ruby-version: 3.3.0
|
||||||
|
bundler-cache: true
|
||||||
|
- name: Run Rubocop
|
||||||
|
run: bundle exec rubocop --parallel
|
||||||
tests:
|
tests:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
@@ -12,17 +27,29 @@ jobs:
|
|||||||
- "2.7"
|
- "2.7"
|
||||||
- "3.1"
|
- "3.1"
|
||||||
- "3.2"
|
- "3.2"
|
||||||
|
- "3.3"
|
||||||
gemfile:
|
gemfile:
|
||||||
- Gemfile
|
- Gemfile
|
||||||
|
- gemfiles/ruby_2.7.gemfile
|
||||||
- gemfiles/rails_edge.gemfile
|
- gemfiles/rails_edge.gemfile
|
||||||
continue-on-error: [false]
|
exclude:
|
||||||
|
- ruby-version: "2.7"
|
||||||
|
gemfile: Gemfile
|
||||||
|
- ruby-version: "2.7"
|
||||||
|
gemfile: gemfiles/rails_edge.gemfile
|
||||||
|
- ruby-version: "3.1"
|
||||||
|
gemfile: gemfiles/ruby_2.7.gemfile
|
||||||
|
- ruby-version: "3.2"
|
||||||
|
gemfile: gemfiles/ruby_2.7.gemfile
|
||||||
|
- ruby-version: "3.3"
|
||||||
|
gemfile: gemfiles/ruby_2.7.gemfile
|
||||||
name: ${{ format('Tests (Ruby {0})', matrix.ruby-version) }}
|
name: ${{ format('Tests (Ruby {0})', matrix.ruby-version) }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
continue-on-error: ${{ matrix.continue-on-error }}
|
continue-on-error: true
|
||||||
env:
|
env:
|
||||||
BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }}
|
BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Ruby
|
- name: Install Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
|
|||||||
18
.github/workflows/docker-publish.yml
vendored
18
.github/workflows/docker-publish.yml
vendored
@@ -1,6 +1,12 @@
|
|||||||
name: Docker
|
name: Docker
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
tagInput:
|
||||||
|
description: 'Tag'
|
||||||
|
required: true
|
||||||
|
|
||||||
release:
|
release:
|
||||||
types: [created]
|
types: [created]
|
||||||
tags:
|
tags:
|
||||||
@@ -29,6 +35,14 @@ jobs:
|
|||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Determine version tag
|
||||||
|
id: version-tag
|
||||||
|
run: |
|
||||||
|
INPUT_VALUE="${{ github.event.inputs.tagInput }}"
|
||||||
|
if [ -z "$INPUT_VALUE" ]; then
|
||||||
|
INPUT_VALUE="${{ github.ref_name }}"
|
||||||
|
fi
|
||||||
|
echo "::set-output name=value::$INPUT_VALUE"
|
||||||
-
|
-
|
||||||
name: Build and push
|
name: Build and push
|
||||||
uses: docker/build-push-action@v3
|
uses: docker/build-push-action@v3
|
||||||
@@ -37,5 +51,5 @@ jobs:
|
|||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
ghcr.io/mrsked/mrsk:latest
|
ghcr.io/basecamp/kamal:latest
|
||||||
ghcr.io/mrsked/mrsk:${{ github.ref_name }}
|
ghcr.io/basecamp/kamal:${{ steps.version-tag.outputs.value }}
|
||||||
|
|||||||
2
.rubocop.yml
Normal file
2
.rubocop.yml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
inherit_gem:
|
||||||
|
rubocop-rails-omakase: rubocop.yml
|
||||||
6
Gemfile
6
Gemfile
@@ -1,4 +1,8 @@
|
|||||||
source 'https://rubygems.org'
|
source "https://rubygems.org"
|
||||||
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
|
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
|
||||||
|
|
||||||
gemspec
|
gemspec
|
||||||
|
|
||||||
|
group :rubocop do
|
||||||
|
gem "rubocop-rails-omakase", require: false
|
||||||
|
end
|
||||||
|
|||||||
170
Gemfile.lock
170
Gemfile.lock
@@ -1,96 +1,171 @@
|
|||||||
PATH
|
PATH
|
||||||
remote: .
|
remote: .
|
||||||
specs:
|
specs:
|
||||||
kamal (0.16.0)
|
kamal (1.5.2)
|
||||||
activesupport (>= 7.0)
|
activesupport (>= 7.0)
|
||||||
|
base64 (~> 0.2)
|
||||||
bcrypt_pbkdf (~> 1.0)
|
bcrypt_pbkdf (~> 1.0)
|
||||||
concurrent-ruby (~> 1.2)
|
concurrent-ruby (~> 1.2)
|
||||||
dotenv (~> 2.8)
|
dotenv (~> 2.8)
|
||||||
ed25519 (~> 1.2)
|
ed25519 (~> 1.2)
|
||||||
net-ssh (~> 7.0)
|
net-ssh (~> 7.0)
|
||||||
sshkit (~> 1.21)
|
sshkit (>= 1.22.2, < 2.0)
|
||||||
thor (~> 1.2)
|
thor (~> 1.2)
|
||||||
zeitwerk (~> 2.5)
|
zeitwerk (~> 2.5)
|
||||||
|
|
||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
actionpack (7.0.4.3)
|
actionpack (7.1.2)
|
||||||
actionview (= 7.0.4.3)
|
actionview (= 7.1.2)
|
||||||
activesupport (= 7.0.4.3)
|
activesupport (= 7.1.2)
|
||||||
rack (~> 2.0, >= 2.2.0)
|
nokogiri (>= 1.8.5)
|
||||||
|
racc
|
||||||
|
rack (>= 2.2.4)
|
||||||
|
rack-session (>= 1.0.1)
|
||||||
rack-test (>= 0.6.3)
|
rack-test (>= 0.6.3)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.2)
|
||||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
rails-html-sanitizer (~> 1.6)
|
||||||
actionview (7.0.4.3)
|
actionview (7.1.2)
|
||||||
activesupport (= 7.0.4.3)
|
activesupport (= 7.1.2)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
erubi (~> 1.4)
|
erubi (~> 1.11)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.2)
|
||||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
rails-html-sanitizer (~> 1.6)
|
||||||
activesupport (7.0.4.3)
|
activesupport (7.1.2)
|
||||||
|
base64
|
||||||
|
bigdecimal
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
|
connection_pool (>= 2.2.5)
|
||||||
|
drb
|
||||||
i18n (>= 1.6, < 2)
|
i18n (>= 1.6, < 2)
|
||||||
minitest (>= 5.1)
|
minitest (>= 5.1)
|
||||||
|
mutex_m
|
||||||
tzinfo (~> 2.0)
|
tzinfo (~> 2.0)
|
||||||
|
ast (2.4.2)
|
||||||
|
base64 (0.2.0)
|
||||||
bcrypt_pbkdf (1.1.0)
|
bcrypt_pbkdf (1.1.0)
|
||||||
|
bigdecimal (3.1.5)
|
||||||
builder (3.2.4)
|
builder (3.2.4)
|
||||||
concurrent-ruby (1.2.2)
|
concurrent-ruby (1.2.2)
|
||||||
|
connection_pool (2.4.1)
|
||||||
crass (1.0.6)
|
crass (1.0.6)
|
||||||
debug (1.7.2)
|
debug (1.9.1)
|
||||||
irb (>= 1.5.0)
|
irb (~> 1.10)
|
||||||
reline (>= 0.3.1)
|
reline (>= 0.3.8)
|
||||||
dotenv (2.8.1)
|
dotenv (2.8.1)
|
||||||
|
drb (2.2.0)
|
||||||
|
ruby2_keywords
|
||||||
ed25519 (1.3.0)
|
ed25519 (1.3.0)
|
||||||
erubi (1.12.0)
|
erubi (1.12.0)
|
||||||
i18n (1.12.0)
|
i18n (1.14.1)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
io-console (0.6.0)
|
io-console (0.7.1)
|
||||||
irb (1.6.3)
|
irb (1.11.0)
|
||||||
reline (>= 0.3.0)
|
rdoc
|
||||||
loofah (2.20.0)
|
reline (>= 0.3.8)
|
||||||
|
json (2.7.1)
|
||||||
|
language_server-protocol (3.17.0.3)
|
||||||
|
loofah (2.22.0)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.5.9)
|
nokogiri (>= 1.12.0)
|
||||||
method_source (1.0.0)
|
minitest (5.20.0)
|
||||||
minitest (5.18.0)
|
mocha (2.1.0)
|
||||||
mocha (2.0.2)
|
|
||||||
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-ssh (7.1.0)
|
net-sftp (4.0.0)
|
||||||
nokogiri (1.14.2-arm64-darwin)
|
net-ssh (>= 5.0.0, < 8.0.0)
|
||||||
|
net-ssh (7.2.1)
|
||||||
|
nokogiri (1.16.0-arm64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.14.2-x86_64-darwin)
|
nokogiri (1.16.0-x86_64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.14.2-x86_64-linux)
|
nokogiri (1.16.0-x86_64-linux)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
racc (1.6.2)
|
parallel (1.24.0)
|
||||||
rack (2.2.6.4)
|
parser (3.3.0.5)
|
||||||
|
ast (~> 2.4.1)
|
||||||
|
racc
|
||||||
|
psych (5.1.2)
|
||||||
|
stringio
|
||||||
|
racc (1.7.3)
|
||||||
|
rack (3.0.8)
|
||||||
|
rack-session (2.0.0)
|
||||||
|
rack (>= 3.0.0)
|
||||||
rack-test (2.1.0)
|
rack-test (2.1.0)
|
||||||
rack (>= 1.3)
|
rack (>= 1.3)
|
||||||
rails-dom-testing (2.0.3)
|
rackup (2.1.0)
|
||||||
activesupport (>= 4.2.0)
|
rack (>= 3)
|
||||||
|
webrick (~> 1.8)
|
||||||
|
rails-dom-testing (2.2.0)
|
||||||
|
activesupport (>= 5.0.0)
|
||||||
|
minitest
|
||||||
nokogiri (>= 1.6)
|
nokogiri (>= 1.6)
|
||||||
rails-html-sanitizer (1.5.0)
|
rails-html-sanitizer (1.6.0)
|
||||||
loofah (~> 2.19, >= 2.19.1)
|
loofah (~> 2.21)
|
||||||
railties (7.0.4.3)
|
nokogiri (~> 1.14)
|
||||||
actionpack (= 7.0.4.3)
|
railties (7.1.2)
|
||||||
activesupport (= 7.0.4.3)
|
actionpack (= 7.1.2)
|
||||||
method_source
|
activesupport (= 7.1.2)
|
||||||
|
irb
|
||||||
|
rackup (>= 1.0.0)
|
||||||
rake (>= 12.2)
|
rake (>= 12.2)
|
||||||
thor (~> 1.0)
|
thor (~> 1.0, >= 1.2.2)
|
||||||
zeitwerk (~> 2.5)
|
zeitwerk (~> 2.6)
|
||||||
rake (13.0.6)
|
rainbow (3.1.1)
|
||||||
reline (0.3.3)
|
rake (13.1.0)
|
||||||
|
rdoc (6.6.2)
|
||||||
|
psych (>= 4.0.0)
|
||||||
|
regexp_parser (2.9.0)
|
||||||
|
reline (0.4.2)
|
||||||
io-console (~> 0.5)
|
io-console (~> 0.5)
|
||||||
|
rexml (3.2.6)
|
||||||
|
rubocop (1.62.1)
|
||||||
|
json (~> 2.3)
|
||||||
|
language_server-protocol (>= 3.17.0)
|
||||||
|
parallel (~> 1.10)
|
||||||
|
parser (>= 3.3.0.2)
|
||||||
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
|
regexp_parser (>= 1.8, < 3.0)
|
||||||
|
rexml (>= 3.2.5, < 4.0)
|
||||||
|
rubocop-ast (>= 1.31.1, < 2.0)
|
||||||
|
ruby-progressbar (~> 1.7)
|
||||||
|
unicode-display_width (>= 2.4.0, < 3.0)
|
||||||
|
rubocop-ast (1.31.2)
|
||||||
|
parser (>= 3.3.0.4)
|
||||||
|
rubocop-minitest (0.35.0)
|
||||||
|
rubocop (>= 1.61, < 2.0)
|
||||||
|
rubocop-ast (>= 1.31.1, < 2.0)
|
||||||
|
rubocop-performance (1.20.2)
|
||||||
|
rubocop (>= 1.48.1, < 2.0)
|
||||||
|
rubocop-ast (>= 1.30.0, < 2.0)
|
||||||
|
rubocop-rails (2.24.0)
|
||||||
|
activesupport (>= 4.2.0)
|
||||||
|
rack (>= 1.1)
|
||||||
|
rubocop (>= 1.33.0, < 2.0)
|
||||||
|
rubocop-ast (>= 1.31.1, < 2.0)
|
||||||
|
rubocop-rails-omakase (1.0.0)
|
||||||
|
rubocop
|
||||||
|
rubocop-minitest
|
||||||
|
rubocop-performance
|
||||||
|
rubocop-rails
|
||||||
|
ruby-progressbar (1.13.0)
|
||||||
ruby2_keywords (0.0.5)
|
ruby2_keywords (0.0.5)
|
||||||
sshkit (1.21.4)
|
sshkit (1.22.2)
|
||||||
|
base64
|
||||||
|
mutex_m
|
||||||
net-scp (>= 1.1.2)
|
net-scp (>= 1.1.2)
|
||||||
|
net-sftp (>= 2.1.2)
|
||||||
net-ssh (>= 2.8.0)
|
net-ssh (>= 2.8.0)
|
||||||
thor (1.2.1)
|
stringio (3.1.0)
|
||||||
|
thor (1.3.0)
|
||||||
tzinfo (2.0.6)
|
tzinfo (2.0.6)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
zeitwerk (2.6.7)
|
unicode-display_width (2.5.0)
|
||||||
|
webrick (1.8.1)
|
||||||
|
zeitwerk (2.6.12)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
arm64-darwin
|
arm64-darwin
|
||||||
@@ -102,6 +177,7 @@ DEPENDENCIES
|
|||||||
kamal!
|
kamal!
|
||||||
mocha
|
mocha
|
||||||
railties
|
railties
|
||||||
|
rubocop-rails-omakase
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.4.3
|
2.4.3
|
||||||
|
|||||||
6
gemfiles/ruby_2.7.gemfile
Normal file
6
gemfiles/ruby_2.7.gemfile
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
source 'https://rubygems.org'
|
||||||
|
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
|
||||||
|
|
||||||
|
gemspec path: "../"
|
||||||
|
|
||||||
|
gem "nokogiri", "~> 1.15.0"
|
||||||
@@ -5,15 +5,14 @@ Gem::Specification.new do |spec|
|
|||||||
spec.version = Kamal::VERSION
|
spec.version = Kamal::VERSION
|
||||||
spec.authors = [ "David Heinemeier Hansson" ]
|
spec.authors = [ "David Heinemeier Hansson" ]
|
||||||
spec.email = "dhh@hey.com"
|
spec.email = "dhh@hey.com"
|
||||||
spec.homepage = "https://github.com/rails/kamal"
|
spec.homepage = "https://github.com/basecamp/kamal"
|
||||||
spec.summary = "Deploy web apps in containers to servers running Docker with zero downtime."
|
spec.summary = "Deploy web apps in containers to servers running Docker with zero downtime."
|
||||||
spec.license = "MIT"
|
spec.license = "MIT"
|
||||||
|
|
||||||
spec.files = Dir["lib/**/*", "MIT-LICENSE", "README.md"]
|
spec.files = Dir["lib/**/*", "MIT-LICENSE", "README.md"]
|
||||||
spec.executables = %w[ kamal ]
|
spec.executables = %w[ kamal ]
|
||||||
|
|
||||||
spec.add_dependency "activesupport", ">= 7.0"
|
spec.add_dependency "activesupport", ">= 7.0"
|
||||||
spec.add_dependency "sshkit", "~> 1.21"
|
spec.add_dependency "sshkit", ">= 1.22.2", "< 2.0"
|
||||||
spec.add_dependency "net-ssh", "~> 7.0"
|
spec.add_dependency "net-ssh", "~> 7.0"
|
||||||
spec.add_dependency "thor", "~> 1.2"
|
spec.add_dependency "thor", "~> 1.2"
|
||||||
spec.add_dependency "dotenv", "~> 2.8"
|
spec.add_dependency "dotenv", "~> 2.8"
|
||||||
@@ -21,6 +20,7 @@ Gem::Specification.new do |spec|
|
|||||||
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"
|
||||||
|
spec.add_dependency "base64", "~> 0.2"
|
||||||
|
|
||||||
spec.add_development_dependency "debug"
|
spec.add_development_dependency "debug"
|
||||||
spec.add_development_dependency "mocha"
|
spec.add_development_dependency "mocha"
|
||||||
|
|||||||
@@ -5,6 +5,6 @@ require "active_support"
|
|||||||
require "zeitwerk"
|
require "zeitwerk"
|
||||||
|
|
||||||
loader = Zeitwerk::Loader.for_gem
|
loader = Zeitwerk::Loader.for_gem
|
||||||
loader.ignore("#{__dir__}/kamal/sshkit_with_ext.rb")
|
loader.ignore(File.join(__dir__, "kamal", "sshkit_with_ext.rb"))
|
||||||
loader.setup
|
loader.setup
|
||||||
loader.eager_load # We need all commands loaded.
|
loader.eager_load # We need all commands loaded.
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
module Kamal::Cli
|
module Kamal::Cli
|
||||||
class LockError < StandardError; end
|
class LockError < StandardError; end
|
||||||
class HookError < StandardError; end
|
class HookError < StandardError; end
|
||||||
|
class BootError < StandardError; end
|
||||||
end
|
end
|
||||||
|
|
||||||
# SSHKit uses instance eval, so we need a global const for ergonomics
|
# SSHKit uses instance eval, so we need a global const for ergonomics
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
class Kamal::Cli::Accessory < Kamal::Cli::Base
|
class Kamal::Cli::Accessory < Kamal::Cli::Base
|
||||||
desc "boot [NAME]", "Boot new accessory service on host (use NAME=all to boot all accessories)"
|
desc "boot [NAME]", "Boot new accessory service on host (use NAME=all to boot all accessories)"
|
||||||
def boot(name, login: true)
|
def boot(name, login: true)
|
||||||
mutating do
|
with_lock do
|
||||||
if name == "all"
|
if name == "all"
|
||||||
KAMAL.accessory_names.each { |accessory_name| boot(accessory_name) }
|
KAMAL.accessory_names.each { |accessory_name| boot(accessory_name) }
|
||||||
else
|
else
|
||||||
with_accessory(name) do |accessory|
|
with_accessory(name) do |accessory, hosts|
|
||||||
directories(name)
|
directories(name)
|
||||||
upload(name)
|
upload(name)
|
||||||
|
|
||||||
on(accessory.hosts) do
|
on(hosts) do
|
||||||
execute *KAMAL.registry.login if login
|
execute *KAMAL.registry.login if login
|
||||||
execute *KAMAL.auditor.record("Booted #{name} accessory"), verbosity: :debug
|
execute *KAMAL.auditor.record("Booted #{name} accessory"), verbosity: :debug
|
||||||
execute *accessory.run
|
execute *accessory.run
|
||||||
@@ -21,9 +21,9 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
|
|||||||
|
|
||||||
desc "upload [NAME]", "Upload accessory files to host", hide: true
|
desc "upload [NAME]", "Upload accessory files to host", hide: true
|
||||||
def upload(name)
|
def upload(name)
|
||||||
mutating do
|
with_lock do
|
||||||
with_accessory(name) do |accessory|
|
with_accessory(name) do |accessory, hosts|
|
||||||
on(accessory.hosts) do
|
on(hosts) do
|
||||||
accessory.files.each do |(local, remote)|
|
accessory.files.each do |(local, remote)|
|
||||||
accessory.ensure_local_file_present(local)
|
accessory.ensure_local_file_present(local)
|
||||||
|
|
||||||
@@ -38,9 +38,9 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
|
|||||||
|
|
||||||
desc "directories [NAME]", "Create accessory directories on host", hide: true
|
desc "directories [NAME]", "Create accessory directories on host", hide: true
|
||||||
def directories(name)
|
def directories(name)
|
||||||
mutating do
|
with_lock do
|
||||||
with_accessory(name) do |accessory|
|
with_accessory(name) do |accessory, hosts|
|
||||||
on(accessory.hosts) do
|
on(hosts) do
|
||||||
accessory.directories.keys.each do |host_path|
|
accessory.directories.keys.each do |host_path|
|
||||||
execute *accessory.make_directory(host_path)
|
execute *accessory.make_directory(host_path)
|
||||||
end
|
end
|
||||||
@@ -49,26 +49,30 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
desc "reboot [NAME]", "Reboot existing accessory on host (stop container, remove container, start new container)"
|
desc "reboot [NAME]", "Reboot existing accessory on host (stop container, remove container, start new container; use NAME=all to boot all accessories)"
|
||||||
def reboot(name)
|
def reboot(name)
|
||||||
mutating do
|
with_lock do
|
||||||
with_accessory(name) do |accessory|
|
if name == "all"
|
||||||
on(accessory.hosts) do
|
KAMAL.accessory_names.each { |accessory_name| reboot(accessory_name) }
|
||||||
execute *KAMAL.registry.login
|
else
|
||||||
end
|
with_accessory(name) do |accessory, hosts|
|
||||||
|
on(hosts) do
|
||||||
|
execute *KAMAL.registry.login
|
||||||
|
end
|
||||||
|
|
||||||
stop(name)
|
stop(name)
|
||||||
remove_container(name)
|
remove_container(name)
|
||||||
boot(name, login: false)
|
boot(name, login: false)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
desc "start [NAME]", "Start existing accessory container on host"
|
desc "start [NAME]", "Start existing accessory container on host"
|
||||||
def start(name)
|
def start(name)
|
||||||
mutating do
|
with_lock do
|
||||||
with_accessory(name) do |accessory|
|
with_accessory(name) do |accessory, hosts|
|
||||||
on(accessory.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
|
||||||
end
|
end
|
||||||
@@ -78,9 +82,9 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
|
|||||||
|
|
||||||
desc "stop [NAME]", "Stop existing accessory container on host"
|
desc "stop [NAME]", "Stop existing accessory container on host"
|
||||||
def stop(name)
|
def stop(name)
|
||||||
mutating do
|
with_lock do
|
||||||
with_accessory(name) do |accessory|
|
with_accessory(name) do |accessory, hosts|
|
||||||
on(accessory.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
|
||||||
end
|
end
|
||||||
@@ -90,7 +94,7 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
|
|||||||
|
|
||||||
desc "restart [NAME]", "Restart existing accessory container on host"
|
desc "restart [NAME]", "Restart existing accessory container on host"
|
||||||
def restart(name)
|
def restart(name)
|
||||||
mutating do
|
with_lock do
|
||||||
with_accessory(name) do
|
with_accessory(name) do
|
||||||
stop(name)
|
stop(name)
|
||||||
start(name)
|
start(name)
|
||||||
@@ -103,8 +107,9 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
|
|||||||
if name == "all"
|
if name == "all"
|
||||||
KAMAL.accessory_names.each { |accessory_name| details(accessory_name) }
|
KAMAL.accessory_names.each { |accessory_name| details(accessory_name) }
|
||||||
else
|
else
|
||||||
with_accessory(name) do |accessory|
|
type = "Accessory #{name}"
|
||||||
on(accessory.hosts) { puts capture_with_info(*accessory.info) }
|
with_accessory(name) do |accessory, hosts|
|
||||||
|
on(hosts) { puts_by_host host, capture_with_info(*accessory.info), type: type }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -113,7 +118,7 @@ class Kamal::Cli::Accessory < 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"
|
||||||
def exec(name, cmd)
|
def exec(name, cmd)
|
||||||
with_accessory(name) do |accessory|
|
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 with via SSH from existing container...", :magenta
|
||||||
@@ -125,14 +130,14 @@ 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(accessory.hosts) do
|
on(hosts) do
|
||||||
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))
|
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(accessory.hosts) do
|
on(hosts) do
|
||||||
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))
|
capture_with_info(*accessory.execute_in_new_container(cmd))
|
||||||
end
|
end
|
||||||
@@ -146,12 +151,12 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
|
|||||||
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 :follow, aliases: "-f", desc: "Follow logs on primary server (or specific host set by --hosts)"
|
option :follow, aliases: "-f", desc: "Follow logs on primary server (or specific host set by --hosts)"
|
||||||
def logs(name)
|
def logs(name)
|
||||||
with_accessory(name) do |accessory|
|
with_accessory(name) do |accessory, hosts|
|
||||||
grep = options[:grep]
|
grep = options[:grep]
|
||||||
|
|
||||||
if options[:follow]
|
if options[:follow]
|
||||||
run_locally do
|
run_locally do
|
||||||
info "Following logs on #{accessory.hosts}..."
|
info "Following logs on #{hosts}..."
|
||||||
info accessory.follow_logs(grep: grep)
|
info accessory.follow_logs(grep: grep)
|
||||||
exec accessory.follow_logs(grep: grep)
|
exec accessory.follow_logs(grep: grep)
|
||||||
end
|
end
|
||||||
@@ -159,7 +164,7 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
|
|||||||
since = options[:since]
|
since = options[:since]
|
||||||
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
|
||||||
|
|
||||||
on(accessory.hosts) do
|
on(hosts) do
|
||||||
puts capture_with_info(*accessory.logs(since: since, lines: lines, grep: grep))
|
puts capture_with_info(*accessory.logs(since: since, lines: lines, grep: grep))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -169,17 +174,12 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
|
|||||||
desc "remove [NAME]", "Remove accessory container, image and data directory from host (use NAME=all to remove all accessories)"
|
desc "remove [NAME]", "Remove accessory container, image and data directory from host (use NAME=all to remove all accessories)"
|
||||||
option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
|
option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
|
||||||
def remove(name)
|
def remove(name)
|
||||||
mutating do
|
confirming "This will remove all containers, images and data directories for #{name}. Are you sure?" do
|
||||||
if name == "all"
|
with_lock do
|
||||||
KAMAL.accessory_names.each { |accessory_name| remove(accessory_name) }
|
if name == "all"
|
||||||
else
|
KAMAL.accessory_names.each { |accessory_name| remove_accessory(accessory_name) }
|
||||||
if options[:confirmed] || ask("This will remove all containers, images and data directories for #{name}. Are you sure?", limited_to: %w( y N ), default: "N") == "y"
|
else
|
||||||
with_accessory(name) do
|
remove_accessory(name)
|
||||||
stop(name)
|
|
||||||
remove_container(name)
|
|
||||||
remove_image(name)
|
|
||||||
remove_service_directory(name)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -187,9 +187,9 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
|
|||||||
|
|
||||||
desc "remove_container [NAME]", "Remove accessory container from host", hide: true
|
desc "remove_container [NAME]", "Remove accessory container from host", hide: true
|
||||||
def remove_container(name)
|
def remove_container(name)
|
||||||
mutating do
|
with_lock do
|
||||||
with_accessory(name) do |accessory|
|
with_accessory(name) do |accessory, hosts|
|
||||||
on(accessory.hosts) do
|
on(hosts) do
|
||||||
execute *KAMAL.auditor.record("Remove #{name} accessory container"), verbosity: :debug
|
execute *KAMAL.auditor.record("Remove #{name} accessory container"), verbosity: :debug
|
||||||
execute *accessory.remove_container
|
execute *accessory.remove_container
|
||||||
end
|
end
|
||||||
@@ -199,9 +199,9 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
|
|||||||
|
|
||||||
desc "remove_image [NAME]", "Remove accessory image from host", hide: true
|
desc "remove_image [NAME]", "Remove accessory image from host", hide: true
|
||||||
def remove_image(name)
|
def remove_image(name)
|
||||||
mutating do
|
with_lock do
|
||||||
with_accessory(name) do |accessory|
|
with_accessory(name) do |accessory, hosts|
|
||||||
on(accessory.hosts) do
|
on(hosts) do
|
||||||
execute *KAMAL.auditor.record("Removed #{name} accessory image"), verbosity: :debug
|
execute *KAMAL.auditor.record("Removed #{name} accessory image"), verbosity: :debug
|
||||||
execute *accessory.remove_image
|
execute *accessory.remove_image
|
||||||
end
|
end
|
||||||
@@ -211,9 +211,9 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
|
|||||||
|
|
||||||
desc "remove_service_directory [NAME]", "Remove accessory directory used for uploaded files and data directories from host", hide: true
|
desc "remove_service_directory [NAME]", "Remove accessory directory used for uploaded files and data directories from host", hide: true
|
||||||
def remove_service_directory(name)
|
def remove_service_directory(name)
|
||||||
mutating do
|
with_lock do
|
||||||
with_accessory(name) do |accessory|
|
with_accessory(name) do |accessory, hosts|
|
||||||
on(accessory.hosts) do
|
on(hosts) do
|
||||||
execute *accessory.remove_service_directory
|
execute *accessory.remove_service_directory
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -222,8 +222,9 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
|
|||||||
|
|
||||||
private
|
private
|
||||||
def with_accessory(name)
|
def with_accessory(name)
|
||||||
if accessory = KAMAL.accessory(name)
|
if KAMAL.config.accessory(name)
|
||||||
yield accessory
|
accessory = KAMAL.accessory(name)
|
||||||
|
yield accessory, accessory_hosts(accessory)
|
||||||
else
|
else
|
||||||
error_on_missing_accessory(name)
|
error_on_missing_accessory(name)
|
||||||
end
|
end
|
||||||
@@ -236,4 +237,21 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
|
|||||||
"No accessory by the name of '#{name}'" +
|
"No accessory by the name of '#{name}'" +
|
||||||
(options ? " (options: #{options.to_sentence})" : "")
|
(options ? " (options: #{options.to_sentence})" : "")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def accessory_hosts(accessory)
|
||||||
|
if KAMAL.specific_hosts&.any?
|
||||||
|
KAMAL.specific_hosts & accessory.hosts
|
||||||
|
else
|
||||||
|
accessory.hosts
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_accessory(name)
|
||||||
|
with_accessory(name) do
|
||||||
|
stop(name)
|
||||||
|
remove_container(name)
|
||||||
|
remove_image(name)
|
||||||
|
remove_service_directory(name)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,55 +1,54 @@
|
|||||||
class Kamal::Cli::App < Kamal::Cli::Base
|
class Kamal::Cli::App < Kamal::Cli::Base
|
||||||
desc "boot", "Boot app on servers (or reboot app if already running)"
|
desc "boot", "Boot app on servers (or reboot app if already running)"
|
||||||
def boot
|
def boot
|
||||||
mutating do
|
with_lock do
|
||||||
hold_lock_on_error do
|
say "Get most recent version available as an image...", :magenta unless options[:version]
|
||||||
say "Get most recent version available as an image...", :magenta unless options[:version]
|
using_version(version_or_latest) do |version|
|
||||||
using_version(version_or_latest) do |version|
|
say "Start container with version #{version} using a #{KAMAL.config.readiness_delay}s readiness delay (or reboot if already running)...", :magenta
|
||||||
say "Start container with version #{version} using a #{KAMAL.config.readiness_delay}s readiness delay (or reboot if already running)...", :magenta
|
|
||||||
|
|
||||||
on(KAMAL.hosts) do
|
# Assets are prepared in a separate step to ensure they are on all hosts before booting
|
||||||
execute *KAMAL.auditor.record("Tagging #{KAMAL.config.absolute_image} as the latest image"), verbosity: :debug
|
on(KAMAL.hosts) do
|
||||||
execute *KAMAL.app.tag_current_as_latest
|
KAMAL.roles_on(host).each do |role|
|
||||||
|
PrepareAssets.new(host, role, self).run
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
on(KAMAL.hosts, **KAMAL.boot_strategy) do |host|
|
# Primary hosts and roles are returned first, so they can open the barrier
|
||||||
roles = KAMAL.roles_on(host)
|
barrier = Barrier.new if KAMAL.roles.many?
|
||||||
|
|
||||||
roles.each do |role|
|
on(KAMAL.hosts, **KAMAL.boot_strategy) do |host|
|
||||||
app = KAMAL.app(role: role)
|
KAMAL.roles_on(host).each do |role|
|
||||||
auditor = KAMAL.auditor(role: role)
|
Boot.new(host, role, self, version, barrier).run
|
||||||
|
|
||||||
if capture_with_info(*app.container_id_for_version(version, only_running: true), raise_on_non_zero_exit: false).present?
|
|
||||||
tmp_version = "#{version}_replaced_#{SecureRandom.hex(8)}"
|
|
||||||
info "Renaming container #{version} to #{tmp_version} as already deployed on #{host}"
|
|
||||||
execute *auditor.record("Renaming container #{version} to #{tmp_version}"), verbosity: :debug
|
|
||||||
execute *app.rename_container(version: version, new_version: tmp_version)
|
|
||||||
end
|
|
||||||
|
|
||||||
execute *auditor.record("Booted app version #{version}"), verbosity: :debug
|
|
||||||
|
|
||||||
old_version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
|
|
||||||
execute *app.start_or_run(hostname: "#{host}-#{SecureRandom.hex(6)}")
|
|
||||||
|
|
||||||
Kamal::Utils::HealthcheckPoller.wait_for_healthy(pause_after_ready: true) { capture_with_info(*app.status(version: version)) }
|
|
||||||
|
|
||||||
execute *app.stop(version: old_version), raise_on_non_zero_exit: false if old_version.present?
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Tag once the app booted on all hosts
|
||||||
|
on(KAMAL.hosts) do |host|
|
||||||
|
execute *KAMAL.auditor.record("Tagging #{KAMAL.config.absolute_image} as the latest image"), verbosity: :debug
|
||||||
|
execute *KAMAL.app.tag_latest_image
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
desc "start", "Start existing app container on servers"
|
desc "start", "Start existing app container on servers"
|
||||||
def start
|
def start
|
||||||
mutating do
|
with_lock do
|
||||||
on(KAMAL.hosts) do |host|
|
on(KAMAL.hosts) do |host|
|
||||||
roles = KAMAL.roles_on(host)
|
roles = KAMAL.roles_on(host)
|
||||||
|
|
||||||
roles.each do |role|
|
roles.each do |role|
|
||||||
|
app = KAMAL.app(role: role, host: host)
|
||||||
execute *KAMAL.auditor.record("Started app version #{KAMAL.config.version}"), verbosity: :debug
|
execute *KAMAL.auditor.record("Started app version #{KAMAL.config.version}"), verbosity: :debug
|
||||||
execute *KAMAL.app(role: role).start, raise_on_non_zero_exit: false
|
execute *app.start, raise_on_non_zero_exit: false
|
||||||
|
|
||||||
|
if role.running_proxy?
|
||||||
|
version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
|
||||||
|
endpoint = capture_with_info(*app.container_endpoint(version: version)).strip
|
||||||
|
raise Kamal::Cli::BootError, "Failed to get endpoint for #{role} on #{host}, did the container boot?" if endpoint.empty?
|
||||||
|
|
||||||
|
execute *KAMAL.proxy.deploy(role.container_prefix, target: endpoint)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -57,13 +56,24 @@ class Kamal::Cli::App < Kamal::Cli::Base
|
|||||||
|
|
||||||
desc "stop", "Stop app container on servers"
|
desc "stop", "Stop app container on servers"
|
||||||
def stop
|
def stop
|
||||||
mutating do
|
with_lock do
|
||||||
on(KAMAL.hosts) do |host|
|
on(KAMAL.hosts) do |host|
|
||||||
roles = KAMAL.roles_on(host)
|
roles = KAMAL.roles_on(host)
|
||||||
|
|
||||||
roles.each do |role|
|
roles.each do |role|
|
||||||
|
app = KAMAL.app(role: role, host: host)
|
||||||
|
version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
|
||||||
|
|
||||||
execute *KAMAL.auditor.record("Stopped app", role: role), verbosity: :debug
|
execute *KAMAL.auditor.record("Stopped app", role: role), verbosity: :debug
|
||||||
execute *KAMAL.app(role: role).stop, raise_on_non_zero_exit: false
|
|
||||||
|
if role.running_proxy?
|
||||||
|
endpoint = capture_with_info(*app.container_endpoint(version: version)).strip
|
||||||
|
if endpoint.present?
|
||||||
|
execute *KAMAL.proxy.remove(role.container_prefix, target: endpoint), raise_on_non_zero_exit: false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
execute *app.stop, raise_on_non_zero_exit: false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -76,28 +86,32 @@ class Kamal::Cli::App < Kamal::Cli::Base
|
|||||||
roles = KAMAL.roles_on(host)
|
roles = KAMAL.roles_on(host)
|
||||||
|
|
||||||
roles.each do |role|
|
roles.each do |role|
|
||||||
puts_by_host host, capture_with_info(*KAMAL.app(role: role).info)
|
puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).info)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
desc "exec [CMD]", "Execute a custom command on servers (use --help to show options)"
|
desc "exec [CMD]", "Execute a custom command on servers within the app container (use --help to show options)"
|
||||||
option :interactive, aliases: "-i", type: :boolean, default: false, desc: "Execute command over ssh for an interactive shell (use for console/bash)"
|
option :interactive, aliases: "-i", type: :boolean, default: false, desc: "Execute command over ssh for an interactive shell (use for console/bash)"
|
||||||
option :reuse, type: :boolean, default: false, desc: "Reuse currently running container instead of starting a new one"
|
option :reuse, type: :boolean, default: false, desc: "Reuse currently running container instead of starting a new one"
|
||||||
|
option :env, aliases: "-e", type: :hash, desc: "Set environment variables for the command"
|
||||||
def exec(cmd)
|
def exec(cmd)
|
||||||
|
env = options[:env]
|
||||||
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]
|
||||||
using_version(options[:version] || current_running_version) do |version|
|
using_version(options[:version] || current_running_version) do |version|
|
||||||
say "Launching interactive command with version #{version} via SSH from existing container on #{KAMAL.primary_host}...", :magenta
|
say "Launching interactive command with version #{version} via SSH from existing container on #{KAMAL.primary_host}...", :magenta
|
||||||
run_locally { exec KAMAL.app(role: "web").execute_in_existing_container_over_ssh(cmd, host: KAMAL.primary_host) }
|
run_locally { exec KAMAL.app(role: KAMAL.primary_role, host: KAMAL.primary_host).execute_in_existing_container_over_ssh(cmd, env: env) }
|
||||||
end
|
end
|
||||||
|
|
||||||
when options[:interactive]
|
when options[:interactive]
|
||||||
say "Get most recent version available as an image...", :magenta unless options[:version]
|
say "Get most recent version available as an image...", :magenta unless options[:version]
|
||||||
using_version(version_or_latest) do |version|
|
using_version(version_or_latest) do |version|
|
||||||
say "Launching interactive command with version #{version} via SSH from new container on #{KAMAL.primary_host}...", :magenta
|
say "Launching interactive command with version #{version} via SSH from new container on #{KAMAL.primary_host}...", :magenta
|
||||||
run_locally { exec KAMAL.app(role: KAMAL.primary_host.roles.first).execute_in_new_container_over_ssh(cmd, host: KAMAL.primary_host) }
|
run_locally do
|
||||||
|
exec KAMAL.app(role: KAMAL.primary_role, host: KAMAL.primary_host).execute_in_new_container_over_ssh(cmd, env: env)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
when options[:reuse]
|
when options[:reuse]
|
||||||
@@ -110,7 +124,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}", role: role), verbosity: :debug
|
execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on app version #{version}", role: role), verbosity: :debug
|
||||||
puts_by_host host, capture_with_info(*KAMAL.app(role: role).execute_in_existing_container(cmd))
|
puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).execute_in_existing_container(cmd, env: env))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -120,8 +134,12 @@ class Kamal::Cli::App < Kamal::Cli::Base
|
|||||||
using_version(version_or_latest) do |version|
|
using_version(version_or_latest) do |version|
|
||||||
say "Launching command with version #{version} from new container...", :magenta
|
say "Launching command with version #{version} from new container...", :magenta
|
||||||
on(KAMAL.hosts) do |host|
|
on(KAMAL.hosts) do |host|
|
||||||
execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on app version #{version}"), verbosity: :debug
|
roles = KAMAL.roles_on(host)
|
||||||
puts_by_host host, capture_with_info(*KAMAL.app.execute_in_new_container(cmd))
|
|
||||||
|
roles.each do |role|
|
||||||
|
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))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -135,19 +153,21 @@ class Kamal::Cli::App < Kamal::Cli::Base
|
|||||||
desc "stale_containers", "Detect app stale containers"
|
desc "stale_containers", "Detect app stale containers"
|
||||||
option :stop, aliases: "-s", type: :boolean, default: false, desc: "Stop the stale containers found"
|
option :stop, aliases: "-s", type: :boolean, default: false, desc: "Stop the stale containers found"
|
||||||
def stale_containers
|
def stale_containers
|
||||||
mutating do
|
stop = options[:stop]
|
||||||
stop = options[:stop]
|
|
||||||
|
|
||||||
cli = self
|
|
||||||
|
|
||||||
|
with_lock_if_stopping do
|
||||||
on(KAMAL.hosts) do |host|
|
on(KAMAL.hosts) do |host|
|
||||||
roles = KAMAL.roles_on(host)
|
roles = KAMAL.roles_on(host)
|
||||||
|
|
||||||
roles.each do |role|
|
roles.each do |role|
|
||||||
cli.send(:stale_versions, host: host, role: role).each do |version|
|
app = KAMAL.app(role: role, host: host)
|
||||||
|
versions = capture_with_info(*app.list_versions, raise_on_non_zero_exit: false).split("\n")
|
||||||
|
versions -= [ capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip ]
|
||||||
|
|
||||||
|
versions.each do |version|
|
||||||
if stop
|
if stop
|
||||||
puts_by_host host, "Stopping stale container for role #{role} with version #{version}"
|
puts_by_host host, "Stopping stale container for role #{role} with version #{version}"
|
||||||
execute *KAMAL.app(role: role).stop(version: version), raise_on_non_zero_exit: false
|
execute *app.stop(version: version), raise_on_non_zero_exit: false
|
||||||
else
|
else
|
||||||
puts_by_host host, "Detected stale container for role #{role} with version #{version} (use `kamal app stale_containers --stop` to stop)"
|
puts_by_host host, "Detected stale container for role #{role} with version #{version} (use `kamal app stale_containers --stop` to stop)"
|
||||||
end
|
end
|
||||||
@@ -171,19 +191,21 @@ class Kamal::Cli::App < Kamal::Cli::Base
|
|||||||
# FIXME: Catch when app containers aren't running
|
# FIXME: Catch when app containers aren't running
|
||||||
|
|
||||||
grep = options[:grep]
|
grep = options[:grep]
|
||||||
|
since = options[:since]
|
||||||
if options[:follow]
|
if options[:follow]
|
||||||
|
lines = options[:lines].presence || ((since || grep) ? nil : 10) # Default to 10 lines if since or grep isn't set
|
||||||
|
|
||||||
run_locally do
|
run_locally do
|
||||||
info "Following logs on #{KAMAL.primary_host}..."
|
info "Following logs on #{KAMAL.primary_host}..."
|
||||||
|
|
||||||
KAMAL.specific_roles ||= ["web"]
|
KAMAL.specific_roles ||= [ "web" ]
|
||||||
role = KAMAL.roles_on(KAMAL.primary_host).first
|
role = KAMAL.roles_on(KAMAL.primary_host).first
|
||||||
|
|
||||||
info KAMAL.app(role: role).follow_logs(host: KAMAL.primary_host, grep: grep)
|
app = KAMAL.app(role: role, host: host)
|
||||||
exec KAMAL.app(role: role).follow_logs(host: KAMAL.primary_host, grep: grep)
|
info app.follow_logs(host: KAMAL.primary_host, lines: lines, grep: grep)
|
||||||
|
exec app.follow_logs(host: KAMAL.primary_host, lines: lines, grep: grep)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
since = options[:since]
|
|
||||||
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
|
||||||
|
|
||||||
on(KAMAL.hosts) do |host|
|
on(KAMAL.hosts) do |host|
|
||||||
@@ -191,7 +213,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).logs(since: since, lines: lines, grep: grep))
|
puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).logs(since: since, lines: lines, grep: grep))
|
||||||
rescue SSHKit::Command::Failed
|
rescue SSHKit::Command::Failed
|
||||||
puts_by_host host, "Nothing found"
|
puts_by_host host, "Nothing found"
|
||||||
end
|
end
|
||||||
@@ -202,7 +224,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
|
|||||||
|
|
||||||
desc "remove", "Remove app containers and images from servers"
|
desc "remove", "Remove app containers and images from servers"
|
||||||
def remove
|
def remove
|
||||||
mutating do
|
with_lock do
|
||||||
stop
|
stop
|
||||||
remove_containers
|
remove_containers
|
||||||
remove_images
|
remove_images
|
||||||
@@ -211,13 +233,13 @@ class Kamal::Cli::App < Kamal::Cli::Base
|
|||||||
|
|
||||||
desc "remove_container [VERSION]", "Remove app container with given version from servers", hide: true
|
desc "remove_container [VERSION]", "Remove app container with given version from servers", hide: true
|
||||||
def remove_container(version)
|
def remove_container(version)
|
||||||
mutating do
|
with_lock do
|
||||||
on(KAMAL.hosts) do |host|
|
on(KAMAL.hosts) do |host|
|
||||||
roles = KAMAL.roles_on(host)
|
roles = KAMAL.roles_on(host)
|
||||||
|
|
||||||
roles.each do |role|
|
roles.each do |role|
|
||||||
execute *KAMAL.auditor.record("Removed app container with version #{version}", role: role), verbosity: :debug
|
execute *KAMAL.auditor.record("Removed app container with version #{version}", role: role), verbosity: :debug
|
||||||
execute *KAMAL.app(role: role).remove_container(version: version)
|
execute *KAMAL.app(role: role, host: host).remove_container(version: version)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -225,13 +247,13 @@ class Kamal::Cli::App < Kamal::Cli::Base
|
|||||||
|
|
||||||
desc "remove_containers", "Remove all app containers from servers", hide: true
|
desc "remove_containers", "Remove all app containers from servers", hide: true
|
||||||
def remove_containers
|
def remove_containers
|
||||||
mutating do
|
with_lock do
|
||||||
on(KAMAL.hosts) do |host|
|
on(KAMAL.hosts) do |host|
|
||||||
roles = KAMAL.roles_on(host)
|
roles = KAMAL.roles_on(host)
|
||||||
|
|
||||||
roles.each do |role|
|
roles.each do |role|
|
||||||
execute *KAMAL.auditor.record("Removed all app containers", role: role), verbosity: :debug
|
execute *KAMAL.auditor.record("Removed all app containers", role: role), verbosity: :debug
|
||||||
execute *KAMAL.app(role: role).remove_containers
|
execute *KAMAL.app(role: role, host: host).remove_containers
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -239,7 +261,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
|
|||||||
|
|
||||||
desc "remove_images", "Remove all app images from servers", hide: true
|
desc "remove_images", "Remove all app images from servers", hide: true
|
||||||
def remove_images
|
def remove_images
|
||||||
mutating do
|
with_lock do
|
||||||
on(KAMAL.hosts) do
|
on(KAMAL.hosts) do
|
||||||
execute *KAMAL.auditor.record("Removed all app images"), verbosity: :debug
|
execute *KAMAL.auditor.record("Removed all app images"), verbosity: :debug
|
||||||
execute *KAMAL.app.remove_images
|
execute *KAMAL.app.remove_images
|
||||||
@@ -251,7 +273,7 @@ class Kamal::Cli::App < Kamal::Cli::Base
|
|||||||
def version
|
def version
|
||||||
on(KAMAL.hosts) do |host|
|
on(KAMAL.hosts) do |host|
|
||||||
role = KAMAL.roles_on(host).first
|
role = KAMAL.roles_on(host).first
|
||||||
puts_by_host host, capture_with_info(*KAMAL.app(role: role).current_running_version).strip
|
puts_by_host host, capture_with_info(*KAMAL.app(role: role, host: host).current_running_version).strip
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -274,23 +296,20 @@ class Kamal::Cli::App < Kamal::Cli::Base
|
|||||||
version = nil
|
version = nil
|
||||||
on(host) do
|
on(host) do
|
||||||
role = KAMAL.roles_on(host).first
|
role = KAMAL.roles_on(host).first
|
||||||
version = capture_with_info(*KAMAL.app(role: role).current_running_version).strip
|
version = capture_with_info(*KAMAL.app(role: role, host: host).current_running_version).strip
|
||||||
end
|
end
|
||||||
version.presence
|
version.presence
|
||||||
end
|
end
|
||||||
|
|
||||||
def stale_versions(host:, role:)
|
def version_or_latest
|
||||||
versions = nil
|
options[:version] || KAMAL.config.latest_tag
|
||||||
on(host) do
|
|
||||||
versions = \
|
|
||||||
capture_with_info(*KAMAL.app(role: role).list_versions, raise_on_non_zero_exit: false)
|
|
||||||
.split("\n")
|
|
||||||
.drop(1)
|
|
||||||
end
|
|
||||||
versions
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def version_or_latest
|
def with_lock_if_stopping
|
||||||
options[:version] || "latest"
|
if options[:stop]
|
||||||
|
with_lock { yield }
|
||||||
|
else
|
||||||
|
yield
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
31
lib/kamal/cli/app/barrier.rb
Normal file
31
lib/kamal/cli/app/barrier.rb
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
class Kamal::Cli::App::Barrier
|
||||||
|
def initialize
|
||||||
|
@ivar = Concurrent::IVar.new
|
||||||
|
end
|
||||||
|
|
||||||
|
def close
|
||||||
|
set(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
def open
|
||||||
|
set(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
def wait
|
||||||
|
unless opened?
|
||||||
|
raise Kamal::Cli::BootError.new("Halted at barrier")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def opened?
|
||||||
|
@ivar.value
|
||||||
|
end
|
||||||
|
|
||||||
|
def set(value)
|
||||||
|
@ivar.set(value)
|
||||||
|
true
|
||||||
|
rescue Concurrent::MultipleAssignmentError
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
111
lib/kamal/cli/app/boot.rb
Normal file
111
lib/kamal/cli/app/boot.rb
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
class Kamal::Cli::App::Boot
|
||||||
|
attr_reader :host, :role, :version, :barrier, :sshkit
|
||||||
|
delegate :execute, :capture_with_info, :capture_with_pretty_json, :info, :error, to: :sshkit
|
||||||
|
delegate :assets?, :running_proxy?, to: :role
|
||||||
|
|
||||||
|
def initialize(host, role, sshkit, version, barrier)
|
||||||
|
@host = host
|
||||||
|
@role = role
|
||||||
|
@version = version
|
||||||
|
@barrier = barrier
|
||||||
|
@sshkit = sshkit
|
||||||
|
end
|
||||||
|
|
||||||
|
def run
|
||||||
|
old_version = old_version_renamed_if_clashing
|
||||||
|
|
||||||
|
wait_at_barrier if queuer?
|
||||||
|
|
||||||
|
begin
|
||||||
|
start_new_version
|
||||||
|
rescue => e
|
||||||
|
close_barrier if gatekeeper?
|
||||||
|
stop_new_version
|
||||||
|
raise
|
||||||
|
end
|
||||||
|
|
||||||
|
release_barrier if gatekeeper?
|
||||||
|
|
||||||
|
if old_version
|
||||||
|
stop_old_version(old_version)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def old_version_renamed_if_clashing
|
||||||
|
if capture_with_info(*app.container_id_for_version(version), raise_on_non_zero_exit: false).present?
|
||||||
|
renamed_version = "#{version}_replaced_#{SecureRandom.hex(8)}"
|
||||||
|
info "Renaming container #{version} to #{renamed_version} as already deployed on #{host}"
|
||||||
|
audit("Renaming container #{version} to #{renamed_version}")
|
||||||
|
execute *app.rename_container(version: version, new_version: renamed_version)
|
||||||
|
end
|
||||||
|
|
||||||
|
capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip.presence
|
||||||
|
end
|
||||||
|
|
||||||
|
def start_new_version
|
||||||
|
audit "Booted app version #{version}"
|
||||||
|
hostname = "#{host.to_s[0...51].gsub(/\.+$/, '')}-#{SecureRandom.hex(6)}"
|
||||||
|
execute *app.run(hostname: hostname)
|
||||||
|
if running_proxy?
|
||||||
|
endpoint = capture_with_info(*app.container_endpoint(version: version)).strip
|
||||||
|
raise Kamal::Cli::BootError, "Failed to get endpoint for #{role} on #{host}, did the container boot?" if endpoint.empty?
|
||||||
|
execute *KAMAL.proxy.deploy(role.container_prefix, target: endpoint)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def stop_new_version
|
||||||
|
execute *app.stop(version: version), raise_on_non_zero_exit: false
|
||||||
|
end
|
||||||
|
|
||||||
|
def stop_old_version(version)
|
||||||
|
execute *app.stop(version: version), raise_on_non_zero_exit: false
|
||||||
|
execute *app.clean_up_assets if assets?
|
||||||
|
end
|
||||||
|
|
||||||
|
def release_barrier
|
||||||
|
if barrier.open
|
||||||
|
info "First #{KAMAL.primary_role} container is healthy on #{host}, booting other roles"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def wait_at_barrier
|
||||||
|
info "Waiting for the first healthy #{KAMAL.primary_role} container before booting #{role} on #{host}..."
|
||||||
|
barrier.wait
|
||||||
|
info "First #{KAMAL.primary_role} container is healthy, booting #{role} on #{host}..."
|
||||||
|
rescue Kamal::Cli::BootError
|
||||||
|
info "First #{KAMAL.primary_role} container is unhealthy, not booting #{role} on #{host}"
|
||||||
|
raise
|
||||||
|
end
|
||||||
|
|
||||||
|
def close_barrier
|
||||||
|
if barrier.close
|
||||||
|
info "First #{KAMAL.primary_role} container is unhealthy on #{host}, not booting other roles"
|
||||||
|
error capture_with_info(*app.logs(version: version))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def barrier_role?
|
||||||
|
role == KAMAL.primary_role
|
||||||
|
end
|
||||||
|
|
||||||
|
def app
|
||||||
|
@app ||= KAMAL.app(role: role, host: host)
|
||||||
|
end
|
||||||
|
|
||||||
|
def auditor
|
||||||
|
@auditor = KAMAL.auditor(role: role)
|
||||||
|
end
|
||||||
|
|
||||||
|
def audit(message)
|
||||||
|
execute *auditor.record(message), verbosity: :debug
|
||||||
|
end
|
||||||
|
|
||||||
|
def gatekeeper?
|
||||||
|
barrier && barrier_role?
|
||||||
|
end
|
||||||
|
|
||||||
|
def queuer?
|
||||||
|
barrier && !barrier_role?
|
||||||
|
end
|
||||||
|
end
|
||||||
24
lib/kamal/cli/app/prepare_assets.rb
Normal file
24
lib/kamal/cli/app/prepare_assets.rb
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
class Kamal::Cli::App::PrepareAssets
|
||||||
|
attr_reader :host, :role, :sshkit
|
||||||
|
delegate :execute, :capture_with_info, :info, to: :sshkit
|
||||||
|
delegate :assets?, to: :role
|
||||||
|
|
||||||
|
def initialize(host, role, sshkit)
|
||||||
|
@host = host
|
||||||
|
@role = role
|
||||||
|
@sshkit = sshkit
|
||||||
|
end
|
||||||
|
|
||||||
|
def run
|
||||||
|
if assets?
|
||||||
|
execute *app.extract_assets
|
||||||
|
old_version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
|
||||||
|
execute *app.sync_asset_volumes(old_version: old_version)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def app
|
||||||
|
@app ||= KAMAL.app(role: role, host: host)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -14,8 +14,8 @@ module Kamal::Cli
|
|||||||
class_option :version, desc: "Run commands against a specific app version"
|
class_option :version, desc: "Run commands against a specific app version"
|
||||||
|
|
||||||
class_option :primary, type: :boolean, aliases: "-p", desc: "Run commands only on primary host instead of all"
|
class_option :primary, type: :boolean, aliases: "-p", desc: "Run commands only on primary host instead of all"
|
||||||
class_option :hosts, aliases: "-h", desc: "Run commands on these hosts instead of all (separate by comma)"
|
class_option :hosts, aliases: "-h", desc: "Run commands on these hosts instead of all (separate by comma, supports wildcards with *)"
|
||||||
class_option :roles, aliases: "-r", desc: "Run commands on these roles instead of all (separate by comma)"
|
class_option :roles, aliases: "-r", desc: "Run commands on these roles instead of all (separate by comma, supports wildcards with *)"
|
||||||
|
|
||||||
class_option :config_file, aliases: "-c", default: "config/deploy.yml", desc: "Path to config file"
|
class_option :config_file, aliases: "-c", default: "config/deploy.yml", desc: "Path to config file"
|
||||||
class_option :destination, aliases: "-d", desc: "Specify destination to be used for config file (staging -> deploy.staging.yml)"
|
class_option :destination, aliases: "-d", desc: "Specify destination to be used for config file (staging -> deploy.staging.yml)"
|
||||||
@@ -24,6 +24,7 @@ module Kamal::Cli
|
|||||||
|
|
||||||
def initialize(*)
|
def initialize(*)
|
||||||
super
|
super
|
||||||
|
@original_env = ENV.to_h.dup
|
||||||
load_envs
|
load_envs
|
||||||
initialize_commander(options_with_subcommand_class_options)
|
initialize_commander(options_with_subcommand_class_options)
|
||||||
end
|
end
|
||||||
@@ -37,6 +38,12 @@ module Kamal::Cli
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def reload_envs
|
||||||
|
ENV.clear
|
||||||
|
ENV.update(@original_env)
|
||||||
|
load_envs
|
||||||
|
end
|
||||||
|
|
||||||
def options_with_subcommand_class_options
|
def options_with_subcommand_class_options
|
||||||
options.merge(@_initializer.last[:class_options] || {})
|
options.merge(@_initializer.last[:class_options] || {})
|
||||||
end
|
end
|
||||||
@@ -66,34 +73,43 @@ module Kamal::Cli
|
|||||||
def print_runtime
|
def print_runtime
|
||||||
started_at = Time.now
|
started_at = Time.now
|
||||||
yield
|
yield
|
||||||
return Time.now - started_at
|
Time.now - started_at
|
||||||
ensure
|
ensure
|
||||||
runtime = Time.now - started_at
|
runtime = Time.now - started_at
|
||||||
puts " Finished all in #{sprintf("%.1f seconds", runtime)}"
|
puts " Finished all in #{sprintf("%.1f seconds", runtime)}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def mutating
|
def with_lock
|
||||||
return yield if KAMAL.holding_lock?
|
if KAMAL.holding_lock?
|
||||||
|
|
||||||
KAMAL.config.ensure_env_available
|
|
||||||
|
|
||||||
run_hook "pre-connect"
|
|
||||||
|
|
||||||
acquire_lock
|
|
||||||
|
|
||||||
begin
|
|
||||||
yield
|
yield
|
||||||
rescue
|
else
|
||||||
if KAMAL.hold_lock_on_error?
|
ensure_run_and_locks_directory
|
||||||
error " \e[31mDeploy lock was not released\e[0m"
|
|
||||||
else
|
acquire_lock
|
||||||
release_lock
|
|
||||||
|
begin
|
||||||
|
yield
|
||||||
|
rescue
|
||||||
|
begin
|
||||||
|
release_lock
|
||||||
|
rescue => e
|
||||||
|
say "Error releasing the deploy lock: #{e.message}", :red
|
||||||
|
end
|
||||||
|
raise
|
||||||
end
|
end
|
||||||
|
|
||||||
raise
|
release_lock
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
release_lock
|
def confirming(question)
|
||||||
|
return yield if options[:confirmed]
|
||||||
|
|
||||||
|
if ask(question, limited_to: %w[ y N ], default: "N") == "y"
|
||||||
|
yield
|
||||||
|
else
|
||||||
|
say "Aborted", :red
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def acquire_lock
|
def acquire_lock
|
||||||
@@ -116,36 +132,36 @@ module Kamal::Cli
|
|||||||
yield
|
yield
|
||||||
rescue SSHKit::Runner::ExecuteError => e
|
rescue SSHKit::Runner::ExecuteError => e
|
||||||
if e.message =~ /cannot create directory/
|
if e.message =~ /cannot create directory/
|
||||||
|
say "Deploy lock already in place!", :red
|
||||||
on(KAMAL.primary_host) { puts capture_with_debug(*KAMAL.lock.status) }
|
on(KAMAL.primary_host) { puts capture_with_debug(*KAMAL.lock.status) }
|
||||||
raise LockError, "Deploy lock found"
|
raise LockError, "Deploy lock found. Run 'kamal lock help' for more information"
|
||||||
else
|
else
|
||||||
raise e
|
raise e
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def hold_lock_on_error
|
|
||||||
if KAMAL.hold_lock_on_error?
|
|
||||||
yield
|
|
||||||
else
|
|
||||||
KAMAL.hold_lock_on_error = true
|
|
||||||
yield
|
|
||||||
KAMAL.hold_lock_on_error = false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def run_hook(hook, **extra_details)
|
def run_hook(hook, **extra_details)
|
||||||
if !options[:skip_hooks] && KAMAL.hook.hook_exists?(hook)
|
if !options[:skip_hooks] && KAMAL.hook.hook_exists?(hook)
|
||||||
details = { hosts: KAMAL.hosts.join(","), command: command, subcommand: subcommand }
|
details = { hosts: KAMAL.hosts.join(","), command: command, subcommand: subcommand }
|
||||||
|
|
||||||
say "Running the #{hook} hook...", :magenta
|
say "Running the #{hook} hook...", :magenta
|
||||||
run_locally do
|
run_locally do
|
||||||
KAMAL.with_verbosity(:debug) { execute *KAMAL.hook.run(hook, **details, **extra_details) }
|
execute *KAMAL.hook.run(hook, **details, **extra_details)
|
||||||
rescue SSHKit::Command::Failed
|
rescue SSHKit::Command::Failed => e
|
||||||
raise HookError.new("Hook `#{hook}` failed")
|
raise HookError.new("Hook `#{hook}` failed:\n#{e.message}")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def on(*args, &block)
|
||||||
|
if !KAMAL.connected?
|
||||||
|
run_hook "pre-connect"
|
||||||
|
KAMAL.connected = true
|
||||||
|
end
|
||||||
|
|
||||||
|
super
|
||||||
|
end
|
||||||
|
|
||||||
def command
|
def command
|
||||||
@kamal_command ||= begin
|
@kamal_command ||= begin
|
||||||
invocation_class, invocation_commands = *first_invocation
|
invocation_class, invocation_commands = *first_invocation
|
||||||
@@ -167,5 +183,15 @@ module Kamal::Cli
|
|||||||
def first_invocation
|
def first_invocation
|
||||||
instance_variable_get("@_invocations").first
|
instance_variable_get("@_invocations").first
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
def ensure_run_and_locks_directory
|
||||||
|
on(KAMAL.hosts) do
|
||||||
|
execute(*KAMAL.server.ensure_run_directory)
|
||||||
|
end
|
||||||
|
|
||||||
|
on(KAMAL.primary_host) do
|
||||||
|
execute(*KAMAL.lock.ensure_locks_directory)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,41 +1,54 @@
|
|||||||
|
require "uri"
|
||||||
|
|
||||||
class Kamal::Cli::Build < Kamal::Cli::Base
|
class Kamal::Cli::Build < Kamal::Cli::Base
|
||||||
class BuildError < StandardError; end
|
class BuildError < StandardError; end
|
||||||
|
|
||||||
desc "deliver", "Build app and push app image to registry then pull image on servers"
|
desc "deliver", "Build app and push app image to registry then pull image on servers"
|
||||||
def deliver
|
def deliver
|
||||||
mutating do
|
push
|
||||||
push
|
pull
|
||||||
pull
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
desc "push", "Build and push app image to registry"
|
desc "push", "Build and push app image to registry"
|
||||||
def push
|
def push
|
||||||
mutating do
|
cli = self
|
||||||
cli = self
|
|
||||||
|
|
||||||
verify_local_dependencies
|
verify_local_dependencies
|
||||||
run_hook "pre-build"
|
run_hook "pre-build"
|
||||||
|
|
||||||
if (uncommitted_changes = Kamal::Utils.uncommitted_changes).present?
|
uncommitted_changes = Kamal::Git.uncommitted_changes
|
||||||
say "The following paths have uncommitted changes:\n #{uncommitted_changes}", :yellow
|
|
||||||
|
if KAMAL.config.builder.git_clone?
|
||||||
|
if uncommitted_changes.present?
|
||||||
|
say "Building from a local git clone, so ignoring these uncommitted changes:\n #{uncommitted_changes}", :yellow
|
||||||
end
|
end
|
||||||
|
|
||||||
run_locally do
|
run_locally do
|
||||||
begin
|
Clone.new(self).prepare
|
||||||
KAMAL.with_verbosity(:debug) do
|
end
|
||||||
execute *KAMAL.builder.push
|
elsif uncommitted_changes.present?
|
||||||
end
|
say "Building with uncommitted changes:\n #{uncommitted_changes}", :yellow
|
||||||
rescue SSHKit::Command::Failed => e
|
end
|
||||||
if e.message =~ /(no builder)|(no such file or directory)/
|
|
||||||
error "Missing compatible builder, so creating a new one first"
|
|
||||||
|
|
||||||
if cli.create
|
# Get the command here to ensure the Dir.chdir doesn't interfere with it
|
||||||
KAMAL.with_verbosity(:debug) { execute *KAMAL.builder.push }
|
push = KAMAL.builder.push
|
||||||
|
|
||||||
|
run_locally do
|
||||||
|
begin
|
||||||
|
KAMAL.with_verbosity(:debug) do
|
||||||
|
Dir.chdir(KAMAL.config.builder.build_directory) { execute *push }
|
||||||
|
end
|
||||||
|
rescue SSHKit::Command::Failed => e
|
||||||
|
if e.message =~ /(no builder)|(no such file or directory)/
|
||||||
|
warn "Missing compatible builder, so creating a new one first"
|
||||||
|
|
||||||
|
if cli.create
|
||||||
|
KAMAL.with_verbosity(:debug) do
|
||||||
|
Dir.chdir(KAMAL.config.builder.build_directory) { execute *push }
|
||||||
end
|
end
|
||||||
else
|
|
||||||
raise
|
|
||||||
end
|
end
|
||||||
|
else
|
||||||
|
raise
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -43,29 +56,30 @@ class Kamal::Cli::Build < Kamal::Cli::Base
|
|||||||
|
|
||||||
desc "pull", "Pull app image from registry onto servers"
|
desc "pull", "Pull app image from registry onto servers"
|
||||||
def pull
|
def pull
|
||||||
mutating do
|
on(KAMAL.hosts) do
|
||||||
on(KAMAL.hosts) do
|
execute *KAMAL.auditor.record("Pulled image with version #{KAMAL.config.version}"), verbosity: :debug
|
||||||
execute *KAMAL.auditor.record("Pulled image with version #{KAMAL.config.version}"), verbosity: :debug
|
execute *KAMAL.builder.clean, raise_on_non_zero_exit: false
|
||||||
execute *KAMAL.builder.clean, raise_on_non_zero_exit: false
|
execute *KAMAL.builder.pull
|
||||||
execute *KAMAL.builder.pull
|
execute *KAMAL.builder.validate_image
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
desc "create", "Create a build setup"
|
desc "create", "Create a build setup"
|
||||||
def create
|
def create
|
||||||
mutating do
|
if (remote_host = KAMAL.config.builder.remote_host)
|
||||||
run_locally do
|
connect_to_remote_host(remote_host)
|
||||||
begin
|
end
|
||||||
debug "Using builder: #{KAMAL.builder.name}"
|
|
||||||
execute *KAMAL.builder.create
|
run_locally do
|
||||||
rescue SSHKit::Command::Failed => e
|
begin
|
||||||
if e.message =~ /stderr=(.*)/
|
debug "Using builder: #{KAMAL.builder.name}"
|
||||||
error "Couldn't create remote builder: #{$1}"
|
execute *KAMAL.builder.create
|
||||||
false
|
rescue SSHKit::Command::Failed => e
|
||||||
else
|
if e.message =~ /stderr=(.*)/
|
||||||
raise
|
error "Couldn't create remote builder: #{$1}"
|
||||||
end
|
false
|
||||||
|
else
|
||||||
|
raise
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -73,11 +87,9 @@ class Kamal::Cli::Build < Kamal::Cli::Base
|
|||||||
|
|
||||||
desc "remove", "Remove build setup"
|
desc "remove", "Remove build setup"
|
||||||
def remove
|
def remove
|
||||||
mutating do
|
run_locally do
|
||||||
run_locally do
|
debug "Using builder: #{KAMAL.builder.name}"
|
||||||
debug "Using builder: #{KAMAL.builder.name}"
|
execute *KAMAL.builder.remove
|
||||||
execute *KAMAL.builder.remove
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -103,4 +115,17 @@ class Kamal::Cli::Build < Kamal::Cli::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def connect_to_remote_host(remote_host)
|
||||||
|
remote_uri = URI.parse(remote_host)
|
||||||
|
if remote_uri.scheme == "ssh"
|
||||||
|
host = SSHKit::Host.new(
|
||||||
|
hostname: remote_uri.host,
|
||||||
|
ssh_options: { user: remote_uri.user, port: remote_uri.port }.compact
|
||||||
|
)
|
||||||
|
on(host, options) do
|
||||||
|
execute "true"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
61
lib/kamal/cli/build/clone.rb
Normal file
61
lib/kamal/cli/build/clone.rb
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
require "uri"
|
||||||
|
|
||||||
|
class Kamal::Cli::Build::Clone
|
||||||
|
attr_reader :sshkit
|
||||||
|
delegate :info, :error, :execute, :capture_with_info, to: :sshkit
|
||||||
|
|
||||||
|
def initialize(sshkit)
|
||||||
|
@sshkit = sshkit
|
||||||
|
end
|
||||||
|
|
||||||
|
def prepare
|
||||||
|
begin
|
||||||
|
clone_repo
|
||||||
|
rescue SSHKit::Command::Failed => e
|
||||||
|
if e.message =~ /already exists and is not an empty directory/
|
||||||
|
reset
|
||||||
|
else
|
||||||
|
raise Kamal::Cli::Build::BuildError, "Failed to clone repo: #{e.message}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
validate!
|
||||||
|
rescue Kamal::Cli::Build::BuildError => e
|
||||||
|
error "Error preparing clone: #{e.message}, deleting and retrying..."
|
||||||
|
|
||||||
|
FileUtils.rm_rf KAMAL.config.builder.clone_directory
|
||||||
|
clone_repo
|
||||||
|
validate!
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def clone_repo
|
||||||
|
info "Cloning repo into build directory `#{KAMAL.config.builder.build_directory}`..."
|
||||||
|
|
||||||
|
FileUtils.mkdir_p KAMAL.config.builder.clone_directory
|
||||||
|
execute *KAMAL.builder.clone
|
||||||
|
end
|
||||||
|
|
||||||
|
def reset
|
||||||
|
info "Resetting local clone as `#{KAMAL.config.builder.build_directory}` already exists..."
|
||||||
|
|
||||||
|
KAMAL.builder.clone_reset_steps.each { |step| execute *step }
|
||||||
|
rescue SSHKit::Command::Failed => e
|
||||||
|
raise Kamal::Cli::Build::BuildError, "Failed to clone repo: #{e.message}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate!
|
||||||
|
status = capture_with_info(*KAMAL.builder.clone_status).strip
|
||||||
|
|
||||||
|
unless status.empty?
|
||||||
|
raise Kamal::Cli::Build::BuildError, "Clone in #{KAMAL.config.builder.build_directory} is dirty, #{status}"
|
||||||
|
end
|
||||||
|
|
||||||
|
revision = capture_with_info(*KAMAL.builder.clone_revision).strip
|
||||||
|
if revision != Kamal::Git.revision
|
||||||
|
raise Kamal::Cli::Build::BuildError, "Clone in #{KAMAL.config.builder.build_directory} is not on the correct revision, expected `#{Kamal::Git.revision}` but got `#{revision}`"
|
||||||
|
end
|
||||||
|
rescue SSHKit::Command::Failed => e
|
||||||
|
raise Kamal::Cli::Build::BuildError, "Failed to validate clone: #{e.message}"
|
||||||
|
end
|
||||||
|
end
|
||||||
45
lib/kamal/cli/env.rb
Normal file
45
lib/kamal/cli/env.rb
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
require "tempfile"
|
||||||
|
|
||||||
|
class Kamal::Cli::Env < Kamal::Cli::Base
|
||||||
|
desc "push", "Push the env file to the remote hosts"
|
||||||
|
def push
|
||||||
|
with_lock do
|
||||||
|
on(KAMAL.hosts) do
|
||||||
|
execute *KAMAL.auditor.record("Pushed env files"), verbosity: :debug
|
||||||
|
|
||||||
|
KAMAL.roles_on(host).each do |role|
|
||||||
|
execute *KAMAL.app(role: role, host: host).make_env_directory
|
||||||
|
upload! role.env(host).secrets_io, role.env(host).secrets_file, mode: 400
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
on(KAMAL.accessory_hosts) do
|
||||||
|
KAMAL.accessories_on(host).each do |accessory|
|
||||||
|
accessory_config = KAMAL.config.accessory(accessory)
|
||||||
|
execute *KAMAL.accessory(accessory).make_env_directory
|
||||||
|
upload! accessory_config.env.secrets_io, accessory_config.env.secrets_file, mode: 400
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "delete", "Delete the env file from the remote hosts"
|
||||||
|
def delete
|
||||||
|
with_lock do
|
||||||
|
on(KAMAL.hosts) do
|
||||||
|
execute *KAMAL.auditor.record("Deleted env files"), verbosity: :debug
|
||||||
|
|
||||||
|
KAMAL.roles_on(host).each do |role|
|
||||||
|
execute *KAMAL.app(role: role, host: host).remove_env_file
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
on(KAMAL.accessory_hosts) do
|
||||||
|
KAMAL.accessories_on(host).each do |accessory|
|
||||||
|
accessory_config = KAMAL.config.accessory(accessory)
|
||||||
|
execute *KAMAL.accessory(accessory).remove_env_file
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
class Kamal::Cli::Healthcheck < Kamal::Cli::Base
|
|
||||||
default_command :perform
|
|
||||||
|
|
||||||
desc "perform", "Health check current app version"
|
|
||||||
def perform
|
|
||||||
on(KAMAL.primary_host) do
|
|
||||||
begin
|
|
||||||
execute *KAMAL.healthcheck.run
|
|
||||||
Kamal::Utils::HealthcheckPoller.wait_for_healthy { capture_with_info(*KAMAL.healthcheck.status) }
|
|
||||||
rescue Kamal::Utils::HealthcheckPoller::HealthcheckError => e
|
|
||||||
error capture_with_info(*KAMAL.healthcheck.logs)
|
|
||||||
error capture_with_pretty_json(*KAMAL.healthcheck.container_health_log)
|
|
||||||
raise
|
|
||||||
ensure
|
|
||||||
execute *KAMAL.healthcheck.stop, raise_on_non_zero_exit: false
|
|
||||||
execute *KAMAL.healthcheck.remove, raise_on_non_zero_exit: false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -2,7 +2,10 @@ class Kamal::Cli::Lock < Kamal::Cli::Base
|
|||||||
desc "status", "Report lock status"
|
desc "status", "Report lock status"
|
||||||
def status
|
def status
|
||||||
handle_missing_lock do
|
handle_missing_lock do
|
||||||
on(KAMAL.primary_host) { puts capture_with_debug(*KAMAL.lock.status) }
|
on(KAMAL.primary_host) do
|
||||||
|
execute *KAMAL.server.ensure_run_directory
|
||||||
|
puts capture_with_debug(*KAMAL.lock.status)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -11,7 +14,10 @@ class Kamal::Cli::Lock < Kamal::Cli::Base
|
|||||||
def acquire
|
def acquire
|
||||||
message = options[:message]
|
message = options[:message]
|
||||||
raise_if_locked do
|
raise_if_locked do
|
||||||
on(KAMAL.primary_host) { execute *KAMAL.lock.acquire(message, KAMAL.config.version), verbosity: :debug }
|
on(KAMAL.primary_host) do
|
||||||
|
execute *KAMAL.server.ensure_run_directory
|
||||||
|
execute *KAMAL.lock.acquire(message, KAMAL.config.version), verbosity: :debug
|
||||||
|
end
|
||||||
say "Acquired the deploy lock"
|
say "Acquired the deploy lock"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -19,7 +25,10 @@ class Kamal::Cli::Lock < Kamal::Cli::Base
|
|||||||
desc "release", "Release the deploy lock"
|
desc "release", "Release the deploy lock"
|
||||||
def release
|
def release
|
||||||
handle_missing_lock do
|
handle_missing_lock do
|
||||||
on(KAMAL.primary_host) { execute *KAMAL.lock.release, verbosity: :debug }
|
on(KAMAL.primary_host) do
|
||||||
|
execute *KAMAL.server.ensure_run_directory
|
||||||
|
execute *KAMAL.lock.release, verbosity: :debug
|
||||||
|
end
|
||||||
say "Released the deploy lock"
|
say "Released the deploy lock"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,10 +1,18 @@
|
|||||||
class Kamal::Cli::Main < Kamal::Cli::Base
|
class Kamal::Cli::Main < Kamal::Cli::Base
|
||||||
desc "setup", "Setup all accessories and deploy app to servers"
|
desc "setup", "Setup all accessories, push the env, and deploy app to servers"
|
||||||
|
option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip image build and push"
|
||||||
def setup
|
def setup
|
||||||
print_runtime do
|
print_runtime do
|
||||||
mutating do
|
with_lock do
|
||||||
invoke "kamal:cli:server:bootstrap"
|
invoke_options = deploy_options
|
||||||
invoke "kamal:cli:accessory:boot", [ "all" ]
|
|
||||||
|
say "Ensure Docker is installed...", :magenta
|
||||||
|
invoke "kamal:cli:server:bootstrap", [], invoke_options
|
||||||
|
|
||||||
|
say "Evaluate and push env files...", :magenta
|
||||||
|
invoke "kamal:cli:main:envify", [], invoke_options
|
||||||
|
|
||||||
|
invoke "kamal:cli:accessory:boot", [ "all" ], invoke_options
|
||||||
deploy
|
deploy
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -14,30 +22,27 @@ class Kamal::Cli::Main < Kamal::Cli::Base
|
|||||||
option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip image build and push"
|
option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip image build and push"
|
||||||
def deploy
|
def deploy
|
||||||
runtime = print_runtime do
|
runtime = print_runtime do
|
||||||
mutating do
|
invoke_options = deploy_options
|
||||||
invoke_options = deploy_options
|
|
||||||
|
|
||||||
say "Log into image registry...", :magenta
|
say "Log into image registry...", :magenta
|
||||||
invoke "kamal:cli:registry:login", [], invoke_options
|
invoke "kamal:cli:registry:login", [], invoke_options
|
||||||
|
|
||||||
if options[:skip_push]
|
if options[:skip_push]
|
||||||
say "Pull app image...", :magenta
|
say "Pull app image...", :magenta
|
||||||
invoke "kamal:cli:build:pull", [], invoke_options
|
invoke "kamal:cli:build:pull", [], invoke_options
|
||||||
else
|
else
|
||||||
say "Build and push app image...", :magenta
|
say "Build and push app image...", :magenta
|
||||||
invoke "kamal:cli:build:deliver", [], invoke_options
|
invoke "kamal:cli:build:deliver", [], invoke_options
|
||||||
end
|
end
|
||||||
|
|
||||||
|
with_lock do
|
||||||
run_hook "pre-deploy"
|
run_hook "pre-deploy"
|
||||||
|
|
||||||
say "Ensure Traefik is running...", :magenta
|
say "Ensure proxy is running...", :magenta
|
||||||
invoke "kamal:cli:traefik:boot", [], invoke_options
|
invoke "kamal:cli:proxy:boot", [], invoke_options
|
||||||
|
|
||||||
say "Ensure app can pass healthcheck...", :magenta
|
|
||||||
invoke "kamal:cli:healthcheck:perform", [], invoke_options
|
|
||||||
|
|
||||||
say "Detect stale containers...", :magenta
|
say "Detect stale containers...", :magenta
|
||||||
invoke "kamal:cli:app:stale_containers", [], invoke_options
|
invoke "kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true)
|
||||||
|
|
||||||
invoke "kamal:cli:app:boot", [], invoke_options
|
invoke "kamal:cli:app:boot", [], invoke_options
|
||||||
|
|
||||||
@@ -49,28 +54,25 @@ class Kamal::Cli::Main < Kamal::Cli::Base
|
|||||||
run_hook "post-deploy", runtime: runtime.round
|
run_hook "post-deploy", runtime: runtime.round
|
||||||
end
|
end
|
||||||
|
|
||||||
desc "redeploy", "Deploy app to servers without bootstrapping servers, starting Traefik, pruning, and registry login"
|
desc "redeploy", "Deploy app to servers without bootstrapping servers, starting proxy, pruning, and registry login"
|
||||||
option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip image build and push"
|
option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip image build and push"
|
||||||
def redeploy
|
def redeploy
|
||||||
runtime = print_runtime do
|
runtime = print_runtime do
|
||||||
mutating do
|
invoke_options = deploy_options
|
||||||
invoke_options = deploy_options
|
|
||||||
|
|
||||||
if options[:skip_push]
|
if options[:skip_push]
|
||||||
say "Pull app image...", :magenta
|
say "Pull app image...", :magenta
|
||||||
invoke "kamal:cli:build:pull", [], invoke_options
|
invoke "kamal:cli:build:pull", [], invoke_options
|
||||||
else
|
else
|
||||||
say "Build and push app image...", :magenta
|
say "Build and push app image...", :magenta
|
||||||
invoke "kamal:cli:build:deliver", [], invoke_options
|
invoke "kamal:cli:build:deliver", [], invoke_options
|
||||||
end
|
end
|
||||||
|
|
||||||
|
with_lock do
|
||||||
run_hook "pre-deploy"
|
run_hook "pre-deploy"
|
||||||
|
|
||||||
say "Ensure app can pass healthcheck...", :magenta
|
|
||||||
invoke "kamal:cli:healthcheck:perform", [], invoke_options
|
|
||||||
|
|
||||||
say "Detect stale containers...", :magenta
|
say "Detect stale containers...", :magenta
|
||||||
invoke "kamal:cli:app:stale_containers", [], invoke_options
|
invoke "kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true)
|
||||||
|
|
||||||
invoke "kamal:cli:app:boot", [], invoke_options
|
invoke "kamal:cli:app:boot", [], invoke_options
|
||||||
end
|
end
|
||||||
@@ -83,7 +85,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base
|
|||||||
def rollback(version)
|
def rollback(version)
|
||||||
rolled_back = false
|
rolled_back = false
|
||||||
runtime = print_runtime do
|
runtime = print_runtime do
|
||||||
mutating do
|
with_lock do
|
||||||
invoke_options = deploy_options
|
invoke_options = deploy_options
|
||||||
|
|
||||||
KAMAL.config.version = version
|
KAMAL.config.version = version
|
||||||
@@ -105,7 +107,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base
|
|||||||
|
|
||||||
desc "details", "Show details about all containers"
|
desc "details", "Show details about all containers"
|
||||||
def details
|
def details
|
||||||
invoke "kamal:cli:traefik:details"
|
invoke "kamal:cli:proxy:details"
|
||||||
invoke "kamal:cli:app:details"
|
invoke "kamal:cli:app:details"
|
||||||
invoke "kamal:cli:accessory:details", [ "all" ]
|
invoke "kamal:cli:accessory:details", [ "all" ]
|
||||||
end
|
end
|
||||||
@@ -165,6 +167,7 @@ class Kamal::Cli::Main < Kamal::Cli::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
desc "envify", "Create .env by evaluating .env.erb (or .env.staging.erb -> .env.staging when using -d staging)"
|
desc "envify", "Create .env by evaluating .env.erb (or .env.staging.erb -> .env.staging when using -d staging)"
|
||||||
|
option :skip_push, aliases: "-P", type: :boolean, default: false, desc: "Skip .env file push"
|
||||||
def envify
|
def envify
|
||||||
if destination = options[:destination]
|
if destination = options[:destination]
|
||||||
env_template_path = ".env.#{destination}.erb"
|
env_template_path = ".env.#{destination}.erb"
|
||||||
@@ -174,15 +177,24 @@ class Kamal::Cli::Main < Kamal::Cli::Base
|
|||||||
env_path = ".env"
|
env_path = ".env"
|
||||||
end
|
end
|
||||||
|
|
||||||
File.write(env_path, ERB.new(File.read(env_template_path)).result, perm: 0600)
|
if Pathname.new(File.expand_path(env_template_path)).exist?
|
||||||
|
File.write(env_path, ERB.new(File.read(env_template_path), trim_mode: "-").result, perm: 0600)
|
||||||
|
|
||||||
|
unless options[:skip_push]
|
||||||
|
reload_envs
|
||||||
|
invoke "kamal:cli:env:push", options
|
||||||
|
end
|
||||||
|
else
|
||||||
|
puts "Skipping envify (no #{env_template_path} exist)"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
desc "remove", "Remove Traefik, app, accessories, and registry session from servers"
|
desc "remove", "Remove proxy, app, accessories, and registry session from servers"
|
||||||
option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
|
option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
|
||||||
def remove
|
def remove
|
||||||
mutating do
|
confirming "This will remove all containers and images. Are you sure?" do
|
||||||
if options[:confirmed] || ask("This will remove all containers and images. Are you sure?", limited_to: %w( y N ), default: "N") == "y"
|
with_lock do
|
||||||
invoke "kamal:cli:traefik:remove", [], options.without(:confirmed)
|
invoke "kamal:cli:proxy:remove", [], options.without(:confirmed)
|
||||||
invoke "kamal:cli:app:remove", [], options.without(:confirmed)
|
invoke "kamal:cli:app:remove", [], options.without(:confirmed)
|
||||||
invoke "kamal:cli:accessory:remove", [ "all" ], options
|
invoke "kamal:cli:accessory:remove", [ "all" ], options
|
||||||
invoke "kamal:cli:registry:logout", [], options.without(:confirmed)
|
invoke "kamal:cli:registry:logout", [], options.without(:confirmed)
|
||||||
@@ -204,8 +216,8 @@ class Kamal::Cli::Main < Kamal::Cli::Base
|
|||||||
desc "build", "Build application image"
|
desc "build", "Build application image"
|
||||||
subcommand "build", Kamal::Cli::Build
|
subcommand "build", Kamal::Cli::Build
|
||||||
|
|
||||||
desc "healthcheck", "Healthcheck application"
|
desc "env", "Manage environment files"
|
||||||
subcommand "healthcheck", Kamal::Cli::Healthcheck
|
subcommand "env", Kamal::Cli::Env
|
||||||
|
|
||||||
desc "lock", "Manage the deploy lock"
|
desc "lock", "Manage the deploy lock"
|
||||||
subcommand "lock", Kamal::Cli::Lock
|
subcommand "lock", Kamal::Cli::Lock
|
||||||
@@ -219,19 +231,19 @@ class Kamal::Cli::Main < Kamal::Cli::Base
|
|||||||
desc "server", "Bootstrap servers with curl and Docker"
|
desc "server", "Bootstrap servers with curl and Docker"
|
||||||
subcommand "server", Kamal::Cli::Server
|
subcommand "server", Kamal::Cli::Server
|
||||||
|
|
||||||
desc "traefik", "Manage Traefik load balancer"
|
desc "proxy", "Manage load balancer proxy"
|
||||||
subcommand "traefik", Kamal::Cli::Traefik
|
subcommand "proxy", Kamal::Cli::Proxy
|
||||||
|
|
||||||
private
|
private
|
||||||
def container_available?(version)
|
def container_available?(version)
|
||||||
begin
|
begin
|
||||||
on(KAMAL.hosts) do
|
on(KAMAL.hosts) do
|
||||||
KAMAL.roles_on(host).each do |role|
|
KAMAL.roles_on(host).each do |role|
|
||||||
container_id = capture_with_info(*KAMAL.app(role: role).container_id_for_version(version))
|
container_id = capture_with_info(*KAMAL.app(role: role, host: host).container_id_for_version(version))
|
||||||
raise "Container not found" unless container_id.present?
|
raise "Container not found" unless container_id.present?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
rescue SSHKit::Runner::ExecuteError => e
|
rescue SSHKit::Runner::ExecuteError, SSHKit::Runner::MultipleExecuteError => e
|
||||||
if e.message =~ /Container not found/
|
if e.message =~ /Container not found/
|
||||||
say "Error looking for container version #{version}: #{e.message}"
|
say "Error looking for container version #{version}: #{e.message}"
|
||||||
return false
|
return false
|
||||||
|
|||||||
164
lib/kamal/cli/proxy.rb
Normal file
164
lib/kamal/cli/proxy.rb
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
class Kamal::Cli::Proxy < Kamal::Cli::Base
|
||||||
|
desc "boot", "Boot proxy on servers"
|
||||||
|
def boot
|
||||||
|
with_lock do
|
||||||
|
on(KAMAL.proxy_hosts) do
|
||||||
|
execute *KAMAL.registry.login
|
||||||
|
execute *KAMAL.proxy.start_or_run
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "reboot", "Reboot proxy on servers (stop container, remove container, start new container)"
|
||||||
|
option :rolling, type: :boolean, default: false, desc: "Reboot proxy on hosts in sequence, rather than in parallel"
|
||||||
|
option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
|
||||||
|
def reboot
|
||||||
|
confirming "This will cause a brief outage on each host. Are you sure?" do
|
||||||
|
with_lock do
|
||||||
|
host_groups = options[:rolling] ? KAMAL.proxy_hosts : [ KAMAL.proxy_hosts ]
|
||||||
|
host_groups.each do |hosts|
|
||||||
|
host_list = Array(hosts).join(",")
|
||||||
|
run_hook "pre-proxy-reboot", hosts: host_list
|
||||||
|
on(hosts) do
|
||||||
|
execute *KAMAL.auditor.record("Rebooted proxy"), verbosity: :debug
|
||||||
|
execute *KAMAL.registry.login
|
||||||
|
execute *KAMAL.proxy.stop, raise_on_non_zero_exit: false
|
||||||
|
execute *KAMAL.proxy.remove_container
|
||||||
|
execute *KAMAL.proxy.run
|
||||||
|
end
|
||||||
|
run_hook "post-proxy-reboot", hosts: host_list
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "start", "Start existing proxy container on servers"
|
||||||
|
def start
|
||||||
|
with_lock do
|
||||||
|
on(KAMAL.proxy_hosts) do
|
||||||
|
execute *KAMAL.auditor.record("Started proxy"), verbosity: :debug
|
||||||
|
execute *KAMAL.proxy.start
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "stop", "Stop existing proxy container on servers"
|
||||||
|
def stop
|
||||||
|
with_lock do
|
||||||
|
on(KAMAL.proxy_hosts) do
|
||||||
|
execute *KAMAL.auditor.record("Stopped proxy"), verbosity: :debug
|
||||||
|
execute *KAMAL.proxy.stop, raise_on_non_zero_exit: false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "restart", "Restart existing proxy container on servers"
|
||||||
|
def restart
|
||||||
|
with_lock do
|
||||||
|
stop
|
||||||
|
start
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "update", "Update from Traefik to kamal-proxy, for when moving from Kamal v1 to Kamal v2"
|
||||||
|
option :rolling, type: :boolean, default: false, desc: "Reboot proxy on hosts in sequence, rather than in parallel"
|
||||||
|
option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
|
||||||
|
def update
|
||||||
|
confirming "This will cause a brief outage on each host. Are you sure?" do
|
||||||
|
with_lock do
|
||||||
|
host_groups = options[:rolling] ? KAMAL.proxy_hosts : [ KAMAL.proxy_hosts ]
|
||||||
|
host_groups.each do |hosts|
|
||||||
|
host_list = Array(hosts).join(",")
|
||||||
|
run_hook "pre-proxy-reboot", hosts: host_list
|
||||||
|
on(hosts) do
|
||||||
|
info "Updating proxy from Traefik to kamal-proxy on #{host}..."
|
||||||
|
execute *KAMAL.auditor.record("Updated proxy from Traefik to kamal-proxy"), verbosity: :debug
|
||||||
|
execute *KAMAL.registry.login
|
||||||
|
|
||||||
|
info "Stopping and removing Traefik on #{host}..."
|
||||||
|
execute *KAMAL.proxy.stop(name: "traefik"), raise_on_non_zero_exit: false
|
||||||
|
execute *KAMAL.proxy.remove_container(filter: "label=org.opencontainers.image.title=traefik")
|
||||||
|
execute *KAMAL.proxy.remove_image(filter: "label=org.opencontainers.image.title=traefik")
|
||||||
|
|
||||||
|
info "Stopping and removing kamal-proxy on #{host}, if running..."
|
||||||
|
execute *KAMAL.proxy.stop, raise_on_non_zero_exit: false
|
||||||
|
execute *KAMAL.proxy.remove_container
|
||||||
|
|
||||||
|
info "Starting kamal-proxy on #{host}..."
|
||||||
|
execute *KAMAL.proxy.run
|
||||||
|
|
||||||
|
KAMAL.roles_on(host).select(&:running_proxy?).each do |role|
|
||||||
|
app = KAMAL.app(role: role, host: host)
|
||||||
|
|
||||||
|
version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
|
||||||
|
endpoint = capture_with_info(*app.container_endpoint(version: version)).strip
|
||||||
|
raise Kamal::Cli::BootError, "Failed to get endpoint for #{role} on #{host}, is the app container running?" if endpoint.empty?
|
||||||
|
|
||||||
|
info "Deploying #{endpoint} for role `#{role}` on #{host}..."
|
||||||
|
execute *KAMAL.proxy.deploy(role.container_prefix, target: endpoint)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
run_hook "post-proxy-reboot", hosts: host_list
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "details", "Show details about proxy container from servers"
|
||||||
|
def details
|
||||||
|
on(KAMAL.proxy_hosts) { |host| puts_by_host host, capture_with_info(*KAMAL.proxy.info), type: "Proxy" }
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "logs", "Show log lines from proxy on servers"
|
||||||
|
option :since, aliases: "-s", desc: "Show logs since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)"
|
||||||
|
option :lines, type: :numeric, aliases: "-n", desc: "Number of log lines to pull from each server"
|
||||||
|
option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)"
|
||||||
|
option :follow, aliases: "-f", desc: "Follow logs on primary server (or specific host set by --hosts)"
|
||||||
|
def logs
|
||||||
|
grep = options[:grep]
|
||||||
|
|
||||||
|
if options[:follow]
|
||||||
|
run_locally do
|
||||||
|
info "Following logs on #{KAMAL.primary_host}..."
|
||||||
|
info KAMAL.proxy.follow_logs(host: KAMAL.primary_host, grep: grep)
|
||||||
|
exec KAMAL.proxy.follow_logs(host: KAMAL.primary_host, grep: grep)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
since = options[:since]
|
||||||
|
lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set
|
||||||
|
|
||||||
|
on(KAMAL.proxy_hosts) do |host|
|
||||||
|
puts_by_host host, capture(*KAMAL.proxy.logs(since: since, lines: lines, grep: grep)), type: "Proxy"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "remove", "Remove proxy container and image from servers"
|
||||||
|
def remove
|
||||||
|
with_lock do
|
||||||
|
stop
|
||||||
|
remove_container
|
||||||
|
remove_image
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "remove_container", "Remove proxy container from servers", hide: true
|
||||||
|
def remove_container
|
||||||
|
with_lock do
|
||||||
|
on(KAMAL.proxy_hosts) do
|
||||||
|
execute *KAMAL.auditor.record("Removed proxy container"), verbosity: :debug
|
||||||
|
execute *KAMAL.proxy.remove_container
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "remove_image", "Remove proxy image from servers", hide: true
|
||||||
|
def remove_image
|
||||||
|
with_lock do
|
||||||
|
on(KAMAL.proxy_hosts) do
|
||||||
|
execute *KAMAL.auditor.record("Removed proxy image"), verbosity: :debug
|
||||||
|
execute *KAMAL.proxy.remove_image
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
class Kamal::Cli::Prune < Kamal::Cli::Base
|
class Kamal::Cli::Prune < Kamal::Cli::Base
|
||||||
desc "all", "Prune unused images and stopped containers"
|
desc "all", "Prune unused images and stopped containers"
|
||||||
def all
|
def all
|
||||||
mutating do
|
with_lock do
|
||||||
containers
|
containers
|
||||||
images
|
images
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
desc "images", "Prune dangling images"
|
desc "images", "Prune unused images"
|
||||||
def images
|
def images
|
||||||
mutating do
|
with_lock do
|
||||||
on(KAMAL.hosts) do
|
on(KAMAL.hosts) do
|
||||||
execute *KAMAL.auditor.record("Pruned images"), verbosity: :debug
|
execute *KAMAL.auditor.record("Pruned images"), verbosity: :debug
|
||||||
execute *KAMAL.prune.dangling_images
|
execute *KAMAL.prune.dangling_images
|
||||||
@@ -18,12 +18,16 @@ class Kamal::Cli::Prune < Kamal::Cli::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
desc "containers", "Prune all stopped containers, except the last 5"
|
desc "containers", "Prune all stopped containers, except the last n (default 5)"
|
||||||
|
option :retain, type: :numeric, default: nil, desc: "Number of containers to retain"
|
||||||
def containers
|
def containers
|
||||||
mutating do
|
retain = options.fetch(:retain, KAMAL.config.retain_containers)
|
||||||
|
raise "retain must be at least 1" if retain < 1
|
||||||
|
|
||||||
|
with_lock do
|
||||||
on(KAMAL.hosts) do
|
on(KAMAL.hosts) do
|
||||||
execute *KAMAL.auditor.record("Pruned containers"), verbosity: :debug
|
execute *KAMAL.auditor.record("Pruned containers"), verbosity: :debug
|
||||||
execute *KAMAL.prune.containers
|
execute *KAMAL.prune.app_containers(retain: retain)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,21 +1,49 @@
|
|||||||
class Kamal::Cli::Server < Kamal::Cli::Base
|
class Kamal::Cli::Server < Kamal::Cli::Base
|
||||||
desc "bootstrap", "Set up Docker to run Kamal apps"
|
desc "exec", "Run a custom command on the server (use --help to show options)"
|
||||||
def bootstrap
|
option :interactive, type: :boolean, aliases: "-i", default: false, desc: "Run the command interactively (use for console/bash)"
|
||||||
missing = []
|
def exec(cmd)
|
||||||
|
hosts = KAMAL.hosts | KAMAL.accessory_hosts
|
||||||
|
|
||||||
on(KAMAL.hosts | KAMAL.accessory_hosts) do |host|
|
case
|
||||||
unless execute(*KAMAL.docker.installed?, raise_on_non_zero_exit: false)
|
when options[:interactive]
|
||||||
if execute(*KAMAL.docker.superuser?, raise_on_non_zero_exit: false)
|
host = KAMAL.primary_host
|
||||||
info "Missing Docker on #{host}. Installing…"
|
|
||||||
execute *KAMAL.docker.install
|
say "Running '#{cmd}' on #{host} interactively...", :magenta
|
||||||
else
|
|
||||||
missing << host
|
run_locally { exec KAMAL.server.run_over_ssh(cmd, host: host) }
|
||||||
end
|
else
|
||||||
|
say "Running '#{cmd}' on #{hosts.join(', ')}...", :magenta
|
||||||
|
|
||||||
|
on(hosts) do |host|
|
||||||
|
execute *KAMAL.auditor.record("Executed cmd '#{cmd}' on #{host}"), verbosity: :debug
|
||||||
|
puts_by_host host, capture_with_info(cmd)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
if missing.any?
|
desc "bootstrap", "Set up Docker to run Kamal apps"
|
||||||
raise "Docker is not installed on #{missing.join(", ")} and can't be automatically installed without having root access and the `curl` command available. Install Docker manually: https://docs.docker.com/engine/install/"
|
def bootstrap
|
||||||
|
with_lock do
|
||||||
|
missing = []
|
||||||
|
|
||||||
|
on(KAMAL.hosts | KAMAL.accessory_hosts) do |host|
|
||||||
|
unless execute(*KAMAL.docker.installed?, raise_on_non_zero_exit: false)
|
||||||
|
if execute(*KAMAL.docker.superuser?, raise_on_non_zero_exit: false)
|
||||||
|
info "Missing Docker on #{host}. Installing…"
|
||||||
|
execute *KAMAL.docker.install
|
||||||
|
else
|
||||||
|
missing << host
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
execute(*KAMAL.server.ensure_run_directory)
|
||||||
|
end
|
||||||
|
|
||||||
|
if missing.any?
|
||||||
|
raise "Docker is not installed on #{missing.join(", ")} and can't be automatically installed without having root access and either `wget` or `curl`. Install Docker manually: https://docs.docker.com/engine/install/"
|
||||||
|
end
|
||||||
|
|
||||||
|
run_hook "docker-setup"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ registry:
|
|||||||
- KAMAL_REGISTRY_PASSWORD
|
- KAMAL_REGISTRY_PASSWORD
|
||||||
|
|
||||||
# Inject ENV variables into containers (secrets come from .env).
|
# Inject ENV variables into containers (secrets come from .env).
|
||||||
|
# Remember to run `kamal env push` after making changes!
|
||||||
# env:
|
# env:
|
||||||
# clear:
|
# clear:
|
||||||
# DB_HOST: 192.168.0.2
|
# DB_HOST: 192.168.0.2
|
||||||
@@ -52,7 +53,7 @@ registry:
|
|||||||
# - MYSQL_ROOT_PASSWORD
|
# - MYSQL_ROOT_PASSWORD
|
||||||
# files:
|
# files:
|
||||||
# - config/mysql/production.cnf:/etc/mysql/my.cnf
|
# - config/mysql/production.cnf:/etc/mysql/my.cnf
|
||||||
# - db/production.sql.erb:/docker-entrypoint-initdb.d/setup.sql
|
# - db/production.sql:/docker-entrypoint-initdb.d/setup.sql
|
||||||
# directories:
|
# directories:
|
||||||
# - data:/var/lib/mysql
|
# - data:/var/lib/mysql
|
||||||
# redis:
|
# redis:
|
||||||
@@ -62,13 +63,28 @@ registry:
|
|||||||
# directories:
|
# directories:
|
||||||
# - data:/data
|
# - data:/data
|
||||||
|
|
||||||
# Configure custom arguments for Traefik
|
# Bridge fingerprinted assets, like JS and CSS, between versions to avoid
|
||||||
# traefik:
|
# hitting 404 on in-flight requests. Combines all files from new and old
|
||||||
# args:
|
# version inside the asset_path.
|
||||||
# accesslog: true
|
#
|
||||||
# accesslog.format: json
|
# If your app is using the Sprockets gem, ensure it sets `config.assets.manifest`.
|
||||||
|
# See https://github.com/basecamp/kamal/issues/626 for details
|
||||||
|
#
|
||||||
|
# asset_path: /rails/public/assets
|
||||||
|
|
||||||
# Configure a custom healthcheck (default is /up on port 3000)
|
# Configure rolling deploys by setting a wait time between batches of restarts.
|
||||||
# healthcheck:
|
# boot:
|
||||||
# path: /healthz
|
# limit: 10 # Can also specify as a percentage of total hosts, such as "25%"
|
||||||
# port: 4000
|
# wait: 2
|
||||||
|
|
||||||
|
# Configure the role used to determine the primary_host. This host takes
|
||||||
|
# deploy locks, runs health checks during the deploy, and follow logs, etc.
|
||||||
|
#
|
||||||
|
# Caution: there's no support for role renaming yet, so be careful to cleanup
|
||||||
|
# the previous role on the deployed hosts.
|
||||||
|
# primary_role: web
|
||||||
|
|
||||||
|
# Controls if we abort when see a role with no hosts. Disabling this may be
|
||||||
|
# useful for more complex deploy configurations.
|
||||||
|
#
|
||||||
|
# allow_empty_roles: false
|
||||||
|
|||||||
7
lib/kamal/cli/templates/sample_hooks/docker-setup.sample
Executable file
7
lib/kamal/cli/templates/sample_hooks/docker-setup.sample
Executable file
@@ -0,0 +1,7 @@
|
|||||||
|
#!/usr/bin/env ruby
|
||||||
|
|
||||||
|
# A sample docker-setup hook
|
||||||
|
#
|
||||||
|
# Sets up a Docker network which can then be used by the application’s containers
|
||||||
|
|
||||||
|
ssh user@example.com docker network create kamal
|
||||||
3
lib/kamal/cli/templates/sample_hooks/post-proxy-reboot.sample
Executable file
3
lib/kamal/cli/templates/sample_hooks/post-proxy-reboot.sample
Executable file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
echo "Rebooted proxy on $KAMAL_HOSTS"
|
||||||
@@ -32,7 +32,7 @@ fi
|
|||||||
current_branch=$(git branch --show-current)
|
current_branch=$(git branch --show-current)
|
||||||
|
|
||||||
if [ -z "$current_branch" ]; then
|
if [ -z "$current_branch" ]; then
|
||||||
echo "No git remote set, aborting..." >&2
|
echo "Not on a git branch, aborting..." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
3
lib/kamal/cli/templates/sample_hooks/pre-proxy-reboot.sample
Executable file
3
lib/kamal/cli/templates/sample_hooks/pre-proxy-reboot.sample
Executable file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
echo "Rebooting proxy on $KAMAL_HOSTS..."
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
class Kamal::Cli::Traefik < Kamal::Cli::Base
|
|
||||||
desc "boot", "Boot Traefik on servers"
|
|
||||||
def boot
|
|
||||||
mutating do
|
|
||||||
on(KAMAL.traefik_hosts) do
|
|
||||||
execute *KAMAL.registry.login
|
|
||||||
execute *KAMAL.traefik.start_or_run
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
desc "reboot", "Reboot Traefik on servers (stop container, remove container, start new container)"
|
|
||||||
option :rolling, type: :boolean, default: false, desc: "Reboot traefik on hosts in sequence, rather than in parallel"
|
|
||||||
def reboot
|
|
||||||
mutating do
|
|
||||||
on(KAMAL.traefik_hosts, in: options[:rolling] ? :sequence : :parallel) do
|
|
||||||
execute *KAMAL.auditor.record("Rebooted traefik"), verbosity: :debug
|
|
||||||
execute *KAMAL.registry.login
|
|
||||||
execute *KAMAL.traefik.stop
|
|
||||||
execute *KAMAL.traefik.remove_container
|
|
||||||
execute *KAMAL.traefik.run
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
desc "start", "Start existing Traefik container on servers"
|
|
||||||
def start
|
|
||||||
mutating do
|
|
||||||
on(KAMAL.traefik_hosts) do
|
|
||||||
execute *KAMAL.auditor.record("Started traefik"), verbosity: :debug
|
|
||||||
execute *KAMAL.traefik.start
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
desc "stop", "Stop existing Traefik container on servers"
|
|
||||||
def stop
|
|
||||||
mutating do
|
|
||||||
on(KAMAL.traefik_hosts) do
|
|
||||||
execute *KAMAL.auditor.record("Stopped traefik"), verbosity: :debug
|
|
||||||
execute *KAMAL.traefik.stop
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
desc "restart", "Restart existing Traefik container on servers"
|
|
||||||
def restart
|
|
||||||
mutating do
|
|
||||||
stop
|
|
||||||
start
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
desc "details", "Show details about Traefik container from servers"
|
|
||||||
def details
|
|
||||||
on(KAMAL.traefik_hosts) { |host| puts_by_host host, capture_with_info(*KAMAL.traefik.info), type: "Traefik" }
|
|
||||||
end
|
|
||||||
|
|
||||||
desc "logs", "Show log lines from Traefik on servers"
|
|
||||||
option :since, aliases: "-s", desc: "Show logs since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)"
|
|
||||||
option :lines, type: :numeric, aliases: "-n", desc: "Number of log lines to pull from each server"
|
|
||||||
option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)"
|
|
||||||
option :follow, aliases: "-f", desc: "Follow logs on primary server (or specific host set by --hosts)"
|
|
||||||
def logs
|
|
||||||
grep = options[:grep]
|
|
||||||
|
|
||||||
if options[:follow]
|
|
||||||
run_locally do
|
|
||||||
info "Following logs on #{KAMAL.primary_host}..."
|
|
||||||
info KAMAL.traefik.follow_logs(host: KAMAL.primary_host, grep: grep)
|
|
||||||
exec KAMAL.traefik.follow_logs(host: KAMAL.primary_host, grep: grep)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
since = options[:since]
|
|
||||||
lines = options[:lines].presence || ((since || grep) ? nil : 100) # Default to 100 lines if since or grep isn't set
|
|
||||||
|
|
||||||
on(KAMAL.traefik_hosts) do |host|
|
|
||||||
puts_by_host host, capture(*KAMAL.traefik.logs(since: since, lines: lines, grep: grep)), type: "Traefik"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
desc "remove", "Remove Traefik container and image from servers"
|
|
||||||
def remove
|
|
||||||
mutating do
|
|
||||||
stop
|
|
||||||
remove_container
|
|
||||||
remove_image
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
desc "remove_container", "Remove Traefik container from servers", hide: true
|
|
||||||
def remove_container
|
|
||||||
mutating do
|
|
||||||
on(KAMAL.traefik_hosts) do
|
|
||||||
execute *KAMAL.auditor.record("Removed traefik container"), verbosity: :debug
|
|
||||||
execute *KAMAL.traefik.remove_container
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
desc "remove_image", "Remove Traefik image from servers", hide: true
|
|
||||||
def remove_image
|
|
||||||
mutating do
|
|
||||||
on(KAMAL.traefik_hosts) do
|
|
||||||
execute *KAMAL.auditor.record("Removed traefik image"), verbosity: :debug
|
|
||||||
execute *KAMAL.traefik.remove_image
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -2,12 +2,14 @@ require "active_support/core_ext/enumerable"
|
|||||||
require "active_support/core_ext/module/delegation"
|
require "active_support/core_ext/module/delegation"
|
||||||
|
|
||||||
class Kamal::Commander
|
class Kamal::Commander
|
||||||
attr_accessor :verbosity, :holding_lock, :hold_lock_on_error
|
attr_accessor :verbosity, :holding_lock, :connected
|
||||||
|
delegate :hosts, :roles, :primary_host, :primary_role, :roles_on, :proxy_hosts, :accessory_hosts, to: :specifics
|
||||||
|
|
||||||
def initialize
|
def initialize
|
||||||
self.verbosity = :info
|
self.verbosity = :info
|
||||||
self.holding_lock = false
|
self.holding_lock = false
|
||||||
self.hold_lock_on_error = false
|
self.connected = false
|
||||||
|
@specifics = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def config
|
def config
|
||||||
@@ -24,60 +26,47 @@ class Kamal::Commander
|
|||||||
attr_reader :specific_roles, :specific_hosts
|
attr_reader :specific_roles, :specific_hosts
|
||||||
|
|
||||||
def specific_primary!
|
def specific_primary!
|
||||||
self.specific_hosts = [ config.primary_web_host ]
|
@specifics = nil
|
||||||
|
self.specific_hosts = [ config.primary_host ]
|
||||||
end
|
end
|
||||||
|
|
||||||
def specific_roles=(role_names)
|
def specific_roles=(role_names)
|
||||||
@specific_roles = config.roles.select { |r| role_names.include?(r.name) } if role_names.present?
|
@specifics = nil
|
||||||
|
if role_names.present?
|
||||||
|
@specific_roles = Kamal::Utils.filter_specific_items(role_names, config.roles)
|
||||||
|
|
||||||
|
if @specific_roles.empty?
|
||||||
|
raise ArgumentError, "No --roles match for #{role_names.join(',')}"
|
||||||
|
end
|
||||||
|
|
||||||
|
@specific_roles
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def specific_hosts=(hosts)
|
def specific_hosts=(hosts)
|
||||||
@specific_hosts = config.all_hosts & hosts if hosts.present?
|
@specifics = nil
|
||||||
end
|
if hosts.present?
|
||||||
|
@specific_hosts = Kamal::Utils.filter_specific_items(hosts, config.all_hosts)
|
||||||
|
|
||||||
def primary_host
|
if @specific_hosts.empty?
|
||||||
specific_hosts&.first || specific_roles&.first&.primary_host || config.primary_web_host
|
raise ArgumentError, "No --hosts match for #{hosts.join(',')}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def roles
|
@specific_hosts
|
||||||
(specific_roles || config.roles).select do |role|
|
|
||||||
((specific_hosts || config.all_hosts) & role.hosts).any?
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def hosts
|
|
||||||
(specific_hosts || config.all_hosts).select do |host|
|
|
||||||
(specific_roles || config.roles).flat_map(&:hosts).include?(host)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def boot_strategy
|
|
||||||
if config.boot.limit.present?
|
|
||||||
{ in: :groups, limit: config.boot.limit, wait: config.boot.wait }
|
|
||||||
else
|
|
||||||
{}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def roles_on(host)
|
|
||||||
roles.select { |role| role.hosts.include?(host.to_s) }.map(&:name)
|
|
||||||
end
|
|
||||||
|
|
||||||
def traefik_hosts
|
|
||||||
specific_hosts || config.traefik_hosts
|
|
||||||
end
|
|
||||||
|
|
||||||
def accessory_hosts
|
|
||||||
specific_hosts || config.accessories.flat_map(&:hosts)
|
|
||||||
end
|
|
||||||
|
|
||||||
def accessory_names
|
def accessory_names
|
||||||
config.accessories&.collect(&:name) || []
|
config.accessories&.collect(&:name) || []
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def accessories_on(host)
|
||||||
|
config.accessories.select { |accessory| accessory.hosts.include?(host.to_s) }.map(&:name)
|
||||||
|
end
|
||||||
|
|
||||||
def app(role: nil)
|
|
||||||
Kamal::Commands::App.new(config, role: role)
|
def app(role: nil, host: nil)
|
||||||
|
Kamal::Commands::App.new(config, role: role, host: host)
|
||||||
end
|
end
|
||||||
|
|
||||||
def accessory(name)
|
def accessory(name)
|
||||||
@@ -96,10 +85,6 @@ class Kamal::Commander
|
|||||||
@docker ||= Kamal::Commands::Docker.new(config)
|
@docker ||= Kamal::Commands::Docker.new(config)
|
||||||
end
|
end
|
||||||
|
|
||||||
def healthcheck
|
|
||||||
@healthcheck ||= Kamal::Commands::Healthcheck.new(config)
|
|
||||||
end
|
|
||||||
|
|
||||||
def hook
|
def hook
|
||||||
@hook ||= Kamal::Commands::Hook.new(config)
|
@hook ||= Kamal::Commands::Hook.new(config)
|
||||||
end
|
end
|
||||||
@@ -116,10 +101,15 @@ class Kamal::Commander
|
|||||||
@registry ||= Kamal::Commands::Registry.new(config)
|
@registry ||= Kamal::Commands::Registry.new(config)
|
||||||
end
|
end
|
||||||
|
|
||||||
def traefik
|
def server
|
||||||
@traefik ||= Kamal::Commands::Traefik.new(config)
|
@server ||= Kamal::Commands::Server.new(config)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def proxy
|
||||||
|
@proxy ||= Kamal::Commands::Proxy.new(config)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
def with_verbosity(level)
|
def with_verbosity(level)
|
||||||
old_level = self.verbosity
|
old_level = self.verbosity
|
||||||
|
|
||||||
@@ -132,12 +122,20 @@ class Kamal::Commander
|
|||||||
SSHKit.config.output_verbosity = old_level
|
SSHKit.config.output_verbosity = old_level
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def boot_strategy
|
||||||
|
if config.boot.limit.present?
|
||||||
|
{ in: :groups, limit: config.boot.limit, wait: config.boot.wait }
|
||||||
|
else
|
||||||
|
{}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def holding_lock?
|
def holding_lock?
|
||||||
self.holding_lock
|
self.holding_lock
|
||||||
end
|
end
|
||||||
|
|
||||||
def hold_lock_on_error?
|
def connected?
|
||||||
self.hold_lock_on_error
|
self.connected
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@@ -151,4 +149,8 @@ class Kamal::Commander
|
|||||||
SSHKit.config.command_map[:docker] = "docker" # No need to use /usr/bin/env, just clogs up the logs
|
SSHKit.config.command_map[:docker] = "docker" # No need to use /usr/bin/env, just clogs up the logs
|
||||||
SSHKit.config.output_verbosity = verbosity
|
SSHKit.config.output_verbosity = verbosity
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def specifics
|
||||||
|
@specifics ||= Kamal::Commander::Specifics.new(config, specific_hosts, specific_roles)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
49
lib/kamal/commander/specifics.rb
Normal file
49
lib/kamal/commander/specifics.rb
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
class Kamal::Commander::Specifics
|
||||||
|
attr_reader :primary_host, :primary_role, :hosts, :roles
|
||||||
|
delegate :stable_sort!, to: Kamal::Utils
|
||||||
|
|
||||||
|
def initialize(config, specific_hosts, specific_roles)
|
||||||
|
@config, @specific_hosts, @specific_roles = config, specific_hosts, specific_roles
|
||||||
|
|
||||||
|
@roles, @hosts = specified_roles, specified_hosts
|
||||||
|
|
||||||
|
@primary_host = specific_hosts&.first || primary_specific_role&.primary_host || config.primary_host
|
||||||
|
@primary_role = primary_or_first_role(roles_on(primary_host))
|
||||||
|
|
||||||
|
stable_sort!(roles) { |role| role == primary_role ? 0 : 1 }
|
||||||
|
stable_sort!(hosts) { |host| roles_on(host).any? { |role| role == primary_role } ? 0 : 1 }
|
||||||
|
end
|
||||||
|
|
||||||
|
def roles_on(host)
|
||||||
|
roles.select { |role| role.hosts.include?(host.to_s) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def proxy_hosts
|
||||||
|
config.proxy_hosts & specified_hosts
|
||||||
|
end
|
||||||
|
|
||||||
|
def accessory_hosts
|
||||||
|
specific_hosts || config.accessories.flat_map(&:hosts)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
attr_reader :config, :specific_hosts, :specific_roles
|
||||||
|
|
||||||
|
def primary_specific_role
|
||||||
|
primary_or_first_role(specific_roles) if specific_roles.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def primary_or_first_role(roles)
|
||||||
|
roles.detect { |role| role == config.primary_role } || roles.first
|
||||||
|
end
|
||||||
|
|
||||||
|
def specified_roles
|
||||||
|
(specific_roles || config.roles) \
|
||||||
|
.select { |role| ((specific_hosts || config.all_hosts) & role.hosts).any? }
|
||||||
|
end
|
||||||
|
|
||||||
|
def specified_hosts
|
||||||
|
(specific_hosts || config.all_hosts) \
|
||||||
|
.select { |host| (specific_roles || config.roles).flat_map(&:hosts).include?(host) }
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -86,14 +86,6 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def make_directory_for(remote_file)
|
|
||||||
make_directory Pathname.new(remote_file).dirname.to_s
|
|
||||||
end
|
|
||||||
|
|
||||||
def make_directory(path)
|
|
||||||
[ :mkdir, "-p", path ]
|
|
||||||
end
|
|
||||||
|
|
||||||
def remove_service_directory
|
def remove_service_directory
|
||||||
[ :rm, "-rf", service_name ]
|
[ :rm, "-rf", service_name ]
|
||||||
end
|
end
|
||||||
@@ -106,6 +98,14 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base
|
|||||||
docker :image, :rm, "--force", image
|
docker :image, :rm, "--force", image
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def make_env_directory
|
||||||
|
make_directory accessory_config.env.secrets_directory
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_env_file
|
||||||
|
[ :rm, "-f", accessory_config.env.secrets_file ]
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def service_filter
|
def service_filter
|
||||||
[ "--filter", "label=service=#{service_name}" ]
|
[ "--filter", "label=service=#{service_name}" ]
|
||||||
|
|||||||
@@ -1,30 +1,28 @@
|
|||||||
class Kamal::Commands::App < Kamal::Commands::Base
|
class Kamal::Commands::App < Kamal::Commands::Base
|
||||||
|
include Assets, Containers, Execution, Images, Logging
|
||||||
|
|
||||||
ACTIVE_DOCKER_STATUSES = [ :running, :restarting ]
|
ACTIVE_DOCKER_STATUSES = [ :running, :restarting ]
|
||||||
|
|
||||||
attr_reader :role
|
attr_reader :role, :host
|
||||||
|
|
||||||
def initialize(config, role: nil)
|
def initialize(config, role: nil, host: nil)
|
||||||
super(config)
|
super(config)
|
||||||
@role = role
|
@role = role
|
||||||
end
|
@host = host
|
||||||
|
|
||||||
def start_or_run(hostname: nil)
|
|
||||||
combine start, run(hostname: hostname), by: "||"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def run(hostname: nil)
|
def run(hostname: nil)
|
||||||
role = config.role(self.role)
|
|
||||||
|
|
||||||
docker :run,
|
docker :run,
|
||||||
"--detach",
|
"--detach",
|
||||||
"--restart unless-stopped",
|
"--restart unless-stopped",
|
||||||
"--name", container_name,
|
"--name", container_name,
|
||||||
*(["--hostname", hostname] if hostname),
|
*([ "--hostname", hostname ] if hostname),
|
||||||
"-e", "KAMAL_CONTAINER_NAME=\"#{container_name}\"",
|
"-e", "KAMAL_CONTAINER_NAME=\"#{container_name}\"",
|
||||||
*role.env_args,
|
"-e", "KAMAL_VERSION=\"#{config.version}\"",
|
||||||
*role.health_check_args,
|
*role.env_args(host),
|
||||||
*config.logging_args,
|
*role.logging_args,
|
||||||
*config.volume_args,
|
*config.volume_args,
|
||||||
|
*role.asset_volume_args,
|
||||||
*role.label_args,
|
*role.label_args,
|
||||||
*role.option_args,
|
*role.option_args,
|
||||||
config.absolute_image,
|
config.absolute_image,
|
||||||
@@ -50,117 +48,70 @@ class Kamal::Commands::App < Kamal::Commands::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
def logs(since: nil, lines: nil, grep: nil)
|
|
||||||
pipe \
|
|
||||||
current_running_container_id,
|
|
||||||
"xargs docker logs#{" --since #{since}" if since}#{" --tail #{lines}" if lines} 2>&1",
|
|
||||||
("grep '#{grep}'" if grep)
|
|
||||||
end
|
|
||||||
|
|
||||||
def follow_logs(host:, grep: nil)
|
|
||||||
run_over_ssh \
|
|
||||||
pipe(
|
|
||||||
current_running_container_id,
|
|
||||||
"xargs docker logs --timestamps --tail 10 --follow 2>&1",
|
|
||||||
(%(grep "#{grep}") if grep)
|
|
||||||
),
|
|
||||||
host: host
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
def execute_in_existing_container(*command, interactive: false)
|
|
||||||
docker :exec,
|
|
||||||
("-it" if interactive),
|
|
||||||
container_name,
|
|
||||||
*command
|
|
||||||
end
|
|
||||||
|
|
||||||
def execute_in_new_container(*command, interactive: false)
|
|
||||||
role = config.role(self.role)
|
|
||||||
|
|
||||||
docker :run,
|
|
||||||
("-it" if interactive),
|
|
||||||
"--rm",
|
|
||||||
*config.env_args,
|
|
||||||
*config.volume_args,
|
|
||||||
*role&.option_args,
|
|
||||||
config.absolute_image,
|
|
||||||
*command
|
|
||||||
end
|
|
||||||
|
|
||||||
def execute_in_existing_container_over_ssh(*command, host:)
|
|
||||||
run_over_ssh execute_in_existing_container(*command, interactive: true), host: host
|
|
||||||
end
|
|
||||||
|
|
||||||
def execute_in_new_container_over_ssh(*command, host:)
|
|
||||||
run_over_ssh execute_in_new_container(*command, interactive: true), host: host
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
def current_running_container_id
|
def current_running_container_id
|
||||||
docker :ps, "--quiet", *filter_args(statuses: ACTIVE_DOCKER_STATUSES), "--latest"
|
current_running_container(format: "--quiet")
|
||||||
end
|
end
|
||||||
|
|
||||||
def container_id_for_version(version, only_running: false)
|
def container_id_for_version(version, only_running: false)
|
||||||
container_id_for(container_name: container_name(version), only_running: only_running)
|
container_id_for(container_name: container_name(version), only_running: only_running)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def container_name(version = nil)
|
||||||
|
[ role.container_prefix, version || config.version ].compact.join("-")
|
||||||
|
end
|
||||||
|
|
||||||
def current_running_version
|
def current_running_version
|
||||||
list_versions("--latest", statuses: ACTIVE_DOCKER_STATUSES)
|
pipe \
|
||||||
|
current_running_container(format: "--format '{{.Names}}'"),
|
||||||
|
extract_version_from_name
|
||||||
end
|
end
|
||||||
|
|
||||||
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, *filter_args(statuses: statuses), *docker_args, "--format", '"{{.Names}}"'),
|
||||||
%(while read line; do echo ${line##{service_role_dest}-}; done) # Extract SHA from "service-role-dest-SHA"
|
extract_version_from_name
|
||||||
end
|
end
|
||||||
|
|
||||||
def list_containers
|
|
||||||
docker :container, :ls, "--all", *filter_args
|
def make_env_directory
|
||||||
|
make_directory role.env(host).secrets_directory
|
||||||
end
|
end
|
||||||
|
|
||||||
def list_container_names
|
def remove_env_file
|
||||||
[ *list_containers, "--format", "'{{ .Names }}'" ]
|
[ :rm, "-f", role.env(host).secrets_file ]
|
||||||
end
|
|
||||||
|
|
||||||
def remove_container(version:)
|
|
||||||
pipe \
|
|
||||||
container_id_for(container_name: container_name(version)),
|
|
||||||
xargs(docker(:container, :rm))
|
|
||||||
end
|
|
||||||
|
|
||||||
def rename_container(version:, new_version:)
|
|
||||||
docker :rename, container_name(version), container_name(new_version)
|
|
||||||
end
|
|
||||||
|
|
||||||
def remove_containers
|
|
||||||
docker :container, :prune, "--force", *filter_args
|
|
||||||
end
|
|
||||||
|
|
||||||
def list_images
|
|
||||||
docker :image, :ls, config.repository
|
|
||||||
end
|
|
||||||
|
|
||||||
def remove_images
|
|
||||||
docker :image, :prune, "--all", "--force", *filter_args
|
|
||||||
end
|
|
||||||
|
|
||||||
def tag_current_as_latest
|
|
||||||
docker :tag, config.absolute_image, config.latest_image
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
private
|
private
|
||||||
def container_name(version = nil)
|
def container_name(version = nil)
|
||||||
[ config.service, role, config.destination, version || config.version ].compact.join("-")
|
[ role.container_prefix, version || config.version ].compact.join("-")
|
||||||
|
end
|
||||||
|
|
||||||
|
def latest_image_id
|
||||||
|
docker :image, :ls, *argumentize("--filter", "reference=#{config.latest_image}"), "--format", "'{{.ID}}'"
|
||||||
|
end
|
||||||
|
|
||||||
|
def current_running_container(format:)
|
||||||
|
pipe \
|
||||||
|
shell(chain(latest_image_container(format: format), latest_container(format: format))),
|
||||||
|
[ :head, "-1" ]
|
||||||
|
end
|
||||||
|
|
||||||
|
def latest_image_container(format:)
|
||||||
|
latest_container format: format, filters: [ "ancestor=$(#{latest_image_id.join(" ")})" ]
|
||||||
|
end
|
||||||
|
|
||||||
|
def latest_container(format:, filters: nil)
|
||||||
|
docker :ps, "--latest", *format, *filter_args(statuses: ACTIVE_DOCKER_STATUSES), argumentize("--filter", filters)
|
||||||
end
|
end
|
||||||
|
|
||||||
def filter_args(statuses: nil)
|
def filter_args(statuses: nil)
|
||||||
argumentize "--filter", filters(statuses: statuses)
|
argumentize "--filter", filters(statuses: statuses)
|
||||||
end
|
end
|
||||||
|
|
||||||
def service_role_dest
|
def extract_version_from_name
|
||||||
[config.service, role, config.destination].compact.join("-")
|
# Extract SHA from "service-role-dest-SHA"
|
||||||
|
%(while read line; do echo ${line##{role.container_prefix}-}; done)
|
||||||
end
|
end
|
||||||
|
|
||||||
def filters(statuses: nil)
|
def filters(statuses: nil)
|
||||||
|
|||||||
51
lib/kamal/commands/app/assets.rb
Normal file
51
lib/kamal/commands/app/assets.rb
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
module Kamal::Commands::App::Assets
|
||||||
|
def extract_assets
|
||||||
|
asset_container = "#{role.container_prefix}-assets"
|
||||||
|
|
||||||
|
combine \
|
||||||
|
make_directory(role.asset_extracted_path),
|
||||||
|
[ *docker(:stop, "-t 1", asset_container, "2> /dev/null"), "|| true" ],
|
||||||
|
docker(:run, "--name", asset_container, "--detach", "--rm", config.absolute_image, "sleep 1000000"),
|
||||||
|
docker(:cp, "-L", "#{asset_container}:#{role.asset_path}/.", role.asset_extracted_path),
|
||||||
|
docker(:stop, "-t 1", asset_container),
|
||||||
|
by: "&&"
|
||||||
|
end
|
||||||
|
|
||||||
|
def sync_asset_volumes(old_version: nil)
|
||||||
|
new_extracted_path, new_volume_path = role.asset_extracted_path(config.version), role.asset_volume.host_path
|
||||||
|
if old_version.present?
|
||||||
|
old_extracted_path, old_volume_path = role.asset_extracted_path(old_version), role.asset_volume(old_version).host_path
|
||||||
|
end
|
||||||
|
|
||||||
|
commands = [ make_directory(new_volume_path), copy_contents(new_extracted_path, new_volume_path) ]
|
||||||
|
|
||||||
|
if old_version.present?
|
||||||
|
commands << copy_contents(new_extracted_path, old_volume_path, continue_on_error: true)
|
||||||
|
commands << copy_contents(old_extracted_path, new_volume_path, continue_on_error: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
chain *commands
|
||||||
|
end
|
||||||
|
|
||||||
|
def clean_up_assets
|
||||||
|
chain \
|
||||||
|
find_and_remove_older_siblings(role.asset_extracted_path),
|
||||||
|
find_and_remove_older_siblings(role.asset_volume_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def find_and_remove_older_siblings(path)
|
||||||
|
[
|
||||||
|
:find,
|
||||||
|
Pathname.new(path).dirname.to_s,
|
||||||
|
"-maxdepth 1",
|
||||||
|
"-name", "'#{role.container_prefix}-*'",
|
||||||
|
"!", "-name", Pathname.new(path).basename.to_s,
|
||||||
|
"-exec rm -rf \"{}\" +"
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
def copy_contents(source, destination, continue_on_error: false)
|
||||||
|
[ :cp, "-rnT", "#{source}", destination, *("|| true" if continue_on_error) ]
|
||||||
|
end
|
||||||
|
end
|
||||||
32
lib/kamal/commands/app/containers.rb
Normal file
32
lib/kamal/commands/app/containers.rb
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
module Kamal::Commands::App::Containers
|
||||||
|
DOCKER_HEALTH_LOG_FORMAT = "'{{json .State.Health}}'"
|
||||||
|
|
||||||
|
def list_containers
|
||||||
|
docker :container, :ls, "--all", *filter_args
|
||||||
|
end
|
||||||
|
|
||||||
|
def list_container_names
|
||||||
|
[ *list_containers, "--format", "'{{ .Names }}'" ]
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_container(version:)
|
||||||
|
pipe \
|
||||||
|
container_id_for(container_name: container_name(version)),
|
||||||
|
xargs(docker(:container, :rm))
|
||||||
|
end
|
||||||
|
|
||||||
|
def rename_container(version:, new_version:)
|
||||||
|
docker :rename, container_name(version), container_name(new_version)
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_containers
|
||||||
|
docker :container, :prune, "--force", *filter_args
|
||||||
|
end
|
||||||
|
|
||||||
|
def container_endpoint(version:)
|
||||||
|
pipe \
|
||||||
|
container_id_for(container_name: container_name(version)),
|
||||||
|
xargs(docker(:inspect, "--format", "'{{.NetworkSettings.IPAddress}}{{range $k, $v := .NetworkSettings.Ports}}{{printf \":%s\" $k}}{{break}}{{end}}'")),
|
||||||
|
[ :sed, "-e", "'s/\\/tcp$//'" ]
|
||||||
|
end
|
||||||
|
end
|
||||||
29
lib/kamal/commands/app/execution.rb
Normal file
29
lib/kamal/commands/app/execution.rb
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
module Kamal::Commands::App::Execution
|
||||||
|
def execute_in_existing_container(*command, interactive: false, env:)
|
||||||
|
docker :exec,
|
||||||
|
("-it" if interactive),
|
||||||
|
*argumentize("--env", env),
|
||||||
|
container_name,
|
||||||
|
*command
|
||||||
|
end
|
||||||
|
|
||||||
|
def execute_in_new_container(*command, interactive: false, env:)
|
||||||
|
docker :run,
|
||||||
|
("-it" if interactive),
|
||||||
|
"--rm",
|
||||||
|
*role&.env_args(host),
|
||||||
|
*argumentize("--env", env),
|
||||||
|
*config.volume_args,
|
||||||
|
*role&.option_args,
|
||||||
|
config.absolute_image,
|
||||||
|
*command
|
||||||
|
end
|
||||||
|
|
||||||
|
def execute_in_existing_container_over_ssh(*command, env:)
|
||||||
|
run_over_ssh execute_in_existing_container(*command, interactive: true, env: env), host: host
|
||||||
|
end
|
||||||
|
|
||||||
|
def execute_in_new_container_over_ssh(*command, env:)
|
||||||
|
run_over_ssh execute_in_new_container(*command, interactive: true, env: env), host: host
|
||||||
|
end
|
||||||
|
end
|
||||||
13
lib/kamal/commands/app/images.rb
Normal file
13
lib/kamal/commands/app/images.rb
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
module Kamal::Commands::App::Images
|
||||||
|
def list_images
|
||||||
|
docker :image, :ls, config.repository
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_images
|
||||||
|
docker :image, :prune, "--all", "--force", *filter_args
|
||||||
|
end
|
||||||
|
|
||||||
|
def tag_latest_image
|
||||||
|
docker :tag, config.absolute_image, config.latest_image
|
||||||
|
end
|
||||||
|
end
|
||||||
18
lib/kamal/commands/app/logging.rb
Normal file
18
lib/kamal/commands/app/logging.rb
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
module Kamal::Commands::App::Logging
|
||||||
|
def logs(version: nil, since: nil, lines: nil, grep: nil)
|
||||||
|
pipe \
|
||||||
|
version ? container_id_for_version(version) : current_running_container_id,
|
||||||
|
"xargs docker logs#{" --since #{since}" if since}#{" --tail #{lines}" if lines} 2>&1",
|
||||||
|
("grep '#{grep}'" if grep)
|
||||||
|
end
|
||||||
|
|
||||||
|
def follow_logs(host:, lines: nil, grep: nil)
|
||||||
|
run_over_ssh \
|
||||||
|
pipe(
|
||||||
|
current_running_container_id,
|
||||||
|
"xargs docker logs --timestamps#{" --tail #{lines}" if lines} --follow 2>&1",
|
||||||
|
(%(grep "#{grep}") if grep)
|
||||||
|
),
|
||||||
|
host: host
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -19,7 +19,9 @@ class Kamal::Commands::Auditor < Kamal::Commands::Base
|
|||||||
|
|
||||||
private
|
private
|
||||||
def audit_log_file
|
def audit_log_file
|
||||||
[ "kamal", config.service, config.destination, "audit.log" ].compact.join("-")
|
file = [ config.service, config.destination, "audit.log" ].compact.join("-")
|
||||||
|
|
||||||
|
File.join(config.run_directory, file)
|
||||||
end
|
end
|
||||||
|
|
||||||
def audit_tags(**details)
|
def audit_tags(**details)
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ module Kamal::Commands
|
|||||||
delegate :sensitive, :argumentize, to: Kamal::Utils
|
delegate :sensitive, :argumentize, to: Kamal::Utils
|
||||||
|
|
||||||
DOCKER_HEALTH_STATUS_FORMAT = "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'"
|
DOCKER_HEALTH_STATUS_FORMAT = "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'"
|
||||||
DOCKER_HEALTH_LOG_FORMAT = "'{{json .State.Health}}'"
|
|
||||||
|
|
||||||
attr_accessor :config
|
attr_accessor :config
|
||||||
|
|
||||||
@@ -18,7 +17,7 @@ module Kamal::Commands
|
|||||||
elsif config.ssh.proxy && config.ssh.proxy.is_a?(Net::SSH::Proxy::Command)
|
elsif config.ssh.proxy && config.ssh.proxy.is_a?(Net::SSH::Proxy::Command)
|
||||||
cmd << " -o ProxyCommand='#{config.ssh.proxy.command_line_template}'"
|
cmd << " -o ProxyCommand='#{config.ssh.proxy.command_line_template}'"
|
||||||
end
|
end
|
||||||
cmd << " -t #{config.ssh.user}@#{host} '#{command.join(" ")}'"
|
cmd << " -t #{config.ssh.user}@#{host} -p #{config.ssh.port} '#{command.join(" ").gsub("'", "'\\\\''")}'"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -26,6 +25,18 @@ module Kamal::Commands
|
|||||||
docker :container, :ls, *("--all" unless only_running), "--filter", "name=^#{container_name}$", "--quiet"
|
docker :container, :ls, *("--all" unless only_running), "--filter", "name=^#{container_name}$", "--quiet"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def make_directory_for(remote_file)
|
||||||
|
make_directory Pathname.new(remote_file).dirname.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
def make_directory(path)
|
||||||
|
[ :mkdir, "-p", path ]
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_directory(path)
|
||||||
|
[ :rm, "-r", path ]
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def combine(*commands, by: "&&")
|
def combine(*commands, by: "&&")
|
||||||
commands
|
commands
|
||||||
@@ -50,14 +61,26 @@ module Kamal::Commands
|
|||||||
combine *commands, by: ">"
|
combine *commands, by: ">"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def any(*commands)
|
||||||
|
combine *commands, by: "||"
|
||||||
|
end
|
||||||
|
|
||||||
def xargs(command)
|
def xargs(command)
|
||||||
[ :xargs, command ].flatten
|
[ :xargs, command ].flatten
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def shell(command)
|
||||||
|
[ :sh, "-c", "'#{command.flatten.join(" ").gsub("'", "'\\\\''")}'" ]
|
||||||
|
end
|
||||||
|
|
||||||
def docker(*args)
|
def docker(*args)
|
||||||
args.compact.unshift :docker
|
args.compact.unshift :docker
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def git(*args, path: nil)
|
||||||
|
[ :git, *([ "-C", path ] if path), *args.compact ]
|
||||||
|
end
|
||||||
|
|
||||||
def tags(**details)
|
def tags(**details)
|
||||||
Kamal::Tags.from_config(config, **details)
|
Kamal::Tags.from_config(config, **details)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
|
require "active_support/core_ext/string/filters"
|
||||||
|
|
||||||
class Kamal::Commands::Builder < Kamal::Commands::Base
|
class Kamal::Commands::Builder < Kamal::Commands::Base
|
||||||
delegate :create, :remove, :push, :clean, :pull, :info, to: :target
|
delegate :create, :remove, :push, :clean, :pull, :info, :validate_image, to: :target
|
||||||
|
|
||||||
|
include Clone
|
||||||
|
|
||||||
def name
|
def name
|
||||||
target.class.to_s.remove("Kamal::Commands::Builder::").underscore.inquiry
|
target.class.to_s.remove("Kamal::Commands::Builder::").underscore.inquiry
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
|
|||||||
class BuilderError < StandardError; end
|
class BuilderError < StandardError; end
|
||||||
|
|
||||||
delegate :argumentize, to: Kamal::Utils
|
delegate :argumentize, to: Kamal::Utils
|
||||||
delegate :args, :secrets, :dockerfile, :local_arch, :local_host, :remote_arch, :remote_host, :cache_from, :cache_to, to: :builder_config
|
delegate :args, :secrets, :dockerfile, :target, :local_arch, :local_host, :remote_arch, :remote_host, :cache_from, :cache_to, :ssh, to: :builder_config
|
||||||
|
|
||||||
def clean
|
def clean
|
||||||
docker :image, :rm, "--force", config.absolute_image
|
docker :image, :rm, "--force", config.absolute_image
|
||||||
@@ -14,13 +14,22 @@ 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_tags, *build_cache, *build_labels, *build_args, *build_secrets, *build_dockerfile, *build_target, *build_ssh ]
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_context
|
def build_context
|
||||||
config.builder.context
|
config.builder.context
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def validate_image
|
||||||
|
pipe \
|
||||||
|
docker(:inspect, "-f", "'{{ .Config.Labels.service }}'", config.absolute_image),
|
||||||
|
any(
|
||||||
|
[ :grep, "-x", config.service ],
|
||||||
|
"(echo \"Image #{config.absolute_image} is missing the 'service' label\" && exit 1)"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
private
|
private
|
||||||
def build_tags
|
def build_tags
|
||||||
@@ -29,8 +38,8 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
|
|||||||
|
|
||||||
def build_cache
|
def build_cache
|
||||||
if cache_to && cache_from
|
if cache_to && cache_from
|
||||||
["--cache-to", cache_to,
|
[ "--cache-to", cache_to,
|
||||||
"--cache-from", cache_from]
|
"--cache-from", cache_from ]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -54,6 +63,14 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def build_target
|
||||||
|
argumentize "--target", target if target.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_ssh
|
||||||
|
argumentize "--ssh", ssh if ssh.present?
|
||||||
|
end
|
||||||
|
|
||||||
def builder_config
|
def builder_config
|
||||||
config.builder
|
config.builder
|
||||||
end
|
end
|
||||||
|
|||||||
28
lib/kamal/commands/builder/clone.rb
Normal file
28
lib/kamal/commands/builder/clone.rb
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
module Kamal::Commands::Builder::Clone
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
delegate :clone_directory, :build_directory, to: :"config.builder"
|
||||||
|
end
|
||||||
|
|
||||||
|
def clone
|
||||||
|
git :clone, Kamal::Git.root, path: clone_directory
|
||||||
|
end
|
||||||
|
|
||||||
|
def clone_reset_steps
|
||||||
|
[
|
||||||
|
git(:remote, "set-url", :origin, Kamal::Git.root, path: build_directory),
|
||||||
|
git(:fetch, :origin, path: build_directory),
|
||||||
|
git(:reset, "--hard", Kamal::Git.revision, path: build_directory),
|
||||||
|
git(:clean, "-fdx", path: build_directory)
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
def clone_status
|
||||||
|
git :status, "--porcelain", path: build_directory
|
||||||
|
end
|
||||||
|
|
||||||
|
def clone_revision
|
||||||
|
git :"rev-parse", :HEAD, path: build_directory
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -7,23 +7,31 @@ class Kamal::Commands::Builder::Multiarch < Kamal::Commands::Builder::Base
|
|||||||
docker :buildx, :rm, builder_name
|
docker :buildx, :rm, builder_name
|
||||||
end
|
end
|
||||||
|
|
||||||
def push
|
|
||||||
docker :buildx, :build,
|
|
||||||
"--push",
|
|
||||||
"--platform", "linux/amd64,linux/arm64",
|
|
||||||
"--builder", builder_name,
|
|
||||||
*build_options,
|
|
||||||
build_context
|
|
||||||
end
|
|
||||||
|
|
||||||
def info
|
def info
|
||||||
combine \
|
combine \
|
||||||
docker(:context, :ls),
|
docker(:context, :ls),
|
||||||
docker(:buildx, :ls)
|
docker(:buildx, :ls)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def push
|
||||||
|
docker :buildx, :build,
|
||||||
|
"--push",
|
||||||
|
"--platform", platform_names,
|
||||||
|
"--builder", builder_name,
|
||||||
|
*build_options,
|
||||||
|
build_context
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def builder_name
|
def builder_name
|
||||||
"kamal-#{config.service}-multiarch"
|
"kamal-#{config.service}-multiarch"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def platform_names
|
||||||
|
if local_arch
|
||||||
|
"linux/#{local_arch}"
|
||||||
|
else
|
||||||
|
"linux/amd64,linux/arm64"
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -7,14 +7,14 @@ class Kamal::Commands::Builder::Native < Kamal::Commands::Builder::Base
|
|||||||
# No-op on native without cache
|
# No-op on native without cache
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def info
|
||||||
|
# No-op on native
|
||||||
|
end
|
||||||
|
|
||||||
def push
|
def push
|
||||||
combine \
|
combine \
|
||||||
docker(:build, *build_options, build_context),
|
docker(:build, *build_options, build_context),
|
||||||
docker(:push, config.absolute_image),
|
docker(:push, config.absolute_image),
|
||||||
docker(:push, config.latest_image)
|
docker(:push, config.latest_image)
|
||||||
end
|
end
|
||||||
|
|
||||||
def info
|
|
||||||
# No-op on native
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -11,21 +11,21 @@ class Kamal::Commands::Builder::Native::Remote < Kamal::Commands::Builder::Nativ
|
|||||||
remove_buildx
|
remove_buildx
|
||||||
end
|
end
|
||||||
|
|
||||||
def push
|
|
||||||
docker :buildx, :build,
|
|
||||||
"--push",
|
|
||||||
"--platform", platform,
|
|
||||||
"--builder", builder_name,
|
|
||||||
*build_options,
|
|
||||||
build_context
|
|
||||||
end
|
|
||||||
|
|
||||||
def info
|
def info
|
||||||
chain \
|
chain \
|
||||||
docker(:context, :ls),
|
docker(:context, :ls),
|
||||||
docker(:buildx, :ls)
|
docker(:buildx, :ls)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def push
|
||||||
|
docker :buildx, :build,
|
||||||
|
"--push",
|
||||||
|
"--platform", platform,
|
||||||
|
"--builder", builder_name,
|
||||||
|
*build_options,
|
||||||
|
build_context
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
private
|
private
|
||||||
def builder_name
|
def builder_name
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
class Kamal::Commands::Docker < Kamal::Commands::Base
|
class Kamal::Commands::Docker < Kamal::Commands::Base
|
||||||
# Install Docker using the https://github.com/docker/docker-install convenience script.
|
# Install Docker using the https://github.com/docker/docker-install convenience script.
|
||||||
def install
|
def install
|
||||||
pipe [ :curl, "-fsSL", "https://get.docker.com" ], :sh
|
pipe get_docker, :sh
|
||||||
end
|
end
|
||||||
|
|
||||||
# Checks the Docker client version. Fails if Docker is not installed.
|
# Checks the Docker client version. Fails if Docker is not installed.
|
||||||
@@ -16,6 +16,15 @@ class Kamal::Commands::Docker < Kamal::Commands::Base
|
|||||||
|
|
||||||
# Do we have superuser access to install Docker and start system services?
|
# Do we have superuser access to install Docker and start system services?
|
||||||
def superuser?
|
def superuser?
|
||||||
[ '[ "${EUID:-$(id -u)}" -eq 0 ]' ]
|
[ '[ "${EUID:-$(id -u)}" -eq 0 ] || command -v sudo >/dev/null || command -v su >/dev/null' ]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def get_docker
|
||||||
|
shell \
|
||||||
|
any \
|
||||||
|
[ :curl, "-fsSL", "https://get.docker.com" ],
|
||||||
|
[ :wget, "-O -", "https://get.docker.com" ],
|
||||||
|
[ :echo, "\"exit 1\"" ]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
class Kamal::Commands::Healthcheck < Kamal::Commands::Base
|
|
||||||
EXPOSED_PORT = 3999
|
|
||||||
|
|
||||||
def run
|
|
||||||
web = config.role(:web)
|
|
||||||
|
|
||||||
docker :run,
|
|
||||||
"--detach",
|
|
||||||
"--name", container_name_with_version,
|
|
||||||
"--publish", "#{EXPOSED_PORT}:#{config.healthcheck["port"]}",
|
|
||||||
"--label", "service=#{container_name}",
|
|
||||||
"-e", "KAMAL_CONTAINER_NAME=\"#{container_name}\"",
|
|
||||||
*web.env_args,
|
|
||||||
*web.health_check_args,
|
|
||||||
*config.volume_args,
|
|
||||||
*web.option_args,
|
|
||||||
config.absolute_image,
|
|
||||||
web.cmd
|
|
||||||
end
|
|
||||||
|
|
||||||
def status
|
|
||||||
pipe container_id, xargs(docker(:inspect, "--format", DOCKER_HEALTH_STATUS_FORMAT))
|
|
||||||
end
|
|
||||||
|
|
||||||
def container_health_log
|
|
||||||
pipe container_id, xargs(docker(:inspect, "--format", DOCKER_HEALTH_LOG_FORMAT))
|
|
||||||
end
|
|
||||||
|
|
||||||
def logs
|
|
||||||
pipe container_id, xargs(docker(:logs, "--tail", 50, "2>&1"))
|
|
||||||
end
|
|
||||||
|
|
||||||
def stop
|
|
||||||
pipe container_id, xargs(docker(:stop))
|
|
||||||
end
|
|
||||||
|
|
||||||
def remove
|
|
||||||
pipe container_id, xargs(docker(:container, :rm))
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
def container_name
|
|
||||||
[ "healthcheck", config.service, config.destination ].compact.join("-")
|
|
||||||
end
|
|
||||||
|
|
||||||
def container_name_with_version
|
|
||||||
"#{container_name}-#{config.version}"
|
|
||||||
end
|
|
||||||
|
|
||||||
def container_id
|
|
||||||
container_id_for(container_name: container_name_with_version)
|
|
||||||
end
|
|
||||||
|
|
||||||
def health_url
|
|
||||||
"http://localhost:#{EXPOSED_PORT}#{config.healthcheck["path"]}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -9,6 +9,6 @@ class Kamal::Commands::Hook < Kamal::Commands::Base
|
|||||||
|
|
||||||
private
|
private
|
||||||
def hook_file(hook)
|
def hook_file(hook)
|
||||||
"#{config.hooks_path}/#{hook}"
|
File.join(config.hooks_path, hook)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
require "active_support/duration"
|
require "active_support/duration"
|
||||||
require "time"
|
require "time"
|
||||||
|
require "base64"
|
||||||
|
|
||||||
class Kamal::Commands::Lock < Kamal::Commands::Base
|
class Kamal::Commands::Lock < Kamal::Commands::Base
|
||||||
def acquire(message, version)
|
def acquire(message, version)
|
||||||
combine \
|
combine \
|
||||||
[:mkdir, lock_dir],
|
[ :mkdir, lock_dir ],
|
||||||
write_lock_details(message, version)
|
write_lock_details(message, version)
|
||||||
end
|
end
|
||||||
|
|
||||||
def release
|
def release
|
||||||
combine \
|
combine \
|
||||||
[:rm, lock_details_file],
|
[ :rm, lock_details_file ],
|
||||||
[:rm, "-r", lock_dir]
|
[ :rm, "-r", lock_dir ]
|
||||||
end
|
end
|
||||||
|
|
||||||
def status
|
def status
|
||||||
@@ -20,31 +21,41 @@ class Kamal::Commands::Lock < Kamal::Commands::Base
|
|||||||
read_lock_details
|
read_lock_details
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def ensure_locks_directory
|
||||||
|
[ :mkdir, "-p", locks_dir ]
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def write_lock_details(message, version)
|
def write_lock_details(message, version)
|
||||||
write \
|
write \
|
||||||
[:echo, "\"#{Base64.encode64(lock_details(message, version))}\""],
|
[ :echo, "\"#{Base64.encode64(lock_details(message, version))}\"" ],
|
||||||
lock_details_file
|
lock_details_file
|
||||||
end
|
end
|
||||||
|
|
||||||
def read_lock_details
|
def read_lock_details
|
||||||
pipe \
|
pipe \
|
||||||
[:cat, lock_details_file],
|
[ :cat, lock_details_file ],
|
||||||
[:base64, "-d"]
|
[ :base64, "-d" ]
|
||||||
end
|
end
|
||||||
|
|
||||||
def stat_lock_dir
|
def stat_lock_dir
|
||||||
write \
|
write \
|
||||||
[:stat, lock_dir],
|
[ :stat, lock_dir ],
|
||||||
"/dev/null"
|
"/dev/null"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def locks_dir
|
||||||
|
File.join(config.run_directory, "locks")
|
||||||
|
end
|
||||||
|
|
||||||
def lock_dir
|
def lock_dir
|
||||||
"kamal_lock-#{config.service}"
|
dir_name = [ config.service, config.destination ].compact.join("-")
|
||||||
|
|
||||||
|
File.join(locks_dir, dir_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
def lock_details_file
|
def lock_details_file
|
||||||
[lock_dir, :details].join("/")
|
File.join(lock_dir, "details")
|
||||||
end
|
end
|
||||||
|
|
||||||
def lock_details(message, version)
|
def lock_details(message, version)
|
||||||
@@ -56,7 +67,7 @@ class Kamal::Commands::Lock < Kamal::Commands::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def locked_by
|
def locked_by
|
||||||
`git config user.name`.strip
|
Kamal::Git.user_name
|
||||||
rescue Errno::ENOENT
|
rescue Errno::ENOENT
|
||||||
"Unknown"
|
"Unknown"
|
||||||
end
|
end
|
||||||
|
|||||||
79
lib/kamal/commands/proxy.rb
Normal file
79
lib/kamal/commands/proxy.rb
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
class Kamal::Commands::Proxy < Kamal::Commands::Base
|
||||||
|
delegate :argumentize, :optionize, to: Kamal::Utils
|
||||||
|
delegate :container_name, to: :proxy_config
|
||||||
|
|
||||||
|
attr_reader :proxy_config
|
||||||
|
|
||||||
|
def initialize(config)
|
||||||
|
super
|
||||||
|
@proxy_config = config.proxy
|
||||||
|
end
|
||||||
|
|
||||||
|
def run
|
||||||
|
docker :run,
|
||||||
|
"--name", container_name,
|
||||||
|
"--detach",
|
||||||
|
"--restart", "unless-stopped",
|
||||||
|
*proxy_config.publish_args,
|
||||||
|
"--volume", "/var/run/docker.sock:/var/run/docker.sock",
|
||||||
|
"--volume", "#{container_name}:/root/.config/kamal-proxy",
|
||||||
|
*config.logging_args,
|
||||||
|
*proxy_config.docker_options_args,
|
||||||
|
proxy_config.image
|
||||||
|
end
|
||||||
|
|
||||||
|
def start
|
||||||
|
docker :container, :start, container_name
|
||||||
|
end
|
||||||
|
|
||||||
|
def stop(name: container_name)
|
||||||
|
docker :container, :stop, name
|
||||||
|
end
|
||||||
|
|
||||||
|
def start_or_run
|
||||||
|
combine start, run, by: "||"
|
||||||
|
end
|
||||||
|
|
||||||
|
def deploy(service, target:)
|
||||||
|
optionize({ target: target })
|
||||||
|
docker :exec, container_name, "kamal-proxy", :deploy, service, *optionize({ target: target }), *proxy_config.deploy_command_args
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove(service, target:)
|
||||||
|
docker :exec, container_name, "kamal-proxy", :remove, service, *optionize({ target: target })
|
||||||
|
end
|
||||||
|
|
||||||
|
def info
|
||||||
|
docker :ps, "--filter", "name=^#{container_name}$"
|
||||||
|
end
|
||||||
|
|
||||||
|
def logs(since: nil, lines: nil, grep: nil)
|
||||||
|
pipe \
|
||||||
|
docker(:logs, container_name, (" --since #{since}" if since), (" --tail #{lines}" if lines), "--timestamps", "2>&1"),
|
||||||
|
("grep '#{grep}'" if grep)
|
||||||
|
end
|
||||||
|
|
||||||
|
def follow_logs(host:, grep: nil)
|
||||||
|
run_over_ssh pipe(
|
||||||
|
docker(:logs, container_name, "--timestamps", "--tail", "10", "--follow", "2>&1"),
|
||||||
|
(%(grep "#{grep}") if grep)
|
||||||
|
).join(" "), host: host
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_container(filter: container_filter)
|
||||||
|
docker :container, :prune, "--force", "--filter", filter
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_image(filter: image_filter)
|
||||||
|
docker :image, :prune, "--all", "--force", "--filter", filter
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def container_filter
|
||||||
|
"label=org.opencontainers.image.title=kamal-proxy"
|
||||||
|
end
|
||||||
|
|
||||||
|
def image_filter
|
||||||
|
"label=org.opencontainers.image.title=kamal-proxy"
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -3,7 +3,7 @@ require "active_support/core_ext/numeric/time"
|
|||||||
|
|
||||||
class Kamal::Commands::Prune < Kamal::Commands::Base
|
class Kamal::Commands::Prune < Kamal::Commands::Base
|
||||||
def dangling_images
|
def dangling_images
|
||||||
docker :image, :prune, "--force", "--filter", "label=service=#{config.service}", "--filter", "dangling=true"
|
docker :image, :prune, "--force", "--filter", "label=service=#{config.service}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def tagged_images
|
def tagged_images
|
||||||
@@ -13,16 +13,16 @@ class Kamal::Commands::Prune < Kamal::Commands::Base
|
|||||||
"while read image tag; do docker rmi $tag; done"
|
"while read image tag; do docker rmi $tag; done"
|
||||||
end
|
end
|
||||||
|
|
||||||
def containers(keep_last: 5)
|
def app_containers(retain:)
|
||||||
pipe \
|
pipe \
|
||||||
docker(:ps, "-q", "-a", *service_filter, *stopped_containers_filters),
|
docker(:ps, "-q", "-a", *service_filter, *stopped_containers_filters),
|
||||||
"tail -n +#{keep_last + 1}",
|
"tail -n +#{retain + 1}",
|
||||||
"while read container_id; do docker rm $container_id; done"
|
"while read container_id; do docker rm $container_id; done"
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def stopped_containers_filters
|
def stopped_containers_filters
|
||||||
[ "created", "exited", "dead" ].flat_map { |status| ["--filter", "status=#{status}"] }
|
[ "created", "exited", "dead" ].flat_map { |status| [ "--filter", "status=#{status}" ] }
|
||||||
end
|
end
|
||||||
|
|
||||||
def active_image_list
|
def active_image_list
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ class Kamal::Commands::Registry < Kamal::Commands::Base
|
|||||||
delegate :registry, to: :config
|
delegate :registry, to: :config
|
||||||
|
|
||||||
def login
|
def login
|
||||||
docker :login, registry["server"], "-u", sensitive(lookup("username")), "-p", sensitive(lookup("password"))
|
docker :login,
|
||||||
|
registry["server"],
|
||||||
|
"-u", sensitive(Kamal::Utils.escape_shell_value(lookup("username"))),
|
||||||
|
"-p", sensitive(Kamal::Utils.escape_shell_value(lookup("password")))
|
||||||
end
|
end
|
||||||
|
|
||||||
def logout
|
def logout
|
||||||
|
|||||||
5
lib/kamal/commands/server.rb
Normal file
5
lib/kamal/commands/server.rb
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
class Kamal::Commands::Server < Kamal::Commands::Base
|
||||||
|
def ensure_run_directory
|
||||||
|
[ :mkdir, "-p", config.run_directory ]
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
class Kamal::Commands::Traefik < Kamal::Commands::Base
|
|
||||||
delegate :argumentize, :argumentize_env_with_secrets, :optionize, to: Kamal::Utils
|
|
||||||
|
|
||||||
DEFAULT_IMAGE = "traefik:v2.9"
|
|
||||||
CONTAINER_PORT = 80
|
|
||||||
DEFAULT_ARGS = {
|
|
||||||
'log.level' => 'DEBUG'
|
|
||||||
}
|
|
||||||
|
|
||||||
def run
|
|
||||||
docker :run, "--name traefik",
|
|
||||||
"--detach",
|
|
||||||
"--restart", "unless-stopped",
|
|
||||||
"--publish", port,
|
|
||||||
"--volume", "/var/run/docker.sock:/var/run/docker.sock",
|
|
||||||
*env_args,
|
|
||||||
*config.logging_args,
|
|
||||||
*label_args,
|
|
||||||
*docker_options_args,
|
|
||||||
image,
|
|
||||||
"--providers.docker",
|
|
||||||
*cmd_option_args
|
|
||||||
end
|
|
||||||
|
|
||||||
def start
|
|
||||||
docker :container, :start, "traefik"
|
|
||||||
end
|
|
||||||
|
|
||||||
def stop
|
|
||||||
docker :container, :stop, "traefik"
|
|
||||||
end
|
|
||||||
|
|
||||||
def start_or_run
|
|
||||||
combine start, run, by: "||"
|
|
||||||
end
|
|
||||||
|
|
||||||
def info
|
|
||||||
docker :ps, "--filter", "name=^traefik$"
|
|
||||||
end
|
|
||||||
|
|
||||||
def logs(since: nil, lines: nil, grep: nil)
|
|
||||||
pipe \
|
|
||||||
docker(:logs, "traefik", (" --since #{since}" if since), (" --tail #{lines}" if lines), "--timestamps", "2>&1"),
|
|
||||||
("grep '#{grep}'" if grep)
|
|
||||||
end
|
|
||||||
|
|
||||||
def follow_logs(host:, grep: nil)
|
|
||||||
run_over_ssh pipe(
|
|
||||||
docker(:logs, "traefik", "--timestamps", "--tail", "10", "--follow", "2>&1"),
|
|
||||||
(%(grep "#{grep}") if grep)
|
|
||||||
).join(" "), host: host
|
|
||||||
end
|
|
||||||
|
|
||||||
def remove_container
|
|
||||||
docker :container, :prune, "--force", "--filter", "label=org.opencontainers.image.title=Traefik"
|
|
||||||
end
|
|
||||||
|
|
||||||
def remove_image
|
|
||||||
docker :image, :prune, "--all", "--force", "--filter", "label=org.opencontainers.image.title=Traefik"
|
|
||||||
end
|
|
||||||
|
|
||||||
def port
|
|
||||||
"#{host_port}:#{CONTAINER_PORT}"
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
def label_args
|
|
||||||
argumentize "--label", labels
|
|
||||||
end
|
|
||||||
|
|
||||||
def env_args
|
|
||||||
env_config = config.traefik["env"] || {}
|
|
||||||
|
|
||||||
if env_config.present?
|
|
||||||
argumentize_env_with_secrets(env_config)
|
|
||||||
else
|
|
||||||
[]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def labels
|
|
||||||
config.traefik["labels"] || []
|
|
||||||
end
|
|
||||||
|
|
||||||
def image
|
|
||||||
config.traefik.fetch("image") { DEFAULT_IMAGE }
|
|
||||||
end
|
|
||||||
|
|
||||||
def docker_options_args
|
|
||||||
optionize(config.traefik["options"] || {})
|
|
||||||
end
|
|
||||||
|
|
||||||
def cmd_option_args
|
|
||||||
if args = config.traefik["args"]
|
|
||||||
optionize DEFAULT_ARGS.merge(args), with: "="
|
|
||||||
else
|
|
||||||
optionize DEFAULT_ARGS, with: "="
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def host_port
|
|
||||||
config.traefik["host_port"] || CONTAINER_PORT
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -6,11 +6,10 @@ require "erb"
|
|||||||
require "net/ssh/proxy/jump"
|
require "net/ssh/proxy/jump"
|
||||||
|
|
||||||
class Kamal::Configuration
|
class Kamal::Configuration
|
||||||
delegate :service, :image, :servers, :env, :labels, :registry, :stop_wait_time, :hooks_path, to: :raw_config, allow_nil: true
|
delegate :service, :image, :port, :servers, :labels, :registry, :stop_wait_time, :hooks_path, :logging, to: :raw_config, allow_nil: true
|
||||||
delegate :argumentize, :argumentize_env_with_secrets, :optionize, to: Kamal::Utils
|
delegate :argumentize, :optionize, to: Kamal::Utils
|
||||||
|
|
||||||
attr_accessor :destination
|
attr_reader :destination, :raw_config
|
||||||
attr_accessor :raw_config
|
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
def create_from(config_file:, destination: nil, version: nil)
|
def create_from(config_file:, destination: nil, version: nil)
|
||||||
@@ -26,7 +25,9 @@ class Kamal::Configuration
|
|||||||
|
|
||||||
def load_config_file(file)
|
def load_config_file(file)
|
||||||
if file.exist?
|
if file.exist?
|
||||||
YAML.load(ERB.new(IO.read(file)).result).symbolize_keys
|
# Newer Psych doesn't load aliases by default
|
||||||
|
load_method = YAML.respond_to?(:unsafe_load) ? :unsafe_load : :load
|
||||||
|
YAML.send(load_method, ERB.new(IO.read(file)).result).symbolize_keys
|
||||||
else
|
else
|
||||||
raise "Configuration file not found in #{file}"
|
raise "Configuration file not found in #{file}"
|
||||||
end
|
end
|
||||||
@@ -54,7 +55,18 @@ class Kamal::Configuration
|
|||||||
end
|
end
|
||||||
|
|
||||||
def abbreviated_version
|
def abbreviated_version
|
||||||
Kamal::Utils.abbreviate_version(version)
|
if version
|
||||||
|
# Don't abbreviate <sha>_uncommitted_<etc>
|
||||||
|
if version.include?("_")
|
||||||
|
version
|
||||||
|
else
|
||||||
|
version[0...7]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def minimum_version
|
||||||
|
raw_config.minimum_version
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
@@ -76,21 +88,36 @@ class Kamal::Configuration
|
|||||||
|
|
||||||
|
|
||||||
def all_hosts
|
def all_hosts
|
||||||
roles.flat_map(&:hosts).uniq
|
(roles + accessories).flat_map(&:hosts).uniq
|
||||||
end
|
end
|
||||||
|
|
||||||
def primary_web_host
|
def primary_host
|
||||||
role(:web).primary_host
|
primary_role&.primary_host
|
||||||
end
|
end
|
||||||
|
|
||||||
def traefik_hosts
|
def primary_role_name
|
||||||
roles.select(&:running_traefik?).flat_map(&:hosts).uniq
|
raw_config.primary_role || "web"
|
||||||
end
|
end
|
||||||
|
|
||||||
def boot
|
def primary_role
|
||||||
Kamal::Configuration::Boot.new(config: self)
|
role(primary_role_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def allow_empty_roles?
|
||||||
|
raw_config.allow_empty_roles
|
||||||
|
end
|
||||||
|
|
||||||
|
def proxy_roles
|
||||||
|
roles.select(&:running_proxy?)
|
||||||
|
end
|
||||||
|
|
||||||
|
def proxy_role_names
|
||||||
|
proxy_roles.flat_map(&:name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def proxy_hosts
|
||||||
|
proxy_roles.flat_map(&:hosts).uniq
|
||||||
|
end
|
||||||
|
|
||||||
def repository
|
def repository
|
||||||
[ raw_config.registry["server"], image ].compact.join("/")
|
[ raw_config.registry["server"], image ].compact.join("/")
|
||||||
@@ -101,22 +128,26 @@ class Kamal::Configuration
|
|||||||
end
|
end
|
||||||
|
|
||||||
def latest_image
|
def latest_image
|
||||||
"#{repository}:latest"
|
"#{repository}:#{latest_tag}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def latest_tag
|
||||||
|
[ "latest", *destination ].join("-")
|
||||||
end
|
end
|
||||||
|
|
||||||
def service_with_version
|
def service_with_version
|
||||||
"#{service}-#{version}"
|
"#{service}-#{version}"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def require_destination?
|
||||||
def env_args
|
raw_config.require_destination
|
||||||
if raw_config.env.present?
|
|
||||||
argumentize_env_with_secrets(raw_config.env)
|
|
||||||
else
|
|
||||||
[]
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def retain_containers
|
||||||
|
raw_config.retain_containers || 5
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
def volume_args
|
def volume_args
|
||||||
if raw_config.volumes.present?
|
if raw_config.volumes.present?
|
||||||
argumentize "--volume", raw_config.volumes
|
argumentize "--volume", raw_config.volumes
|
||||||
@@ -126,15 +157,27 @@ class Kamal::Configuration
|
|||||||
end
|
end
|
||||||
|
|
||||||
def logging_args
|
def logging_args
|
||||||
if raw_config.logging.present?
|
if logging.present?
|
||||||
optionize({ "log-driver" => raw_config.logging["driver"] }.compact) +
|
optionize({ "log-driver" => logging["driver"] }.compact) +
|
||||||
argumentize("--log-opt", raw_config.logging["options"])
|
argumentize("--log-opt", logging["options"])
|
||||||
else
|
else
|
||||||
argumentize("--log-opt", { "max-size" => "10m" })
|
argumentize("--log-opt", { "max-size" => "10m" })
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
def boot
|
||||||
|
Kamal::Configuration::Boot.new(config: self)
|
||||||
|
end
|
||||||
|
|
||||||
|
def builder
|
||||||
|
Kamal::Configuration::Builder.new(config: self)
|
||||||
|
end
|
||||||
|
|
||||||
|
def proxy
|
||||||
|
Kamal::Configuration::Proxy.new(config: self)
|
||||||
|
end
|
||||||
|
|
||||||
def ssh
|
def ssh
|
||||||
Kamal::Configuration::Ssh.new(config: self)
|
Kamal::Configuration::Ssh.new(config: self)
|
||||||
end
|
end
|
||||||
@@ -144,65 +187,90 @@ class Kamal::Configuration
|
|||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
def healthcheck
|
|
||||||
{ "path" => "/up", "port" => 3000, "max_attempts" => 7 }.merge(raw_config.healthcheck || {})
|
|
||||||
end
|
|
||||||
|
|
||||||
def readiness_delay
|
def readiness_delay
|
||||||
raw_config.readiness_delay || 7
|
raw_config.readiness_delay || 7
|
||||||
end
|
end
|
||||||
|
|
||||||
def minimum_version
|
def run_id
|
||||||
raw_config.minimum_version
|
@run_id ||= SecureRandom.hex(16)
|
||||||
end
|
|
||||||
|
|
||||||
def valid?
|
|
||||||
ensure_required_keys_present && ensure_valid_kamal_version
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
def to_h
|
def run_directory
|
||||||
{
|
raw_config.run_directory || ".kamal"
|
||||||
roles: role_names,
|
|
||||||
hosts: all_hosts,
|
|
||||||
primary_host: primary_web_host,
|
|
||||||
version: version,
|
|
||||||
repository: repository,
|
|
||||||
absolute_image: absolute_image,
|
|
||||||
service_with_version: service_with_version,
|
|
||||||
env_args: env_args,
|
|
||||||
volume_args: volume_args,
|
|
||||||
ssh_options: ssh.to_h,
|
|
||||||
sshkit: sshkit.to_h,
|
|
||||||
builder: builder.to_h,
|
|
||||||
accessories: raw_config.accessories,
|
|
||||||
logging: logging_args,
|
|
||||||
healthcheck: healthcheck
|
|
||||||
}.compact
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def traefik
|
def run_directory_as_docker_volume
|
||||||
raw_config.traefik || {}
|
if Pathname.new(run_directory).absolute?
|
||||||
|
run_directory
|
||||||
|
else
|
||||||
|
File.join "$(pwd)", run_directory
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def hooks_path
|
def hooks_path
|
||||||
raw_config.hooks_path || ".kamal/hooks"
|
raw_config.hooks_path || ".kamal/hooks"
|
||||||
end
|
end
|
||||||
|
|
||||||
def builder
|
def asset_path
|
||||||
Kamal::Configuration::Builder.new(config: self)
|
raw_config.asset_path
|
||||||
end
|
end
|
||||||
|
|
||||||
# Will raise KeyError if any secret ENVs are missing
|
|
||||||
def ensure_env_available
|
|
||||||
env_args
|
|
||||||
roles.each(&:env_args)
|
|
||||||
|
|
||||||
true
|
def host_env_directory
|
||||||
|
File.join(run_directory, "env")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def env
|
||||||
|
raw_config.env || {}
|
||||||
|
end
|
||||||
|
|
||||||
|
def env_tags
|
||||||
|
@env_tags ||= if (tags = raw_config.env["tags"])
|
||||||
|
tags.collect { |name, config| Kamal::Configuration::Env::Tag.new(name, config: config) }
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def env_tag(name)
|
||||||
|
env_tags.detect { |t| t.name == name.to_s }
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
def valid?
|
||||||
|
ensure_destination_if_required && ensure_required_keys_present && ensure_valid_kamal_version && ensure_retain_containers_valid && ensure_valid_service_name
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_h
|
||||||
|
{
|
||||||
|
roles: role_names,
|
||||||
|
hosts: all_hosts,
|
||||||
|
primary_host: primary_host,
|
||||||
|
version: version,
|
||||||
|
repository: repository,
|
||||||
|
absolute_image: absolute_image,
|
||||||
|
service_with_version: service_with_version,
|
||||||
|
volume_args: volume_args,
|
||||||
|
ssh_options: ssh.to_h,
|
||||||
|
sshkit: sshkit.to_h,
|
||||||
|
builder: builder.to_h,
|
||||||
|
accessories: raw_config.accessories,
|
||||||
|
logging: logging_args
|
||||||
|
}.compact
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
private
|
private
|
||||||
# Will raise ArgumentError if any required config keys are missing
|
# Will raise ArgumentError if any required config keys are missing
|
||||||
|
def ensure_destination_if_required
|
||||||
|
if require_destination? && destination.nil?
|
||||||
|
raise ArgumentError, "You must specify a destination"
|
||||||
|
end
|
||||||
|
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
def ensure_required_keys_present
|
def ensure_required_keys_present
|
||||||
%i[ service image registry servers ].each do |key|
|
%i[ service image registry servers ].each do |key|
|
||||||
raise ArgumentError, "Missing required configuration for #{key}" unless raw_config[key].present?
|
raise ArgumentError, "Missing required configuration for #{key}" unless raw_config[key].present?
|
||||||
@@ -216,15 +284,31 @@ class Kamal::Configuration
|
|||||||
raise ArgumentError, "You must specify a password for the registry in config/deploy.yml (or set the ENV variable if that's used)"
|
raise ArgumentError, "You must specify a password for the registry in config/deploy.yml (or set the ENV variable if that's used)"
|
||||||
end
|
end
|
||||||
|
|
||||||
roles.each do |role|
|
unless role_names.include?(primary_role_name)
|
||||||
if role.hosts.empty?
|
raise ArgumentError, "The primary_role #{primary_role_name} isn't defined"
|
||||||
raise ArgumentError, "No servers specified for the #{role.name} role"
|
end
|
||||||
|
|
||||||
|
if primary_role.hosts.empty?
|
||||||
|
raise ArgumentError, "No servers specified for the #{primary_role.name} primary_role"
|
||||||
|
end
|
||||||
|
|
||||||
|
unless allow_empty_roles?
|
||||||
|
roles.each do |role|
|
||||||
|
if role.hosts.empty?
|
||||||
|
raise ArgumentError, "No servers specified for the #{role.name} role. You can ignore this with allow_empty_roles: true"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def ensure_valid_service_name
|
||||||
|
raise ArgumentError, "Service name can only include alphanumeric characters, hyphens, and underscores" unless raw_config[:service] =~ /^[a-z0-9_-]+$/i
|
||||||
|
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
def ensure_valid_kamal_version
|
def ensure_valid_kamal_version
|
||||||
if minimum_version && Gem::Version.new(minimum_version) > Gem::Version.new(Kamal::VERSION)
|
if minimum_version && Gem::Version.new(minimum_version) > Gem::Version.new(Kamal::VERSION)
|
||||||
raise ArgumentError, "Current version is #{Kamal::VERSION}, minimum required is #{minimum_version}"
|
raise ArgumentError, "Current version is #{Kamal::VERSION}, minimum required is #{minimum_version}"
|
||||||
@@ -233,6 +317,12 @@ class Kamal::Configuration
|
|||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def ensure_retain_containers_valid
|
||||||
|
raise ArgumentError, "Must retain at least 1 container" if retain_containers < 1
|
||||||
|
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
def role_names
|
def role_names
|
||||||
raw_config.servers.is_a?(Array) ? [ "web" ] : raw_config.servers.keys.sort
|
raw_config.servers.is_a?(Array) ? [ "web" ] : raw_config.servers.keys.sort
|
||||||
@@ -240,10 +330,11 @@ class Kamal::Configuration
|
|||||||
|
|
||||||
def git_version
|
def git_version
|
||||||
@git_version ||=
|
@git_version ||=
|
||||||
if system("git rev-parse")
|
if Kamal::Git.used?
|
||||||
uncommitted_suffix = Kamal::Utils.uncommitted_changes.present? ? "_uncommitted_#{SecureRandom.hex(8)}" : ""
|
if Kamal::Git.uncommitted_changes.present? && !builder.git_clone?
|
||||||
|
uncommitted_suffix = "_uncommitted_#{SecureRandom.hex(8)}"
|
||||||
"#{`git rev-parse HEAD`.strip}#{uncommitted_suffix}"
|
end
|
||||||
|
[ Kamal::Git.revision, uncommitted_suffix ].compact.join
|
||||||
else
|
else
|
||||||
raise "Can't use commit hash as version, no git repository found in #{Dir.pwd}"
|
raise "Can't use commit hash as version, no git repository found in #{Dir.pwd}"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
class Kamal::Configuration::Accessory
|
class Kamal::Configuration::Accessory
|
||||||
delegate :argumentize, :argumentize_env_with_secrets, :optionize, to: Kamal::Utils
|
delegate :argumentize, :optionize, to: Kamal::Utils
|
||||||
|
|
||||||
attr_accessor :name, :specifics
|
attr_accessor :name, :specifics
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@ class Kamal::Configuration::Accessory
|
|||||||
end
|
end
|
||||||
|
|
||||||
def service_name
|
def service_name
|
||||||
"#{config.service}-#{name}"
|
specifics["service"] || "#{config.service}-#{name}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def image
|
def image
|
||||||
@@ -16,7 +16,7 @@ class Kamal::Configuration::Accessory
|
|||||||
end
|
end
|
||||||
|
|
||||||
def hosts
|
def hosts
|
||||||
if (specifics.keys & ["host", "hosts", "roles"]).size != 1
|
if (specifics.keys & [ "host", "hosts", "roles" ]).size != 1
|
||||||
raise ArgumentError, "Specify one of `host`, `hosts` or `roles` for accessory `#{name}`"
|
raise ArgumentError, "Specify one of `host`, `hosts` or `roles` for accessory `#{name}`"
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -42,11 +42,13 @@ class Kamal::Configuration::Accessory
|
|||||||
end
|
end
|
||||||
|
|
||||||
def env
|
def env
|
||||||
specifics["env"] || {}
|
Kamal::Configuration::Env.from_config \
|
||||||
|
config: specifics.fetch("env", {}),
|
||||||
|
secrets_file: File.join(config.host_env_directory, "accessories", "#{service_name}.env")
|
||||||
end
|
end
|
||||||
|
|
||||||
def env_args
|
def env_args
|
||||||
argumentize_env_with_secrets env
|
env.args
|
||||||
end
|
end
|
||||||
|
|
||||||
def files
|
def files
|
||||||
@@ -58,8 +60,8 @@ class Kamal::Configuration::Accessory
|
|||||||
|
|
||||||
def directories
|
def directories
|
||||||
specifics["directories"]&.to_h do |host_to_container_mapping|
|
specifics["directories"]&.to_h do |host_to_container_mapping|
|
||||||
host_relative_path, container_path = host_to_container_mapping.split(":")
|
host_path, container_path = host_to_container_mapping.split(":")
|
||||||
[ expand_host_path(host_relative_path), container_path ]
|
[ expand_host_path(host_path), container_path ]
|
||||||
end || {}
|
end || {}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -99,10 +101,10 @@ class Kamal::Configuration::Accessory
|
|||||||
end
|
end
|
||||||
|
|
||||||
def with_clear_env_loaded
|
def with_clear_env_loaded
|
||||||
(env["clear"] || env).each { |k, v| ENV[k] = v }
|
env.clear.each { |k, v| ENV[k] = v }
|
||||||
yield
|
yield
|
||||||
ensure
|
ensure
|
||||||
(env["clear"] || env).each { |k, v| ENV.delete(k) }
|
env.clear.each { |k, v| ENV.delete(k) }
|
||||||
end
|
end
|
||||||
|
|
||||||
def read_dynamic_file(local_file)
|
def read_dynamic_file(local_file)
|
||||||
@@ -126,13 +128,17 @@ class Kamal::Configuration::Accessory
|
|||||||
|
|
||||||
def remote_directories_as_volumes
|
def remote_directories_as_volumes
|
||||||
specifics["directories"]&.collect do |host_to_container_mapping|
|
specifics["directories"]&.collect do |host_to_container_mapping|
|
||||||
host_relative_path, container_path = host_to_container_mapping.split(":")
|
host_path, container_path = host_to_container_mapping.split(":")
|
||||||
[ expand_host_path(host_relative_path), container_path ].join(":")
|
[ expand_host_path(host_path), container_path ].join(":")
|
||||||
end || []
|
end || []
|
||||||
end
|
end
|
||||||
|
|
||||||
def expand_host_path(host_relative_path)
|
def expand_host_path(host_path)
|
||||||
"#{service_data_directory}/#{host_relative_path}"
|
absolute_path?(host_path) ? host_path : File.join(service_data_directory, host_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
def absolute_path?(path)
|
||||||
|
Pathname.new(path).absolute?
|
||||||
end
|
end
|
||||||
|
|
||||||
def service_data_directory
|
def service_data_directory
|
||||||
@@ -143,7 +149,7 @@ class Kamal::Configuration::Accessory
|
|||||||
if specifics.key?("host")
|
if specifics.key?("host")
|
||||||
host = specifics["host"]
|
host = specifics["host"]
|
||||||
if host
|
if host
|
||||||
[host]
|
[ host ]
|
||||||
else
|
else
|
||||||
raise ArgumentError, "Missing host for accessory `#{name}`"
|
raise ArgumentError, "Missing host for accessory `#{name}`"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ class Kamal::Configuration::Boot
|
|||||||
limit = @options["limit"]
|
limit = @options["limit"]
|
||||||
|
|
||||||
if limit.to_s.end_with?("%")
|
if limit.to_s.end_with?("%")
|
||||||
@host_count * limit.to_i / 100
|
[ @host_count * limit.to_i / 100, 1 ].max
|
||||||
else
|
else
|
||||||
limit
|
limit
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ class Kamal::Configuration::Builder
|
|||||||
@options = config.raw_config.builder || {}
|
@options = config.raw_config.builder || {}
|
||||||
@image = config.image
|
@image = config.image
|
||||||
@server = config.registry["server"]
|
@server = config.registry["server"]
|
||||||
|
@service = config.service
|
||||||
|
@destination = config.destination
|
||||||
|
|
||||||
valid?
|
valid?
|
||||||
end
|
end
|
||||||
@@ -39,6 +41,10 @@ class Kamal::Configuration::Builder
|
|||||||
@options["dockerfile"] || "Dockerfile"
|
@options["dockerfile"] || "Dockerfile"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def target
|
||||||
|
@options["target"]
|
||||||
|
end
|
||||||
|
|
||||||
def context
|
def context
|
||||||
@options["context"] || "."
|
@options["context"] || "."
|
||||||
end
|
end
|
||||||
@@ -81,10 +87,31 @@ class Kamal::Configuration::Builder
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def ssh
|
||||||
|
@options["ssh"]
|
||||||
|
end
|
||||||
|
|
||||||
|
def git_clone?
|
||||||
|
Kamal::Git.used? && @options["context"].nil?
|
||||||
|
end
|
||||||
|
|
||||||
|
def clone_directory
|
||||||
|
@clone_directory ||= File.join Dir.tmpdir, "kamal-clones", [ @service, pwd_sha ].compact.join("-")
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_directory
|
||||||
|
@build_directory ||=
|
||||||
|
if git_clone?
|
||||||
|
File.join clone_directory, repo_basename, repo_relative_pwd
|
||||||
|
else
|
||||||
|
"."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def valid?
|
def valid?
|
||||||
if @options["cache"] && @options["cache"]["type"]
|
if @options["cache"] && @options["cache"]["type"]
|
||||||
raise ArgumentError, "Invalid cache type: #{@options["cache"]["type"]}" unless ["gha", "registry"].include?(@options["cache"]["type"])
|
raise ArgumentError, "Invalid cache type: #{@options["cache"]["type"]}" unless [ "gha", "registry" ].include?(@options["cache"]["type"])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -105,10 +132,22 @@ class Kamal::Configuration::Builder
|
|||||||
end
|
end
|
||||||
|
|
||||||
def cache_to_config_for_gha
|
def cache_to_config_for_gha
|
||||||
[ "type=gha", @options["cache"]&.fetch("options", nil)].compact.join(",")
|
[ "type=gha", @options["cache"]&.fetch("options", nil) ].compact.join(",")
|
||||||
end
|
end
|
||||||
|
|
||||||
def cache_to_config_for_registry
|
def cache_to_config_for_registry
|
||||||
[ "type=registry", @options["cache"]&.fetch("options", nil), "ref=#{cache_image_ref}" ].compact.join(",")
|
[ "type=registry", @options["cache"]&.fetch("options", nil), "ref=#{cache_image_ref}" ].compact.join(",")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def repo_basename
|
||||||
|
File.basename(Kamal::Git.root)
|
||||||
|
end
|
||||||
|
|
||||||
|
def repo_relative_pwd
|
||||||
|
Dir.pwd.delete_prefix(Kamal::Git.root)
|
||||||
|
end
|
||||||
|
|
||||||
|
def pwd_sha
|
||||||
|
Digest::SHA256.hexdigest(Dir.pwd)[0..12]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
40
lib/kamal/configuration/env.rb
Normal file
40
lib/kamal/configuration/env.rb
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
class Kamal::Configuration::Env
|
||||||
|
attr_reader :secrets_keys, :clear, :secrets_file
|
||||||
|
delegate :argumentize, to: Kamal::Utils
|
||||||
|
|
||||||
|
def self.from_config(config:, secrets_file: nil)
|
||||||
|
secrets_keys = config.fetch("secret", [])
|
||||||
|
clear = config.fetch("clear", config.key?("secret") || config.key?("tags") ? {} : config)
|
||||||
|
|
||||||
|
new clear: clear, secrets_keys: secrets_keys, secrets_file: secrets_file
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(clear:, secrets_keys:, secrets_file:)
|
||||||
|
@clear = clear
|
||||||
|
@secrets_keys = secrets_keys
|
||||||
|
@secrets_file = secrets_file
|
||||||
|
end
|
||||||
|
|
||||||
|
def args
|
||||||
|
[ "--env-file", secrets_file, *argumentize("--env", clear) ]
|
||||||
|
end
|
||||||
|
|
||||||
|
def secrets_io
|
||||||
|
StringIO.new(Kamal::EnvFile.new(secrets).to_s)
|
||||||
|
end
|
||||||
|
|
||||||
|
def secrets
|
||||||
|
@secrets ||= secrets_keys.to_h { |key| [ key, ENV.fetch(key) ] }
|
||||||
|
end
|
||||||
|
|
||||||
|
def secrets_directory
|
||||||
|
File.dirname(secrets_file)
|
||||||
|
end
|
||||||
|
|
||||||
|
def merge(other)
|
||||||
|
self.class.new \
|
||||||
|
clear: @clear.merge(other.clear),
|
||||||
|
secrets_keys: @secrets_keys | other.secrets_keys,
|
||||||
|
secrets_file: secrets_file
|
||||||
|
end
|
||||||
|
end
|
||||||
12
lib/kamal/configuration/env/tag.rb
vendored
Normal file
12
lib/kamal/configuration/env/tag.rb
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
class Kamal::Configuration::Env::Tag
|
||||||
|
attr_reader :name, :config
|
||||||
|
|
||||||
|
def initialize(name, config:)
|
||||||
|
@name = name
|
||||||
|
@config = config
|
||||||
|
end
|
||||||
|
|
||||||
|
def env
|
||||||
|
Kamal::Configuration::Env.from_config(config: config)
|
||||||
|
end
|
||||||
|
end
|
||||||
50
lib/kamal/configuration/proxy.rb
Normal file
50
lib/kamal/configuration/proxy.rb
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
class Kamal::Configuration::Proxy
|
||||||
|
DEFAULT_HTTP_PORT = 80
|
||||||
|
DEFAULT_HTTPS_PORT = 443
|
||||||
|
DEFAULT_IMAGE = "basecamp/kamal-proxy:latest"
|
||||||
|
|
||||||
|
delegate :argumentize, :optionize, to: Kamal::Utils
|
||||||
|
|
||||||
|
def initialize(config:)
|
||||||
|
@options = config.raw_config.proxy || {}
|
||||||
|
end
|
||||||
|
|
||||||
|
def image
|
||||||
|
options.fetch("image", DEFAULT_IMAGE)
|
||||||
|
end
|
||||||
|
|
||||||
|
def debug?
|
||||||
|
!!options[:debug]
|
||||||
|
end
|
||||||
|
|
||||||
|
def http_port
|
||||||
|
options.fetch(:http_port, DEFAULT_HTTP_PORT)
|
||||||
|
end
|
||||||
|
|
||||||
|
def https_port
|
||||||
|
options.fetch(:http_port, DEFAULT_HTTPS_PORT)
|
||||||
|
end
|
||||||
|
|
||||||
|
def container_name
|
||||||
|
"kamal-proxy"
|
||||||
|
end
|
||||||
|
|
||||||
|
def docker_options_args
|
||||||
|
optionize(options.fetch("options", {}))
|
||||||
|
end
|
||||||
|
|
||||||
|
def publish_args
|
||||||
|
argumentize "--publish", [ *("#{http_port}:#{DEFAULT_HTTP_PORT}" if http_port), *("#{https_port}:#{DEFAULT_HTTPS_PORT}" if https_port) ]
|
||||||
|
end
|
||||||
|
|
||||||
|
def deploy_options
|
||||||
|
options.fetch(:deploy, {})
|
||||||
|
end
|
||||||
|
|
||||||
|
def deploy_command_args
|
||||||
|
optionize deploy_options
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
attr_accessor :options
|
||||||
|
end
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
class Kamal::Configuration::Role
|
class Kamal::Configuration::Role
|
||||||
delegate :argumentize, :argumentize_env_with_secrets, :optionize, to: Kamal::Utils
|
delegate :argumentize, :optionize, to: Kamal::Utils
|
||||||
|
|
||||||
attr_accessor :name
|
attr_accessor :name
|
||||||
|
alias to_s name
|
||||||
|
|
||||||
def initialize(name, config:)
|
def initialize(name, config:)
|
||||||
@name, @config = name.inquiry, config
|
@name, @config = name.inquiry, config
|
||||||
|
@tagged_hosts ||= extract_tagged_hosts_from_config
|
||||||
end
|
end
|
||||||
|
|
||||||
def primary_host
|
def primary_host
|
||||||
@@ -12,49 +14,11 @@ class Kamal::Configuration::Role
|
|||||||
end
|
end
|
||||||
|
|
||||||
def hosts
|
def hosts
|
||||||
@hosts ||= extract_hosts_from_config
|
tagged_hosts.keys
|
||||||
end
|
end
|
||||||
|
|
||||||
def labels
|
def env_tags(host)
|
||||||
default_labels.merge(traefik_labels).merge(custom_labels)
|
tagged_hosts.fetch(host).collect { |tag| config.env_tag(tag) }
|
||||||
end
|
|
||||||
|
|
||||||
def label_args
|
|
||||||
argumentize "--label", labels
|
|
||||||
end
|
|
||||||
|
|
||||||
def env
|
|
||||||
if config.env && config.env["secret"]
|
|
||||||
merged_env_with_secrets
|
|
||||||
else
|
|
||||||
merged_env
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def env_args
|
|
||||||
argumentize_env_with_secrets env
|
|
||||||
end
|
|
||||||
|
|
||||||
def health_check_args
|
|
||||||
if health_check_cmd.present?
|
|
||||||
optionize({ "health-cmd" => health_check_cmd, "health-interval" => health_check_interval })
|
|
||||||
else
|
|
||||||
[]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def health_check_cmd
|
|
||||||
options = specializations["healthcheck"] || {}
|
|
||||||
options = config.healthcheck.merge(options) if running_traefik?
|
|
||||||
|
|
||||||
options["cmd"] || http_health_check(port: options["port"], path: options["path"])
|
|
||||||
end
|
|
||||||
|
|
||||||
def health_check_interval
|
|
||||||
options = specializations["healthcheck"] || {}
|
|
||||||
options = config.healthcheck.merge(options) if running_traefik?
|
|
||||||
|
|
||||||
options["interval"] || "1s"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def cmd
|
def cmd
|
||||||
@@ -69,12 +33,105 @@ class Kamal::Configuration::Role
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def running_traefik?
|
def labels
|
||||||
name.web? || specializations["traefik"]
|
default_labels.merge(custom_labels)
|
||||||
|
end
|
||||||
|
|
||||||
|
def label_args
|
||||||
|
argumentize "--label", labels
|
||||||
|
end
|
||||||
|
|
||||||
|
def logging_args
|
||||||
|
args = config.logging || {}
|
||||||
|
args.deep_merge!(specializations["logging"]) if specializations["logging"].present?
|
||||||
|
|
||||||
|
if args.any?
|
||||||
|
optionize({ "log-driver" => args["driver"] }.compact) +
|
||||||
|
argumentize("--log-opt", args["options"])
|
||||||
|
else
|
||||||
|
config.logging_args
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
def env(host)
|
||||||
|
@envs ||= {}
|
||||||
|
@envs[host] ||= [ base_env, specialized_env, *env_tags(host).map(&:env) ].reduce(:merge)
|
||||||
|
end
|
||||||
|
|
||||||
|
def env_args(host)
|
||||||
|
env(host).args
|
||||||
|
end
|
||||||
|
|
||||||
|
def asset_volume_args
|
||||||
|
asset_volume&.docker_args
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
def running_proxy?
|
||||||
|
if specializations["proxy"].nil?
|
||||||
|
primary?
|
||||||
|
else
|
||||||
|
specializations["proxy"]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def primary?
|
||||||
|
self == @config.primary_role
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
def container_name(version = nil)
|
||||||
|
[ container_prefix, version || config.version ].compact.join("-")
|
||||||
|
end
|
||||||
|
|
||||||
|
def container_prefix
|
||||||
|
[ config.service, name, config.destination ].compact.join("-")
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
def asset_path
|
||||||
|
specializations["asset_path"] || config.asset_path
|
||||||
|
end
|
||||||
|
|
||||||
|
def assets?
|
||||||
|
asset_path.present? && running_proxy?
|
||||||
|
end
|
||||||
|
|
||||||
|
def asset_volume(version = nil)
|
||||||
|
if assets?
|
||||||
|
Kamal::Configuration::Volume.new \
|
||||||
|
host_path: asset_volume_path(version), container_path: asset_path
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def asset_extracted_path(version = nil)
|
||||||
|
File.join config.run_directory, "assets", "extracted", container_name(version)
|
||||||
|
end
|
||||||
|
|
||||||
|
def asset_volume_path(version = nil)
|
||||||
|
File.join config.run_directory, "assets", "volumes", container_name(version)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
attr_accessor :config
|
attr_accessor :config, :tagged_hosts
|
||||||
|
|
||||||
|
def extract_tagged_hosts_from_config
|
||||||
|
{}.tap do |tagged_hosts|
|
||||||
|
extract_hosts_from_config.map do |host_config|
|
||||||
|
if host_config.is_a?(Hash)
|
||||||
|
raise ArgumentError, "Multiple hosts found: #{host_config.inspect}" unless host_config.size == 1
|
||||||
|
|
||||||
|
host, tags = host_config.first
|
||||||
|
tagged_hosts[host] = Array(tags)
|
||||||
|
elsif host_config.is_a?(String) || host_config.is_a?(Symbol)
|
||||||
|
tagged_hosts[host_config] = []
|
||||||
|
else
|
||||||
|
raise ArgumentError, "Invalid host config: #{host_config.inspect}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def extract_hosts_from_config
|
def extract_hosts_from_config
|
||||||
if config.servers.is_a?(Array)
|
if config.servers.is_a?(Array)
|
||||||
@@ -86,31 +143,7 @@ class Kamal::Configuration::Role
|
|||||||
end
|
end
|
||||||
|
|
||||||
def default_labels
|
def default_labels
|
||||||
if config.destination
|
{ "service" => config.service, "role" => name, "destination" => config.destination }
|
||||||
{ "service" => config.service, "role" => name, "destination" => config.destination }
|
|
||||||
else
|
|
||||||
{ "service" => config.service, "role" => name }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def traefik_labels
|
|
||||||
if running_traefik?
|
|
||||||
{
|
|
||||||
# Setting a service property ensures that the generated service name will be consistent between versions
|
|
||||||
"traefik.http.services.#{traefik_service}.loadbalancer.server.scheme" => "http",
|
|
||||||
|
|
||||||
"traefik.http.routers.#{traefik_service}.rule" => "PathPrefix(`/`)",
|
|
||||||
"traefik.http.middlewares.#{traefik_service}-retry.retry.attempts" => "5",
|
|
||||||
"traefik.http.middlewares.#{traefik_service}-retry.retry.initialinterval" => "500ms",
|
|
||||||
"traefik.http.routers.#{traefik_service}.middlewares" => "#{traefik_service}-retry@docker"
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def traefik_service
|
|
||||||
[ config.service, name, config.destination ].compact.join("-")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def custom_labels
|
def custom_labels
|
||||||
@@ -122,34 +155,20 @@ class Kamal::Configuration::Role
|
|||||||
|
|
||||||
def specializations
|
def specializations
|
||||||
if config.servers.is_a?(Array) || config.servers[name].is_a?(Array)
|
if config.servers.is_a?(Array) || config.servers[name].is_a?(Array)
|
||||||
{ }
|
{}
|
||||||
else
|
else
|
||||||
config.servers[name].except("hosts")
|
config.servers[name].except("hosts")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def specialized_env
|
def specialized_env
|
||||||
specializations["env"] || {}
|
Kamal::Configuration::Env.from_config config: specializations.fetch("env", {})
|
||||||
end
|
|
||||||
|
|
||||||
def merged_env
|
|
||||||
config.env&.merge(specialized_env) || {}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Secrets are stored in an array, which won't merge by default, so have to do it by hand.
|
# Secrets are stored in an array, which won't merge by default, so have to do it by hand.
|
||||||
def merged_env_with_secrets
|
def base_env
|
||||||
merged_env.tap do |new_env|
|
Kamal::Configuration::Env.from_config \
|
||||||
new_env["secret"] = Array(config.env["secret"]) + Array(specialized_env["secret"])
|
config: config.env,
|
||||||
|
secrets_file: File.join(config.host_env_directory, "roles", "#{container_prefix}.env")
|
||||||
# If there's no secret/clear split, everything is clear
|
|
||||||
clear_app_env = config.env["secret"] ? Array(config.env["clear"]) : Array(config.env["clear"] || config.env)
|
|
||||||
clear_role_env = specialized_env["secret"] ? Array(specialized_env["clear"]) : Array(specialized_env["clear"] || specialized_env)
|
|
||||||
|
|
||||||
new_env["clear"] = (clear_app_env + clear_role_env).uniq
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def http_health_check(port:, path:)
|
|
||||||
"curl -f #{URI.join("http://localhost:#{port}", path)} || exit 1" if path.present? || port.present?
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ class Kamal::Configuration::Ssh
|
|||||||
config.fetch("user", "root")
|
config.fetch("user", "root")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def port
|
||||||
|
config.fetch("port", 22)
|
||||||
|
end
|
||||||
|
|
||||||
def proxy
|
def proxy
|
||||||
if (proxy = config["proxy"])
|
if (proxy = config["proxy"])
|
||||||
Net::SSH::Proxy::Jump.new(proxy.include?("@") ? proxy : "root@#{proxy}")
|
Net::SSH::Proxy::Jump.new(proxy.include?("@") ? proxy : "root@#{proxy}")
|
||||||
@@ -18,7 +22,7 @@ class Kamal::Configuration::Ssh
|
|||||||
end
|
end
|
||||||
|
|
||||||
def options
|
def options
|
||||||
{ user: user, proxy: proxy, auth_methods: [ "publickey" ], logger: logger, keepalive: true, keepalive_interval: 30 }.compact
|
{ user: user, port: port, proxy: proxy, logger: logger, keepalive: true, keepalive_interval: 30 }.compact
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_h
|
def to_h
|
||||||
|
|||||||
22
lib/kamal/configuration/volume.rb
Normal file
22
lib/kamal/configuration/volume.rb
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
class Kamal::Configuration::Volume
|
||||||
|
attr_reader :host_path, :container_path
|
||||||
|
delegate :argumentize, to: Kamal::Utils
|
||||||
|
|
||||||
|
def initialize(host_path:, container_path:)
|
||||||
|
@host_path = host_path
|
||||||
|
@container_path = container_path
|
||||||
|
end
|
||||||
|
|
||||||
|
def docker_args
|
||||||
|
argumentize "--volume", "#{host_path_for_docker_volume}:#{container_path}"
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def host_path_for_docker_volume
|
||||||
|
if Pathname.new(host_path).absolute?
|
||||||
|
host_path
|
||||||
|
else
|
||||||
|
File.join "$(pwd)", host_path
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
38
lib/kamal/env_file.rb
Normal file
38
lib/kamal/env_file.rb
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# Encode an env hash as a string where secret values have been looked up and all values escaped for Docker.
|
||||||
|
class Kamal::EnvFile
|
||||||
|
def initialize(env)
|
||||||
|
@env = env
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_s
|
||||||
|
env_file = StringIO.new.tap do |contents|
|
||||||
|
@env.each do |key, value|
|
||||||
|
contents << docker_env_file_line(key, value)
|
||||||
|
end
|
||||||
|
end.string
|
||||||
|
|
||||||
|
# Ensure the file has some contents to avoid the SSHKIT empty file warning
|
||||||
|
env_file.presence || "\n"
|
||||||
|
end
|
||||||
|
|
||||||
|
alias to_str to_s
|
||||||
|
|
||||||
|
private
|
||||||
|
def docker_env_file_line(key, value)
|
||||||
|
"#{key}=#{escape_docker_env_file_value(value)}\n"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Escape a value to make it safe to dump in a docker file.
|
||||||
|
def escape_docker_env_file_value(value)
|
||||||
|
# keep non-ascii(UTF-8) characters as it is
|
||||||
|
value.to_s.scan(/[\x00-\x7F]+|[^\x00-\x7F]+/).map do |part|
|
||||||
|
part.ascii_only? ? escape_docker_env_file_ascii_value(part) : part
|
||||||
|
end.join
|
||||||
|
end
|
||||||
|
|
||||||
|
def escape_docker_env_file_ascii_value(value)
|
||||||
|
# Doublequotes are treated literally in docker env files
|
||||||
|
# so remove leading and trailing ones and unescape any others
|
||||||
|
value.to_s.dump[1..-2].gsub(/\\"/, "\"")
|
||||||
|
end
|
||||||
|
end
|
||||||
23
lib/kamal/git.rb
Normal file
23
lib/kamal/git.rb
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
module Kamal::Git
|
||||||
|
extend self
|
||||||
|
|
||||||
|
def used?
|
||||||
|
system("git rev-parse")
|
||||||
|
end
|
||||||
|
|
||||||
|
def user_name
|
||||||
|
`git config user.name`.strip
|
||||||
|
end
|
||||||
|
|
||||||
|
def revision
|
||||||
|
`git rev-parse HEAD`.strip
|
||||||
|
end
|
||||||
|
|
||||||
|
def uncommitted_changes
|
||||||
|
`git status --porcelain`.strip
|
||||||
|
end
|
||||||
|
|
||||||
|
def root
|
||||||
|
`git rev-parse --show-toplevel`.strip
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
require "sshkit"
|
require "sshkit"
|
||||||
require "sshkit/dsl"
|
require "sshkit/dsl"
|
||||||
|
require "net/scp"
|
||||||
require "active_support/core_ext/hash/deep_merge"
|
require "active_support/core_ext/hash/deep_merge"
|
||||||
require "json"
|
require "json"
|
||||||
|
|
||||||
@@ -102,3 +103,39 @@ class SSHKit::Backend::Netssh
|
|||||||
|
|
||||||
prepend LimitConcurrentStartsInstance
|
prepend LimitConcurrentStartsInstance
|
||||||
end
|
end
|
||||||
|
|
||||||
|
class SSHKit::Runner::Parallel
|
||||||
|
# SSHKit joins the threads in sequence and fails on the first error it encounters, which means that we wait threads
|
||||||
|
# before the first failure to complete but not for ones after.
|
||||||
|
#
|
||||||
|
# We'll patch it to wait for them all to complete, and to record all the threads that errored so we can see when a
|
||||||
|
# problem occurs on multiple hosts.
|
||||||
|
module CompleteAll
|
||||||
|
def execute
|
||||||
|
threads = hosts.map do |host|
|
||||||
|
Thread.new(host) do |h|
|
||||||
|
backend(h, &block).run
|
||||||
|
rescue ::StandardError => e
|
||||||
|
e2 = SSHKit::Runner::ExecuteError.new e
|
||||||
|
raise e2, "Exception while executing #{host.user ? "as #{host.user}@" : "on host "}#{host}: #{e.message}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
exceptions = []
|
||||||
|
threads.each do |t|
|
||||||
|
begin
|
||||||
|
t.join
|
||||||
|
rescue SSHKit::Runner::ExecuteError => e
|
||||||
|
exceptions << e
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if exceptions.one?
|
||||||
|
raise exceptions.first
|
||||||
|
elsif exceptions.many?
|
||||||
|
raise exceptions.first, [ "Exceptions on #{exceptions.count} hosts:", exceptions.map(&:message) ].join("\n")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
prepend CompleteAll
|
||||||
|
end
|
||||||
|
|||||||
@@ -9,23 +9,13 @@ module Kamal::Utils
|
|||||||
if value.present?
|
if value.present?
|
||||||
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 ]
|
||||||
else
|
else
|
||||||
[ argument, key ]
|
[ argument, key ]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Return a list of shell arguments using the same named argument against the passed attributes,
|
|
||||||
# but redacts and expands secrets.
|
|
||||||
def argumentize_env_with_secrets(env)
|
|
||||||
if (secrets = env["secret"]).present?
|
|
||||||
argumentize("-e", secrets.to_h { |key| [ key, ENV.fetch(key) ] }, sensitive: true) + argumentize("-e", env["clear"])
|
|
||||||
else
|
|
||||||
argumentize "-e", env.fetch("clear", env)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Returns a list of shell-dashed option arguments. If the value is true, it's treated like a value-less option.
|
# Returns a list of shell-dashed option arguments. If the value is true, it's treated like a value-less option.
|
||||||
def optionize(args, with: nil)
|
def optionize(args, with: nil)
|
||||||
options = if with
|
options = if with
|
||||||
@@ -39,7 +29,7 @@ module Kamal::Utils
|
|||||||
|
|
||||||
# Flattens a one-to-many structure into an array of two-element arrays each containing a key-value pair
|
# Flattens a one-to-many structure into an array of two-element arrays each containing a key-value pair
|
||||||
def flatten_args(args)
|
def flatten_args(args)
|
||||||
args.flat_map { |key, value| value.try(:map) { |entry| [key, entry] } || [ [ key, value ] ] }
|
args.flat_map { |key, value| value.try(:map) { |entry| [ key, entry ] } || [ [ key, value ] ] }
|
||||||
end
|
end
|
||||||
|
|
||||||
# Marks sensitive values for redaction in logs and human-visible output.
|
# Marks sensitive values for redaction in logs and human-visible output.
|
||||||
@@ -62,19 +52,6 @@ module Kamal::Utils
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def unredacted(value)
|
|
||||||
case
|
|
||||||
when value.respond_to?(:unredacted)
|
|
||||||
value.unredacted
|
|
||||||
when value.respond_to?(:transform_values)
|
|
||||||
value.transform_values { |value| unredacted value }
|
|
||||||
when value.respond_to?(:map)
|
|
||||||
value.map { |element| unredacted element }
|
|
||||||
else
|
|
||||||
value
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Escape a value to make it safe for shell use.
|
# Escape a value to make it safe for shell use.
|
||||||
def escape_shell_value(value)
|
def escape_shell_value(value)
|
||||||
value.to_s.dump
|
value.to_s.dump
|
||||||
@@ -82,19 +59,22 @@ module Kamal::Utils
|
|||||||
.gsub(DOLLAR_SIGN_WITHOUT_SHELL_EXPANSION_REGEX, '\$')
|
.gsub(DOLLAR_SIGN_WITHOUT_SHELL_EXPANSION_REGEX, '\$')
|
||||||
end
|
end
|
||||||
|
|
||||||
# Abbreviate a git revhash for concise display
|
# Apply a list of host or role filters, including wildcard matches
|
||||||
def abbreviate_version(version)
|
def filter_specific_items(filters, items)
|
||||||
if version
|
matches = []
|
||||||
# Don't abbreviate <sha>_uncommitted_<etc>
|
|
||||||
if version.include?("_")
|
Array(filters).select do |filter|
|
||||||
version
|
matches += Array(items).select do |item|
|
||||||
else
|
# Only allow * for a wildcard
|
||||||
version[0...7]
|
# items are roles or hosts
|
||||||
|
File.fnmatch(filter, item.to_s, File::FNM_EXTGLOB)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
matches.uniq
|
||||||
end
|
end
|
||||||
|
|
||||||
def uncommitted_changes
|
def stable_sort!(elements, &block)
|
||||||
`git status --porcelain`.strip
|
elements.sort_by!.with_index { |element, index| [ block.call(element), index ] }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
class Kamal::Utils::HealthcheckPoller
|
|
||||||
TRAEFIK_HEALTHY_DELAY = 2
|
|
||||||
|
|
||||||
class HealthcheckError < StandardError; end
|
|
||||||
|
|
||||||
class << self
|
|
||||||
def wait_for_healthy(pause_after_ready: false, &block)
|
|
||||||
attempt = 1
|
|
||||||
max_attempts = KAMAL.config.healthcheck["max_attempts"]
|
|
||||||
|
|
||||||
begin
|
|
||||||
case status = block.call
|
|
||||||
when "healthy"
|
|
||||||
sleep TRAEFIK_HEALTHY_DELAY if pause_after_ready
|
|
||||||
when "running" # No health check configured
|
|
||||||
sleep KAMAL.config.readiness_delay if pause_after_ready
|
|
||||||
else
|
|
||||||
raise HealthcheckError, "container not ready (#{status})"
|
|
||||||
end
|
|
||||||
rescue HealthcheckError => e
|
|
||||||
if attempt <= max_attempts
|
|
||||||
info "#{e.message}, retrying in #{attempt}s (attempt #{attempt}/#{max_attempts})..."
|
|
||||||
sleep attempt
|
|
||||||
attempt += 1
|
|
||||||
retry
|
|
||||||
else
|
|
||||||
raise
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
info "Container is healthy!"
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
def info(message)
|
|
||||||
SSHKit.config.output.info(message)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
require "active_support/core_ext/module/delegation"
|
require "active_support/core_ext/module/delegation"
|
||||||
|
require "sshkit"
|
||||||
|
|
||||||
class Kamal::Utils::Sensitive
|
class Kamal::Utils::Sensitive
|
||||||
# So SSHKit knows to redact these values.
|
# So SSHKit knows to redact these values.
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
module Kamal
|
module Kamal
|
||||||
VERSION = "0.16.0"
|
VERSION = "1.5.2"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ class CliAccessoryTest < CliTestCase
|
|||||||
|
|
||||||
run_command("boot", "mysql").tap do |output|
|
run_command("boot", "mysql").tap do |output|
|
||||||
assert_match /docker login.*on 1.1.1.3/, output
|
assert_match /docker login.*on 1.1.1.3/, output
|
||||||
assert_match "docker run --name app-mysql --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 3306:3306 -e [REDACTED] -e MYSQL_ROOT_HOST=\"%\" --volume $PWD/app-mysql/etc/mysql/my.cnf:/etc/mysql/my.cnf --volume $PWD/app-mysql/data:/var/lib/mysql --label service=\"app-mysql\" mysql:5.7 on 1.1.1.3", output
|
assert_match "docker run --name app-mysql --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 3306:3306 --env-file .kamal/env/accessories/app-mysql.env --env MYSQL_ROOT_HOST=\"%\" --volume $PWD/app-mysql/etc/mysql/my.cnf:/etc/mysql/my.cnf --volume $PWD/app-mysql/data:/var/lib/mysql --label service=\"app-mysql\" mysql:5.7 on 1.1.1.3", output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -21,9 +21,9 @@ class CliAccessoryTest < CliTestCase
|
|||||||
assert_match /docker login.*on 1.1.1.3/, output
|
assert_match /docker login.*on 1.1.1.3/, output
|
||||||
assert_match /docker login.*on 1.1.1.1/, output
|
assert_match /docker login.*on 1.1.1.1/, output
|
||||||
assert_match /docker login.*on 1.1.1.2/, output
|
assert_match /docker login.*on 1.1.1.2/, output
|
||||||
assert_match "docker run --name app-mysql --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 3306:3306 -e [REDACTED] -e MYSQL_ROOT_HOST=\"%\" --volume $PWD/app-mysql/etc/mysql/my.cnf:/etc/mysql/my.cnf --volume $PWD/app-mysql/data:/var/lib/mysql --label service=\"app-mysql\" mysql:5.7 on 1.1.1.3", output
|
assert_match "docker run --name app-mysql --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 3306:3306 --env-file .kamal/env/accessories/app-mysql.env --env MYSQL_ROOT_HOST=\"%\" --volume $PWD/app-mysql/etc/mysql/my.cnf:/etc/mysql/my.cnf --volume $PWD/app-mysql/data:/var/lib/mysql --label service=\"app-mysql\" mysql:5.7 on 1.1.1.3", output
|
||||||
assert_match "docker run --name app-redis --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 6379:6379 --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.1", output
|
assert_match "docker run --name app-redis --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/env/accessories/app-redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.1", output
|
||||||
assert_match "docker run --name app-redis --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 6379:6379 --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.2", output
|
assert_match "docker run --name app-redis --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/env/accessories/app-redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.2", output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -48,6 +48,18 @@ class CliAccessoryTest < CliTestCase
|
|||||||
run_command("reboot", "mysql")
|
run_command("reboot", "mysql")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "reboot all" do
|
||||||
|
Kamal::Commands::Registry.any_instance.expects(:login).times(3)
|
||||||
|
Kamal::Cli::Accessory.any_instance.expects(:stop).with("mysql")
|
||||||
|
Kamal::Cli::Accessory.any_instance.expects(:remove_container).with("mysql")
|
||||||
|
Kamal::Cli::Accessory.any_instance.expects(:boot).with("mysql", login: false)
|
||||||
|
Kamal::Cli::Accessory.any_instance.expects(:stop).with("redis")
|
||||||
|
Kamal::Cli::Accessory.any_instance.expects(:remove_container).with("redis")
|
||||||
|
Kamal::Cli::Accessory.any_instance.expects(:boot).with("redis", login: false)
|
||||||
|
|
||||||
|
run_command("reboot", "all")
|
||||||
|
end
|
||||||
|
|
||||||
test "start" do
|
test "start" do
|
||||||
assert_match "docker container start app-mysql", run_command("start", "mysql")
|
assert_match "docker container start app-mysql", run_command("start", "mysql")
|
||||||
end
|
end
|
||||||
@@ -64,11 +76,20 @@ class CliAccessoryTest < CliTestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "details" do
|
test "details" do
|
||||||
assert_match "docker ps --filter label=service=app-mysql", run_command("details", "mysql")
|
run_command("details", "mysql").tap do |output|
|
||||||
|
assert_match "docker ps --filter label=service=app-mysql", output
|
||||||
|
assert_match "Accessory mysql Host: 1.1.1.3", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "details with non-existent accessory" do
|
||||||
|
assert_equal "No accessory by the name of 'hello' (options: mysql and redis)", stderred { run_command("details", "hello") }
|
||||||
end
|
end
|
||||||
|
|
||||||
test "details with all" do
|
test "details with all" do
|
||||||
run_command("details", "all").tap do |output|
|
run_command("details", "all").tap do |output|
|
||||||
|
assert_match "Accessory mysql Host: 1.1.1.3", output
|
||||||
|
assert_match "Accessory redis Host: 1.1.1.2", output
|
||||||
assert_match "docker ps --filter label=service=app-mysql", output
|
assert_match "docker ps --filter label=service=app-mysql", output
|
||||||
assert_match "docker ps --filter label=service=app-redis", output
|
assert_match "docker ps --filter label=service=app-redis", output
|
||||||
end
|
end
|
||||||
@@ -97,7 +118,7 @@ class CliAccessoryTest < CliTestCase
|
|||||||
|
|
||||||
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.3 'docker logs app-mysql --timestamps --tail 10 --follow 2>&1'")
|
.with("ssh -t root@1.1.1.3 -p 22 'docker logs app-mysql --timestamps --tail 10 --follow 2>&1'")
|
||||||
|
|
||||||
assert_match "docker logs app-mysql --timestamps --tail 10 --follow 2>&1", run_command("logs", "mysql", "--follow")
|
assert_match "docker logs app-mysql --timestamps --tail 10 --follow 2>&1", run_command("logs", "mysql", "--follow")
|
||||||
end
|
end
|
||||||
@@ -136,8 +157,32 @@ class CliAccessoryTest < CliTestCase
|
|||||||
assert_match "rm -rf app-mysql", run_command("remove_service_directory", "mysql")
|
assert_match "rm -rf app-mysql", run_command("remove_service_directory", "mysql")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "hosts param respected" do
|
||||||
|
Kamal::Cli::Accessory.any_instance.expects(:directories).with("redis")
|
||||||
|
Kamal::Cli::Accessory.any_instance.expects(:upload).with("redis")
|
||||||
|
|
||||||
|
run_command("boot", "redis", "--hosts", "1.1.1.1").tap do |output|
|
||||||
|
assert_match /docker login.*on 1.1.1.1/, output
|
||||||
|
assert_no_match /docker login.*on 1.1.1.2/, output
|
||||||
|
assert_match "docker run --name app-redis --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/env/accessories/app-redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.1", output
|
||||||
|
assert_no_match "docker run --name app-redis --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/env/accessories/app-redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.2", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "hosts param intersected with configuration" do
|
||||||
|
Kamal::Cli::Accessory.any_instance.expects(:directories).with("redis")
|
||||||
|
Kamal::Cli::Accessory.any_instance.expects(:upload).with("redis")
|
||||||
|
|
||||||
|
run_command("boot", "redis", "--hosts", "1.1.1.1,1.1.1.3").tap do |output|
|
||||||
|
assert_match /docker login.*on 1.1.1.1/, output
|
||||||
|
assert_no_match /docker login.*on 1.1.1.3/, output
|
||||||
|
assert_match "docker run --name app-redis --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/env/accessories/app-redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.1", output
|
||||||
|
assert_no_match "docker run --name app-redis --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/env/accessories/app-redis.env --volume $PWD/app-redis/data:/data --label service=\"app-redis\" redis:latest on 1.1.1.3", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def run_command(*command)
|
def run_command(*command)
|
||||||
stdouted { Kamal::Cli::Accessory.start([*command, "-c", "test/fixtures/deploy_with_accessories.yml"]) }
|
stdouted { Kamal::Cli::Accessory.start([ *command, "-c", "test/fixtures/deploy_with_accessories.yml" ]) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -14,16 +14,17 @@ class CliAppTest < CliTestCase
|
|||||||
run_command("details") # Preheat Kamal const
|
run_command("details") # Preheat Kamal const
|
||||||
|
|
||||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
.with(:docker, :container, :ls, "--filter", "name=^app-web-latest$", "--quiet", raise_on_non_zero_exit: false)
|
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", raise_on_non_zero_exit: false)
|
||||||
.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(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'")
|
.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)
|
||||||
.returns("running") # health check
|
.returns("123") # old version
|
||||||
|
|
||||||
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", "--filter", "status=running", "--filter", "status=restarting", "--latest", "--format", "\"{{.Names}}\"", "|", "while read line; do echo ${line#app-web-}; done", raise_on_non_zero_exit: false)
|
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{.NetworkSettings.IPAddress}}{{range $k, $v := .NetworkSettings.Ports}}{{printf \":%s\" $k}}{{break}}{{end}}'", "|", :sed, "-e", "'s/\\/tcp$//'")
|
||||||
.returns("123") # old version
|
.returns("172.1.0.2:80")
|
||||||
|
.at_least_once
|
||||||
|
|
||||||
run_command("boot").tap do |output|
|
run_command("boot").tap do |output|
|
||||||
assert_match /Renaming container .* to .* as already deployed on 1.1.1.1/, output # Rename
|
assert_match /Renaming container .* to .* as already deployed on 1.1.1.1/, output # Rename
|
||||||
@@ -36,7 +37,7 @@ class CliAppTest < CliTestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "boot uses group strategy when specified" do
|
test "boot uses group strategy when specified" do
|
||||||
Kamal::Cli::App.any_instance.stubs(:on).with("1.1.1.1").twice # acquire & release lock
|
Kamal::Cli::App.any_instance.stubs(:on).with("1.1.1.1").times(3) # ensure locks dir, acquire & release lock
|
||||||
Kamal::Cli::App.any_instance.stubs(:on).with([ "1.1.1.1" ]) # tag container
|
Kamal::Cli::App.any_instance.stubs(:on).with([ "1.1.1.1" ]) # tag container
|
||||||
|
|
||||||
# Strategy is used when booting the containers
|
# Strategy is used when booting the containers
|
||||||
@@ -45,19 +46,123 @@ class CliAppTest < CliTestCase
|
|||||||
run_command("boot", config: :with_boot_strategy)
|
run_command("boot", config: :with_boot_strategy)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "boot errors leave lock in place" do
|
test "boot errors don't leave lock in place" do
|
||||||
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "version" => "999" }
|
|
||||||
|
|
||||||
Kamal::Cli::App.any_instance.expects(:using_version).raises(RuntimeError)
|
Kamal::Cli::App.any_instance.expects(:using_version).raises(RuntimeError)
|
||||||
|
|
||||||
assert !KAMAL.holding_lock?
|
assert_not KAMAL.holding_lock?
|
||||||
assert_raises(RuntimeError) do
|
assert_raises(RuntimeError) do
|
||||||
stderred { run_command("boot") }
|
stderred { run_command("boot") }
|
||||||
end
|
end
|
||||||
assert KAMAL.holding_lock?
|
assert_not KAMAL.holding_lock?
|
||||||
|
end
|
||||||
|
|
||||||
|
test "boot with assets" do
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info) .with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", raise_on_non_zero_exit: false)
|
||||||
|
.returns("12345678") # running version
|
||||||
|
|
||||||
|
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)
|
||||||
|
.returns("123").twice # old version
|
||||||
|
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
|
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{.NetworkSettings.IPAddress}}{{range $k, $v := .NetworkSettings.Ports}}{{printf \":%s\" $k}}{{break}}{{end}}'", "|", :sed, "-e", "'s/\\/tcp$//'")
|
||||||
|
.returns("172.1.0.2:80").at_least_once
|
||||||
|
|
||||||
|
run_command("boot", config: :with_assets).tap do |output|
|
||||||
|
assert_match "docker tag dhh/app:latest dhh/app:latest", output
|
||||||
|
assert_match "/usr/bin/env mkdir -p .kamal/assets/volumes/app-web-latest ; cp -rnT .kamal/assets/extracted/app-web-latest .kamal/assets/volumes/app-web-latest ; cp -rnT .kamal/assets/extracted/app-web-latest .kamal/assets/volumes/app-web-123 || true ; cp -rnT .kamal/assets/extracted/app-web-123 .kamal/assets/volumes/app-web-latest || true", output
|
||||||
|
assert_match "/usr/bin/env mkdir -p .kamal/assets/extracted/app-web-latest && docker stop -t 1 app-web-assets 2> /dev/null || true && docker run --name app-web-assets --detach --rm dhh/app:latest sleep 1000000 && docker cp -L app-web-assets:/public/assets/. .kamal/assets/extracted/app-web-latest && docker stop -t 1 app-web-assets", output
|
||||||
|
assert_match /docker run --detach --restart unless-stopped --name app-web-latest --hostname 1.1.1.1-[0-9a-f]{12} /, output
|
||||||
|
assert_match "docker container ls --all --filter name=^app-web-123$ --quiet | xargs docker stop", output
|
||||||
|
assert_match "/usr/bin/env find .kamal/assets/extracted -maxdepth 1 -name 'app-web-*' ! -name app-web-latest -exec rm -rf \"{}\" + ; find .kamal/assets/volumes -maxdepth 1 -name 'app-web-*' ! -name app-web-latest -exec rm -rf \"{}\" +", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "boot with host tags" do
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
|
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", raise_on_non_zero_exit: false)
|
||||||
|
.returns("12345678") # running version
|
||||||
|
|
||||||
|
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)
|
||||||
|
.returns("123") # old version
|
||||||
|
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
|
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{.NetworkSettings.IPAddress}}{{range $k, $v := .NetworkSettings.Ports}}{{printf \":%s\" $k}}{{break}}{{end}}'", "|", :sed, "-e", "'s/\\/tcp$//'")
|
||||||
|
.returns("172.1.0.2:80").at_least_once
|
||||||
|
|
||||||
|
run_command("boot", config: :with_env_tags).tap do |output|
|
||||||
|
assert_match "docker tag dhh/app:latest dhh/app:latest", output
|
||||||
|
assert_match %r{docker run --detach --restart unless-stopped --name app-web-latest --hostname 1.1.1.1-[0-9a-f]{12} -e KAMAL_CONTAINER_NAME="app-web-latest" -e KAMAL_VERSION="latest" --env-file .kamal/env/roles/app-web.env --env TEST="root" --env EXPERIMENT="disabled" --env SITE="site1"}, output
|
||||||
|
assert_match "docker container ls --all --filter name=^app-web-123$ --quiet | xargs docker stop", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "boot with web barrier opened" do
|
||||||
|
SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info).returns("")
|
||||||
|
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
|
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{.NetworkSettings.IPAddress}}{{range $k, $v := .NetworkSettings.Ports}}{{printf \":%s\" $k}}{{break}}{{end}}'", "|", :sed, "-e", "'s/\\/tcp$//'")
|
||||||
|
.returns("172.1.0.2:80").at_least_once
|
||||||
|
|
||||||
|
run_command("boot", config: :with_roles, host: nil).tap do |output|
|
||||||
|
assert_match "Waiting for the first healthy web container before booting workers on 1.1.1.3...", output
|
||||||
|
assert_match "Waiting for the first healthy web container before booting workers on 1.1.1.4...", output
|
||||||
|
assert_match "First web container is healthy, booting workers on 1.1.1.3", output
|
||||||
|
assert_match "First web container is healthy, booting workers on 1.1.1.4", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "boot with web barrier closed" do
|
||||||
|
Thread.report_on_exception = false
|
||||||
|
|
||||||
|
SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info).returns("")
|
||||||
|
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
|
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", raise_on_non_zero_exit: false)
|
||||||
|
.returns("abcdef123456")
|
||||||
|
.twice # web container id
|
||||||
|
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
|
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-workers-latest$", "--quiet", raise_on_non_zero_exit: false)
|
||||||
|
.returns("abcdef123456")
|
||||||
|
.twice # worker container id
|
||||||
|
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info).with { |*args| args[0..1] == [ :sh, "-c" ] }.returns("123").at_least_once
|
||||||
|
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
|
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{.NetworkSettings.IPAddress}}{{range $k, $v := .NetworkSettings.Ports}}{{printf \":%s\" $k}}{{break}}{{end}}'", "|", :sed, "-e", "'s/\\/tcp$//'")
|
||||||
|
.returns("172.1.0.2:80")
|
||||||
|
.at_least_once
|
||||||
|
|
||||||
|
SSHKit::Backend::Abstract.any_instance.stubs(:execute).returns("")
|
||||||
|
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:docker, :exec, "kamal-proxy", "kamal-proxy", :deploy, "app-web", "--target", "\"172.1.0.2:80\"").raises(SSHKit::Command::Failed, "Deploy failed").at_least_once
|
||||||
|
|
||||||
|
stderred do
|
||||||
|
run_command("boot", config: :with_roles, host: nil, allowed_error_message: "Deploy failed").tap do |output|
|
||||||
|
assert_match "Waiting for the first healthy web container before booting workers on 1.1.1.3...", output
|
||||||
|
assert_match "Waiting for the first healthy web container before booting workers on 1.1.1.4...", output
|
||||||
|
assert_match "First web container is unhealthy, not booting workers on 1.1.1.3", output
|
||||||
|
assert_match "First web container is unhealthy, not booting workers on 1.1.1.4", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
ensure
|
||||||
|
Thread.report_on_exception = true
|
||||||
end
|
end
|
||||||
|
|
||||||
test "start" do
|
test "start" do
|
||||||
|
# SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info).returns("")
|
||||||
|
|
||||||
|
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)
|
||||||
|
.returns("123") # current version
|
||||||
|
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
|
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-123$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{.NetworkSettings.IPAddress}}{{range $k, $v := .NetworkSettings.Ports}}{{printf \":%s\" $k}}{{break}}{{end}}'", "|", :sed, "-e", "'s/\\/tcp$//'")
|
||||||
|
.returns("172.1.0.2:80")
|
||||||
|
.at_least_once
|
||||||
|
|
||||||
run_command("start").tap do |output|
|
run_command("start").tap do |output|
|
||||||
assert_match "docker start app-web-999", output
|
assert_match "docker start app-web-999", output
|
||||||
end
|
end
|
||||||
@@ -65,14 +170,18 @@ class CliAppTest < CliTestCase
|
|||||||
|
|
||||||
test "stop" do
|
test "stop" do
|
||||||
run_command("stop").tap do |output|
|
run_command("stop").tap do |output|
|
||||||
assert_match "docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | xargs docker stop", 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
|
||||||
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=role=web", "--format", "\"{{.Names}}\"", "|", "while read line; do echo ${line#app-web-}; done", raise_on_non_zero_exit: false)
|
||||||
.returns("12345678\n87654321")
|
.returns("12345678\n87654321\n")
|
||||||
|
|
||||||
|
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)
|
||||||
|
.returns("12345678\n")
|
||||||
|
|
||||||
run_command("stale_containers").tap do |output|
|
run_command("stale_containers").tap do |output|
|
||||||
assert_match /Detected stale container for role web with version 87654321/, output
|
assert_match /Detected stale container for role web with version 87654321/, output
|
||||||
@@ -82,7 +191,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=role=web", "--format", "\"{{.Names}}\"", "|", "while read line; do echo ${line#app-web-}; done", raise_on_non_zero_exit: false)
|
||||||
.returns("12345678\n87654321")
|
.returns("12345678\n87654321\n")
|
||||||
|
|
||||||
|
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)
|
||||||
|
.returns("12345678\n")
|
||||||
|
|
||||||
run_command("stale_containers", "--stop").tap do |output|
|
run_command("stale_containers", "--stop").tap do |output|
|
||||||
assert_match /Stopping stale container for role web with version 87654321/, output
|
assert_match /Stopping stale container for role web with version 87654321/, output
|
||||||
@@ -98,7 +211,7 @@ class CliAppTest < CliTestCase
|
|||||||
|
|
||||||
test "remove" do
|
test "remove" do
|
||||||
run_command("remove").tap do |output|
|
run_command("remove").tap do |output|
|
||||||
assert_match /#{Regexp.escape("docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | xargs docker stop")}/, 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("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
|
||||||
@@ -124,17 +237,36 @@ 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 dhh/app:latest ruby -v", output
|
assert_match "docker run --rm --env-file .kamal/env/roles/app-web.env dhh/app:latest ruby -v", output
|
||||||
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 "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", output # Get current version
|
assert_match "sh -c 'docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest --format '\\''{{.ID}}'\\'') ; docker ps --latest --format '\\''{{.Names}}'\\'' --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting' | head -1 | while read line; do echo ${line#app-web-}; done", output # Get current version
|
||||||
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
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:exec)
|
||||||
|
.with("ssh -t root@1.1.1.1 -p 22 'docker run -it --rm --env-file .kamal/env/roles/app-web.env dhh/app:latest ruby -v'")
|
||||||
|
run_command("exec", "-i", "ruby -v").tap do |output|
|
||||||
|
assert_match "Get most recent version available as an image...", output
|
||||||
|
assert_match "Launching interactive command with version latest via SSH from new container on 1.1.1.1...", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "exec interactive with reuse" do
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:exec)
|
||||||
|
.with("ssh -t root@1.1.1.1 -p 22 'docker exec -it app-web-999 ruby -v'")
|
||||||
|
run_command("exec", "-i", "--reuse", "ruby -v").tap do |output|
|
||||||
|
assert_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 "Launching interactive command with version 999 via SSH from existing container on 1.1.1.1...", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
test "containers" do
|
test "containers" do
|
||||||
run_command("containers").tap do |output|
|
run_command("containers").tap do |output|
|
||||||
assert_match "docker container ls --all --filter label=service=app", output
|
assert_match "docker container ls --all --filter label=service=app", output
|
||||||
@@ -149,41 +281,61 @@ 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 'docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest| 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=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'")
|
||||||
|
|
||||||
assert_match "docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | xargs docker logs --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 --tail 100 2>&1", run_command("logs")
|
||||||
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 'docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | 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=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'")
|
||||||
|
|
||||||
assert_match "docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | 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=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")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "version" do
|
test "version" do
|
||||||
run_command("version").tap do |output|
|
run_command("version").tap do |output|
|
||||||
assert_match "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", 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
|
||||||
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 "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", 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
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "long hostname" do
|
||||||
|
stub_running
|
||||||
|
|
||||||
|
hostname = "this-hostname-is-really-unacceptably-long-to-be-honest.example.com"
|
||||||
|
|
||||||
|
stdouted { Kamal::Cli::App.start([ "boot", "-c", "test/fixtures/deploy_with_uncommon_hostnames.yml", "--hosts", hostname ]) }.tap do |output|
|
||||||
|
assert_match /docker run --detach --restart unless-stopped --name app-web-latest --hostname this-hostname-is-really-unacceptably-long-to-be-hon-[0-9a-f]{12} /, output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "hostname is trimmed if will end with a period" do
|
||||||
|
stub_running
|
||||||
|
|
||||||
|
hostname = "this-hostname-with-random-part-is-too-long.example.com"
|
||||||
|
|
||||||
|
stdouted { Kamal::Cli::App.start([ "boot", "-c", "test/fixtures/deploy_with_uncommon_hostnames.yml", "--hosts", hostname ]) }.tap do |output|
|
||||||
|
assert_match /docker run --detach --restart unless-stopped --name app-web-latest --hostname this-hostname-with-random-part-is-too-long.example-[0-9a-f]{12} /, output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def run_command(*command, config: :with_accessories)
|
def run_command(*command, config: :with_accessories, host: "1.1.1.1", allowed_error_message: nil)
|
||||||
stdouted { Kamal::Cli::App.start([*command, "-c", "test/fixtures/deploy_#{config}.yml", "--hosts", "1.1.1.1"]) }
|
stdouted do
|
||||||
|
Kamal::Cli::App.start([ *command, "-c", "test/fixtures/deploy_#{config}.yml", *([ "--hosts", host ] if host) ])
|
||||||
|
rescue SSHKit::Runner::ExecuteError => e
|
||||||
|
raise e unless allowed_error_message && e.message.include?(allowed_error_message)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def stub_running
|
def stub_running
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info).returns("123") # old version
|
SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info).returns("123") # old version
|
||||||
|
|
||||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
|
||||||
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-latest$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'")
|
|
||||||
.returns("running") # health check
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -9,34 +9,142 @@ class CliBuildTest < CliTestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "push" do
|
test "push" do
|
||||||
|
with_build_directory do |build_directory|
|
||||||
|
Kamal::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)
|
||||||
|
hook_variables = { version: 999, service_version: "app@999", hosts: "1.1.1.1,1.1.1.2,1.1.1.3,1.1.1.4", command: "build", subcommand: "push" }
|
||||||
|
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
|
.with(:git, "-C", anything, :"rev-parse", :HEAD)
|
||||||
|
.returns(Kamal::Git.revision)
|
||||||
|
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
|
.with(:git, "-C", anything, :status, "--porcelain")
|
||||||
|
.returns("")
|
||||||
|
|
||||||
|
run_command("push", "--verbose").tap do |output|
|
||||||
|
assert_hook_ran "pre-build", output, **hook_variables
|
||||||
|
assert_match /Cloning repo into build directory/, output
|
||||||
|
assert_match /git -C #{Dir.tmpdir}\/kamal-clones\/app-#{pwd_sha} clone #{Dir.pwd}/, output
|
||||||
|
assert_match /docker --version && docker buildx version/, output
|
||||||
|
assert_match /docker buildx build --push --platform linux\/amd64,linux\/arm64 --builder kamal-app-multiarch -t dhh\/app:999 -t dhh\/app:latest --label service="app" --file Dockerfile \. as .*@localhost/, output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "push resetting clone" do
|
||||||
|
with_build_directory do |build_directory|
|
||||||
|
stub_setup
|
||||||
|
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:docker, "--version", "&&", :docker, :buildx, "version")
|
||||||
|
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:execute)
|
||||||
|
.with(:git, "-C", "#{Dir.tmpdir}/kamal-clones/app-#{pwd_sha}", :clone, Dir.pwd)
|
||||||
|
.raises(SSHKit::Command::Failed.new("fatal: destination path 'kamal' already exists and is not an empty directory"))
|
||||||
|
.then
|
||||||
|
.returns(true)
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:git, "-C", build_directory, :remote, "set-url", :origin, Dir.pwd)
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:git, "-C", build_directory, :fetch, :origin)
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:git, "-C", build_directory, :reset, "--hard", Kamal::Git.revision)
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:git, "-C", build_directory, :clean, "-fdx")
|
||||||
|
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:execute)
|
||||||
|
.with(:docker, :buildx, :build, "--push", "--platform", "linux/amd64,linux/arm64", "--builder", "kamal-app-multiarch", "-t", "dhh/app:999", "-t", "dhh/app:latest", "--label", "service=\"app\"", "--file", "Dockerfile", ".")
|
||||||
|
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
|
.with(:git, "-C", anything, :"rev-parse", :HEAD)
|
||||||
|
.returns(Kamal::Git.revision)
|
||||||
|
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
|
.with(:git, "-C", anything, :status, "--porcelain")
|
||||||
|
.returns("")
|
||||||
|
|
||||||
|
run_command("push", "--verbose").tap do |output|
|
||||||
|
assert_match /Cloning repo into build directory/, output
|
||||||
|
assert_match /Resetting local clone/, output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "push without clone" do
|
||||||
Kamal::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)
|
Kamal::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)
|
||||||
hook_variables = { version: 999, service_version: "app@999", hosts: "1.1.1.1,1.1.1.2,1.1.1.3,1.1.1.4", command: "build", subcommand: "push" }
|
hook_variables = { version: 999, service_version: "app@999", hosts: "1.1.1.1,1.1.1.2,1.1.1.3,1.1.1.4", command: "build", subcommand: "push" }
|
||||||
|
|
||||||
run_command("push").tap do |output|
|
run_command("push", "--verbose", fixture: :without_clone).tap do |output|
|
||||||
|
assert_no_match /Cloning repo into build directory/, output
|
||||||
assert_hook_ran "pre-build", output, **hook_variables
|
assert_hook_ran "pre-build", output, **hook_variables
|
||||||
assert_match /docker --version && docker buildx version/, output
|
assert_match /docker --version && docker buildx version/, output
|
||||||
assert_match /docker buildx build --push --platform linux\/amd64,linux\/arm64 --builder kamal-app-multiarch -t dhh\/app:999 -t dhh\/app:latest --label service="app" --file Dockerfile \. as .*@localhost/, output
|
assert_match /docker buildx build --push --platform linux\/amd64,linux\/arm64 --builder kamal-app-multiarch -t dhh\/app:999 -t dhh\/app:latest --label service="app" --file Dockerfile . as .*@localhost/, output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "push with corrupt clone" do
|
||||||
|
with_build_directory do |build_directory|
|
||||||
|
stub_setup
|
||||||
|
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:docker, "--version", "&&", :docker, :buildx, "version")
|
||||||
|
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:execute)
|
||||||
|
.with(:git, "-C", "#{Dir.tmpdir}/kamal-clones/app-#{pwd_sha}", :clone, Dir.pwd)
|
||||||
|
.raises(SSHKit::Command::Failed.new("fatal: destination path 'kamal' already exists and is not an empty directory"))
|
||||||
|
.then
|
||||||
|
.returns(true)
|
||||||
|
.twice
|
||||||
|
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:git, "-C", build_directory, :remote, "set-url", :origin, Dir.pwd)
|
||||||
|
.raises(SSHKit::Command::Failed.new("fatal: not a git repository"))
|
||||||
|
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
|
.with(:git, "-C", anything, :"rev-parse", :HEAD)
|
||||||
|
.returns(Kamal::Git.revision)
|
||||||
|
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
|
.with(:git, "-C", anything, :status, "--porcelain")
|
||||||
|
.returns("")
|
||||||
|
|
||||||
|
Dir.stubs(:chdir)
|
||||||
|
|
||||||
|
run_command("push", "--verbose") do |output|
|
||||||
|
assert_match /Cloning repo into build directory `#{build_directory}`\.\.\..*Cloning repo into build directory `#{build_directory}`\.\.\./, output
|
||||||
|
assert_match "Resetting local clone as `#{build_directory}` already exists...", output
|
||||||
|
assert_match "Error preparing clone: Failed to clone repo: fatal: not a git repository, deleting and retrying...", output
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "push without builder" do
|
test "push without builder" do
|
||||||
stub_locking
|
with_build_directory do |build_directory|
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
stub_setup
|
||||||
.with(:docker, "--version", "&&", :docker, :buildx, "version")
|
|
||||||
|
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
SSHKit::Backend::Abstract.any_instance.expects(:execute)
|
||||||
.with { |*args| args[0..1] == [:docker, :buildx] }
|
.with(:docker, "--version", "&&", :docker, :buildx, "version")
|
||||||
.raises(SSHKit::Command::Failed.new("no builder"))
|
|
||||||
.then
|
|
||||||
.returns(true)
|
|
||||||
|
|
||||||
run_command("push").tap do |output|
|
SSHKit::Backend::Abstract.any_instance.expects(:execute)
|
||||||
assert_match /Missing compatible builder, so creating a new one first/, output
|
.with(:docker, :buildx, :create, "--use", "--name", "kamal-app-multiarch")
|
||||||
|
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:execute)
|
||||||
|
.with { |*args| args[0..1] == [ :docker, :buildx ] }
|
||||||
|
.raises(SSHKit::Command::Failed.new("no builder"))
|
||||||
|
.then
|
||||||
|
.returns(true)
|
||||||
|
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:execute).with { |*args| args.first.start_with?("git") }
|
||||||
|
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
|
.with(:git, "-C", anything, :"rev-parse", :HEAD)
|
||||||
|
.returns(Kamal::Git.revision)
|
||||||
|
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
|
.with(:git, "-C", anything, :status, "--porcelain")
|
||||||
|
.returns("")
|
||||||
|
|
||||||
|
run_command("push").tap do |output|
|
||||||
|
assert_match /WARN Missing compatible builder, so creating a new one first/, output
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "push with no buildx plugin" do
|
test "push with no buildx plugin" do
|
||||||
stub_locking
|
stub_setup
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||||
.with(:docker, "--version", "&&", :docker, :buildx, "version")
|
.with(:docker, "--version", "&&", :docker, :buildx, "version")
|
||||||
.raises(SSHKit::Command::Failed.new("no buildx"))
|
.raises(SSHKit::Command::Failed.new("no buildx"))
|
||||||
@@ -48,15 +156,17 @@ class CliBuildTest < CliTestCase
|
|||||||
test "push pre-build hook failure" do
|
test "push pre-build hook failure" do
|
||||||
fail_hook("pre-build")
|
fail_hook("pre-build")
|
||||||
|
|
||||||
assert_raises(Kamal::Cli::HookError) { run_command("push") }
|
error = assert_raises(Kamal::Cli::HookError) { run_command("push") }
|
||||||
|
assert_equal "Hook `pre-build` failed:\nfailed", error.message
|
||||||
|
|
||||||
assert @executions.none? { |args| args[0..2] == [:docker, :buildx, :build] }
|
assert @executions.none? { |args| args[0..2] == [ :docker, :buildx, :build ] }
|
||||||
end
|
end
|
||||||
|
|
||||||
test "pull" do
|
test "pull" do
|
||||||
run_command("pull").tap do |output|
|
run_command("pull").tap do |output|
|
||||||
assert_match /docker image rm --force dhh\/app:999/, output
|
assert_match /docker image rm --force dhh\/app:999/, output
|
||||||
assert_match /docker pull dhh\/app:999/, output
|
assert_match /docker pull dhh\/app:999/, output
|
||||||
|
assert_match "docker inspect -f '{{ .Config.Labels.service }}' dhh/app:999 | grep -x app || (echo \"Image dhh/app:999 is missing the 'service' label\" && exit 1)", output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -66,8 +176,24 @@ class CliBuildTest < CliTestCase
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "create remote" do
|
||||||
|
run_command("create", fixture: :with_remote_builder).tap do |output|
|
||||||
|
assert_match "Running /usr/bin/env true on 1.1.1.5", output
|
||||||
|
assert_match "docker context create kamal-app-native-remote-amd64 --description 'kamal-app-native-remote amd64 native host' --docker 'host=ssh://app@1.1.1.5'", output
|
||||||
|
assert_match "docker buildx create --name kamal-app-native-remote kamal-app-native-remote-amd64 --platform linux/amd64", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "create remote with custom ports" do
|
||||||
|
run_command("create", fixture: :with_remote_builder_and_custom_ports).tap do |output|
|
||||||
|
assert_match "Running /usr/bin/env true on 1.1.1.5", output
|
||||||
|
assert_match "docker context create kamal-app-native-remote-amd64 --description 'kamal-app-native-remote amd64 native host' --docker 'host=ssh://app@1.1.1.5:2122'", output
|
||||||
|
assert_match "docker buildx create --name kamal-app-native-remote kamal-app-native-remote-amd64 --platform linux/amd64", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
test "create with error" do
|
test "create with error" do
|
||||||
stub_locking
|
stub_setup
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||||
.with { |arg| arg == :docker }
|
.with { |arg| arg == :docker }
|
||||||
.raises(SSHKit::Command::Failed.new("stderr=error"))
|
.raises(SSHKit::Command::Failed.new("stderr=error"))
|
||||||
@@ -95,14 +221,27 @@ class CliBuildTest < CliTestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def run_command(*command)
|
def run_command(*command, fixture: :with_accessories)
|
||||||
stdouted { Kamal::Cli::Build.start([*command, "-c", "test/fixtures/deploy_with_accessories.yml"]) }
|
stdouted { Kamal::Cli::Build.start([ *command, "-c", "test/fixtures/deploy_#{fixture}.yml" ]) }
|
||||||
end
|
end
|
||||||
|
|
||||||
def stub_dependency_checks
|
def stub_dependency_checks
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||||
.with(:docker, "--version", "&&", :docker, :buildx, "version")
|
.with(:docker, "--version", "&&", :docker, :buildx, "version")
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||||
.with { |*args| args[0..1] == [:docker, :buildx] }
|
.with { |*args| args[0..1] == [ :docker, :buildx ] }
|
||||||
|
end
|
||||||
|
|
||||||
|
def with_build_directory
|
||||||
|
build_directory = File.join Dir.tmpdir, "kamal-clones", "app-#{pwd_sha}", "kamal"
|
||||||
|
FileUtils.mkdir_p build_directory
|
||||||
|
FileUtils.touch File.join build_directory, "Dockerfile"
|
||||||
|
yield build_directory + "/"
|
||||||
|
ensure
|
||||||
|
FileUtils.rm_rf build_directory
|
||||||
|
end
|
||||||
|
|
||||||
|
def pwd_sha
|
||||||
|
Digest::SHA256.hexdigest(Dir.pwd)[0..12]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -21,20 +21,24 @@ class CliTestCase < ActiveSupport::TestCase
|
|||||||
Kamal::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)
|
Kamal::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)
|
||||||
|
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||||
.with { |*args| @executions << args; args != [".kamal/hooks/#{hook}"] }
|
.with { |*args| @executions << args; args != [ ".kamal/hooks/#{hook}" ] }
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||||
.with { |*args| args.first == ".kamal/hooks/#{hook}" }
|
.with { |*args| args.first == ".kamal/hooks/#{hook}" }
|
||||||
.raises(SSHKit::Command::Failed.new("failed"))
|
.raises(SSHKit::Command::Failed.new("failed"))
|
||||||
end
|
end
|
||||||
|
|
||||||
def stub_locking
|
def stub_setup
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||||
.with { |arg1, arg2| arg1 == :mkdir && arg2 == "kamal_lock-app" }
|
.with { |*args| args == [ :mkdir, "-p", ".kamal" ] }
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||||
.with { |arg1, arg2| arg1 == :rm && arg2 == "kamal_lock-app/details" }
|
.with { |arg1, arg2, arg3| arg1 == :mkdir && arg2 == "-p" && arg3 == ".kamal/locks" }
|
||||||
|
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||||
|
.with { |arg1, arg2| arg1 == :mkdir && arg2 == ".kamal/locks/app" }
|
||||||
|
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||||
|
.with { |arg1, arg2| arg1 == :rm && arg2 == ".kamal/locks/app/details" }
|
||||||
end
|
end
|
||||||
|
|
||||||
def assert_hook_ran(hook, output, version:, service_version:, hosts:, command:, subcommand: nil, runtime: nil)
|
def assert_hook_ran(hook, output, version:, service_version:, hosts:, command:, subcommand: nil, runtime: false)
|
||||||
performer = `whoami`.strip
|
performer = `whoami`.strip
|
||||||
|
|
||||||
assert_match "Running the #{hook} hook...\n", output
|
assert_match "Running the #{hook} hook...\n", output
|
||||||
@@ -48,7 +52,7 @@ class CliTestCase < ActiveSupport::TestCase
|
|||||||
KAMAL_HOSTS=\"#{hosts}\"\s
|
KAMAL_HOSTS=\"#{hosts}\"\s
|
||||||
KAMAL_COMMAND=\"#{command}\"\s
|
KAMAL_COMMAND=\"#{command}\"\s
|
||||||
#{"KAMAL_SUBCOMMAND=\\\"#{subcommand}\\\"\\s" if subcommand}
|
#{"KAMAL_SUBCOMMAND=\\\"#{subcommand}\\\"\\s" if subcommand}
|
||||||
#{"KAMAL_RUNTIME=\\\"#{runtime}\\\"\\s" if runtime}
|
#{"KAMAL_RUNTIME=\\\"\\d+\\\"\\s" if runtime}
|
||||||
;\s/usr/bin/env\s\.kamal/hooks/#{hook} }x
|
;\s/usr/bin/env\s\.kamal/hooks/#{hook} }x
|
||||||
|
|
||||||
assert_match expected, output
|
assert_match expected, output
|
||||||
|
|||||||
32
test/cli/env_test.rb
Normal file
32
test/cli/env_test.rb
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
require_relative "cli_test_case"
|
||||||
|
|
||||||
|
class CliEnvTest < CliTestCase
|
||||||
|
test "push" do
|
||||||
|
run_command("push").tap do |output|
|
||||||
|
assert_match "Running /usr/bin/env mkdir -p .kamal/env/roles on 1.1.1.1", output
|
||||||
|
assert_match "Running /usr/bin/env mkdir -p .kamal/env/accessories on 1.1.1.1", output
|
||||||
|
assert_match "Running /usr/bin/env mkdir -p .kamal/env/roles on 1.1.1.1", output
|
||||||
|
assert_match "Running /usr/bin/env mkdir -p .kamal/env/accessories on 1.1.1.1", output
|
||||||
|
assert_match ".kamal/env/roles/app-web.env", output
|
||||||
|
assert_match ".kamal/env/roles/app-workers.env", output
|
||||||
|
assert_match ".kamal/env/accessories/app-redis.env", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "delete" do
|
||||||
|
run_command("delete").tap do |output|
|
||||||
|
assert_match "Running /usr/bin/env rm -f .kamal/env/roles/app-web.env on 1.1.1.1", output
|
||||||
|
assert_match "Running /usr/bin/env rm -f .kamal/env/roles/app-web.env on 1.1.1.2", output
|
||||||
|
assert_match "Running /usr/bin/env rm -f .kamal/env/roles/app-workers.env on 1.1.1.3", output
|
||||||
|
assert_match "Running /usr/bin/env rm -f .kamal/env/roles/app-workers.env on 1.1.1.4", output
|
||||||
|
assert_match "Running /usr/bin/env rm -f .kamal/env/accessories/app-redis.env on 1.1.1.1", output
|
||||||
|
assert_match "Running /usr/bin/env rm -f .kamal/env/accessories/app-redis.env on 1.1.1.2", output
|
||||||
|
assert_match "Running /usr/bin/env rm -f .kamal/env/accessories/app-mysql.env on 1.1.1.3", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def run_command(*command)
|
||||||
|
stdouted { Kamal::Cli::Env.start([ *command, "-c", "test/fixtures/deploy_with_accessories.yml" ]) }
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
require_relative "cli_test_case"
|
|
||||||
|
|
||||||
class CliHealthcheckTest < CliTestCase
|
|
||||||
test "perform" do
|
|
||||||
# Prevent expected failures from outputting to terminal
|
|
||||||
Thread.report_on_exception = false
|
|
||||||
|
|
||||||
Kamal::Utils::HealthcheckPoller.stubs(:sleep) # No sleeping when retrying
|
|
||||||
|
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
|
||||||
.with(:docker, :container, :ls, "--all", "--filter", "name=^healthcheck-app-999$", "--quiet", "|", :xargs, :docker, :stop, raise_on_non_zero_exit: false)
|
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
|
||||||
.with(:docker, :run, "--detach", "--name", "healthcheck-app-999", "--publish", "3999:3000", "--label", "service=healthcheck-app", "-e", "KAMAL_CONTAINER_NAME=\"healthcheck-app\"", "--health-cmd", "\"curl -f http://localhost:3000/up || exit 1\"", "--health-interval", "\"1s\"", "dhh/app:999")
|
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
|
||||||
.with(:docker, :container, :ls, "--all", "--filter", "name=^healthcheck-app-999$", "--quiet", "|", :xargs, :docker, :container, :rm, raise_on_non_zero_exit: false)
|
|
||||||
|
|
||||||
# Fail twice to test retry logic
|
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info)
|
|
||||||
.with(:docker, :container, :ls, "--all", "--filter", "name=^healthcheck-app-999$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'")
|
|
||||||
.returns("starting")
|
|
||||||
.then
|
|
||||||
.returns("unhealthy")
|
|
||||||
.then
|
|
||||||
.returns("healthy")
|
|
||||||
|
|
||||||
run_command("perform").tap do |output|
|
|
||||||
assert_match "container not ready (starting), retrying in 1s (attempt 1/7)...", output
|
|
||||||
assert_match "container not ready (unhealthy), retrying in 2s (attempt 2/7)...", output
|
|
||||||
assert_match "Container is healthy!", output
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
test "perform failing to become healthy" do
|
|
||||||
# Prevent expected failures from outputting to terminal
|
|
||||||
Thread.report_on_exception = false
|
|
||||||
|
|
||||||
Kamal::Utils::HealthcheckPoller.stubs(:sleep) # No sleeping when retrying
|
|
||||||
|
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
|
||||||
.with(:docker, :container, :ls, "--all", "--filter", "name=^healthcheck-app-999$", "--quiet", "|", :xargs, :docker, :stop, raise_on_non_zero_exit: false)
|
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
|
||||||
.with(:docker, :run, "--detach", "--name", "healthcheck-app-999", "--publish", "3999:3000", "--label", "service=healthcheck-app", "-e", "KAMAL_CONTAINER_NAME=\"healthcheck-app\"", "--health-cmd", "\"curl -f http://localhost:3000/up || exit 1\"", "--health-interval", "\"1s\"", "dhh/app:999")
|
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
|
||||||
.with(:docker, :container, :ls, "--all", "--filter", "name=^healthcheck-app-999$", "--quiet", "|", :xargs, :docker, :container, :rm, raise_on_non_zero_exit: false)
|
|
||||||
|
|
||||||
# Continually report unhealthy
|
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info)
|
|
||||||
.with(:docker, :container, :ls, "--all", "--filter", "name=^healthcheck-app-999$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'")
|
|
||||||
.returns("unhealthy")
|
|
||||||
|
|
||||||
# Capture logs when failing
|
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_info)
|
|
||||||
.with(:docker, :container, :ls, "--all", "--filter", "name=^healthcheck-app-999$", "--quiet", "|", :xargs, :docker, :logs, "--tail", 50, "2>&1")
|
|
||||||
.returns("some log output")
|
|
||||||
|
|
||||||
# Capture container health log when failing
|
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:capture_with_pretty_json)
|
|
||||||
.with(:docker, :container, :ls, "--all", "--filter", "name=^healthcheck-app-999$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{json .State.Health}}'")
|
|
||||||
.returns('{"Status":"unhealthy","Log":[{"ExitCode": 1,"Output": "/bin/sh: 1: curl: not found\n"}]}"')
|
|
||||||
|
|
||||||
exception = assert_raises do
|
|
||||||
run_command("perform")
|
|
||||||
end
|
|
||||||
assert_match "container not ready (unhealthy)", exception.message
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
def run_command(*command)
|
|
||||||
stdouted { Kamal::Cli::Healthcheck.start([*command, "-c", "test/fixtures/deploy_with_accessories.yml"]) }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -2,19 +2,19 @@ require_relative "cli_test_case"
|
|||||||
|
|
||||||
class CliLockTest < CliTestCase
|
class CliLockTest < CliTestCase
|
||||||
test "status" do
|
test "status" do
|
||||||
run_command("status") do |output|
|
run_command("status").tap do |output|
|
||||||
assert_match "stat lock", output
|
assert_match "Running /usr/bin/env stat .kamal/locks/app > /dev/null && cat .kamal/locks/app/details | base64 -d on 1.1.1.1", output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "release" do
|
test "release" do
|
||||||
run_command("release") do |output|
|
run_command("release").tap do |output|
|
||||||
assert_match "rm -rf lock", output
|
assert_match "Released the deploy lock", output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def run_command(*command)
|
def run_command(*command)
|
||||||
stdouted { Kamal::Cli::Lock.start([*command, "-c", "test/fixtures/deploy_with_accessories.yml"]) }
|
stdouted { Kamal::Cli::Lock.start([ *command, "-v", "-c", "test/fixtures/deploy_with_accessories.yml" ]) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -2,37 +2,69 @@ require_relative "cli_test_case"
|
|||||||
|
|
||||||
class CliMainTest < CliTestCase
|
class CliMainTest < CliTestCase
|
||||||
test "setup" do
|
test "setup" do
|
||||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:server:bootstrap")
|
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "version" => "999", "skip_hooks" => false }
|
||||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:accessory:boot", [ "all" ])
|
|
||||||
|
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:server:bootstrap", [], invoke_options)
|
||||||
|
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:main:envify", [], invoke_options)
|
||||||
|
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:accessory:boot", [ "all" ], invoke_options)
|
||||||
Kamal::Cli::Main.any_instance.expects(:deploy)
|
Kamal::Cli::Main.any_instance.expects(:deploy)
|
||||||
|
|
||||||
run_command("setup")
|
run_command("setup").tap do |output|
|
||||||
|
assert_match /Ensure Docker is installed.../, output
|
||||||
|
assert_match /Evaluate and push env files.../, output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "setup with skip_push" do
|
||||||
|
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "version" => "999", "skip_hooks" => false }
|
||||||
|
|
||||||
|
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:server:bootstrap", [], invoke_options)
|
||||||
|
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:main:envify", [], invoke_options)
|
||||||
|
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:accessory:boot", [ "all" ], invoke_options)
|
||||||
|
# deploy
|
||||||
|
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:registry:login", [], invoke_options)
|
||||||
|
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:build:pull", [], invoke_options)
|
||||||
|
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:proxy:boot", [], invoke_options)
|
||||||
|
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true))
|
||||||
|
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:boot", [], invoke_options)
|
||||||
|
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:prune:all", [], invoke_options)
|
||||||
|
|
||||||
|
run_command("setup", "--skip_push").tap do |output|
|
||||||
|
assert_match /Ensure Docker is installed.../, output
|
||||||
|
assert_match /Evaluate and push env files.../, output
|
||||||
|
# deploy
|
||||||
|
assert_match /Acquiring the deploy lock/, output
|
||||||
|
assert_match /Log into image registry/, output
|
||||||
|
assert_match /Pull app image/, output
|
||||||
|
assert_match /Ensure proxy is running/, output
|
||||||
|
assert_match /Detect stale containers/, output
|
||||||
|
assert_match /Prune old containers and images/, output
|
||||||
|
assert_match /Releasing the deploy lock/, output
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "deploy" do
|
test "deploy" do
|
||||||
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "version" => "999", "skip_hooks" => false }
|
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "version" => "999", "skip_hooks" => false, "verbose" => true }
|
||||||
|
|
||||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:registry:login", [], invoke_options)
|
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:registry:login", [], invoke_options)
|
||||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:build:deliver", [], invoke_options)
|
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:build:deliver", [], invoke_options)
|
||||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:traefik:boot", [], invoke_options)
|
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:proxy:boot", [], invoke_options)
|
||||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:healthcheck:perform", [], invoke_options)
|
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true))
|
||||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:stale_containers", [], invoke_options)
|
|
||||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:boot", [], invoke_options)
|
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:boot", [], invoke_options)
|
||||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:prune:all", [], invoke_options)
|
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:prune:all", [], invoke_options)
|
||||||
|
|
||||||
Kamal::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)
|
Kamal::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)
|
||||||
hook_variables = { version: 999, service_version: "app@999", hosts: "1.1.1.1,1.1.1.2", command: "deploy" }
|
hook_variables = { version: 999, service_version: "app@999", hosts: "1.1.1.1,1.1.1.2", command: "deploy" }
|
||||||
|
|
||||||
run_command("deploy").tap do |output|
|
run_command("deploy", "--verbose").tap do |output|
|
||||||
assert_hook_ran "pre-connect", output, **hook_variables
|
assert_hook_ran "pre-connect", output, **hook_variables
|
||||||
assert_match /Log into image registry/, output
|
assert_match /Log into image registry/, output
|
||||||
assert_match /Build and push app image/, output
|
assert_match /Build and push app image/, output
|
||||||
assert_hook_ran "pre-deploy", output, **hook_variables
|
assert_hook_ran "pre-deploy", output, **hook_variables
|
||||||
assert_match /Ensure Traefik is running/, output
|
assert_match /Ensure proxy is running/, output
|
||||||
assert_match /Ensure app can pass healthcheck/, output
|
|
||||||
assert_match /Detect stale containers/, output
|
assert_match /Detect stale containers/, output
|
||||||
assert_match /Prune old containers and images/, output
|
assert_match /Prune old containers and images/, output
|
||||||
assert_hook_ran "post-deploy", output, **hook_variables, runtime: 0
|
assert_hook_ran "post-deploy", output, **hook_variables, runtime: true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -41,9 +73,8 @@ class CliMainTest < CliTestCase
|
|||||||
|
|
||||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:registry:login", [], invoke_options)
|
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:registry:login", [], invoke_options)
|
||||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:build:pull", [], invoke_options)
|
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:build:pull", [], invoke_options)
|
||||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:traefik:boot", [], invoke_options)
|
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:proxy:boot", [], invoke_options)
|
||||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:healthcheck:perform", [], invoke_options)
|
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true))
|
||||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:stale_containers", [], invoke_options)
|
|
||||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:boot", [], invoke_options)
|
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:boot", [], invoke_options)
|
||||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:prune:all", [], invoke_options)
|
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:prune:all", [], invoke_options)
|
||||||
|
|
||||||
@@ -51,8 +82,7 @@ class CliMainTest < CliTestCase
|
|||||||
assert_match /Acquiring the deploy lock/, output
|
assert_match /Acquiring the deploy lock/, output
|
||||||
assert_match /Log into image registry/, output
|
assert_match /Log into image registry/, output
|
||||||
assert_match /Pull app image/, output
|
assert_match /Pull app image/, output
|
||||||
assert_match /Ensure Traefik is running/, output
|
assert_match /Ensure proxy is running/, output
|
||||||
assert_match /Ensure app can pass healthcheck/, output
|
|
||||||
assert_match /Detect stale containers/, output
|
assert_match /Detect stale containers/, output
|
||||||
assert_match /Prune old containers and images/, output
|
assert_match /Prune old containers and images/, output
|
||||||
assert_match /Releasing the deploy lock/, output
|
assert_match /Releasing the deploy lock/, output
|
||||||
@@ -63,11 +93,28 @@ class CliMainTest < CliTestCase
|
|||||||
Thread.report_on_exception = false
|
Thread.report_on_exception = false
|
||||||
|
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||||
.with { |*arg| arg[0..1] == [:mkdir, 'kamal_lock-app'] }
|
Dir.stubs(:chdir)
|
||||||
.raises(RuntimeError, "mkdir: cannot create directory ‘kamal_lock-app’: File exists")
|
|
||||||
|
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||||
|
.with { |*args| args == [ :mkdir, "-p", ".kamal" ] }
|
||||||
|
|
||||||
|
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||||
|
.with { |*args| args == [ :mkdir, "-p", ".kamal/locks" ] }
|
||||||
|
|
||||||
|
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||||
|
.with { |*arg| arg[0..1] == [ :mkdir, ".kamal/locks/app" ] }
|
||||||
|
.raises(RuntimeError, "mkdir: cannot create directory ‘kamal/locks/app’: File exists")
|
||||||
|
|
||||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_debug)
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_debug)
|
||||||
.with(:stat, 'kamal_lock-app', ">", "/dev/null", "&&", :cat, "kamal_lock-app/details", "|", :base64, "-d")
|
.with(:stat, ".kamal/locks/app", ">", "/dev/null", "&&", :cat, ".kamal/locks/app/details", "|", :base64, "-d")
|
||||||
|
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
|
.with(:git, "-C", anything, :"rev-parse", :HEAD)
|
||||||
|
.returns(Kamal::Git.revision)
|
||||||
|
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
|
.with(:git, "-C", anything, :status, "--porcelain")
|
||||||
|
.returns("")
|
||||||
|
|
||||||
assert_raises(Kamal::Cli::LockError) do
|
assert_raises(Kamal::Cli::LockError) do
|
||||||
run_command("deploy")
|
run_command("deploy")
|
||||||
@@ -78,9 +125,26 @@ class CliMainTest < CliTestCase
|
|||||||
Thread.report_on_exception = false
|
Thread.report_on_exception = false
|
||||||
|
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||||
.with { |*arg| arg[0..1] == [:mkdir, 'kamal_lock-app'] }
|
Dir.stubs(:chdir)
|
||||||
|
|
||||||
|
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||||
|
.with { |*args| args == [ :mkdir, "-p", ".kamal" ] }
|
||||||
|
|
||||||
|
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||||
|
.with { |*args| args == [ :mkdir, "-p", ".kamal/locks" ] }
|
||||||
|
|
||||||
|
SSHKit::Backend::Abstract.any_instance.stubs(:execute)
|
||||||
|
.with { |*arg| arg[0..1] == [ :mkdir, ".kamal/locks/app" ] }
|
||||||
.raises(SocketError, "getaddrinfo: nodename nor servname provided, or not known")
|
.raises(SocketError, "getaddrinfo: nodename nor servname provided, or not known")
|
||||||
|
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
|
.with(:git, "-C", anything, :"rev-parse", :HEAD)
|
||||||
|
.returns(Kamal::Git.revision)
|
||||||
|
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
|
.with(:git, "-C", anything, :status, "--porcelain")
|
||||||
|
.returns("")
|
||||||
|
|
||||||
assert_raises(SSHKit::Runner::ExecuteError) do
|
assert_raises(SSHKit::Runner::ExecuteError) do
|
||||||
run_command("deploy")
|
run_command("deploy")
|
||||||
end
|
end
|
||||||
@@ -93,11 +157,11 @@ class CliMainTest < CliTestCase
|
|||||||
.with("kamal:cli:registry:login", [], invoke_options)
|
.with("kamal:cli:registry:login", [], invoke_options)
|
||||||
.raises(RuntimeError)
|
.raises(RuntimeError)
|
||||||
|
|
||||||
assert !KAMAL.holding_lock?
|
assert_not KAMAL.holding_lock?
|
||||||
assert_raises(RuntimeError) do
|
assert_raises(RuntimeError) do
|
||||||
stderred { run_command("deploy") }
|
stderred { run_command("deploy") }
|
||||||
end
|
end
|
||||||
assert !KAMAL.holding_lock?
|
assert_not KAMAL.holding_lock?
|
||||||
end
|
end
|
||||||
|
|
||||||
test "deploy with skipped hooks" do
|
test "deploy with skipped hooks" do
|
||||||
@@ -105,42 +169,46 @@ class CliMainTest < CliTestCase
|
|||||||
|
|
||||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:registry:login", [], invoke_options)
|
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:registry:login", [], invoke_options)
|
||||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:build:deliver", [], invoke_options)
|
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:build:deliver", [], invoke_options)
|
||||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:traefik:boot", [], invoke_options)
|
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:proxy:boot", [], invoke_options)
|
||||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:healthcheck:perform", [], invoke_options)
|
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true))
|
||||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:stale_containers", [], invoke_options)
|
|
||||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:boot", [], invoke_options)
|
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:boot", [], invoke_options)
|
||||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:prune:all", [], invoke_options)
|
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:prune:all", [], invoke_options)
|
||||||
|
|
||||||
run_command("deploy", "--skip_hooks") do
|
run_command("deploy", "--skip_hooks") do
|
||||||
refute_match /Running the post-deploy hook.../, output
|
assert_no_match /Running the post-deploy hook.../, output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "deploy with missing secrets" do
|
test "deploy with missing secrets" do
|
||||||
assert_raises(KeyError) do
|
invoke_options = { "config_file" => "test/fixtures/deploy_with_secrets.yml", "version" => "999", "skip_hooks" => false }
|
||||||
run_command("deploy", config_file: "deploy_with_secrets")
|
|
||||||
end
|
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:registry:login", [], invoke_options)
|
||||||
|
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:build:deliver", [], invoke_options)
|
||||||
|
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:proxy:boot", [], invoke_options)
|
||||||
|
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true))
|
||||||
|
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:boot", [], invoke_options)
|
||||||
|
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:prune:all", [], invoke_options)
|
||||||
|
|
||||||
|
run_command("deploy", config_file: "deploy_with_secrets")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "redeploy" do
|
test "redeploy" do
|
||||||
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "version" => "999", "skip_hooks" => false }
|
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "version" => "999", "skip_hooks" => false, "verbose" => true }
|
||||||
|
|
||||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:build:deliver", [], invoke_options)
|
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:build:deliver", [], invoke_options)
|
||||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:healthcheck:perform", [], invoke_options)
|
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true))
|
||||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:stale_containers", [], invoke_options)
|
|
||||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:boot", [], invoke_options)
|
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:boot", [], invoke_options)
|
||||||
|
|
||||||
Kamal::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)
|
Kamal::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)
|
||||||
|
|
||||||
hook_variables = { version: 999, service_version: "app@999", hosts: "1.1.1.1,1.1.1.2", command: "redeploy" }
|
hook_variables = { version: 999, service_version: "app@999", hosts: "1.1.1.1,1.1.1.2", command: "redeploy" }
|
||||||
|
|
||||||
run_command("redeploy").tap do |output|
|
run_command("redeploy", "--verbose").tap do |output|
|
||||||
assert_hook_ran "pre-connect", output, **hook_variables
|
assert_hook_ran "pre-connect", output, **hook_variables
|
||||||
assert_match /Build and push app image/, output
|
assert_match /Build and push app image/, output
|
||||||
assert_hook_ran "pre-deploy", output, **hook_variables
|
assert_hook_ran "pre-deploy", output, **hook_variables
|
||||||
assert_match /Running the pre-deploy hook.../, output
|
assert_match /Running the pre-deploy hook.../, output
|
||||||
assert_match /Ensure app can pass healthcheck/, output
|
assert_hook_ran "post-deploy", output, **hook_variables, runtime: true
|
||||||
assert_hook_ran "post-deploy", output, **hook_variables, runtime: "0"
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -148,13 +216,11 @@ class CliMainTest < CliTestCase
|
|||||||
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "version" => "999", "skip_hooks" => false }
|
invoke_options = { "config_file" => "test/fixtures/deploy_simple.yml", "version" => "999", "skip_hooks" => false }
|
||||||
|
|
||||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:build:pull", [], invoke_options)
|
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:build:pull", [], invoke_options)
|
||||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:healthcheck:perform", [], invoke_options)
|
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:stale_containers", [], invoke_options.merge(stop: true))
|
||||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:stale_containers", [], invoke_options)
|
|
||||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:boot", [], invoke_options)
|
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:boot", [], invoke_options)
|
||||||
|
|
||||||
run_command("redeploy", "--skip_push").tap do |output|
|
run_command("redeploy", "--skip_push").tap do |output|
|
||||||
assert_match /Pull app image/, output
|
assert_match /Pull app image/, output
|
||||||
assert_match /Ensure app can pass healthcheck/, output
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -172,56 +238,53 @@ class CliMainTest < CliTestCase
|
|||||||
test "rollback good version" do
|
test "rollback good version" do
|
||||||
[ "web", "workers" ].each do |role|
|
[ "web", "workers" ].each do |role|
|
||||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
.with(:docker, :container, :ls, "--filter", "name=^app-#{role}-123$", "--quiet", raise_on_non_zero_exit: false)
|
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-#{role}-123$", "--quiet", raise_on_non_zero_exit: false)
|
||||||
.returns("").at_least_once
|
.returns("").at_least_once
|
||||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
.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(:docker, :ps, "--filter", "label=service=app", "--filter", "label=role=#{role}", "--filter", "status=running", "--filter", "status=restarting", "--latest", "--format", "\"{{.Names}}\"", "|", "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=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)
|
||||||
.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)
|
|
||||||
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-#{role}-123$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'")
|
|
||||||
.returns("running").at_least_once # health check
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
|
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-123$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{.NetworkSettings.IPAddress}}{{range $k, $v := .NetworkSettings.Ports}}{{printf \":%s\" $k}}{{break}}{{end}}'", "|", :sed, "-e", "'s/\\/tcp$//'")
|
||||||
|
.returns("172.1.0.2:80").at_least_once
|
||||||
|
|
||||||
Kamal::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)
|
Kamal::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)
|
||||||
hook_variables = { version: 123, service_version: "app@123", hosts: "1.1.1.1,1.1.1.2,1.1.1.3,1.1.1.4", command: "rollback" }
|
hook_variables = { version: 123, service_version: "app@123", hosts: "1.1.1.1,1.1.1.2,1.1.1.3,1.1.1.4", command: "rollback" }
|
||||||
|
|
||||||
run_command("rollback", "123", config_file: "deploy_with_accessories").tap do |output|
|
run_command("rollback", "--verbose", "123", config_file: "deploy_with_accessories").tap do |output|
|
||||||
assert_match "Start container with version 123", output
|
|
||||||
assert_hook_ran "pre-deploy", output, **hook_variables
|
assert_hook_ran "pre-deploy", output, **hook_variables
|
||||||
assert_match "docker tag dhh/app:123 dhh/app:latest", output
|
assert_match "docker tag dhh/app:123 dhh/app:latest", output
|
||||||
assert_match "docker start app-web-123", output
|
assert_match "docker run --detach --restart unless-stopped --name app-web-123", output
|
||||||
assert_match "docker container ls --all --filter name=^app-web-version-to-rollback$ --quiet | xargs docker stop", output, "Should stop the container that was previously running"
|
assert_match "docker container ls --all --filter name=^app-web-version-to-rollback$ --quiet | xargs docker stop", output, "Should stop the container that was previously running"
|
||||||
assert_hook_ran "post-deploy", output, **hook_variables, runtime: "0"
|
assert_hook_ran "post-deploy", output, **hook_variables, runtime: true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "rollback without old version" do
|
test "rollback without old version" do
|
||||||
Kamal::Cli::Main.any_instance.stubs(:container_available?).returns(true)
|
Kamal::Cli::Main.any_instance.stubs(:container_available?).returns(true)
|
||||||
|
|
||||||
Kamal::Utils::HealthcheckPoller.stubs(:sleep)
|
|
||||||
|
|
||||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
.with(:docker, :container, :ls, "--filter", "name=^app-web-123$", "--quiet", raise_on_non_zero_exit: false)
|
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-123$", "--quiet", raise_on_non_zero_exit: false)
|
||||||
.returns("").at_least_once
|
.returns("").at_least_once
|
||||||
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", "--filter", "status=running", "--filter", "status=restarting", "--latest", "--format", "\"{{.Names}}\"", "|", "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=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)
|
||||||
.returns("").at_least_once
|
.returns("").at_least_once
|
||||||
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-123$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'")
|
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-123$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{.NetworkSettings.IPAddress}}{{range $k, $v := .NetworkSettings.Ports}}{{printf \":%s\" $k}}{{break}}{{end}}'", "|", :sed, "-e", "'s/\\/tcp$//'")
|
||||||
.returns("running").at_least_once # health check
|
.returns("127.1.0.4:80").at_least_once
|
||||||
|
|
||||||
run_command("rollback", "123").tap do |output|
|
run_command("rollback", "123").tap do |output|
|
||||||
assert_match "Start container with version 123", output
|
assert_match "docker run --detach --restart unless-stopped --name app-web-123", output
|
||||||
assert_match "docker start app-web-123 || docker run --detach --restart unless-stopped --name app-web-123", output
|
|
||||||
assert_no_match "docker stop", output
|
assert_no_match "docker stop", output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "details" do
|
test "details" do
|
||||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:traefik:details")
|
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:proxy:details")
|
||||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:details")
|
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:app:details")
|
||||||
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:accessory:details", [ "all" ])
|
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:accessory:details", [ "all" ])
|
||||||
|
|
||||||
@@ -230,7 +293,7 @@ class CliMainTest < CliTestCase
|
|||||||
|
|
||||||
test "audit" do
|
test "audit" do
|
||||||
run_command("audit").tap do |output|
|
run_command("audit").tap do |output|
|
||||||
assert_match /tail -n 50 kamal-app-audit.log on 1.1.1.1/, output
|
assert_match %r{tail -n 50 \.kamal/app-audit.log on 1.1.1.1}, output
|
||||||
assert_match /App Host: 1.1.1.1/, output
|
assert_match /App Host: 1.1.1.1/, output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -239,8 +302,8 @@ class CliMainTest < CliTestCase
|
|||||||
run_command("config", config_file: "deploy_simple").tap do |output|
|
run_command("config", config_file: "deploy_simple").tap do |output|
|
||||||
config = YAML.load(output)
|
config = YAML.load(output)
|
||||||
|
|
||||||
assert_equal ["web"], config[:roles]
|
assert_equal [ "web" ], config[:roles]
|
||||||
assert_equal ["1.1.1.1", "1.1.1.2"], config[:hosts]
|
assert_equal [ "1.1.1.1", "1.1.1.2" ], config[:hosts]
|
||||||
assert_equal "999", config[:version]
|
assert_equal "999", config[:version]
|
||||||
assert_equal "dhh/app", config[:repository]
|
assert_equal "dhh/app", config[:repository]
|
||||||
assert_equal "dhh/app:999", config[:absolute_image]
|
assert_equal "dhh/app:999", config[:absolute_image]
|
||||||
@@ -252,8 +315,8 @@ class CliMainTest < CliTestCase
|
|||||||
run_command("config", config_file: "deploy_with_roles").tap do |output|
|
run_command("config", config_file: "deploy_with_roles").tap do |output|
|
||||||
config = YAML.load(output)
|
config = YAML.load(output)
|
||||||
|
|
||||||
assert_equal ["web", "workers"], config[:roles]
|
assert_equal [ "web", "workers" ], config[:roles]
|
||||||
assert_equal ["1.1.1.1", "1.1.1.2", "1.1.1.3", "1.1.1.4"], config[:hosts]
|
assert_equal [ "1.1.1.1", "1.1.1.2", "1.1.1.3", "1.1.1.4" ], config[:hosts]
|
||||||
assert_equal "999", config[:version]
|
assert_equal "999", config[:version]
|
||||||
assert_equal "registry.digitalocean.com/dhh/app", config[:repository]
|
assert_equal "registry.digitalocean.com/dhh/app", config[:repository]
|
||||||
assert_equal "registry.digitalocean.com/dhh/app:999", config[:absolute_image]
|
assert_equal "registry.digitalocean.com/dhh/app:999", config[:absolute_image]
|
||||||
@@ -261,12 +324,35 @@ class CliMainTest < CliTestCase
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "config with primary web role override" do
|
||||||
|
run_command("config", config_file: "deploy_primary_web_role_override").tap do |output|
|
||||||
|
config = YAML.load(output)
|
||||||
|
|
||||||
|
assert_equal [ "web_chicago", "web_tokyo" ], config[:roles]
|
||||||
|
assert_equal [ "1.1.1.1", "1.1.1.2", "1.1.1.3", "1.1.1.4" ], config[:hosts]
|
||||||
|
assert_equal "1.1.1.3", config[:primary_host]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
test "config with destination" do
|
test "config with destination" do
|
||||||
run_command("config", "-d", "world", config_file: "deploy_for_dest").tap do |output|
|
run_command("config", "-d", "world", config_file: "deploy_for_dest").tap do |output|
|
||||||
config = YAML.load(output)
|
config = YAML.load(output)
|
||||||
|
|
||||||
assert_equal ["web"], config[:roles]
|
assert_equal [ "web" ], config[:roles]
|
||||||
assert_equal ["1.1.1.1", "1.1.1.2"], config[:hosts]
|
assert_equal [ "1.1.1.1", "1.1.1.2" ], config[:hosts]
|
||||||
|
assert_equal "999", config[:version]
|
||||||
|
assert_equal "registry.digitalocean.com/dhh/app", config[:repository]
|
||||||
|
assert_equal "registry.digitalocean.com/dhh/app:999", config[:absolute_image]
|
||||||
|
assert_equal "app-999", config[:service_with_version]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "config with aliases" do
|
||||||
|
run_command("config", config_file: "deploy_with_aliases").tap do |output|
|
||||||
|
config = YAML.load(output)
|
||||||
|
|
||||||
|
assert_equal [ "web", "web_tokyo", "workers", "workers_tokyo" ], config[:roles]
|
||||||
|
assert_equal [ "1.1.1.1", "1.1.1.2", "1.1.1.3", "1.1.1.4" ], config[:hosts]
|
||||||
assert_equal "999", config[:version]
|
assert_equal "999", config[:version]
|
||||||
assert_equal "registry.digitalocean.com/dhh/app", config[:repository]
|
assert_equal "registry.digitalocean.com/dhh/app", config[:repository]
|
||||||
assert_equal "registry.digitalocean.com/dhh/app:999", config[:absolute_image]
|
assert_equal "registry.digitalocean.com/dhh/app:999", config[:absolute_image]
|
||||||
@@ -326,24 +412,50 @@ class CliMainTest < CliTestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "envify" do
|
test "envify" do
|
||||||
|
Pathname.any_instance.expects(:exist?).returns(true).times(3)
|
||||||
File.expects(:read).with(".env.erb").returns("HELLO=<%= 'world' %>")
|
File.expects(:read).with(".env.erb").returns("HELLO=<%= 'world' %>")
|
||||||
File.expects(:write).with(".env", "HELLO=world", perm: 0600)
|
File.expects(:write).with(".env", "HELLO=world", perm: 0600)
|
||||||
|
|
||||||
run_command("envify")
|
run_command("envify")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "envify with destination" do
|
test "envify with blank line trimming" do
|
||||||
File.expects(:read).with(".env.staging.erb").returns("HELLO=<%= 'world' %>")
|
file = <<~EOF
|
||||||
File.expects(:write).with(".env.staging", "HELLO=world", perm: 0600)
|
HELLO=<%= 'world' %>
|
||||||
|
<% if true -%>
|
||||||
|
KEY=value
|
||||||
|
<% end -%>
|
||||||
|
EOF
|
||||||
|
|
||||||
run_command("envify", "-d", "staging")
|
Pathname.any_instance.expects(:exist?).returns(true).times(3)
|
||||||
|
File.expects(:read).with(".env.erb").returns(file.strip)
|
||||||
|
File.expects(:write).with(".env", "HELLO=world\nKEY=value\n", perm: 0600)
|
||||||
|
|
||||||
|
run_command("envify")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "envify with destination" do
|
||||||
|
Pathname.any_instance.expects(:exist?).returns(true).times(4)
|
||||||
|
File.expects(:read).with(".env.world.erb").returns("HELLO=<%= 'world' %>")
|
||||||
|
File.expects(:write).with(".env.world", "HELLO=world", perm: 0600)
|
||||||
|
|
||||||
|
run_command("envify", "-d", "world", config_file: "deploy_for_dest")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "envify with skip_push" do
|
||||||
|
Pathname.any_instance.expects(:exist?).returns(true).times(1)
|
||||||
|
File.expects(:read).with(".env.erb").returns("HELLO=<%= 'world' %>")
|
||||||
|
File.expects(:write).with(".env", "HELLO=world", perm: 0600)
|
||||||
|
|
||||||
|
Kamal::Cli::Main.any_instance.expects(:invoke).with("kamal:cli:env:push").never
|
||||||
|
run_command("envify", "--skip-push")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "remove with confirmation" do
|
test "remove with confirmation" do
|
||||||
run_command("remove", "-y", config_file: "deploy_with_accessories").tap do |output|
|
run_command("remove", "-y", config_file: "deploy_with_accessories").tap do |output|
|
||||||
assert_match /docker container stop traefik/, output
|
assert_match /docker container stop kamal-proxy/, output
|
||||||
assert_match /docker container prune --force --filter label=org.opencontainers.image.title=Traefik/, output
|
assert_match /docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy/, output
|
||||||
assert_match /docker image prune --all --force --filter label=org.opencontainers.image.title=Traefik/, output
|
assert_match /docker image prune --all --force --filter label=org.opencontainers.image.title=kamal-proxy/, output
|
||||||
|
|
||||||
assert_match /docker ps --quiet --filter label=service=app | xargs docker stop/, output
|
assert_match /docker ps --quiet --filter label=service=app | xargs docker stop/, output
|
||||||
assert_match /docker container prune --force --filter label=service=app/, output
|
assert_match /docker container prune --force --filter label=service=app/, output
|
||||||
@@ -370,6 +482,6 @@ class CliMainTest < CliTestCase
|
|||||||
|
|
||||||
private
|
private
|
||||||
def run_command(*command, config_file: "deploy_simple")
|
def run_command(*command, config_file: "deploy_simple")
|
||||||
stdouted { Kamal::Cli::Main.start([*command, "-c", "test/fixtures/#{config_file}.yml"]) }
|
stdouted { Kamal::Cli::Main.start([ *command, "-c", "test/fixtures/#{config_file}.yml" ]) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
116
test/cli/proxy_test.rb
Normal file
116
test/cli/proxy_test.rb
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
require_relative "cli_test_case"
|
||||||
|
|
||||||
|
class CliProxyTest < CliTestCase
|
||||||
|
test "boot" do
|
||||||
|
run_command("boot").tap do |output|
|
||||||
|
assert_match "docker login", output
|
||||||
|
assert_match "docker run --name kamal-proxy --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume kamal-proxy:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "reboot" do
|
||||||
|
Kamal::Commands::Registry.any_instance.expects(:login).twice
|
||||||
|
|
||||||
|
run_command("reboot", "-y").tap do |output|
|
||||||
|
assert_match "docker container stop kamal-proxy", output
|
||||||
|
assert_match "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy", output
|
||||||
|
assert_match "docker run --name kamal-proxy --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume kamal-proxy:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "reboot --rolling" do
|
||||||
|
run_command("reboot", "--rolling", "-y").tap do |output|
|
||||||
|
assert_match "Running docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy on 1.1.1.1", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "start" do
|
||||||
|
run_command("start").tap do |output|
|
||||||
|
assert_match "docker container start kamal-proxy", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "stop" do
|
||||||
|
run_command("stop").tap do |output|
|
||||||
|
assert_match "docker container stop kamal-proxy", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "restart" do
|
||||||
|
Kamal::Cli::Proxy.any_instance.expects(:stop)
|
||||||
|
Kamal::Cli::Proxy.any_instance.expects(:start)
|
||||||
|
|
||||||
|
run_command("restart")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "details" do
|
||||||
|
run_command("details").tap do |output|
|
||||||
|
assert_match "docker ps --filter name=^kamal-proxy$", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "logs" do
|
||||||
|
SSHKit::Backend::Abstract.any_instance.stubs(:capture)
|
||||||
|
.with(:docker, :logs, "kamal-proxy", " --tail 100", "--timestamps", "2>&1")
|
||||||
|
.returns("Log entry")
|
||||||
|
|
||||||
|
run_command("logs").tap do |output|
|
||||||
|
assert_match "Proxy Host: 1.1.1.1", output
|
||||||
|
assert_match "Log entry", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "logs with follow" do
|
||||||
|
SSHKit::Backend::Abstract.any_instance.stubs(:exec)
|
||||||
|
.with("ssh -t root@1.1.1.1 -p 22 'docker logs kamal-proxy --timestamps --tail 10 --follow 2>&1'")
|
||||||
|
|
||||||
|
assert_match "docker logs kamal-proxy --timestamps --tail 10 --follow", run_command("logs", "--follow")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "remove" do
|
||||||
|
Kamal::Cli::Proxy.any_instance.expects(:stop)
|
||||||
|
Kamal::Cli::Proxy.any_instance.expects(:remove_container)
|
||||||
|
Kamal::Cli::Proxy.any_instance.expects(:remove_image)
|
||||||
|
|
||||||
|
run_command("remove")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "remove_container" do
|
||||||
|
run_command("remove_container").tap do |output|
|
||||||
|
assert_match "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "remove_image" do
|
||||||
|
run_command("remove_image").tap do |output|
|
||||||
|
assert_match "docker image prune --all --force --filter label=org.opencontainers.image.title=kamal-proxy", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "update" do
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
|
.with(:docker, :container, :ls, "--all", "--filter", "name=^app-web-123$", "--quiet", "|", :xargs, :docker, :inspect, "--format", "'{{.NetworkSettings.IPAddress}}{{range $k, $v := .NetworkSettings.Ports}}{{printf \":%s\" $k}}{{break}}{{end}}'", "|", :sed, "-e", "'s/\\/tcp$//'")
|
||||||
|
.returns("172.1.0.2:80")
|
||||||
|
.at_least_once
|
||||||
|
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:capture_with_info)
|
||||||
|
.with { |*args| args[0..1] == [ :sh, "-c" ] }
|
||||||
|
.returns("123")
|
||||||
|
.at_least_once
|
||||||
|
|
||||||
|
run_command("update", "-y").tap do |output|
|
||||||
|
assert_match "docker container stop traefik", output
|
||||||
|
assert_match "docker container prune --force --filter label=org.opencontainers.image.title=traefik", output
|
||||||
|
assert_match "docker image prune --all --force --filter label=org.opencontainers.image.title=traefik", output
|
||||||
|
assert_match "docker container stop kamal-proxy", output
|
||||||
|
assert_match "docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy", output
|
||||||
|
assert_match "docker run --name kamal-proxy --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume kamal-proxy:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}", output
|
||||||
|
assert_match "docker exec kamal-proxy kamal-proxy deploy app-web --target \"172.1.0.2:80\"", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def run_command(*command)
|
||||||
|
stdouted { Kamal::Cli::Proxy.start([ *command, "-c", "test/fixtures/deploy_with_accessories.yml" ]) }
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -10,7 +10,7 @@ class CliPruneTest < CliTestCase
|
|||||||
|
|
||||||
test "images" do
|
test "images" do
|
||||||
run_command("images").tap do |output|
|
run_command("images").tap do |output|
|
||||||
assert_match "docker image prune --force --filter label=service=app --filter dangling=true on 1.1.1.", output
|
assert_match "docker image prune --force --filter label=service=app on 1.1.1.", output
|
||||||
assert_match "docker image ls --filter label=service=app --format '{{.ID}} {{.Repository}}:{{.Tag}}' | grep -v -w \"$(docker container ls -a --format '{{.Image}}\\|' --filter label=service=app | tr -d '\\n')dhh/app:latest\\|dhh/app:<none>\" | while read image tag; do docker rmi $tag; done on 1.1.1.", output
|
assert_match "docker image ls --filter label=service=app --format '{{.ID}} {{.Repository}}:{{.Tag}}' | grep -v -w \"$(docker container ls -a --format '{{.Image}}\\|' --filter label=service=app | tr -d '\\n')dhh/app:latest\\|dhh/app:<none>\" | while read image tag; do docker rmi $tag; done on 1.1.1.", output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -18,11 +18,19 @@ class CliPruneTest < CliTestCase
|
|||||||
test "containers" do
|
test "containers" do
|
||||||
run_command("containers").tap do |output|
|
run_command("containers").tap do |output|
|
||||||
assert_match /docker ps -q -a --filter label=service=app --filter status=created --filter status=exited --filter status=dead | tail -n +6 | while read container_id; do docker rm $container_id; done on 1.1.1.\d/, output
|
assert_match /docker ps -q -a --filter label=service=app --filter status=created --filter status=exited --filter status=dead | tail -n +6 | while read container_id; do docker rm $container_id; done on 1.1.1.\d/, output
|
||||||
|
end
|
||||||
|
|
||||||
|
run_command("containers", "--retain", "10").tap do |output|
|
||||||
|
assert_match /docker ps -q -a --filter label=service=app --filter status=created --filter status=exited --filter status=dead | tail -n +11 | while read container_id; do docker rm $container_id; done on 1.1.1.\d/, output
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_raises(RuntimeError, "retain must be at least 1") do
|
||||||
|
run_command("containers", "--retain", "0")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def run_command(*command)
|
def run_command(*command)
|
||||||
stdouted { Kamal::Cli::Prune.start([*command, "-c", "test/fixtures/deploy_with_accessories.yml"]) }
|
stdouted { Kamal::Cli::Prune.start([ *command, "-c", "test/fixtures/deploy_with_accessories.yml" ]) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -16,6 +16,6 @@ class CliRegistryTest < CliTestCase
|
|||||||
|
|
||||||
private
|
private
|
||||||
def run_command(*command)
|
def run_command(*command)
|
||||||
stdouted { Kamal::Cli::Registry.start([*command, "-c", "test/fixtures/deploy_with_accessories.yml"]) }
|
stdouted { Kamal::Cli::Registry.start([ *command, "-c", "test/fixtures/deploy_with_accessories.yml" ]) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,15 +1,33 @@
|
|||||||
require_relative "cli_test_case"
|
require_relative "cli_test_case"
|
||||||
|
|
||||||
class CliServerTest < CliTestCase
|
class CliServerTest < CliTestCase
|
||||||
test "bootstrap already installed" do
|
test "running a command with exec" do
|
||||||
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:docker, "-v", raise_on_non_zero_exit: false).returns(true).at_least_once
|
SSHKit::Backend::Abstract.any_instance.stubs(:capture)
|
||||||
|
.with("date", verbosity: 1)
|
||||||
|
.returns("Today")
|
||||||
|
|
||||||
assert_equal "", run_command("bootstrap")
|
hosts = "1.1.1.1".."1.1.1.4"
|
||||||
|
run_command("exec", "date").tap do |output|
|
||||||
|
hosts.map do |host|
|
||||||
|
assert_match "Running 'date' on #{hosts.to_a.join(', ')}...", output
|
||||||
|
assert_match "App Host: #{host}\nToday", output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "bootstrap already installed" do
|
||||||
|
stub_setup
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:docker, "-v", raise_on_non_zero_exit: false).returns(true).at_least_once
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:mkdir, "-p", ".kamal").returns("").at_least_once
|
||||||
|
|
||||||
|
assert_equal "Acquiring the deploy lock...\nReleasing the deploy lock...", run_command("bootstrap")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "bootstrap install as non-root user" do
|
test "bootstrap install as non-root user" do
|
||||||
|
stub_setup
|
||||||
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:docker, "-v", raise_on_non_zero_exit: false).returns(false).at_least_once
|
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:docker, "-v", raise_on_non_zero_exit: false).returns(false).at_least_once
|
||||||
SSHKit::Backend::Abstract.any_instance.expects(:execute).with('[ "${EUID:-$(id -u)}" -eq 0 ]', raise_on_non_zero_exit: false).returns(false).at_least_once
|
SSHKit::Backend::Abstract.any_instance.expects(:execute).with('[ "${EUID:-$(id -u)}" -eq 0 ] || command -v sudo >/dev/null || command -v su >/dev/null', raise_on_non_zero_exit: false).returns(false).at_least_once
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:mkdir, "-p", ".kamal").returns("").at_least_once
|
||||||
|
|
||||||
assert_raise RuntimeError, "Docker is not installed on 1.1.1.1, 1.1.1.3, 1.1.1.4, 1.1.1.2 and can't be automatically installed without having root access and the `curl` command available. Install Docker manually: https://docs.docker.com/engine/install/" do
|
assert_raise RuntimeError, "Docker is not installed on 1.1.1.1, 1.1.1.3, 1.1.1.4, 1.1.1.2 and can't be automatically installed without having root access and the `curl` command available. Install Docker manually: https://docs.docker.com/engine/install/" do
|
||||||
run_command("bootstrap")
|
run_command("bootstrap")
|
||||||
@@ -17,19 +35,25 @@ class CliServerTest < CliTestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "bootstrap install as root user" do
|
test "bootstrap install as root user" do
|
||||||
|
stub_setup
|
||||||
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:docker, "-v", raise_on_non_zero_exit: false).returns(false).at_least_once
|
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:docker, "-v", raise_on_non_zero_exit: false).returns(false).at_least_once
|
||||||
SSHKit::Backend::Abstract.any_instance.expects(:execute).with('[ "${EUID:-$(id -u)}" -eq 0 ]', raise_on_non_zero_exit: false).returns(true).at_least_once
|
SSHKit::Backend::Abstract.any_instance.expects(:execute).with('[ "${EUID:-$(id -u)}" -eq 0 ] || command -v sudo >/dev/null || command -v su >/dev/null', raise_on_non_zero_exit: false).returns(true).at_least_once
|
||||||
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:curl, "-fsSL", "https://get.docker.com", "|", :sh).at_least_once
|
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:sh, "-c", "'curl -fsSL https://get.docker.com || wget -O - https://get.docker.com || echo \"exit 1\"'", "|", :sh).at_least_once
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(:mkdir, "-p", ".kamal").returns("").at_least_once
|
||||||
|
Kamal::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true)
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(".kamal/hooks/pre-connect", anything).at_least_once
|
||||||
|
SSHKit::Backend::Abstract.any_instance.expects(:execute).with(".kamal/hooks/docker-setup", anything).at_least_once
|
||||||
|
|
||||||
run_command("bootstrap").tap do |output|
|
run_command("bootstrap").tap do |output|
|
||||||
("1.1.1.1".."1.1.1.4").map do |host|
|
("1.1.1.1".."1.1.1.4").map do |host|
|
||||||
assert_match "Missing Docker on #{host}. Installing…", output
|
assert_match "Missing Docker on #{host}. Installing…", output
|
||||||
|
assert_match "Running the docker-setup hook", output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def run_command(*command)
|
def run_command(*command)
|
||||||
stdouted { Kamal::Cli::Server.start([*command, "-c", "test/fixtures/deploy_with_accessories.yml"]) }
|
stdouted { Kamal::Cli::Server.start([ *command, "-c", "test/fixtures/deploy_with_accessories.yml" ]) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,94 +0,0 @@
|
|||||||
require_relative "cli_test_case"
|
|
||||||
|
|
||||||
class CliTraefikTest < CliTestCase
|
|
||||||
test "boot" do
|
|
||||||
run_command("boot").tap do |output|
|
|
||||||
assert_match "docker login", output
|
|
||||||
assert_match "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --log-opt max-size=\"10m\" #{Kamal::Commands::Traefik::DEFAULT_IMAGE} --providers.docker --log.level=\"DEBUG\"", output
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
test "reboot" do
|
|
||||||
Kamal::Commands::Registry.any_instance.expects(:login).twice
|
|
||||||
|
|
||||||
run_command("reboot").tap do |output|
|
|
||||||
assert_match "docker container stop traefik", output
|
|
||||||
assert_match "docker container prune --force --filter label=org.opencontainers.image.title=Traefik", output
|
|
||||||
assert_match "docker run --name traefik --detach --restart unless-stopped --publish 80:80 --volume /var/run/docker.sock:/var/run/docker.sock --log-opt max-size=\"10m\" #{Kamal::Commands::Traefik::DEFAULT_IMAGE} --providers.docker --log.level=\"DEBUG\"", output
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
test "reboot --rolling" do
|
|
||||||
run_command("reboot", "--rolling").tap do |output|
|
|
||||||
assert_match "Running docker container prune --force --filter label=org.opencontainers.image.title=Traefik on 1.1.1.1", output.lines[3]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
test "start" do
|
|
||||||
run_command("start").tap do |output|
|
|
||||||
assert_match "docker container start traefik", output
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
test "stop" do
|
|
||||||
run_command("stop").tap do |output|
|
|
||||||
assert_match "docker container stop traefik", output
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
test "restart" do
|
|
||||||
Kamal::Cli::Traefik.any_instance.expects(:stop)
|
|
||||||
Kamal::Cli::Traefik.any_instance.expects(:start)
|
|
||||||
|
|
||||||
run_command("restart")
|
|
||||||
end
|
|
||||||
|
|
||||||
test "details" do
|
|
||||||
run_command("details").tap do |output|
|
|
||||||
assert_match "docker ps --filter name=^traefik$", output
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
test "logs" do
|
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:capture)
|
|
||||||
.with(:docker, :logs, "traefik", " --tail 100", "--timestamps", "2>&1")
|
|
||||||
.returns("Log entry")
|
|
||||||
|
|
||||||
run_command("logs").tap do |output|
|
|
||||||
assert_match "Traefik Host: 1.1.1.1", output
|
|
||||||
assert_match "Log entry", output
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
test "logs with follow" do
|
|
||||||
SSHKit::Backend::Abstract.any_instance.stubs(:exec)
|
|
||||||
.with("ssh -t root@1.1.1.1 'docker logs traefik --timestamps --tail 10 --follow 2>&1'")
|
|
||||||
|
|
||||||
assert_match "docker logs traefik --timestamps --tail 10 --follow", run_command("logs", "--follow")
|
|
||||||
end
|
|
||||||
|
|
||||||
test "remove" do
|
|
||||||
Kamal::Cli::Traefik.any_instance.expects(:stop)
|
|
||||||
Kamal::Cli::Traefik.any_instance.expects(:remove_container)
|
|
||||||
Kamal::Cli::Traefik.any_instance.expects(:remove_image)
|
|
||||||
|
|
||||||
run_command("remove")
|
|
||||||
end
|
|
||||||
|
|
||||||
test "remove_container" do
|
|
||||||
run_command("remove_container").tap do |output|
|
|
||||||
assert_match "docker container prune --force --filter label=org.opencontainers.image.title=Traefik", output
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
test "remove_image" do
|
|
||||||
run_command("remove_image").tap do |output|
|
|
||||||
assert_match "docker image prune --all --force --filter label=org.opencontainers.image.title=Traefik", output
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
def run_command(*command)
|
|
||||||
stdouted { Kamal::Cli::Traefik.start([*command, "-c", "test/fixtures/deploy_with_accessories.yml"]) }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -14,6 +14,23 @@ class CommanderTest < ActiveSupport::TestCase
|
|||||||
|
|
||||||
@kamal.specific_hosts = [ "1.1.1.1", "1.1.1.2" ]
|
@kamal.specific_hosts = [ "1.1.1.1", "1.1.1.2" ]
|
||||||
assert_equal [ "1.1.1.1", "1.1.1.2" ], @kamal.hosts
|
assert_equal [ "1.1.1.1", "1.1.1.2" ], @kamal.hosts
|
||||||
|
|
||||||
|
@kamal.specific_hosts = [ "1.1.1.1*" ]
|
||||||
|
assert_equal [ "1.1.1.1" ], @kamal.hosts
|
||||||
|
|
||||||
|
@kamal.specific_hosts = [ "1.1.1.*", "*.1.2.*" ]
|
||||||
|
assert_equal [ "1.1.1.1", "1.1.1.2", "1.1.1.3", "1.1.1.4" ], @kamal.hosts
|
||||||
|
|
||||||
|
@kamal.specific_hosts = [ "*" ]
|
||||||
|
assert_equal [ "1.1.1.1", "1.1.1.2", "1.1.1.3", "1.1.1.4" ], @kamal.hosts
|
||||||
|
|
||||||
|
@kamal.specific_hosts = [ "1.1.1.[12]" ]
|
||||||
|
assert_equal [ "1.1.1.1", "1.1.1.2" ], @kamal.hosts
|
||||||
|
|
||||||
|
exception = assert_raises(ArgumentError) do
|
||||||
|
@kamal.specific_hosts = [ "*miss" ]
|
||||||
|
end
|
||||||
|
assert_match /hosts match for \*miss/, exception.message
|
||||||
end
|
end
|
||||||
|
|
||||||
test "filtering hosts by filtering roles" do
|
test "filtering hosts by filtering roles" do
|
||||||
@@ -21,6 +38,11 @@ class CommanderTest < ActiveSupport::TestCase
|
|||||||
|
|
||||||
@kamal.specific_roles = [ "web" ]
|
@kamal.specific_roles = [ "web" ]
|
||||||
assert_equal [ "1.1.1.1", "1.1.1.2" ], @kamal.hosts
|
assert_equal [ "1.1.1.1", "1.1.1.2" ], @kamal.hosts
|
||||||
|
|
||||||
|
exception = assert_raises(ArgumentError) do
|
||||||
|
@kamal.specific_roles = [ "*miss" ]
|
||||||
|
end
|
||||||
|
assert_match /roles match for \*miss/, exception.message
|
||||||
end
|
end
|
||||||
|
|
||||||
test "filtering roles" do
|
test "filtering roles" do
|
||||||
@@ -28,6 +50,23 @@ class CommanderTest < ActiveSupport::TestCase
|
|||||||
|
|
||||||
@kamal.specific_roles = [ "workers" ]
|
@kamal.specific_roles = [ "workers" ]
|
||||||
assert_equal [ "workers" ], @kamal.roles.map(&:name)
|
assert_equal [ "workers" ], @kamal.roles.map(&:name)
|
||||||
|
|
||||||
|
@kamal.specific_roles = [ "w*" ]
|
||||||
|
assert_equal [ "web", "workers" ], @kamal.roles.map(&:name)
|
||||||
|
|
||||||
|
@kamal.specific_roles = [ "we*", "*orkers" ]
|
||||||
|
assert_equal [ "web", "workers" ], @kamal.roles.map(&:name)
|
||||||
|
|
||||||
|
@kamal.specific_roles = [ "*" ]
|
||||||
|
assert_equal [ "web", "workers" ], @kamal.roles.map(&:name)
|
||||||
|
|
||||||
|
@kamal.specific_roles = [ "w{eb,orkers}" ]
|
||||||
|
assert_equal [ "web", "workers" ], @kamal.roles.map(&:name)
|
||||||
|
|
||||||
|
exception = assert_raises(ArgumentError) do
|
||||||
|
@kamal.specific_roles = [ "*miss" ]
|
||||||
|
end
|
||||||
|
assert_match /roles match for \*miss/, exception.message
|
||||||
end
|
end
|
||||||
|
|
||||||
test "filtering roles by filtering hosts" do
|
test "filtering roles by filtering hosts" do
|
||||||
@@ -49,9 +88,20 @@ class CommanderTest < ActiveSupport::TestCase
|
|||||||
assert_equal "1.1.1.3", @kamal.primary_host
|
assert_equal "1.1.1.3", @kamal.primary_host
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "primary_role" do
|
||||||
|
assert_equal "web", @kamal.primary_role.name
|
||||||
|
@kamal.specific_roles = "workers"
|
||||||
|
assert_equal "workers", @kamal.primary_role.name
|
||||||
|
end
|
||||||
|
|
||||||
test "roles_on" do
|
test "roles_on" do
|
||||||
assert_equal [ "web" ], @kamal.roles_on("1.1.1.1")
|
assert_equal [ "web" ], @kamal.roles_on("1.1.1.1").map(&:name)
|
||||||
assert_equal [ "workers" ], @kamal.roles_on("1.1.1.3")
|
assert_equal [ "workers" ], @kamal.roles_on("1.1.1.3").map(&:name)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "roles_on web comes first" do
|
||||||
|
configure_with(:deploy_with_two_roles_one_host)
|
||||||
|
assert_equal [ "web", "workers" ], @kamal.roles_on("1.1.1.1").map(&:name)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "default group strategy" do
|
test "default group strategy" do
|
||||||
@@ -70,6 +120,36 @@ class CommanderTest < ActiveSupport::TestCase
|
|||||||
assert_equal({ in: :groups, limit: 1, wait: 2 }, @kamal.boot_strategy)
|
assert_equal({ in: :groups, limit: 1, wait: 2 }, @kamal.boot_strategy)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "percentage-based group strategy limit is at least 1" do
|
||||||
|
configure_with(:deploy_with_low_percentage_boot_strategy)
|
||||||
|
|
||||||
|
assert_equal({ in: :groups, limit: 1, wait: 2 }, @kamal.boot_strategy)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "try to match the primary role from a list of specific roles" do
|
||||||
|
configure_with(:deploy_primary_web_role_override)
|
||||||
|
|
||||||
|
@kamal.specific_roles = [ "web_*" ]
|
||||||
|
assert_equal [ "web_tokyo", "web_chicago" ], @kamal.roles.map(&:name)
|
||||||
|
assert_equal "web_tokyo", @kamal.primary_role.name
|
||||||
|
assert_equal "1.1.1.3", @kamal.primary_host
|
||||||
|
assert_equal [ "1.1.1.3", "1.1.1.4", "1.1.1.1", "1.1.1.2" ], @kamal.hosts
|
||||||
|
end
|
||||||
|
|
||||||
|
test "proxy hosts should observe filtered roles" do
|
||||||
|
configure_with(:deploy_with_aliases)
|
||||||
|
|
||||||
|
@kamal.specific_roles = [ "web_tokyo" ]
|
||||||
|
assert_equal [ "1.1.1.3", "1.1.1.4" ], @kamal.proxy_hosts
|
||||||
|
end
|
||||||
|
|
||||||
|
test "proxy hosts should observe filtered hosts" do
|
||||||
|
configure_with(:deploy_with_aliases)
|
||||||
|
|
||||||
|
@kamal.specific_hosts = [ "1.1.1.4" ]
|
||||||
|
assert_equal [ "1.1.1.4" ], @kamal.proxy_hosts
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def configure_with(variant)
|
def configure_with(variant)
|
||||||
@kamal = Kamal::Commander.new.tap do |kamal|
|
@kamal = Kamal::Commander.new.tap do |kamal|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"busybox" => {
|
"busybox" => {
|
||||||
|
"service" => "custom-busybox",
|
||||||
"image" => "busybox:latest",
|
"image" => "busybox:latest",
|
||||||
"host" => "1.1.1.7"
|
"host" => "1.1.1.7"
|
||||||
}
|
}
|
||||||
@@ -49,15 +50,15 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
|
|||||||
|
|
||||||
test "run" do
|
test "run" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker run --name app-mysql --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 3306:3306 -e MYSQL_ROOT_PASSWORD=\"secret123\" -e MYSQL_ROOT_HOST=\"%\" --label service=\"app-mysql\" private.registry/mysql:8.0",
|
"docker run --name app-mysql --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 3306:3306 --env-file .kamal/env/accessories/app-mysql.env --env MYSQL_ROOT_HOST=\"%\" --label service=\"app-mysql\" private.registry/mysql:8.0",
|
||||||
new_command(:mysql).run.join(" ")
|
new_command(:mysql).run.join(" ")
|
||||||
|
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker run --name app-redis --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 6379:6379 -e SOMETHING=\"else\" --volume /var/lib/redis:/data --label service=\"app-redis\" --label cache=\"true\" redis:latest",
|
"docker run --name app-redis --detach --restart unless-stopped --log-opt max-size=\"10m\" --publish 6379:6379 --env-file .kamal/env/accessories/app-redis.env --env SOMETHING=\"else\" --volume /var/lib/redis:/data --label service=\"app-redis\" --label cache=\"true\" redis:latest",
|
||||||
new_command(:redis).run.join(" ")
|
new_command(:redis).run.join(" ")
|
||||||
|
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker run --name app-busybox --detach --restart unless-stopped --log-opt max-size=\"10m\" --label service=\"app-busybox\" busybox:latest",
|
"docker run --name custom-busybox --detach --restart unless-stopped --log-opt max-size=\"10m\" --env-file .kamal/env/accessories/custom-busybox.env --label service=\"custom-busybox\" busybox:latest",
|
||||||
new_command(:busybox).run.join(" ")
|
new_command(:busybox).run.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -65,7 +66,7 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
|
|||||||
@config[:logging] = { "driver" => "local", "options" => { "max-size" => "100m", "max-file" => "3" } }
|
@config[:logging] = { "driver" => "local", "options" => { "max-size" => "100m", "max-file" => "3" } }
|
||||||
|
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker run --name app-busybox --detach --restart unless-stopped --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --label service=\"app-busybox\" busybox:latest",
|
"docker run --name custom-busybox --detach --restart unless-stopped --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --env-file .kamal/env/accessories/custom-busybox.env --label service=\"custom-busybox\" busybox:latest",
|
||||||
new_command(:busybox).run.join(" ")
|
new_command(:busybox).run.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -90,7 +91,7 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
|
|||||||
|
|
||||||
test "execute in new container" do
|
test "execute in new container" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker run --rm -e MYSQL_ROOT_PASSWORD=\"secret123\" -e MYSQL_ROOT_HOST=\"%\" private.registry/mysql:8.0 mysql -u root",
|
"docker run --rm --env-file .kamal/env/accessories/app-mysql.env --env MYSQL_ROOT_HOST=\"%\" private.registry/mysql:8.0 mysql -u root",
|
||||||
new_command(:mysql).execute_in_new_container("mysql", "-u", "root").join(" ")
|
new_command(:mysql).execute_in_new_container("mysql", "-u", "root").join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -102,14 +103,14 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
|
|||||||
|
|
||||||
test "execute in new container over ssh" do
|
test "execute in new container over ssh" do
|
||||||
new_command(:mysql).stub(:run_over_ssh, ->(cmd) { cmd.join(" ") }) do
|
new_command(:mysql).stub(:run_over_ssh, ->(cmd) { cmd.join(" ") }) do
|
||||||
assert_match %r|docker run -it --rm -e MYSQL_ROOT_PASSWORD=\"secret123\" -e MYSQL_ROOT_HOST=\"%\" private.registry/mysql:8.0 mysql -u root|,
|
assert_match %r{docker run -it --rm --env-file .kamal/env/accessories/app-mysql.env --env MYSQL_ROOT_HOST=\"%\" private.registry/mysql:8.0 mysql -u root},
|
||||||
new_command(:mysql).execute_in_new_container_over_ssh("mysql", "-u", "root")
|
new_command(:mysql).execute_in_new_container_over_ssh("mysql", "-u", "root")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "execute in existing container over ssh" do
|
test "execute in existing container over ssh" do
|
||||||
new_command(:mysql).stub(:run_over_ssh, ->(cmd) { cmd.join(" ") }) do
|
new_command(:mysql).stub(:run_over_ssh, ->(cmd) { cmd.join(" ") }) do
|
||||||
assert_match %r|docker exec -it app-mysql mysql -u root|,
|
assert_match %r{docker exec -it app-mysql mysql -u root},
|
||||||
new_command(:mysql).execute_in_existing_container_over_ssh("mysql", "-u", "root")
|
new_command(:mysql).execute_in_existing_container_over_ssh("mysql", "-u", "root")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -128,7 +129,7 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
|
|||||||
|
|
||||||
test "follow logs" do
|
test "follow logs" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"ssh -t root@1.1.1.5 'docker logs app-mysql --timestamps --tail 10 --follow 2>&1'",
|
"ssh -t root@1.1.1.5 -p 22 'docker logs app-mysql --timestamps --tail 10 --follow 2>&1'",
|
||||||
new_command(:mysql).follow_logs
|
new_command(:mysql).follow_logs
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -144,6 +145,14 @@ class CommandsAccessoryTest < ActiveSupport::TestCase
|
|||||||
new_command(:mysql).remove_image.join(" ")
|
new_command(:mysql).remove_image.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "make_env_directory" do
|
||||||
|
assert_equal "mkdir -p .kamal/env/accessories", new_command(:mysql).make_env_directory.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "remove_env_file" do
|
||||||
|
assert_equal "rm -f .kamal/env/accessories/app-mysql.env", new_command(:mysql).remove_env_file.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def new_command(accessory)
|
def new_command(accessory)
|
||||||
Kamal::Commands::Accessory.new(Kamal::Configuration.new(@config), name: accessory)
|
Kamal::Commands::Accessory.new(Kamal::Configuration.new(@config), name: accessory)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ require "test_helper"
|
|||||||
class CommandsAppTest < ActiveSupport::TestCase
|
class CommandsAppTest < ActiveSupport::TestCase
|
||||||
setup do
|
setup do
|
||||||
ENV["RAILS_MASTER_KEY"] = "456"
|
ENV["RAILS_MASTER_KEY"] = "456"
|
||||||
|
Kamal::Configuration.any_instance.stubs(:run_id).returns("12345678901234567890123456789012")
|
||||||
|
|
||||||
@config = { service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ], env: { "secret" => [ "RAILS_MASTER_KEY" ] } }
|
@config = { service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ], env: { "secret" => [ "RAILS_MASTER_KEY" ] } }
|
||||||
end
|
end
|
||||||
@@ -13,60 +14,54 @@ class CommandsAppTest < ActiveSupport::TestCase
|
|||||||
|
|
||||||
test "run" do
|
test "run" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e RAILS_MASTER_KEY=\"456\" --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999",
|
"docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-web.env --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label destination dhh/app:999",
|
||||||
new_command.run.join(" ")
|
new_command.run.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "run with hostname" do
|
test "run with hostname" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker run --detach --restart unless-stopped --name app-web-999 --hostname myhost -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e RAILS_MASTER_KEY=\"456\" --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999",
|
"docker run --detach --restart unless-stopped --name app-web-999 --hostname myhost -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-web.env --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label destination dhh/app:999",
|
||||||
new_command.run(hostname: "myhost").join(" ")
|
new_command.run(hostname: "myhost").join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "run with volumes" do
|
test "run with volumes" do
|
||||||
@config[:volumes] = ["/local/path:/container/path" ]
|
@config[:volumes] = [ "/local/path:/container/path" ]
|
||||||
|
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e RAILS_MASTER_KEY=\"456\" --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" --log-opt max-size=\"10m\" --volume /local/path:/container/path --label service=\"app\" --label role=\"web\" --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999",
|
"docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-web.env --log-opt max-size=\"10m\" --volume /local/path:/container/path --label service=\"app\" --label role=\"web\" --label destination dhh/app:999",
|
||||||
new_command.run.join(" ")
|
|
||||||
end
|
|
||||||
|
|
||||||
test "run with custom healthcheck path" do
|
|
||||||
@config[:healthcheck] = { "path" => "/healthz" }
|
|
||||||
|
|
||||||
assert_equal \
|
|
||||||
"docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e RAILS_MASTER_KEY=\"456\" --health-cmd \"curl -f http://localhost:3000/healthz || exit 1\" --health-interval \"1s\" --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999",
|
|
||||||
new_command.run.join(" ")
|
|
||||||
end
|
|
||||||
|
|
||||||
test "run with custom healthcheck command" do
|
|
||||||
@config[:healthcheck] = { "cmd" => "/bin/up" }
|
|
||||||
|
|
||||||
assert_equal \
|
|
||||||
"docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e RAILS_MASTER_KEY=\"456\" --health-cmd \"/bin/up\" --health-interval \"1s\" --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999",
|
|
||||||
new_command.run.join(" ")
|
|
||||||
end
|
|
||||||
|
|
||||||
test "run with role-specific healthcheck options" do
|
|
||||||
@config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "healthcheck" => { "cmd" => "/bin/healthy" } } }
|
|
||||||
|
|
||||||
assert_equal \
|
|
||||||
"docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e RAILS_MASTER_KEY=\"456\" --health-cmd \"/bin/healthy\" --health-interval \"1s\" --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999",
|
|
||||||
new_command.run.join(" ")
|
new_command.run.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "run with custom options" do
|
test "run with custom options" do
|
||||||
@config[:servers] = { "web" => [ "1.1.1.1" ], "jobs" => { "hosts" => [ "1.1.1.2" ], "cmd" => "bin/jobs", "options" => { "mount" => "somewhere", "cap-add" => true } } }
|
@config[:servers] = { "web" => [ "1.1.1.1" ], "jobs" => { "hosts" => [ "1.1.1.2" ], "cmd" => "bin/jobs", "options" => { "mount" => "somewhere", "cap-add" => true } } }
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker run --detach --restart unless-stopped --name app-jobs-999 -e KAMAL_CONTAINER_NAME=\"app-jobs-999\" -e RAILS_MASTER_KEY=\"456\" --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"jobs\" --mount \"somewhere\" --cap-add dhh/app:999 bin/jobs",
|
"docker run --detach --restart unless-stopped --name app-jobs-999 -e KAMAL_CONTAINER_NAME=\"app-jobs-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-jobs.env --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"jobs\" --label destination --mount \"somewhere\" --cap-add dhh/app:999 bin/jobs",
|
||||||
new_command(role: "jobs").run.join(" ")
|
new_command(role: "jobs", host: "1.1.1.2").run.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "run with logging config" do
|
test "run with logging config" do
|
||||||
@config[:logging] = { "driver" => "local", "options" => { "max-size" => "100m", "max-file" => "3" } }
|
@config[:logging] = { "driver" => "local", "options" => { "max-size" => "100m", "max-file" => "3" } }
|
||||||
|
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e RAILS_MASTER_KEY=\"456\" --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --label service=\"app\" --label role=\"web\" --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999",
|
"docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-web.env --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --label service=\"app\" --label role=\"web\" --label destination dhh/app:999",
|
||||||
|
new_command.run.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "run with role logging config" do
|
||||||
|
@config[:logging] = { "driver" => "local", "options" => { "max-size" => "10m", "max-file" => "3" } }
|
||||||
|
@config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "logging" => { "driver" => "local", "options" => { "max-size" => "100m" } } } }
|
||||||
|
|
||||||
|
assert_equal \
|
||||||
|
"docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-web.env --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" --label service=\"app\" --label role=\"web\" --label destination dhh/app:999",
|
||||||
|
new_command.run.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "run with tags" do
|
||||||
|
@config[:servers] = [ { "1.1.1.1" => "tag1" } ]
|
||||||
|
@config[:env]["tags"] = { "tag1" => { "ENV1" => "value1" } }
|
||||||
|
|
||||||
|
assert_equal \
|
||||||
|
"docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e KAMAL_VERSION=\"999\" --env-file .kamal/env/roles/app-web.env --env ENV1=\"value1\" --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label destination dhh/app:999",
|
||||||
new_command.run.join(" ")
|
new_command.run.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -83,28 +78,16 @@ class CommandsAppTest < ActiveSupport::TestCase
|
|||||||
new_command.start.join(" ")
|
new_command.start.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "start_or_run" do
|
|
||||||
assert_equal \
|
|
||||||
"docker start app-web-999 || docker run --detach --restart unless-stopped --name app-web-999 -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e RAILS_MASTER_KEY=\"456\" --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999",
|
|
||||||
new_command.start_or_run.join(" ")
|
|
||||||
end
|
|
||||||
|
|
||||||
test "start_or_run with hostname" do
|
|
||||||
assert_equal \
|
|
||||||
"docker start app-web-999 || docker run --detach --restart unless-stopped --name app-web-999 --hostname myhost -e KAMAL_CONTAINER_NAME=\"app-web-999\" -e RAILS_MASTER_KEY=\"456\" --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" --log-opt max-size=\"10m\" --label service=\"app\" --label role=\"web\" --label traefik.http.services.app-web.loadbalancer.server.scheme=\"http\" --label traefik.http.routers.app-web.rule=\"PathPrefix(\\`/\\`)\" --label traefik.http.middlewares.app-web-retry.retry.attempts=\"5\" --label traefik.http.middlewares.app-web-retry.retry.initialinterval=\"500ms\" --label traefik.http.routers.app-web.middlewares=\"app-web-retry@docker\" dhh/app:999",
|
|
||||||
new_command.start_or_run(hostname: "myhost").join(" ")
|
|
||||||
end
|
|
||||||
|
|
||||||
test "stop" do
|
test "stop" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | xargs docker stop",
|
"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",
|
||||||
new_command.stop.join(" ")
|
new_command.stop.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "stop with custom stop wait time" do
|
test "stop with custom stop wait time" do
|
||||||
@config[:stop_wait_time] = 30
|
@config[:stop_wait_time] = 30
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | xargs docker stop -t 30",
|
"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 -t 30",
|
||||||
new_command.stop.join(" ")
|
new_command.stop.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -130,115 +113,157 @@ class CommandsAppTest < ActiveSupport::TestCase
|
|||||||
|
|
||||||
test "logs" do
|
test "logs" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | xargs docker logs 2>&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 2>&1",
|
||||||
new_command.logs.join(" ")
|
new_command.logs.join(" ")
|
||||||
|
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | xargs docker logs --since 5m 2>&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 --since 5m 2>&1",
|
||||||
new_command.logs(since: "5m").join(" ")
|
new_command.logs(since: "5m").join(" ")
|
||||||
|
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | xargs docker logs --tail 100 2>&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 --tail 100 2>&1",
|
||||||
new_command.logs(lines: "100").join(" ")
|
new_command.logs(lines: "100").join(" ")
|
||||||
|
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | xargs docker logs --since 5m --tail 100 2>&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 --since 5m --tail 100 2>&1",
|
||||||
new_command.logs(since: "5m", lines: "100").join(" ")
|
new_command.logs(since: "5m", lines: "100").join(" ")
|
||||||
|
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | xargs docker logs 2>&1 | grep 'my-id'",
|
"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 2>&1 | grep 'my-id'",
|
||||||
new_command.logs(grep: "my-id").join(" ")
|
new_command.logs(grep: "my-id").join(" ")
|
||||||
|
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | xargs docker logs --since 5m 2>&1 | grep 'my-id'",
|
"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 --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_match \
|
assert_equal \
|
||||||
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | xargs docker logs --timestamps --tail 10 --follow 2>&1",
|
"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'",
|
||||||
new_command.follow_logs(host: "app-1")
|
new_command.follow_logs(host: "app-1")
|
||||||
|
|
||||||
assert_match \
|
assert_equal \
|
||||||
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest | xargs docker logs --timestamps --tail 10 --follow 2>&1 | grep \"Completed\"",
|
"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\"'",
|
||||||
new_command.follow_logs(host: "app-1", grep: "Completed")
|
new_command.follow_logs(host: "app-1", grep: "Completed")
|
||||||
|
|
||||||
|
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'",
|
||||||
|
new_command.follow_logs(host: "app-1", lines: 123)
|
||||||
|
|
||||||
|
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\"'",
|
||||||
|
new_command.follow_logs(host: "app-1", 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 -e RAILS_MASTER_KEY=\"456\" dhh/app:999 bin/rails db:setup",
|
"docker run --rm --env-file .kamal/env/roles/app-web.env dhh/app:999 bin/rails db:setup",
|
||||||
new_command.execute_in_new_container("bin/rails", "db:setup").join(" ")
|
new_command.execute_in_new_container("bin/rails", "db:setup", env: {}).join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "execute in new container with env" do
|
||||||
|
assert_equal \
|
||||||
|
"docker run --rm --env-file .kamal/env/roles/app-web.env --env foo=\"bar\" dhh/app:999 bin/rails db:setup",
|
||||||
|
new_command.execute_in_new_container("bin/rails", "db:setup", env: { "foo" => "bar" }).join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "execute in new container with tags" do
|
||||||
|
@config[:servers] = [ { "1.1.1.1" => "tag1" } ]
|
||||||
|
@config[:env]["tags"] = { "tag1" => { "ENV1" => "value1" } }
|
||||||
|
|
||||||
|
assert_equal \
|
||||||
|
"docker run --rm --env-file .kamal/env/roles/app-web.env --env ENV1=\"value1\" dhh/app:999 bin/rails db:setup",
|
||||||
|
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 -e RAILS_MASTER_KEY=\"456\" --mount \"somewhere\" --cap-add dhh/app:999 bin/rails db:setup",
|
"docker run --rm --env-file .kamal/env/roles/app-web.env --mount \"somewhere\" --cap-add dhh/app:999 bin/rails db:setup",
|
||||||
new_command.execute_in_new_container("bin/rails", "db:setup").join(" ")
|
new_command.execute_in_new_container("bin/rails", "db:setup", env: {}).join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "execute in existing container" do
|
test "execute in existing container" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker exec app-web-999 bin/rails db:setup",
|
"docker exec app-web-999 bin/rails db:setup",
|
||||||
new_command.execute_in_existing_container("bin/rails", "db:setup").join(" ")
|
new_command.execute_in_existing_container("bin/rails", "db:setup", env: {}).join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "execute in existing container with env" do
|
||||||
|
assert_equal \
|
||||||
|
"docker exec --env foo=\"bar\" app-web-999 bin/rails db:setup",
|
||||||
|
new_command.execute_in_existing_container("bin/rails", "db:setup", env: { "foo" => "bar" }).join(" ")
|
||||||
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 -e RAILS_MASTER_KEY=\"456\" dhh/app:999 bin/rails c|,
|
assert_match %r{docker run -it --rm --env-file .kamal/env/roles/app-web.env dhh/app:999 bin/rails c},
|
||||||
new_command.execute_in_new_container_over_ssh("bin/rails", "c", host: "app-1")
|
new_command.execute_in_new_container_over_ssh("bin/rails", "c", env: {})
|
||||||
|
end
|
||||||
|
|
||||||
|
test "execute in new container over ssh with tags" do
|
||||||
|
@config[:servers] = [ { "1.1.1.1" => "tag1" } ]
|
||||||
|
@config[:env]["tags"] = { "tag1" => { "ENV1" => "value1" } }
|
||||||
|
|
||||||
|
assert_equal "ssh -t root@1.1.1.1 -p 22 'docker run -it --rm --env-file .kamal/env/roles/app-web.env --env ENV1=\"value1\" dhh/app:999 bin/rails c'",
|
||||||
|
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 -e RAILS_MASTER_KEY=\"456\" --mount \"somewhere\" --cap-add dhh/app:999 bin/rails c|,
|
assert_match %r{docker run -it --rm --env-file .kamal/env/roles/app-web.env --mount \"somewhere\" --cap-add dhh/app:999 bin/rails c},
|
||||||
new_command.execute_in_new_container_over_ssh("bin/rails", "c", host: "app-1")
|
new_command.execute_in_new_container_over_ssh("bin/rails", "c", env: {})
|
||||||
end
|
end
|
||||||
|
|
||||||
test "execute in existing container over ssh" do
|
test "execute in existing container over ssh" do
|
||||||
assert_match %r|docker exec -it app-web-999 bin/rails c|,
|
assert_match %r{docker exec -it app-web-999 bin/rails c},
|
||||||
new_command.execute_in_existing_container_over_ssh("bin/rails", "c", host: "app-1")
|
new_command.execute_in_existing_container_over_ssh("bin/rails", "c", env: {})
|
||||||
end
|
end
|
||||||
|
|
||||||
test "run over ssh" do
|
test "run over ssh" do
|
||||||
assert_equal "ssh -t root@1.1.1.1 'ls'", new_command.run_over_ssh("ls", host: "1.1.1.1")
|
assert_equal "ssh -t root@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 custom user" do
|
test "run over ssh with custom user" do
|
||||||
@config[:ssh] = { "user" => "app" }
|
@config[:ssh] = { "user" => "app" }
|
||||||
assert_equal "ssh -t app@1.1.1.1 'ls'", new_command.run_over_ssh("ls", host: "1.1.1.1")
|
assert_equal "ssh -t app@1.1.1.1 -p 22 'ls'", new_command.run_over_ssh("ls", host: "1.1.1.1")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "run over ssh with custom port" do
|
||||||
|
@config[:ssh] = { "port" => "2222" }
|
||||||
|
assert_equal "ssh -t root@1.1.1.1 -p 2222 'ls'", new_command.run_over_ssh("ls", host: "1.1.1.1")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "run over ssh with proxy" do
|
test "run over ssh with proxy" do
|
||||||
@config[:ssh] = { "proxy" => "2.2.2.2" }
|
@config[:ssh] = { "proxy" => "2.2.2.2" }
|
||||||
assert_equal "ssh -J root@2.2.2.2 -t root@1.1.1.1 'ls'", new_command.run_over_ssh("ls", host: "1.1.1.1")
|
assert_equal "ssh -J root@2.2.2.2 -t root@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 proxy user" do
|
test "run over ssh with proxy user" do
|
||||||
@config[:ssh] = { "proxy" => "app@2.2.2.2" }
|
@config[:ssh] = { "proxy" => "app@2.2.2.2" }
|
||||||
assert_equal "ssh -J app@2.2.2.2 -t root@1.1.1.1 'ls'", new_command.run_over_ssh("ls", host: "1.1.1.1")
|
assert_equal "ssh -J app@2.2.2.2 -t root@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 custom user with proxy" do
|
test "run over ssh with custom user with proxy" do
|
||||||
@config[:ssh] = { "user" => "app", "proxy" => "2.2.2.2" }
|
@config[:ssh] = { "user" => "app", "proxy" => "2.2.2.2" }
|
||||||
assert_equal "ssh -J root@2.2.2.2 -t app@1.1.1.1 '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 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 '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")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "current_running_container_id" do
|
test "current_running_container_id" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker ps --quiet --filter label=service=app --filter label=role=web --filter status=running --filter status=restarting --latest",
|
"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",
|
||||||
new_command.current_running_container_id.join(" ")
|
new_command.current_running_container_id.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "current_running_container_id with destination" do
|
test "current_running_container_id with destination" do
|
||||||
@destination = "staging"
|
@destination = "staging"
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker ps --quiet --filter label=service=app --filter label=destination=staging --filter label=role=web --filter status=running --filter status=restarting --latest",
|
"sh -c 'docker ps --latest --quiet --filter label=service=app --filter label=destination=staging --filter label=role=web --filter status=running --filter status=restarting --filter ancestor=$(docker image ls --filter reference=dhh/app:latest-staging --format '\\''{{.ID}}'\\'') ; docker ps --latest --quiet --filter label=service=app --filter label=destination=staging --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
|
||||||
|
|
||||||
@@ -250,7 +275,7 @@ class CommandsAppTest < ActiveSupport::TestCase
|
|||||||
|
|
||||||
test "current_running_version" do
|
test "current_running_version" do
|
||||||
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",
|
"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",
|
||||||
new_command.current_running_version.join(" ")
|
new_command.current_running_version.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -328,14 +353,61 @@ class CommandsAppTest < ActiveSupport::TestCase
|
|||||||
new_command.remove_images.join(" ")
|
new_command.remove_images.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "tag_current_as_latest" do
|
test "tag_latest_image" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker tag dhh/app:999 dhh/app:latest",
|
"docker tag dhh/app:999 dhh/app:latest",
|
||||||
new_command.tag_current_as_latest.join(" ")
|
new_command.tag_latest_image.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "tag_latest_image with destination" do
|
||||||
|
@destination = "staging"
|
||||||
|
assert_equal \
|
||||||
|
"docker tag dhh/app:999 dhh/app:latest-staging",
|
||||||
|
new_command.tag_latest_image.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "make_env_directory" do
|
||||||
|
assert_equal "mkdir -p .kamal/env/roles", new_command.make_env_directory.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "remove_env_file" do
|
||||||
|
assert_equal "rm -f .kamal/env/roles/app-web.env", new_command.remove_env_file.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "extract assets" do
|
||||||
|
assert_equal [
|
||||||
|
:mkdir, "-p", ".kamal/assets/extracted/app-web-999", "&&",
|
||||||
|
:docker, :stop, "-t 1", "app-web-assets", "2> /dev/null", "|| true", "&&",
|
||||||
|
:docker, :run, "--name", "app-web-assets", "--detach", "--rm", "dhh/app:999", "sleep 1000000", "&&",
|
||||||
|
:docker, :cp, "-L", "app-web-assets:/public/assets/.", ".kamal/assets/extracted/app-web-999", "&&",
|
||||||
|
:docker, :stop, "-t 1", "app-web-assets"
|
||||||
|
], new_command(asset_path: "/public/assets").extract_assets
|
||||||
|
end
|
||||||
|
|
||||||
|
test "sync asset volumes" do
|
||||||
|
assert_equal [
|
||||||
|
:mkdir, "-p", ".kamal/assets/volumes/app-web-999", ";",
|
||||||
|
:cp, "-rnT", ".kamal/assets/extracted/app-web-999", ".kamal/assets/volumes/app-web-999"
|
||||||
|
], new_command(asset_path: "/public/assets").sync_asset_volumes
|
||||||
|
|
||||||
|
assert_equal [
|
||||||
|
:mkdir, "-p", ".kamal/assets/volumes/app-web-999", ";",
|
||||||
|
:cp, "-rnT", ".kamal/assets/extracted/app-web-999", ".kamal/assets/volumes/app-web-999", ";",
|
||||||
|
:cp, "-rnT", ".kamal/assets/extracted/app-web-999", ".kamal/assets/volumes/app-web-998", "|| true", ";",
|
||||||
|
:cp, "-rnT", ".kamal/assets/extracted/app-web-998", ".kamal/assets/volumes/app-web-999", "|| true"
|
||||||
|
], new_command(asset_path: "/public/assets").sync_asset_volumes(old_version: 998)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "clean up assets" do
|
||||||
|
assert_equal [
|
||||||
|
:find, ".kamal/assets/extracted", "-maxdepth 1", "-name", "'app-web-*'", "!", "-name", "app-web-999", "-exec rm -rf \"{}\" +", ";",
|
||||||
|
:find, ".kamal/assets/volumes", "-maxdepth 1", "-name", "'app-web-*'", "!", "-name", "app-web-999", "-exec rm -rf \"{}\" +"
|
||||||
|
], new_command(asset_path: "/public/assets").clean_up_assets
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def new_command(role: "web")
|
def new_command(role: "web", host: "1.1.1.1", **additional_config)
|
||||||
Kamal::Commands::App.new(Kamal::Configuration.new(@config, destination: @destination, version: "999"), role: role)
|
config = Kamal::Configuration.new(@config.merge(additional_config), destination: @destination, version: "999")
|
||||||
|
Kamal::Commands::App.new(config, role: config.role(role), host: host)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class CommandsAuditorTest < ActiveSupport::TestCase
|
|||||||
:echo,
|
:echo,
|
||||||
"[#{@recorded_at}] [#{@performer}]",
|
"[#{@recorded_at}] [#{@performer}]",
|
||||||
"app removed container",
|
"app removed container",
|
||||||
">>", "kamal-app-audit.log"
|
">>", ".kamal/app-audit.log"
|
||||||
], @auditor.record("app removed container")
|
], @auditor.record("app removed container")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ class CommandsAuditorTest < ActiveSupport::TestCase
|
|||||||
:echo,
|
:echo,
|
||||||
"[#{@recorded_at}] [#{@performer}] [staging]",
|
"[#{@recorded_at}] [#{@performer}] [staging]",
|
||||||
"app removed container",
|
"app removed container",
|
||||||
">>", "kamal-app-staging-audit.log"
|
">>", ".kamal/app-staging-audit.log"
|
||||||
], auditor.record("app removed container")
|
], auditor.record("app removed container")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -42,7 +42,7 @@ class CommandsAuditorTest < ActiveSupport::TestCase
|
|||||||
:echo,
|
:echo,
|
||||||
"[#{@recorded_at}] [#{@performer}] [web]",
|
"[#{@recorded_at}] [#{@performer}] [web]",
|
||||||
"app removed container",
|
"app removed container",
|
||||||
">>", "kamal-app-audit.log"
|
">>", ".kamal/app-audit.log"
|
||||||
], auditor.record("app removed container")
|
], auditor.record("app removed container")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -52,7 +52,7 @@ class CommandsAuditorTest < ActiveSupport::TestCase
|
|||||||
:echo,
|
:echo,
|
||||||
"[#{@recorded_at}] [#{@performer}] [value]",
|
"[#{@recorded_at}] [#{@performer}] [value]",
|
||||||
"app removed container",
|
"app removed container",
|
||||||
">>", "kamal-app-audit.log"
|
">>", ".kamal/app-audit.log"
|
||||||
], @auditor.record("app removed container", detail: "value")
|
], @auditor.record("app removed container", detail: "value")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "target multiarch by default" do
|
test "target multiarch by default" do
|
||||||
builder = new_builder_command(builder: { "cache" => { "type" => "gha" }})
|
builder = new_builder_command(builder: { "cache" => { "type" => "gha" } })
|
||||||
assert_equal "multiarch", builder.name
|
assert_equal "multiarch", builder.name
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker buildx build --push --platform linux/amd64,linux/arm64 --builder kamal-app-multiarch -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile .",
|
"docker buildx build --push --platform linux/amd64,linux/arm64 --builder kamal-app-multiarch -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile .",
|
||||||
@@ -22,7 +22,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "target native cached when multiarch is off and cache is set" do
|
test "target native cached when multiarch is off and cache is set" do
|
||||||
builder = new_builder_command(builder: { "multiarch" => false, "cache" => { "type" => "gha" }})
|
builder = new_builder_command(builder: { "multiarch" => false, "cache" => { "type" => "gha" } })
|
||||||
assert_equal "native/cached", builder.name
|
assert_equal "native/cached", builder.name
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker buildx build --push -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile .",
|
"docker buildx build --push -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile .",
|
||||||
@@ -30,13 +30,21 @@ class CommandsBuilderTest < ActiveSupport::TestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "target multiarch remote when local and remote is set" do
|
test "target multiarch remote when local and remote is set" do
|
||||||
builder = new_builder_command(builder: { "local" => { }, "remote" => { }, "cache" => { "type" => "gha" } })
|
builder = new_builder_command(builder: { "local" => {}, "remote" => {}, "cache" => { "type" => "gha" } })
|
||||||
assert_equal "multiarch/remote", builder.name
|
assert_equal "multiarch/remote", builder.name
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker buildx build --push --platform linux/amd64,linux/arm64 --builder kamal-app-multiarch-remote -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile .",
|
"docker buildx build --push --platform linux/amd64,linux/arm64 --builder kamal-app-multiarch-remote -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile .",
|
||||||
builder.push.join(" ")
|
builder.push.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "target multiarch local when arch is set" do
|
||||||
|
builder = new_builder_command(builder: { "local" => { "arch" => "amd64" } })
|
||||||
|
assert_equal "multiarch", builder.name
|
||||||
|
assert_equal \
|
||||||
|
"docker buildx build --push --platform linux/amd64 --builder kamal-app-multiarch -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile .",
|
||||||
|
builder.push.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
test "target native remote when only remote is set" do
|
test "target native remote when only remote is set" do
|
||||||
builder = new_builder_command(builder: { "remote" => { "arch" => "amd64" }, "cache" => { "type" => "gha" } })
|
builder = new_builder_command(builder: { "remote" => { "arch" => "amd64" }, "cache" => { "type" => "gha" } })
|
||||||
assert_equal "native/remote", builder.name
|
assert_equal "native/remote", builder.name
|
||||||
@@ -53,7 +61,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "build secrets" do
|
test "build secrets" do
|
||||||
builder = new_builder_command(builder: { "secrets" => ["token_a", "token_b"] })
|
builder = new_builder_command(builder: { "secrets" => [ "token_a", "token_b" ] })
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"-t dhh/app:123 -t dhh/app:latest --label service=\"app\" --secret id=\"token_a\" --secret id=\"token_b\" --file Dockerfile",
|
"-t dhh/app:123 -t dhh/app:latest --label service=\"app\" --secret id=\"token_a\" --secret id=\"token_b\" --file Dockerfile",
|
||||||
builder.target.build_options.join(" ")
|
builder.target.build_options.join(" ")
|
||||||
@@ -75,6 +83,13 @@ class CommandsBuilderTest < ActiveSupport::TestCase
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "build target" do
|
||||||
|
builder = new_builder_command(builder: { "target" => "prod" })
|
||||||
|
assert_equal \
|
||||||
|
"-t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile --target prod",
|
||||||
|
builder.target.build_options.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
test "build context" do
|
test "build context" do
|
||||||
builder = new_builder_command(builder: { "context" => ".." })
|
builder = new_builder_command(builder: { "context" => ".." })
|
||||||
assert_equal \
|
assert_equal \
|
||||||
@@ -103,8 +118,52 @@ class CommandsBuilderTest < ActiveSupport::TestCase
|
|||||||
builder.push.join(" ")
|
builder.push.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "build with ssh agent socket" do
|
||||||
|
builder = new_builder_command(builder: { "ssh" => "default=$SSH_AUTH_SOCK" })
|
||||||
|
|
||||||
|
assert_equal \
|
||||||
|
"-t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile --ssh default=$SSH_AUTH_SOCK",
|
||||||
|
builder.target.build_options.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "validate image" do
|
||||||
|
assert_equal "docker inspect -f '{{ .Config.Labels.service }}' dhh/app:123 | grep -x app || (echo \"Image dhh/app:123 is missing the 'service' label\" && exit 1)", new_builder_command.validate_image.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "multiarch context build" do
|
||||||
|
builder = new_builder_command(builder: { "context" => "./foo" })
|
||||||
|
assert_equal \
|
||||||
|
"docker buildx build --push --platform linux/amd64,linux/arm64 --builder kamal-app-multiarch -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ./foo",
|
||||||
|
builder.push.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "native context build" do
|
||||||
|
builder = new_builder_command(builder: { "multiarch" => false, "context" => "./foo" })
|
||||||
|
assert_equal \
|
||||||
|
"docker build -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ./foo && docker push dhh/app:123 && docker push dhh/app:latest",
|
||||||
|
builder.push.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "cached context build" do
|
||||||
|
builder = new_builder_command(builder: { "multiarch" => false, "context" => "./foo", "cache" => { "type" => "gha" } })
|
||||||
|
assert_equal \
|
||||||
|
"docker buildx build --push -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile ./foo",
|
||||||
|
builder.push.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "remote context build" do
|
||||||
|
builder = new_builder_command(builder: { "remote" => { "arch" => "amd64" }, "context" => "./foo" })
|
||||||
|
assert_equal \
|
||||||
|
"docker buildx build --push --platform linux/amd64 --builder kamal-app-native-remote -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ./foo",
|
||||||
|
builder.push.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def new_builder_command(additional_config = {})
|
def new_builder_command(additional_config = {})
|
||||||
Kamal::Commands::Builder.new(Kamal::Configuration.new(@config.merge(additional_config), version: "123"))
|
Kamal::Commands::Builder.new(Kamal::Configuration.new(@config.merge(additional_config), version: "123"))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def build_directory
|
||||||
|
"#{Dir.tmpdir}/kamal-clones/app/kamal/"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class CommandsDockerTest < ActiveSupport::TestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "install" do
|
test "install" do
|
||||||
assert_equal "curl -fsSL https://get.docker.com | sh", @docker.install.join(" ")
|
assert_equal "sh -c 'curl -fsSL https://get.docker.com || wget -O - https://get.docker.com || echo \"exit 1\"' | sh", @docker.install.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "installed?" do
|
test "installed?" do
|
||||||
@@ -21,6 +21,6 @@ class CommandsDockerTest < ActiveSupport::TestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "superuser?" do
|
test "superuser?" do
|
||||||
assert_equal '[ "${EUID:-$(id -u)}" -eq 0 ]', @docker.superuser?.join(" ")
|
assert_equal '[ "${EUID:-$(id -u)}" -eq 0 ] || command -v sudo >/dev/null || command -v su >/dev/null', @docker.superuser?.join(" ")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,106 +0,0 @@
|
|||||||
require "test_helper"
|
|
||||||
|
|
||||||
class CommandsHealthcheckTest < ActiveSupport::TestCase
|
|
||||||
setup do
|
|
||||||
@config = {
|
|
||||||
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ],
|
|
||||||
traefik: { "args" => { "accesslog.format" => "json", "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } }
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
test "run" do
|
|
||||||
assert_equal \
|
|
||||||
"docker run --detach --name healthcheck-app-123 --publish 3999:3000 --label service=healthcheck-app -e KAMAL_CONTAINER_NAME=\"healthcheck-app\" --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" dhh/app:123",
|
|
||||||
new_command.run.join(" ")
|
|
||||||
end
|
|
||||||
|
|
||||||
test "run with custom port" do
|
|
||||||
@config[:healthcheck] = { "port" => 3001 }
|
|
||||||
|
|
||||||
assert_equal \
|
|
||||||
"docker run --detach --name healthcheck-app-123 --publish 3999:3001 --label service=healthcheck-app -e KAMAL_CONTAINER_NAME=\"healthcheck-app\" --health-cmd \"curl -f http://localhost:3001/up || exit 1\" --health-interval \"1s\" dhh/app:123",
|
|
||||||
new_command.run.join(" ")
|
|
||||||
end
|
|
||||||
|
|
||||||
test "run with destination" do
|
|
||||||
@destination = "staging"
|
|
||||||
|
|
||||||
assert_equal \
|
|
||||||
"docker run --detach --name healthcheck-app-staging-123 --publish 3999:3000 --label service=healthcheck-app-staging -e KAMAL_CONTAINER_NAME=\"healthcheck-app-staging\" --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" dhh/app:123",
|
|
||||||
new_command.run.join(" ")
|
|
||||||
end
|
|
||||||
|
|
||||||
test "run with custom healthcheck" do
|
|
||||||
@config[:healthcheck] = { "cmd" => "/bin/up" }
|
|
||||||
|
|
||||||
assert_equal \
|
|
||||||
"docker run --detach --name healthcheck-app-123 --publish 3999:3000 --label service=healthcheck-app -e KAMAL_CONTAINER_NAME=\"healthcheck-app\" --health-cmd \"/bin/up\" --health-interval \"1s\" dhh/app:123",
|
|
||||||
new_command.run.join(" ")
|
|
||||||
end
|
|
||||||
|
|
||||||
test "run with custom options" do
|
|
||||||
@config[:servers] = { "web" => { "hosts" => [ "1.1.1.1" ], "options" => { "mount" => "somewhere" } } }
|
|
||||||
assert_equal \
|
|
||||||
"docker run --detach --name healthcheck-app-123 --publish 3999:3000 --label service=healthcheck-app -e KAMAL_CONTAINER_NAME=\"healthcheck-app\" --health-cmd \"curl -f http://localhost:3000/up || exit 1\" --health-interval \"1s\" --mount \"somewhere\" dhh/app:123",
|
|
||||||
new_command.run.join(" ")
|
|
||||||
end
|
|
||||||
|
|
||||||
test "status" do
|
|
||||||
assert_equal \
|
|
||||||
"docker container ls --all --filter name=^healthcheck-app-123$ --quiet | xargs docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'",
|
|
||||||
new_command.status.join(" ")
|
|
||||||
end
|
|
||||||
|
|
||||||
test "container_health_log" do
|
|
||||||
assert_equal \
|
|
||||||
"docker container ls --all --filter name=^healthcheck-app-123$ --quiet | xargs docker inspect --format '{{json .State.Health}}'",
|
|
||||||
new_command.container_health_log.join(" ")
|
|
||||||
end
|
|
||||||
|
|
||||||
test "stop" do
|
|
||||||
assert_equal \
|
|
||||||
"docker container ls --all --filter name=^healthcheck-app-123$ --quiet | xargs docker stop",
|
|
||||||
new_command.stop.join(" ")
|
|
||||||
end
|
|
||||||
|
|
||||||
test "stop with destination" do
|
|
||||||
@destination = "staging"
|
|
||||||
|
|
||||||
assert_equal \
|
|
||||||
"docker container ls --all --filter name=^healthcheck-app-staging-123$ --quiet | xargs docker stop",
|
|
||||||
new_command.stop.join(" ")
|
|
||||||
end
|
|
||||||
|
|
||||||
test "remove" do
|
|
||||||
assert_equal \
|
|
||||||
"docker container ls --all --filter name=^healthcheck-app-123$ --quiet | xargs docker container rm",
|
|
||||||
new_command.remove.join(" ")
|
|
||||||
end
|
|
||||||
|
|
||||||
test "remove with destination" do
|
|
||||||
@destination = "staging"
|
|
||||||
|
|
||||||
assert_equal \
|
|
||||||
"docker container ls --all --filter name=^healthcheck-app-staging-123$ --quiet | xargs docker container rm",
|
|
||||||
new_command.remove.join(" ")
|
|
||||||
end
|
|
||||||
|
|
||||||
test "logs" do
|
|
||||||
assert_equal \
|
|
||||||
"docker container ls --all --filter name=^healthcheck-app-123$ --quiet | xargs docker logs --tail 50 2>&1",
|
|
||||||
new_command.logs.join(" ")
|
|
||||||
end
|
|
||||||
|
|
||||||
test "logs with destination" do
|
|
||||||
@destination = "staging"
|
|
||||||
|
|
||||||
assert_equal \
|
|
||||||
"docker container ls --all --filter name=^healthcheck-app-staging-123$ --quiet | xargs docker logs --tail 50 2>&1",
|
|
||||||
new_command.logs.join(" ")
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
def new_command
|
|
||||||
Kamal::Commands::Healthcheck.new(Kamal::Configuration.new(@config, destination: @destination, version: "123"))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -7,8 +7,7 @@ class CommandsHookTest < ActiveSupport::TestCase
|
|||||||
freeze_time
|
freeze_time
|
||||||
|
|
||||||
@config = {
|
@config = {
|
||||||
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ],
|
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ]
|
||||||
traefik: { "args" => { "accesslog.format" => "json", "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@performer = `whoami`.strip
|
@performer = `whoami`.strip
|
||||||
|
|||||||
@@ -3,31 +3,30 @@ require "test_helper"
|
|||||||
class CommandsLockTest < ActiveSupport::TestCase
|
class CommandsLockTest < ActiveSupport::TestCase
|
||||||
setup do
|
setup do
|
||||||
@config = {
|
@config = {
|
||||||
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ],
|
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ]
|
||||||
traefik: { "args" => { "accesslog.format" => "json", "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } }
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
test "status" do
|
test "status" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"stat kamal_lock-app > /dev/null && cat kamal_lock-app/details | base64 -d",
|
"stat .kamal/locks/app-production > /dev/null && cat .kamal/locks/app-production/details | base64 -d",
|
||||||
new_command.status.join(" ")
|
new_command.status.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "acquire" do
|
test "acquire" do
|
||||||
assert_match \
|
assert_match \
|
||||||
/mkdir kamal_lock-app && echo ".*" > kamal_lock-app\/details/m,
|
%r{mkdir \.kamal/locks/app-production && echo ".*" > \.kamal/locks/app-production/details}m,
|
||||||
new_command.acquire("Hello", "123").join(" ")
|
new_command.acquire("Hello", "123").join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "release" do
|
test "release" do
|
||||||
assert_match \
|
assert_match \
|
||||||
"rm kamal_lock-app/details && rm -r kamal_lock-app",
|
"rm .kamal/locks/app-production/details && rm -r .kamal/locks/app-production",
|
||||||
new_command.release.join(" ")
|
new_command.release.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def new_command
|
def new_command
|
||||||
Kamal::Commands::Lock.new(Kamal::Configuration.new(@config, version: "123"))
|
Kamal::Commands::Lock.new(Kamal::Configuration.new(@config, version: "123", destination: "production"))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
126
test/commands/proxy_test.rb
Normal file
126
test/commands/proxy_test.rb
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class CommandsProxyTest < ActiveSupport::TestCase
|
||||||
|
setup do
|
||||||
|
@config = {
|
||||||
|
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
ENV["EXAMPLE_API_KEY"] = "456"
|
||||||
|
end
|
||||||
|
|
||||||
|
teardown do
|
||||||
|
ENV.delete("EXAMPLE_API_KEY")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "run" do
|
||||||
|
assert_equal \
|
||||||
|
"docker run --name kamal-proxy --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume kamal-proxy:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}",
|
||||||
|
new_command.run.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "run with ports configured" do
|
||||||
|
assert_equal \
|
||||||
|
"docker run --name kamal-proxy --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume kamal-proxy:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}",
|
||||||
|
new_command.run.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "run without configuration" do
|
||||||
|
@config.delete(:proxy)
|
||||||
|
|
||||||
|
assert_equal \
|
||||||
|
"docker run --name kamal-proxy --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume kamal-proxy:/root/.config/kamal-proxy --log-opt max-size=\"10m\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}",
|
||||||
|
new_command.run.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "run with logging config" do
|
||||||
|
@config[:logging] = { "driver" => "local", "options" => { "max-size" => "100m", "max-file" => "3" } }
|
||||||
|
|
||||||
|
assert_equal \
|
||||||
|
"docker run --name kamal-proxy --detach --restart unless-stopped --publish 80:80 --publish 443:443 --volume /var/run/docker.sock:/var/run/docker.sock --volume kamal-proxy:/root/.config/kamal-proxy --log-driver \"local\" --log-opt max-size=\"100m\" --log-opt max-file=\"3\" #{Kamal::Configuration::Proxy::DEFAULT_IMAGE}",
|
||||||
|
new_command.run.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "proxy start" do
|
||||||
|
assert_equal \
|
||||||
|
"docker container start kamal-proxy",
|
||||||
|
new_command.start.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "proxy stop" do
|
||||||
|
assert_equal \
|
||||||
|
"docker container stop kamal-proxy",
|
||||||
|
new_command.stop.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "proxy info" do
|
||||||
|
assert_equal \
|
||||||
|
"docker ps --filter name=^kamal-proxy$",
|
||||||
|
new_command.info.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "proxy logs" do
|
||||||
|
assert_equal \
|
||||||
|
"docker logs kamal-proxy --timestamps 2>&1",
|
||||||
|
new_command.logs.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "proxy logs since 2h" do
|
||||||
|
assert_equal \
|
||||||
|
"docker logs kamal-proxy --since 2h --timestamps 2>&1",
|
||||||
|
new_command.logs(since: "2h").join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "proxy logs last 10 lines" do
|
||||||
|
assert_equal \
|
||||||
|
"docker logs kamal-proxy --tail 10 --timestamps 2>&1",
|
||||||
|
new_command.logs(lines: 10).join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "proxy logs with grep hello!" do
|
||||||
|
assert_equal \
|
||||||
|
"docker logs kamal-proxy --timestamps 2>&1 | grep 'hello!'",
|
||||||
|
new_command.logs(grep: "hello!").join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "proxy remove container" do
|
||||||
|
assert_equal \
|
||||||
|
"docker container prune --force --filter label=org.opencontainers.image.title=kamal-proxy",
|
||||||
|
new_command.remove_container.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "proxy remove image" do
|
||||||
|
assert_equal \
|
||||||
|
"docker image prune --all --force --filter label=org.opencontainers.image.title=kamal-proxy",
|
||||||
|
new_command.remove_image.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "proxy follow logs" do
|
||||||
|
assert_equal \
|
||||||
|
"ssh -t root@1.1.1.1 -p 22 'docker logs kamal-proxy --timestamps --tail 10 --follow 2>&1'",
|
||||||
|
new_command.follow_logs(host: @config[:servers].first)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "proxy follow logs with grep hello!" do
|
||||||
|
assert_equal \
|
||||||
|
"ssh -t root@1.1.1.1 -p 22 'docker logs kamal-proxy --timestamps --tail 10 --follow 2>&1 | grep \"hello!\"'",
|
||||||
|
new_command.follow_logs(host: @config[:servers].first, grep: "hello!")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "deploy" do
|
||||||
|
assert_equal \
|
||||||
|
"docker exec kamal-proxy kamal-proxy deploy service --target \"172.1.0.2:80\"",
|
||||||
|
new_command.deploy("service", target: "172.1.0.2:80").join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "remove" do
|
||||||
|
assert_equal \
|
||||||
|
"docker exec kamal-proxy kamal-proxy remove service --target \"172.1.0.2:80\"",
|
||||||
|
new_command.remove("service", target: "172.1.0.2:80").join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def new_command
|
||||||
|
Kamal::Commands::Proxy.new(Kamal::Configuration.new(@config, version: "123"))
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -3,14 +3,13 @@ require "test_helper"
|
|||||||
class CommandsPruneTest < ActiveSupport::TestCase
|
class CommandsPruneTest < ActiveSupport::TestCase
|
||||||
setup do
|
setup do
|
||||||
@config = {
|
@config = {
|
||||||
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ],
|
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ]
|
||||||
traefik: { "args" => { "accesslog.format" => "json", "metrics.prometheus.buckets" => "0.1,0.3,1.2,5.0" } }
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
test "dangling images" do
|
test "dangling images" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker image prune --force --filter label=service=app --filter dangling=true",
|
"docker image prune --force --filter label=service=app",
|
||||||
new_command.dangling_images.join(" ")
|
new_command.dangling_images.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -20,10 +19,14 @@ class CommandsPruneTest < ActiveSupport::TestCase
|
|||||||
new_command.tagged_images.join(" ")
|
new_command.tagged_images.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "containers" do
|
test "app containers" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker ps -q -a --filter label=service=app --filter status=created --filter status=exited --filter status=dead | tail -n +6 | while read container_id; do docker rm $container_id; done",
|
"docker ps -q -a --filter label=service=app --filter status=created --filter status=exited --filter status=dead | tail -n +6 | while read container_id; do docker rm $container_id; done",
|
||||||
new_command.containers.join(" ")
|
new_command.app_containers(retain: 5).join(" ")
|
||||||
|
|
||||||
|
assert_equal \
|
||||||
|
"docker ps -q -a --filter label=service=app --filter status=created --filter status=exited --filter status=dead | tail -n +4 | while read container_id; do docker rm $container_id; done",
|
||||||
|
new_command.app_containers(retain: 3).join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class CommandsRegistryTest < ActiveSupport::TestCase
|
|||||||
|
|
||||||
test "registry login" do
|
test "registry login" do
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker login hub.docker.com -u dhh -p secret",
|
"docker login hub.docker.com -u \"dhh\" -p \"secret\"",
|
||||||
@registry.login.join(" ")
|
@registry.login.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -24,7 +24,18 @@ class CommandsRegistryTest < ActiveSupport::TestCase
|
|||||||
@config[:registry]["password"] = [ "KAMAL_REGISTRY_PASSWORD" ]
|
@config[:registry]["password"] = [ "KAMAL_REGISTRY_PASSWORD" ]
|
||||||
|
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker login hub.docker.com -u dhh -p more-secret",
|
"docker login hub.docker.com -u \"dhh\" -p \"more-secret\"",
|
||||||
|
@registry.login.join(" ")
|
||||||
|
ensure
|
||||||
|
ENV.delete("KAMAL_REGISTRY_PASSWORD")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "registry login escape password" do
|
||||||
|
ENV["KAMAL_REGISTRY_PASSWORD"] = "more-secret'\""
|
||||||
|
@config[:registry]["password"] = [ "KAMAL_REGISTRY_PASSWORD" ]
|
||||||
|
|
||||||
|
assert_equal \
|
||||||
|
"docker login hub.docker.com -u \"dhh\" -p \"more-secret'\\\"\"",
|
||||||
@registry.login.join(" ")
|
@registry.login.join(" ")
|
||||||
ensure
|
ensure
|
||||||
ENV.delete("KAMAL_REGISTRY_PASSWORD")
|
ENV.delete("KAMAL_REGISTRY_PASSWORD")
|
||||||
@@ -35,7 +46,7 @@ class CommandsRegistryTest < ActiveSupport::TestCase
|
|||||||
@config[:registry]["username"] = [ "KAMAL_REGISTRY_USERNAME" ]
|
@config[:registry]["username"] = [ "KAMAL_REGISTRY_USERNAME" ]
|
||||||
|
|
||||||
assert_equal \
|
assert_equal \
|
||||||
"docker login hub.docker.com -u also-secret -p secret",
|
"docker login hub.docker.com -u \"also-secret\" -p \"secret\"",
|
||||||
@registry.login.join(" ")
|
@registry.login.join(" ")
|
||||||
ensure
|
ensure
|
||||||
ENV.delete("KAMAL_REGISTRY_USERNAME")
|
ENV.delete("KAMAL_REGISTRY_USERNAME")
|
||||||
|
|||||||
22
test/commands/server_test.rb
Normal file
22
test/commands/server_test.rb
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class CommandsServerTest < ActiveSupport::TestCase
|
||||||
|
setup do
|
||||||
|
@config = {
|
||||||
|
service: "app", image: "dhh/app", registry: { "username" => "dhh", "password" => "secret" }, servers: [ "1.1.1.1" ]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "ensure run directory" do
|
||||||
|
assert_equal "mkdir -p .kamal", new_command.ensure_run_directory.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "ensure non default run directory" do
|
||||||
|
assert_equal "mkdir -p /var/run/kamal", new_command(run_directory: "/var/run/kamal").ensure_run_directory.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def new_command(extra_config = {})
|
||||||
|
Kamal::Commands::Server.new(Kamal::Configuration.new(@config.merge(extra_config)))
|
||||||
|
end
|
||||||
|
end
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user