Compare commits

...

139 Commits

Author SHA1 Message Date
Andras Bacsai
5ce449aa08 Merge pull request #1381 from coollabsio/next
v4.0.0-beta.109
2023-11-06 13:16:55 +01:00
Andras Bacsai
6203804713 handle 2023-11-06 13:07:29 +01:00
Andras Bacsai
0858faf628 fix: remove filter 2023-11-06 12:53:43 +01:00
Andras Bacsai
a84f3e0577 fix link 2023-11-06 12:46:58 +01:00
Andras Bacsai
8d571a5eab fix: add nixpacks info 2023-11-06 12:40:53 +01:00
Andras Bacsai
7a117c61c4 fix: separate delete with validation of server 2023-11-06 12:31:02 +01:00
Andras Bacsai
8b034f15fc fix: delete resource if server is not functional
fix: set status to exited on all resources
2023-11-06 11:51:20 +01:00
Andras Bacsai
b4a6499c83 fix: port number should be int 2023-11-06 10:58:00 +01:00
Andras Bacsai
c083acaeef fix: resourcesdelete command 2023-11-06 10:55:46 +01:00
Andras Bacsai
9c6d8320d8 fix: UI 2023-11-06 10:54:11 +01:00
Andras Bacsai
0e7a304610 fix: private key not found error 2023-11-06 10:53:01 +01:00
Andras Bacsai
83993cbbb2 fix: telegram text 2023-11-06 10:49:35 +01:00
Andras Bacsai
6840ddd3e6 fix: no environments 2023-11-06 10:48:30 +01:00
Andras Bacsai
2c6ece62bb fixes 2023-11-06 10:45:06 +01:00
Andras Bacsai
3f8514050e fix: set default from/sender names 2023-11-06 10:26:56 +01:00
Andras Bacsai
a4a653603e fix: missing $mailMessage 2023-11-06 10:23:51 +01:00
Andras Bacsai
b6d8851c99 fix: no id found 2023-11-06 10:22:46 +01:00
Andras Bacsai
bcd7697f50 fix: delete destination 2023-11-06 10:20:13 +01:00
Andras Bacsai
f1da735c40 fix: gh webhook response 200 to installation_repositories 2023-11-06 10:16:21 +01:00
Andras Bacsai
01331c287b fix: notification url in containerstatusjob 2023-11-06 10:10:40 +01:00
Andras Bacsai
3320de787a fix: network service parse 2023-11-06 09:55:22 +01:00
Andras Bacsai
2bddb09384 fix: set labels on generate domain 2023-11-06 09:27:00 +01:00
Andras Bacsai
6f673d7a07 fixes 2023-11-05 09:49:23 +01:00
Andras Bacsai
0a5a101ef4 update github actions 2023-11-03 18:01:17 +01:00
Andras Bacsai
88590fbf0f fix: dockerfile build pack fix 2023-11-03 17:55:53 +01:00
Andras Bacsai
90291b2edf fix: deployments ui 2023-11-03 17:45:30 +01:00
Andras Bacsai
070573f0df fix: local dev repo 2023-11-03 15:11:06 +01:00
Andras Bacsai
e583beb753 fix: invoice.paid should sleep for 5 seconds 2023-11-03 14:51:29 +01:00
Andras Bacsai
d31683df61 update 2023-11-03 14:39:11 +01:00
Andras Bacsai
0a83ed82fa tinkerwell 2023-11-03 14:38:34 +01:00
Andras Bacsai
0cd3a3d848 fix: increase polling time for services
fix: allow domain as ip address
2023-11-03 10:57:58 +01:00
Andras Bacsai
a0031efce0 resale license check needs to be updated 2023-11-02 14:10:29 +01:00
Andras Bacsai
3bffe3f010 fix: missing environment variables prevewi on service 2023-11-02 14:03:02 +01:00
Andras Bacsai
b9a37233a2 disable license check for now 2023-11-02 11:45:43 +01:00
Andras Bacsai
aab122d97e add cache-key nixpacks 2023-11-01 21:05:24 +01:00
Andras Bacsai
658d608f55 ok, it is not nixpacks problem 2023-11-01 21:02:05 +01:00
Andras Bacsai
0838343841 fix: pull requests
feat: add follow for full screen logs
2023-11-01 20:55:21 +01:00
Andras Bacsai
b557ea1e1d revert nixpacks version 2023-11-01 20:54:50 +01:00
Andras Bacsai
4520070df3 fix: pull requests deployments
feat: filter deployments logs by pull requests
2023-11-01 15:39:47 +01:00
Andras Bacsai
be8ea78b1b feat: deployment logs fullscreen 2023-11-01 14:06:15 +01:00
Andras Bacsai
1175d68ab5 feat: full screen logs
fix: logs are in order now
2023-11-01 13:47:40 +01:00
Andras Bacsai
f56d373ed2 update nixpacks 2023-11-01 12:54:49 +01:00
Andras Bacsai
c6253658ca feat: restart application
fix: a few things in application deployment job
2023-11-01 12:19:08 +01:00
Andras Bacsai
4249aec936 Merge branch 'main' into next 2023-11-01 10:56:03 +01:00
Andras Bacsai
25f80aba5f Merge pull request #1376 from mauvehed/fix/basedir-permissions 2023-10-31 09:18:22 +01:00
Andras Bacsai
4550983761 Update install.sh 2023-10-31 08:13:50 +01:00
Andras Bacsai
b0238372a2 Update install.sh
Do not change permission on /data
2023-10-31 08:13:06 +01:00
mauvehed
a021b71496 fix(install.sh): change ownership and permissions only for /data/coolify directory instead of /data
The ownership and permissions are now set only for the /data/coolify directory instead of the entire /data directory. This ensures that the ownership and permissions are applied only to the necessary directory and not to other directories within /data.
2023-10-29 10:05:16 -05:00
Andras Bacsai
e3958d9626 added a few services 2023-10-27 14:22:35 +02:00
Andras Bacsai
55891d7001 Merge pull request #1367 from itishermann/main
[+] Template: Kuzzle, Moodle, Sonarqube, RabbitMQ
2023-10-27 13:16:52 +02:00
Andras Bacsai
b12ac8bb29 Merge pull request #1364 from theh2so4/main
[+] Template: BudgE, Duplicati, Jellyfin, phpMyAdmin, Vaultwarden, Whoogle and FileBrowser
2023-10-27 12:57:00 +02:00
TheH2SO4
728a9f88eb [!] Template: FileBrowser 2023-10-27 12:23:16 +02:00
TheH2SO4
57267c3ee0 [+] Template: FileBrowser
🆕 **New Template**:

-> ℹ️ **FileBrowser**: FileBrowser simplifies file and folder management on various storage systems.
2023-10-27 12:21:14 +02:00
Andras Bacsai
d3d133ed1f version++ 2023-10-27 12:07:48 +02:00
Andras Bacsai
f5240abbe5 Merge pull request #1371 from coollabsio/next
v4.0.0-beta.108
2023-10-27 11:44:35 +02:00
Andras Bacsai
abf5840f97 fixing 2023-10-27 11:44:10 +02:00
Andras Bacsai
dc6d5af4aa Merge pull request #1370 from coollabsio/next
v4.0.0-beta.107
2023-10-27 11:24:03 +02:00
Andras Bacsai
0b88cd69f2 fix: remove coolify labels from ui 2023-10-27 11:23:29 +02:00
Andras Bacsai
ce165719d6 Merge pull request #1369 from coollabsio/next
v4.0.0-beta.106
2023-10-27 10:43:54 +02:00
Andras Bacsai
4f543ce20f remove ray 2023-10-27 10:43:05 +02:00
Andras Bacsai
55f957df21 fix: git ls-remote 2023-10-27 10:42:56 +02:00
Andras Bacsai
38f59b9410 revert 2023-10-27 10:30:15 +02:00
Andras Bacsai
ebe6655349 update invoice paid 2023-10-27 10:28:43 +02:00
Andras Bacsai
038ea08ca7 add payment_intent.payment_failed to subs 2023-10-27 10:26:35 +02:00
Andras Bacsai
ba424efd39 cloud: fix subs 2023-10-27 10:17:13 +02:00
Andras Bacsai
75aef0e60b Merge pull request #1366 from coollabsio/next
v4.0.0-beta.105
2023-10-27 09:31:06 +02:00
Andras Bacsai
eda8b34297 fix 2023-10-27 09:28:43 +02:00
Andras Bacsai
d8151ddb2e fix: add ssh options to git ls-remote 2023-10-27 09:25:15 +02:00
Hermann Kao
7925228f97 add service template for sonarqube 2023-10-27 01:41:07 +02:00
Hermann Kao
a7dc62aaa0 add service template for rabbitmq 2023-10-27 00:42:15 +02:00
Hermann Kao
632dbd155b add service template for moodle based on bitnami images 2023-10-27 00:18:06 +02:00
Hermann Kao
fe092bb7a5 add service template for kuzzle 2023-10-26 23:29:59 +02:00
Andras Bacsai
928345c8ea fix: force password reset on invited accounts 2023-10-26 20:45:38 +02:00
Andras Bacsai
52d6fb51d5 pocketbase 2023-10-26 15:53:42 +02:00
Andras Bacsai
06d7c69487 add nocodb 2023-10-26 13:32:23 +02:00
Andras Bacsai
756c7f81ca fix: if user is invited, that means its email is verified 2023-10-26 13:00:40 +02:00
Andras Bacsai
d7af57a95e fix: custom labels only should have non-coolify labels
fix: pull helper image every 10 minutes instead of every deployment
2023-10-26 11:38:37 +02:00
TheH2SO4
722ff15fbd [+] Template: Vaultwarden
🆕 **New Template**:

-> ℹ️ **Vaultwarden**: Vaultwarden is an open-source password manager that allows you to securely store and manage your passwords, helping you stay organized and protected.
2023-10-26 11:21:31 +02:00
Andras Bacsai
f9c469497e version++ 2023-10-26 11:15:37 +02:00
Andras Bacsai
7ecbedb48a Merge pull request #1365 from coollabsio/next
v4.0.0-beta.104
2023-10-26 11:11:59 +02:00
Andras Bacsai
76878f66b9 remove ray 2023-10-26 11:07:25 +02:00
Andras Bacsai
b9afef50c4 version++ 2023-10-26 10:35:14 +02:00
Andras Bacsai
83ebd1e649 feat: improve deployment time by a lot 2023-10-26 10:33:57 +02:00
Andras Bacsai
76431c3fd5 service updates 2023-10-26 10:02:51 +02:00
Andras Bacsai
96a4d0bbb0 fix: lock SERVICE_FQDN envs 2023-10-26 10:02:45 +02:00
Andras Bacsai
4cfc739730 add openblocks 2023-10-26 09:34:02 +02:00
TheH2SO4
fcd0d8d359 [+] Template: BudgE
🆕 **New Template**:

-> ℹ️ **BudgE**: Budge is an open-source 'budgeting with envelopes' personal finance app, helping you manage your finances effectively.
2023-10-25 22:22:20 +02:00
TheH2SO4
3fcac0ac35 [+] Template: phpMyAdmin
🆕 **New Template**:

-> ℹ️ **phpMyAdmin**: phpMyAdmin is a web-based database management tool for administering your MySQL and MariaDB databases through a user-friendly interface.
2023-10-25 21:57:03 +02:00
TheH2SO4
fcc8a7f0ed [!] Template Fix: Whoogle
🐛 **Bug Fix**:

ℹ️ **Whoogle**: Tags section was not added, it's now fixed.
2023-10-25 21:50:55 +02:00
TheH2SO4
6950ead041 [+] Template: Jellyfin
🆕 **New Template**:

-> ℹ️ **Jellyfin**: Jellyfin is an open-source media server for hosting and streaming your media collection, providing an alternative to proprietary media platforms.
2023-10-25 21:46:52 +02:00
TheH2SO4
f78c49fc82 [+] Template: Whoogle
🆕 **New Template**:

-> ℹ️ **Whoogle**: Whoogle is a self-hosted, privacy-focused search engine front-end for accessing Google search results without tracking and data collection.
2023-10-25 21:40:38 +02:00
TheH2SO4
5f2581020b [+] Template: Duplicati
🆕 **New Template**:

-> ℹ️ **Duplicati**: Duplicati is an open-source backup solution, allowing you to safeguard your data with ease through scheduled backups and encryption.
2023-10-25 21:36:57 +02:00
TheH2SO4
2fb674ae85 Merge pull request #4 from coollabsio/main
Update
2023-10-25 21:27:42 +02:00
Andras Bacsai
a95bd906bc Merge pull request #1363 from coollabsio/next
v4.0.0-beta.103
2023-10-25 20:21:27 +02:00
Andras Bacsai
21795cf788 fix: space in build args 2023-10-25 20:19:38 +02:00
Andras Bacsai
6e98fd9403 grafana + openblocks 2023-10-25 20:13:45 +02:00
Andras Bacsai
ead1edc2b9 Services 2023-10-25 15:44:34 +02:00
Andras Bacsai
db822cb876 Merge pull request #1357 from theh2so4/main
[+] Templates: Dashboard, Emby, EmbyStat and Grocy
2023-10-25 15:41:54 +02:00
Andras Bacsai
65bfce43c0 fix: server settings guarded 2023-10-25 11:50:22 +02:00
Andras Bacsai
50fc05ab52 update init script 2023-10-25 11:43:18 +02:00
Andras Bacsai
c9cf5c486f Merge pull request #1362 from coollabsio/next
v4.0.0-beta.102
2023-10-25 11:07:17 +02:00
Andras Bacsai
379f4b9dff feat: show webhook on ui
feat: n8n service
2023-10-25 10:43:07 +02:00
Andras Bacsai
aa02b8d433 fix: rate limit for api + add mariadb + mysql 2023-10-25 09:56:58 +02:00
Andras Bacsai
70ecb92e82 cleanup ssh dir on start 2023-10-25 09:41:41 +02:00
Andras Bacsai
d5cc2a2eed feat: download local backups 2023-10-25 09:28:26 +02:00
Andras Bacsai
2b91bd24c5 Merge pull request #1361 from coollabsio/next
v4.0.0-beta.101
2023-10-24 15:51:54 +02:00
Andras Bacsai
5e8ac1b48e fix: mongodb healtcheck command 2023-10-24 15:47:29 +02:00
Andras Bacsai
dc86170ef5 version++ 2023-10-24 15:41:44 +02:00
Andras Bacsai
0232cf5b4c feat: lock environment variables 2023-10-24 15:41:21 +02:00
Andras Bacsai
6e73f7f2e4 fix: encrypt mongodb password 2023-10-24 15:40:29 +02:00
Andras Bacsai
61c43804e3 Merge pull request #1360 from coollabsio/next
v4.0.0-beta.100
2023-10-24 14:44:31 +02:00
Andras Bacsai
72421d692b add slogans 2023-10-24 14:36:43 +02:00
Andras Bacsai
f801bb98cd feat: mysql, mariadb 2023-10-24 14:31:28 +02:00
Andras Bacsai
b2d111e49a feat: simple search functionality 2023-10-24 12:33:49 +02:00
Andras Bacsai
c82e02218f version++ 2023-10-24 11:08:59 +02:00
Andras Bacsai
29f64076de fix: syncbunny command 2023-10-24 11:08:15 +02:00
Andras Bacsai
393c334b12 version++ 2023-10-24 11:08:11 +02:00
Andras Bacsai
678b264688 fix: make sure coolfiy network exists on install 2023-10-24 11:08:05 +02:00
Andras Bacsai
2620bfbf08 Merge pull request #1359 from coollabsio/next
v4.0.0-beta.99
2023-10-24 10:59:36 +02:00
Andras Bacsai
18c32decad guarded 2023-10-24 10:43:34 +02:00
Andras Bacsai
a6f9e5f0af fixes 2023-10-24 10:42:33 +02:00
Andras Bacsai
f187040b7e fix: mongodb backup 2023-10-24 10:42:28 +02:00
Andras Bacsai
5510321776 syncbunny update 2023-10-24 10:22:36 +02:00
Andras Bacsai
69691b2ca7 fix: service template generator + appwrite 2023-10-24 10:19:12 +02:00
Andras Bacsai
8bfc1a7c06 fix: do not allow to delete env if a resource is defined 2023-10-24 10:11:21 +02:00
Andras Bacsai
554222abc7 fix: cleanup stucked resources on start 2023-10-24 10:10:55 +02:00
Andras Bacsai
b1a1aeeb75 fix: clone to with the same environment name 2023-10-24 10:10:45 +02:00
Andras Bacsai
91acd4cb6a fix: backups should be done with internal db url
fix: create default database on mongodb start with a collection
2023-10-24 09:34:35 +02:00
TheH2SO4
6c5a1c317a [!] Mistake 2023-10-23 13:14:43 +02:00
TheH2SO4
b09a9f871e [+] Template: Fenrus
🆕 **New Template**:

-> ℹ️ **Fenrus**: A personal home page for quick access to all your personal apps/sites.
2023-10-23 13:12:50 +02:00
TheH2SO4
b5506f006b [+] Template: Dashboard
🆕 **New Template**:

-> ℹ️ **Dashboard**: A dashboard. Inspired by SUI, it offers simple customization through JSON-files and a handy search bar to help you browse the internet more efficiently.
2023-10-23 13:05:22 +02:00
TheH2SO4
a6c3594448 [+] Template: Grocy
🆕 **New Template**:

-> ℹ️ **Grocy**: Grocy is a self-hosted, web-based household management and grocery list application, designed to simplify your household chores and grocery shopping.
2023-10-23 12:48:13 +02:00
TheH2SO4
5dd3952230 [+] Template: EmbyStat
🆕 **New Template**:

-> ℹ️ **EmbyStat**: EmyStat is an open-source, self-hosted web analytics tool, designed to provide insight into website traffic and user behavior, of your local Emby deployement, all within your control.
2023-10-23 12:41:48 +02:00
TheH2SO4
22ec0f8826 [+] Template: Emby
🆕 **New Template**:

-> ℹ️ **Emby**: A media server software that allows you to organize, stream, and access your multimedia content effortlessly, making it easy to enjoy your favorite movies, TV shows, music, and more.
2023-10-23 11:30:01 +02:00
TheH2SO4
da6e04bb1a Merge pull request #3 from coollabsio/main
Update
2023-10-21 22:57:27 +02:00
Andras Bacsai
aaeacad781 Merge pull request #1350 from coollabsio/next
v4.0.0-beta.98
2023-10-20 18:17:50 +02:00
Andras Bacsai
b539f40fa5 fix 2023-10-20 18:16:47 +02:00
Andras Bacsai
fae340afcb fix: boarding 2023-10-20 18:15:25 +02:00
TheH2SO4
1bfce6716c Merge pull request #2 from coollabsio/next
Next
2023-10-19 11:18:19 +02:00
171 changed files with 3623 additions and 586 deletions

View File

@@ -13,7 +13,7 @@ env:
jobs: jobs:
amd64: amd64:
runs-on: [self-hosted, x64] runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Login to ghcr.io - name: Login to ghcr.io

View File

@@ -10,7 +10,7 @@ env:
jobs: jobs:
amd64: amd64:
runs-on: [self-hosted, x64] runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Login to ghcr.io - name: Login to ghcr.io

View File

@@ -0,0 +1,28 @@
<?php
/**
* @label Send Email
* @description Send email to all users
*/
use App\Models\User;
use Illuminate\Support\Facades\Mail;
set_transanctional_email_settings();
$users = User::whereEmail('andras.bacsai@gmail.com');
foreach ($users as $user) {
Mail::send([], [], function ($message) use ($user) {
$message
->to($user->email)
->subject("Testing")
->text(
<<<EOF
Hello,
Welcome to Coolify Cloud.
Here is your user id: $user->id
EOF
);
});
}

View File

