Compare commits
1759 Commits
Author | SHA1 | Date | |
---|---|---|---|
361e0a0430 | |||
|
8901d63677 | ||
|
eb6d9d31a0 | ||
|
1ad3c204ea | ||
|
2cae6807dc | ||
|
ddaaf50319 | ||
|
c2f6689c9a | ||
|
576486a869 | ||
|
19a4ecff3d | ||
|
11999df81a | ||
|
c901b57333 | ||
|
ed24abea0f | ||
|
0d7bae65b9 | ||
|
b7fd1dbbfc | ||
|
e3ef28ec1a | ||
|
cad0a6d81f | ||
|
60c120a1e5 | ||
|
6d8138009e | ||
|
21f646cec5 | ||
|
e33a793c5d | ||
|
75e2a9f4c2 | ||
|
9e7591987c | ||
|
0d8d8b1247 | ||
|
2cc3e11ac7 | ||
|
befd46b3dd | ||
|
6a268ce65b | ||
|
e2bcfbadf8 | ||
|
d8f398eef9 | ||
|
e013bc4db9 | ||
|
0ca2d41e57 | ||
|
7d9281aef2 | ||
|
f15e0a1682 | ||
|
00dee2fa62 | ||
|
a82f360154 | ||
|
cf2642d6d4 | ||
|
a3a0a69992 | ||
|
2b07be51f3 | ||
|
8a2d3e9674 | ||
|
c834d436ef | ||
|
f25bf71d2c | ||
|
e7b3274b24 | ||
|
afbfe4619b | ||
|
fcb310b15b | ||
|
d625c7500c | ||
|
9a171118e9 | ||
|
560ec9d840 | ||
|
5206c7611c | ||
|
1aba61824d | ||
|
0efc847be8 | ||
|
71a7246940 | ||
|
e3b1e8a837 | ||
|
9563ef4d51 | ||
|
7391719705 | ||
|
c385dbe439 | ||
|
e86a523070 | ||
|
7b28ee32bc | ||
|
1b076cfffc | ||
|
f76d45b826 | ||
|
6cc86a3c82 | ||
|
f1e5b61970 | ||
|
65380646f7 | ||
|
189a8347ed | ||
|
e96e8f6fec | ||
|
38299ab507 | ||
|
f134171855 | ||
|
320204d854 | ||
|
b68199a482 | ||
|
6f4436fd5e | ||
|
0d8cc19698 | ||
|
a3a1ff69e1 | ||
|
5df7e23aa4 | ||
|
35d9691b3f | ||
|
465f649641 | ||
|
d909e7d802 | ||
|
06db6b8502 | ||
|
12261b9082 | ||
|
583ec432e8 | ||
|
8ffbccf7db | ||
|
439fe43a04 | ||
|
7fd9a799b5 | ||
|
7459ab22d1 | ||
|
133a68f3eb | ||
|
3224110583 | ||
|
810488b115 | ||
|
b2276147ad | ||
|
6c1293c63e | ||
|
526d675272 | ||
|
14b2442d40 | ||
|
d6d194d414 | ||
|
0e99f97855 | ||
|
14dc933219 | ||
|
9497f123b4 | ||
|
6feb439d0a | ||
|
e4ca5ee5f5 | ||
|
f21c12f39b | ||
|
6c1e50a914 | ||
|
da064def7a | ||
|
3af3fa5773 | ||
|
005bd55fb2 | ||
|
82a5b4c55d | ||
|
b8e95b2099 | ||
|
8ea50dc029 | ||
|
ec191af874 | ||
|
d98c742aff | ||
|
2529496594 | ||
|
1b6114036a | ||
|
b33fb6c39a | ||
|
0a6826af58 | ||
|
1c7034ff78 | ||
|
7e11698c55 | ||
|
1c4eb31d59 | ||
|
b4b6a4294a | ||
|
4c031a7c05 | ||
|
997a262b6c | ||
|
c0e88df3e8 | ||
|
85e1cbad53 | ||
|
c37398af72 | ||
|
19cfe4e514 | ||
|
23a1b1925f | ||
|
1fb8d1e14c | ||
|
804c70b575 | ||
|
548c4a4c64 | ||
|
2978042162 | ||
|
4225ec7060 | ||
|
893339fc8e | ||
|
356e7b57d2 | ||
|
4ee1f1a507 | ||
|
7d64df60cd | ||
|
eb3a4ca157 | ||
|
a7b5157fa6 | ||
|
793e6d19eb | ||
|
674fa4d09c | ||
|
0089e86dd1 | ||
|
e1d802b507 | ||
|
9927b71af9 | ||
|
b1c0f105ab | ||
|
35cae1d4dc | ||
|
3dab3365e2 | ||
|
1bdc7c87ba | ||
|
cab8ad0ca0 | ||
|
43409f3ff0 | ||
|
a5dd4cab52 | ||
|
a815240f4e | ||
|
2a44e7c5bd | ||
|
28c7e439b1 | ||
|
4396c786b4 | ||
|
b67bb8595f | ||
|
bec47487dd | ||
|
b110d0c12b | ||
|
ae425475b4 | ||
|
dc6aee44b3 | ||
|
4ffea311e8 | ||
|
77a6a6e46a | ||
|
2278ba31e7 | ||
|
aaeec3d340 | ||
|
2cbe530b7e | ||
|
6ada6d145c | ||
|
0f55e83591 | ||
|
4017ea7b65 | ||
|
a85066c644 | ||
|
b08d38f339 | ||
|
d4f4632461 | ||
|
666aa041f4 | ||
|
1c565fd502 | ||
|
7de2b8cbd7 | ||
|
852e906736 | ||
|
5778466947 | ||
|
7006239b0d | ||
|
49d011574d | ||
|
046a358ae0 | ||
|
d23f5af957 | ||
|
20a3f4b200 | ||
|
73acda833e | ||
|
fa895db76e | ||
|
88f33be5b6 | ||
|
21612cccf7 | ||
|
39a7332343 | ||
|
21825876fb | ||
|
aaee887d3e | ||
|
cb44373eff | ||
|
4e6ea4f584 | ||
|
62a93d3e51 | ||
|
f60c281e80 | ||
|
43c40cdb09 | ||
|
c851262d81 | ||
|
91783ccc3e | ||
|
6ba3d5f86e | ||
|
a9a20755a9 | ||
|
d2693c1ac8 | ||
|
aaa6f434a9 | ||
|
314a3ac83f | ||
|
36e177479e | ||
|
cbeebed6c9 | ||
|
4b905dbfad | ||
|
6072e7efc7 | ||
|
19097c6692 | ||
|
d37f63c63c | ||
|
574bafd950 | ||
|
2b805f869a | ||
|
36c4be1d17 | ||
|
f2d82e16d6 | ||
|
c6658e1ac7 | ||
|
22a7d85e58 | ||
|
b3421b47b6 | ||
|
b5247f77ec | ||
|
e63e806572 | ||
|
62b84add36 | ||
|
858ae1266f | ||
|
3ae990aa40 | ||
|
deb4b16ae1 | ||
|
b37dc4c73e | ||
|
6b08100819 | ||
|
2c45e7146b | ||
|
7c4a722d72 | ||
|
f4bccefaba | ||
|
491bb93e95 | ||
|
f35700c9ee | ||
|
bd26aca3d9 | ||
|
71d24773b6 | ||
|
781bd29f40 | ||
|
fd4dd1edfa | ||
|
d2db26cb5e | ||
|
01977839f7 | ||
|
0116892b6b | ||
|
4d88873524 | ||
|
8e46c0186d | ||
|
1b0e589aab | ||
|
02ba149e26 | ||
|
2981aa876c | ||
|
82057e1f50 | ||
|
4ce36631e0 | ||
|
995324d6b3 | ||
|
c61ad9cd95 | ||
|
26f4bcc77e | ||
|
c2b2d06e47 | ||
|
cbcc7f6d88 | ||
|
d05e23264b | ||
|
db9faed184 | ||
|
e7feac848a | ||
|
33b965d9db | ||
|
6c33bd9c72 | ||
|
c72fd2fc9d | ||
|
906a3dc9b4 | ||
|
2d3a6a4528 | ||
|
01abc26316 | ||
|
2dfe43fc3c | ||
|
f71861300a | ||
|
52d7841334 | ||
|
9c821e2480 | ||
|
f8f0aa171c | ||
|
38d9999814 | ||
|
920305432b | ||
|
42fb8ab379 | ||
|
88ab385100 | ||
|
479a3540ec | ||
|
47f5a0de81 | ||
|
311c118834 | ||
|
0c40c0d795 | ||
|
25f0a8f0b7 | ||
|
f58a1a9ecf | ||
|
efa2ae5177 | ||
|
5e55c799ec | ||
|
46e61cb409 | ||
|
b24a489c77 | ||
|
65a618d019 | ||
|
4459c9f73d | ||
|
3c13f1ff61 | ||
|
c39d6dd407 | ||
|
1249b1ece9 | ||
|
da6f2da3d0 | ||
|
dbc235d84a | ||
|
b86924bc0e | ||
|
0fb8cf4241 | ||
|
30b7e831c0 | ||
|
f1b4ebcde2 | ||
|
e3c4ebb121 | ||
|
2dd17cfac5 | ||
|
93d04ef426 | ||
|
70bfd4dd8a | ||
|
ca917d9d21 | ||
|
be633f0560 | ||
|
613e980267 | ||
|
4fb37054df | ||
|
07508df8fd | ||
|
2a52fb5872 | ||
|
f45b3cab55 | ||
|
eb76d63117 | ||
|
0964c7a338 | ||
|
7af151d44e | ||
|
7028391e57 | ||
|
cbae0845e7 | ||
|
0e512962c6 | ||
|
ac694b855b | ||
|
490d45e788 | ||
|
b0863eb5ea | ||
|
ee199ed038 | ||
|
41268fa20b | ||
|
7474896368 | ||
|
54c4296a25 | ||
|
116f5afe3c | ||
|
c015c8f45d | ||
|
fe26b3d759 | ||
|
afee2d8ca8 | ||
|
e9158b7305 | ||
|
0f5690db85 | ||
|
3ebb35a5cd | ||
|
f557cd0933 | ||
|
063aa702b1 | ||
|
4f1070083a | ||
|
5e625f71c5 | ||
|
cc36a0ecd1 | ||
|
a849c25672 | ||
|
8b95b93c72 | ||
|
3d9557bc50 | ||
|
01f2f56be6 | ||
|
e658ed993a | ||
|
58fc897ea5 | ||
|
f8c5a35b56 | ||
|
2c92cc40e1 | ||
|
1266810c4d | ||
|
1de8657a56 | ||
|
408c24c700 | ||
|
3a1e5f7f0c | ||
|
8d85976ac0 | ||
|
fe7eaa594f | ||
|
5500a1edb3 | ||
|
eb27e34972 | ||
|
8335c299b8 | ||
|
feadc60b14 | ||
|
b59799dc2b | ||
|
f0b2f6eb00 | ||
|
c3fb126a0a | ||
|
f6708f6e47 | ||
|
d48b5a9079 | ||
|
99354f0d7d | ||
|
0fa8552aa3 | ||
|
4869c388b2 | ||
|
2adade0927 | ||
|
87b7337d9e | ||
|
351c9c1ad3 | ||
|
7759fe2cdf | ||
|
5d2651afc1 | ||
|
ce3b2de5e7 | ||
|
1782f59a96 | ||
|
3612096b56 | ||
|
97aa6139ea | ||
|
e5d915a7a9 | ||
|
5bc31c305c | ||
|
8c7590a249 | ||
|
d54d524cef | ||
|
a3d3ada500 | ||
|
ca43d197f9 | ||
|
73692a0c73 | ||
|
50191221b9 | ||
|
61236b45fb | ||
|
837545f4e7 | ||
|
31810477b2 | ||
|
4c652b5818 | ||
|
5201818f52 | ||
|
7e9e333d24 | ||
|
ab3b72bd1f | ||
|
f2c8a6bac5 | ||
|
758fab9976 | ||
|
3c2b985769 | ||
|
73bc07c7fb | ||
|
8298aa7124 | ||
|
92049cba92 | ||
|
83d8963051 | ||
|
bedcf6c058 | ||
|
f8b7c17efd | ||
|
76ba365325 | ||
|
a3255f3ab0 | ||
|
44af75613f | ||
|
df53e8beda | ||
|
003f97af24 | ||
|
c897aaafa7 | ||
|
76a0659335 | ||
|
4acdd8df12 | ||
|
cdabdb4558 | ||
|
f43fc1e376 | ||
|
70cd5d364c | ||
|
62aa807d0f | ||
|
5f3fed3c8f | ||
|
f95e879a58 | ||
|
34508a2fd1 | ||
|
cd85094113 | ||
|
72033279c2 | ||
|
9919224226 | ||
|
c6a1eac586 | ||
|
b657aa33fa | ||
|
834562190e | ||
|
453b28baf7 | ||
|
28522418ff | ||
|
18bab41605 | ||
|
c17079e045 | ||
|
f5cea7d9e3 | ||
|
c5083ea897 | ||
|
f607aa1233 | ||
|
db39458295 | ||
|
3d75e0773f | ||
|
bdcb467208 | ||
|
575a789d1d | ||
|
fcb3d71cb4 | ||
|
93c890ce41 | ||
|
2421f7c35c | ||
|
44920944db | ||
|
1b135be3c5 | ||
|
fff7ec9ba7 | ||
|
0468f255e7 | ||
|
5da5158b14 | ||
|
6493ce3fe0 | ||
|
078772495a | ||
|
c81ad5cd03 | ||
|
439bee1203 | ||
|
0eccbf64f4 | ||
|
5232ce6e52 | ||
|
ada278d8da | ||
|
24d5fef20f | ||
|
b7fd1b9f4c | ||
|
b16cd1ec5c | ||
|
278ec88dd0 | ||
|
b91699f317 | ||
|
105ed56dfc | ||
|
f9267a96ed | ||
|
1135f45dc9 | ||
|
b74a75f4c6 | ||
|
8a787f48b3 | ||
|
e956642982 | ||
|
d3d08168de | ||
|
919f56a292 | ||
|
b5be17c2d2 | ||
|
5221aa0ab7 | ||
|
49ad8b7cc1 | ||
|
fa99cbce4a | ||
|
07e55b2636 | ||
|
d8984c42b5 | ||
|
36f251e710 | ||
|
f3beacdc3f | ||
|
e023be1f25 | ||
|
d1a5f97f59 | ||
|
a43de75b42 | ||
|
5530f7263f | ||
|
d688244664 | ||
|
c8155c8a32 | ||
|
ce15f8f1dd | ||
|
23ed697b98 | ||
|
55854653b0 | ||
|
83983bbb32 | ||
|
768c917a0e | ||
|
039df94b86 | ||
|
f46cb7a670 | ||
|
9d05276f94 | ||
|
dc4916dc19 | ||
|
1d0a1ab16a | ||
|
1ae6106782 | ||
|
2f87deb10b | ||
|
65253ca54e | ||
|
af38d0cc07 | ||
|
5ec2cbdf19 | ||
|
f6f44d8e8f | ||
|
2b6fc16637 | ||
|
e0a2e3bd0c | ||
|
1ecd0307ed | ||
|
f10f3456d7 | ||
|
b17be37aee | ||
|
ddbef494e3 | ||
|
49d91c498e | ||
|
7cc4a21383 | ||
|
af464c2af7 | ||
|
ef1d06b8dc | ||
|
f1562ccfd5 | ||
|
bfe2fb6da8 | ||
|
6f23f6352d | ||
|
a47816f45d | ||
|
30ac392af4 | ||
|
e4ee149085 | ||
|
a521d8549a | ||
|
566faba6e3 | ||
|
e4e9de0a53 | ||
|
07ae971ae1 | ||
|
4f5102b2dc | ||
|
04d1915121 | ||
|
c35f6e926d | ||
|
dffcee6cf1 | ||
|
7485c1240b | ||
|
95d3ebdc2d | ||
|
6bb565ee67 | ||
|
19b38074a5 | ||
|
25649f578d | ||
|
7a63a17b66 | ||
|
3424cb1f29 | ||
|
5c8277ea1d | ||
|
9e3ffea22c | ||
|
9592076d45 | ||
|
2e01665340 | ||
|
bba0ef522c | ||
|
019cdd2b3a | ||
|
ce24352974 | ||
|
407b8de2b1 | ||
|
f332a73122 | ||
|
2335abac91 | ||
|
5cf6804615 | ||
|
58b04b5fc8 | ||
|
c58f468dc9 | ||
|
e922bc207a | ||
|
784cfb8fba | ||
|
b53bb44e42 | ||
|
e7e85456ea | ||
|
440baf6009 | ||
|
1876e80094 | ||
|
d20b3a5b8b | ||
|
6d9454b351 | ||
|
a138bb61bc | ||
|
85d313a791 | ||
|
de7380fb0c | ||
|
35bf4a111c | ||
|
d5be00d29d | ||
|
e63710dfb4 | ||
|
8b95b3c1bf | ||
|
b8ec3f5704 | ||
|
5dc79159b4 | ||
|
5368c49e68 | ||
|
8731d0416b | ||
|
d05151466c | ||
|
367a373904 | ||
|
533587ce4e | ||
|
db0ddee9f4 | ||
|
d5bad67e17 | ||
|
9e4c8c52d2 | ||
|
8ca84ee6f4 | ||
|
5ebbd769e8 | ||
|
47051127af | ||
|
7ad2e1ca05 | ||
|
7345ccbbee | ||
|
e282686f97 | ||
|
5524f80ea3 | ||
|
d86274cc37 | ||
|
41fb6a1fc9 | ||
|
8e9842df14 | ||
|
f4904047b5 | ||
|
4083f4db9f | ||
|
a125c0032b | ||
|
2e4fd1530c | ||
|
be08a2650c | ||
|
e08dc777df | ||
|
239f7fb35d | ||
|
1cb8354aca | ||
|
2465b4ffd7 | ||
|
4f73ea0879 | ||
|
f4eb17f616 | ||
|
b5ce738ba2 | ||
|
3155d4afdb | ||
|
f444696b84 | ||
|
bdde9b063c | ||
|
ef98bd107f | ||
|
dfb259d822 | ||
|
13c34fd26d | ||
|
665cd454ef | ||
|
b3bde5782a | ||
|
cc8f09f05e | ||
|
2beda08717 | ||
|
b455d153ae | ||
|
5fddf01820 | ||
|
c80434141d | ||
|
82e7348cf2 | ||
|
d1128c7a1e | ||
|
5674879e23 | ||
|
aae81313a6 | ||
|
4667f96b40 | ||
|
d6c2c7ef02 | ||
|
2af1ccd8b2 | ||
|
55e2e29696 | ||
|
b60f8df17a | ||
|
28c320ae97 | ||
|
00ed54799f | ||
|
fae77231a5 | ||
|
2083941361 | ||
|
5ec517e3bc | ||
|
780a3b1827 | ||
|
61aa086cb5 | ||
|
0a08f9d3f8 | ||
|
c3de13e0d8 | ||
|
45017efe00 | ||
|
a20290cac8 | ||
|
31e02a154c | ||
|
023ee5db99 | ||
|
05d2e15ab5 | ||
|
7d6590c60a | ||
|
3152ce183b | ||
|
d9f1a7c4d0 | ||
|
952aed3c49 | ||
|
ab3c433450 | ||
|
2b5e4a34d4 | ||
|
35cea852ca | ||
|
88581c8983 | ||
|
a7a9aab189 | ||
|
370c9b63cf | ||
|
7cb08849de | ||
|
7f052163e3 | ||
|
277d939033 | ||
|
26fbdcfab0 | ||
|
6d63ba9d4d | ||
|
8963f4fd62 | ||
|
463021a9f3 | ||
|
f71a8e9fef | ||
|
2dd5be1b4e | ||
|
608838045f | ||
|
899d506faa | ||
|
21b3e3ea05 | ||
|
a68951541c | ||
|
7fd0deedb1 | ||
|
e9e12ad843 | ||
|
4fd3185d12 | ||
|
f5ccebfd41 | ||
|
294721eef9 | ||
|
8af509992d | ||
|
11fccb8e89 | ||
|
1e126dd2c3 | ||
|
cfe2f889a4 | ||
|
1bd76b0e07 | ||
|
6d8c935cc7 | ||
|
7144cee0f6 | ||
|
f75a8d56f2 | ||
|
2f321bcfd9 | ||
|
a157f4f17b | ||
|
7723c623d5 | ||
|
bc3bb78916 | ||
|
0ebf5e49fb | ||
|
c340921fbb | ||
|
30e26b101c | ||
|
1b17cab663 | ||
|
ab039adf97 | ||
|
62fe10df31 | ||
|
2004a751dd | ||
|
ace127acf4 | ||
|
82c5497a06 | ||
|
dbb7989027 | ||
|
103f677a93 | ||
|
cb6bf78595 | ||
|
62334ddef7 | ||
|
778f67f2e4 | ||
|
48737f8e60 | ||
|
94de62e503 | ||
|
0445052898 | ||
|
ccde90ea91 | ||
|
ed94355019 | ||
|
02fcd1b5fc | ||
|
ca934e7cdf | ||
|
0ddd5f0a79 | ||
|
099d6801b7 | ||
|
62293926ec | ||
|
c96daad12c | ||
|
9cc3be152f | ||
|
f841c0d4ba | ||
|
3cd1d8135e | ||
|
86474d9f90 | ||
|
9bf87a3033 | ||
|
71e32520cf | ||
|
e3e938c8eb | ||
|
03aa440424 | ||
|
85ca38be90 | ||
|
7c9790dff0 | ||
|
40a71a11cb | ||
|
46a500f5e5 | ||
|
2d1d03bf8e | ||
|
1092d00c7a | ||
|
cd58e0d01e | ||
|
30a9783348 | ||
|
c839cf50af | ||
|
f8d607b06f | ||
|
cfadeb07b1 | ||
|
072850be0b | ||
|
71d120bc4e | ||
|
68d3cea528 | ||
|
07e801f44d | ||
|
ee5c694aa2 | ||
|
efa5eb1770 | ||
|
42e37246f3 | ||
|
dabb08ff4a | ||
|
66b0e04cc6 | ||
|
74824b7737 | ||
|
df2bcdb854 | ||
|
a8e9ee2e95 | ||
|
5093697b27 | ||
|
5bacd63805 | ||
|
1d5932e63f | ||
|
022762c0c9 | ||
|
e16bd194a3 | ||
|
a3765c19e3 | ||
|
a845d92d88 | ||
|
668c9e5a64 | ||
|
aaa06f4120 | ||
|
e26f4ce707 | ||
|
a8c3a2d991 | ||
|
6d52cef73a | ||
|
edacfcdec7 | ||
|
683872ef4e | ||
|
7a299ba1f9 | ||
|
f5eaedfc72 | ||
|
11b6afbe09 | ||
|
cd7340915b | ||
|
b38bb3df5d | ||
|
1f7725ada3 | ||
|
622095ebc4 | ||
|
397b7fefe3 | ||
|
10d38b709b | ||
|
98985690f0 | ||
|
f50c483c64 | ||
|
a15eca137d | ||
|
ba4be02e75 | ||
|
d4f6a86a57 | ||
|
6a058372bb | ||
|
e6cce350bd | ||
|
7c0c1e6cf8 | ||
|
39f787b7db | ||
|
424437446d | ||
|
60a1859d89 | ||
|
ad5c1639e8 | ||
|
92828b22fa | ||
|
e470096e4e | ||
|
3c41608ee9 | ||
|
035e145cd1 | ||
|
2f621279c2 | ||
|
c30185c6ae | ||
|
908c74eb27 | ||
|
10c17fc9a9 | ||
|
a6a0cb928a | ||
|
8bca988520 | ||
|
0af0af8d8a | ||
|
97da13c3c4 | ||
|
7134b46cdc | ||
|
aecdf7a3d3 | ||
|
37e37d1998 | ||
|
6103a8590d | ||
|
ba62dadc00 | ||
|
26073b82fd | ||
|
9b73bca79d | ||
|
0a0bb0ca13 | ||
|
652df47c5c | ||
|
9248be2177 | ||
|
b3800fc42e | ||
|
15f304736f | ||
|
21cdf59065 | ||
|
e3693afb75 | ||
|
009a753585 | ||
|
5c72541044 | ||
|
a01e604443 | ||
|
52b339d0b8 | ||
|
579ed5b9c0 | ||
|
63e64b8bcc | ||
|
64f8583975 | ||
|
75e8064044 | ||
|
6f3e38e392 | ||
|
cb4d244f19 | ||
|
de3b8a10a0 | ||
|
8feece702c | ||
|
900308afec | ||
|
b47925a319 | ||
|
853325d9fd | ||
|
494be37715 | ||
|
d35cb5d072 | ||
|
df9ec711c5 | ||
|
d9d0837024 | ||
|
086138fbd9 | ||
|
bdbd4b57b7 | ||
|
5475448af5 | ||
|
c3da3f11d9 | ||
|
244c81587c | ||
|
a3877a2cb1 | ||
|
206df82d63 | ||
|
54e1e7684d | ||
|
27eef36677 | ||
|
d91953e70b | ||
|
bbc5a49054 | ||
|
f89fe9fbab | ||
|
0c042bfe50 | ||
|
1a7894b15e | ||
|
543f983e41 | ||
|
97e7e473b8 | ||
|
79c30f7a94 | ||
|
cccf86d388 | ||
|
c102c23831 | ||
|
80ada9c90a | ||
|
6c3b4070ba | ||
|
4b287b758d | ||
|
b6d129a5c1 | ||
|
4d08147647 | ||
|
ace7c17f2b | ||
|
10f3d8aa0f | ||
|
3237ca0d97 | ||
|
5682ab9570 | ||
|
a3d73634e7 | ||
|
98b6aec203 | ||
|
7feb788ed3 | ||
|
bea490081b | ||
|
7adc3ca003 | ||
|
f8cbc63ab0 | ||
|
418590fb35 | ||
|
56144482f1 | ||
|
59f681e6af | ||
|
d3296f5180 | ||
|
c6fff0aa13 | ||
|
41e0c42282 | ||
|
ede2274816 | ||
|
ead672afb2 | ||
|
73bc7b045e | ||
|
3281502c25 | ||
|
ca35e536db | ||
|
bb8e0eb7bf | ||
|
bb451ac3b5 | ||
|
b0b7842f9c | ||
|
aad661cb65 | ||
|
ed9b63520d | ||
|
0d89d4d0d3 | ||
|
5de1246827 | ||
|
edc3b014cd | ||
|
fec98f45ce | ||
|
94810d5066 | ||
|
431cc796d8 | ||
|
e9d2dbcc92 | ||
|
7a618ef89c | ||
|
5b56249d12 | ||
|
e2ba5abe76 | ||
|
70a4b7c863 | ||
|
10fde1b1ef | ||
|
6131746180 | ||
|
bf953bf1b5 | ||
|
d81da10dfa | ||
|
ed9cdc1ab7 | ||
|
9f506eb83a | ||
|
36e8c52b01 | ||
|
e6d1233bfe | ||
|
6847459022 | ||
|
285623a02b | ||
|
fbf64f8037 | ||
|
2b74ca2746 | ||
|
b106a82308 | ||
|
d71a0ddd66 | ||
|
5700f2f78a | ||
|
c5ca6abb90 | ||
|
6826b6e1f8 | ||
|
27c4fa2fcf | ||
|
6ef1aff991 | ||
|
57a026a7a1 | ||
|
cfc785358e | ||
|
c2d2b3f3b2 | ||
|
4aa153fcf1 | ||
|
824f63a3ad | ||
|
2446dc6950 | ||
|
f98405188d | ||
|
a5cf24773c | ||
|
01c1e4f8cb | ||
|
0759fb6436 | ||
|
ed5188069b | ||
|
aa0a9bde76 | ||
|
56a450a936 | ||
|
1e01106b94 | ||
|
b9a755d6d3 | ||
|
1d9d6c899d | ||
|
444dffb458 | ||
|
3f1b7192ff | ||
|
b992b19c66 | ||
|
e341121f61 | ||
|
cd3e2963b3 | ||
|
346faf1d07 | ||
|
1e09b2bbd8 | ||
|
0ffba45517 | ||
|
32ff346154 | ||
|
0008f44255 | ||
|
a3c519a061 | ||
|
5b8a923cb5 | ||
|
576cbc0b90 | ||
|
e95e2cf152 | ||
|
317dc10af4 | ||
|
f06065337c | ||
|
cc870ca302 | ||
|
80f5506b18 | ||
|
5321a75272 | ||
|
b70a78b7aa | ||
|
5ad08791ea | ||
|
9dc3ec0bf8 | ||
|
69dd9d0cac | ||
|
24f923e88e | ||
|
b5552a216d | ||
|
f3fe4433ae | ||
|
1988c617a0 | ||
|
5e531d6f96 | ||
|
1fb7e97700 | ||
|
64d27156f5 | ||
|
b528a0f4ec | ||
|
7f265a6692 | ||
|
55e00e35c1 | ||
|
d94e1ba55b | ||
|
00e7167174 | ||
|
9d7b69fc0e | ||
|
db9a68e9c9 | ||
|
b5d9d6e268 | ||
|
5ff0c563ec | ||
|
e2131523ec | ||
|
1f5f51e3e5 | ||
|
1bb0d54dce | ||
|
094bb37049 | ||
|
58601db5ef | ||
|
b5bef98a9b | ||
|
1026f1efa5 | ||
|
0c673fb524 | ||
|
7ee2c9478d | ||
|
4b51f8251b | ||
|
e91a64b1cc | ||
|
8920762fc5 | ||
|
ba40f93386 | ||
|
781bf52a8e | ||
|
cca1a9832e | ||
|
1a152a5597 | ||
|
76a5290351 | ||
|
829e17ef2b | ||
|
bc5d3bea14 | ||
|
edf2a2e68d | ||
|
5df443a016 | ||
|
f6396f2e74 | ||
|
c618e58a11 | ||
|
2ea27acdde | ||
|
331cad276e | ||
|
b74eab8377 | ||
|
fb80318553 | ||
|
3eb4aed867 | ||
|
f6f959a897 | ||
|
2b422a542a | ||
|
d0e9d58a43 | ||
|
8a1933b9b2 | ||
|
96a587f343 | ||
|
2bb6a71874 | ||
|
94acd12f1c | ||
|
5e44a61068 | ||
|
a54f0ed94d | ||
|
662c6f3cc2 | ||
|
d93c635a0a | ||
|
17b73aaf91 | ||
|
c194911458 | ||
|
92e99e3fb4 | ||
|
848e6102a1 | ||
|
ef37bf9b1a | ||
|
eb41e023c7 | ||
|
6b4987bf39 | ||
|
d46ff76887 | ||
|
d53a9e672c | ||
|
80ef99a24b | ||
|
6062a1f8c7 | ||
|
d974bd9a07 | ||
|
fb8c9566d5 | ||
|
7431bd69e9 | ||
|
5d7f393e94 | ||
|
ef25d100e1 | ||
|
17f82c8972 | ||
|
05c937743c | ||
|
bf2e7ff130 | ||
|
1b30ee606f | ||
|
3235907266 | ||
|
99c7e417d6 | ||
|
d81906d348 | ||
|
296872d2e4 | ||
|
7cd02b4916 | ||
|
0e217f48be | ||
|
61fdf4b6c7 | ||
|
91dbf1f01a | ||
|
d52aac76c0 | ||
|
6102e441d6 | ||
|
1a5fec39c0 | ||
|
efa5091b98 | ||
|
be4386658a | ||
|
0cddce7a37 | ||
|
a86d13632e | ||
|
d71682a3f7 | ||
|
c901ace21a | ||
|
418398a870 | ||
|
baca57062e | ||
|
52df8e6e8b | ||
|
b51747378a | ||
|
424a6b0428 | ||
|
4549223d6d | ||
|
91dd3468d5 | ||
|
83a3871d43 | ||
|
8b6023c45a | ||
|
a401d4e760 | ||
|
a8520f6593 | ||
|
9a95d207cf | ||
|
c75d779f85 | ||
|
b8f2066408 | ||
|
cc48b6cd96 | ||
|
b36c735445 | ||
|
5f0f26a3ac | ||
|
6b81eaa65d | ||
|
644f15e80d | ||
|
a8ddf4c2df | ||
|
3f9833117e | ||
|
6140d0c849 | ||
|
f4cb7cea21 | ||
|
140ec1f90f | ||
|
31d7220785 | ||
|
38e1d36684 | ||
|
6397655f7d | ||
|
692047e4c8 | ||
|
821bfe68c1 | ||
|
596eaa7590 | ||
|
08fd0f0fa9 | ||
|
77c925ce4b | ||
|
3338352b41 | ||
|
e3aa4143c7 | ||
|
0a93808076 | ||
|
0cc21f9b99 | ||
|
d3543ea291 | ||
|
e824f7a28c | ||
|
56be0744f0 | ||
|
d874c71e0b | ||
|
487177af87 | ||
|
0452a4e1ac | ||
|
959a03214a | ||
|
fe22dfc531 | ||
|
139e258664 | ||
|
d1ab14966b | ||
|
40ff3c731e | ||
|
75d77a3648 | ||
|
921da7242d | ||
|
67c9937e67 | ||
|
bdcb3af6fa | ||
|
17e81ab6bd | ||
|
70e1ec2cd2 | ||
|
c6a7fd405b | ||
|
4f0d1704c4 | ||
|
a966a5097e | ||
|
080db4d462 | ||
|
4af766162e | ||
|
57d67bc4a8 | ||
|
16278f36ec | ||
|
9cb981f068 | ||
|
05c6d67cab | ||
|
0c516843d8 | ||
|
d400ac57d5 | ||
|
2644efd9f7 | ||
|
11baa97ef6 | ||
|
c36636bd2d | ||
|
6bb05a6780 | ||
|
360f5db2cf | ||
|
bbbeacee4d | ||
|
94ad56fc96 | ||
|
3df80f2653 | ||
|
bb6c9cf49e | ||
|
ae12222687 | ||
|
094dfde048 | ||
|
714b887274 | ||
|
8cfa88eff8 | ||
|
3976b57100 | ||
|
bbcf484f7f | ||
|
249f35f948 | ||
|
b71f1a79c8 | ||
|
e4aa3d310f | ||
|
99f57269fb | ||
|
93def3a557 | ||
|
a04674d93d | ||
|
539cc187a8 | ||
|
fcdd975751 | ||
|
8a4c2bf208 | ||
|
50c5d533b0 | ||
|
f952553c76 | ||
|
9a9be466f7 | ||
|
bbad029aa1 | ||
|
eb748554c5 | ||
|
c8b494e909 | ||
|
bd7fd725f6 | ||
|
3e0440ba53 | ||
|
83a29f8d85 | ||
|
8551e6e74a | ||
|
3582cb3f46 | ||
|
23de13b82c | ||
|
93acd4f18f | ||
|
96883dabe3 | ||
|
da521020f6 | ||
|
ac78738061 | ||
|
b78ca71c0f | ||
|
cd7272d00a | ||
|
cd7489c569 | ||
|
1f3f369df0 | ||
|
911e29f2ea | ||
|
d7b359e432 | ||
|
0b2c92cbe5 | ||
|
f626d964c5 | ||
|
b0e0b38549 | ||
|
94af4b2c7b | ||
|
0b235e146f | ||
|
40f3301324 | ||
|
4fe90e59a8 | ||
|
b28611ed08 | ||
|
672ad22e4f | ||
|
c17bd5ec3a | ||
|
2770755f9d | ||
|
ee69cdbf7b | ||
|
a72a25640f | ||
|
e86f8ddd09 | ||
|
66bdb39f1a | ||
|
aa368c3a63 | ||
|
b499a2ab87 | ||
|
49e4482947 | ||
|
6e7e4250e6 | ||
|
5a8ea15c4f | ||
|
f9aa029e8e | ||
|
6653e379b3 | ||
|
930a611374 | ||
|
51e918be6d | ||
|
b0170eea8f | ||
|
144b34ca2e | ||
|
676c022e41 | ||
|
7c0b98bb70 | ||
|
7779713392 | ||
|
5e1396025c | ||
|
d09c88f71c | ||
|
7980f21b1b | ||
|
3843994a05 | ||
|
a1b08ca037 | ||
|
144cdd11ec | ||
|
b6531cdb10 | ||
|
0eef4a5fa1 | ||
|
42baaf8f2d | ||
|
08d9dff8eb | ||
|
01b3aab9bc | ||
|
fde34ef178 | ||
|
5195abec94 | ||
|
aa79acd09e | ||
|
67011ccd72 | ||
|
e2cd7fe17e | ||
|
4c0624f489 | ||
|
8c033250b1 | ||
|
05b5a6fddf | ||
|
df5bde7b8e | ||
|
ca5f52c48c | ||
|
3656adf059 | ||
|
4381792b05 | ||
|
0a3c20b08a | ||
|
4ab3f41665 | ||
|
c75ce9cbba | ||
|
10356a4376 | ||
|
4fb4e19e99 | ||
|
98c2056f53 | ||
|
74e8ae4d79 | ||
|
6b890cf3b3 | ||
|
d62b471afe | ||
|
b97240963e | ||
|
55ed0f2374 | ||
|
2c1b3d5790 | ||
|
9c3b757578 | ||
|
bc04ad96b5 | ||
|
35b07a9c18 | ||
|
17955fc419 | ||
|
ac98f5862a | ||
|
63758c6679 | ||
|
7c765c47e6 | ||
|
58523b0000 | ||
|
41f4b36593 | ||
|
379212b8fe | ||
|
a04c7831b1 | ||
|
1aec16a240 | ||
|
498626748d | ||
|
f24ec97607 | ||
|
f0ad260eab | ||
|
92ee2be021 | ||
|
02e4528f3e | ||
|
f871353acc | ||
|
faa1120e14 | ||
|
8a8000c80a | ||
|
8164610105 | ||
|
3935a3c885 | ||
|
15ec39bc56 | ||
|
5dd2b4c439 | ||
|
1ee05dda4b | ||
|
5a86588cb5 | ||
|
af3269b362 | ||
|
a7a4baa06e | ||
|
9a497b9bba | ||
|
d2a1c449da | ||
|
504524ea56 | ||
|
51e22b406c | ||
|
1ec81482cc | ||
|
fd610414fd | ||
|
1802d5d5da | ||
|
e7e6c76b4e | ||
|
84c7bd63f5 | ||
|
f7187bb798 | ||
|
00db5b69ab | ||
|
b16109ca56 | ||
|
a00b5aa136 | ||
|
0e43e5d295 | ||
|
9f1e2d6192 | ||
|
52c794a259 | ||
|
fcf4c5f328 | ||
|
85b33a60b3 | ||
|
c99bb4cfd7 | ||
|
5b0a942b42 | ||
|
d94c9e522c | ||
|
8fab15abd3 | ||
|
7717304549 | ||
|
b3f1cc3edd | ||
|
16f9c727f2 | ||
|
007ba5fce9 | ||
|
5b36f07493 | ||
|
cbd2580736 | ||
|
fe872a59ca | ||
|
c4dfd99a8c | ||
|
5a34fe4704 | ||
|
2fc3dcce7e | ||
|
1a932fb749 | ||
|
f8a2ee9a8d | ||
|
a972d456ba | ||
|
e9f4c9e687 | ||
|
58f7cdc15e | ||
|
3f9b0643ee | ||
|
24bd4da94b | ||
|
a18b4ffd0f | ||
|
c9e032e350 | ||
|
e1679ad433 | ||
|
933e395945 | ||
|
1f64a00bbf | ||
|
7eebc0c3a5 | ||
|
a2c9dabd77 | ||
|
6187c826b4 | ||
|
b8987963b3 | ||
|
dc7f56b7aa | ||
|
2295ddddee | ||
|
3328d20b2d | ||
|
d444211c68 | ||
|
2de8cd1d6f | ||
|
115813ac44 | ||
|
7c0e8d9421 | ||
|
1c349cf55c | ||
|
7b4d8a8f05 | ||
|
6e7e2de490 | ||
|
0168ef55f0 | ||
|
0a9a92ee7d | ||
|
3bfac7ef3b | ||
|
1581e0e439 | ||
|
6773848874 | ||
|
3e9219173c | ||
|
d46a565e6c | ||
|
6267804d38 | ||
|
25c4d282ce | ||
|
45281360d5 | ||
|
7a32b8d1d2 | ||
|
7b0018b661 | ||
|
9032879e20 | ||
|
8b668cf8b7 | ||
|
3323533fd0 | ||
|
50e96baea1 | ||
|
2ac92df9d3 | ||
|
63e64cea60 | ||
|
0267c76de2 | ||
|
2382a10bba | ||
|
a8db40e99a | ||
|
8553fffffe | ||
|
d9d38a27ff | ||
|
1e7a5562ab | ||
|
e71032a8fc | ||
|
484a3dbe5c | ||
|
9c7f40e4fe | ||
|
f35b7ab6f4 | ||
|
a38dd3d5c8 | ||
|
00ad35fc3e | ||
|
0d78b44c80 | ||
|
fb614637a9 | ||
|
d0482d66c2 | ||
|
ef178dfd24 | ||
|
6cdc6e0780 | ||
|
c961e6d9c7 | ||
|
c6844ff47a | ||
|
605a630411 | ||
|
2b75cf1508 | ||
|
007054301b | ||
|
cf01d32237 | ||
|
5ec553f68d | ||
|
d95e5a169d | ||
|
8debefddad | ||
|
bd25860ccf | ||
|
b21cb5c0e9 | ||
|
ff79a2d3f4 | ||
|
9ae3743a58 | ||
|
ffcdbcc802 | ||
|
3ff78a3a47 | ||
|
4d78ac4789 | ||
|
be24f2d520 | ||
|
ff7fccb6a2 | ||
|
ee5a2b3c38 | ||
|
72b9001447 | ||
|
353245bb7d | ||
|
f9411bf0ed | ||
|
0eedbd2aa1 | ||
|
5e36c37838 | ||
|
35c76c8e2a | ||
|
90ad46b7c5 | ||
|
1c06e1e2a4 | ||
|
15d2c0e436 | ||
|
9984aea42f | ||
|
a9cc5cc351 | ||
|
a6cbabfba5 | ||
|
c3da3499d8 | ||
|
1ef85242ec | ||
|
ffe75b0856 | ||
|
25e3c4fcdc | ||
|
a922497f5d | ||
|
acb100908b | ||
|
7fa2cbc746 | ||
|
b36491af8e | ||
|
ea4fe81cb2 | ||
|
2191f1b826 | ||
|
08f6367752 | ||
|
e1b09f4844 | ||
|
884c46b054 | ||
|
d1cde123dc | ||
|
5eaded7e3a | ||
|
4ae4e88800 | ||
|
85fb27a631 | ||
|
8bf0561009 | ||
|
ddfded048c | ||
|
a2e889587e | ||
|
b63aaad645 | ||
|
2c41b5d4fb | ||
|
34aa4c6412 | ||
|
82372ea252 | ||
|
1f4440bcf9 | ||
|
31fcff4afc | ||
|
082f17f940 | ||
|
aa2ac3865c | ||
|
db8ffe50ac | ||
|
8fe658bacc | ||
|
c71d5e2dfb | ||
|
36c31dcd67 | ||
|
c223408c3c | ||
|
3cdccb49ef | ||
|
0844645a8b | ||
|
30bfad455c | ||
|
d84fdc3cd5 | ||
|
2b64b9de63 | ||
|
cdb3d863db | ||
|
74e3524e92 | ||
|
d31f75d1ec | ||
|
0b34207148 | ||
|
3c2beded68 | ||
|
f33fdb3bfd | ||
|
96a0f29f19 | ||
|
49b3a75a8b | ||
|
f13fc737f1 | ||
|
b7121c5000 | ||
|
22a1d3882e | ||
|
82f74e2264 | ||
|
3dd5699cde | ||
|
4d8553f959 | ||
|
eee1534da7 | ||
|
a198bfc5c0 | ||
|
38c20f5737 | ||
|
132807b55d | ||
|
735081af50 | ||
|
31651aeaab | ||
|
7f4230d026 | ||
|
6333d3fd13 | ||
|
fd9dae6e4b | ||
|
db5d7857c8 | ||
|
0f4eab3cf2 | ||
|
75b9f4fcbf | ||
|
b9b58b8985 | ||
|
64b8aa1c01 | ||
|
6b21dc132d | ||
|
ba85c5159d | ||
|
0388c5d5bc | ||
|
7aca4930db | ||
|
3714c80adb | ||
|
7a92ecfa30 | ||
|
171f6f4608 | ||
|
d569c8d31f | ||
|
51d716253f | ||
|
971b17b364 | ||
|
9616d858cf | ||
|
db5ff7f16d | ||
|
5db045f392 | ||
|
42c143d19e | ||
|
7c1948ebd9 | ||
|
9cd15645a2 | ||
|
c0a4a5c2f5 | ||
|
518004afbc | ||
|
833a4b9367 | ||
|
6aa82724b4 | ||
|
cfbee40ecd | ||
|
79d589c7a9 | ||
|
735c1a23ea | ||
|
6b82fc3011 | ||
|
d921456036 | ||
|
6cc93250b8 | ||
|
39082541ff | ||
|
8cff40fdd4 | ||
|
a777db1234 | ||
|
3fca169096 | ||
|
58b451f616 | ||
|
f9b82f711f | ||
|
d92b5db320 | ||
|
a164e4bf3a | ||
|
be3cbd9e21 | ||
|
43fed96af1 | ||
|
3d3d31ef29 | ||
|
d51e70bcaa | ||
|
32f4c6c982 | ||
|
8f47761200 | ||
|
88cfc96bd3 | ||
|
11f389289d | ||
|
342ebecef2 | ||
|
220a8fe2cc | ||
|
d9389c91ee | ||
|
480cb00098 | ||
|
07ed550dc2 | ||
|
6045870398 | ||
|
2bf102cdf1 | ||
|
889a5b2bce | ||
|
df964a094b | ||
|
e395d4ecee | ||
|
d077e0c83c | ||
|
d931241edc | ||
|
647376ab3f | ||
|
64a9a72457 | ||
|
a8417aca16 | ||
|
b7b2ecad59 | ||
|
dcaa2f4168 | ||
|
7c7f54d224 | ||
|
b942f8c726 | ||
|
f661f23ee5 | ||
|
5a631df2a2 | ||
|
fc9bb7dac6 | ||
|
0a82dc2e8e | ||
|
c5eff85c28 | ||
|
d1627276a6 | ||
|
2be2a2621e | ||
|
995c303f27 | ||
|
9c03525369 | ||
|
afe0673fd1 | ||
|
37333f7fbe | ||
|
c9160cabc5 | ||
|
9e289d5e97 | ||
|
901a580e11 | ||
|
d196292551 | ||
|
8d856b0ec6 | ||
|
90fad52760 | ||
|
2817875461 | ||
|
bcbdee1dcc | ||
|
3de4f2805a | ||
|
995197cad9 | ||
|
89cc4d1df4 | ||
|
61f3b3592f | ||
|
8cb6f67a60 | ||
|
7c580f898c | ||
|
9ad6ce5851 | ||
|
a66090b594 | ||
|
d992a3f7d7 | ||
|
b418a78e2e | ||
|
cae9ae51ad | ||
|
04c92ec4bd | ||
|
54834891fb | ||
|
c9e2f4244d | ||
|
503b86ac13 | ||
|
ec051eba38 | ||
|
2f50f64ecf | ||
|
66fe124dd1 | ||
|
87e56c2f66 | ||
|
f044b0292c | ||
|
ee1d4cd45d | ||
|
b25f83e096 | ||
|
8b7e1e4169 | ||
|
ca9a2cb13a | ||
|
93af92743c | ||
|
0ebef3792d | ||
|
7a3bb14653 | ||
|
fbc0a39a1c | ||
|
473bad24b7 | ||
|
313d968985 | ||
|
b5775ff9d2 | ||
|
553c1eacfc | ||
|
8f66a41c09 | ||
|
6721471c63 | ||
|
638421de40 | ||
|
7bc5338cb3 | ||
|
07c9db9b54 | ||
|
fafc4fb71e | ||
|
6b49d32102 | ||
|
3a391aa3cb | ||
|
42019321e3 | ||
|
b61860b3ab | ||
|
91950e1891 | ||
|
0032d3cf6c | ||
|
42715bba50 | ||
|
0aacad655d | ||
|
bd81cc1cc4 | ||
|
289480c954 | ||
|
3da7746629 | ||
|
8d48051a8d | ||
|
ec16c0f0f4 | ||
|
bc36fcb722 | ||
|
19d19112d9 | ||
|
d9f1c2a406 | ||
|
ef2be40478 | ||
|
8e2ee5e5e4 | ||
|
c3da2bfade | ||
|
bbd6780971 | ||
|
0a6dab1f24 | ||
|
faa9a982a9 | ||
|
de8bb8a951 | ||
|
b766eef5ef | ||
|
a185787044 | ||
|
76f7cd08ee | ||
|
460451bcce | ||
|
71edb68995 | ||
|
e188482247 | ||
|
047d320665 | ||
|
5c4d9a85be | ||
|
56c08056d2 | ||
|
b39ac73cd8 | ||
|
c9054e7d8c | ||
|
adf5c9bd46 | ||
|
3ea3674407 | ||
|
657c7d8cff | ||
|
350e32326f | ||
|
1894573c2f | ||
|
73f889ac9f | ||
|
a336dae84c | ||
|
467c826c04 | ||
|
a4c164a57e | ||
|
11cd553949 | ||
|
8c0c22a925 | ||
|
c8d528ffc4 | ||
|
bb7b1f9e0c | ||
|
f994f83ce1 | ||
|
2af083b2e5 | ||
|
de4d0961da | ||
|
c8d48ccbff | ||
|
695d3b82b5 | ||
|
965625ad01 | ||
|
379733938c | ||
|
3198999746 | ||
|
1f03499fc5 | ||
|
a53d888747 | ||
|
62905f084f | ||
|
ba7ee4fba7 | ||
|
6cb3df9350 | ||
|
5c1c71c625 | ||
|
7fd0cfc85f | ||
|
efad3b1284 | ||
|
a06de9682c | ||
|
0d4ad05c1c | ||
|
aef088a9d2 | ||
|
e8f3aa681e | ||
|
42293fb11a | ||
|
65e0eb5205 | ||
|
2f1a7f8f40 | ||
|
73e9410264 | ||
|
c835c02bf2 | ||
|
336d44a5cc | ||
|
7027931095 | ||
|
87b56d538d | ||
|
0519ce2001 | ||
|
d4d0330f70 | ||
|
25ae54cab7 | ||
|
a67576b447 | ||
|
4d181eef8e | ||
|
1835a91467 | ||
|
bcc61b0d8b | ||
|
f8055e7976 | ||
|
2509406d1c | ||
|
b3d15f91e4 | ||
|
85c36df2a3 | ||
|
6ef79f5213 | ||
|
b576014d07 | ||
|
1f37318f79 | ||
|
6950966b06 | ||
|
8eacf67725 | ||
|
52120e7a38 | ||
|
45a3f82c8d | ||
|
1490828069 | ||
|
9bdad6bb67 | ||
|
f24063cfea | ||
|
1defed27a0 | ||
|
34d6a12d95 | ||
|
5d3de967f0 | ||
|
8b73f9da17 | ||
|
820099622e | ||
|
e9c9a51b8d | ||
|
366d39a7a3 | ||
|
853a14c6b8 | ||
|
15ca68f7e1 | ||
|
8b9548a463 | ||
|
6688120aee | ||
|
93e4e723fa | ||
|
8b74e50c50 | ||
|
129a644781 | ||
|
bbfbd4a105 | ||
|
9d31d990fc | ||
|
c7f15c42fa | ||
|
515d401746 | ||
|
2a03b452d3 | ||
|
7aa8c765f6 | ||
|
038f65aae6 | ||
|
db24828a5a | ||
|
c7693d0ec3 | ||
|
5e2afd4b4d | ||
|
7a21312daf | ||
|
051a1405e7 | ||
|
a6669ed876 | ||
|
f1b00436aa | ||
|
e699103d3e | ||
|
ba9cb88ca3 | ||
|
365850d922 | ||
|
46ed17c99e | ||
|
fadff798a7 | ||
|
81512bb3b7 | ||
|
3dd00dd91a | ||
|
fd97c5085b | ||
|
fa6a249fb4 | ||
|
0aa9b1735b | ||
|
6027bee3b8 | ||
|
4d72787c83 | ||
|
c6740cfea0 | ||
|
9c1d585c43 | ||
|
863acf988e | ||
|
a6b3beafbb | ||
|
2ffc3f497b | ||
|
f3a279be26 | ||
|
9ad6631747 | ||
|
0131f5e341 | ||
|
b5ab9a8da6 | ||
|
57fa2709da | ||
|
96c6a198d7 | ||
|
d106d4bd4e | ||
|
53cd3091f7 | ||
|
f1e7b870aa | ||
|
99fe076b5a | ||
|
65fcaa17d9 | ||
|
89c6563e00 | ||
|
76b0bef32e | ||
|
c20aa0b256 | ||
|
b4908cfcb4 | ||
|
4fb5b04d27 | ||
|
8385bbb0a0 | ||
|
cee6b54033 | ||
|
0dd591a5ff | ||
|
62278126e4 | ||
|
0aa85a3701 | ||
|
0e1ba64836 | ||
|
f7e1ce8656 | ||
|
5030c14dc2 | ||
|
1333cd1d84 | ||
|
112c259d27 | ||
|
130d1e1756 | ||
|
a7df9fa625 | ||
|
9064aedc89 | ||
|
fda5d23d32 | ||
|
60be51dbe0 | ||
|
e9f451339f | ||
|
4d8ffd05a9 | ||
|
b630105572 | ||
|
9fa71f847f | ||
|
f70a9c6974 | ||
|
a4d173c733 | ||
|
2eb7712e09 | ||
|
54923b7640 | ||
|
bb927505fe | ||
|
5e66e314d2 | ||
|
6fe791c1f1 | ||
|
860c537f81 | ||
|
a352e4cbf7 | ||
|
5322d446bd | ||
|
604ab0afd8 | ||
|
3d87a88d3d | ||
|
10f9e22a8e | ||
|
8edda0cdda | ||
|
21047afc02 | ||
|
2e9793ffb2 | ||
|
fcd100df39 | ||
|
dfd564a3a4 | ||
|
a43c916009 | ||
|
c8332ca9bf | ||
|
e98170f921 | ||
|
b8f25406cd | ||
|
76dcc12b13 | ||
|
baa2228c9b | ||
|
5275ae8e9c | ||
|
c71e1e107e | ||
|
8ab72c7e10 | ||
|
a8970df91b | ||
|
2468251f56 | ||
|
6e74f3e40e | ||
|
407f84a4bb | ||
|
91632f0adb | ||
|
af3c575d84 | ||
|
bf1475441d | ||
|
9268f9db1d | ||
|
600c43827a | ||
|
74092ea95b | ||
|
b67abe58e8 | ||
|
678647f39a | ||
|
453956172b | ||
|
b550c32f9b | ||
|
f6b886adbc | ||
|
9642453052 | ||
|
64fca99c26 | ||
|
c7da43f50d | ||
|
6efa2dd9ba | ||
|
5e980c5fe0 | ||
|
c8c7a415ea | ||
|
c3cfb8d23b | ||
|
1b055f0316 | ||
|
1fcbf0b363 | ||
|
61dbc81765 | ||
|
b8b76dfa40 | ||
|
297b314904 | ||
|
55dd1ab0a1 | ||
|
8c803f1c4b | ||
|
3b942049a2 | ||
|
f78fd212bb | ||
|
f931ebece8 | ||
|
ea0a9763bf | ||
|
b59e47dcf9 | ||
|
ce09ef8848 | ||
|
188727daba | ||
|
1150633fef | ||
|
62ae845f4b | ||
|
0757fd741e | ||
|
a07fa8ccd2 | ||
|
c77f32e696 | ||
|
4391771416 | ||
|
01ab820459 | ||
|
c7218f2856 | ||
|
592221b4bf | ||
|
836458ad85 | ||
|
7233c86f3d | ||
|
154b1b05e4 | ||
|
4d88638d4d | ||
|
9403986643 | ||
|
ad48610b2f | ||
|
63487cf3ec | ||
|
4ae2087c2e | ||
|
5179129a6b | ||
|
6a00d8c88c | ||
|
50f43f9396 | ||
|
6f205f8931 | ||
|
3776ffa49b | ||
|
318b5beac1 | ||
|
344c5d6d12 | ||
|
4d319a8caa | ||
|
74b24a0690 | ||
|
1ca0464957 | ||
|
f7ebc8a88c | ||
|
a102099ac1 | ||
|
59ac22aa05 | ||
|
cd7244b3d7 | ||
|
f81b676abe | ||
|
234b154053 | ||
|
a1d09ad574 | ||
|
0538c2f478 | ||
|
77a0179822 |
32
.github/ISSUE_TEMPLATE/BUG_REPORT.yml
vendored
32
.github/ISSUE_TEMPLATE/BUG_REPORT.yml
vendored
@ -1,17 +1,28 @@
|
||||
name: Bug report
|
||||
description: Create a new bug report
|
||||
title: '[Bug]: '
|
||||
description: "Create a new bug report."
|
||||
title: "[Bug]: "
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: >-
|
||||
# 💎 Bounty program (with
|
||||
[algora.io](https://console.algora.io/org/coollabsio/bounties/new))
|
||||
|
||||
|
||||
If you would like to prioritize the issue resolution, you can add bounty
|
||||
to this issue.
|
||||
|
||||
|
||||
Click [here](https://console.algora.io/org/coollabsio/bounties/new) to
|
||||
get started.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Description
|
||||
description: A clear and concise description of the problem
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Minimal Reproduction (if possible, example repository)
|
||||
description: Please provide a step by step guide to reproduce the issue
|
||||
description: Please provide a step by step guide to reproduce the issue.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
@ -21,6 +32,15 @@ body:
|
||||
- type: input
|
||||
attributes:
|
||||
label: Version
|
||||
description: Coolify's version (see bottom left corner).
|
||||
description: Coolify's version (see top of your screen).
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Cloud?
|
||||
description: "Are you using the cloud version of Coolify?"
|
||||
options:
|
||||
- label: 'Yes'
|
||||
required: false
|
||||
- label: 'No'
|
||||
required: false
|
||||
|
1
.github/pull_request_template.md
vendored
Normal file
1
.github/pull_request_template.md
vendored
Normal file
@ -0,0 +1 @@
|
||||
> Always use `next` branch as destination branch for PRs, not `main`
|
20
.github/workflows/coolify-helper-next.yml
vendored
20
.github/workflows/coolify-helper-next.yml
vendored
@ -18,15 +18,15 @@ jobs:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Login to ghcr.io
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build image and push to registry
|
||||
uses: docker/build-push-action@v3
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
no-cache: true
|
||||
context: .
|
||||
@ -40,15 +40,15 @@ jobs:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Login to ghcr.io
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build image and push to registry
|
||||
uses: docker/build-push-action@v3
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
no-cache: true
|
||||
context: .
|
||||
@ -64,13 +64,13 @@ jobs:
|
||||
needs: [ amd64, aarch64 ]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Login to ghcr.io
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
|
20
.github/workflows/coolify-helper.yml
vendored
20
.github/workflows/coolify-helper.yml
vendored
@ -18,15 +18,15 @@ jobs:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Login to ghcr.io
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build image and push to registry
|
||||
uses: docker/build-push-action@v3
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
no-cache: true
|
||||
context: .
|
||||
@ -40,15 +40,15 @@ jobs:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Login to ghcr.io
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build image and push to registry
|
||||
uses: docker/build-push-action@v3
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
no-cache: true
|
||||
context: .
|
||||
@ -64,13 +64,13 @@ jobs:
|
||||
needs: [ amd64, aarch64 ]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Login to ghcr.io
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
|
20
.github/workflows/coolify-testing-host.yml
vendored
20
.github/workflows/coolify-testing-host.yml
vendored
@ -18,15 +18,15 @@ jobs:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Login to ghcr.io
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build image and push to registry
|
||||
uses: docker/build-push-action@v3
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
no-cache: true
|
||||
context: .
|
||||
@ -40,15 +40,15 @@ jobs:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Login to ghcr.io
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build image and push to registry
|
||||
uses: docker/build-push-action@v3
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
no-cache: true
|
||||
context: .
|
||||
@ -64,13 +64,13 @@ jobs:
|
||||
needs: [ amd64, aarch64 ]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Login to ghcr.io
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
|
4
.github/workflows/development-build.yml
vendored
4
.github/workflows/development-build.yml
vendored
@ -26,7 +26,7 @@ jobs:
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: docker/prod-ssu/Dockerfile
|
||||
file: docker/prod/Dockerfile
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}
|
||||
@ -47,7 +47,7 @@ jobs:
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: docker/prod-ssu/Dockerfile
|
||||
file: docker/prod/Dockerfile
|
||||
platforms: linux/aarch64
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}-aarch64
|
||||
|
2
.github/workflows/docker-image.yml
vendored
2
.github/workflows/docker-image.yml
vendored
@ -14,7 +14,7 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
|
25
.github/workflows/fix-php-code-style-issues.yml
vendored
Normal file
25
.github/workflows/fix-php-code-style-issues.yml
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
name: Fix PHP code style issues
|
||||
|
||||
on: [push]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
php-code-styling:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
|
||||
- name: Fix PHP code style issues
|
||||
uses: aglipanci/laravel-pint-action@2.4
|
||||
|
||||
- name: Commit changes
|
||||
uses: stefanzweifel/git-auto-commit-action@v5
|
||||
with:
|
||||
commit_message: Fix styling
|
6
.github/workflows/production-build.yml
vendored
6
.github/workflows/production-build.yml
vendored
@ -3,6 +3,8 @@ name: Production Build (v4)
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
paths-ignore:
|
||||
- templates/service-templates.json
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
@ -27,7 +29,7 @@ jobs:
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: docker/prod-ssu/Dockerfile
|
||||
file: docker/prod/Dockerfile
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}
|
||||
@ -49,7 +51,7 @@ jobs:
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: docker/prod-ssu/Dockerfile
|
||||
file: docker/prod/Dockerfile
|
||||
platforms: linux/aarch64
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64
|
||||
|
@ -3,7 +3,7 @@ tasks:
|
||||
# Fix because of https://github.com/gitpod-io/gitpod/issues/16614
|
||||
before: sudo curl -o /usr/local/bin/docker-compose -fsSL https://github.com/docker/compose/releases/download/v2.16.0/docker-compose-linux-$(uname -m)
|
||||
init: |
|
||||
cp .env.example .env &&
|
||||
cp .env.development.example .env &&
|
||||
sed -i "s#APP_URL=http://localhost#APP_URL=$(gp url 8000)#g" .env
|
||||
sed -i "s#USERID=#USERID=33333#g" .env
|
||||
sed -i "s#GROUPID=#GROUPID=33333#g" .env
|
||||
@ -20,7 +20,7 @@ tasks:
|
||||
echo "Waiting for Sail environment to boot up."
|
||||
gp sync-await spin-is-ready
|
||||
./vendor/bin/spin exec vite npm install
|
||||
./vendor/bin/spin exec vite npm run dev
|
||||
./vendor/bin/spin exec vite npm run dev -- --host
|
||||
|
||||
- name: Laravel Queue Worker, listening to code changes
|
||||
command: |
|
||||
|
@ -1,22 +0,0 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
$email = 'test@example.com';
|
||||
$user = User::whereEmail($email)->first();
|
||||
$teams = $user->teams;
|
||||
foreach ($teams as $team) {
|
||||
$servers = $team->servers;
|
||||
if ($servers->count() > 0) {
|
||||
foreach ($servers as $server) {
|
||||
dump($server);
|
||||
$server->delete();
|
||||
}
|
||||
}
|
||||
dump($team);
|
||||
$team->delete();
|
||||
}
|
||||
if ($user) {
|
||||
dump($user);
|
||||
$user->delete();
|
||||
}
|
34
CONTRIBUTION.md
Normal file
34
CONTRIBUTION.md
Normal file
@ -0,0 +1,34 @@
|
||||
# Contributing
|
||||
|
||||
> "First, thanks for considering to contribute to my project.
|
||||
It really means a lot!" - [@andrasbacsai](https://github.com/andrasbacsai)
|
||||
|
||||
You can ask for guidance anytime on our
|
||||
[Discord server](https://coollabs.io/discord) in the `#contribution` channel.
|
||||
|
||||
## Code Contribution
|
||||
|
||||
### 1) Setup your development environment
|
||||
|
||||
- You need to have Docker Engine (or equivalent) [installed](https://docs.docker.com/engine/install/) on your system.
|
||||
- For better DX, install [Spin](https://serversideup.net/open-source/spin/).
|
||||
|
||||
### 2) Set your environment variables
|
||||
|
||||
- Copy [.env.development.example](./.env.development.example) to .env.
|
||||
|
||||
## 3) Start & setup Coolify
|
||||
|
||||
- Run `spin up` - You can notice that errors will be thrown. Don't worry.
|
||||
- If you see weird permission errors, especially on Mac, run `sudo spin up` instead.
|
||||
|
||||
### 4) Start development
|
||||
You can login your Coolify instance at `localhost:8000` with `test@example.com` and `password`.
|
||||
|
||||
Your horizon (Laravel scheduler): `localhost:8000/horizon` - Only reachable if you logged in with root user.
|
||||
|
||||
Mails are caught by Mailpit: `localhost:8025`
|
||||
|
||||
## New Service Contribution
|
||||
Check out the docs [here](https://coolify.io/docs/knowledge-base/add-a-service).
|
||||
|
16
SECURITY.md
Normal file
16
SECURITY.md
Normal file
@ -0,0 +1,16 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Use this section to tell people about which versions of your project are
|
||||
currently being supported with security updates.
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| > 4 | :white_check_mark: |
|
||||
| 3 | :x: |
|
||||
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you have any vulnerability please report at hi@coollabs.io
|
16
app/Actions/Application/LoadComposeFile.php
Normal file
16
app/Actions/Application/LoadComposeFile.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Application;
|
||||
|
||||
use App\Models\Application;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class LoadComposeFile
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(Application $application)
|
||||
{
|
||||
$application->loadComposeFile();
|
||||
}
|
||||
}
|
@ -3,17 +3,17 @@
|
||||
namespace App\Actions\Application;
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Notifications\Application\StatusChanged;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class StopApplication
|
||||
{
|
||||
use AsAction;
|
||||
public function handle(Application $application)
|
||||
|
||||
public function handle(Application $application, bool $previewDeployments = false)
|
||||
{
|
||||
if ($application->destination->server->isSwarm()) {
|
||||
instant_remote_process(["docker stack rm {$application->uuid}"], $application->destination->server);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@ -23,10 +23,15 @@ public function handle(Application $application)
|
||||
$servers->push($server);
|
||||
});
|
||||
foreach ($servers as $server) {
|
||||
if (!$server->isFunctional()) {
|
||||
if (! $server->isFunctional()) {
|
||||
return 'Server is not functional';
|
||||
}
|
||||
if ($previewDeployments) {
|
||||
$containers = getCurrentApplicationContainerStatus($server, $application->id, includePullrequests: true);
|
||||
} else {
|
||||
$containers = getCurrentApplicationContainerStatus($server, $application->id, 0);
|
||||
}
|
||||
ray($containers);
|
||||
if ($containers->count() > 0) {
|
||||
foreach ($containers as $container) {
|
||||
$containerName = data_get($container, 'Names');
|
||||
@ -38,6 +43,12 @@ public function handle(Application $application)
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($application->build_pack === 'dockercompose') {
|
||||
// remove network
|
||||
$uuid = $application->uuid;
|
||||
instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false);
|
||||
instant_remote_process(["docker network rm {$uuid}"], $server, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,12 +9,13 @@
|
||||
class StopApplicationOneServer
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(Application $application, Server $server)
|
||||
{
|
||||
if ($application->destination->server->isSwarm()) {
|
||||
return;
|
||||
}
|
||||
if (!$server->isFunctional()) {
|
||||
if (! $server->isFunctional()) {
|
||||
return 'Server is not functional';
|
||||
}
|
||||
try {
|
||||
@ -32,6 +33,7 @@ public function handle(Application $application, Server $server)
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
ray($e->getMessage());
|
||||
|
||||
return $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
namespace App\Actions\CoolifyTask;
|
||||
|
||||
use App\Data\CoolifyTaskArgs;
|
||||
use App\Enums\ActivityTypes;
|
||||
use App\Jobs\CoolifyTask;
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
|
||||
@ -14,6 +15,7 @@
|
||||
class PrepareCoolifyTask
|
||||
{
|
||||
protected Activity $activity;
|
||||
|
||||
protected CoolifyTaskArgs $remoteProcessArgs;
|
||||
|
||||
public function __construct(CoolifyTaskArgs $remoteProcessArgs)
|
||||
@ -28,20 +30,31 @@ public function __construct(CoolifyTaskArgs $remoteProcessArgs)
|
||||
->withProperties($properties)
|
||||
->performedOn($remoteProcessArgs->model)
|
||||
->event($remoteProcessArgs->type)
|
||||
->log("[]");
|
||||
->log('[]');
|
||||
} else {
|
||||
$this->activity = activity()
|
||||
->withProperties($remoteProcessArgs->toArray())
|
||||
->event($remoteProcessArgs->type)
|
||||
->log("[]");
|
||||
->log('[]');
|
||||
}
|
||||
}
|
||||
|
||||
public function __invoke(): Activity
|
||||
{
|
||||
$job = new CoolifyTask($this->activity, ignore_errors: $this->remoteProcessArgs->ignore_errors, call_event_on_finish: $this->remoteProcessArgs->call_event_on_finish);
|
||||
$job = new CoolifyTask(
|
||||
activity: $this->activity,
|
||||
ignore_errors: $this->remoteProcessArgs->ignore_errors,
|
||||
call_event_on_finish: $this->remoteProcessArgs->call_event_on_finish,
|
||||
call_event_data: $this->remoteProcessArgs->call_event_data,
|
||||
);
|
||||
if ($this->remoteProcessArgs->type === ActivityTypes::COMMAND->value) {
|
||||
ray('Dispatching a high priority job');
|
||||
dispatch($job)->onQueue('high');
|
||||
} else {
|
||||
dispatch($job);
|
||||
}
|
||||
$this->activity->refresh();
|
||||
|
||||
return $this->activity;
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,8 @@ class RunRemoteProcess
|
||||
|
||||
public $call_event_on_finish = null;
|
||||
|
||||
public $call_event_data = null;
|
||||
|
||||
protected $time_start;
|
||||
|
||||
protected $current_time;
|
||||
@ -34,10 +36,10 @@ class RunRemoteProcess
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(Activity $activity, bool $hide_from_output = false, bool $ignore_errors = false, $call_event_on_finish = null)
|
||||
public function __construct(Activity $activity, bool $hide_from_output = false, bool $ignore_errors = false, $call_event_on_finish = null, $call_event_data = null)
|
||||
{
|
||||
|
||||
if ($activity->getExtraProperty('type') !== ActivityTypes::INLINE->value) {
|
||||
if ($activity->getExtraProperty('type') !== ActivityTypes::INLINE->value && $activity->getExtraProperty('type') !== ActivityTypes::COMMAND->value) {
|
||||
throw new \RuntimeException('Incompatible Activity to run a remote command.');
|
||||
}
|
||||
|
||||
@ -45,6 +47,7 @@ public function __construct(Activity $activity, bool $hide_from_output = false,
|
||||
$this->hide_from_output = $hide_from_output;
|
||||
$this->ignore_errors = $ignore_errors;
|
||||
$this->call_event_on_finish = $call_event_on_finish;
|
||||
$this->call_event_data = $call_event_data;
|
||||
}
|
||||
|
||||
public static function decodeOutput(?Activity $activity = null): string
|
||||
@ -57,7 +60,7 @@ public static function decodeOutput(?Activity $activity = null): string
|
||||
$decoded = json_decode(
|
||||
data_get($activity, 'description'),
|
||||
associative: true,
|
||||
flags: JSON_THROW_ON_ERROR
|
||||
flags: JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE
|
||||
);
|
||||
} catch (\JsonException $exception) {
|
||||
return '';
|
||||
@ -66,7 +69,7 @@ public static function decodeOutput(?Activity $activity = null): string
|
||||
return collect($decoded)
|
||||
->sortBy(fn ($i) => $i['order'])
|
||||
->map(fn ($i) => $i['output'])
|
||||
->implode("");
|
||||
->implode('');
|
||||
}
|
||||
|
||||
public function __invoke(): ProcessResult
|
||||
@ -88,7 +91,7 @@ public function __invoke(): ProcessResult
|
||||
if ($processResult->exitCode() == 0) {
|
||||
$status = ProcessStatus::FINISHED;
|
||||
}
|
||||
if ($processResult->exitCode() != 0 && !$this->ignore_errors) {
|
||||
if ($processResult->exitCode() != 0 && ! $this->ignore_errors) {
|
||||
$status = ProcessStatus::ERROR;
|
||||
}
|
||||
// if (($processResult->exitCode() == 0 && $this->is_finished) || $this->activity->properties->get('status') === ProcessStatus::FINISHED->value) {
|
||||
@ -106,18 +109,25 @@ public function __invoke(): ProcessResult
|
||||
'status' => $status->value,
|
||||
]);
|
||||
$this->activity->save();
|
||||
if ($processResult->exitCode() != 0 && !$this->ignore_errors) {
|
||||
if ($processResult->exitCode() != 0 && ! $this->ignore_errors) {
|
||||
throw new \RuntimeException($processResult->errorOutput(), $processResult->exitCode());
|
||||
}
|
||||
if ($this->call_event_on_finish) {
|
||||
try {
|
||||
if ($this->call_event_data) {
|
||||
event(resolve("App\\Events\\$this->call_event_on_finish", [
|
||||
'data' => $this->call_event_data,
|
||||
]));
|
||||
} else {
|
||||
event(resolve("App\\Events\\$this->call_event_on_finish", [
|
||||
'userId' => $this->activity->causer_id,
|
||||
]));
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
ray($e);
|
||||
}
|
||||
}
|
||||
|
||||
return $processResult;
|
||||
}
|
||||
|
||||
@ -155,8 +165,7 @@ protected function elapsedTime(): int
|
||||
|
||||
public function encodeOutput($type, $output)
|
||||
{
|
||||
$outputStack = json_decode($this->activity->description, associative: true, flags: JSON_THROW_ON_ERROR);
|
||||
|
||||
$outputStack = json_decode($this->activity->description, associative: true, flags: JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
|
||||
$outputStack[] = [
|
||||
'type' => $type,
|
||||
'output' => $output,
|
||||
@ -165,15 +174,16 @@ public function encodeOutput($type, $output)
|
||||
'order' => $this->getLatestCounter(),
|
||||
];
|
||||
|
||||
return json_encode($outputStack, flags: JSON_THROW_ON_ERROR);
|
||||
return json_encode($outputStack, flags: JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
|
||||
protected function getLatestCounter(): int
|
||||
{
|
||||
$description = json_decode($this->activity->description, associative: true, flags: JSON_THROW_ON_ERROR);
|
||||
$description = json_decode($this->activity->description, associative: true, flags: JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
|
||||
if ($description === null || count($description) === 0) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return end($description)['order'] + 1;
|
||||
}
|
||||
|
||||
|
29
app/Actions/Database/RestartDatabase.php
Normal file
29
app/Actions/Database/RestartDatabase.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Database;
|
||||
|
||||
use App\Models\StandaloneClickhouse;
|
||||
use App\Models\StandaloneDragonfly;
|
||||
use App\Models\StandaloneKeydb;
|
||||
use App\Models\StandaloneMariadb;
|
||||
use App\Models\StandaloneMongodb;
|
||||
use App\Models\StandaloneMysql;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\StandaloneRedis;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class RestartDatabase
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $database)
|
||||
{
|
||||
$server = $database->destination->server;
|
||||
if (! $server->isFunctional()) {
|
||||
return 'Server is not functional';
|
||||
}
|
||||
StopDatabase::run($database);
|
||||
|
||||
return StartDatabase::run($database);
|
||||
}
|
||||
}
|
167
app/Actions/Database/StartClickhouse.php
Normal file
167
app/Actions/Database/StartClickhouse.php
Normal file
@ -0,0 +1,167 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Database;
|
||||
|
||||
use App\Models\StandaloneClickhouse;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
class StartClickhouse
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public StandaloneClickhouse $database;
|
||||
|
||||
public array $commands = [];
|
||||
|
||||
public string $configuration_dir;
|
||||
|
||||
public function handle(StandaloneClickhouse $database)
|
||||
{
|
||||
$this->database = $database;
|
||||
|
||||
$container_name = $this->database->uuid;
|
||||
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
||||
|
||||
$this->commands = [
|
||||
"echo 'Starting {$database->name}.'",
|
||||
"mkdir -p $this->configuration_dir",
|
||||
];
|
||||
|
||||
$persistent_storages = $this->generate_local_persistent_volumes();
|
||||
$persistent_file_volumes = $this->database->fileStorages()->get();
|
||||
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
|
||||
$environment_variables = $this->generate_environment_variables();
|
||||
|
||||
$docker_compose = [
|
||||
'services' => [
|
||||
$container_name => [
|
||||
'image' => $this->database->image,
|
||||
'container_name' => $container_name,
|
||||
'environment' => $environment_variables,
|
||||
'restart' => RESTART_MODE,
|
||||
'networks' => [
|
||||
$this->database->destination->network,
|
||||
],
|
||||
'ulimits' => [
|
||||
'nofile' => [
|
||||
'soft' => 262144,
|
||||
'hard' => 262144,
|
||||
],
|
||||
],
|
||||
'labels' => [
|
||||
'coolify.managed' => 'true',
|
||||
],
|
||||
'healthcheck' => [
|
||||
'test' => "clickhouse-client --password {$this->database->clickhouse_admin_password} --query 'SELECT 1'",
|
||||
'interval' => '5s',
|
||||
'timeout' => '5s',
|
||||
'retries' => 10,
|
||||
'start_period' => '5s',
|
||||
],
|
||||
'mem_limit' => $this->database->limits_memory,
|
||||
'memswap_limit' => $this->database->limits_memory_swap,
|
||||
'mem_swappiness' => $this->database->limits_memory_swappiness,
|
||||
'mem_reservation' => $this->database->limits_memory_reservation,
|
||||
'cpus' => (float) $this->database->limits_cpus,
|
||||
'cpu_shares' => $this->database->limits_cpu_shares,
|
||||
],
|
||||
],
|
||||
'networks' => [
|
||||
$this->database->destination->network => [
|
||||
'external' => true,
|
||||
'name' => $this->database->destination->network,
|
||||
'attachable' => true,
|
||||
],
|
||||
],
|
||||
];
|
||||
if (! is_null($this->database->limits_cpuset)) {
|
||||
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
|
||||
}
|
||||
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
||||
$docker_compose['services'][$container_name]['logging'] = [
|
||||
'driver' => 'fluentd',
|
||||
'options' => [
|
||||
'fluentd-address' => 'tcp://127.0.0.1:24224',
|
||||
'fluentd-async' => 'true',
|
||||
'fluentd-sub-second-precision' => 'true',
|
||||
],
|
||||
];
|
||||
}
|
||||
if (count($this->database->ports_mappings_array) > 0) {
|
||||
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
|
||||
}
|
||||
if (count($persistent_storages) > 0) {
|
||||
$docker_compose['services'][$container_name]['volumes'] = $persistent_storages;
|
||||
}
|
||||
if (count($persistent_file_volumes) > 0) {
|
||||
$docker_compose['services'][$container_name]['volumes'] = $persistent_file_volumes->map(function ($item) {
|
||||
return "$item->fs_path:$item->mount_path";
|
||||
})->toArray();
|
||||
}
|
||||
if (count($volume_names) > 0) {
|
||||
$docker_compose['volumes'] = $volume_names;
|
||||
}
|
||||
$docker_compose = Yaml::dump($docker_compose, 10);
|
||||
$docker_compose_base64 = base64_encode($docker_compose);
|
||||
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
|
||||
$readme = generate_readme_file($this->database->name, now());
|
||||
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
|
||||
$this->commands[] = "echo 'Pulling {$database->image} image.'";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
||||
$this->commands[] = "echo 'Database started.'";
|
||||
|
||||
return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged');
|
||||
}
|
||||
|
||||
private function generate_local_persistent_volumes()
|
||||
{
|
||||
$local_persistent_volumes = [];
|
||||
foreach ($this->database->persistentStorages as $persistentStorage) {
|
||||
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
|
||||
$local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path;
|
||||
} else {
|
||||
$volume_name = $persistentStorage->name;
|
||||
$local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path;
|
||||
}
|
||||
}
|
||||
|
||||
return $local_persistent_volumes;
|
||||
}
|
||||
|
||||
private function generate_local_persistent_volumes_only_volume_names()
|
||||
{
|
||||
$local_persistent_volumes_names = [];
|
||||
foreach ($this->database->persistentStorages as $persistentStorage) {
|
||||
if ($persistentStorage->host_path) {
|
||||
continue;
|
||||
}
|
||||
$name = $persistentStorage->name;
|
||||
$local_persistent_volumes_names[$name] = [
|
||||
'name' => $name,
|
||||
'external' => false,
|
||||
];
|
||||
}
|
||||
|
||||
return $local_persistent_volumes_names;
|
||||
}
|
||||
|
||||
private function generate_environment_variables()
|
||||
{
|
||||
$environment_variables = collect();
|
||||
foreach ($this->database->runtime_environment_variables as $env) {
|
||||
$environment_variables->push("$env->key=$env->real_value");
|
||||
}
|
||||
|
||||
if ($environment_variables->filter(fn ($env) => str($env)->contains('CLICKHOUSE_ADMIN_USER'))->isEmpty()) {
|
||||
$environment_variables->push("CLICKHOUSE_ADMIN_USER={$this->database->clickhouse_admin_user}");
|
||||
}
|
||||
|
||||
if ($environment_variables->filter(fn ($env) => str($env)->contains('CLICKHOUSE_ADMIN_PASSWORD'))->isEmpty()) {
|
||||
$environment_variables->push("CLICKHOUSE_ADMIN_PASSWORD={$this->database->clickhouse_admin_password}");
|
||||
}
|
||||
|
||||
return $environment_variables->all();
|
||||
}
|
||||
}
|
57
app/Actions/Database/StartDatabase.php
Normal file
57
app/Actions/Database/StartDatabase.php
Normal file
@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Database;
|
||||
|
||||
use App\Models\StandaloneClickhouse;
|
||||
use App\Models\StandaloneDragonfly;
|
||||
use App\Models\StandaloneKeydb;
|
||||
use App\Models\StandaloneMariadb;
|
||||
use App\Models\StandaloneMongodb;
|
||||
use App\Models\StandaloneMysql;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\StandaloneRedis;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class StartDatabase
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $database)
|
||||
{
|
||||
$server = $database->destination->server;
|
||||
if (! $server->isFunctional()) {
|
||||
return 'Server is not functional';
|
||||
}
|
||||
switch ($database->getMorphClass()) {
|
||||
case 'App\Models\StandalonePostgresql':
|
||||
$activity = StartPostgresql::run($database);
|
||||
break;
|
||||
case 'App\Models\StandaloneRedis':
|
||||
$activity = StartRedis::run($database);
|
||||
break;
|
||||
case 'App\Models\StandaloneMongodb':
|
||||
$activity = StartMongodb::run($database);
|
||||
break;
|
||||
case 'App\Models\StandaloneMysql':
|
||||
$activity = StartMysql::run($database);
|
||||
break;
|
||||
case 'App\Models\StandaloneMariadb':
|
||||
$activity = StartMariadb::run($database);
|
||||
break;
|
||||
case 'App\Models\StandaloneKeydb':
|
||||
$activity = StartKeydb::run($database);
|
||||
break;
|
||||
case 'App\Models\StandaloneDragonfly':
|
||||
$activity = StartDragonfly::run($database);
|
||||
break;
|
||||
case 'App\Models\StandaloneClickhouse':
|
||||
$activity = StartClickhouse::run($database);
|
||||
break;
|
||||
}
|
||||
if ($database->is_public && $database->public_port) {
|
||||
StartDatabaseProxy::dispatch($database);
|
||||
}
|
||||
|
||||
return $activity;
|
||||
}
|
||||
}
|
@ -3,6 +3,9 @@
|
||||
namespace App\Actions\Database;
|
||||
|
||||
use App\Models\ServiceDatabase;
|
||||
use App\Models\StandaloneClickhouse;
|
||||
use App\Models\StandaloneDragonfly;
|
||||
use App\Models\StandaloneKeydb;
|
||||
use App\Models\StandaloneMariadb;
|
||||
use App\Models\StandaloneMongodb;
|
||||
use App\Models\StandaloneMysql;
|
||||
@ -15,7 +18,7 @@ class StartDatabaseProxy
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|ServiceDatabase $database)
|
||||
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse|ServiceDatabase $database)
|
||||
{
|
||||
$internalPort = null;
|
||||
$type = $database->getMorphClass();
|
||||
@ -25,7 +28,8 @@ public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|St
|
||||
$proxyContainerName = "{$database->uuid}-proxy";
|
||||
if ($database->getMorphClass() === 'App\Models\ServiceDatabase') {
|
||||
$databaseType = $database->databaseType();
|
||||
$network = data_get($database, 'service.destination.network');
|
||||
// $connectPredefined = data_get($database, 'service.connect_to_docker_network');
|
||||
$network = $database->service->uuid;
|
||||
$server = data_get($database, 'service.destination.server');
|
||||
$proxyContainerName = "{$database->service->uuid}-proxy";
|
||||
switch ($databaseType) {
|
||||
@ -49,18 +53,36 @@ public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|St
|
||||
$type = 'App\Models\StandaloneRedis';
|
||||
$containerName = "redis-{$database->service->uuid}";
|
||||
break;
|
||||
case 'standalone-keydb':
|
||||
$type = 'App\Models\StandaloneKeydb';
|
||||
$containerName = "keydb-{$database->service->uuid}";
|
||||
break;
|
||||
case 'standalone-dragonfly':
|
||||
$type = 'App\Models\StandaloneDragonfly';
|
||||
$containerName = "dragonfly-{$database->service->uuid}";
|
||||
break;
|
||||
case 'standalone-clickhouse':
|
||||
$type = 'App\Models\StandaloneClickhouse';
|
||||
$containerName = "clickhouse-{$database->service->uuid}";
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($type === 'App\Models\StandaloneRedis') {
|
||||
$internalPort = 6379;
|
||||
} else if ($type === 'App\Models\StandalonePostgresql') {
|
||||
} elseif ($type === 'App\Models\StandalonePostgresql') {
|
||||
$internalPort = 5432;
|
||||
} else if ($type === 'App\Models\StandaloneMongodb') {
|
||||
} elseif ($type === 'App\Models\StandaloneMongodb') {
|
||||
$internalPort = 27017;
|
||||
} else if ($type === 'App\Models\StandaloneMysql') {
|
||||
} elseif ($type === 'App\Models\StandaloneMysql') {
|
||||
$internalPort = 3306;
|
||||
} else if ($type === 'App\Models\StandaloneMariadb') {
|
||||
} elseif ($type === 'App\Models\StandaloneMariadb') {
|
||||
$internalPort = 3306;
|
||||
} elseif ($type === 'App\Models\StandaloneKeydb') {
|
||||
$internalPort = 6379;
|
||||
} elseif ($type === 'App\Models\StandaloneDragonfly') {
|
||||
$internalPort = 6379;
|
||||
} elseif ($type === 'App\Models\StandaloneClickhouse') {
|
||||
$internalPort = 9000;
|
||||
}
|
||||
$configuration_dir = database_proxy_dir($database->uuid);
|
||||
$nginxconf = <<<EOF
|
||||
@ -79,20 +101,19 @@ public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|St
|
||||
}
|
||||
}
|
||||
EOF;
|
||||
$dockerfile = <<< EOF
|
||||
$dockerfile = <<< 'EOF'
|
||||
FROM nginx:stable-alpine
|
||||
|
||||
COPY nginx.conf /etc/nginx/nginx.conf
|
||||
EOF;
|
||||
$docker_compose = [
|
||||
'version' => '3.8',
|
||||
'services' => [
|
||||
$proxyContainerName => [
|
||||
'build' => [
|
||||
'context' => $configuration_dir,
|
||||
'dockerfile' => 'Dockerfile',
|
||||
],
|
||||
'image' => "nginx:stable-alpine",
|
||||
'image' => 'nginx:stable-alpine',
|
||||
'container_name' => $proxyContainerName,
|
||||
'restart' => RESTART_MODE,
|
||||
'ports' => [
|
||||
@ -109,26 +130,27 @@ public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|St
|
||||
'interval' => '5s',
|
||||
'timeout' => '5s',
|
||||
'retries' => 3,
|
||||
'start_period' => '1s'
|
||||
'start_period' => '1s',
|
||||
],
|
||||
],
|
||||
]
|
||||
],
|
||||
'networks' => [
|
||||
$network => [
|
||||
'external' => true,
|
||||
'name' => $network,
|
||||
'attachable' => true,
|
||||
]
|
||||
]
|
||||
],
|
||||
],
|
||||
];
|
||||
$dockercompose_base64 = base64_encode(Yaml::dump($docker_compose, 4, 2));
|
||||
$nginxconf_base64 = base64_encode($nginxconf);
|
||||
$dockerfile_base64 = base64_encode($dockerfile);
|
||||
instant_remote_process(["docker rm -f $proxyContainerName"], $server, false);
|
||||
instant_remote_process([
|
||||
"mkdir -p $configuration_dir",
|
||||
"echo '{$dockerfile_base64}' | base64 -d > $configuration_dir/Dockerfile",
|
||||
"echo '{$nginxconf_base64}' | base64 -d > $configuration_dir/nginx.conf",
|
||||
"echo '{$dockercompose_base64}' | base64 -d > $configuration_dir/docker-compose.yaml",
|
||||
"echo '{$dockerfile_base64}' | base64 -d | tee $configuration_dir/Dockerfile > /dev/null",
|
||||
"echo '{$nginxconf_base64}' | base64 -d | tee $configuration_dir/nginx.conf > /dev/null",
|
||||
"echo '{$dockercompose_base64}' | base64 -d | tee $configuration_dir/docker-compose.yaml > /dev/null",
|
||||
"docker compose --project-directory {$configuration_dir} pull",
|
||||
"docker compose --project-directory {$configuration_dir} up --build -d",
|
||||
], $server);
|
||||
|
163
app/Actions/Database/StartDragonfly.php
Normal file
163
app/Actions/Database/StartDragonfly.php
Normal file
@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Database;
|
||||
|
||||
use App\Models\StandaloneDragonfly;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
class StartDragonfly
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public StandaloneDragonfly $database;
|
||||
|
||||
public array $commands = [];
|
||||
|
||||
public string $configuration_dir;
|
||||
|
||||
public function handle(StandaloneDragonfly $database)
|
||||
{
|
||||
$this->database = $database;
|
||||
|
||||
$startCommand = "dragonfly --requirepass {$this->database->dragonfly_password}";
|
||||
|
||||
$container_name = $this->database->uuid;
|
||||
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
||||
|
||||
$this->commands = [
|
||||
"echo 'Starting {$database->name}.'",
|
||||
"mkdir -p $this->configuration_dir",
|
||||
];
|
||||
|
||||
$persistent_storages = $this->generate_local_persistent_volumes();
|
||||
$persistent_file_volumes = $this->database->fileStorages()->get();
|
||||
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
|
||||
$environment_variables = $this->generate_environment_variables();
|
||||
|
||||
$docker_compose = [
|
||||
'services' => [
|
||||
$container_name => [
|
||||
'image' => $this->database->image,
|
||||
'command' => $startCommand,
|
||||
'container_name' => $container_name,
|
||||
'environment' => $environment_variables,
|
||||
'restart' => RESTART_MODE,
|
||||
'networks' => [
|
||||
$this->database->destination->network,
|
||||
],
|
||||
'ulimits' => [
|
||||
'memlock' => '-1',
|
||||
],
|
||||
'labels' => [
|
||||
'coolify.managed' => 'true',
|
||||
],
|
||||
'healthcheck' => [
|
||||
'test' => "redis-cli -a {$this->database->dragonfly_password} ping",
|
||||
'interval' => '5s',
|
||||
'timeout' => '5s',
|
||||
'retries' => 10,
|
||||
'start_period' => '5s',
|
||||
],
|
||||
'mem_limit' => $this->database->limits_memory,
|
||||
'memswap_limit' => $this->database->limits_memory_swap,
|
||||
'mem_swappiness' => $this->database->limits_memory_swappiness,
|
||||
'mem_reservation' => $this->database->limits_memory_reservation,
|
||||
'cpus' => (float) $this->database->limits_cpus,
|
||||
'cpu_shares' => $this->database->limits_cpu_shares,
|
||||
],
|
||||
],
|
||||
'networks' => [
|
||||
$this->database->destination->network => [
|
||||
'external' => true,
|
||||
'name' => $this->database->destination->network,
|
||||
'attachable' => true,
|
||||
],
|
||||
],
|
||||
];
|
||||
if (! is_null($this->database->limits_cpuset)) {
|
||||
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
|
||||
}
|
||||
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
||||
$docker_compose['services'][$container_name]['logging'] = [
|
||||
'driver' => 'fluentd',
|
||||
'options' => [
|
||||
'fluentd-address' => 'tcp://127.0.0.1:24224',
|
||||
'fluentd-async' => 'true',
|
||||
'fluentd-sub-second-precision' => 'true',
|
||||
],
|
||||
];
|
||||
}
|
||||
if (count($this->database->ports_mappings_array) > 0) {
|
||||
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
|
||||
}
|
||||
if (count($persistent_storages) > 0) {
|
||||
$docker_compose['services'][$container_name]['volumes'] = $persistent_storages;
|
||||
}
|
||||
if (count($persistent_file_volumes) > 0) {
|
||||
$docker_compose['services'][$container_name]['volumes'] = $persistent_file_volumes->map(function ($item) {
|
||||
return "$item->fs_path:$item->mount_path";
|
||||
})->toArray();
|
||||
}
|
||||
if (count($volume_names) > 0) {
|
||||
$docker_compose['volumes'] = $volume_names;
|
||||
}
|
||||
$docker_compose = Yaml::dump($docker_compose, 10);
|
||||
$docker_compose_base64 = base64_encode($docker_compose);
|
||||
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
|
||||
$readme = generate_readme_file($this->database->name, now());
|
||||
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
|
||||
$this->commands[] = "echo 'Pulling {$database->image} image.'";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
||||
$this->commands[] = "echo 'Database started.'";
|
||||
|
||||
return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged');
|
||||
}
|
||||
|
||||
private function generate_local_persistent_volumes()
|
||||
{
|
||||
$local_persistent_volumes = [];
|
||||
foreach ($this->database->persistentStorages as $persistentStorage) {
|
||||
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
|
||||
$local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path;
|
||||
} else {
|
||||
$volume_name = $persistentStorage->name;
|
||||
$local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path;
|
||||
}
|
||||
}
|
||||
|
||||
return $local_persistent_volumes;
|
||||
}
|
||||
|
||||
private function generate_local_persistent_volumes_only_volume_names()
|
||||
{
|
||||
$local_persistent_volumes_names = [];
|
||||
foreach ($this->database->persistentStorages as $persistentStorage) {
|
||||
if ($persistentStorage->host_path) {
|
||||
continue;
|
||||
}
|
||||
$name = $persistentStorage->name;
|
||||
$local_persistent_volumes_names[$name] = [
|
||||
'name' => $name,
|
||||
'external' => false,
|
||||
];
|
||||
}
|
||||
|
||||
return $local_persistent_volumes_names;
|
||||
}
|
||||
|
||||
private function generate_environment_variables()
|
||||
{
|
||||
$environment_variables = collect();
|
||||
foreach ($this->database->runtime_environment_variables as $env) {
|
||||
$environment_variables->push("$env->key=$env->real_value");
|
||||
}
|
||||
|
||||
if ($environment_variables->filter(fn ($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) {
|
||||
$environment_variables->push("REDIS_PASSWORD={$this->database->dragonfly_password}");
|
||||
}
|
||||
|
||||
return $environment_variables->all();
|
||||
}
|
||||
}
|
183
app/Actions/Database/StartKeydb.php
Normal file
183
app/Actions/Database/StartKeydb.php
Normal file
@ -0,0 +1,183 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Database;
|
||||
|
||||
use App\Models\StandaloneKeydb;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
class StartKeydb
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public StandaloneKeydb $database;
|
||||
|
||||
public array $commands = [];
|
||||
|
||||
public string $configuration_dir;
|
||||
|
||||
public function handle(StandaloneKeydb $database)
|
||||
{
|
||||
$this->database = $database;
|
||||
|
||||
$startCommand = "keydb-server --requirepass {$this->database->keydb_password} --appendonly yes";
|
||||
|
||||
$container_name = $this->database->uuid;
|
||||
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
||||
|
||||
$this->commands = [
|
||||
"echo 'Starting {$database->name}.'",
|
||||
"mkdir -p $this->configuration_dir",
|
||||
];
|
||||
|
||||
$persistent_storages = $this->generate_local_persistent_volumes();
|
||||
$persistent_file_volumes = $this->database->fileStorages()->get();
|
||||
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
|
||||
$environment_variables = $this->generate_environment_variables();
|
||||
$this->add_custom_keydb();
|
||||
|
||||
$docker_compose = [
|
||||
'services' => [
|
||||
$container_name => [
|
||||
'image' => $this->database->image,
|
||||
'command' => $startCommand,
|
||||
'container_name' => $container_name,
|
||||
'environment' => $environment_variables,
|
||||
'restart' => RESTART_MODE,
|
||||
'networks' => [
|
||||
$this->database->destination->network,
|
||||
],
|
||||
'labels' => [
|
||||
'coolify.managed' => 'true',
|
||||
],
|
||||
'healthcheck' => [
|
||||
'test' => "keydb-cli --pass {$this->database->keydb_password} ping",
|
||||
'interval' => '5s',
|
||||
'timeout' => '5s',
|
||||
'retries' => 10,
|
||||
'start_period' => '5s',
|
||||
],
|
||||
'mem_limit' => $this->database->limits_memory,
|
||||
'memswap_limit' => $this->database->limits_memory_swap,
|
||||
'mem_swappiness' => $this->database->limits_memory_swappiness,
|
||||
'mem_reservation' => $this->database->limits_memory_reservation,
|
||||
'cpus' => (float) $this->database->limits_cpus,
|
||||
'cpu_shares' => $this->database->limits_cpu_shares,
|
||||
],
|
||||
],
|
||||
'networks' => [
|
||||
$this->database->destination->network => [
|
||||
'external' => true,
|
||||
'name' => $this->database->destination->network,
|
||||
'attachable' => true,
|
||||
],
|
||||
],
|
||||
];
|
||||
if (! is_null($this->database->limits_cpuset)) {
|
||||
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
|
||||
}
|
||||
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
||||
$docker_compose['services'][$container_name]['logging'] = [
|
||||
'driver' => 'fluentd',
|
||||
'options' => [
|
||||
'fluentd-address' => 'tcp://127.0.0.1:24224',
|
||||
'fluentd-async' => 'true',
|
||||
'fluentd-sub-second-precision' => 'true',
|
||||
],
|
||||
];
|
||||
}
|
||||
if (count($this->database->ports_mappings_array) > 0) {
|
||||
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
|
||||
}
|
||||
if (count($persistent_storages) > 0) {
|
||||
$docker_compose['services'][$container_name]['volumes'] = $persistent_storages;
|
||||
}
|
||||
if (count($persistent_file_volumes) > 0) {
|
||||
$docker_compose['services'][$container_name]['volumes'] = $persistent_file_volumes->map(function ($item) {
|
||||
return "$item->fs_path:$item->mount_path";
|
||||
})->toArray();
|
||||
}
|
||||
if (count($volume_names) > 0) {
|
||||
$docker_compose['volumes'] = $volume_names;
|
||||
}
|
||||
if (! is_null($this->database->keydb_conf) || ! empty($this->database->keydb_conf)) {
|
||||
$docker_compose['services'][$container_name]['volumes'][] = [
|
||||
'type' => 'bind',
|
||||
'source' => $this->configuration_dir.'/keydb.conf',
|
||||
'target' => '/etc/keydb/keydb.conf',
|
||||
'read_only' => true,
|
||||
];
|
||||
$docker_compose['services'][$container_name]['command'] = "keydb-server /etc/keydb/keydb.conf --requirepass {$this->database->keydb_password} --appendonly yes";
|
||||
}
|
||||
$docker_compose = Yaml::dump($docker_compose, 10);
|
||||
$docker_compose_base64 = base64_encode($docker_compose);
|
||||
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
|
||||
$readme = generate_readme_file($this->database->name, now());
|
||||
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
|
||||
$this->commands[] = "echo 'Pulling {$database->image} image.'";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
||||
$this->commands[] = "echo 'Database started.'";
|
||||
|
||||
return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged');
|
||||
}
|
||||
|
||||
private function generate_local_persistent_volumes()
|
||||
{
|
||||
$local_persistent_volumes = [];
|
||||
foreach ($this->database->persistentStorages as $persistentStorage) {
|
||||
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
|
||||
$local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path;
|
||||
} else {
|
||||
$volume_name = $persistentStorage->name;
|
||||
$local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path;
|
||||
}
|
||||
}
|
||||
|
||||
return $local_persistent_volumes;
|
||||
}
|
||||
|
||||
private function generate_local_persistent_volumes_only_volume_names()
|
||||
{
|
||||
$local_persistent_volumes_names = [];
|
||||
foreach ($this->database->persistentStorages as $persistentStorage) {
|
||||
if ($persistentStorage->host_path) {
|
||||
continue;
|
||||
}
|
||||
$name = $persistentStorage->name;
|
||||
$local_persistent_volumes_names[$name] = [
|
||||
'name' => $name,
|
||||
'external' => false,
|
||||
];
|
||||
}
|
||||
|
||||
return $local_persistent_volumes_names;
|
||||
}
|
||||
|
||||
private function generate_environment_variables()
|
||||
{
|
||||
$environment_variables = collect();
|
||||
foreach ($this->database->runtime_environment_variables as $env) {
|
||||
$environment_variables->push("$env->key=$env->real_value");
|
||||
}
|
||||
|
||||
if ($environment_variables->filter(fn ($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) {
|
||||
$environment_variables->push("REDIS_PASSWORD={$this->database->keydb_password}");
|
||||
}
|
||||
|
||||
return $environment_variables->all();
|
||||
}
|
||||
|
||||
private function add_custom_keydb()
|
||||
{
|
||||
if (is_null($this->database->keydb_conf) || empty($this->database->keydb_conf)) {
|
||||
return;
|
||||
}
|
||||
$filename = 'keydb.conf';
|
||||
Storage::disk('local')->put("tmp/keydb.conf_{$this->database->uuid}", $this->database->keydb_conf);
|
||||
$path = Storage::path("tmp/keydb.conf_{$this->database->uuid}");
|
||||
instant_scp($path, "{$this->configuration_dir}/{$filename}", $this->database->destination->server);
|
||||
Storage::disk('local')->delete("tmp/keydb.conf_{$this->database->uuid}");
|
||||
}
|
||||
}
|
@ -3,16 +3,17 @@
|
||||
namespace App\Actions\Database;
|
||||
|
||||
use App\Models\StandaloneMariadb;
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
class StartMariadb
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public StandaloneMariadb $database;
|
||||
|
||||
public array $commands = [];
|
||||
|
||||
public string $configuration_dir;
|
||||
|
||||
public function handle(StandaloneMariadb $database)
|
||||
@ -20,7 +21,7 @@ public function handle(StandaloneMariadb $database)
|
||||
$this->database = $database;
|
||||
|
||||
$container_name = $this->database->uuid;
|
||||
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
|
||||
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
||||
|
||||
$this->commands = [
|
||||
"echo 'Starting {$database->name}.'",
|
||||
@ -28,11 +29,11 @@ public function handle(StandaloneMariadb $database)
|
||||
];
|
||||
|
||||
$persistent_storages = $this->generate_local_persistent_volumes();
|
||||
$persistent_file_volumes = $this->database->fileStorages()->get();
|
||||
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
|
||||
$environment_variables = $this->generate_environment_variables();
|
||||
$this->add_custom_mysql();
|
||||
$docker_compose = [
|
||||
'version' => '3.8',
|
||||
'services' => [
|
||||
$container_name => [
|
||||
'image' => $this->database->image,
|
||||
@ -46,11 +47,11 @@ public function handle(StandaloneMariadb $database)
|
||||
'coolify.managed' => 'true',
|
||||
],
|
||||
'healthcheck' => [
|
||||
'test' => ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"],
|
||||
'test' => ['CMD', 'healthcheck.sh', '--connect', '--innodb_initialized'],
|
||||
'interval' => '5s',
|
||||
'timeout' => '5s',
|
||||
'retries' => 10,
|
||||
'start_period' => '5s'
|
||||
'start_period' => '5s',
|
||||
],
|
||||
'mem_limit' => $this->database->limits_memory,
|
||||
'memswap_limit' => $this->database->limits_memory_swap,
|
||||
@ -58,27 +59,27 @@ public function handle(StandaloneMariadb $database)
|
||||
'mem_reservation' => $this->database->limits_memory_reservation,
|
||||
'cpus' => (float) $this->database->limits_cpus,
|
||||
'cpu_shares' => $this->database->limits_cpu_shares,
|
||||
]
|
||||
],
|
||||
],
|
||||
'networks' => [
|
||||
$this->database->destination->network => [
|
||||
'external' => true,
|
||||
'name' => $this->database->destination->network,
|
||||
'attachable' => true,
|
||||
]
|
||||
]
|
||||
],
|
||||
],
|
||||
];
|
||||
if (!is_null($this->database->limits_cpuset)) {
|
||||
if (! is_null($this->database->limits_cpuset)) {
|
||||
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
|
||||
}
|
||||
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
||||
$docker_compose['services'][$container_name]['logging'] = [
|
||||
'driver' => 'fluentd',
|
||||
'options' => [
|
||||
'fluentd-address' => "tcp://127.0.0.1:24224",
|
||||
'fluentd-async' => "true",
|
||||
'fluentd-sub-second-precision' => "true",
|
||||
]
|
||||
'fluentd-address' => 'tcp://127.0.0.1:24224',
|
||||
'fluentd-async' => 'true',
|
||||
'fluentd-sub-second-precision' => 'true',
|
||||
],
|
||||
];
|
||||
}
|
||||
if (count($this->database->ports_mappings_array) > 0) {
|
||||
@ -87,26 +88,32 @@ public function handle(StandaloneMariadb $database)
|
||||
if (count($persistent_storages) > 0) {
|
||||
$docker_compose['services'][$container_name]['volumes'] = $persistent_storages;
|
||||
}
|
||||
if (count($persistent_file_volumes) > 0) {
|
||||
$docker_compose['services'][$container_name]['volumes'] = $persistent_file_volumes->map(function ($item) {
|
||||
return "$item->fs_path:$item->mount_path";
|
||||
})->toArray();
|
||||
}
|
||||
if (count($volume_names) > 0) {
|
||||
$docker_compose['volumes'] = $volume_names;
|
||||
}
|
||||
if (!is_null($this->database->mariadb_conf)) {
|
||||
if (! is_null($this->database->mariadb_conf) || ! empty($this->database->mariadb_conf)) {
|
||||
$docker_compose['services'][$container_name]['volumes'][] = [
|
||||
'type' => 'bind',
|
||||
'source' => $this->configuration_dir . '/custom-config.cnf',
|
||||
'source' => $this->configuration_dir.'/custom-config.cnf',
|
||||
'target' => '/etc/mysql/conf.d/custom-config.cnf',
|
||||
'read_only' => true,
|
||||
];
|
||||
}
|
||||
$docker_compose = Yaml::dump($docker_compose, 10);
|
||||
$docker_compose_base64 = base64_encode($docker_compose);
|
||||
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml";
|
||||
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
|
||||
$readme = generate_readme_file($this->database->name, now());
|
||||
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
|
||||
$this->commands[] = "echo 'Pulling {$database->image} image.'";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
||||
$this->commands[] = "echo 'Database started.'";
|
||||
|
||||
return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged');
|
||||
}
|
||||
|
||||
@ -114,9 +121,14 @@ private function generate_local_persistent_volumes()
|
||||
{
|
||||
$local_persistent_volumes = [];
|
||||
foreach ($this->database->persistentStorages as $persistentStorage) {
|
||||
$volume_name = $persistentStorage->host_path ?? $persistentStorage->name;
|
||||
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
|
||||
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
|
||||
$local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path;
|
||||
} else {
|
||||
$volume_name = $persistentStorage->name;
|
||||
$local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path;
|
||||
}
|
||||
}
|
||||
|
||||
return $local_persistent_volumes;
|
||||
}
|
||||
|
||||
@ -133,6 +145,7 @@ private function generate_local_persistent_volumes_only_volume_names()
|
||||
'external' => false,
|
||||
];
|
||||
}
|
||||
|
||||
return $local_persistent_volumes_names;
|
||||
}
|
||||
|
||||
@ -143,30 +156,32 @@ private function generate_environment_variables()
|
||||
$environment_variables->push("$env->key=$env->real_value");
|
||||
}
|
||||
|
||||
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MARIADB_ROOT_PASSWORD'))->isEmpty()) {
|
||||
if ($environment_variables->filter(fn ($env) => str($env)->contains('MARIADB_ROOT_PASSWORD'))->isEmpty()) {
|
||||
$environment_variables->push("MARIADB_ROOT_PASSWORD={$this->database->mariadb_root_password}");
|
||||
}
|
||||
|
||||
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MARIADB_DATABASE'))->isEmpty()) {
|
||||
if ($environment_variables->filter(fn ($env) => str($env)->contains('MARIADB_DATABASE'))->isEmpty()) {
|
||||
$environment_variables->push("MARIADB_DATABASE={$this->database->mariadb_database}");
|
||||
}
|
||||
|
||||
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MARIADB_USER'))->isEmpty()) {
|
||||
if ($environment_variables->filter(fn ($env) => str($env)->contains('MARIADB_USER'))->isEmpty()) {
|
||||
$environment_variables->push("MARIADB_USER={$this->database->mariadb_user}");
|
||||
}
|
||||
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MARIADB_PASSWORD'))->isEmpty()) {
|
||||
if ($environment_variables->filter(fn ($env) => str($env)->contains('MARIADB_PASSWORD'))->isEmpty()) {
|
||||
$environment_variables->push("MARIADB_PASSWORD={$this->database->mariadb_password}");
|
||||
}
|
||||
|
||||
return $environment_variables->all();
|
||||
}
|
||||
|
||||
private function add_custom_mysql()
|
||||
{
|
||||
if (is_null($this->database->mariadb_conf)) {
|
||||
if (is_null($this->database->mariadb_conf) || empty($this->database->mariadb_conf)) {
|
||||
return;
|
||||
}
|
||||
$filename = 'custom-config.cnf';
|
||||
$content = $this->database->mariadb_conf;
|
||||
$content_base64 = base64_encode($content);
|
||||
$this->commands[] = "echo '{$content_base64}' | base64 -d > $this->configuration_dir/{$filename}";
|
||||
$this->commands[] = "echo '{$content_base64}' | base64 -d | tee $this->configuration_dir/{$filename} > /dev/null";
|
||||
}
|
||||
}
|
||||
|
@ -3,26 +3,27 @@
|
||||
namespace App\Actions\Database;
|
||||
|
||||
use App\Models\StandaloneMongodb;
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
class StartMongodb
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public StandaloneMongodb $database;
|
||||
|
||||
public array $commands = [];
|
||||
|
||||
public string $configuration_dir;
|
||||
|
||||
public function handle(StandaloneMongodb $database)
|
||||
{
|
||||
$this->database = $database;
|
||||
|
||||
$startCommand = "mongod";
|
||||
$startCommand = 'mongod';
|
||||
|
||||
$container_name = $this->database->uuid;
|
||||
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
|
||||
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
||||
|
||||
$this->commands = [
|
||||
"echo 'Starting {$database->name}.'",
|
||||
@ -30,12 +31,12 @@ public function handle(StandaloneMongodb $database)
|
||||
];
|
||||
|
||||
$persistent_storages = $this->generate_local_persistent_volumes();
|
||||
$persistent_file_volumes = $this->database->fileStorages()->get();
|
||||
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
|
||||
$environment_variables = $this->generate_environment_variables();
|
||||
$this->add_custom_mongo_conf();
|
||||
|
||||
$docker_compose = [
|
||||
'version' => '3.8',
|
||||
'services' => [
|
||||
$container_name => [
|
||||
'image' => $this->database->image,
|
||||
@ -51,13 +52,14 @@ public function handle(StandaloneMongodb $database)
|
||||
],
|
||||
'healthcheck' => [
|
||||
'test' => [
|
||||
'CMD-SHELL',
|
||||
'mongosh --eval "printjson(db.runCommand(\"ping\"))"'
|
||||
'CMD',
|
||||
'echo',
|
||||
'ok',
|
||||
],
|
||||
'interval' => '5s',
|
||||
'timeout' => '5s',
|
||||
'retries' => 10,
|
||||
'start_period' => '5s'
|
||||
'start_period' => '5s',
|
||||
],
|
||||
'mem_limit' => $this->database->limits_memory,
|
||||
'memswap_limit' => $this->database->limits_memory_swap,
|
||||
@ -65,27 +67,27 @@ public function handle(StandaloneMongodb $database)
|
||||
'mem_reservation' => $this->database->limits_memory_reservation,
|
||||
'cpus' => (float) $this->database->limits_cpus,
|
||||
'cpu_shares' => $this->database->limits_cpu_shares,
|
||||
]
|
||||
],
|
||||
],
|
||||
'networks' => [
|
||||
$this->database->destination->network => [
|
||||
'external' => true,
|
||||
'name' => $this->database->destination->network,
|
||||
'attachable' => true,
|
||||
]
|
||||
]
|
||||
],
|
||||
],
|
||||
];
|
||||
if (!is_null($this->database->limits_cpuset)) {
|
||||
if (! is_null($this->database->limits_cpuset)) {
|
||||
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
|
||||
}
|
||||
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
||||
$docker_compose['services'][$container_name]['logging'] = [
|
||||
'driver' => 'fluentd',
|
||||
'options' => [
|
||||
'fluentd-address' => "tcp://127.0.0.1:24224",
|
||||
'fluentd-async' => "true",
|
||||
'fluentd-sub-second-precision' => "true",
|
||||
]
|
||||
'fluentd-address' => 'tcp://127.0.0.1:24224',
|
||||
'fluentd-async' => 'true',
|
||||
'fluentd-sub-second-precision' => 'true',
|
||||
],
|
||||
];
|
||||
}
|
||||
if (count($this->database->ports_mappings_array) > 0) {
|
||||
@ -94,35 +96,41 @@ public function handle(StandaloneMongodb $database)
|
||||
if (count($persistent_storages) > 0) {
|
||||
$docker_compose['services'][$container_name]['volumes'] = $persistent_storages;
|
||||
}
|
||||
if (count($persistent_file_volumes) > 0) {
|
||||
$docker_compose['services'][$container_name]['volumes'] = $persistent_file_volumes->map(function ($item) {
|
||||
return "$item->fs_path:$item->mount_path";
|
||||
})->toArray();
|
||||
}
|
||||
if (count($volume_names) > 0) {
|
||||
$docker_compose['volumes'] = $volume_names;
|
||||
}
|
||||
if (!is_null($this->database->mongo_conf)) {
|
||||
if (! is_null($this->database->mongo_conf) || ! empty($this->database->mongo_conf)) {
|
||||
$docker_compose['services'][$container_name]['volumes'][] = [
|
||||
'type' => 'bind',
|
||||
'source' => $this->configuration_dir . '/mongod.conf',
|
||||
'source' => $this->configuration_dir.'/mongod.conf',
|
||||
'target' => '/etc/mongo/mongod.conf',
|
||||
'read_only' => true,
|
||||
];
|
||||
$docker_compose['services'][$container_name]['command'] = $startCommand . ' --config /etc/mongo/mongod.conf';
|
||||
$docker_compose['services'][$container_name]['command'] = $startCommand.' --config /etc/mongo/mongod.conf';
|
||||
}
|
||||
$this->add_default_database();
|
||||
$docker_compose['services'][$container_name]['volumes'][] = [
|
||||
'type' => 'bind',
|
||||
'source' => $this->configuration_dir . '/docker-entrypoint-initdb.d',
|
||||
'source' => $this->configuration_dir.'/docker-entrypoint-initdb.d',
|
||||
'target' => '/docker-entrypoint-initdb.d',
|
||||
'read_only' => true,
|
||||
];
|
||||
|
||||
$docker_compose = Yaml::dump($docker_compose, 10);
|
||||
$docker_compose_base64 = base64_encode($docker_compose);
|
||||
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml";
|
||||
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
|
||||
$readme = generate_readme_file($this->database->name, now());
|
||||
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
|
||||
$this->commands[] = "echo 'Pulling {$database->image} image.'";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
||||
$this->commands[] = "echo 'Database started.'";
|
||||
|
||||
return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged');
|
||||
}
|
||||
|
||||
@ -130,9 +138,14 @@ private function generate_local_persistent_volumes()
|
||||
{
|
||||
$local_persistent_volumes = [];
|
||||
foreach ($this->database->persistentStorages as $persistentStorage) {
|
||||
$volume_name = $persistentStorage->host_path ?? $persistentStorage->name;
|
||||
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
|
||||
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
|
||||
$local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path;
|
||||
} else {
|
||||
$volume_name = $persistentStorage->name;
|
||||
$local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path;
|
||||
}
|
||||
}
|
||||
|
||||
return $local_persistent_volumes;
|
||||
}
|
||||
|
||||
@ -149,6 +162,7 @@ private function generate_local_persistent_volumes_only_volume_names()
|
||||
'external' => false,
|
||||
];
|
||||
}
|
||||
|
||||
return $local_persistent_volumes_names;
|
||||
}
|
||||
|
||||
@ -159,34 +173,37 @@ private function generate_environment_variables()
|
||||
$environment_variables->push("$env->key=$env->real_value");
|
||||
}
|
||||
|
||||
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MONGO_INITDB_ROOT_USERNAME'))->isEmpty()) {
|
||||
if ($environment_variables->filter(fn ($env) => str($env)->contains('MONGO_INITDB_ROOT_USERNAME'))->isEmpty()) {
|
||||
$environment_variables->push("MONGO_INITDB_ROOT_USERNAME={$this->database->mongo_initdb_root_username}");
|
||||
}
|
||||
|
||||
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MONGO_INITDB_ROOT_PASSWORD'))->isEmpty()) {
|
||||
if ($environment_variables->filter(fn ($env) => str($env)->contains('MONGO_INITDB_ROOT_PASSWORD'))->isEmpty()) {
|
||||
$environment_variables->push("MONGO_INITDB_ROOT_PASSWORD={$this->database->mongo_initdb_root_password}");
|
||||
}
|
||||
|
||||
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MONGO_INITDB_DATABASE'))->isEmpty()) {
|
||||
if ($environment_variables->filter(fn ($env) => str($env)->contains('MONGO_INITDB_DATABASE'))->isEmpty()) {
|
||||
$environment_variables->push("MONGO_INITDB_DATABASE={$this->database->mongo_initdb_database}");
|
||||
}
|
||||
|
||||
return $environment_variables->all();
|
||||
}
|
||||
|
||||
private function add_custom_mongo_conf()
|
||||
{
|
||||
if (is_null($this->database->mongo_conf)) {
|
||||
if (is_null($this->database->mongo_conf) || empty($this->database->mongo_conf)) {
|
||||
return;
|
||||
}
|
||||
$filename = 'mongod.conf';
|
||||
$content = $this->database->mongo_conf;
|
||||
$content_base64 = base64_encode($content);
|
||||
$this->commands[] = "echo '{$content_base64}' | base64 -d > $this->configuration_dir/{$filename}";
|
||||
$this->commands[] = "echo '{$content_base64}' | base64 -d | tee $this->configuration_dir/{$filename} > /dev/null";
|
||||
}
|
||||
|
||||
private function add_default_database()
|
||||
{
|
||||
$content = "db = db.getSiblingDB(\"{$this->database->mongo_initdb_database}\");db.createCollection('init_collection');db.createUser({user: \"{$this->database->mongo_initdb_root_username}\", pwd: \"{$this->database->mongo_initdb_root_password}\",roles: [{role:\"readWrite\",db:\"{$this->database->mongo_initdb_database}\"}]});";
|
||||
$content_base64 = base64_encode($content);
|
||||
$this->commands[] = "mkdir -p $this->configuration_dir/docker-entrypoint-initdb.d";
|
||||
$this->commands[] = "echo '{$content_base64}' | base64 -d > $this->configuration_dir/docker-entrypoint-initdb.d/01-default-database.js";
|
||||
$this->commands[] = "echo '{$content_base64}' | base64 -d | tee $this->configuration_dir/docker-entrypoint-initdb.d/01-default-database.js > /dev/null";
|
||||
}
|
||||
}
|
||||
|
@ -3,16 +3,17 @@
|
||||
namespace App\Actions\Database;
|
||||
|
||||
use App\Models\StandaloneMysql;
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
class StartMysql
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public StandaloneMysql $database;
|
||||
|
||||
public array $commands = [];
|
||||
|
||||
public string $configuration_dir;
|
||||
|
||||
public function handle(StandaloneMysql $database)
|
||||
@ -20,7 +21,7 @@ public function handle(StandaloneMysql $database)
|
||||
$this->database = $database;
|
||||
|
||||
$container_name = $this->database->uuid;
|
||||
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
|
||||
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
||||
|
||||
$this->commands = [
|
||||
"echo 'Starting {$database->name}.'",
|
||||
@ -28,11 +29,11 @@ public function handle(StandaloneMysql $database)
|
||||
];
|
||||
|
||||
$persistent_storages = $this->generate_local_persistent_volumes();
|
||||
$persistent_file_volumes = $this->database->fileStorages()->get();
|
||||
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
|
||||
$environment_variables = $this->generate_environment_variables();
|
||||
$this->add_custom_mysql();
|
||||
$docker_compose = [
|
||||
'version' => '3.8',
|
||||
'services' => [
|
||||
$container_name => [
|
||||
'image' => $this->database->image,
|
||||
@ -46,11 +47,11 @@ public function handle(StandaloneMysql $database)
|
||||
'coolify.managed' => 'true',
|
||||
],
|
||||
'healthcheck' => [
|
||||
'test' => ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p{$this->database->mysql_root_password}"],
|
||||
'test' => ['CMD', 'mysqladmin', 'ping', '-h', 'localhost', '-u', 'root', "-p{$this->database->mysql_root_password}"],
|
||||
'interval' => '5s',
|
||||
'timeout' => '5s',
|
||||
'retries' => 10,
|
||||
'start_period' => '5s'
|
||||
'start_period' => '5s',
|
||||
],
|
||||
'mem_limit' => $this->database->limits_memory,
|
||||
'memswap_limit' => $this->database->limits_memory_swap,
|
||||
@ -58,27 +59,27 @@ public function handle(StandaloneMysql $database)
|
||||
'mem_reservation' => $this->database->limits_memory_reservation,
|
||||
'cpus' => (float) $this->database->limits_cpus,
|
||||
'cpu_shares' => $this->database->limits_cpu_shares,
|
||||
]
|
||||
],
|
||||
],
|
||||
'networks' => [
|
||||
$this->database->destination->network => [
|
||||
'external' => true,
|
||||
'name' => $this->database->destination->network,
|
||||
'attachable' => true,
|
||||
]
|
||||
]
|
||||
],
|
||||
],
|
||||
];
|
||||
if (!is_null($this->database->limits_cpuset)) {
|
||||
if (! is_null($this->database->limits_cpuset)) {
|
||||
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
|
||||
}
|
||||
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
||||
$docker_compose['services'][$container_name]['logging'] = [
|
||||
'driver' => 'fluentd',
|
||||
'options' => [
|
||||
'fluentd-address' => "tcp://127.0.0.1:24224",
|
||||
'fluentd-async' => "true",
|
||||
'fluentd-sub-second-precision' => "true",
|
||||
]
|
||||
'fluentd-address' => 'tcp://127.0.0.1:24224',
|
||||
'fluentd-async' => 'true',
|
||||
'fluentd-sub-second-precision' => 'true',
|
||||
],
|
||||
];
|
||||
}
|
||||
if (count($this->database->ports_mappings_array) > 0) {
|
||||
@ -87,36 +88,47 @@ public function handle(StandaloneMysql $database)
|
||||
if (count($persistent_storages) > 0) {
|
||||
$docker_compose['services'][$container_name]['volumes'] = $persistent_storages;
|
||||
}
|
||||
if (count($persistent_file_volumes) > 0) {
|
||||
$docker_compose['services'][$container_name]['volumes'] = $persistent_file_volumes->map(function ($item) {
|
||||
return "$item->fs_path:$item->mount_path";
|
||||
})->toArray();
|
||||
}
|
||||
if (count($volume_names) > 0) {
|
||||
$docker_compose['volumes'] = $volume_names;
|
||||
}
|
||||
if (!is_null($this->database->mysql_conf)) {
|
||||
if (! is_null($this->database->mysql_conf) || ! empty($this->database->mysql_conf)) {
|
||||
$docker_compose['services'][$container_name]['volumes'][] = [
|
||||
'type' => 'bind',
|
||||
'source' => $this->configuration_dir . '/custom-config.cnf',
|
||||
'source' => $this->configuration_dir.'/custom-config.cnf',
|
||||
'target' => '/etc/mysql/conf.d/custom-config.cnf',
|
||||
'read_only' => true,
|
||||
];
|
||||
}
|
||||
$docker_compose = Yaml::dump($docker_compose, 10);
|
||||
$docker_compose_base64 = base64_encode($docker_compose);
|
||||
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml";
|
||||
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
|
||||
$readme = generate_readme_file($this->database->name, now());
|
||||
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
|
||||
$this->commands[] = "echo 'Pulling {$database->image} image.'";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
||||
$this->commands[] = "echo 'Database started.'";
|
||||
return remote_process($this->commands, $database->destination->server,callEventOnFinish: 'DatabaseStatusChanged');
|
||||
|
||||
return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged');
|
||||
}
|
||||
|
||||
private function generate_local_persistent_volumes()
|
||||
{
|
||||
$local_persistent_volumes = [];
|
||||
foreach ($this->database->persistentStorages as $persistentStorage) {
|
||||
$volume_name = $persistentStorage->host_path ?? $persistentStorage->name;
|
||||
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
|
||||
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
|
||||
$local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path;
|
||||
} else {
|
||||
$volume_name = $persistentStorage->name;
|
||||
$local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path;
|
||||
}
|
||||
}
|
||||
|
||||
return $local_persistent_volumes;
|
||||
}
|
||||
|
||||
@ -133,6 +145,7 @@ private function generate_local_persistent_volumes_only_volume_names()
|
||||
'external' => false,
|
||||
];
|
||||
}
|
||||
|
||||
return $local_persistent_volumes_names;
|
||||
}
|
||||
|
||||
@ -143,30 +156,32 @@ private function generate_environment_variables()
|
||||
$environment_variables->push("$env->key=$env->real_value");
|
||||
}
|
||||
|
||||
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MYSQL_ROOT_PASSWORD'))->isEmpty()) {
|
||||
if ($environment_variables->filter(fn ($env) => str($env)->contains('MYSQL_ROOT_PASSWORD'))->isEmpty()) {
|
||||
$environment_variables->push("MYSQL_ROOT_PASSWORD={$this->database->mysql_root_password}");
|
||||
}
|
||||
|
||||
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MYSQL_DATABASE'))->isEmpty()) {
|
||||
if ($environment_variables->filter(fn ($env) => str($env)->contains('MYSQL_DATABASE'))->isEmpty()) {
|
||||
$environment_variables->push("MYSQL_DATABASE={$this->database->mysql_database}");
|
||||
}
|
||||
|
||||
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MYSQL_USER'))->isEmpty()) {
|
||||
if ($environment_variables->filter(fn ($env) => str($env)->contains('MYSQL_USER'))->isEmpty()) {
|
||||
$environment_variables->push("MYSQL_USER={$this->database->mysql_user}");
|
||||
}
|
||||
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MYSQL_PASSWORD'))->isEmpty()) {
|
||||
if ($environment_variables->filter(fn ($env) => str($env)->contains('MYSQL_PASSWORD'))->isEmpty()) {
|
||||
$environment_variables->push("MYSQL_PASSWORD={$this->database->mysql_password}");
|
||||
}
|
||||
|
||||
return $environment_variables->all();
|
||||
}
|
||||
|
||||
private function add_custom_mysql()
|
||||
{
|
||||
if (is_null($this->database->mysql_conf)) {
|
||||
if (is_null($this->database->mysql_conf) || empty($this->database->mysql_conf)) {
|
||||
return;
|
||||
}
|
||||
$filename = 'custom-config.cnf';
|
||||
$content = $this->database->mysql_conf;
|
||||
$content_base64 = base64_encode($content);
|
||||
$this->commands[] = "echo '{$content_base64}' | base64 -d > $this->configuration_dir/{$filename}";
|
||||
$this->commands[] = "echo '{$content_base64}' | base64 -d | tee $this->configuration_dir/{$filename} > /dev/null";
|
||||
}
|
||||
}
|
||||
|
@ -3,39 +3,41 @@
|
||||
namespace App\Actions\Database;
|
||||
|
||||
use App\Models\StandalonePostgresql;
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
class StartPostgresql
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public StandalonePostgresql $database;
|
||||
|
||||
public array $commands = [];
|
||||
|
||||
public array $init_scripts = [];
|
||||
|
||||
public string $configuration_dir;
|
||||
|
||||
public function handle(StandalonePostgresql $database)
|
||||
{
|
||||
$this->database = $database;
|
||||
$container_name = $this->database->uuid;
|
||||
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
|
||||
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
||||
|
||||
$this->commands = [
|
||||
"echo 'Starting {$database->name}.'",
|
||||
"mkdir -p $this->configuration_dir",
|
||||
"mkdir -p $this->configuration_dir/docker-entrypoint-initdb.d/"
|
||||
"mkdir -p $this->configuration_dir/docker-entrypoint-initdb.d/",
|
||||
];
|
||||
|
||||
$persistent_storages = $this->generate_local_persistent_volumes();
|
||||
$persistent_file_volumes = $this->database->fileStorages()->get();
|
||||
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
|
||||
$environment_variables = $this->generate_environment_variables();
|
||||
$this->generate_init_scripts();
|
||||
$this->add_custom_conf();
|
||||
|
||||
$docker_compose = [
|
||||
'version' => '3.8',
|
||||
'services' => [
|
||||
$container_name => [
|
||||
'image' => $this->database->image,
|
||||
@ -50,13 +52,13 @@ public function handle(StandalonePostgresql $database)
|
||||
],
|
||||
'healthcheck' => [
|
||||
'test' => [
|
||||
"CMD-SHELL",
|
||||
"psql -U {$this->database->postgres_user} -d {$this->database->postgres_db} -c 'SELECT 1' || exit 1"
|
||||
'CMD-SHELL',
|
||||
"psql -U {$this->database->postgres_user} -d {$this->database->postgres_db} -c 'SELECT 1' || exit 1",
|
||||
],
|
||||
'interval' => '5s',
|
||||
'timeout' => '5s',
|
||||
'retries' => 10,
|
||||
'start_period' => '5s'
|
||||
'start_period' => '5s',
|
||||
],
|
||||
'mem_limit' => $this->database->limits_memory,
|
||||
'memswap_limit' => $this->database->limits_memory_swap,
|
||||
@ -64,28 +66,27 @@ public function handle(StandalonePostgresql $database)
|
||||
'mem_reservation' => $this->database->limits_memory_reservation,
|
||||
'cpus' => (float) $this->database->limits_cpus,
|
||||
'cpu_shares' => $this->database->limits_cpu_shares,
|
||||
]
|
||||
],
|
||||
],
|
||||
'networks' => [
|
||||
$this->database->destination->network => [
|
||||
'external' => true,
|
||||
'name' => $this->database->destination->network,
|
||||
'attachable' => true,
|
||||
]
|
||||
]
|
||||
],
|
||||
],
|
||||
];
|
||||
if (!is_null($this->database->limits_cpuset)) {
|
||||
if (! is_null($this->database->limits_cpuset)) {
|
||||
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
|
||||
}
|
||||
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
||||
ray('Log Drain Enabled');
|
||||
$docker_compose['services'][$container_name]['logging'] = [
|
||||
'driver' => 'fluentd',
|
||||
'options' => [
|
||||
'fluentd-address' => "tcp://127.0.0.1:24224",
|
||||
'fluentd-async' => "true",
|
||||
'fluentd-sub-second-precision' => "true",
|
||||
]
|
||||
'fluentd-address' => 'tcp://127.0.0.1:24224',
|
||||
'fluentd-async' => 'true',
|
||||
'fluentd-sub-second-precision' => 'true',
|
||||
],
|
||||
];
|
||||
}
|
||||
if (count($this->database->ports_mappings_array) > 0) {
|
||||
@ -94,6 +95,11 @@ public function handle(StandalonePostgresql $database)
|
||||
if (count($persistent_storages) > 0) {
|
||||
$docker_compose['services'][$container_name]['volumes'] = $persistent_storages;
|
||||
}
|
||||
if (count($persistent_file_volumes) > 0) {
|
||||
$docker_compose['services'][$container_name]['volumes'] = $persistent_file_volumes->map(function ($item) {
|
||||
return "$item->fs_path:$item->mount_path";
|
||||
})->toArray();
|
||||
}
|
||||
if (count($volume_names) > 0) {
|
||||
$docker_compose['volumes'] = $volume_names;
|
||||
}
|
||||
@ -102,15 +108,15 @@ public function handle(StandalonePostgresql $database)
|
||||
$docker_compose['services'][$container_name]['volumes'][] = [
|
||||
'type' => 'bind',
|
||||
'source' => $init_script,
|
||||
'target' => '/docker-entrypoint-initdb.d/' . basename($init_script),
|
||||
'target' => '/docker-entrypoint-initdb.d/'.basename($init_script),
|
||||
'read_only' => true,
|
||||
];
|
||||
}
|
||||
}
|
||||
if (!is_null($this->database->postgres_conf)) {
|
||||
if (! is_null($this->database->postgres_conf) && ! empty($this->database->postgres_conf)) {
|
||||
$docker_compose['services'][$container_name]['volumes'][] = [
|
||||
'type' => 'bind',
|
||||
'source' => $this->configuration_dir . '/custom-postgres.conf',
|
||||
'source' => $this->configuration_dir.'/custom-postgres.conf',
|
||||
'target' => '/etc/postgresql/postgresql.conf',
|
||||
'read_only' => true,
|
||||
];
|
||||
@ -122,13 +128,14 @@ public function handle(StandalonePostgresql $database)
|
||||
}
|
||||
$docker_compose = Yaml::dump($docker_compose, 10);
|
||||
$docker_compose_base64 = base64_encode($docker_compose);
|
||||
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml";
|
||||
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
|
||||
$readme = generate_readme_file($this->database->name, now());
|
||||
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
|
||||
$this->commands[] = "echo 'Pulling {$database->image} image.'";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
||||
$this->commands[] = "echo 'Database started.'";
|
||||
|
||||
return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged');
|
||||
}
|
||||
|
||||
@ -136,9 +143,14 @@ private function generate_local_persistent_volumes()
|
||||
{
|
||||
$local_persistent_volumes = [];
|
||||
foreach ($this->database->persistentStorages as $persistentStorage) {
|
||||
$volume_name = $persistentStorage->host_path ?? $persistentStorage->name;
|
||||
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
|
||||
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
|
||||
$local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path;
|
||||
} else {
|
||||
$volume_name = $persistentStorage->name;
|
||||
$local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path;
|
||||
}
|
||||
}
|
||||
|
||||
return $local_persistent_volumes;
|
||||
}
|
||||
|
||||
@ -155,32 +167,32 @@ private function generate_local_persistent_volumes_only_volume_names()
|
||||
'external' => false,
|
||||
];
|
||||
}
|
||||
|
||||
return $local_persistent_volumes_names;
|
||||
}
|
||||
|
||||
private function generate_environment_variables()
|
||||
{
|
||||
$environment_variables = collect();
|
||||
ray('Generate Environment Variables')->green();
|
||||
ray($this->database->runtime_environment_variables)->green();
|
||||
foreach ($this->database->runtime_environment_variables as $env) {
|
||||
$environment_variables->push("$env->key=$env->real_value");
|
||||
}
|
||||
|
||||
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('POSTGRES_USER'))->isEmpty()) {
|
||||
if ($environment_variables->filter(fn ($env) => str($env)->contains('POSTGRES_USER'))->isEmpty()) {
|
||||
$environment_variables->push("POSTGRES_USER={$this->database->postgres_user}");
|
||||
}
|
||||
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('PGUSER'))->isEmpty()) {
|
||||
if ($environment_variables->filter(fn ($env) => str($env)->contains('PGUSER'))->isEmpty()) {
|
||||
$environment_variables->push("PGUSER={$this->database->postgres_user}");
|
||||
}
|
||||
|
||||
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('POSTGRES_PASSWORD'))->isEmpty()) {
|
||||
if ($environment_variables->filter(fn ($env) => str($env)->contains('POSTGRES_PASSWORD'))->isEmpty()) {
|
||||
$environment_variables->push("POSTGRES_PASSWORD={$this->database->postgres_password}");
|
||||
}
|
||||
|
||||
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('POSTGRES_DB'))->isEmpty()) {
|
||||
if ($environment_variables->filter(fn ($env) => str($env)->contains('POSTGRES_DB'))->isEmpty()) {
|
||||
$environment_variables->push("POSTGRES_DB={$this->database->postgres_db}");
|
||||
}
|
||||
|
||||
return $environment_variables->all();
|
||||
}
|
||||
|
||||
@ -193,18 +205,24 @@ private function generate_init_scripts()
|
||||
$filename = data_get($init_script, 'filename');
|
||||
$content = data_get($init_script, 'content');
|
||||
$content_base64 = base64_encode($content);
|
||||
$this->commands[] = "echo '{$content_base64}' | base64 -d > $this->configuration_dir/docker-entrypoint-initdb.d/{$filename}";
|
||||
$this->commands[] = "echo '{$content_base64}' | base64 -d | tee $this->configuration_dir/docker-entrypoint-initdb.d/{$filename} > /dev/null";
|
||||
$this->init_scripts[] = "$this->configuration_dir/docker-entrypoint-initdb.d/{$filename}";
|
||||
}
|
||||
}
|
||||
|
||||
private function add_custom_conf()
|
||||
{
|
||||
if (is_null($this->database->postgres_conf)) {
|
||||
if (is_null($this->database->postgres_conf) || empty($this->database->postgres_conf)) {
|
||||
return;
|
||||
}
|
||||
$filename = 'custom-postgres.conf';
|
||||
$content = $this->database->postgres_conf;
|
||||
if (! str($content)->contains('listen_addresses')) {
|
||||
$content .= "\nlisten_addresses = '*'";
|
||||
$this->database->postgres_conf = $content;
|
||||
$this->database->save();
|
||||
}
|
||||
$content_base64 = base64_encode($content);
|
||||
$this->commands[] = "echo '{$content_base64}' | base64 -d > $this->configuration_dir/{$filename}";
|
||||
$this->commands[] = "echo '{$content_base64}' | base64 -d | tee $this->configuration_dir/{$filename} > /dev/null";
|
||||
}
|
||||
}
|
||||
|
@ -4,18 +4,18 @@
|
||||
|
||||
use App\Models\StandaloneRedis;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
class StartRedis
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public StandaloneRedis $database;
|
||||
public array $commands = [];
|
||||
public string $configuration_dir;
|
||||
|
||||
public array $commands = [];
|
||||
|
||||
public string $configuration_dir;
|
||||
|
||||
public function handle(StandaloneRedis $database)
|
||||
{
|
||||
@ -24,7 +24,7 @@ public function handle(StandaloneRedis $database)
|
||||
$startCommand = "redis-server --requirepass {$this->database->redis_password} --appendonly yes";
|
||||
|
||||
$container_name = $this->database->uuid;
|
||||
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
|
||||
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
||||
|
||||
$this->commands = [
|
||||
"echo 'Starting {$database->name}.'",
|
||||
@ -32,12 +32,12 @@ public function handle(StandaloneRedis $database)
|
||||
];
|
||||
|
||||
$persistent_storages = $this->generate_local_persistent_volumes();
|
||||
$persistent_file_volumes = $this->database->fileStorages()->get();
|
||||
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
|
||||
$environment_variables = $this->generate_environment_variables();
|
||||
$this->add_custom_redis();
|
||||
|
||||
$docker_compose = [
|
||||
'version' => '3.8',
|
||||
'services' => [
|
||||
$container_name => [
|
||||
'image' => $this->database->image,
|
||||
@ -55,12 +55,12 @@ public function handle(StandaloneRedis $database)
|
||||
'test' => [
|
||||
'CMD-SHELL',
|
||||
'redis-cli',
|
||||
'ping'
|
||||
'ping',
|
||||
],
|
||||
'interval' => '5s',
|
||||
'timeout' => '5s',
|
||||
'retries' => 10,
|
||||
'start_period' => '5s'
|
||||
'start_period' => '5s',
|
||||
],
|
||||
'mem_limit' => $this->database->limits_memory,
|
||||
'memswap_limit' => $this->database->limits_memory_swap,
|
||||
@ -68,27 +68,27 @@ public function handle(StandaloneRedis $database)
|
||||
'mem_reservation' => $this->database->limits_memory_reservation,
|
||||
'cpus' => (float) $this->database->limits_cpus,
|
||||
'cpu_shares' => $this->database->limits_cpu_shares,
|
||||
]
|
||||
],
|
||||
],
|
||||
'networks' => [
|
||||
$this->database->destination->network => [
|
||||
'external' => true,
|
||||
'name' => $this->database->destination->network,
|
||||
'attachable' => true,
|
||||
]
|
||||
]
|
||||
],
|
||||
],
|
||||
];
|
||||
if (!is_null($this->database->limits_cpuset)) {
|
||||
if (! is_null($this->database->limits_cpuset)) {
|
||||
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
|
||||
}
|
||||
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
||||
$docker_compose['services'][$container_name]['logging'] = [
|
||||
'driver' => 'fluentd',
|
||||
'options' => [
|
||||
'fluentd-address' => "tcp://127.0.0.1:24224",
|
||||
'fluentd-async' => "true",
|
||||
'fluentd-sub-second-precision' => "true",
|
||||
]
|
||||
'fluentd-address' => 'tcp://127.0.0.1:24224',
|
||||
'fluentd-async' => 'true',
|
||||
'fluentd-sub-second-precision' => 'true',
|
||||
],
|
||||
];
|
||||
}
|
||||
if (count($this->database->ports_mappings_array) > 0) {
|
||||
@ -97,13 +97,18 @@ public function handle(StandaloneRedis $database)
|
||||
if (count($persistent_storages) > 0) {
|
||||
$docker_compose['services'][$container_name]['volumes'] = $persistent_storages;
|
||||
}
|
||||
if (count($persistent_file_volumes) > 0) {
|
||||
$docker_compose['services'][$container_name]['volumes'] = $persistent_file_volumes->map(function ($item) {
|
||||
return "$item->fs_path:$item->mount_path";
|
||||
})->toArray();
|
||||
}
|
||||
if (count($volume_names) > 0) {
|
||||
$docker_compose['volumes'] = $volume_names;
|
||||
}
|
||||
if (!is_null($this->database->redis_conf)) {
|
||||
if (! is_null($this->database->redis_conf) || ! empty($this->database->redis_conf)) {
|
||||
$docker_compose['services'][$container_name]['volumes'][] = [
|
||||
'type' => 'bind',
|
||||
'source' => $this->configuration_dir . '/redis.conf',
|
||||
'source' => $this->configuration_dir.'/redis.conf',
|
||||
'target' => '/usr/local/etc/redis/redis.conf',
|
||||
'read_only' => true,
|
||||
];
|
||||
@ -111,13 +116,14 @@ public function handle(StandaloneRedis $database)
|
||||
}
|
||||
$docker_compose = Yaml::dump($docker_compose, 10);
|
||||
$docker_compose_base64 = base64_encode($docker_compose);
|
||||
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml";
|
||||
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
|
||||
$readme = generate_readme_file($this->database->name, now());
|
||||
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
|
||||
$this->commands[] = "echo 'Pulling {$database->image} image.'";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
||||
$this->commands[] = "echo 'Database started.'";
|
||||
|
||||
return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged');
|
||||
}
|
||||
|
||||
@ -125,9 +131,14 @@ private function generate_local_persistent_volumes()
|
||||
{
|
||||
$local_persistent_volumes = [];
|
||||
foreach ($this->database->persistentStorages as $persistentStorage) {
|
||||
$volume_name = $persistentStorage->host_path ?? $persistentStorage->name;
|
||||
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
|
||||
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
|
||||
$local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path;
|
||||
} else {
|
||||
$volume_name = $persistentStorage->name;
|
||||
$local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path;
|
||||
}
|
||||
}
|
||||
|
||||
return $local_persistent_volumes;
|
||||
}
|
||||
|
||||
@ -144,6 +155,7 @@ private function generate_local_persistent_volumes_only_volume_names()
|
||||
'external' => false,
|
||||
];
|
||||
}
|
||||
|
||||
return $local_persistent_volumes_names;
|
||||
}
|
||||
|
||||
@ -154,15 +166,16 @@ private function generate_environment_variables()
|
||||
$environment_variables->push("$env->key=$env->real_value");
|
||||
}
|
||||
|
||||
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('REDIS_PASSWORD'))->isEmpty()) {
|
||||
if ($environment_variables->filter(fn ($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) {
|
||||
$environment_variables->push("REDIS_PASSWORD={$this->database->redis_password}");
|
||||
}
|
||||
|
||||
return $environment_variables->all();
|
||||
}
|
||||
|
||||
private function add_custom_redis()
|
||||
{
|
||||
if (is_null($this->database->redis_conf)) {
|
||||
if (is_null($this->database->redis_conf) || empty($this->database->redis_conf)) {
|
||||
return;
|
||||
}
|
||||
$filename = 'redis.conf';
|
||||
|
@ -2,7 +2,9 @@
|
||||
|
||||
namespace App\Actions\Database;
|
||||
|
||||
use App\Events\DatabaseStatusChanged;
|
||||
use App\Models\StandaloneClickhouse;
|
||||
use App\Models\StandaloneDragonfly;
|
||||
use App\Models\StandaloneKeydb;
|
||||
use App\Models\StandaloneMariadb;
|
||||
use App\Models\StandaloneMongodb;
|
||||
use App\Models\StandaloneMysql;
|
||||
@ -14,10 +16,10 @@ class StopDatabase
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb $database)
|
||||
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $database)
|
||||
{
|
||||
$server = $database->destination->server;
|
||||
if (!$server->isFunctional()) {
|
||||
if (! $server->isFunctional()) {
|
||||
return 'Server is not functional';
|
||||
}
|
||||
instant_remote_process(
|
||||
@ -27,7 +29,5 @@ public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|St
|
||||
if ($database->is_public) {
|
||||
StopDatabaseProxy::run($database);
|
||||
}
|
||||
// TODO: make notification for services
|
||||
// $database->environment->project->team->notify(new StatusChanged($database));
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,11 @@
|
||||
|
||||
namespace App\Actions\Database;
|
||||
|
||||
use App\Events\DatabaseStatusChanged;
|
||||
use App\Models\ServiceDatabase;
|
||||
use App\Models\StandaloneClickhouse;
|
||||
use App\Models\StandaloneDragonfly;
|
||||
use App\Models\StandaloneKeydb;
|
||||
use App\Models\StandaloneMariadb;
|
||||
use App\Models\StandaloneMongodb;
|
||||
use App\Models\StandaloneMysql;
|
||||
@ -14,14 +18,16 @@ class StopDatabaseProxy
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|ServiceDatabase $database)
|
||||
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|ServiceDatabase|StandaloneDragonfly|StandaloneClickhouse $database)
|
||||
{
|
||||
$server = data_get($database, 'destination.server');
|
||||
$uuid = $database->uuid;
|
||||
if ($database->getMorphClass() === 'App\Models\ServiceDatabase') {
|
||||
$uuid = $database->service->uuid;
|
||||
$server = data_get($database, 'service.server');
|
||||
}
|
||||
instant_remote_process(["docker rm -f {$database->uuid}-proxy"], $server);
|
||||
$database->is_public = false;
|
||||
instant_remote_process(["docker rm -f {$uuid}-proxy"], $server);
|
||||
$database->save();
|
||||
DatabaseStatusChanged::dispatch();
|
||||
}
|
||||
}
|
||||
|
686
app/Actions/Docker/GetContainersStatus.php
Normal file
686
app/Actions/Docker/GetContainersStatus.php
Normal file
@ -0,0 +1,686 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Docker;
|
||||
|
||||
use App\Actions\Database\StartDatabaseProxy;
|
||||
use App\Actions\Proxy\CheckProxy;
|
||||
use App\Actions\Proxy\StartProxy;
|
||||
use App\Actions\Shared\ComplexStatusCheck;
|
||||
use App\Models\ApplicationPreview;
|
||||
use App\Models\Server;
|
||||
use App\Models\ServiceDatabase;
|
||||
use App\Notifications\Container\ContainerRestarted;
|
||||
use App\Notifications\Container\ContainerStopped;
|
||||
use Illuminate\Support\Arr;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class GetContainersStatus
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public $applications;
|
||||
|
||||
public $server;
|
||||
|
||||
public function handle(Server $server)
|
||||
{
|
||||
// if (isDev()) {
|
||||
// $server = Server::find(0);
|
||||
// }
|
||||
$this->server = $server;
|
||||
if (! $this->server->isFunctional()) {
|
||||
return 'Server is not ready.';
|
||||
}
|
||||
$this->applications = $this->server->applications();
|
||||
$skip_these_applications = collect([]);
|
||||
foreach ($this->applications as $application) {
|
||||
if ($application->additional_servers->count() > 0) {
|
||||
$skip_these_applications->push($application);
|
||||
ComplexStatusCheck::run($application);
|
||||
$this->applications = $this->applications->filter(function ($value, $key) use ($application) {
|
||||
return $value->id !== $application->id;
|
||||
});
|
||||
}
|
||||
}
|
||||
$this->applications = $this->applications->filter(function ($value, $key) use ($skip_these_applications) {
|
||||
return ! $skip_these_applications->pluck('id')->contains($value->id);
|
||||
});
|
||||
$this->old_way();
|
||||
// if ($this->server->isSwarm()) {
|
||||
// $this->old_way();
|
||||
// } else {
|
||||
// if (!$this->server->is_metrics_enabled) {
|
||||
// $this->old_way();
|
||||
// return;
|
||||
// }
|
||||
// $sentinel_found = instant_remote_process(["docker inspect coolify-sentinel"], $this->server, false);
|
||||
// $sentinel_found = json_decode($sentinel_found, true);
|
||||
// $status = data_get($sentinel_found, '0.State.Status', 'exited');
|
||||
// if ($status === 'running') {
|
||||
// ray('Checking with Sentinel');
|
||||
// $this->sentinel();
|
||||
// } else {
|
||||
// ray('Checking the Old way');
|
||||
// $this->old_way();
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
private function sentinel()
|
||||
{
|
||||
try {
|
||||
$containers = $this->server->getContainers();
|
||||
if ($containers->count() === 0) {
|
||||
return;
|
||||
}
|
||||
$databases = $this->server->databases();
|
||||
$services = $this->server->services()->get();
|
||||
$previews = $this->server->previews();
|
||||
$foundApplications = [];
|
||||
$foundApplicationPreviews = [];
|
||||
$foundDatabases = [];
|
||||
$foundServices = [];
|
||||
|
||||
foreach ($containers as $container) {
|
||||
$labels = Arr::undot(data_get($container, 'labels'));
|
||||
$containerStatus = data_get($container, 'state');
|
||||
$containerHealth = data_get($container, 'health_status', 'unhealthy');
|
||||
$containerStatus = "$containerStatus ($containerHealth)";
|
||||
$applicationId = data_get($labels, 'coolify.applicationId');
|
||||
if ($applicationId) {
|
||||
$pullRequestId = data_get($labels, 'coolify.pullRequestId');
|
||||
if ($pullRequestId) {
|
||||
if (str($applicationId)->contains('-')) {
|
||||
$applicationId = str($applicationId)->before('-');
|
||||
}
|
||||
$preview = ApplicationPreview::where('application_id', $applicationId)->where('pull_request_id', $pullRequestId)->first();
|
||||
if ($preview) {
|
||||
$foundApplicationPreviews[] = $preview->id;
|
||||
$statusFromDb = $preview->status;
|
||||
if ($statusFromDb !== $containerStatus) {
|
||||
$preview->update(['status' => $containerStatus]);
|
||||
}
|
||||
} else {
|
||||
//Notify user that this container should not be there.
|
||||
}
|
||||
} else {
|
||||
$application = $this->applications->where('id', $applicationId)->first();
|
||||
if ($application) {
|
||||
$foundApplications[] = $application->id;
|
||||
$statusFromDb = $application->status;
|
||||
if ($statusFromDb !== $containerStatus) {
|
||||
$application->update(['status' => $containerStatus]);
|
||||
}
|
||||
} else {
|
||||
//Notify user that this container should not be there.
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$uuid = data_get($labels, 'com.docker.compose.service');
|
||||
$type = data_get($labels, 'coolify.type');
|
||||
if ($uuid) {
|
||||
if ($type === 'service') {
|
||||
$database_id = data_get($labels, 'coolify.service.subId');
|
||||
if ($database_id) {
|
||||
$service_db = ServiceDatabase::where('id', $database_id)->first();
|
||||
if ($service_db) {
|
||||
$uuid = $service_db->service->uuid;
|
||||
$isPublic = data_get($service_db, 'is_public');
|
||||
if ($isPublic) {
|
||||
$foundTcpProxy = $containers->filter(function ($value, $key) use ($uuid) {
|
||||
if ($this->server->isSwarm()) {
|
||||
// TODO: fix this with sentinel
|
||||
return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid";
|
||||
} else {
|
||||
return data_get($value, 'name') === "$uuid-proxy";
|
||||
}
|
||||
})->first();
|
||||
if (! $foundTcpProxy) {
|
||||
StartDatabaseProxy::run($service_db);
|
||||
// $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$service_db->service->name}", $this->server));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$database = $databases->where('uuid', $uuid)->first();
|
||||
if ($database) {
|
||||
$isPublic = data_get($database, 'is_public');
|
||||
$foundDatabases[] = $database->id;
|
||||
$statusFromDb = $database->status;
|
||||
if ($statusFromDb !== $containerStatus) {
|
||||
$database->update(['status' => $containerStatus]);
|
||||
}
|
||||
if ($isPublic) {
|
||||
$foundTcpProxy = $containers->filter(function ($value, $key) use ($uuid) {
|
||||
if ($this->server->isSwarm()) {
|
||||
// TODO: fix this with sentinel
|
||||
return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid";
|
||||
} else {
|
||||
return data_get($value, 'name') === "$uuid-proxy";
|
||||
}
|
||||
})->first();
|
||||
if (! $foundTcpProxy) {
|
||||
StartDatabaseProxy::run($database);
|
||||
$this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Notify user that this container should not be there.
|
||||
}
|
||||
}
|
||||
}
|
||||
if (data_get($container, 'name') === 'coolify-db') {
|
||||
$foundDatabases[] = 0;
|
||||
}
|
||||
}
|
||||
$serviceLabelId = data_get($labels, 'coolify.serviceId');
|
||||
if ($serviceLabelId) {
|
||||
$subType = data_get($labels, 'coolify.service.subType');
|
||||
$subId = data_get($labels, 'coolify.service.subId');
|
||||
$service = $services->where('id', $serviceLabelId)->first();
|
||||
if (! $service) {
|
||||
continue;
|
||||
}
|
||||
if ($subType === 'application') {
|
||||
$service = $service->applications()->where('id', $subId)->first();
|
||||
} else {
|
||||
$service = $service->databases()->where('id', $subId)->first();
|
||||
}
|
||||
if ($service) {
|
||||
$foundServices[] = "$service->id-$service->name";
|
||||
$statusFromDb = $service->status;
|
||||
if ($statusFromDb !== $containerStatus) {
|
||||
// ray('Updating status: ' . $containerStatus);
|
||||
$service->update(['status' => $containerStatus]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$exitedServices = collect([]);
|
||||
foreach ($services as $service) {
|
||||
$apps = $service->applications()->get();
|
||||
$dbs = $service->databases()->get();
|
||||
foreach ($apps as $app) {
|
||||
if (in_array("$app->id-$app->name", $foundServices)) {
|
||||
continue;
|
||||
} else {
|
||||
$exitedServices->push($app);
|
||||
}
|
||||
}
|
||||
foreach ($dbs as $db) {
|
||||
if (in_array("$db->id-$db->name", $foundServices)) {
|
||||
continue;
|
||||
} else {
|
||||
$exitedServices->push($db);
|
||||
}
|
||||
}
|
||||
}
|
||||
$exitedServices = $exitedServices->unique('id');
|
||||
foreach ($exitedServices as $exitedService) {
|
||||
if (str($exitedService->status)->startsWith('exited')) {
|
||||
continue;
|
||||
}
|
||||
$name = data_get($exitedService, 'name');
|
||||
$fqdn = data_get($exitedService, 'fqdn');
|
||||
if ($name) {
|
||||
if ($fqdn) {
|
||||
$containerName = "$name, available at $fqdn";
|
||||
} else {
|
||||
$containerName = $name;
|
||||
}
|
||||
} else {
|
||||
if ($fqdn) {
|
||||
$containerName = $fqdn;
|
||||
} else {
|
||||
$containerName = null;
|
||||
}
|
||||
}
|
||||
$projectUuid = data_get($service, 'environment.project.uuid');
|
||||
$serviceUuid = data_get($service, 'uuid');
|
||||
$environmentName = data_get($service, 'environment.name');
|
||||
|
||||
if ($projectUuid && $serviceUuid && $environmentName) {
|
||||
$url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/service/'.$serviceUuid;
|
||||
} else {
|
||||
$url = null;
|
||||
}
|
||||
// $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
|
||||
$exitedService->update(['status' => 'exited']);
|
||||
}
|
||||
|
||||
$notRunningApplications = $this->applications->pluck('id')->diff($foundApplications);
|
||||
foreach ($notRunningApplications as $applicationId) {
|
||||
$application = $this->applications->where('id', $applicationId)->first();
|
||||
if (str($application->status)->startsWith('exited')) {
|
||||
continue;
|
||||
}
|
||||
$application->update(['status' => 'exited']);
|
||||
|
||||
$name = data_get($application, 'name');
|
||||
$fqdn = data_get($application, 'fqdn');
|
||||
|
||||
$containerName = $name ? "$name ($fqdn)" : $fqdn;
|
||||
|
||||
$projectUuid = data_get($application, 'environment.project.uuid');
|
||||
$applicationUuid = data_get($application, 'uuid');
|
||||
$environment = data_get($application, 'environment.name');
|
||||
|
||||
if ($projectUuid && $applicationUuid && $environment) {
|
||||
$url = base_url().'/project/'.$projectUuid.'/'.$environment.'/application/'.$applicationUuid;
|
||||
} else {
|
||||
$url = null;
|
||||
}
|
||||
|
||||
// $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
|
||||
}
|
||||
$notRunningApplicationPreviews = $previews->pluck('id')->diff($foundApplicationPreviews);
|
||||
foreach ($notRunningApplicationPreviews as $previewId) {
|
||||
$preview = $previews->where('id', $previewId)->first();
|
||||
if (str($preview->status)->startsWith('exited')) {
|
||||
continue;
|
||||
}
|
||||
$preview->update(['status' => 'exited']);
|
||||
|
||||
$name = data_get($preview, 'name');
|
||||
$fqdn = data_get($preview, 'fqdn');
|
||||
|
||||
$containerName = $name ? "$name ($fqdn)" : $fqdn;
|
||||
|
||||
$projectUuid = data_get($preview, 'application.environment.project.uuid');
|
||||
$environmentName = data_get($preview, 'application.environment.name');
|
||||
$applicationUuid = data_get($preview, 'application.uuid');
|
||||
|
||||
if ($projectUuid && $applicationUuid && $environmentName) {
|
||||
$url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/application/'.$applicationUuid;
|
||||
} else {
|
||||
$url = null;
|
||||
}
|
||||
|
||||
// $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
|
||||
}
|
||||
$notRunningDatabases = $databases->pluck('id')->diff($foundDatabases);
|
||||
foreach ($notRunningDatabases as $database) {
|
||||
$database = $databases->where('id', $database)->first();
|
||||
if (str($database->status)->startsWith('exited')) {
|
||||
continue;
|
||||
}
|
||||
$database->update(['status' => 'exited']);
|
||||
|
||||
$name = data_get($database, 'name');
|
||||
$fqdn = data_get($database, 'fqdn');
|
||||
|
||||
$containerName = $name;
|
||||
|
||||
$projectUuid = data_get($database, 'environment.project.uuid');
|
||||
$environmentName = data_get($database, 'environment.name');
|
||||
$databaseUuid = data_get($database, 'uuid');
|
||||
|
||||
if ($projectUuid && $databaseUuid && $environmentName) {
|
||||
$url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/database/'.$databaseUuid;
|
||||
} else {
|
||||
$url = null;
|
||||
}
|
||||
// $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
|
||||
}
|
||||
|
||||
// Check if proxy is running
|
||||
$this->server->proxyType();
|
||||
$foundProxyContainer = $containers->filter(function ($value, $key) {
|
||||
if ($this->server->isSwarm()) {
|
||||
// TODO: fix this with sentinel
|
||||
return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik';
|
||||
} else {
|
||||
return data_get($value, 'name') === 'coolify-proxy';
|
||||
}
|
||||
})->first();
|
||||
if (! $foundProxyContainer) {
|
||||
try {
|
||||
$shouldStart = CheckProxy::run($this->server);
|
||||
if ($shouldStart) {
|
||||
StartProxy::run($this->server, false);
|
||||
$this->server->team?->notify(new ContainerRestarted('coolify-proxy', $this->server));
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
ray($e);
|
||||
}
|
||||
} else {
|
||||
$this->server->proxy->status = data_get($foundProxyContainer, 'state');
|
||||
$this->server->save();
|
||||
$connectProxyToDockerNetworks = connectProxyToNetworks($this->server);
|
||||
instant_remote_process($connectProxyToDockerNetworks, $this->server, false);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// send_internal_notification("ContainerStatusJob failed on ({$this->server->id}) with: " . $e->getMessage());
|
||||
ray($e->getMessage());
|
||||
|
||||
return handleError($e);
|
||||
}
|
||||
}
|
||||
|
||||
private function old_way()
|
||||
{
|
||||
if ($this->server->isSwarm()) {
|
||||
$containers = instant_remote_process(["docker service inspect $(docker service ls -q) --format '{{json .}}'"], $this->server, false);
|
||||
$containerReplicates = instant_remote_process(["docker service ls --format '{{json .}}'"], $this->server, false);
|
||||
} else {
|
||||
// Precheck for containers
|
||||
$containers = instant_remote_process(['docker container ls -q'], $this->server, false);
|
||||
if (! $containers) {
|
||||
return;
|
||||
}
|
||||
$containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this->server, false);
|
||||
$containerReplicates = null;
|
||||
}
|
||||
if (is_null($containers)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$containers = format_docker_command_output_to_json($containers);
|
||||
if ($containerReplicates) {
|
||||
$containerReplicates = format_docker_command_output_to_json($containerReplicates);
|
||||
foreach ($containerReplicates as $containerReplica) {
|
||||
$name = data_get($containerReplica, 'Name');
|
||||
$containers = $containers->map(function ($container) use ($name, $containerReplica) {
|
||||
if (data_get($container, 'Spec.Name') === $name) {
|
||||
$replicas = data_get($containerReplica, 'Replicas');
|
||||
$running = str($replicas)->explode('/')[0];
|
||||
$total = str($replicas)->explode('/')[1];
|
||||
if ($running === $total) {
|
||||
data_set($container, 'State.Status', 'running');
|
||||
data_set($container, 'State.Health.Status', 'healthy');
|
||||
} else {
|
||||
data_set($container, 'State.Status', 'starting');
|
||||
data_set($container, 'State.Health.Status', 'unhealthy');
|
||||
}
|
||||
}
|
||||
|
||||
return $container;
|
||||
});
|
||||
}
|
||||
}
|
||||
$databases = $this->server->databases();
|
||||
$services = $this->server->services()->get();
|
||||
$previews = $this->server->previews();
|
||||
$foundApplications = [];
|
||||
$foundApplicationPreviews = [];
|
||||
$foundDatabases = [];
|
||||
$foundServices = [];
|
||||
|
||||
foreach ($containers as $container) {
|
||||
if ($this->server->isSwarm()) {
|
||||
$labels = data_get($container, 'Spec.Labels');
|
||||
$uuid = data_get($labels, 'coolify.name');
|
||||
} else {
|
||||
$labels = data_get($container, 'Config.Labels');
|
||||
}
|
||||
$containerStatus = data_get($container, 'State.Status');
|
||||
$containerHealth = data_get($container, 'State.Health.Status', 'unhealthy');
|
||||
$containerStatus = "$containerStatus ($containerHealth)";
|
||||
$labels = Arr::undot(format_docker_labels_to_json($labels));
|
||||
$applicationId = data_get($labels, 'coolify.applicationId');
|
||||
if ($applicationId) {
|
||||
$pullRequestId = data_get($labels, 'coolify.pullRequestId');
|
||||
if ($pullRequestId) {
|
||||
if (str($applicationId)->contains('-')) {
|
||||
$applicationId = str($applicationId)->before('-');
|
||||
}
|
||||
$preview = ApplicationPreview::where('application_id', $applicationId)->where('pull_request_id', $pullRequestId)->first();
|
||||
if ($preview) {
|
||||
$foundApplicationPreviews[] = $preview->id;
|
||||
$statusFromDb = $preview->status;
|
||||
if ($statusFromDb !== $containerStatus) {
|
||||
$preview->update(['status' => $containerStatus]);
|
||||
}
|
||||
} else {
|
||||
//Notify user that this container should not be there.
|
||||
}
|
||||
} else {
|
||||
$application = $this->applications->where('id', $applicationId)->first();
|
||||
if ($application) {
|
||||
$foundApplications[] = $application->id;
|
||||
$statusFromDb = $application->status;
|
||||
if ($statusFromDb !== $containerStatus) {
|
||||
$application->update(['status' => $containerStatus]);
|
||||
}
|
||||
} else {
|
||||
//Notify user that this container should not be there.
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$uuid = data_get($labels, 'com.docker.compose.service');
|
||||
$type = data_get($labels, 'coolify.type');
|
||||
|
||||
if ($uuid) {
|
||||
if ($type === 'service') {
|
||||
$database_id = data_get($labels, 'coolify.service.subId');
|
||||
if ($database_id) {
|
||||
$service_db = ServiceDatabase::where('id', $database_id)->first();
|
||||
if ($service_db) {
|
||||
$uuid = data_get($service_db, 'service.uuid');
|
||||
if ($uuid) {
|
||||
$isPublic = data_get($service_db, 'is_public');
|
||||
if ($isPublic) {
|
||||
$foundTcpProxy = $containers->filter(function ($value, $key) use ($uuid) {
|
||||
if ($this->server->isSwarm()) {
|
||||
return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid";
|
||||
} else {
|
||||
return data_get($value, 'Name') === "/$uuid-proxy";
|
||||
}
|
||||
})->first();
|
||||
if (! $foundTcpProxy) {
|
||||
StartDatabaseProxy::run($service_db);
|
||||
// $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$service_db->service->name}", $this->server));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$database = $databases->where('uuid', $uuid)->first();
|
||||
if ($database) {
|
||||
$isPublic = data_get($database, 'is_public');
|
||||
$foundDatabases[] = $database->id;
|
||||
$statusFromDb = $database->status;
|
||||
if ($statusFromDb !== $containerStatus) {
|
||||
$database->update(['status' => $containerStatus]);
|
||||
}
|
||||
if ($isPublic) {
|
||||
$foundTcpProxy = $containers->filter(function ($value, $key) use ($uuid) {
|
||||
if ($this->server->isSwarm()) {
|
||||
return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid";
|
||||
} else {
|
||||
return data_get($value, 'Name') === "/$uuid-proxy";
|
||||
}
|
||||
})->first();
|
||||
if (! $foundTcpProxy) {
|
||||
StartDatabaseProxy::run($database);
|
||||
$this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Notify user that this container should not be there.
|
||||
}
|
||||
}
|
||||
}
|
||||
if (data_get($container, 'Name') === '/coolify-db') {
|
||||
$foundDatabases[] = 0;
|
||||
}
|
||||
}
|
||||
$serviceLabelId = data_get($labels, 'coolify.serviceId');
|
||||
if ($serviceLabelId) {
|
||||
$subType = data_get($labels, 'coolify.service.subType');
|
||||
$subId = data_get($labels, 'coolify.service.subId');
|
||||
$service = $services->where('id', $serviceLabelId)->first();
|
||||
if (! $service) {
|
||||
continue;
|
||||
}
|
||||
if ($subType === 'application') {
|
||||
$service = $service->applications()->where('id', $subId)->first();
|
||||
} else {
|
||||
$service = $service->databases()->where('id', $subId)->first();
|
||||
}
|
||||
if ($service) {
|
||||
$foundServices[] = "$service->id-$service->name";
|
||||
$statusFromDb = $service->status;
|
||||
if ($statusFromDb !== $containerStatus) {
|
||||
// ray('Updating status: ' . $containerStatus);
|
||||
$service->update(['status' => $containerStatus]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$exitedServices = collect([]);
|
||||
foreach ($services as $service) {
|
||||
$apps = $service->applications()->get();
|
||||
$dbs = $service->databases()->get();
|
||||
foreach ($apps as $app) {
|
||||
if (in_array("$app->id-$app->name", $foundServices)) {
|
||||
continue;
|
||||
} else {
|
||||
$exitedServices->push($app);
|
||||
}
|
||||
}
|
||||
foreach ($dbs as $db) {
|
||||
if (in_array("$db->id-$db->name", $foundServices)) {
|
||||
continue;
|
||||
} else {
|
||||
$exitedServices->push($db);
|
||||
}
|
||||
}
|
||||
}
|
||||
$exitedServices = $exitedServices->unique('id');
|
||||
foreach ($exitedServices as $exitedService) {
|
||||
if (str($exitedService->status)->startsWith('exited')) {
|
||||
continue;
|
||||
}
|
||||
$name = data_get($exitedService, 'name');
|
||||
$fqdn = data_get($exitedService, 'fqdn');
|
||||
if ($name) {
|
||||
if ($fqdn) {
|
||||
$containerName = "$name, available at $fqdn";
|
||||
} else {
|
||||
$containerName = $name;
|
||||
}
|
||||
} else {
|
||||
if ($fqdn) {
|
||||
$containerName = $fqdn;
|
||||
} else {
|
||||
$containerName = null;
|
||||
}
|
||||
}
|
||||
$projectUuid = data_get($service, 'environment.project.uuid');
|
||||
$serviceUuid = data_get($service, 'uuid');
|
||||
$environmentName = data_get($service, 'environment.name');
|
||||
|
||||
if ($projectUuid && $serviceUuid && $environmentName) {
|
||||
$url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/service/'.$serviceUuid;
|
||||
} else {
|
||||
$url = null;
|
||||
}
|
||||
// $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
|
||||
$exitedService->update(['status' => 'exited']);
|
||||
}
|
||||
|
||||
$notRunningApplications = $this->applications->pluck('id')->diff($foundApplications);
|
||||
foreach ($notRunningApplications as $applicationId) {
|
||||
$application = $this->applications->where('id', $applicationId)->first();
|
||||
if (str($application->status)->startsWith('exited')) {
|
||||
continue;
|
||||
}
|
||||
$application->update(['status' => 'exited']);
|
||||
|
||||
$name = data_get($application, 'name');
|
||||
$fqdn = data_get($application, 'fqdn');
|
||||
|
||||
$containerName = $name ? "$name ($fqdn)" : $fqdn;
|
||||
|
||||
$projectUuid = data_get($application, 'environment.project.uuid');
|
||||
$applicationUuid = data_get($application, 'uuid');
|
||||
$environment = data_get($application, 'environment.name');
|
||||
|
||||
if ($projectUuid && $applicationUuid && $environment) {
|
||||
$url = base_url().'/project/'.$projectUuid.'/'.$environment.'/application/'.$applicationUuid;
|
||||
} else {
|
||||
$url = null;
|
||||
}
|
||||
|
||||
// $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
|
||||
}
|
||||
$notRunningApplicationPreviews = $previews->pluck('id')->diff($foundApplicationPreviews);
|
||||
foreach ($notRunningApplicationPreviews as $previewId) {
|
||||
$preview = $previews->where('id', $previewId)->first();
|
||||
if (str($preview->status)->startsWith('exited')) {
|
||||
continue;
|
||||
}
|
||||
$preview->update(['status' => 'exited']);
|
||||
|
||||
$name = data_get($preview, 'name');
|
||||
$fqdn = data_get($preview, 'fqdn');
|
||||
|
||||
$containerName = $name ? "$name ($fqdn)" : $fqdn;
|
||||
|
||||
$projectUuid = data_get($preview, 'application.environment.project.uuid');
|
||||
$environmentName = data_get($preview, 'application.environment.name');
|
||||
$applicationUuid = data_get($preview, 'application.uuid');
|
||||
|
||||
if ($projectUuid && $applicationUuid && $environmentName) {
|
||||
$url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/application/'.$applicationUuid;
|
||||
} else {
|
||||
$url = null;
|
||||
}
|
||||
|
||||
// $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
|
||||
}
|
||||
$notRunningDatabases = $databases->pluck('id')->diff($foundDatabases);
|
||||
foreach ($notRunningDatabases as $database) {
|
||||
$database = $databases->where('id', $database)->first();
|
||||
if (str($database->status)->startsWith('exited')) {
|
||||
continue;
|
||||
}
|
||||
$database->update(['status' => 'exited']);
|
||||
|
||||
$name = data_get($database, 'name');
|
||||
$fqdn = data_get($database, 'fqdn');
|
||||
|
||||
$containerName = $name;
|
||||
|
||||
$projectUuid = data_get($database, 'environment.project.uuid');
|
||||
$environmentName = data_get($database, 'environment.name');
|
||||
$databaseUuid = data_get($database, 'uuid');
|
||||
|
||||
if ($projectUuid && $databaseUuid && $environmentName) {
|
||||
$url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/database/'.$databaseUuid;
|
||||
} else {
|
||||
$url = null;
|
||||
}
|
||||
// $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
|
||||
}
|
||||
|
||||
// Check if proxy is running
|
||||
$this->server->proxyType();
|
||||
$foundProxyContainer = $containers->filter(function ($value, $key) {
|
||||
if ($this->server->isSwarm()) {
|
||||
return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik';
|
||||
} else {
|
||||
return data_get($value, 'Name') === '/coolify-proxy';
|
||||
}
|
||||
})->first();
|
||||
if (! $foundProxyContainer) {
|
||||
try {
|
||||
$shouldStart = CheckProxy::run($this->server);
|
||||
if ($shouldStart) {
|
||||
StartProxy::run($this->server, false);
|
||||
$this->server->team?->notify(new ContainerRestarted('coolify-proxy', $this->server));
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
ray($e);
|
||||
}
|
||||
} else {
|
||||
$this->server->proxy->status = data_get($foundProxyContainer, 'State.Status');
|
||||
$this->server->save();
|
||||
$connectProxyToDockerNetworks = connectProxyToNetworks($this->server);
|
||||
instant_remote_process($connectProxyToDockerNetworks, $this->server, false);
|
||||
}
|
||||
}
|
||||
}
|
@ -21,7 +21,7 @@ class CreateNewUser implements CreatesNewUsers
|
||||
public function create(array $input): User
|
||||
{
|
||||
$settings = InstanceSettings::get();
|
||||
if (!$settings->is_registration_enabled) {
|
||||
if (! $settings->is_registration_enabled) {
|
||||
abort(403);
|
||||
}
|
||||
Validator::make($input, [
|
||||
@ -66,6 +66,7 @@ public function create(array $input): User
|
||||
}
|
||||
// Set session variable
|
||||
session(['currentTeam' => $user->currentTeam = $team]);
|
||||
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
|
@ -6,10 +6,10 @@
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
|
||||
class CheckResaleLicense
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle()
|
||||
{
|
||||
try {
|
||||
@ -18,6 +18,7 @@ public function handle()
|
||||
$settings->update([
|
||||
'is_resale_license_active' => true,
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
// if (!$settings->resale_license) {
|
||||
@ -38,6 +39,7 @@ public function handle()
|
||||
$settings->update([
|
||||
'is_resale_license_active' => true,
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
$data = Http::withHeaders([
|
||||
@ -51,6 +53,7 @@ public function handle()
|
||||
$settings->update([
|
||||
'is_resale_license_active' => true,
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
if (data_get($data, 'license_key.status') === 'active') {
|
||||
|
@ -2,27 +2,32 @@
|
||||
|
||||
namespace App\Actions\Proxy;
|
||||
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Support\Str;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class CheckConfiguration
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(Server $server, bool $reset = false)
|
||||
{
|
||||
$proxy_path = get_proxy_path();
|
||||
$proxy_configuration = instant_remote_process([
|
||||
$proxyType = $server->proxyType();
|
||||
if ($proxyType === 'NONE') {
|
||||
return 'OK';
|
||||
}
|
||||
$proxy_path = $server->proxyPath();
|
||||
$payload = [
|
||||
"mkdir -p $proxy_path",
|
||||
"cat $proxy_path/docker-compose.yml",
|
||||
], $server, false);
|
||||
];
|
||||
$proxy_configuration = instant_remote_process($payload, $server, false);
|
||||
if ($reset || ! $proxy_configuration || is_null($proxy_configuration)) {
|
||||
$proxy_configuration = str(generate_default_proxy_configuration($server))->trim()->value;
|
||||
}
|
||||
if (! $proxy_configuration || is_null($proxy_configuration)) {
|
||||
throw new \Exception('Could not generate proxy configuration');
|
||||
}
|
||||
|
||||
if ($reset || !$proxy_configuration || is_null($proxy_configuration)) {
|
||||
$proxy_configuration = Str::of(generate_default_proxy_configuration($server))->trim()->value;
|
||||
}
|
||||
if (!$proxy_configuration || is_null($proxy_configuration)) {
|
||||
throw new \Exception("Could not generate proxy configuration");
|
||||
}
|
||||
return $proxy_configuration;
|
||||
}
|
||||
}
|
||||
|
@ -8,11 +8,31 @@
|
||||
class CheckProxy
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(Server $server, $fromUI = false)
|
||||
{
|
||||
if (!$server->isProxyShouldRun()) {
|
||||
if (! $server->isFunctional()) {
|
||||
return false;
|
||||
}
|
||||
if ($server->isBuildServer()) {
|
||||
if ($server->proxy) {
|
||||
$server->proxy = null;
|
||||
$server->save();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
$proxyType = $server->proxyType();
|
||||
if (is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop) {
|
||||
return false;
|
||||
}
|
||||
['uptime' => $uptime, 'error' => $error] = $server->validateConnection();
|
||||
if (! $uptime) {
|
||||
throw new \Exception($error);
|
||||
}
|
||||
if (! $server->isProxyShouldRun()) {
|
||||
if ($fromUI) {
|
||||
throw new \Exception("Proxy should not run. You selected the Custom Proxy.");
|
||||
throw new \Exception('Proxy should not run. You selected the Custom Proxy.');
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
@ -24,12 +44,17 @@ public function handle(Server $server, $fromUI = false)
|
||||
if ($status === 'running') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} else {
|
||||
$status = getContainerStatus($server, 'coolify-proxy');
|
||||
if ($status === 'running') {
|
||||
$server->proxy->set('status', 'running');
|
||||
$server->save();
|
||||
|
||||
return false;
|
||||
}
|
||||
if ($server->settings->is_cloudflare_tunnel) {
|
||||
return false;
|
||||
}
|
||||
$ip = $server->ip;
|
||||
@ -43,18 +68,19 @@ public function handle(Server $server, $fromUI = false)
|
||||
$port443 = is_resource($connection443) && fclose($connection443);
|
||||
if ($port80) {
|
||||
if ($fromUI) {
|
||||
throw new \Exception("Port 80 is in use.<br>You must stop the process using this port.<br>Docs: <a target='_blank' href='https://coolify.io/docs'>https://coolify.io/docs</a> <br> Discord: <a target='_blank' href='https://coollabs.io/discord'>https://coollabs.io/discord</a>");
|
||||
throw new \Exception("Port 80 is in use.<br>You must stop the process using this port.<br>Docs: <a target='_blank' href='https://coolify.io/docs'>https://coolify.io/docs</a><br>Discord: <a target='_blank' href='https://coollabs.io/discord'>https://coollabs.io/discord</a>");
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if ($port443) {
|
||||
if ($fromUI) {
|
||||
throw new \Exception("Port 443 is in use.<br>You must stop the process using this port.<br>Docs: <a target='_blank' href='https://coolify.io/docs'>https://coolify.io/docs</a> <br> Discord: <a target='_blank' href='https://coollabs.io/discord'>https://coollabs.io/discord</a>");
|
||||
throw new \Exception("Port 443 is in use.<br>You must stop the process using this port.<br>Docs: <a target='_blank' href='https://coolify.io/docs'>https://coolify.io/docs</a><br>Discord: <a target='_blank' href='https://coollabs.io/discord'>https://coollabs.io/discord</a>");
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,6 @@
|
||||
namespace App\Actions\Proxy;
|
||||
|
||||
use App\Models\Server;
|
||||
use Illuminate\Support\Str;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class SaveConfiguration
|
||||
@ -15,15 +14,15 @@ public function handle(Server $server, ?string $proxy_settings = null)
|
||||
if (is_null($proxy_settings)) {
|
||||
$proxy_settings = CheckConfiguration::run($server, true);
|
||||
}
|
||||
$proxy_path = get_proxy_path();
|
||||
$proxy_path = $server->proxyPath();
|
||||
$docker_compose_yml_base64 = base64_encode($proxy_settings);
|
||||
|
||||
$server->proxy->last_saved_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value;
|
||||
$server->proxy->last_saved_settings = str($docker_compose_yml_base64)->pipe('md5')->value;
|
||||
$server->save();
|
||||
|
||||
return instant_remote_process([
|
||||
"mkdir -p $proxy_path",
|
||||
"echo '$docker_compose_yml_base64' | base64 -d > $proxy_path/docker-compose.yml",
|
||||
"echo '$docker_compose_yml_base64' | base64 -d | tee $proxy_path/docker-compose.yml > /dev/null",
|
||||
], $server);
|
||||
}
|
||||
}
|
||||
|
@ -2,60 +2,70 @@
|
||||
|
||||
namespace App\Actions\Proxy;
|
||||
|
||||
use App\Events\ProxyStatusChanged;
|
||||
use App\Events\ProxyStarted;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Support\Str;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
|
||||
class StartProxy
|
||||
{
|
||||
use AsAction;
|
||||
public function handle(Server $server, bool $async = true): string|Activity
|
||||
|
||||
public function handle(Server $server, bool $async = true, bool $force = false): string|Activity
|
||||
{
|
||||
try {
|
||||
$proxyType = $server->proxyType();
|
||||
if ((is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop || $server->isBuildServer()) && $force === false) {
|
||||
return 'OK';
|
||||
}
|
||||
$commands = collect([]);
|
||||
$proxy_path = get_proxy_path();
|
||||
$proxy_path = $server->proxyPath();
|
||||
$configuration = CheckConfiguration::run($server);
|
||||
if (!$configuration) {
|
||||
throw new \Exception("Configuration is not synced");
|
||||
if (! $configuration) {
|
||||
throw new \Exception('Configuration is not synced');
|
||||
}
|
||||
SaveConfiguration::run($server, $configuration);
|
||||
$docker_compose_yml_base64 = base64_encode($configuration);
|
||||
$server->proxy->last_applied_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value;
|
||||
$server->proxy->last_applied_settings = str($docker_compose_yml_base64)->pipe('md5')->value;
|
||||
$server->save();
|
||||
if ($server->isSwarm()) {
|
||||
$commands = $commands->merge([
|
||||
"mkdir -p $proxy_path/dynamic && cd $proxy_path",
|
||||
"mkdir -p $proxy_path/dynamic",
|
||||
"cd $proxy_path",
|
||||
"echo 'Creating required Docker Compose file.'",
|
||||
"echo 'Starting coolify-proxy.'",
|
||||
"cd $proxy_path && docker stack deploy -c docker-compose.yml coolify-proxy",
|
||||
"echo 'Proxy started successfully.'"
|
||||
'docker stack deploy -c docker-compose.yml coolify-proxy',
|
||||
"echo 'Proxy started successfully.'",
|
||||
]);
|
||||
} else {
|
||||
$caddfile = 'import /dynamic/*.caddy';
|
||||
$commands = $commands->merge([
|
||||
"mkdir -p $proxy_path/dynamic && cd $proxy_path",
|
||||
"mkdir -p $proxy_path/dynamic",
|
||||
"cd $proxy_path",
|
||||
"echo '$caddfile' > $proxy_path/dynamic/Caddyfile",
|
||||
"echo 'Creating required Docker Compose file.'",
|
||||
"echo 'Pulling docker image.'",
|
||||
'docker compose pull',
|
||||
"echo 'Stopping existing coolify-proxy.'",
|
||||
"docker compose down -v --remove-orphans > /dev/null 2>&1",
|
||||
'docker compose down -v --remove-orphans > /dev/null 2>&1',
|
||||
"echo 'Starting coolify-proxy.'",
|
||||
'docker compose up -d --remove-orphans',
|
||||
"echo 'Proxy started successfully.'"
|
||||
"echo 'Proxy started successfully.'",
|
||||
]);
|
||||
$commands = $commands->merge(connectProxyToNetworks($server));
|
||||
}
|
||||
|
||||
if ($async) {
|
||||
$activity = remote_process($commands, $server);
|
||||
$activity = remote_process($commands, $server, callEventOnFinish: 'ProxyStarted', callEventData: $server);
|
||||
|
||||
return $activity;
|
||||
} else {
|
||||
instant_remote_process($commands, $server);
|
||||
$server->proxy->set('status', 'running');
|
||||
$server->proxy->set('type', $proxyType);
|
||||
$server->save();
|
||||
ProxyStarted::dispatch($server);
|
||||
|
||||
return 'OK';
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
|
@ -2,14 +2,17 @@
|
||||
|
||||
namespace App\Actions\Server;
|
||||
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use App\Models\Server;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class CleanupDocker
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(Server $server, bool $force = true)
|
||||
{
|
||||
|
||||
// cleanup docker images, containers, and builder caches
|
||||
if ($force) {
|
||||
instant_remote_process(['docker image prune -af'], $server, false);
|
||||
instant_remote_process(['docker container prune -f --filter "label=coolify.managed=true"'], $server, false);
|
||||
@ -19,5 +22,15 @@ public function handle(Server $server, bool $force = true)
|
||||
instant_remote_process(['docker container prune -f --filter "label=coolify.managed=true"'], $server, false);
|
||||
instant_remote_process(['docker builder prune -f'], $server, false);
|
||||
}
|
||||
// cleanup networks
|
||||
// $networks = collectDockerNetworksByServer($server);
|
||||
// $proxyNetworks = collectProxyDockerNetworksByServer($server);
|
||||
// $diff = $proxyNetworks->diff($networks);
|
||||
// if ($diff->count() > 0) {
|
||||
// $diff->map(function ($network) use ($server) {
|
||||
// instant_remote_process(["docker network disconnect $network coolify-proxy"], $server);
|
||||
// instant_remote_process(["docker network rm $network"], $server);
|
||||
// });
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
51
app/Actions/Server/ConfigureCloudflared.php
Normal file
51
app/Actions/Server/ConfigureCloudflared.php
Normal file
@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Server;
|
||||
|
||||
use App\Models\Server;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
class ConfigureCloudflared
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(Server $server, string $cloudflare_token)
|
||||
{
|
||||
try {
|
||||
$config = [
|
||||
'services' => [
|
||||
'coolify-cloudflared' => [
|
||||
'container_name' => 'coolify-cloudflared',
|
||||
'image' => 'cloudflare/cloudflared:latest',
|
||||
'restart' => RESTART_MODE,
|
||||
'network_mode' => 'host',
|
||||
'command' => 'tunnel run',
|
||||
'environment' => [
|
||||
"TUNNEL_TOKEN={$cloudflare_token}",
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
$config = Yaml::dump($config, 12, 2);
|
||||
$docker_compose_yml_base64 = base64_encode($config);
|
||||
$commands = collect([
|
||||
'mkdir -p /tmp/cloudflared',
|
||||
'cd /tmp/cloudflared',
|
||||
"echo '$docker_compose_yml_base64' | base64 -d | tee docker-compose.yml > /dev/null",
|
||||
'docker compose pull',
|
||||
'docker compose down -v --remove-orphans > /dev/null 2>&1',
|
||||
'docker compose up -d --remove-orphans',
|
||||
]);
|
||||
instant_remote_process($commands, $server);
|
||||
} catch (\Throwable $e) {
|
||||
ray($e);
|
||||
throw $e;
|
||||
} finally {
|
||||
$commands = collect([
|
||||
'rm -fr /tmp/cloudflared',
|
||||
]);
|
||||
instant_remote_process($commands, $server);
|
||||
}
|
||||
}
|
||||
}
|
@ -2,20 +2,21 @@
|
||||
|
||||
namespace App\Actions\Server;
|
||||
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use App\Models\Server;
|
||||
use App\Models\StandaloneDocker;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class InstallDocker
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(Server $server)
|
||||
{
|
||||
$supported_os_type = $server->validateOS();
|
||||
if (!$supported_os_type) {
|
||||
throw new \Exception('Server OS type is not supported for automated installation. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://coolify.io/docs/servers#install-docker-engine-manually">documentation</a>.');
|
||||
if (! $supported_os_type) {
|
||||
throw new \Exception('Server OS type is not supported for automated installation. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://coolify.io/docs/installation#manually">documentation</a>.');
|
||||
}
|
||||
ray('Installing Docker on server: ' . $server->name . ' (' . $server->ip . ')' . ' with OS type: ' . $supported_os_type);
|
||||
ray('Installing Docker on server: '.$server->name.' ('.$server->ip.')'.' with OS type: '.$supported_os_type);
|
||||
$dockerVersion = '24.0';
|
||||
$config = base64_encode('{
|
||||
"log-driver": "json-file",
|
||||
@ -36,32 +37,41 @@ public function handle(Server $server)
|
||||
if (isDev() && $server->id === 0) {
|
||||
$command = $command->merge([
|
||||
"echo 'Installing Prerequisites...'",
|
||||
"sleep 1",
|
||||
'sleep 1',
|
||||
"echo 'Installing Docker Engine...'",
|
||||
"echo 'Configuring Docker Engine (merging existing configuration with the required)...'",
|
||||
"sleep 4",
|
||||
'sleep 4',
|
||||
"echo 'Restarting Docker Engine...'",
|
||||
"ls -l /tmp"
|
||||
'ls -l /tmp',
|
||||
]);
|
||||
|
||||
return remote_process($command, $server);
|
||||
} else {
|
||||
if ($supported_os_type->contains('debian')) {
|
||||
$command = $command->merge([
|
||||
"echo 'Installing Prerequisites...'",
|
||||
"command -v jq >/dev/null || apt-get update -y",
|
||||
"command -v jq >/dev/null || apt install -y curl wget git jq",
|
||||
|
||||
'apt-get update -y',
|
||||
'command -v curl >/dev/null || apt install -y curl',
|
||||
'command -v wget >/dev/null || apt install -y wget',
|
||||
'command -v git >/dev/null || apt install -y git',
|
||||
'command -v jq >/dev/null || apt install -y jq',
|
||||
]);
|
||||
} else if ($supported_os_type->contains('rhel')) {
|
||||
} elseif ($supported_os_type->contains('rhel')) {
|
||||
$command = $command->merge([
|
||||
"echo 'Installing Prerequisites...'",
|
||||
"command -v jq >/dev/null || dnf install -y curl wget git jq",
|
||||
'command -v curl >/dev/null || dnf install -y curl',
|
||||
'command -v wget >/dev/null || dnf install -y wget',
|
||||
'command -v git >/dev/null || dnf install -y git',
|
||||
'command -v jq >/dev/null || dnf install -y jq',
|
||||
]);
|
||||
} else if ($supported_os_type->contains('sles')) {
|
||||
} elseif ($supported_os_type->contains('sles')) {
|
||||
$command = $command->merge([
|
||||
"echo 'Installing Prerequisites...'",
|
||||
"command -v jq >/dev/null || zypper update -y",
|
||||
"command -v jq >/dev/null || zypper install -y curl wget git jq",
|
||||
'zypper update -y',
|
||||
'command -v curl >/dev/null || zypper install -y curl',
|
||||
'command -v wget >/dev/null || zypper install -y wget',
|
||||
'command -v git >/dev/null || zypper install -y git',
|
||||
'command -v jq >/dev/null || zypper install -y jq',
|
||||
]);
|
||||
} else {
|
||||
throw new \Exception('Unsupported OS');
|
||||
@ -70,26 +80,30 @@ public function handle(Server $server)
|
||||
"echo 'Installing Docker Engine...'",
|
||||
"curl https://releases.rancher.com/install-docker/{$dockerVersion}.sh | sh || curl https://get.docker.com | sh -s -- --version {$dockerVersion}",
|
||||
"echo 'Configuring Docker Engine (merging existing configuration with the required)...'",
|
||||
"test -s /etc/docker/daemon.json && cp /etc/docker/daemon.json \"/etc/docker/daemon.json.original-`date +\"%Y%m%d-%H%M%S\"`\" || echo '{$config}' | base64 -d > /etc/docker/daemon.json",
|
||||
"echo '{$config}' | base64 -d > /etc/docker/daemon.json.coolify",
|
||||
"cat <<< $(jq . /etc/docker/daemon.json.coolify) > /etc/docker/daemon.json.coolify",
|
||||
"cat <<< $(jq -s '.[0] * .[1]' /etc/docker/daemon.json /etc/docker/daemon.json.coolify) > /etc/docker/daemon.json",
|
||||
'test -s /etc/docker/daemon.json && cp /etc/docker/daemon.json "/etc/docker/daemon.json.original-$(date +"%Y%m%d-%H%M%S")"',
|
||||
"test ! -s /etc/docker/daemon.json && echo '{$config}' | base64 -d | tee /etc/docker/daemon.json > /dev/null",
|
||||
"echo '{$config}' | base64 -d | tee /etc/docker/daemon.json.coolify > /dev/null",
|
||||
'jq . /etc/docker/daemon.json.coolify | tee /etc/docker/daemon.json.coolify.pretty > /dev/null',
|
||||
'mv /etc/docker/daemon.json.coolify.pretty /etc/docker/daemon.json.coolify',
|
||||
"jq -s '.[0] * .[1]' /etc/docker/daemon.json.coolify /etc/docker/daemon.json | tee /etc/docker/daemon.json.appended > /dev/null",
|
||||
'mv /etc/docker/daemon.json.appended /etc/docker/daemon.json',
|
||||
"echo 'Restarting Docker Engine...'",
|
||||
"systemctl enable docker >/dev/null 2>&1 || true",
|
||||
"systemctl restart docker",
|
||||
'systemctl enable docker >/dev/null 2>&1 || true',
|
||||
'systemctl restart docker',
|
||||
]);
|
||||
if ($server->isSwarm()) {
|
||||
$command = $command->merge([
|
||||
"docker network create --attachable --driver overlay coolify-overlay >/dev/null 2>&1 || true",
|
||||
'docker network create --attachable --driver overlay coolify-overlay >/dev/null 2>&1 || true',
|
||||
]);
|
||||
} else {
|
||||
$command = $command->merge([
|
||||
"docker network create --attachable coolify >/dev/null 2>&1 || true",
|
||||
'docker network create --attachable coolify >/dev/null 2>&1 || true',
|
||||
]);
|
||||
$command = $command->merge([
|
||||
"echo 'Done!'",
|
||||
]);
|
||||
}
|
||||
|
||||
return remote_process($command, $server);
|
||||
}
|
||||
}
|
||||
|
@ -2,21 +2,22 @@
|
||||
|
||||
namespace App\Actions\Server;
|
||||
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use App\Models\Server;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class InstallLogDrain
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(Server $server)
|
||||
{
|
||||
if ($server->settings->is_logdrain_newrelic_enabled) {
|
||||
$type = 'newrelic';
|
||||
} else if ($server->settings->is_logdrain_highlight_enabled) {
|
||||
} elseif ($server->settings->is_logdrain_highlight_enabled) {
|
||||
$type = 'highlight';
|
||||
} else if ($server->settings->is_logdrain_axiom_enabled) {
|
||||
} elseif ($server->settings->is_logdrain_axiom_enabled) {
|
||||
$type = 'axiom';
|
||||
} else if ($server->settings->is_logdrain_custom_enabled) {
|
||||
} elseif ($server->settings->is_logdrain_custom_enabled) {
|
||||
$type = 'custom';
|
||||
} else {
|
||||
$type = 'none';
|
||||
@ -25,11 +26,12 @@ public function handle(Server $server)
|
||||
if ($type === 'none') {
|
||||
$command = [
|
||||
"echo 'Stopping old Fluent Bit'",
|
||||
"docker rm -f coolify-log-drain || true",
|
||||
'docker rm -f coolify-log-drain || true',
|
||||
];
|
||||
|
||||
return instant_remote_process($command, $server);
|
||||
} else if ($type === 'newrelic') {
|
||||
if (!$server->settings->is_logdrain_newrelic_enabled) {
|
||||
} elseif ($type === 'newrelic') {
|
||||
if (! $server->settings->is_logdrain_newrelic_enabled) {
|
||||
throw new \Exception('New Relic log drain is not enabled.');
|
||||
}
|
||||
$config = base64_encode("
|
||||
@ -59,11 +61,11 @@ public function handle(Server $server)
|
||||
# https://log-api.newrelic.com/log/v1 - US
|
||||
base_uri \${BASE_URI}
|
||||
");
|
||||
} else if ($type === 'highlight') {
|
||||
if (!$server->settings->is_logdrain_highlight_enabled) {
|
||||
} elseif ($type === 'highlight') {
|
||||
if (! $server->settings->is_logdrain_highlight_enabled) {
|
||||
throw new \Exception('Highlight log drain is not enabled.');
|
||||
}
|
||||
$config = base64_encode("
|
||||
$config = base64_encode('
|
||||
[SERVICE]
|
||||
Flush 5
|
||||
Daemon off
|
||||
@ -71,7 +73,7 @@ public function handle(Server $server)
|
||||
Parsers_File parsers.conf
|
||||
[INPUT]
|
||||
Name forward
|
||||
tag \${HIGHLIGHT_PROJECT_ID}
|
||||
tag ${HIGHLIGHT_PROJECT_ID}
|
||||
Buffer_Chunk_Size 1M
|
||||
Buffer_Max_Size 6M
|
||||
[OUTPUT]
|
||||
@ -79,9 +81,9 @@ public function handle(Server $server)
|
||||
Match *
|
||||
Host otel.highlight.io
|
||||
Port 24224
|
||||
");
|
||||
} else if ($type === 'axiom') {
|
||||
if (!$server->settings->is_logdrain_axiom_enabled) {
|
||||
');
|
||||
} elseif ($type === 'axiom') {
|
||||
if (! $server->settings->is_logdrain_axiom_enabled) {
|
||||
throw new \Exception('Axiom log drain is not enabled.');
|
||||
}
|
||||
$config = base64_encode("
|
||||
@ -116,8 +118,8 @@ public function handle(Server $server)
|
||||
json_date_format iso8601
|
||||
tls On
|
||||
");
|
||||
} else if ($type === 'custom') {
|
||||
if (!$server->settings->is_logdrain_custom_enabled) {
|
||||
} elseif ($type === 'custom') {
|
||||
if (! $server->settings->is_logdrain_custom_enabled) {
|
||||
throw new \Exception('Custom log drain is not enabled.');
|
||||
}
|
||||
$config = base64_encode($server->settings->logdrain_custom_config);
|
||||
@ -133,7 +135,7 @@ public function handle(Server $server)
|
||||
Regex /^(?!\s*$).+/
|
||||
");
|
||||
}
|
||||
$compose = base64_encode("
|
||||
$compose = base64_encode('
|
||||
services:
|
||||
coolify-log-drain:
|
||||
image: cr.fluentbit.io/fluent/fluent-bit:2.0
|
||||
@ -147,7 +149,7 @@ public function handle(Server $server)
|
||||
ports:
|
||||
- 127.0.0.1:24224:24224
|
||||
restart: unless-stopped
|
||||
");
|
||||
');
|
||||
$readme = base64_encode('# New Relic Log Drain
|
||||
This log drain is based on [Fluent Bit](https://fluentbit.io/) and New Relic Log Forwarder.
|
||||
|
||||
@ -160,18 +162,18 @@ public function handle(Server $server)
|
||||
$base_uri = $server->settings->logdrain_newrelic_base_uri;
|
||||
$base_path = config('coolify.base_config_path');
|
||||
|
||||
$config_path = $base_path . '/log-drains';
|
||||
$fluent_bit_config = $config_path . '/fluent-bit.conf';
|
||||
$parsers_config = $config_path . '/parsers.conf';
|
||||
$compose_path = $config_path . '/docker-compose.yml';
|
||||
$readme_path = $config_path . '/README.md';
|
||||
$config_path = $base_path.'/log-drains';
|
||||
$fluent_bit_config = $config_path.'/fluent-bit.conf';
|
||||
$parsers_config = $config_path.'/parsers.conf';
|
||||
$compose_path = $config_path.'/docker-compose.yml';
|
||||
$readme_path = $config_path.'/README.md';
|
||||
$command = [
|
||||
"echo 'Saving configuration'",
|
||||
"mkdir -p $config_path",
|
||||
"echo '{$parsers}' | base64 -d > $parsers_config",
|
||||
"echo '{$config}' | base64 -d > $fluent_bit_config",
|
||||
"echo '{$compose}' | base64 -d > $compose_path",
|
||||
"echo '{$readme}' | base64 -d > $readme_path",
|
||||
"echo '{$parsers}' | base64 -d | tee $parsers_config > /dev/null",
|
||||
"echo '{$config}' | base64 -d | tee $fluent_bit_config > /dev/null",
|
||||
"echo '{$compose}' | base64 -d | tee $compose_path > /dev/null",
|
||||
"echo '{$readme}' | base64 -d | tee $readme_path > /dev/null",
|
||||
"test -f $config_path/.env && rm $config_path/.env",
|
||||
|
||||
];
|
||||
@ -180,18 +182,18 @@ public function handle(Server $server)
|
||||
"echo LICENSE_KEY=$license_key >> $config_path/.env",
|
||||
"echo BASE_URI=$base_uri >> $config_path/.env",
|
||||
];
|
||||
} else if ($type === 'highlight') {
|
||||
} elseif ($type === 'highlight') {
|
||||
$add_envs_command = [
|
||||
"echo HIGHLIGHT_PROJECT_ID={$server->settings->logdrain_highlight_project_id} >> $config_path/.env",
|
||||
];
|
||||
} else if ($type === 'axiom') {
|
||||
} elseif ($type === 'axiom') {
|
||||
$add_envs_command = [
|
||||
"echo AXIOM_DATASET_NAME={$server->settings->logdrain_axiom_dataset_name} >> $config_path/.env",
|
||||
"echo AXIOM_API_KEY={$server->settings->logdrain_axiom_api_key} >> $config_path/.env",
|
||||
];
|
||||
} else if ($type === 'custom') {
|
||||
} elseif ($type === 'custom') {
|
||||
$add_envs_command = [
|
||||
"touch $config_path/.env"
|
||||
"touch $config_path/.env",
|
||||
];
|
||||
} else {
|
||||
throw new \Exception('Unknown log drain type.');
|
||||
@ -203,6 +205,7 @@ public function handle(Server $server)
|
||||
"cd $config_path && docker compose up -d --remove-orphans",
|
||||
];
|
||||
$command = array_merge($command, $add_envs_command, $restart_command);
|
||||
|
||||
return instant_remote_process($command, $server);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e);
|
||||
|
19
app/Actions/Server/RunCommand.php
Normal file
19
app/Actions/Server/RunCommand.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Server;
|
||||
|
||||
use App\Enums\ActivityTypes;
|
||||
use App\Models\Server;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class RunCommand
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(Server $server, $command)
|
||||
{
|
||||
$activity = remote_process(command: [$command], server: $server, ignore_errors: true, type: ActivityTypes::COMMAND->value);
|
||||
|
||||
return $activity;
|
||||
}
|
||||
}
|
26
app/Actions/Server/StartSentinel.php
Normal file
26
app/Actions/Server/StartSentinel.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Server;
|
||||
|
||||
use App\Models\Server;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class StartSentinel
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(Server $server, $version = 'latest', bool $restart = false)
|
||||
{
|
||||
if ($restart) {
|
||||
StopSentinel::run($server);
|
||||
}
|
||||
$metrics_history = $server->settings->metrics_history_days;
|
||||
$refresh_rate = $server->settings->metrics_refresh_rate_seconds;
|
||||
$token = $server->settings->metrics_token;
|
||||
instant_remote_process([
|
||||
"docker run --rm --pull always -d -e \"TOKEN={$token}\" -e \"SCHEDULER=true\" -e \"METRICS_HISTORY={$metrics_history}\" -e \"REFRESH_RATE={$refresh_rate}\" --name coolify-sentinel -v /var/run/docker.sock:/var/run/docker.sock -v /data/coolify/metrics:/app/metrics -v /data/coolify/logs:/app/logs --pid host --health-cmd \"curl --fail http://127.0.0.1:8888/api/health || exit 1\" --health-interval 10s --health-retries 3 ghcr.io/coollabsio/sentinel:$version",
|
||||
'chown -R 9999:root /data/coolify/metrics /data/coolify/logs',
|
||||
'chmod -R 700 /data/coolify/metrics /data/coolify/logs',
|
||||
], $server, true);
|
||||
}
|
||||
}
|
16
app/Actions/Server/StopSentinel.php
Normal file
16
app/Actions/Server/StopSentinel.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Server;
|
||||
|
||||
use App\Models\Server;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class StopSentinel
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(Server $server)
|
||||
{
|
||||
instant_remote_process(['docker rm -f coolify-sentinel'], $server, false);
|
||||
}
|
||||
}
|
@ -2,18 +2,21 @@
|
||||
|
||||
namespace App\Actions\Server;
|
||||
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Server;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class UpdateCoolify
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public ?Server $server = null;
|
||||
|
||||
public ?string $latestVersion = null;
|
||||
|
||||
public ?string $currentVersion = null;
|
||||
|
||||
public function handle(bool $force)
|
||||
public function handle($manual_update = false)
|
||||
{
|
||||
try {
|
||||
$settings = InstanceSettings::get();
|
||||
@ -22,32 +25,22 @@ public function handle(bool $force)
|
||||
if (!$this->server) {
|
||||
return;
|
||||
}
|
||||
CleanupDocker::run($this->server, false);
|
||||
CleanupDocker::dispatch($this->server, false)->onQueue('high');
|
||||
$this->latestVersion = get_latest_version_of_coolify();
|
||||
$this->currentVersion = config('version');
|
||||
if ($settings->next_channel) {
|
||||
ray('next channel enabled');
|
||||
$this->latestVersion = 'next';
|
||||
}
|
||||
if ($force) {
|
||||
$this->update();
|
||||
} else {
|
||||
if (!$manual_update) {
|
||||
if (!$settings->is_auto_update_enabled) {
|
||||
return 'Auto update is disabled';
|
||||
return;
|
||||
}
|
||||
if ($this->latestVersion === $this->currentVersion) {
|
||||
return 'Already on latest version';
|
||||
return;
|
||||
}
|
||||
if (version_compare($this->latestVersion, $this->currentVersion, '<')) {
|
||||
return 'Latest version is lower than current version?!';
|
||||
return;
|
||||
}
|
||||
}
|
||||
$this->update();
|
||||
}
|
||||
send_internal_notification("Instance updated from {$this->currentVersion} -> {$this->latestVersion}");
|
||||
} catch (\Throwable $e) {
|
||||
ray('InstanceAutoUpdateJob failed');
|
||||
ray($e->getMessage());
|
||||
send_internal_notification('InstanceAutoUpdateJob failed: ' . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
@ -55,7 +48,7 @@ public function handle(bool $force)
|
||||
private function update()
|
||||
{
|
||||
if (isDev()) {
|
||||
ray("Running update on local docker container. Updating to $this->latestVersion");
|
||||
ray('Running in dev mode');
|
||||
remote_process([
|
||||
"sleep 10"
|
||||
], $this->server);
|
||||
@ -67,7 +60,12 @@ private function update()
|
||||
"curl -fsSL https://cdn.lasthourhosting.org/lasthourcloud/scripts/upgrade.sh -o /data/coolify/source/upgrade.sh",
|
||||
"bash /data/coolify/source/upgrade.sh $this->latestVersion"
|
||||
], $this->server);
|
||||
|
||||
return;
|
||||
}
|
||||
remote_process([
|
||||
'curl -fsSL https://cdn.coollabs.io/coolify/upgrade.sh -o /data/coolify/source/upgrade.sh',
|
||||
"bash /data/coolify/source/upgrade.sh $this->latestVersion",
|
||||
], $this->server);
|
||||
}
|
||||
}
|
||||
|
67
app/Actions/Server/ValidateServer.php
Normal file
67
app/Actions/Server/ValidateServer.php
Normal file
@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Server;
|
||||
|
||||
use App\Models\Server;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class ValidateServer
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public ?string $uptime = null;
|
||||
|
||||
public ?string $error = null;
|
||||
|
||||
public ?string $supported_os_type = null;
|
||||
|
||||
public ?string $docker_installed = null;
|
||||
|
||||
public ?string $docker_compose_installed = null;
|
||||
|
||||
public ?string $docker_version = null;
|
||||
|
||||
public function handle(Server $server)
|
||||
{
|
||||
$server->update([
|
||||
'validation_logs' => null,
|
||||
]);
|
||||
['uptime' => $this->uptime, 'error' => $error] = $server->validateConnection();
|
||||
if (! $this->uptime) {
|
||||
$this->error = 'Server is not reachable. Please validate your configuration and connection.<br>Check this <a target="_blank" class="text-black underline dark:text-white" href="https://coolify.io/docs/knowledge-base/server/openssh">documentation</a> for further help. <br><br><div class="text-error">Error: '.$error.'</div>';
|
||||
$server->update([
|
||||
'validation_logs' => $this->error,
|
||||
]);
|
||||
throw new \Exception($this->error);
|
||||
}
|
||||
$this->supported_os_type = $server->validateOS();
|
||||
if (! $this->supported_os_type) {
|
||||
$this->error = 'Server OS type is not supported. Please install Docker manually before continuing: <a target="_blank" class="text-black underline dark:text-white" href="https://docs.docker.com/engine/install/#server">documentation</a>.';
|
||||
$server->update([
|
||||
'validation_logs' => $this->error,
|
||||
]);
|
||||
throw new \Exception($this->error);
|
||||
}
|
||||
|
||||
$this->docker_installed = $server->validateDockerEngine();
|
||||
$this->docker_compose_installed = $server->validateDockerCompose();
|
||||
if (! $this->docker_installed || ! $this->docker_compose_installed) {
|
||||
$this->error = 'Docker Engine is not installed. Please install Docker manually before continuing: <a target="_blank" class="text-black underline dark:text-white" href="https://docs.docker.com/engine/install/#server">documentation</a>.';
|
||||
$server->update([
|
||||
'validation_logs' => $this->error,
|
||||
]);
|
||||
throw new \Exception($this->error);
|
||||
}
|
||||
$this->docker_version = $server->validateDockerEngineVersion();
|
||||
|
||||
if ($this->docker_version) {
|
||||
return 'OK';
|
||||
} else {
|
||||
$this->error = 'Docker Engine is not installed. Please install Docker manually before continuing: <a target="_blank" class="text-black underline dark:text-white" href="https://docs.docker.com/engine/install/#server">documentation</a>.';
|
||||
$server->update([
|
||||
'validation_logs' => $this->error,
|
||||
]);
|
||||
throw new \Exception($this->error);
|
||||
}
|
||||
}
|
||||
}
|
@ -2,12 +2,13 @@
|
||||
|
||||
namespace App\Actions\Service;
|
||||
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use App\Models\Service;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class DeleteService
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(Service $service)
|
||||
{
|
||||
try {
|
||||
|
18
app/Actions/Service/RestartService.php
Normal file
18
app/Actions/Service/RestartService.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Service;
|
||||
|
||||
use App\Models\Service;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class RestartService
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(Service $service)
|
||||
{
|
||||
StopService::run($service);
|
||||
|
||||
return StartService::run($service);
|
||||
}
|
||||
}
|
@ -2,26 +2,27 @@
|
||||
|
||||
namespace App\Actions\Service;
|
||||
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use App\Models\Service;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
class StartService
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(Service $service)
|
||||
{
|
||||
ray('Starting service: ' . $service->name);
|
||||
ray('Starting service: '.$service->name);
|
||||
$service->saveComposeConfigs();
|
||||
$commands[] = "cd " . $service->workdir();
|
||||
$commands[] = 'cd '.$service->workdir();
|
||||
$commands[] = "echo 'Saved configuration files to {$service->workdir()}.'";
|
||||
$commands[] = "echo 'Creating Docker network.'";
|
||||
$commands[] = "docker network inspect $service->uuid >/dev/null 2>&1 || docker network create --attachable $service->uuid >/dev/null 2>&1 || true";
|
||||
$commands[] = "echo Starting service.";
|
||||
$commands[] = "docker network inspect $service->uuid >/dev/null 2>&1 || docker network create --attachable $service->uuid";
|
||||
$commands[] = 'echo Starting service.';
|
||||
$commands[] = "echo 'Pulling images.'";
|
||||
$commands[] = "docker compose pull";
|
||||
$commands[] = 'docker compose pull';
|
||||
$commands[] = "echo 'Starting containers.'";
|
||||
$commands[] = "docker compose up -d --remove-orphans --force-recreate --build";
|
||||
$commands[] = 'docker compose up -d --remove-orphans --force-recreate --build';
|
||||
$commands[] = "docker network connect $service->uuid coolify-proxy >/dev/null 2>&1 || true";
|
||||
if (data_get($service, 'connect_to_docker_network')) {
|
||||
$compose = data_get($service, 'docker_compose', []);
|
||||
@ -32,6 +33,7 @@ public function handle(Service $service)
|
||||
}
|
||||
}
|
||||
$activity = remote_process($commands, $service->server, type_uuid: $service->uuid, callEventOnFinish: 'ServiceStatusChanged');
|
||||
|
||||
return $activity;
|
||||
}
|
||||
}
|
||||
|
@ -2,37 +2,37 @@
|
||||
|
||||
namespace App\Actions\Service;
|
||||
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use App\Models\Service;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class StopService
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(Service $service)
|
||||
{
|
||||
try {
|
||||
$server = $service->destination->server;
|
||||
if (!$server->isFunctional()) {
|
||||
if (! $server->isFunctional()) {
|
||||
return 'Server is not functional';
|
||||
}
|
||||
ray('Stopping service: ' . $service->name);
|
||||
ray('Stopping service: '.$service->name);
|
||||
$applications = $service->applications()->get();
|
||||
foreach ($applications as $application) {
|
||||
instant_remote_process(["docker rm -f {$application->name}-{$service->uuid}"], $service->server);
|
||||
instant_remote_process(["docker rm -f {$application->name}-{$service->uuid}"], $service->server, false);
|
||||
$application->update(['status' => 'exited']);
|
||||
}
|
||||
$dbs = $service->databases()->get();
|
||||
foreach ($dbs as $db) {
|
||||
instant_remote_process(["docker rm -f {$db->name}-{$service->uuid}"], $service->server);
|
||||
instant_remote_process(["docker rm -f {$db->name}-{$service->uuid}"], $service->server, false);
|
||||
$db->update(['status' => 'exited']);
|
||||
}
|
||||
instant_remote_process(["docker network disconnect {$service->uuid} coolify-proxy 2>/dev/null"], $service->server, false);
|
||||
instant_remote_process(["docker network rm {$service->uuid} 2>/dev/null"], $service->server, false);
|
||||
// TODO: make notification for databases
|
||||
// $service->environment->project->team->notify(new StatusChanged($service));
|
||||
instant_remote_process(["docker network disconnect {$service->uuid} coolify-proxy"], $service->server);
|
||||
instant_remote_process(["docker network rm {$service->uuid}"], $service->server);
|
||||
} catch (\Exception $e) {
|
||||
echo $e->getMessage();
|
||||
ray($e->getMessage());
|
||||
|
||||
return $e->getMessage();
|
||||
}
|
||||
|
||||
|
@ -8,18 +8,21 @@
|
||||
class ComplexStatusCheck
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(Application $application)
|
||||
{
|
||||
$servers = $application->additional_servers;
|
||||
$servers->push($application->destination->server);
|
||||
foreach ($servers as $server) {
|
||||
$is_main_server = $application->destination->server->id === $server->id;
|
||||
if (!$server->isFunctional()) {
|
||||
if (! $server->isFunctional()) {
|
||||
if ($is_main_server) {
|
||||
$application->update(['status' => 'exited:unhealthy']);
|
||||
|
||||
continue;
|
||||
} else {
|
||||
$application->additional_servers()->updateExistingPivot($server->id, ['status' => 'exited:unhealthy']);
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@ -44,9 +47,11 @@ public function handle(Application $application)
|
||||
} else {
|
||||
if ($is_main_server) {
|
||||
$application->update(['status' => 'exited:unhealthy']);
|
||||
|
||||
continue;
|
||||
} else {
|
||||
$application->additional_servers()->updateExistingPivot($server->id, ['status' => 'exited:unhealthy']);
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
@ -8,17 +8,20 @@
|
||||
class PullImage
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(Service $resource)
|
||||
{
|
||||
$resource->saveComposeConfigs();
|
||||
|
||||
$commands[] = "cd " . $resource->workdir();
|
||||
$commands[] = 'cd '.$resource->workdir();
|
||||
$commands[] = "echo 'Saved configuration files to {$resource->workdir()}.'";
|
||||
$commands[] = "docker compose pull";
|
||||
$commands[] = 'docker compose pull';
|
||||
|
||||
$server = data_get($resource, 'server');
|
||||
|
||||
if (!$server) return;
|
||||
if (! $server) {
|
||||
return;
|
||||
}
|
||||
|
||||
instant_remote_process($commands, $resource->server);
|
||||
}
|
||||
|
56
app/Console/Commands/AdminRemoveUser.php
Normal file
56
app/Console/Commands/AdminRemoveUser.php
Normal file
@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class AdminRemoveUser extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'admin:remove-user {email}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Remove User from database';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
try {
|
||||
$email = $this->argument('email');
|
||||
$confirm = $this->confirm('Are you sure you want to remove user with email: '.$email.'?');
|
||||
if (! $confirm) {
|
||||
$this->info('User removal cancelled.');
|
||||
|
||||
return;
|
||||
}
|
||||
$this->info("Removing user with email: $email");
|
||||
$user = User::whereEmail($email)->firstOrFail();
|
||||
$teams = $user->teams;
|
||||
foreach ($teams as $team) {
|
||||
if ($team->members->count() > 1) {
|
||||
$this->error('User is a member of a team with more than one member. Please remove user from team first.');
|
||||
|
||||
return;
|
||||
}
|
||||
$team->delete();
|
||||
}
|
||||
$user->delete();
|
||||
} catch (\Exception $e) {
|
||||
$this->error('Failed to remove user.');
|
||||
$this->error($e->getMessage());
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
26
app/Console/Commands/CleanupApplicationDeploymentQueue.php
Normal file
26
app/Console/Commands/CleanupApplicationDeploymentQueue.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\ApplicationDeploymentQueue;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class CleanupApplicationDeploymentQueue extends Command
|
||||
{
|
||||
protected $signature = 'cleanup:application-deployment-queue {--team-id=}';
|
||||
|
||||
protected $description = 'CleanupApplicationDeploymentQueue';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$team_id = $this->option('team-id');
|
||||
$servers = \App\Models\Server::where('team_id', $team_id)->get();
|
||||
foreach ($servers as $server) {
|
||||
$deployments = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->where('server_id', $server->id)->get();
|
||||
foreach ($deployments as $deployment) {
|
||||
$deployment->update(['status' => 'failed']);
|
||||
instant_remote_process(['docker rm -f '.$deployment->deployment_uuid], $server, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
64
app/Console/Commands/CleanupDatabase.php
Normal file
64
app/Console/Commands/CleanupDatabase.php
Normal file
@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class CleanupDatabase extends Command
|
||||
{
|
||||
protected $signature = 'cleanup:database {--yes}';
|
||||
|
||||
protected $description = 'Cleanup database';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
if ($this->option('yes')) {
|
||||
echo "Running database cleanup...\n";
|
||||
} else {
|
||||
echo "Running database cleanup in dry-run mode...\n";
|
||||
}
|
||||
$keep_days = 60;
|
||||
echo "Keep days: $keep_days\n";
|
||||
// Cleanup failed jobs table
|
||||
$failed_jobs = DB::table('failed_jobs')->where('failed_at', '<', now()->subDays(1));
|
||||
$count = $failed_jobs->count();
|
||||
echo "Delete $count entries from failed_jobs.\n";
|
||||
if ($this->option('yes')) {
|
||||
$failed_jobs->delete();
|
||||
}
|
||||
|
||||
// Cleanup sessions table
|
||||
$sessions = DB::table('sessions')->where('last_activity', '<', now()->subDays($keep_days)->timestamp);
|
||||
$count = $sessions->count();
|
||||
echo "Delete $count entries from sessions.\n";
|
||||
if ($this->option('yes')) {
|
||||
$sessions->delete();
|
||||
}
|
||||
|
||||
// Cleanup activity_log table
|
||||
$activity_log = DB::table('activity_log')->where('created_at', '<', now()->subDays($keep_days))->orderBy('created_at', 'desc')->skip(10);
|
||||
$count = $activity_log->count();
|
||||
echo "Delete $count entries from activity_log.\n";
|
||||
if ($this->option('yes')) {
|
||||
$activity_log->delete();
|
||||
}
|
||||
|
||||
// Cleanup application_deployment_queues table
|
||||
$application_deployment_queues = DB::table('application_deployment_queues')->where('created_at', '<', now()->subDays($keep_days))->orderBy('created_at', 'desc')->skip(10);
|
||||
$count = $application_deployment_queues->count();
|
||||
echo "Delete $count entries from application_deployment_queues.\n";
|
||||
if ($this->option('yes')) {
|
||||
$application_deployment_queues->delete();
|
||||
}
|
||||
|
||||
// Cleanup webhooks table
|
||||
$webhooks = DB::table('webhooks')->where('created_at', '<', now()->subDays($keep_days));
|
||||
$count = $webhooks->count();
|
||||
echo "Delete $count entries from webhooks.\n";
|
||||
if ($this->option('yes')) {
|
||||
$webhooks->delete();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@
|
||||
class CleanupQueue extends Command
|
||||
{
|
||||
protected $signature = 'cleanup:queue';
|
||||
|
||||
protected $description = 'Cleanup Queue';
|
||||
|
||||
public function handle()
|
||||
|
@ -7,6 +7,9 @@
|
||||
use App\Models\Service;
|
||||
use App\Models\ServiceApplication;
|
||||
use App\Models\ServiceDatabase;
|
||||
use App\Models\StandaloneClickhouse;
|
||||
use App\Models\StandaloneDragonfly;
|
||||
use App\Models\StandaloneKeydb;
|
||||
use App\Models\StandaloneMariadb;
|
||||
use App\Models\StandaloneMongodb;
|
||||
use App\Models\StandaloneMysql;
|
||||
@ -17,6 +20,7 @@
|
||||
class CleanupStuckedResources extends Command
|
||||
{
|
||||
protected $signature = 'cleanup:stucked-resources';
|
||||
|
||||
protected $description = 'Cleanup Stucked Resources';
|
||||
|
||||
public function handle()
|
||||
@ -25,6 +29,7 @@ public function handle()
|
||||
echo "Running cleanup stucked resources.\n";
|
||||
$this->cleanup_stucked_resources();
|
||||
}
|
||||
|
||||
private function cleanup_stucked_resources()
|
||||
{
|
||||
|
||||
@ -55,6 +60,33 @@ private function cleanup_stucked_resources()
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in cleaning stuck redis: {$e->getMessage()}\n";
|
||||
}
|
||||
try {
|
||||
$keydbs = StandaloneKeydb::withTrashed()->whereNotNull('deleted_at')->get();
|
||||
foreach ($keydbs as $keydb) {
|
||||
echo "Deleting stuck keydb: {$keydb->name}\n";
|
||||
$keydb->forceDelete();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in cleaning stuck keydb: {$e->getMessage()}\n";
|
||||
}
|
||||
try {
|
||||
$dragonflies = StandaloneDragonfly::withTrashed()->whereNotNull('deleted_at')->get();
|
||||
foreach ($dragonflies as $dragonfly) {
|
||||
echo "Deleting stuck dragonfly: {$dragonfly->name}\n";
|
||||
$dragonfly->forceDelete();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in cleaning stuck dragonfly: {$e->getMessage()}\n";
|
||||
}
|
||||
try {
|
||||
$clickhouses = StandaloneClickhouse::withTrashed()->whereNotNull('deleted_at')->get();
|
||||
foreach ($clickhouses as $clickhouse) {
|
||||
echo "Deleting stuck clickhouse: {$clickhouse->name}\n";
|
||||
$clickhouse->forceDelete();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in cleaning stuck clickhouse: {$e->getMessage()}\n";
|
||||
}
|
||||
try {
|
||||
$mongodbs = StandaloneMongodb::withTrashed()->whereNotNull('deleted_at')->get();
|
||||
foreach ($mongodbs as $mongodb) {
|
||||
@ -112,7 +144,7 @@ private function cleanup_stucked_resources()
|
||||
try {
|
||||
$scheduled_tasks = ScheduledTask::all();
|
||||
foreach ($scheduled_tasks as $scheduled_task) {
|
||||
if (!$scheduled_task->service && !$scheduled_task->application) {
|
||||
if (! $scheduled_task->service && ! $scheduled_task->application) {
|
||||
echo "Deleting stuck scheduledtask: {$scheduled_task->name}\n";
|
||||
$scheduled_task->delete();
|
||||
}
|
||||
@ -125,19 +157,22 @@ private function cleanup_stucked_resources()
|
||||
try {
|
||||
$applications = Application::all();
|
||||
foreach ($applications as $application) {
|
||||
if (!data_get($application, 'environment')) {
|
||||
echo 'Application without environment: ' . $application->name . '\n';
|
||||
if (! data_get($application, 'environment')) {
|
||||
echo 'Application without environment: '.$application->name.'\n';
|
||||
$application->forceDelete();
|
||||
|
||||
continue;
|
||||
}
|
||||
if (!$application->destination()) {
|
||||
echo 'Application without destination: ' . $application->name . '\n';
|
||||
if (! $application->destination()) {
|
||||
echo 'Application without destination: '.$application->name.'\n';
|
||||
$application->forceDelete();
|
||||
|
||||
continue;
|
||||
}
|
||||
if (!data_get($application, 'destination.server')) {
|
||||
echo 'Application without server: ' . $application->name . '\n';
|
||||
if (! data_get($application, 'destination.server')) {
|
||||
echo 'Application without server: '.$application->name.'\n';
|
||||
$application->forceDelete();
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@ -147,19 +182,22 @@ private function cleanup_stucked_resources()
|
||||
try {
|
||||
$postgresqls = StandalonePostgresql::all()->where('id', '!=', 0);
|
||||
foreach ($postgresqls as $postgresql) {
|
||||
if (!data_get($postgresql, 'environment')) {
|
||||
echo 'Postgresql without environment: ' . $postgresql->name . '\n';
|
||||
if (! data_get($postgresql, 'environment')) {
|
||||
echo 'Postgresql without environment: '.$postgresql->name.'\n';
|
||||
$postgresql->forceDelete();
|
||||
|
||||
continue;
|
||||
}
|
||||
if (!$postgresql->destination()) {
|
||||
echo 'Postgresql without destination: ' . $postgresql->name . '\n';
|
||||
if (! $postgresql->destination()) {
|
||||
echo 'Postgresql without destination: '.$postgresql->name.'\n';
|
||||
$postgresql->forceDelete();
|
||||
|
||||
continue;
|
||||
}
|
||||
if (!data_get($postgresql, 'destination.server')) {
|
||||
echo 'Postgresql without server: ' . $postgresql->name . '\n';
|
||||
if (! data_get($postgresql, 'destination.server')) {
|
||||
echo 'Postgresql without server: '.$postgresql->name.'\n';
|
||||
$postgresql->forceDelete();
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@ -169,19 +207,22 @@ private function cleanup_stucked_resources()
|
||||
try {
|
||||
$redis = StandaloneRedis::all();
|
||||
foreach ($redis as $redis) {
|
||||
if (!data_get($redis, 'environment')) {
|
||||
echo 'Redis without environment: ' . $redis->name . '\n';
|
||||
if (! data_get($redis, 'environment')) {
|
||||
echo 'Redis without environment: '.$redis->name.'\n';
|
||||
$redis->forceDelete();
|
||||
|
||||
continue;
|
||||
}
|
||||
if (!$redis->destination()) {
|
||||
echo 'Redis without destination: ' . $redis->name . '\n';
|
||||
if (! $redis->destination()) {
|
||||
echo 'Redis without destination: '.$redis->name.'\n';
|
||||
$redis->forceDelete();
|
||||
|
||||
continue;
|
||||
}
|
||||
if (!data_get($redis, 'destination.server')) {
|
||||
echo 'Redis without server: ' . $redis->name . '\n';
|
||||
if (! data_get($redis, 'destination.server')) {
|
||||
echo 'Redis without server: '.$redis->name.'\n';
|
||||
$redis->forceDelete();
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@ -192,19 +233,22 @@ private function cleanup_stucked_resources()
|
||||
try {
|
||||
$mongodbs = StandaloneMongodb::all();
|
||||
foreach ($mongodbs as $mongodb) {
|
||||
if (!data_get($mongodb, 'environment')) {
|
||||
echo 'Mongodb without environment: ' . $mongodb->name . '\n';
|
||||
if (! data_get($mongodb, 'environment')) {
|
||||
echo 'Mongodb without environment: '.$mongodb->name.'\n';
|
||||
$mongodb->forceDelete();
|
||||
|
||||
continue;
|
||||
}
|
||||
if (!$mongodb->destination()) {
|
||||
echo 'Mongodb without destination: ' . $mongodb->name . '\n';
|
||||
if (! $mongodb->destination()) {
|
||||
echo 'Mongodb without destination: '.$mongodb->name.'\n';
|
||||
$mongodb->forceDelete();
|
||||
|
||||
continue;
|
||||
}
|
||||
if (!data_get($mongodb, 'destination.server')) {
|
||||
echo 'Mongodb without server: ' . $mongodb->name . '\n';
|
||||
if (! data_get($mongodb, 'destination.server')) {
|
||||
echo 'Mongodb without server: '.$mongodb->name.'\n';
|
||||
$mongodb->forceDelete();
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@ -215,19 +259,22 @@ private function cleanup_stucked_resources()
|
||||
try {
|
||||
$mysqls = StandaloneMysql::all();
|
||||
foreach ($mysqls as $mysql) {
|
||||
if (!data_get($mysql, 'environment')) {
|
||||
echo 'Mysql without environment: ' . $mysql->name . '\n';
|
||||
if (! data_get($mysql, 'environment')) {
|
||||
echo 'Mysql without environment: '.$mysql->name.'\n';
|
||||
$mysql->forceDelete();
|
||||
|
||||
continue;
|
||||
}
|
||||
if (!$mysql->destination()) {
|
||||
echo 'Mysql without destination: ' . $mysql->name . '\n';
|
||||
if (! $mysql->destination()) {
|
||||
echo 'Mysql without destination: '.$mysql->name.'\n';
|
||||
$mysql->forceDelete();
|
||||
|
||||
continue;
|
||||
}
|
||||
if (!data_get($mysql, 'destination.server')) {
|
||||
echo 'Mysql without server: ' . $mysql->name . '\n';
|
||||
if (! data_get($mysql, 'destination.server')) {
|
||||
echo 'Mysql without server: '.$mysql->name.'\n';
|
||||
$mysql->forceDelete();
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@ -238,19 +285,22 @@ private function cleanup_stucked_resources()
|
||||
try {
|
||||
$mariadbs = StandaloneMariadb::all();
|
||||
foreach ($mariadbs as $mariadb) {
|
||||
if (!data_get($mariadb, 'environment')) {
|
||||
echo 'Mariadb without environment: ' . $mariadb->name . '\n';
|
||||
if (! data_get($mariadb, 'environment')) {
|
||||
echo 'Mariadb without environment: '.$mariadb->name.'\n';
|
||||
$mariadb->forceDelete();
|
||||
|
||||
continue;
|
||||
}
|
||||
if (!$mariadb->destination()) {
|
||||
echo 'Mariadb without destination: ' . $mariadb->name . '\n';
|
||||
if (! $mariadb->destination()) {
|
||||
echo 'Mariadb without destination: '.$mariadb->name.'\n';
|
||||
$mariadb->forceDelete();
|
||||
|
||||
continue;
|
||||
}
|
||||
if (!data_get($mariadb, 'destination.server')) {
|
||||
echo 'Mariadb without server: ' . $mariadb->name . '\n';
|
||||
if (! data_get($mariadb, 'destination.server')) {
|
||||
echo 'Mariadb without server: '.$mariadb->name.'\n';
|
||||
$mariadb->forceDelete();
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@ -261,19 +311,22 @@ private function cleanup_stucked_resources()
|
||||
try {
|
||||
$services = Service::all();
|
||||
foreach ($services as $service) {
|
||||
if (!data_get($service, 'environment')) {
|
||||
echo 'Service without environment: ' . $service->name . '\n';
|
||||
if (! data_get($service, 'environment')) {
|
||||
echo 'Service without environment: '.$service->name.'\n';
|
||||
$service->forceDelete();
|
||||
|
||||
continue;
|
||||
}
|
||||
if (!$service->destination()) {
|
||||
echo 'Service without destination: ' . $service->name . '\n';
|
||||
if (! $service->destination()) {
|
||||
echo 'Service without destination: '.$service->name.'\n';
|
||||
$service->forceDelete();
|
||||
|
||||
continue;
|
||||
}
|
||||
if (!data_get($service, 'server')) {
|
||||
echo 'Service without server: ' . $service->name . '\n';
|
||||
if (! data_get($service, 'server')) {
|
||||
echo 'Service without server: '.$service->name.'\n';
|
||||
$service->forceDelete();
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@ -283,9 +336,10 @@ private function cleanup_stucked_resources()
|
||||
try {
|
||||
$serviceApplications = ServiceApplication::all();
|
||||
foreach ($serviceApplications as $service) {
|
||||
if (!data_get($service, 'service')) {
|
||||
echo 'ServiceApplication without service: ' . $service->name . '\n';
|
||||
if (! data_get($service, 'service')) {
|
||||
echo 'ServiceApplication without service: '.$service->name.'\n';
|
||||
$service->forceDelete();
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@ -295,9 +349,10 @@ private function cleanup_stucked_resources()
|
||||
try {
|
||||
$serviceDatabases = ServiceDatabase::all();
|
||||
foreach ($serviceDatabases as $service) {
|
||||
if (!data_get($service, 'service')) {
|
||||
echo 'ServiceDatabase without service: ' . $service->name . '\n';
|
||||
if (! data_get($service, 'service')) {
|
||||
echo 'ServiceDatabase without service: '.$service->name.'\n';
|
||||
$service->forceDelete();
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
@ -8,17 +8,19 @@
|
||||
class CleanupUnreachableServers extends Command
|
||||
{
|
||||
protected $signature = 'cleanup:unreachable-servers';
|
||||
protected $description = 'Cleanup Unreachable Servers (3 days)';
|
||||
|
||||
protected $description = 'Cleanup Unreachable Servers (7 days)';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
echo "Running unreachable server cleanup...\n";
|
||||
$servers = Server::where('unreachable_count', 3)->where('unreachable_notification_sent', true)->where('updated_at', '<', now()->subDays(3))->get();
|
||||
$servers = Server::where('unreachable_count', 3)->where('unreachable_notification_sent', true)->where('updated_at', '<', now()->subDays(7))->get();
|
||||
if ($servers->count() > 0) {
|
||||
foreach ($servers as $server) {
|
||||
echo "Cleanup unreachable server ($server->id) with name $server->name";
|
||||
// send_internal_notification("Server $server->name is unreachable for 7 days. Cleaning up...");
|
||||
$server->update([
|
||||
'ip' => '1.2.3.4'
|
||||
'ip' => '1.2.3.4',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@ -1,33 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Server;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class Cloud extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'cloud:unused-servers';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Get Unused Servers from Cloud';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
Server::all()->whereNotNull('team.subscription')->where('team.subscription.stripe_trial_already_ended',true)->each(function($server){
|
||||
$this->info($server->name);
|
||||
});
|
||||
}
|
||||
}
|
101
app/Console/Commands/CloudCleanupSubscriptions.php
Normal file
101
app/Console/Commands/CloudCleanupSubscriptions.php
Normal file
@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Team;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class CloudCleanupSubscriptions extends Command
|
||||
{
|
||||
protected $signature = 'cloud:cleanup-subs';
|
||||
|
||||
protected $description = 'Cleanup subcriptions teams';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
try {
|
||||
if (! isCloud()) {
|
||||
$this->error('This command can only be run on cloud');
|
||||
|
||||
return;
|
||||
}
|
||||
ray()->clearAll();
|
||||
$this->info('Cleaning up subcriptions teams');
|
||||
$stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key'));
|
||||
|
||||
$teams = Team::all()->filter(function ($team) {
|
||||
return $team->id !== 0;
|
||||
})->sortBy('id');
|
||||
foreach ($teams as $team) {
|
||||
if ($team) {
|
||||
$this->info("Checking team {$team->id}");
|
||||
}
|
||||
if (! data_get($team, 'subscription')) {
|
||||
$this->disableServers($team);
|
||||
|
||||
continue;
|
||||
}
|
||||
// If the team has no subscription id and the invoice is paid, we need to reset the invoice paid status
|
||||
if (! (data_get($team, 'subscription.stripe_subscription_id'))) {
|
||||
$this->info("Resetting invoice paid status for team {$team->id} {$team->name}");
|
||||
|
||||
$team->subscription->update([
|
||||
'stripe_invoice_paid' => false,
|
||||
'stripe_trial_already_ended' => false,
|
||||
'stripe_subscription_id' => null,
|
||||
]);
|
||||
$this->disableServers($team);
|
||||
|
||||
continue;
|
||||
} else {
|
||||
$subscription = $stripe->subscriptions->retrieve(data_get($team, 'subscription.stripe_subscription_id'), []);
|
||||
$status = data_get($subscription, 'status');
|
||||
if ($status === 'active' || $status === 'past_due') {
|
||||
$team->subscription->update([
|
||||
'stripe_invoice_paid' => true,
|
||||
'stripe_trial_already_ended' => false,
|
||||
]);
|
||||
|
||||
continue;
|
||||
}
|
||||
$this->info('Subscription status: '.$status);
|
||||
$this->info('Subscription id: '.data_get($team, 'subscription.stripe_subscription_id'));
|
||||
$confirm = $this->confirm('Do you want to cancel the subscription?', true);
|
||||
if (! $confirm) {
|
||||
$this->info("Skipping team {$team->id} {$team->name}");
|
||||
} else {
|
||||
$this->info("Cancelling subscription for team {$team->id} {$team->name}");
|
||||
$team->subscription->update([
|
||||
'stripe_invoice_paid' => false,
|
||||
'stripe_trial_already_ended' => false,
|
||||
'stripe_subscription_id' => null,
|
||||
]);
|
||||
$this->disableServers($team);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->error($e->getMessage());
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private function disableServers(Team $team)
|
||||
{
|
||||
foreach ($team->servers as $server) {
|
||||
if ($server->settings->is_usable === true || $server->settings->is_reachable === true || $server->ip !== '1.2.3.4') {
|
||||
$this->info("Disabling server {$server->id} {$server->name}");
|
||||
$server->settings()->update([
|
||||
'is_usable' => false,
|
||||
'is_reachable' => false,
|
||||
]);
|
||||
$server->update([
|
||||
'ip' => '1.2.3.4',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -9,19 +9,48 @@
|
||||
|
||||
class Dev extends Command
|
||||
{
|
||||
protected $signature = 'dev:init';
|
||||
protected $description = 'Init the app in dev mode';
|
||||
protected $signature = 'dev {--init} {--generate-openapi}';
|
||||
|
||||
protected $description = 'Helper commands for development.';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
if ($this->option('init')) {
|
||||
$this->init();
|
||||
|
||||
return;
|
||||
}
|
||||
if ($this->option('generate-openapi')) {
|
||||
$this->generateOpenApi();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function generateOpenApi()
|
||||
{
|
||||
// Generate OpenAPI documentation
|
||||
echo "Generating OpenAPI documentation.\n";
|
||||
$process = Process::run(['/var/www/html/vendor/bin/openapi', 'app', '-o', 'openapi.yaml']);
|
||||
$error = $process->errorOutput();
|
||||
$error = preg_replace('/^.*an object literal,.*$/m', '', $error);
|
||||
$error = preg_replace('/^\h*\v+/m', '', $error);
|
||||
echo $error;
|
||||
echo $process->output();
|
||||
}
|
||||
|
||||
public function init()
|
||||
{
|
||||
// Generate APP_KEY if not exists
|
||||
|
||||
if (empty(env('APP_KEY'))) {
|
||||
echo "Generating APP_KEY.\n";
|
||||
Artisan::call('key:generate');
|
||||
}
|
||||
// Seed database if it's empty
|
||||
$settings = InstanceSettings::find(0);
|
||||
if (!$settings) {
|
||||
if (! $settings) {
|
||||
echo "Initializing instance, seeding database.\n";
|
||||
Artisan::call('migrate --seed');
|
||||
} else {
|
||||
|
@ -15,6 +15,7 @@
|
||||
use App\Notifications\Application\StatusChanged;
|
||||
use App\Notifications\Database\BackupFailed;
|
||||
use App\Notifications\Database\BackupSuccess;
|
||||
use App\Notifications\Database\DailyBackup;
|
||||
use App\Notifications\Test;
|
||||
use Exception;
|
||||
use Illuminate\Console\Command;
|
||||
@ -46,7 +47,9 @@ class Emails extends Command
|
||||
* Execute the console command.
|
||||
*/
|
||||
private ?MailMessage $mail = null;
|
||||
|
||||
private ?string $email = null;
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$type = select(
|
||||
@ -54,6 +57,8 @@ public function handle()
|
||||
options: [
|
||||
'updates' => 'Send Update Email to all users',
|
||||
'emails-test' => 'Test',
|
||||
'database-backup-statuses-daily' => 'Database - Backup Statuses (Daily)',
|
||||
'application-deployment-success-daily' => 'Application - Deployment Success (Daily)',
|
||||
'application-deployment-success' => 'Application - Deployment Success',
|
||||
'application-deployment-failed' => 'Application - Deployment Failed',
|
||||
'application-status-changed' => 'Application - Status Changed',
|
||||
@ -67,18 +72,23 @@ public function handle()
|
||||
],
|
||||
);
|
||||
$emailsGathered = ['realusers-before-trial', 'realusers-server-lost-connection'];
|
||||
if (!in_array($type, $emailsGathered)) {
|
||||
$this->email = text('Email Address to send to');
|
||||
if (isDev()) {
|
||||
$this->email = 'test@example.com';
|
||||
} else {
|
||||
if (! in_array($type, $emailsGathered)) {
|
||||
$this->email = text('Email Address to send to:');
|
||||
}
|
||||
}
|
||||
set_transanctional_email_settings();
|
||||
|
||||
$this->mail = new MailMessage();
|
||||
$this->mail->subject("Test Email");
|
||||
$this->mail = new MailMessage;
|
||||
$this->mail->subject('Test Email');
|
||||
switch ($type) {
|
||||
case 'updates':
|
||||
$teams = Team::all();
|
||||
if (!$teams || $teams->isEmpty()) {
|
||||
echo 'No teams found.' . PHP_EOL;
|
||||
if (! $teams || $teams->isEmpty()) {
|
||||
echo 'No teams found.'.PHP_EOL;
|
||||
|
||||
return;
|
||||
}
|
||||
$emails = [];
|
||||
@ -90,27 +100,56 @@ public function handle()
|
||||
}
|
||||
}
|
||||
$emails = array_unique($emails);
|
||||
$this->info("Sending to " . count($emails) . " emails.");
|
||||
$this->info('Sending to '.count($emails).' emails.');
|
||||
foreach ($emails as $email) {
|
||||
$this->info($email);
|
||||
}
|
||||
$confirmed = confirm('Are you sure?');
|
||||
if ($confirmed) {
|
||||
foreach ($emails as $email) {
|
||||
$this->mail = new MailMessage();
|
||||
$this->mail = new MailMessage;
|
||||
$this->mail->subject('One-click Services, Docker Compose support');
|
||||
$unsubscribeUrl = route('unsubscribe.marketing.emails', [
|
||||
'token' => encrypt($email),
|
||||
]);
|
||||
$this->mail->view('emails.updates',["unsubscribeUrl" => $unsubscribeUrl]);
|
||||
$this->mail->view('emails.updates', ['unsubscribeUrl' => $unsubscribeUrl]);
|
||||
$this->sendEmail($email);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'emails-test':
|
||||
$this->mail = (new Test())->toMail();
|
||||
$this->mail = (new Test)->toMail();
|
||||
$this->sendEmail();
|
||||
break;
|
||||
case 'database-backup-statuses-daily':
|
||||
$scheduled_backups = ScheduledDatabaseBackup::all();
|
||||
$databases = collect();
|
||||
foreach ($scheduled_backups as $scheduled_backup) {
|
||||
$last_days_backups = $scheduled_backup->get_last_days_backup_status();
|
||||
if ($last_days_backups->isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
$failed = $last_days_backups->where('status', 'failed');
|
||||
$database = $scheduled_backup->database;
|
||||
$databases->put($database->name, [
|
||||
'failed_count' => $failed->count(),
|
||||
]);
|
||||
}
|
||||
$this->mail = (new DailyBackup($databases))->toMail();
|
||||
$this->sendEmail();
|
||||
break;
|
||||
case 'application-deployment-success-daily':
|
||||
$applications = Application::all();
|
||||
foreach ($applications as $application) {
|
||||
$deployments = $application->get_last_days_deployments();
|
||||
ray($deployments);
|
||||
if ($deployments->isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
$this->mail = (new DeploymentSuccess($application, 'test'))->toMail();
|
||||
$this->sendEmail();
|
||||
}
|
||||
break;
|
||||
case 'application-deployment-success':
|
||||
$application = Application::all()->first();
|
||||
$this->mail = (new DeploymentSuccess($application, 'test'))->toMail();
|
||||
@ -119,7 +158,7 @@ public function handle()
|
||||
case 'application-deployment-failed':
|
||||
$application = Application::all()->first();
|
||||
$preview = ApplicationPreview::all()->first();
|
||||
if (!$preview) {
|
||||
if (! $preview) {
|
||||
$preview = ApplicationPreview::create([
|
||||
'application_id' => $application->id,
|
||||
'pull_request_id' => 1,
|
||||
@ -140,7 +179,7 @@ public function handle()
|
||||
case 'backup-failed':
|
||||
$backup = ScheduledDatabaseBackup::all()->first();
|
||||
$db = StandalonePostgresql::all()->first();
|
||||
if (!$backup) {
|
||||
if (! $backup) {
|
||||
$backup = ScheduledDatabaseBackup::create([
|
||||
'enabled' => true,
|
||||
'frequency' => 'daily',
|
||||
@ -150,14 +189,14 @@ public function handle()
|
||||
'team_id' => 0,
|
||||
]);
|
||||
}
|
||||
$output = 'Because of an error, the backup of the database ' . $db->name . ' failed.';
|
||||
$output = 'Because of an error, the backup of the database '.$db->name.' failed.';
|
||||
$this->mail = (new BackupFailed($backup, $db, $output))->toMail();
|
||||
$this->sendEmail();
|
||||
break;
|
||||
case 'backup-success':
|
||||
$backup = ScheduledDatabaseBackup::all()->first();
|
||||
$db = StandalonePostgresql::all()->first();
|
||||
if (!$backup) {
|
||||
if (! $backup) {
|
||||
$backup = ScheduledDatabaseBackup::create([
|
||||
'enabled' => true,
|
||||
'frequency' => 'daily',
|
||||
@ -185,7 +224,7 @@ public function handle()
|
||||
// $this->sendEmail();
|
||||
// break;
|
||||
case 'waitlist-invitation-link':
|
||||
$this->mail = new MailMessage();
|
||||
$this->mail = new MailMessage;
|
||||
$this->mail->view('emails.waitlist-invitation', [
|
||||
'loginLink' => 'https://coolify.io',
|
||||
]);
|
||||
@ -202,12 +241,13 @@ public function handle()
|
||||
|
||||
break;
|
||||
case 'realusers-before-trial':
|
||||
$this->mail = new MailMessage();
|
||||
$this->mail = new MailMessage;
|
||||
$this->mail->view('emails.before-trial-conversion');
|
||||
$this->mail->subject('Trial period has been added for all subscription plans.');
|
||||
$teams = Team::doesntHave('subscription')->where('id', '!=', 0)->get();
|
||||
if (!$teams || $teams->isEmpty()) {
|
||||
echo 'No teams found.' . PHP_EOL;
|
||||
if (! $teams || $teams->isEmpty()) {
|
||||
echo 'No teams found.'.PHP_EOL;
|
||||
|
||||
return;
|
||||
}
|
||||
$emails = [];
|
||||
@ -219,7 +259,7 @@ public function handle()
|
||||
}
|
||||
}
|
||||
$emails = array_unique($emails);
|
||||
$this->info("Sending to " . count($emails) . " emails.");
|
||||
$this->info('Sending to '.count($emails).' emails.');
|
||||
foreach ($emails as $email) {
|
||||
$this->info($email);
|
||||
}
|
||||
@ -233,7 +273,7 @@ public function handle()
|
||||
case 'realusers-server-lost-connection':
|
||||
$serverId = text('Server Id');
|
||||
$server = Server::find($serverId);
|
||||
if (!$server) {
|
||||
if (! $server) {
|
||||
throw new Exception('Server not found');
|
||||
}
|
||||
$admins = [];
|
||||
@ -243,22 +283,23 @@ public function handle()
|
||||
$admins[] = $member->email;
|
||||
}
|
||||
}
|
||||
$this->info('Sending to ' . count($admins) . ' admins.');
|
||||
$this->info('Sending to '.count($admins).' admins.');
|
||||
foreach ($admins as $admin) {
|
||||
$this->info($admin);
|
||||
}
|
||||
$this->mail = new MailMessage();
|
||||
$this->mail = new MailMessage;
|
||||
$this->mail->view('emails.server-lost-connection', [
|
||||
'name' => $server->name,
|
||||
]);
|
||||
$this->mail->subject('Action required: Server ' . $server->name . ' lost connection.');
|
||||
$this->mail->subject('Action required: Server '.$server->name.' lost connection.');
|
||||
foreach ($admins as $email) {
|
||||
$this->sendEmail($email);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
private function sendEmail(string $email = null)
|
||||
|
||||
private function sendEmail(?string $email = null)
|
||||
{
|
||||
if ($email) {
|
||||
$this->email = $email;
|
||||
@ -269,7 +310,7 @@ private function sendEmail(string $email = null)
|
||||
fn (Message $message) => $message
|
||||
->to($this->email)
|
||||
->subject($this->mail->subject)
|
||||
->html((string)$this->mail->render())
|
||||
->html((string) $this->mail->render())
|
||||
);
|
||||
$this->info("Email sent to $this->email successfully. 📧");
|
||||
}
|
||||
|
23
app/Console/Commands/Horizon.php
Normal file
23
app/Console/Commands/Horizon.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class Horizon extends Command
|
||||
{
|
||||
protected $signature = 'start:horizon';
|
||||
|
||||
protected $description = 'Start Horizon';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
if (config('coolify.is_horizon_enabled')) {
|
||||
$this->info('Horizon is enabled. Starting.');
|
||||
$this->call('horizon');
|
||||
exit(0);
|
||||
} else {
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
}
|
@ -2,9 +2,11 @@
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Actions\Server\StopSentinel;
|
||||
use App\Enums\ApplicationDeploymentStatus;
|
||||
use App\Jobs\CleanupHelperContainersJob;
|
||||
use App\Models\ApplicationDeploymentQueue;
|
||||
use App\Models\Environment;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
use App\Models\Server;
|
||||
@ -15,16 +17,31 @@
|
||||
class Init extends Command
|
||||
{
|
||||
protected $signature = 'app:init {--full-cleanup} {--cleanup-deployments}';
|
||||
|
||||
protected $description = 'Cleanup instance related stuffs';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$this->alive();
|
||||
get_public_ips();
|
||||
if (version_compare('4.0.0-beta.312', config('version'), '<=')) {
|
||||
$servers = Server::all();
|
||||
foreach ($servers as $server) {
|
||||
$server->settings->update(['is_metrics_enabled' => false]);
|
||||
if ($server->isFunctional()) {
|
||||
StopSentinel::dispatch($server);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$full_cleanup = $this->option('full-cleanup');
|
||||
$cleanup_deployments = $this->option('cleanup-deployments');
|
||||
|
||||
$this->replace_slash_in_environment_name();
|
||||
if ($cleanup_deployments) {
|
||||
echo "Running cleanup deployments.\n";
|
||||
$this->cleanup_in_progress_application_deployments();
|
||||
|
||||
return;
|
||||
}
|
||||
if ($full_cleanup) {
|
||||
@ -34,25 +51,30 @@ public function handle()
|
||||
$this->cleanup_stucked_helper_containers();
|
||||
$this->call('cleanup:queue');
|
||||
$this->call('cleanup:stucked-resources');
|
||||
if (! isCloud()) {
|
||||
try {
|
||||
setup_dynamic_configuration();
|
||||
$server = Server::find(0)->first();
|
||||
$server->setupDynamicProxyConfiguration();
|
||||
} catch (\Throwable $e) {
|
||||
echo "Could not setup dynamic configuration: {$e->getMessage()}\n";
|
||||
}
|
||||
}
|
||||
|
||||
$settings = InstanceSettings::get();
|
||||
if (!is_null(env('AUTOUPDATE', null))) {
|
||||
if (! is_null(env('AUTOUPDATE', null))) {
|
||||
if (env('AUTOUPDATE') == true) {
|
||||
$settings->update(['is_auto_update_enabled' => true]);
|
||||
} else {
|
||||
$settings->update(['is_auto_update_enabled' => false]);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
$this->cleanup_stucked_helper_containers();
|
||||
$this->call('cleanup:stucked-resources');
|
||||
}
|
||||
|
||||
private function restore_coolify_db_backup()
|
||||
{
|
||||
try {
|
||||
@ -61,7 +83,7 @@ private function restore_coolify_db_backup()
|
||||
echo "Restoring Last Hour Cloud db backup\n";
|
||||
$database->restore();
|
||||
$scheduledBackup = ScheduledDatabaseBackup::find(0);
|
||||
if (!$scheduledBackup) {
|
||||
if (! $scheduledBackup) {
|
||||
ScheduledDatabaseBackup::create([
|
||||
'id' => 0,
|
||||
'enabled' => true,
|
||||
@ -77,6 +99,7 @@ private function restore_coolify_db_backup()
|
||||
echo "Error in restoring Last Hour Cloud db backup: {$e->getMessage()}\n";
|
||||
}
|
||||
}
|
||||
|
||||
private function cleanup_stucked_helper_containers()
|
||||
{
|
||||
$servers = Server::all();
|
||||
@ -86,6 +109,7 @@ private function cleanup_stucked_helper_containers()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function alive()
|
||||
{
|
||||
$id = config('app.id');
|
||||
@ -94,6 +118,7 @@ private function alive()
|
||||
$do_not_track = data_get($settings, 'do_not_track');
|
||||
if ($do_not_track == true) {
|
||||
echo "Skipping alive as do_not_track is enabled\n";
|
||||
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@ -139,4 +164,15 @@ private function cleanup_in_progress_application_deployments()
|
||||
echo "Error: {$e->getMessage()}\n";
|
||||
}
|
||||
}
|
||||
|
||||
private function replace_slash_in_environment_name()
|
||||
{
|
||||
$environments = Environment::all();
|
||||
foreach ($environments as $environment) {
|
||||
if (str_contains($environment->name, '/')) {
|
||||
$environment->name = str_replace('/', '-', $environment->name);
|
||||
$environment->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
use function Termwind\ask;
|
||||
use function Termwind\render;
|
||||
use function Termwind\style;
|
||||
@ -32,6 +33,7 @@ public function handle()
|
||||
|
||||
if (blank($channel)) {
|
||||
$this->showHelp();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -35,6 +35,7 @@ public function handle()
|
||||
$this->info('Root user\'s email updated successfully.');
|
||||
} catch (\Exception $e) {
|
||||
$this->error('Failed to update root user\'s email.');
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -29,12 +29,12 @@ class RootResetPassword extends Command
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
//
|
||||
$this->info('You are about to reset the root password.');
|
||||
$password = password('Give me a new password for root user: ');
|
||||
$passwordAgain = password('Again');
|
||||
if ($password != $passwordAgain) {
|
||||
$this->error('Passwords do not match.');
|
||||
|
||||
return;
|
||||
}
|
||||
$this->info('Updating root password...');
|
||||
@ -43,6 +43,7 @@ public function handle()
|
||||
$this->info('Root password updated successfully.');
|
||||
} catch (\Exception $e) {
|
||||
$this->error('Failed to update root password.');
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
23
app/Console/Commands/Scheduler.php
Normal file
23
app/Console/Commands/Scheduler.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class Scheduler extends Command
|
||||
{
|
||||
protected $signature = 'start:scheduler';
|
||||
|
||||
protected $description = 'Start Scheduler';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
if (config('coolify.is_scheduler_enabled')) {
|
||||
$this->info('Scheduler is enabled. Starting.');
|
||||
$this->call('schedule:work');
|
||||
exit(0);
|
||||
} else {
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
}
|
@ -48,11 +48,13 @@ public function handle()
|
||||
$this->deleteServer();
|
||||
}
|
||||
}
|
||||
|
||||
private function deleteServer()
|
||||
{
|
||||
$servers = Server::all();
|
||||
if ($servers->count() === 0) {
|
||||
$this->error('There are no applications to delete.');
|
||||
|
||||
return;
|
||||
}
|
||||
$serversToDelete = multiselect(
|
||||
@ -64,19 +66,21 @@ private function deleteServer()
|
||||
$toDelete = $servers->where('id', $server)->first();
|
||||
if ($toDelete) {
|
||||
$this->info($toDelete);
|
||||
$confirmed = confirm("Are you sure you want to delete all selected resources?");
|
||||
if (!$confirmed) {
|
||||
$confirmed = confirm('Are you sure you want to delete all selected resources?');
|
||||
if (! $confirmed) {
|
||||
break;
|
||||
}
|
||||
$toDelete->delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function deleteApplication()
|
||||
{
|
||||
$applications = Application::all();
|
||||
if ($applications->count() === 0) {
|
||||
$this->error('There are no applications to delete.');
|
||||
|
||||
return;
|
||||
}
|
||||
$applicationsToDelete = multiselect(
|
||||
@ -88,19 +92,21 @@ private function deleteApplication()
|
||||
$toDelete = $applications->where('id', $application)->first();
|
||||
if ($toDelete) {
|
||||
$this->info($toDelete);
|
||||
$confirmed = confirm("Are you sure you want to delete all selected resources? ");
|
||||
if (!$confirmed) {
|
||||
$confirmed = confirm('Are you sure you want to delete all selected resources? ');
|
||||
if (! $confirmed) {
|
||||
break;
|
||||
}
|
||||
DeleteResourceJob::dispatch($toDelete);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function deleteDatabase()
|
||||
{
|
||||
$databases = StandalonePostgresql::all();
|
||||
if ($databases->count() === 0) {
|
||||
$this->error('There are no databases to delete.');
|
||||
|
||||
return;
|
||||
}
|
||||
$databasesToDelete = multiselect(
|
||||
@ -112,19 +118,21 @@ private function deleteDatabase()
|
||||
$toDelete = $databases->where('id', $database)->first();
|
||||
if ($toDelete) {
|
||||
$this->info($toDelete);
|
||||
$confirmed = confirm("Are you sure you want to delete all selected resources?");
|
||||
if (!$confirmed) {
|
||||
$confirmed = confirm('Are you sure you want to delete all selected resources?');
|
||||
if (! $confirmed) {
|
||||
return;
|
||||
}
|
||||
DeleteResourceJob::dispatch($toDelete);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function deleteService()
|
||||
{
|
||||
$services = Service::all();
|
||||
if ($services->count() === 0) {
|
||||
$this->error('There are no services to delete.');
|
||||
|
||||
return;
|
||||
}
|
||||
$servicesToDelete = multiselect(
|
||||
@ -136,8 +144,8 @@ private function deleteService()
|
||||
$toDelete = $services->where('id', $service)->first();
|
||||
if ($toDelete) {
|
||||
$this->info($toDelete);
|
||||
$confirmed = confirm("Are you sure you want to delete all selected resources?");
|
||||
if (!$confirmed) {
|
||||
$confirmed = confirm('Are you sure you want to delete all selected resources?');
|
||||
if (! $confirmed) {
|
||||
return;
|
||||
}
|
||||
DeleteResourceJob::dispatch($toDelete);
|
||||
|
@ -26,7 +26,6 @@ class ServicesGenerate extends Command
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
// ray()->clearAll();
|
||||
$files = array_diff(scandir(base_path('templates/compose')), ['.', '..']);
|
||||
$files = array_filter($files, function ($file) {
|
||||
return strpos($file, '.yaml') !== false;
|
||||
@ -40,7 +39,7 @@ public function handle()
|
||||
$serviceTemplatesJson[$name] = $parsed;
|
||||
}
|
||||
}
|
||||
$serviceTemplatesJson = json_encode($serviceTemplatesJson, JSON_PRETTY_PRINT);
|
||||
$serviceTemplatesJson = json_encode($serviceTemplatesJson);
|
||||
file_put_contents(base_path('templates/service-templates.json'), $serviceTemplatesJson);
|
||||
}
|
||||
|
||||
@ -51,18 +50,20 @@ private function process_file($file)
|
||||
// $this->info($content);
|
||||
$ignore = collect(preg_grep('/^# ignore:/', explode("\n", $content)))->values();
|
||||
if ($ignore->count() > 0) {
|
||||
$ignore = (bool)str($ignore[0])->after('# ignore:')->trim()->value();
|
||||
$ignore = (bool) str($ignore[0])->after('# ignore:')->trim()->value();
|
||||
} else {
|
||||
$ignore = false;
|
||||
}
|
||||
if ($ignore) {
|
||||
$this->info("Ignoring $file");
|
||||
|
||||
return;
|
||||
}
|
||||
$this->info("Processing $file");
|
||||
$documentation = collect(preg_grep('/^# documentation:/', explode("\n", $content)))->values();
|
||||
if ($documentation->count() > 0) {
|
||||
$documentation = str($documentation[0])->after('# documentation:')->trim()->value();
|
||||
$documentation = str($documentation)->append('?utm_source=coolify.io');
|
||||
} else {
|
||||
$documentation = 'https://coolify.io/docs';
|
||||
}
|
||||
@ -73,6 +74,18 @@ private function process_file($file)
|
||||
} else {
|
||||
$slogan = str($file)->headline()->value();
|
||||
}
|
||||
$logo = collect(preg_grep('/^# logo:/', explode("\n", $content)))->values();
|
||||
if ($logo->count() > 0) {
|
||||
$logo = str($logo[0])->after('# logo:')->trim()->value();
|
||||
} else {
|
||||
$logo = 'svgs/unknown.svg';
|
||||
}
|
||||
$minversion = collect(preg_grep('/^# minversion:/', explode("\n", $content)))->values();
|
||||
if ($minversion->count() > 0) {
|
||||
$minversion = str($minversion[0])->after('# minversion:')->trim()->value();
|
||||
} else {
|
||||
$minversion = '0.0.0';
|
||||
}
|
||||
$env_file = collect(preg_grep('/^# env_file:/', explode("\n", $content)))->values();
|
||||
if ($env_file->count() > 0) {
|
||||
$env_file = str($env_file[0])->after('# env_file:')->trim()->value();
|
||||
@ -88,6 +101,12 @@ private function process_file($file)
|
||||
} else {
|
||||
$tags = null;
|
||||
}
|
||||
$port = collect(preg_grep('/^# port:/', explode("\n", $content)))->values();
|
||||
if ($port->count() > 0) {
|
||||
$port = str($port[0])->after('# port:')->trim()->value();
|
||||
} else {
|
||||
$port = null;
|
||||
}
|
||||
$json = Yaml::parse($content);
|
||||
$yaml = base64_encode(Yaml::dump($json, 10, 2));
|
||||
$payload = [
|
||||
@ -96,12 +115,18 @@ private function process_file($file)
|
||||
'slogan' => $slogan,
|
||||
'compose' => $yaml,
|
||||
'tags' => $tags,
|
||||
'logo' => $logo,
|
||||
'minversion' => $minversion,
|
||||
];
|
||||
if ($port) {
|
||||
$payload['port'] = $port;
|
||||
}
|
||||
if ($env_file) {
|
||||
$env_file_content = file_get_contents(base_path("templates/compose/$env_file"));
|
||||
$env_file_base64 = base64_encode($env_file_content);
|
||||
$payload['envs'] = $env_file_base64;
|
||||
}
|
||||
|
||||
return $payload;
|
||||
}
|
||||
}
|
||||
|
@ -33,30 +33,31 @@ public function handle()
|
||||
$that = $this;
|
||||
$only_template = $this->option('templates');
|
||||
$only_version = $this->option('release');
|
||||
$bunny_cdn = "https://cdn.coollabs.io";
|
||||
$bunny_cdn_path = "coolify";
|
||||
$bunny_cdn_storage_name = "coolcdn";
|
||||
$bunny_cdn = 'https://cdn.coollabs.io';
|
||||
$bunny_cdn_path = 'coolify';
|
||||
$bunny_cdn_storage_name = 'coolcdn';
|
||||
|
||||
$parent_dir = realpath(dirname(__FILE__) . '/../../..');
|
||||
$parent_dir = realpath(dirname(__FILE__).'/../../..');
|
||||
|
||||
$compose_file = "docker-compose.yml";
|
||||
$compose_file_prod = "docker-compose.prod.yml";
|
||||
$install_script = "install.sh";
|
||||
$upgrade_script = "upgrade.sh";
|
||||
$production_env = ".env.production";
|
||||
$service_template = "service-templates.json";
|
||||
$compose_file = 'docker-compose.yml';
|
||||
$compose_file_prod = 'docker-compose.prod.yml';
|
||||
$install_script = 'install.sh';
|
||||
$upgrade_script = 'upgrade.sh';
|
||||
$production_env = '.env.production';
|
||||
$service_template = 'service-templates.json';
|
||||
|
||||
$versions = "versions.json";
|
||||
$versions = 'versions.json';
|
||||
|
||||
PendingRequest::macro('storage', function ($fileName) use ($that) {
|
||||
$headers = [
|
||||
'AccessKey' => env('BUNNY_STORAGE_API_KEY'),
|
||||
'Accept' => 'application/json',
|
||||
'Content-Type' => 'application/octet-stream'
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
];
|
||||
$fileStream = fopen($fileName, "r");
|
||||
$fileStream = fopen($fileName, 'r');
|
||||
$file = fread($fileStream, filesize($fileName));
|
||||
$that->info('Uploading: ' . $fileName);
|
||||
$that->info('Uploading: '.$fileName);
|
||||
|
||||
return PendingRequest::baseUrl('https://storage.bunnycdn.com')->withHeaders($headers)->withBody($file)->throw();
|
||||
});
|
||||
PendingRequest::macro('purge', function ($url) use ($that) {
|
||||
@ -64,20 +65,21 @@ public function handle()
|
||||
'AccessKey' => env('BUNNY_API_KEY'),
|
||||
'Accept' => 'application/json',
|
||||
];
|
||||
$that->info('Purging: ' . $url);
|
||||
$that->info('Purging: '.$url);
|
||||
|
||||
return PendingRequest::withHeaders($headers)->get('https://api.bunny.net/purge', [
|
||||
"url" => $url,
|
||||
"async" => false
|
||||
'url' => $url,
|
||||
'async' => false,
|
||||
]);
|
||||
});
|
||||
try {
|
||||
if (!$only_template && !$only_version) {
|
||||
if (! $only_template && ! $only_version) {
|
||||
$this->info('About to sync files (docker-compose.prod.yaml, upgrade.sh, install.sh, etc) to BunnyCDN.');
|
||||
}
|
||||
if ($only_template) {
|
||||
$this->info('About to sync service-templates.json to BunnyCDN.');
|
||||
$confirmed = confirm("Are you sure you want to sync?");
|
||||
if (!$confirmed) {
|
||||
$confirmed = confirm('Are you sure you want to sync?');
|
||||
if (! $confirmed) {
|
||||
return;
|
||||
}
|
||||
Http::pool(fn (Pool $pool) => [
|
||||
@ -85,15 +87,16 @@ public function handle()
|
||||
$pool->purge("$bunny_cdn/$bunny_cdn_path/$service_template"),
|
||||
]);
|
||||
$this->info('Service template uploaded & purged...');
|
||||
|
||||
return;
|
||||
} else if ($only_version) {
|
||||
} elseif ($only_version) {
|
||||
$this->info('About to sync versions.json to BunnyCDN.');
|
||||
$file = file_get_contents("$parent_dir/$versions");
|
||||
$json = json_decode($file, true);
|
||||
$actual_version = data_get($json, 'coolify.v4.version');
|
||||
|
||||
$confirmed = confirm("Are you sure you want to sync to {$actual_version}?");
|
||||
if (!$confirmed) {
|
||||
if (! $confirmed) {
|
||||
return;
|
||||
}
|
||||
Http::pool(fn (Pool $pool) => [
|
||||
@ -101,10 +104,10 @@ public function handle()
|
||||
$pool->purge("$bunny_cdn/$bunny_cdn_path/$versions"),
|
||||
]);
|
||||
$this->info('versions.json uploaded & purged...');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Http::pool(fn (Pool $pool) => [
|
||||
$pool->storage(fileName: "$parent_dir/$compose_file")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$compose_file"),
|
||||
$pool->storage(fileName: "$parent_dir/$compose_file_prod")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$compose_file_prod"),
|
||||
@ -119,9 +122,9 @@ public function handle()
|
||||
$pool->purge("$bunny_cdn/$bunny_cdn_path/$upgrade_script"),
|
||||
$pool->purge("$bunny_cdn/$bunny_cdn_path/$install_script"),
|
||||
]);
|
||||
$this->info("All files uploaded & purged...");
|
||||
$this->info('All files uploaded & purged...');
|
||||
} catch (\Throwable $e) {
|
||||
$this->error("Error: " . $e->getMessage());
|
||||
$this->error('Error: '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,9 @@
|
||||
class WaitlistInvite extends Command
|
||||
{
|
||||
public Waitlist|User|null $next_patient = null;
|
||||
public string|null $password = null;
|
||||
|
||||
public ?string $password = null;
|
||||
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
@ -38,7 +40,9 @@ public function handle()
|
||||
$this->main();
|
||||
}
|
||||
}
|
||||
private function main() {
|
||||
|
||||
private function main()
|
||||
{
|
||||
if ($this->argument('email')) {
|
||||
if ($this->option('only-email')) {
|
||||
$this->next_patient = User::whereEmail($this->argument('email'))->first();
|
||||
@ -50,8 +54,9 @@ private function main() {
|
||||
} else {
|
||||
$this->next_patient = Waitlist::where('email', $this->argument('email'))->first();
|
||||
}
|
||||
if (!$this->next_patient) {
|
||||
if (! $this->next_patient) {
|
||||
$this->error("{$this->argument('email')} not found in the waitlist.");
|
||||
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
@ -60,6 +65,7 @@ private function main() {
|
||||
if ($this->next_patient) {
|
||||
if ($this->option('only-email')) {
|
||||
$this->send_email();
|
||||
|
||||
return;
|
||||
}
|
||||
$this->register_user();
|
||||
@ -69,13 +75,14 @@ private function main() {
|
||||
$this->info('No verified user found in the waitlist. 👀');
|
||||
}
|
||||
}
|
||||
|
||||
private function register_user()
|
||||
{
|
||||
$already_registered = User::whereEmail($this->next_patient->email)->first();
|
||||
if (!$already_registered) {
|
||||
if (! $already_registered) {
|
||||
$this->password = Str::password();
|
||||
User::create([
|
||||
'name' => Str::of($this->next_patient->email)->before('@'),
|
||||
'name' => str($this->next_patient->email)->before('@'),
|
||||
'email' => $this->next_patient->email,
|
||||
'password' => Hash::make($this->password),
|
||||
'force_password_reset' => true,
|
||||
@ -85,21 +92,23 @@ private function register_user()
|
||||
throw new \Exception('User already registered');
|
||||
}
|
||||
}
|
||||
|
||||
private function remove_from_waitlist()
|
||||
{
|
||||
$this->next_patient->delete();
|
||||
$this->info("User removed from waitlist successfully.");
|
||||
$this->info('User removed from waitlist successfully.');
|
||||
}
|
||||
|
||||
private function send_email()
|
||||
{
|
||||
$token = Crypt::encryptString("{$this->next_patient->email}@@@$this->password");
|
||||
$loginLink = route('auth.link', ['token' => $token]);
|
||||
$mail = new MailMessage();
|
||||
$mail = new MailMessage;
|
||||
$mail->view('emails.waitlist-invitation', [
|
||||
'loginLink' => $loginLink,
|
||||
]);
|
||||
$mail->subject('Congratulations! You are invited to join Coolify Cloud.');
|
||||
send_user_an_email($mail, $this->next_patient->email);
|
||||
$this->info("Email sent successfully. 📧");
|
||||
$this->info('Email sent successfully. 📧');
|
||||
}
|
||||
}
|
||||
|
@ -4,14 +4,15 @@
|
||||
|
||||
use App\Jobs\CheckLogDrainContainerJob;
|
||||
use App\Jobs\CleanupInstanceStuffsJob;
|
||||
use App\Jobs\ComplexContainerStatusJob;
|
||||
use App\Jobs\DatabaseBackupJob;
|
||||
use App\Jobs\ScheduledTaskJob;
|
||||
use App\Jobs\InstanceAutoUpdateJob;
|
||||
use App\Jobs\ContainerStatusJob;
|
||||
use App\Jobs\DatabaseBackupJob;
|
||||
use App\Jobs\DockerCleanupJob;
|
||||
use App\Jobs\PullCoolifyImageJob;
|
||||
use App\Jobs\PullHelperImageJob;
|
||||
use App\Jobs\PullSentinelImageJob;
|
||||
use App\Jobs\PullTemplatesFromCDN;
|
||||
use App\Jobs\ScheduledTaskJob;
|
||||
use App\Jobs\ServerStatusJob;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
use App\Models\ScheduledTask;
|
||||
use App\Models\Server;
|
||||
@ -21,51 +22,62 @@
|
||||
|
||||
class Kernel extends ConsoleKernel
|
||||
{
|
||||
private $all_servers;
|
||||
|
||||
protected function schedule(Schedule $schedule): void
|
||||
{
|
||||
$this->all_servers = Server::all();
|
||||
if (isDev()) {
|
||||
// Instance Jobs
|
||||
$schedule->command('horizon:snapshot')->everyMinute();
|
||||
$schedule->job(new CleanupInstanceStuffsJob)->everyMinute()->onOneServer();
|
||||
// $schedule->job(new CheckResaleLicenseJob)->hourly()->onOneServer();
|
||||
$schedule->job(new PullTemplatesFromCDN)->everyTwoHours()->onOneServer();
|
||||
// Server Jobs
|
||||
$this->check_scheduled_backups($schedule);
|
||||
$this->check_resources($schedule);
|
||||
$this->check_scheduled_backups($schedule);
|
||||
$this->pull_helper_image($schedule);
|
||||
$this->check_scheduled_tasks($schedule);
|
||||
$schedule->command('uploads:clear')->everyTwoMinutes();
|
||||
} else {
|
||||
// Instance Jobs
|
||||
$schedule->command('horizon:snapshot')->everyFiveMinutes();
|
||||
$schedule->command('cleanup:unreachable-servers')->daily();
|
||||
|
||||
$schedule->job(new PullCoolifyImageJob)->everyTenMinutes()->onOneServer();
|
||||
$schedule->job(new PullTemplatesFromCDN)->everyThirtyMinutes()->onOneServer();
|
||||
$schedule->job(new CleanupInstanceStuffsJob)->everyTwoMinutes()->onOneServer();
|
||||
// $schedule->job(new CheckResaleLicenseJob)->hourly()->onOneServer();
|
||||
|
||||
// Server Jobs
|
||||
$this->instance_auto_update($schedule);
|
||||
$this->check_scheduled_backups($schedule);
|
||||
$this->check_resources($schedule);
|
||||
$this->pull_helper_image($schedule);
|
||||
$this->pull_images($schedule);
|
||||
$this->check_scheduled_tasks($schedule);
|
||||
|
||||
$schedule->command('cleanup:database --yes')->daily();
|
||||
$schedule->command('uploads:clear')->everyTwoMinutes();
|
||||
}
|
||||
}
|
||||
private function pull_helper_image($schedule)
|
||||
|
||||
private function pull_images($schedule)
|
||||
{
|
||||
$servers = Server::all()->where('settings.is_usable', true)->where('settings.is_reachable', true)->where('ip', '!=', '1.2.3.4');
|
||||
$servers = $this->all_servers->where('settings.is_usable', true)->where('settings.is_reachable', true)->where('ip', '!=', '1.2.3.4');
|
||||
foreach ($servers as $server) {
|
||||
$schedule->job(new PullHelperImageJob($server))->everyTenMinutes()->onOneServer();
|
||||
if ($server->isSentinelEnabled()) {
|
||||
$schedule->job(new PullSentinelImageJob($server))->everyFiveMinutes()->onOneServer();
|
||||
}
|
||||
$schedule->job(new PullHelperImageJob($server))->everyFiveMinutes()->onOneServer();
|
||||
}
|
||||
}
|
||||
|
||||
private function check_resources($schedule)
|
||||
{
|
||||
if (isCloud()) {
|
||||
$servers = Server::all()->whereNotNull('team.subscription')->where('team.subscription.stripe_trial_already_ended', false)->where('ip', '!=', '1.2.3.4');
|
||||
$servers = $this->all_servers->whereNotNull('team.subscription')->where('team.subscription.stripe_trial_already_ended', false)->where('ip', '!=', '1.2.3.4');
|
||||
$own = Team::find(0)->servers;
|
||||
$servers = $servers->merge($own);
|
||||
$containerServers = $servers->where('settings.is_swarm_worker', false)->where('settings.is_build_server', false);
|
||||
} else {
|
||||
$servers = Server::all()->where('ip', '!=', '1.2.3.4');
|
||||
$servers = $this->all_servers->where('ip', '!=', '1.2.3.4');
|
||||
$containerServers = $servers->where('settings.is_swarm_worker', false)->where('settings.is_build_server', false);
|
||||
}
|
||||
foreach ($containerServers as $server) {
|
||||
@ -76,18 +88,10 @@ private function check_resources($schedule)
|
||||
}
|
||||
foreach ($servers as $server) {
|
||||
$schedule->job(new ServerStatusJob($server))->everyMinute()->onOneServer();
|
||||
$schedule->job(new DockerCleanupJob($server))->everyTenMinutes()->onOneServer();
|
||||
}
|
||||
}
|
||||
private function instance_auto_update($schedule)
|
||||
{
|
||||
if (isDev()) {
|
||||
return;
|
||||
}
|
||||
$settings = InstanceSettings::get();
|
||||
if ($settings->is_auto_update_enabled) {
|
||||
$schedule->job(new InstanceAutoUpdateJob)->everyTenMinutes()->onOneServer();
|
||||
}
|
||||
}
|
||||
|
||||
private function check_scheduled_backups($schedule)
|
||||
{
|
||||
$scheduled_backups = ScheduledDatabaseBackup::all();
|
||||
@ -95,12 +99,13 @@ private function check_scheduled_backups($schedule)
|
||||
return;
|
||||
}
|
||||
foreach ($scheduled_backups as $scheduled_backup) {
|
||||
if (!$scheduled_backup->enabled) {
|
||||
if (! $scheduled_backup->enabled) {
|
||||
continue;
|
||||
}
|
||||
if (is_null(data_get($scheduled_backup, 'database'))) {
|
||||
ray('database not found');
|
||||
$scheduled_backup->delete();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -120,15 +125,28 @@ private function check_scheduled_tasks($schedule)
|
||||
return;
|
||||
}
|
||||
foreach ($scheduled_tasks as $scheduled_task) {
|
||||
if ($scheduled_task->enabled === false) {
|
||||
continue;
|
||||
}
|
||||
$service = $scheduled_task->service;
|
||||
$application = $scheduled_task->application;
|
||||
|
||||
if (!$application && !$service) {
|
||||
if (! $application && ! $service) {
|
||||
ray('application/service attached to scheduled task does not exist');
|
||||
$scheduled_task->delete();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($application) {
|
||||
if (str($application->status)->contains('running') === false) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ($service) {
|
||||
if (str($service->status())->contains('running') === false) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (isset(VALID_CRON_STRINGS[$scheduled_task->frequency])) {
|
||||
$scheduled_task->frequency = VALID_CRON_STRINGS[$scheduled_task->frequency];
|
||||
}
|
||||
@ -140,7 +158,7 @@ private function check_scheduled_tasks($schedule)
|
||||
|
||||
protected function commands(): void
|
||||
{
|
||||
$this->load(__DIR__ . '/Commands');
|
||||
$this->load(__DIR__.'/Commands');
|
||||
|
||||
require base_path('routes/console.php');
|
||||
}
|
||||
|
@ -18,11 +18,12 @@ public function __construct(
|
||||
public ?string $type_uuid = null,
|
||||
public ?int $process_id = null,
|
||||
public ?Model $model = null,
|
||||
public ?string $status = null ,
|
||||
public ?string $status = null,
|
||||
public bool $ignore_errors = false,
|
||||
public $call_event_on_finish = null,
|
||||
public $call_event_data = null
|
||||
) {
|
||||
if(is_null($status)){
|
||||
if (is_null($status)) {
|
||||
$this->status = ProcessStatus::QUEUED->value;
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,5 @@ class ServerMetadata extends Data
|
||||
public function __construct(
|
||||
public ?ProxyTypes $type,
|
||||
public ?ProxyStatus $status
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
}
|
||||
|
@ -5,4 +5,5 @@
|
||||
enum ActivityTypes: string
|
||||
{
|
||||
case INLINE = 'inline';
|
||||
case COMMAND = 'command';
|
||||
}
|
||||
|
11
app/Enums/BuildPackTypes.php
Normal file
11
app/Enums/BuildPackTypes.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum BuildPackTypes: string
|
||||
{
|
||||
case NIXPACKS = 'nixpacks';
|
||||
case STATIC = 'static';
|
||||
case DOCKERFILE = 'dockerfile';
|
||||
case DOCKERCOMPOSE = 'dockercompose';
|
||||
}
|
15
app/Enums/NewDatabaseTypes.php
Normal file
15
app/Enums/NewDatabaseTypes.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum NewDatabaseTypes: string
|
||||
{
|
||||
case POSTGRESQL = 'postgresql';
|
||||
case MYSQL = 'mysql';
|
||||
case MONGODB = 'mongodb';
|
||||
case REDIS = 'redis';
|
||||
case MARIADB = 'mariadb';
|
||||
case KEYDB = 'keydb';
|
||||
case DRAGONFLY = 'dragonfly';
|
||||
case CLICKHOUSE = 'clickhouse';
|
||||
}
|
22
app/Enums/NewResourceTypes.php
Normal file
22
app/Enums/NewResourceTypes.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum NewResourceTypes: string
|
||||
{
|
||||
case PUBLIC = 'public';
|
||||
case PRIVATE_GH_APP = 'private-gh-app';
|
||||
case PRIVATE_DEPLOY_KEY = 'private-deploy-key';
|
||||
case DOCKERFILE = 'dockerfile';
|
||||
case DOCKERCOMPOSE = 'dockercompose';
|
||||
case DOCKER_IMAGE = 'docker-image';
|
||||
case SERVICE = 'service';
|
||||
case POSTGRESQL = 'postgresql';
|
||||
case MYSQL = 'mysql';
|
||||
case MONGODB = 'mongodb';
|
||||
case REDIS = 'redis';
|
||||
case MARIADB = 'mariadb';
|
||||
case KEYDB = 'keydb';
|
||||
case DRAGONFLY = 'dragonfly';
|
||||
case CLICKHOUSE = 'clickhouse';
|
||||
}
|
10
app/Enums/RedirectTypes.php
Normal file
10
app/Enums/RedirectTypes.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum RedirectTypes: string
|
||||
{
|
||||
case BOTH = 'both';
|
||||
case WWW = 'www';
|
||||
case NON_WWW = 'non-www';
|
||||
}
|
@ -2,9 +2,7 @@
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use Illuminate\Broadcasting\Channel;
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Broadcasting\PresenceChannel;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
@ -13,14 +11,16 @@
|
||||
class ApplicationStatusChanged implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public $teamId;
|
||||
|
||||
public function __construct($teamId = null)
|
||||
{
|
||||
if (is_null($teamId)) {
|
||||
$teamId = auth()->user()->currentTeam()->id ?? null;
|
||||
}
|
||||
if (is_null($teamId)) {
|
||||
throw new \Exception("Team id is null");
|
||||
throw new \Exception('Team id is null');
|
||||
}
|
||||
$this->teamId = $teamId;
|
||||
}
|
||||
|
@ -2,9 +2,7 @@
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use Illuminate\Broadcasting\Channel;
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Broadcasting\PresenceChannel;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
@ -13,14 +11,16 @@
|
||||
class BackupCreated implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public $teamId;
|
||||
|
||||
public function __construct($teamId = null)
|
||||
{
|
||||
if (is_null($teamId)) {
|
||||
$teamId = auth()->user()->currentTeam()->id ?? null;
|
||||
}
|
||||
if (is_null($teamId)) {
|
||||
throw new \Exception("Team id is null");
|
||||
throw new \Exception('Team id is null');
|
||||
}
|
||||
$this->teamId = $teamId;
|
||||
}
|
||||
|
@ -2,9 +2,7 @@
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use Illuminate\Broadcasting\Channel;
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Broadcasting\PresenceChannel;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
@ -13,22 +11,28 @@
|
||||
class DatabaseStatusChanged implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
public $userId;
|
||||
|
||||
public ?string $userId = null;
|
||||
|
||||
public function __construct($userId = null)
|
||||
{
|
||||
if (is_null($userId)) {
|
||||
$userId = auth()->user()->id ?? null;
|
||||
}
|
||||
if (is_null($userId)) {
|
||||
throw new \Exception("User id is null");
|
||||
return false;
|
||||
}
|
||||
$this->userId = $userId;
|
||||
}
|
||||
|
||||
public function broadcastOn(): array
|
||||
public function broadcastOn(): ?array
|
||||
{
|
||||
if ($this->userId) {
|
||||
return [
|
||||
new PrivateChannel("user.{$this->userId}"),
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
14
app/Events/ProxyStarted.php
Normal file
14
app/Events/ProxyStarted.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class ProxyStarted
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public function __construct(public $data) {}
|
||||
}
|
@ -2,9 +2,7 @@
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use Illuminate\Broadcasting\Channel;
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Broadcasting\PresenceChannel;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
@ -13,14 +11,16 @@
|
||||
class ProxyStatusChanged implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public $teamId;
|
||||
|
||||
public function __construct($teamId = null)
|
||||
{
|
||||
if (is_null($teamId)) {
|
||||
$teamId = auth()->user()->currentTeam()->id ?? null;
|
||||
}
|
||||
if (is_null($teamId)) {
|
||||
throw new \Exception("Team id is null");
|
||||
throw new \Exception('Team id is null');
|
||||
}
|
||||
$this->teamId = $teamId;
|
||||
}
|
||||
|
@ -2,9 +2,7 @@
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use Illuminate\Broadcasting\Channel;
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Broadcasting\PresenceChannel;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
@ -13,22 +11,28 @@
|
||||
class ServiceStatusChanged implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
public $userId;
|
||||
|
||||
public ?string $userId = null;
|
||||
|
||||
public function __construct($userId = null)
|
||||
{
|
||||
if (is_null($userId)) {
|
||||
$userId = auth()->user()->id ?? null;
|
||||
}
|
||||
if (is_null($userId)) {
|
||||
throw new \Exception("User id is null");
|
||||
return false;
|
||||
}
|
||||
$this->userId = $userId;
|
||||
}
|
||||
|
||||
public function broadcastOn(): array
|
||||
public function broadcastOn(): ?array
|
||||
{
|
||||
if (! is_null($this->userId)) {
|
||||
return [
|
||||
new PrivateChannel("user.{$this->userId}"),
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -2,9 +2,7 @@
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use Illuminate\Broadcasting\Channel;
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Broadcasting\PresenceChannel;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
@ -13,7 +11,9 @@
|
||||
class TestEvent implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public $teamId;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->teamId = auth()->user()->currentTeam()->id;
|
||||
|
@ -13,7 +13,6 @@
|
||||
|
||||
class Handler extends ExceptionHandler
|
||||
{
|
||||
|
||||
/**
|
||||
* A list of exception types with their corresponding custom log levels.
|
||||
*
|
||||
@ -22,14 +21,16 @@ class Handler extends ExceptionHandler
|
||||
protected $levels = [
|
||||
//
|
||||
];
|
||||
|
||||
/**
|
||||
* A list of the exception types that are not reported.
|
||||
*
|
||||
* @var array<int, class-string<\Throwable>>
|
||||
*/
|
||||
protected $dontReport = [
|
||||
ProcessException::class
|
||||
ProcessException::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* A list of the inputs that are never flashed to the session on validation exceptions.
|
||||
*
|
||||
@ -40,6 +41,7 @@ class Handler extends ExceptionHandler
|
||||
'password',
|
||||
'password_confirmation',
|
||||
];
|
||||
|
||||
private InstanceSettings $settings;
|
||||
|
||||
protected function unauthenticated($request, AuthenticationException $exception)
|
||||
@ -47,8 +49,10 @@ protected function unauthenticated($request, AuthenticationException $exception)
|
||||
if ($request->is('api/*') || $request->expectsJson() || $this->shouldReturnJson($request, $exception)) {
|
||||
return response()->json(['message' => $exception->getMessage()], 401);
|
||||
}
|
||||
return redirect()->guest($exception->redirectTo() ?? route('login'));
|
||||
|
||||
return redirect()->guest($exception->redirectTo($request) ?? route('login'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the exception handling callbacks for the application.
|
||||
*/
|
||||
@ -56,12 +60,12 @@ public function register(): void
|
||||
{
|
||||
$this->reportable(function (Throwable $e) {
|
||||
if (isDev()) {
|
||||
// return;
|
||||
return;
|
||||
}
|
||||
if ($e instanceof RuntimeException) {
|
||||
return;
|
||||
}
|
||||
$this->settings = InstanceSettings::get();
|
||||
$this->settings = \App\Models\InstanceSettings::get();
|
||||
if ($this->settings->do_not_track) {
|
||||
return;
|
||||
}
|
||||
@ -72,11 +76,14 @@ function (Scope $scope) {
|
||||
$scope->setUser(
|
||||
[
|
||||
'email' => $email,
|
||||
'instanceAdmin' => $instanceAdmin
|
||||
'instanceAdmin' => $instanceAdmin,
|
||||
]
|
||||
);
|
||||
}
|
||||
);
|
||||
if (str($e->getMessage())->contains('No space left on device')) {
|
||||
return;
|
||||
}
|
||||
ray('reporting to sentry');
|
||||
Integration::captureUnhandledException($e);
|
||||
});
|
||||
|
@ -4,7 +4,4 @@
|
||||
|
||||
use Exception;
|
||||
|
||||
class ProcessException extends Exception
|
||||
{
|
||||
|
||||
}
|
||||
class ProcessException extends Exception {}
|
||||
|
2570
app/Http/Controllers/Api/ApplicationsController.php
Normal file
2570
app/Http/Controllers/Api/ApplicationsController.php
Normal file
File diff suppressed because it is too large
Load Diff
1818
app/Http/Controllers/Api/DatabasesController.php
Normal file
1818
app/Http/Controllers/Api/DatabasesController.php
Normal file
File diff suppressed because it is too large
Load Diff
@ -9,13 +9,33 @@
|
||||
use App\Actions\Database\StartRedis;
|
||||
use App\Actions\Service\StartService;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\ApplicationDeploymentQueue;
|
||||
use App\Models\Server;
|
||||
use App\Models\Tag;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Collection;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class Deploy extends Controller
|
||||
{
|
||||
public function deployments(Request $request)
|
||||
{
|
||||
$teamId = get_team_id_from_token();
|
||||
if (is_null($teamId)) {
|
||||
return invalid_token();
|
||||
}
|
||||
$servers = Server::whereTeamId($teamId)->get();
|
||||
$deployments_per_server = ApplicationDeploymentQueue::whereIn("status", ["in_progress", "queued"])->whereIn("server_id", $servers->pluck("id"))->get([
|
||||
"id",
|
||||
"application_id",
|
||||
"application_name",
|
||||
"deployment_url",
|
||||
"pull_request_id",
|
||||
"server_name",
|
||||
"server_id",
|
||||
"status"
|
||||
])->sortBy('id')->toArray();
|
||||
return response()->json($deployments_per_server, 200);
|
||||
}
|
||||
public function deploy(Request $request)
|
||||
{
|
||||
$teamId = get_team_id_from_token();
|
||||
@ -27,7 +47,7 @@ public function deploy(Request $request)
|
||||
return response()->json(['error' => 'You can only use uuid or tag, not both.', 'upstream docs' => 'https://coolify.io/docs/api/deploy-webhook'], 400);
|
||||
}
|
||||
if (is_null($teamId)) {
|
||||
return response()->json(['error' => 'Invalid token.', 'upstream docs' => 'https://coolify.io/docs/api/authentication'], 400);
|
||||
return invalid_token();
|
||||
}
|
||||
if ($tags) {
|
||||
return $this->by_tags($tags, $teamId, $force);
|
||||
@ -44,16 +64,22 @@ private function by_uuids(string $uuid, int $teamId, bool $force = false)
|
||||
if (count($uuids) === 0) {
|
||||
return response()->json(['error' => 'No UUIDs provided.', 'upstream docs' => 'https://coolify.io/docs/api/deploy-webhook'], 400);
|
||||
}
|
||||
$message = collect([]);
|
||||
$deployments = collect();
|
||||
$payload = collect();
|
||||
foreach ($uuids as $uuid) {
|
||||
$resource = getResourceByUuid($uuid, $teamId);
|
||||
if ($resource) {
|
||||
$return_message = $this->deploy_resource($resource, $force);
|
||||
$message = $message->merge($return_message);
|
||||
['message' => $return_message, 'deployment_uuid' => $deployment_uuid] = $this->deploy_resource($resource, $force);
|
||||
if ($deployment_uuid) {
|
||||
$deployments->push(['message' => $return_message, 'resource_uuid' => $uuid, 'deployment_uuid' => $deployment_uuid->toString()]);
|
||||
} else {
|
||||
$deployments->push(['message' => $return_message, 'resource_uuid' => $uuid]);
|
||||
}
|
||||
}
|
||||
if ($message->count() > 0) {
|
||||
return response()->json(['message' => $message->toArray()], 200);
|
||||
}
|
||||
if ($deployments->count() > 0) {
|
||||
$payload->put('deployments', $deployments->toArray());
|
||||
return response()->json($payload->toArray(), 200);
|
||||
}
|
||||
return response()->json(['error' => "No resources found.", 'upstream docs' => 'https://coolify.io/docs/api/deploy-webhook'], 404);
|
||||
}
|
||||
@ -66,10 +92,12 @@ public function by_tags(string $tags, int $team_id, bool $force = false)
|
||||
return response()->json(['error' => 'No TAGs provided.', 'upstream docs' => 'https://coolify.io/docs/api/deploy-webhook'], 400);
|
||||
}
|
||||
$message = collect([]);
|
||||
$deployments = collect();
|
||||
$payload = collect();
|
||||
foreach ($tags as $tag) {
|
||||
$found_tag = Tag::where(['name' => $tag, 'team_id' => $team_id])->first();
|
||||
if (!$found_tag) {
|
||||
$message->push("Tag {$tag} not found.");
|
||||
// $message->push("Tag {$tag} not found.");
|
||||
continue;
|
||||
}
|
||||
$applications = $found_tag->applications()->get();
|
||||
@ -79,83 +107,78 @@ public function by_tags(string $tags, int $team_id, bool $force = false)
|
||||
continue;
|
||||
}
|
||||
foreach ($applications as $resource) {
|
||||
$return_message = $this->deploy_resource($resource, $force);
|
||||
['message' => $return_message, 'deployment_uuid' => $deployment_uuid] = $this->deploy_resource($resource, $force);
|
||||
if ($deployment_uuid) {
|
||||
$deployments->push(['resource_uuid' => $resource->uuid, 'deployment_uuid' => $deployment_uuid->toString()]);
|
||||
}
|
||||
$message = $message->merge($return_message);
|
||||
}
|
||||
foreach ($services as $resource) {
|
||||
$return_message = $this->deploy_resource($resource, $force);
|
||||
['message' => $return_message] = $this->deploy_resource($resource, $force);
|
||||
$message = $message->merge($return_message);
|
||||
}
|
||||
}
|
||||
ray($message);
|
||||
if ($message->count() > 0) {
|
||||
return response()->json(['message' => $message->toArray()], 200);
|
||||
$payload->put('message', $message->toArray());
|
||||
if ($deployments->count() > 0) {
|
||||
$payload->put('details', $deployments->toArray());
|
||||
}
|
||||
return response()->json($payload->toArray(), 200);
|
||||
}
|
||||
|
||||
return response()->json(['error' => "No resources found.", 'upstream docs' => 'https://coolify.io/docs/api/deploy-webhook'], 404);
|
||||
return response()->json(['error' => "No resources found with this tag.", 'upstream docs' => 'https://coolify.io/docs/api/deploy-webhook'], 404);
|
||||
}
|
||||
public function deploy_resource($resource, bool $force = false): Collection
|
||||
public function deploy_resource($resource, bool $force = false): array
|
||||
{
|
||||
$message = collect([]);
|
||||
$message = null;
|
||||
$deployment_uuid = null;
|
||||
if (gettype($resource) !== 'object') {
|
||||
return $message->push("Resource ($resource) not found.");
|
||||
return ['message' => "Resource ($resource) not found.", 'deployment_uuid' => $deployment_uuid];
|
||||
}
|
||||
$type = $resource?->getMorphClass();
|
||||
if ($type === 'App\Models\Application') {
|
||||
$deployment_uuid = new Cuid2(7);
|
||||
queue_application_deployment(
|
||||
application: $resource,
|
||||
deployment_uuid: new Cuid2(7),
|
||||
deployment_uuid: $deployment_uuid,
|
||||
force_rebuild: $force,
|
||||
);
|
||||
$message->push("Application {$resource->name} deployment queued.");
|
||||
$message = "Application {$resource->name} deployment queued.";
|
||||
} else if ($type === 'App\Models\StandalonePostgresql') {
|
||||
if (str($resource->status)->startsWith('running')) {
|
||||
$message->push("Database {$resource->name} already running.");
|
||||
}
|
||||
StartPostgresql::run($resource);
|
||||
$resource->update([
|
||||
'started_at' => now(),
|
||||
]);
|
||||
$message->push("Database {$resource->name} started.");
|
||||
$message = "Database {$resource->name} started.";
|
||||
} else if ($type === 'App\Models\StandaloneRedis') {
|
||||
if (str($resource->status)->startsWith('running')) {
|
||||
$message->push("Database {$resource->name} already running.");
|
||||
}
|
||||
StartRedis::run($resource);
|
||||
$resource->update([
|
||||
'started_at' => now(),
|
||||
]);
|
||||
$message->push("Database {$resource->name} started.");
|
||||
$message = "Database {$resource->name} started.";
|
||||
} else if ($type === 'App\Models\StandaloneMongodb') {
|
||||
if (str($resource->status)->startsWith('running')) {
|
||||
$message->push("Database {$resource->name} already running.");
|
||||
}
|
||||
StartMongodb::run($resource);
|
||||
$resource->update([
|
||||
'started_at' => now(),
|
||||
]);
|
||||
$message->push("Database {$resource->name} started.");
|
||||
$message = "Database {$resource->name} started.";
|
||||
} else if ($type === 'App\Models\StandaloneMysql') {
|
||||
if (str($resource->status)->startsWith('running')) {
|
||||
$message->push("Database {$resource->name} already running.");
|
||||
}
|
||||
StartMysql::run($resource);
|
||||
$resource->update([
|
||||
'started_at' => now(),
|
||||
]);
|
||||
$message->push("Database {$resource->name} started.");
|
||||
$message = "Database {$resource->name} started.";
|
||||
} else if ($type === 'App\Models\StandaloneMariadb') {
|
||||
if (str($resource->status)->startsWith('running')) {
|
||||
$message->push("Database {$resource->name} already running.");
|
||||
}
|
||||
StartMariadb::run($resource);
|
||||
$resource->update([
|
||||
'started_at' => now(),
|
||||
]);
|
||||
$message->push("Database {$resource->name} started.");
|
||||
$message = "Database {$resource->name} started.";
|
||||
} else if ($type === 'App\Models\Service') {
|
||||
StartService::run($resource);
|
||||
$message->push("Service {$resource->name} started. It could take a while, be patient.");
|
||||
$message = "Service {$resource->name} started. It could take a while, be patient.";
|
||||
}
|
||||
return $message;
|
||||
return ['message' => $message, 'deployment_uuid' => $deployment_uuid];
|
||||
}
|
||||
}
|
||||
|
317
app/Http/Controllers/Api/DeployController.php
Normal file
317
app/Http/Controllers/Api/DeployController.php
Normal file
@ -0,0 +1,317 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Actions\Database\StartDatabase;
|
||||
use App\Actions\Service\StartService;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\ApplicationDeploymentQueue;
|
||||
use App\Models\Server;
|
||||
use App\Models\Tag;
|
||||
use Illuminate\Http\Request;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class DeployController extends Controller
|
||||
{
|
||||
private function removeSensitiveData($deployment)
|
||||
{
|
||||
$token = auth()->user()->currentAccessToken();
|
||||
if ($token->can('view:sensitive')) {
|
||||
return serializeApiResponse($deployment);
|
||||
}
|
||||
|
||||
$deployment->makeHidden([
|
||||
'logs',
|
||||
]);
|
||||
|
||||
return serializeApiResponse($deployment);
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
summary: 'List',
|
||||
description: 'List currently running deployments',
|
||||
path: '/deployments',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Deployments'],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Get all currently running deployments.',
|
||||
content: [
|
||||
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'array',
|
||||
items: new OA\Items(ref: '#/components/schemas/ApplicationDeploymentQueue'),
|
||||
)
|
||||
),
|
||||
]),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 400,
|
||||
ref: '#/components/responses/400',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function deployments(Request $request)
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
$servers = Server::whereTeamId($teamId)->get();
|
||||
$deployments_per_server = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('server_id', $servers->pluck('id'))->get()->sortBy('id');
|
||||
$deployments_per_server = $deployments_per_server->map(function ($deployment) {
|
||||
return $this->removeSensitiveData($deployment);
|
||||
});
|
||||
|
||||
return response()->json($deployments_per_server);
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
summary: 'Get',
|
||||
description: 'Get deployment by UUID.',
|
||||
path: '/deployments/{uuid}',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Deployments'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Deployment Uuid', schema: new OA\Schema(type: 'integer')),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Get deployment by UUID.',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
ref: '#/components/schemas/ApplicationDeploymentQueue',
|
||||
)
|
||||
),
|
||||
]),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 400,
|
||||
ref: '#/components/responses/400',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 404,
|
||||
ref: '#/components/responses/404',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function deployment_by_uuid(Request $request)
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
$uuid = $request->route('uuid');
|
||||
if (! $uuid) {
|
||||
return response()->json(['message' => 'UUID is required.'], 400);
|
||||
}
|
||||
$deployment = ApplicationDeploymentQueue::where('deployment_uuid', $uuid)->first();
|
||||
if (! $deployment) {
|
||||
return response()->json(['message' => 'Deployment not found.'], 404);
|
||||
}
|
||||
|
||||
return response()->json($this->removeSensitiveData($deployment));
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
summary: 'Deploy',
|
||||
description: 'Deploy by tag or uuid. `Post` request also accepted.',
|
||||
path: '/deploy',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Deployments'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'tag', in: 'query', description: 'Tag name(s). Comma separated list is also accepted.', schema: new OA\Schema(type: 'string')),
|
||||
new OA\Parameter(name: 'uuid', in: 'query', description: 'Resource UUID(s). Comma separated list is also accepted.', schema: new OA\Schema(type: 'string')),
|
||||
new OA\Parameter(name: 'force', in: 'query', description: 'Force rebuild (without cache)', schema: new OA\Schema(type: 'boolean')),
|
||||
],
|
||||
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Get deployment(s) Uuid\'s',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'deployments' => new OA\Property(
|
||||
property: 'deployments',
|
||||
type: 'array',
|
||||
items: new OA\Items(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'message' => ['type' => 'string'],
|
||||
'resource_uuid' => ['type' => 'string'],
|
||||
'deployment_uuid' => ['type' => 'string'],
|
||||
]
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
),
|
||||
]),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 400,
|
||||
ref: '#/components/responses/400',
|
||||
),
|
||||
|
||||
]
|
||||
)]
|
||||
public function deploy(Request $request)
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
$uuids = $request->query->get('uuid');
|
||||
$tags = $request->query->get('tag');
|
||||
$force = $request->query->get('force') ?? false;
|
||||
|
||||
if ($uuids && $tags) {
|
||||
return response()->json(['message' => 'You can only use uuid or tag, not both.'], 400);
|
||||
}
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
if ($tags) {
|
||||
return $this->by_tags($tags, $teamId, $force);
|
||||
} elseif ($uuids) {
|
||||
return $this->by_uuids($uuids, $teamId, $force);
|
||||
}
|
||||
|
||||
return response()->json(['message' => 'You must provide uuid or tag.'], 400);
|
||||
}
|
||||
|
||||
private function by_uuids(string $uuid, int $teamId, bool $force = false)
|
||||
{
|
||||
$uuids = explode(',', $uuid);
|
||||
$uuids = collect(array_filter($uuids));
|
||||
|
||||
if (count($uuids) === 0) {
|
||||
return response()->json(['message' => 'No UUIDs provided.'], 400);
|
||||
}
|
||||
$deployments = collect();
|
||||
$payload = collect();
|
||||
foreach ($uuids as $uuid) {
|
||||
$resource = getResourceByUuid($uuid, $teamId);
|
||||
if ($resource) {
|
||||
['message' => $return_message, 'deployment_uuid' => $deployment_uuid] = $this->deploy_resource($resource, $force);
|
||||
if ($deployment_uuid) {
|
||||
$deployments->push(['message' => $return_message, 'resource_uuid' => $uuid, 'deployment_uuid' => $deployment_uuid->toString()]);
|
||||
} else {
|
||||
$deployments->push(['message' => $return_message, 'resource_uuid' => $uuid]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($deployments->count() > 0) {
|
||||
$payload->put('deployments', $deployments->toArray());
|
||||
|
||||
return response()->json(serializeApiResponse($payload->toArray()));
|
||||
}
|
||||
|
||||
return response()->json(['message' => 'No resources found.'], 404);
|
||||
}
|
||||
|
||||
public function by_tags(string $tags, int $team_id, bool $force = false)
|
||||
{
|
||||
$tags = explode(',', $tags);
|
||||
$tags = collect(array_filter($tags));
|
||||
|
||||
if (count($tags) === 0) {
|
||||
return response()->json(['message' => 'No TAGs provided.'], 400);
|
||||
}
|
||||
$message = collect([]);
|
||||
$deployments = collect();
|
||||
$payload = collect();
|
||||
foreach ($tags as $tag) {
|
||||
$found_tag = Tag::where(['name' => $tag, 'team_id' => $team_id])->first();
|
||||
if (! $found_tag) {
|
||||
// $message->push("Tag {$tag} not found.");
|
||||
continue;
|
||||
}
|
||||
$applications = $found_tag->applications()->get();
|
||||
$services = $found_tag->services()->get();
|
||||
if ($applications->count() === 0 && $services->count() === 0) {
|
||||
$message->push("No resources found for tag {$tag}.");
|
||||
|
||||
continue;
|
||||
}
|
||||
foreach ($applications as $resource) {
|
||||
['message' => $return_message, 'deployment_uuid' => $deployment_uuid] = $this->deploy_resource($resource, $force);
|
||||
if ($deployment_uuid) {
|
||||
$deployments->push(['resource_uuid' => $resource->uuid, 'deployment_uuid' => $deployment_uuid->toString()]);
|
||||
}
|
||||
$message = $message->merge($return_message);
|
||||
}
|
||||
foreach ($services as $resource) {
|
||||
['message' => $return_message] = $this->deploy_resource($resource, $force);
|
||||
$message = $message->merge($return_message);
|
||||
}
|
||||
}
|
||||
if ($message->count() > 0) {
|
||||
$payload->put('message', $message->toArray());
|
||||
if ($deployments->count() > 0) {
|
||||
$payload->put('details', $deployments->toArray());
|
||||
}
|
||||
|
||||
return response()->json(serializeApiResponse($payload->toArray()));
|
||||
}
|
||||
|
||||
return response()->json(['message' => 'No resources found with this tag.'], 404);
|
||||
}
|
||||
|
||||
public function deploy_resource($resource, bool $force = false): array
|
||||
{
|
||||
$message = null;
|
||||
$deployment_uuid = null;
|
||||
if (gettype($resource) !== 'object') {
|
||||
return ['message' => "Resource ($resource) not found.", 'deployment_uuid' => $deployment_uuid];
|
||||
}
|
||||
switch ($resource?->getMorphClass()) {
|
||||
case 'App\Models\Application':
|
||||
$deployment_uuid = new Cuid2(7);
|
||||
queue_application_deployment(
|
||||
application: $resource,
|
||||
deployment_uuid: $deployment_uuid,
|
||||
force_rebuild: $force,
|
||||
);
|
||||
$message = "Application {$resource->name} deployment queued.";
|
||||
break;
|
||||
case 'App\Models\Service':
|
||||
StartService::run($resource);
|
||||
$message = "Service {$resource->name} started. It could take a while, be patient.";
|
||||
break;
|
||||
default:
|
||||
// Database resource
|
||||
StartDatabase::dispatch($resource);
|
||||
$resource->update([
|
||||
'started_at' => now(),
|
||||
]);
|
||||
$message = "Database {$resource->name} started.";
|
||||
break;
|
||||
}
|
||||
|
||||
return ['message' => $message, 'deployment_uuid' => $deployment_uuid];
|
||||
}
|
||||
}
|
35
app/Http/Controllers/Api/EnvironmentVariablesController.php
Normal file
35
app/Http/Controllers/Api/EnvironmentVariablesController.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\EnvironmentVariable;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class EnvironmentVariablesController extends Controller
|
||||
{
|
||||
public function delete_env_by_uuid(Request $request)
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
$env = EnvironmentVariable::where('uuid', $request->env_uuid)->first();
|
||||
if (! $env) {
|
||||
return response()->json([
|
||||
'message' => 'Environment variable not found.',
|
||||
], 404);
|
||||
}
|
||||
$found_app = $env->resource()->whereRelation('environment.project.team', 'id', $teamId)->first();
|
||||
if (! $found_app) {
|
||||
return response()->json([
|
||||
'message' => 'Environment variable not found.',
|
||||
], 404);
|
||||
}
|
||||
$env->delete();
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Environment variable deleted.',
|
||||
]);
|
||||
}
|
||||
}
|
51
app/Http/Controllers/Api/OpenApi.php
Normal file
51
app/Http/Controllers/Api/OpenApi.php
Normal file
@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
#[OA\Info(title: 'Coolify', version: '0.1')]
|
||||
#[OA\Server(url: 'https://app.coolify.io/api/v1')]
|
||||
#[OA\SecurityScheme(
|
||||
type: 'http',
|
||||
scheme: 'bearer',
|
||||
securityScheme: 'bearerAuth',
|
||||
description: 'Go to `Keys & Tokens` / `API tokens` and create a new token. Use the token as the bearer token.')]
|
||||
#[OA\Components(
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 400,
|
||||
description: 'Invalid token.',
|
||||
content: new OA\JsonContent(
|
||||
type: 'object',
|
||||
properties: [
|
||||
new OA\Property(property: 'message', type: 'string', example: 'Invalid token.'),
|
||||
]
|
||||
)),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
description: 'Unauthenticated.',
|
||||
content: new OA\JsonContent(
|
||||
type: 'object',
|
||||
properties: [
|
||||
new OA\Property(property: 'message', type: 'string', example: 'Unauthenticated.'),
|
||||
]
|
||||
)),
|
||||
new OA\Response(
|
||||
response: 404,
|
||||
description: 'Resource not found.',
|
||||
content: new OA\JsonContent(
|
||||
type: 'object',
|
||||
properties: [
|
||||
new OA\Property(property: 'message', type: 'string', example: 'Resource not found.'),
|
||||
]
|
||||
)),
|
||||
],
|
||||
)]
|
||||
class OpenApi
|
||||
{
|
||||
// This class is used to generate OpenAPI documentation
|
||||
// for the Coolify API. It is not a controller and does
|
||||
// not contain any routes. It is used to define the
|
||||
// OpenAPI metadata and security scheme for the API.
|
||||
}
|
183
app/Http/Controllers/Api/OtherController.php
Normal file
183
app/Http/Controllers/Api/OtherController.php
Normal file
@ -0,0 +1,183 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
class OtherController extends Controller
|
||||
{
|
||||
#[OA\Get(
|
||||
summary: 'Version',
|
||||
description: 'Get Coolify version.',
|
||||
path: '/version',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Returns the version of the application',
|
||||
content: new OA\JsonContent(
|
||||
type: 'string',
|
||||
example: 'v4.0.0',
|
||||
)),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 400,
|
||||
ref: '#/components/responses/400',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function version(Request $request)
|
||||
{
|
||||
return response(config('version'));
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
summary: 'Enable API',
|
||||
description: 'Enable API (only with root permissions).',
|
||||
path: '/enable',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Enable API.',
|
||||
content: new OA\JsonContent(
|
||||
type: 'object',
|
||||
properties: [
|
||||
new OA\Property(property: 'message', type: 'string', example: 'API enabled.'),
|
||||
]
|
||||
)),
|
||||
new OA\Response(
|
||||
response: 403,
|
||||
description: 'You are not allowed to enable the API.',
|
||||
content: new OA\JsonContent(
|
||||
type: 'object',
|
||||
properties: [
|
||||
new OA\Property(property: 'message', type: 'string', example: 'You are not allowed to enable the API.'),
|
||||
]
|
||||
)),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 400,
|
||||
ref: '#/components/responses/400',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function enable_api(Request $request)
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
if ($teamId !== '0') {
|
||||
return response()->json(['message' => 'You are not allowed to enable the API.'], 403);
|
||||
}
|
||||
$settings = \App\Models\InstanceSettings::get();
|
||||
$settings->update(['is_api_enabled' => true]);
|
||||
|
||||
return response()->json(['message' => 'API enabled.'], 200);
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
summary: 'Disable API',
|
||||
description: 'Disable API (only with root permissions).',
|
||||
path: '/disable',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Disable API.',
|
||||
content: new OA\JsonContent(
|
||||
type: 'object',
|
||||
properties: [
|
||||
new OA\Property(property: 'message', type: 'string', example: 'API disabled.'),
|
||||
]
|
||||
)),
|
||||
new OA\Response(
|
||||
response: 403,
|
||||
description: 'You are not allowed to disable the API.',
|
||||
content: new OA\JsonContent(
|
||||
type: 'object',
|
||||
properties: [
|
||||
new OA\Property(property: 'message', type: 'string', example: 'You are not allowed to disable the API.'),
|
||||
]
|
||||
)),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 400,
|
||||
ref: '#/components/responses/400',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function disable_api(Request $request)
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
if ($teamId !== '0') {
|
||||
return response()->json(['message' => 'You are not allowed to disable the API.'], 403);
|
||||
}
|
||||
$settings = \App\Models\InstanceSettings::get();
|
||||
$settings->update(['is_api_enabled' => false]);
|
||||
|
||||
return response()->json(['message' => 'API disabled.'], 200);
|
||||
}
|
||||
|
||||
public function feedback(Request $request)
|
||||
{
|
||||
$content = $request->input('content');
|
||||
$webhook_url = config('coolify.feedback_discord_webhook');
|
||||
if ($webhook_url) {
|
||||
Http::post($webhook_url, [
|
||||
'content' => $content,
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json(['message' => 'Feedback sent.'], 200);
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
summary: 'Healthcheck',
|
||||
description: 'Healthcheck endpoint.',
|
||||
path: '/healthcheck',
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Healthcheck endpoint.',
|
||||
content: new OA\JsonContent(
|
||||
type: 'string',
|
||||
example: 'OK',
|
||||
)),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 400,
|
||||
ref: '#/components/responses/400',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function healthcheck(Request $request)
|
||||
{
|
||||
return 'OK';
|
||||
}
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Project as ModelsProject;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class Project extends Controller
|
||||
{
|
||||
public function projects(Request $request)
|
||||
{
|
||||
$teamId = get_team_id_from_token();
|
||||
if (is_null($teamId)) {
|
||||
return response()->json(['error' => 'Invalid token.', 'upstream docs' => 'https://coolify.io/docs/api/authentication'], 400);
|
||||
}
|
||||
$projects = ModelsProject::whereTeamId($teamId)->select('id', 'name', 'uuid')->get();
|
||||
return response()->json($projects);
|
||||
}
|
||||
public function project_by_uuid(Request $request)
|
||||
{
|
||||
$teamId = get_team_id_from_token();
|
||||
if (is_null($teamId)) {
|
||||
return response()->json(['error' => 'Invalid token.', 'upstream docs' => 'https://coolify.io/docs/api/authentication'], 400);
|
||||
}
|
||||
$project = ModelsProject::whereTeamId($teamId)->whereUuid(request()->uuid)->first()->load(['environments']);
|
||||
return response()->json($project);
|
||||
}
|
||||
public function environment_details(Request $request)
|
||||
{
|
||||
$teamId = get_team_id_from_token();
|
||||
if (is_null($teamId)) {
|
||||
return response()->json(['error' => 'Invalid token.', 'upstream docs' => 'https://coolify.io/docs/api/authentication'], 400);
|
||||
}
|
||||
$project = ModelsProject::whereTeamId($teamId)->whereUuid(request()->uuid)->first();
|
||||
$environment = $project->environments()->whereName(request()->environment_name)->first()->load(['applications', 'postgresqls', 'redis', 'mongodbs', 'mysqls', 'mariadbs', 'services']);
|
||||
return response()->json($environment);
|
||||
}
|
||||
}
|
425
app/Http/Controllers/Api/ProjectController.php
Normal file
425
app/Http/Controllers/Api/ProjectController.php
Normal file
@ -0,0 +1,425 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Project;
|
||||
use Illuminate\Http\Request;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
class ProjectController extends Controller
|
||||
{
|
||||
#[OA\Get(
|
||||
summary: 'List',
|
||||
description: 'list projects.',
|
||||
path: '/projects',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Projects'],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Get all projects.',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'array',
|
||||
items: new OA\Items(ref: '#/components/schemas/Project')
|
||||
)
|
||||
),
|
||||
]),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 400,
|
||||
ref: '#/components/responses/400',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function projects(Request $request)
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
$projects = Project::whereTeamId($teamId)->select('id', 'name', 'uuid')->get();
|
||||
|
||||
return response()->json(serializeApiResponse($projects),
|
||||
);
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
summary: 'Get',
|
||||
description: 'Get project by Uuid.',
|
||||
path: '/projects/{uuid}',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Projects'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Project UUID', schema: new OA\Schema(type: 'integer')),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Project details',
|
||||
content: new OA\JsonContent(ref: '#/components/schemas/Project')),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 400,
|
||||
ref: '#/components/responses/400',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 404,
|
||||
description: 'Project not found.',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function project_by_uuid(Request $request)
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
$project = Project::whereTeamId($teamId)->whereUuid(request()->uuid)->first()->load(['environments']);
|
||||
if (! $project) {
|
||||
return response()->json(['message' => 'Project not found.'], 404);
|
||||
}
|
||||
|
||||
return response()->json(
|
||||
serializeApiResponse($project),
|
||||
);
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
summary: 'Environment',
|
||||
description: 'Get environment by name.',
|
||||
path: '/projects/{uuid}/{environment_name}',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Projects'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Project UUID', schema: new OA\Schema(type: 'integer')),
|
||||
new OA\Parameter(name: 'environment_name', in: 'path', required: true, description: 'Environment name', schema: new OA\Schema(type: 'string')),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Project details',
|
||||
content: new OA\JsonContent(ref: '#/components/schemas/Environment')),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 400,
|
||||
ref: '#/components/responses/400',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 404,
|
||||
ref: '#/components/responses/404',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function environment_details(Request $request)
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
if (! $request->uuid) {
|
||||
return response()->json(['message' => 'Uuid is required.'], 422);
|
||||
}
|
||||
if (! $request->environment_name) {
|
||||
return response()->json(['message' => 'Environment name is required.'], 422);
|
||||
}
|
||||
$project = Project::whereTeamId($teamId)->whereUuid($request->uuid)->first();
|
||||
$environment = $project->environments()->whereName($request->environment_name)->first();
|
||||
if (! $environment) {
|
||||
return response()->json(['message' => 'Environment not found.'], 404);
|
||||
}
|
||||
$environment = $environment->load(['applications', 'postgresqls', 'redis', 'mongodbs', 'mysqls', 'mariadbs', 'services']);
|
||||
|
||||
return response()->json(serializeApiResponse($environment));
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
summary: 'Create',
|
||||
description: 'Create Project.',
|
||||
path: '/projects',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Projects'],
|
||||
requestBody: new OA\RequestBody(
|
||||
required: true,
|
||||
description: 'Project created.',
|
||||
content: new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'uuid' => ['type' => 'string', 'description' => 'The name of the project.'],
|
||||
'description' => ['type' => 'string', 'description' => 'The description of the project.'],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 201,
|
||||
description: 'Project created.',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'uuid' => ['type' => 'string', 'example' => 'og888os', 'description' => 'The UUID of the project.'],
|
||||
]
|
||||
)
|
||||
),
|
||||
]),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 400,
|
||||
ref: '#/components/responses/400',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 404,
|
||||
ref: '#/components/responses/404',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function create_project(Request $request)
|
||||
{
|
||||
$allowedFields = ['name', 'description'];
|
||||
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
$return = validateIncomingRequest($request);
|
||||
if ($return instanceof \Illuminate\Http\JsonResponse) {
|
||||
return $return;
|
||||
}
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'name' => 'string|max:255|required',
|
||||
'description' => 'string|nullable',
|
||||
]);
|
||||
|
||||
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
|
||||
if ($validator->fails() || ! empty($extraFields)) {
|
||||
$errors = $validator->errors();
|
||||
if (! empty($extraFields)) {
|
||||
foreach ($extraFields as $field) {
|
||||
$errors->add($field, 'This field is not allowed.');
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $errors,
|
||||
], 422);
|
||||
}
|
||||
|
||||
$project = Project::create([
|
||||
'name' => $request->name,
|
||||
'description' => $request->description,
|
||||
'team_id' => $teamId,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'uuid' => $project->uuid,
|
||||
])->setStatusCode(201);
|
||||
}
|
||||
|
||||
#[OA\Patch(
|
||||
summary: 'Update',
|
||||
description: 'Update Project.',
|
||||
path: '/projects/{uuid}',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Projects'],
|
||||
requestBody: new OA\RequestBody(
|
||||
required: true,
|
||||
description: 'Project updated.',
|
||||
content: new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'name' => ['type' => 'string', 'description' => 'The name of the project.'],
|
||||
'description' => ['type' => 'string', 'description' => 'The description of the project.'],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 201,
|
||||
description: 'Project updated.',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'uuid' => ['type' => 'string', 'example' => 'og888os'],
|
||||
'name' => ['type' => 'string', 'example' => 'Project Name'],
|
||||
'description' => ['type' => 'string', 'example' => 'Project Description'],
|
||||
]
|
||||
)
|
||||
),
|
||||
]),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 400,
|
||||
ref: '#/components/responses/400',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 404,
|
||||
ref: '#/components/responses/404',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function update_project(Request $request)
|
||||
{
|
||||
$allowedFields = ['name', 'description'];
|
||||
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
$return = validateIncomingRequest($request);
|
||||
if ($return instanceof \Illuminate\Http\JsonResponse) {
|
||||
return $return;
|
||||
}
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'name' => 'string|max:255|nullable',
|
||||
'description' => 'string|nullable',
|
||||
]);
|
||||
|
||||
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
|
||||
if ($validator->fails() || ! empty($extraFields)) {
|
||||
$errors = $validator->errors();
|
||||
if (! empty($extraFields)) {
|
||||
foreach ($extraFields as $field) {
|
||||
$errors->add($field, 'This field is not allowed.');
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $errors,
|
||||
], 422);
|
||||
}
|
||||
$uuid = $request->uuid;
|
||||
if (! $uuid) {
|
||||
return response()->json(['message' => 'Uuid is required.'], 422);
|
||||
}
|
||||
|
||||
$project = Project::whereTeamId($teamId)->whereUuid($uuid)->first();
|
||||
if (! $project) {
|
||||
return response()->json(['message' => 'Project not found.'], 404);
|
||||
}
|
||||
|
||||
$project->update($request->only($allowedFields));
|
||||
|
||||
return response()->json([
|
||||
'uuid' => $project->uuid,
|
||||
'name' => $project->name,
|
||||
'description' => $project->description,
|
||||
])->setStatusCode(201);
|
||||
|
||||
}
|
||||
|
||||
#[OA\Delete(
|
||||
summary: 'Delete',
|
||||
description: 'Delete project by UUID.',
|
||||
path: '/projects/{uuid}',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Projects'],
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the application.',
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
)
|
||||
),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Project deleted.',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'message' => ['type' => 'string', 'example' => 'Project deleted.'],
|
||||
]
|
||||
)
|
||||
),
|
||||
]),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 400,
|
||||
ref: '#/components/responses/400',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 404,
|
||||
ref: '#/components/responses/404',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function delete_project(Request $request)
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
if (! $request->uuid) {
|
||||
return response()->json(['message' => 'Uuid is required.'], 422);
|
||||
}
|
||||
$project = Project::whereTeamId($teamId)->whereUuid($request->uuid)->first();
|
||||
if (! $project) {
|
||||
return response()->json(['message' => 'Project not found.'], 404);
|
||||
}
|
||||
if ($project->resource_count() > 0) {
|
||||
return response()->json(['message' => 'Project has resources, so it cannot be deleted.'], 400);
|
||||
}
|
||||
|
||||
$project->delete();
|
||||
|
||||
return response()->json(['message' => 'Project deleted.']);
|
||||
}
|
||||
}
|
67
app/Http/Controllers/Api/ResourcesController.php
Normal file
67
app/Http/Controllers/Api/ResourcesController.php
Normal file
@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Project;
|
||||
use Illuminate\Http\Request;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
class ResourcesController extends Controller
|
||||
{
|
||||
#[OA\Get(
|
||||
summary: 'List',
|
||||
description: 'Get all resources.',
|
||||
path: '/resources',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Resources'],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Get all resources',
|
||||
content: new OA\JsonContent(
|
||||
type: 'string',
|
||||
example: 'Content is very complex. Will be implemented later.',
|
||||
),
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 400,
|
||||
ref: '#/components/responses/400',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function resources(Request $request)
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
$projects = Project::where('team_id', $teamId)->get();
|
||||
$resources = collect();
|
||||
$resources->push($projects->pluck('applications')->flatten());
|
||||
$resources->push($projects->pluck('services')->flatten());
|
||||
foreach (collect(DATABASE_TYPES) as $db) {
|
||||
$resources->push($projects->pluck(str($db)->plural(2))->flatten());
|
||||
}
|
||||
$resources = $resources->flatten();
|
||||
$resources = $resources->map(function ($resource) {
|
||||
$payload = $resource->toArray();
|
||||
if ($resource->getMorphClass() === 'App\Models\Service') {
|
||||
$payload['status'] = $resource->status();
|
||||
} else {
|
||||
$payload['status'] = $resource->status;
|
||||
}
|
||||
$payload['type'] = $resource->type();
|
||||
|
||||
return $payload;
|
||||
});
|
||||
|
||||
return response()->json(serializeApiResponse($resources));
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user