Compare commits
659 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81f3508507 | ||
|
|
9a16873f21 | ||
|
|
e5ca53db6e | ||
|
|
82a436fa02 | ||
|
|
7be2e7e0ba | ||
|
|
4f7ebd73a3 | ||
|
|
279bda2770 | ||
|
|
aa15fa532a | ||
|
|
276b469c2b | ||
|
|
c10b3fb07a | ||
|
|
f47fd13e5b | ||
|
|
1d8c40f5d2 | ||
|
|
73c78079bc | ||
|
|
cd12f95a97 | ||
|
|
641e9056b3 | ||
|
|
b4bcf35f78 | ||
|
|
7f6095c9eb | ||
|
|
ef1271df47 | ||
|
|
df1232d90f | ||
|
|
e75365c8c6 | ||
|
|
e441399255 | ||
|
|
af992ce755 | ||
|
|
32caf4b148 | ||
|
|
28a02262df | ||
|
|
b11fb93a6c | ||
|
|
67ad7662ab | ||
|
|
c63ec39f07 | ||
|
|
8df7d7d92d | ||
|
|
1d48a0fb0a | ||
|
|
f331605efa | ||
|
|
7ea995db91 | ||
|
|
994a8faf6b | ||
|
|
6c75fe40df | ||
|
|
7567cae964 | ||
|
|
ecd842ab9b | ||
|
|
91ae1dd7b9 | ||
|
|
0f815e17e4 | ||
|
|
a310aa8fef | ||
|
|
29b02f5c30 | ||
|
|
6d63c4e9c6 | ||
|
|
472d163cc7 | ||
|
|
dadac999d7 | ||
|
|
5036f8843f | ||
|
|
14d0396581 | ||
|
|
8c32e6af07 | ||
|
|
a765c501a3 | ||
|
|
ae990efd02 | ||
|
|
b3a6921118 | ||
|
|
325bf9a797 | ||
|
|
7bdf6cd2e8 | ||
|
|
7633fe0293 | ||
|
|
f6851048a6 | ||
|
|
f0d7f786fa | ||
|
|
4d8387b1c9 | ||
|
|
0258ac4297 | ||
|
|
2b0810d063 | ||
|
|
098f1855e2 | ||
|
|
88351312bf | ||
|
|
4a13803119 | ||
|
|
bda252835b | ||
|
|
0f5dfa204f | ||
|
|
9dde204480 | ||
|
|
b6cd4f8070 | ||
|
|
e71bfcbadd | ||
|
|
567309596a | ||
|
|
b89ec2bf63 | ||
|
|
3172adca30 | ||
|
|
04d21f45bb | ||
|
|
eabd57350c | ||
|
|
487f6f5f53 | ||
|
|
d98500982d | ||
|
|
8693e968c1 | ||
|
|
6ab5fc9459 | ||
|
|
6fc2915884 | ||
|
|
afa6898a82 | ||
|
|
384b36d158 | ||
|
|
6df169a4fb | ||
|
|
ab109afc52 | ||
|
|
a6a48c456c | ||
|
|
a4e5dbe5d4 | ||
|
|
56e90906b1 | ||
|
|
6e65968bdc | ||
|
|
85f1e14b97 | ||
|
|
2c829a4824 | ||
|
|
45a58f7e15 | ||
|
|
834b343ded | ||
|
|
9fe1821cae | ||
|
|
1d7c9fec1d | ||
|
|
a6b983de06 | ||
|
|
3ec4ad2ea5 | ||
|
|
63f854ea18 | ||
|
|
fd0cdc1ca1 | ||
|
|
d218264b69 | ||
|
|
684f7ac148 | ||
|
|
8bcd896242 | ||
|
|
600bbd77ef | ||
|
|
34effef70a | ||
|
|
e07ac070aa | ||
|
|
46c0836cd4 | ||
|
|
bd54c74682 | ||
|
|
f183419f7a | ||
|
|
190dbd1ea3 | ||
|
|
d6eda3d741 | ||
|
|
0fe6a17a91 | ||
|
|
7f15fd143f | ||
|
|
434490bd0c | ||
|
|
267b526438 | ||
|
|
1f721739d6 | ||
|
|
6c51e596ae | ||
|
|
7f31510aec | ||
|
|
e8ff233e81 | ||
|
|
a316e51eda | ||
|
|
bf91d6c1ca | ||
|
|
a84ee6315f | ||
|
|
3c39086613 | ||
|
|
8b965b0a31 | ||
|
|
d2672c771e | ||
|
|
24031fefb0 | ||
|
|
35fe9c154d | ||
|
|
b8972a6833 | ||
|
|
d7d6fa34b0 | ||
|
|
c21757f747 | ||
|
|
cb73c730f9 | ||
|
|
109339189a | ||
|
|
33834a266a | ||
|
|
e1016b2469 | ||
|
|
a40b644145 | ||
|
|
ccb7424197 | ||
|
|
2125327d54 | ||
|
|
f4d309c5cc | ||
|
|
5bca8015bc | ||
|
|
27a7b339a6 | ||
|
|
dcd4778dd9 | ||
|
|
6f2eaed398 | ||
|
|
e9d480b514 | ||
|
|
2fdc59a3aa | ||
|
|
b33c999125 | ||
|
|
2056351c38 | ||
|
|
9c2d5f83f7 | ||
|
|
f347ef7e44 | ||
|
|
63ebeda489 | ||
|
|
13bdf50ceb | ||
|
|
bd6558630f | ||
|
|
53903ddcd2 | ||
|
|
55756fa6f3 | ||
|
|
fe0c656de5 | ||
|
|
418d8045d8 | ||
|
|
d63ff8f251 | ||
|
|
eab717e0cf | ||
|
|
66d5e25834 | ||
|
|
6bbbd81da1 | ||
|
|
876eebc7c5 | ||
|
|
dc1bbac3c8 | ||
|
|
045aa7d167 | ||
|
|
0660895e75 | ||
|
|
debdf00cca | ||
|
|
9089c41f30 | ||
|
|
c9946808b1 | ||
|
|
deb2a6d298 | ||
|
|
0cb69a84f5 | ||
|
|
aa630f156a | ||
|
|
63d0b5ddfa | ||
|
|
06f4caa866 | ||
|
|
5aa3d1aeb0 | ||
|
|
a4d668cd39 | ||
|
|
7156c80f34 | ||
|
|
aed2ef99d0 | ||
|
|
57cbf7cdb5 | ||
|
|
b99c044327 | ||
|
|
8ad6a0ed16 | ||
|
|
8b62e2694a | ||
|
|
be1df4356a | ||
|
|
8210e8e768 | ||
|
|
9b96ef2412 | ||
|
|
1522d94ac9 | ||
|
|
a68294c384 | ||
|
|
31a347c285 | ||
|
|
3d502ab12d | ||
|
|
5226d52f8a | ||
|
|
9deb8af4a0 | ||
|
|
068aaa0bd0 | ||
|
|
a726a86a17 | ||
|
|
b2e1a4d4c1 | ||
|
|
9ade79fc84 | ||
|
|
79731da619 | ||
|
|
0ae8046905 | ||
|
|
d5ecca0fd4 | ||
|
|
0c6a593554 | ||
|
|
3f37fea7c3 | ||
|
|
7daaabd4d4 | ||
|
|
fcdef5fa06 | ||
|
|
5480b40ba3 | ||
|
|
1d0e81b00a | ||
|
|
5910249d02 | ||
|
|
b464c4fd4a | ||
|
|
56754fe40c | ||
|
|
6a06efc9d9 | ||
|
|
5c4c33e0a8 | ||
|
|
0b5506f6f2 | ||
|
|
a2549b1f60 | ||
|
|
9b9e60ec7f | ||
|
|
e557eea79c | ||
|
|
d7e785cd36 | ||
|
|
5cda3086c4 | ||
|
|
362f5d00f6 | ||
|
|
6adf3c117f | ||
|
|
9f0b10425c | ||
|
|
5f2384f123 | ||
|
|
eab7d3adc5 | ||
|
|
d2d0223c37 | ||
|
|
56268d724d | ||
|
|
cffb6c3d7e | ||
|
|
bd1726f305 | ||
|
|
7ddb122a22 | ||
|
|
98c951bbdb | ||
|
|
374c117b79 | ||
|
|
d6a5cf3c78 | ||
|
|
2aeabda455 | ||
|
|
c048c097ed | ||
|
|
ed148628fb | ||
|
|
d48080c772 | ||
|
|
3f64338929 | ||
|
|
0ab838bc25 | ||
|
|
b7382ceeaf | ||
|
|
69367fbc6b | ||
|
|
2515bd705c | ||
|
|
579e169be2 | ||
|
|
d6f5da92be | ||
|
|
9ccfe20b10 | ||
|
|
e871d347d5 | ||
|
|
b8af719bb7 | ||
|
|
190f4fba28 | ||
|
|
f48987aa03 | ||
|
|
ef051eca1b | ||
|
|
173d44ee0a | ||
|
|
4e811372f8 | ||
|
|
ec4aa45852 | ||
|
|
5e11a64181 | ||
|
|
57d9ce177a | ||
|
|
b12de87388 | ||
|
|
8a98949634 | ||
|
|
0eb9f48082 | ||
|
|
9db6fc0704 | ||
|
|
27fede3caa | ||
|
|
29c723f7ec | ||
|
|
2755582c47 | ||
|
|
fa73d722ea | ||
|
|
c535e4e44f | ||
|
|
0ea07b1760 | ||
|
|
03b531f179 | ||
|
|
d8570d1c2c | ||
|
|
3fe70b458d | ||
|
|
ade8b43599 | ||
|
|
d24fc3ca4e | ||
|
|
7c244bbb98 | ||
|
|
1369c46a83 | ||
|
|
deccf1cfaf | ||
|
|
1573cebadf | ||
|
|
85a2926cde | ||
|
|
58a51b079e | ||
|
|
f1f3fc566f | ||
|
|
44726ff65a | ||
|
|
fd0d4af21f | ||
|
|
13409ada5a | ||
|
|
9a1379be6c | ||
|
|
31d6c198da | ||
|
|
22afe4de77 | ||
|
|
b63982c3a7 | ||
|
|
9e12d32cc3 | ||
|
|
ff03891d47 | ||
|
|
f21dc30875 | ||
|
|
69fa7286e2 | ||
|
|
e160852e4d | ||
|
|
4697f89441 | ||
|
|
dde637ffff | ||
|
|
f8f88af534 | ||
|
|
f6a9698f55 | ||
|
|
3da7fad9ee | ||
|
|
1109a864d0 | ||
|
|
da599d90c1 | ||
|
|
6bf3f4888a | ||
|
|
0a6b0b7133 | ||
|
|
6d6670a221 | ||
|
|
10e3229d7c | ||
|
|
c7bd377fa5 | ||
|
|
bdd951b756 | ||
|
|
080897dc4d | ||
|
|
d652221100 | ||
|
|
00e0e5073e | ||
|
|
b52e66814a | ||
|
|
29fbe7a98f | ||
|
|
4f317b8499 | ||
|
|
6e60ab918a | ||
|
|
90ecb6a12a | ||
|
|
2c2053558a | ||
|
|
beac539d8c | ||
|
|
eb79d93139 | ||
|
|
89994c8b20 | ||
|
|
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 |
27
.github/workflows/ci.yml
vendored
27
.github/workflows/ci.yml
vendored
@@ -1,28 +1,45 @@
|
|||||||
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:
|
||||||
ruby-version:
|
ruby-version:
|
||||||
- "2.7"
|
|
||||||
- "3.1"
|
- "3.1"
|
||||||
- "3.2"
|
- "3.2"
|
||||||
|
- "3.3"
|
||||||
gemfile:
|
gemfile:
|
||||||
- Gemfile
|
- Gemfile
|
||||||
- gemfiles/rails_edge.gemfile
|
- gemfiles/rails_edge.gemfile
|
||||||
continue-on-error: [false]
|
exclude:
|
||||||
|
- ruby-version: "3.1"
|
||||||
|
gemfile: gemfiles/rails_edge.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
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# Use the official Ruby 3.2.0 Alpine image as the base image
|
# Use the official Ruby 3.2.0 Alpine image as the base image
|
||||||
FROM ruby:3.2.0-alpine
|
FROM ruby:3.2.0-alpine
|
||||||
|
|
||||||
# Install docker/buildx-bin
|
# Install docker/buildx-bin
|
||||||
COPY --from=docker/buildx-bin /buildx /usr/libexec/docker/cli-plugins/docker-buildx
|
COPY --from=docker/buildx-bin /buildx /usr/libexec/docker/cli-plugins/docker-buildx
|
||||||
|
|
||||||
# Set the working directory to /kamal
|
# Set the working directory to /kamal
|
||||||
@@ -14,7 +14,7 @@ COPY Gemfile Gemfile.lock kamal.gemspec ./
|
|||||||
COPY lib/kamal/version.rb /kamal/lib/kamal/version.rb
|
COPY lib/kamal/version.rb /kamal/lib/kamal/version.rb
|
||||||
|
|
||||||
# Install system dependencies
|
# Install system dependencies
|
||||||
RUN apk add --no-cache --update build-base git docker openrc openssh-client-default \
|
RUN apk add --no-cache build-base git docker openrc openssh-client-default \
|
||||||
&& rc-update add docker boot \
|
&& rc-update add docker boot \
|
||||||
&& gem install bundler --version=2.4.3 \
|
&& gem install bundler --version=2.4.3 \
|
||||||
&& bundle install
|
&& bundle install
|
||||||
@@ -33,7 +33,7 @@ WORKDIR /workdir
|
|||||||
|
|
||||||
# Tell git it's safe to access /workdir/.git even if
|
# Tell git it's safe to access /workdir/.git even if
|
||||||
# the directory is owned by a different user
|
# the directory is owned by a different user
|
||||||
RUN git config --global --add safe.directory /workdir
|
RUN git config --global --add safe.directory '*'
|
||||||
|
|
||||||
# Set the entrypoint to run the installed binary in /workdir
|
# Set the entrypoint to run the installed binary in /workdir
|
||||||
# Example: docker run -it -v "$PWD:/workdir" kamal init
|
# Example: docker run -it -v "$PWD:/workdir" kamal init
|
||||||
|
|||||||
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
|
||||||
|
|||||||
186
Gemfile.lock
186
Gemfile.lock
@@ -1,96 +1,173 @@
|
|||||||
PATH
|
PATH
|
||||||
remote: .
|
remote: .
|
||||||
specs:
|
specs:
|
||||||
kamal (0.16.0)
|
kamal (2.1.1)
|
||||||
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 (~> 3.1)
|
||||||
ed25519 (~> 1.2)
|
ed25519 (~> 1.2)
|
||||||
net-ssh (~> 7.0)
|
net-ssh (~> 7.0)
|
||||||
sshkit (~> 1.21)
|
sshkit (>= 1.23.0, < 2.0)
|
||||||
thor (~> 1.2)
|
thor (~> 1.3)
|
||||||
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.3.4)
|
||||||
actionview (= 7.0.4.3)
|
actionview (= 7.1.3.4)
|
||||||
activesupport (= 7.0.4.3)
|
activesupport (= 7.1.3.4)
|
||||||
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.3.4)
|
||||||
activesupport (= 7.0.4.3)
|
activesupport (= 7.1.3.4)
|
||||||
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.3.4)
|
||||||
|
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)
|
||||||
bcrypt_pbkdf (1.1.0)
|
ast (2.4.2)
|
||||||
builder (3.2.4)
|
base64 (0.2.0)
|
||||||
concurrent-ruby (1.2.2)
|
bcrypt_pbkdf (1.1.1)
|
||||||
|
bcrypt_pbkdf (1.1.1-arm64-darwin)
|
||||||
|
bcrypt_pbkdf (1.1.1-x86_64-darwin)
|
||||||
|
bigdecimal (3.1.8)
|
||||||
|
builder (3.3.0)
|
||||||
|
concurrent-ruby (1.3.3)
|
||||||
|
connection_pool (2.4.1)
|
||||||
crass (1.0.6)
|
crass (1.0.6)
|
||||||
debug (1.7.2)
|
debug (1.9.2)
|
||||||
irb (>= 1.5.0)
|
irb (~> 1.10)
|
||||||
reline (>= 0.3.1)
|
reline (>= 0.3.8)
|
||||||
dotenv (2.8.1)
|
dotenv (3.1.2)
|
||||||
|
drb (2.2.1)
|
||||||
ed25519 (1.3.0)
|
ed25519 (1.3.0)
|
||||||
erubi (1.12.0)
|
erubi (1.13.0)
|
||||||
i18n (1.12.0)
|
i18n (1.14.5)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
io-console (0.6.0)
|
io-console (0.7.2)
|
||||||
irb (1.6.3)
|
irb (1.14.0)
|
||||||
reline (>= 0.3.0)
|
rdoc (>= 4.0.0)
|
||||||
loofah (2.20.0)
|
reline (>= 0.4.2)
|
||||||
|
json (2.7.2)
|
||||||
|
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.24.1)
|
||||||
minitest (5.18.0)
|
mocha (2.4.5)
|
||||||
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.3)
|
||||||
|
nokogiri (1.16.7-arm64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.14.2-x86_64-darwin)
|
nokogiri (1.16.7-x86_64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.14.2-x86_64-linux)
|
nokogiri (1.16.7-x86_64-linux)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
racc (1.6.2)
|
parallel (1.25.1)
|
||||||
rack (2.2.6.4)
|
parser (3.3.4.0)
|
||||||
|
ast (~> 2.4.1)
|
||||||
|
racc
|
||||||
|
psych (5.1.2)
|
||||||
|
stringio
|
||||||
|
racc (1.8.1)
|
||||||
|
rack (3.1.7)
|
||||||
|
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.3.4)
|
||||||
activesupport (= 7.0.4.3)
|
actionpack (= 7.1.3.4)
|
||||||
method_source
|
activesupport (= 7.1.3.4)
|
||||||
|
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.2.1)
|
||||||
|
rdoc (6.7.0)
|
||||||
|
psych (>= 4.0.0)
|
||||||
|
regexp_parser (2.9.2)
|
||||||
|
reline (0.5.9)
|
||||||
io-console (~> 0.5)
|
io-console (~> 0.5)
|
||||||
|
rexml (3.3.4)
|
||||||
|
strscan
|
||||||
|
rubocop (1.65.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 (>= 2.4, < 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.32.0)
|
||||||
|
parser (>= 3.3.1.0)
|
||||||
|
rubocop-minitest (0.35.1)
|
||||||
|
rubocop (>= 1.61, < 2.0)
|
||||||
|
rubocop-ast (>= 1.31.1, < 2.0)
|
||||||
|
rubocop-performance (1.21.1)
|
||||||
|
rubocop (>= 1.48.1, < 2.0)
|
||||||
|
rubocop-ast (>= 1.31.1, < 2.0)
|
||||||
|
rubocop-rails (2.25.1)
|
||||||
|
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.23.0)
|
||||||
|
base64
|
||||||
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.1)
|
||||||
|
strscan (3.1.0)
|
||||||
|
thor (1.3.1)
|
||||||
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.17)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
arm64-darwin
|
arm64-darwin
|
||||||
@@ -102,6 +179,7 @@ DEPENDENCIES
|
|||||||
kamal!
|
kamal!
|
||||||
mocha
|
mocha
|
||||||
railties
|
railties
|
||||||
|
rubocop-rails-omakase
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.4.3
|
2.4.3
|
||||||
|
|||||||
141
bin/docs
Executable file
141
bin/docs
Executable file
@@ -0,0 +1,141 @@
|
|||||||
|
#!/usr/bin/env ruby
|
||||||
|
require "stringio"
|
||||||
|
|
||||||
|
def usage
|
||||||
|
puts "Usage: #{$0} <kamal_site_repo>"
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
|
||||||
|
usage if ARGV.size != 1
|
||||||
|
|
||||||
|
kamal_site_repo = ARGV[0]
|
||||||
|
|
||||||
|
if !File.directory?(kamal_site_repo)
|
||||||
|
puts "Error: #{kamal_site_repo} is not a directory"
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
|
||||||
|
DOCS = {
|
||||||
|
"accessory" => "Accessories",
|
||||||
|
"alias" => "Aliases",
|
||||||
|
"boot" => "Booting",
|
||||||
|
"builder" => "Builders",
|
||||||
|
"configuration" => "Configuration overview",
|
||||||
|
"env" => "Environment variables",
|
||||||
|
"logging" => "Logging",
|
||||||
|
"proxy" => "Proxy",
|
||||||
|
"registry" => "Docker Registry",
|
||||||
|
"role" => "Roles",
|
||||||
|
"servers" => "Servers",
|
||||||
|
"ssh" => "SSH",
|
||||||
|
"sshkit" => "SSHKit"
|
||||||
|
}
|
||||||
|
DOCS_PATH = "lib/kamal/configuration/docs"
|
||||||
|
|
||||||
|
class DocWriter
|
||||||
|
attr_reader :from_file, :to_file, :key, :heading, :body, :output, :in_yaml
|
||||||
|
|
||||||
|
def initialize(from_file, to_dir)
|
||||||
|
@from_file = from_file
|
||||||
|
@key = File.basename(from_file, ".yml")
|
||||||
|
@to_file = File.join(to_dir, "#{linkify(DOCS[key])}.md")
|
||||||
|
@body = File.readlines(from_file)
|
||||||
|
@heading = body.shift.chomp("\n")
|
||||||
|
@output = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def write
|
||||||
|
puts "Writing #{to_file}"
|
||||||
|
generate_markdown
|
||||||
|
File.write(to_file, output.string)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def generate_markdown
|
||||||
|
@output = StringIO.new
|
||||||
|
|
||||||
|
generate_header
|
||||||
|
|
||||||
|
place = :in_section
|
||||||
|
|
||||||
|
loop do
|
||||||
|
line = body.shift&.chomp("\n")
|
||||||
|
break if line.nil?
|
||||||
|
|
||||||
|
case place
|
||||||
|
when :new_section, :in_section
|
||||||
|
if line.empty?
|
||||||
|
output.puts
|
||||||
|
place = :new_section
|
||||||
|
elsif line =~ /^ *#/
|
||||||
|
generate_line(line, heading: place == :new_section)
|
||||||
|
place = :in_section
|
||||||
|
else
|
||||||
|
output.puts
|
||||||
|
output.puts "```yaml"
|
||||||
|
output.puts line
|
||||||
|
place = :in_yaml
|
||||||
|
end
|
||||||
|
when :in_yaml, :in_empty_line_yaml
|
||||||
|
if line =~ /^ *#/
|
||||||
|
output.puts "```"
|
||||||
|
output.puts
|
||||||
|
generate_line(line, heading: place == :in_empty_line_yaml)
|
||||||
|
place = :in_section
|
||||||
|
elsif line.empty?
|
||||||
|
place = :in_empty_line_yaml
|
||||||
|
else
|
||||||
|
output.puts line
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
output.puts "```" if place == :in_yaml
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_header
|
||||||
|
output.puts "---"
|
||||||
|
output.puts "# This file has been generated from the Kamal source, do not edit directly."
|
||||||
|
output.puts "# Find the source of this file at #{DOCS_PATH}/#{key}.yml in the Kamal repository."
|
||||||
|
output.puts "title: #{heading[2..-1]}"
|
||||||
|
output.puts "---"
|
||||||
|
output.puts
|
||||||
|
output.puts heading
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_line(line, heading: false)
|
||||||
|
line = line.gsub(/^ *#\s?/, "")
|
||||||
|
|
||||||
|
if line =~ /(.*)kamal docs ([a-z]*)(.*)/
|
||||||
|
line = "#{$1}[#{DOCS[$2]}](../#{linkify(DOCS[$2])})#{$3}"
|
||||||
|
end
|
||||||
|
|
||||||
|
if line =~ /(.*)https:\/\/kamal-deploy.org([a-z\/-]*)(.*)/
|
||||||
|
line = "#{$1}[#{titlify($2.split("/").last)}](#{$2})#{$3}"
|
||||||
|
end
|
||||||
|
|
||||||
|
if heading
|
||||||
|
output.puts "## [#{line}](##{linkify(line)})"
|
||||||
|
else
|
||||||
|
output.puts line
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def linkify(text)
|
||||||
|
if text == "Configuration overview"
|
||||||
|
"overview"
|
||||||
|
else
|
||||||
|
text.downcase.gsub(" ", "-")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def titlify(text)
|
||||||
|
text.capitalize.gsub("-", " ")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
from_dir = File.join(File.dirname(__FILE__), "../#{DOCS_PATH}")
|
||||||
|
to_dir = File.join(kamal_site_repo, "docs/configuration")
|
||||||
|
Dir.glob("#{from_dir}/*") do |from_file|
|
||||||
|
DocWriter.new(from_file, to_dir).write
|
||||||
|
end
|
||||||
@@ -5,22 +5,22 @@ 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.23.0", "< 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.3"
|
||||||
spec.add_dependency "dotenv", "~> 2.8"
|
spec.add_dependency "dotenv", "~> 3.1"
|
||||||
spec.add_dependency "zeitwerk", "~> 2.5"
|
spec.add_dependency "zeitwerk", "~> 2.5"
|
||||||
spec.add_dependency "ed25519", "~> 1.2"
|
spec.add_dependency "ed25519", "~> 1.2"
|
||||||
spec.add_dependency "bcrypt_pbkdf", "~> 1.0"
|
spec.add_dependency "bcrypt_pbkdf", "~> 1.0"
|
||||||
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"
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
module Kamal
|
module Kamal
|
||||||
|
class ConfigurationError < StandardError; end
|
||||||
end
|
end
|
||||||
|
|
||||||
require "active_support"
|
require "active_support"
|
||||||
require "zeitwerk"
|
require "zeitwerk"
|
||||||
|
require "yaml"
|
||||||
|
require "tmpdir"
|
||||||
|
require "pathname"
|
||||||
|
|
||||||
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_namespace(Kamal::Cli) # We need all commands loaded.
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
module Kamal::Cli
|
module Kamal::Cli
|
||||||
class LockError < StandardError; end
|
class BootError < StandardError; end
|
||||||
class HookError < StandardError; end
|
class HookError < StandardError; end
|
||||||
|
class LockError < 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,17 +1,20 @@
|
|||||||
class Kamal::Cli::Accessory < Kamal::Cli::Base
|
class Kamal::Cli::Accessory < Kamal::Cli::Base
|
||||||
desc "boot [NAME]", "Boot new accessory service on host (use NAME=all to boot all accessories)"
|
desc "boot [NAME]", "Boot new accessory service on host (use NAME=all to boot all accessories)"
|
||||||
def boot(name, login: true)
|
def boot(name, prepare: true)
|
||||||
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|
|
prepare(name) if prepare
|
||||||
|
|
||||||
|
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.auditor.record("Booted #{name} accessory"), verbosity: :debug
|
execute *KAMAL.auditor.record("Booted #{name} accessory"), verbosity: :debug
|
||||||
|
execute *accessory.ensure_env_directory
|
||||||
|
upload! accessory.secrets_io, accessory.secrets_path, mode: "0600"
|
||||||
execute *accessory.run
|
execute *accessory.run
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -21,9 +24,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 +41,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 +52,25 @@ 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
|
prepare(name)
|
||||||
|
|
||||||
stop(name)
|
stop(name)
|
||||||
remove_container(name)
|
remove_container(name)
|
||||||
boot(name, login: false)
|
boot(name, prepare: false)
|
||||||
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 +80,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,11 +92,9 @@ 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
|
stop(name)
|
||||||
stop(name)
|
start(name)
|
||||||
start(name)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -103,8 +103,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 +114,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 +126,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
|
||||||
@@ -144,23 +145,27 @@ class Kamal::Cli::Accessory < Kamal::Cli::Base
|
|||||||
option :since, aliases: "-s", desc: "Show logs since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)"
|
option :since, aliases: "-s", desc: "Show logs since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)"
|
||||||
option :lines, type: :numeric, aliases: "-n", desc: "Number of log lines to pull from each server"
|
option :lines, type: :numeric, aliases: "-n", desc: "Number of log lines to pull from each server"
|
||||||
option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)"
|
option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)"
|
||||||
|
option :grep_options, aliases: "-o", desc: "Additional options supplied to grep"
|
||||||
option :follow, aliases: "-f", desc: "Follow logs on primary server (or specific host set by --hosts)"
|
option :follow, aliases: "-f", desc: "Follow logs on primary server (or specific host set by --hosts)"
|
||||||
|
option :skip_timestamps, type: :boolean, aliases: "-T", desc: "Skip appending timestamps to logging output"
|
||||||
def logs(name)
|
def logs(name)
|
||||||
with_accessory(name) do |accessory|
|
with_accessory(name) do |accessory, hosts|
|
||||||
grep = options[:grep]
|
grep = options[:grep]
|
||||||
|
grep_options = options[:grep_options]
|
||||||
|
timestamps = !options[:skip_timestamps]
|
||||||
|
|
||||||
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(timestamps: timestamps, grep: grep, grep_options: grep_options)
|
||||||
exec accessory.follow_logs(grep: grep)
|
exec accessory.follow_logs(timestamps: timestamps, grep: grep, grep_options: grep_options)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
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(timestamps: timestamps, since: since, lines: lines, grep: grep, grep_options: grep_options))
|
||||||
end
|
end
|
||||||
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,19 +211,39 @@ 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
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
desc "upgrade", "Upgrade accessories from Kamal 1.x to 2.0 (restart them in 'kamal' network)"
|
||||||
|
option :rolling, type: :boolean, default: false, desc: "Upgrade one host at a time"
|
||||||
|
option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
|
||||||
|
def upgrade(name)
|
||||||
|
confirming "This will restart all accessories" do
|
||||||
|
with_lock do
|
||||||
|
host_groups = options[:rolling] ? KAMAL.accessory_hosts : [ KAMAL.accessory_hosts ]
|
||||||
|
host_groups.each do |hosts|
|
||||||
|
host_list = Array(hosts).join(",")
|
||||||
|
KAMAL.with_specific_hosts(hosts) do
|
||||||
|
say "Upgrading #{name} accessories on #{host_list}...", :magenta
|
||||||
|
reboot name
|
||||||
|
say "Upgraded #{name} accessories on #{host_list}...", :magenta
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
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 +256,30 @@ 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)
|
||||||
|
stop(name)
|
||||||
|
remove_container(name)
|
||||||
|
remove_image(name)
|
||||||
|
remove_service_directory(name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def prepare(name)
|
||||||
|
with_accessory(name) do |accessory, hosts|
|
||||||
|
on(hosts) do
|
||||||
|
execute *KAMAL.registry.login
|
||||||
|
execute *KAMAL.docker.create_network
|
||||||
|
rescue SSHKit::Command::Failed => e
|
||||||
|
raise unless e.message.include?("already exists")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
9
lib/kamal/cli/alias/command.rb
Normal file
9
lib/kamal/cli/alias/command.rb
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
class Kamal::Cli::Alias::Command < Thor::DynamicCommand
|
||||||
|
def run(instance, args = [])
|
||||||
|
if (_alias = KAMAL.config.aliases[name])
|
||||||
|
Kamal::Cli::Main.start(Shellwords.split(_alias.command) + ARGV[1..-1])
|
||||||
|
else
|
||||||
|
super
|
||||||
|
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} (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|
|
||||||
|
Kamal::Cli::App::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 = Kamal::Cli::Healthcheck::Barrier.new
|
||||||
|
|
||||||
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)
|
Kamal::Cli::App::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_id_for_version(version)).strip
|
||||||
|
raise Kamal::Cli::BootError, "Failed to get endpoint for #{role} on #{host}, did the container boot?" if endpoint.empty?
|
||||||
|
|
||||||
|
execute *app.deploy(target: endpoint)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -57,13 +56,23 @@ 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)
|
||||||
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?
|
||||||
|
version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
|
||||||
|
endpoint = capture_with_info(*app.container_id_for_version(version)).strip
|
||||||
|
if endpoint.present?
|
||||||
|
execute *app.remove, 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 +85,33 @@ 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"
|
||||||
def exec(cmd)
|
option :env, aliases: "-e", type: :hash, desc: "Set environment variables for the command"
|
||||||
|
def exec(*cmd)
|
||||||
|
cmd = Kamal::Utils.join_commands(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
|
||||||
@@ -166,24 +186,31 @@ class Kamal::Cli::App < Kamal::Cli::Base
|
|||||||
option :since, aliases: "-s", desc: "Show lines since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)"
|
option :since, aliases: "-s", desc: "Show lines since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)"
|
||||||
option :lines, type: :numeric, aliases: "-n", desc: "Number of lines to show from each server"
|
option :lines, type: :numeric, aliases: "-n", desc: "Number of lines to show from each server"
|
||||||
option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)"
|
option :grep, aliases: "-g", desc: "Show lines with grep match only (use this to fetch specific requests by id)"
|
||||||
|
option :grep_options, aliases: "-o", desc: "Additional options supplied to grep"
|
||||||
option :follow, aliases: "-f", desc: "Follow log on primary server (or specific host set by --hosts)"
|
option :follow, aliases: "-f", desc: "Follow log on primary server (or specific host set by --hosts)"
|
||||||
|
option :skip_timestamps, type: :boolean, aliases: "-T", desc: "Skip appending timestamps to logging output"
|
||||||
def logs
|
def logs
|
||||||
# FIXME: Catch when app containers aren't running
|
# FIXME: Catch when app containers aren't running
|
||||||
|
|
||||||
grep = options[:grep]
|
grep = options[:grep]
|
||||||
|
grep_options = options[:grep_options]
|
||||||
|
since = options[:since]
|
||||||
|
timestamps = !options[:skip_timestamps]
|
||||||
|
|
||||||
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 ||= [ KAMAL.primary_role.name ]
|
||||||
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, timestamps: timestamps, lines: lines, grep: grep, grep_options: grep_options)
|
||||||
|
exec app.follow_logs(host: KAMAL.primary_host, timestamps: timestamps, lines: lines, grep: grep, grep_options: grep_options)
|
||||||
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 +218,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(timestamps: timestamps, since: since, lines: lines, grep: grep, grep_options: grep_options))
|
||||||
rescue SSHKit::Command::Failed
|
rescue SSHKit::Command::Failed
|
||||||
puts_by_host host, "Nothing found"
|
puts_by_host host, "Nothing found"
|
||||||
end
|
end
|
||||||
@@ -202,22 +229,23 @@ 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
|
||||||
|
remove_app_directory
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
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 +253,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 +267,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
|
||||||
@@ -247,11 +275,25 @@ class Kamal::Cli::App < Kamal::Cli::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
desc "remove_app_directory", "Remove the service directory from servers", hide: true
|
||||||
|
def remove_app_directory
|
||||||
|
with_lock do
|
||||||
|
on(KAMAL.hosts) do |host|
|
||||||
|
roles = KAMAL.roles_on(host)
|
||||||
|
|
||||||
|
roles.each do |role|
|
||||||
|
execute *KAMAL.auditor.record("Removed #{KAMAL.config.app_directory} on all servers", role: role), verbosity: :debug
|
||||||
|
execute *KAMAL.server.remove_app_directory, raise_on_non_zero_exit: false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
desc "version", "Show app version currently running on servers"
|
desc "version", "Show app version currently running on servers"
|
||||||
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 +316,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
|
||||||
|
|||||||
125
lib/kamal/cli/app/boot.rb
Normal file
125
lib/kamal/cli/app/boot.rb
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
class Kamal::Cli::App::Boot
|
||||||
|
attr_reader :host, :role, :version, :barrier, :sshkit
|
||||||
|
delegate :execute, :capture_with_info, :capture_with_pretty_json, :info, :error, :upload!, 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.ensure_env_directory
|
||||||
|
upload! role.secrets_io(host), role.secrets_path, mode: "0600"
|
||||||
|
|
||||||
|
execute *app.run(hostname: hostname)
|
||||||
|
if running_proxy?
|
||||||
|
endpoint = capture_with_info(*app.container_id_for_version(version)).strip
|
||||||
|
raise Kamal::Cli::BootError, "Failed to get endpoint for #{role} on #{host}, did the container boot?" if endpoint.empty?
|
||||||
|
execute *app.deploy(target: endpoint)
|
||||||
|
else
|
||||||
|
Kamal::Cli::Healthcheck::Poller.wait_for_healthy(pause_after_ready: true) { capture_with_info(*app.status(version: version)) }
|
||||||
|
end
|
||||||
|
rescue => e
|
||||||
|
error "Failed to boot #{role} on #{host}"
|
||||||
|
raise e
|
||||||
|
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 any 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::Healthcheck::Error
|
||||||
|
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 any other roles"
|
||||||
|
begin
|
||||||
|
error capture_with_info(*app.logs(version: version))
|
||||||
|
error capture_with_info(*app.container_health_log(version: version))
|
||||||
|
rescue SSHKit::Command::Failed
|
||||||
|
error "Could not fetch logs for #{version}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
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
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
require "thor"
|
require "thor"
|
||||||
require "dotenv"
|
|
||||||
require "kamal/sshkit_with_ext"
|
require "kamal/sshkit_with_ext"
|
||||||
|
|
||||||
module Kamal::Cli
|
module Kamal::Cli
|
||||||
class Base < Thor
|
class Base < Thor
|
||||||
include SSHKit::DSL
|
include SSHKit::DSL
|
||||||
|
|
||||||
def self.exit_on_failure?() true end
|
def self.exit_on_failure?() false end
|
||||||
|
def self.dynamic_command_class() Kamal::Cli::Alias::Command end
|
||||||
|
|
||||||
class_option :verbose, type: :boolean, aliases: "-v", desc: "Detailed logging"
|
class_option :verbose, type: :boolean, aliases: "-v", desc: "Detailed logging"
|
||||||
class_option :quiet, type: :boolean, aliases: "-q", desc: "Minimal logging"
|
class_option :quiet, type: :boolean, aliases: "-q", desc: "Minimal logging"
|
||||||
@@ -14,34 +14,31 @@ 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)"
|
||||||
|
|
||||||
class_option :skip_hooks, aliases: "-H", type: :boolean, default: false, desc: "Don't run hooks"
|
class_option :skip_hooks, aliases: "-H", type: :boolean, default: false, desc: "Don't run hooks"
|
||||||
|
|
||||||
def initialize(*)
|
def initialize(args = [], local_options = {}, config = {})
|
||||||
super
|
if config[:current_command].is_a?(Kamal::Cli::Alias::Command)
|
||||||
load_envs
|
# When Thor generates a dynamic command, it doesn't attempt to parse the arguments.
|
||||||
initialize_commander(options_with_subcommand_class_options)
|
# For our purposes, it means the arguments are passed in args rather than local_options.
|
||||||
|
super([], args, config)
|
||||||
|
else
|
||||||
|
super
|
||||||
|
end
|
||||||
|
initialize_commander unless KAMAL.configured?
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def load_envs
|
|
||||||
if destination = options[:destination]
|
|
||||||
Dotenv.load(".env.#{destination}", ".env")
|
|
||||||
else
|
|
||||||
Dotenv.load(".env")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def options_with_subcommand_class_options
|
def options_with_subcommand_class_options
|
||||||
options.merge(@_initializer.last[:class_options] || {})
|
options.merge(@_initializer.last[:class_options] || {})
|
||||||
end
|
end
|
||||||
|
|
||||||
def initialize_commander(options)
|
def initialize_commander
|
||||||
KAMAL.tap do |commander|
|
KAMAL.tap do |commander|
|
||||||
if options[:verbose]
|
if options[:verbose]
|
||||||
ENV["VERBOSE"] = "1" # For backtraces via cli/start
|
ENV["VERBOSE"] = "1" # For backtraces via cli/start
|
||||||
@@ -66,37 +63,46 @@ 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?
|
acquire_lock
|
||||||
error " \e[31mDeploy lock was not released\e[0m"
|
|
||||||
else
|
begin
|
||||||
release_lock
|
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
|
||||||
|
ensure_run_directory
|
||||||
|
|
||||||
raise_if_locked do
|
raise_if_locked do
|
||||||
say "Acquiring the deploy lock...", :magenta
|
say "Acquiring the deploy lock...", :magenta
|
||||||
on(KAMAL.primary_host) { execute *KAMAL.lock.acquire("Automatic deploy lock", KAMAL.config.version), verbosity: :debug }
|
on(KAMAL.primary_host) { execute *KAMAL.lock.acquire("Automatic deploy lock", KAMAL.config.version), verbosity: :debug }
|
||||||
@@ -116,36 +122,38 @@ 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
|
with_env KAMAL.hook.env(**details, **extra_details) do
|
||||||
KAMAL.with_verbosity(:debug) { execute *KAMAL.hook.run(hook, **details, **extra_details) }
|
run_locally do
|
||||||
rescue SSHKit::Command::Failed
|
execute *KAMAL.hook.run(hook)
|
||||||
raise HookError.new("Hook `#{hook}` failed")
|
end
|
||||||
|
rescue SSHKit::Command::Failed => e
|
||||||
|
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 +175,24 @@ module Kamal::Cli
|
|||||||
def first_invocation
|
def first_invocation
|
||||||
instance_variable_get("@_invocations").first
|
instance_variable_get("@_invocations").first
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
def reset_invocation(cli_class)
|
||||||
|
instance_variable_get("@_invocations")[cli_class].pop
|
||||||
|
end
|
||||||
|
|
||||||
|
def ensure_run_directory
|
||||||
|
on(KAMAL.hosts) do
|
||||||
|
execute(*KAMAL.server.ensure_run_directory)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def with_env(env)
|
||||||
|
current_env = ENV.to_h.dup
|
||||||
|
ENV.update(env)
|
||||||
|
yield
|
||||||
|
ensure
|
||||||
|
ENV.clear
|
||||||
|
ENV.update(current_env)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,71 +1,92 @@
|
|||||||
|
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
|
with_env(KAMAL.config.builder.secrets) do
|
||||||
KAMAL.with_verbosity(:debug) { execute *KAMAL.builder.push }
|
run_locally do
|
||||||
|
begin
|
||||||
|
execute *KAMAL.builder.inspect_builder
|
||||||
|
rescue SSHKit::Command::Failed => e
|
||||||
|
if e.message =~ /(context not found|no builder|no compatible builder|does not exist)/
|
||||||
|
warn "Missing compatible builder, so creating a new one first"
|
||||||
|
begin
|
||||||
|
cli.remove
|
||||||
|
rescue SSHKit::Command::Failed
|
||||||
|
raise unless e.message =~ /(context not found|no builder|does not exist)/
|
||||||
end
|
end
|
||||||
|
cli.create
|
||||||
else
|
else
|
||||||
raise
|
raise
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Get the command here to ensure the Dir.chdir doesn't interfere with it
|
||||||
|
push = KAMAL.builder.push
|
||||||
|
|
||||||
|
KAMAL.with_verbosity(:debug) do
|
||||||
|
Dir.chdir(KAMAL.config.builder.build_directory) { execute *push }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
desc "pull", "Pull app image from registry onto servers"
|
desc "pull", "Pull app image from registry onto servers"
|
||||||
def pull
|
def pull
|
||||||
mutating do
|
if (first_hosts = mirror_hosts).any?
|
||||||
on(KAMAL.hosts) do
|
# Pull on a single host per mirror first to seed them
|
||||||
execute *KAMAL.auditor.record("Pulled image with version #{KAMAL.config.version}"), verbosity: :debug
|
say "Pulling image on #{first_hosts.join(", ")} to seed the #{"mirror".pluralize(first_hosts.count)}...", :magenta
|
||||||
execute *KAMAL.builder.clean, raise_on_non_zero_exit: false
|
pull_on_hosts(first_hosts)
|
||||||
execute *KAMAL.builder.pull
|
say "Pulling image on remaining hosts...", :magenta
|
||||||
end
|
pull_on_hosts(KAMAL.hosts - first_hosts)
|
||||||
|
else
|
||||||
|
pull_on_hosts(KAMAL.hosts)
|
||||||
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)
|
||||||
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 +94,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 +122,41 @@ 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
|
||||||
|
|
||||||
|
def mirror_hosts
|
||||||
|
if KAMAL.hosts.many?
|
||||||
|
mirror_hosts = Concurrent::Hash.new
|
||||||
|
on(KAMAL.hosts) do |host|
|
||||||
|
first_mirror = capture_with_info(*KAMAL.builder.first_mirror).strip.presence
|
||||||
|
mirror_hosts[first_mirror] ||= host.to_s if first_mirror
|
||||||
|
rescue SSHKit::Command::Failed => e
|
||||||
|
raise unless e.message =~ /error calling index: reflect: slice index out of range/
|
||||||
|
end
|
||||||
|
mirror_hosts.values
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def pull_on_hosts(hosts)
|
||||||
|
on(hosts) do
|
||||||
|
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.pull
|
||||||
|
execute *KAMAL.builder.validate_image
|
||||||
|
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
|
||||||
@@ -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
|
|
||||||
33
lib/kamal/cli/healthcheck/barrier.rb
Normal file
33
lib/kamal/cli/healthcheck/barrier.rb
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
require "concurrent/ivar"
|
||||||
|
|
||||||
|
class Kamal::Cli::Healthcheck::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::Healthcheck::Error.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
|
||||||
2
lib/kamal/cli/healthcheck/error.rb
Normal file
2
lib/kamal/cli/healthcheck/error.rb
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
class Kamal::Cli::Healthcheck::Error < StandardError
|
||||||
|
end
|
||||||
42
lib/kamal/cli/healthcheck/poller.rb
Normal file
42
lib/kamal/cli/healthcheck/poller.rb
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
module Kamal::Cli::Healthcheck::Poller
|
||||||
|
extend self
|
||||||
|
|
||||||
|
def wait_for_healthy(role, &block)
|
||||||
|
attempt = 1
|
||||||
|
timeout_at = Time.now + KAMAL.config.deploy_timeout
|
||||||
|
readiness_delay = KAMAL.config.readiness_delay
|
||||||
|
|
||||||
|
begin
|
||||||
|
status = block.call
|
||||||
|
|
||||||
|
if status == "running"
|
||||||
|
# Wait for the readiness delay and confirm it is still running
|
||||||
|
if readiness_delay > 0
|
||||||
|
info "Container is running, waiting for readiness delay of #{readiness_delay} seconds"
|
||||||
|
sleep readiness_delay
|
||||||
|
status = block.call
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
unless %w[ running healthy ].include?(status)
|
||||||
|
raise Kamal::Cli::Healthcheck::Error, "container not ready after #{KAMAL.config.deploy_timeout} seconds (#{status})"
|
||||||
|
end
|
||||||
|
rescue Kamal::Cli::Healthcheck::Error => e
|
||||||
|
time_left = timeout_at - Time.now
|
||||||
|
if time_left > 0
|
||||||
|
sleep [ attempt, time_left ].min
|
||||||
|
attempt += 1
|
||||||
|
retry
|
||||||
|
else
|
||||||
|
raise
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
info "Container is healthy!"
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def info(message)
|
||||||
|
SSHKit.config.output.info(message)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -2,7 +2,9 @@ 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
|
||||||
|
puts capture_with_debug(*KAMAL.lock.status)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -10,8 +12,12 @@ class Kamal::Cli::Lock < Kamal::Cli::Base
|
|||||||
option :message, aliases: "-m", type: :string, desc: "A lock message", required: true
|
option :message, aliases: "-m", type: :string, desc: "A lock message", required: true
|
||||||
def acquire
|
def acquire
|
||||||
message = options[:message]
|
message = options[:message]
|
||||||
|
ensure_run_directory
|
||||||
|
|
||||||
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.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,9 @@ 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.lock.release, verbosity: :debug
|
||||||
|
end
|
||||||
say "Released the deploy lock"
|
say "Released the deploy lock"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
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
|
||||||
|
|
||||||
|
invoke "kamal:cli:accessory:boot", [ "all" ], invoke_options
|
||||||
deploy
|
deploy
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -14,30 +19,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.merge(skip_local: options[:skip_push])
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
run_hook "pre-deploy"
|
with_lock do
|
||||||
|
run_hook "pre-deploy", secrets: true
|
||||||
|
|
||||||
say "Ensure Traefik is running...", :magenta
|
say "Ensure kamal-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
|
||||||
|
|
||||||
@@ -46,51 +48,48 @@ class Kamal::Cli::Main < Kamal::Cli::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
run_hook "post-deploy", runtime: runtime.round
|
run_hook "post-deploy", secrets: true, runtime: runtime.round.to_s
|
||||||
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 kamal-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
|
||||||
|
|
||||||
run_hook "pre-deploy"
|
with_lock do
|
||||||
|
run_hook "pre-deploy", secrets: true
|
||||||
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
|
||||||
end
|
end
|
||||||
|
|
||||||
run_hook "post-deploy", runtime: runtime.round
|
run_hook "post-deploy", secrets: true, runtime: runtime.round.to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
desc "rollback [VERSION]", "Rollback app to VERSION"
|
desc "rollback [VERSION]", "Rollback app to VERSION"
|
||||||
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
|
||||||
old_version = nil
|
old_version = nil
|
||||||
|
|
||||||
if container_available?(version)
|
if container_available?(version)
|
||||||
run_hook "pre-deploy"
|
run_hook "pre-deploy", secrets: true
|
||||||
|
|
||||||
invoke "kamal:cli:app:boot", [], invoke_options.merge(version: version)
|
invoke "kamal:cli:app:boot", [], invoke_options.merge(version: version)
|
||||||
rolled_back = true
|
rolled_back = true
|
||||||
@@ -100,12 +99,12 @@ class Kamal::Cli::Main < Kamal::Cli::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
run_hook "post-deploy", runtime: runtime.round if rolled_back
|
run_hook "post-deploy", secrets: true, runtime: runtime.round.to_s if rolled_back
|
||||||
end
|
end
|
||||||
|
|
||||||
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
|
||||||
@@ -124,6 +123,18 @@ class Kamal::Cli::Main < Kamal::Cli::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
desc "docs [SECTION]", "Show Kamal configuration documentation"
|
||||||
|
def docs(section = nil)
|
||||||
|
case section
|
||||||
|
when NilClass
|
||||||
|
puts Kamal::Configuration.validation_doc
|
||||||
|
else
|
||||||
|
puts Kamal::Configuration.const_get(section.titlecase.to_sym).validation_doc
|
||||||
|
end
|
||||||
|
rescue NameError
|
||||||
|
puts "No documentation found for #{section}"
|
||||||
|
end
|
||||||
|
|
||||||
desc "init", "Create config stub in config/deploy.yml and env stub in .env"
|
desc "init", "Create config stub in config/deploy.yml and env stub in .env"
|
||||||
option :bundle, type: :boolean, default: false, desc: "Add Kamal to the Gemfile and create a bin/kamal binstub"
|
option :bundle, type: :boolean, default: false, desc: "Add Kamal to the Gemfile and create a bin/kamal binstub"
|
||||||
def init
|
def init
|
||||||
@@ -137,9 +148,10 @@ class Kamal::Cli::Main < Kamal::Cli::Base
|
|||||||
puts "Created configuration file in config/deploy.yml"
|
puts "Created configuration file in config/deploy.yml"
|
||||||
end
|
end
|
||||||
|
|
||||||
unless (deploy_file = Pathname.new(File.expand_path(".env"))).exist?
|
unless (secrets_file = Pathname.new(File.expand_path(".kamal/secrets"))).exist?
|
||||||
FileUtils.cp_r Pathname.new(File.expand_path("templates/template.env", __dir__)), deploy_file
|
FileUtils.mkdir_p secrets_file.dirname
|
||||||
puts "Created .env file"
|
FileUtils.cp_r Pathname.new(File.expand_path("templates/secrets", __dir__)), secrets_file
|
||||||
|
puts "Created .kamal/secrets file"
|
||||||
end
|
end
|
||||||
|
|
||||||
unless (hooks_dir = Pathname.new(File.expand_path(".kamal/hooks"))).exist?
|
unless (hooks_dir = Pathname.new(File.expand_path(".kamal/hooks"))).exist?
|
||||||
@@ -164,28 +176,46 @@ class Kamal::Cli::Main < Kamal::Cli::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
desc "envify", "Create .env by evaluating .env.erb (or .env.staging.erb -> .env.staging when using -d staging)"
|
desc "remove", "Remove kamal-proxy, app, accessories, and registry session from servers"
|
||||||
def envify
|
|
||||||
if destination = options[:destination]
|
|
||||||
env_template_path = ".env.#{destination}.erb"
|
|
||||||
env_path = ".env.#{destination}"
|
|
||||||
else
|
|
||||||
env_template_path = ".env.erb"
|
|
||||||
env_path = ".env"
|
|
||||||
end
|
|
||||||
|
|
||||||
File.write(env_path, ERB.new(File.read(env_template_path)).result, perm: 0600)
|
|
||||||
end
|
|
||||||
|
|
||||||
desc "remove", "Remove Traefik, app, accessories, and registry session from servers"
|
|
||||||
option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
|
option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
|
||||||
def remove
|
def remove
|
||||||
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:app:remove", [], options.without(:confirmed)
|
invoke "kamal:cli:app:remove", [], options.without(:confirmed)
|
||||||
|
invoke "kamal:cli:proxy: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).merge(skip_local: true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "upgrade", "Upgrade from Kamal 1.x to 2.0"
|
||||||
|
option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
|
||||||
|
option :rolling, type: :boolean, default: false, desc: "Upgrade one host at a time"
|
||||||
|
def upgrade
|
||||||
|
confirming "This will replace Traefik with kamal-proxy and restart all accessories" do
|
||||||
|
with_lock do
|
||||||
|
if options[:rolling]
|
||||||
|
(KAMAL.hosts | KAMAL.accessory_hosts).each do |host|
|
||||||
|
KAMAL.with_specific_hosts(host) do
|
||||||
|
say "Upgrading #{host}...", :magenta
|
||||||
|
if KAMAL.hosts.include?(host)
|
||||||
|
invoke "kamal:cli:proxy:upgrade", [], options.merge(confirmed: true, rolling: false)
|
||||||
|
reset_invocation(Kamal::Cli::Proxy)
|
||||||
|
end
|
||||||
|
if KAMAL.accessory_hosts.include?(host)
|
||||||
|
invoke "kamal:cli:accessory:upgrade", [ "all" ], options.merge(confirmed: true, rolling: false)
|
||||||
|
reset_invocation(Kamal::Cli::Accessory)
|
||||||
|
end
|
||||||
|
say "Upgraded #{host}", :magenta
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
say "Upgrading all hosts...", :magenta
|
||||||
|
invoke "kamal:cli:proxy:upgrade", [], options.merge(confirmed: true)
|
||||||
|
invoke "kamal:cli:accessory:upgrade", [ "all" ], options.merge(confirmed: true)
|
||||||
|
say "Upgraded all hosts", :magenta
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -204,34 +234,34 @@ 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"
|
|
||||||
subcommand "healthcheck", Kamal::Cli::Healthcheck
|
|
||||||
|
|
||||||
desc "lock", "Manage the deploy lock"
|
desc "lock", "Manage the deploy lock"
|
||||||
subcommand "lock", Kamal::Cli::Lock
|
subcommand "lock", Kamal::Cli::Lock
|
||||||
|
|
||||||
|
desc "proxy", "Manage kamal-proxy"
|
||||||
|
subcommand "proxy", Kamal::Cli::Proxy
|
||||||
|
|
||||||
desc "prune", "Prune old application images and containers"
|
desc "prune", "Prune old application images and containers"
|
||||||
subcommand "prune", Kamal::Cli::Prune
|
subcommand "prune", Kamal::Cli::Prune
|
||||||
|
|
||||||
desc "registry", "Login and -out of the image registry"
|
desc "registry", "Login and -out of the image registry"
|
||||||
subcommand "registry", Kamal::Cli::Registry
|
subcommand "registry", Kamal::Cli::Registry
|
||||||
|
|
||||||
|
desc "secrets", "Helpers for extracting secrets"
|
||||||
|
subcommand "secrets", Kamal::Cli::Secrets
|
||||||
|
|
||||||
desc "server", "Bootstrap servers with curl and Docker"
|
desc "server", "Bootstrap servers with curl and Docker"
|
||||||
subcommand "server", Kamal::Cli::Server
|
subcommand "server", Kamal::Cli::Server
|
||||||
|
|
||||||
desc "traefik", "Manage Traefik load balancer"
|
|
||||||
subcommand "traefik", Kamal::Cli::Traefik
|
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
255
lib/kamal/cli/proxy.rb
Normal file
255
lib/kamal/cli/proxy.rb
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
class Kamal::Cli::Proxy < Kamal::Cli::Base
|
||||||
|
desc "boot", "Boot proxy on servers"
|
||||||
|
def boot
|
||||||
|
with_lock do
|
||||||
|
on(KAMAL.hosts) do |host|
|
||||||
|
execute *KAMAL.docker.create_network
|
||||||
|
rescue SSHKit::Command::Failed => e
|
||||||
|
raise unless e.message.include?("already exists")
|
||||||
|
end
|
||||||
|
|
||||||
|
on(KAMAL.proxy_hosts) do |host|
|
||||||
|
execute *KAMAL.registry.login
|
||||||
|
|
||||||
|
version = capture_with_info(*KAMAL.proxy.version).strip.presence
|
||||||
|
|
||||||
|
if version && Kamal::Utils.older_version?(version, Kamal::Configuration::PROXY_MINIMUM_VERSION)
|
||||||
|
raise "kamal-proxy version #{version} is too old, please reboot to update to at least #{Kamal::Configuration::PROXY_MINIMUM_VERSION}"
|
||||||
|
end
|
||||||
|
execute *KAMAL.proxy.start_or_run
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "boot_config <set|get|clear>", "Mange kamal-proxy boot configuration"
|
||||||
|
option :publish, type: :boolean, default: true, desc: "Publish the proxy ports on the host"
|
||||||
|
option :http_port, type: :numeric, default: Kamal::Configuration::PROXY_HTTP_PORT, desc: "HTTP port to publish on the host"
|
||||||
|
option :https_port, type: :numeric, default: Kamal::Configuration::PROXY_HTTPS_PORT, desc: "HTTPS port to publish on the host"
|
||||||
|
option :docker_options, type: :array, default: [], desc: "Docker options to pass to the proxy container", banner: "option=value option2=value2"
|
||||||
|
def boot_config(subcommand)
|
||||||
|
case subcommand
|
||||||
|
when "set"
|
||||||
|
boot_options = [
|
||||||
|
*(KAMAL.config.proxy_publish_args(options[:http_port], options[:https_port]) if options[:publish]),
|
||||||
|
*options[:docker_options].map { |option| "--#{option}" }
|
||||||
|
]
|
||||||
|
|
||||||
|
on(KAMAL.proxy_hosts) do |host|
|
||||||
|
execute(*KAMAL.proxy.ensure_proxy_directory)
|
||||||
|
upload! StringIO.new(boot_options.join(" ")), KAMAL.config.proxy_options_file
|
||||||
|
end
|
||||||
|
when "get"
|
||||||
|
on(KAMAL.proxy_hosts) do |host|
|
||||||
|
puts "Host #{host}: #{capture_with_info(*KAMAL.proxy.get_boot_options)}"
|
||||||
|
end
|
||||||
|
when "reset"
|
||||||
|
on(KAMAL.proxy_hosts) do |host|
|
||||||
|
execute *KAMAL.proxy.reset_boot_options
|
||||||
|
end
|
||||||
|
else
|
||||||
|
raise ArgumentError, "Unknown boot_config subcommand #{subcommand}"
|
||||||
|
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 |host|
|
||||||
|
execute *KAMAL.auditor.record("Rebooted proxy"), verbosity: :debug
|
||||||
|
execute *KAMAL.registry.login
|
||||||
|
|
||||||
|
"Stopping and removing Traefik on #{host}, if running..."
|
||||||
|
execute *KAMAL.proxy.cleanup_traefik
|
||||||
|
|
||||||
|
"Stopping and removing kamal-proxy on #{host}, if running..."
|
||||||
|
execute *KAMAL.proxy.stop, raise_on_non_zero_exit: false
|
||||||
|
execute *KAMAL.proxy.remove_container
|
||||||
|
|
||||||
|
execute *KAMAL.proxy.run
|
||||||
|
|
||||||
|
KAMAL.roles_on(host).select(&:running_proxy?).each do |role|
|
||||||
|
app = KAMAL.app(role: role, host: host)
|
||||||
|
|
||||||
|
version = capture_with_info(*app.current_running_version, raise_on_non_zero_exit: false).strip
|
||||||
|
endpoint = capture_with_info(*app.container_id_for_version(version)).strip
|
||||||
|
|
||||||
|
if endpoint.present?
|
||||||
|
info "Deploying #{endpoint} for role `#{role}` on #{host}..."
|
||||||
|
execute *app.deploy(target: endpoint)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
run_hook "post-proxy-reboot", hosts: host_list
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "upgrade", "Upgrade to kamal-proxy on servers (stop container, remove container, start new container, reboot app)", hide: true
|
||||||
|
option :rolling, type: :boolean, default: false, desc: "Reboot proxy on hosts in sequence, rather than in parallel"
|
||||||
|
option :confirmed, aliases: "-y", type: :boolean, default: false, desc: "Proceed without confirmation question"
|
||||||
|
def upgrade
|
||||||
|
invoke_options = { "version" => KAMAL.config.latest_tag }.merge(options)
|
||||||
|
|
||||||
|
confirming "This will cause a brief outage on each host. Are you sure?" do
|
||||||
|
host_groups = options[:rolling] ? KAMAL.hosts : [ KAMAL.hosts ]
|
||||||
|
host_groups.each do |hosts|
|
||||||
|
host_list = Array(hosts).join(",")
|
||||||
|
say "Upgrading proxy on #{host_list}...", :magenta
|
||||||
|
run_hook "pre-proxy-reboot", hosts: host_list
|
||||||
|
on(hosts) do |host|
|
||||||
|
execute *KAMAL.auditor.record("Rebooted proxy"), verbosity: :debug
|
||||||
|
execute *KAMAL.registry.login
|
||||||
|
|
||||||
|
"Stopping and removing Traefik on #{host}, if running..."
|
||||||
|
execute *KAMAL.proxy.cleanup_traefik
|
||||||
|
|
||||||
|
"Stopping and removing kamal-proxy on #{host}, if running..."
|
||||||
|
execute *KAMAL.proxy.stop, raise_on_non_zero_exit: false
|
||||||
|
execute *KAMAL.proxy.remove_container
|
||||||
|
execute *KAMAL.proxy.remove_image
|
||||||
|
end
|
||||||
|
|
||||||
|
KAMAL.with_specific_hosts(hosts) do
|
||||||
|
invoke "kamal:cli:proxy:boot", [], invoke_options
|
||||||
|
reset_invocation(Kamal::Cli::Proxy)
|
||||||
|
invoke "kamal:cli:app:boot", [], invoke_options
|
||||||
|
reset_invocation(Kamal::Cli::App)
|
||||||
|
invoke "kamal:cli:prune:all", [], invoke_options
|
||||||
|
reset_invocation(Kamal::Cli::Prune)
|
||||||
|
end
|
||||||
|
|
||||||
|
run_hook "post-proxy-reboot", hosts: host_list
|
||||||
|
say "Upgraded proxy on #{host_list}", :magenta
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "start", "Start existing proxy container on servers"
|
||||||
|
def start
|
||||||
|
with_lock do
|
||||||
|
on(KAMAL.proxy_hosts) do |host|
|
||||||
|
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 |host|
|
||||||
|
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 "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)"
|
||||||
|
option :skip_timestamps, type: :boolean, aliases: "-T", desc: "Skip appending timestamps to logging output"
|
||||||
|
def logs
|
||||||
|
grep = options[:grep]
|
||||||
|
timestamps = !options[:skip_timestamps]
|
||||||
|
|
||||||
|
if options[:follow]
|
||||||
|
run_locally do
|
||||||
|
info "Following logs on #{KAMAL.primary_host}..."
|
||||||
|
info KAMAL.proxy.follow_logs(host: KAMAL.primary_host, timestamps: timestamps, grep: grep)
|
||||||
|
exec KAMAL.proxy.follow_logs(host: KAMAL.primary_host, timestamps: timestamps, 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(timestamps: timestamps, since: since, lines: lines, grep: grep)), type: "Proxy"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "remove", "Remove proxy container and image from servers"
|
||||||
|
option :force, type: :boolean, default: false, desc: "Force removing proxy when apps are still installed"
|
||||||
|
def remove
|
||||||
|
with_lock do
|
||||||
|
if removal_allowed?(options[:force])
|
||||||
|
stop
|
||||||
|
remove_container
|
||||||
|
remove_image
|
||||||
|
remove_proxy_directory
|
||||||
|
end
|
||||||
|
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
|
||||||
|
|
||||||
|
desc "remove_proxy_directory", "Remove the proxy directory from servers", hide: true
|
||||||
|
def remove_proxy_directory
|
||||||
|
with_lock do
|
||||||
|
on(KAMAL.proxy_hosts) do
|
||||||
|
execute *KAMAL.proxy.remove_proxy_directory, raise_on_non_zero_exit: false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def removal_allowed?(force)
|
||||||
|
on(KAMAL.proxy_hosts) do |host|
|
||||||
|
app_count = capture_with_info(*KAMAL.server.app_directory_count).chomp.to_i
|
||||||
|
raise "The are other applications installed on #{host}" if app_count > 0
|
||||||
|
end
|
||||||
|
|
||||||
|
true
|
||||||
|
rescue SSHKit::Runner::ExecuteError => e
|
||||||
|
raise unless e.message.include?("The are other applications installed on")
|
||||||
|
|
||||||
|
if force
|
||||||
|
say "Forcing, so removing the proxy, even though other apps are installed", :magenta
|
||||||
|
else
|
||||||
|
say "Not removing the proxy, as other apps are installed, ignore this check with kamal proxy remove --force", :magenta
|
||||||
|
end
|
||||||
|
|
||||||
|
force
|
||||||
|
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,18 +1,17 @@
|
|||||||
class Kamal::Cli::Registry < Kamal::Cli::Base
|
class Kamal::Cli::Registry < Kamal::Cli::Base
|
||||||
desc "login", "Log in to registry locally and remotely"
|
desc "login", "Log in to registry locally and remotely"
|
||||||
|
option :skip_local, aliases: "-L", type: :boolean, default: false, desc: "Skip local login"
|
||||||
|
option :skip_remote, aliases: "-R", type: :boolean, default: false, desc: "Skip remote login"
|
||||||
def login
|
def login
|
||||||
run_locally { execute *KAMAL.registry.login }
|
run_locally { execute *KAMAL.registry.login } unless options[:skip_local]
|
||||||
on(KAMAL.hosts) { execute *KAMAL.registry.login }
|
on(KAMAL.hosts) { execute *KAMAL.registry.login } unless options[:skip_remote]
|
||||||
# FIXME: This rescue needed?
|
|
||||||
rescue ArgumentError => e
|
|
||||||
puts e.message
|
|
||||||
end
|
end
|
||||||
|
|
||||||
desc "logout", "Log out of registry remotely"
|
desc "logout", "Log out of registry locally and remotely"
|
||||||
|
option :skip_local, aliases: "-L", type: :boolean, default: false, desc: "Skip local login"
|
||||||
|
option :skip_remote, aliases: "-R", type: :boolean, default: false, desc: "Skip remote login"
|
||||||
def logout
|
def logout
|
||||||
on(KAMAL.hosts) { execute *KAMAL.registry.logout }
|
run_locally { execute *KAMAL.registry.logout } unless options[:skip_local]
|
||||||
# FIXME: This rescue needed?
|
on(KAMAL.hosts) { execute *KAMAL.registry.logout } unless options[:skip_remote]
|
||||||
rescue ArgumentError => e
|
|
||||||
puts e.message
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
43
lib/kamal/cli/secrets.rb
Normal file
43
lib/kamal/cli/secrets.rb
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
class Kamal::Cli::Secrets < Kamal::Cli::Base
|
||||||
|
desc "fetch [SECRETS...]", "Fetch secrets from a vault"
|
||||||
|
option :adapter, type: :string, aliases: "-a", required: true, desc: "Which vault adapter to use"
|
||||||
|
option :account, type: :string, required: true, desc: "The account identifier or username"
|
||||||
|
option :from, type: :string, required: false, desc: "A vault or folder to fetch the secrets from"
|
||||||
|
option :inline, type: :boolean, required: false, hidden: true
|
||||||
|
def fetch(*secrets)
|
||||||
|
results = adapter(options[:adapter]).fetch(secrets, **options.slice(:account, :from).symbolize_keys)
|
||||||
|
|
||||||
|
return_or_puts JSON.dump(results).shellescape, inline: options[:inline]
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "extract", "Extract a single secret from the results of a fetch call"
|
||||||
|
option :inline, type: :boolean, required: false, hidden: true
|
||||||
|
def extract(name, secrets)
|
||||||
|
parsed_secrets = JSON.parse(secrets)
|
||||||
|
value = parsed_secrets[name] || parsed_secrets.find { |k, v| k.end_with?("/#{name}") }&.last
|
||||||
|
|
||||||
|
raise "Could not find secret #{name}" if value.nil?
|
||||||
|
|
||||||
|
return_or_puts value, inline: options[:inline]
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "print", "Print the secrets (for debugging)"
|
||||||
|
def print
|
||||||
|
KAMAL.config.secrets.to_h.each do |key, value|
|
||||||
|
puts "#{key}=#{value}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def adapter(adapter)
|
||||||
|
Kamal::Secrets::Adapters.lookup(adapter)
|
||||||
|
end
|
||||||
|
|
||||||
|
def return_or_puts(value, inline: nil)
|
||||||
|
if inline
|
||||||
|
value
|
||||||
|
else
|
||||||
|
puts value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,21 +1,48 @@
|
|||||||
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)
|
||||||
|
cmd = Kamal::Utils.join_commands(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
|
||||||
|
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
|
||||||
|
|||||||
@@ -2,11 +2,24 @@
|
|||||||
service: my-app
|
service: my-app
|
||||||
|
|
||||||
# Name of the container image.
|
# Name of the container image.
|
||||||
image: user/my-app
|
image: my-user/my-app
|
||||||
|
|
||||||
# Deploy to these servers.
|
# Deploy to these servers.
|
||||||
servers:
|
servers:
|
||||||
- 192.168.0.1
|
web:
|
||||||
|
- 192.168.0.1
|
||||||
|
# job:
|
||||||
|
# hosts:
|
||||||
|
# - 192.168.0.1
|
||||||
|
# cmd: bin/jobs
|
||||||
|
|
||||||
|
# Enable SSL auto certification via Let's Encrypt (and allow for multiple apps on one server).
|
||||||
|
# Set ssl: false if using something like Cloudflare to terminate SSL (but keep host!).
|
||||||
|
proxy:
|
||||||
|
ssl: true
|
||||||
|
host: app.example.com
|
||||||
|
# kamal-proxy connects to your container over port 80, use `app_port` to specify a different port.
|
||||||
|
# app_port: 3000
|
||||||
|
|
||||||
# Credentials for your image host.
|
# Credentials for your image host.
|
||||||
registry:
|
registry:
|
||||||
@@ -14,32 +27,52 @@ registry:
|
|||||||
# server: registry.digitalocean.com / ghcr.io / ...
|
# server: registry.digitalocean.com / ghcr.io / ...
|
||||||
username: my-user
|
username: my-user
|
||||||
|
|
||||||
# Always use an access token rather than real password when possible.
|
# Always use an access token rather than real password (pulled from .kamal/secrets).
|
||||||
password:
|
password:
|
||||||
- KAMAL_REGISTRY_PASSWORD
|
- KAMAL_REGISTRY_PASSWORD
|
||||||
|
|
||||||
# Inject ENV variables into containers (secrets come from .env).
|
# Configure builder setup.
|
||||||
|
builder:
|
||||||
|
arch: amd64
|
||||||
|
|
||||||
|
# Inject ENV variables into containers (secrets come from .kamal/secrets).
|
||||||
|
#
|
||||||
# env:
|
# env:
|
||||||
# clear:
|
# clear:
|
||||||
# DB_HOST: 192.168.0.2
|
# DB_HOST: 192.168.0.2
|
||||||
# secret:
|
# secret:
|
||||||
# - RAILS_MASTER_KEY
|
# - RAILS_MASTER_KEY
|
||||||
|
|
||||||
|
# Aliases are triggered with "bin/kamal <alias>". You can overwrite arguments on invocation:
|
||||||
|
# "bin/kamal logs -r job" will tail logs from the first server in the job section.
|
||||||
|
#
|
||||||
|
# aliases:
|
||||||
|
# shell: app exec --interactive --reuse "bash"
|
||||||
|
|
||||||
# Use a different ssh user than root
|
# Use a different ssh user than root
|
||||||
|
#
|
||||||
# ssh:
|
# ssh:
|
||||||
# user: app
|
# user: app
|
||||||
|
|
||||||
# Configure builder setup.
|
# Use a persistent storage volume.
|
||||||
# builder:
|
#
|
||||||
# args:
|
# volumes:
|
||||||
# RUBY_VERSION: 3.2.0
|
# - "app_storage:/app/storage"
|
||||||
# secrets:
|
|
||||||
# - GITHUB_TOKEN
|
|
||||||
# remote:
|
|
||||||
# arch: amd64
|
|
||||||
# host: ssh://app@192.168.0.1
|
|
||||||
|
|
||||||
# Use accessory services (secrets come from .env).
|
# Bridge fingerprinted assets, like JS and CSS, between versions to avoid
|
||||||
|
# hitting 404 on in-flight requests. Combines all files from new and old
|
||||||
|
# version inside the asset_path.
|
||||||
|
#
|
||||||
|
# asset_path: /app/public/assets
|
||||||
|
|
||||||
|
# Configure rolling deploys by setting a wait time between batches of restarts.
|
||||||
|
#
|
||||||
|
# boot:
|
||||||
|
# limit: 10 # Can also specify as a percentage of total hosts, such as "25%"
|
||||||
|
# wait: 2
|
||||||
|
|
||||||
|
# Use accessory services (secrets come from .kamal/secrets).
|
||||||
|
#
|
||||||
# accessories:
|
# accessories:
|
||||||
# db:
|
# db:
|
||||||
# image: mysql:8.0
|
# image: mysql:8.0
|
||||||
@@ -52,7 +85,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:
|
||||||
@@ -61,14 +94,3 @@ registry:
|
|||||||
# port: 6379
|
# port: 6379
|
||||||
# directories:
|
# directories:
|
||||||
# - data:/data
|
# - data:/data
|
||||||
|
|
||||||
# Configure custom arguments for Traefik
|
|
||||||
# traefik:
|
|
||||||
# args:
|
|
||||||
# accesslog: true
|
|
||||||
# accesslog.format: json
|
|
||||||
|
|
||||||
# Configure a custom healthcheck (default is /up on port 3000)
|
|
||||||
# healthcheck:
|
|
||||||
# path: /healthz
|
|
||||||
# port: 4000
|
|
||||||
|
|||||||
13
lib/kamal/cli/templates/sample_hooks/docker-setup.sample
Executable file
13
lib/kamal/cli/templates/sample_hooks/docker-setup.sample
Executable file
@@ -0,0 +1,13 @@
|
|||||||
|
#!/usr/bin/env ruby
|
||||||
|
|
||||||
|
# A sample docker-setup hook
|
||||||
|
#
|
||||||
|
# Sets up a Docker network on defined hosts which can then be used by the application’s containers
|
||||||
|
|
||||||
|
hosts = ENV["KAMAL_HOSTS"].split(",")
|
||||||
|
|
||||||
|
hosts.each do |ip|
|
||||||
|
destination = "root@#{ip}"
|
||||||
|
puts "Creating a Docker network \"kamal\" on #{destination}"
|
||||||
|
`ssh #{destination} docker network create kamal`
|
||||||
|
end
|
||||||
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 kamal-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 kamal-proxy on $KAMAL_HOSTS..."
|
||||||
17
lib/kamal/cli/templates/secrets
Normal file
17
lib/kamal/cli/templates/secrets
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Secrets defined here are available for reference under registry/password, env/secret, builder/secrets,
|
||||||
|
# and accessories/*/env/secret in config/deploy.yml. All secrets should be pulled from either
|
||||||
|
# password manager, ENV, or a file. DO NOT ENTER RAW CREDENTIALS HERE! This file needs to be safe for git.
|
||||||
|
|
||||||
|
# Option 1: Read secrets from the environment
|
||||||
|
KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD
|
||||||
|
|
||||||
|
# Option 2: Read secrets via a command
|
||||||
|
# RAILS_MASTER_KEY=$(cat config/master.key)
|
||||||
|
|
||||||
|
# Option 3: Read secrets via kamal secrets helpers
|
||||||
|
# These will handle logging in and fetching the secrets in as few calls as possible
|
||||||
|
# There are adapters for 1Password, LastPass + Bitwarden
|
||||||
|
#
|
||||||
|
# SECRETS=$(kamal secrets fetch --adapter 1password --account my-account --from MyVault/MyItem KAMAL_REGISTRY_PASSWORD RAILS_MASTER_KEY)
|
||||||
|
# KAMAL_REGISTRY_PASSWORD=$(kamal secrets extract KAMAL_REGISTRY_PASSWORD $SECRETS)
|
||||||
|
# RAILS_MASTER_KEY=$(kamal secrets extract RAILS_MASTER_KEY $SECRETS)
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
KAMAL_REGISTRY_PASSWORD=change-this
|
|
||||||
RAILS_MASTER_KEY=another-env
|
|
||||||
@@ -1,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
|
|
||||||
@@ -1,13 +1,16 @@
|
|||||||
require "active_support/core_ext/enumerable"
|
require "active_support/core_ext/enumerable"
|
||||||
require "active_support/core_ext/module/delegation"
|
require "active_support/core_ext/module/delegation"
|
||||||
|
require "active_support/core_ext/object/blank"
|
||||||
|
|
||||||
class Kamal::Commander
|
class Kamal::Commander
|
||||||
attr_accessor :verbosity, :holding_lock, :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
|
||||||
@@ -21,63 +24,65 @@ class Kamal::Commander
|
|||||||
@config, @config_kwargs = nil, kwargs
|
@config, @config_kwargs = nil, kwargs
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def configured?
|
||||||
|
@config || @config_kwargs
|
||||||
|
end
|
||||||
|
|
||||||
attr_reader :specific_roles, :specific_hosts
|
attr_reader :specific_roles, :specific_hosts
|
||||||
|
|
||||||
def specific_primary!
|
def specific_primary!
|
||||||
self.specific_hosts = [ config.primary_web_host ]
|
@specifics = nil
|
||||||
|
if specific_roles.present?
|
||||||
|
self.specific_hosts = [ specific_roles.first.primary_host ]
|
||||||
|
else
|
||||||
|
self.specific_hosts = [ config.primary_host ]
|
||||||
|
end
|
||||||
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
|
def with_specific_hosts(hosts)
|
||||||
(specific_hosts || config.all_hosts).select do |host|
|
original_hosts, self.specific_hosts = specific_hosts, hosts
|
||||||
(specific_roles || config.roles).flat_map(&:hosts).include?(host)
|
yield
|
||||||
end
|
ensure
|
||||||
end
|
self.specific_hosts = original_hosts
|
||||||
|
|
||||||
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
|
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 +101,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
|
||||||
@@ -108,6 +109,10 @@ class Kamal::Commander
|
|||||||
@lock ||= Kamal::Commands::Lock.new(config)
|
@lock ||= Kamal::Commands::Lock.new(config)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def proxy
|
||||||
|
@proxy ||= Kamal::Commands::Proxy.new(config)
|
||||||
|
end
|
||||||
|
|
||||||
def prune
|
def prune
|
||||||
@prune ||= Kamal::Commands::Prune.new(config)
|
@prune ||= Kamal::Commands::Prune.new(config)
|
||||||
end
|
end
|
||||||
@@ -116,10 +121,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 alias(name)
|
||||||
|
config.aliases[name]
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
def with_verbosity(level)
|
def with_verbosity(level)
|
||||||
old_level = self.verbosity
|
old_level = self.verbosity
|
||||||
|
|
||||||
@@ -132,12 +142,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 +169,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
|
||||||
|
|||||||
54
lib/kamal/commander/specifics.rb
Normal file
54
lib/kamal/commander/specifics.rb
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
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
|
||||||
|
config.accessories.flat_map(&:hosts) & specified_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
|
||||||
|
specified_hosts = specific_hosts || config.all_hosts
|
||||||
|
|
||||||
|
if (specific_role_hosts = specific_roles&.flat_map(&:hosts)).present?
|
||||||
|
specified_hosts.select { |host| specific_role_hosts.include?(host) }
|
||||||
|
else
|
||||||
|
specified_hosts
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
class Kamal::Commands::Accessory < Kamal::Commands::Base
|
class Kamal::Commands::Accessory < Kamal::Commands::Base
|
||||||
attr_reader :accessory_config
|
attr_reader :accessory_config
|
||||||
delegate :service_name, :image, :hosts, :port, :files, :directories, :cmd,
|
delegate :service_name, :image, :hosts, :port, :files, :directories, :cmd,
|
||||||
:publish_args, :env_args, :volume_args, :label_args, :option_args, to: :accessory_config
|
:publish_args, :env_args, :volume_args, :label_args, :option_args,
|
||||||
|
:secrets_io, :secrets_path, :env_directory,
|
||||||
|
to: :accessory_config
|
||||||
|
|
||||||
def initialize(config, name:)
|
def initialize(config, name:)
|
||||||
super(config)
|
super(config)
|
||||||
@@ -13,6 +15,7 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base
|
|||||||
"--name", service_name,
|
"--name", service_name,
|
||||||
"--detach",
|
"--detach",
|
||||||
"--restart", "unless-stopped",
|
"--restart", "unless-stopped",
|
||||||
|
"--network", "kamal",
|
||||||
*config.logging_args,
|
*config.logging_args,
|
||||||
*publish_args,
|
*publish_args,
|
||||||
*env_args,
|
*env_args,
|
||||||
@@ -36,17 +39,17 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
def logs(since: nil, lines: nil, grep: nil)
|
def logs(timestamps: true, since: nil, lines: nil, grep: nil, grep_options: nil)
|
||||||
pipe \
|
pipe \
|
||||||
docker(:logs, service_name, (" --since #{since}" if since), (" --tail #{lines}" if lines), "--timestamps", "2>&1"),
|
docker(:logs, service_name, (" --since #{since}" if since), (" --tail #{lines}" if lines), ("--timestamps" if timestamps), "2>&1"),
|
||||||
("grep '#{grep}'" if grep)
|
("grep '#{grep}'#{" #{grep_options}" if grep_options}" if grep)
|
||||||
end
|
end
|
||||||
|
|
||||||
def follow_logs(grep: nil)
|
def follow_logs(timestamps: true, grep: nil, grep_options: nil)
|
||||||
run_over_ssh \
|
run_over_ssh \
|
||||||
pipe \
|
pipe \
|
||||||
docker(:logs, service_name, "--timestamps", "--tail", "10", "--follow", "2>&1"),
|
docker(:logs, service_name, ("--timestamps" if timestamps), "--tail", "10", "--follow", "2>&1"),
|
||||||
(%(grep "#{grep}") if grep)
|
(%(grep "#{grep}"#{" #{grep_options}" if grep_options}) if grep)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
@@ -61,6 +64,7 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base
|
|||||||
docker :run,
|
docker :run,
|
||||||
("-it" if interactive),
|
("-it" if interactive),
|
||||||
"--rm",
|
"--rm",
|
||||||
|
"--network", "kamal",
|
||||||
*env_args,
|
*env_args,
|
||||||
*volume_args,
|
*volume_args,
|
||||||
image,
|
image,
|
||||||
@@ -86,14 +90,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 +102,10 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base
|
|||||||
docker :image, :rm, "--force", image
|
docker :image, :rm, "--force", image
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def ensure_env_directory
|
||||||
|
make_directory env_directory
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def service_filter
|
def service_filter
|
||||||
[ "--filter", "label=service=#{service_name}" ]
|
[ "--filter", "label=service=#{service_name}" ]
|
||||||
|
|||||||
@@ -1,30 +1,31 @@
|
|||||||
class Kamal::Commands::App < Kamal::Commands::Base
|
class Kamal::Commands::App < Kamal::Commands::Base
|
||||||
|
include Assets, Containers, Execution, Images, Logging, Proxy
|
||||||
|
|
||||||
ACTIVE_DOCKER_STATUSES = [ :running, :restarting ]
|
ACTIVE_DOCKER_STATUSES = [ :running, :restarting ]
|
||||||
|
|
||||||
attr_reader :role
|
attr_reader :role, :host
|
||||||
|
|
||||||
def initialize(config, role: nil)
|
delegate :container_name, to: :role
|
||||||
|
|
||||||
|
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),
|
"--network", "kamal",
|
||||||
|
*([ "--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,
|
||||||
@@ -42,7 +43,7 @@ class Kamal::Commands::App < Kamal::Commands::Base
|
|||||||
def stop(version: nil)
|
def stop(version: nil)
|
||||||
pipe \
|
pipe \
|
||||||
version ? container_id_for_version(version) : current_running_container_id,
|
version ? container_id_for_version(version) : current_running_container_id,
|
||||||
xargs(config.stop_wait_time ? docker(:stop, "-t", config.stop_wait_time) : docker(:stop))
|
xargs(docker(:stop, *role.stop_args))
|
||||||
end
|
end
|
||||||
|
|
||||||
def info
|
def info
|
||||||
@@ -50,55 +51,8 @@ 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)
|
||||||
@@ -106,61 +60,47 @@ class Kamal::Commands::App < Kamal::Commands::Base
|
|||||||
end
|
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
|
def ensure_env_directory
|
||||||
docker :container, :ls, "--all", *filter_args
|
make_directory role.env_directory
|
||||||
end
|
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 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
|
|
||||||
|
|
||||||
|
|
||||||
private
|
private
|
||||||
def container_name(version = nil)
|
def latest_image_id
|
||||||
[ config.service, role, config.destination, version || config.version ].compact.join("-")
|
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_directory),
|
||||||
|
[ *docker(:stop, "-t 1", asset_container, "2> /dev/null"), "|| true" ],
|
||||||
|
docker(:run, "--name", asset_container, "--detach", "--rm", "--entrypoint", "sleep", config.absolute_image, "1000000"),
|
||||||
|
docker(:cp, "-L", "#{asset_container}:#{role.asset_path}/.", role.asset_extracted_directory),
|
||||||
|
docker(:stop, "-t 1", asset_container),
|
||||||
|
by: "&&"
|
||||||
|
end
|
||||||
|
|
||||||
|
def sync_asset_volumes(old_version: nil)
|
||||||
|
new_extracted_path, new_volume_path = role.asset_extracted_directory(config.version), role.asset_volume.host_path
|
||||||
|
if old_version.present?
|
||||||
|
old_extracted_path, old_volume_path = role.asset_extracted_directory(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_directory),
|
||||||
|
find_and_remove_older_siblings(role.asset_volume_directory)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def find_and_remove_older_siblings(path)
|
||||||
|
[
|
||||||
|
:find,
|
||||||
|
Pathname.new(path).dirname.to_s,
|
||||||
|
"-maxdepth 1",
|
||||||
|
"-name", "'#{role.name}-*'",
|
||||||
|
"!", "-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
|
||||||
31
lib/kamal/commands/app/containers.rb
Normal file
31
lib/kamal/commands/app/containers.rb
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
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_health_log(version:)
|
||||||
|
pipe \
|
||||||
|
container_id_for(container_name: container_name(version)),
|
||||||
|
xargs(docker(:inspect, "--format", DOCKER_HEALTH_LOG_FORMAT))
|
||||||
|
end
|
||||||
|
end
|
||||||
30
lib/kamal/commands/app/execution.rb
Normal file
30
lib/kamal/commands/app/execution.rb
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
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",
|
||||||
|
"--network", "kamal",
|
||||||
|
*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, timestamps: true, since: nil, lines: nil, grep: nil, grep_options: nil)
|
||||||
|
pipe \
|
||||||
|
version ? container_id_for_version(version) : current_running_container_id,
|
||||||
|
"xargs docker logs#{" --timestamps" if timestamps}#{" --since #{since}" if since}#{" --tail #{lines}" if lines} 2>&1",
|
||||||
|
("grep '#{grep}'#{" #{grep_options}" if grep_options}" if grep)
|
||||||
|
end
|
||||||
|
|
||||||
|
def follow_logs(host:, timestamps: true, lines: nil, grep: nil, grep_options: nil)
|
||||||
|
run_over_ssh \
|
||||||
|
pipe(
|
||||||
|
current_running_container_id,
|
||||||
|
"xargs docker logs#{" --timestamps" if timestamps}#{" --tail #{lines}" if lines} --follow 2>&1",
|
||||||
|
(%(grep "#{grep}"#{" #{grep_options}" if grep_options}) if grep)
|
||||||
|
),
|
||||||
|
host: host
|
||||||
|
end
|
||||||
|
end
|
||||||
16
lib/kamal/commands/app/proxy.rb
Normal file
16
lib/kamal/commands/app/proxy.rb
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
module Kamal::Commands::App::Proxy
|
||||||
|
delegate :proxy_container_name, to: :config
|
||||||
|
|
||||||
|
def deploy(target:)
|
||||||
|
proxy_exec :deploy, role.container_prefix, *role.proxy.deploy_command_args(target: target)
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove
|
||||||
|
proxy_exec :remove, role.container_prefix
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def proxy_exec(*command)
|
||||||
|
docker :exec, proxy_container_name, "kamal-proxy", *command
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -8,9 +8,12 @@ class Kamal::Commands::Auditor < Kamal::Commands::Base
|
|||||||
|
|
||||||
# Runs remotely
|
# Runs remotely
|
||||||
def record(line, **details)
|
def record(line, **details)
|
||||||
append \
|
combine \
|
||||||
[ :echo, audit_tags(**details).except(:version, :service_version).to_s, line ],
|
[ :mkdir, "-p", config.run_directory ],
|
||||||
audit_log_file
|
append(
|
||||||
|
[ :echo, audit_tags(**details).except(:version, :service_version, :service).to_s, line ],
|
||||||
|
audit_log_file
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def reveal
|
def reveal
|
||||||
@@ -19,7 +22,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,22 @@ 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
|
||||||
|
|
||||||
|
def remove_file(path)
|
||||||
|
[ :rm, path ]
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def combine(*commands, by: "&&")
|
def combine(*commands, by: "&&")
|
||||||
commands
|
commands
|
||||||
@@ -50,14 +65,30 @@ 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 grep(*args)
|
||||||
|
args.compact.unshift :grep
|
||||||
|
end
|
||||||
|
|
||||||
def tags(**details)
|
def tags(**details)
|
||||||
Kamal::Tags.from_config(config, **details)
|
Kamal::Tags.from_config(config, **details)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,43 +1,37 @@
|
|||||||
|
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, :inspect_builder, :validate_image, :first_mirror, to: :target
|
||||||
|
delegate :local?, :remote?, to: "config.builder"
|
||||||
|
|
||||||
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
def target
|
def target
|
||||||
case
|
if remote?
|
||||||
when !config.builder.multiarch? && !config.builder.cached?
|
if local?
|
||||||
native
|
hybrid
|
||||||
when !config.builder.multiarch? && config.builder.cached?
|
else
|
||||||
native_cached
|
remote
|
||||||
when config.builder.local? && config.builder.remote?
|
end
|
||||||
multiarch_remote
|
|
||||||
when config.builder.remote?
|
|
||||||
native_remote
|
|
||||||
else
|
else
|
||||||
multiarch
|
local
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def native
|
def remote
|
||||||
@native ||= Kamal::Commands::Builder::Native.new(config)
|
@remote ||= Kamal::Commands::Builder::Remote.new(config)
|
||||||
end
|
end
|
||||||
|
|
||||||
def native_cached
|
def local
|
||||||
@native ||= Kamal::Commands::Builder::Native::Cached.new(config)
|
@local ||= Kamal::Commands::Builder::Local.new(config)
|
||||||
end
|
end
|
||||||
|
|
||||||
def native_remote
|
def hybrid
|
||||||
@native ||= Kamal::Commands::Builder::Native::Remote.new(config)
|
@hybrid ||= Kamal::Commands::Builder::Hybrid.new(config)
|
||||||
end
|
|
||||||
|
|
||||||
def multiarch
|
|
||||||
@multiarch ||= Kamal::Commands::Builder::Multiarch.new(config)
|
|
||||||
end
|
|
||||||
|
|
||||||
def multiarch_remote
|
|
||||||
@multiarch_remote ||= Kamal::Commands::Builder::Multiarch::Remote.new(config)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,61 @@
|
|||||||
|
|
||||||
class Kamal::Commands::Builder::Base < Kamal::Commands::Base
|
class Kamal::Commands::Builder::Base < Kamal::Commands::Base
|
||||||
class BuilderError < StandardError; end
|
class BuilderError < StandardError; end
|
||||||
|
|
||||||
|
ENDPOINT_DOCKER_HOST_INSPECT = "'{{.Endpoints.docker.Host}}'"
|
||||||
|
|
||||||
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, :arches, :local_arches, :remote_arches, :remote,
|
||||||
|
:cache_from, :cache_to, :ssh, :driver, :docker_driver?,
|
||||||
|
to: :builder_config
|
||||||
|
|
||||||
def clean
|
def clean
|
||||||
docker :image, :rm, "--force", config.absolute_image
|
docker :image, :rm, "--force", config.absolute_image
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def push
|
||||||
|
docker :buildx, :build,
|
||||||
|
"--push",
|
||||||
|
*platform_options(arches),
|
||||||
|
*([ "--builder", builder_name ] unless docker_driver?),
|
||||||
|
*build_options,
|
||||||
|
build_context
|
||||||
|
end
|
||||||
|
|
||||||
def pull
|
def pull
|
||||||
docker :pull, config.absolute_image
|
docker :pull, config.absolute_image
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def info
|
||||||
|
combine \
|
||||||
|
docker(:context, :ls),
|
||||||
|
docker(:buildx, :ls)
|
||||||
|
end
|
||||||
|
|
||||||
|
def inspect_builder
|
||||||
|
docker :buildx, :inspect, builder_name unless docker_driver?
|
||||||
|
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
|
||||||
|
|
||||||
|
def first_mirror
|
||||||
|
docker(:info, "--format '{{index .RegistryConfig.Mirrors 0}}'")
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def build_tags
|
def build_tags
|
||||||
@@ -29,8 +64,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
|
||||||
|
|
||||||
@@ -43,7 +78,7 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def build_secrets
|
def build_secrets
|
||||||
argumentize "--secret", secrets.collect { |secret| [ "id", secret ] }
|
argumentize "--secret", secrets.keys.collect { |secret| [ "id", secret ] }
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_dockerfile
|
def build_dockerfile
|
||||||
@@ -54,7 +89,19 @@ 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
|
||||||
|
|
||||||
|
def platform_options(arches)
|
||||||
|
argumentize "--platform", arches.map { |arch| "linux/#{arch}" }.join(",") if arches.any?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
29
lib/kamal/commands/builder/clone.rb
Normal file
29
lib/kamal/commands/builder/clone.rb
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
module Kamal::Commands::Builder::Clone
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
delegate :clone_directory, :build_directory, to: :"config.builder"
|
||||||
|
end
|
||||||
|
|
||||||
|
def clone
|
||||||
|
git :clone, Kamal::Git.root, "--recurse-submodules", path: clone_directory
|
||||||
|
end
|
||||||
|
|
||||||
|
def clone_reset_steps
|
||||||
|
[
|
||||||
|
git(:remote, "set-url", :origin, Kamal::Git.root, path: build_directory),
|
||||||
|
git(:fetch, :origin, path: build_directory),
|
||||||
|
git(:reset, "--hard", Kamal::Git.revision, path: build_directory),
|
||||||
|
git(:clean, "-fdx", path: build_directory),
|
||||||
|
git(:submodule, :update, "--init", path: build_directory)
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
def clone_status
|
||||||
|
git :status, "--porcelain", path: build_directory
|
||||||
|
end
|
||||||
|
|
||||||
|
def clone_revision
|
||||||
|
git :"rev-parse", :HEAD, path: build_directory
|
||||||
|
end
|
||||||
|
end
|
||||||
21
lib/kamal/commands/builder/hybrid.rb
Normal file
21
lib/kamal/commands/builder/hybrid.rb
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
class Kamal::Commands::Builder::Hybrid < Kamal::Commands::Builder::Remote
|
||||||
|
def create
|
||||||
|
combine \
|
||||||
|
create_local_buildx,
|
||||||
|
create_remote_context,
|
||||||
|
append_remote_buildx
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def builder_name
|
||||||
|
"kamal-hybrid-#{driver}-#{remote.gsub(/[^a-z0-9_-]/, "-")}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_local_buildx
|
||||||
|
docker :buildx, :create, *platform_options(local_arches), "--name", builder_name, "--driver=#{driver}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def append_remote_buildx
|
||||||
|
docker :buildx, :create, *platform_options(remote_arches), "--append", "--name", builder_name, remote_context_name
|
||||||
|
end
|
||||||
|
end
|
||||||
14
lib/kamal/commands/builder/local.rb
Normal file
14
lib/kamal/commands/builder/local.rb
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
class Kamal::Commands::Builder::Local < Kamal::Commands::Builder::Base
|
||||||
|
def create
|
||||||
|
docker :buildx, :create, "--name", builder_name, "--driver=#{driver}" unless docker_driver?
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove
|
||||||
|
docker :buildx, :rm, builder_name unless docker_driver?
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def builder_name
|
||||||
|
"kamal-local-#{driver}"
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
class Kamal::Commands::Builder::Multiarch < Kamal::Commands::Builder::Base
|
|
||||||
def create
|
|
||||||
docker :buildx, :create, "--use", "--name", builder_name
|
|
||||||
end
|
|
||||||
|
|
||||||
def remove
|
|
||||||
docker :buildx, :rm, builder_name
|
|
||||||
end
|
|
||||||
|
|
||||||
def push
|
|
||||||
docker :buildx, :build,
|
|
||||||
"--push",
|
|
||||||
"--platform", "linux/amd64,linux/arm64",
|
|
||||||
"--builder", builder_name,
|
|
||||||
*build_options,
|
|
||||||
build_context
|
|
||||||
end
|
|
||||||
|
|
||||||
def info
|
|
||||||
combine \
|
|
||||||
docker(:context, :ls),
|
|
||||||
docker(:buildx, :ls)
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
def builder_name
|
|
||||||
"kamal-#{config.service}-multiarch"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
class Kamal::Commands::Builder::Multiarch::Remote < Kamal::Commands::Builder::Multiarch
|
|
||||||
def create
|
|
||||||
combine \
|
|
||||||
create_contexts,
|
|
||||||
create_local_buildx,
|
|
||||||
append_remote_buildx
|
|
||||||
end
|
|
||||||
|
|
||||||
def remove
|
|
||||||
combine \
|
|
||||||
remove_contexts,
|
|
||||||
super
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
def builder_name
|
|
||||||
super + "-remote"
|
|
||||||
end
|
|
||||||
|
|
||||||
def builder_name_with_arch(arch)
|
|
||||||
"#{builder_name}-#{arch}"
|
|
||||||
end
|
|
||||||
|
|
||||||
def create_local_buildx
|
|
||||||
docker :buildx, :create, "--name", builder_name, builder_name_with_arch(local_arch), "--platform", "linux/#{local_arch}"
|
|
||||||
end
|
|
||||||
|
|
||||||
def append_remote_buildx
|
|
||||||
docker :buildx, :create, "--append", "--name", builder_name, builder_name_with_arch(remote_arch), "--platform", "linux/#{remote_arch}"
|
|
||||||
end
|
|
||||||
|
|
||||||
def create_contexts
|
|
||||||
combine \
|
|
||||||
create_context(local_arch, local_host),
|
|
||||||
create_context(remote_arch, remote_host)
|
|
||||||
end
|
|
||||||
|
|
||||||
def create_context(arch, host)
|
|
||||||
docker :context, :create, builder_name_with_arch(arch), "--description", "'#{builder_name} #{arch} native host'", "--docker", "'host=#{host}'"
|
|
||||||
end
|
|
||||||
|
|
||||||
def remove_contexts
|
|
||||||
combine \
|
|
||||||
remove_context(local_arch),
|
|
||||||
remove_context(remote_arch)
|
|
||||||
end
|
|
||||||
|
|
||||||
def remove_context(arch)
|
|
||||||
docker :context, :rm, builder_name_with_arch(arch)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
class Kamal::Commands::Builder::Native < Kamal::Commands::Builder::Base
|
|
||||||
def create
|
|
||||||
# No-op on native without cache
|
|
||||||
end
|
|
||||||
|
|
||||||
def remove
|
|
||||||
# No-op on native without cache
|
|
||||||
end
|
|
||||||
|
|
||||||
def push
|
|
||||||
combine \
|
|
||||||
docker(:build, *build_options, build_context),
|
|
||||||
docker(:push, config.absolute_image),
|
|
||||||
docker(:push, config.latest_image)
|
|
||||||
end
|
|
||||||
|
|
||||||
def info
|
|
||||||
# No-op on native
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
class Kamal::Commands::Builder::Native::Cached < Kamal::Commands::Builder::Native
|
|
||||||
def create
|
|
||||||
docker :buildx, :create, "--use", "--driver=docker-container"
|
|
||||||
end
|
|
||||||
|
|
||||||
def remove
|
|
||||||
docker :buildx, :rm, builder_name
|
|
||||||
end
|
|
||||||
|
|
||||||
def push
|
|
||||||
docker :buildx, :build,
|
|
||||||
"--push",
|
|
||||||
*build_options,
|
|
||||||
build_context
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
class Kamal::Commands::Builder::Native::Remote < Kamal::Commands::Builder::Native
|
|
||||||
def create
|
|
||||||
chain \
|
|
||||||
create_context,
|
|
||||||
create_buildx
|
|
||||||
end
|
|
||||||
|
|
||||||
def remove
|
|
||||||
chain \
|
|
||||||
remove_context,
|
|
||||||
remove_buildx
|
|
||||||
end
|
|
||||||
|
|
||||||
def push
|
|
||||||
docker :buildx, :build,
|
|
||||||
"--push",
|
|
||||||
"--platform", platform,
|
|
||||||
"--builder", builder_name,
|
|
||||||
*build_options,
|
|
||||||
build_context
|
|
||||||
end
|
|
||||||
|
|
||||||
def info
|
|
||||||
chain \
|
|
||||||
docker(:context, :ls),
|
|
||||||
docker(:buildx, :ls)
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
private
|
|
||||||
def builder_name
|
|
||||||
"kamal-#{config.service}-native-remote"
|
|
||||||
end
|
|
||||||
|
|
||||||
def builder_name_with_arch
|
|
||||||
"#{builder_name}-#{remote_arch}"
|
|
||||||
end
|
|
||||||
|
|
||||||
def platform
|
|
||||||
"linux/#{remote_arch}"
|
|
||||||
end
|
|
||||||
|
|
||||||
def create_context
|
|
||||||
docker :context, :create,
|
|
||||||
builder_name_with_arch, "--description", "'#{builder_name} #{remote_arch} native host'", "--docker", "'host=#{remote_host}'"
|
|
||||||
end
|
|
||||||
|
|
||||||
def remove_context
|
|
||||||
docker :context, :rm, builder_name_with_arch
|
|
||||||
end
|
|
||||||
|
|
||||||
def create_buildx
|
|
||||||
docker :buildx, :create, "--name", builder_name, builder_name_with_arch, "--platform", platform
|
|
||||||
end
|
|
||||||
|
|
||||||
def remove_buildx
|
|
||||||
docker :buildx, :rm, builder_name
|
|
||||||
end
|
|
||||||
end
|
|
||||||
63
lib/kamal/commands/builder/remote.rb
Normal file
63
lib/kamal/commands/builder/remote.rb
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
class Kamal::Commands::Builder::Remote < Kamal::Commands::Builder::Base
|
||||||
|
def create
|
||||||
|
chain \
|
||||||
|
create_remote_context,
|
||||||
|
create_buildx
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove
|
||||||
|
chain \
|
||||||
|
remove_remote_context,
|
||||||
|
remove_buildx
|
||||||
|
end
|
||||||
|
|
||||||
|
def info
|
||||||
|
chain \
|
||||||
|
docker(:context, :ls),
|
||||||
|
docker(:buildx, :ls)
|
||||||
|
end
|
||||||
|
|
||||||
|
def inspect_builder
|
||||||
|
combine \
|
||||||
|
combine inspect_buildx, inspect_remote_context,
|
||||||
|
[ "(echo no compatible builder && exit 1)" ],
|
||||||
|
by: "||"
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def builder_name
|
||||||
|
"kamal-remote-#{remote.gsub(/[^a-z0-9_-]/, "-")}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def remote_context_name
|
||||||
|
"#{builder_name}-context"
|
||||||
|
end
|
||||||
|
|
||||||
|
def inspect_buildx
|
||||||
|
pipe \
|
||||||
|
docker(:buildx, :inspect, builder_name),
|
||||||
|
grep("-q", "Endpoint:.*#{remote_context_name}")
|
||||||
|
end
|
||||||
|
|
||||||
|
def inspect_remote_context
|
||||||
|
pipe \
|
||||||
|
docker(:context, :inspect, remote_context_name, "--format", ENDPOINT_DOCKER_HOST_INSPECT),
|
||||||
|
grep("-xq", remote)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_remote_context
|
||||||
|
docker :context, :create, remote_context_name, "--description", "'#{builder_name} host'", "--docker", "'host=#{remote}'"
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_remote_context
|
||||||
|
docker :context, :rm, remote_context_name
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_buildx
|
||||||
|
docker :buildx, :create, "--name", builder_name, remote_context_name
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_buildx
|
||||||
|
docker :buildx, :rm, builder_name
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -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,19 @@ 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
|
||||||
|
|
||||||
|
def create_network
|
||||||
|
docker :network, :create, :kamal
|
||||||
|
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
|
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
class Kamal::Commands::Hook < Kamal::Commands::Base
|
class Kamal::Commands::Hook < Kamal::Commands::Base
|
||||||
def run(hook, **details)
|
def run(hook)
|
||||||
[ hook_file(hook), env: tags(**details).env ]
|
[ hook_file(hook) ]
|
||||||
|
end
|
||||||
|
|
||||||
|
def env(secrets: false, **details)
|
||||||
|
tags(**details).env.tap do |env|
|
||||||
|
env.merge!(config.secrets.to_h) if secrets
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def hook_exists?(hook)
|
def hook_exists?(hook)
|
||||||
@@ -9,6 +15,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,37 @@ 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 lock_dir
|
def lock_dir
|
||||||
"kamal_lock-#{config.service}"
|
dir_name = [ "lock", config.service, config.destination ].compact.join("-")
|
||||||
|
|
||||||
|
File.join(config.run_directory, 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 +63,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
|
||||||
|
|||||||
87
lib/kamal/commands/proxy.rb
Normal file
87
lib/kamal/commands/proxy.rb
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
class Kamal::Commands::Proxy < Kamal::Commands::Base
|
||||||
|
delegate :argumentize, :optionize, to: Kamal::Utils
|
||||||
|
|
||||||
|
def run
|
||||||
|
docker :run,
|
||||||
|
"--name", container_name,
|
||||||
|
"--network", "kamal",
|
||||||
|
"--detach",
|
||||||
|
"--restart", "unless-stopped",
|
||||||
|
"--volume", "kamal-proxy-config:/home/kamal-proxy/.config/kamal-proxy",
|
||||||
|
"\$\(#{get_boot_options.join(" ")}\)",
|
||||||
|
config.proxy_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 info
|
||||||
|
docker :ps, "--filter", "name=^#{container_name}$"
|
||||||
|
end
|
||||||
|
|
||||||
|
def version
|
||||||
|
pipe \
|
||||||
|
docker(:inspect, container_name, "--format '{{.Config.Image}}'"),
|
||||||
|
[ :cut, "-d:", "-f2" ]
|
||||||
|
end
|
||||||
|
|
||||||
|
def logs(timestamps: true, since: nil, lines: nil, grep: nil, grep_options: nil)
|
||||||
|
pipe \
|
||||||
|
docker(:logs, container_name, ("--since #{since}" if since), ("--tail #{lines}" if lines), ("--timestamps" if timestamps), "2>&1"),
|
||||||
|
("grep '#{grep}'#{" #{grep_options}" if grep_options}" if grep)
|
||||||
|
end
|
||||||
|
|
||||||
|
def follow_logs(host:, timestamps: true, grep: nil, grep_options: nil)
|
||||||
|
run_over_ssh pipe(
|
||||||
|
docker(:logs, container_name, ("--timestamps" if timestamps), "--tail", "10", "--follow", "2>&1"),
|
||||||
|
(%(grep "#{grep}"#{" #{grep_options}" if grep_options}) if grep)
|
||||||
|
).join(" "), host: host
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_container
|
||||||
|
docker :container, :prune, "--force", "--filter", "label=org.opencontainers.image.title=kamal-proxy"
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_image
|
||||||
|
docker :image, :prune, "--all", "--force", "--filter", "label=org.opencontainers.image.title=kamal-proxy"
|
||||||
|
end
|
||||||
|
|
||||||
|
def cleanup_traefik
|
||||||
|
chain \
|
||||||
|
docker(:container, :stop, "traefik"),
|
||||||
|
combine(
|
||||||
|
docker(:container, :prune, "--force", "--filter", "label=org.opencontainers.image.title=Traefik"),
|
||||||
|
docker(:image, :prune, "--all", "--force", "--filter", "label=org.opencontainers.image.title=Traefik")
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def ensure_proxy_directory
|
||||||
|
make_directory config.proxy_directory
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_proxy_directory
|
||||||
|
remove_directory config.proxy_directory
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_boot_options
|
||||||
|
combine [ :cat, config.proxy_options_file ], [ :echo, "\"#{config.proxy_options_default.join(" ")}\"" ], by: "||"
|
||||||
|
end
|
||||||
|
|
||||||
|
def reset_boot_options
|
||||||
|
remove_file config.proxy_options_file
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def container_name
|
||||||
|
config.proxy_container_name
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -3,26 +3,26 @@ 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
|
||||||
pipe \
|
pipe \
|
||||||
docker(:image, :ls, *service_filter, "--format", "'{{.ID}} {{.Repository}}:{{.Tag}}'"),
|
docker(:image, :ls, *service_filter, "--format", "'{{.ID}} {{.Repository}}:{{.Tag}}'"),
|
||||||
"grep -v -w \"#{active_image_list}\"",
|
grep("-v -w \"#{active_image_list}\""),
|
||||||
"while read image tag; do docker rmi $tag; done"
|
"while read image tag; do docker rmi $tag; done"
|
||||||
end
|
end
|
||||||
|
|
||||||
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,19 +2,13 @@ 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(registry.username)),
|
||||||
|
"-p", sensitive(Kamal::Utils.escape_shell_value(registry.password))
|
||||||
end
|
end
|
||||||
|
|
||||||
def logout
|
def logout
|
||||||
docker :logout, registry["server"]
|
docker :logout, registry.server
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
def lookup(key)
|
|
||||||
if registry[key].is_a?(Array)
|
|
||||||
ENV.fetch(registry[key].first).dup
|
|
||||||
else
|
|
||||||
registry[key]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
15
lib/kamal/commands/server.rb
Normal file
15
lib/kamal/commands/server.rb
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
class Kamal::Commands::Server < Kamal::Commands::Base
|
||||||
|
def ensure_run_directory
|
||||||
|
make_directory config.run_directory
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_app_directory
|
||||||
|
remove_directory config.app_directory
|
||||||
|
end
|
||||||
|
|
||||||
|
def app_directory_count
|
||||||
|
pipe \
|
||||||
|
[ :ls, config.apps_directory ],
|
||||||
|
[ :wc, "-l" ]
|
||||||
|
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
|
|
||||||
@@ -1,19 +1,27 @@
|
|||||||
require "active_support/ordered_options"
|
require "active_support/ordered_options"
|
||||||
require "active_support/core_ext/string/inquiry"
|
require "active_support/core_ext/string/inquiry"
|
||||||
require "active_support/core_ext/module/delegation"
|
require "active_support/core_ext/module/delegation"
|
||||||
require "pathname"
|
require "active_support/core_ext/hash/keys"
|
||||||
require "erb"
|
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, :labels, :hooks_path, 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, :secrets
|
||||||
attr_accessor :raw_config
|
attr_reader :accessories, :aliases, :boot, :builder, :env, :logging, :proxy, :servers, :ssh, :sshkit, :registry
|
||||||
|
|
||||||
|
include Validation
|
||||||
|
|
||||||
|
PROXY_MINIMUM_VERSION = "v0.7.0"
|
||||||
|
PROXY_HTTP_PORT = 80
|
||||||
|
PROXY_HTTPS_PORT = 443
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
def create_from(config_file:, destination: nil, version: nil)
|
def create_from(config_file:, destination: nil, version: nil)
|
||||||
|
ENV["KAMAL_DESTINATION"] = destination
|
||||||
|
|
||||||
raw_config = load_config_files(config_file, *destination_config_file(config_file, destination))
|
raw_config = load_config_files(config_file, *destination_config_file(config_file, destination))
|
||||||
|
|
||||||
new raw_config, destination: destination, version: version
|
new raw_config, destination: destination, version: version
|
||||||
@@ -26,7 +34,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
|
||||||
@@ -41,7 +51,34 @@ class Kamal::Configuration
|
|||||||
@raw_config = ActiveSupport::InheritableOptions.new(raw_config)
|
@raw_config = ActiveSupport::InheritableOptions.new(raw_config)
|
||||||
@destination = destination
|
@destination = destination
|
||||||
@declared_version = version
|
@declared_version = version
|
||||||
valid? if validate
|
|
||||||
|
validate! raw_config, example: validation_yml.symbolize_keys, context: "", with: Kamal::Configuration::Validator::Configuration
|
||||||
|
|
||||||
|
@secrets = Kamal::Secrets.new(destination: destination)
|
||||||
|
|
||||||
|
# Eager load config to validate it, these are first as they have dependencies later on
|
||||||
|
@servers = Servers.new(config: self)
|
||||||
|
@registry = Registry.new(config: self)
|
||||||
|
|
||||||
|
@accessories = @raw_config.accessories&.keys&.collect { |name| Accessory.new(name, config: self) } || []
|
||||||
|
@aliases = @raw_config.aliases&.keys&.to_h { |name| [ name, Alias.new(name, config: self) ] } || {}
|
||||||
|
@boot = Boot.new(config: self)
|
||||||
|
@builder = Builder.new(config: self)
|
||||||
|
@env = Env.new(config: @raw_config.env || {}, secrets: secrets)
|
||||||
|
|
||||||
|
@logging = Logging.new(logging_config: @raw_config.logging)
|
||||||
|
@proxy = Proxy.new(config: self, proxy_config: @raw_config.proxy || {})
|
||||||
|
@ssh = Ssh.new(config: self)
|
||||||
|
@sshkit = Sshkit.new(config: self)
|
||||||
|
|
||||||
|
ensure_destination_if_required
|
||||||
|
ensure_required_keys_present
|
||||||
|
ensure_valid_kamal_version
|
||||||
|
ensure_retain_containers_valid
|
||||||
|
ensure_valid_service_name
|
||||||
|
ensure_no_traefik_reboot_hooks
|
||||||
|
ensure_one_host_for_ssl_roles
|
||||||
|
ensure_unique_hosts_for_ssl_roles
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
@@ -54,46 +91,68 @@ 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
|
||||||
|
|
||||||
|
|
||||||
def roles
|
def roles
|
||||||
@roles ||= role_names.collect { |role_name| Role.new(role_name, config: self) }
|
servers.roles
|
||||||
end
|
end
|
||||||
|
|
||||||
def role(name)
|
def role(name)
|
||||||
roles.detect { |r| r.name == name.to_s }
|
roles.detect { |r| r.name == name.to_s }
|
||||||
end
|
end
|
||||||
|
|
||||||
def accessories
|
|
||||||
@accessories ||= raw_config.accessories&.keys&.collect { |name| Kamal::Configuration::Accessory.new(name, config: self) } || []
|
|
||||||
end
|
|
||||||
|
|
||||||
def accessory(name)
|
def accessory(name)
|
||||||
accessories.detect { |a| a.name == name.to_s }
|
accessories.detect { |a| a.name == name.to_s }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
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("/")
|
[ registry.server, image ].compact.join("/")
|
||||||
end
|
end
|
||||||
|
|
||||||
def absolute_image
|
def absolute_image
|
||||||
@@ -101,22 +160,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,38 +189,87 @@ class Kamal::Configuration
|
|||||||
end
|
end
|
||||||
|
|
||||||
def logging_args
|
def logging_args
|
||||||
if raw_config.logging.present?
|
logging.args
|
||||||
optionize({ "log-driver" => raw_config.logging["driver"] }.compact) +
|
|
||||||
argumentize("--log-opt", raw_config.logging["options"])
|
|
||||||
else
|
|
||||||
argumentize("--log-opt", { "max-size" => "10m" })
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
def ssh
|
|
||||||
Kamal::Configuration::Ssh.new(config: self)
|
|
||||||
end
|
|
||||||
|
|
||||||
def sshkit
|
|
||||||
Kamal::Configuration::Sshkit.new(config: self)
|
|
||||||
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 deploy_timeout
|
||||||
raw_config.minimum_version
|
raw_config.deploy_timeout || 30
|
||||||
end
|
end
|
||||||
|
|
||||||
def valid?
|
def drain_timeout
|
||||||
ensure_required_keys_present && ensure_valid_kamal_version
|
raw_config.drain_timeout || 30
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
def run_directory
|
||||||
|
".kamal"
|
||||||
|
end
|
||||||
|
|
||||||
|
def apps_directory
|
||||||
|
File.join run_directory, "apps"
|
||||||
|
end
|
||||||
|
|
||||||
|
def app_directory
|
||||||
|
File.join apps_directory, [ service, destination ].compact.join("-")
|
||||||
|
end
|
||||||
|
|
||||||
|
def env_directory
|
||||||
|
File.join app_directory, "env"
|
||||||
|
end
|
||||||
|
|
||||||
|
def assets_directory
|
||||||
|
File.join app_directory, "assets"
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
def hooks_path
|
||||||
|
raw_config.hooks_path || ".kamal/hooks"
|
||||||
|
end
|
||||||
|
|
||||||
|
def asset_path
|
||||||
|
raw_config.asset_path
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
def env_tags
|
||||||
|
@env_tags ||= if (tags = raw_config.env["tags"])
|
||||||
|
tags.collect { |name, config| Env::Tag.new(name, config: config, secrets: secrets) }
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def env_tag(name)
|
||||||
|
env_tags.detect { |t| t.name == name.to_s }
|
||||||
|
end
|
||||||
|
|
||||||
|
def proxy_publish_args(http_port, https_port)
|
||||||
|
argumentize "--publish", [ "#{http_port}:#{PROXY_HTTP_PORT}", "#{https_port}:#{PROXY_HTTPS_PORT}" ]
|
||||||
|
end
|
||||||
|
|
||||||
|
def proxy_options_default
|
||||||
|
proxy_publish_args PROXY_HTTP_PORT, PROXY_HTTPS_PORT
|
||||||
|
end
|
||||||
|
|
||||||
|
def proxy_image
|
||||||
|
"basecamp/kamal-proxy:#{PROXY_MINIMUM_VERSION}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def proxy_container_name
|
||||||
|
"kamal-proxy"
|
||||||
|
end
|
||||||
|
|
||||||
|
def proxy_directory
|
||||||
|
File.join run_directory, "proxy"
|
||||||
|
end
|
||||||
|
|
||||||
|
def proxy_options_file
|
||||||
|
File.join proxy_directory, "options"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
@@ -165,74 +277,98 @@ class Kamal::Configuration
|
|||||||
{
|
{
|
||||||
roles: role_names,
|
roles: role_names,
|
||||||
hosts: all_hosts,
|
hosts: all_hosts,
|
||||||
primary_host: primary_web_host,
|
primary_host: primary_host,
|
||||||
version: version,
|
version: version,
|
||||||
repository: repository,
|
repository: repository,
|
||||||
absolute_image: absolute_image,
|
absolute_image: absolute_image,
|
||||||
service_with_version: service_with_version,
|
service_with_version: service_with_version,
|
||||||
env_args: env_args,
|
|
||||||
volume_args: volume_args,
|
volume_args: volume_args,
|
||||||
ssh_options: ssh.to_h,
|
ssh_options: ssh.to_h,
|
||||||
sshkit: sshkit.to_h,
|
sshkit: sshkit.to_h,
|
||||||
builder: builder.to_h,
|
builder: builder.to_h,
|
||||||
accessories: raw_config.accessories,
|
accessories: raw_config.accessories,
|
||||||
logging: logging_args,
|
logging: logging_args
|
||||||
healthcheck: healthcheck
|
|
||||||
}.compact
|
}.compact
|
||||||
end
|
end
|
||||||
|
|
||||||
def traefik
|
|
||||||
raw_config.traefik || {}
|
|
||||||
end
|
|
||||||
|
|
||||||
def hooks_path
|
|
||||||
raw_config.hooks_path || ".kamal/hooks"
|
|
||||||
end
|
|
||||||
|
|
||||||
def builder
|
|
||||||
Kamal::Configuration::Builder.new(config: self)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Will raise KeyError if any secret ENVs are missing
|
|
||||||
def ensure_env_available
|
|
||||||
env_args
|
|
||||||
roles.each(&:env_args)
|
|
||||||
|
|
||||||
true
|
|
||||||
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 Kamal::ConfigurationError, "Missing required configuration for #{key}" unless raw_config[key].present?
|
||||||
end
|
end
|
||||||
|
|
||||||
if raw_config.registry["username"].blank?
|
unless role(primary_role_name).present?
|
||||||
raise ArgumentError, "You must specify a username for the registry in config/deploy.yml"
|
raise Kamal::ConfigurationError, "The primary_role #{primary_role_name} isn't defined"
|
||||||
end
|
end
|
||||||
|
|
||||||
if raw_config.registry["password"].blank?
|
if primary_role.hosts.empty?
|
||||||
raise ArgumentError, "You must specify a password for the registry in config/deploy.yml (or set the ENV variable if that's used)"
|
raise Kamal::ConfigurationError, "No servers specified for the #{primary_role.name} primary_role"
|
||||||
end
|
end
|
||||||
|
|
||||||
roles.each do |role|
|
unless allow_empty_roles?
|
||||||
if role.hosts.empty?
|
roles.each do |role|
|
||||||
raise ArgumentError, "No servers specified for the #{role.name} role"
|
if role.hosts.empty?
|
||||||
|
raise Kamal::ConfigurationError, "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 Kamal::ConfigurationError, "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 Kamal::ConfigurationError, "Current version is #{Kamal::VERSION}, minimum required is #{minimum_version}"
|
||||||
end
|
end
|
||||||
|
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def ensure_retain_containers_valid
|
||||||
|
raise Kamal::ConfigurationError, "Must retain at least 1 container" if retain_containers < 1
|
||||||
|
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def ensure_no_traefik_reboot_hooks
|
||||||
|
hooks = %w[ pre-traefik-reboot post-traefik-reboot ].select { |hook_file| File.exist?(File.join(hooks_path, hook_file)) }
|
||||||
|
|
||||||
|
if hooks.any?
|
||||||
|
raise Kamal::ConfigurationError, "Found #{hooks.join(", ")}, these should be renamed to (pre|post)-proxy-reboot"
|
||||||
|
end
|
||||||
|
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def ensure_one_host_for_ssl_roles
|
||||||
|
roles.each(&:ensure_one_host_for_ssl)
|
||||||
|
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def ensure_unique_hosts_for_ssl_roles
|
||||||
|
hosts = roles.select(&:ssl?).flat_map { |role| role.proxy.hosts }
|
||||||
|
duplicates = hosts.tally.filter_map { |host, count| host if count > 1 }
|
||||||
|
|
||||||
|
raise Kamal::ConfigurationError, "Different roles can't share the same host for SSL: #{duplicates.join(", ")}" if duplicates.any?
|
||||||
|
|
||||||
|
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 +376,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,30 +1,39 @@
|
|||||||
class Kamal::Configuration::Accessory
|
class Kamal::Configuration::Accessory
|
||||||
delegate :argumentize, :argumentize_env_with_secrets, :optionize, to: Kamal::Utils
|
include Kamal::Configuration::Validation
|
||||||
|
|
||||||
attr_accessor :name, :specifics
|
delegate :argumentize, :optionize, to: Kamal::Utils
|
||||||
|
|
||||||
|
attr_reader :name, :accessory_config, :env
|
||||||
|
|
||||||
def initialize(name, config:)
|
def initialize(name, config:)
|
||||||
@name, @config, @specifics = name.inquiry, config, config.raw_config["accessories"][name]
|
@name, @config, @accessory_config = name.inquiry, config, config.raw_config["accessories"][name]
|
||||||
|
|
||||||
|
validate! \
|
||||||
|
accessory_config,
|
||||||
|
example: validation_yml["accessories"]["mysql"],
|
||||||
|
context: "accessories/#{name}",
|
||||||
|
with: Kamal::Configuration::Validator::Accessory
|
||||||
|
|
||||||
|
@env = Kamal::Configuration::Env.new \
|
||||||
|
config: accessory_config.fetch("env", {}),
|
||||||
|
secrets: config.secrets,
|
||||||
|
context: "accessories/#{name}/env"
|
||||||
end
|
end
|
||||||
|
|
||||||
def service_name
|
def service_name
|
||||||
"#{config.service}-#{name}"
|
accessory_config["service"] || "#{config.service}-#{name}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def image
|
def image
|
||||||
specifics["image"]
|
accessory_config["image"]
|
||||||
end
|
end
|
||||||
|
|
||||||
def hosts
|
def hosts
|
||||||
if (specifics.keys & ["host", "hosts", "roles"]).size != 1
|
|
||||||
raise ArgumentError, "Specify one of `host`, `hosts` or `roles` for accessory `#{name}`"
|
|
||||||
end
|
|
||||||
|
|
||||||
hosts_from_host || hosts_from_hosts || hosts_from_roles
|
hosts_from_host || hosts_from_hosts || hosts_from_roles
|
||||||
end
|
end
|
||||||
|
|
||||||
def port
|
def port
|
||||||
if port = specifics["port"]&.to_s
|
if port = accessory_config["port"]&.to_s
|
||||||
port.include?(":") ? port : "#{port}:#{port}"
|
port.include?(":") ? port : "#{port}:#{port}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -34,32 +43,40 @@ class Kamal::Configuration::Accessory
|
|||||||
end
|
end
|
||||||
|
|
||||||
def labels
|
def labels
|
||||||
default_labels.merge(specifics["labels"] || {})
|
default_labels.merge(accessory_config["labels"] || {})
|
||||||
end
|
end
|
||||||
|
|
||||||
def label_args
|
def label_args
|
||||||
argumentize "--label", labels
|
argumentize "--label", labels
|
||||||
end
|
end
|
||||||
|
|
||||||
def env
|
def env_args
|
||||||
specifics["env"] || {}
|
[ *env.clear_args, *argumentize("--env-file", secrets_path) ]
|
||||||
end
|
end
|
||||||
|
|
||||||
def env_args
|
def env_directory
|
||||||
argumentize_env_with_secrets env
|
File.join(config.env_directory, "accessories")
|
||||||
|
end
|
||||||
|
|
||||||
|
def secrets_io
|
||||||
|
env.secrets_io
|
||||||
|
end
|
||||||
|
|
||||||
|
def secrets_path
|
||||||
|
File.join(config.env_directory, "accessories", "#{name}.env")
|
||||||
end
|
end
|
||||||
|
|
||||||
def files
|
def files
|
||||||
specifics["files"]&.to_h do |local_to_remote_mapping|
|
accessory_config["files"]&.to_h do |local_to_remote_mapping|
|
||||||
local_file, remote_file = local_to_remote_mapping.split(":")
|
local_file, remote_file = local_to_remote_mapping.split(":")
|
||||||
[ expand_local_file(local_file), expand_remote_file(remote_file) ]
|
[ expand_local_file(local_file), expand_remote_file(remote_file) ]
|
||||||
end || {}
|
end || {}
|
||||||
end
|
end
|
||||||
|
|
||||||
def directories
|
def directories
|
||||||
specifics["directories"]&.to_h do |host_to_container_mapping|
|
accessory_config["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
|
||||||
|
|
||||||
@@ -72,7 +89,7 @@ class Kamal::Configuration::Accessory
|
|||||||
end
|
end
|
||||||
|
|
||||||
def option_args
|
def option_args
|
||||||
if args = specifics["options"]
|
if args = accessory_config["options"]
|
||||||
optionize args
|
optionize args
|
||||||
else
|
else
|
||||||
[]
|
[]
|
||||||
@@ -80,7 +97,7 @@ class Kamal::Configuration::Accessory
|
|||||||
end
|
end
|
||||||
|
|
||||||
def cmd
|
def cmd
|
||||||
specifics["cmd"]
|
accessory_config["cmd"]
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@@ -99,10 +116,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)
|
||||||
@@ -114,25 +131,29 @@ class Kamal::Configuration::Accessory
|
|||||||
end
|
end
|
||||||
|
|
||||||
def specific_volumes
|
def specific_volumes
|
||||||
specifics["volumes"] || []
|
accessory_config["volumes"] || []
|
||||||
end
|
end
|
||||||
|
|
||||||
def remote_files_as_volumes
|
def remote_files_as_volumes
|
||||||
specifics["files"]&.collect do |local_to_remote_mapping|
|
accessory_config["files"]&.collect do |local_to_remote_mapping|
|
||||||
_, remote_file = local_to_remote_mapping.split(":")
|
_, remote_file = local_to_remote_mapping.split(":")
|
||||||
"#{service_data_directory + remote_file}:#{remote_file}"
|
"#{service_data_directory + remote_file}:#{remote_file}"
|
||||||
end || []
|
end || []
|
||||||
end
|
end
|
||||||
|
|
||||||
def remote_directories_as_volumes
|
def remote_directories_as_volumes
|
||||||
specifics["directories"]&.collect do |host_to_container_mapping|
|
accessory_config["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
|
||||||
@@ -140,30 +161,16 @@ class Kamal::Configuration::Accessory
|
|||||||
end
|
end
|
||||||
|
|
||||||
def hosts_from_host
|
def hosts_from_host
|
||||||
if specifics.key?("host")
|
[ accessory_config["host"] ] if accessory_config.key?("host")
|
||||||
host = specifics["host"]
|
|
||||||
if host
|
|
||||||
[host]
|
|
||||||
else
|
|
||||||
raise ArgumentError, "Missing host for accessory `#{name}`"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def hosts_from_hosts
|
def hosts_from_hosts
|
||||||
if specifics.key?("hosts")
|
accessory_config["hosts"] if accessory_config.key?("hosts")
|
||||||
hosts = specifics["hosts"]
|
|
||||||
if hosts.is_a?(Array)
|
|
||||||
hosts
|
|
||||||
else
|
|
||||||
raise ArgumentError, "Hosts should be an Array for accessory `#{name}`"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def hosts_from_roles
|
def hosts_from_roles
|
||||||
if specifics.key?("roles")
|
if accessory_config.key?("roles")
|
||||||
specifics["roles"].flat_map { |role| config.role(role).hosts }
|
accessory_config["roles"].flat_map { |role| config.role(role).hosts }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
15
lib/kamal/configuration/alias.rb
Normal file
15
lib/kamal/configuration/alias.rb
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
class Kamal::Configuration::Alias
|
||||||
|
include Kamal::Configuration::Validation
|
||||||
|
|
||||||
|
attr_reader :name, :command
|
||||||
|
|
||||||
|
def initialize(name, config:)
|
||||||
|
@name, @command = name.inquiry, config.raw_config["aliases"][name]
|
||||||
|
|
||||||
|
validate! \
|
||||||
|
command,
|
||||||
|
example: validation_yml["aliases"]["uname"],
|
||||||
|
context: "aliases/#{name}",
|
||||||
|
with: Kamal::Configuration::Validator::Alias
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,20 +1,25 @@
|
|||||||
class Kamal::Configuration::Boot
|
class Kamal::Configuration::Boot
|
||||||
|
include Kamal::Configuration::Validation
|
||||||
|
|
||||||
|
attr_reader :boot_config, :host_count
|
||||||
|
|
||||||
def initialize(config:)
|
def initialize(config:)
|
||||||
@options = config.raw_config.boot || {}
|
@boot_config = config.raw_config.boot || {}
|
||||||
@host_count = config.all_hosts.count
|
@host_count = config.all_hosts.count
|
||||||
|
validate! boot_config
|
||||||
end
|
end
|
||||||
|
|
||||||
def limit
|
def limit
|
||||||
limit = @options["limit"]
|
limit = boot_config["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
|
||||||
end
|
end
|
||||||
|
|
||||||
def wait
|
def wait
|
||||||
@options["wait"]
|
boot_config["wait"]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,67 +1,93 @@
|
|||||||
class Kamal::Configuration::Builder
|
class Kamal::Configuration::Builder
|
||||||
def initialize(config:)
|
include Kamal::Configuration::Validation
|
||||||
@options = config.raw_config.builder || {}
|
|
||||||
@image = config.image
|
|
||||||
@server = config.registry["server"]
|
|
||||||
|
|
||||||
valid?
|
attr_reader :config, :builder_config
|
||||||
|
delegate :image, :service, to: :config
|
||||||
|
delegate :server, to: :"config.registry"
|
||||||
|
|
||||||
|
def initialize(config:)
|
||||||
|
@config = config
|
||||||
|
@builder_config = config.raw_config.builder || {}
|
||||||
|
@image = config.image
|
||||||
|
@server = config.registry.server
|
||||||
|
@service = config.service
|
||||||
|
|
||||||
|
validate! builder_config, with: Kamal::Configuration::Validator::Builder
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_h
|
def to_h
|
||||||
@options
|
builder_config
|
||||||
end
|
end
|
||||||
|
|
||||||
def multiarch?
|
def remote
|
||||||
@options["multiarch"] != false
|
builder_config["remote"]
|
||||||
end
|
end
|
||||||
|
|
||||||
def local?
|
def arches
|
||||||
!!@options["local"]
|
Array(builder_config.fetch("arch", default_arch))
|
||||||
|
end
|
||||||
|
|
||||||
|
def local_arches
|
||||||
|
@local_arches ||= if local_disabled?
|
||||||
|
[]
|
||||||
|
elsif remote
|
||||||
|
arches & [ Kamal::Utils.docker_arch ]
|
||||||
|
else
|
||||||
|
arches
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def remote_arches
|
||||||
|
@remote_arches ||= if remote
|
||||||
|
arches - local_arches
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def remote?
|
def remote?
|
||||||
!!@options["remote"]
|
remote_arches.any?
|
||||||
|
end
|
||||||
|
|
||||||
|
def local?
|
||||||
|
!local_disabled? && (arches.empty? || local_arches.any?)
|
||||||
end
|
end
|
||||||
|
|
||||||
def cached?
|
def cached?
|
||||||
!!@options["cache"]
|
!!builder_config["cache"]
|
||||||
end
|
end
|
||||||
|
|
||||||
def args
|
def args
|
||||||
@options["args"] || {}
|
builder_config["args"] || {}
|
||||||
end
|
end
|
||||||
|
|
||||||
def secrets
|
def secrets
|
||||||
@options["secrets"] || []
|
(builder_config["secrets"] || []).to_h { |key| [ key, config.secrets[key] ] }
|
||||||
end
|
end
|
||||||
|
|
||||||
def dockerfile
|
def dockerfile
|
||||||
@options["dockerfile"] || "Dockerfile"
|
builder_config["dockerfile"] || "Dockerfile"
|
||||||
|
end
|
||||||
|
|
||||||
|
def target
|
||||||
|
builder_config["target"]
|
||||||
end
|
end
|
||||||
|
|
||||||
def context
|
def context
|
||||||
@options["context"] || "."
|
builder_config["context"] || "."
|
||||||
end
|
end
|
||||||
|
|
||||||
def local_arch
|
def driver
|
||||||
@options["local"]["arch"] if local?
|
builder_config.fetch("driver", "docker-container")
|
||||||
end
|
end
|
||||||
|
|
||||||
def local_host
|
def local_disabled?
|
||||||
@options["local"]["host"] if local?
|
builder_config["local"] == false
|
||||||
end
|
|
||||||
|
|
||||||
def remote_arch
|
|
||||||
@options["remote"]["arch"] if remote?
|
|
||||||
end
|
|
||||||
|
|
||||||
def remote_host
|
|
||||||
@options["remote"]["host"] if remote?
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def cache_from
|
def cache_from
|
||||||
if cached?
|
if cached?
|
||||||
case @options["cache"]["type"]
|
case builder_config["cache"]["type"]
|
||||||
when "gha"
|
when "gha"
|
||||||
cache_from_config_for_gha
|
cache_from_config_for_gha
|
||||||
when "registry"
|
when "registry"
|
||||||
@@ -72,7 +98,7 @@ class Kamal::Configuration::Builder
|
|||||||
|
|
||||||
def cache_to
|
def cache_to
|
||||||
if cached?
|
if cached?
|
||||||
case @options["cache"]["type"]
|
case builder_config["cache"]["type"]
|
||||||
when "gha"
|
when "gha"
|
||||||
cache_to_config_for_gha
|
cache_to_config_for_gha
|
||||||
when "registry"
|
when "registry"
|
||||||
@@ -81,19 +107,50 @@ class Kamal::Configuration::Builder
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def ssh
|
||||||
|
builder_config["ssh"]
|
||||||
|
end
|
||||||
|
|
||||||
|
def git_clone?
|
||||||
|
Kamal::Git.used? && builder_config["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
|
||||||
|
|
||||||
|
def docker_driver?
|
||||||
|
driver == "docker"
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def valid?
|
def valid?
|
||||||
|
if docker_driver?
|
||||||
|
raise ArgumentError, "Invalid builder configuration: the `docker` driver does not not support remote builders" if remote
|
||||||
|
raise ArgumentError, "Invalid builder configuration: the `docker` driver does not not support caching" if cached?
|
||||||
|
raise ArgumentError, "Invalid builder configuration: the `docker` driver does not not support multiple arches" if arches.many?
|
||||||
|
end
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
def cache_image
|
def cache_image
|
||||||
@options["cache"]&.fetch("image", nil) || "#{@image}-build-cache"
|
builder_config["cache"]&.fetch("image", nil) || "#{image}-build-cache"
|
||||||
end
|
end
|
||||||
|
|
||||||
def cache_image_ref
|
def cache_image_ref
|
||||||
[ @server, cache_image ].compact.join("/")
|
[ server, cache_image ].compact.join("/")
|
||||||
end
|
end
|
||||||
|
|
||||||
def cache_from_config_for_gha
|
def cache_from_config_for_gha
|
||||||
@@ -105,10 +162,26 @@ 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", builder_config["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", builder_config["cache"]&.fetch("options", nil), "ref=#{cache_image_ref}" ].compact.join(",")
|
||||||
|
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
|
||||||
|
|
||||||
|
def default_arch
|
||||||
|
docker_driver? ? [] : [ "amd64", "arm64" ]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
92
lib/kamal/configuration/docs/accessory.yml
Normal file
92
lib/kamal/configuration/docs/accessory.yml
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
# Accessories
|
||||||
|
#
|
||||||
|
# Accessories can be booted on a single host, a list of hosts, or on specific roles.
|
||||||
|
# The hosts do not need to be defined in the Kamal servers configuration.
|
||||||
|
#
|
||||||
|
# Accessories are managed separately from the main service — they are not updated
|
||||||
|
# when you deploy, and they do not have zero-downtime deployments.
|
||||||
|
#
|
||||||
|
# Run `kamal accessory boot <accessory>` to boot an accessory.
|
||||||
|
# See `kamal accessory --help` for more information.
|
||||||
|
|
||||||
|
# Configuring accessories
|
||||||
|
#
|
||||||
|
# First, define the accessory in the `accessories`:
|
||||||
|
accessories:
|
||||||
|
mysql:
|
||||||
|
|
||||||
|
# Service name
|
||||||
|
#
|
||||||
|
# This is used in the service label and defaults to `<service>-<accessory>`,
|
||||||
|
# where `<service>` is the main service name from the root configuration:
|
||||||
|
service: mysql
|
||||||
|
|
||||||
|
# Image
|
||||||
|
#
|
||||||
|
# The Docker image to use, prefix it with a registry if not using Docker Hub:
|
||||||
|
image: mysql:8.0
|
||||||
|
|
||||||
|
# Accessory hosts
|
||||||
|
#
|
||||||
|
# Specify one of `host`, `hosts`, or `roles`:
|
||||||
|
host: mysql-db1
|
||||||
|
hosts:
|
||||||
|
- mysql-db1
|
||||||
|
- mysql-db2
|
||||||
|
roles:
|
||||||
|
- mysql
|
||||||
|
|
||||||
|
# Custom command
|
||||||
|
#
|
||||||
|
# You can set a custom command to run in the container if you do not want to use the default:
|
||||||
|
cmd: "bin/mysqld"
|
||||||
|
|
||||||
|
# Port mappings
|
||||||
|
#
|
||||||
|
# See https://docs.docker.com/network/, and especially note the warning about the security
|
||||||
|
# implications of exposing ports publicly.
|
||||||
|
port: "127.0.0.1:3306:3306"
|
||||||
|
|
||||||
|
# Labels
|
||||||
|
labels:
|
||||||
|
app: myapp
|
||||||
|
|
||||||
|
# Options
|
||||||
|
#
|
||||||
|
# These are passed to the Docker run command in the form `--<name> <value>`:
|
||||||
|
options:
|
||||||
|
restart: always
|
||||||
|
cpus: 2
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
#
|
||||||
|
# See kamal docs env for more information:
|
||||||
|
env:
|
||||||
|
...
|
||||||
|
|
||||||
|
# Copying files
|
||||||
|
#
|
||||||
|
# You can specify files to mount into the container.
|
||||||
|
# The format is `local:remote`, where `local` is the path to the file on the local machine
|
||||||
|
# and `remote` is the path to the file in the container.
|
||||||
|
#
|
||||||
|
# They will be uploaded from the local repo to the host and then mounted.
|
||||||
|
#
|
||||||
|
# ERB files will be evaluated before being copied.
|
||||||
|
files:
|
||||||
|
- config/my.cnf.erb:/etc/mysql/my.cnf
|
||||||
|
- config/myoptions.cnf:/etc/mysql/myoptions.cnf
|
||||||
|
|
||||||
|
# Directories
|
||||||
|
#
|
||||||
|
# You can specify directories to mount into the container. They will be created on the host
|
||||||
|
# before being mounted:
|
||||||
|
directories:
|
||||||
|
- mysql-logs:/var/log/mysql
|
||||||
|
|
||||||
|
# Volumes
|
||||||
|
#
|
||||||
|
# Any other volumes to mount, in addition to the files and directories.
|
||||||
|
# They are not created or copied before mounting:
|
||||||
|
volumes:
|
||||||
|
- /path/to/mysql-logs:/var/log/mysql
|
||||||
26
lib/kamal/configuration/docs/alias.yml
Normal file
26
lib/kamal/configuration/docs/alias.yml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Aliases
|
||||||
|
#
|
||||||
|
# Aliases are shortcuts for Kamal commands.
|
||||||
|
#
|
||||||
|
# For example, for a Rails app, you might open a console with:
|
||||||
|
#
|
||||||
|
# ```shell
|
||||||
|
# kamal app exec -i -r console "rails console"
|
||||||
|
# ```
|
||||||
|
#
|
||||||
|
# By defining an alias, like this:
|
||||||
|
aliases:
|
||||||
|
console: app exec -r console -i "rails console"
|
||||||
|
# You can now open the console with:
|
||||||
|
#
|
||||||
|
# ```shell
|
||||||
|
# kamal console
|
||||||
|
# ```
|
||||||
|
|
||||||
|
# Configuring aliases
|
||||||
|
#
|
||||||
|
# Aliases are defined in the root config under the alias key.
|
||||||
|
#
|
||||||
|
# Each alias is named and can only contain lowercase letters, numbers, dashes, and underscores:
|
||||||
|
aliases:
|
||||||
|
uname: app exec -p -q -r web "uname -a"
|
||||||
19
lib/kamal/configuration/docs/boot.yml
Normal file
19
lib/kamal/configuration/docs/boot.yml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Booting
|
||||||
|
#
|
||||||
|
# When deploying to large numbers of hosts, you might prefer not to restart your services on every host at the same time.
|
||||||
|
#
|
||||||
|
# Kamal’s default is to boot new containers on all hosts in parallel. However, you can control this with the boot configuration.
|
||||||
|
|
||||||
|
# Fixed group sizes
|
||||||
|
#
|
||||||
|
# Here, we boot 2 hosts at a time with a 10-second gap between each group:
|
||||||
|
boot:
|
||||||
|
limit: 2
|
||||||
|
wait: 10
|
||||||
|
|
||||||
|
# Percentage of hosts
|
||||||
|
#
|
||||||
|
# Here, we boot 25% of the hosts at a time with a 2-second gap between each group:
|
||||||
|
boot:
|
||||||
|
limit: 25%
|
||||||
|
wait: 2
|
||||||
104
lib/kamal/configuration/docs/builder.yml
Normal file
104
lib/kamal/configuration/docs/builder.yml
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
# Builder
|
||||||
|
#
|
||||||
|
# The builder configuration controls how the application is built with `docker build`.
|
||||||
|
#
|
||||||
|
# See https://kamal-deploy.org/docs/configuration/builder-examples/ for more information.
|
||||||
|
|
||||||
|
# Builder options
|
||||||
|
#
|
||||||
|
# Options go under the builder key in the root configuration.
|
||||||
|
builder:
|
||||||
|
|
||||||
|
# Arch
|
||||||
|
#
|
||||||
|
# The architectures to build for — you can set an array or just a single value.
|
||||||
|
#
|
||||||
|
# Allowed values are `amd64` and `arm64`:
|
||||||
|
arch:
|
||||||
|
- amd64
|
||||||
|
|
||||||
|
# Remote
|
||||||
|
#
|
||||||
|
# The connection string for a remote builder. If supplied, Kamal will use this
|
||||||
|
# for builds that do not match the local architecture of the deployment host.
|
||||||
|
remote: ssh://docker@docker-builder
|
||||||
|
|
||||||
|
# Local
|
||||||
|
#
|
||||||
|
# If set to false, Kamal will always use the remote builder even when building
|
||||||
|
# the local architecture.
|
||||||
|
#
|
||||||
|
# Defaults to true:
|
||||||
|
local: true
|
||||||
|
|
||||||
|
# Builder cache
|
||||||
|
#
|
||||||
|
# The type must be either 'gha' or 'registry'.
|
||||||
|
#
|
||||||
|
# The image is only used for registry cache and is not compatible with the Docker driver:
|
||||||
|
cache:
|
||||||
|
type: registry
|
||||||
|
options: mode=max
|
||||||
|
image: kamal-app-build-cache
|
||||||
|
|
||||||
|
# Build context
|
||||||
|
#
|
||||||
|
# If this is not set, then a local Git clone of the repo is used.
|
||||||
|
# This ensures a clean build with no uncommitted changes.
|
||||||
|
#
|
||||||
|
# To use the local checkout instead, you can set the context to `.`, or a path to another directory.
|
||||||
|
context: .
|
||||||
|
|
||||||
|
# Dockerfile
|
||||||
|
#
|
||||||
|
# The Dockerfile to use for building, defaults to `Dockerfile`:
|
||||||
|
dockerfile: Dockerfile.production
|
||||||
|
|
||||||
|
# Build target
|
||||||
|
#
|
||||||
|
# If not set, then the default target is used:
|
||||||
|
target: production
|
||||||
|
|
||||||
|
# Build arguments
|
||||||
|
#
|
||||||
|
# Any additional build arguments, passed to `docker build` with `--build-arg <key>=<value>`:
|
||||||
|
args:
|
||||||
|
ENVIRONMENT: production
|
||||||
|
|
||||||
|
# Referencing build arguments
|
||||||
|
#
|
||||||
|
# ```shell
|
||||||
|
# ARG RUBY_VERSION
|
||||||
|
# FROM ruby:$RUBY_VERSION-slim as base
|
||||||
|
# ```
|
||||||
|
|
||||||
|
# Build secrets
|
||||||
|
#
|
||||||
|
# Values are read from `.kamal/secrets`:
|
||||||
|
secrets:
|
||||||
|
- SECRET1
|
||||||
|
- SECRET2
|
||||||
|
|
||||||
|
# Referencing build secrets
|
||||||
|
#
|
||||||
|
# ```shell
|
||||||
|
# # Copy Gemfiles
|
||||||
|
# COPY Gemfile Gemfile.lock ./
|
||||||
|
#
|
||||||
|
# # Install dependencies, including private repositories via access token
|
||||||
|
# # Then remove bundle cache with exposed GITHUB_TOKEN
|
||||||
|
# RUN --mount=type=secret,id=GITHUB_TOKEN \
|
||||||
|
# BUNDLE_GITHUB__COM=x-access-token:$(cat /run/secrets/GITHUB_TOKEN) \
|
||||||
|
# bundle install && \
|
||||||
|
# rm -rf /usr/local/bundle/cache
|
||||||
|
# ```
|
||||||
|
|
||||||
|
# SSH
|
||||||
|
#
|
||||||
|
# SSH agent socket or keys to expose to the build:
|
||||||
|
ssh: default=$SSH_AUTH_SOCK
|
||||||
|
|
||||||
|
# Driver
|
||||||
|
#
|
||||||
|
# The build driver to use, defaults to `docker-container`:
|
||||||
|
driver: docker
|
||||||
178
lib/kamal/configuration/docs/configuration.yml
Normal file
178
lib/kamal/configuration/docs/configuration.yml
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
# Kamal Configuration
|
||||||
|
#
|
||||||
|
# Configuration is read from the `config/deploy.yml`.
|
||||||
|
|
||||||
|
# Destinations
|
||||||
|
#
|
||||||
|
# When running commands, you can specify a destination with the `-d` flag,
|
||||||
|
# e.g., `kamal deploy -d staging`.
|
||||||
|
#
|
||||||
|
# In this case, the configuration will also be read from `config/deploy.staging.yml`
|
||||||
|
# and merged with the base configuration.
|
||||||
|
|
||||||
|
# Extensions
|
||||||
|
#
|
||||||
|
# Kamal will not accept unrecognized keys in the configuration file.
|
||||||
|
#
|
||||||
|
# However, you might want to declare a configuration block using YAML anchors
|
||||||
|
# and aliases to avoid repetition.
|
||||||
|
#
|
||||||
|
# You can prefix a configuration section with `x-` to indicate that it is an
|
||||||
|
# extension. Kamal will ignore the extension and not raise an error.
|
||||||
|
|
||||||
|
# The service name
|
||||||
|
#
|
||||||
|
# This is a required value. It is used as the container name prefix.
|
||||||
|
service: myapp
|
||||||
|
|
||||||
|
# The Docker image name
|
||||||
|
#
|
||||||
|
# The image will be pushed to the configured registry.
|
||||||
|
image: my-image
|
||||||
|
|
||||||
|
# Labels
|
||||||
|
#
|
||||||
|
# Additional labels to add to the container:
|
||||||
|
labels:
|
||||||
|
my-label: my-value
|
||||||
|
|
||||||
|
# Volumes
|
||||||
|
#
|
||||||
|
# Additional volumes to mount into the container:
|
||||||
|
volumes:
|
||||||
|
- /path/on/host:/path/in/container:ro
|
||||||
|
|
||||||
|
# Registry
|
||||||
|
#
|
||||||
|
# The Docker registry configuration, see kamal docs registry:
|
||||||
|
registry:
|
||||||
|
...
|
||||||
|
|
||||||
|
# Servers
|
||||||
|
#
|
||||||
|
# The servers to deploy to, optionally with custom roles, see kamal docs servers:
|
||||||
|
servers:
|
||||||
|
...
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
#
|
||||||
|
# See kamal docs env:
|
||||||
|
env:
|
||||||
|
...
|
||||||
|
|
||||||
|
# Asset path
|
||||||
|
#
|
||||||
|
# Used for asset bridging across deployments, default to `nil`.
|
||||||
|
#
|
||||||
|
# If there are changes to CSS or JS files, we may get requests
|
||||||
|
# for the old versions on the new container, and vice versa.
|
||||||
|
#
|
||||||
|
# To avoid 404s, we can specify an asset path.
|
||||||
|
# Kamal will replace that path in the container with a mapped
|
||||||
|
# volume containing both sets of files.
|
||||||
|
# This requires that file names change when the contents change
|
||||||
|
# (e.g., by including a hash of the contents in the name).
|
||||||
|
#
|
||||||
|
# To configure this, set the path to the assets:
|
||||||
|
asset_path: /path/to/assets
|
||||||
|
|
||||||
|
# Hooks path
|
||||||
|
#
|
||||||
|
# Path to hooks, defaults to `.kamal/hooks`.
|
||||||
|
# See https://kamal-deploy.org/docs/hooks for more information:
|
||||||
|
hooks_path: /user_home/kamal/hooks
|
||||||
|
|
||||||
|
# Require destinations
|
||||||
|
#
|
||||||
|
# Whether deployments require a destination to be specified, defaults to `false`:
|
||||||
|
require_destination: true
|
||||||
|
|
||||||
|
# Primary role
|
||||||
|
#
|
||||||
|
# This defaults to `web`, but if you have no web role, you can change this:
|
||||||
|
primary_role: workers
|
||||||
|
|
||||||
|
# Allowing empty roles
|
||||||
|
#
|
||||||
|
# Whether roles with no servers are allowed. Defaults to `false`:
|
||||||
|
allow_empty_roles: false
|
||||||
|
|
||||||
|
# Retain containers
|
||||||
|
#
|
||||||
|
# How many old containers and images we retain, defaults to 5:
|
||||||
|
retain_containers: 3
|
||||||
|
|
||||||
|
# Minimum version
|
||||||
|
#
|
||||||
|
# The minimum version of Kamal required to deploy this configuration, defaults to `nil`:
|
||||||
|
minimum_version: 1.3.0
|
||||||
|
|
||||||
|
# Readiness delay
|
||||||
|
#
|
||||||
|
# Seconds to wait for a container to boot after it is running, default 7.
|
||||||
|
#
|
||||||
|
# This only applies to containers that do not run a proxy or specify a healthcheck:
|
||||||
|
readiness_delay: 4
|
||||||
|
|
||||||
|
# Deploy timeout
|
||||||
|
#
|
||||||
|
# How long to wait for a container to become ready, default 30:
|
||||||
|
deploy_timeout: 10
|
||||||
|
|
||||||
|
# Drain timeout
|
||||||
|
#
|
||||||
|
# How long to wait for a container to drain, default 30:
|
||||||
|
drain_timeout: 10
|
||||||
|
|
||||||
|
# Run directory
|
||||||
|
#
|
||||||
|
# Directory to store kamal runtime files in on the host, default `.kamal`:
|
||||||
|
run_directory: /etc/kamal
|
||||||
|
|
||||||
|
# SSH options
|
||||||
|
#
|
||||||
|
# See kamal docs ssh:
|
||||||
|
ssh:
|
||||||
|
...
|
||||||
|
|
||||||
|
# Builder options
|
||||||
|
#
|
||||||
|
# See kamal docs builder:
|
||||||
|
builder:
|
||||||
|
...
|
||||||
|
|
||||||
|
# Accessories
|
||||||
|
#
|
||||||
|
# Additional services to run in Docker, see kamal docs accessory:
|
||||||
|
accessories:
|
||||||
|
...
|
||||||
|
|
||||||
|
# Proxy
|
||||||
|
#
|
||||||
|
# Configuration for kamal-proxy, see kamal docs proxy:
|
||||||
|
proxy:
|
||||||
|
...
|
||||||
|
|
||||||
|
# SSHKit
|
||||||
|
#
|
||||||
|
# See kamal docs sshkit:
|
||||||
|
sshkit:
|
||||||
|
...
|
||||||
|
|
||||||
|
# Boot options
|
||||||
|
#
|
||||||
|
# See kamal docs boot:
|
||||||
|
boot:
|
||||||
|
...
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
#
|
||||||
|
# Docker logging configuration, see kamal docs logging:
|
||||||
|
logging:
|
||||||
|
...
|
||||||
|
|
||||||
|
# Aliases
|
||||||
|
#
|
||||||
|
# Alias configuration, see kamal docs alias:
|
||||||
|
aliases:
|
||||||
|
...
|
||||||
85
lib/kamal/configuration/docs/env.yml
Normal file
85
lib/kamal/configuration/docs/env.yml
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
# Environment variables
|
||||||
|
#
|
||||||
|
# Environment variables can be set directly in the Kamal configuration or
|
||||||
|
# read from `.kamal/secrets`.
|
||||||
|
|
||||||
|
# Reading environment variables from the configuration
|
||||||
|
#
|
||||||
|
# Environment variables can be set directly in the configuration file.
|
||||||
|
#
|
||||||
|
# These are passed to the `docker run` command when deploying.
|
||||||
|
env:
|
||||||
|
DATABASE_HOST: mysql-db1
|
||||||
|
DATABASE_PORT: 3306
|
||||||
|
|
||||||
|
# Secrets
|
||||||
|
#
|
||||||
|
# Kamal uses dotenv to automatically load environment variables set in the `.kamal/secrets` file.
|
||||||
|
#
|
||||||
|
# If you are using destinations, secrets will instead be read from `.kamal/secrets.<DESTINATION>` if
|
||||||
|
# it exists.
|
||||||
|
#
|
||||||
|
# Common secrets across all destinations can be set in `.kamal/secrets-common`.
|
||||||
|
#
|
||||||
|
# This file can be used to set variables like `KAMAL_REGISTRY_PASSWORD` or database passwords.
|
||||||
|
# You can use variable or command substitution in the secrets file.
|
||||||
|
#
|
||||||
|
# ```shell
|
||||||
|
# KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD
|
||||||
|
# RAILS_MASTER_KEY=$(cat config/master.key)
|
||||||
|
# ```
|
||||||
|
#
|
||||||
|
# You can also use [secret helpers](../../commands/secrets) for some common password managers.
|
||||||
|
#
|
||||||
|
# ```shell
|
||||||
|
# SECRETS=$(kamal secrets fetch ...)
|
||||||
|
#
|
||||||
|
# REGISTRY_PASSWORD=$(kamal secrets extract REGISTRY_PASSWORD $SECRETS)
|
||||||
|
# DB_PASSWORD=$(kamal secrets extract DB_PASSWORD $SECRETS)
|
||||||
|
# ```
|
||||||
|
#
|
||||||
|
# If you store secrets directly in `.kamal/secrets`, ensure that it is not checked into version control.
|
||||||
|
#
|
||||||
|
# To pass the secrets, you should list them under the `secret` key. When you do this, the
|
||||||
|
# other variables need to be moved under the `clear` key.
|
||||||
|
#
|
||||||
|
# Unlike clear values, secrets are not passed directly to the container
|
||||||
|
# but are stored in an env file on the host:
|
||||||
|
env:
|
||||||
|
clear:
|
||||||
|
DB_USER: app
|
||||||
|
secret:
|
||||||
|
- DB_PASSWORD
|
||||||
|
|
||||||
|
# Tags
|
||||||
|
#
|
||||||
|
# Tags are used to add extra env variables to specific hosts.
|
||||||
|
# See kamal docs servers for how to tag hosts.
|
||||||
|
#
|
||||||
|
# Tags are only allowed in the top-level env configuration (i.e., not under a role-specific env).
|
||||||
|
#
|
||||||
|
# The env variables can be specified with secret and clear values as explained above.
|
||||||
|
env:
|
||||||
|
tags:
|
||||||
|
<tag1>:
|
||||||
|
MYSQL_USER: monitoring
|
||||||
|
<tag2>:
|
||||||
|
clear:
|
||||||
|
MYSQL_USER: readonly
|
||||||
|
secret:
|
||||||
|
- MYSQL_PASSWORD
|
||||||
|
|
||||||
|
# Example configuration
|
||||||
|
env:
|
||||||
|
clear:
|
||||||
|
MYSQL_USER: app
|
||||||
|
secret:
|
||||||
|
- MYSQL_PASSWORD
|
||||||
|
tags:
|
||||||
|
monitoring:
|
||||||
|
MYSQL_USER: monitoring
|
||||||
|
replica:
|
||||||
|
clear:
|
||||||
|
MYSQL_USER: readonly
|
||||||
|
secret:
|
||||||
|
- READONLY_PASSWORD
|
||||||
21
lib/kamal/configuration/docs/logging.yml
Normal file
21
lib/kamal/configuration/docs/logging.yml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Custom logging configuration
|
||||||
|
#
|
||||||
|
# Set these to control the Docker logging driver and options.
|
||||||
|
|
||||||
|
# Logging settings
|
||||||
|
#
|
||||||
|
# These go under the logging key in the configuration file.
|
||||||
|
#
|
||||||
|
# This can be specified at the root level or for a specific role.
|
||||||
|
logging:
|
||||||
|
|
||||||
|
# Driver
|
||||||
|
#
|
||||||
|
# The logging driver to use, passed to Docker via `--log-driver`:
|
||||||
|
driver: json-file
|
||||||
|
|
||||||
|
# Options
|
||||||
|
#
|
||||||
|
# Any logging options to pass to the driver, passed to Docker via `--log-opt`:
|
||||||
|
options:
|
||||||
|
max-size: 100m
|
||||||
105
lib/kamal/configuration/docs/proxy.yml
Normal file
105
lib/kamal/configuration/docs/proxy.yml
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
# Proxy
|
||||||
|
#
|
||||||
|
# Kamal uses [kamal-proxy](https://github.com/basecamp/kamal-proxy) to provide
|
||||||
|
# gapless deployments. It runs on ports 80 and 443 and forwards requests to the
|
||||||
|
# application container.
|
||||||
|
#
|
||||||
|
# The proxy is configured in the root configuration under `proxy`. These are
|
||||||
|
# options that are set when deploying the application, not when booting the proxy.
|
||||||
|
#
|
||||||
|
# They are application-specific, so they are not shared when multiple applications
|
||||||
|
# run on the same proxy.
|
||||||
|
#
|
||||||
|
# The proxy is enabled by default on the primary role but can be disabled by
|
||||||
|
# setting `proxy: false`.
|
||||||
|
#
|
||||||
|
# It is disabled by default on all other roles but can be enabled by setting
|
||||||
|
# `proxy: true` or providing a proxy configuration.
|
||||||
|
proxy:
|
||||||
|
|
||||||
|
# Hosts
|
||||||
|
#
|
||||||
|
# The hosts that will be used to serve the app. The proxy will only route requests
|
||||||
|
# to this host to your app.
|
||||||
|
#
|
||||||
|
# If no hosts are set, then all requests will be forwarded, except for matching
|
||||||
|
# requests for other apps deployed on that server that do have a host set.
|
||||||
|
#
|
||||||
|
# Specify one of `host` or `hosts`.
|
||||||
|
host: foo.example.com
|
||||||
|
hosts:
|
||||||
|
- foo.example.com
|
||||||
|
- bar.example.com
|
||||||
|
|
||||||
|
# App port
|
||||||
|
#
|
||||||
|
# The port the application container is exposed on.
|
||||||
|
#
|
||||||
|
# Defaults to 80:
|
||||||
|
app_port: 3000
|
||||||
|
|
||||||
|
# SSL
|
||||||
|
#
|
||||||
|
# kamal-proxy can provide automatic HTTPS for your application via Let's Encrypt.
|
||||||
|
#
|
||||||
|
# This requires that we are deploying to one server and the host option is set.
|
||||||
|
# The host value must point to the server we are deploying to, and port 443 must be
|
||||||
|
# open for the Let's Encrypt challenge to succeed.
|
||||||
|
#
|
||||||
|
# Defaults to `false`:
|
||||||
|
ssl: true
|
||||||
|
|
||||||
|
# Response timeout
|
||||||
|
#
|
||||||
|
# How long to wait for requests to complete before timing out, defaults to 30 seconds:
|
||||||
|
response_timeout: 10
|
||||||
|
|
||||||
|
# Healthcheck
|
||||||
|
#
|
||||||
|
# When deploying, the proxy will by default hit `/up` once every second until we hit
|
||||||
|
# the deploy timeout, with a 5-second timeout for each request.
|
||||||
|
#
|
||||||
|
# Once the app is up, the proxy will stop hitting the healthcheck endpoint.
|
||||||
|
healthcheck:
|
||||||
|
interval: 3
|
||||||
|
path: /health
|
||||||
|
timeout: 3
|
||||||
|
|
||||||
|
# Buffering
|
||||||
|
#
|
||||||
|
# Whether to buffer request and response bodies in the proxy.
|
||||||
|
#
|
||||||
|
# By default, buffering is enabled with a max request body size of 1GB and no limit
|
||||||
|
# for response size.
|
||||||
|
#
|
||||||
|
# You can also set the memory limit for buffering, which defaults to 1MB; anything
|
||||||
|
# larger than that is written to disk.
|
||||||
|
buffering:
|
||||||
|
requests: true
|
||||||
|
responses: true
|
||||||
|
max_request_body: 40_000_000
|
||||||
|
max_response_body: 0
|
||||||
|
memory: 2_000_000
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
#
|
||||||
|
# Configure request logging for the proxy.
|
||||||
|
# You can specify request and response headers to log.
|
||||||
|
# By default, `Cache-Control`, `Last-Modified`, and `User-Agent` request headers are logged:
|
||||||
|
logging:
|
||||||
|
request_headers:
|
||||||
|
- Cache-Control
|
||||||
|
- X-Forwarded-Proto
|
||||||
|
response_headers:
|
||||||
|
- X-Request-ID
|
||||||
|
- X-Request-Start
|
||||||
|
|
||||||
|
# Forward headers
|
||||||
|
#
|
||||||
|
# Whether to forward the `X-Forwarded-For` and `X-Forwarded-Proto` headers.
|
||||||
|
#
|
||||||
|
# If you are behind a trusted proxy, you can set this to `true` to forward the headers.
|
||||||
|
#
|
||||||
|
# By default, kamal-proxy will not forward the headers if the `ssl` option is set to `true`, and
|
||||||
|
# will forward them if it is set to `false`.
|
||||||
|
forward_headers: true
|
||||||
52
lib/kamal/configuration/docs/registry.yml
Normal file
52
lib/kamal/configuration/docs/registry.yml
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# Registry
|
||||||
|
#
|
||||||
|
# The default registry is Docker Hub, but you can change it using `registry/server`.
|
||||||
|
#
|
||||||
|
# A reference to a secret (in this case, `DOCKER_REGISTRY_TOKEN`) will look up the secret
|
||||||
|
# in the local environment:
|
||||||
|
registry:
|
||||||
|
server: registry.digitalocean.com
|
||||||
|
username:
|
||||||
|
- DOCKER_REGISTRY_TOKEN
|
||||||
|
password:
|
||||||
|
- DOCKER_REGISTRY_TOKEN
|
||||||
|
|
||||||
|
# Using AWS ECR as the container registry
|
||||||
|
#
|
||||||
|
# You will need to have the AWS CLI installed locally for this to work.
|
||||||
|
# AWS ECR’s access token is only valid for 12 hours. In order to avoid having to manually regenerate the token every time, you can use ERB in the `deploy.yml` file to shell out to the AWS CLI command and obtain the token:
|
||||||
|
registry:
|
||||||
|
server: <your aws account id>.dkr.ecr.<your aws region id>.amazonaws.com
|
||||||
|
username: AWS
|
||||||
|
password: <%= %x(aws ecr get-login-password) %>
|
||||||
|
|
||||||
|
# Using GCP Artifact Registry as the container registry
|
||||||
|
#
|
||||||
|
# To sign into Artifact Registry, you need to
|
||||||
|
# [create a service account](https://cloud.google.com/iam/docs/service-accounts-create#creating)
|
||||||
|
# and [set up roles and permissions](https://cloud.google.com/artifact-registry/docs/access-control#permissions).
|
||||||
|
# Normally, assigning the `roles/artifactregistry.writer` role should be sufficient.
|
||||||
|
#
|
||||||
|
# Once the service account is ready, you need to generate and download a JSON key and base64 encode it:
|
||||||
|
#
|
||||||
|
# ```shell
|
||||||
|
# base64 -i /path/to/key.json | tr -d "\\n"
|
||||||
|
# ```
|
||||||
|
#
|
||||||
|
# You'll then need to set the `KAMAL_REGISTRY_PASSWORD` secret to that value.
|
||||||
|
#
|
||||||
|
# Use the environment variable as the password along with `_json_key_base64` as the username.
|
||||||
|
# Here’s the final configuration:
|
||||||
|
registry:
|
||||||
|
server: <your registry region>-docker.pkg.dev
|
||||||
|
username: _json_key_base64
|
||||||
|
password:
|
||||||
|
- KAMAL_REGISTRY_PASSWORD
|
||||||
|
|
||||||
|
# Validating the configuration
|
||||||
|
#
|
||||||
|
# You can validate the configuration by running:
|
||||||
|
#
|
||||||
|
# ```shell
|
||||||
|
# kamal registry login
|
||||||
|
# ```
|
||||||
53
lib/kamal/configuration/docs/role.yml
Normal file
53
lib/kamal/configuration/docs/role.yml
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# Roles
|
||||||
|
#
|
||||||
|
# Roles are used to configure different types of servers in the deployment.
|
||||||
|
# The most common use for this is to run web servers and job servers.
|
||||||
|
#
|
||||||
|
# Kamal expects there to be a `web` role, unless you set a different `primary_role`
|
||||||
|
# in the root configuration.
|
||||||
|
|
||||||
|
# Role configuration
|
||||||
|
#
|
||||||
|
# Roles are specified under the servers key:
|
||||||
|
servers:
|
||||||
|
|
||||||
|
# Simple role configuration
|
||||||
|
#
|
||||||
|
# This can be a list of hosts if you don't need custom configuration for the role.
|
||||||
|
#
|
||||||
|
# You can set tags on the hosts for custom env variables (see kamal docs env):
|
||||||
|
web:
|
||||||
|
- 172.1.0.1
|
||||||
|
- 172.1.0.2: experiment1
|
||||||
|
- 172.1.0.2: [ experiment1, experiment2 ]
|
||||||
|
|
||||||
|
# Custom role configuration
|
||||||
|
#
|
||||||
|
# When there are other options to set, the list of hosts goes under the `hosts` key.
|
||||||
|
#
|
||||||
|
# By default, only the primary role uses a proxy.
|
||||||
|
#
|
||||||
|
# For other roles, you can set it to `proxy: true` to enable it and inherit the root proxy
|
||||||
|
# configuration or provide a map of options to override the root configuration.
|
||||||
|
#
|
||||||
|
# For the primary role, you can set `proxy: false` to disable the proxy.
|
||||||
|
#
|
||||||
|
# You can also set a custom `cmd` to run in the container and overwrite other settings
|
||||||
|
# from the root configuration.
|
||||||
|
workers:
|
||||||
|
hosts:
|
||||||
|
- 172.1.0.3
|
||||||
|
- 172.1.0.4: experiment1
|
||||||
|
cmd: "bin/jobs"
|
||||||
|
options:
|
||||||
|
memory: 2g
|
||||||
|
cpus: 4
|
||||||
|
logging:
|
||||||
|
...
|
||||||
|
proxy:
|
||||||
|
...
|
||||||
|
labels:
|
||||||
|
my-label: workers
|
||||||
|
env:
|
||||||
|
...
|
||||||
|
asset_path: /public
|
||||||
27
lib/kamal/configuration/docs/servers.yml
Normal file
27
lib/kamal/configuration/docs/servers.yml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# Servers
|
||||||
|
#
|
||||||
|
# Servers are split into different roles, with each role having its own configuration.
|
||||||
|
#
|
||||||
|
# For simpler deployments, though, where all servers are identical, you can just specify a list of servers.
|
||||||
|
# They will be implicitly assigned to the `web` role.
|
||||||
|
servers:
|
||||||
|
- 172.0.0.1
|
||||||
|
- 172.0.0.2
|
||||||
|
- 172.0.0.3
|
||||||
|
|
||||||
|
# Tagging servers
|
||||||
|
#
|
||||||
|
# Servers can be tagged, with the tags used to add custom env variables (see kamal docs env).
|
||||||
|
servers:
|
||||||
|
- 172.0.0.1
|
||||||
|
- 172.0.0.2: experiments
|
||||||
|
- 172.0.0.3: [ experiments, three ]
|
||||||
|
|
||||||
|
# Roles
|
||||||
|
#
|
||||||
|
# For more complex deployments (e.g., if you are running job hosts), you can specify roles and configure each separately (see kamal docs role):
|
||||||
|
servers:
|
||||||
|
web:
|
||||||
|
...
|
||||||
|
workers:
|
||||||
|
...
|
||||||
70
lib/kamal/configuration/docs/ssh.yml
Normal file
70
lib/kamal/configuration/docs/ssh.yml
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
# SSH configuration
|
||||||
|
#
|
||||||
|
# Kamal uses SSH to connect and run commands on your hosts.
|
||||||
|
# By default, it will attempt to connect to the root user on port 22.
|
||||||
|
#
|
||||||
|
# If you are using a non-root user, you may need to bootstrap your servers manually before using them with Kamal. On Ubuntu, you’d do:
|
||||||
|
#
|
||||||
|
# ```shell
|
||||||
|
# sudo apt update
|
||||||
|
# sudo apt upgrade -y
|
||||||
|
# sudo apt install -y docker.io curl git
|
||||||
|
# sudo usermod -a -G docker app
|
||||||
|
# ```
|
||||||
|
|
||||||
|
# SSH options
|
||||||
|
#
|
||||||
|
# The options are specified under the ssh key in the configuration file.
|
||||||
|
ssh:
|
||||||
|
|
||||||
|
# The SSH user
|
||||||
|
#
|
||||||
|
# Defaults to `root`:
|
||||||
|
user: app
|
||||||
|
|
||||||
|
# The SSH port
|
||||||
|
#
|
||||||
|
# Defaults to 22:
|
||||||
|
port: "2222"
|
||||||
|
|
||||||
|
# Proxy host
|
||||||
|
#
|
||||||
|
# Specified in the form <host> or <user>@<host>:
|
||||||
|
proxy: root@proxy-host
|
||||||
|
|
||||||
|
# Proxy command
|
||||||
|
#
|
||||||
|
# A custom proxy command, required for older versions of SSH:
|
||||||
|
proxy_command: "ssh -W %h:%p user@proxy"
|
||||||
|
|
||||||
|
# Log level
|
||||||
|
#
|
||||||
|
# Defaults to `fatal`. Set this to `debug` if you are having SSH connection issues.
|
||||||
|
log_level: debug
|
||||||
|
|
||||||
|
# Keys only
|
||||||
|
#
|
||||||
|
# Set to `true` to use only private keys from the `keys` and `key_data` parameters,
|
||||||
|
# even if ssh-agent offers more identities. This option is intended for
|
||||||
|
# situations where ssh-agent offers many different identities or you
|
||||||
|
# need to overwrite all identities and force a single one.
|
||||||
|
keys_only: false
|
||||||
|
|
||||||
|
# Keys
|
||||||
|
#
|
||||||
|
# An array of file names of private keys to use for public key
|
||||||
|
# and host-based authentication:
|
||||||
|
keys: [ "~/.ssh/id.pem" ]
|
||||||
|
|
||||||
|
# Key data
|
||||||
|
#
|
||||||
|
# An array of strings, with each element of the array being
|
||||||
|
# a raw private key in PEM format.
|
||||||
|
key_data: [ "-----BEGIN OPENSSH PRIVATE KEY-----" ]
|
||||||
|
|
||||||
|
# Config
|
||||||
|
#
|
||||||
|
# Set to true to load the default OpenSSH config files (~/.ssh/config,
|
||||||
|
# /etc/ssh_config), to false ignore config files, or to a file path
|
||||||
|
# (or array of paths) to load specific configuration. Defaults to true.
|
||||||
|
config: true
|
||||||
23
lib/kamal/configuration/docs/sshkit.yml
Normal file
23
lib/kamal/configuration/docs/sshkit.yml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# SSHKit
|
||||||
|
#
|
||||||
|
# [SSHKit](https://github.com/capistrano/sshkit) is the SSH toolkit used by Kamal.
|
||||||
|
#
|
||||||
|
# The default, settings should be sufficient for most use cases, but
|
||||||
|
# when connecting to a large number of hosts, you may need to adjust.
|
||||||
|
|
||||||
|
# SSHKit options
|
||||||
|
#
|
||||||
|
# The options are specified under the sshkit key in the configuration file.
|
||||||
|
sshkit:
|
||||||
|
|
||||||
|
# Max concurrent starts
|
||||||
|
#
|
||||||
|
# Creating SSH connections concurrently can be an issue when deploying to many servers.
|
||||||
|
# By default, Kamal will limit concurrent connection starts to 30 at a time.
|
||||||
|
max_concurrent_starts: 10
|
||||||
|
|
||||||
|
# Pool idle timeout
|
||||||
|
#
|
||||||
|
# Kamal sets a long idle timeout of 900 seconds on connections to try to avoid
|
||||||
|
# re-connection storms after an idle period, such as building an image or waiting for CI.
|
||||||
|
pool_idle_timeout: 300
|
||||||
29
lib/kamal/configuration/env.rb
Normal file
29
lib/kamal/configuration/env.rb
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
class Kamal::Configuration::Env
|
||||||
|
include Kamal::Configuration::Validation
|
||||||
|
|
||||||
|
attr_reader :context, :secrets
|
||||||
|
attr_reader :clear, :secret_keys
|
||||||
|
delegate :argumentize, to: Kamal::Utils
|
||||||
|
|
||||||
|
def initialize(config:, secrets:, context: "env")
|
||||||
|
@clear = config.fetch("clear", config.key?("secret") || config.key?("tags") ? {} : config)
|
||||||
|
@secrets = secrets
|
||||||
|
@secret_keys = config.fetch("secret", [])
|
||||||
|
@context = context
|
||||||
|
validate! config, context: context, with: Kamal::Configuration::Validator::Env
|
||||||
|
end
|
||||||
|
|
||||||
|
def clear_args
|
||||||
|
argumentize("--env", clear)
|
||||||
|
end
|
||||||
|
|
||||||
|
def secrets_io
|
||||||
|
Kamal::EnvFile.new(secret_keys.to_h { |key| [ key, secrets[key] ] }).to_io
|
||||||
|
end
|
||||||
|
|
||||||
|
def merge(other)
|
||||||
|
self.class.new \
|
||||||
|
config: { "clear" => clear.merge(other.clear), "secret" => secret_keys | other.secret_keys },
|
||||||
|
secrets: secrets
|
||||||
|
end
|
||||||
|
end
|
||||||
13
lib/kamal/configuration/env/tag.rb
vendored
Normal file
13
lib/kamal/configuration/env/tag.rb
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
class Kamal::Configuration::Env::Tag
|
||||||
|
attr_reader :name, :config, :secrets
|
||||||
|
|
||||||
|
def initialize(name, config:, secrets:)
|
||||||
|
@name = name
|
||||||
|
@config = config
|
||||||
|
@secrets = secrets
|
||||||
|
end
|
||||||
|
|
||||||
|
def env
|
||||||
|
Kamal::Configuration::Env.new(config: config, secrets: secrets)
|
||||||
|
end
|
||||||
|
end
|
||||||
33
lib/kamal/configuration/logging.rb
Normal file
33
lib/kamal/configuration/logging.rb
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
class Kamal::Configuration::Logging
|
||||||
|
delegate :optionize, :argumentize, to: Kamal::Utils
|
||||||
|
|
||||||
|
include Kamal::Configuration::Validation
|
||||||
|
|
||||||
|
attr_reader :logging_config
|
||||||
|
|
||||||
|
def initialize(logging_config:, context: "logging")
|
||||||
|
@logging_config = logging_config || {}
|
||||||
|
validate! @logging_config, context: context
|
||||||
|
end
|
||||||
|
|
||||||
|
def driver
|
||||||
|
logging_config["driver"]
|
||||||
|
end
|
||||||
|
|
||||||
|
def options
|
||||||
|
logging_config.fetch("options", {})
|
||||||
|
end
|
||||||
|
|
||||||
|
def merge(other)
|
||||||
|
self.class.new logging_config: logging_config.deep_merge(other.logging_config)
|
||||||
|
end
|
||||||
|
|
||||||
|
def args
|
||||||
|
if driver.present? || options.present?
|
||||||
|
optionize({ "log-driver" => driver }.compact) +
|
||||||
|
argumentize("--log-opt", options)
|
||||||
|
else
|
||||||
|
argumentize("--log-opt", { "max-size" => "10m" })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
62
lib/kamal/configuration/proxy.rb
Normal file
62
lib/kamal/configuration/proxy.rb
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
class Kamal::Configuration::Proxy
|
||||||
|
include Kamal::Configuration::Validation
|
||||||
|
|
||||||
|
DEFAULT_LOG_REQUEST_HEADERS = [ "Cache-Control", "Last-Modified", "User-Agent" ]
|
||||||
|
CONTAINER_NAME = "kamal-proxy"
|
||||||
|
|
||||||
|
delegate :argumentize, :optionize, to: Kamal::Utils
|
||||||
|
|
||||||
|
attr_reader :config, :proxy_config
|
||||||
|
|
||||||
|
def initialize(config:, proxy_config:, context: "proxy")
|
||||||
|
@config = config
|
||||||
|
@proxy_config = proxy_config
|
||||||
|
validate! @proxy_config, with: Kamal::Configuration::Validator::Proxy, context: context
|
||||||
|
end
|
||||||
|
|
||||||
|
def app_port
|
||||||
|
proxy_config.fetch("app_port", 80)
|
||||||
|
end
|
||||||
|
|
||||||
|
def ssl?
|
||||||
|
proxy_config.fetch("ssl", false)
|
||||||
|
end
|
||||||
|
|
||||||
|
def hosts
|
||||||
|
proxy_config["hosts"] || proxy_config["host"]&.split(",") || []
|
||||||
|
end
|
||||||
|
|
||||||
|
def deploy_options
|
||||||
|
{
|
||||||
|
host: hosts,
|
||||||
|
tls: proxy_config["ssl"],
|
||||||
|
"deploy-timeout": seconds_duration(config.deploy_timeout),
|
||||||
|
"drain-timeout": seconds_duration(config.drain_timeout),
|
||||||
|
"health-check-interval": seconds_duration(proxy_config.dig("healthcheck", "interval")),
|
||||||
|
"health-check-timeout": seconds_duration(proxy_config.dig("healthcheck", "timeout")),
|
||||||
|
"health-check-path": proxy_config.dig("healthcheck", "path"),
|
||||||
|
"target-timeout": seconds_duration(proxy_config["response_timeout"]),
|
||||||
|
"buffer-requests": proxy_config.fetch("buffering", { "requests": true }).fetch("requests", true),
|
||||||
|
"buffer-responses": proxy_config.fetch("buffering", { "responses": true }).fetch("responses", true),
|
||||||
|
"buffer-memory": proxy_config.dig("buffering", "memory"),
|
||||||
|
"max-request-body": proxy_config.dig("buffering", "max_request_body"),
|
||||||
|
"max-response-body": proxy_config.dig("buffering", "max_response_body"),
|
||||||
|
"forward-headers": proxy_config.dig("forward_headers"),
|
||||||
|
"log-request-header": proxy_config.dig("logging", "request_headers") || DEFAULT_LOG_REQUEST_HEADERS,
|
||||||
|
"log-response-header": proxy_config.dig("logging", "response_headers")
|
||||||
|
}.compact
|
||||||
|
end
|
||||||
|
|
||||||
|
def deploy_command_args(target:)
|
||||||
|
optionize ({ target: "#{target}:#{app_port}" }).merge(deploy_options), with: "="
|
||||||
|
end
|
||||||
|
|
||||||
|
def merge(other)
|
||||||
|
self.class.new config: config, proxy_config: proxy_config.deep_merge(other.proxy_config)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def seconds_duration(value)
|
||||||
|
value ? "#{value}s" : nil
|
||||||
|
end
|
||||||
|
end
|
||||||
32
lib/kamal/configuration/registry.rb
Normal file
32
lib/kamal/configuration/registry.rb
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
class Kamal::Configuration::Registry
|
||||||
|
include Kamal::Configuration::Validation
|
||||||
|
|
||||||
|
attr_reader :registry_config, :secrets
|
||||||
|
|
||||||
|
def initialize(config:)
|
||||||
|
@registry_config = config.raw_config.registry || {}
|
||||||
|
@secrets = config.secrets
|
||||||
|
validate! registry_config, with: Kamal::Configuration::Validator::Registry
|
||||||
|
end
|
||||||
|
|
||||||
|
def server
|
||||||
|
registry_config["server"]
|
||||||
|
end
|
||||||
|
|
||||||
|
def username
|
||||||
|
lookup("username")
|
||||||
|
end
|
||||||
|
|
||||||
|
def password
|
||||||
|
lookup("password")
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def lookup(key)
|
||||||
|
if registry_config[key].is_a?(Array)
|
||||||
|
secrets[registry_config[key].first]
|
||||||
|
else
|
||||||
|
registry_config[key]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,10 +1,30 @@
|
|||||||
class Kamal::Configuration::Role
|
class Kamal::Configuration::Role
|
||||||
delegate :argumentize, :argumentize_env_with_secrets, :optionize, to: Kamal::Utils
|
include Kamal::Configuration::Validation
|
||||||
|
|
||||||
attr_accessor :name
|
delegate :argumentize, :optionize, to: Kamal::Utils
|
||||||
|
|
||||||
|
attr_reader :name, :config, :specialized_env, :specialized_logging, :specialized_proxy
|
||||||
|
|
||||||
|
alias to_s name
|
||||||
|
|
||||||
def initialize(name, config:)
|
def initialize(name, config:)
|
||||||
@name, @config = name.inquiry, config
|
@name, @config = name.inquiry, config
|
||||||
|
validate! \
|
||||||
|
specializations,
|
||||||
|
example: validation_yml["servers"]["workers"],
|
||||||
|
context: "servers/#{name}",
|
||||||
|
with: Kamal::Configuration::Validator::Role
|
||||||
|
|
||||||
|
@specialized_env = Kamal::Configuration::Env.new \
|
||||||
|
config: specializations.fetch("env", {}),
|
||||||
|
secrets: config.secrets,
|
||||||
|
context: "servers/#{name}/env"
|
||||||
|
|
||||||
|
@specialized_logging = Kamal::Configuration::Logging.new \
|
||||||
|
logging_config: specializations.fetch("logging", {}),
|
||||||
|
context: "servers/#{name}/logging"
|
||||||
|
|
||||||
|
initialize_specialized_proxy
|
||||||
end
|
end
|
||||||
|
|
||||||
def primary_host
|
def primary_host
|
||||||
@@ -12,49 +32,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,87 +51,170 @@ 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
|
||||||
|
logging.args
|
||||||
|
end
|
||||||
|
|
||||||
|
def logging
|
||||||
|
@logging ||= config.logging.merge(specialized_logging)
|
||||||
|
end
|
||||||
|
|
||||||
|
def proxy
|
||||||
|
@proxy ||= config.proxy.merge(specialized_proxy) if running_proxy?
|
||||||
|
end
|
||||||
|
|
||||||
|
def running_proxy?
|
||||||
|
@running_proxy
|
||||||
|
end
|
||||||
|
|
||||||
|
def ssl?
|
||||||
|
running_proxy? && proxy.ssl?
|
||||||
|
end
|
||||||
|
|
||||||
|
def stop_args
|
||||||
|
# When deploying with the proxy, kamal-proxy will drain request before returning so we don't need to wait.
|
||||||
|
timeout = running_proxy? ? nil : config.drain_timeout
|
||||||
|
|
||||||
|
[ *argumentize("-t", timeout) ]
|
||||||
|
end
|
||||||
|
|
||||||
|
def env(host)
|
||||||
|
@envs ||= {}
|
||||||
|
@envs[host] ||= [ config.env, specialized_env, *env_tags(host).map(&:env) ].reduce(:merge)
|
||||||
|
end
|
||||||
|
|
||||||
|
def env_args(host)
|
||||||
|
[ *env(host).clear_args, *argumentize("--env-file", secrets_path) ]
|
||||||
|
end
|
||||||
|
|
||||||
|
def env_directory
|
||||||
|
File.join(config.env_directory, "roles")
|
||||||
|
end
|
||||||
|
|
||||||
|
def secrets_io(host)
|
||||||
|
env(host).secrets_io
|
||||||
|
end
|
||||||
|
|
||||||
|
def secrets_path
|
||||||
|
File.join(config.env_directory, "roles", "#{name}.env")
|
||||||
|
end
|
||||||
|
|
||||||
|
def asset_volume_args
|
||||||
|
asset_volume&.docker_args
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
def primary?
|
||||||
|
name == @config.primary_role_name
|
||||||
|
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 = config.version)
|
||||||
|
if assets?
|
||||||
|
Kamal::Configuration::Volume.new \
|
||||||
|
host_path: asset_volume_directory(version), container_path: asset_path
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def asset_extracted_directory(version = config.version)
|
||||||
|
File.join config.assets_directory, "extracted", [ name, version ].join("-")
|
||||||
|
end
|
||||||
|
|
||||||
|
def asset_volume_directory(version = config.version)
|
||||||
|
File.join config.assets_directory, "volumes", [ name, version ].join("-")
|
||||||
|
end
|
||||||
|
|
||||||
|
def ensure_one_host_for_ssl
|
||||||
|
if running_proxy? && proxy.ssl? && hosts.size > 1
|
||||||
|
raise Kamal::ConfigurationError, "SSL is only supported on a single server, found #{hosts.size} servers for role #{name}"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
attr_accessor :config
|
def initialize_specialized_proxy
|
||||||
|
proxy_specializations = specializations["proxy"]
|
||||||
|
|
||||||
|
if primary?
|
||||||
|
# only false means no proxy for non-primary roles
|
||||||
|
@running_proxy = proxy_specializations != false
|
||||||
|
else
|
||||||
|
# false and nil both mean no proxy for non-primary roles
|
||||||
|
@running_proxy = !!proxy_specializations
|
||||||
|
end
|
||||||
|
|
||||||
|
if running_proxy?
|
||||||
|
proxy_config = proxy_specializations == true || proxy_specializations.nil? ? {} : proxy_specializations
|
||||||
|
|
||||||
|
@specialized_proxy = Kamal::Configuration::Proxy.new \
|
||||||
|
config: config,
|
||||||
|
proxy_config: proxy_config,
|
||||||
|
context: "servers/#{name}/proxy"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def tagged_hosts
|
||||||
|
{}.tap do |tagged_hosts|
|
||||||
|
extract_hosts_from_config.map do |host_config|
|
||||||
|
if host_config.is_a?(Hash)
|
||||||
|
host, tags = host_config.first
|
||||||
|
tagged_hosts[host] = Array(tags)
|
||||||
|
elsif host_config.is_a?(String)
|
||||||
|
tagged_hosts[host_config] = []
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def extract_hosts_from_config
|
def extract_hosts_from_config
|
||||||
if config.servers.is_a?(Array)
|
if config.raw_config.servers.is_a?(Array)
|
||||||
config.servers
|
config.raw_config.servers
|
||||||
else
|
else
|
||||||
servers = config.servers[name]
|
servers = config.raw_config.servers[name]
|
||||||
servers.is_a?(Array) ? servers : Array(servers["hosts"])
|
servers.is_a?(Array) ? servers : Array(servers["hosts"])
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
||||||
def traefik_labels
|
def specializations
|
||||||
if running_traefik?
|
if config.raw_config.servers.is_a?(Array) || config.raw_config.servers[name].is_a?(Array)
|
||||||
{
|
|
||||||
# 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
|
|
||||||
{}
|
{}
|
||||||
|
else
|
||||||
|
config.raw_config.servers[name]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def traefik_service
|
|
||||||
[ config.service, name, config.destination ].compact.join("-")
|
|
||||||
end
|
|
||||||
|
|
||||||
def custom_labels
|
def custom_labels
|
||||||
Hash.new.tap do |labels|
|
Hash.new.tap do |labels|
|
||||||
labels.merge!(config.labels) if config.labels.present?
|
labels.merge!(config.labels) if config.labels.present?
|
||||||
labels.merge!(specializations["labels"]) if specializations["labels"].present?
|
labels.merge!(specializations["labels"]) if specializations["labels"].present?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def specializations
|
|
||||||
if config.servers.is_a?(Array) || config.servers[name].is_a?(Array)
|
|
||||||
{ }
|
|
||||||
else
|
|
||||||
config.servers[name].except("hosts")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def specialized_env
|
|
||||||
specializations["env"] || {}
|
|
||||||
end
|
|
||||||
|
|
||||||
def merged_env
|
|
||||||
config.env&.merge(specialized_env) || {}
|
|
||||||
end
|
|
||||||
|
|
||||||
# Secrets are stored in an array, which won't merge by default, so have to do it by hand.
|
|
||||||
def merged_env_with_secrets
|
|
||||||
merged_env.tap do |new_env|
|
|
||||||
new_env["secret"] = Array(config.env["secret"]) + Array(specialized_env["secret"])
|
|
||||||
|
|
||||||
# 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
|
||||||
|
|||||||
18
lib/kamal/configuration/servers.rb
Normal file
18
lib/kamal/configuration/servers.rb
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
class Kamal::Configuration::Servers
|
||||||
|
include Kamal::Configuration::Validation
|
||||||
|
|
||||||
|
attr_reader :config, :servers_config, :roles
|
||||||
|
|
||||||
|
def initialize(config:)
|
||||||
|
@config = config
|
||||||
|
@servers_config = config.raw_config.servers
|
||||||
|
validate! servers_config, with: Kamal::Configuration::Validator::Servers
|
||||||
|
|
||||||
|
@roles = role_names.map { |role_name| Kamal::Configuration::Role.new role_name, config: config }
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def role_names
|
||||||
|
servers_config.is_a?(Array) ? [ "web" ] : servers_config.keys.sort
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,24 +1,45 @@
|
|||||||
class Kamal::Configuration::Ssh
|
class Kamal::Configuration::Ssh
|
||||||
LOGGER = ::Logger.new(STDERR)
|
LOGGER = ::Logger.new(STDERR)
|
||||||
|
|
||||||
|
include Kamal::Configuration::Validation
|
||||||
|
|
||||||
|
attr_reader :ssh_config
|
||||||
|
|
||||||
def initialize(config:)
|
def initialize(config:)
|
||||||
@config = config.raw_config.ssh || {}
|
@ssh_config = config.raw_config.ssh || {}
|
||||||
|
validate! ssh_config
|
||||||
end
|
end
|
||||||
|
|
||||||
def user
|
def user
|
||||||
config.fetch("user", "root")
|
ssh_config.fetch("user", "root")
|
||||||
|
end
|
||||||
|
|
||||||
|
def port
|
||||||
|
ssh_config.fetch("port", 22)
|
||||||
end
|
end
|
||||||
|
|
||||||
def proxy
|
def proxy
|
||||||
if (proxy = config["proxy"])
|
if (proxy = ssh_config["proxy"])
|
||||||
Net::SSH::Proxy::Jump.new(proxy.include?("@") ? proxy : "root@#{proxy}")
|
Net::SSH::Proxy::Jump.new(proxy.include?("@") ? proxy : "root@#{proxy}")
|
||||||
elsif (proxy_command = config["proxy_command"])
|
elsif (proxy_command = ssh_config["proxy_command"])
|
||||||
Net::SSH::Proxy::Command.new(proxy_command)
|
Net::SSH::Proxy::Command.new(proxy_command)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def keys_only
|
||||||
|
ssh_config["keys_only"]
|
||||||
|
end
|
||||||
|
|
||||||
|
def keys
|
||||||
|
ssh_config["keys"]
|
||||||
|
end
|
||||||
|
|
||||||
|
def key_data
|
||||||
|
ssh_config["key_data"]
|
||||||
|
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, keys_only: keys_only, keys: keys, key_data: key_data }.compact
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_h
|
def to_h
|
||||||
@@ -26,13 +47,11 @@ class Kamal::Configuration::Ssh
|
|||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
attr_accessor :config
|
|
||||||
|
|
||||||
def logger
|
def logger
|
||||||
LOGGER.tap { |logger| logger.level = log_level }
|
LOGGER.tap { |logger| logger.level = log_level }
|
||||||
end
|
end
|
||||||
|
|
||||||
def log_level
|
def log_level
|
||||||
config.fetch("log_level", :fatal)
|
ssh_config.fetch("log_level", :fatal)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,20 +1,22 @@
|
|||||||
class Kamal::Configuration::Sshkit
|
class Kamal::Configuration::Sshkit
|
||||||
|
include Kamal::Configuration::Validation
|
||||||
|
|
||||||
|
attr_reader :sshkit_config
|
||||||
|
|
||||||
def initialize(config:)
|
def initialize(config:)
|
||||||
@options = config.raw_config.sshkit || {}
|
@sshkit_config = config.raw_config.sshkit || {}
|
||||||
|
validate! sshkit_config
|
||||||
end
|
end
|
||||||
|
|
||||||
def max_concurrent_starts
|
def max_concurrent_starts
|
||||||
options.fetch("max_concurrent_starts", 30)
|
sshkit_config.fetch("max_concurrent_starts", 30)
|
||||||
end
|
end
|
||||||
|
|
||||||
def pool_idle_timeout
|
def pool_idle_timeout
|
||||||
options.fetch("pool_idle_timeout", 900)
|
sshkit_config.fetch("pool_idle_timeout", 900)
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_h
|
def to_h
|
||||||
options
|
sshkit_config
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
attr_accessor :options
|
|
||||||
end
|
end
|
||||||
|
|||||||
27
lib/kamal/configuration/validation.rb
Normal file
27
lib/kamal/configuration/validation.rb
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
require "yaml"
|
||||||
|
require "active_support/inflector"
|
||||||
|
|
||||||
|
module Kamal::Configuration::Validation
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
class_methods do
|
||||||
|
def validation_doc
|
||||||
|
@validation_doc ||= File.read(File.join(File.dirname(__FILE__), "docs", "#{validation_config_key}.yml"))
|
||||||
|
end
|
||||||
|
|
||||||
|
def validation_config_key
|
||||||
|
@validation_config_key ||= name.demodulize.underscore
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate!(config, example: nil, context: nil, with: Kamal::Configuration::Validator)
|
||||||
|
context ||= self.class.validation_config_key
|
||||||
|
example ||= validation_yml[self.class.validation_config_key]
|
||||||
|
|
||||||
|
with.new(config, example: example, context: context).validate!
|
||||||
|
end
|
||||||
|
|
||||||
|
def validation_yml
|
||||||
|
@validation_yml ||= YAML.load(self.class.validation_doc)
|
||||||
|
end
|
||||||
|
end
|
||||||
171
lib/kamal/configuration/validator.rb
Normal file
171
lib/kamal/configuration/validator.rb
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
class Kamal::Configuration::Validator
|
||||||
|
attr_reader :config, :example, :context
|
||||||
|
|
||||||
|
def initialize(config, example:, context:)
|
||||||
|
@config = config
|
||||||
|
@example = example
|
||||||
|
@context = context
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate!
|
||||||
|
validate_against_example! config, example
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def validate_against_example!(validation_config, example)
|
||||||
|
validate_type! validation_config, example.class
|
||||||
|
|
||||||
|
if example.class == Hash
|
||||||
|
check_unknown_keys! validation_config, example
|
||||||
|
|
||||||
|
validation_config.each do |key, value|
|
||||||
|
next if extension?(key)
|
||||||
|
with_context(key) do
|
||||||
|
example_value = example[key]
|
||||||
|
|
||||||
|
if example_value == "..."
|
||||||
|
unless key.to_s == "proxy" && boolean?(value.class)
|
||||||
|
validate_type! value, *(Array if key == :servers), Hash
|
||||||
|
end
|
||||||
|
elsif key == "hosts"
|
||||||
|
validate_servers! value
|
||||||
|
elsif example_value.is_a?(Array)
|
||||||
|
if key == "arch"
|
||||||
|
validate_array_of_or_type! value, example_value.first.class
|
||||||
|
else
|
||||||
|
validate_array_of! value, example_value.first.class
|
||||||
|
end
|
||||||
|
elsif example_value.is_a?(Hash)
|
||||||
|
case key.to_s
|
||||||
|
when "options", "args"
|
||||||
|
validate_type! value, Hash
|
||||||
|
when "labels"
|
||||||
|
validate_hash_of! value, example_value.first[1].class
|
||||||
|
else
|
||||||
|
validate_against_example! value, example_value
|
||||||
|
end
|
||||||
|
else
|
||||||
|
validate_type! value, example_value.class
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
def valid_type?(value, type)
|
||||||
|
value.is_a?(type) ||
|
||||||
|
(type == String && stringish?(value)) ||
|
||||||
|
(boolean?(type) && boolean?(value.class))
|
||||||
|
end
|
||||||
|
|
||||||
|
def type_description(type)
|
||||||
|
if type == Integer || type == Array
|
||||||
|
"an #{type.name.downcase}"
|
||||||
|
elsif type == TrueClass || type == FalseClass
|
||||||
|
"a boolean"
|
||||||
|
else
|
||||||
|
"a #{type.name.downcase}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def boolean?(type)
|
||||||
|
type == TrueClass || type == FalseClass
|
||||||
|
end
|
||||||
|
|
||||||
|
def stringish?(value)
|
||||||
|
value.is_a?(String) || value.is_a?(Symbol) || value.is_a?(Numeric) || value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_array_of_or_type!(value, type)
|
||||||
|
if value.is_a?(Array)
|
||||||
|
validate_array_of! value, type
|
||||||
|
else
|
||||||
|
validate_type! value, type
|
||||||
|
end
|
||||||
|
rescue Kamal::ConfigurationError
|
||||||
|
type_error(Array, type)
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_array_of!(array, type)
|
||||||
|
validate_type! array, Array
|
||||||
|
|
||||||
|
array.each_with_index do |value, index|
|
||||||
|
with_context(index) do
|
||||||
|
validate_type! value, type
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_hash_of!(hash, type)
|
||||||
|
validate_type! hash, Hash
|
||||||
|
|
||||||
|
hash.each do |key, value|
|
||||||
|
with_context(key) do
|
||||||
|
validate_type! value, type
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_servers!(servers)
|
||||||
|
validate_type! servers, Array
|
||||||
|
|
||||||
|
servers.each_with_index do |server, index|
|
||||||
|
with_context(index) do
|
||||||
|
validate_type! server, String, Hash
|
||||||
|
|
||||||
|
if server.is_a?(Hash)
|
||||||
|
error "multiple hosts found" unless server.size == 1
|
||||||
|
host, tags = server.first
|
||||||
|
|
||||||
|
with_context(host) do
|
||||||
|
validate_type! tags, String, Array
|
||||||
|
validate_array_of! tags, String if tags.is_a?(Array)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_type!(value, *types)
|
||||||
|
type_error(*types) unless types.any? { |type| valid_type?(value, type) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def error(message)
|
||||||
|
raise Kamal::ConfigurationError, "#{error_context}#{message}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def type_error(*expected_types)
|
||||||
|
error "should be #{expected_types.map { |type| type_description(type) }.join(" or ")}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def unknown_keys_error(unknown_keys)
|
||||||
|
error "unknown #{"key".pluralize(unknown_keys.count)}: #{unknown_keys.join(", ")}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def error_context
|
||||||
|
"#{context}: " if context.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def with_context(context)
|
||||||
|
old_context = @context
|
||||||
|
@context = [ @context, context ].select(&:present?).join("/")
|
||||||
|
yield
|
||||||
|
ensure
|
||||||
|
@context = old_context
|
||||||
|
end
|
||||||
|
|
||||||
|
def allow_extensions?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def extension?(key)
|
||||||
|
key.to_s.start_with?("x-")
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_unknown_keys!(config, example)
|
||||||
|
unknown_keys = config.keys - example.keys
|
||||||
|
unknown_keys.reject! { |key| extension?(key) } if allow_extensions?
|
||||||
|
unknown_keys_error unknown_keys if unknown_keys.present?
|
||||||
|
end
|
||||||
|
end
|
||||||
9
lib/kamal/configuration/validator/accessory.rb
Normal file
9
lib/kamal/configuration/validator/accessory.rb
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
class Kamal::Configuration::Validator::Accessory < Kamal::Configuration::Validator
|
||||||
|
def validate!
|
||||||
|
super
|
||||||
|
|
||||||
|
if (config.keys & [ "host", "hosts", "roles" ]).size != 1
|
||||||
|
error "specify one of `host`, `hosts` or `roles`"
|
||||||
|
end
|
||||||
|
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