@@ -2,7 +2,9 @@
namespace App\Actions\Database; namespace App\Actions\Database;
use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb; use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql; use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis; use App\Models\StandaloneRedis;
use Lorisleiva\Actions\Concerns\AsAction; use Lorisleiva\Actions\Concerns\AsAction;
@@ -12,7 +14,7 @@ class StartDatabaseProxy
{ {
use AsAction; use AsAction;
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb $database) public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb $database)
{ {
$internalPort = null; $internalPort = null;
if ($database->getMorphClass() === 'App\Models\StandaloneRedis') { if ($database->getMorphClass() === 'App\Models\StandaloneRedis') {
@@ -21,6 +23,10 @@ class StartDatabaseProxy
$internalPort = 5432; $internalPort = 5432;
} else if ($database->getMorphClass() === 'App\Models\StandaloneMongodb') { } else if ($database->getMorphClass() === 'App\Models\StandaloneMongodb') {
$internalPort = 27017; $internalPort = 27017;
} else if ($database->getMorphClass() === 'App\Models\StandaloneMysql') {
$internalPort = 3306;
} else if ($database->getMorphClass() === 'App\Models\StandaloneMariadb') {
$internalPort = 3306;
} }
$containerName = "{$database->uuid}-proxy"; $containerName = "{$database->uuid}-proxy";
$configuration_dir = database_proxy_dir($database->uuid); $configuration_dir = database_proxy_dir($database->uuid);

View File

@@ -0,0 +1,158 @@
<?php
namespace App\Actions\Database;
use App\Models\StandaloneMariadb;
use Illuminate\Support\Str;
use Symfony\Component\Yaml\Yaml;
use Lorisleiva\Actions\Concerns\AsAction;
class StartMariadb
{
use AsAction;
public StandaloneMariadb $database;
public array $commands = [];
public string $configuration_dir;
public function handle(StandaloneMariadb $database)
{
$this->database = $database;
$container_name = $this->database->uuid;
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->commands = [
"echo '####### Starting {$database->name}.'",
"mkdir -p $this->configuration_dir",
];
$persistent_storages = $this->generate_local_persistent_volumes();
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
$environment_variables = $this->generate_environment_variables();
$this->add_custom_mysql();
$docker_compose = [
'version' => '3.8',
'services' => [
$container_name => [
'image' => $this->database->image,
'container_name' => $container_name,
'environment' => $environment_variables,
'restart' => RESTART_MODE,
'networks' => [
$this->database->destination->network,
],
'labels' => [
'coolify.managed' => 'true',
],
'healthcheck' => [
'test' => ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"],
'interval' => '5s',
'timeout' => '5s',
'retries' => 10,
'start_period' => '5s'
],
'mem_limit' => $this->database->limits_memory,
'memswap_limit' => $this->database->limits_memory_swap,
'mem_swappiness' => $this->database->limits_memory_swappiness,
'mem_reservation' => $this->database->limits_memory_reservation,
'cpus' => $this->database->limits_cpus,
'cpuset' => $this->database->limits_cpuset,
'cpu_shares' => $this->database->limits_cpu_shares,
]
],
'networks' => [
$this->database->destination->network => [
'external' => true,
'name' => $this->database->destination->network,
'attachable' => true,
]
]
];
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
}
if (count($persistent_storages) > 0) {
$docker_compose['services'][$container_name]['volumes'] = $persistent_storages;
}
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
if (!is_null($this->database->mariadb_conf)) {
$docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind',
'source' => $this->configuration_dir . '/custom-config.cnf',
'target' => '/etc/mysql/conf.d/custom-config.cnf',
'read_only' => true,
];
}
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml";
$readme = generate_readme_file($this->database->name, now());
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$this->commands[] = "echo '####### {$database->name} started.'";
return remote_process($this->commands, $database->destination->server);
}
private function generate_local_persistent_volumes()
{
$local_persistent_volumes = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
$volume_name = $persistentStorage->host_path ?? $persistentStorage->name;
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
}
return $local_persistent_volumes;
}
private function generate_local_persistent_volumes_only_volume_names()
{
$local_persistent_volumes_names = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
if ($persistentStorage->host_path) {
continue;
}
$name = $persistentStorage->name;
$local_persistent_volumes_names[$name] = [
'name' => $name,
'external' => false,
];
}
return $local_persistent_volumes_names;
}
private function generate_environment_variables()
{
$environment_variables = collect();
foreach ($this->database->runtime_environment_variables as $env) {
$environment_variables->push("$env->key=$env->value");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MARIADB_ROOT_PASSWORD'))->isEmpty()) {
$environment_variables->push("MARIADB_ROOT_PASSWORD={$this->database->mariadb_root_password}");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MARIADB_DATABASE'))->isEmpty()) {
$environment_variables->push("MARIADB_DATABASE={$this->database->mariadb_database}");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MARIADB_USER'))->isEmpty()) {
$environment_variables->push("MARIADB_USER={$this->database->mariadb_user}");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MARIADB_PASSWORD'))->isEmpty()) {
$environment_variables->push("MARIADB_PASSWORD={$this->database->mariadb_password}");
}
return $environment_variables->all();
}
private function add_custom_mysql()
{
if (is_null($this->database->mariadb_conf)) {
return;
}
$filename = 'custom-config.cnf';
$content = $this->database->mariadb_conf;
$content_base64 = base64_encode($content);
$this->commands[] = "echo '{$content_base64}' | base64 -d > $this->configuration_dir/{$filename}";
}
}

View File

@@ -52,7 +52,7 @@ class StartMongodb
'healthcheck' => [ 'healthcheck' => [
'test' => [ 'test' => [
'CMD-SHELL', 'CMD-SHELL',
'mongo --eval "printjson(db.serverStatus())" | grep uptime | grep -v grep' 'mongosh --eval "printjson(db.runCommand(\"ping\"))"'
], ],
'interval' => '5s', 'interval' => '5s',
'timeout' => '5s', 'timeout' => '5s',
@@ -94,6 +94,14 @@ class StartMongodb
]; ];
$docker_compose['services'][$container_name]['command'] = $startCommand . ' --config /etc/mongo/mongod.conf'; $docker_compose['services'][$container_name]['command'] = $startCommand . ' --config /etc/mongo/mongod.conf';
} }
$this->add_default_database();
$docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind',
'source' => $this->configuration_dir . '/docker-entrypoint-initdb.d',
'target' => '/docker-entrypoint-initdb.d',
'read_only' => true,
];
$docker_compose = Yaml::dump($docker_compose, 10); $docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose); $docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml"; $this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml";
@@ -160,4 +168,11 @@ class StartMongodb
$content_base64 = base64_encode($content); $content_base64 = base64_encode($content);
$this->commands[] = "echo '{$content_base64}' | base64 -d > $this->configuration_dir/{$filename}"; $this->commands[] = "echo '{$content_base64}' | base64 -d > $this->configuration_dir/{$filename}";
} }
private function add_default_database()
{
$content = "db = db.getSiblingDB(\"{$this->database->mongo_initdb_database}\");db.createCollection('init_collection');db.createUser({user: \"{$this->database->mongo_initdb_root_username}\", pwd: \"{$this->database->mongo_initdb_root_password}\",roles: [{role:\"readWrite\",db:\"{$this->database->mongo_initdb_database}\"}]});";
$content_base64 = base64_encode($content);
$this->commands[] = "mkdir -p $this->configuration_dir/docker-entrypoint-initdb.d";
$this->commands[] = "echo '{$content_base64}' | base64 -d > $this->configuration_dir/docker-entrypoint-initdb.d/01-default-database.js";
}
} }

View File

@@ -0,0 +1,158 @@
<?php
namespace App\Actions\Database;
use App\Models\StandaloneMysql;
use Illuminate\Support\Str;
use Symfony\Component\Yaml\Yaml;
use Lorisleiva\Actions\Concerns\AsAction;
class StartMysql
{
use AsAction;
public StandaloneMysql $database;
public array $commands = [];
public string $configuration_dir;
public function handle(StandaloneMysql $database)
{
$this->database = $database;
$container_name = $this->database->uuid;
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->commands = [
"echo '####### Starting {$database->name}.'",
"mkdir -p $this->configuration_dir",
];
$persistent_storages = $this->generate_local_persistent_volumes();
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
$environment_variables = $this->generate_environment_variables();
$this->add_custom_mysql();
$docker_compose = [
'version' => '3.8',
'services' => [
$container_name => [
'image' => $this->database->image,
'container_name' => $container_name,
'environment' => $environment_variables,
'restart' => RESTART_MODE,
'networks' => [
$this->database->destination->network,
],
'labels' => [
'coolify.managed' => 'true',
],
'healthcheck' => [
'test' => ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p{$this->database->mysql_root_password}"],
'interval' => '5s',
'timeout' => '5s',
'retries' => 10,
'start_period' => '5s'
],
'mem_limit' => $this->database->limits_memory,
'memswap_limit' => $this->database->limits_memory_swap,
'mem_swappiness' => $this->database->limits_memory_swappiness,
'mem_reservation' => $this->database->limits_memory_reservation,
'cpus' => $this->database->limits_cpus,
'cpuset' => $this->database->limits_cpuset,
'cpu_shares' => $this->database->limits_cpu_shares,
]
],
'networks' => [
$this->database->destination->network => [
'external' => true,
'name' => $this->database->destination->network,
'attachable' => true,
]
]
];
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
}
if (count($persistent_storages) > 0) {
$docker_compose['services'][$container_name]['volumes'] = $persistent_storages;
}
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
if (!is_null($this->database->mysql_conf)) {
$docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind',
'source' => $this->configuration_dir . '/custom-config.cnf',
'target' => '/etc/mysql/conf.d/custom-config.cnf',
'read_only' => true,
];
}
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml";
$readme = generate_readme_file($this->database->name, now());
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$this->commands[] = "echo '####### {$database->name} started.'";
return remote_process($this->commands, $database->destination->server);
}
private function generate_local_persistent_volumes()
{
$local_persistent_volumes = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
$volume_name = $persistentStorage->host_path ?? $persistentStorage->name;
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
}
return $local_persistent_volumes;
}
private function generate_local_persistent_volumes_only_volume_names()
{
$local_persistent_volumes_names = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
if ($persistentStorage->host_path) {
continue;
}
$name = $persistentStorage->name;
$local_persistent_volumes_names[$name] = [
'name' => $name,
'external' => false,
];
}
return $local_persistent_volumes_names;
}
private function generate_environment_variables()
{
$environment_variables = collect();
foreach ($this->database->runtime_environment_variables as $env) {
$environment_variables->push("$env->key=$env->value");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MYSQL_ROOT_PASSWORD'))->isEmpty()) {
$environment_variables->push("MYSQL_ROOT_PASSWORD={$this->database->mysql_root_password}");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MYSQL_DATABASE'))->isEmpty()) {
$environment_variables->push("MYSQL_DATABASE={$this->database->mysql_database}");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MYSQL_USER'))->isEmpty()) {
$environment_variables->push("MYSQL_USER={$this->database->mysql_user}");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MYSQL_PASSWORD'))->isEmpty()) {
$environment_variables->push("MYSQL_PASSWORD={$this->database->mysql_password}");
}
return $environment_variables->all();
}
private function add_custom_mysql()
{
if (is_null($this->database->mysql_conf)) {
return;
}
$filename = 'custom-config.cnf';
$content = $this->database->mysql_conf;
$content_base64 = base64_encode($content);
$this->commands[] = "echo '{$content_base64}' | base64 -d > $this->configuration_dir/{$filename}";
}
}

View File

@@ -2,7 +2,9 @@
namespace App\Actions\Database; namespace App\Actions\Database;
use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb; use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql; use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis; use App\Models\StandaloneRedis;
use Lorisleiva\Actions\Concerns\AsAction; use Lorisleiva\Actions\Concerns\AsAction;
@@ -11,7 +13,7 @@ class StopDatabase
{ {
use AsAction; use AsAction;
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb $database) public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb $database)
{ {
$server = $database->destination->server; $server = $database->destination->server;
instant_remote_process( instant_remote_process(

View File

@@ -2,7 +2,9 @@
namespace App\Actions\Database; namespace App\Actions\Database;
use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb; use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql; use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis; use App\Models\StandaloneRedis;
use Lorisleiva\Actions\Concerns\AsAction; use Lorisleiva\Actions\Concerns\AsAction;
@@ -11,7 +13,7 @@ class StopDatabaseProxy
{ {
use AsAction; use AsAction;
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb $database) public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb $database)
{ {
instant_remote_process(["docker rm -f {$database->uuid}-proxy"], $database->destination->server); instant_remote_process(["docker rm -f {$database->uuid}-proxy"], $database->destination->server);
$database->is_public = false; $database->is_public = false;

View File

@@ -4,26 +4,26 @@ namespace App\Actions\License;
use App\Models\InstanceSettings; use App\Models\InstanceSettings;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Lorisleiva\Actions\Concerns\AsAction;
class CheckResaleLicense class CheckResaleLicense
{ {
public function __invoke() use AsAction;
public function handle()
{ {
try { try {
$settings = InstanceSettings::get(); $settings = InstanceSettings::get();
$settings->update([
'is_resale_license_active' => false,
]);
if (isDev()) { if (isDev()) {
$settings->update([
'is_resale_license_active' => true,
]);
return; return;
} }
if (!$settings->resale_license) { // if (!$settings->resale_license) {
return; // return;
} // }
$base_url = config('coolify.license_url'); $base_url = config('coolify.license_url');
if (isDev()) {
$base_url = 'http://host.docker.internal:8787';
}
$instance_id = config('app.id'); $instance_id = config('app.id');
ray("Checking license key against $base_url/lemon/validate"); ray("Checking license key against $base_url/lemon/validate");

View File

@@ -4,7 +4,6 @@ namespace App\Actions\Service;
use Lorisleiva\Actions\Concerns\AsAction; use Lorisleiva\Actions\Concerns\AsAction;
use App\Models\Service; use App\Models\Service;
use App\Notifications\Application\StatusChanged;
class StopService class StopService
{ {

View File

@@ -3,8 +3,15 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use App\Enums\ApplicationDeploymentStatus; use App\Enums\ApplicationDeploymentStatus;
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue; use App\Models\ApplicationDeploymentQueue;
use App\Models\Service;
use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
class Init extends Command class Init extends Command
{ {
@@ -13,9 +20,27 @@ class Init extends Command
public function handle() public function handle()
{ {
ray()->clearAll();
$this->cleanup_in_progress_application_deployments(); $this->cleanup_in_progress_application_deployments();
$this->cleanup_stucked_resources();
// $this->cleanup_ssh();
} }
private function cleanup_ssh()
{
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: {$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
@@ -30,4 +55,93 @@ class Init extends Command
echo "Error: {$e->getMessage()}\n"; echo "Error: {$e->getMessage()}\n";
} }
} }
private function cleanup_stucked_resources()
{
// Cleanup any resources that are not attached to any environment or destination or server
try {
$applications = Application::all();
foreach ($applications as $application) {
if (!$application->environment) {
ray('Application without environment', $application->name);
$application->delete();
}
if (!$application->destination()) {
ray('Application without destination', $application->name);
$application->delete();
}
}
$postgresqls = StandalonePostgresql::all();
foreach ($postgresqls as $postgresql) {
if (!$postgresql->environment) {
ray('Postgresql without environment', $postgresql->name);
$postgresql->delete();
}
if (!$postgresql->destination()) {
ray('Postgresql without destination', $postgresql->name);
$postgresql->delete();
}
}
$redis = StandaloneRedis::all();
foreach ($redis as $redis) {
if (!$redis->environment) {
ray('Redis without environment', $redis->name);
$redis->delete();
}
if (!$redis->destination()) {
ray('Redis without destination', $redis->name);
$redis->delete();
}
}
$mongodbs = StandaloneMongodb::all();
foreach ($mongodbs as $mongodb) {
if (!$mongodb->environment) {
ray('Mongodb without environment', $mongodb->name);
$mongodb->delete();
}
if (!$mongodb->destination()) {
ray('Mongodb without destination', $mongodb->name);
$mongodb->delete();
}
}
$mysqls = StandaloneMysql::all();
foreach ($mysqls as $mysql) {
if (!$mysql->environment) {
ray('Mysql without environment', $mysql->name);
$mysql->delete();
}
if (!$mysql->destination()) {
ray('Mysql without destination', $mysql->name);
$mysql->delete();
}
}
$mariadbs = StandaloneMysql::all();
foreach ($mariadbs as $mariadb) {
if (!$mariadb->environment) {
ray('Mariadb without environment', $mariadb->name);
$mariadb->delete();
}
if (!$mariadb->destination()) {
ray('Mariadb without destination', $mariadb->name);
$mariadb->delete();
}
}
$services = Service::all();
foreach ($services as $service) {
if (!$service->environment) {
ray('Service without environment', $service->name);
$service->delete();
}
if (!$service->server) {
ray('Service without server', $service->name);
$service->delete();
}
if (!$service->destination()) {
ray('Service without destination', $service->name);
$service->delete();
}
}
} catch (\Throwable $e) {
echo "Error: {$e->getMessage()}\n";
}
}
} }

View File

@@ -61,12 +61,14 @@ class ResourcesDelete extends Command
foreach ($serversToDelete as $server) { foreach ($serversToDelete as $server) {
$toDelete = $servers->where('id', $server)->first(); $toDelete = $servers->where('id', $server)->first();
$this->info($toDelete); if ($toDelete) {
$confirmed = confirm("Are you sure you want to delete all selected resources?"); $this->info($toDelete);
if (!$confirmed) { $confirmed = confirm("Are you sure you want to delete all selected resources?");
break; if (!$confirmed) {
break;
}
$toDelete->delete();
} }
$toDelete->delete();
} }
} }
private function deleteApplication() private function deleteApplication()
@@ -82,14 +84,15 @@ class ResourcesDelete extends Command
); );
foreach ($applicationsToDelete as $application) { foreach ($applicationsToDelete as $application) {
ray($application);
$toDelete = $applications->where('id', $application)->first(); $toDelete = $applications->where('id', $application)->first();
$this->info($toDelete); if ($toDelete) {
$confirmed = confirm("Are you sure you want to delete all selected resources? "); $this->info($toDelete);
if (!$confirmed) { $confirmed = confirm("Are you sure you want to delete all selected resources? ");
break; if (!$confirmed) {
break;
}
$toDelete->delete();
} }
$toDelete->delete();
} }
} }
private function deleteDatabase() private function deleteDatabase()
@@ -106,12 +109,14 @@ class ResourcesDelete extends Command
foreach ($databasesToDelete as $database) { foreach ($databasesToDelete as $database) {
$toDelete = $databases->where('id', $database)->first(); $toDelete = $databases->where('id', $database)->first();
$this->info($toDelete); if ($toDelete) {
$confirmed = confirm("Are you sure you want to delete all selected resources?"); $this->info($toDelete);
if (!$confirmed) { $confirmed = confirm("Are you sure you want to delete all selected resources?");
return; if (!$confirmed) {
return;
}
$toDelete->delete();
} }
$toDelete->delete();
} }
} }
private function deleteService() private function deleteService()
@@ -128,12 +133,14 @@ class ResourcesDelete extends Command
foreach ($servicesToDelete as $service) { foreach ($servicesToDelete as $service) {
$toDelete = $services->where('id', $service)->first(); $toDelete = $services->where('id', $service)->first();
$this->info($toDelete); if ($toDelete) {
$confirmed = confirm("Are you sure you want to delete all selected resources?"); $this->info($toDelete);
if (!$confirmed) { $confirmed = confirm("Are you sure you want to delete all selected resources?");
return; if (!$confirmed) {
return;
}
$toDelete->delete();
} }
$toDelete->delete();
} }
} }
} }

View File

