mirror of
https://github.com/ershisan99/coolify.git
synced 2025-12-18 12:33:06 +00:00
Compare commits
386 Commits
php83
...
v4.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c3beb6715 | ||
|
|
a807016cab | ||
|
|
2c2173e773 | ||
|
|
9a1c9124ae | ||
|
|
306c4dcbc5 | ||
|
|
3535dbb98f | ||
|
|
84b2af53d8 | ||
|
|
ba70451765 | ||
|
|
f3ec4ca4a3 | ||
|
|
174923de98 | ||
|
|
68ab8dc14c | ||
|
|
b218356f2d | ||
|
|
e83aecebc3 | ||
|
|
0bb1f57ea7 | ||
|
|
d006edc485 | ||
|
|
aa28c99827 | ||
|
|
6bd8583eab | ||
|
|
567bbe9d0b | ||
|
|
59d2c9748a | ||
|
|
bd2e1ad9fe | ||
|
|
43df918e43 | ||
|
|
1a8d15390d | ||
|
|
4af6caa79c | ||
|
|
8e91d958be | ||
|
|
17d55630d5 | ||
|
|
70dfa101ef | ||
|
|
d6b4e33db3 | ||
|
|
a9670bd6eb | ||
|
|
02165c2b9f | ||
|
|
afbdd2eb06 | ||
|
|
b0e6014e8f | ||
|
|
0f13af2eca | ||
|
|
d4d9268f12 | ||
|
|
62459f9a95 | ||
|
|
eb80a7e2f7 | ||
|
|
57d8930f9e | ||
|
|
688c27c901 | ||
|
|
480ae3de8a | ||
|
|
21a5b4ce80 | ||
|
|
12a0d71e47 | ||
|
|
5ed7ae3d3e | ||
|
|
6dd3adba4d | ||
|
|
08d58eb8f8 | ||
|
|
46a7937761 | ||
|
|
13a5536cff | ||
|
|
2381706465 | ||
|
|
7a0f054f1a | ||
|
|
00e1464185 | ||
|
|
fac4a8aaf9 | ||
|
|
2841675691 | ||
|
|
573e5c4913 | ||
|
|
6934a746f7 | ||
|
|
b570ccd7d3 | ||
|
|
68efd4b553 | ||
|
|
e00ec2f75b | ||
|
|
1bb192f3e2 | ||
|
|
e039f183da | ||
|
|
7aab44645f | ||
|
|
04e504bb8b | ||
|
|
960f970822 | ||
|
|
ae62781c6a | ||
|
|
b42c15b2e5 | ||
|
|
c77cc45e0d | ||
|
|
310708f1b6 | ||
|
|
8386aaf95b | ||
|
|
6d8d91802b | ||
|
|
c0a1f15d34 | ||
|
|
0222aa137d | ||
|
|
4d36bc4742 | ||
|
|
6639379ba6 | ||
|
|
d60ff107c4 | ||
|
|
a468ce77f0 | ||
|
|
1fb0ee0db5 | ||
|
|
9281dda03f | ||
|
|
d3485cea8d | ||
|
|
92772f9132 | ||
|
|
6f8c4a4ce4 | ||
|
|
85ab772acd | ||
|
|
6336125ca2 | ||
|
|
341c7e3598 | ||
|
|
ff9b68b450 | ||
|
|
a072a00926 | ||
|
|
b84d39ba56 | ||
|
|
4e167dc539 | ||
|
|
b7139f8af8 | ||
|
|
a5b3fef1d8 | ||
|
|
b9a026b488 | ||
|
|
a4d0f1da9e | ||
|
|
eb9bbf3eda | ||
|
|
be42f15711 | ||
|
|
fa375e2fd8 | ||
|
|
760cf8aeb5 | ||
|
|
f9238ce263 | ||
|
|
c0898f0568 | ||
|
|
5b00b66f24 | ||
|
|
b81f9114d9 | ||
|
|
be8573c828 | ||
|
|
f1881d5c35 | ||
|
|
699e76637b | ||
|
|
dbc723089b | ||
|
|
fc6f5d82db | ||
|
|
1815c9dccf | ||
|
|
7d54fe9c18 | ||
|
|
e9fbb7d2b0 | ||
|
|
2d94ebf7f1 | ||
|
|
c874261c5b | ||
|
|
1c5fd8d668 | ||
|
|
874e04e844 | ||
|
|
c46715db3c | ||
|
|
aa0fe922ea | ||
|
|
60e9fcc202 | ||
|
|
91485b5d4d | ||
|
|
6ec1b9b3f5 | ||
|
|
240352f4b2 | ||
|
|
cfd9ae9817 | ||
|
|
f65789bdbb | ||
|
|
9518040d23 | ||
|
|
631b4e6438 | ||
|
|
d47bd047bf | ||
|
|
a65b62332b | ||
|
|
eb10bbb888 | ||
|
|
4b02debf97 | ||
|
|
430fdcf2e9 | ||
|
|
636de24b6c | ||
|
|
2e3267ee94 | ||
|
|
98e744e808 | ||
|
|
532f5e351e | ||
|
|
182087cf1b | ||
|
|
6676c6bd64 | ||
|
|
8153e2f63b | ||
|
|
35857de697 | ||
|
|
5ec45d547a | ||
|
|
d2cf53578a | ||
|
|
1fa42ec82d | ||
|
|
e42c7e258c | ||
|
|
2c210abf57 | ||
|
|
98ba7ac28c | ||
|
|
5772f2c3d9 | ||
|
|
cb9c569d4b | ||
|
|
5616e588e9 | ||
|
|
40e844fab4 | ||
|
|
2e56edd939 | ||
|
|
8179a5c6a3 | ||
|
|
d0518153fb | ||
|
|
29236bf101 | ||
|
|
2d306d56ab | ||
|
|
e5768999e4 | ||
|
|
b076db2eed | ||
|
|
3534424dc8 | ||
|
|
3b6c3609c3 | ||
|
|
95a97cf9cd | ||
|
|
7b33ef705b | ||
|
|
39449584ac | ||
|
|
19458e7445 | ||
|
|
2d9c728a64 | ||
|
|
12a8e9b0e1 | ||
|
|
649cc2dac2 | ||
|
|
c169f1f64b | ||
|
|
5ecf31d1fc | ||
|
|
e937d30545 | ||
|
|
bf48b33e64 | ||
|
|
595a2414b1 | ||
|
|
07ed726c88 | ||
|
|
d373815f98 | ||
|
|
d9181bd00b | ||
|
|
d13e2c0865 | ||
|
|
42ff7b19a4 | ||
|
|
283fcc87a5 | ||
|
|
ea3501ada6 | ||
|
|
175f4b9ae1 | ||
|
|
2bc74c75e1 | ||
|
|
4ac2758d70 | ||
|
|
bdc0fc87f0 | ||
|
|
845d32c94c | ||
|
|
6a6b947fba | ||
|
|
2ec66fd146 | ||
|
|
ccbbfd8908 | ||
|
|
43895419ff | ||
|
|
1c78067386 | ||
|
|
871d09bd96 | ||
|
|
2d8bda4fa6 | ||
|
|
95070ab48d | ||
|
|
8bb8a7faa3 | ||
|
|
428c40aab5 | ||
|
|
52c4994d44 | ||
|
|
6f578855a0 | ||
|
|
8967315c49 | ||
|
|
162fb7bfc5 | ||
|
|
144508218e | ||
|
|
4bdb5c9030 | ||
|
|
a2ea8814cf | ||
|
|
cbc2f1f015 | ||
|
|
35b9b7fdf2 | ||
|
|
d92819ab60 | ||
|
|
ea877f3623 | ||
|
|
5818c9cf6b | ||
|
|
f9375f91ec | ||
|
|
86722939cd | ||
|
|
70b757df5b | ||
|
|
451272bf11 | ||
|
|
a68fbefadb | ||
|
|
b79b4015d7 | ||
|
|
0bfdc1c531 | ||
|
|
b09017ea46 | ||
|
|
95fcf38d45 | ||
|
|
54c03fae41 | ||
|
|
b2bab451d3 | ||
|
|
7b4559c5e6 | ||
|
|
ba636a95dc | ||
|
|
682b45a2b5 | ||
|
|
d44e3a1091 | ||
|
|
d2d56f136b | ||
|
|
9b48a99798 | ||
|
|
1322dc9c23 | ||
|
|
f71fb7266d | ||
|
|
35f23cfb96 | ||
|
|
0c4ce55a15 | ||
|
|
77ee80b562 | ||
|
|
2621292dc1 | ||
|
|
62a4d7055a | ||
|
|
3fd41c0a92 | ||
|
|
0e8291cd86 | ||
|
|
175b89ced2 | ||
|
|
3aee8e030e | ||
|
|
02017334e5 | ||
|
|
f9b7841572 | ||
|
|
7d39a5089c | ||
|
|
2313fed546 | ||
|
|
e1a6c3e776 | ||
|
|
7037c779e2 | ||
|
|
f124a1e60d | ||
|
|
7ebb0a4579 | ||
|
|
4962c606bc | ||
|
|
b728d69ab0 | ||
|
|
3d6e53602c | ||
|
|
1c6450da24 | ||
|
|
15fe5bd864 | ||
|
|
08e4afbbc4 | ||
|
|
06fd7286da | ||
|
|
fce1e34fc8 | ||
|
|
0739e0f5e7 | ||
|
|
faf7ba50e6 | ||
|
|
e8179ec519 | ||
|
|
7948a0309f | ||
|
|
47277a68ec | ||
|
|
60f75f9deb | ||
|
|
e90d1af884 | ||
|
|
e3046698a5 | ||
|
|
bb773e1118 | ||
|
|
dcf91cc034 | ||
|
|
cddf8476de | ||
|
|
51c43e7457 | ||
|
|
7cac243589 | ||
|
|
0cfd8ed5f0 | ||
|
|
8318598cb5 | ||
|
|
2f692da1c9 | ||
|
|
ecd98bfcd5 | ||
|
|
ba860398f3 | ||
|
|
1f9af39fa7 | ||
|
|
62b995d26c | ||
|
|
b28d118609 | ||
|
|
81d0589c55 | ||
|
|
07893b432b | ||
|
|
568d47d1dd | ||
|
|
85cce4e453 | ||
|
|
888c1f7697 | ||
|
|
79c8ce7572 | ||
|
|
121afaa18c | ||
|
|
e48ad87cdb | ||
|
|
a587cae251 | ||
|
|
c04c1530f5 | ||
|
|
f4ee61fc6a | ||
|
|
b6080c2c8e | ||
|
|
1738286983 | ||
|
|
703bf51705 | ||
|
|
aa4980289d | ||
|
|
dd8a2dd3c1 | ||
|
|
3086ef1462 | ||
|
|
b9b33c831c | ||
|
|
9310af0301 | ||
|
|
0d3d5f40fd | ||
|
|
35dfb1b0f8 | ||
|
|
2edcd01493 | ||
|
|
117fbeb07c | ||
|
|
33e9c9b0f9 | ||
|
|
fc3c69f687 | ||
|
|
08df814408 | ||
|
|
a7b78dcf41 | ||
|
|
2b5df8d2fd | ||
|
|
f4263ee022 | ||
|
|
44f3f6001e | ||
|
|
9105c1aa51 | ||
|
|
3e04a7958e | ||
|
|
505127dae5 | ||
|
|
371fe53911 | ||
|
|
9515bc6162 | ||
|
|
93a4a3e09c | ||
|
|
bbbd5cbaa1 | ||
|
|
7fe3b78d45 | ||
|
|
a29353c1ae | ||
|
|
c16e914be4 | ||
|
|
bec974dca4 | ||
|
|
b314b08f25 | ||
|
|
9a2d5be354 | ||
|
|
d94e39ccc7 | ||
|
|
d5b7e9ed83 | ||
|
|
f29bc52fa5 | ||
|
|
3d21f1a2a4 | ||
|
|
20558d438a | ||
|
|
792f6bc163 | ||
|
|
3d1c730703 | ||
|
|
c3188958b4 | ||
|
|
c24fec9c45 | ||
|
|
70043c24cf | ||
|
|
ff1e08cf8b | ||
|
|
a4d1ae1341 | ||
|
|
5944ee5524 | ||
|
|
1b0c5f8d69 | ||
|
|
dfd218ec06 | ||
|
|
843e3fb599 | ||
|
|
a97ccd206c | ||
|
|
776d41613d | ||
|
|
f857bbc437 | ||
|
|
f8226cf892 | ||
|
|
d2a0621f93 | ||
|
|
38845d7eb0 | ||
|
|
3b3bc6c33b | ||
|
|
2adac01034 | ||
|
|
b656cabb33 | ||
|
|
830c047ccf | ||
|
|
a3dd48de1d | ||
|
|
b118a627d0 | ||
|
|
bcfca40f3a | ||
|
|
76cb473db8 | ||
|
|
73dfdb83a7 | ||
|
|
bff6964d4a | ||
|
|
b807601d19 | ||
|
|
9136d7acdc | ||
|
|
da0398f35d | ||
|
|
6820fcc084 | ||
|
|
d984bec175 | ||
|
|
182af1ec18 | ||
|
|
a22e757ab7 | ||
|
|
354c74e920 | ||
|
|
62b7900855 | ||
|
|
c92659994d | ||
|
|
a2651ab3c1 | ||
|
|
1b51d46b3d | ||
|
|
141752b9ad | ||
|
|
73068aaa75 | ||
|
|
8d2a02dc3c | ||
|
|
4726676248 | ||
|
|
9040f5d2a1 | ||
|
|
ac50d8b4d8 | ||
|
|
2a581147aa | ||
|
|
2b8c9920d8 | ||
|
|
548fc21e40 | ||
|
|
c2ea8996ee | ||
|
|
5b54dc8792 | ||
|
|
b5360e5e75 | ||
|
|
840e225aa8 | ||
|
|
7d1179e7c8 | ||
|
|
16a5c601e3 | ||
|
|
c566152f37 | ||
|
|
2ca6ffb84e | ||
|
|
41be1f7666 | ||
|
|
450351921e | ||
|
|
a4bb87d13b | ||
|
|
1cfddfd529 | ||
|
|
72bcf03cbb | ||
|
|
d177e49e62 | ||
|
|
5595853379 | ||
|
|
97c2bedda2 | ||
|
|
d980c7a425 | ||
|
|
53dff4ca4f | ||
|
|
7722809c52 | ||
|
|
e67e03f73f | ||
|
|
86a087056e | ||
|
|
51071da700 | ||
|
|
70aa05bde9 | ||
|
|
0135e2b5c0 | ||
|
|
2f95349888 | ||
|
|
4d0acee95c | ||
|
|
74bea37b43 | ||
|
|
070daee28e | ||
|
|
df796dffa2 |
@@ -6,7 +6,7 @@ APP_KEY=
|
|||||||
APP_URL=http://localhost
|
APP_URL=http://localhost
|
||||||
APP_PORT=8000
|
APP_PORT=8000
|
||||||
APP_DEBUG=true
|
APP_DEBUG=true
|
||||||
SSH_MUX_ENABLED=false
|
SSH_MUX_ENABLED=true
|
||||||
|
|
||||||
# PostgreSQL Database Configuration
|
# PostgreSQL Database Configuration
|
||||||
DB_DATABASE=coolify
|
DB_DATABASE=coolify
|
||||||
@@ -19,11 +19,7 @@ DB_PORT=5432
|
|||||||
# Set to true to enable Ray
|
# Set to true to enable Ray
|
||||||
RAY_ENABLED=false
|
RAY_ENABLED=false
|
||||||
# Set custom ray port
|
# Set custom ray port
|
||||||
RAY_PORT=
|
# RAY_PORT=
|
||||||
|
|
||||||
# Clockwork Configuration
|
|
||||||
CLOCKWORK_ENABLED=false
|
|
||||||
CLOCKWORK_QUEUE_COLLECT=true
|
|
||||||
|
|
||||||
# Enable Laravel Telescope for debugging
|
# Enable Laravel Telescope for debugging
|
||||||
TELESCOPE_ENABLED=false
|
TELESCOPE_ENABLED=false
|
||||||
|
|||||||
75
.github/ISSUE_TEMPLATE/BUG_REPORT.yml
vendored
75
.github/ISSUE_TEMPLATE/BUG_REPORT.yml
vendored
@@ -1,46 +1,65 @@
|
|||||||
name: Bug report
|
name: 🐞 Bug Report
|
||||||
description: "Create a new bug report."
|
description: "File a new bug report."
|
||||||
title: "[Bug]: "
|
title: "[Bug]: "
|
||||||
|
labels: ["🐛 Bug", "🔍 Triage"]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: >-
|
value: |
|
||||||
# 💎 Bounty program (with
|
> [!IMPORTANT]
|
||||||
[algora.io](https://console.algora.io/org/coollabsio/bounties/new))
|
> **Please ensure you are using the latest version of Coolify before submitting an issue, as the bug may have already been fixed in a recent update.** (Of course, if you're experiencing an issue on the latest version that wasn't present in a previous version, please let us know.)
|
||||||
|
|
||||||
|
# 💎 Bounty Program (with [algora.io](https://console.algora.io/org/coollabsio/bounties/new))
|
||||||
|
- If you would like to prioritize the issue resolution, consider adding a bounty to this issue through our [Bounty Program](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
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Description
|
label: Error Message and Logs
|
||||||
description: A clear and concise description of the problem
|
description: Provide a detailed description of the error or exception you encountered, along with any relevant log output.
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Minimal Reproduction (if possible, example repository)
|
|
||||||
description: Please provide a step by step guide to reproduce the issue.
|
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Exception or Error
|
label: Steps to Reproduce
|
||||||
description: Please provide error logs if possible.
|
description: Please provide a step-by-step guide to reproduce the issue. Be as detailed as possible, otherwise we may not be able to assist you.
|
||||||
|
value: |
|
||||||
|
1.
|
||||||
|
2.
|
||||||
|
3.
|
||||||
|
4.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
- type: input
|
- type: input
|
||||||
attributes:
|
attributes:
|
||||||
label: Version
|
label: Example Repository URL
|
||||||
description: Coolify's version (see top of your screen).
|
description: If applicable, provide a URL to a repository demonstrating the issue.
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: Coolify Version
|
||||||
|
description: Please provide the Coolify version you are using. This can be found in the top left corner of your Coolify dashboard.
|
||||||
|
placeholder: "v4.0.0-beta.335"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: checkboxes
|
|
||||||
|
- type: dropdown
|
||||||
attributes:
|
attributes:
|
||||||
label: Cloud?
|
label: Are you using Coolify Cloud?
|
||||||
description: "Are you using the cloud version of Coolify?"
|
|
||||||
options:
|
options:
|
||||||
- label: 'Yes'
|
- "No (self-hosted)"
|
||||||
required: false
|
- "Yes (Coolify Cloud)"
|
||||||
- label: 'No'
|
validations:
|
||||||
required: false
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: Operating System and Version (self-hosted)
|
||||||
|
description: Run `cat /etc/os-release` or `lsb_release -a` in your terminal and provide the operating system and version.
|
||||||
|
placeholder: "Ubuntu 22.04"
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Additional Information
|
||||||
|
description: Any other relevant details about the issue.
|
||||||
|
|||||||
31
.github/ISSUE_TEMPLATE/ENHANCEMENT_BOUNTY.yml
vendored
Normal file
31
.github/ISSUE_TEMPLATE/ENHANCEMENT_BOUNTY.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
name: 💎 Enhancement Bounty
|
||||||
|
description: "Propose a new feature, service, or improvement with an attached bounty."
|
||||||
|
title: "[Enhancement]: "
|
||||||
|
labels: ["✨ Enhancement", "🔍 Triage"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> **This issue template is exclusively for proposing new features, services, or improvements with an attached bounty.** Enhancements without a bounty can be discussed in the appropriate category of [Github Discussions](https://github.com/coollabsio/coolify/discussions).
|
||||||
|
|
||||||
|
# 💎 Add a Bounty (with [algora.io](https://console.algora.io/org/coollabsio/bounties/new))
|
||||||
|
- [Click here to add the required bounty](https://console.algora.io/org/coollabsio/bounties/new)
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: Request Type
|
||||||
|
description: Select the type of request you are making.
|
||||||
|
options:
|
||||||
|
- New Feature
|
||||||
|
- New Service
|
||||||
|
- Improvement
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Description
|
||||||
|
description: Provide a detailed description of the feature, improvement, or service you are proposing.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
20
.github/ISSUE_TEMPLATE/config.yml
vendored
20
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,8 +1,18 @@
|
|||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
|
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: 🤔 Community Support (Chat)
|
- name: 🤔 Questions and Community Support
|
||||||
url: https://coollabs.io/discord
|
url: https://coollabs.io/discord
|
||||||
about: Reach out to us on Discord.
|
about: If you have any questions, reach out to us on Discord inside the "#support" channel.
|
||||||
- name: 🙋♂️ Feature Requests
|
|
||||||
url: https://github.com/coollabsio/coolify/discussions/categories/new-features
|
- name: 💡 Feature Request
|
||||||
about: All feature requests will be discussed here.
|
url: https://github.com/coollabsio/coolify/discussions/categories/feature-requests
|
||||||
|
about: Suggest a new feature for Coolify.
|
||||||
|
|
||||||
|
- name: ⚙️ Service Request
|
||||||
|
url: https://github.com/coollabsio/coolify/discussions/categories/service-requests
|
||||||
|
about: Request a new service integration for Coolify.
|
||||||
|
|
||||||
|
- name: 🔧 Improvements
|
||||||
|
url: https://github.com/coollabsio/coolify/discussions/categories/improvements
|
||||||
|
about: Suggest improvements to existing features for Coolify.
|
||||||
|
|||||||
14
.github/pull_request_template.md
vendored
14
.github/pull_request_template.md
vendored
@@ -1 +1,13 @@
|
|||||||
> Always use `next` branch as destination branch for PRs, not `main`
|
## Submit Checklist (REMOVE THIS SECTION BEFORE SUBMITTING)
|
||||||
|
- [ ] I have selected the `next` branch as the destination for my PR, not `main`.
|
||||||
|
- [ ] I have listed all changes in the `Changes` section.
|
||||||
|
- [ ] I have filled out the `Issues` section with the issue/discussion link(s) (if applicable).
|
||||||
|
- [ ] I have tested my changes.
|
||||||
|
- [ ] I have considered backwards compatibility.
|
||||||
|
- [ ] I have removed this checklist and any unused sections.
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
-
|
||||||
|
|
||||||
|
## Issues
|
||||||
|
- fix #
|
||||||
|
|||||||
2
.github/workflows/coolify-helper.yml
vendored
2
.github/workflows/coolify-helper.yml
vendored
@@ -2,7 +2,7 @@ name: Coolify Helper Image (v4)
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ "main", "next" ]
|
branches: [ "main" ]
|
||||||
paths:
|
paths:
|
||||||
- .github/workflows/coolify-helper.yml
|
- .github/workflows/coolify-helper.yml
|
||||||
- docker/coolify-helper/Dockerfile
|
- docker/coolify-helper/Dockerfile
|
||||||
|
|||||||
103
.github/workflows/coolify-realtime.yml
vendored
Normal file
103
.github/workflows/coolify-realtime.yml
vendored
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
name: Coolify Realtime (v4)
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "main", "next" ]
|
||||||
|
paths:
|
||||||
|
- .github/workflows/coolify-realtime.yml
|
||||||
|
- docker/coolify-realtime/Dockerfile
|
||||||
|
- docker/coolify-realtime/terminal-server.js
|
||||||
|
- docker/coolify-realtime/package.json
|
||||||
|
- docker/coolify-realtime/soketi-entrypoint.sh
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: ghcr.io
|
||||||
|
IMAGE_NAME: "coollabsio/coolify-realtime"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
amd64:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Login to ghcr.io
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Get Version
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT
|
||||||
|
- name: Build image and push to registry
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
no-cache: true
|
||||||
|
context: .
|
||||||
|
file: docker/coolify-realtime/Dockerfile
|
||||||
|
platforms: linux/amd64
|
||||||
|
push: true
|
||||||
|
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}
|
||||||
|
labels: |
|
||||||
|
coolify.managed=true
|
||||||
|
aarch64:
|
||||||
|
runs-on: [ self-hosted, arm64 ]
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Login to ghcr.io
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Get Version
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT
|
||||||
|
- name: Build image and push to registry
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
no-cache: true
|
||||||
|
context: .
|
||||||
|
file: docker/coolify-realtime/Dockerfile
|
||||||
|
platforms: linux/aarch64
|
||||||
|
push: true
|
||||||
|
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64
|
||||||
|
labels: |
|
||||||
|
coolify.managed=true
|
||||||
|
merge-manifest:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
needs: [ amd64, aarch64 ]
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
- name: Login to ghcr.io
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Get Version
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT
|
||||||
|
- name: Create & publish manifest
|
||||||
|
run: |
|
||||||
|
docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||||
|
- uses: sarisia/actions-status-discord@v1
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
webhook: ${{ secrets.DISCORD_WEBHOOK_PROD_RELEASE_CHANNEL }}
|
||||||
78
.github/workflows/remove-labels-and-assignees-on-close.yml
vendored
Normal file
78
.github/workflows/remove-labels-and-assignees-on-close.yml
vendored
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
name: Remove Labels and Assignees on Issue Close
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [closed]
|
||||||
|
pull_request:
|
||||||
|
types: [closed]
|
||||||
|
pull_request_target:
|
||||||
|
types: [closed]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
remove-labels-and-assignees:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Remove labels and assignees
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
script: |
|
||||||
|
const { owner, repo } = context.repo;
|
||||||
|
|
||||||
|
async function processIssue(issueNumber) {
|
||||||
|
try {
|
||||||
|
const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issue_number: issueNumber
|
||||||
|
});
|
||||||
|
|
||||||
|
const labelsToKeep = currentLabels
|
||||||
|
.filter(label => label.name === '⏱︎ Stale')
|
||||||
|
.map(label => label.name);
|
||||||
|
|
||||||
|
await github.rest.issues.setLabels({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issue_number: issueNumber,
|
||||||
|
labels: labelsToKeep
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: issue } = await github.rest.issues.get({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issue_number: issueNumber
|
||||||
|
});
|
||||||
|
|
||||||
|
if (issue.assignees && issue.assignees.length > 0) {
|
||||||
|
await github.rest.issues.removeAssignees({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issue_number: issueNumber,
|
||||||
|
assignees: issue.assignees.map(assignee => assignee.login)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error.status !== 404) {
|
||||||
|
console.error(`Error processing issue ${issueNumber}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.eventName === 'issues' || context.eventName === 'pull_request' || context.eventName === 'pull_request_target') {
|
||||||
|
const issue = context.payload.issue || context.payload.pull_request;
|
||||||
|
await processIssue(issue.number);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.eventName === 'pull_request' || context.eventName === 'pull_request_target') {
|
||||||
|
const pr = context.payload.pull_request;
|
||||||
|
if (pr.body) {
|
||||||
|
const issueReferences = pr.body.match(/#(\d+)/g);
|
||||||
|
if (issueReferences) {
|
||||||
|
for (const reference of issueReferences) {
|
||||||
|
const issueNumber = parseInt(reference.substring(1));
|
||||||
|
await processIssue(issueNumber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,15 +6,19 @@ You can ask for guidance anytime on our [Discord server](https://coollabs.io/dis
|
|||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
1. [Setup Development Environment](#1-setup-development-environment)
|
- [Contributing to Coolify](#contributing-to-coolify)
|
||||||
2. [Verify Installation](#2-verify-installation-optional)
|
- [Table of Contents](#table-of-contents)
|
||||||
3. [Fork and Setup Local Repository](#3-fork-and-setup-local-repository)
|
- [1. Setup Development Environment](#1-setup-development-environment)
|
||||||
4. [Set up Environment Variables](#4-set-up-environment-variables)
|
- [2. Verify Installation (Optional)](#2-verify-installation-optional)
|
||||||
5. [Start Coolify](#5-start-coolify)
|
- [3. Fork and Setup Local Repository](#3-fork-and-setup-local-repository)
|
||||||
6. [Start Development](#6-start-development)
|
- [4. Set up Environment Variables](#4-set-up-environment-variables)
|
||||||
7. [Development Notes](#7-development-notes)
|
- [5. Start Coolify](#5-start-coolify)
|
||||||
8. [Create a Pull Request](#8-create-a-pull-request)
|
- [6. Start Development](#6-start-development)
|
||||||
9. [Additional Contribution Guidelines](#additional-contribution-guidelines)
|
- [7. Development Notes](#7-development-notes)
|
||||||
|
- [8. Create a Pull Request](#8-create-a-pull-request)
|
||||||
|
- [Additional Contribution Guidelines](#additional-contribution-guidelines)
|
||||||
|
- [Contributing a New Service](#contributing-a-new-service)
|
||||||
|
- [Contributing to Documentation](#contributing-to-documentation)
|
||||||
|
|
||||||
## 1. Setup Development Environment
|
## 1. Setup Development Environment
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Actions\Application;
|
namespace App\Actions\Application;
|
||||||
|
|
||||||
|
use App\Actions\Server\CleanupDocker;
|
||||||
use App\Models\Application;
|
use App\Models\Application;
|
||||||
use Lorisleiva\Actions\Concerns\AsAction;
|
use Lorisleiva\Actions\Concerns\AsAction;
|
||||||
|
|
||||||
@@ -9,44 +10,35 @@ class StopApplication
|
|||||||
{
|
{
|
||||||
use AsAction;
|
use AsAction;
|
||||||
|
|
||||||
public function handle(Application $application, bool $previewDeployments = false)
|
public function handle(Application $application, bool $previewDeployments = false, bool $dockerCleanup = true)
|
||||||
{
|
{
|
||||||
if ($application->destination->server->isSwarm()) {
|
try {
|
||||||
instant_remote_process(["docker stack rm {$application->uuid}"], $application->destination->server);
|
$server = $application->destination->server;
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$servers = collect([]);
|
|
||||||
$servers->push($application->destination->server);
|
|
||||||
$application->additional_servers->map(function ($server) use ($servers) {
|
|
||||||
$servers->push($server);
|
|
||||||
});
|
|
||||||
foreach ($servers as $server) {
|
|
||||||
if (! $server->isFunctional()) {
|
if (! $server->isFunctional()) {
|
||||||
return 'Server is not functional';
|
return 'Server is not functional';
|
||||||
}
|
}
|
||||||
if ($previewDeployments) {
|
ray('Stopping application: '.$application->name);
|
||||||
$containers = getCurrentApplicationContainerStatus($server, $application->id, includePullrequests: true);
|
|
||||||
} else {
|
if ($server->isSwarm()) {
|
||||||
$containers = getCurrentApplicationContainerStatus($server, $application->id, 0);
|
instant_remote_process(["docker stack rm {$application->uuid}"], $server);
|
||||||
}
|
|
||||||
if ($containers->count() > 0) {
|
return;
|
||||||
foreach ($containers as $container) {
|
|
||||||
$containerName = data_get($container, 'Names');
|
|
||||||
if ($containerName) {
|
|
||||||
instant_remote_process(command: ["docker stop --time=30 $containerName"], server: $server, throwError: false);
|
|
||||||
instant_remote_process(command: ["docker rm $containerName"], server: $server, throwError: false);
|
|
||||||
instant_remote_process(command: ["docker rm -f {$containerName}"], server: $server, throwError: false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$containersToStop = $application->getContainersToStop($previewDeployments);
|
||||||
|
$application->stopContainers($containersToStop, $server);
|
||||||
|
|
||||||
if ($application->build_pack === 'dockercompose') {
|
if ($application->build_pack === 'dockercompose') {
|
||||||
// remove network
|
$application->delete_connected_networks($application->uuid);
|
||||||
$uuid = $application->uuid;
|
|
||||||
instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false);
|
|
||||||
instant_remote_process(["docker network rm {$uuid}"], $server, false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($dockerCleanup) {
|
||||||
|
CleanupDocker::dispatch($server, true);
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
ray($e->getMessage());
|
||||||
|
|
||||||
|
return $e->getMessage();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ namespace App\Actions\CoolifyTask;
|
|||||||
|
|
||||||
use App\Enums\ActivityTypes;
|
use App\Enums\ActivityTypes;
|
||||||
use App\Enums\ProcessStatus;
|
use App\Enums\ProcessStatus;
|
||||||
|
use App\Helpers\SshMultiplexingHelper;
|
||||||
use App\Jobs\ApplicationDeploymentJob;
|
use App\Jobs\ApplicationDeploymentJob;
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
use Illuminate\Process\ProcessResult;
|
use Illuminate\Process\ProcessResult;
|
||||||
@@ -137,7 +138,7 @@ class RunRemoteProcess
|
|||||||
$command = $this->activity->getExtraProperty('command');
|
$command = $this->activity->getExtraProperty('command');
|
||||||
$server = Server::whereUuid($server_uuid)->firstOrFail();
|
$server = Server::whereUuid($server_uuid)->firstOrFail();
|
||||||
|
|
||||||
return generateSshCommand($server, $command);
|
return SshMultiplexingHelper::generateSshCommand($server, $command);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function handleOutput(string $type, string $output)
|
protected function handleOutput(string $type, string $output)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class StartDragonfly
|
|||||||
$startCommand = "dragonfly --requirepass {$this->database->dragonfly_password}";
|
$startCommand = "dragonfly --requirepass {$this->database->dragonfly_password}";
|
||||||
|
|
||||||
$container_name = $this->database->uuid;
|
$container_name = $this->database->uuid;
|
||||||
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
|
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
||||||
|
|
||||||
$this->commands = [
|
$this->commands = [
|
||||||
"echo 'Starting {$database->name}.'",
|
"echo 'Starting {$database->name}.'",
|
||||||
@@ -75,7 +75,7 @@ class StartDragonfly
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
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);
|
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
|
||||||
}
|
}
|
||||||
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
||||||
@@ -118,10 +118,10 @@ class StartDragonfly
|
|||||||
$local_persistent_volumes = [];
|
$local_persistent_volumes = [];
|
||||||
foreach ($this->database->persistentStorages as $persistentStorage) {
|
foreach ($this->database->persistentStorages as $persistentStorage) {
|
||||||
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
|
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
|
||||||
$local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path;
|
$local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path;
|
||||||
} else {
|
} else {
|
||||||
$volume_name = $persistentStorage->name;
|
$volume_name = $persistentStorage->name;
|
||||||
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
|
$local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,7 +152,7 @@ class StartDragonfly
|
|||||||
$environment_variables->push("$env->key=$env->real_value");
|
$environment_variables->push("$env->key=$env->real_value");
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($environment_variables->filter(fn($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) {
|
if ($environment_variables->filter(fn ($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) {
|
||||||
$environment_variables->push("REDIS_PASSWORD={$this->database->dragonfly_password}");
|
$environment_variables->push("REDIS_PASSWORD={$this->database->dragonfly_password}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class StartKeydb
|
|||||||
$startCommand = "keydb-server --requirepass {$this->database->keydb_password} --appendonly yes";
|
$startCommand = "keydb-server --requirepass {$this->database->keydb_password} --appendonly yes";
|
||||||
|
|
||||||
$container_name = $this->database->uuid;
|
$container_name = $this->database->uuid;
|
||||||
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
|
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
||||||
|
|
||||||
$this->commands = [
|
$this->commands = [
|
||||||
"echo 'Starting {$database->name}.'",
|
"echo 'Starting {$database->name}.'",
|
||||||
@@ -74,7 +74,7 @@ class StartKeydb
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
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);
|
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
|
||||||
}
|
}
|
||||||
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
||||||
@@ -94,10 +94,10 @@ class StartKeydb
|
|||||||
if (count($volume_names) > 0) {
|
if (count($volume_names) > 0) {
|
||||||
$docker_compose['volumes'] = $volume_names;
|
$docker_compose['volumes'] = $volume_names;
|
||||||
}
|
}
|
||||||
if (!is_null($this->database->keydb_conf) || !empty($this->database->keydb_conf)) {
|
if (! is_null($this->database->keydb_conf) || ! empty($this->database->keydb_conf)) {
|
||||||
$docker_compose['services'][$container_name]['volumes'][] = [
|
$docker_compose['services'][$container_name]['volumes'][] = [
|
||||||
'type' => 'bind',
|
'type' => 'bind',
|
||||||
'source' => $this->configuration_dir . '/keydb.conf',
|
'source' => $this->configuration_dir.'/keydb.conf',
|
||||||
'target' => '/etc/keydb/keydb.conf',
|
'target' => '/etc/keydb/keydb.conf',
|
||||||
'read_only' => true,
|
'read_only' => true,
|
||||||
];
|
];
|
||||||
@@ -125,10 +125,10 @@ class StartKeydb
|
|||||||
$local_persistent_volumes = [];
|
$local_persistent_volumes = [];
|
||||||
foreach ($this->database->persistentStorages as $persistentStorage) {
|
foreach ($this->database->persistentStorages as $persistentStorage) {
|
||||||
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
|
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
|
||||||
$local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path;
|
$local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path;
|
||||||
} else {
|
} else {
|
||||||
$volume_name = $persistentStorage->name;
|
$volume_name = $persistentStorage->name;
|
||||||
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
|
$local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,7 +159,7 @@ class StartKeydb
|
|||||||
$environment_variables->push("$env->key=$env->real_value");
|
$environment_variables->push("$env->key=$env->real_value");
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($environment_variables->filter(fn($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) {
|
if ($environment_variables->filter(fn ($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) {
|
||||||
$environment_variables->push("REDIS_PASSWORD={$this->database->keydb_password}");
|
$environment_variables->push("REDIS_PASSWORD={$this->database->keydb_password}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class StartMariadb
|
|||||||
$this->database = $database;
|
$this->database = $database;
|
||||||
|
|
||||||
$container_name = $this->database->uuid;
|
$container_name = $this->database->uuid;
|
||||||
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
|
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
||||||
|
|
||||||
$this->commands = [
|
$this->commands = [
|
||||||
"echo 'Starting {$database->name}.'",
|
"echo 'Starting {$database->name}.'",
|
||||||
@@ -69,7 +69,7 @@ class StartMariadb
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
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);
|
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
|
||||||
}
|
}
|
||||||
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
||||||
@@ -89,10 +89,10 @@ class StartMariadb
|
|||||||
if (count($volume_names) > 0) {
|
if (count($volume_names) > 0) {
|
||||||
$docker_compose['volumes'] = $volume_names;
|
$docker_compose['volumes'] = $volume_names;
|
||||||
}
|
}
|
||||||
if (!is_null($this->database->mariadb_conf) || !empty($this->database->mariadb_conf)) {
|
if (! is_null($this->database->mariadb_conf) || ! empty($this->database->mariadb_conf)) {
|
||||||
$docker_compose['services'][$container_name]['volumes'][] = [
|
$docker_compose['services'][$container_name]['volumes'][] = [
|
||||||
'type' => 'bind',
|
'type' => 'bind',
|
||||||
'source' => $this->configuration_dir . '/custom-config.cnf',
|
'source' => $this->configuration_dir.'/custom-config.cnf',
|
||||||
'target' => '/etc/mysql/conf.d/custom-config.cnf',
|
'target' => '/etc/mysql/conf.d/custom-config.cnf',
|
||||||
'read_only' => true,
|
'read_only' => true,
|
||||||
];
|
];
|
||||||
@@ -120,10 +120,10 @@ class StartMariadb
|
|||||||
$local_persistent_volumes = [];
|
$local_persistent_volumes = [];
|
||||||
foreach ($this->database->persistentStorages as $persistentStorage) {
|
foreach ($this->database->persistentStorages as $persistentStorage) {
|
||||||
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
|
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
|
||||||
$local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path;
|
$local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path;
|
||||||
} else {
|
} else {
|
||||||
$volume_name = $persistentStorage->name;
|
$volume_name = $persistentStorage->name;
|
||||||
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
|
$local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,18 +154,18 @@ class StartMariadb
|
|||||||
$environment_variables->push("$env->key=$env->real_value");
|
$environment_variables->push("$env->key=$env->real_value");
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($environment_variables->filter(fn($env) => str($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}");
|
$environment_variables->push("MARIADB_ROOT_PASSWORD={$this->database->mariadb_root_password}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($environment_variables->filter(fn($env) => str($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}");
|
$environment_variables->push("MARIADB_DATABASE={$this->database->mariadb_database}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($environment_variables->filter(fn($env) => str($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}");
|
$environment_variables->push("MARIADB_USER={$this->database->mariadb_user}");
|
||||||
}
|
}
|
||||||
if ($environment_variables->filter(fn($env) => str($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}");
|
$environment_variables->push("MARIADB_PASSWORD={$this->database->mariadb_password}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class StartMongodb
|
|||||||
$startCommand = 'mongod';
|
$startCommand = 'mongod';
|
||||||
|
|
||||||
$container_name = $this->database->uuid;
|
$container_name = $this->database->uuid;
|
||||||
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
|
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
||||||
|
|
||||||
$this->commands = [
|
$this->commands = [
|
||||||
"echo 'Starting {$database->name}.'",
|
"echo 'Starting {$database->name}.'",
|
||||||
@@ -77,7 +77,7 @@ class StartMongodb
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
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);
|
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
|
||||||
}
|
}
|
||||||
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
||||||
@@ -97,19 +97,19 @@ class StartMongodb
|
|||||||
if (count($volume_names) > 0) {
|
if (count($volume_names) > 0) {
|
||||||
$docker_compose['volumes'] = $volume_names;
|
$docker_compose['volumes'] = $volume_names;
|
||||||
}
|
}
|
||||||
if (!is_null($this->database->mongo_conf) || !empty($this->database->mongo_conf)) {
|
if (! is_null($this->database->mongo_conf) || ! empty($this->database->mongo_conf)) {
|
||||||
$docker_compose['services'][$container_name]['volumes'][] = [
|
$docker_compose['services'][$container_name]['volumes'][] = [
|
||||||
'type' => 'bind',
|
'type' => 'bind',
|
||||||
'source' => $this->configuration_dir . '/mongod.conf',
|
'source' => $this->configuration_dir.'/mongod.conf',
|
||||||
'target' => '/etc/mongo/mongod.conf',
|
'target' => '/etc/mongo/mongod.conf',
|
||||||
'read_only' => true,
|
'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();
|
$this->add_default_database();
|
||||||
$docker_compose['services'][$container_name]['volumes'][] = [
|
$docker_compose['services'][$container_name]['volumes'][] = [
|
||||||
'type' => 'bind',
|
'type' => 'bind',
|
||||||
'source' => $this->configuration_dir . '/docker-entrypoint-initdb.d',
|
'source' => $this->configuration_dir.'/docker-entrypoint-initdb.d',
|
||||||
'target' => '/docker-entrypoint-initdb.d',
|
'target' => '/docker-entrypoint-initdb.d',
|
||||||
'read_only' => true,
|
'read_only' => true,
|
||||||
];
|
];
|
||||||
@@ -136,10 +136,10 @@ class StartMongodb
|
|||||||
$local_persistent_volumes = [];
|
$local_persistent_volumes = [];
|
||||||
foreach ($this->database->persistentStorages as $persistentStorage) {
|
foreach ($this->database->persistentStorages as $persistentStorage) {
|
||||||
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
|
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
|
||||||
$local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path;
|
$local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path;
|
||||||
} else {
|
} else {
|
||||||
$volume_name = $persistentStorage->name;
|
$volume_name = $persistentStorage->name;
|
||||||
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
|
$local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,15 +170,15 @@ class StartMongodb
|
|||||||
$environment_variables->push("$env->key=$env->real_value");
|
$environment_variables->push("$env->key=$env->real_value");
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($environment_variables->filter(fn($env) => str($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}");
|
$environment_variables->push("MONGO_INITDB_ROOT_USERNAME={$this->database->mongo_initdb_root_username}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($environment_variables->filter(fn($env) => str($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}");
|
$environment_variables->push("MONGO_INITDB_ROOT_PASSWORD={$this->database->mongo_initdb_root_password}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($environment_variables->filter(fn($env) => str($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}");
|
$environment_variables->push("MONGO_INITDB_DATABASE={$this->database->mongo_initdb_database}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class StartMysql
|
|||||||
$this->database = $database;
|
$this->database = $database;
|
||||||
|
|
||||||
$container_name = $this->database->uuid;
|
$container_name = $this->database->uuid;
|
||||||
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
|
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
||||||
|
|
||||||
$this->commands = [
|
$this->commands = [
|
||||||
"echo 'Starting {$database->name}.'",
|
"echo 'Starting {$database->name}.'",
|
||||||
@@ -69,7 +69,7 @@ class StartMysql
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
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);
|
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
|
||||||
}
|
}
|
||||||
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
||||||
@@ -89,10 +89,10 @@ class StartMysql
|
|||||||
if (count($volume_names) > 0) {
|
if (count($volume_names) > 0) {
|
||||||
$docker_compose['volumes'] = $volume_names;
|
$docker_compose['volumes'] = $volume_names;
|
||||||
}
|
}
|
||||||
if (!is_null($this->database->mysql_conf) || !empty($this->database->mysql_conf)) {
|
if (! is_null($this->database->mysql_conf) || ! empty($this->database->mysql_conf)) {
|
||||||
$docker_compose['services'][$container_name]['volumes'][] = [
|
$docker_compose['services'][$container_name]['volumes'][] = [
|
||||||
'type' => 'bind',
|
'type' => 'bind',
|
||||||
'source' => $this->configuration_dir . '/custom-config.cnf',
|
'source' => $this->configuration_dir.'/custom-config.cnf',
|
||||||
'target' => '/etc/mysql/conf.d/custom-config.cnf',
|
'target' => '/etc/mysql/conf.d/custom-config.cnf',
|
||||||
'read_only' => true,
|
'read_only' => true,
|
||||||
];
|
];
|
||||||
@@ -120,10 +120,10 @@ class StartMysql
|
|||||||
$local_persistent_volumes = [];
|
$local_persistent_volumes = [];
|
||||||
foreach ($this->database->persistentStorages as $persistentStorage) {
|
foreach ($this->database->persistentStorages as $persistentStorage) {
|
||||||
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
|
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
|
||||||
$local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path;
|
$local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path;
|
||||||
} else {
|
} else {
|
||||||
$volume_name = $persistentStorage->name;
|
$volume_name = $persistentStorage->name;
|
||||||
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
|
$local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,18 +154,18 @@ class StartMysql
|
|||||||
$environment_variables->push("$env->key=$env->real_value");
|
$environment_variables->push("$env->key=$env->real_value");
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($environment_variables->filter(fn($env) => str($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}");
|
$environment_variables->push("MYSQL_ROOT_PASSWORD={$this->database->mysql_root_password}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($environment_variables->filter(fn($env) => str($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}");
|
$environment_variables->push("MYSQL_DATABASE={$this->database->mysql_database}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($environment_variables->filter(fn($env) => str($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}");
|
$environment_variables->push("MYSQL_USER={$this->database->mysql_user}");
|
||||||
}
|
}
|
||||||
if ($environment_variables->filter(fn($env) => str($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}");
|
$environment_variables->push("MYSQL_PASSWORD={$this->database->mysql_password}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ class StartPostgresql
|
|||||||
$this->generate_init_scripts();
|
$this->generate_init_scripts();
|
||||||
$this->add_custom_conf();
|
$this->add_custom_conf();
|
||||||
|
|
||||||
|
|
||||||
$docker_compose = [
|
$docker_compose = [
|
||||||
'services' => [
|
'services' => [
|
||||||
$container_name => [
|
$container_name => [
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class StartRedis
|
|||||||
$startCommand = "redis-server --requirepass {$this->database->redis_password} --appendonly yes";
|
$startCommand = "redis-server --requirepass {$this->database->redis_password} --appendonly yes";
|
||||||
|
|
||||||
$container_name = $this->database->uuid;
|
$container_name = $this->database->uuid;
|
||||||
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
|
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
||||||
|
|
||||||
$this->commands = [
|
$this->commands = [
|
||||||
"echo 'Starting {$database->name}.'",
|
"echo 'Starting {$database->name}.'",
|
||||||
@@ -78,7 +78,7 @@ class StartRedis
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
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);
|
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
|
||||||
}
|
}
|
||||||
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
|
||||||
@@ -98,10 +98,10 @@ class StartRedis
|
|||||||
if (count($volume_names) > 0) {
|
if (count($volume_names) > 0) {
|
||||||
$docker_compose['volumes'] = $volume_names;
|
$docker_compose['volumes'] = $volume_names;
|
||||||
}
|
}
|
||||||
if (!is_null($this->database->redis_conf) || !empty($this->database->redis_conf)) {
|
if (! is_null($this->database->redis_conf) || ! empty($this->database->redis_conf)) {
|
||||||
$docker_compose['services'][$container_name]['volumes'][] = [
|
$docker_compose['services'][$container_name]['volumes'][] = [
|
||||||
'type' => 'bind',
|
'type' => 'bind',
|
||||||
'source' => $this->configuration_dir . '/redis.conf',
|
'source' => $this->configuration_dir.'/redis.conf',
|
||||||
'target' => '/usr/local/etc/redis/redis.conf',
|
'target' => '/usr/local/etc/redis/redis.conf',
|
||||||
'read_only' => true,
|
'read_only' => true,
|
||||||
];
|
];
|
||||||
@@ -130,10 +130,10 @@ class StartRedis
|
|||||||
$local_persistent_volumes = [];
|
$local_persistent_volumes = [];
|
||||||
foreach ($this->database->persistentStorages as $persistentStorage) {
|
foreach ($this->database->persistentStorages as $persistentStorage) {
|
||||||
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
|
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
|
||||||
$local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path;
|
$local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path;
|
||||||
} else {
|
} else {
|
||||||
$volume_name = $persistentStorage->name;
|
$volume_name = $persistentStorage->name;
|
||||||
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
|
$local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,7 +164,7 @@ class StartRedis
|
|||||||
$environment_variables->push("$env->key=$env->real_value");
|
$environment_variables->push("$env->key=$env->real_value");
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($environment_variables->filter(fn($env) => str($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}");
|
$environment_variables->push("REDIS_PASSWORD={$this->database->redis_password}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Actions\Database;
|
namespace App\Actions\Database;
|
||||||
|
|
||||||
|
use App\Actions\Server\CleanupDocker;
|
||||||
use App\Models\StandaloneClickhouse;
|
use App\Models\StandaloneClickhouse;
|
||||||
use App\Models\StandaloneDragonfly;
|
use App\Models\StandaloneDragonfly;
|
||||||
use App\Models\StandaloneKeydb;
|
use App\Models\StandaloneKeydb;
|
||||||
@@ -10,25 +11,65 @@ use App\Models\StandaloneMongodb;
|
|||||||
use App\Models\StandaloneMysql;
|
use App\Models\StandaloneMysql;
|
||||||
use App\Models\StandalonePostgresql;
|
use App\Models\StandalonePostgresql;
|
||||||
use App\Models\StandaloneRedis;
|
use App\Models\StandaloneRedis;
|
||||||
|
use Illuminate\Support\Facades\Process;
|
||||||
use Lorisleiva\Actions\Concerns\AsAction;
|
use Lorisleiva\Actions\Concerns\AsAction;
|
||||||
|
|
||||||
class StopDatabase
|
class StopDatabase
|
||||||
{
|
{
|
||||||
use AsAction;
|
use AsAction;
|
||||||
|
|
||||||
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $database)
|
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $database, bool $isDeleteOperation = false, bool $dockerCleanup = true)
|
||||||
{
|
{
|
||||||
$server = $database->destination->server;
|
$server = $database->destination->server;
|
||||||
if (! $server->isFunctional()) {
|
if (! $server->isFunctional()) {
|
||||||
return 'Server is not functional';
|
return 'Server is not functional';
|
||||||
}
|
}
|
||||||
|
|
||||||
instant_remote_process(command: ["docker stop --time=30 $database->uuid"], server: $server, throwError: false);
|
$this->stopContainer($database, $database->uuid, 300);
|
||||||
instant_remote_process(command: ["docker rm $database->uuid"], server: $server, throwError: false);
|
if (! $isDeleteOperation) {
|
||||||
instant_remote_process(command: ["docker rm -f $database->uuid"], server: $server, throwError: false);
|
if ($dockerCleanup) {
|
||||||
|
CleanupDocker::dispatch($server, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ($database->is_public) {
|
if ($database->is_public) {
|
||||||
StopDatabaseProxy::run($database);
|
StopDatabaseProxy::run($database);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return 'Database stopped successfully';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function stopContainer($database, string $containerName, int $timeout = 300): void
|
||||||
|
{
|
||||||
|
$server = $database->destination->server;
|
||||||
|
|
||||||
|
$process = Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
|
||||||
|
|
||||||
|
$startTime = time();
|
||||||
|
while ($process->running()) {
|
||||||
|
if (time() - $startTime >= $timeout) {
|
||||||
|
$this->forceStopContainer($containerName, $server);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
usleep(100000);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->removeContainer($containerName, $server);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function forceStopContainer(string $containerName, $server): void
|
||||||
|
{
|
||||||
|
instant_remote_process(command: ["docker kill $containerName"], server: $server, throwError: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function removeContainer(string $containerName, $server): void
|
||||||
|
{
|
||||||
|
instant_remote_process(command: ["docker rm -f $containerName"], server: $server, throwError: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function deleteConnectedNetworks($uuid, $server)
|
||||||
|
{
|
||||||
|
instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false);
|
||||||
|
instant_remote_process(["docker network rm {$uuid}"], $server, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -543,7 +543,7 @@ class GetContainersStatus
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$exitedServices = $exitedServices->unique('id');
|
$exitedServices = $exitedServices->unique('uuid');
|
||||||
foreach ($exitedServices as $exitedService) {
|
foreach ($exitedServices as $exitedService) {
|
||||||
if (str($exitedService->status)->startsWith('exited')) {
|
if (str($exitedService->status)->startsWith('exited')) {
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class CheckProxy
|
|||||||
if (is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop) {
|
if (is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
['uptime' => $uptime, 'error' => $error] = $server->validateConnection();
|
['uptime' => $uptime, 'error' => $error] = $server->validateConnection(false);
|
||||||
if (! $uptime) {
|
if (! $uptime) {
|
||||||
throw new \Exception($error);
|
throw new \Exception($error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,8 @@ class StartProxy
|
|||||||
"echo 'Pulling docker image.'",
|
"echo 'Pulling docker image.'",
|
||||||
'docker compose pull',
|
'docker compose pull',
|
||||||
"echo 'Stopping existing coolify-proxy.'",
|
"echo 'Stopping existing coolify-proxy.'",
|
||||||
'docker compose down -v --remove-orphans > /dev/null 2>&1',
|
'docker stop -t 10 coolify-proxy || true',
|
||||||
|
'docker rm coolify-proxy || true',
|
||||||
"echo 'Starting coolify-proxy.'",
|
"echo 'Starting coolify-proxy.'",
|
||||||
'docker compose up -d --remove-orphans',
|
'docker compose up -d --remove-orphans',
|
||||||
"echo 'Proxy started successfully.'",
|
"echo 'Proxy started successfully.'",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Actions\Server;
|
namespace App\Actions\Server;
|
||||||
|
|
||||||
|
use App\Events\CloudflareTunnelConfigured;
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
use Lorisleiva\Actions\Concerns\AsAction;
|
use Lorisleiva\Actions\Concerns\AsAction;
|
||||||
use Symfony\Component\Yaml\Yaml;
|
use Symfony\Component\Yaml\Yaml;
|
||||||
@@ -40,12 +41,17 @@ class ConfigureCloudflared
|
|||||||
instant_remote_process($commands, $server);
|
instant_remote_process($commands, $server);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
ray($e);
|
ray($e);
|
||||||
|
$server->settings->is_cloudflare_tunnel = false;
|
||||||
|
$server->settings->save();
|
||||||
throw $e;
|
throw $e;
|
||||||
} finally {
|
} finally {
|
||||||
|
CloudflareTunnelConfigured::dispatch($server->team_id);
|
||||||
|
|
||||||
$commands = collect([
|
$commands = collect([
|
||||||
'rm -fr /tmp/cloudflared',
|
'rm -fr /tmp/cloudflared',
|
||||||
]);
|
]);
|
||||||
instant_remote_process($commands, $server);
|
instant_remote_process($commands, $server);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Actions\Server;
|
namespace App\Actions\Server;
|
||||||
|
|
||||||
|
use App\Jobs\PullHelperImageJob;
|
||||||
use App\Models\InstanceSettings;
|
use App\Models\InstanceSettings;
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
use Lorisleiva\Actions\Concerns\AsAction;
|
use Lorisleiva\Actions\Concerns\AsAction;
|
||||||
@@ -55,6 +56,13 @@ class UpdateCoolify
|
|||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$all_servers = Server::all();
|
||||||
|
$servers = $all_servers->where('settings.is_usable', true)->where('settings.is_reachable', true)->where('ip', '!=', '1.2.3.4');
|
||||||
|
foreach ($servers as $server) {
|
||||||
|
PullHelperImageJob::dispatch($server);
|
||||||
|
}
|
||||||
|
|
||||||
instant_remote_process(["docker pull -q ghcr.io/coollabsio/coolify:{$this->latestVersion}"], $this->server, false);
|
instant_remote_process(["docker pull -q ghcr.io/coollabsio/coolify:{$this->latestVersion}"], $this->server, false);
|
||||||
|
|
||||||
remote_process([
|
remote_process([
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Actions\Service;
|
namespace App\Actions\Service;
|
||||||
|
|
||||||
|
use App\Actions\Server\CleanupDocker;
|
||||||
use App\Models\Service;
|
use App\Models\Service;
|
||||||
use Lorisleiva\Actions\Concerns\AsAction;
|
use Lorisleiva\Actions\Concerns\AsAction;
|
||||||
|
|
||||||
@@ -9,11 +10,11 @@ class DeleteService
|
|||||||
{
|
{
|
||||||
use AsAction;
|
use AsAction;
|
||||||
|
|
||||||
public function handle(Service $service)
|
public function handle(Service $service, bool $deleteConfigurations, bool $deleteVolumes, bool $dockerCleanup, bool $deleteConnectedNetworks)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$server = data_get($service, 'server');
|
$server = data_get($service, 'server');
|
||||||
if ($server->isFunctional()) {
|
if ($deleteVolumes && $server->isFunctional()) {
|
||||||
$storagesToDelete = collect([]);
|
$storagesToDelete = collect([]);
|
||||||
|
|
||||||
$service->environment_variables()->delete();
|
$service->environment_variables()->delete();
|
||||||
@@ -33,13 +34,29 @@ class DeleteService
|
|||||||
foreach ($storagesToDelete as $storage) {
|
foreach ($storagesToDelete as $storage) {
|
||||||
$commands[] = "docker volume rm -f $storage->name";
|
$commands[] = "docker volume rm -f $storage->name";
|
||||||
}
|
}
|
||||||
$commands[] = "docker rm -f $service->uuid";
|
|
||||||
|
|
||||||
instant_remote_process($commands, $server, false);
|
// Execute volume deletion first, this must be done first otherwise volumes will not be deleted.
|
||||||
|
if (! empty($commands)) {
|
||||||
|
foreach ($commands as $command) {
|
||||||
|
$result = instant_remote_process([$command], $server, false);
|
||||||
|
if ($result !== 0) {
|
||||||
|
ray("Failed to execute: $command");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($deleteConnectedNetworks) {
|
||||||
|
$service->delete_connected_networks($service->uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
instant_remote_process(["docker rm -f $service->uuid"], $server, throwError: false);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
throw new \Exception($e->getMessage());
|
throw new \Exception($e->getMessage());
|
||||||
} finally {
|
} finally {
|
||||||
|
if ($deleteConfigurations) {
|
||||||
|
$service->delete_configurations();
|
||||||
|
}
|
||||||
foreach ($service->applications()->get() as $application) {
|
foreach ($service->applications()->get() as $application) {
|
||||||
$application->forceDelete();
|
$application->forceDelete();
|
||||||
}
|
}
|
||||||
@@ -50,6 +67,11 @@ class DeleteService
|
|||||||
$task->delete();
|
$task->delete();
|
||||||
}
|
}
|
||||||
$service->tags()->detach();
|
$service->tags()->detach();
|
||||||
|
$service->forceDelete();
|
||||||
|
|
||||||
|
if ($dockerCleanup) {
|
||||||
|
CleanupDocker::dispatch($server, true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class StartService
|
|||||||
$service->saveComposeConfigs();
|
$service->saveComposeConfigs();
|
||||||
$commands[] = 'cd '.$service->workdir();
|
$commands[] = 'cd '.$service->workdir();
|
||||||
$commands[] = "echo 'Saved configuration files to {$service->workdir()}.'";
|
$commands[] = "echo 'Saved configuration files to {$service->workdir()}.'";
|
||||||
if($service->networks()->count() > 0){
|
if ($service->networks()->count() > 0) {
|
||||||
$commands[] = "echo 'Creating Docker network.'";
|
$commands[] = "echo 'Creating Docker network.'";
|
||||||
$commands[] = "docker network inspect $service->uuid >/dev/null 2>&1 || docker network create --attachable $service->uuid";
|
$commands[] = "docker network inspect $service->uuid >/dev/null 2>&1 || docker network create --attachable $service->uuid";
|
||||||
}
|
}
|
||||||
@@ -31,7 +31,7 @@ class StartService
|
|||||||
$network = $service->destination->network;
|
$network = $service->destination->network;
|
||||||
$serviceNames = data_get(Yaml::parse($compose), 'services', []);
|
$serviceNames = data_get(Yaml::parse($compose), 'services', []);
|
||||||
foreach ($serviceNames as $serviceName => $serviceConfig) {
|
foreach ($serviceNames as $serviceName => $serviceConfig) {
|
||||||
$commands[] = "docker network connect --alias {$serviceName}-{$service->uuid} $network {$serviceName}-{$service->uuid} || true";
|
$commands[] = "docker network connect --alias {$serviceName}-{$service->uuid} $network {$serviceName}-{$service->uuid} >/dev/null 2>&1 || true";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$activity = remote_process($commands, $service->server, type_uuid: $service->uuid, callEventOnFinish: 'ServiceStatusChanged');
|
$activity = remote_process($commands, $service->server, type_uuid: $service->uuid, callEventOnFinish: 'ServiceStatusChanged');
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Actions\Service;
|
namespace App\Actions\Service;
|
||||||
|
|
||||||
|
use App\Actions\Server\CleanupDocker;
|
||||||
use App\Models\Service;
|
use App\Models\Service;
|
||||||
use Lorisleiva\Actions\Concerns\AsAction;
|
use Lorisleiva\Actions\Concerns\AsAction;
|
||||||
|
|
||||||
@@ -9,40 +10,27 @@ class StopService
|
|||||||
{
|
{
|
||||||
use AsAction;
|
use AsAction;
|
||||||
|
|
||||||
public function handle(Service $service)
|
public function handle(Service $service, bool $isDeleteOperation = false, bool $dockerCleanup = true)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$server = $service->destination->server;
|
$server = $service->destination->server;
|
||||||
if (! $server->isFunctional()) {
|
if (! $server->isFunctional()) {
|
||||||
return 'Server is not functional';
|
return 'Server is not functional';
|
||||||
}
|
}
|
||||||
ray('Stopping service: '.$service->name);
|
|
||||||
$applications = $service->applications()->get();
|
|
||||||
foreach ($applications as $application) {
|
|
||||||
if ($applications->count() < 6) {
|
|
||||||
instant_remote_process(command: ["docker stop --time=10 {$application->name}-{$service->uuid}"], server: $server, throwError: false);
|
|
||||||
}
|
|
||||||
instant_remote_process(command: ["docker rm {$application->name}-{$service->uuid}"], server: $server, throwError: false);
|
|
||||||
instant_remote_process(command: ["docker rm -f {$application->name}-{$service->uuid}"], server: $server, throwError: false);
|
|
||||||
$application->update(['status' => 'exited']);
|
|
||||||
}
|
|
||||||
$dbs = $service->databases()->get();
|
|
||||||
foreach ($dbs as $db) {
|
|
||||||
if ($dbs->count() < 6) {
|
|
||||||
|
|
||||||
instant_remote_process(command: ["docker stop --time=10 {$db->name}-{$service->uuid}"], server: $server, throwError: false);
|
$containersToStop = $service->getContainersToStop();
|
||||||
|
$service->stopContainers($containersToStop, $server);
|
||||||
|
|
||||||
|
if (! $isDeleteOperation) {
|
||||||
|
$service->delete_connected_networks($service->uuid);
|
||||||
|
if ($dockerCleanup) {
|
||||||
|
CleanupDocker::dispatch($server, true);
|
||||||
}
|
}
|
||||||
instant_remote_process(command: ["docker rm {$db->name}-{$service->uuid}"], server: $server, throwError: false);
|
|
||||||
instant_remote_process(command: ["docker rm -f {$db->name}-{$service->uuid}"], server: $server, throwError: false);
|
|
||||||
$db->update(['status' => 'exited']);
|
|
||||||
}
|
}
|
||||||
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) {
|
} catch (\Exception $e) {
|
||||||
ray($e->getMessage());
|
ray($e->getMessage());
|
||||||
|
|
||||||
return $e->getMessage();
|
return $e->getMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Console\Commands;
|
|
||||||
|
|
||||||
use Illuminate\Console\Command;
|
|
||||||
use Illuminate\Support\Facades\Redis;
|
|
||||||
|
|
||||||
class CleanupQueue extends Command
|
|
||||||
{
|
|
||||||
protected $signature = 'cleanup:queue';
|
|
||||||
|
|
||||||
protected $description = 'Cleanup Queue';
|
|
||||||
|
|
||||||
public function handle()
|
|
||||||
{
|
|
||||||
echo "Running queue cleanup...\n";
|
|
||||||
$prefix = config('database.redis.options.prefix');
|
|
||||||
$keys = Redis::connection()->keys('*:laravel*');
|
|
||||||
foreach ($keys as $key) {
|
|
||||||
$keyWithoutPrefix = str_replace($prefix, '', $key);
|
|
||||||
Redis::connection()->del($keyWithoutPrefix);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
31
app/Console/Commands/CleanupRedis.php
Normal file
31
app/Console/Commands/CleanupRedis.php
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\Redis;
|
||||||
|
|
||||||
|
class CleanupRedis extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'cleanup:redis';
|
||||||
|
|
||||||
|
protected $description = 'Cleanup Redis';
|
||||||
|
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
echo "Cleanup Redis keys.\n";
|
||||||
|
$prefix = config('database.redis.options.prefix');
|
||||||
|
|
||||||
|
$keys = Redis::connection()->keys('*:laravel*');
|
||||||
|
collect($keys)->each(function ($key) use ($prefix) {
|
||||||
|
$keyWithoutPrefix = str_replace($prefix, '', $key);
|
||||||
|
Redis::connection()->del($keyWithoutPrefix);
|
||||||
|
});
|
||||||
|
|
||||||
|
$queueOverlaps = Redis::connection()->keys('*laravel-queue-overlap*');
|
||||||
|
collect($queueOverlaps)->each(function ($key) {
|
||||||
|
Redis::connection()->del($key);
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,10 +2,12 @@
|
|||||||
|
|
||||||
namespace App\Console\Commands;
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Jobs\CleanupHelperContainersJob;
|
||||||
use App\Models\Application;
|
use App\Models\Application;
|
||||||
use App\Models\ApplicationPreview;
|
use App\Models\ApplicationPreview;
|
||||||
use App\Models\ScheduledDatabaseBackup;
|
use App\Models\ScheduledDatabaseBackup;
|
||||||
use App\Models\ScheduledTask;
|
use App\Models\ScheduledTask;
|
||||||
|
use App\Models\Server;
|
||||||
use App\Models\Service;
|
use App\Models\Service;
|
||||||
use App\Models\ServiceApplication;
|
use App\Models\ServiceApplication;
|
||||||
use App\Models\ServiceDatabase;
|
use App\Models\ServiceDatabase;
|
||||||
@@ -35,6 +37,16 @@ class CleanupStuckedResources extends Command
|
|||||||
private function cleanup_stucked_resources()
|
private function cleanup_stucked_resources()
|
||||||
{
|
{
|
||||||
|
|
||||||
|
try {
|
||||||
|
$servers = Server::all()->filter(function ($server) {
|
||||||
|
return $server->isFunctional();
|
||||||
|
});
|
||||||
|
foreach ($servers as $server) {
|
||||||
|
CleanupHelperContainersJob::dispatch($server);
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
echo "Error in cleaning stucked resources: {$e->getMessage()}\n";
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
$applications = Application::withTrashed()->whereNotNull('deleted_at')->get();
|
$applications = Application::withTrashed()->whereNotNull('deleted_at')->get();
|
||||||
foreach ($applications as $application) {
|
foreach ($applications as $application) {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ namespace App\Console\Commands;
|
|||||||
use App\Actions\Server\StopSentinel;
|
use App\Actions\Server\StopSentinel;
|
||||||
use App\Enums\ActivityTypes;
|
use App\Enums\ActivityTypes;
|
||||||
use App\Enums\ApplicationDeploymentStatus;
|
use App\Enums\ApplicationDeploymentStatus;
|
||||||
use App\Jobs\CleanupHelperContainersJob;
|
|
||||||
use App\Models\ApplicationDeploymentQueue;
|
use App\Models\ApplicationDeploymentQueue;
|
||||||
use App\Models\Environment;
|
use App\Models\Environment;
|
||||||
use App\Models\InstanceSettings;
|
use App\Models\InstanceSettings;
|
||||||
@@ -18,7 +17,7 @@ use Illuminate\Support\Facades\Http;
|
|||||||
|
|
||||||
class Init extends Command
|
class Init extends Command
|
||||||
{
|
{
|
||||||
protected $signature = 'app:init {--full-cleanup} {--cleanup-deployments} {--cleanup-proxy-networks}';
|
protected $signature = 'app:init {--force-cloud}';
|
||||||
|
|
||||||
protected $description = 'Cleanup instance related stuffs';
|
protected $description = 'Cleanup instance related stuffs';
|
||||||
|
|
||||||
@@ -26,9 +25,63 @@ class Init extends Command
|
|||||||
|
|
||||||
public function handle()
|
public function handle()
|
||||||
{
|
{
|
||||||
|
if (isCloud() && ! $this->option('force-cloud')) {
|
||||||
|
echo "Skipping init as we are on cloud and --force-cloud option is not set\n";
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$this->servers = Server::all();
|
$this->servers = Server::all();
|
||||||
$this->alive();
|
if (isCloud()) {
|
||||||
get_public_ips();
|
|
||||||
|
} else {
|
||||||
|
$this->send_alive_signal();
|
||||||
|
get_public_ips();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backward compatibility
|
||||||
|
$this->disable_metrics();
|
||||||
|
$this->replace_slash_in_environment_name();
|
||||||
|
$this->restore_coolify_db_backup();
|
||||||
|
//
|
||||||
|
$this->update_traefik_labels();
|
||||||
|
if (! isCloud() || $this->option('force-cloud')) {
|
||||||
|
$this->cleanup_unused_network_from_coolify_proxy();
|
||||||
|
}
|
||||||
|
if (isCloud()) {
|
||||||
|
$this->cleanup_unnecessary_dynamic_proxy_configuration();
|
||||||
|
} else {
|
||||||
|
$this->cleanup_in_progress_application_deployments();
|
||||||
|
}
|
||||||
|
$this->call('cleanup:redis');
|
||||||
|
$this->call('cleanup:stucked-resources');
|
||||||
|
|
||||||
|
if (isCloud()) {
|
||||||
|
$response = Http::retry(3, 1000)->get(config('constants.services.official'));
|
||||||
|
if ($response->successful()) {
|
||||||
|
$services = $response->json();
|
||||||
|
File::put(base_path('templates/service-templates.json'), json_encode($services));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
$localhost = $this->servers->where('id', 0)->first();
|
||||||
|
$localhost->setupDynamicProxyConfiguration();
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
echo "Could not setup dynamic configuration: {$e->getMessage()}\n";
|
||||||
|
}
|
||||||
|
$settings = InstanceSettings::get();
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function disable_metrics()
|
||||||
|
{
|
||||||
if (version_compare('4.0.0-beta.312', config('version'), '<=')) {
|
if (version_compare('4.0.0-beta.312', config('version'), '<=')) {
|
||||||
foreach ($this->servers as $server) {
|
foreach ($this->servers as $server) {
|
||||||
if ($server->settings->is_metrics_enabled === true) {
|
if ($server->settings->is_metrics_enabled === true) {
|
||||||
@@ -39,62 +92,6 @@ class Init extends Command
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$full_cleanup = $this->option('full-cleanup');
|
|
||||||
$cleanup_deployments = $this->option('cleanup-deployments');
|
|
||||||
$cleanup_proxy_networks = $this->option('cleanup-proxy-networks');
|
|
||||||
$this->replace_slash_in_environment_name();
|
|
||||||
if ($cleanup_deployments) {
|
|
||||||
echo "Running cleanup deployments.\n";
|
|
||||||
$this->cleanup_in_progress_application_deployments();
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if ($cleanup_proxy_networks) {
|
|
||||||
echo "Running cleanup proxy networks.\n";
|
|
||||||
$this->cleanup_unused_network_from_coolify_proxy();
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if ($full_cleanup) {
|
|
||||||
// Required for falsely deleted coolify db
|
|
||||||
$this->restore_coolify_db_backup();
|
|
||||||
$this->update_traefik_labels();
|
|
||||||
$this->cleanup_unused_network_from_coolify_proxy();
|
|
||||||
$this->cleanup_unnecessary_dynamic_proxy_configuration();
|
|
||||||
$this->cleanup_in_progress_application_deployments();
|
|
||||||
$this->cleanup_stucked_helper_containers();
|
|
||||||
$this->call('cleanup:queue');
|
|
||||||
$this->call('cleanup:stucked-resources');
|
|
||||||
if (! isCloud()) {
|
|
||||||
try {
|
|
||||||
$localhost = $this->servers->where('id', 0)->first();
|
|
||||||
$localhost->setupDynamicProxyConfiguration();
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
echo "Could not setup dynamic configuration: {$e->getMessage()}\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$settings = InstanceSettings::get();
|
|
||||||
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]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (isCloud()) {
|
|
||||||
$response = Http::retry(3, 1000)->get(config('constants.services.official'));
|
|
||||||
if ($response->successful()) {
|
|
||||||
$services = $response->json();
|
|
||||||
File::put(base_path('templates/service-templates.json'), json_encode($services));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$this->cleanup_stucked_helper_containers();
|
|
||||||
$this->call('cleanup:stucked-resources');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function update_traefik_labels()
|
private function update_traefik_labels()
|
||||||
@@ -108,33 +105,28 @@ class Init extends Command
|
|||||||
|
|
||||||
private function cleanup_unnecessary_dynamic_proxy_configuration()
|
private function cleanup_unnecessary_dynamic_proxy_configuration()
|
||||||
{
|
{
|
||||||
if (isCloud()) {
|
foreach ($this->servers as $server) {
|
||||||
foreach ($this->servers as $server) {
|
try {
|
||||||
try {
|
if (! $server->isFunctional()) {
|
||||||
if (! $server->isFunctional()) {
|
continue;
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if ($server->id === 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$file = $server->proxyPath().'/dynamic/coolify.yaml';
|
|
||||||
|
|
||||||
return instant_remote_process([
|
|
||||||
"rm -f $file",
|
|
||||||
], $server, false);
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
echo "Error in cleaning up unnecessary dynamic proxy configuration: {$e->getMessage()}\n";
|
|
||||||
}
|
}
|
||||||
|
if ($server->id === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$file = $server->proxyPath().'/dynamic/coolify.yaml';
|
||||||
|
|
||||||
|
return instant_remote_process([
|
||||||
|
"rm -f $file",
|
||||||
|
], $server, false);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
echo "Error in cleaning up unnecessary dynamic proxy configuration: {$e->getMessage()}\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function cleanup_unused_network_from_coolify_proxy()
|
private function cleanup_unused_network_from_coolify_proxy()
|
||||||
{
|
{
|
||||||
if (isCloud()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
foreach ($this->servers as $server) {
|
foreach ($this->servers as $server) {
|
||||||
if (! $server->isFunctional()) {
|
if (! $server->isFunctional()) {
|
||||||
continue;
|
continue;
|
||||||
@@ -175,39 +167,32 @@ class Init extends Command
|
|||||||
|
|
||||||
private function restore_coolify_db_backup()
|
private function restore_coolify_db_backup()
|
||||||
{
|
{
|
||||||
try {
|
if (version_compare('4.0.0-beta.179', config('version'), '<=')) {
|
||||||
$database = StandalonePostgresql::withTrashed()->find(0);
|
try {
|
||||||
if ($database && $database->trashed()) {
|
$database = StandalonePostgresql::withTrashed()->find(0);
|
||||||
echo "Restoring coolify db backup\n";
|
if ($database && $database->trashed()) {
|
||||||
$database->restore();
|
echo "Restoring coolify db backup\n";
|
||||||
$scheduledBackup = ScheduledDatabaseBackup::find(0);
|
$database->restore();
|
||||||
if (! $scheduledBackup) {
|
$scheduledBackup = ScheduledDatabaseBackup::find(0);
|
||||||
ScheduledDatabaseBackup::create([
|
if (! $scheduledBackup) {
|
||||||
'id' => 0,
|
ScheduledDatabaseBackup::create([
|
||||||
'enabled' => true,
|
'id' => 0,
|
||||||
'save_s3' => false,
|
'enabled' => true,
|
||||||
'frequency' => '0 0 * * *',
|
'save_s3' => false,
|
||||||
'database_id' => $database->id,
|
'frequency' => '0 0 * * *',
|
||||||
'database_type' => 'App\Models\StandalonePostgresql',
|
'database_id' => $database->id,
|
||||||
'team_id' => 0,
|
'database_type' => 'App\Models\StandalonePostgresql',
|
||||||
]);
|
'team_id' => 0,
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
} catch (\Throwable $e) {
|
||||||
} catch (\Throwable $e) {
|
echo "Error in restoring coolify db backup: {$e->getMessage()}\n";
|
||||||
echo "Error in restoring coolify db backup: {$e->getMessage()}\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function cleanup_stucked_helper_containers()
|
|
||||||
{
|
|
||||||
foreach ($this->servers as $server) {
|
|
||||||
if ($server->isFunctional()) {
|
|
||||||
CleanupHelperContainersJob::dispatch($server);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function alive()
|
private function send_alive_signal()
|
||||||
{
|
{
|
||||||
$id = config('app.id');
|
$id = config('app.id');
|
||||||
$version = config('version');
|
$version = config('version');
|
||||||
@@ -225,23 +210,7 @@ class Init extends Command
|
|||||||
echo "Error in alive: {$e->getMessage()}\n";
|
echo "Error in alive: {$e->getMessage()}\n";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// private function cleanup_ssh()
|
|
||||||
// {
|
|
||||||
|
|
||||||
// TODO: it will cleanup id.root@host.docker.internal
|
|
||||||
// try {
|
|
||||||
// $files = Storage::allFiles('ssh/keys');
|
|
||||||
// foreach ($files as $file) {
|
|
||||||
// Storage::delete($file);
|
|
||||||
// }
|
|
||||||
// $files = Storage::allFiles('ssh/mux');
|
|
||||||
// foreach ($files as $file) {
|
|
||||||
// Storage::delete($file);
|
|
||||||
// }
|
|
||||||
// } catch (\Throwable $e) {
|
|
||||||
// echo "Error in cleaning ssh: {$e->getMessage()}\n";
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
private function cleanup_in_progress_application_deployments()
|
private function cleanup_in_progress_application_deployments()
|
||||||
{
|
{
|
||||||
// Cleanup any failed deployments
|
// Cleanup any failed deployments
|
||||||
@@ -263,11 +232,13 @@ class Init extends Command
|
|||||||
|
|
||||||
private function replace_slash_in_environment_name()
|
private function replace_slash_in_environment_name()
|
||||||
{
|
{
|
||||||
$environments = Environment::all();
|
if (version_compare('4.0.0-beta.298', config('version'), '<=')) {
|
||||||
foreach ($environments as $environment) {
|
$environments = Environment::all();
|
||||||
if (str_contains($environment->name, '/')) {
|
foreach ($environments as $environment) {
|
||||||
$environment->name = str_replace('/', '-', $environment->name);
|
if (str_contains($environment->name, '/')) {
|
||||||
$environment->save();
|
$environment->name = str_replace('/', '-', $environment->name);
|
||||||
|
$environment->save();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ class Kernel extends ConsoleKernel
|
|||||||
$schedule->command('uploads:clear')->everyTwoMinutes();
|
$schedule->command('uploads:clear')->everyTwoMinutes();
|
||||||
|
|
||||||
$schedule->command('telescope:prune')->daily();
|
$schedule->command('telescope:prune')->daily();
|
||||||
|
|
||||||
|
$schedule->job(new PullHelperImageJob)->everyFiveMinutes()->onOneServer();
|
||||||
} else {
|
} else {
|
||||||
// Instance Jobs
|
// Instance Jobs
|
||||||
$schedule->command('horizon:snapshot')->everyFiveMinutes();
|
$schedule->command('horizon:snapshot')->everyFiveMinutes();
|
||||||
@@ -77,11 +79,11 @@ class Kernel extends ConsoleKernel
|
|||||||
}
|
}
|
||||||
})->cron($settings->update_check_frequency)->timezone($settings->instance_timezone)->onOneServer();
|
})->cron($settings->update_check_frequency)->timezone($settings->instance_timezone)->onOneServer();
|
||||||
}
|
}
|
||||||
$schedule->job(new PullHelperImageJob($server))
|
|
||||||
->cron($settings->update_check_frequency)
|
|
||||||
->timezone($settings->instance_timezone)
|
|
||||||
->onOneServer();
|
|
||||||
}
|
}
|
||||||
|
$schedule->job(new PullHelperImageJob)
|
||||||
|
->cron($settings->update_check_frequency)
|
||||||
|
->timezone($settings->instance_timezone)
|
||||||
|
->onOneServer();
|
||||||
}
|
}
|
||||||
|
|
||||||
private function schedule_updates($schedule)
|
private function schedule_updates($schedule)
|
||||||
|
|||||||
34
app/Events/CloudflareTunnelConfigured.php
Normal file
34
app/Events/CloudflareTunnelConfigured.php
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||||
|
use Illuminate\Broadcasting\PrivateChannel;
|
||||||
|
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||||
|
use Illuminate\Foundation\Events\Dispatchable;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
class CloudflareTunnelConfigured 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');
|
||||||
|
}
|
||||||
|
$this->teamId = $teamId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function broadcastOn(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
new PrivateChannel("team.{$this->teamId}"),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
184
app/Helpers/SshMultiplexingHelper.php
Normal file
184
app/Helpers/SshMultiplexingHelper.php
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Helpers;
|
||||||
|
|
||||||
|
use App\Models\PrivateKey;
|
||||||
|
use App\Models\Server;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Support\Facades\Process;
|
||||||
|
|
||||||
|
class SshMultiplexingHelper
|
||||||
|
{
|
||||||
|
public static function serverSshConfiguration(Server $server)
|
||||||
|
{
|
||||||
|
$privateKey = PrivateKey::findOrFail($server->private_key_id);
|
||||||
|
$sshKeyLocation = $privateKey->getKeyLocation();
|
||||||
|
$muxFilename = '/var/www/html/storage/app/ssh/mux/mux_'.$server->uuid;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'sshKeyLocation' => $sshKeyLocation,
|
||||||
|
'muxFilename' => $muxFilename,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function ensureMultiplexedConnection(Server $server)
|
||||||
|
{
|
||||||
|
if (! self::isMultiplexingEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sshConfig = self::serverSshConfiguration($server);
|
||||||
|
$muxSocket = $sshConfig['muxFilename'];
|
||||||
|
$sshKeyLocation = $sshConfig['sshKeyLocation'];
|
||||||
|
|
||||||
|
self::validateSshKey($sshKeyLocation);
|
||||||
|
|
||||||
|
$checkCommand = "ssh -O check -o ControlPath=$muxSocket ";
|
||||||
|
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
|
||||||
|
$checkCommand .= '-o ProxyCommand="cloudflared access ssh --hostname %h" ';
|
||||||
|
}
|
||||||
|
$checkCommand .= "{$server->user}@{$server->ip}";
|
||||||
|
$process = Process::run($checkCommand);
|
||||||
|
|
||||||
|
if ($process->exitCode() !== 0) {
|
||||||
|
self::establishNewMultiplexedConnection($server);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function establishNewMultiplexedConnection(Server $server)
|
||||||
|
{
|
||||||
|
$sshConfig = self::serverSshConfiguration($server);
|
||||||
|
$sshKeyLocation = $sshConfig['sshKeyLocation'];
|
||||||
|
$muxSocket = $sshConfig['muxFilename'];
|
||||||
|
|
||||||
|
$connectionTimeout = config('constants.ssh.connection_timeout');
|
||||||
|
$serverInterval = config('constants.ssh.server_interval');
|
||||||
|
$muxPersistTime = config('constants.ssh.mux_persist_time');
|
||||||
|
|
||||||
|
$establishCommand = "ssh -fNM -o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
|
||||||
|
|
||||||
|
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
|
||||||
|
$establishCommand .= ' -o ProxyCommand="cloudflared access ssh --hostname %h" ';
|
||||||
|
}
|
||||||
|
|
||||||
|
$establishCommand .= self::getCommonSshOptions($server, $sshKeyLocation, $connectionTimeout, $serverInterval);
|
||||||
|
$establishCommand .= "{$server->user}@{$server->ip}";
|
||||||
|
|
||||||
|
$establishProcess = Process::run($establishCommand);
|
||||||
|
|
||||||
|
if ($establishProcess->exitCode() !== 0) {
|
||||||
|
throw new \RuntimeException('Failed to establish multiplexed connection: '.$establishProcess->errorOutput());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function removeMuxFile(Server $server)
|
||||||
|
{
|
||||||
|
$sshConfig = self::serverSshConfiguration($server);
|
||||||
|
$muxSocket = $sshConfig['muxFilename'];
|
||||||
|
|
||||||
|
$closeCommand = "ssh -O exit -o ControlPath=$muxSocket ";
|
||||||
|
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
|
||||||
|
$closeCommand .= '-o ProxyCommand="cloudflared access ssh --hostname %h" ';
|
||||||
|
}
|
||||||
|
$closeCommand .= "{$server->user}@{$server->ip}";
|
||||||
|
Process::run($closeCommand);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function generateScpCommand(Server $server, string $source, string $dest)
|
||||||
|
{
|
||||||
|
$sshConfig = self::serverSshConfiguration($server);
|
||||||
|
$sshKeyLocation = $sshConfig['sshKeyLocation'];
|
||||||
|
$muxSocket = $sshConfig['muxFilename'];
|
||||||
|
|
||||||
|
$timeout = config('constants.ssh.command_timeout');
|
||||||
|
$muxPersistTime = config('constants.ssh.mux_persist_time');
|
||||||
|
|
||||||
|
$scp_command = "timeout $timeout scp ";
|
||||||
|
|
||||||
|
if (self::isMultiplexingEnabled()) {
|
||||||
|
$scp_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
|
||||||
|
self::ensureMultiplexedConnection($server);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
|
||||||
|
$scp_command .= '-o ProxyCommand="cloudflared access ssh --hostname %h" ';
|
||||||
|
}
|
||||||
|
|
||||||
|
$scp_command .= self::getCommonSshOptions($server, $sshKeyLocation, config('constants.ssh.connection_timeout'), config('constants.ssh.server_interval'), isScp: true);
|
||||||
|
$scp_command .= "{$source} {$server->user}@{$server->ip}:{$dest}";
|
||||||
|
|
||||||
|
return $scp_command;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function generateSshCommand(Server $server, string $command)
|
||||||
|
{
|
||||||
|
if ($server->settings->force_disabled) {
|
||||||
|
throw new \RuntimeException('Server is disabled.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$sshConfig = self::serverSshConfiguration($server);
|
||||||
|
$sshKeyLocation = $sshConfig['sshKeyLocation'];
|
||||||
|
$muxSocket = $sshConfig['muxFilename'];
|
||||||
|
|
||||||
|
$timeout = config('constants.ssh.command_timeout');
|
||||||
|
$muxPersistTime = config('constants.ssh.mux_persist_time');
|
||||||
|
|
||||||
|
$ssh_command = "timeout $timeout ssh ";
|
||||||
|
|
||||||
|
if (self::isMultiplexingEnabled()) {
|
||||||
|
$ssh_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
|
||||||
|
self::ensureMultiplexedConnection($server);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
|
||||||
|
$ssh_command .= "-o ProxyCommand='cloudflared access ssh --hostname %h' ";
|
||||||
|
}
|
||||||
|
|
||||||
|
$ssh_command .= self::getCommonSshOptions($server, $sshKeyLocation, config('constants.ssh.connection_timeout'), config('constants.ssh.server_interval'));
|
||||||
|
|
||||||
|
$command = "PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/host/usr/local/sbin:/host/usr/local/bin:/host/usr/sbin:/host/usr/bin:/host/sbin:/host/bin && $command";
|
||||||
|
$delimiter = Hash::make($command);
|
||||||
|
$command = str_replace($delimiter, '', $command);
|
||||||
|
|
||||||
|
$ssh_command .= "{$server->user}@{$server->ip} 'bash -se' << \\$delimiter".PHP_EOL
|
||||||
|
.$command.PHP_EOL
|
||||||
|
.$delimiter;
|
||||||
|
|
||||||
|
return $ssh_command;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function isMultiplexingEnabled(): bool
|
||||||
|
{
|
||||||
|
return config('constants.ssh.mux_enabled') && ! config('coolify.is_windows_docker_desktop');
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function validateSshKey(string $sshKeyLocation): void
|
||||||
|
{
|
||||||
|
$checkKeyCommand = "ls $sshKeyLocation 2>/dev/null";
|
||||||
|
$keyCheckProcess = Process::run($checkKeyCommand);
|
||||||
|
|
||||||
|
if ($keyCheckProcess->exitCode() !== 0) {
|
||||||
|
throw new \RuntimeException("SSH key file not accessible: $sshKeyLocation");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function getCommonSshOptions(Server $server, string $sshKeyLocation, int $connectionTimeout, int $serverInterval, bool $isScp = false): string
|
||||||
|
{
|
||||||
|
$options = "-i {$sshKeyLocation} "
|
||||||
|
.'-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null '
|
||||||
|
.'-o PasswordAuthentication=no '
|
||||||
|
."-o ConnectTimeout=$connectionTimeout "
|
||||||
|
."-o ServerAliveInterval=$serverInterval "
|
||||||
|
.'-o RequestTTY=no '
|
||||||
|
.'-o LogLevel=ERROR ';
|
||||||
|
|
||||||
|
// Bruh
|
||||||
|
if ($isScp) {
|
||||||
|
$options .= "-P {$server->port} ";
|
||||||
|
} else {
|
||||||
|
$options .= "-p {$server->port} ";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $options;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2529,6 +2529,131 @@ class ApplicationsController extends Controller
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Post(
|
||||||
|
summary: 'Execute Command',
|
||||||
|
description: "Execute a command on the application's current container.",
|
||||||
|
path: '/applications/{uuid}/execute',
|
||||||
|
operationId: 'execute-command-application',
|
||||||
|
security: [
|
||||||
|
['bearerAuth' => []],
|
||||||
|
],
|
||||||
|
tags: ['Applications'],
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(
|
||||||
|
name: 'uuid',
|
||||||
|
in: 'path',
|
||||||
|
description: 'UUID of the application.',
|
||||||
|
required: true,
|
||||||
|
schema: new OA\Schema(
|
||||||
|
type: 'string',
|
||||||
|
format: 'uuid',
|
||||||
|
)
|
||||||
|
),
|
||||||
|
],
|
||||||
|
requestBody: new OA\RequestBody(
|
||||||
|
required: true,
|
||||||
|
description: 'Command to execute.',
|
||||||
|
content: new OA\MediaType(
|
||||||
|
mediaType: 'application/json',
|
||||||
|
schema: new OA\Schema(
|
||||||
|
type: 'object',
|
||||||
|
properties: [
|
||||||
|
'command' => ['type' => 'string', 'description' => 'Command to execute.'],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: "Execute a command on the application's current container.",
|
||||||
|
content: [
|
||||||
|
new OA\MediaType(
|
||||||
|
mediaType: 'application/json',
|
||||||
|
schema: new OA\Schema(
|
||||||
|
type: 'object',
|
||||||
|
properties: [
|
||||||
|
'message' => ['type' => 'string', 'example' => 'Command executed.'],
|
||||||
|
'response' => ['type' => 'string'],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
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 execute_command_by_uuid(Request $request)
|
||||||
|
{
|
||||||
|
// TODO: Need to review this from security perspective, to not allow arbitrary command execution
|
||||||
|
$allowedFields = ['command'];
|
||||||
|
$teamId = getTeamIdFromToken();
|
||||||
|
if (is_null($teamId)) {
|
||||||
|
return invalidTokenResponse();
|
||||||
|
}
|
||||||
|
$uuid = $request->route('uuid');
|
||||||
|
if (! $uuid) {
|
||||||
|
return response()->json(['message' => 'UUID is required.'], 400);
|
||||||
|
}
|
||||||
|
$application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first();
|
||||||
|
if (! $application) {
|
||||||
|
return response()->json(['message' => 'Application not found.'], 404);
|
||||||
|
}
|
||||||
|
$return = validateIncomingRequest($request);
|
||||||
|
if ($return instanceof \Illuminate\Http\JsonResponse) {
|
||||||
|
return $return;
|
||||||
|
}
|
||||||
|
$validator = customApiValidator($request->all(), [
|
||||||
|
'command' => 'string|required',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$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);
|
||||||
|
}
|
||||||
|
|
||||||
|
$container = getCurrentApplicationContainerStatus($application->destination->server, $application->id)->firstOrFail();
|
||||||
|
$status = getContainerStatus($application->destination->server, $container['Names']);
|
||||||
|
|
||||||
|
if ($status !== 'running') {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Application is not running.',
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$commands = collect([
|
||||||
|
executeInDocker($container['Names'], $request->command),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$res = instant_remote_process(command: $commands, server: $application->destination->server);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Command executed.',
|
||||||
|
'response' => $res,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
private function validateDataApplications(Request $request, Server $server)
|
private function validateDataApplications(Request $request, Server $server)
|
||||||
{
|
{
|
||||||
$teamId = getTeamIdFromToken();
|
$teamId = getTeamIdFromToken();
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ use Illuminate\Foundation\Bus\Dispatchable;
|
|||||||
use Illuminate\Queue\InteractsWithQueue;
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Facades\Process;
|
||||||
use Illuminate\Support\Sleep;
|
use Illuminate\Support\Sleep;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
@@ -210,7 +211,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
|||||||
}
|
}
|
||||||
ray('New container name: ', $this->container_name)->green();
|
ray('New container name: ', $this->container_name)->green();
|
||||||
|
|
||||||
savePrivateKeyToFs($this->server);
|
|
||||||
$this->saved_outputs = collect();
|
$this->saved_outputs = collect();
|
||||||
|
|
||||||
// Set preview fqdn
|
// Set preview fqdn
|
||||||
@@ -514,7 +514,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
|||||||
'hidden' => true,
|
'hidden' => true,
|
||||||
'ignore_errors' => true,
|
'ignore_errors' => true,
|
||||||
], [
|
], [
|
||||||
"docker network connect {$networkId} coolify-proxy || true",
|
"docker network connect {$networkId} coolify-proxy >/dev/null 2>&1 || true",
|
||||||
'hidden' => true,
|
'hidden' => true,
|
||||||
'ignore_errors' => true,
|
'ignore_errors' => true,
|
||||||
]);
|
]);
|
||||||
@@ -919,10 +919,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
|||||||
}
|
}
|
||||||
if ($this->application->build_pack !== 'dockercompose' || $this->application->compose_parsing_version === '1' || $this->application->compose_parsing_version === '2') {
|
if ($this->application->build_pack !== 'dockercompose' || $this->application->compose_parsing_version === '1' || $this->application->compose_parsing_version === '2') {
|
||||||
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_BRANCH')->isEmpty()) {
|
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_BRANCH')->isEmpty()) {
|
||||||
$envs->push("COOLIFY_BRANCH={$local_branch}");
|
$envs->push("COOLIFY_BRANCH=\"{$local_branch}\"");
|
||||||
}
|
}
|
||||||
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) {
|
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) {
|
||||||
$envs->push("COOLIFY_CONTAINER_NAME={$this->container_name}");
|
$envs->push("COOLIFY_CONTAINER_NAME=\"{$this->container_name}\"");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -978,10 +978,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
|||||||
}
|
}
|
||||||
if ($this->application->build_pack !== 'dockercompose' || $this->application->compose_parsing_version === '1' || $this->application->compose_parsing_version === '2') {
|
if ($this->application->build_pack !== 'dockercompose' || $this->application->compose_parsing_version === '1' || $this->application->compose_parsing_version === '2') {
|
||||||
if ($this->application->environment_variables->where('key', 'COOLIFY_BRANCH')->isEmpty()) {
|
if ($this->application->environment_variables->where('key', 'COOLIFY_BRANCH')->isEmpty()) {
|
||||||
$envs->push("COOLIFY_BRANCH={$local_branch}");
|
$envs->push("COOLIFY_BRANCH=\"{$local_branch}\"");
|
||||||
}
|
}
|
||||||
if ($this->application->environment_variables->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) {
|
if ($this->application->environment_variables->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) {
|
||||||
$envs->push("COOLIFY_CONTAINER_NAME={$this->container_name}");
|
$envs->push("COOLIFY_CONTAINER_NAME=\"{$this->container_name}\"");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1456,10 +1456,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
|||||||
executeInDocker($this->deployment_uuid, 'chmod 600 /root/.ssh/id_rsa'),
|
executeInDocker($this->deployment_uuid, 'chmod 600 /root/.ssh/id_rsa'),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
executeInDocker($this->deployment_uuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git ls-remote {$this->fullRepoUrl} {$local_branch}"),
|
executeInDocker($this->deployment_uuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git ls-remote {$this->fullRepoUrl} {$local_branch}"),
|
||||||
'hidden' => true,
|
'hidden' => true,
|
||||||
'save' => 'git_commit_sha',
|
'save' => 'git_commit_sha',
|
||||||
],
|
]
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
$this->execute_remote_command(
|
$this->execute_remote_command(
|
||||||
@@ -2049,6 +2049,10 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
|||||||
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
|
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
|
||||||
'hidden' => true,
|
'hidden' => true,
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
|
||||||
|
'hidden' => true,
|
||||||
|
],
|
||||||
[
|
[
|
||||||
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
|
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
|
||||||
'hidden' => true,
|
'hidden' => true,
|
||||||
@@ -2068,6 +2072,10 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
|||||||
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
|
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
|
||||||
'hidden' => true,
|
'hidden' => true,
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
|
||||||
|
'hidden' => true,
|
||||||
|
],
|
||||||
[
|
[
|
||||||
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
|
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
|
||||||
'hidden' => true,
|
'hidden' => true,
|
||||||
@@ -2110,6 +2118,10 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
|||||||
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
|
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
|
||||||
'hidden' => true,
|
'hidden' => true,
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
|
||||||
|
'hidden' => true,
|
||||||
|
],
|
||||||
[
|
[
|
||||||
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
|
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
|
||||||
'hidden' => true,
|
'hidden' => true,
|
||||||
@@ -2129,6 +2141,10 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
|||||||
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
|
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
|
||||||
'hidden' => true,
|
'hidden' => true,
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
|
||||||
|
'hidden' => true,
|
||||||
|
],
|
||||||
[
|
[
|
||||||
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
|
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
|
||||||
'hidden' => true,
|
'hidden' => true,
|
||||||
@@ -2157,6 +2173,10 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
|||||||
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
|
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
|
||||||
'hidden' => true,
|
'hidden' => true,
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
|
||||||
|
'hidden' => true,
|
||||||
|
],
|
||||||
[
|
[
|
||||||
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
|
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
|
||||||
'hidden' => true,
|
'hidden' => true,
|
||||||
@@ -2176,6 +2196,10 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
|||||||
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
|
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
|
||||||
'hidden' => true,
|
'hidden' => true,
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
|
||||||
|
'hidden' => true,
|
||||||
|
],
|
||||||
[
|
[
|
||||||
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
|
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
|
||||||
'hidden' => true,
|
'hidden' => true,
|
||||||
@@ -2187,20 +2211,40 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
|||||||
$this->application_deployment_queue->addLogEntry('Building docker image completed.');
|
$this->application_deployment_queue->addLogEntry('Building docker image completed.');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private function graceful_shutdown_container(string $containerName, int $timeout = 300)
|
||||||
* @param int $timeout in seconds
|
|
||||||
*/
|
|
||||||
private function graceful_shutdown_container(string $containerName, int $timeout = 30)
|
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$this->execute_remote_command(
|
$process = Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
|
||||||
["docker stop --time=$timeout $containerName", 'hidden' => true, 'ignore_errors' => true],
|
|
||||||
["docker rm $containerName", 'hidden' => true, 'ignore_errors' => true]
|
$startTime = time();
|
||||||
);
|
while ($process->running()) {
|
||||||
|
if (time() - $startTime >= $timeout) {
|
||||||
|
$this->execute_remote_command(
|
||||||
|
["docker kill $containerName", 'hidden' => true, 'ignore_errors' => true]
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
usleep(100000);
|
||||||
|
}
|
||||||
|
|
||||||
|
$isRunning = $this->execute_remote_command(
|
||||||
|
["docker inspect -f '{{.State.Running}}' $containerName", 'hidden' => true, 'ignore_errors' => true]
|
||||||
|
) === 'true';
|
||||||
|
|
||||||
|
if ($isRunning) {
|
||||||
|
$this->execute_remote_command(
|
||||||
|
["docker kill $containerName", 'hidden' => true, 'ignore_errors' => true]
|
||||||
|
);
|
||||||
|
}
|
||||||
} catch (\Exception $error) {
|
} catch (\Exception $error) {
|
||||||
// report error if needed
|
$this->application_deployment_queue->addLogEntry("Error stopping container $containerName: ".$error->getMessage(), 'stderr');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->remove_container($containerName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function remove_container(string $containerName)
|
||||||
|
{
|
||||||
$this->execute_remote_command(
|
$this->execute_remote_command(
|
||||||
["docker rm -f $containerName", 'hidden' => true, 'ignore_errors' => true]
|
["docker rm -f $containerName", 'hidden' => true, 'ignore_errors' => true]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ use Illuminate\Contracts\Queue\ShouldQueue;
|
|||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
use Illuminate\Queue\InteractsWithQueue;
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
use Illuminate\Support\Facades\Http;
|
|
||||||
use Illuminate\Support\Facades\File;
|
use Illuminate\Support\Facades\File;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
|
||||||
class CheckForUpdatesJob implements ShouldBeEncrypted, ShouldQueue
|
class CheckForUpdatesJob implements ShouldBeEncrypted, ShouldQueue
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -21,11 +21,10 @@ class CleanupHelperContainersJob implements ShouldBeEncrypted, ShouldBeUnique, S
|
|||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
ray('Cleaning up helper containers on '.$this->server->name);
|
ray('Cleaning up helper containers on '.$this->server->name);
|
||||||
$containers = instant_remote_process(['docker container ps --filter "ancestor=ghcr.io/coollabsio/coolify-helper:next" --filter "ancestor=ghcr.io/coollabsio/coolify-helper:latest" --format \'{{json .}}\''], $this->server, false);
|
$containers = instant_remote_process(['docker container ps --format \'{{json .}}\' | jq -s \'map(select(.Image | contains("ghcr.io/coollabsio/coolify-helper")))\''], $this->server, false);
|
||||||
$containers = format_docker_command_output_to_json($containers);
|
$containerIds = collect(json_decode($containers))->pluck('ID');
|
||||||
if ($containers->count() > 0) {
|
if ($containerIds->count() > 0) {
|
||||||
foreach ($containers as $container) {
|
foreach ($containerIds as $containerId) {
|
||||||
$containerId = data_get($container, 'ID');
|
|
||||||
ray('Removing container '.$containerId);
|
ray('Removing container '.$containerId);
|
||||||
instant_remote_process(['docker container rm -f '.$containerId], $this->server, false);
|
instant_remote_process(['docker container rm -f '.$containerId], $this->server, false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,14 @@
|
|||||||
namespace App\Jobs;
|
namespace App\Jobs;
|
||||||
|
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
|
use Carbon\Carbon;
|
||||||
use Illuminate\Bus\Queueable;
|
use Illuminate\Bus\Queueable;
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
use Illuminate\Queue\InteractsWithQueue;
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
use Illuminate\Support\Facades\Process;
|
use Illuminate\Support\Facades\Process;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
class CleanupStaleMultiplexedConnections implements ShouldQueue
|
class CleanupStaleMultiplexedConnections implements ShouldQueue
|
||||||
{
|
{
|
||||||
@@ -16,22 +18,65 @@ class CleanupStaleMultiplexedConnections implements ShouldQueue
|
|||||||
|
|
||||||
public function handle()
|
public function handle()
|
||||||
{
|
{
|
||||||
Server::chunk(100, function ($servers) {
|
$this->cleanupStaleConnections();
|
||||||
foreach ($servers as $server) {
|
$this->cleanupNonExistentServerConnections();
|
||||||
$this->cleanupStaleConnection($server);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function cleanupStaleConnection(Server $server)
|
private function cleanupStaleConnections()
|
||||||
{
|
{
|
||||||
$muxSocket = "/tmp/mux_{$server->id}";
|
$muxFiles = Storage::disk('ssh-mux')->files();
|
||||||
$checkCommand = "ssh -O check -o ControlPath=$muxSocket {$server->user}@{$server->ip} 2>/dev/null";
|
|
||||||
$checkProcess = Process::run($checkCommand);
|
|
||||||
|
|
||||||
if ($checkProcess->exitCode() !== 0) {
|
foreach ($muxFiles as $muxFile) {
|
||||||
$closeCommand = "ssh -O exit -o ControlPath=$muxSocket {$server->user}@{$server->ip} 2>/dev/null";
|
$serverUuid = $this->extractServerUuidFromMuxFile($muxFile);
|
||||||
Process::run($closeCommand);
|
$server = Server::where('uuid', $serverUuid)->first();
|
||||||
|
|
||||||
|
if (! $server) {
|
||||||
|
$this->removeMultiplexFile($muxFile);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$muxSocket = "/var/www/html/storage/app/ssh/mux/{$muxFile}";
|
||||||
|
$checkCommand = "ssh -O check -o ControlPath={$muxSocket} {$server->user}@{$server->ip} 2>/dev/null";
|
||||||
|
$checkProcess = Process::run($checkCommand);
|
||||||
|
|
||||||
|
if ($checkProcess->exitCode() !== 0) {
|
||||||
|
$this->removeMultiplexFile($muxFile);
|
||||||
|
} else {
|
||||||
|
$muxContent = Storage::disk('ssh-mux')->get($muxFile);
|
||||||
|
$establishedAt = Carbon::parse(substr($muxContent, 37));
|
||||||
|
$expirationTime = $establishedAt->addSeconds(config('constants.ssh.mux_persist_time'));
|
||||||
|
|
||||||
|
if (Carbon::now()->isAfter($expirationTime)) {
|
||||||
|
$this->removeMultiplexFile($muxFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function cleanupNonExistentServerConnections()
|
||||||
|
{
|
||||||
|
$muxFiles = Storage::disk('ssh-mux')->files();
|
||||||
|
$existingServerUuids = Server::pluck('uuid')->toArray();
|
||||||
|
|
||||||
|
foreach ($muxFiles as $muxFile) {
|
||||||
|
$serverUuid = $this->extractServerUuidFromMuxFile($muxFile);
|
||||||
|
if (! in_array($serverUuid, $existingServerUuids)) {
|
||||||
|
$this->removeMultiplexFile($muxFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function extractServerUuidFromMuxFile($muxFile)
|
||||||
|
{
|
||||||
|
return substr($muxFile, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function removeMultiplexFile($muxFile)
|
||||||
|
{
|
||||||
|
$muxSocket = "/var/www/html/storage/app/ssh/mux/{$muxFile}";
|
||||||
|
$closeCommand = "ssh -O exit -o ControlPath={$muxSocket} localhost 2>/dev/null";
|
||||||
|
Process::run($closeCommand);
|
||||||
|
Storage::disk('ssh-mux')->delete($muxFile);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
|||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
use Illuminate\Queue\InteractsWithQueue;
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
class ContainerStatusJob implements ShouldBeEncrypted, ShouldQueue
|
class ContainerStatusJob implements ShouldBeEncrypted, ShouldQueue
|
||||||
@@ -25,16 +24,6 @@ class ContainerStatusJob implements ShouldBeEncrypted, ShouldQueue
|
|||||||
|
|
||||||
public function __construct(public Server $server) {}
|
public function __construct(public Server $server) {}
|
||||||
|
|
||||||
public function middleware(): array
|
|
||||||
{
|
|
||||||
return [(new WithoutOverlapping($this->server->uuid))];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function uniqueId(): int
|
|
||||||
{
|
|
||||||
return $this->server->uuid;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function handle()
|
public function handle()
|
||||||
{
|
{
|
||||||
GetContainersStatus::run($this->server);
|
GetContainersStatus::run($this->server);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ namespace App\Jobs;
|
|||||||
|
|
||||||
use App\Actions\Database\StopDatabase;
|
use App\Actions\Database\StopDatabase;
|
||||||
use App\Events\BackupCreated;
|
use App\Events\BackupCreated;
|
||||||
|
use App\Models\InstanceSettings;
|
||||||
use App\Models\S3Storage;
|
use App\Models\S3Storage;
|
||||||
use App\Models\ScheduledDatabaseBackup;
|
use App\Models\ScheduledDatabaseBackup;
|
||||||
use App\Models\ScheduledDatabaseBackupExecution;
|
use App\Models\ScheduledDatabaseBackupExecution;
|
||||||
@@ -22,7 +23,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
|||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
use Illuminate\Queue\InteractsWithQueue;
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Visus\Cuid2\Cuid2;
|
use Visus\Cuid2\Cuid2;
|
||||||
@@ -79,16 +79,6 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function middleware(): array
|
|
||||||
{
|
|
||||||
return [new WithoutOverlapping($this->backup->id)];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function uniqueId(): int
|
|
||||||
{
|
|
||||||
return $this->backup->id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function handle(): void
|
public function handle(): void
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
@@ -478,10 +468,37 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// private function upload_to_s3(): void
|
||||||
|
// {
|
||||||
|
// try {
|
||||||
|
// if (is_null($this->s3)) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// $key = $this->s3->key;
|
||||||
|
// $secret = $this->s3->secret;
|
||||||
|
// // $region = $this->s3->region;
|
||||||
|
// $bucket = $this->s3->bucket;
|
||||||
|
// $endpoint = $this->s3->endpoint;
|
||||||
|
// $this->s3->testConnection(shouldSave: true);
|
||||||
|
// $configName = new Cuid2;
|
||||||
|
|
||||||
|
// $s3_copy_dir = str($this->backup_location)->replace(backup_dir(), '/var/www/html/storage/app/backups/');
|
||||||
|
// $commands[] = "docker exec coolify bash -c 'mc config host add {$configName} {$endpoint} $key $secret'";
|
||||||
|
// $commands[] = "docker exec coolify bash -c 'mc cp $s3_copy_dir {$configName}/{$bucket}{$this->backup_dir}/'";
|
||||||
|
// instant_remote_process($commands, $this->server);
|
||||||
|
// $this->add_to_backup_output('Uploaded to S3.');
|
||||||
|
// } catch (\Throwable $e) {
|
||||||
|
// $this->add_to_backup_output($e->getMessage());
|
||||||
|
// throw $e;
|
||||||
|
// } finally {
|
||||||
|
// $removeConfigCommands[] = "docker exec coolify bash -c 'mc config remove {$configName}'";
|
||||||
|
// $removeConfigCommands[] = "docker exec coolify bash -c 'mc alias rm {$configName}'";
|
||||||
|
// instant_remote_process($removeConfigCommands, $this->server, false);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
private function upload_to_s3(): void
|
private function upload_to_s3(): void
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
ray($this->backup_location);
|
|
||||||
if (is_null($this->s3)) {
|
if (is_null($this->s3)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -491,20 +508,64 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
|||||||
$bucket = $this->s3->bucket;
|
$bucket = $this->s3->bucket;
|
||||||
$endpoint = $this->s3->endpoint;
|
$endpoint = $this->s3->endpoint;
|
||||||
$this->s3->testConnection(shouldSave: true);
|
$this->s3->testConnection(shouldSave: true);
|
||||||
$configName = new Cuid2;
|
if (data_get($this->backup, 'database_type') === 'App\Models\ServiceDatabase') {
|
||||||
|
$network = $this->database->service->destination->network;
|
||||||
|
} else {
|
||||||
|
$network = $this->database->destination->network;
|
||||||
|
}
|
||||||
|
|
||||||
$s3_copy_dir = str($this->backup_location)->replace(backup_dir(), '/var/www/html/storage/app/backups/');
|
$this->ensureHelperImageAvailable();
|
||||||
$commands[] = "docker exec coolify bash -c 'mc config host add {$configName} {$endpoint} $key $secret'";
|
|
||||||
$commands[] = "docker exec coolify bash -c 'mc cp $s3_copy_dir {$configName}/{$bucket}{$this->backup_dir}/'";
|
$fullImageName = $this->getFullImageName();
|
||||||
|
$commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $this->backup_location:$this->backup_location:ro {$fullImageName}";
|
||||||
|
$commands[] = "docker exec backup-of-{$this->backup->uuid} mc config host add temporary {$endpoint} $key $secret";
|
||||||
|
$commands[] = "docker exec backup-of-{$this->backup->uuid} mc cp $this->backup_location temporary/$bucket{$this->backup_dir}/";
|
||||||
instant_remote_process($commands, $this->server);
|
instant_remote_process($commands, $this->server);
|
||||||
$this->add_to_backup_output('Uploaded to S3.');
|
$this->add_to_backup_output('Uploaded to S3.');
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$this->add_to_backup_output($e->getMessage());
|
$this->add_to_backup_output($e->getMessage());
|
||||||
throw $e;
|
throw $e;
|
||||||
} finally {
|
} finally {
|
||||||
$removeConfigCommands[] = "docker exec coolify bash -c 'mc config remove {$configName}'";
|
$command = "docker rm -f backup-of-{$this->backup->uuid}";
|
||||||
$removeConfigCommands[] = "docker exec coolify bash -c 'mc alias rm {$configName}'";
|
instant_remote_process([$command], $this->server);
|
||||||
instant_remote_process($removeConfigCommands, $this->server, false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function ensureHelperImageAvailable(): void
|
||||||
|
{
|
||||||
|
$fullImageName = $this->getFullImageName();
|
||||||
|
|
||||||
|
$imageExists = $this->checkImageExists($fullImageName);
|
||||||
|
|
||||||
|
if (! $imageExists) {
|
||||||
|
$this->pullHelperImage($fullImageName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function checkImageExists(string $fullImageName): bool
|
||||||
|
{
|
||||||
|
$result = instant_remote_process(["docker image inspect {$fullImageName} >/dev/null 2>&1 && echo 'exists' || echo 'not exists'"], $this->server, false);
|
||||||
|
|
||||||
|
return trim($result) === 'exists';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function pullHelperImage(string $fullImageName): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
instant_remote_process(["docker pull {$fullImageName}"], $this->server);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$errorMessage = 'Failed to pull helper image: '.$e->getMessage();
|
||||||
|
$this->add_to_backup_output($errorMessage);
|
||||||
|
throw new \RuntimeException($errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getFullImageName(): string
|
||||||
|
{
|
||||||
|
$settings = InstanceSettings::get();
|
||||||
|
$helperImage = config('coolify.helper_image');
|
||||||
|
$latestVersion = $settings->helper_version;
|
||||||
|
|
||||||
|
return "{$helperImage}:{$latestVersion}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Jobs;
|
|
||||||
|
|
||||||
use App\Models\ScheduledDatabaseBackup;
|
|
||||||
use App\Models\Team;
|
|
||||||
use App\Notifications\Database\DailyBackup;
|
|
||||||
use Illuminate\Bus\Queueable;
|
|
||||||
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
|
||||||
use Illuminate\Queue\InteractsWithQueue;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
|
||||||
|
|
||||||
class DatabaseBackupStatusJob implements ShouldBeEncrypted, ShouldQueue
|
|
||||||
{
|
|
||||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
|
||||||
|
|
||||||
public $tries = 1;
|
|
||||||
|
|
||||||
public function __construct() {}
|
|
||||||
|
|
||||||
public function handle()
|
|
||||||
{
|
|
||||||
// $teams = Team::all();
|
|
||||||
// foreach ($teams as $team) {
|
|
||||||
// $scheduled_backups = $team->scheduledDatabaseBackups()->get();
|
|
||||||
// if ($scheduled_backups->isEmpty()) {
|
|
||||||
// continue;
|
|
||||||
// }
|
|
||||||
// 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');
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// $scheduled_backups = ScheduledDatabaseBackup::all();
|
|
||||||
// $databases = collect();
|
|
||||||
// $teams = 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;
|
|
||||||
// $team = $database->team();
|
|
||||||
// $teams->put($team->id, $team);
|
|
||||||
// $databases->put("{$team->id}:{$database->name}", [
|
|
||||||
// 'failed_count' => $failed->count(),
|
|
||||||
// ]);
|
|
||||||
// }
|
|
||||||
// foreach ($databases as $name => $database) {
|
|
||||||
// [$team_id, $name] = explode(':', $name);
|
|
||||||
// $team = $teams->get($team_id);
|
|
||||||
// $team?->notify(new DailyBackup($databases));
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,6 +4,7 @@ namespace App\Jobs;
|
|||||||
|
|
||||||
use App\Actions\Application\StopApplication;
|
use App\Actions\Application\StopApplication;
|
||||||
use App\Actions\Database\StopDatabase;
|
use App\Actions\Database\StopDatabase;
|
||||||
|
use App\Actions\Server\CleanupDocker;
|
||||||
use App\Actions\Service\DeleteService;
|
use App\Actions\Service\DeleteService;
|
||||||
use App\Actions\Service\StopService;
|
use App\Actions\Service\StopService;
|
||||||
use App\Models\Application;
|
use App\Models\Application;
|
||||||
@@ -30,8 +31,11 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
|
|||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource,
|
public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource,
|
||||||
public bool $deleteConfigurations = false,
|
public bool $deleteConfigurations,
|
||||||
public bool $deleteVolumes = false) {}
|
public bool $deleteVolumes,
|
||||||
|
public bool $dockerCleanup,
|
||||||
|
public bool $deleteConnectedNetworks
|
||||||
|
) {}
|
||||||
|
|
||||||
public function handle()
|
public function handle()
|
||||||
{
|
{
|
||||||
@@ -51,11 +55,11 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
|
|||||||
case 'standalone-dragonfly':
|
case 'standalone-dragonfly':
|
||||||
case 'standalone-clickhouse':
|
case 'standalone-clickhouse':
|
||||||
$persistentStorages = $this->resource?->persistentStorages()?->get();
|
$persistentStorages = $this->resource?->persistentStorages()?->get();
|
||||||
StopDatabase::run($this->resource);
|
StopDatabase::run($this->resource, true);
|
||||||
break;
|
break;
|
||||||
case 'service':
|
case 'service':
|
||||||
StopService::run($this->resource);
|
StopService::run($this->resource, true);
|
||||||
DeleteService::run($this->resource);
|
DeleteService::run($this->resource, $this->deleteConfigurations, $this->deleteVolumes, $this->dockerCleanup, $this->deleteConnectedNetworks);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,12 +69,31 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
|
|||||||
if ($this->deleteConfigurations) {
|
if ($this->deleteConfigurations) {
|
||||||
$this->resource?->delete_configurations();
|
$this->resource?->delete_configurations();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$isDatabase = $this->resource instanceof StandalonePostgresql
|
||||||
|
|| $this->resource instanceof StandaloneRedis
|
||||||
|
|| $this->resource instanceof StandaloneMongodb
|
||||||
|
|| $this->resource instanceof StandaloneMysql
|
||||||
|
|| $this->resource instanceof StandaloneMariadb
|
||||||
|
|| $this->resource instanceof StandaloneKeydb
|
||||||
|
|| $this->resource instanceof StandaloneDragonfly
|
||||||
|
|| $this->resource instanceof StandaloneClickhouse;
|
||||||
|
$server = data_get($this->resource, 'server') ?? data_get($this->resource, 'destination.server');
|
||||||
|
if (($this->dockerCleanup || $isDatabase) && $server) {
|
||||||
|
CleanupDocker::dispatch($server, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->deleteConnectedNetworks && ! $isDatabase) {
|
||||||
|
$this->resource?->delete_connected_networks($this->resource->uuid);
|
||||||
|
}
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
ray($e->getMessage());
|
|
||||||
send_internal_notification('ContainerStoppingJob failed with: '.$e->getMessage());
|
send_internal_notification('ContainerStoppingJob failed with: '.$e->getMessage());
|
||||||
throw $e;
|
throw $e;
|
||||||
} finally {
|
} finally {
|
||||||
$this->resource->forceDelete();
|
$this->resource->forceDelete();
|
||||||
|
if ($this->dockerCleanup) {
|
||||||
|
CleanupDocker::dispatch($server, true);
|
||||||
|
}
|
||||||
Artisan::queue('cleanup:stucked-resources');
|
Artisan::queue('cleanup:stucked-resources');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
|||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
use Illuminate\Queue\InteractsWithQueue;
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
@@ -26,16 +25,6 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
|
|||||||
|
|
||||||
public function __construct(public Server $server) {}
|
public function __construct(public Server $server) {}
|
||||||
|
|
||||||
public function middleware(): array
|
|
||||||
{
|
|
||||||
return [new WithoutOverlapping($this->server->id)];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function uniqueId(): int
|
|
||||||
{
|
|
||||||
return $this->server->id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function handle(): void
|
public function handle(): void
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
|||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
use Illuminate\Queue\InteractsWithQueue;
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
use Illuminate\Support\Facades\Http;
|
use Illuminate\Support\Facades\Http;
|
||||||
|
|
||||||
@@ -25,16 +24,6 @@ class GithubAppPermissionJob implements ShouldBeEncrypted, ShouldQueue
|
|||||||
|
|
||||||
public function __construct(public GithubApp $github_app) {}
|
public function __construct(public GithubApp $github_app) {}
|
||||||
|
|
||||||
public function middleware(): array
|
|
||||||
{
|
|
||||||
return [(new WithoutOverlapping($this->github_app->uuid))];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function uniqueId(): int
|
|
||||||
{
|
|
||||||
return $this->github_app->uuid;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function handle()
|
public function handle()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
|||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
use Illuminate\Queue\InteractsWithQueue;
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
use Illuminate\Support\Facades\Http;
|
use Illuminate\Support\Facades\Http;
|
||||||
|
|
||||||
@@ -19,17 +18,7 @@ class PullHelperImageJob implements ShouldBeEncrypted, ShouldQueue
|
|||||||
|
|
||||||
public $timeout = 1000;
|
public $timeout = 1000;
|
||||||
|
|
||||||
public function middleware(): array
|
public function __construct() {}
|
||||||
{
|
|
||||||
return [(new WithoutOverlapping($this->server->uuid))];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function uniqueId(): string
|
|
||||||
{
|
|
||||||
return $this->server->uuid;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function __construct(public Server $server) {}
|
|
||||||
|
|
||||||
public function handle(): void
|
public function handle(): void
|
||||||
{
|
{
|
||||||
@@ -42,8 +31,8 @@ class PullHelperImageJob implements ShouldBeEncrypted, ShouldQueue
|
|||||||
$current_version = $settings->helper_version;
|
$current_version = $settings->helper_version;
|
||||||
if (version_compare($latest_version, $current_version, '>')) {
|
if (version_compare($latest_version, $current_version, '>')) {
|
||||||
// New version available
|
// New version available
|
||||||
$helperImage = config('coolify.helper_image');
|
// $helperImage = config('coolify.helper_image');
|
||||||
instant_remote_process(["docker pull -q {$helperImage}:{$latest_version}"], $this->server);
|
// instant_remote_process(["docker pull -q {$helperImage}:{$latest_version}"], $this->server);
|
||||||
$settings->update(['helper_version' => $latest_version]);
|
$settings->update(['helper_version' => $latest_version]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
|||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
use Illuminate\Queue\InteractsWithQueue;
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
class PullSentinelImageJob implements ShouldBeEncrypted, ShouldQueue
|
class PullSentinelImageJob implements ShouldBeEncrypted, ShouldQueue
|
||||||
@@ -18,16 +17,6 @@ class PullSentinelImageJob implements ShouldBeEncrypted, ShouldQueue
|
|||||||
|
|
||||||
public $timeout = 1000;
|
public $timeout = 1000;
|
||||||
|
|
||||||
public function middleware(): array
|
|
||||||
{
|
|
||||||
return [(new WithoutOverlapping($this->server->uuid))];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function uniqueId(): string
|
|
||||||
{
|
|
||||||
return $this->server->uuid;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function __construct(public Server $server) {}
|
public function __construct(public Server $server) {}
|
||||||
|
|
||||||
public function handle(): void
|
public function handle(): void
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ use Illuminate\Bus\Queueable;
|
|||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
use Illuminate\Queue\InteractsWithQueue;
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
class ScheduledTaskJob implements ShouldQueue
|
class ScheduledTaskJob implements ShouldQueue
|
||||||
@@ -56,24 +55,17 @@ class ScheduledTaskJob implements ShouldQueue
|
|||||||
{
|
{
|
||||||
if ($this->resource instanceof Application) {
|
if ($this->resource instanceof Application) {
|
||||||
$timezone = $this->resource->destination->server->settings->server_timezone;
|
$timezone = $this->resource->destination->server->settings->server_timezone;
|
||||||
|
|
||||||
return $timezone;
|
return $timezone;
|
||||||
} elseif ($this->resource instanceof Service) {
|
} elseif ($this->resource instanceof Service) {
|
||||||
$timezone = $this->resource->server->settings->server_timezone;
|
$timezone = $this->resource->server->settings->server_timezone;
|
||||||
|
|
||||||
return $timezone;
|
return $timezone;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'UTC';
|
return 'UTC';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function middleware(): array
|
|
||||||
{
|
|
||||||
return [new WithoutOverlapping($this->task->id)];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function uniqueId(): int
|
|
||||||
{
|
|
||||||
return $this->task->id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function handle(): void
|
public function handle(): void
|
||||||
{
|
{
|
||||||
|
|
||||||
@@ -94,12 +86,12 @@ class ScheduledTaskJob implements ShouldQueue
|
|||||||
} elseif ($this->resource->type() == 'service') {
|
} elseif ($this->resource->type() == 'service') {
|
||||||
$this->resource->applications()->get()->each(function ($application) {
|
$this->resource->applications()->get()->each(function ($application) {
|
||||||
if (str(data_get($application, 'status'))->contains('running')) {
|
if (str(data_get($application, 'status'))->contains('running')) {
|
||||||
$this->containers[] = data_get($application, 'name') . '-' . data_get($this->resource, 'uuid');
|
$this->containers[] = data_get($application, 'name').'-'.data_get($this->resource, 'uuid');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
$this->resource->databases()->get()->each(function ($database) {
|
$this->resource->databases()->get()->each(function ($database) {
|
||||||
if (str(data_get($database, 'status'))->contains('running')) {
|
if (str(data_get($database, 'status'))->contains('running')) {
|
||||||
$this->containers[] = data_get($database, 'name') . '-' . data_get($this->resource, 'uuid');
|
$this->containers[] = data_get($database, 'name').'-'.data_get($this->resource, 'uuid');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -112,8 +104,8 @@ class ScheduledTaskJob implements ShouldQueue
|
|||||||
}
|
}
|
||||||
|
|
||||||
foreach ($this->containers as $containerName) {
|
foreach ($this->containers as $containerName) {
|
||||||
if (count($this->containers) == 1 || str_starts_with($containerName, $this->task->container . '-' . $this->resource->uuid)) {
|
if (count($this->containers) == 1 || str_starts_with($containerName, $this->task->container.'-'.$this->resource->uuid)) {
|
||||||
$cmd = "sh -c '" . str_replace("'", "'\''", $this->task->command) . "'";
|
$cmd = "sh -c '".str_replace("'", "'\''", $this->task->command)."'";
|
||||||
$exec = "docker exec {$containerName} {$cmd}";
|
$exec = "docker exec {$containerName} {$cmd}";
|
||||||
$this->task_output = instant_remote_process([$exec], $this->server, true);
|
$this->task_output = instant_remote_process([$exec], $this->server, true);
|
||||||
$this->task_log->update([
|
$this->task_log->update([
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
|||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
use Illuminate\Queue\InteractsWithQueue;
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
use Illuminate\Support\Arr;
|
use Illuminate\Support\Arr;
|
||||||
|
|
||||||
@@ -24,7 +23,7 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
|
|||||||
{
|
{
|
||||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
public $tries = 3;
|
public $tries = 1;
|
||||||
|
|
||||||
public $timeout = 60;
|
public $timeout = 60;
|
||||||
|
|
||||||
@@ -45,16 +44,6 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
|
|||||||
|
|
||||||
public function __construct(public Server $server) {}
|
public function __construct(public Server $server) {}
|
||||||
|
|
||||||
public function middleware(): array
|
|
||||||
{
|
|
||||||
return [(new WithoutOverlapping($this->server->id))];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function uniqueId(): int
|
|
||||||
{
|
|
||||||
return $this->server->id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function handle()
|
public function handle()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
@@ -93,7 +82,7 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
|
|||||||
|
|
||||||
private function serverStatus()
|
private function serverStatus()
|
||||||
{
|
{
|
||||||
['uptime' => $uptime] = $this->server->validateConnection();
|
['uptime' => $uptime] = $this->server->validateConnection(false);
|
||||||
if ($uptime) {
|
if ($uptime) {
|
||||||
if ($this->server->unreachable_notification_sent === true) {
|
if ($this->server->unreachable_notification_sent === true) {
|
||||||
$this->server->update(['unreachable_notification_sent' => false]);
|
$this->server->update(['unreachable_notification_sent' => false]);
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
|||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
use Illuminate\Queue\InteractsWithQueue;
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
use Illuminate\Queue\Middleware\;
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
class ServerLimitCheckJob implements ShouldBeEncrypted, ShouldQueue
|
class ServerLimitCheckJob implements ShouldBeEncrypted, ShouldQueue
|
||||||
@@ -26,16 +26,6 @@ class ServerLimitCheckJob implements ShouldBeEncrypted, ShouldQueue
|
|||||||
|
|
||||||
public function __construct(public Team $team) {}
|
public function __construct(public Team $team) {}
|
||||||
|
|
||||||
public function middleware(): array
|
|
||||||
{
|
|
||||||
return [(new WithoutOverlapping($this->team->uuid))];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function uniqueId(): int
|
|
||||||
{
|
|
||||||
return $this->team->uuid;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function handle()
|
public function handle()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
|||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
use Illuminate\Queue\InteractsWithQueue;
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
class ServerStatusJob implements ShouldBeEncrypted, ShouldQueue
|
class ServerStatusJob implements ShouldBeEncrypted, ShouldQueue
|
||||||
@@ -26,16 +25,6 @@ class ServerStatusJob implements ShouldBeEncrypted, ShouldQueue
|
|||||||
|
|
||||||
public function __construct(public Server $server) {}
|
public function __construct(public Server $server) {}
|
||||||
|
|
||||||
public function middleware(): array
|
|
||||||
{
|
|
||||||
return [(new WithoutOverlapping($this->server->uuid))];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function uniqueId(): int
|
|
||||||
{
|
|
||||||
return $this->server->uuid;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function handle()
|
public function handle()
|
||||||
{
|
{
|
||||||
if (! $this->server->isServerReady($this->tries)) {
|
if (! $this->server->isServerReady($this->tries)) {
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
|
|||||||
if (! $this->createdServer) {
|
if (! $this->createdServer) {
|
||||||
return $this->dispatch('error', 'Localhost server is not found. Something went wrong during installation. Please try to reinstall or contact support.');
|
return $this->dispatch('error', 'Localhost server is not found. Something went wrong during installation. Please try to reinstall or contact support.');
|
||||||
}
|
}
|
||||||
$this->serverPublicKey = $this->createdServer->privateKey->publicKey();
|
$this->serverPublicKey = $this->createdServer->privateKey->getPublicKey();
|
||||||
|
|
||||||
return $this->validateServer('localhost');
|
return $this->validateServer('localhost');
|
||||||
} elseif ($this->selectedServerType === 'remote') {
|
} elseif ($this->selectedServerType === 'remote') {
|
||||||
@@ -175,7 +175,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$this->selectedExistingPrivateKey = $this->createdServer->privateKey->id;
|
$this->selectedExistingPrivateKey = $this->createdServer->privateKey->id;
|
||||||
$this->serverPublicKey = $this->createdServer->privateKey->publicKey();
|
$this->serverPublicKey = $this->createdServer->privateKey->getPublicKey();
|
||||||
$this->updateServerDetails();
|
$this->updateServerDetails();
|
||||||
$this->currentState = 'validate-server';
|
$this->currentState = 'validate-server';
|
||||||
}
|
}
|
||||||
@@ -231,17 +231,24 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
|
|||||||
public function savePrivateKey()
|
public function savePrivateKey()
|
||||||
{
|
{
|
||||||
$this->validate([
|
$this->validate([
|
||||||
'privateKeyName' => 'required',
|
'privateKeyName' => 'required|string|max:255',
|
||||||
'privateKey' => 'required',
|
'privateKeyDescription' => 'nullable|string|max:255',
|
||||||
|
'privateKey' => 'required|string',
|
||||||
]);
|
]);
|
||||||
$this->createdPrivateKey = PrivateKey::create([
|
|
||||||
'name' => $this->privateKeyName,
|
try {
|
||||||
'description' => $this->privateKeyDescription,
|
$privateKey = PrivateKey::createAndStore([
|
||||||
'private_key' => $this->privateKey,
|
'name' => $this->privateKeyName,
|
||||||
'team_id' => currentTeam()->id,
|
'description' => $this->privateKeyDescription,
|
||||||
]);
|
'private_key' => $this->privateKey,
|
||||||
$this->createdPrivateKey->save();
|
'team_id' => currentTeam()->id,
|
||||||
$this->currentState = 'create-server';
|
]);
|
||||||
|
|
||||||
|
$this->createdPrivateKey = $privateKey;
|
||||||
|
$this->currentState = 'create-server';
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->addError('privateKey', 'Failed to save private key: '.$e->getMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function saveServer()
|
public function saveServer()
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Livewire\CommandCenter;
|
|
||||||
|
|
||||||
use App\Models\Server;
|
|
||||||
use Livewire\Component;
|
|
||||||
|
|
||||||
class Index extends Component
|
|
||||||
{
|
|
||||||
public $servers = [];
|
|
||||||
|
|
||||||
public function mount()
|
|
||||||
{
|
|
||||||
$this->servers = Server::isReachable()->get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function render()
|
|
||||||
{
|
|
||||||
return view('livewire.command-center.index');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -30,7 +30,6 @@ class Dashboard extends Component
|
|||||||
|
|
||||||
public function cleanup_queue()
|
public function cleanup_queue()
|
||||||
{
|
{
|
||||||
$this->dispatch('success', 'Cleanup started.');
|
|
||||||
Artisan::queue('cleanup:application-deployment-queue', [
|
Artisan::queue('cleanup:application-deployment-queue', [
|
||||||
'--team-id' => currentTeam()->id,
|
'--team-id' => currentTeam()->id,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class Form extends Component
|
|||||||
}
|
}
|
||||||
$this->destination->delete();
|
$this->destination->delete();
|
||||||
|
|
||||||
return redirect()->route('dashboard');
|
return redirect()->route('destination.all');
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
return handleError($e, $this);
|
return handleError($e, $this);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,28 @@
|
|||||||
|
|
||||||
namespace App\Livewire;
|
namespace App\Livewire;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
class NavbarDeleteTeam extends Component
|
class NavbarDeleteTeam extends Component
|
||||||
{
|
{
|
||||||
public function delete()
|
public $team;
|
||||||
|
|
||||||
|
public function mount()
|
||||||
{
|
{
|
||||||
|
$this->team = currentTeam()->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete($password)
|
||||||
|
{
|
||||||
|
if (! Hash::check($password, Auth::user()->password)) {
|
||||||
|
$this->addError('password', 'The provided password is incorrect.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$currentTeam = currentTeam();
|
$currentTeam = currentTeam();
|
||||||
$currentTeam->delete();
|
$currentTeam->delete();
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ namespace App\Livewire\Project\Application\Deployment;
|
|||||||
|
|
||||||
use App\Models\Application;
|
use App\Models\Application;
|
||||||
use App\Models\ApplicationDeploymentQueue;
|
use App\Models\ApplicationDeploymentQueue;
|
||||||
use Illuminate\Support\Collection;
|
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
class Show extends Component
|
class Show extends Component
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ class Heading extends Component
|
|||||||
|
|
||||||
protected string $deploymentUuid;
|
protected string $deploymentUuid;
|
||||||
|
|
||||||
|
public bool $docker_cleanup = true;
|
||||||
|
|
||||||
public function getListeners()
|
public function getListeners()
|
||||||
{
|
{
|
||||||
$teamId = auth()->user()->currentTeam()->id;
|
$teamId = auth()->user()->currentTeam()->id;
|
||||||
@@ -102,7 +104,7 @@ class Heading extends Component
|
|||||||
|
|
||||||
public function stop()
|
public function stop()
|
||||||
{
|
{
|
||||||
StopApplication::run($this->application);
|
StopApplication::run($this->application, false, $this->docker_cleanup);
|
||||||
$this->application->status = 'exited';
|
$this->application->status = 'exited';
|
||||||
$this->application->save();
|
$this->application->save();
|
||||||
if ($this->application->additional_servers->count() > 0) {
|
if ($this->application->additional_servers->count() > 0) {
|
||||||
@@ -135,4 +137,13 @@ class Heading extends Component
|
|||||||
'environment_name' => $this->parameters['environment_name'],
|
'environment_name' => $this->parameters['environment_name'],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return view('livewire.project.application.heading', [
|
||||||
|
'checkboxes' => [
|
||||||
|
['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ namespace App\Livewire\Project\Application;
|
|||||||
use App\Actions\Docker\GetContainersStatus;
|
use App\Actions\Docker\GetContainersStatus;
|
||||||
use App\Models\Application;
|
use App\Models\Application;
|
||||||
use App\Models\ApplicationPreview;
|
use App\Models\ApplicationPreview;
|
||||||
|
use Illuminate\Process\InvokedProcess;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Facades\Process;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
use Spatie\Url\Url;
|
use Spatie\Url\Url;
|
||||||
use Visus\Cuid2\Cuid2;
|
use Visus\Cuid2\Cuid2;
|
||||||
@@ -184,17 +186,20 @@ class Previews extends Component
|
|||||||
public function stop(int $pull_request_id)
|
public function stop(int $pull_request_id)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
$server = $this->application->destination->server;
|
||||||
|
$timeout = 300;
|
||||||
|
|
||||||
if ($this->application->destination->server->isSwarm()) {
|
if ($this->application->destination->server->isSwarm()) {
|
||||||
instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $this->application->destination->server);
|
instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $server);
|
||||||
} else {
|
} else {
|
||||||
$containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id, $pull_request_id);
|
$containers = getCurrentApplicationContainerStatus($server, $this->application->id, $pull_request_id)->toArray();
|
||||||
foreach ($containers as $container) {
|
$this->stopContainers($containers, $server, $timeout);
|
||||||
$name = str_replace('/', '', $container['Names']);
|
|
||||||
instant_remote_process(["docker rm -f $name"], $this->application->destination->server, throwError: false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
GetContainersStatus::dispatchSync($this->application->destination->server)->onQueue('high');
|
|
||||||
$this->dispatch('reloadWindow');
|
GetContainersStatus::run($server);
|
||||||
|
$this->application->refresh();
|
||||||
|
$this->dispatch('containerStatusUpdated');
|
||||||
|
$this->dispatch('success', 'Preview Deployment stopped.');
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
return handleError($e, $this);
|
return handleError($e, $this);
|
||||||
}
|
}
|
||||||
@@ -203,16 +208,21 @@ class Previews extends Component
|
|||||||
public function delete(int $pull_request_id)
|
public function delete(int $pull_request_id)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
$server = $this->application->destination->server;
|
||||||
|
$timeout = 300;
|
||||||
|
|
||||||
if ($this->application->destination->server->isSwarm()) {
|
if ($this->application->destination->server->isSwarm()) {
|
||||||
instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $this->application->destination->server);
|
instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $server);
|
||||||
} else {
|
} else {
|
||||||
$containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id, $pull_request_id);
|
$containers = getCurrentApplicationContainerStatus($server, $this->application->id, $pull_request_id)->toArray();
|
||||||
foreach ($containers as $container) {
|
$this->stopContainers($containers, $server, $timeout);
|
||||||
$name = str_replace('/', '', $container['Names']);
|
|
||||||
instant_remote_process(["docker rm -f $name"], $this->application->destination->server, throwError: false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first()->delete();
|
|
||||||
|
ApplicationPreview::where('application_id', $this->application->id)
|
||||||
|
->where('pull_request_id', $pull_request_id)
|
||||||
|
->first()
|
||||||
|
->delete();
|
||||||
|
|
||||||
$this->application->refresh();
|
$this->application->refresh();
|
||||||
$this->dispatch('update_links');
|
$this->dispatch('update_links');
|
||||||
$this->dispatch('success', 'Preview deleted.');
|
$this->dispatch('success', 'Preview deleted.');
|
||||||
@@ -220,4 +230,49 @@ class Previews extends Component
|
|||||||
return handleError($e, $this);
|
return handleError($e, $this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function stopContainers(array $containers, $server, int $timeout)
|
||||||
|
{
|
||||||
|
$processes = [];
|
||||||
|
foreach ($containers as $container) {
|
||||||
|
$containerName = str_replace('/', '', $container['Names']);
|
||||||
|
$processes[$containerName] = $this->stopContainer($containerName, $timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
$startTime = time();
|
||||||
|
while (count($processes) > 0) {
|
||||||
|
$finishedProcesses = array_filter($processes, function ($process) {
|
||||||
|
return ! $process->running();
|
||||||
|
});
|
||||||
|
foreach (array_keys($finishedProcesses) as $containerName) {
|
||||||
|
unset($processes[$containerName]);
|
||||||
|
$this->removeContainer($containerName, $server);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (time() - $startTime >= $timeout) {
|
||||||
|
$this->forceStopRemainingContainers(array_keys($processes), $server);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
usleep(100000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function stopContainer(string $containerName, int $timeout): InvokedProcess
|
||||||
|
{
|
||||||
|
return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
|
||||||
|
}
|
||||||
|
|
||||||
|
private function removeContainer(string $containerName, $server)
|
||||||
|
{
|
||||||
|
instant_remote_process(["docker rm -f $containerName"], $server, throwError: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function forceStopRemainingContainers(array $containerNames, $server)
|
||||||
|
{
|
||||||
|
foreach ($containerNames as $containerName) {
|
||||||
|
instant_remote_process(["docker kill $containerName"], $server, throwError: false);
|
||||||
|
$this->removeContainer($containerName, $server);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
namespace App\Livewire\Project\Database;
|
namespace App\Livewire\Project\Database;
|
||||||
|
|
||||||
use App\Models\ScheduledDatabaseBackup;
|
use App\Models\ScheduledDatabaseBackup;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
use Spatie\Url\Url;
|
use Spatie\Url\Url;
|
||||||
|
|
||||||
@@ -12,6 +14,12 @@ class BackupEdit extends Component
|
|||||||
|
|
||||||
public $s3s;
|
public $s3s;
|
||||||
|
|
||||||
|
public bool $delete_associated_backups_locally = false;
|
||||||
|
|
||||||
|
public bool $delete_associated_backups_s3 = false;
|
||||||
|
|
||||||
|
public bool $delete_associated_backups_sftp = false;
|
||||||
|
|
||||||
public ?string $status = null;
|
public ?string $status = null;
|
||||||
|
|
||||||
public array $parameters;
|
public array $parameters;
|
||||||
@@ -46,10 +54,24 @@ class BackupEdit extends Component
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete()
|
public function delete($password)
|
||||||
{
|
{
|
||||||
|
if (! Hash::check($password, Auth::user()->password)) {
|
||||||
|
$this->addError('password', 'The provided password is incorrect.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if ($this->delete_associated_backups_locally) {
|
||||||
|
$this->deleteAssociatedBackupsLocally();
|
||||||
|
}
|
||||||
|
if ($this->delete_associated_backups_s3) {
|
||||||
|
$this->deleteAssociatedBackupsS3();
|
||||||
|
}
|
||||||
|
|
||||||
$this->backup->delete();
|
$this->backup->delete();
|
||||||
|
|
||||||
if ($this->backup->database->getMorphClass() === 'App\Models\ServiceDatabase') {
|
if ($this->backup->database->getMorphClass() === 'App\Models\ServiceDatabase') {
|
||||||
$previousUrl = url()->previous();
|
$previousUrl = url()->previous();
|
||||||
$url = Url::fromString($previousUrl);
|
$url = Url::fromString($previousUrl);
|
||||||
@@ -104,4 +126,66 @@ class BackupEdit extends Component
|
|||||||
$this->dispatch('error', $e->getMessage());
|
$this->dispatch('error', $e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function deleteAssociatedBackupsLocally()
|
||||||
|
{
|
||||||
|
$executions = $this->backup->executions;
|
||||||
|
$backupFolder = null;
|
||||||
|
|
||||||
|
foreach ($executions as $execution) {
|
||||||
|
if ($this->backup->database->getMorphClass() === 'App\Models\ServiceDatabase') {
|
||||||
|
$server = $this->backup->database->service->destination->server;
|
||||||
|
} else {
|
||||||
|
$server = $this->backup->database->destination->server;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $backupFolder) {
|
||||||
|
$backupFolder = dirname($execution->filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete_backup_locally($execution->filename, $server);
|
||||||
|
$execution->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($backupFolder) {
|
||||||
|
$this->deleteEmptyBackupFolder($backupFolder, $server);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleteAssociatedBackupsS3()
|
||||||
|
{
|
||||||
|
//Add function to delete backups from S3
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleteAssociatedBackupsSftp()
|
||||||
|
{
|
||||||
|
//Add function to delete backups from SFTP
|
||||||
|
}
|
||||||
|
|
||||||
|
private function deleteEmptyBackupFolder($folderPath, $server)
|
||||||
|
{
|
||||||
|
$checkEmpty = instant_remote_process(["[ -z \"$(ls -A '$folderPath')\" ] && echo 'empty' || echo 'not empty'"], $server);
|
||||||
|
|
||||||
|
if (trim($checkEmpty) === 'empty') {
|
||||||
|
instant_remote_process(["rmdir '$folderPath'"], $server);
|
||||||
|
|
||||||
|
$parentFolder = dirname($folderPath);
|
||||||
|
$checkParentEmpty = instant_remote_process(["[ -z \"$(ls -A '$parentFolder')\" ] && echo 'empty' || echo 'not empty'"], $server);
|
||||||
|
|
||||||
|
if (trim($checkParentEmpty) === 'empty') {
|
||||||
|
instant_remote_process(["rmdir '$parentFolder'"], $server);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return view('livewire.project.database.backup-edit', [
|
||||||
|
'checkboxes' => [
|
||||||
|
['id' => 'delete_associated_backups_locally', 'label' => 'All backups associated with this backup job from this database will be permanently deleted from local storage.'],
|
||||||
|
// ['id' => 'delete_associated_backups_s3', 'label' => 'All backups associated with this backup job from this database will be permanently deleted from the selected S3 Storage.']
|
||||||
|
// ['id' => 'delete_associated_backups_sftp', 'label' => 'All backups associated with this backup job from this database will be permanently deleted from the selected SFTP Storage.']
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,18 +3,28 @@
|
|||||||
namespace App\Livewire\Project\Database;
|
namespace App\Livewire\Project\Database;
|
||||||
|
|
||||||
use App\Models\ScheduledDatabaseBackup;
|
use App\Models\ScheduledDatabaseBackup;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Livewire\Attributes\On;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
class BackupExecutions extends Component
|
class BackupExecutions extends Component
|
||||||
{
|
{
|
||||||
public ?ScheduledDatabaseBackup $backup = null;
|
public ?ScheduledDatabaseBackup $backup = null;
|
||||||
|
|
||||||
public $database;
|
public $database;
|
||||||
|
|
||||||
public $executions = [];
|
public $executions = [];
|
||||||
|
|
||||||
public $setDeletableBackup;
|
public $setDeletableBackup;
|
||||||
|
|
||||||
|
public $delete_backup_s3 = true;
|
||||||
|
|
||||||
|
public $delete_backup_sftp = true;
|
||||||
|
|
||||||
public function getListeners()
|
public function getListeners()
|
||||||
{
|
{
|
||||||
$userId = auth()->user()->id;
|
$userId = Auth::id();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
"echo-private:team.{$userId},BackupCreated" => 'refreshBackupExecutions',
|
"echo-private:team.{$userId},BackupCreated" => 'refreshBackupExecutions',
|
||||||
@@ -31,19 +41,36 @@ class BackupExecutions extends Component
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function deleteBackup($exeuctionId)
|
#[On('deleteBackup')]
|
||||||
|
public function deleteBackup($executionId, $password)
|
||||||
{
|
{
|
||||||
$execution = $this->backup->executions()->where('id', $exeuctionId)->first();
|
if (! Hash::check($password, Auth::user()->password)) {
|
||||||
|
$this->addError('password', 'The provided password is incorrect.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$execution = $this->backup->executions()->where('id', $executionId)->first();
|
||||||
if (is_null($execution)) {
|
if (is_null($execution)) {
|
||||||
$this->dispatch('error', 'Backup execution not found.');
|
$this->dispatch('error', 'Backup execution not found.');
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($execution->scheduledDatabaseBackup->database->getMorphClass() === 'App\Models\ServiceDatabase') {
|
if ($execution->scheduledDatabaseBackup->database->getMorphClass() === 'App\Models\ServiceDatabase') {
|
||||||
delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->service->destination->server);
|
delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->service->destination->server);
|
||||||
} else {
|
} else {
|
||||||
delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->destination->server);
|
delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->destination->server);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($this->delete_backup_s3) {
|
||||||
|
// Add logic to delete from S3
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->delete_backup_sftp) {
|
||||||
|
// Add logic to delete from SFTP
|
||||||
|
}
|
||||||
|
|
||||||
$execution->delete();
|
$execution->delete();
|
||||||
$this->dispatch('success', 'Backup deleted.');
|
$this->dispatch('success', 'Backup deleted.');
|
||||||
$this->refreshBackupExecutions();
|
$this->refreshBackupExecutions();
|
||||||
@@ -82,16 +109,18 @@ class BackupExecutions extends Component
|
|||||||
return $server;
|
return $server;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getServerTimezone()
|
public function getServerTimezone()
|
||||||
{
|
{
|
||||||
$server = $this->server();
|
$server = $this->server();
|
||||||
if (!$server) {
|
if (! $server) {
|
||||||
return 'UTC';
|
return 'UTC';
|
||||||
}
|
}
|
||||||
$serverTimezone = $server->settings->server_timezone;
|
$serverTimezone = $server->settings->server_timezone;
|
||||||
|
|
||||||
return $serverTimezone;
|
return $serverTimezone;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,6 +133,17 @@ class BackupExecutions extends Component
|
|||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
$dateObj->setTimezone(new \DateTimeZone('UTC'));
|
$dateObj->setTimezone(new \DateTimeZone('UTC'));
|
||||||
}
|
}
|
||||||
|
|
||||||
return $dateObj->format('Y-m-d H:i:s T');
|
return $dateObj->format('Y-m-d H:i:s T');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return view('livewire.project.database.backup-executions', [
|
||||||
|
'checkboxes' => [
|
||||||
|
['id' => 'delete_backup_s3', 'label' => 'Delete the selected backup permanently form S3 Storage'],
|
||||||
|
['id' => 'delete_backup_sftp', 'label' => 'Delete the selected backup permanently form SFTP Storage'],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ class General extends Component
|
|||||||
public function instantSaveAdvanced()
|
public function instantSaveAdvanced()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
if (!$this->server->isLogDrainEnabled()) {
|
if (! $this->server->isLogDrainEnabled()) {
|
||||||
$this->database->is_log_drain_enabled = false;
|
$this->database->is_log_drain_enabled = false;
|
||||||
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
|
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
|
||||||
|
|
||||||
@@ -73,14 +73,14 @@ class General extends Component
|
|||||||
public function instantSave()
|
public function instantSave()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
if ($this->database->is_public && !$this->database->public_port) {
|
if ($this->database->is_public && ! $this->database->public_port) {
|
||||||
$this->dispatch('error', 'Public port is required.');
|
$this->dispatch('error', 'Public port is required.');
|
||||||
$this->database->is_public = false;
|
$this->database->is_public = false;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if ($this->database->is_public) {
|
if ($this->database->is_public) {
|
||||||
if (!str($this->database->status)->startsWith('running')) {
|
if (! str($this->database->status)->startsWith('running')) {
|
||||||
$this->dispatch('error', 'Database must be started to be publicly accessible.');
|
$this->dispatch('error', 'Database must be started to be publicly accessible.');
|
||||||
$this->database->is_public = false;
|
$this->database->is_public = false;
|
||||||
|
|
||||||
@@ -95,7 +95,7 @@ class General extends Component
|
|||||||
$this->db_url_public = $this->database->external_db_url;
|
$this->db_url_public = $this->database->external_db_url;
|
||||||
$this->database->save();
|
$this->database->save();
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$this->database->is_public = !$this->database->is_public;
|
$this->database->is_public = ! $this->database->is_public;
|
||||||
|
|
||||||
return handleError($e, $this);
|
return handleError($e, $this);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ class General extends Component
|
|||||||
public function instantSaveAdvanced()
|
public function instantSaveAdvanced()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
if (!$this->server->isLogDrainEnabled()) {
|
if (! $this->server->isLogDrainEnabled()) {
|
||||||
$this->database->is_log_drain_enabled = false;
|
$this->database->is_log_drain_enabled = false;
|
||||||
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
|
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
|
||||||
|
|
||||||
@@ -88,14 +88,14 @@ class General extends Component
|
|||||||
public function instantSave()
|
public function instantSave()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
if ($this->database->is_public && !$this->database->public_port) {
|
if ($this->database->is_public && ! $this->database->public_port) {
|
||||||
$this->dispatch('error', 'Public port is required.');
|
$this->dispatch('error', 'Public port is required.');
|
||||||
$this->database->is_public = false;
|
$this->database->is_public = false;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if ($this->database->is_public) {
|
if ($this->database->is_public) {
|
||||||
if (!str($this->database->status)->startsWith('running')) {
|
if (! str($this->database->status)->startsWith('running')) {
|
||||||
$this->dispatch('error', 'Database must be started to be publicly accessible.');
|
$this->dispatch('error', 'Database must be started to be publicly accessible.');
|
||||||
$this->database->is_public = false;
|
$this->database->is_public = false;
|
||||||
|
|
||||||
@@ -110,7 +110,7 @@ class General extends Component
|
|||||||
$this->db_url_public = $this->database->external_db_url;
|
$this->db_url_public = $this->database->external_db_url;
|
||||||
$this->database->save();
|
$this->database->save();
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$this->database->is_public = !$this->database->is_public;
|
$this->database->is_public = ! $this->database->is_public;
|
||||||
|
|
||||||
return handleError($e, $this);
|
return handleError($e, $this);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ class Heading extends Component
|
|||||||
|
|
||||||
public array $parameters;
|
public array $parameters;
|
||||||
|
|
||||||
|
public $docker_cleanup = true;
|
||||||
|
|
||||||
public function getListeners()
|
public function getListeners()
|
||||||
{
|
{
|
||||||
$userId = auth()->user()->id;
|
$userId = auth()->user()->id;
|
||||||
@@ -54,7 +56,7 @@ class Heading extends Component
|
|||||||
|
|
||||||
public function stop()
|
public function stop()
|
||||||
{
|
{
|
||||||
StopDatabase::run($this->database);
|
StopDatabase::run($this->database, false, $this->docker_cleanup);
|
||||||
$this->database->status = 'exited';
|
$this->database->status = 'exited';
|
||||||
$this->database->save();
|
$this->database->save();
|
||||||
$this->check_status();
|
$this->check_status();
|
||||||
@@ -71,4 +73,13 @@ class Heading extends Component
|
|||||||
$activity = StartDatabase::run($this->database);
|
$activity = StartDatabase::run($this->database);
|
||||||
$this->dispatch('activityMonitor', $activity->id);
|
$this->dispatch('activityMonitor', $activity->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return view('livewire.project.database.heading', [
|
||||||
|
'checkboxes' => [
|
||||||
|
['id' => 'docker_cleanup', 'label' => 'Cleanup docker build cache and unused images (next deployment could take longer).'],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ class General extends Component
|
|||||||
public function instantSaveAdvanced()
|
public function instantSaveAdvanced()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
if (!$this->server->isLogDrainEnabled()) {
|
if (! $this->server->isLogDrainEnabled()) {
|
||||||
$this->database->is_log_drain_enabled = false;
|
$this->database->is_log_drain_enabled = false;
|
||||||
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
|
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
|
||||||
|
|
||||||
@@ -94,14 +94,14 @@ class General extends Component
|
|||||||
public function instantSave()
|
public function instantSave()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
if ($this->database->is_public && !$this->database->public_port) {
|
if ($this->database->is_public && ! $this->database->public_port) {
|
||||||
$this->dispatch('error', 'Public port is required.');
|
$this->dispatch('error', 'Public port is required.');
|
||||||
$this->database->is_public = false;
|
$this->database->is_public = false;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if ($this->database->is_public) {
|
if ($this->database->is_public) {
|
||||||
if (!str($this->database->status)->startsWith('running')) {
|
if (! str($this->database->status)->startsWith('running')) {
|
||||||
$this->dispatch('error', 'Database must be started to be publicly accessible.');
|
$this->dispatch('error', 'Database must be started to be publicly accessible.');
|
||||||
$this->database->is_public = false;
|
$this->database->is_public = false;
|
||||||
|
|
||||||
@@ -116,7 +116,7 @@ class General extends Component
|
|||||||
$this->db_url_public = $this->database->external_db_url;
|
$this->db_url_public = $this->database->external_db_url;
|
||||||
$this->database->save();
|
$this->database->save();
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$this->database->is_public = !$this->database->is_public;
|
$this->database->is_public = ! $this->database->is_public;
|
||||||
|
|
||||||
return handleError($e, $this);
|
return handleError($e, $this);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ class General extends Component
|
|||||||
public function instantSaveAdvanced()
|
public function instantSaveAdvanced()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
if (!$this->server->isLogDrainEnabled()) {
|
if (! $this->server->isLogDrainEnabled()) {
|
||||||
$this->database->is_log_drain_enabled = false;
|
$this->database->is_log_drain_enabled = false;
|
||||||
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
|
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
|
||||||
|
|
||||||
@@ -100,14 +100,14 @@ class General extends Component
|
|||||||
public function instantSave()
|
public function instantSave()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
if ($this->database->is_public && !$this->database->public_port) {
|
if ($this->database->is_public && ! $this->database->public_port) {
|
||||||
$this->dispatch('error', 'Public port is required.');
|
$this->dispatch('error', 'Public port is required.');
|
||||||
$this->database->is_public = false;
|
$this->database->is_public = false;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if ($this->database->is_public) {
|
if ($this->database->is_public) {
|
||||||
if (!str($this->database->status)->startsWith('running')) {
|
if (! str($this->database->status)->startsWith('running')) {
|
||||||
$this->dispatch('error', 'Database must be started to be publicly accessible.');
|
$this->dispatch('error', 'Database must be started to be publicly accessible.');
|
||||||
$this->database->is_public = false;
|
$this->database->is_public = false;
|
||||||
|
|
||||||
@@ -122,7 +122,7 @@ class General extends Component
|
|||||||
$this->db_url_public = $this->database->external_db_url;
|
$this->db_url_public = $this->database->external_db_url;
|
||||||
$this->database->save();
|
$this->database->save();
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$this->database->is_public = !$this->database->is_public;
|
$this->database->is_public = ! $this->database->is_public;
|
||||||
|
|
||||||
return handleError($e, $this);
|
return handleError($e, $this);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ class General extends Component
|
|||||||
public function instantSaveAdvanced()
|
public function instantSaveAdvanced()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
if (!$this->server->isLogDrainEnabled()) {
|
if (! $this->server->isLogDrainEnabled()) {
|
||||||
$this->database->is_log_drain_enabled = false;
|
$this->database->is_log_drain_enabled = false;
|
||||||
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
|
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
|
||||||
|
|
||||||
@@ -101,14 +101,14 @@ class General extends Component
|
|||||||
public function instantSave()
|
public function instantSave()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
if ($this->database->is_public && !$this->database->public_port) {
|
if ($this->database->is_public && ! $this->database->public_port) {
|
||||||
$this->dispatch('error', 'Public port is required.');
|
$this->dispatch('error', 'Public port is required.');
|
||||||
$this->database->is_public = false;
|
$this->database->is_public = false;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if ($this->database->is_public) {
|
if ($this->database->is_public) {
|
||||||
if (!str($this->database->status)->startsWith('running')) {
|
if (! str($this->database->status)->startsWith('running')) {
|
||||||
$this->dispatch('error', 'Database must be started to be publicly accessible.');
|
$this->dispatch('error', 'Database must be started to be publicly accessible.');
|
||||||
$this->database->is_public = false;
|
$this->database->is_public = false;
|
||||||
|
|
||||||
@@ -123,7 +123,7 @@ class General extends Component
|
|||||||
$this->db_url_public = $this->database->external_db_url;
|
$this->db_url_public = $this->database->external_db_url;
|
||||||
$this->database->save();
|
$this->database->save();
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$this->database->is_public = !$this->database->is_public;
|
$this->database->is_public = ! $this->database->is_public;
|
||||||
|
|
||||||
return handleError($e, $this);
|
return handleError($e, $this);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ class General extends Component
|
|||||||
public function instantSaveAdvanced()
|
public function instantSaveAdvanced()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
if (!$this->server->isLogDrainEnabled()) {
|
if (! $this->server->isLogDrainEnabled()) {
|
||||||
$this->database->is_log_drain_enabled = false;
|
$this->database->is_log_drain_enabled = false;
|
||||||
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
|
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
|
||||||
|
|
||||||
@@ -99,14 +99,14 @@ class General extends Component
|
|||||||
public function instantSave()
|
public function instantSave()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
if ($this->database->is_public && !$this->database->public_port) {
|
if ($this->database->is_public && ! $this->database->public_port) {
|
||||||
$this->dispatch('error', 'Public port is required.');
|
$this->dispatch('error', 'Public port is required.');
|
||||||
$this->database->is_public = false;
|
$this->database->is_public = false;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if ($this->database->is_public) {
|
if ($this->database->is_public) {
|
||||||
if (!str($this->database->status)->startsWith('running')) {
|
if (! str($this->database->status)->startsWith('running')) {
|
||||||
$this->dispatch('error', 'Database must be started to be publicly accessible.');
|
$this->dispatch('error', 'Database must be started to be publicly accessible.');
|
||||||
$this->database->is_public = false;
|
$this->database->is_public = false;
|
||||||
|
|
||||||
@@ -121,7 +121,7 @@ class General extends Component
|
|||||||
$this->db_url_public = $this->database->external_db_url;
|
$this->db_url_public = $this->database->external_db_url;
|
||||||
$this->database->save();
|
$this->database->save();
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$this->database->is_public = !$this->database->is_public;
|
$this->database->is_public = ! $this->database->is_public;
|
||||||
|
|
||||||
return handleError($e, $this);
|
return handleError($e, $this);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ class General extends Component
|
|||||||
public function instantSaveAdvanced()
|
public function instantSaveAdvanced()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
if (!$this->server->isLogDrainEnabled()) {
|
if (! $this->server->isLogDrainEnabled()) {
|
||||||
$this->database->is_log_drain_enabled = false;
|
$this->database->is_log_drain_enabled = false;
|
||||||
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
|
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
|
||||||
|
|
||||||
@@ -88,14 +88,14 @@ class General extends Component
|
|||||||
public function instantSave()
|
public function instantSave()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
if ($this->database->is_public && !$this->database->public_port) {
|
if ($this->database->is_public && ! $this->database->public_port) {
|
||||||
$this->dispatch('error', 'Public port is required.');
|
$this->dispatch('error', 'Public port is required.');
|
||||||
$this->database->is_public = false;
|
$this->database->is_public = false;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if ($this->database->is_public) {
|
if ($this->database->is_public) {
|
||||||
if (!str($this->database->status)->startsWith('running')) {
|
if (! str($this->database->status)->startsWith('running')) {
|
||||||
$this->dispatch('error', 'Database must be started to be publicly accessible.');
|
$this->dispatch('error', 'Database must be started to be publicly accessible.');
|
||||||
$this->database->is_public = false;
|
$this->database->is_public = false;
|
||||||
|
|
||||||
@@ -110,7 +110,7 @@ class General extends Component
|
|||||||
$this->db_url_public = $this->database->external_db_url;
|
$this->db_url_public = $this->database->external_db_url;
|
||||||
$this->database->save();
|
$this->database->save();
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$this->database->is_public = !$this->database->is_public;
|
$this->database->is_public = ! $this->database->is_public;
|
||||||
|
|
||||||
return handleError($e, $this);
|
return handleError($e, $this);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,9 +13,12 @@ class DeleteEnvironment extends Component
|
|||||||
|
|
||||||
public bool $disabled = false;
|
public bool $disabled = false;
|
||||||
|
|
||||||
|
public string $environmentName = '';
|
||||||
|
|
||||||
public function mount()
|
public function mount()
|
||||||
{
|
{
|
||||||
$this->parameters = get_route_parameters();
|
$this->parameters = get_route_parameters();
|
||||||
|
$this->environmentName = Environment::findOrFail($this->environment_id)->name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete()
|
public function delete()
|
||||||
|
|||||||
@@ -13,9 +13,12 @@ class DeleteProject extends Component
|
|||||||
|
|
||||||
public bool $disabled = false;
|
public bool $disabled = false;
|
||||||
|
|
||||||
|
public string $projectName = '';
|
||||||
|
|
||||||
public function mount()
|
public function mount()
|
||||||
{
|
{
|
||||||
$this->parameters = get_route_parameters();
|
$this->parameters = get_route_parameters();
|
||||||
|
$this->projectName = Project::findOrFail($this->project_id)->name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete()
|
public function delete()
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ class Configuration extends Component
|
|||||||
$application = $this->service->applications->find($id);
|
$application = $this->service->applications->find($id);
|
||||||
if ($application) {
|
if ($application) {
|
||||||
$application->restart();
|
$application->restart();
|
||||||
$this->dispatch('success', 'Application restarted successfully.');
|
$this->dispatch('success', 'Service application restarted successfully.');
|
||||||
}
|
}
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return handleError($e, $this);
|
return handleError($e, $this);
|
||||||
@@ -65,7 +65,7 @@ class Configuration extends Component
|
|||||||
$database = $this->service->databases->find($id);
|
$database = $this->service->databases->find($id);
|
||||||
if ($database) {
|
if ($database) {
|
||||||
$database->restart();
|
$database->restart();
|
||||||
$this->dispatch('success', 'Database restarted successfully.');
|
$this->dispatch('success', 'Service database restarted successfully.');
|
||||||
}
|
}
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return handleError($e, $this);
|
return handleError($e, $this);
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ use App\Models\StandaloneMongodb;
|
|||||||
use App\Models\StandaloneMysql;
|
use App\Models\StandaloneMysql;
|
||||||
use App\Models\StandalonePostgresql;
|
use App\Models\StandalonePostgresql;
|
||||||
use App\Models\StandaloneRedis;
|
use App\Models\StandaloneRedis;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
class FileStorage extends Component
|
class FileStorage extends Component
|
||||||
@@ -83,8 +85,14 @@ class FileStorage extends Component
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete()
|
public function delete($password)
|
||||||
{
|
{
|
||||||
|
if (! Hash::check($password, Auth::user()->password)) {
|
||||||
|
$this->addError('password', 'The provided password is incorrect.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$message = 'File deleted.';
|
$message = 'File deleted.';
|
||||||
if ($this->fileStorage->is_directory) {
|
if ($this->fileStorage->is_directory) {
|
||||||
@@ -129,6 +137,13 @@ class FileStorage extends Component
|
|||||||
|
|
||||||
public function render()
|
public function render()
|
||||||
{
|
{
|
||||||
return view('livewire.project.service.file-storage');
|
return view('livewire.project.service.file-storage', [
|
||||||
|
'directoryDeletionCheckboxes' => [
|
||||||
|
['id' => 'permanently_delete', 'label' => 'The selected directory and all its contents will be permantely deleted form the server.'],
|
||||||
|
],
|
||||||
|
'fileDeletionCheckboxes' => [
|
||||||
|
['id' => 'permanently_delete', 'label' => 'The selected file will be permanently deleted form the server.'],
|
||||||
|
],
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ class Navbar extends Component
|
|||||||
|
|
||||||
public $isDeploymentProgress = false;
|
public $isDeploymentProgress = false;
|
||||||
|
|
||||||
|
public $docker_cleanup = true;
|
||||||
|
|
||||||
|
public $title = 'Configuration';
|
||||||
|
|
||||||
public function mount()
|
public function mount()
|
||||||
{
|
{
|
||||||
if (str($this->service->status())->contains('running') && is_null($this->service->config_hash)) {
|
if (str($this->service->status())->contains('running') && is_null($this->service->config_hash)) {
|
||||||
@@ -40,7 +44,7 @@ class Navbar extends Component
|
|||||||
|
|
||||||
public function serviceStarted()
|
public function serviceStarted()
|
||||||
{
|
{
|
||||||
$this->dispatch('success', 'Service status changed.');
|
// $this->dispatch('success', 'Service status changed.');
|
||||||
if (is_null($this->service->config_hash) || $this->service->isConfigurationChanged()) {
|
if (is_null($this->service->config_hash) || $this->service->isConfigurationChanged()) {
|
||||||
$this->service->isConfigurationChanged(true);
|
$this->service->isConfigurationChanged(true);
|
||||||
$this->dispatch('configurationChanged');
|
$this->dispatch('configurationChanged');
|
||||||
@@ -60,11 +64,6 @@ class Navbar extends Component
|
|||||||
$this->dispatch('success', 'Service status updated.');
|
$this->dispatch('success', 'Service status updated.');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function render()
|
|
||||||
{
|
|
||||||
return view('livewire.project.service.navbar');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function checkDeployments()
|
public function checkDeployments()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
@@ -95,14 +94,9 @@ class Navbar extends Component
|
|||||||
$this->dispatch('activityMonitor', $activity->id);
|
$this->dispatch('activityMonitor', $activity->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function stop(bool $forceCleanup = false)
|
public function stop()
|
||||||
{
|
{
|
||||||
StopService::run($this->service);
|
StopService::run($this->service, false, $this->docker_cleanup);
|
||||||
if ($forceCleanup) {
|
|
||||||
$this->dispatch('success', 'Containers cleaned up.');
|
|
||||||
} else {
|
|
||||||
$this->dispatch('success', 'Service stopped.');
|
|
||||||
}
|
|
||||||
ServiceStatusChanged::dispatch();
|
ServiceStatusChanged::dispatch();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,4 +115,13 @@ class Navbar extends Component
|
|||||||
$activity = StartService::run($this->service);
|
$activity = StartService::run($this->service);
|
||||||
$this->dispatch('activityMonitor', $activity->id);
|
$this->dispatch('activityMonitor', $activity->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return view('livewire.project.service.navbar', [
|
||||||
|
'checkboxes' => [
|
||||||
|
['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
namespace App\Livewire\Project\Service;
|
namespace App\Livewire\Project\Service;
|
||||||
|
|
||||||
use App\Models\ServiceApplication;
|
use App\Models\ServiceApplication;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
class ServiceApplicationView extends Component
|
class ServiceApplicationView extends Component
|
||||||
@@ -11,6 +13,10 @@ class ServiceApplicationView extends Component
|
|||||||
|
|
||||||
public $parameters;
|
public $parameters;
|
||||||
|
|
||||||
|
public $docker_cleanup = true;
|
||||||
|
|
||||||
|
public $delete_volumes = true;
|
||||||
|
|
||||||
protected $rules = [
|
protected $rules = [
|
||||||
'application.human_name' => 'nullable',
|
'application.human_name' => 'nullable',
|
||||||
'application.description' => 'nullable',
|
'application.description' => 'nullable',
|
||||||
@@ -23,11 +29,6 @@ class ServiceApplicationView extends Component
|
|||||||
'application.is_stripprefix_enabled' => 'nullable|boolean',
|
'application.is_stripprefix_enabled' => 'nullable|boolean',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function render()
|
|
||||||
{
|
|
||||||
return view('livewire.project.service.service-application-view');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function updatedApplicationFqdn()
|
public function updatedApplicationFqdn()
|
||||||
{
|
{
|
||||||
$this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
|
$this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
|
||||||
@@ -56,8 +57,14 @@ class ServiceApplicationView extends Component
|
|||||||
$this->dispatch('success', 'You need to restart the service for the changes to take effect.');
|
$this->dispatch('success', 'You need to restart the service for the changes to take effect.');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete()
|
public function delete($password)
|
||||||
{
|
{
|
||||||
|
if (! Hash::check($password, Auth::user()->password)) {
|
||||||
|
$this->addError('password', 'The provided password is incorrect.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$this->application->delete();
|
$this->application->delete();
|
||||||
$this->dispatch('success', 'Application deleted.');
|
$this->dispatch('success', 'Application deleted.');
|
||||||
@@ -91,4 +98,17 @@ class ServiceApplicationView extends Component
|
|||||||
$this->dispatch('generateDockerCompose');
|
$this->dispatch('generateDockerCompose');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return view('livewire.project.service.service-application-view', [
|
||||||
|
'checkboxes' => [
|
||||||
|
['id' => 'delete_volumes', 'label' => __('resource.delete_volumes')],
|
||||||
|
['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')],
|
||||||
|
// ['id' => 'delete_associated_backups_locally', 'label' => 'All backups associated with this Ressource will be permanently deleted from local storage.'],
|
||||||
|
// ['id' => 'delete_associated_backups_s3', 'label' => 'All backups associated with this Ressource will be permanently deleted from the selected S3 Storage.'],
|
||||||
|
// ['id' => 'delete_associated_backups_sftp', 'label' => 'All backups associated with this Ressource will be permanently deleted from the selected SFTP Storage.']
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class StackForm extends Component
|
|||||||
$key = data_get($field, 'key');
|
$key = data_get($field, 'key');
|
||||||
$value = data_get($field, 'value');
|
$value = data_get($field, 'value');
|
||||||
$rules = data_get($field, 'rules', 'nullable');
|
$rules = data_get($field, 'rules', 'nullable');
|
||||||
$isPassword = data_get($field, 'isPassword');
|
$isPassword = data_get($field, 'isPassword', false);
|
||||||
$this->fields->put($key, [
|
$this->fields->put($key, [
|
||||||
'serviceName' => $serviceName,
|
'serviceName' => $serviceName,
|
||||||
'key' => $key,
|
'key' => $key,
|
||||||
@@ -47,7 +47,15 @@ class StackForm extends Component
|
|||||||
$this->validationAttributes["fields.$key.value"] = $fieldKey;
|
$this->validationAttributes["fields.$key.value"] = $fieldKey;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$this->fields = $this->fields->sortBy('name');
|
$this->fields = $this->fields->groupBy('serviceName')->map(function ($group) {
|
||||||
|
return $group->sortBy(function ($field) {
|
||||||
|
return data_get($field, 'isPassword') ? 1 : 0;
|
||||||
|
})->mapWithKeys(function ($field) {
|
||||||
|
return [$field['key'] => $field];
|
||||||
|
});
|
||||||
|
})->flatMap(function ($group) {
|
||||||
|
return $group;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public function saveCompose($raw)
|
public function saveCompose($raw)
|
||||||
|
|||||||
@@ -3,6 +3,11 @@
|
|||||||
namespace App\Livewire\Project\Shared;
|
namespace App\Livewire\Project\Shared;
|
||||||
|
|
||||||
use App\Jobs\DeleteResourceJob;
|
use App\Jobs\DeleteResourceJob;
|
||||||
|
use App\Models\Service;
|
||||||
|
use App\Models\ServiceApplication;
|
||||||
|
use App\Models\ServiceDatabase;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
use Visus\Cuid2\Cuid2;
|
use Visus\Cuid2\Cuid2;
|
||||||
|
|
||||||
@@ -10,6 +15,8 @@ class Danger extends Component
|
|||||||
{
|
{
|
||||||
public $resource;
|
public $resource;
|
||||||
|
|
||||||
|
public $resourceName;
|
||||||
|
|
||||||
public $projectUuid;
|
public $projectUuid;
|
||||||
|
|
||||||
public $environmentName;
|
public $environmentName;
|
||||||
@@ -18,22 +25,93 @@ class Danger extends Component
|
|||||||
|
|
||||||
public bool $delete_volumes = true;
|
public bool $delete_volumes = true;
|
||||||
|
|
||||||
|
public bool $docker_cleanup = true;
|
||||||
|
|
||||||
|
public bool $delete_connected_networks = true;
|
||||||
|
|
||||||
public ?string $modalId = null;
|
public ?string $modalId = null;
|
||||||
|
|
||||||
|
public string $resourceDomain = '';
|
||||||
|
|
||||||
public function mount()
|
public function mount()
|
||||||
{
|
{
|
||||||
$this->modalId = new Cuid2;
|
|
||||||
$parameters = get_route_parameters();
|
$parameters = get_route_parameters();
|
||||||
|
$this->modalId = new Cuid2;
|
||||||
$this->projectUuid = data_get($parameters, 'project_uuid');
|
$this->projectUuid = data_get($parameters, 'project_uuid');
|
||||||
$this->environmentName = data_get($parameters, 'environment_name');
|
$this->environmentName = data_get($parameters, 'environment_name');
|
||||||
|
|
||||||
|
if ($this->resource === null) {
|
||||||
|
if (isset($parameters['service_uuid'])) {
|
||||||
|
$this->resource = Service::where('uuid', $parameters['service_uuid'])->first();
|
||||||
|
} elseif (isset($parameters['stack_service_uuid'])) {
|
||||||
|
$this->resource = ServiceApplication::where('uuid', $parameters['stack_service_uuid'])->first()
|
||||||
|
?? ServiceDatabase::where('uuid', $parameters['stack_service_uuid'])->first();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->resource === null) {
|
||||||
|
$this->resourceName = 'Unknown Resource';
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! method_exists($this->resource, 'type')) {
|
||||||
|
$this->resourceName = 'Unknown Resource';
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($this->resource->type()) {
|
||||||
|
case 'application':
|
||||||
|
$this->resourceName = $this->resource->name ?? 'Application';
|
||||||
|
break;
|
||||||
|
case 'standalone-postgresql':
|
||||||
|
case 'standalone-redis':
|
||||||
|
case 'standalone-mongodb':
|
||||||
|
case 'standalone-mysql':
|
||||||
|
case 'standalone-mariadb':
|
||||||
|
case 'standalone-keydb':
|
||||||
|
case 'standalone-dragonfly':
|
||||||
|
case 'standalone-clickhouse':
|
||||||
|
$this->resourceName = $this->resource->name ?? 'Database';
|
||||||
|
break;
|
||||||
|
case 'service':
|
||||||
|
$this->resourceName = $this->resource->name ?? 'Service';
|
||||||
|
break;
|
||||||
|
case 'service-application':
|
||||||
|
$this->resourceName = $this->resource->name ?? 'Service Application';
|
||||||
|
break;
|
||||||
|
case 'service-database':
|
||||||
|
$this->resourceName = $this->resource->name ?? 'Service Database';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$this->resourceName = 'Unknown Resource';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete()
|
public function delete($password)
|
||||||
{
|
{
|
||||||
|
if (! Hash::check($password, Auth::user()->password)) {
|
||||||
|
$this->addError('password', 'The provided password is incorrect.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $this->resource) {
|
||||||
|
$this->addError('resource', 'Resource not found.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// $this->authorize('delete', $this->resource);
|
|
||||||
$this->resource->delete();
|
$this->resource->delete();
|
||||||
DeleteResourceJob::dispatch($this->resource, $this->delete_configurations, $this->delete_volumes);
|
DeleteResourceJob::dispatch(
|
||||||
|
$this->resource,
|
||||||
|
$this->delete_configurations,
|
||||||
|
$this->delete_volumes,
|
||||||
|
$this->docker_cleanup,
|
||||||
|
$this->delete_connected_networks
|
||||||
|
);
|
||||||
|
|
||||||
return redirect()->route('project.resource.index', [
|
return redirect()->route('project.resource.index', [
|
||||||
'project_uuid' => $this->projectUuid,
|
'project_uuid' => $this->projectUuid,
|
||||||
@@ -43,4 +121,19 @@ class Danger extends Component
|
|||||||
return handleError($e, $this);
|
return handleError($e, $this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return view('livewire.project.shared.danger', [
|
||||||
|
'checkboxes' => [
|
||||||
|
['id' => 'delete_volumes', 'label' => __('resource.delete_volumes')],
|
||||||
|
['id' => 'delete_connected_networks', 'label' => __('resource.delete_connected_networks')],
|
||||||
|
['id' => 'delete_configurations', 'label' => __('resource.delete_configurations')],
|
||||||
|
['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')],
|
||||||
|
// ['id' => 'delete_associated_backups_locally', 'label' => 'All backups associated with this Ressource will be permanently deleted from local storage.'],
|
||||||
|
// ['id' => 'delete_associated_backups_s3', 'label' => 'All backups associated with this Ressource will be permanently deleted from the selected S3 Storage.'],
|
||||||
|
// ['id' => 'delete_associated_backups_sftp', 'label' => 'All backups associated with this Ressource will be permanently deleted from the selected SFTP Storage.']
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ use App\Events\ApplicationStatusChanged;
|
|||||||
use App\Jobs\ContainerStatusJob;
|
use App\Jobs\ContainerStatusJob;
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
use App\Models\StandaloneDocker;
|
use App\Models\StandaloneDocker;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
use Visus\Cuid2\Cuid2;
|
use Visus\Cuid2\Cuid2;
|
||||||
|
|
||||||
@@ -115,8 +117,14 @@ class Destination extends Component
|
|||||||
ApplicationStatusChanged::dispatch(data_get($this->resource, 'environment.project.team.id'));
|
ApplicationStatusChanged::dispatch(data_get($this->resource, 'environment.project.team.id'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function removeServer(int $network_id, int $server_id)
|
public function removeServer(int $network_id, int $server_id, $password)
|
||||||
{
|
{
|
||||||
|
if (! Hash::check($password, Auth::user()->password)) {
|
||||||
|
$this->addError('password', 'The provided password is incorrect.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if ($this->resource->destination->server->id == $server_id && $this->resource->destination->id == $network_id) {
|
if ($this->resource->destination->server->id == $server_id && $this->resource->destination->id == $network_id) {
|
||||||
$this->dispatch('error', 'You cannot remove this destination server.', 'You are trying to remove the main server.');
|
$this->dispatch('error', 'You cannot remove this destination server.', 'You are trying to remove the main server.');
|
||||||
|
|
||||||
|
|||||||
@@ -2,18 +2,16 @@
|
|||||||
|
|
||||||
namespace App\Livewire\Project\Shared;
|
namespace App\Livewire\Project\Shared;
|
||||||
|
|
||||||
use App\Actions\Server\RunCommand;
|
|
||||||
use App\Models\Application;
|
use App\Models\Application;
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
use App\Models\Service;
|
use App\Models\Service;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
use Livewire\Attributes\On;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
class ExecuteContainerCommand extends Component
|
class ExecuteContainerCommand extends Component
|
||||||
{
|
{
|
||||||
public string $command;
|
public $container;
|
||||||
|
|
||||||
public string $container;
|
|
||||||
|
|
||||||
public Collection $containers;
|
public Collection $containers;
|
||||||
|
|
||||||
@@ -23,8 +21,6 @@ class ExecuteContainerCommand extends Component
|
|||||||
|
|
||||||
public string $type;
|
public string $type;
|
||||||
|
|
||||||
public string $workDir = '';
|
|
||||||
|
|
||||||
public Server $server;
|
public Server $server;
|
||||||
|
|
||||||
public Collection $servers;
|
public Collection $servers;
|
||||||
@@ -33,11 +29,13 @@ class ExecuteContainerCommand extends Component
|
|||||||
'server' => 'required',
|
'server' => 'required',
|
||||||
'container' => 'required',
|
'container' => 'required',
|
||||||
'command' => 'required',
|
'command' => 'required',
|
||||||
'workDir' => 'nullable',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
public function mount()
|
public function mount()
|
||||||
{
|
{
|
||||||
|
if (! auth()->user()->isAdmin()) {
|
||||||
|
abort(403);
|
||||||
|
}
|
||||||
$this->parameters = get_route_parameters();
|
$this->parameters = get_route_parameters();
|
||||||
$this->containers = collect();
|
$this->containers = collect();
|
||||||
$this->servers = collect();
|
$this->servers = collect();
|
||||||
@@ -62,24 +60,13 @@ class ExecuteContainerCommand extends Component
|
|||||||
if ($this->resource->destination->server->isFunctional()) {
|
if ($this->resource->destination->server->isFunctional()) {
|
||||||
$this->servers = $this->servers->push($this->resource->destination->server);
|
$this->servers = $this->servers->push($this->resource->destination->server);
|
||||||
}
|
}
|
||||||
$this->container = $this->resource->uuid;
|
|
||||||
$this->containers->push($this->container);
|
|
||||||
} elseif (data_get($this->parameters, 'service_uuid')) {
|
} elseif (data_get($this->parameters, 'service_uuid')) {
|
||||||
$this->type = 'service';
|
$this->type = 'service';
|
||||||
$this->resource = Service::where('uuid', $this->parameters['service_uuid'])->firstOrFail();
|
$this->resource = Service::where('uuid', $this->parameters['service_uuid'])->firstOrFail();
|
||||||
$this->resource->applications()->get()->each(function ($application) {
|
|
||||||
$this->containers->push(data_get($application, 'name').'-'.data_get($this->resource, 'uuid'));
|
|
||||||
});
|
|
||||||
$this->resource->databases()->get()->each(function ($database) {
|
|
||||||
$this->containers->push(data_get($database, 'name').'-'.data_get($this->resource, 'uuid'));
|
|
||||||
});
|
|
||||||
if ($this->resource->server->isFunctional()) {
|
if ($this->resource->server->isFunctional()) {
|
||||||
$this->servers = $this->servers->push($this->resource->server);
|
$this->servers = $this->servers->push($this->resource->server);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($this->containers->count() > 0) {
|
|
||||||
$this->container = $this->containers->first();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function loadContainers()
|
public function loadContainers()
|
||||||
@@ -102,44 +89,65 @@ class ExecuteContainerCommand extends Component
|
|||||||
];
|
];
|
||||||
$this->containers = $this->containers->push($payload);
|
$this->containers = $this->containers->push($payload);
|
||||||
}
|
}
|
||||||
|
} elseif (data_get($this->parameters, 'database_uuid')) {
|
||||||
|
if ($this->resource->isRunning()) {
|
||||||
|
$this->containers = $this->containers->push([
|
||||||
|
'server' => $server,
|
||||||
|
'container' => [
|
||||||
|
'Names' => $this->resource->uuid,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
} elseif (data_get($this->parameters, 'service_uuid')) {
|
||||||
|
$this->resource->applications()->get()->each(function ($application) {
|
||||||
|
ray($application);
|
||||||
|
if ($application->isRunning()) {
|
||||||
|
$this->containers->push([
|
||||||
|
'server' => $this->resource->server,
|
||||||
|
'container' => [
|
||||||
|
'Names' => data_get($application, 'name').'-'.data_get($this->resource, 'uuid'),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$this->resource->databases()->get()->each(function ($database) {
|
||||||
|
if ($database->isRunning()) {
|
||||||
|
$this->containers->push([
|
||||||
|
'server' => $this->resource->server,
|
||||||
|
'container' => [
|
||||||
|
'Names' => data_get($database, 'name').'-'.data_get($this->resource, 'uuid'),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
if ($this->containers->count() > 0) {
|
if ($this->containers->count() > 0) {
|
||||||
if (data_get($this->parameters, 'application_uuid')) {
|
$this->container = $this->containers->first();
|
||||||
$this->container = data_get($this->containers->first(), 'container.Names');
|
|
||||||
} elseif (data_get($this->parameters, 'database_uuid')) {
|
|
||||||
$this->container = $this->containers->first();
|
|
||||||
} elseif (data_get($this->parameters, 'service_uuid')) {
|
|
||||||
$this->container = $this->containers->first();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function runCommand()
|
#[On('connectToContainer')]
|
||||||
|
public function connectToContainer()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
if (data_get($this->parameters, 'application_uuid')) {
|
$container_name = data_get($this->container, 'container.Names');
|
||||||
$container = $this->containers->where('container.Names', $this->container)->first();
|
if (is_null($container_name)) {
|
||||||
$container_name = data_get($container, 'container.Names');
|
throw new \RuntimeException('Container not found.');
|
||||||
if (is_null($container)) {
|
|
||||||
throw new \RuntimeException('Container not found.');
|
|
||||||
}
|
|
||||||
$server = data_get($container, 'server');
|
|
||||||
} else {
|
|
||||||
$container_name = $this->container;
|
|
||||||
$server = $this->servers->first();
|
|
||||||
}
|
}
|
||||||
|
$server = data_get($this->container, 'server');
|
||||||
|
|
||||||
if ($server->isForceDisabled()) {
|
if ($server->isForceDisabled()) {
|
||||||
throw new \RuntimeException('Server is disabled.');
|
throw new \RuntimeException('Server is disabled.');
|
||||||
}
|
}
|
||||||
$cmd = "sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; ".str_replace("'", "'\''", $this->command)."'";
|
|
||||||
if (! empty($this->workDir)) {
|
$this->dispatch('send-terminal-command',
|
||||||
$exec = "docker exec -w {$this->workDir} {$container_name} {$cmd}";
|
true,
|
||||||
} else {
|
$container_name,
|
||||||
$exec = "docker exec {$container_name} {$cmd}";
|
$server->uuid,
|
||||||
}
|
);
|
||||||
$activity = RunCommand::run(server: $server, command: $exec);
|
|
||||||
$this->dispatch('activityMonitor', $activity->id);
|
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
return handleError($e, $this);
|
return handleError($e, $this);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Livewire\Project\Shared;
|
namespace App\Livewire\Project\Shared;
|
||||||
|
|
||||||
|
use App\Helpers\SshMultiplexingHelper;
|
||||||
use App\Models\Application;
|
use App\Models\Application;
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
use App\Models\Service;
|
use App\Models\Service;
|
||||||
@@ -108,14 +109,14 @@ class GetLogs extends Component
|
|||||||
$command = parseCommandsByLineForSudo(collect($command), $this->server);
|
$command = parseCommandsByLineForSudo(collect($command), $this->server);
|
||||||
$command = $command[0];
|
$command = $command[0];
|
||||||
}
|
}
|
||||||
$sshCommand = generateSshCommand($this->server, $command);
|
$sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command);
|
||||||
} else {
|
} else {
|
||||||
$command = "docker logs -n {$this->numberOfLines} -t {$this->container}";
|
$command = "docker logs -n {$this->numberOfLines} -t {$this->container}";
|
||||||
if ($this->server->isNonRoot()) {
|
if ($this->server->isNonRoot()) {
|
||||||
$command = parseCommandsByLineForSudo(collect($command), $this->server);
|
$command = parseCommandsByLineForSudo(collect($command), $this->server);
|
||||||
$command = $command[0];
|
$command = $command[0];
|
||||||
}
|
}
|
||||||
$sshCommand = generateSshCommand($this->server, $command);
|
$sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if ($this->server->isSwarm()) {
|
if ($this->server->isSwarm()) {
|
||||||
@@ -124,14 +125,14 @@ class GetLogs extends Component
|
|||||||
$command = parseCommandsByLineForSudo(collect($command), $this->server);
|
$command = parseCommandsByLineForSudo(collect($command), $this->server);
|
||||||
$command = $command[0];
|
$command = $command[0];
|
||||||
}
|
}
|
||||||
$sshCommand = generateSshCommand($this->server, $command);
|
$sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command);
|
||||||
} else {
|
} else {
|
||||||
$command = "docker logs -n {$this->numberOfLines} {$this->container}";
|
$command = "docker logs -n {$this->numberOfLines} {$this->container}";
|
||||||
if ($this->server->isNonRoot()) {
|
if ($this->server->isNonRoot()) {
|
||||||
$command = parseCommandsByLineForSudo(collect($command), $this->server);
|
$command = parseCommandsByLineForSudo(collect($command), $this->server);
|
||||||
$command = $command[0];
|
$command = $command[0];
|
||||||
}
|
}
|
||||||
$sshCommand = generateSshCommand($this->server, $command);
|
$sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($refresh) {
|
if ($refresh) {
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ use Livewire\Component;
|
|||||||
class Executions extends Component
|
class Executions extends Component
|
||||||
{
|
{
|
||||||
public $executions = [];
|
public $executions = [];
|
||||||
|
|
||||||
public $selectedKey;
|
public $selectedKey;
|
||||||
|
|
||||||
public $task;
|
public $task;
|
||||||
|
|
||||||
public function getListeners()
|
public function getListeners()
|
||||||
@@ -29,7 +31,7 @@ class Executions extends Component
|
|||||||
|
|
||||||
public function server()
|
public function server()
|
||||||
{
|
{
|
||||||
if (!$this->task) {
|
if (! $this->task) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,16 +44,18 @@ class Executions extends Component
|
|||||||
return $this->task->service->destination->server;
|
return $this->task->service->destination->server;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getServerTimezone()
|
public function getServerTimezone()
|
||||||
{
|
{
|
||||||
$server = $this->server();
|
$server = $this->server();
|
||||||
if (!$server) {
|
if (! $server) {
|
||||||
return 'UTC';
|
return 'UTC';
|
||||||
}
|
}
|
||||||
$serverTimezone = $server->settings->server_timezone;
|
$serverTimezone = $server->settings->server_timezone;
|
||||||
|
|
||||||
return $serverTimezone;
|
return $serverTimezone;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,6 +68,7 @@ class Executions extends Component
|
|||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
$dateObj->setTimezone(new \DateTimeZone('UTC'));
|
$dateObj->setTimezone(new \DateTimeZone('UTC'));
|
||||||
}
|
}
|
||||||
|
|
||||||
return $dateObj->format('Y-m-d H:i:s T');
|
return $dateObj->format('Y-m-d H:i:s T');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ class Show extends Component
|
|||||||
|
|
||||||
public string $type;
|
public string $type;
|
||||||
|
|
||||||
|
public string $scheduledTaskName;
|
||||||
|
|
||||||
protected $rules = [
|
protected $rules = [
|
||||||
'task.enabled' => 'required|boolean',
|
'task.enabled' => 'required|boolean',
|
||||||
'task.name' => 'required|string',
|
'task.name' => 'required|string',
|
||||||
@@ -49,6 +51,7 @@ class Show extends Component
|
|||||||
|
|
||||||
$this->modalId = new Cuid2;
|
$this->modalId = new Cuid2;
|
||||||
$this->task = ModelsScheduledTask::where('uuid', request()->route('task_uuid'))->first();
|
$this->task = ModelsScheduledTask::where('uuid', request()->route('task_uuid'))->first();
|
||||||
|
$this->scheduledTaskName = $this->task->name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function instantSave()
|
public function instantSave()
|
||||||
@@ -75,9 +78,9 @@ class Show extends Component
|
|||||||
$this->task->delete();
|
$this->task->delete();
|
||||||
|
|
||||||
if ($this->type == 'application') {
|
if ($this->type == 'application') {
|
||||||
return redirect()->route('project.application.configuration', $this->parameters);
|
return redirect()->route('project.application.configuration', $this->parameters, $this->scheduledTaskName);
|
||||||
} else {
|
} else {
|
||||||
return redirect()->route('project.service.configuration', $this->parameters);
|
return redirect()->route('project.service.configuration', $this->parameters, $this->scheduledTaskName);
|
||||||
}
|
}
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return handleError($e);
|
return handleError($e);
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
namespace App\Livewire\Project\Shared\Storages;
|
namespace App\Livewire\Project\Shared\Storages;
|
||||||
|
|
||||||
use App\Models\LocalPersistentVolume;
|
use App\Models\LocalPersistentVolume;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
class Show extends Component
|
class Show extends Component
|
||||||
@@ -36,8 +38,14 @@ class Show extends Component
|
|||||||
$this->dispatch('success', 'Storage updated successfully');
|
$this->dispatch('success', 'Storage updated successfully');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete()
|
public function delete($password)
|
||||||
{
|
{
|
||||||
|
if (! Hash::check($password, Auth::user()->password)) {
|
||||||
|
$this->addError('password', 'The provided password is incorrect.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$this->storage->delete();
|
$this->storage->delete();
|
||||||
$this->dispatch('refreshStorages');
|
$this->dispatch('refreshStorages');
|
||||||
}
|
}
|
||||||
|
|||||||
58
app/Livewire/Project/Shared/Terminal.php
Normal file
58
app/Livewire/Project/Shared/Terminal.php
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Project\Shared;
|
||||||
|
|
||||||
|
use App\Helpers\SshMultiplexingHelper;
|
||||||
|
use App\Models\Server;
|
||||||
|
use Livewire\Attributes\On;
|
||||||
|
use Livewire\Component;
|
||||||
|
|
||||||
|
class Terminal extends Component
|
||||||
|
{
|
||||||
|
public function getListeners()
|
||||||
|
{
|
||||||
|
$teamId = auth()->user()->currentTeam()->id;
|
||||||
|
|
||||||
|
return [
|
||||||
|
"echo-private:team.{$teamId},ApplicationStatusChanged" => 'closeTerminal',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function closeTerminal()
|
||||||
|
{
|
||||||
|
$this->dispatch('reloadWindow');
|
||||||
|
}
|
||||||
|
|
||||||
|
#[On('send-terminal-command')]
|
||||||
|
public function sendTerminalCommand($isContainer, $identifier, $serverUuid)
|
||||||
|
{
|
||||||
|
|
||||||
|
$server = Server::ownedByCurrentTeam()->whereUuid($serverUuid)->firstOrFail();
|
||||||
|
|
||||||
|
if ($isContainer) {
|
||||||
|
$status = getContainerStatus($server, $identifier);
|
||||||
|
if ($status !== 'running') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$command = SshMultiplexingHelper::generateSshCommand($server, "docker exec -it {$identifier} sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'");
|
||||||
|
} else {
|
||||||
|
$command = SshMultiplexingHelper::generateSshCommand($server, "sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ssh command is sent back to frontend then to websocket
|
||||||
|
// this is done because the websocket connection is not available here
|
||||||
|
// a better solution would be to remove websocket on NodeJS and work with something like
|
||||||
|
// 1. Laravel Pusher/Echo connection (not possible without a sdk)
|
||||||
|
// 2. Ratchet / Revolt / ReactPHP / Event Loop (possible but hard to implement and huge dependencies)
|
||||||
|
// 3. Just found out about this https://github.com/sirn-se/websocket-php, perhaps it can be used
|
||||||
|
// 4. Follow-up discussions here:
|
||||||
|
// - https://github.com/coollabsio/coolify/issues/2298
|
||||||
|
// - https://github.com/coollabsio/coolify/discussions/3362
|
||||||
|
$this->dispatch('send-back-command', $command);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return view('livewire.project.shared.terminal');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Livewire;
|
|
||||||
|
|
||||||
use App\Actions\Server\RunCommand as ServerRunCommand;
|
|
||||||
use App\Models\Server;
|
|
||||||
use Livewire\Component;
|
|
||||||
|
|
||||||
class RunCommand extends Component
|
|
||||||
{
|
|
||||||
public string $command;
|
|
||||||
|
|
||||||
public $server;
|
|
||||||
|
|
||||||
public $servers = [];
|
|
||||||
|
|
||||||
protected $rules = [
|
|
||||||
'server' => 'required',
|
|
||||||
'command' => 'required',
|
|
||||||
];
|
|
||||||
|
|
||||||
protected $validationAttributes = [
|
|
||||||
'server' => 'server',
|
|
||||||
'command' => 'command',
|
|
||||||
];
|
|
||||||
|
|
||||||
public function mount($servers)
|
|
||||||
{
|
|
||||||
$this->servers = $servers;
|
|
||||||
$this->server = $servers[0]->uuid;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function runCommand()
|
|
||||||
{
|
|
||||||
$this->validate();
|
|
||||||
try {
|
|
||||||
$activity = ServerRunCommand::run(server: Server::where('uuid', $this->server)->first(), command: $this->command);
|
|
||||||
$this->dispatch('activityMonitor', $activity->id);
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
return handleError($e, $this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,17 +3,13 @@
|
|||||||
namespace App\Livewire\Security\PrivateKey;
|
namespace App\Livewire\Security\PrivateKey;
|
||||||
|
|
||||||
use App\Models\PrivateKey;
|
use App\Models\PrivateKey;
|
||||||
use DanHarrin\LivewireRateLimiting\WithRateLimiting;
|
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
use phpseclib3\Crypt\PublicKeyLoader;
|
|
||||||
|
|
||||||
class Create extends Component
|
class Create extends Component
|
||||||
{
|
{
|
||||||
use WithRateLimiting;
|
public string $name = '';
|
||||||
|
|
||||||
public string $name;
|
public string $value = '';
|
||||||
|
|
||||||
public string $value;
|
|
||||||
|
|
||||||
public ?string $from = null;
|
public ?string $from = null;
|
||||||
|
|
||||||
@@ -26,72 +22,69 @@ class Create extends Component
|
|||||||
'value' => 'required|string',
|
'value' => 'required|string',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $validationAttributes = [
|
|
||||||
'name' => 'name',
|
|
||||||
'value' => 'private Key',
|
|
||||||
];
|
|
||||||
|
|
||||||
public function generateNewRSAKey()
|
public function generateNewRSAKey()
|
||||||
{
|
{
|
||||||
try {
|
$this->generateNewKey('rsa');
|
||||||
$this->rateLimit(10);
|
|
||||||
$this->name = generate_random_name();
|
|
||||||
$this->description = 'Created by Coolify';
|
|
||||||
['private' => $this->value, 'public' => $this->publicKey] = generateSSHKey();
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
return handleError($e, $this);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function generateNewEDKey()
|
public function generateNewEDKey()
|
||||||
{
|
{
|
||||||
try {
|
$this->generateNewKey('ed25519');
|
||||||
$this->rateLimit(10);
|
|
||||||
$this->name = generate_random_name();
|
|
||||||
$this->description = 'Created by Coolify';
|
|
||||||
['private' => $this->value, 'public' => $this->publicKey] = generateSSHKey('ed25519');
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
return handleError($e, $this);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function updated($updateProperty)
|
private function generateNewKey($type)
|
||||||
{
|
{
|
||||||
if ($updateProperty === 'value') {
|
$keyData = PrivateKey::generateNewKeyPair($type);
|
||||||
try {
|
$this->setKeyData($keyData);
|
||||||
$this->publicKey = PublicKeyLoader::load($this->$updateProperty)->getPublicKey()->toString('OpenSSH', ['comment' => '']);
|
}
|
||||||
} catch (\Throwable $e) {
|
|
||||||
if ($this->$updateProperty === '') {
|
public function updated($property)
|
||||||
$this->publicKey = '';
|
{
|
||||||
} else {
|
if ($property === 'value') {
|
||||||
$this->publicKey = 'Invalid private key';
|
$this->validatePrivateKey();
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
$this->validateOnly($updateProperty);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function createPrivateKey()
|
public function createPrivateKey()
|
||||||
{
|
{
|
||||||
$this->validate();
|
$this->validate();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$this->value = trim($this->value);
|
$privateKey = PrivateKey::createAndStore([
|
||||||
if (! str_ends_with($this->value, "\n")) {
|
|
||||||
$this->value .= "\n";
|
|
||||||
}
|
|
||||||
$private_key = PrivateKey::create([
|
|
||||||
'name' => $this->name,
|
'name' => $this->name,
|
||||||
'description' => $this->description,
|
'description' => $this->description,
|
||||||
'private_key' => $this->value,
|
'private_key' => trim($this->value)."\n",
|
||||||
'team_id' => currentTeam()->id,
|
'team_id' => currentTeam()->id,
|
||||||
]);
|
]);
|
||||||
if ($this->from === 'server') {
|
|
||||||
return redirect()->route('dashboard');
|
|
||||||
}
|
|
||||||
|
|
||||||
return redirect()->route('security.private-key.show', ['private_key_uuid' => $private_key->uuid]);
|
return $this->redirectAfterCreation($privateKey);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
return handleError($e, $this);
|
return handleError($e, $this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function setKeyData(array $keyData)
|
||||||
|
{
|
||||||
|
$this->name = $keyData['name'];
|
||||||
|
$this->description = $keyData['description'];
|
||||||
|
$this->value = $keyData['private_key'];
|
||||||
|
$this->publicKey = $keyData['public_key'];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function validatePrivateKey()
|
||||||
|
{
|
||||||
|
$validationResult = PrivateKey::validateAndExtractPublicKey($this->value);
|
||||||
|
$this->publicKey = $validationResult['publicKey'];
|
||||||
|
|
||||||
|
if (! $validationResult['isValid']) {
|
||||||
|
$this->addError('value', 'Invalid private key');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function redirectAfterCreation(PrivateKey $privateKey)
|
||||||
|
{
|
||||||
|
return $this->from === 'server'
|
||||||
|
? redirect()->route('dashboard')
|
||||||
|
: redirect()->route('security.private-key.show', ['private_key_uuid' => $privateKey->uuid]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
24
app/Livewire/Security/PrivateKey/Index.php
Normal file
24
app/Livewire/Security/PrivateKey/Index.php
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Security\PrivateKey;
|
||||||
|
|
||||||
|
use App\Models\PrivateKey;
|
||||||
|
use Livewire\Component;
|
||||||
|
|
||||||
|
class Index extends Component
|
||||||
|
{
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
$privateKeys = PrivateKey::ownedByCurrentTeam(['name', 'uuid', 'is_git_related', 'description'])->get();
|
||||||
|
|
||||||
|
return view('livewire.security.private-key.index', [
|
||||||
|
'privateKeys' => $privateKeys,
|
||||||
|
])->layout('components.layout');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function cleanupUnusedKeys()
|
||||||
|
{
|
||||||
|
PrivateKey::cleanupUnusedKeys();
|
||||||
|
$this->dispatch('success', 'Unused keys have been cleaned up.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,25 +29,27 @@ class Show extends Component
|
|||||||
try {
|
try {
|
||||||
$this->private_key = PrivateKey::ownedByCurrentTeam(['name', 'description', 'private_key', 'is_git_related'])->whereUuid(request()->private_key_uuid)->firstOrFail();
|
$this->private_key = PrivateKey::ownedByCurrentTeam(['name', 'description', 'private_key', 'is_git_related'])->whereUuid(request()->private_key_uuid)->firstOrFail();
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
return handleError($e, $this);
|
abort(404);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function loadPublicKey()
|
public function loadPublicKey()
|
||||||
{
|
{
|
||||||
$this->public_key = $this->private_key->publicKey();
|
$this->public_key = $this->private_key->getPublicKey();
|
||||||
|
if ($this->public_key === 'Error loading private key') {
|
||||||
|
$this->dispatch('error', 'Failed to load public key. The private key may be invalid.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete()
|
public function delete()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
if ($this->private_key->isEmpty()) {
|
$this->private_key->safeDelete();
|
||||||
$this->private_key->delete();
|
currentTeam()->privateKeys = PrivateKey::where('team_id', currentTeam()->id)->get();
|
||||||
currentTeam()->privateKeys = PrivateKey::where('team_id', currentTeam()->id)->get();
|
|
||||||
|
|
||||||
return redirect()->route('security.private-key.index');
|
return redirect()->route('security.private-key.index');
|
||||||
}
|
} catch (\Exception $e) {
|
||||||
$this->dispatch('error', 'This private key is in use and cannot be deleted. Please delete all servers, applications, and GitHub/GitLab apps that use this private key before deleting it.');
|
$this->dispatch('error', $e->getMessage());
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
return handleError($e, $this);
|
return handleError($e, $this);
|
||||||
}
|
}
|
||||||
@@ -56,8 +58,9 @@ class Show extends Component
|
|||||||
public function changePrivateKey()
|
public function changePrivateKey()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$this->private_key->private_key = formatPrivateKey($this->private_key->private_key);
|
$this->private_key->updatePrivateKey([
|
||||||
$this->private_key->save();
|
'private_key' => formatPrivateKey($this->private_key->private_key),
|
||||||
|
]);
|
||||||
refresh_server_connection($this->private_key);
|
refresh_server_connection($this->private_key);
|
||||||
$this->dispatch('success', 'Private key updated.');
|
$this->dispatch('success', 'Private key updated.');
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
|
|||||||
@@ -31,13 +31,12 @@ class ConfigureCloudflareTunnels extends Component
|
|||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$server = Server::ownedByCurrentTeam()->where('id', $this->server_id)->firstOrFail();
|
$server = Server::ownedByCurrentTeam()->where('id', $this->server_id)->firstOrFail();
|
||||||
ConfigureCloudflared::run($server, $this->cloudflare_token);
|
ConfigureCloudflared::dispatch($server, $this->cloudflare_token);
|
||||||
$server->settings->is_cloudflare_tunnel = true;
|
$server->settings->is_cloudflare_tunnel = true;
|
||||||
$server->ip = $this->ssh_domain;
|
$server->ip = $this->ssh_domain;
|
||||||
$server->save();
|
$server->save();
|
||||||
$server->settings->save();
|
$server->settings->save();
|
||||||
$this->dispatch('success', 'Cloudflare Tunnels configured successfully.');
|
$this->dispatch('warning', 'Cloudflare Tunnels configuration started.');
|
||||||
$this->dispatch('refreshServerShow');
|
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
return handleError($e, $this);
|
return handleError($e, $this);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
namespace App\Livewire\Server;
|
namespace App\Livewire\Server;
|
||||||
|
|
||||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
class Delete extends Component
|
class Delete extends Component
|
||||||
@@ -11,8 +13,13 @@ class Delete extends Component
|
|||||||
|
|
||||||
public $server;
|
public $server;
|
||||||
|
|
||||||
public function delete()
|
public function delete($password)
|
||||||
{
|
{
|
||||||
|
if (! Hash::check($password, Auth::user()->password)) {
|
||||||
|
$this->addError('password', 'The provided password is incorrect.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
$this->authorize('delete', $this->server);
|
$this->authorize('delete', $this->server);
|
||||||
if ($this->server->hasDefinedResources()) {
|
if ($this->server->hasDefinedResources()) {
|
||||||
|
|||||||
@@ -24,11 +24,16 @@ class Form extends Component
|
|||||||
|
|
||||||
public $timezones;
|
public $timezones;
|
||||||
|
|
||||||
protected $listeners = [
|
public function getListeners()
|
||||||
'serverInstalled',
|
{
|
||||||
'refreshServerShow' => 'serverInstalled',
|
$teamId = auth()->user()->currentTeam()->id;
|
||||||
'revalidate' => '$refresh',
|
|
||||||
];
|
return [
|
||||||
|
"echo-private:team.{$teamId},CloudflareTunnelConfigured" => 'cloudflareTunnelConfigured',
|
||||||
|
'refreshServerShow' => 'serverInstalled',
|
||||||
|
'revalidate' => '$refresh',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
protected $rules = [
|
protected $rules = [
|
||||||
'server.name' => 'required',
|
'server.name' => 'required',
|
||||||
@@ -96,6 +101,12 @@ class Form extends Component
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function cloudflareTunnelConfigured()
|
||||||
|
{
|
||||||
|
$this->serverInstalled();
|
||||||
|
$this->dispatch('success', 'Cloudflare Tunnels configured successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
public function serverInstalled()
|
public function serverInstalled()
|
||||||
{
|
{
|
||||||
$this->server->refresh();
|
$this->server->refresh();
|
||||||
@@ -238,4 +249,12 @@ class Form extends Component
|
|||||||
$this->server->settings->save();
|
$this->server->settings->save();
|
||||||
$this->dispatch('success', 'Server timezone updated.');
|
$this->dispatch('success', 'Server timezone updated.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function manualCloudflareConfig()
|
||||||
|
{
|
||||||
|
$this->server->settings->is_cloudflare_tunnel = true;
|
||||||
|
$this->server->settings->save();
|
||||||
|
$this->server->refresh();
|
||||||
|
$this->dispatch('success', 'Cloudflare Tunnels enabled.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ class Proxy extends Component
|
|||||||
{
|
{
|
||||||
$this->server->proxy = null;
|
$this->server->proxy = null;
|
||||||
$this->server->save();
|
$this->server->save();
|
||||||
|
$this->dispatch('proxyChanged');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function selectProxy($proxy_type)
|
public function selectProxy($proxy_type)
|
||||||
@@ -47,7 +48,7 @@ class Proxy extends Component
|
|||||||
$this->server->proxy->set('type', $proxy_type);
|
$this->server->proxy->set('type', $proxy_type);
|
||||||
$this->server->save();
|
$this->server->save();
|
||||||
$this->selectedProxy = $this->server->proxy->type;
|
$this->selectedProxy = $this->server->proxy->type;
|
||||||
if ($this->selectedProxy !== 'NONE') {
|
if ($this->server->proxySet()) {
|
||||||
StartProxy::run($this->server, false);
|
StartProxy::run($this->server, false);
|
||||||
}
|
}
|
||||||
$this->dispatch('proxyStatusUpdated');
|
$this->dispatch('proxyStatusUpdated');
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ use App\Actions\Proxy\CheckProxy;
|
|||||||
use App\Actions\Proxy\StartProxy;
|
use App\Actions\Proxy\StartProxy;
|
||||||
use App\Events\ProxyStatusChanged;
|
use App\Events\ProxyStatusChanged;
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
|
use Illuminate\Process\InvokedProcess;
|
||||||
|
use Illuminate\Support\Facades\Process;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
class Deploy extends Component
|
class Deploy extends Component
|
||||||
@@ -29,6 +31,7 @@ class Deploy extends Component
|
|||||||
'serverRefresh' => 'proxyStatusUpdated',
|
'serverRefresh' => 'proxyStatusUpdated',
|
||||||
'checkProxy',
|
'checkProxy',
|
||||||
'startProxy',
|
'startProxy',
|
||||||
|
'proxyChanged' => 'proxyStatusUpdated',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,21 +97,43 @@ class Deploy extends Component
|
|||||||
public function stop(bool $forceStop = true)
|
public function stop(bool $forceStop = true)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
if ($this->server->isSwarm()) {
|
$containerName = $this->server->isSwarm() ? 'coolify-proxy_traefik' : 'coolify-proxy';
|
||||||
instant_remote_process([
|
$timeout = 30;
|
||||||
'docker service rm coolify-proxy_traefik',
|
|
||||||
], $this->server);
|
$process = $this->stopContainer($containerName, $timeout);
|
||||||
} else {
|
|
||||||
instant_remote_process([
|
$startTime = time();
|
||||||
'docker rm -f coolify-proxy',
|
while ($process->running()) {
|
||||||
], $this->server);
|
if (time() - $startTime >= $timeout) {
|
||||||
|
$this->forceStopContainer($containerName);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
usleep(100000);
|
||||||
}
|
}
|
||||||
$this->server->proxy->status = 'exited';
|
|
||||||
$this->server->proxy->force_stop = $forceStop;
|
$this->removeContainer($containerName);
|
||||||
$this->server->save();
|
|
||||||
$this->dispatch('proxyStatusUpdated');
|
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
return handleError($e, $this);
|
return handleError($e, $this);
|
||||||
|
} finally {
|
||||||
|
$this->server->proxy->force_stop = $forceStop;
|
||||||
|
$this->server->proxy->status = 'exited';
|
||||||
|
$this->server->save();
|
||||||
|
$this->dispatch('proxyStatusUpdated');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function stopContainer(string $containerName, int $timeout): InvokedProcess
|
||||||
|
{
|
||||||
|
return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
|
||||||
|
}
|
||||||
|
|
||||||
|
private function forceStopContainer(string $containerName)
|
||||||
|
{
|
||||||
|
instant_remote_process(["docker kill $containerName"], $this->server, throwError: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function removeContainer(string $containerName)
|
||||||
|
{
|
||||||
|
instant_remote_process(["docker rm -f $containerName"], $this->server, throwError: false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class Show extends Component
|
|||||||
|
|
||||||
public $parameters = [];
|
public $parameters = [];
|
||||||
|
|
||||||
protected $listeners = ['proxyStatusUpdated'];
|
protected $listeners = ['proxyStatusUpdated', 'proxyChanged' => 'proxyStatusUpdated'];
|
||||||
|
|
||||||
public function proxyStatusUpdated()
|
public function proxyStatusUpdated()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Livewire\Server;
|
namespace App\Livewire\Server;
|
||||||
|
|
||||||
|
use App\Models\PrivateKey;
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
@@ -13,25 +14,15 @@ class ShowPrivateKey extends Component
|
|||||||
|
|
||||||
public $parameters;
|
public $parameters;
|
||||||
|
|
||||||
public function setPrivateKey($newPrivateKeyId)
|
public function setPrivateKey($privateKeyId)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$oldPrivateKeyId = $this->server->private_key_id;
|
$privateKey = PrivateKey::findOrFail($privateKeyId);
|
||||||
refresh_server_connection($this->server->privateKey);
|
$this->server->update(['private_key_id' => $privateKey->id]);
|
||||||
$this->server->update([
|
|
||||||
'private_key_id' => $newPrivateKeyId,
|
|
||||||
]);
|
|
||||||
$this->server->refresh();
|
$this->server->refresh();
|
||||||
refresh_server_connection($this->server->privateKey);
|
$this->dispatch('success', 'Private key updated successfully.');
|
||||||
$this->checkConnection();
|
} catch (\Exception $e) {
|
||||||
} catch (\Throwable $e) {
|
$this->dispatch('error', 'Failed to update private key: '.$e->getMessage());
|
||||||
$this->server->update([
|
|
||||||
'private_key_id' => $oldPrivateKeyId,
|
|
||||||
]);
|
|
||||||
$this->server->refresh();
|
|
||||||
refresh_server_connection($this->server->privateKey);
|
|
||||||
|
|
||||||
return handleError($e, $this);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,7 +34,7 @@ class ShowPrivateKey extends Component
|
|||||||
$this->dispatch('success', 'Server is reachable.');
|
$this->dispatch('success', 'Server is reachable.');
|
||||||
} else {
|
} else {
|
||||||
ray($error);
|
ray($error);
|
||||||
$this->dispatch('error', 'Server is not reachable.<br>Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/openssh">documentation</a> for further help.');
|
$this->dispatch('error', 'Server is not reachable.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/openssh">documentation</a> for further help.<br><br>Error: '.$error);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ namespace App\Livewire\Team;
|
|||||||
|
|
||||||
use App\Models\Team;
|
use App\Models\Team;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
class AdminView extends Component
|
class AdminView extends Component
|
||||||
@@ -73,8 +75,13 @@ class AdminView extends Component
|
|||||||
$team->delete();
|
$team->delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete($id)
|
public function delete($id, $password)
|
||||||
{
|
{
|
||||||
|
if (! Hash::check($password, Auth::user()->password)) {
|
||||||
|
$this->addError('password', 'The provided password is incorrect.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (! auth()->user()->isInstanceAdmin()) {
|
if (! auth()->user()->isInstanceAdmin()) {
|
||||||
return $this->dispatch('error', 'You are not authorized to delete users');
|
return $this->dispatch('error', 'You are not authorized to delete users');
|
||||||
}
|
}
|
||||||
|
|||||||
76
app/Livewire/Terminal/Index.php
Normal file
76
app/Livewire/Terminal/Index.php
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Terminal;
|
||||||
|
|
||||||
|
use App\Models\Server;
|
||||||
|
use Livewire\Attributes\On;
|
||||||
|
use Livewire\Component;
|
||||||
|
|
||||||
|
class Index extends Component
|
||||||
|
{
|
||||||
|
public $selected_uuid = 'default';
|
||||||
|
|
||||||
|
public $servers = [];
|
||||||
|
|
||||||
|
public $containers = [];
|
||||||
|
|
||||||
|
public function mount()
|
||||||
|
{
|
||||||
|
if (! auth()->user()->isAdmin()) {
|
||||||
|
abort(403);
|
||||||
|
}
|
||||||
|
$this->servers = Server::isReachable()->get();
|
||||||
|
$this->containers = $this->getAllActiveContainers();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getAllActiveContainers()
|
||||||
|
{
|
||||||
|
return collect($this->servers)->flatMap(function ($server) {
|
||||||
|
if (! $server->isFunctional()) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $server->loadAllContainers()->map(function ($container) use ($server) {
|
||||||
|
$state = data_get_str($container, 'State')->lower();
|
||||||
|
if ($state->contains('running')) {
|
||||||
|
return [
|
||||||
|
'name' => data_get($container, 'Names'),
|
||||||
|
'connection_name' => data_get($container, 'Names'),
|
||||||
|
'uuid' => data_get($container, 'Names'),
|
||||||
|
'status' => data_get_str($container, 'State')->lower(),
|
||||||
|
'server' => $server,
|
||||||
|
'server_uuid' => $server->uuid,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
})->filter();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updatedSelectedUuid()
|
||||||
|
{
|
||||||
|
$this->connectToContainer();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[On('connectToContainer')]
|
||||||
|
public function connectToContainer()
|
||||||
|
{
|
||||||
|
if ($this->selected_uuid === 'default') {
|
||||||
|
$this->dispatch('error', 'Please select a server or a container.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$container = collect($this->containers)->firstWhere('uuid', $this->selected_uuid);
|
||||||
|
$this->dispatch('send-terminal-command',
|
||||||
|
isset($container),
|
||||||
|
$container['connection_name'] ?? $this->selected_uuid,
|
||||||
|
$container['server_uuid'] ?? $this->selected_uuid
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return view('livewire.terminal.index');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,9 @@ use App\Enums\ApplicationDeploymentStatus;
|
|||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
use Illuminate\Process\InvokedProcess;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Facades\Process;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use OpenApi\Attributes as OA;
|
use OpenApi\Attributes as OA;
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
@@ -149,12 +151,64 @@ class Application extends BaseModel
|
|||||||
return Application::whereRelation('environment.project.team', 'id', $teamId)->orderBy('name');
|
return Application::whereRelation('environment.project.team', 'id', $teamId)->orderBy('name');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getContainersToStop(bool $previewDeployments = false): array
|
||||||
|
{
|
||||||
|
$containers = $previewDeployments
|
||||||
|
? getCurrentApplicationContainerStatus($this->destination->server, $this->id, includePullrequests: true)
|
||||||
|
: getCurrentApplicationContainerStatus($this->destination->server, $this->id, 0);
|
||||||
|
|
||||||
|
return $containers->pluck('Names')->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stopContainers(array $containerNames, $server, int $timeout = 600)
|
||||||
|
{
|
||||||
|
$processes = [];
|
||||||
|
foreach ($containerNames as $containerName) {
|
||||||
|
$processes[$containerName] = $this->stopContainer($containerName, $server, $timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
$startTime = time();
|
||||||
|
while (count($processes) > 0) {
|
||||||
|
$finishedProcesses = array_filter($processes, function ($process) {
|
||||||
|
return ! $process->running();
|
||||||
|
});
|
||||||
|
foreach ($finishedProcesses as $containerName => $process) {
|
||||||
|
unset($processes[$containerName]);
|
||||||
|
$this->removeContainer($containerName, $server);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (time() - $startTime >= $timeout) {
|
||||||
|
$this->forceStopRemainingContainers(array_keys($processes), $server);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
usleep(100000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stopContainer(string $containerName, $server, int $timeout): InvokedProcess
|
||||||
|
{
|
||||||
|
return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeContainer(string $containerName, $server)
|
||||||
|
{
|
||||||
|
instant_remote_process(command: ["docker rm -f $containerName"], server: $server, throwError: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function forceStopRemainingContainers(array $containerNames, $server)
|
||||||
|
{
|
||||||
|
foreach ($containerNames as $containerName) {
|
||||||
|
instant_remote_process(command: ["docker kill $containerName"], server: $server, throwError: false);
|
||||||
|
$this->removeContainer($containerName, $server);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function delete_configurations()
|
public function delete_configurations()
|
||||||
{
|
{
|
||||||
$server = data_get($this, 'destination.server');
|
$server = data_get($this, 'destination.server');
|
||||||
$workdir = $this->workdir();
|
$workdir = $this->workdir();
|
||||||
if (str($workdir)->endsWith($this->uuid)) {
|
if (str($workdir)->endsWith($this->uuid)) {
|
||||||
ray('Deleting workdir');
|
|
||||||
instant_remote_process(['rm -rf '.$this->workdir()], $server, false);
|
instant_remote_process(['rm -rf '.$this->workdir()], $server, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -176,6 +230,13 @@ class Application extends BaseModel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function delete_connected_networks($uuid)
|
||||||
|
{
|
||||||
|
$server = data_get($this, 'destination.server');
|
||||||
|
instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false);
|
||||||
|
instant_remote_process(["docker network rm {$uuid}"], $server, false);
|
||||||
|
}
|
||||||
|
|
||||||
public function additional_servers()
|
public function additional_servers()
|
||||||
{
|
{
|
||||||
return $this->belongsToMany(Server::class, 'additional_destinations')
|
return $this->belongsToMany(Server::class, 'additional_destinations')
|
||||||
@@ -1034,6 +1095,7 @@ class Application extends BaseModel
|
|||||||
throw new \Exception($e->getMessage());
|
throw new \Exception($e->getMessage());
|
||||||
}
|
}
|
||||||
$services = data_get($yaml, 'services');
|
$services = data_get($yaml, 'services');
|
||||||
|
|
||||||
$commands = collect([]);
|
$commands = collect([]);
|
||||||
$services = collect($services)->map(function ($service) use ($commands) {
|
$services = collect($services)->map(function ($service) use ($commands) {
|
||||||
$serviceVolumes = collect(data_get($service, 'volumes', []));
|
$serviceVolumes = collect(data_get($service, 'volumes', []));
|
||||||
@@ -1166,7 +1228,6 @@ class Application extends BaseModel
|
|||||||
} else {
|
} else {
|
||||||
throw new \RuntimeException("Docker Compose file not found at: $workdir$composeFile<br><br>Check if you used the right extension (.yaml or .yml) in the compose file name.");
|
throw new \RuntimeException("Docker Compose file not found at: $workdir$composeFile<br><br>Check if you used the right extension (.yaml or .yml) in the compose file name.");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function parseContainerLabels(?ApplicationPreview $preview = null)
|
public function parseContainerLabels(?ApplicationPreview $preview = null)
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use DanHarrin\LivewireRateLimiting\WithRateLimiting;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
use OpenApi\Attributes as OA;
|
use OpenApi\Attributes as OA;
|
||||||
use phpseclib3\Crypt\PublicKeyLoader;
|
use phpseclib3\Crypt\PublicKeyLoader;
|
||||||
|
|
||||||
@@ -22,48 +25,144 @@ use phpseclib3\Crypt\PublicKeyLoader;
|
|||||||
)]
|
)]
|
||||||
class PrivateKey extends BaseModel
|
class PrivateKey extends BaseModel
|
||||||
{
|
{
|
||||||
|
use WithRateLimiting;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'name',
|
'name',
|
||||||
'description',
|
'description',
|
||||||
'private_key',
|
'private_key',
|
||||||
'is_git_related',
|
'is_git_related',
|
||||||
'team_id',
|
'team_id',
|
||||||
|
'fingerprint',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'private_key' => 'encrypted',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected static function booted()
|
protected static function booted()
|
||||||
{
|
{
|
||||||
static::saving(function ($key) {
|
static::saving(function ($key) {
|
||||||
$privateKey = data_get($key, 'private_key');
|
$key->private_key = formatPrivateKey($key->private_key);
|
||||||
if (substr($privateKey, -1) !== "\n") {
|
|
||||||
$key->private_key = $privateKey."\n";
|
if (! self::validatePrivateKey($key->private_key)) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'private_key' => ['The private key is invalid.'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$key->fingerprint = self::generateFingerprint($key->private_key);
|
||||||
|
if (self::fingerprintExists($key->fingerprint, $key->id)) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'private_key' => ['This private key already exists.'],
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
static::deleted(function ($key) {
|
||||||
|
self::deleteFromStorage($key);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPublicKey()
|
||||||
|
{
|
||||||
|
return self::extractPublicKeyFromPrivate($this->private_key) ?? 'Error loading private key';
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function ownedByCurrentTeam(array $select = ['*'])
|
public static function ownedByCurrentTeam(array $select = ['*'])
|
||||||
{
|
{
|
||||||
$selectArray = collect($select)->concat(['id']);
|
$selectArray = collect($select)->concat(['id']);
|
||||||
|
|
||||||
return PrivateKey::whereTeamId(currentTeam()->id)->select($selectArray->all());
|
return self::whereTeamId(currentTeam()->id)->select($selectArray->all());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function publicKey()
|
public static function validatePrivateKey($privateKey)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
return PublicKeyLoader::load($this->private_key)->getPublicKey()->toString('OpenSSH', ['comment' => '']);
|
PublicKeyLoader::load($privateKey);
|
||||||
|
|
||||||
|
return true;
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
return 'Error loading private key';
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isEmpty()
|
public static function createAndStore(array $data)
|
||||||
{
|
{
|
||||||
if ($this->servers()->count() === 0 && $this->applications()->count() === 0 && $this->githubApps()->count() === 0 && $this->gitlabApps()->count() === 0) {
|
$privateKey = new self($data);
|
||||||
return true;
|
$privateKey->save();
|
||||||
}
|
$privateKey->storeInFileSystem();
|
||||||
|
|
||||||
return false;
|
return $privateKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function generateNewKeyPair($type = 'rsa')
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$instance = new self;
|
||||||
|
$instance->rateLimit(10);
|
||||||
|
$name = generate_random_name();
|
||||||
|
$description = 'Created by Coolify';
|
||||||
|
$keyPair = generateSSHKey($type === 'ed25519' ? 'ed25519' : 'rsa');
|
||||||
|
|
||||||
|
return [
|
||||||
|
'name' => $name,
|
||||||
|
'description' => $description,
|
||||||
|
'private_key' => $keyPair['private'],
|
||||||
|
'public_key' => $keyPair['public'],
|
||||||
|
];
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
throw new \Exception("Failed to generate new {$type} key: ".$e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function extractPublicKeyFromPrivate($privateKey)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$key = PublicKeyLoader::load($privateKey);
|
||||||
|
|
||||||
|
return $key->getPublicKey()->toString('OpenSSH', ['comment' => '']);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function validateAndExtractPublicKey($privateKey)
|
||||||
|
{
|
||||||
|
$isValid = self::validatePrivateKey($privateKey);
|
||||||
|
$publicKey = $isValid ? self::extractPublicKeyFromPrivate($privateKey) : '';
|
||||||
|
|
||||||
|
return [
|
||||||
|
'isValid' => $isValid,
|
||||||
|
'publicKey' => $publicKey,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function storeInFileSystem()
|
||||||
|
{
|
||||||
|
$filename = "ssh_key@{$this->uuid}";
|
||||||
|
Storage::disk('ssh-keys')->put($filename, $this->private_key);
|
||||||
|
|
||||||
|
return "/var/www/html/storage/app/ssh/keys/{$filename}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function deleteFromStorage(self $privateKey)
|
||||||
|
{
|
||||||
|
$filename = "ssh_key@{$privateKey->uuid}";
|
||||||
|
Storage::disk('ssh-keys')->delete($filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getKeyLocation()
|
||||||
|
{
|
||||||
|
return "/var/www/html/storage/app/ssh/keys/ssh_key@{$this->uuid}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updatePrivateKey(array $data)
|
||||||
|
{
|
||||||
|
$this->update($data);
|
||||||
|
$this->storeInFileSystem();
|
||||||
|
|
||||||
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function servers()
|
public function servers()
|
||||||
@@ -85,4 +184,53 @@ class PrivateKey extends BaseModel
|
|||||||
{
|
{
|
||||||
return $this->hasMany(GitlabApp::class);
|
return $this->hasMany(GitlabApp::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function isInUse()
|
||||||
|
{
|
||||||
|
return $this->servers()->exists()
|
||||||
|
|| $this->applications()->exists()
|
||||||
|
|| $this->githubApps()->exists()
|
||||||
|
|| $this->gitlabApps()->exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function safeDelete()
|
||||||
|
{
|
||||||
|
if (! $this->isInUse()) {
|
||||||
|
$this->delete();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function generateFingerprint($privateKey)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$key = PublicKeyLoader::load($privateKey);
|
||||||
|
$publicKey = $key->getPublicKey();
|
||||||
|
|
||||||
|
return $publicKey->getFingerprint('sha256');
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function fingerprintExists($fingerprint, $excludeId = null)
|
||||||
|
{
|
||||||
|
$query = self::where('fingerprint', $fingerprint);
|
||||||
|
|
||||||
|
if (! is_null($excludeId)) {
|
||||||
|
$query->where('id', '!=', $excludeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $query->exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function cleanupUnusedKeys()
|
||||||
|
{
|
||||||
|
self::ownedByCurrentTeam()->each(function ($privateKey) {
|
||||||
|
$privateKey->safeDelete();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,14 +35,17 @@ class ScheduledDatabaseBackup extends BaseModel
|
|||||||
{
|
{
|
||||||
return $this->hasMany(ScheduledDatabaseBackupExecution::class)->where('created_at', '>=', now()->subDays($days))->get();
|
return $this->hasMany(ScheduledDatabaseBackupExecution::class)->where('created_at', '>=', now()->subDays($days))->get();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function server()
|
public function server()
|
||||||
{
|
{
|
||||||
if ($this->database) {
|
if ($this->database) {
|
||||||
if ($this->database->destination && $this->database->destination->server) {
|
if ($this->database->destination && $this->database->destination->server) {
|
||||||
$server = $this->database->destination->server;
|
$server = $this->database->destination->server;
|
||||||
|
|
||||||
return $server;
|
return $server;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user