@@ -5,7 +5,7 @@ namespace App\Console\Commands;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Symfony\Component\Yaml\Yaml; use Symfony\Component\Yaml\Yaml;
class GenerateServiceTemplates extends Command class ServicesGenerate extends Command
{ {
/** /**
* The name and signature of the console command. * The name and signature of the console command.
@@ -80,6 +80,14 @@ class GenerateServiceTemplates extends Command
$env_file = null; $env_file = null;
} }
$tags = collect(preg_grep('/^# tags:/', explode("\n", $content)))->values();
if ($tags->count() > 0) {
$tags = str($tags[0])->after('# tags:')->trim()->explode(',')->map(function ($tag) {
return str($tag)->trim()->lower()->value();
})->values();
} else {
$tags = null;
}
$json = Yaml::parse($content); $json = Yaml::parse($content);
$yaml = base64_encode(Yaml::dump($json, 10, 2)); $yaml = base64_encode(Yaml::dump($json, 10, 2));
$payload = [ $payload = [
@@ -87,9 +95,12 @@ class GenerateServiceTemplates extends Command
'documentation' => $documentation, 'documentation' => $documentation,
'slogan' => $slogan, 'slogan' => $slogan,
'compose' => $yaml, 'compose' => $yaml,
'tags' => $tags,
]; ];
if ($env_file) { if ($env_file) {
$payload['envs'] = $env_file; $env_file_content = file_get_contents(base_path("templates/compose/$env_file"));
$env_file_base64 = base64_encode($env_file_content);
$payload['envs'] = $env_file_base64;
} }
return $payload; return $payload;
} }

View File

@@ -16,7 +16,7 @@ class SyncBunny extends Command
* *
* @var string * @var string
*/ */
protected $signature = 'sync:bunny {--only-template} {--only-version}'; protected $signature = 'sync:bunny {--templates} {--release}';
/** /**
* The console command description. * The console command description.
@@ -31,8 +31,8 @@ class SyncBunny extends Command
public function handle() public function handle()
{ {
$that = $this; $that = $this;
$only_template = $this->option('only-template'); $only_template = $this->option('templates');
$only_version = $this->option('only-version'); $only_version = $this->option('release');
$bunny_cdn = "https://cdn.coollabs.io"; $bunny_cdn = "https://cdn.coollabs.io";
$bunny_cdn_path = "coolify"; $bunny_cdn_path = "coolify";
$bunny_cdn_storage_name = "coolcdn"; $bunny_cdn_storage_name = "coolcdn";

View File

@@ -8,6 +8,7 @@ use App\Jobs\DatabaseBackupJob;
use App\Jobs\DockerCleanupJob; use App\Jobs\DockerCleanupJob;
use App\Jobs\InstanceAutoUpdateJob; use App\Jobs\InstanceAutoUpdateJob;
use App\Jobs\ContainerStatusJob; use App\Jobs\ContainerStatusJob;
use App\Jobs\PullHelperImageJob;
use App\Models\InstanceSettings; use App\Models\InstanceSettings;
use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledDatabaseBackup;
use App\Models\Server; use App\Models\Server;
@@ -19,20 +20,35 @@ class Kernel extends ConsoleKernel
protected function schedule(Schedule $schedule): void protected function schedule(Schedule $schedule): void
{ {
if (isDev()) { if (isDev()) {
// Instance Jobs
$schedule->command('horizon:snapshot')->everyMinute(); $schedule->command('horizon:snapshot')->everyMinute();
$schedule->job(new CleanupInstanceStuffsJob)->everyMinute()->onOneServer(); $schedule->job(new CleanupInstanceStuffsJob)->everyMinute()->onOneServer();
// $schedule->job(new CheckResaleLicenseJob)->hourly()->onOneServer();
// Server Jobs
$this->check_scheduled_backups($schedule); $this->check_scheduled_backups($schedule);
$this->check_resources($schedule); $this->check_resources($schedule);
$this->cleanup_servers($schedule); $this->cleanup_servers($schedule);
$this->check_scheduled_backups($schedule); $this->check_scheduled_backups($schedule);
$this->pull_helper_image($schedule);
} else { } else {
// Instance Jobs
$schedule->command('horizon:snapshot')->everyFiveMinutes(); $schedule->command('horizon:snapshot')->everyFiveMinutes();
$schedule->job(new CleanupInstanceStuffsJob)->everyTwoMinutes()->onOneServer(); $schedule->job(new CleanupInstanceStuffsJob)->everyTwoMinutes()->onOneServer();
$schedule->job(new CheckResaleLicenseJob)->hourly()->onOneServer(); // $schedule->job(new CheckResaleLicenseJob)->hourly()->onOneServer();
// Server Jobs
$this->instance_auto_update($schedule); $this->instance_auto_update($schedule);
$this->check_scheduled_backups($schedule); $this->check_scheduled_backups($schedule);
$this->check_resources($schedule); $this->check_resources($schedule);
$this->cleanup_servers($schedule); $this->cleanup_servers($schedule);
$this->pull_helper_image($schedule);
}
}
private function pull_helper_image($schedule)
{
$servers = Server::all()->where('settings.is_usable', true)->where('settings.is_reachable', true);
foreach ($servers as $server) {
$schedule->job(new PullHelperImageJob($server))->everyTenMinutes()->onOneServer();
} }
} }
private function cleanup_servers($schedule) private function cleanup_servers($schedule)

View File

@@ -41,7 +41,7 @@ class ApplicationController extends Controller
if (!$application) { if (!$application) {
return redirect()->route('dashboard'); return redirect()->route('dashboard');
} }
['deployments' => $deployments, 'count' => $count] = $application->deployments(0, 8); ['deployments' => $deployments, 'count' => $count] = $application->deployments(0, 40);
return view('project.application.deployments', ['application' => $application, 'deployments' => $deployments, 'deployments_count' => $count]); return view('project.application.deployments', ['application' => $application, 'deployments' => $deployments, 'deployments_count' => $count]);
} }

View File

@@ -39,6 +39,10 @@ class Controller extends BaseController
} else { } else {
$team = $user->teams()->first(); $team = $user->teams()->first();
} }
if (is_null(data_get($user, 'email_verified_at'))){
$user->email_verified_at = now();
$user->save();
}
Auth::login($user); Auth::login($user);
session(['currentTeam' => $team]); session(['currentTeam' => $team]);
return redirect()->route('dashboard'); return redirect()->route('dashboard');

View File

@@ -32,8 +32,14 @@ class MagicController extends Controller
public function environments() public function environments()
{ {
$project = Project::ownedByCurrentTeam()->whereUuid(request()->query('project_uuid'))->first();
if (!$project) {
return response()->json([
'environments' => []
]);
}
return response()->json([ return response()->json([
'environments' => Project::ownedByCurrentTeam()->whereUuid(request()->query('project_uuid'))->first()->environments 'environments' => $project->environments
]); ]);
} }

View File

@@ -63,8 +63,12 @@ class ProjectController extends Controller
$database = create_standalone_postgresql($environment->id, $destination_uuid); $database = create_standalone_postgresql($environment->id, $destination_uuid);
} else if ($type->value() === 'redis') { } else if ($type->value() === 'redis') {
$database = create_standalone_redis($environment->id, $destination_uuid); $database = create_standalone_redis($environment->id, $destination_uuid);
} else if ($type->value() === 'mongodb') { } else if ($type->value() === 'mongodb') {
$database = create_standalone_mongodb($environment->id, $destination_uuid); $database = create_standalone_mongodb($environment->id, $destination_uuid);
} else if ($type->value() === 'mysql') {
$database = create_standalone_mysql($environment->id, $destination_uuid);
}else if ($type->value() === 'mariadb') {
$database = create_standalone_mariadb($environment->id, $destination_uuid);
} }
return redirect()->route('project.database.configuration', [ return redirect()->route('project.database.configuration', [
'project_uuid' => $project->uuid, 'project_uuid' => $project->uuid,
@@ -72,7 +76,7 @@ class ProjectController extends Controller
'database_uuid' => $database->uuid, 'database_uuid' => $database->uuid,
]); ]);
} }
if ($type->startsWith('one-click-service-') && !is_null( (int)$server_id)) { if ($type->startsWith('one-click-service-') && !is_null((int)$server_id)) {
$oneClickServiceName = $type->after('one-click-service-')->value(); $oneClickServiceName = $type->after('one-click-service-')->value();
$oneClickService = data_get($services, "$oneClickServiceName.compose"); $oneClickService = data_get($services, "$oneClickServiceName.compose");
$oneClickDotEnvs = data_get($services, "$oneClickServiceName.envs", null); $oneClickDotEnvs = data_get($services, "$oneClickServiceName.envs", null);

View File

@@ -164,7 +164,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
{ {
$this->validate([ $this->validate([
'remoteServerName' => 'required', 'remoteServerName' => 'required',
'remoteServerHost' => 'required|ip', 'remoteServerHost' => 'required',
'remoteServerPort' => 'required|integer', 'remoteServerPort' => 'required|integer',
'remoteServerUser' => 'required', 'remoteServerUser' => 'required',
]); ]);
@@ -213,7 +213,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
]); ]);
$this->getProxyType(); $this->getProxyType();
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->dockerInstallationStarted = false; // $this->dockerInstallationStarted = false;
return handleError(error: $e, customErrorMessage: $customErrorMessage, livewire: $this); return handleError(error: $e, customErrorMessage: $customErrorMessage, livewire: $this);
} }
} }

View File

@@ -32,7 +32,7 @@ class CheckLicense extends Component
$this->settings->save(); $this->settings->save();
if ($this->settings->resale_license) { if ($this->settings->resale_license) {
try { try {
resolve(CheckResaleLicense::class)(); CheckResaleLicense::run();
$this->emit('reloadWindow'); $this->emit('reloadWindow');
} catch (\Throwable $e) { } catch (\Throwable $e) {
session()->flash('error', 'Something went wrong. Please contact support. <br>Error: ' . $e->getMessage()); session()->flash('error', 'Something went wrong. Please contact support. <br>Error: ' . $e->getMessage());

View File

@@ -16,7 +16,7 @@ class Form extends Component
protected $validationAttributes = [ protected $validationAttributes = [
'destination.name' => 'name', 'destination.name' => 'name',
'destination.network' => 'network', 'destination.network' => 'network',
'destination.server.ip' => 'IP Address', 'destination.server.ip' => 'IP Address/Domain',
]; ];
public function submit() public function submit()

View File

@@ -3,24 +3,31 @@
namespace App\Http\Livewire\Project\Application; namespace App\Http\Livewire\Project\Application;
use App\Models\Application; use App\Models\Application;
use Illuminate\Support\Collection;
use Livewire\Component; use Livewire\Component;
class Deployments extends Component class Deployments extends Component
{ {
public Application $application; public Application $application;
public $deployments = []; public Array|Collection $deployments = [];
public int $deployments_count = 0; public int $deployments_count = 0;
public string $current_url; public string $current_url;
public int $skip = 0; public int $skip = 0;
public int $default_take = 8; public int $default_take = 40;
public bool $show_next = false; public bool $show_next = false;
public ?string $pull_request_id = null;
protected $queryString = ['pull_request_id'];
public function mount() public function mount()
{ {
$this->current_url = url()->current(); $this->current_url = url()->current();
$this->show_pull_request_only();
$this->show_more(); $this->show_more();
} }
private function show_pull_request_only() {
if ($this->pull_request_id) {
$this->deployments = $this->deployments->where('pull_request_id', $this->pull_request_id);
}
}
private function show_more() private function show_more()
{ {
if (count($this->deployments) !== 0) { if (count($this->deployments) !== 0) {
@@ -47,6 +54,7 @@ class Deployments extends Component
['deployments' => $deployments, 'count' => $count] = $this->application->deployments($this->skip, $take); ['deployments' => $deployments, 'count' => $count] = $this->application->deployments($this->skip, $take);
$this->deployments = $deployments; $this->deployments = $deployments;
$this->deployments_count = $count; $this->deployments_count = $count;
$this->show_pull_request_only();
$this->show_more(); $this->show_more();
} }
} }

View File

@@ -152,7 +152,7 @@ class General extends Component
$fqdn = generateFqdn($server, $this->application->uuid); $fqdn = generateFqdn($server, $this->application->uuid);
$this->application->fqdn = $fqdn; $this->application->fqdn = $fqdn;
$this->application->save(); $this->application->save();
$this->emit('success', 'Application settings updated!'); $this->updatedApplicationFqdn();
} }
} }
public function resetDefaultLabels($showToaster = true) public function resetDefaultLabels($showToaster = true)

View File

@@ -65,4 +65,18 @@ class Heading extends Component
$this->application->save(); $this->application->save();
$this->application->refresh(); $this->application->refresh();
} }
public function restart() {
$this->setDeploymentUuid();
queue_application_deployment(
application_id: $this->application->id,
deployment_uuid: $this->deploymentUuid,
restart_only: true,
);
return redirect()->route('project.application.deployment', [
'project_uuid' => $this->parameters['project_uuid'],
'application_uuid' => $this->parameters['application_uuid'],
'deployment_uuid' => $this->deploymentUuid,
'environment_name' => $this->parameters['environment_name'],
]);
}
} }

View File

@@ -55,18 +55,21 @@ class CloneProject extends Component
'selectedServer' => 'required', 'selectedServer' => 'required',
'newProjectName' => 'required', 'newProjectName' => 'required',
]); ]);
$foundProject = Project::where('name', $this->newProjectName)->first();
if ($foundProject) {
throw new \Exception('Project with the same name already exists.');
}
$newProject = Project::create([ $newProject = Project::create([
'name' => $this->newProjectName, 'name' => $this->newProjectName,
'team_id' => currentTeam()->id, 'team_id' => currentTeam()->id,
'description' => $this->project->description . ' (clone)', 'description' => $this->project->description . ' (clone)',
]); ]);
if ($this->environment->id !== 1) { if ($this->environment->name !== 'production') {
$newProject->environments()->create([ $newProject->environments()->create([
'name' => $this->environment->name, 'name' => $this->environment->name,
]); ]);
$newProject->environments()->find(1)->delete();
} }
$newEnvironment = $newProject->environments->first(); $newEnvironment = $newProject->environments->where('name', $this->environment->name)->first();
// Clone Applications // Clone Applications
$applications = $this->environment->applications; $applications = $this->environment->applications;
$databases = $this->environment->databases(); $databases = $this->environment->databases();
@@ -80,7 +83,6 @@ class CloneProject extends Component
'environment_id' => $newEnvironment->id, 'environment_id' => $newEnvironment->id,
'destination_id' => $this->selectedServer, 'destination_id' => $this->selectedServer,
]); ]);
$newApplication->environment_id = $newProject->environments->first()->id;
$newApplication->save(); $newApplication->save();
$environmentVaribles = $application->environment_variables()->get(); $environmentVaribles = $application->environment_variables()->get();
foreach ($environmentVaribles as $environmentVarible) { foreach ($environmentVaribles as $environmentVarible) {
@@ -105,7 +107,6 @@ class CloneProject extends Component
'environment_id' => $newEnvironment->id, 'environment_id' => $newEnvironment->id,
'destination_id' => $this->selectedServer, 'destination_id' => $this->selectedServer,
]); ]);
$newDatabase->environment_id = $newProject->environments->first()->id;
$newDatabase->save(); $newDatabase->save();
$environmentVaribles = $database->environment_variables()->get(); $environmentVaribles = $database->environment_variables()->get();
foreach ($environmentVaribles as $environmentVarible) { foreach ($environmentVaribles as $environmentVarible) {
@@ -116,6 +117,10 @@ class CloneProject extends Component
$payload['standalone_redis_id'] = $newDatabase->id; $payload['standalone_redis_id'] = $newDatabase->id;
} else if ($database->type() === 'standalone_mongodb') { } else if ($database->type() === 'standalone_mongodb') {
$payload['standalone_mongodb_id'] = $newDatabase->id; $payload['standalone_mongodb_id'] = $newDatabase->id;
} else if ($database->type() === 'standalone_mysql') {
$payload['standalone_mysql_id'] = $newDatabase->id;
}else if ($database->type() === 'standalone_mariadb') {
$payload['standalone_mariadb_id'] = $newDatabase->id;
} }
$newEnvironmentVariable = $environmentVarible->replicate()->fill($payload); $newEnvironmentVariable = $environmentVarible->replicate()->fill($payload);
$newEnvironmentVariable->save(); $newEnvironmentVariable->save();
@@ -128,7 +133,6 @@ class CloneProject extends Component
'environment_id' => $newEnvironment->id, 'environment_id' => $newEnvironment->id,
'destination_id' => $this->selectedServer, 'destination_id' => $this->selectedServer,
]); ]);
$newService->environment_id = $newProject->environments->first()->id;
$newService->save(); $newService->save();
$newService->parse(); $newService->parse();
} }

View File

@@ -1,23 +0,0 @@
<?php
namespace App\Http\Livewire\Project\Database;
use App\Models\ScheduledDatabaseBackupExecution;
use Livewire\Component;
class BackupExecution extends Component
{
public ScheduledDatabaseBackupExecution $execution;
public function download()
{
}
public function delete(): void
{
delete_backup_locally($this->execution->filename, $this->execution->scheduledDatabaseBackup->database->destination->server);
$this->execution->delete();
$this->emit('success', 'Backup deleted successfully.');
$this->emit('refreshBackupExecutions');
}
}

View File

@@ -2,14 +2,51 @@
namespace App\Http\Livewire\Project\Database; namespace App\Http\Livewire\Project\Database;
use Illuminate\Support\Facades\Storage;
use Livewire\Component; use Livewire\Component;
class BackupExecutions extends Component class BackupExecutions extends Component
{ {
public $backup; public $backup;
public $executions; public $executions;
protected $listeners = ['refreshBackupExecutions']; public $setDeletableBackup;
protected $listeners = ['refreshBackupExecutions', 'deleteBackup'];
public function deleteBackup($exeuctionId)
{
$execution = $this->backup->executions()->where('id', $exeuctionId)->first();
if (is_null($execution)) {
$this->emit('error', 'Backup execution not found.');
return;
}
delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->destination->server);
$execution->delete();
$this->emit('success', 'Backup deleted successfully.');
$this->emit('refreshBackupExecutions');
}
public function download($exeuctionId)
{
try {
$execution = $this->backup->executions()->where('id', $exeuctionId)->first();
if (is_null($execution)) {
$this->emit('error', 'Backup execution not found.');
return;
}
$filename = data_get($execution, 'filename');
$server = $execution->scheduledDatabaseBackup->database->destination->server;
$privateKeyLocation = savePrivateKeyToFs($server);
$disk = Storage::build([
'driver' => 'sftp',
'host' => $server->ip,
'port' => $server->port,
'username' => $server->user,
'privateKey' => $privateKeyLocation,
]);
return $disk->download($filename);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function refreshBackupExecutions(): void public function refreshBackupExecutions(): void
{ {
$this->executions = $this->backup->executions; $this->executions = $this->backup->executions;

View File

@@ -48,6 +48,10 @@ class CreateScheduledBackup extends Component
]; ];
if ($this->database->type() === 'standalone-postgresql') { if ($this->database->type() === 'standalone-postgresql') {
$payload['databases_to_backup'] = $this->database->postgres_db; $payload['databases_to_backup'] = $this->database->postgres_db;
} else if ($this->database->type() === 'standalone-mysql') {
$payload['databases_to_backup'] = $this->database->mysql_database;
}else if ($this->database->type() === 'standalone-mariadb') {
$payload['databases_to_backup'] = $this->database->mariadb_database;
} }
ScheduledDatabaseBackup::create($payload); ScheduledDatabaseBackup::create($payload);
$this->emit('refreshScheduledBackups'); $this->emit('refreshScheduledBackups');

View File

@@ -2,7 +2,9 @@
namespace App\Http\Livewire\Project\Database; namespace App\Http\Livewire\Project\Database;
use App\Actions\Database\StartMariadb;
use App\Actions\Database\StartMongodb; use App\Actions\Database\StartMongodb;
use App\Actions\Database\StartMysql;
use App\Actions\Database\StartPostgresql; use App\Actions\Database\StartPostgresql;
use App\Actions\Database\StartRedis; use App\Actions\Database\StartRedis;
use App\Actions\Database\StopDatabase; use App\Actions\Database\StopDatabase;
@@ -49,14 +51,18 @@ class Heading extends Component
if ($this->database->type() === 'standalone-postgresql') { if ($this->database->type() === 'standalone-postgresql') {
$activity = StartPostgresql::run($this->database); $activity = StartPostgresql::run($this->database);
$this->emit('newMonitorActivity', $activity->id); $this->emit('newMonitorActivity', $activity->id);
} } else if ($this->database->type() === 'standalone-redis') {
if ($this->database->type() === 'standalone-redis') {
$activity = StartRedis::run($this->database); $activity = StartRedis::run($this->database);
$this->emit('newMonitorActivity', $activity->id); $this->emit('newMonitorActivity', $activity->id);
} } else if ($this->database->type() === 'standalone-mongodb') {
if ($this->database->type() === 'standalone-mongodb') {
$activity = StartMongodb::run($this->database); $activity = StartMongodb::run($this->database);
$this->emit('newMonitorActivity', $activity->id); $this->emit('newMonitorActivity', $activity->id);
} else if ($this->database->type() === 'standalone-mysql') {
$activity = StartMysql::run($this->database);
$this->emit('newMonitorActivity', $activity->id);
} else if ($this->database->type() === 'standalone-mariadb') {
$activity = StartMariadb::run($this->database);
$this->emit('newMonitorActivity', $activity->id);
} }
} }
} }

View File

@@ -0,0 +1,95 @@
<?php
namespace App\Http\Livewire\Project\Database\Mariadb;
use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Database\StopDatabaseProxy;
use App\Models\StandaloneMariadb;
use Exception;
use Livewire\Component;
class General extends Component
{
protected $listeners = ['refresh'];
public StandaloneMariadb $database;
public string $db_url;
protected $rules = [
'database.name' => 'required',
'database.description' => 'nullable',
'database.mariadb_root_password' => 'required',
'database.mariadb_user' => 'required',
'database.mariadb_password' => 'required',
'database.mariadb_database' => 'required',
'database.mariadb_conf' => 'nullable',
'database.image' => 'required',
'database.ports_mappings' => 'nullable',
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
];
protected $validationAttributes = [
'database.name' => 'Name',
'database.description' => 'Description',
'database.mariadb_root_password' => 'Root Password',
'database.mariadb_user' => 'User',
'database.mariadb_password' => 'Password',
'database.mariadb_database' => 'Database',
'database.mariadb_conf' => 'MariaDB Configuration',
'database.image' => 'Image',
'database.ports_mappings' => 'Port Mapping',
'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port',
];
public function submit()
{
try {
$this->validate();
$this->database->save();
$this->emit('success', 'Database updated successfully.');
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function instantSave()
{
try {
if ($this->database->is_public && !$this->database->public_port) {
$this->emit('error', 'Public port is required.');
$this->database->is_public = false;
return;
}
if ($this->database->is_public) {
if (!str($this->database->status)->startsWith('running')) {
$this->emit('error', 'Database must be started to be publicly accessible.');
$this->database->is_public = false;
return;
}
StartDatabaseProxy::run($this->database);
$this->emit('success', 'Database is now publicly accessible.');
} else {
StopDatabaseProxy::run($this->database);
$this->emit('success', 'Database is no longer publicly accessible.');
}
$this->db_url = $this->database->getDbUrl();
$this->database->save();
} catch (\Throwable $e) {
$this->database->is_public = !$this->database->is_public;
return handleError($e, $this);
}
}
public function refresh(): void
{
$this->database->refresh();
}
public function mount()
{
$this->db_url = $this->database->getDbUrl();
}
public function render()
{
return view('livewire.project.database.mariadb.general');
}
}

View File

@@ -39,7 +39,8 @@ class General extends Component
'database.is_public' => 'Is Public', 'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port', 'database.public_port' => 'Public Port',
]; ];
public function submit() { public function submit()
{
try { try {
$this->validate(); $this->validate();
if ($this->database->mongo_conf === "") { if ($this->database->mongo_conf === "") {
@@ -60,7 +61,11 @@ class General extends Component
return; return;
} }
if ($this->database->is_public) { if ($this->database->is_public) {
$this->emit('success', 'Starting TCP proxy...'); if (!str($this->database->status)->startsWith('running')) {
$this->emit('error', 'Database must be started to be publicly accessible.');
$this->database->is_public = false;
return;
}
StartDatabaseProxy::run($this->database); StartDatabaseProxy::run($this->database);
$this->emit('success', 'Database is now publicly accessible.'); $this->emit('success', 'Database is now publicly accessible.');
} else { } else {
@@ -69,7 +74,7 @@ class General extends Component
} }
$this->db_url = $this->database->getDbUrl(); $this->db_url = $this->database->getDbUrl();
$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);
} }

View File

@@ -0,0 +1,95 @@
<?php
namespace App\Http\Livewire\Project\Database\Mysql;
use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Database\StopDatabaseProxy;
use App\Models\StandaloneMysql;
use Exception;
use Livewire\Component;
class General extends Component
{
protected $listeners = ['refresh'];
public StandaloneMysql $database;
public string $db_url;
protected $rules = [
'database.name' => 'required',
'database.description' => 'nullable',
'database.mysql_root_password' => 'required',
'database.mysql_user' => 'required',
'database.mysql_password' => 'required',
'database.mysql_database' => 'required',
'database.mysql_conf' => 'nullable',
'database.image' => 'required',
'database.ports_mappings' => 'nullable',
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
];
protected $validationAttributes = [
'database.name' => 'Name',
'database.description' => 'Description',
'database.mysql_root_password' => 'Root Password',
'database.mysql_user' => 'User',
'database.mysql_password' => 'Password',
'database.mysql_database' => 'Database',
'database.mysql_conf' => 'MySQL Configuration',
'database.image' => 'Image',
'database.ports_mappings' => 'Port Mapping',
'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port',
];
public function submit()
{
try {
$this->validate();
$this->database->save();
$this->emit('success', 'Database updated successfully.');
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function instantSave()
{
try {
if ($this->database->is_public && !$this->database->public_port) {
$this->emit('error', 'Public port is required.');
$this->database->is_public = false;
return;
}
if ($this->database->is_public) {
if (!str($this->database->status)->startsWith('running')) {
$this->emit('error', 'Database must be started to be publicly accessible.');
$this->database->is_public = false;
return;
}
StartDatabaseProxy::run($this->database);
$this->emit('success', 'Database is now publicly accessible.');
} else {
StopDatabaseProxy::run($this->database);
$this->emit('success', 'Database is no longer publicly accessible.');
}
$this->db_url = $this->database->getDbUrl();
$this->database->save();
} catch (\Throwable $e) {
$this->database->is_public = !$this->database->is_public;
return handleError($e, $this);
}
}
public function refresh(): void
{
$this->database->refresh();
}
public function mount()
{
$this->db_url = $this->database->getDbUrl();
}
public function render()
{
return view('livewire.project.database.mysql.general');
}
}

View File

@@ -60,7 +60,11 @@ class General extends Component
return; return;
} }
if ($this->database->is_public) { if ($this->database->is_public) {
$this->emit('success', 'Starting TCP proxy...'); if (!str($this->database->status)->startsWith('running')) {
$this->emit('error', 'Database must be started to be publicly accessible.');
$this->database->is_public = false;
return;
}
StartDatabaseProxy::run($this->database); StartDatabaseProxy::run($this->database);
$this->emit('success', 'Database is now publicly accessible.'); $this->emit('success', 'Database is now publicly accessible.');
} else { } else {
@@ -69,11 +73,10 @@ class General extends Component
} }
$this->db_url = $this->database->getDbUrl(); $this->db_url = $this->database->getDbUrl();
$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);
} }
} }
public function save_init_script($script) public function save_init_script($script)
{ {

View File

@@ -35,7 +35,8 @@ class General extends Component
'database.is_public' => 'Is Public', 'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port', 'database.public_port' => 'Public Port',
]; ];
public function submit() { public function submit()
{
try { try {
$this->validate(); $this->validate();
if ($this->database->redis_conf === "") { if ($this->database->redis_conf === "") {
@@ -56,7 +57,11 @@ class General extends Component
return; return;
} }
if ($this->database->is_public) { if ($this->database->is_public) {
$this->emit('success', 'Starting TCP proxy...'); if (!str($this->database->status)->startsWith('running')) {
$this->emit('error', 'Database must be started to be publicly accessible.');
$this->database->is_public = false;
return;
}
StartDatabaseProxy::run($this->database); StartDatabaseProxy::run($this->database);
$this->emit('success', 'Database is now publicly accessible.'); $this->emit('success', 'Database is now publicly accessible.');
} else { } else {
@@ -65,7 +70,7 @@ class General extends Component
} }
$this->db_url = $this->database->getDbUrl(); $this->db_url = $this->database->getDbUrl();
$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);
} }

View File

@@ -21,10 +21,10 @@ class DeleteEnvironment extends Component
'environment_id' => 'required|int', 'environment_id' => 'required|int',
]); ]);
$environment = Environment::findOrFail($this->environment_id); $environment = Environment::findOrFail($this->environment_id);
if ($environment->applications->count() > 0) { if ($environment->isEmpty()) {
return $this->emit('error', 'Environment has resources defined, please delete them first.'); $environment->delete();
return redirect()->route('project.show', ['project_uuid' => $this->parameters['project_uuid']]);
} }
$environment->delete(); return $this->emit('error', 'Environment has defined resources, please delete them first.');
return redirect()->route('project.show', ['project_uuid' => $this->parameters['project_uuid']]);
} }
} }

View File

@@ -11,7 +11,6 @@ use App\Traits\SaveFromRedirect;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Livewire\Component; use Livewire\Component;
use Spatie\Url\Url;
class GithubPrivateRepository extends Component class GithubPrivateRepository extends Component
{ {

View File

@@ -21,14 +21,18 @@ class Select extends Component
public Collection|array $swarmDockers = []; public Collection|array $swarmDockers = [];
public array $parameters; public array $parameters;
public Collection|array $services = []; public Collection|array $services = [];
public Collection|array $allServices = [];
public bool $loadingServices = true; public bool $loadingServices = true;
public bool $loading = false; public bool $loading = false;
public $environments = []; public $environments = [];
public ?string $selectedEnvironment = null; public ?string $selectedEnvironment = null;
public ?string $existingPostgresqlUrl = null; public ?string $existingPostgresqlUrl = null;
public ?string $search = null;
protected $queryString = [ protected $queryString = [
'server', 'server',
'search'
]; ];
public function mount() public function mount()
@@ -41,6 +45,11 @@ class Select extends Component
$this->environments = Project::whereUuid($projectUuid)->first()->environments; $this->environments = Project::whereUuid($projectUuid)->first()->environments;
$this->selectedEnvironment = data_get($this->parameters, 'environment_name'); $this->selectedEnvironment = data_get($this->parameters, 'environment_name');
} }
public function render()
{
$this->loadServices();
return view('livewire.project.new.select');
}
public function updatedSelectedEnvironment() public function updatedSelectedEnvironment()
{ {
@@ -49,6 +58,7 @@ class Select extends Component
'environment_name' => $this->selectedEnvironment, 'environment_name' => $this->selectedEnvironment,
]); ]);
} }
// public function addExistingPostgresql() // public function addExistingPostgresql()
// { // {
// try { // try {
@@ -59,19 +69,28 @@ class Select extends Component
// } // }
// } // }
public function loadThings() public function loadServices(bool $force = false)
{
$this->loadServices();
$this->loadServers();
}
public function loadServices(bool $forceReload = false)
{ {
try { try {
if ($forceReload) { if (count($this->allServices) > 0 && !$force) {
Cache::forget('services'); if (!$this->search) {
$this->services = $this->allServices;
return;
}
$this->services = $this->allServices->filter(function ($service, $key) {
$tags = collect(data_get($service, 'tags', []));
return str_contains(strtolower($key), strtolower($this->search)) || $tags->contains(function ($tag) {
return str_contains(strtolower($tag), strtolower($this->search));
});
});
} else {
$this->search = null;
$this->allServices = getServiceTemplates();
$this->services = $this->allServices->filter(function ($service, $key) {
return str_contains(strtolower($key), strtolower($this->search));
});;
$this->emit('success', 'Successfully loaded services.');
} }
$this->services = getServiceTemplates();
$this->emit('success', 'Successfully loaded services.');
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} finally { } finally {

View File

@@ -6,8 +6,8 @@ use Livewire\Component;
class ComposeModal extends Component class ComposeModal extends Component
{ {
public string $raw; public ?string $raw = null;
public string $actual; public ?string $actual = null;
public function render() public function render()
{ {
return view('livewire.project.service.compose-modal'); return view('livewire.project.service.compose-modal');

View File

@@ -13,7 +13,7 @@ class Index extends Component
public $databases; public $databases;
public array $parameters; public array $parameters;
public array $query; public array $query;
protected $listeners = ["refreshStacks","checkStatus"]; protected $listeners = ["refreshStacks", "checkStatus"];
public function render() public function render()
{ {
return view('livewire.project.service.index'); return view('livewire.project.service.index');

View File

@@ -4,7 +4,6 @@ namespace App\Http\Livewire\Project\Service;
use App\Actions\Service\StartService; use App\Actions\Service\StartService;
use App\Actions\Service\StopService; use App\Actions\Service\StopService;
use App\Jobs\ContainerStatusJob;
use App\Models\Service; use App\Models\Service;
use Livewire\Component; use Livewire\Component;
@@ -13,15 +12,14 @@ class Navbar extends Component
public Service $service; public Service $service;
public array $parameters; public array $parameters;
public array $query; public array $query;
protected $listeners = ["checkStatus"];
public function render() public function render()
{ {
return view('livewire.project.service.navbar'); return view('livewire.project.service.navbar');
} }
public function checkStatus() {
public function checkStatus() $this->service->refresh();
{
$this->emit('checkStatus');
} }
public function deploy() public function deploy()
{ {
@@ -34,6 +32,6 @@ class Navbar extends Component
StopService::run($this->service); StopService::run($this->service);
$this->service->refresh(); $this->service->refresh();
$this->emit('success', 'Service stopped successfully.'); $this->emit('success', 'Service stopped successfully.');
$this->checkStatus(); $this->emit('checkStatus');
} }
} }

View File

@@ -2,7 +2,7 @@
namespace App\Http\Livewire\Project\Shared; namespace App\Http\Livewire\Project\Shared;
use App\Jobs\StopResourceJob; use App\Jobs\DeleteResourceJob;
use Livewire\Component; use Livewire\Component;
use Visus\Cuid2\Cuid2; use Visus\Cuid2\Cuid2;
@@ -21,7 +21,7 @@ class Danger extends Component
public function delete() public function delete()
{ {
try { try {
StopResourceJob::dispatchSync($this->resource); DeleteResourceJob::dispatchSync($this->resource);
return redirect()->route('project.resources', [ return redirect()->route('project.resources', [
'project_uuid' => $this->parameters['project_uuid'], 'project_uuid' => $this->parameters['project_uuid'],
'environment_name' => $this->parameters['environment_name'] 'environment_name' => $this->parameters['environment_name']

View File

@@ -31,11 +31,17 @@ class All extends Component
public function getDevView() public function getDevView()
{ {
$this->variables = $this->resource->environment_variables->map(function ($item) { $this->variables = $this->resource->environment_variables->map(function ($item) {
if ($item->is_shown_once) {
return "$item->key=(locked secret)";
}
return "$item->key=$item->value"; return "$item->key=$item->value";
})->sort()->join(' })->sort()->join('
'); ');
if ($this->showPreview) { if ($this->showPreview) {
$this->variablesPreview = $this->resource->environment_variables_preview->map(function ($item) { $this->variablesPreview = $this->resource->environment_variables_preview->map(function ($item) {
if ($item->is_shown_once) {
return "$item->key=(locked secret)";
}
return "$item->key=$item->value"; return "$item->key=$item->value";
})->sort()->join(' })->sort()->join('
'); ');
@@ -49,19 +55,27 @@ class All extends Component
{ {
if ($isPreview) { if ($isPreview) {
$variables = parseEnvFormatToArray($this->variablesPreview); $variables = parseEnvFormatToArray($this->variablesPreview);
$existingVariables = $this->resource->environment_variables_preview();
$this->resource->environment_variables_preview()->delete();
} else { } else {
$variables = parseEnvFormatToArray($this->variables); $variables = parseEnvFormatToArray($this->variables);
$existingVariables = $this->resource->environment_variables();
$this->resource->environment_variables()->delete();
} }
foreach ($variables as $key => $variable) { foreach ($variables as $key => $variable) {
$found = $existingVariables->where('key', $key)->first(); $found = $this->resource->environment_variables()->where('key', $key)->first();
$foundPreview = $this->resource->environment_variables_preview()->where('key', $key)->first();
if ($found) { if ($found) {
if ($found->is_shown_once) {
continue;
}
$found->value = $variable; $found->value = $variable;
$found->save(); $found->save();
continue; continue;
}
if ($foundPreview) {
if ($foundPreview->is_shown_once) {
continue;
}
$foundPreview->value = $variable;
$foundPreview->save();
continue;
} else { } else {
$environment = new EnvironmentVariable(); $environment = new EnvironmentVariable();
$environment->key = $key; $environment->key = $key;
@@ -81,6 +95,12 @@ class All extends Component
case 'standalone-mongodb': case 'standalone-mongodb':
$environment->standalone_mongodb_id = $this->resource->id; $environment->standalone_mongodb_id = $this->resource->id;
break; break;
case 'standalone-mysql':
$environment->standalone_mysql_id = $this->resource->id;
break;
case 'standalone-mariadb':
$environment->standalone_mariadb_id = $this->resource->id;
break;
case 'service': case 'service':
$environment->service_id = $this->resource->id; $environment->service_id = $this->resource->id;
break; break;

View File

@@ -5,7 +5,6 @@ namespace App\Http\Livewire\Project\Shared\EnvironmentVariable;
use App\Models\EnvironmentVariable as ModelsEnvironmentVariable; use App\Models\EnvironmentVariable as ModelsEnvironmentVariable;
use Livewire\Component; use Livewire\Component;
use Visus\Cuid2\Cuid2; use Visus\Cuid2\Cuid2;
use Illuminate\Support\Str;
class Show extends Component class Show extends Component
{ {
@@ -13,29 +12,45 @@ class Show extends Component
public ModelsEnvironmentVariable $env; public ModelsEnvironmentVariable $env;
public ?string $modalId = null; public ?string $modalId = null;
public bool $isDisabled = false; public bool $isDisabled = false;
public bool $isLocked = false;
public string $type; public string $type;
protected $rules = [ protected $rules = [
'env.key' => 'required|string', 'env.key' => 'required|string',
'env.value' => 'nullable', 'env.value' => 'nullable',
'env.is_build_time' => 'required|boolean', 'env.is_build_time' => 'required|boolean',
'env.is_shown_once' => 'required|boolean',
]; ];
protected $validationAttributes = [ protected $validationAttributes = [
'key' => 'key', 'key' => 'Key',
'value' => 'value', 'value' => 'Value',
'is_build_time' => 'build', 'is_build_time' => 'Build Time',
'is_shown_once' => 'Shown Once',
]; ];
public function mount() public function mount()
{ {
$this->isDisabled = false;
if (Str::of($this->env->key)->startsWith('SERVICE_FQDN') || Str::of($this->env->key)->startsWith('SERVICE_URL')) {
$this->isDisabled = true;
}
$this->modalId = new Cuid2(7); $this->modalId = new Cuid2(7);
$this->parameters = get_route_parameters(); $this->parameters = get_route_parameters();
$this->checkEnvs();
}
public function checkEnvs()
{
$this->isDisabled = false;
if (str($this->env->key)->startsWith('SERVICE_FQDN') || str($this->env->key)->startsWith('SERVICE_URL')) {
$this->isDisabled = true;
}
if ($this->env->is_shown_once) {
$this->isLocked = true;
}
}
public function lock()
{
$this->env->is_shown_once = true;
$this->env->save();
$this->checkEnvs();
$this->emit('refreshEnvs');
} }
public function instantSave() public function instantSave()
{ {
$this->submit(); $this->submit();

View File

@@ -33,9 +33,11 @@ class GetLogs extends Component
if ($refresh) { if ($refresh) {
$this->outputs = ''; $this->outputs = '';
} }
Process::run($sshCommand, function (string $type, string $output) { $command = Process::run($sshCommand);
$this->doSomethingWithThisChunkOfOutput($output); $output = $command->output();
}); $error = $command->errorOutput();
$this->doSomethingWithThisChunkOfOutput($output);
$this->doSomethingWithThisChunkOfOutput($error);
} }
} }
public function render() public function render()

View File

@@ -5,7 +5,9 @@ namespace App\Http\Livewire\Project\Shared;
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 App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb; use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql; use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis; use App\Models\StandaloneRedis;
use Livewire\Component; use Livewire\Component;
@@ -13,7 +15,7 @@ use Livewire\Component;
class Logs extends Component class Logs extends Component
{ {
public ?string $type = null; public ?string $type = null;
public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb $resource; public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb $resource;
public Server $server; public Server $server;
public ?string $container = null; public ?string $container = null;
public $parameters; public $parameters;
@@ -41,11 +43,16 @@ class Logs extends Component
if (is_null($resource)) { if (is_null($resource)) {
$resource = StandaloneMongodb::where('uuid', $this->parameters['database_uuid'])->first(); $resource = StandaloneMongodb::where('uuid', $this->parameters['database_uuid'])->first();
if (is_null($resource)) { if (is_null($resource)) {
abort(404); $resource = StandaloneMysql::where('uuid', $this->parameters['database_uuid'])->first();
if (is_null($resource)) {
$resource = StandaloneMariadb::where('uuid', $this->parameters['database_uuid'])->first();
if (is_null($resource)) {
abort(404);
}
}
} }
} }
} }
$this->resource = $resource; $this->resource = $resource;
$this->status = $this->resource->status; $this->status = $this->resource->status;
$this->server = $this->resource->destination->server; $this->server = $this->resource->destination->server;

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Http\Livewire\Project\Shared;
use Livewire\Component;
class Webhooks extends Component
{
public $resource;
public ?string $deploywebhook = null;
public function mount()
{
$this->deploywebhook = generateDeployWebhook($this->resource);
}
public function render()
{
return view('livewire.project.shared.webhooks');
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Http\Livewire\Server;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component;
class Delete extends Component
{
use AuthorizesRequests;
public $server;
public function delete()
{
try {
$this->authorize('delete', $this->server);
if (!$this->server->isEmpty()) {
$this->emit('error', 'Server has defined resources. Please delete them first.');
return;
}
$this->server->delete();
return redirect()->route('server.all');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function render()
{
return view('livewire.server.delete');
}
}

View File

@@ -4,12 +4,10 @@ namespace App\Http\Livewire\Server;
use App\Actions\Server\InstallDocker; use App\Actions\Server\InstallDocker;
use App\Models\Server; use App\Models\Server;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component; use Livewire\Component;
class Form extends Component class Form extends Component
{ {
use AuthorizesRequests;
public Server $server; public Server $server;
public bool $isValidConnection = false; public bool $isValidConnection = false;
public bool $isValidDocker = false; public bool $isValidDocker = false;
@@ -32,7 +30,7 @@ class Form extends Component
protected $validationAttributes = [ protected $validationAttributes = [
'server.name' => 'Name', 'server.name' => 'Name',
'server.description' => 'Description', 'server.description' => 'Description',
'server.ip' => 'IP address', 'server.ip' => 'IP address/Domain',
'server.user' => 'User', 'server.user' => 'User',
'server.port' => 'Port', 'server.port' => 'Port',
'server.settings.is_cloudflare_tunnel' => 'Cloudflare Tunnel', 'server.settings.is_cloudflare_tunnel' => 'Cloudflare Tunnel',
@@ -106,26 +104,12 @@ class Form extends Component
} }
} }
public function delete()
{
try {
$this->authorize('delete', $this->server);
if (!$this->server->isEmpty()) {
$this->emit('error', 'Server has defined resources. Please delete them first.');
return;
}
$this->server->delete();
return redirect()->route('server.all');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function submit() public function submit()
{ {
if(isCloud() && !isDev()) { if (isCloud() && !isDev()) {
$this->validate(); $this->validate();
$this->validate([ $this->validate([
'server.ip' => 'required|ip', 'server.ip' => 'required',
]); ]);
} else { } else {
$this->validate(); $this->validate();

View File

@@ -26,14 +26,14 @@ class ByIp extends Component
protected $rules = [ protected $rules = [
'name' => 'required|string', 'name' => 'required|string',
'description' => 'nullable|string', 'description' => 'nullable|string',
'ip' => 'required|ip', 'ip' => 'required',
'user' => 'required|string', 'user' => 'required|string',
'port' => 'required|integer', 'port' => 'required|integer',
]; ];
protected $validationAttributes = [ protected $validationAttributes = [
'name' => 'Name', 'name' => 'Name',
'description' => 'Description', 'description' => 'Description',
'ip' => 'IP Address', 'ip' => 'IP Address/Domain',
'user' => 'User', 'user' => 'User',
'port' => 'Port', 'port' => 'Port',
]; ];

View File

@@ -12,7 +12,7 @@ class DecideWhatToDoWithUser
public function handle(Request $request, Closure $next): Response public function handle(Request $request, Closure $next): Response
{ {
if (!auth()->user() || !isCloud() || isInstanceAdmin()) { if (!auth()->user() || !isCloud() || isInstanceAdmin()) {
if (!isCloud() && showBoarding() && !in_array($request->path(), allowedPathsForBoardingAccounts())) { if (!isCloud() && showBoarding() && !in_array($request->path(), allowedPathsForBoardingAccounts())) {
return redirect('boarding'); return redirect('boarding');
} }
return $next($request); return $next($request);

View File

@@ -44,6 +44,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private int $pull_request_id; private int $pull_request_id;
private string $commit; private string $commit;
private bool $force_rebuild; private bool $force_rebuild;
private bool $restart_only;
private ?string $dockerImage = null; private ?string $dockerImage = null;
private ?string $dockerImageTag = null; private ?string $dockerImageTag = null;
@@ -77,6 +78,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private int $customPort = 22; private int $customPort = 22;
private ?string $fullRepoUrl = null;
private ?string $branch = null;
public $tries = 1; public $tries = 1;
public function __construct(int $application_deployment_queue_id) public function __construct(int $application_deployment_queue_id)
{ {
@@ -91,6 +95,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->pull_request_id = $this->application_deployment_queue->pull_request_id; $this->pull_request_id = $this->application_deployment_queue->pull_request_id;
$this->commit = $this->application_deployment_queue->commit; $this->commit = $this->application_deployment_queue->commit;
$this->force_rebuild = $this->application_deployment_queue->force_rebuild; $this->force_rebuild = $this->application_deployment_queue->force_rebuild;
$this->restart_only = $this->application_deployment_queue->restart_only;
$source = data_get($this->application, 'source'); $source = data_get($this->application, 'source');
if ($source) { if ($source) {
@@ -133,9 +138,16 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
public function handle(): void public function handle(): void
{ {
// ray()->measure(); // ray()->measure();
$containers = getCurrentApplicationContainerStatus($this->server, $this->application->id); $containers = getCurrentApplicationContainerStatus($this->server, $this->application->id, $this->pull_request_id);
if ($containers->count() > 0) { if ($containers->count() === 1) {
$this->currently_running_container_name = data_get($containers[0], 'Names'); $this->currently_running_container_name = data_get($containers[0], 'Names');
} else {
$foundContainer = $containers->filter(function ($container) {
return !str(data_get($container, 'Names'))->startsWith("{$this->application->uuid}-pr-");
})->first();
if ($foundContainer) {
$this->currently_running_container_name = data_get($foundContainer, 'Names');
}
} }
if ($this->pull_request_id !== 0 && $this->pull_request_id !== null) { if ($this->pull_request_id !== 0 && $this->pull_request_id !== null) {
$this->currently_running_container_name = $this->container_name; $this->currently_running_container_name = $this->container_name;
@@ -179,7 +191,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->application->git_repository = "$gitHost:$gitRepo"; $this->application->git_repository = "$gitHost:$gitRepo";
} }
try { try {
if ($this->application->dockerfile) { if ($this->restart_only) {
$this->just_restart();
} else if ($this->application->dockerfile) {
$this->deploy_simple_dockerfile(); $this->deploy_simple_dockerfile();
} else if ($this->application->build_pack === 'dockerimage') { } else if ($this->application->build_pack === 'dockerimage') {
$this->deploy_dockerimage_buildpack(); $this->deploy_dockerimage_buildpack();
@@ -261,6 +275,49 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
// [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up -d"), "hidden" => true], // [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up -d"), "hidden" => true],
// ); // );
// } // }
private function generate_image_names()
{
if ($this->application->dockerfile) {
$this->build_image_name = Str::lower("{$this->application->git_repository}:build");
$this->production_image_name = Str::lower("{$this->application->uuid}:latest");
} else if ($this->application->build_pack === 'dockerimage') {
$this->production_image_name = Str::lower("{$this->dockerImage}:{$this->dockerImageTag}");
} else if ($this->pull_request_id !== 0) {
$this->build_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}-build");
$this->production_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}");
} else {
$tag = Str::of("{$this->commit}-{$this->application->id}-{$this->pull_request_id}");
if (strlen($tag) > 128) {
$tag = $tag->substr(0, 128);
}
$this->build_image_name = Str::lower("{$this->application->git_repository}:{$tag}-build");
$this->production_image_name = Str::lower("{$this->application->uuid}:{$tag}");
}
ray('Build Image Name: ' . $this->build_image_name . ' & Production Image Name: ' . $this->production_image_name)->green();
}
private function just_restart()
{
$this->execute_remote_command(
[
"echo 'Starting deployment of {$this->application->git_repository}:{$this->application->git_branch}.'"
],
);
$this->prepare_builder_image();
$this->check_git_if_build_needed();
$this->set_base_dir();
$this->generate_image_names();
$this->execute_remote_command([
"docker images -q {$this->production_image_name} 2>/dev/null", "hidden" => true, "save" => "local_image_found"
]);
if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty()) {
$this->generate_compose_file();
$this->rolling_update();
return;
}
$this->execute_remote_command([
"echo 'Cannot find image {$this->production_image_name} locally. Please redeploy the application.'",
]);
}
private function save_environment_variables() private function save_environment_variables()
{ {
$envs = collect([]); $envs = collect([]);
@@ -288,9 +345,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
executeInDocker($this->deployment_uuid, "echo '$dockerfile_base64' | base64 -d > $this->workdir/Dockerfile") executeInDocker($this->deployment_uuid, "echo '$dockerfile_base64' | base64 -d > $this->workdir/Dockerfile")
], ],
); );
$this->build_image_name = Str::lower("{$this->application->git_repository}:build"); $this->generate_image_names();
$this->production_image_name = Str::lower("{$this->application->uuid}:latest");
// ray('Build Image Name: ' . $this->build_image_name . ' & Production Image Name: ' . $this->production_image_name)->green();
$this->generate_compose_file(); $this->generate_compose_file();
$this->generate_build_env_variables(); $this->generate_build_env_variables();
$this->add_build_env_variables_to_dockerfile(); $this->add_build_env_variables_to_dockerfile();
@@ -308,7 +363,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
"echo 'Starting deployment of {$this->dockerImage}:{$this->dockerImageTag}.'" "echo 'Starting deployment of {$this->dockerImage}:{$this->dockerImageTag}.'"
], ],
); );
$this->production_image_name = Str::lower("{$this->dockerImage}:{$this->dockerImageTag}"); $this->generate_image_names();
$this->prepare_builder_image(); $this->prepare_builder_image();
$this->generate_compose_file(); $this->generate_compose_file();
$this->rolling_update(); $this->rolling_update();
@@ -325,16 +380,10 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
], ],
); );
$this->prepare_builder_image(); $this->prepare_builder_image();
$this->check_git_if_build_needed();
$this->clone_repository(); $this->clone_repository();
$this->set_base_dir(); $this->set_base_dir();
$tag = Str::of("{$this->commit}-{$this->application->id}-{$this->pull_request_id}"); $this->generate_image_names();
if (strlen($tag) > 128) {
$tag = $tag->substr(0, 128);
}
$this->build_image_name = Str::lower("{$this->application->git_repository}:{$tag}-build");
$this->production_image_name = Str::lower("{$this->application->uuid}:{$tag}");
// ray('Build Image Name: ' . $this->build_image_name . ' & Production Image Name: ' . $this->production_image_name)->green();
$this->cleanup_git(); $this->cleanup_git();
$this->generate_compose_file(); $this->generate_compose_file();
$this->generate_build_env_variables(); $this->generate_build_env_variables();
@@ -350,17 +399,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
], ],
); );
$this->prepare_builder_image(); $this->prepare_builder_image();
$this->clone_repository(); $this->check_git_if_build_needed();
$this->set_base_dir(); $this->set_base_dir();
$tag = Str::of("{$this->commit}-{$this->application->id}-{$this->pull_request_id}"); $this->generate_image_names();
if (strlen($tag) > 128) {
$tag = $tag->substr(0, 128);
}
$this->build_image_name = Str::lower("{$this->application->git_repository}:{$tag}-build");
$this->production_image_name = Str::lower("{$this->application->uuid}:{$tag}");
// ray('Build Image Name: ' . $this->build_image_name . ' & Production Image Name: ' . $this->production_image_name)->green();
if (!$this->force_rebuild) { if (!$this->force_rebuild) {
$this->execute_remote_command([ $this->execute_remote_command([
"docker images -q {$this->production_image_name} 2>/dev/null", "hidden" => true, "save" => "local_image_found" "docker images -q {$this->production_image_name} 2>/dev/null", "hidden" => true, "save" => "local_image_found"
@@ -379,6 +420,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
]); ]);
} }
} }
$this->clone_repository();
$this->cleanup_git(); $this->cleanup_git();
$this->generate_nixpacks_confs(); $this->generate_nixpacks_confs();
$this->generate_compose_file(); $this->generate_compose_file();
@@ -392,7 +434,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
{ {
if (count($this->application->ports_mappings_array) > 0) { if (count($this->application->ports_mappings_array) > 0) {
$this->execute_remote_command( $this->execute_remote_command(
["echo -n 'Application has ports mapped to the host system, rolling update is not supported. Stopping current container.'"], ["echo -n 'Application has ports mapped to the host system, rolling update is not supported.'"],
); );
$this->stop_running_container(force: true); $this->stop_running_container(force: true);
$this->start_by_compose_file(); $this->start_by_compose_file();
@@ -453,9 +495,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
} }
private function deploy_pull_request() private function deploy_pull_request()
{ {
$this->build_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}-build"); $this->generate_image_names();
$this->production_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}");
// ray('Build Image Name: ' . $this->build_image_name . ' & Production Image Name: ' . $this->production_image_name)->green();
$this->execute_remote_command([ $this->execute_remote_command([
"echo 'Starting pull request (#{$this->pull_request_id}) deployment of {$this->application->git_repository}:{$this->application->git_branch}.'", "echo 'Starting pull request (#{$this->pull_request_id}) deployment of {$this->application->git_repository}:{$this->application->git_branch}.'",
]); ]);
@@ -471,7 +511,12 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
// $this->generate_build_env_variables(); // $this->generate_build_env_variables();
// $this->add_build_env_variables_to_dockerfile(); // $this->add_build_env_variables_to_dockerfile();
$this->build_image(); $this->build_image();
$this->stop_running_container(); if ($this->currently_running_container_name) {
$this->execute_remote_command(
["echo -n 'Removing old version of your application.'"],
[executeInDocker($this->deployment_uuid, "docker rm -f $this->currently_running_container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true],
);
}
$this->execute_remote_command( $this->execute_remote_command(
["echo -n 'Starting preview deployment.'"], ["echo -n 'Starting preview deployment.'"],
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up -d"), "hidden" => true], [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up -d"), "hidden" => true],
@@ -480,17 +525,16 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function prepare_builder_image() private function prepare_builder_image()
{ {
$pull = "--pull=always";
$helperImage = config('coolify.helper_image'); $helperImage = config('coolify.helper_image');
if ($this->dockerConfigFileExists === 'OK') { if ($this->dockerConfigFileExists === 'OK') {
$runCommand = "docker run {$pull} -d --network {$this->destination->network} -v /:/host --name {$this->deployment_uuid} --rm -v {$this->serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}"; $runCommand = "docker run -d --network {$this->destination->network} -v /:/host --name {$this->deployment_uuid} --rm -v {$this->serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
} else { } else {
$runCommand = "docker run {$pull} -d --network {$this->destination->network} -v /:/host --name {$this->deployment_uuid} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}"; $runCommand = "docker run -d --network {$this->destination->network} -v /:/host --name {$this->deployment_uuid} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
} }
$this->execute_remote_command( $this->execute_remote_command(
[ [
"echo -n 'Pulling helper image from $helperImage.'", "echo -n 'Preparing container with helper image: $helperImage.'",
], ],
[ [
$runCommand, $runCommand,
@@ -510,27 +554,58 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
], ],
); );
} }
private function check_git_if_build_needed()
{
$this->generate_git_import_commands();
$private_key = data_get($this->application, 'private_key.private_key');
if ($private_key) {
$private_key = base64_encode($private_key);
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "mkdir -p /root/.ssh")
],
[
executeInDocker($this->deployment_uuid, "echo '{$private_key}' | base64 -d > /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} {$this->branch}"),
"hidden" => true,
"save" => "git_commit_sha"
],
);
} else {
$this->execute_remote_command(
[
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} {$this->branch}"),
"hidden" => true,
"save" => "git_commit_sha"
],
);
}
if ($this->saved_outputs->get('git_commit_sha')) {
$this->commit = $this->saved_outputs->get('git_commit_sha')->before("\t");
}
}
private function clone_repository() private function clone_repository()
{ {
$importCommands = $this->generate_git_import_commands();
$this->execute_remote_command( $this->execute_remote_command(
[ [
"echo -n 'Importing {$this->application->git_repository}:{$this->application->git_branch} (commit sha {$this->application->git_commit_sha}) to {$this->basedir}. '" "echo -n 'Importing {$this->application->git_repository}:{$this->application->git_branch} (commit sha {$this->application->git_commit_sha}) to {$this->basedir}. '"
], ],
[ [
$this->importing_git_repository(), "hidden" => true $importCommands, "hidden" => true
], ]
[
executeInDocker($this->deployment_uuid, "cd {$this->basedir} && git rev-parse HEAD"),
"hidden" => true,
"save" => "git_commit_sha"
],
); );
$this->commit = $this->saved_outputs->get('git_commit_sha');
} }
private function importing_git_repository() private function generate_git_import_commands()
{ {
$this->branch = $this->application->git_branch;
$commands = collect([]); $commands = collect([]);
$git_clone_command = "git clone -q -b {$this->application->git_branch}"; $git_clone_command = "git clone -q -b {$this->application->git_branch}";
if ($this->pull_request_id !== 0) { if ($this->pull_request_id !== 0) {
@@ -545,6 +620,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if ($this->source->getMorphClass() == 'App\Models\GithubApp') { if ($this->source->getMorphClass() == 'App\Models\GithubApp') {
if ($this->source->is_public) { if ($this->source->is_public) {
$this->fullRepoUrl = "{$this->source->html_url}/{$this->application->git_repository}";
$git_clone_command = "{$git_clone_command} {$this->source->html_url}/{$this->application->git_repository} {$this->basedir}"; $git_clone_command = "{$git_clone_command} {$this->source->html_url}/{$this->application->git_repository} {$this->basedir}";
$git_clone_command = $this->set_git_import_settings($git_clone_command); $git_clone_command = $this->set_git_import_settings($git_clone_command);
@@ -552,15 +628,22 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
} else { } else {
$github_access_token = generate_github_installation_token($this->source); $github_access_token = generate_github_installation_token($this->source);
$commands->push(executeInDocker($this->deployment_uuid, "git clone -q -b {$this->application->git_branch} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$this->application->git_repository}.git {$this->basedir}")); $commands->push(executeInDocker($this->deployment_uuid, "git clone -q -b {$this->application->git_branch} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$this->application->git_repository}.git {$this->basedir}"));
$this->fullRepoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$this->application->git_repository}.git";
} }
if ($this->pull_request_id !== 0) { if ($this->pull_request_id !== 0) {
$this->branch = "pull/{$this->pull_request_id}/head:$pr_branch_name";
$commands->push(executeInDocker($this->deployment_uuid, "cd {$this->basedir} && git fetch origin pull/{$this->pull_request_id}/head:$pr_branch_name && git checkout $pr_branch_name")); $commands->push(executeInDocker($this->deployment_uuid, "cd {$this->basedir} && git fetch origin pull/{$this->pull_request_id}/head:$pr_branch_name && git checkout $pr_branch_name"));
} }
return $commands->implode(' && '); return $commands->implode(' && ');
} }
} }
if ($this->application->deploymentType() === 'deploy_key') { if ($this->application->deploymentType() === 'deploy_key') {
$private_key = base64_encode($this->application->private_key->private_key); $this->fullRepoUrl = $this->application->git_repository;
$private_key = data_get($this->application, 'private_key.private_key');
if (is_null($private_key)) {
throw new Exception('Private key not found. Please add a private key to the application and try again.');
}
$private_key = base64_encode($private_key);
$git_clone_command = "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_clone_command} {$this->application->git_repository} {$this->basedir}"; $git_clone_command = "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_clone_command} {$this->application->git_repository} {$this->basedir}";
$git_clone_command = $this->set_git_import_settings($git_clone_command); $git_clone_command = $this->set_git_import_settings($git_clone_command);
$commands = collect([ $commands = collect([
@@ -572,10 +655,10 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
return $commands->implode(' && '); return $commands->implode(' && ');
} }
if ($this->application->deploymentType() === 'other') { if ($this->application->deploymentType() === 'other') {
$this->fullRepoUrl = $this->application->git_repository;
$git_clone_command = "{$git_clone_command} {$this->application->git_repository} {$this->basedir}"; $git_clone_command = "{$git_clone_command} {$this->application->git_repository} {$this->basedir}";
$git_clone_command = $this->set_git_import_settings($git_clone_command); $git_clone_command = $this->set_git_import_settings($git_clone_command);
$commands->push(executeInDocker($this->deployment_uuid, $git_clone_command)); $commands->push(executeInDocker($this->deployment_uuid, $git_clone_command));
ray($commands);
return $commands->implode(' && '); return $commands->implode(' && ');
} }
} }
@@ -623,7 +706,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function nixpacks_build_cmd() private function nixpacks_build_cmd()
{ {
$this->generate_env_variables(); $this->generate_env_variables();
$nixpacks_command = "nixpacks build --no-cache -o {$this->workdir} {$this->env_args} --no-error-without-start"; $nixpacks_command = "nixpacks build --cache-key '{$this->application->uuid}' -o {$this->workdir} {$this->env_args} --no-error-without-start";
if ($this->application->build_command) { if ($this->application->build_command) {
$nixpacks_command .= " --build-cmd \"{$this->application->build_command}\""; $nixpacks_command .= " --build-cmd \"{$this->application->build_command}\"";
} }
@@ -661,10 +744,34 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$volume_names = $this->generate_local_persistent_volumes_only_volume_names(); $volume_names = $this->generate_local_persistent_volumes_only_volume_names();
$environment_variables = $this->generate_environment_variables($ports); $environment_variables = $this->generate_environment_variables($ports);
$labels = generateLabelsApplication($this->application, $this->preview);
if (data_get($this->application, 'custom_labels')) { if (data_get($this->application, 'custom_labels')) {
$labels = str($this->application->custom_labels)->explode(',')->toArray(); $labels = collect(str($this->application->custom_labels)->explode(','));
$labels = $labels->filter(function ($value, $key) {
return !Str::startsWith($value, 'coolify.');
});
$this->application->custom_labels = $labels->implode(',');
$this->application->save();
} else {
$labels = collect(generateLabelsApplication($this->application, $this->preview));
} }
if ($this->pull_request_id !== 0) {
$newLabels = collect(generateLabelsApplication($this->application, $this->preview));
$newHostLabel = $newLabels->filter(function ($label) {
return str($label)->contains('Host');
});
$labels = $labels->reject(function ($label) {
return str($label)->contains('Host');
});
$labels = $labels->map(function ($label) {
$pattern = '/([a-zA-Z0-9]+)-(\d+)-(http|https)/';
$replacement = "$1-pr-{$this->pull_request_id}-$2-$3";
$newLabel = preg_replace($pattern, $replacement, $label);
return $newLabel;
});
$labels = $labels->merge($newHostLabel);
}
$labels = $labels->merge(defaultLabels($this->application->id, $this->application->uuid, $this->pull_request_id))->toArray();
$docker_compose = [ $docker_compose = [
'version' => '3.8', 'version' => '3.8',
'services' => [ 'services' => [
@@ -864,12 +971,12 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
if ($this->newVersionIsHealthy || $force) { if ($this->newVersionIsHealthy || $force) {
$this->execute_remote_command( $this->execute_remote_command(
["echo -n 'Removing old version of your application.'"], ["echo -n 'Removing old version of your application.'"],
[executeInDocker($this->deployment_uuid, "docker rm -f $this->currently_running_container_name >/dev/null 2>&1"), "hidden" => true], [executeInDocker($this->deployment_uuid, "docker rm -f $this->currently_running_container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true],
); );
} else { } else {
$this->execute_remote_command( $this->execute_remote_command(
["echo -n 'New version is not healthy, rolling back to the old version.'"], ["echo -n 'New version is not healthy, rolling back to the old version.'"],
[executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true], [executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true],
); );
} }
} }
@@ -885,14 +992,14 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
private function generate_build_env_variables() private function generate_build_env_variables()
{ {
$this->build_args = collect(["--build-arg SOURCE_COMMIT={$this->commit}"]); $this->build_args = collect(["--build-arg SOURCE_COMMIT=\"{$this->commit}\""]);
if ($this->pull_request_id === 0) { if ($this->pull_request_id === 0) {
foreach ($this->application->build_environment_variables as $env) { foreach ($this->application->build_environment_variables as $env) {
$this->build_args->push("--build-arg {$env->key}={$env->value}"); $this->build_args->push("--build-arg {$env->key}=\"{$env->value}\"");
} }
} else { } else {
foreach ($this->application->build_environment_variables_preview as $env) { foreach ($this->application->build_environment_variables_preview as $env) {
$this->build_args->push("--build-arg {$env->key}={$env->value}"); $this->build_args->push("--build-arg {$env->key}=\"{$env->value}\"");
} }
} }

View File

@@ -21,7 +21,7 @@ class CheckResaleLicenseJob implements ShouldQueue, ShouldBeEncrypted
public function handle(): void public function handle(): void
{ {
try { try {
resolve(CheckResaleLicense::class)(); CheckResaleLicense::run();
} catch (\Throwable $e) { } catch (\Throwable $e) {
send_internal_notification('CheckResaleLicenseJob failed with: ' . $e->getMessage()); send_internal_notification('CheckResaleLicenseJob failed with: ' . $e->getMessage());
ray($e); ray($e);

View File

@@ -18,7 +18,6 @@ use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Str;
class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
{ {
@@ -26,6 +25,7 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
public function __construct(public Server $server) public function __construct(public Server $server)
{ {
$this->handle();
} }
public function middleware(): array public function middleware(): array
{ {
@@ -39,7 +39,7 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
public function handle(): void public function handle(): void
{ {
ray("checking server status for {$this->server->id}"); // ray("checking server status for {$this->server->id}");
try { try {
// ray()->clearAll(); // ray()->clearAll();
$serverUptimeCheckNumber = $this->server->unreachable_count; $serverUptimeCheckNumber = $this->server->unreachable_count;
@@ -58,6 +58,23 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
$this->server->update([ $this->server->update([
'unreachable_count' => 0, 'unreachable_count' => 0,
]); ]);
// Update all applications, databases and services to exited
foreach ($this->server->applications() as $application) {
$application->update(['status' => 'exited']);
}
foreach ($this->server->databases() as $database) {
$database->update(['status' => 'exited']);
}
foreach ($this->server->services() as $service) {
$apps = $service->applications()->get();
$dbs = $service->databases()->get();
foreach ($apps as $app) {
$app->update(['status' => 'exited']);
}
foreach ($dbs as $db) {
$db->update(['status' => 'exited']);
}
}
return; return;
} }
$result = $this->server->validateConnection(); $result = $this->server->validateConnection();
@@ -138,11 +155,10 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
$containerStatus = "$containerStatus ($containerHealth)"; $containerStatus = "$containerStatus ($containerHealth)";
$labels = data_get($container, 'Config.Labels'); $labels = data_get($container, 'Config.Labels');
$labels = Arr::undot(format_docker_labels_to_json($labels)); $labels = Arr::undot(format_docker_labels_to_json($labels));
$labelId = data_get($labels, 'coolify.applicationId'); $applicationId = data_get($labels, 'coolify.applicationId');
if ($labelId) { if ($applicationId) {
if (str_contains($labelId, '-pr-')) { $pullRequestId = data_get($labels, 'coolify.pullRequestId');
$pullRequestId = data_get($labels, 'coolify.pullRequestId'); if ($pullRequestId) {
$applicationId = (int) Str::before($labelId, '-pr-');
$preview = ApplicationPreview::where('application_id', $applicationId)->where('pull_request_id', $pullRequestId)->first(); $preview = ApplicationPreview::where('application_id', $applicationId)->where('pull_request_id', $pullRequestId)->first();
if ($preview) { if ($preview) {
$foundApplicationPreviews[] = $preview->id; $foundApplicationPreviews[] = $preview->id;
@@ -154,7 +170,7 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
//Notify user that this container should not be there. //Notify user that this container should not be there.
} }
} else { } else {
$application = $applications->where('id', $labelId)->first(); $application = $applications->where('id', $applicationId)->first();
if ($application) { if ($application) {
$foundApplications[] = $application->id; $foundApplications[] = $application->id;
$statusFromDb = $application->status; $statusFromDb = $application->status;
@@ -230,10 +246,13 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
$name = data_get($exitedService, 'name'); $name = data_get($exitedService, 'name');
$fqdn = data_get($exitedService, 'fqdn'); $fqdn = data_get($exitedService, 'fqdn');
$containerName = $name ? "$name ($fqdn)" : $fqdn; $containerName = $name ? "$name ($fqdn)" : $fqdn;
$project = data_get($service, 'environment.project'); $projectUuid = data_get($service, 'environment.project.uuid');
$environment = data_get($service, 'environment'); $serviceUuid = data_get($service, 'uuid');
$environmentName = data_get($service, 'environment.name');
$url = base_url() . '/project/' . $project->uuid . "/" . $environment->name . "/service/" . $service->uuid; if ($projectUuid && $serviceUuid && $environmentName) {
$url = base_url() . '/project/' . $projectUuid . "/" . $environmentName . "/service/" . $serviceUuid;
}
$this->server->team->notify(new ContainerStopped($containerName, $this->server, $url)); $this->server->team->notify(new ContainerStopped($containerName, $this->server, $url));
$exitedService->update(['status' => 'exited']); $exitedService->update(['status' => 'exited']);
} }
@@ -251,10 +270,13 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
$containerName = $name ? "$name ($fqdn)" : $fqdn; $containerName = $name ? "$name ($fqdn)" : $fqdn;
$project = data_get($application, 'environment.project'); $projectUuid = data_get($application, 'environment.project.uuid');
$environment = data_get($application, 'environment'); $applicationUuid = data_get($application, 'uuid');
$environment = data_get($application, 'environment.name');
$url = base_url() . '/project/' . $project->uuid . "/" . $environment->name . "/application/" . $application->uuid; if ($projectUuid && $applicationUuid && $environment) {
$url = base_url() . '/project/' . $projectUuid . "/" . $environment . "/application/" . $applicationUuid;
}
$this->server->team->notify(new ContainerStopped($containerName, $this->server, $url)); $this->server->team->notify(new ContainerStopped($containerName, $this->server, $url));
} }
@@ -271,10 +293,14 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
$containerName = $name ? "$name ($fqdn)" : $fqdn; $containerName = $name ? "$name ($fqdn)" : $fqdn;
$project = data_get($preview, 'application.environment.project'); $projectUuid = data_get($preview, 'application.environment.project.uuid');
$environment = data_get($preview, 'application.environment'); $environmentName = data_get($preview, 'application.environment.name');
$applicationUuid = data_get($preview, 'application.uuid');
if ($projectUuid && $applicationUuid && $environmentName) {
$url = base_url() . '/project/' . $projectUuid . "/" . $environmentName . "/application/" . $applicationUuid;
}
$url = base_url() . '/project/' . $project->uuid . "/" . $environment->name . "/application/" . $preview->application->uuid;
$this->server->team->notify(new ContainerStopped($containerName, $this->server, $url)); $this->server->team->notify(new ContainerStopped($containerName, $this->server, $url));
} }
$notRunningDatabases = $databases->pluck('id')->diff($foundDatabases); $notRunningDatabases = $databases->pluck('id')->diff($foundDatabases);
@@ -290,10 +316,13 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
$containerName = $name; $containerName = $name;
$project = data_get($database, 'environment.project'); $projectUuid = data_get($database, 'environment.project.uuid');
$environment = data_get($database, 'environment'); $environmentName = data_get($database, 'environment.name');
$databaseUuid = data_get($database, 'uuid');
$url = base_url() . '/project/' . $project->uuid . "/" . $environment->name . "/database/" . $database->uuid; if ($projectUuid && $databaseUuid && $environmentName) {
$url = base_url() . '/project/' . $projectUuid . "/" . $environmentName . "/database/" . $databaseUuid;
}
$this->server->team->notify(new ContainerStopped($containerName, $this->server, $url)); $this->server->team->notify(new ContainerStopped($containerName, $this->server, $url));
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {

View File

@@ -2,11 +2,14 @@
namespace App\Jobs; namespace App\Jobs;
use App\Actions\Database\StopDatabase;
use App\Models\S3Storage; use App\Models\S3Storage;
use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledDatabaseBackup;
use App\Models\ScheduledDatabaseBackupExecution; use App\Models\ScheduledDatabaseBackupExecution;
use App\Models\Server; use App\Models\Server;
use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb; use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql; use App\Models\StandalonePostgresql;
use App\Models\Team; use App\Models\Team;
use App\Notifications\Database\BackupFailed; use App\Notifications\Database\BackupFailed;
@@ -20,6 +23,7 @@ use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Throwable;
class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
{ {
@@ -28,7 +32,7 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
public ?Team $team = null; public ?Team $team = null;
public Server $server; public Server $server;
public ScheduledDatabaseBackup $backup; public ScheduledDatabaseBackup $backup;
public StandalonePostgresql|StandaloneMongodb $database; public StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb $database;
public ?string $container_name = null; public ?string $container_name = null;
public ?ScheduledDatabaseBackupExecution $backup_log = null; public ?ScheduledDatabaseBackupExecution $backup_log = null;
@@ -62,6 +66,14 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
public function handle(): void public function handle(): void
{ {
try { try {
// Check if team is exists
if (is_null($this->team)) {
$this->backup->update(['status' => 'failed']);
StopDatabase::run($this->database);
$this->database->delete();
return;
}
$status = Str::of(data_get($this->database, 'status')); $status = Str::of(data_get($this->database, 'status'));
if (!$status->startsWith('running') && $this->database->id !== 0) { if (!$status->startsWith('running') && $this->database->id !== 0) {
ray('database not running'); ray('database not running');
@@ -75,6 +87,10 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
$databasesToBackup = [$this->database->postgres_db]; $databasesToBackup = [$this->database->postgres_db];
} else if ($databaseType === 'standalone-mongodb') { } else if ($databaseType === 'standalone-mongodb') {
$databasesToBackup = ['*']; $databasesToBackup = ['*'];
} else if ($databaseType === 'standalone-mysql') {
$databasesToBackup = [$this->database->mysql_database];
} else if ($databaseType === 'standalone-mariadb') {
$databasesToBackup = [$this->database->mariadb_database];
} else { } else {
return; return;
} }
@@ -88,6 +104,14 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
$databasesToBackup = explode('|', $databasesToBackup); $databasesToBackup = explode('|', $databasesToBackup);
$databasesToBackup = array_map('trim', $databasesToBackup); $databasesToBackup = array_map('trim', $databasesToBackup);
ray($databasesToBackup); ray($databasesToBackup);
} else if ($databaseType === 'standalone-mysql') {
// Format: db1,db2,db3
$databasesToBackup = explode(',', $databasesToBackup);
$databasesToBackup = array_map('trim', $databasesToBackup);
} else if ($databaseType === 'standalone-mariadb') {
// Format: db1,db2,db3
$databasesToBackup = explode(',', $databasesToBackup);
$databasesToBackup = array_map('trim', $databasesToBackup);
} else { } else {
return; return;
} }
@@ -124,7 +148,6 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
} else { } else {
$databaseName = $database; $databaseName = $database;
} }
ray($databaseName);
} }
$this->backup_file = "/mongo-dump-$databaseName-" . Carbon::now()->timestamp . ".tar.gz"; $this->backup_file = "/mongo-dump-$databaseName-" . Carbon::now()->timestamp . ".tar.gz";
$this->backup_location = $this->backup_dir . $this->backup_file; $this->backup_location = $this->backup_dir . $this->backup_file;
@@ -134,6 +157,24 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
'scheduled_database_backup_id' => $this->backup->id, 'scheduled_database_backup_id' => $this->backup->id,
]); ]);
$this->backup_standalone_mongodb($database); $this->backup_standalone_mongodb($database);
} else if ($databaseType === 'standalone-mysql') {
$this->backup_file = "/mysql-dump-$database-" . Carbon::now()->timestamp . ".dmp";
$this->backup_location = $this->backup_dir . $this->backup_file;
$this->backup_log = ScheduledDatabaseBackupExecution::create([
'database_name' => $database,
'filename' => $this->backup_location,
'scheduled_database_backup_id' => $this->backup->id,
]);
$this->backup_standalone_mysql($database);
} else if ($databaseType === 'standalone-mariadb') {
$this->backup_file = "/mariadb-dump-$database-" . Carbon::now()->timestamp . ".dmp";
$this->backup_location = $this->backup_dir . $this->backup_file;
$this->backup_log = ScheduledDatabaseBackupExecution::create([
'database_name' => $database,
'filename' => $this->backup_location,
'scheduled_database_backup_id' => $this->backup->id,
]);
$this->backup_standalone_mariadb($database);
} else { } else {
throw new \Exception('Unsupported database type'); throw new \Exception('Unsupported database type');
} }
@@ -170,18 +211,25 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
private function backup_standalone_mongodb(string $databaseWithCollections): void private function backup_standalone_mongodb(string $databaseWithCollections): void
{ {
try { try {
$url = $this->database->getDbUrl(); $url = $this->database->getDbUrl(useInternal: true);
if ($databaseWithCollections === 'all') { if ($databaseWithCollections === 'all') {
$commands[] = "mkdir -p " . $this->backup_dir; $commands[] = "mkdir -p " . $this->backup_dir;
$commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --gzip --archive > $this->backup_location"; $commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --gzip --archive > $this->backup_location";
} else { } else {
$collectionsToExclude = str($databaseWithCollections)->after(':')->explode(','); if (str($databaseWithCollections)->contains(':')) {
$databaseName = str($databaseWithCollections)->before(':'); $databaseName = str($databaseWithCollections)->before(':');
$collectionsToExclude = str($databaseWithCollections)->after(':')->explode(',');
} else {
$databaseName = $databaseWithCollections;
$collectionsToExclude = collect();
}
$commands[] = "mkdir -p " . $this->backup_dir; $commands[] = "mkdir -p " . $this->backup_dir;
$commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --db $databaseName --gzip --excludeCollection " . $collectionsToExclude->implode(' --excludeCollection ') . " --archive > $this->backup_location"; if ($collectionsToExclude->count() === 0) {
$commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --db $databaseName --gzip --archive > $this->backup_location";
} else {
$commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --db $databaseName --gzip --excludeCollection " . $collectionsToExclude->implode(' --excludeCollection ') . " --archive > $this->backup_location";
}
} }
ray($commands);
$this->backup_output = instant_remote_process($commands, $this->server); $this->backup_output = instant_remote_process($commands, $this->server);
$this->backup_output = trim($this->backup_output); $this->backup_output = trim($this->backup_output);
if ($this->backup_output === '') { if ($this->backup_output === '') {
@@ -211,7 +259,42 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
throw $e; throw $e;
} }
} }
private function backup_standalone_mysql(string $database): void
{
try {
$commands[] = "mkdir -p " . $this->backup_dir;
$commands[] = "docker exec $this->container_name mysqldump -u root -p{$this->database->mysql_root_password} $database > $this->backup_location";
ray($commands);
$this->backup_output = instant_remote_process($commands, $this->server);
$this->backup_output = trim($this->backup_output);
if ($this->backup_output === '') {
$this->backup_output = null;
}
ray('Backup done for ' . $this->container_name . ' at ' . $this->server->name . ':' . $this->backup_location);
} catch (\Throwable $e) {
$this->add_to_backup_output($e->getMessage());
ray('Backup failed for ' . $this->container_name . ' at ' . $this->server->name . ':' . $this->backup_location . '\n\nError:' . $e->getMessage());
throw $e;
}
}
private function backup_standalone_mariadb(string $database): void
{
try {
$commands[] = "mkdir -p " . $this->backup_dir;
$commands[] = "docker exec $this->container_name mariadb-dump -u root -p{$this->database->mariadb_root_password} $database > $this->backup_location";
ray($commands);
$this->backup_output = instant_remote_process($commands, $this->server);
$this->backup_output = trim($this->backup_output);
if ($this->backup_output === '') {
$this->backup_output = null;
}
ray('Backup done for ' . $this->container_name . ' at ' . $this->server->name . ':' . $this->backup_location);
} catch (\Throwable $e) {
$this->add_to_backup_output($e->getMessage());
ray('Backup failed for ' . $this->container_name . ' at ' . $this->server->name . ':' . $this->backup_location . '\n\nError:' . $e->getMessage());
throw $e;
}
}
private function add_to_backup_output($output): void private function add_to_backup_output($output): void
{ {
if ($this->backup_output) { if ($this->backup_output) {

View File

@@ -7,7 +7,9 @@ use App\Actions\Database\StopDatabase;
use App\Actions\Service\StopService; use App\Actions\Service\StopService;
use App\Models\Application; use App\Models\Application;
use App\Models\Service; use App\Models\Service;
use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb; use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql; use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis; use App\Models\StandaloneRedis;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
@@ -17,11 +19,11 @@ use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
class StopResourceJob implements ShouldQueue, ShouldBeEncrypted class DeleteResourceJob implements ShouldQueue, ShouldBeEncrypted
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb $resource) public function __construct(public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb $resource)
{ {
} }
@@ -30,6 +32,7 @@ class StopResourceJob implements ShouldQueue, ShouldBeEncrypted
try { try {
$server = $this->resource->destination->server; $server = $this->resource->destination->server;
if (!$server->isFunctional()) { if (!$server->isFunctional()) {
$this->resource->delete();
return 'Server is not functional'; return 'Server is not functional';
} }
switch ($this->resource->type()) { switch ($this->resource->type()) {
@@ -45,15 +48,20 @@ class StopResourceJob implements ShouldQueue, ShouldBeEncrypted
case 'standalone-mongodb': case 'standalone-mongodb':
StopDatabase::run($this->resource); StopDatabase::run($this->resource);
break; break;
case 'standalone-mysql':
StopDatabase::run($this->resource);
break;
case 'standalone-mariadb':
StopDatabase::run($this->resource);
break;
case 'service': case 'service':
StopService::run($this->resource); StopService::run($this->resource);
break; break;
} }
$this->resource->delete();
} catch (\Throwable $e) { } catch (\Throwable $e) {
send_internal_notification('ContainerStoppingJob failed with: ' . $e->getMessage()); send_internal_notification('ContainerStoppingJob failed with: ' . $e->getMessage());
throw $e; throw $e;
} finally {
$this->resource->delete();
} }
} }
} }

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Jobs;
use App\Models\Server;
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\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
class PullHelperImageJob implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $timeout = 1000;
public function middleware(): array
{
return [(new WithoutOverlapping($this->server->uuid))->dontRelease()];
}
public function uniqueId(): string
{
return $this->server->uuid;
}
public function __construct(public Server $server)
{
}
public function handle(): void
{
try {
$helperImage = config('coolify.helper_image');
ray("Pulling {$helperImage}");
instant_remote_process(["docker pull -q {$helperImage}"], $this->server, false);
ray('PullHelperImageJob done');
} catch (\Throwable $e) {
send_internal_notification('PullHelperImageJob failed with: ' . $e->getMessage());
ray($e->getMessage());
throw $e;
}
}
}

View File

@@ -46,11 +46,12 @@ class SendMessageToTelegramJob implements ShouldQueue, ShouldBeEncrypted
if (!empty($this->buttons)) { if (!empty($this->buttons)) {
foreach ($this->buttons as $button) { foreach ($this->buttons as $button) {
$buttonUrl = data_get($button, 'url'); $buttonUrl = data_get($button, 'url');
$text = data_get($button, 'text', 'Click here');
if ($buttonUrl && Str::contains($buttonUrl, 'http://localhost')) { if ($buttonUrl && Str::contains($buttonUrl, 'http://localhost')) {
$buttonUrl = str_replace('http://localhost', config('app.url'), $buttonUrl); $buttonUrl = str_replace('http://localhost', config('app.url'), $buttonUrl);
} }
$inlineButtons[] = [ $inlineButtons[] = [
'text' => $button['text'], 'text' => $text,
'url' => $buttonUrl, 'url' => $buttonUrl,
]; ];
} }

View File

@@ -7,12 +7,8 @@ use Illuminate\Database\Eloquent\Model;
class Environment extends Model class Environment extends Model
{ {
protected $fillable = [ protected $guarded = [];
'name', public function isEmpty()
'project_id',
];
public function can_delete_environment()
{ {
return $this->applications()->count() == 0 && return $this->applications()->count() == 0 &&
$this->redis()->count() == 0 && $this->redis()->count() == 0 &&
@@ -38,13 +34,23 @@ class Environment extends Model
{ {
return $this->hasMany(StandaloneMongodb::class); return $this->hasMany(StandaloneMongodb::class);
} }
public function mysqls()
{
return $this->hasMany(StandaloneMysql::class);
}
public function mariadbs()
{
return $this->hasMany(StandaloneMariadb::class);
}
public function databases() public function databases()
{ {
$postgresqls = $this->postgresqls; $postgresqls = $this->postgresqls;
$redis = $this->redis; $redis = $this->redis;
$mongodbs = $this->mongodbs; $mongodbs = $this->mongodbs;
return $postgresqls->concat($redis)->concat($mongodbs); $mysqls = $this->mysqls;
$mariadbs = $this->mariadbs;
return $postgresqls->concat($redis)->concat($mongodbs)->concat($mysqls)->concat($mariadbs);
} }
public function project() public function project()

View File

@@ -11,7 +11,7 @@ class EnvironmentVariable extends Model
{ {
protected $guarded = []; protected $guarded = [];
protected $casts = [ protected $casts = [
"key" => 'string', 'key' => 'string',
'value' => 'encrypted', 'value' => 'encrypted',
'is_build_time' => 'boolean', 'is_build_time' => 'boolean',
]; ];
@@ -21,6 +21,10 @@ class EnvironmentVariable extends Model
static::created(function ($environment_variable) { static::created(function ($environment_variable) {
if ($environment_variable->application_id && !$environment_variable->is_preview) { if ($environment_variable->application_id && !$environment_variable->is_preview) {
$found = ModelsEnvironmentVariable::where('key', $environment_variable->key)->where('application_id', $environment_variable->application_id)->where('is_preview', true)->first(); $found = ModelsEnvironmentVariable::where('key', $environment_variable->key)->where('application_id', $environment_variable->application_id)->where('is_preview', true)->first();
$application = Application::find($environment_variable->application_id);
if ($application->build_pack === 'dockerfile') {
return;
}
if (!$found) { if (!$found) {
ModelsEnvironmentVariable::create([ ModelsEnvironmentVariable::create([
'key' => $environment_variable->key, 'key' => $environment_variable->key,
@@ -33,7 +37,8 @@ class EnvironmentVariable extends Model
} }
}); });
} }
public function service() { public function service()
{
return $this->belongsTo(Service::class); return $this->belongsTo(Service::class);
} }
protected function value(): Attribute protected function value(): Attribute
@@ -55,9 +60,9 @@ class EnvironmentVariable extends Model
$variable = Str::after($environment_variable, 'global.'); $variable = Str::after($environment_variable, 'global.');
$variable = Str::before($variable, '}}'); $variable = Str::before($variable, '}}');
$variable = Str::of($variable)->trim()->value; $variable = Str::of($variable)->trim()->value;
// $environment_variable = GlobalEnvironmentVariable::where('name', $environment_variable)->where('team_id', $team_id)->first()?->value; // $environment_variable = GlobalEnvironmentVariable::where('name', $environment_variable)->where('team_id', $team_id)->first()?->value;
ray('global env variable'); ray('global env variable');
return $environment_variable; return $environment_variable;
} }
return $environment_variable; return $environment_variable;
} }
@@ -77,5 +82,4 @@ class EnvironmentVariable extends Model
set: fn (string $value) => Str::of($value)->trim(), set: fn (string $value) => Str::of($value)->trim(),
); );
} }
} }

View File

@@ -18,7 +18,7 @@ class Project extends BaseModel
'project_id' => $project->id, 'project_id' => $project->id,
]); ]);
Environment::create([ Environment::create([
'name' => 'Production', 'name' => 'production',
'project_id' => $project->id, 'project_id' => $project->id,
]); ]);
}); });
@@ -56,4 +56,16 @@ class Project extends BaseModel
{ {
return $this->hasManyThrough(StandaloneRedis::class, Environment::class); return $this->hasManyThrough(StandaloneRedis::class, Environment::class);
} }
public function mongodbs()
{
return $this->hasManyThrough(StandaloneMongodb::class, Environment::class);
}
public function mysqls()
{
return $this->hasMany(StandaloneMysql::class, Environment::class);
}
public function mariadbs()
{
return $this->hasMany(StandaloneMariadb::class, Environment::class);
}
} }

View File

@@ -122,10 +122,12 @@ class Server extends BaseModel
public function databases() public function databases()
{ {
return $this->destinations()->map(function ($standaloneDocker) { return $this->destinations()->map(function ($standaloneDocker) {
$postgresqls = $standaloneDocker->postgresqls; $postgresqls = data_get($standaloneDocker, 'postgresqls', collect([]));
$redis = $standaloneDocker->redis; $redis = data_get($standaloneDocker, 'redis', collect([]));
$mongodbs = $standaloneDocker->mongodbs; $mongodbs = data_get($standaloneDocker, 'mongodbs', collect([]));
return $postgresqls->concat($redis)->concat($mongodbs); $mysqls = data_get($standaloneDocker, 'mysqls', collect([]));
$mariadbs = data_get($standaloneDocker, 'mariadbs', collect([]));
return $postgresqls->concat($redis)->concat($mongodbs)->concat($mysqls)->concat($mariadbs);
})->flatten(); })->flatten();
} }
public function applications() public function applications()
@@ -258,7 +260,8 @@ class Server extends BaseModel
$this->settings->save(); $this->settings->save();
return true; return true;
} }
public function validateCoolifyNetwork() { public function validateCoolifyNetwork()
{
return instant_remote_process(["docker network create coolify --attachable >/dev/null 2>&1 || true"], $this, false); return instant_remote_process(["docker network create coolify --attachable >/dev/null 2>&1 || true"], $this, false);
} }
} }

View File

@@ -6,10 +6,7 @@ use Illuminate\Database\Eloquent\Model;
class ServerSetting extends Model class ServerSetting extends Model
{ {
protected $fillable = [ protected $guarded = [];
'server_id',
'is_usable',
];
public function server() public function server()
{ {

View File

@@ -40,7 +40,6 @@ class Service extends BaseModel
instant_remote_process(["docker volume rm -f $storage->name"], $service->server, false); instant_remote_process(["docker volume rm -f $storage->name"], $service->server, false);
}); });
} }
instant_remote_process(["docker network rm {$service->uuid}"], $service->server, false);
}); });
} }
public function type() public function type()
@@ -90,6 +89,10 @@ class Service extends BaseModel
{ {
return $this->hasMany(EnvironmentVariable::class)->orderBy('key', 'asc'); return $this->hasMany(EnvironmentVariable::class)->orderBy('key', 'asc');
} }
public function environment_variables_preview(): HasMany
{
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', true)->orderBy('key', 'asc');
}
public function workdir() public function workdir()
{ {
return service_configuration_dir() . "/{$this->uuid}"; return service_configuration_dir() . "/{$this->uuid}";
@@ -257,7 +260,7 @@ class Service extends BaseModel
$networks = $serviceNetworks->toArray(); $networks = $serviceNetworks->toArray();
foreach ($definedNetwork as $key => $network) { foreach ($definedNetwork as $key => $network) {
$networks = array_merge($networks, [ $networks = array_merge($networks, [
$network $network => null
]); ]);
} }
data_set($service, 'networks', $networks); data_set($service, 'networks', $networks);

View File

@@ -24,6 +24,14 @@ class StandaloneDocker extends BaseModel
{ {
return $this->morphMany(StandaloneMongodb::class, 'destination'); return $this->morphMany(StandaloneMongodb::class, 'destination');
} }
public function mysqls()
{
return $this->morphMany(StandaloneMysql::class, 'destination');
}
public function mariadbs()
{
return $this->morphMany(StandaloneMariadb::class, 'destination');
}
public function server() public function server()
{ {
@@ -35,8 +43,18 @@ class StandaloneDocker extends BaseModel
return $this->morphMany(Service::class, 'destination'); return $this->morphMany(Service::class, 'destination');
} }
public function databases()
{
$postgresqls = $this->postgresqls;
$redis = $this->redis;
$mongodbs = $this->mongodbs;
$mysqls = $this->mysqls;
$mariadbs = $this->mariadbs;
return $postgresqls->concat($redis)->concat($mongodbs)->concat($mysqls)->concat($mariadbs);
}
public function attachedTo() public function attachedTo()
{ {
return $this->applications?->count() > 0 || $this->databases?->count() > 0; return $this->applications?->count() > 0 || $this->databases()->count() > 0;
} }
} }

View File

@@ -0,0 +1,106 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class StandaloneMariadb extends BaseModel
{
use HasFactory;
protected $guarded = [];
protected $casts = [
'mariadb_password' => 'encrypted',
];
protected static function booted()
{
static::created(function ($database) {
LocalPersistentVolume::create([
'name' => 'mariadb-data-' . $database->uuid,
'mount_path' => '/var/lib/mysql',
'host_path' => null,
'resource_id' => $database->id,
'resource_type' => $database->getMorphClass(),
'is_readonly' => true
]);
});
static::deleting(function ($database) {
$storages = $database->persistentStorages()->get();
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $database->destination->server, false);
}
$database->scheduledBackups()->delete();
$database->persistentStorages()->delete();
$database->environment_variables()->delete();
});
}
public function type(): string
{
return 'standalone-mariadb';
}
public function portsMappings(): Attribute
{
return Attribute::make(
set: fn ($value) => $value === "" ? null : $value,
);
}
public function portsMappingsArray(): Attribute
{
return Attribute::make(
get: fn () => is_null($this->ports_mappings)
? []
: explode(',', $this->ports_mappings),
);
}
public function getDbUrl(bool $useInternal = false): string
{
if ($this->is_public && !$useInternal) {
return "mysql://{$this->mariadb_user}:{$this->mariadb_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mariadb_database}";
} else {
return "mysql://{$this->mariadb_user}:{$this->mariadb_password}@{$this->uuid}:3306/{$this->mariadb_database}";
}
}
public function environment()
{
return $this->belongsTo(Environment::class);
}
public function fileStorages()
{
return $this->morphMany(LocalFileVolume::class, 'resource');
}
public function destination()
{
return $this->morphTo();
}
public function environment_variables(): HasMany
{
return $this->hasMany(EnvironmentVariable::class);
}
public function runtime_environment_variables(): HasMany
{
return $this->hasMany(EnvironmentVariable::class);
}
public function persistentStorages()
{
return $this->morphMany(LocalPersistentVolume::class, 'resource');
}
public function scheduledBackups()
{
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
}

View File

@@ -15,8 +15,16 @@ class StandaloneMongodb extends BaseModel
{ {
static::created(function ($database) { static::created(function ($database) {
LocalPersistentVolume::create([ LocalPersistentVolume::create([
'name' => 'mongodb-data-' . $database->uuid, 'name' => 'mongodb-configdb-' . $database->uuid,
'mount_path' => '/data', 'mount_path' => '/data/configdb',
'host_path' => null,
'resource_id' => $database->id,
'resource_type' => $database->getMorphClass(),
'is_readonly' => true
]);
LocalPersistentVolume::create([
'name' => 'mongodb-db-' . $database->uuid,
'mount_path' => '/data/db',
'host_path' => null, 'host_path' => null,
'resource_id' => $database->id, 'resource_id' => $database->id,
'resource_type' => $database->getMorphClass(), 'resource_type' => $database->getMorphClass(),
@@ -24,16 +32,30 @@ class StandaloneMongodb extends BaseModel
]); ]);
}); });
static::deleting(function ($database) { static::deleting(function ($database) {
$database->scheduledBackups()->delete();
$storages = $database->persistentStorages()->get(); $storages = $database->persistentStorages()->get();
foreach ($storages as $storage) { foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $database->destination->server, false); instant_remote_process(["docker volume rm -f $storage->name"], $database->destination->server, false);
} }
$database->scheduledBackups()->delete();
$database->persistentStorages()->delete(); $database->persistentStorages()->delete();
$database->environment_variables()->delete(); $database->environment_variables()->delete();
}); });
} }
public function mongoInitdbRootPassword(): Attribute
{
return Attribute::make(
get: function ($value) {
try {
return decrypt($value);
} catch (\Throwable $th) {
$this->mongo_initdb_root_password = encrypt($value);
$this->save();
return $value;
}
}
);
}
public function portsMappings(): Attribute public function portsMappings(): Attribute
{ {
return Attribute::make( return Attribute::make(
@@ -55,8 +77,9 @@ class StandaloneMongodb extends BaseModel
{ {
return 'standalone-mongodb'; return 'standalone-mongodb';
} }
public function getDbUrl() { public function getDbUrl(bool $useInternal = false)
if ($this->is_public) { {
if ($this->is_public && !$useInternal) {
return "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->destination->server->getIp}:{$this->public_port}/?directConnection=true"; return "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->destination->server->getIp}:{$this->public_port}/?directConnection=true";
} else { } else {
return "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->uuid}:27017/?directConnection=true"; return "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->uuid}:27017/?directConnection=true";

View File

@@ -0,0 +1,106 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
class StandaloneMysql extends BaseModel
{
use HasFactory;
protected $guarded = [];
protected $casts = [
'mysql_password' => 'encrypted',
'mysql_root_password' => 'encrypted',
];
protected static function booted()
{
static::created(function ($database) {
LocalPersistentVolume::create([
'name' => 'mysql-data-' . $database->uuid,
'mount_path' => '/var/lib/mysql',
'host_path' => null,
'resource_id' => $database->id,
'resource_type' => $database->getMorphClass(),
'is_readonly' => true
]);
});
static::deleting(function ($database) {
$storages = $database->persistentStorages()->get();
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $database->destination->server, false);
}
$database->scheduledBackups()->delete();
$database->persistentStorages()->delete();
$database->environment_variables()->delete();
});
}
public function type(): string
{
return 'standalone-mysql';
}
public function portsMappings(): Attribute
{
return Attribute::make(
set: fn ($value) => $value === "" ? null : $value,
);
}
public function portsMappingsArray(): Attribute
{
return Attribute::make(
get: fn () => is_null($this->ports_mappings)
? []
: explode(',', $this->ports_mappings),
);
}
public function getDbUrl(bool $useInternal = false): string
{
if ($this->is_public && !$useInternal) {
return "mysql://{$this->mysql_user}:{$this->mysql_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mysql_database}";
} else {
return "mysql://{$this->mysql_user}:{$this->mysql_password}@{$this->uuid}:3306/{$this->mysql_database}";
}
}
public function environment()
{
return $this->belongsTo(Environment::class);
}
public function fileStorages()
{
return $this->morphMany(LocalFileVolume::class, 'resource');
}
public function destination()
{
return $this->morphTo();
}
public function environment_variables(): HasMany
{
return $this->hasMany(EnvironmentVariable::class);
}
public function runtime_environment_variables(): HasMany
{
return $this->hasMany(EnvironmentVariable::class);
}
public function persistentStorages()
{
return $this->morphMany(LocalPersistentVolume::class, 'resource');
}
public function scheduledBackups()
{
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
}

View File

@@ -46,8 +46,6 @@ class StandalonePostgresql extends BaseModel
); );
} }
// Normal Deployments
public function portsMappingsArray(): Attribute public function portsMappingsArray(): Attribute
{ {
return Attribute::make( return Attribute::make(
@@ -62,9 +60,9 @@ class StandalonePostgresql extends BaseModel
{ {
return 'standalone-postgresql'; return 'standalone-postgresql';
} }
public function getDbUrl(): string public function getDbUrl(bool $useInternal = false): string
{ {
if ($this->is_public) { if ($this->is_public && !$useInternal) {
return "postgres://{$this->postgres_user}:{$this->postgres_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->postgres_db}"; return "postgres://{$this->postgres_user}:{$this->postgres_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->postgres_db}";
} else { } else {
return "postgres://{$this->postgres_user}:{$this->postgres_password}@{$this->uuid}:5432/{$this->postgres_db}"; return "postgres://{$this->postgres_user}:{$this->postgres_password}@{$this->uuid}:5432/{$this->postgres_db}";

View File

@@ -39,14 +39,18 @@ class Subscription extends Model
if (!$subscription) { if (!$subscription) {
return null; return null;
} }
$subscriptionPlanId = data_get($subscription,'stripe_plan_id'); $subscriptionPlanId = data_get($subscription, 'stripe_plan_id');
if (!$subscriptionPlanId) { if (!$subscriptionPlanId) {
return null; return null;
} }
$subscriptionInvoicePaid = data_get($subscription, 'stripe_invoice_paid');
if (!$subscriptionInvoicePaid) {
return null;
}
$subscriptionConfigs = collect(config('subscription')); $subscriptionConfigs = collect(config('subscription'));
$stripePlanId = null; $stripePlanId = null;
$subscriptionConfigs->map(function ($value, $key) use ($subscriptionPlanId, &$stripePlanId) { $subscriptionConfigs->map(function ($value, $key) use ($subscriptionPlanId, &$stripePlanId) {
if ($value === $subscriptionPlanId){ if ($value === $subscriptionPlanId) {
$stripePlanId = $key; $stripePlanId = $key;
}; };
})->first(); })->first();

View File

@@ -34,7 +34,9 @@ class EmailChannel
if (isset($recepients)) { if (isset($recepients)) {
$message .= implode(', ', $recepients); $message .= implode(', ', $recepients);
} }
$message .= " with subject: {$mailMessage->subject}"; if (isset($mailMessage)) {
$message .= " with subject: {$mailMessage->subject}";
}
send_internal_notification($message); send_internal_notification($message);
throw $e; throw $e;
} }
@@ -49,8 +51,8 @@ class EmailChannel
} }
return; return;
} }
config()->set('mail.from.address', data_get($notifiable, 'smtp_from_address')); config()->set('mail.from.address', data_get($notifiable, 'smtp_from_address', 'test@example.com'));
config()->set('mail.from.name', data_get($notifiable, 'smtp_from_name')); config()->set('mail.from.name', data_get($notifiable, 'smtp_from_name', 'Test'));
if (data_get($notifiable, 'resend_enabled')) { if (data_get($notifiable, 'resend_enabled')) {
config()->set('mail.default', 'resend'); config()->set('mail.default', 'resend');
config()->set('resend.api_key', data_get($notifiable, 'resend_api_key')); config()->set('resend.api_key', data_get($notifiable, 'resend_api_key'));

View File

@@ -48,7 +48,7 @@ class RouteServiceProvider extends ServiceProvider
if ($request->path() === 'api/health') { if ($request->path() === 'api/health') {
return Limit::perMinute(1000)->by($request->user()?->id ?: $request->ip()); return Limit::perMinute(1000)->by($request->user()?->id ?: $request->ip());
} }
return Limit::perMinute(30)->by($request->user()?->id ?: $request->ip()); return Limit::perMinute(200)->by($request->user()?->id ?: $request->ip());
}); });
RateLimiter::for('5', function (Request $request) { RateLimiter::for('5', function (Request $request) {
return Limit::perMinute(5)->by($request->user()?->id ?: $request->ip()); return Limit::perMinute(5)->by($request->user()?->id ?: $request->ip());

View File

@@ -4,7 +4,7 @@ use App\Jobs\ApplicationDeploymentJob;
use App\Models\Application; use App\Models\Application;
use App\Models\ApplicationDeploymentQueue; use App\Models\ApplicationDeploymentQueue;
function queue_application_deployment(int $application_id, string $deployment_uuid, int | null $pull_request_id = 0, string $commit = 'HEAD', bool $force_rebuild = false, bool $is_webhook = false) function queue_application_deployment(int $application_id, string $deployment_uuid, int | null $pull_request_id = 0, string $commit = 'HEAD', bool $force_rebuild = false, bool $is_webhook = false, bool $restart_only = false)
{ {
$deployment = ApplicationDeploymentQueue::create([ $deployment = ApplicationDeploymentQueue::create([
'application_id' => $application_id, 'application_id' => $application_id,
@@ -12,6 +12,7 @@ function queue_application_deployment(int $application_id, string $deployment_uu
'pull_request_id' => $pull_request_id, 'pull_request_id' => $pull_request_id,
'force_rebuild' => $force_rebuild, 'force_rebuild' => $force_rebuild,
'is_webhook' => $is_webhook, 'is_webhook' => $is_webhook,
'restart_only' => $restart_only,
'commit' => $commit, 'commit' => $commit,
]); ]);
$queued_deployments = ApplicationDeploymentQueue::where('application_id', $application_id)->where('status', 'queued')->get()->sortByDesc('created_at'); $queued_deployments = ApplicationDeploymentQueue::where('application_id', $application_id)->where('status', 'queued')->get()->sortByDesc('created_at');

View File

@@ -1,6 +1,6 @@
<?php <?php
const DATABASE_TYPES = ['postgresql','redis', 'mongodb']; const DATABASE_TYPES = ['postgresql', 'redis', 'mongodb', 'mysql', 'mariadb'];
const VALID_CRON_STRINGS = [ const VALID_CRON_STRINGS = [
'every_minute' => '* * * * *', 'every_minute' => '* * * * *',
'hourly' => '0 * * * *', 'hourly' => '0 * * * *',

View File

@@ -2,7 +2,9 @@
use App\Models\Server; use App\Models\Server;
use App\Models\StandaloneDocker; use App\Models\StandaloneDocker;
use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb; use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql; use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis; use App\Models\StandaloneRedis;
use Visus\Cuid2\Cuid2; use Visus\Cuid2\Cuid2;
@@ -58,6 +60,36 @@ function create_standalone_mongodb($environment_id, $destination_uuid): Standalo
'destination_type' => $destination->getMorphClass(), 'destination_type' => $destination->getMorphClass(),
]); ]);
} }
function create_standalone_mysql($environment_id, $destination_uuid): StandaloneMysql
{
$destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
if (!$destination) {
throw new Exception('Destination not found');
}
return StandaloneMysql::create([
'name' => generate_database_name('mysql'),
'mysql_root_password' => \Illuminate\Support\Str::password(symbols: false),
'mysql_password' => \Illuminate\Support\Str::password(symbols: false),
'environment_id' => $environment_id,
'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(),
]);
}
function create_standalone_mariadb($environment_id, $destination_uuid): StandaloneMariadb
{
$destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
if (!$destination) {
throw new Exception('Destination not found');
}
return StandaloneMariadb::create([
'name' => generate_database_name('mariadb'),
'mariadb_root_password' => \Illuminate\Support\Str::password(symbols: false),
'mariadb_password' => \Illuminate\Support\Str::password(symbols: false),
'environment_id' => $environment_id,
'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(),
]);
}
/** /**
* Delete file locally on the filesystem. * Delete file locally on the filesystem.

View File

@@ -6,11 +6,14 @@ use App\Models\Server;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Spatie\Url\Url; use Spatie\Url\Url;
use Visus\Cuid2\Cuid2;
function getCurrentApplicationContainerStatus(Server $server, int $id): Collection function getCurrentApplicationContainerStatus(Server $server, int $id, ?int $pullRequestId = null): Collection
{ {
$containers = instant_remote_process(["docker ps -a --filter='label=coolify.applicationId={$id}' --format '{{json .}}' "], $server); if ($pullRequestId) {
$containers = instant_remote_process(["docker ps -a --filter='label=coolify.applicationId={$id}' --filter='label=coolify.pullRequestId={$pullRequestId}' --format '{{json .}}' "], $server);
} else {
$containers = instant_remote_process(["docker ps -a --filter='label=coolify.applicationId={$id}' --format '{{json .}}'"], $server);
}
if (!$containers) { if (!$containers) {
return collect([]); return collect([]);
} }
@@ -77,20 +80,6 @@ function executeInDocker(string $containerId, string $command)
// return "docker exec {$this->deployment_uuid} bash -c '{$command} |& tee -a /proc/1/fd/1; [ \$PIPESTATUS -eq 0 ] || exit \$PIPESTATUS'"; // return "docker exec {$this->deployment_uuid} bash -c '{$command} |& tee -a /proc/1/fd/1; [ \$PIPESTATUS -eq 0 ] || exit \$PIPESTATUS'";
} }
function getApplicationContainerStatus(Application $application)
{
$server = data_get($application, 'destination.server');
$id = $application->id;
if (!$server) {
return 'exited';
}
$containers = getCurrentApplicationContainerStatus($server, $id);
if ($containers->count() > 0) {
$status = data_get($containers[0], 'State', 'exited');
return $status;
}
return 'exited';
}
function getContainerStatus(Server $server, string $container_id, bool $all_data = false, bool $throwError = false) function getContainerStatus(Server $server, string $container_id, bool $all_data = false, bool $throwError = false)
{ {
$container = instant_remote_process(["docker inspect --format '{{json .}}' {$container_id}"], $server, $throwError); $container = instant_remote_process(["docker inspect --format '{{json .}}' {$container_id}"], $server, $throwError);
@@ -212,13 +201,11 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
$onlyPort = $ports[0]; $onlyPort = $ports[0];
} }
$pull_request_id = data_get($preview, 'pull_request_id', 0); $pull_request_id = data_get($preview, 'pull_request_id', 0);
// $container_name = generateApplicationContainerName($application, $pull_request_id); $appUuid = $application->uuid;
$appId = $application->id; if ($pull_request_id !== 0) {
if ($pull_request_id !== 0 && $pull_request_id !== null) { $appUuid = $appUuid . '-pr-' . $pull_request_id;
$appId = $appId . '-pr-' . $pull_request_id;
} }
$labels = collect([]); $labels = collect([]);
$labels = $labels->merge(defaultLabels($appId, $application->uuid, $pull_request_id));
if ($application->fqdn) { if ($application->fqdn) {
if ($pull_request_id !== 0) { if ($pull_request_id !== 0) {
$domains = Str::of(data_get($preview, 'fqdn'))->explode(','); $domains = Str::of(data_get($preview, 'fqdn'))->explode(',');
@@ -226,7 +213,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
$domains = Str::of(data_get($application, 'fqdn'))->explode(','); $domains = Str::of(data_get($application, 'fqdn'))->explode(',');
} }
// Add Traefik labels no matter which proxy is selected // Add Traefik labels no matter which proxy is selected
$labels = $labels->merge(fqdnLabelsForTraefik($application->uuid, $domains, $application->settings->is_force_https_enabled, $onlyPort)); $labels = $labels->merge(fqdnLabelsForTraefik($appUuid, $domains, $application->settings->is_force_https_enabled, $onlyPort));
} }
return $labels->all(); return $labels->all();
} }

View File

@@ -174,8 +174,11 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d
return $formatted; return $formatted;
} }
function refresh_server_connection(PrivateKey $private_key) function refresh_server_connection(?PrivateKey $private_key = null)
{ {
if (is_null($private_key)) {
return;
}
foreach ($private_key->servers as $server) { foreach ($private_key->servers as $server) {
Storage::disk('ssh-mux')->delete($server->muxFilename()); Storage::disk('ssh-mux')->delete($server->muxFilename());
} }

View File

@@ -4,7 +4,9 @@ use App\Models\Application;
use App\Models\InstanceSettings; use App\Models\InstanceSettings;
use App\Models\Server; use App\Models\Server;
use App\Models\Service; use App\Models\Service;
use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb; use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql; use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis; use App\Models\StandaloneRedis;
use App\Models\Team; use App\Models\Team;
@@ -484,5 +486,18 @@ function queryResourcesByUuid(string $uuid)
if ($redis) return $redis; if ($redis) return $redis;
$mongodb = StandaloneMongodb::whereUuid($uuid)->first(); $mongodb = StandaloneMongodb::whereUuid($uuid)->first();
if ($mongodb) return $mongodb; if ($mongodb) return $mongodb;
$mysql = StandaloneMysql::whereUuid($uuid)->first();
if ($mysql) return $mysql;
$mariadb = StandaloneMariadb::whereUuid($uuid)->first();
if ($mariadb) return $mariadb;
return $resource; return $resource;
} }
function generateDeployWebhook($resource) {
$baseUrl = base_url();
$api = Url::fromString($baseUrl) . '/api/v1';
$endpoint = '/deploy';
$uuid = data_get($resource, 'uuid');
$url = $api . $endpoint . "?uuid=$uuid&force=false";
return $url;
}

View File

@@ -148,6 +148,8 @@ function allowedPathsForInvalidAccounts() {
return [ return [
'logout', 'logout',
'verify', 'verify',
'force-password-reset',
'livewire/message/force-password-reset',
'livewire/message/verify-email', 'livewire/message/verify-email',
'livewire/message/help' 'livewire/message/help'
]; ];

View File

@@ -21,6 +21,7 @@
"laravel/ui": "^4.2", "laravel/ui": "^4.2",
"lcobucci/jwt": "^5.0.0", "lcobucci/jwt": "^5.0.0",
"league/flysystem-aws-s3-v3": "^3.0", "league/flysystem-aws-s3-v3": "^3.0",
"league/flysystem-sftp-v3": "^3.0",
"livewire/livewire": "^v2.12.3", "livewire/livewire": "^v2.12.3",
"lorisleiva/laravel-actions": "^2.7", "lorisleiva/laravel-actions": "^2.7",
"masmerise/livewire-toaster": "^1.2", "masmerise/livewire-toaster": "^1.2",

62
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "de2c45be3f03d43430549d963778dc4a", "content-hash": "21ed976753483557403be75318585442",
"packages": [ "packages": [
{ {
"name": "aws/aws-crt-php", "name": "aws/aws-crt-php",
@@ -2938,6 +2938,66 @@
], ],
"time": "2023-08-30T10:23:59+00:00" "time": "2023-08-30T10:23:59+00:00"
}, },
{
"name": "league/flysystem-sftp-v3",
"version": "3.16.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem-sftp-v3.git",
"reference": "1ba682def8e87fd7fa00883629553c0200d2e974"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/flysystem-sftp-v3/zipball/1ba682def8e87fd7fa00883629553c0200d2e974",
"reference": "1ba682def8e87fd7fa00883629553c0200d2e974",
"shasum": ""
},
"require": {
"league/flysystem": "^3.0.14",
"league/mime-type-detection": "^1.0.0",
"php": "^8.0.2",
"phpseclib/phpseclib": "^3.0"
},
"type": "library",
"autoload": {
"psr-4": {
"League\\Flysystem\\PhpseclibV3\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Frank de Jonge",
"email": "info@frankdejonge.nl"
}
],
"description": "SFTP filesystem adapter for Flysystem.",
"keywords": [
"Flysystem",
"file",
"files",
"filesystem",
"sftp"
],
"support": {
"issues": "https://github.com/thephpleague/flysystem-sftp-v3/issues",
"source": "https://github.com/thephpleague/flysystem-sftp-v3/tree/3.16.0"
},
"funding": [
{
"url": "https://ecologi.com/frankdejonge",
"type": "custom"
},
{
"url": "https://github.com/frankdejonge",
"type": "github"
}
],
"time": "2023-08-30T10:25:05+00:00"
},
{ {
"name": "league/mime-type-detection", "name": "league/mime-type-detection",
"version": "1.13.0", "version": "1.13.0",

View File

@@ -3,11 +3,11 @@
return [ return [
// @see https://docs.sentry.io/product/sentry-basics/dsn-explainer/ // @see https://docs.sentry.io/product/sentry-basics/dsn-explainer/
'dsn' => 'https://72f02655749d5d687297b6b9f078b8b9@o1082494.ingest.sentry.io/4505347448045568', 'dsn' => 'https://c35fe90ee56e18b220bb55e8217d4839@o1082494.ingest.sentry.io/4505347448045568',
// The release version of your application // The release version of your application
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')) // Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
'release' => '4.0.0-beta.97', 'release' => '4.0.0-beta.109',
// When left empty or `null` the Laravel environment will be used // When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'), 'environment' => config('app.env'),

View File

@@ -1,3 +1,3 @@
<?php <?php
return '4.0.0-beta.97'; return '4.0.0-beta.109';

View File

@@ -0,0 +1,57 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('standalone_mysqls', function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->string('name');
$table->string('description')->nullable();
$table->text('mysql_root_password');
$table->string('mysql_user')->default('mysql');
$table->text('mysql_password');
$table->string('mysql_database')->default('default');
$table->longText('mysql_conf')->nullable();
$table->string('status')->default('exited');
$table->string('image')->default('mysql:8');
$table->boolean('is_public')->default(false);
$table->integer('public_port')->nullable();
$table->text('ports_mappings')->nullable();
$table->string('limits_memory')->default("0");
$table->string('limits_memory_swap')->default("0");
$table->integer('limits_memory_swappiness')->default(60);
$table->string('limits_memory_reservation')->default("0");
$table->string('limits_cpus')->default("0");
$table->string('limits_cpuset')->nullable()->default("0");
$table->integer('limits_cpu_shares')->default(1024);
$table->timestamp('started_at')->nullable();
$table->morphs('destination');
$table->foreignId('environment_id')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('standalone_mysqls');
}
};

View File

@@ -0,0 +1,57 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('standalone_mariadbs', function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->string('name');
$table->string('description')->nullable();
$table->text('mariadb_root_password');
$table->string('mariadb_user')->default('mariadb');
$table->text('mariadb_password');
$table->string('mariadb_database')->default('default');
$table->longText('mariadb_conf')->nullable();
$table->string('status')->default('exited');
$table->string('image')->default('mariadb:11');
$table->boolean('is_public')->default(false);
$table->integer('public_port')->nullable();
$table->text('ports_mappings')->nullable();
$table->string('limits_memory')->default("0");
$table->string('limits_memory_swap')->default("0");
$table->integer('limits_memory_swappiness')->default(60);
$table->string('limits_memory_reservation')->default("0");
$table->string('limits_cpus')->default("0");
$table->string('limits_cpuset')->nullable()->default("0");
$table->integer('limits_cpu_shares')->default(1024);
$table->timestamp('started_at')->nullable();
$table->morphs('destination');
$table->foreignId('environment_id')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('standalone_mariadbs');
}
};

View File

@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('environment_variables', function (Blueprint $table) {
$table->foreignId('standalone_mysql_id')->nullable();
$table->foreignId('standalone_mariadb_id')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('environment_variables', function (Blueprint $table) {
$table->dropColumn('standalone_mysql_id');
$table->dropColumn('standalone_mariadb_id');
});
}
};

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('environment_variables', function (Blueprint $table) {
$table->boolean('is_shown_once')->default(false);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('environment_variables', function (Blueprint $table) {
$table->dropColumn('is_shown_once');
});
}
};

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('application_deployment_queues', function (Blueprint $table) {
$table->boolean('restart_only')->default(false);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('application_deployment_queues', function (Blueprint $table) {
$table->dropColumn('restart_only');
});
}
};

View File

@@ -26,7 +26,7 @@ class ApplicationSeeder extends Seeder
'environment_id' => 1, 'environment_id' => 1,
'destination_id' => 0, 'destination_id' => 0,
'destination_type' => StandaloneDocker::class, 'destination_type' => StandaloneDocker::class,
'source_id' => 0, 'source_id' => 1,
'source_type' => GithubApp::class 'source_type' => GithubApp::class
]); ]);
Application::create([ Application::create([

View File

@@ -45,7 +45,7 @@ services:
- /data/coolify/_volumes/redis/:/data - /data/coolify/_volumes/redis/:/data
# - coolify-redis-data-dev:/data # - coolify-redis-data-dev:/data
vite: vite:
image: node:19 image: node:20
working_dir: /var/www/html working_dir: /var/www/html
ports: ports:
- "${VITE_PORT:-5173}:${VITE_PORT:-5173}" - "${VITE_PORT:-5173}:${VITE_PORT:-5173}"

View File

@@ -28,3 +28,4 @@ networks:
coolify: coolify:
name: coolify name: coolify
driver: bridge driver: bridge
external: true

View File

@@ -10,7 +10,7 @@ ARG DOCKER_BUILDX_VERSION=0.11.2
# https://github.com/buildpacks/pack/releases # https://github.com/buildpacks/pack/releases
ARG PACK_VERSION=0.31.0 ARG PACK_VERSION=0.31.0
# https://github.com/railwayapp/nixpacks/releases # https://github.com/railwayapp/nixpacks/releases
ARG NIXPACKS_VERSION=1.17.0 ARG NIXPACKS_VERSION=1.18.0
USER root USER root
WORKDIR /artifacts WORKDIR /artifacts

View File

@@ -121,3 +121,6 @@ tr td:first-child {
.subtitle { .subtitle {
@apply pt-2 pb-10; @apply pt-2 pb-10;
} }
.fullscreen {
@apply fixed top-0 left-0 w-full h-full z-[9999] bg-coolgray-100 overflow-y-auto scrollbar pb-4 ;
}

View File

@@ -17,7 +17,7 @@
@if ($application->status !== 'exited') @if ($application->status !== 'exited')
<button title="With rolling update if possible" wire:click='deploy' class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400"> <button title="With rolling update if possible" wire:click='deploy' class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-warning" viewBox="0 0 24 24" stroke-width="2" <svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-orange-400" viewBox="0 0 24 24" stroke-width="2"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path <path
@@ -27,6 +27,15 @@
</svg> </svg>
Redeploy Redeploy
</button> </button>
<button title="Restart without rebuilding" wire:click='restart' class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg class="w-5 h-5 text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
<path d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747"/>
<path d="M20 4v5h-5"/>
</g>
</svg>
Restart
</button>
<button wire:click='stop' class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400"> <button wire:click='stop' class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24" stroke-width="2" <svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24" stroke-width="2"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">

View File

@@ -7,7 +7,11 @@
href="{{ route('project.database.logs', $parameters) }}"> href="{{ route('project.database.logs', $parameters) }}">
<button>Logs</button> <button>Logs</button>
</a> </a>
@if ($database->getMorphClass() === 'App\Models\StandalonePostgresql' || $database->getMorphClass() === 'App\Models\StandaloneMongodb') @if (
$database->getMorphClass() === 'App\Models\StandalonePostgresql' ||
$database->getMorphClass() === 'App\Models\StandaloneMongodb' ||
$database->getMorphClass() === 'App\Models\StandaloneMysql' ||
$database->getMorphClass() === 'App\Models\StandaloneMariadb')
<a class="{{ request()->routeIs('project.database.backups.all') ? 'text-white' : '' }}" <a class="{{ request()->routeIs('project.database.backups.all') ? 'text-white' : '' }}"
href="{{ route('project.database.backups.all', $parameters) }}"> href="{{ route('project.database.backups.all', $parameters) }}">
<button>Backups</button> <button>Backups</button>

View File

@@ -3,7 +3,7 @@
<li class="inline-flex items-center"> <li class="inline-flex items-center">
<a class="text-xs truncate lg:text-sm" <a class="text-xs truncate lg:text-sm"
href="{{ route('project.show', ['project_uuid' => $this->parameters['project_uuid']]) }}"> href="{{ route('project.show', ['project_uuid' => $this->parameters['project_uuid']]) }}">
{{ $resource->environment->project->name }}</a> {{ data_get($resource, 'environment.project.name', 'Undefined Name') }}</a>
</li> </li>
<li> <li>
<div class="flex items-center"> <div class="flex items-center">

View File

@@ -96,8 +96,7 @@
} }
function copyToClipboard(text) { function copyToClipboard(text) {
navigator.clipboard.writeText(text); navigator?.clipboard?.writeText(text) && Livewire.emit('success', 'Copied to clipboard.');
Livewire.emit('success', 'Copied to clipboard.');
} }
Livewire.on('reloadWindow', (timeout) => { Livewire.on('reloadWindow', (timeout) => {

View File

@@ -225,12 +225,12 @@
Could not find Docker Engine on your server. Do you want me to install it for you? Could not find Docker Engine on your server. Do you want me to install it for you?
</x-slot:question> </x-slot:question>
<x-slot:actions> <x-slot:actions>
@if ($dockerInstallationStarted)
<x-forms.button class="justify-center box" wire:click="installDocker" <x-forms.button class="justify-center box" wire:click="installDocker"
onclick="installDocker.showModal()"> onclick="installDocker.showModal()">
Let's do it!</x-forms.button> Let's do it!</x-forms.button>
@if ($dockerInstallationStarted)
<x-forms.button class="justify-center box" wire:click="dockerInstalledOrSkipped"> <x-forms.button class="justify-center box" wire:click="dockerInstalledOrSkipped">
Next</x-forms.button> Validate Server & Continue</x-forms.button>
@endif @endif
</x-slot:actions> </x-slot:actions>
<x-slot:explanation> <x-slot:explanation>
@@ -314,7 +314,7 @@
I will redirect you to the new resource page, where you can create your first resource. I will redirect you to the new resource page, where you can create your first resource.
</x-slot:question> </x-slot:question>
<x-slot:actions> <x-slot:actions>
<div class="justify-center box" wire:click="showNewResource">Let's do <div class="items-center justify-center box" wire:click="showNewResource">Let's do
it!</div> it!</div>
</x-slot:actions> </x-slot:actions>
<x-slot:explanation> <x-slot:explanation>

View File

@@ -1,4 +1,4 @@
<div class="pt-4"> <div class="pt-4" x-data="{ fullscreen: false, alwaysScroll: false, intervalId: null }">
<livewire:project.application.deployment-navbar :application_deployment_queue="$application_deployment_queue" /> <livewire:project.application.deployment-navbar :application_deployment_queue="$application_deployment_queue" />
@if (data_get($application_deployment_queue, 'status') === 'in_progress') @if (data_get($application_deployment_queue, 'status') === 'in_progress')
<div class="flex items-center gap-1 pt-2 ">Deployment is <div class="flex items-center gap-1 pt-2 ">Deployment is
@@ -6,31 +6,94 @@
</div> </div>
<x-loading class="loading-ring" /> <x-loading class="loading-ring" />
</div> </div>
<div class="">Logs will be updated automatically.</div> {{-- <div class="">Logs will be updated automatically.</div> --}}
@else @else
<div class="pt-2 ">Deployment is <span <div class="pt-2 ">Deployment is <span
class="text-warning">{{ Str::headline(data_get($application_deployment_queue, 'status')) }}</span>. class="text-warning">{{ Str::headline(data_get($application_deployment_queue, 'status')) }}</span>.
</div> </div>
@endif @endif
<div @if ($isKeepAliveOn) wire:poll.2000ms="polling" @endif <div id="screen" :class="fullscreen ? 'fullscreen' : ''">
class="scrollbar flex flex-col-reverse w-full overflow-y-auto border border-dotted rounded border-coolgray-400 max-h-[32rem] p-2 px-4 mt-4 text-xs"> <div @if ($isKeepAliveOn) wire:poll.2000ms="polling" @endif
<span class="flex flex-col"> class="relative flex flex-col-reverse w-full p-2 px-4 mt-4 overflow-y-auto scrollbar border-coolgray-400"
@if (decode_remote_command_output($application_deployment_queue)->count() > 0) :class="fullscreen ? '' : 'max-h-[40rem] border border-dotted rounded'">
@foreach (decode_remote_command_output($application_deployment_queue) as $line) <button title="Minimize" x-show="fullscreen" class="fixed top-4 right-4" x-on:click="makeFullscreen"><svg
<div @class([ class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
'font-mono whitespace-pre-line', <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
'text-neutral-400' => $line['type'] == 'stdout', stroke-width="2" d="M6 14h4m0 0v4m0-4l-6 6m14-10h-4m0 0V6m0 4l6-6" />
'text-error' => $line['type'] == 'stderr', </svg></button>
'text-warning' => $line['hidden'], <button title="Go Top" x-show="fullscreen" class="fixed top-4 right-28" x-on:click="goTop"> <svg
])>[{{ $line['timestamp'] }}] @if ($line['hidden']) class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<br>COMMAND: <br>{{ $line['command'] }} <br><br>OUTPUT: <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
@endif{{ $line['output'] }}@if ($line['hidden']) stroke-width="2" d="M12 5v14m4-10l-4-4M8 9l4-4" />
@endif </svg></button>
</div> <button title="Follow Logs" x-show="fullscreen" :class="alwaysScroll ? 'text-warning' : ''"
@endforeach class="fixed top-4 right-16" x-on:click="toggleScroll"><svg class="icon" viewBox="0 0 24 24"
@else xmlns="http://www.w3.org/2000/svg">
<span class="font-mono text-neutral-400">No logs yet.</span> <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
@endif stroke-width="2" d="M12 5v14m4-4l-4 4m-4-4l4 4" />
</span> </svg></button>
<button title="Fullscreen" x-show="!fullscreen" class="absolute top-2 right-8"
x-on:click="makeFullscreen"><svg class="fixed icon" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<g fill="none">
<path
d="M24 0v24H0V0h24ZM12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035c-.01-.004-.019-.001-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427c-.002-.01-.009-.017-.017-.018Zm.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093c.012.004.023 0 .029-.008l.004-.014l-.034-.614c-.003-.012-.01-.02-.02-.022Zm-.715.002a.023.023 0 0 0-.027.006l-.006.014l-.034.614c0 .012.007.02.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01l-.184-.092Z" />
<path fill="currentColor"
d="M9.793 12.793a1 1 0 0 1 1.497 1.32l-.083.094L6.414 19H9a1 1 0 0 1 .117 1.993L9 21H4a1 1 0 0 1-.993-.883L3 20v-5a1 1 0 0 1 1.993-.117L5 15v2.586l4.793-4.793ZM20 3a1 1 0 0 1 .993.883L21 4v5a1 1 0 0 1-1.993.117L19 9V6.414l-4.793 4.793a1 1 0 0 1-1.497-1.32l.083-.094L17.586 5H15a1 1 0 0 1-.117-1.993L15 3h5Z" />
</g>
</svg></button>
<div id="logs" class="flex flex-col">
@if (decode_remote_command_output($application_deployment_queue)->count() > 0)
@foreach (decode_remote_command_output($application_deployment_queue) as $line)
<div @class([
'font-mono whitespace-pre-line',
'text-neutral-400' => $line['type'] == 'stdout',
'text-error' => $line['type'] == 'stderr',
'text-warning' => $line['hidden'],
])>[{{ $line['timestamp'] }}] @if ($line['hidden'])
<br>COMMAND: <br>{{ $line['command'] }} <br><br>OUTPUT:
@endif{{ $line['output'] }}@if ($line['hidden'])
@endif
</div>
@endforeach
@else
<span class="font-mono text-neutral-400">No logs yet.</span>
@endif
</div>
</div>
</div> </div>
<script>
function makeFullscreen() {
this.fullscreen = !this.fullscreen;
if (this.fullscreen === false) {
this.alwaysScroll = false;
clearInterval(this.intervalId);
}
}
function toggleScroll() {
this.alwaysScroll = !this.alwaysScroll;
if (this.alwaysScroll) {
this.intervalId = setInterval(() => {
const screen = document.getElementById('screen');
const logs = document.getElementById('logs');
if (screen.scrollTop !== logs.scrollHeight) {
screen.scrollTop = logs.scrollHeight;
}
}, 100);
} else {
clearInterval(this.intervalId);
this.intervalId = null;
}
}
function goTop() {
this.alwaysScroll = false;
clearInterval(this.intervalId);
const screen = document.getElementById('screen');
screen.scrollTop = 0;
}
</script>
</div> </div>

Some files were not shown because too many files have changed in this diff Show More