mirror of
https://github.com/ershisan99/coolify.git
synced 2025-12-30 05:09:26 +00:00
Compare commits
191 Commits
v4.0.0-bet
...
v4.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29ad2144b7 | ||
|
|
38d367e709 | ||
|
|
0e81ff970f | ||
|
|
00feef40a3 | ||
|
|
dfba593072 | ||
|
|
c770c8d988 | ||
|
|
0f071031a9 | ||
|
|
99efa857f4 | ||
|
|
80035395ff | ||
|
|
d3490e1c95 | ||
|
|
dab13c92eb | ||
|
|
1f18542960 | ||
|
|
8f21ea9367 | ||
|
|
73e64d9052 | ||
|
|
6cdd87da41 | ||
|
|
2a5d49f9b3 | ||
|
|
cc7ba9eb9f | ||
|
|
c4cc42c8d5 | ||
|
|
2d0838b112 | ||
|
|
07b94a8e48 | ||
|
|
4b08abc144 | ||
|
|
93e4fc2f32 | ||
|
|
6dd86eec30 | ||
|
|
a7ab5d55d3 | ||
|
|
689547463c | ||
|
|
8a0046c571 | ||
|
|
fb3991321a | ||
|
|
ca6543a919 | ||
|
|
364a6aa3a2 | ||
|
|
82b0667277 | ||
|
|
74c126c731 | ||
|
|
d87a0fe74f | ||
|
|
9a858f628d | ||
|
|
fed01fa9d2 | ||
|
|
e1468da36a | ||
|
|
ddfc1440cd | ||
|
|
5fc46384e6 | ||
|
|
48d9df1e43 | ||
|
|
6b62d91f82 | ||
|
|
e6ca8cd167 | ||
|
|
059748ad3b | ||
|
|
a334f998a2 | ||
|
|
53a5ccef31 | ||
|
|
9eea73cefb | ||
|
|
b210e1f243 | ||
|
|
ef3202101c | ||
|
|
22d5159d16 | ||
|
|
1cbd30bd9e | ||
|
|
ad54358de7 | ||
|
|
3689b58b92 | ||
|
|
5a4180a750 | ||
|
|
047922b13a | ||
|
|
798d747164 | ||
|
|
29676ffb22 | ||
|
|
8a50b063d4 | ||
|
|
3d2444ab2e | ||
|
|
7c395edab4 | ||
|
|
7e7f322e21 | ||
|
|
9350fb4b97 | ||
|
|
59c3cc6ce1 | ||
|
|
d7001937ac | ||
|
|
3fe58ec66b | ||
|
|
ed12f73483 | ||
|
|
6914280fb1 | ||
|
|
3dd5546369 | ||
|
|
576bff1af9 | ||
|
|
3c4243d854 | ||
|
|
23d121d67a | ||
|
|
548304765c | ||
|
|
037ba3ff79 | ||
|
|
48b4c17391 | ||
|
|
6acc0e6025 | ||
|
|
43d7f746e4 | ||
|
|
146fee14e5 | ||
|
|
bde7fb2acb | ||
|
|
08a729dc7b | ||
|
|
7554de5993 | ||
|
|
3d7295fec3 | ||
|
|
fd814abd8a | ||
|
|
4c38a59995 | ||
|
|
642a6e3203 | ||
|
|
9edbc15828 | ||
|
|
43eb2fb00b | ||
|
|
9a899deeb8 | ||
|
|
9e1a7d5d9a | ||
|
|
5bdbab7276 | ||
|
|
13bceb934f | ||
|
|
78b194cb16 | ||
|
|
3616fc8ca9 | ||
|
|
10e307f92b | ||
|
|
01f027ac1b | ||
|
|
dadc7aaf08 | ||
|
|
b96807d34c | ||
|
|
45b736bb01 | ||
|
|
3d873a79a0 | ||
|
|
6869c582ff | ||
|
|
9b9e5e939c | ||
|
|
f626c15ecc | ||
|
|
8df1fe2e60 | ||
|
|
fd2a533057 | ||
|
|
0a6401f990 | ||
|
|
1326fcb345 | ||
|
|
8b8e534598 | ||
|
|
0b518a3b76 | ||
|
|
f357f40fc7 | ||
|
|
93fb14884e | ||
|
|
26ccc4afb4 | ||
|
|
5fda1bb932 | ||
|
|
409ba8a1bb | ||
|
|
49f5240ff8 | ||
|
|
0c3ed3d393 | ||
|
|
6e3dc474f2 | ||
|
|
d3eb87561e | ||
|
|
8b58c8f856 | ||
|
|
8c60ef5bd6 | ||
|
|
1d59383c78 | ||
|
|
60f590454d | ||
|
|
dcb61a553e | ||
|
|
e06e31642f | ||
|
|
9dfce48380 | ||
|
|
8eed87e2f7 | ||
|
|
d56d4eb8fc | ||
|
|
fd32cd04ab | ||
|
|
1d3b7ffd3b | ||
|
|
0b5baf60a5 | ||
|
|
bc31df6fb2 | ||
|
|
818399bc23 | ||
|
|
e7fdff0f69 | ||
|
|
6312c0ba84 | ||
|
|
44efe0b5e1 | ||
|
|
de7d584648 | ||
|
|
b9f12d2586 | ||
|
|
c76e8bb0de | ||
|
|
3b655f8e3f | ||
|
|
2b9df41444 | ||
|
|
628fec6904 | ||
|
|
f36135cbfc | ||
|
|
75fe005055 | ||
|
|
8ff7aeb78b | ||
|
|
f1a9e28d5a | ||
|
|
843cd90ee5 | ||
|
|
1cbfd03912 | ||
|
|
ce60a39dc5 | ||
|
|
f1e4395a83 | ||
|
|
52fd7ad571 | ||
|
|
5f797ec0ae | ||
|
|
21e77bf0c1 | ||
|
|
0686e48e89 | ||
|
|
1cef233db2 | ||
|
|
907e52572c | ||
|
|
795c8abf64 | ||
|
|
cc641d8cba | ||
|
|
d4668ef44a | ||
|
|
e8b539c3bd | ||
|
|
6555f0b50c | ||
|
|
bb05058dda | ||
|
|
9667cd4a7a | ||
|
|
3ae9501814 | ||
|
|
73d0948734 | ||
|
|
c3e2a741ea | ||
|
|
4792146f1d | ||
|
|
09b9305aa3 | ||
|
|
ff7d0d442d | ||
|
|
9a127bdc80 | ||
|
|
919e88afb4 | ||
|
|
1d1ec20cb7 | ||
|
|
5c29ecdf10 | ||
|
|
9e09c449cf | ||
|
|
09bcd693f5 | ||
|
|
0c15e45419 | ||
|
|
31cbf552a2 | ||
|
|
f7853ee174 | ||
|
|
de3a7b6eca | ||
|
|
b56c7c34cb | ||
|
|
49845f3da7 | ||
|
|
987409bae4 | ||
|
|
07d8461f96 | ||
|
|
f255a71434 | ||
|
|
2a2818ac0d | ||
|
|
fd3cdc2c7d | ||
|
|
84c3f832ae | ||
|
|
70c28fceeb | ||
|
|
f2c4f83f5a | ||
|
|
c8dd6f07ac | ||
|
|
561e424a7d | ||
|
|
c46d38907e | ||
|
|
5c334bbac6 | ||
|
|
9628072b0c | ||
|
|
39ecff9f90 | ||
|
|
053aa25d2c | ||
|
|
54d8cb9027 |
@@ -3,6 +3,8 @@
|
||||
namespace App\Actions\Application;
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Notifications\Application\StatusChanged;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class StopApplication
|
||||
@@ -10,10 +12,20 @@ class StopApplication
|
||||
use AsAction;
|
||||
public function handle(Application $application)
|
||||
{
|
||||
$server = $application->destination->server;
|
||||
if ($server->isSwarm()) {
|
||||
instant_remote_process(["docker stack rm {$application->uuid}" ], $server);
|
||||
} else {
|
||||
if ($application->destination->server->isSwarm()) {
|
||||
instant_remote_process(["docker stack rm {$application->uuid}"], $application->destination->server);
|
||||
return;
|
||||
}
|
||||
|
||||
$servers = collect([]);
|
||||
$servers->push($application->destination->server);
|
||||
$application->additional_servers->map(function ($server) use ($servers) {
|
||||
$servers->push($server);
|
||||
});
|
||||
foreach ($servers as $server) {
|
||||
if (!$server->isFunctional()) {
|
||||
return 'Server is not functional';
|
||||
}
|
||||
$containers = getCurrentApplicationContainerStatus($server, $application->id, 0);
|
||||
if ($containers->count() > 0) {
|
||||
foreach ($containers as $container) {
|
||||
@@ -25,20 +37,7 @@ class StopApplication
|
||||
);
|
||||
}
|
||||
}
|
||||
// TODO: make notification for application
|
||||
// $application->environment->project->team->notify(new StatusChanged($application));
|
||||
}
|
||||
// Delete Preview Deployments
|
||||
$previewDeployments = $application->previews;
|
||||
foreach ($previewDeployments as $previewDeployment) {
|
||||
$containers = getCurrentApplicationContainerStatus($server, $application->id, $previewDeployment->pull_request_id);
|
||||
foreach ($containers as $container) {
|
||||
$name = str_replace('/', '', $container['Names']);
|
||||
instant_remote_process(["docker rm -f $name"], $application->destination->server, throwError: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
38
app/Actions/Application/StopApplicationOneServer.php
Normal file
38
app/Actions/Application/StopApplicationOneServer.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Application;
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\Server;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class StopApplicationOneServer
|
||||
{
|
||||
use AsAction;
|
||||
public function handle(Application $application, Server $server)
|
||||
{
|
||||
if ($application->destination->server->isSwarm()) {
|
||||
return;
|
||||
}
|
||||
if (!$server->isFunctional()) {
|
||||
return 'Server is not functional';
|
||||
}
|
||||
try {
|
||||
$containers = getCurrentApplicationContainerStatus($server, $application->id, 0);
|
||||
if ($containers->count() > 0) {
|
||||
foreach ($containers as $container) {
|
||||
$containerName = data_get($container, 'Names');
|
||||
if ($containerName) {
|
||||
instant_remote_process(
|
||||
["docker rm -f {$containerName}"],
|
||||
$server
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
ray($e->getMessage());
|
||||
return $e->getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -106,7 +106,8 @@ class StartMariadb
|
||||
$this->commands[] = "echo 'Pulling {$database->image} image.'";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
||||
$this->commands[] = "echo '{$database->name} started.'";
|
||||
$database_name = addslashes($database->name);
|
||||
$this->commands[] = "echo 'Database started.'";
|
||||
return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged');
|
||||
}
|
||||
|
||||
|
||||
@@ -122,7 +122,8 @@ class StartMongodb
|
||||
$this->commands[] = "echo 'Pulling {$database->image} image.'";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
||||
$this->commands[] = "echo '{$database->name} started.'";
|
||||
$database_name = addslashes($database->name);
|
||||
$this->commands[] = "echo 'Database started.'";
|
||||
return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged');
|
||||
}
|
||||
|
||||
|
||||
@@ -106,7 +106,8 @@ class StartMysql
|
||||
$this->commands[] = "echo 'Pulling {$database->image} image.'";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
||||
$this->commands[] = "echo '{$database->name} started.'";
|
||||
$database_name = addslashes($database->name);
|
||||
$this->commands[] = "echo 'Database started.'";
|
||||
return remote_process($this->commands, $database->destination->server,callEventOnFinish: 'DatabaseStatusChanged');
|
||||
}
|
||||
|
||||
|
||||
@@ -128,7 +128,8 @@ class StartPostgresql
|
||||
$this->commands[] = "echo 'Pulling {$database->image} image.'";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
||||
$this->commands[] = "echo '{$database->name} started.'";
|
||||
$database_name = addslashes($database->name);
|
||||
$this->commands[] = "echo 'Database started.'";
|
||||
return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged');
|
||||
}
|
||||
|
||||
|
||||
@@ -117,7 +117,8 @@ class StartRedis
|
||||
$this->commands[] = "echo 'Pulling {$database->image} image.'";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
||||
$this->commands[] = "echo '{$database->name} started.'";
|
||||
$database_name = addslashes($database->name);
|
||||
$this->commands[] = "echo 'Database started.'";
|
||||
return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged');
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,9 @@ class StopDatabase
|
||||
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb $database)
|
||||
{
|
||||
$server = $database->destination->server;
|
||||
if (!$server->isFunctional()) {
|
||||
return 'Server is not functional';
|
||||
}
|
||||
instant_remote_process(
|
||||
["docker rm -f {$database->uuid}"],
|
||||
$server
|
||||
|
||||
@@ -27,7 +27,7 @@ class StartProxy
|
||||
$server->save();
|
||||
if ($server->isSwarm()) {
|
||||
$commands = $commands->merge([
|
||||
"mkdir -p $proxy_path && cd $proxy_path",
|
||||
"mkdir -p $proxy_path/dynamic && cd $proxy_path",
|
||||
"echo 'Creating required Docker Compose file.'",
|
||||
"echo 'Starting coolify-proxy.'",
|
||||
"cd $proxy_path && docker stack deploy -c docker-compose.yml coolify-proxy",
|
||||
@@ -35,7 +35,7 @@ class StartProxy
|
||||
]);
|
||||
} else {
|
||||
$commands = $commands->merge([
|
||||
"mkdir -p $proxy_path && cd $proxy_path",
|
||||
"mkdir -p $proxy_path/dynamic && cd $proxy_path",
|
||||
"echo 'Creating required Docker Compose file.'",
|
||||
"echo 'Pulling docker image.'",
|
||||
'docker compose pull',
|
||||
|
||||
@@ -43,6 +43,7 @@ class InstallDocker
|
||||
"echo 'Restarting Docker Engine...'",
|
||||
"ls -l /tmp"
|
||||
]);
|
||||
return remote_process($command, $server);
|
||||
} else {
|
||||
if ($supported_os_type->contains('debian')) {
|
||||
$command = $command->merge([
|
||||
@@ -89,7 +90,6 @@ class InstallDocker
|
||||
"echo 'Done!'",
|
||||
]);
|
||||
}
|
||||
|
||||
return remote_process($command, $server);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,7 +198,7 @@ Files:
|
||||
}
|
||||
$restart_command = [
|
||||
"echo 'Stopping old Fluent Bit'",
|
||||
"cd $config_path && docker rm -f coolify-log-drain || true",
|
||||
"cd $config_path && docker compose down --remove-orphans || true",
|
||||
"echo 'Starting Fluent Bit'",
|
||||
"cd $config_path && docker compose up -d --remove-orphans",
|
||||
];
|
||||
|
||||
@@ -25,7 +25,6 @@ class UpdateCoolify
|
||||
CleanupDocker::run($this->server, false);
|
||||
$this->latestVersion = get_latest_version_of_coolify();
|
||||
$this->currentVersion = config('version');
|
||||
ray('latest version:' . $this->latestVersion . " current version: " . $this->currentVersion . ' force: ' . $force);
|
||||
if ($settings->next_channel) {
|
||||
ray('next channel enabled');
|
||||
$this->latestVersion = 'next';
|
||||
@@ -44,7 +43,7 @@ class UpdateCoolify
|
||||
}
|
||||
$this->update();
|
||||
}
|
||||
send_internal_notification('InstanceAutoUpdateJob done to version: ' . $this->latestVersion . ' from version: ' . $this->currentVersion);
|
||||
send_internal_notification("Instance updated from {$this->currentVersion} -> {$this->latestVersion}");
|
||||
} catch (\Throwable $e) {
|
||||
ray('InstanceAutoUpdateJob failed');
|
||||
ray($e->getMessage());
|
||||
|
||||
@@ -10,35 +10,45 @@ class DeleteService
|
||||
use AsAction;
|
||||
public function handle(Service $service)
|
||||
{
|
||||
$server = data_get($service, 'server');
|
||||
if ($server->isFunctional()) {
|
||||
StopService::run($service);
|
||||
}
|
||||
$storagesToDelete = collect([]);
|
||||
try {
|
||||
$server = data_get($service, 'server');
|
||||
if ($server->isFunctional()) {
|
||||
$storagesToDelete = collect([]);
|
||||
|
||||
$service->environment_variables()->delete();
|
||||
$commands = [];
|
||||
foreach ($service->applications()->get() as $application) {
|
||||
$storages = $application->persistentStorages()->get();
|
||||
foreach ($storages as $storage) {
|
||||
$storagesToDelete->push($storage);
|
||||
$service->environment_variables()->delete();
|
||||
$commands = [];
|
||||
foreach ($service->applications()->get() as $application) {
|
||||
$storages = $application->persistentStorages()->get();
|
||||
foreach ($storages as $storage) {
|
||||
$storagesToDelete->push($storage);
|
||||
}
|
||||
}
|
||||
foreach ($service->databases()->get() as $database) {
|
||||
$storages = $database->persistentStorages()->get();
|
||||
foreach ($storages as $storage) {
|
||||
$storagesToDelete->push($storage);
|
||||
}
|
||||
}
|
||||
foreach ($storagesToDelete as $storage) {
|
||||
$commands[] = "docker volume rm -f $storage->name";
|
||||
}
|
||||
$commands[] = "docker rm -f $service->uuid";
|
||||
|
||||
instant_remote_process($commands, $server, false);
|
||||
}
|
||||
$application->forceDelete();
|
||||
}
|
||||
foreach ($service->databases()->get() as $database) {
|
||||
$storages = $database->persistentStorages()->get();
|
||||
foreach ($storages as $storage) {
|
||||
$storagesToDelete->push($storage);
|
||||
} catch (\Exception $e) {
|
||||
throw new \Exception($e->getMessage());
|
||||
} finally {
|
||||
foreach ($service->applications()->get() as $application) {
|
||||
$application->forceDelete();
|
||||
}
|
||||
$database->forceDelete();
|
||||
foreach ($service->databases()->get() as $database) {
|
||||
$database->forceDelete();
|
||||
}
|
||||
foreach ($service->scheduled_tasks as $task) {
|
||||
$task->delete();
|
||||
}
|
||||
$service->tags()->detach();
|
||||
}
|
||||
foreach ($storagesToDelete as $storage) {
|
||||
$commands[] = "docker volume rm -f $storage->name";
|
||||
}
|
||||
$commands[] = "docker rm -f $service->uuid";
|
||||
|
||||
instant_remote_process($commands, $server, false);
|
||||
|
||||
$service->forceDelete();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,11 +13,15 @@ class StartService
|
||||
{
|
||||
ray('Starting service: ' . $service->name);
|
||||
$service->saveComposeConfigs();
|
||||
|
||||
$service_name = addslashes($service->name);
|
||||
$server_name = addslashes($service->server->name);
|
||||
|
||||
$commands[] = "cd " . $service->workdir();
|
||||
$commands[] = "echo 'Saved configuration files to {$service->workdir()}.'";
|
||||
$commands[] = "echo 'Creating Docker network.'";
|
||||
$commands[] = "docker network inspect $service->uuid >/dev/null 2>&1 || docker network create --attachable $service->uuid >/dev/null 2>&1 || true";
|
||||
$commands[] = "echo 'Starting service $service->name on {$service->server->name}.'";
|
||||
$commands[] = "echo Starting service.";
|
||||
$commands[] = "echo 'Pulling images.'";
|
||||
$commands[] = "docker compose pull";
|
||||
$commands[] = "echo 'Starting containers.'";
|
||||
|
||||
@@ -10,20 +10,31 @@ class StopService
|
||||
use AsAction;
|
||||
public function handle(Service $service)
|
||||
{
|
||||
ray('Stopping service: ' . $service->name);
|
||||
$applications = $service->applications()->get();
|
||||
foreach ($applications as $application) {
|
||||
instant_remote_process(["docker rm -f {$application->name}-{$service->uuid}"], $service->server);
|
||||
$application->update(['status' => 'exited']);
|
||||
try {
|
||||
$server = $service->destination->server;
|
||||
if (!$server->isFunctional()) {
|
||||
return 'Server is not functional';
|
||||
}
|
||||
ray('Stopping service: ' . $service->name);
|
||||
$applications = $service->applications()->get();
|
||||
foreach ($applications as $application) {
|
||||
instant_remote_process(["docker rm -f {$application->name}-{$service->uuid}"], $service->server);
|
||||
$application->update(['status' => 'exited']);
|
||||
}
|
||||
$dbs = $service->databases()->get();
|
||||
foreach ($dbs as $db) {
|
||||
instant_remote_process(["docker rm -f {$db->name}-{$service->uuid}"], $service->server);
|
||||
$db->update(['status' => 'exited']);
|
||||
}
|
||||
instant_remote_process(["docker network disconnect {$service->uuid} coolify-proxy 2>/dev/null"], $service->server, false);
|
||||
instant_remote_process(["docker network rm {$service->uuid} 2>/dev/null"], $service->server, false);
|
||||
// TODO: make notification for databases
|
||||
// $service->environment->project->team->notify(new StatusChanged($service));
|
||||
} catch (\Exception $e) {
|
||||
echo $e->getMessage();
|
||||
ray($e->getMessage());
|
||||
return $e->getMessage();
|
||||
}
|
||||
$dbs = $service->databases()->get();
|
||||
foreach ($dbs as $db) {
|
||||
instant_remote_process(["docker rm -f {$db->name}-{$service->uuid}"], $service->server);
|
||||
$db->update(['status' => 'exited']);
|
||||
}
|
||||
instant_remote_process(["docker network disconnect {$service->uuid} coolify-proxy 2>/dev/null"], $service->server, false);
|
||||
instant_remote_process(["docker network rm {$service->uuid} 2>/dev/null"], $service->server, false);
|
||||
// TODO: make notification for databases
|
||||
// $service->environment->project->team->notify(new StatusChanged($service));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
55
app/Actions/Shared/ComplexStatusCheck.php
Normal file
55
app/Actions/Shared/ComplexStatusCheck.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Shared;
|
||||
|
||||
use App\Models\Application;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class ComplexStatusCheck
|
||||
{
|
||||
use AsAction;
|
||||
public function handle(Application $application)
|
||||
{
|
||||
$servers = $application->additional_servers;
|
||||
$servers->push($application->destination->server);
|
||||
foreach ($servers as $server) {
|
||||
$is_main_server = $application->destination->server->id === $server->id;
|
||||
if (!$server->isFunctional()) {
|
||||
if ($is_main_server) {
|
||||
$application->update(['status' => 'exited:unhealthy']);
|
||||
continue;
|
||||
} else {
|
||||
$application->additional_servers()->updateExistingPivot($server->id, ['status' => 'exited:unhealthy']);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
$container = instant_remote_process(["docker container inspect $(docker container ls -q --filter 'label=coolify.applicationId={$application->id}' --filter 'label=coolify.pullRequestId=0') --format '{{json .}}'"], $server, false);
|
||||
$container = format_docker_command_output_to_json($container);
|
||||
if ($container->count() === 1) {
|
||||
$container = $container->first();
|
||||
$containerStatus = data_get($container, 'State.Status');
|
||||
$containerHealth = data_get($container, 'State.Health.Status', 'unhealthy');
|
||||
if ($is_main_server) {
|
||||
$statusFromDb = $application->status;
|
||||
if ($statusFromDb !== $containerStatus) {
|
||||
$application->update(['status' => "$containerStatus:$containerHealth"]);
|
||||
}
|
||||
} else {
|
||||
$additional_server = $application->additional_servers()->wherePivot('server_id', $server->id);
|
||||
$statusFromDb = $additional_server->first()->pivot->status;
|
||||
if ($statusFromDb !== $containerStatus) {
|
||||
$additional_server->updateExistingPivot($server->id, ['status' => "$containerStatus:$containerHealth"]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ($is_main_server) {
|
||||
$application->update(['status' => 'exited:unhealthy']);
|
||||
continue;
|
||||
} else {
|
||||
$application->additional_servers()->updateExistingPivot($server->id, ['status' => 'exited:unhealthy']);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
308
app/Console/Commands/CleanupStuckedResources.php
Normal file
308
app/Console/Commands/CleanupStuckedResources.php
Normal file
@@ -0,0 +1,308 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\ScheduledTask;
|
||||
use App\Models\Service;
|
||||
use App\Models\ServiceApplication;
|
||||
use App\Models\ServiceDatabase;
|
||||
use App\Models\StandaloneMariadb;
|
||||
use App\Models\StandaloneMongodb;
|
||||
use App\Models\StandaloneMysql;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\StandaloneRedis;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class CleanupStuckedResources extends Command
|
||||
{
|
||||
protected $signature = 'cleanup:stucked-resources';
|
||||
protected $description = 'Cleanup Stucked Resources';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
ray('Running cleanup stucked resources.');
|
||||
echo "Running cleanup stucked resources.\n";
|
||||
$this->cleanup_stucked_resources();
|
||||
}
|
||||
private function cleanup_stucked_resources()
|
||||
{
|
||||
|
||||
try {
|
||||
$applications = Application::withTrashed()->whereNotNull('deleted_at')->get();
|
||||
foreach ($applications as $application) {
|
||||
echo "Deleting stuck application: {$application->name}\n";
|
||||
$application->forceDelete();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in cleaning stuck application: {$e->getMessage()}\n";
|
||||
}
|
||||
try {
|
||||
$postgresqls = StandalonePostgresql::withTrashed()->whereNotNull('deleted_at')->get();
|
||||
foreach ($postgresqls as $postgresql) {
|
||||
echo "Deleting stuck postgresql: {$postgresql->name}\n";
|
||||
$postgresql->forceDelete();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in cleaning stuck postgresql: {$e->getMessage()}\n";
|
||||
}
|
||||
try {
|
||||
$redis = StandaloneRedis::withTrashed()->whereNotNull('deleted_at')->get();
|
||||
foreach ($redis as $redis) {
|
||||
echo "Deleting stuck redis: {$redis->name}\n";
|
||||
$redis->forceDelete();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in cleaning stuck redis: {$e->getMessage()}\n";
|
||||
}
|
||||
try {
|
||||
$mongodbs = StandaloneMongodb::withTrashed()->whereNotNull('deleted_at')->get();
|
||||
foreach ($mongodbs as $mongodb) {
|
||||
echo "Deleting stuck mongodb: {$mongodb->name}\n";
|
||||
$mongodb->forceDelete();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in cleaning stuck mongodb: {$e->getMessage()}\n";
|
||||
}
|
||||
try {
|
||||
$mysqls = StandaloneMysql::withTrashed()->whereNotNull('deleted_at')->get();
|
||||
foreach ($mysqls as $mysql) {
|
||||
echo "Deleting stuck mysql: {$mysql->name}\n";
|
||||
$mysql->forceDelete();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in cleaning stuck mysql: {$e->getMessage()}\n";
|
||||
}
|
||||
try {
|
||||
$mariadbs = StandaloneMariadb::withTrashed()->whereNotNull('deleted_at')->get();
|
||||
foreach ($mariadbs as $mariadb) {
|
||||
echo "Deleting stuck mariadb: {$mariadb->name}\n";
|
||||
$mariadb->forceDelete();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in cleaning stuck mariadb: {$e->getMessage()}\n";
|
||||
}
|
||||
try {
|
||||
$services = Service::withTrashed()->whereNotNull('deleted_at')->get();
|
||||
foreach ($services as $service) {
|
||||
echo "Deleting stuck service: {$service->name}\n";
|
||||
$service->forceDelete();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in cleaning stuck service: {$e->getMessage()}\n";
|
||||
}
|
||||
try {
|
||||
$serviceApps = ServiceApplication::withTrashed()->whereNotNull('deleted_at')->get();
|
||||
foreach ($serviceApps as $serviceApp) {
|
||||
echo "Deleting stuck serviceapp: {$serviceApp->name}\n";
|
||||
$serviceApp->forceDelete();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in cleaning stuck serviceapp: {$e->getMessage()}\n";
|
||||
}
|
||||
try {
|
||||
$serviceDbs = ServiceDatabase::withTrashed()->whereNotNull('deleted_at')->get();
|
||||
foreach ($serviceDbs as $serviceDb) {
|
||||
echo "Deleting stuck serviceapp: {$serviceDb->name}\n";
|
||||
$serviceDb->forceDelete();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in cleaning stuck serviceapp: {$e->getMessage()}\n";
|
||||
}
|
||||
try {
|
||||
$scheduled_tasks = ScheduledTask::all();
|
||||
foreach ($scheduled_tasks as $scheduled_task) {
|
||||
if (!$scheduled_task->service && !$scheduled_task->application) {
|
||||
echo "Deleting stuck scheduledtask: {$scheduled_task->name}\n";
|
||||
$scheduled_task->delete();
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in cleaning stuck scheduledtasks: {$e->getMessage()}\n";
|
||||
}
|
||||
|
||||
// Cleanup any resources that are not attached to any environment or destination or server
|
||||
try {
|
||||
$applications = Application::all();
|
||||
foreach ($applications as $application) {
|
||||
if (!data_get($application, 'environment')) {
|
||||
echo 'Application without environment: ' . $application->name . '\n';
|
||||
$application->forceDelete();
|
||||
continue;
|
||||
}
|
||||
if (!$application->destination()) {
|
||||
echo 'Application without destination: ' . $application->name . '\n';
|
||||
$application->forceDelete();
|
||||
continue;
|
||||
}
|
||||
if (!data_get($application, 'destination.server')) {
|
||||
echo 'Application without server: ' . $application->name . '\n';
|
||||
$application->forceDelete();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in application: {$e->getMessage()}\n";
|
||||
}
|
||||
try {
|
||||
$postgresqls = StandalonePostgresql::all()->where('id', '!=', 0);
|
||||
foreach ($postgresqls as $postgresql) {
|
||||
if (!data_get($postgresql, 'environment')) {
|
||||
echo 'Postgresql without environment: ' . $postgresql->name . '\n';
|
||||
$postgresql->forceDelete();
|
||||
continue;
|
||||
}
|
||||
if (!$postgresql->destination()) {
|
||||
echo 'Postgresql without destination: ' . $postgresql->name . '\n';
|
||||
$postgresql->forceDelete();
|
||||
continue;
|
||||
}
|
||||
if (!data_get($postgresql, 'destination.server')) {
|
||||
echo 'Postgresql without server: ' . $postgresql->name . '\n';
|
||||
$postgresql->forceDelete();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in postgresql: {$e->getMessage()}\n";
|
||||
}
|
||||
try {
|
||||
$redis = StandaloneRedis::all();
|
||||
foreach ($redis as $redis) {
|
||||
if (!data_get($redis, 'environment')) {
|
||||
echo 'Redis without environment: ' . $redis->name . '\n';
|
||||
$redis->forceDelete();
|
||||
continue;
|
||||
}
|
||||
if (!$redis->destination()) {
|
||||
echo 'Redis without destination: ' . $redis->name . '\n';
|
||||
$redis->forceDelete();
|
||||
continue;
|
||||
}
|
||||
if (!data_get($redis, 'destination.server')) {
|
||||
echo 'Redis without server: ' . $redis->name . '\n';
|
||||
$redis->forceDelete();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in redis: {$e->getMessage()}\n";
|
||||
}
|
||||
|
||||
try {
|
||||
$mongodbs = StandaloneMongodb::all();
|
||||
foreach ($mongodbs as $mongodb) {
|
||||
if (!data_get($mongodb, 'environment')) {
|
||||
echo 'Mongodb without environment: ' . $mongodb->name . '\n';
|
||||
$mongodb->forceDelete();
|
||||
continue;
|
||||
}
|
||||
if (!$mongodb->destination()) {
|
||||
echo 'Mongodb without destination: ' . $mongodb->name . '\n';
|
||||
$mongodb->forceDelete();
|
||||
continue;
|
||||
}
|
||||
if (!data_get($mongodb, 'destination.server')) {
|
||||
echo 'Mongodb without server: ' . $mongodb->name . '\n';
|
||||
$mongodb->forceDelete();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in mongodb: {$e->getMessage()}\n";
|
||||
}
|
||||
|
||||
try {
|
||||
$mysqls = StandaloneMysql::all();
|
||||
foreach ($mysqls as $mysql) {
|
||||
if (!data_get($mysql, 'environment')) {
|
||||
echo 'Mysql without environment: ' . $mysql->name . '\n';
|
||||
$mysql->forceDelete();
|
||||
continue;
|
||||
}
|
||||
if (!$mysql->destination()) {
|
||||
echo 'Mysql without destination: ' . $mysql->name . '\n';
|
||||
$mysql->forceDelete();
|
||||
continue;
|
||||
}
|
||||
if (!data_get($mysql, 'destination.server')) {
|
||||
echo 'Mysql without server: ' . $mysql->name . '\n';
|
||||
$mysql->forceDelete();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in mysql: {$e->getMessage()}\n";
|
||||
}
|
||||
|
||||
try {
|
||||
$mariadbs = StandaloneMariadb::all();
|
||||
foreach ($mariadbs as $mariadb) {
|
||||
if (!data_get($mariadb, 'environment')) {
|
||||
echo 'Mariadb without environment: ' . $mariadb->name . '\n';
|
||||
$mariadb->forceDelete();
|
||||
continue;
|
||||
}
|
||||
if (!$mariadb->destination()) {
|
||||
echo 'Mariadb without destination: ' . $mariadb->name . '\n';
|
||||
$mariadb->forceDelete();
|
||||
continue;
|
||||
}
|
||||
if (!data_get($mariadb, 'destination.server')) {
|
||||
echo 'Mariadb without server: ' . $mariadb->name . '\n';
|
||||
$mariadb->forceDelete();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in mariadb: {$e->getMessage()}\n";
|
||||
}
|
||||
|
||||
try {
|
||||
$services = Service::all();
|
||||
foreach ($services as $service) {
|
||||
if (!data_get($service, 'environment')) {
|
||||
echo 'Service without environment: ' . $service->name . '\n';
|
||||
$service->forceDelete();
|
||||
continue;
|
||||
}
|
||||
if (!$service->destination()) {
|
||||
echo 'Service without destination: ' . $service->name . '\n';
|
||||
$service->forceDelete();
|
||||
continue;
|
||||
}
|
||||
if (!data_get($service, 'server')) {
|
||||
echo 'Service without server: ' . $service->name . '\n';
|
||||
$service->forceDelete();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in service: {$e->getMessage()}\n";
|
||||
}
|
||||
try {
|
||||
$serviceApplications = ServiceApplication::all();
|
||||
foreach ($serviceApplications as $service) {
|
||||
if (!data_get($service, 'service')) {
|
||||
echo 'ServiceApplication without service: ' . $service->name . '\n';
|
||||
$service->forceDelete();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in serviceApplications: {$e->getMessage()}\n";
|
||||
}
|
||||
try {
|
||||
$serviceDatabases = ServiceDatabase::all();
|
||||
foreach ($serviceDatabases as $service) {
|
||||
if (!data_get($service, 'service')) {
|
||||
echo 'ServiceDatabase without service: ' . $service->name . '\n';
|
||||
$service->forceDelete();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in ServiceDatabases: {$e->getMessage()}\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
26
app/Console/Commands/CleanupUnreachableServers.php
Normal file
26
app/Console/Commands/CleanupUnreachableServers.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Server;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class CleanupUnreachableServers extends Command
|
||||
{
|
||||
protected $signature = 'cleanup:unreachable-servers';
|
||||
protected $description = 'Cleanup Unreachable Servers (3 days)';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
echo "Running unreachable server cleanup...\n";
|
||||
$servers = Server::where('unreachable_count', 3)->where('unreachable_notification_sent', true)->where('updated_at', '<', now()->subDays(3))->get();
|
||||
if ($servers->count() > 0) {
|
||||
foreach ($servers as $server) {
|
||||
echo "Cleanup unreachable server ($server->id) with name $server->name";
|
||||
$server->update([
|
||||
'ip' => '1.2.3.4'
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,57 +4,54 @@ namespace App\Console\Commands;
|
||||
|
||||
use App\Enums\ApplicationDeploymentStatus;
|
||||
use App\Jobs\CleanupHelperContainersJob;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationDeploymentQueue;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use App\Models\ServiceApplication;
|
||||
use App\Models\ServiceDatabase;
|
||||
use App\Models\StandaloneMariadb;
|
||||
use App\Models\StandaloneMongodb;
|
||||
use App\Models\StandaloneMysql;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\StandaloneRedis;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class Init extends Command
|
||||
{
|
||||
protected $signature = 'app:init {--cleanup}';
|
||||
protected $signature = 'app:init {--full-cleanup} {--cleanup-deployments}';
|
||||
protected $description = 'Cleanup instance related stuffs';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$this->alive();
|
||||
$cleanup = $this->option('cleanup');
|
||||
if ($cleanup) {
|
||||
echo "Running cleanups...\n";
|
||||
$this->cleanup_stucked_resources();
|
||||
$full_cleanup = $this->option('full-cleanup');
|
||||
$cleanup_deployments = $this->option('cleanup-deployments');
|
||||
if ($cleanup_deployments) {
|
||||
echo "Running cleanup deployments.\n";
|
||||
$this->cleanup_in_progress_application_deployments();
|
||||
return;
|
||||
}
|
||||
if ($full_cleanup) {
|
||||
// Required for falsely deleted coolify db
|
||||
$this->restore_coolify_db_backup();
|
||||
|
||||
// $this->cleanup_ssh();
|
||||
}
|
||||
$this->cleanup_in_progress_application_deployments();
|
||||
$this->cleanup_stucked_helper_containers();
|
||||
|
||||
try {
|
||||
setup_dynamic_configuration();
|
||||
} catch (\Throwable $e) {
|
||||
echo "Could not setup dynamic configuration: {$e->getMessage()}\n";
|
||||
}
|
||||
|
||||
$settings = InstanceSettings::get();
|
||||
if (!is_null(env('AUTOUPDATE', null))) {
|
||||
if (env('AUTOUPDATE') == true) {
|
||||
$settings->update(['is_auto_update_enabled' => true]);
|
||||
} else {
|
||||
$settings->update(['is_auto_update_enabled' => false]);
|
||||
$this->cleanup_in_progress_application_deployments();
|
||||
$this->cleanup_stucked_helper_containers();
|
||||
$this->call('cleanup:queue');
|
||||
$this->call('cleanup:stucked-resources');
|
||||
try {
|
||||
setup_dynamic_configuration();
|
||||
} catch (\Throwable $e) {
|
||||
echo "Could not setup dynamic configuration: {$e->getMessage()}\n";
|
||||
}
|
||||
|
||||
$settings = InstanceSettings::get();
|
||||
if (!is_null(env('AUTOUPDATE', null))) {
|
||||
if (env('AUTOUPDATE') == true) {
|
||||
$settings->update(['is_auto_update_enabled' => true]);
|
||||
} else {
|
||||
$settings->update(['is_auto_update_enabled' => false]);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
$this->call('cleanup:queue');
|
||||
$this->cleanup_stucked_helper_containers();
|
||||
$this->call('cleanup:stucked-resources');
|
||||
}
|
||||
private function restore_coolify_db_backup()
|
||||
{
|
||||
@@ -128,8 +125,13 @@ class Init extends Command
|
||||
// Cleanup any failed deployments
|
||||
|
||||
try {
|
||||
$halted_deployments = ApplicationDeploymentQueue::where('status', '==', ApplicationDeploymentStatus::IN_PROGRESS)->where('status', '==', ApplicationDeploymentStatus::QUEUED)->get();
|
||||
foreach ($halted_deployments as $deployment) {
|
||||
if (isCloud()) {
|
||||
return;
|
||||
}
|
||||
$queued_inprogress_deployments = ApplicationDeploymentQueue::whereIn('status', [ApplicationDeploymentStatus::IN_PROGRESS->value, ApplicationDeploymentStatus::QUEUED->value])->get();
|
||||
foreach ($queued_inprogress_deployments as $deployment) {
|
||||
ray($deployment->id, $deployment->status);
|
||||
echo "Cleaning up deployment: {$deployment->id}\n";
|
||||
$deployment->status = ApplicationDeploymentStatus::FAILED->value;
|
||||
$deployment->save();
|
||||
}
|
||||
@@ -137,273 +139,4 @@ class Init extends Command
|
||||
echo "Error: {$e->getMessage()}\n";
|
||||
}
|
||||
}
|
||||
private function cleanup_stucked_resources()
|
||||
{
|
||||
|
||||
try {
|
||||
$applications = Application::withTrashed()->whereNotNull('deleted_at')->get();
|
||||
foreach ($applications as $application) {
|
||||
echo "Deleting stuck application: {$application->name}\n";
|
||||
$application->forceDelete();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in cleaning stuck application: {$e->getMessage()}\n";
|
||||
}
|
||||
try {
|
||||
$postgresqls = StandalonePostgresql::withTrashed()->whereNotNull('deleted_at')->get();
|
||||
foreach ($postgresqls as $postgresql) {
|
||||
echo "Deleting stuck postgresql: {$postgresql->name}\n";
|
||||
$postgresql->forceDelete();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in cleaning stuck postgresql: {$e->getMessage()}\n";
|
||||
}
|
||||
try {
|
||||
$redis = StandaloneRedis::withTrashed()->whereNotNull('deleted_at')->get();
|
||||
foreach ($redis as $redis) {
|
||||
echo "Deleting stuck redis: {$redis->name}\n";
|
||||
$redis->forceDelete();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in cleaning stuck redis: {$e->getMessage()}\n";
|
||||
}
|
||||
try {
|
||||
$mongodbs = StandaloneMongodb::withTrashed()->whereNotNull('deleted_at')->get();
|
||||
foreach ($mongodbs as $mongodb) {
|
||||
echo "Deleting stuck mongodb: {$mongodb->name}\n";
|
||||
$mongodb->forceDelete();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in cleaning stuck mongodb: {$e->getMessage()}\n";
|
||||
}
|
||||
try {
|
||||
$mysqls = StandaloneMysql::withTrashed()->whereNotNull('deleted_at')->get();
|
||||
foreach ($mysqls as $mysql) {
|
||||
echo "Deleting stuck mysql: {$mysql->name}\n";
|
||||
$mysql->forceDelete();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in cleaning stuck mysql: {$e->getMessage()}\n";
|
||||
}
|
||||
try {
|
||||
$mariadbs = StandaloneMariadb::withTrashed()->whereNotNull('deleted_at')->get();
|
||||
foreach ($mariadbs as $mariadb) {
|
||||
echo "Deleting stuck mariadb: {$mariadb->name}\n";
|
||||
$mariadb->forceDelete();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in cleaning stuck mariadb: {$e->getMessage()}\n";
|
||||
}
|
||||
try {
|
||||
$services = Service::withTrashed()->whereNotNull('deleted_at')->get();
|
||||
foreach ($services as $service) {
|
||||
echo "Deleting stuck service: {$service->name}\n";
|
||||
$service->forceDelete();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in cleaning stuck service: {$e->getMessage()}\n";
|
||||
}
|
||||
try {
|
||||
$serviceApps = ServiceApplication::withTrashed()->whereNotNull('deleted_at')->get();
|
||||
foreach ($serviceApps as $serviceApp) {
|
||||
echo "Deleting stuck serviceapp: {$serviceApp->name}\n";
|
||||
$serviceApp->forceDelete();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in cleaning stuck serviceapp: {$e->getMessage()}\n";
|
||||
}
|
||||
try {
|
||||
$serviceDbs = ServiceDatabase::withTrashed()->whereNotNull('deleted_at')->get();
|
||||
foreach ($serviceDbs as $serviceDb) {
|
||||
echo "Deleting stuck serviceapp: {$serviceDb->name}\n";
|
||||
$serviceDb->forceDelete();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in cleaning stuck serviceapp: {$e->getMessage()}\n";
|
||||
}
|
||||
|
||||
// Cleanup any resources that are not attached to any environment or destination or server
|
||||
try {
|
||||
$applications = Application::all();
|
||||
foreach ($applications as $application) {
|
||||
if (!data_get($application, 'environment')) {
|
||||
echo 'Application without environment: ' . $application->name . ' soft deleting\n';
|
||||
$application->delete();
|
||||
continue;
|
||||
}
|
||||
if (!$application->destination()) {
|
||||
echo 'Application without destination: ' . $application->name . ' soft deleting\n';
|
||||
$application->delete();
|
||||
continue;
|
||||
}
|
||||
if (!data_get($application, 'destination.server')) {
|
||||
echo 'Application without server: ' . $application->name . ' soft deleting\n';
|
||||
$application->delete();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in application: {$e->getMessage()}\n";
|
||||
}
|
||||
try {
|
||||
$postgresqls = StandalonePostgresql::all()->where('id', '!=', 0);
|
||||
foreach ($postgresqls as $postgresql) {
|
||||
if (!data_get($postgresql, 'environment')) {
|
||||
echo 'Postgresql without environment: ' . $postgresql->name . ' soft deleting\n';
|
||||
$postgresql->delete();
|
||||
continue;
|
||||
}
|
||||
if (!$postgresql->destination()) {
|
||||
echo 'Postgresql without destination: ' . $postgresql->name . ' soft deleting\n';
|
||||
$postgresql->delete();
|
||||
continue;
|
||||
}
|
||||
if (!data_get($postgresql, 'destination.server')) {
|
||||
echo 'Postgresql without server: ' . $postgresql->name . ' soft deleting\n';
|
||||
$postgresql->delete();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in postgresql: {$e->getMessage()}\n";
|
||||
}
|
||||
try {
|
||||
$redis = StandaloneRedis::all();
|
||||
foreach ($redis as $redis) {
|
||||
if (!data_get($redis, 'environment')) {
|
||||
echo 'Redis without environment: ' . $redis->name . ' soft deleting\n';
|
||||
$redis->delete();
|
||||
continue;
|
||||
}
|
||||
if (!$redis->destination()) {
|
||||
echo 'Redis without destination: ' . $redis->name . ' soft deleting\n';
|
||||
$redis->delete();
|
||||
continue;
|
||||
}
|
||||
if (!data_get($redis, 'destination.server')) {
|
||||
echo 'Redis without server: ' . $redis->name . ' soft deleting\n';
|
||||
$redis->delete();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in redis: {$e->getMessage()}\n";
|
||||
}
|
||||
|
||||
try {
|
||||
$mongodbs = StandaloneMongodb::all();
|
||||
foreach ($mongodbs as $mongodb) {
|
||||
if (!data_get($mongodb, 'environment')) {
|
||||
echo 'Mongodb without environment: ' . $mongodb->name . ' soft deleting\n';
|
||||
$mongodb->delete();
|
||||
continue;
|
||||
}
|
||||
if (!$mongodb->destination()) {
|
||||
echo 'Mongodb without destination: ' . $mongodb->name . ' soft deleting\n';
|
||||
$mongodb->delete();
|
||||
continue;
|
||||
}
|
||||
if (!data_get($mongodb, 'destination.server')) {
|
||||
echo 'Mongodb without server: ' . $mongodb->name . ' soft deleting\n';
|
||||
$mongodb->delete();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in mongodb: {$e->getMessage()}\n";
|
||||
}
|
||||
|
||||
try {
|
||||
$mysqls = StandaloneMysql::all();
|
||||
foreach ($mysqls as $mysql) {
|
||||
if (!data_get($mysql, 'environment')) {
|
||||
echo 'Mysql without environment: ' . $mysql->name . ' soft deleting\n';
|
||||
$mysql->delete();
|
||||
continue;
|
||||
}
|
||||
if (!$mysql->destination()) {
|
||||
echo 'Mysql without destination: ' . $mysql->name . ' soft deleting\n';
|
||||
$mysql->delete();
|
||||
continue;
|
||||
}
|
||||
if (!data_get($mysql, 'destination.server')) {
|
||||
echo 'Mysql without server: ' . $mysql->name . ' soft deleting\n';
|
||||
$mysql->delete();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in mysql: {$e->getMessage()}\n";
|
||||
}
|
||||
|
||||
try {
|
||||
$mariadbs = StandaloneMariadb::all();
|
||||
foreach ($mariadbs as $mariadb) {
|
||||
if (!data_get($mariadb, 'environment')) {
|
||||
echo 'Mariadb without environment: ' . $mariadb->name . ' soft deleting\n';
|
||||
$mariadb->delete();
|
||||
continue;
|
||||
}
|
||||
if (!$mariadb->destination()) {
|
||||
echo 'Mariadb without destination: ' . $mariadb->name . ' soft deleting\n';
|
||||
$mariadb->delete();
|
||||
continue;
|
||||
}
|
||||
if (!data_get($mariadb, 'destination.server')) {
|
||||
echo 'Mariadb without server: ' . $mariadb->name . ' soft deleting\n';
|
||||
$mariadb->delete();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in mariadb: {$e->getMessage()}\n";
|
||||
}
|
||||
|
||||
try {
|
||||
$services = Service::all();
|
||||
foreach ($services as $service) {
|
||||
if (!data_get($service, 'environment')) {
|
||||
echo 'Service without environment: ' . $service->name . ' soft deleting\n';
|
||||
$service->delete();
|
||||
continue;
|
||||
}
|
||||
if (!$service->destination()) {
|
||||
echo 'Service without destination: ' . $service->name . ' soft deleting\n';
|
||||
$service->delete();
|
||||
continue;
|
||||
}
|
||||
if (!data_get($service, 'server')) {
|
||||
echo 'Service without server: ' . $service->name . ' soft deleting\n';
|
||||
$service->delete();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in service: {$e->getMessage()}\n";
|
||||
}
|
||||
try {
|
||||
$serviceApplications = ServiceApplication::all();
|
||||
foreach ($serviceApplications as $service) {
|
||||
if (!data_get($service, 'service')) {
|
||||
echo 'ServiceApplication without service: ' . $service->name . ' soft deleting\n';
|
||||
$service->delete();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in serviceApplications: {$e->getMessage()}\n";
|
||||
}
|
||||
try {
|
||||
$serviceDatabases = ServiceDatabase::all();
|
||||
foreach ($serviceDatabases as $service) {
|
||||
if (!data_get($service, 'service')) {
|
||||
echo 'ServiceDatabase without service: ' . $service->name . ' soft deleting\n';
|
||||
$service->delete();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in ServiceDatabases: {$e->getMessage()}\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ class SyncBunny extends Command
|
||||
|
||||
$versions = "versions.json";
|
||||
|
||||
PendingRequest::macro('storage', function ($fileName) use($that) {
|
||||
PendingRequest::macro('storage', function ($fileName) use ($that) {
|
||||
$headers = [
|
||||
'AccessKey' => env('BUNNY_STORAGE_API_KEY'),
|
||||
'Accept' => 'application/json',
|
||||
@@ -76,23 +76,26 @@ class SyncBunny extends Command
|
||||
}
|
||||
if ($only_template) {
|
||||
$this->info('About to sync service-templates.json to BunnyCDN.');
|
||||
}
|
||||
if ($only_version) {
|
||||
$this->info('About to sync versions.json to BunnyCDN.');
|
||||
}
|
||||
$confirmed = confirm('Are you sure you want to sync?');
|
||||
if (!$confirmed) {
|
||||
return;
|
||||
}
|
||||
if ($only_template) {
|
||||
$confirmed = confirm("Are you sure you want to sync?");
|
||||
if (!$confirmed) {
|
||||
return;
|
||||
}
|
||||
Http::pool(fn (Pool $pool) => [
|
||||
$pool->storage(fileName: "$parent_dir/templates/$service_template")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$service_template"),
|
||||
$pool->purge("$bunny_cdn/$bunny_cdn_path/$service_template"),
|
||||
]);
|
||||
$this->info('Service template uploaded & purged...');
|
||||
return;
|
||||
}
|
||||
if ($only_version) {
|
||||
} else if ($only_version) {
|
||||
$this->info('About to sync versions.json to BunnyCDN.');
|
||||
$file = file_get_contents("$parent_dir/$versions");
|
||||
$json = json_decode($file, true);
|
||||
$actual_version = data_get($json, 'coolify.v4.version');
|
||||
|
||||
$confirmed = confirm("Are you sure you want to sync to {$actual_version}?");
|
||||
if (!$confirmed) {
|
||||
return;
|
||||
}
|
||||
Http::pool(fn (Pool $pool) => [
|
||||
$pool->storage(fileName: "$parent_dir/$versions")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$versions"),
|
||||
$pool->purge("$bunny_cdn/$bunny_cdn_path/$versions"),
|
||||
@@ -101,6 +104,7 @@ class SyncBunny extends Command
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Http::pool(fn (Pool $pool) => [
|
||||
$pool->storage(fileName: "$parent_dir/$compose_file")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$compose_file"),
|
||||
$pool->storage(fileName: "$parent_dir/$compose_file_prod")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$compose_file_prod"),
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Console;
|
||||
|
||||
use App\Jobs\CheckLogDrainContainerJob;
|
||||
use App\Jobs\CleanupInstanceStuffsJob;
|
||||
use App\Jobs\ComplexContainerStatusJob;
|
||||
use App\Jobs\DatabaseBackupJob;
|
||||
use App\Jobs\ScheduledTaskJob;
|
||||
use App\Jobs\InstanceAutoUpdateJob;
|
||||
@@ -36,6 +37,8 @@ class Kernel extends ConsoleKernel
|
||||
} else {
|
||||
// Instance Jobs
|
||||
$schedule->command('horizon:snapshot')->everyFiveMinutes();
|
||||
$schedule->command('cleanup:unreachable-servers')->daily();
|
||||
|
||||
$schedule->job(new CleanupInstanceStuffsJob)->everyTwoMinutes()->onOneServer();
|
||||
// $schedule->job(new CheckResaleLicenseJob)->hourly()->onOneServer();
|
||||
|
||||
@@ -89,7 +92,6 @@ class Kernel extends ConsoleKernel
|
||||
{
|
||||
$scheduled_backups = ScheduledDatabaseBackup::all();
|
||||
if ($scheduled_backups->isEmpty()) {
|
||||
ray('no scheduled backups');
|
||||
return;
|
||||
}
|
||||
foreach ($scheduled_backups as $scheduled_backup) {
|
||||
@@ -115,12 +117,11 @@ class Kernel extends ConsoleKernel
|
||||
{
|
||||
$scheduled_tasks = ScheduledTask::all();
|
||||
if ($scheduled_tasks->isEmpty()) {
|
||||
ray('no scheduled tasks');
|
||||
return;
|
||||
}
|
||||
foreach ($scheduled_tasks as $scheduled_task) {
|
||||
$service = $scheduled_task->service()->get();
|
||||
$application = $scheduled_task->application()->get();
|
||||
$service = $scheduled_task->service;
|
||||
$application = $scheduled_task->application;
|
||||
|
||||
if (!$application && !$service) {
|
||||
ray('application/service attached to scheduled task does not exist');
|
||||
|
||||
@@ -10,4 +10,5 @@ enum ProcessStatus: string
|
||||
case ERROR = 'error';
|
||||
case KILLED = 'killed';
|
||||
case CANCELLED = 'cancelled';
|
||||
case CLOSED = 'closed';
|
||||
}
|
||||
|
||||
162
app/Http/Controllers/Api/Deploy.php
Normal file
162
app/Http/Controllers/Api/Deploy.php
Normal file
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Actions\Database\StartMariadb;
|
||||
use App\Actions\Database\StartMongodb;
|
||||
use App\Actions\Database\StartMysql;
|
||||
use App\Actions\Database\StartPostgresql;
|
||||
use App\Actions\Database\StartRedis;
|
||||
use App\Actions\Service\StartService;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Tag;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Collection;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class Deploy extends Controller
|
||||
{
|
||||
public function deploy(Request $request)
|
||||
{
|
||||
$token = auth()->user()->currentAccessToken();
|
||||
$teamId = data_get($token, 'team_id');
|
||||
$uuids = $request->query->get('uuid');
|
||||
$tags = $request->query->get('tag');
|
||||
$force = $request->query->get('force') ?? false;
|
||||
|
||||
if ($uuids && $tags) {
|
||||
return response()->json(['error' => 'You can only use uuid or tag, not both.', 'docs' => 'https://coolify.io/docs/api/deploy-webhook'], 400);
|
||||
}
|
||||
if (is_null($teamId)) {
|
||||
return response()->json(['error' => 'Invalid token.', 'docs' => 'https://coolify.io/docs/api/authentication'], 400);
|
||||
}
|
||||
if ($tags) {
|
||||
return $this->by_tags($tags, $teamId, $force);
|
||||
} else if ($uuids) {
|
||||
return $this->by_uuids($uuids, $teamId, $force);
|
||||
}
|
||||
return response()->json(['error' => 'You must provide uuid or tag.', 'docs' => 'https://coolify.io/docs/api/deploy-webhook'], 400);
|
||||
}
|
||||
private function by_uuids(string $uuid, int $teamId, bool $force = false)
|
||||
{
|
||||
$uuids = explode(',', $uuid);
|
||||
$uuids = collect(array_filter($uuids));
|
||||
|
||||
if (count($uuids) === 0) {
|
||||
return response()->json(['error' => 'No UUIDs provided.', 'docs' => 'https://coolify.io/docs/api/deploy-webhook'], 400);
|
||||
}
|
||||
$message = collect([]);
|
||||
foreach ($uuids as $uuid) {
|
||||
$resource = getResourceByUuid($uuid, $teamId);
|
||||
if ($resource) {
|
||||
$return_message = $this->deploy_resource($resource, $force);
|
||||
$message = $message->merge($return_message);
|
||||
}
|
||||
}
|
||||
if ($message->count() > 0) {
|
||||
return response()->json(['message' => $message->toArray()], 200);
|
||||
}
|
||||
return response()->json(['error' => "No resources found.", 'docs' => 'https://coolify.io/docs/api/deploy-webhook'], 404);
|
||||
}
|
||||
public function by_tags(string $tags, int $team_id, bool $force = false)
|
||||
{
|
||||
$tags = explode(',', $tags);
|
||||
$tags = collect(array_filter($tags));
|
||||
|
||||
if (count($tags) === 0) {
|
||||
return response()->json(['error' => 'No TAGs provided.', 'docs' => 'https://coolify.io/docs/api/deploy-webhook'], 400);
|
||||
}
|
||||
$message = collect([]);
|
||||
foreach ($tags as $tag) {
|
||||
$found_tag = Tag::where(['name' => $tag, 'team_id' => $team_id])->first();
|
||||
if (!$found_tag) {
|
||||
$message->push("Tag {$tag} not found.");
|
||||
continue;
|
||||
}
|
||||
$applications = $found_tag->applications()->get();
|
||||
$services = $found_tag->services()->get();
|
||||
if ($applications->count() === 0 && $services->count() === 0) {
|
||||
$message->push("No resources found for tag {$tag}.");
|
||||
continue;
|
||||
}
|
||||
foreach ($applications as $resource) {
|
||||
$return_message = $this->deploy_resource($resource, $force);
|
||||
$message = $message->merge($return_message);
|
||||
}
|
||||
foreach ($services as $resource) {
|
||||
$return_message = $this->deploy_resource($resource, $force);
|
||||
$message = $message->merge($return_message);
|
||||
}
|
||||
}
|
||||
if ($message->count() > 0) {
|
||||
return response()->json(['message' => $message->toArray()], 200);
|
||||
}
|
||||
|
||||
return response()->json(['error' => "No resources found.", 'docs' => 'https://coolify.io/docs/api/deploy-webhook'], 404);
|
||||
}
|
||||
public function deploy_resource($resource, bool $force = false): Collection
|
||||
{
|
||||
$message = collect([]);
|
||||
if (gettype($resource) !== 'object') {
|
||||
return $message->push("Resource ($resource) not found.");
|
||||
}
|
||||
$type = $resource?->getMorphClass();
|
||||
if ($type === 'App\Models\Application') {
|
||||
queue_application_deployment(
|
||||
application: $resource,
|
||||
deployment_uuid: new Cuid2(7),
|
||||
force_rebuild: $force,
|
||||
);
|
||||
$message->push("Application {$resource->name} deployment queued.");
|
||||
} else if ($type === 'App\Models\StandalonePostgresql') {
|
||||
if (str($resource->status)->startsWith('running')) {
|
||||
$message->push("Database {$resource->name} already running.");
|
||||
}
|
||||
StartPostgresql::run($resource);
|
||||
$resource->update([
|
||||
'started_at' => now(),
|
||||
]);
|
||||
$message->push("Database {$resource->name} started.");
|
||||
} else if ($type === 'App\Models\StandaloneRedis') {
|
||||
if (str($resource->status)->startsWith('running')) {
|
||||
$message->push("Database {$resource->name} already running.");
|
||||
}
|
||||
StartRedis::run($resource);
|
||||
$resource->update([
|
||||
'started_at' => now(),
|
||||
]);
|
||||
$message->push("Database {$resource->name} started.");
|
||||
} else if ($type === 'App\Models\StandaloneMongodb') {
|
||||
if (str($resource->status)->startsWith('running')) {
|
||||
$message->push("Database {$resource->name} already running.");
|
||||
}
|
||||
StartMongodb::run($resource);
|
||||
$resource->update([
|
||||
'started_at' => now(),
|
||||
]);
|
||||
$message->push("Database {$resource->name} started.");
|
||||
} else if ($type === 'App\Models\StandaloneMysql') {
|
||||
if (str($resource->status)->startsWith('running')) {
|
||||
$message->push("Database {$resource->name} already running.");
|
||||
}
|
||||
StartMysql::run($resource);
|
||||
$resource->update([
|
||||
'started_at' => now(),
|
||||
]);
|
||||
$message->push("Database {$resource->name} started.");
|
||||
} else if ($type === 'App\Models\StandaloneMariadb') {
|
||||
if (str($resource->status)->startsWith('running')) {
|
||||
$message->push("Database {$resource->name} already running.");
|
||||
}
|
||||
StartMariadb::run($resource);
|
||||
$resource->update([
|
||||
'started_at' => now(),
|
||||
]);
|
||||
$message->push("Database {$resource->name} started.");
|
||||
} else if ($type === 'App\Models\Service') {
|
||||
StartService::run($resource);
|
||||
$message->push("Service {$resource->name} started. It could take a while, be patient.");
|
||||
}
|
||||
return $message;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Enums\ApplicationDeploymentStatus;
|
||||
use App\Enums\ProxyTypes;
|
||||
use App\Enums\ProcessStatus;
|
||||
use App\Events\ApplicationStatusChanged;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationDeploymentQueue;
|
||||
@@ -122,7 +122,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
if ($source) {
|
||||
$this->source = $source->getMorphClass()::where('id', $this->application->source->id)->first();
|
||||
}
|
||||
$this->destination = $this->application->destination->getMorphClass()::where('id', $this->application->destination->id)->first();
|
||||
$this->server = Server::find($this->application_deployment_queue->server_id);
|
||||
$this->timeout = $this->server->settings->dynamic_timeout;
|
||||
$this->destination = $this->server->destinations()->where('id', $this->application_deployment_queue->destination_id)->first();
|
||||
$this->server = $this->mainServer = $this->destination->server;
|
||||
$this->serverUser = $this->server->user;
|
||||
$this->basedir = $this->application->generateBaseDir($this->deployment_uuid);
|
||||
@@ -131,6 +133,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
$this->is_debug_enabled = $this->application->settings->is_debug_enabled;
|
||||
|
||||
$this->container_name = generateApplicationContainerName($this->application, $this->pull_request_id);
|
||||
ray('New container name: ', $this->container_name);
|
||||
|
||||
savePrivateKeyToFs($this->server);
|
||||
$this->saved_outputs = collect();
|
||||
|
||||
@@ -158,15 +162,14 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
$this->preview->fqdn = $preview_fqdn;
|
||||
$this->preview->save();
|
||||
}
|
||||
if ($this->application->is_github_based()) {
|
||||
ApplicationPullRequestUpdateJob::dispatch(application: $this->application, preview: $this->preview, deployment_uuid: $this->deployment_uuid, status: ProcessStatus::IN_PROGRESS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$this->application_deployment_queue->update([
|
||||
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
|
||||
]);
|
||||
|
||||
// Generate custom host<->ip mapping
|
||||
$allContainers = instant_remote_process(["docker network inspect {$this->destination->network} -f '{{json .Containers}}' "], $this->server);
|
||||
if (!is_null($allContainers)) {
|
||||
@@ -229,6 +232,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
$this->next(ApplicationDeploymentStatus::FINISHED->value);
|
||||
$this->application->isConfigurationChanged(false);
|
||||
return;
|
||||
} else if ($this->pull_request_id !== 0) {
|
||||
$this->deploy_pull_request();
|
||||
} else if ($this->application->dockerfile) {
|
||||
$this->deploy_simple_dockerfile();
|
||||
} else if ($this->application->build_pack === 'dockercompose') {
|
||||
@@ -240,22 +245,32 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
} else if ($this->application->build_pack === 'static') {
|
||||
$this->deploy_static_buildpack();
|
||||
} else {
|
||||
if ($this->pull_request_id !== 0) {
|
||||
$this->deploy_pull_request();
|
||||
} else {
|
||||
$this->deploy_nixpacks_buildpack();
|
||||
}
|
||||
$this->deploy_nixpacks_buildpack();
|
||||
}
|
||||
if ($this->server->isProxyShouldRun()) {
|
||||
dispatch(new ContainerStatusJob($this->server));
|
||||
}
|
||||
// Otherwise built image needs to be pushed before from the build server.
|
||||
if (!$this->use_build_server) {
|
||||
$this->push_to_docker_registry();
|
||||
}
|
||||
// ray($this->use_build_server);
|
||||
// if (!$this->use_build_server) {
|
||||
// if ($this->application->additional_servers->count() > 0) {
|
||||
// $this->push_to_docker_registry(forceFail: true);
|
||||
// } else {
|
||||
// $this->push_to_docker_registry();
|
||||
// }
|
||||
// }
|
||||
$this->next(ApplicationDeploymentStatus::FINISHED->value);
|
||||
if ($this->pull_request_id !== 0) {
|
||||
if ($this->application->is_github_based()) {
|
||||
ApplicationPullRequestUpdateJob::dispatch(application: $this->application, preview: $this->preview, deployment_uuid: $this->deployment_uuid, status: ProcessStatus::FINISHED);
|
||||
}
|
||||
}
|
||||
$this->application->isConfigurationChanged(true);
|
||||
} catch (Exception $e) {
|
||||
if ($this->pull_request_id !== 0 && $this->application->is_github_based()) {
|
||||
ApplicationPullRequestUpdateJob::dispatch(application: $this->application, preview: $this->preview, deployment_uuid: $this->deployment_uuid, status: ProcessStatus::ERROR);
|
||||
}
|
||||
ray($e);
|
||||
$this->fail($e);
|
||||
throw $e;
|
||||
} finally {
|
||||
@@ -281,162 +296,13 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
ApplicationStatusChanged::dispatch(data_get($this->application, 'environment.project.team.id'));
|
||||
}
|
||||
}
|
||||
private function write_deployment_configurations()
|
||||
{
|
||||
if (isset($this->docker_compose_base64)) {
|
||||
if ($this->use_build_server) {
|
||||
$this->server = $this->original_server;
|
||||
}
|
||||
$readme = generate_readme_file($this->application->name, $this->application_deployment_queue->updated_at);
|
||||
$composeFileName = "$this->configuration_dir/docker-compose.yml";
|
||||
if ($this->pull_request_id !== 0) {
|
||||
$composeFileName = "$this->configuration_dir/docker-compose-pr-{$this->pull_request_id}.yml";
|
||||
}
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
"mkdir -p $this->configuration_dir"
|
||||
],
|
||||
[
|
||||
"echo '{$this->docker_compose_base64}' | base64 -d > $composeFileName",
|
||||
],
|
||||
[
|
||||
"echo '{$readme}' > $this->configuration_dir/README.md",
|
||||
]
|
||||
);
|
||||
if ($this->use_build_server) {
|
||||
$this->server = $this->build_server;
|
||||
}
|
||||
}
|
||||
}
|
||||
private function push_to_docker_registry($forceFail = false)
|
||||
{
|
||||
if (
|
||||
$this->application->docker_registry_image_name &&
|
||||
$this->application->build_pack !== 'dockerimage' &&
|
||||
!$this->application->destination->server->isSwarm() &&
|
||||
!$this->restart_only &&
|
||||
!(str($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged())
|
||||
) {
|
||||
try {
|
||||
instant_remote_process(["docker images --format '{{json .}}' {$this->production_image_name}"], $this->server);
|
||||
$this->application_deployment_queue->addLogEntry("----------------------------------------");
|
||||
$this->application_deployment_queue->addLogEntry("Pushing image to docker registry ({$this->production_image_name}).");
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, "docker push {$this->production_image_name}"), 'hidden' => true
|
||||
],
|
||||
);
|
||||
if ($this->application->docker_registry_image_tag) {
|
||||
// Tag image with latest
|
||||
$this->application_deployment_queue->addLogEntry("Tagging and pushing image with latest tag.");
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, "docker tag {$this->production_image_name} {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"), 'ignore_errors' => true, 'hidden' => true
|
||||
],
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, "docker push {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"), 'ignore_errors' => true, 'hidden' => true
|
||||
],
|
||||
);
|
||||
}
|
||||
$this->application_deployment_queue->addLogEntry("Image pushed to docker registry.'");
|
||||
} catch (Exception $e) {
|
||||
$this->application_deployment_queue->addLogEntry("Failed to push image to docker registry. Please check debug logs for more information.'");
|
||||
if ($forceFail) {
|
||||
throw $e;
|
||||
}
|
||||
ray($e);
|
||||
}
|
||||
}
|
||||
}
|
||||
private function generate_image_names()
|
||||
{
|
||||
if ($this->application->dockerfile) {
|
||||
if ($this->application->docker_registry_image_name) {
|
||||
$this->build_image_name = Str::lower("{$this->application->docker_registry_image_name}:build");
|
||||
$this->production_image_name = Str::lower("{$this->application->docker_registry_image_name}:latest");
|
||||
} else {
|
||||
$this->build_image_name = Str::lower("{$this->application->uuid}: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) {
|
||||
if ($this->application->docker_registry_image_name) {
|
||||
$this->build_image_name = Str::lower("{$this->application->docker_registry_image_name}:pr-{$this->pull_request_id}-build");
|
||||
$this->production_image_name = Str::lower("{$this->application->docker_registry_image_name}:pr-{$this->pull_request_id}");
|
||||
} else {
|
||||
$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 {
|
||||
$this->dockerImageTag = str($this->commit)->substr(0, 128);
|
||||
if ($this->application->docker_registry_image_name) {
|
||||
$this->build_image_name = Str::lower("{$this->application->docker_registry_image_name}:{$this->dockerImageTag}-build");
|
||||
$this->production_image_name = Str::lower("{$this->application->docker_registry_image_name}:{$this->dockerImageTag}");
|
||||
} else {
|
||||
$this->build_image_name = Str::lower("{$this->application->uuid}:{$this->dockerImageTag}-build");
|
||||
$this->production_image_name = Str::lower("{$this->application->uuid}:{$this->dockerImageTag}");
|
||||
}
|
||||
}
|
||||
}
|
||||
private function just_restart()
|
||||
{
|
||||
$this->application_deployment_queue->addLogEntry("Starting deployment of {$this->customRepository}:{$this->application->git_branch}.");
|
||||
$this->prepare_builder_image();
|
||||
$this->check_git_if_build_needed();
|
||||
$this->set_base_dir();
|
||||
$this->generate_image_names();
|
||||
$this->check_image_locally_or_remotely();
|
||||
if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty()) {
|
||||
$this->application_deployment_queue->addLogEntry("Image found ({$this->production_image_name}) with the same Git Commit SHA. Restarting container.");
|
||||
$this->create_workdir();
|
||||
$this->generate_compose_file();
|
||||
$this->rolling_update();
|
||||
return;
|
||||
}
|
||||
throw new RuntimeException('Cannot find image anywhere. Please redeploy the application.');
|
||||
}
|
||||
private function check_image_locally_or_remotely()
|
||||
{
|
||||
$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'))->isEmpty() && $this->application->docker_registry_image_name) {
|
||||
$this->execute_remote_command([
|
||||
"docker pull {$this->production_image_name} 2>/dev/null", "ignore_errors" => true, "hidden" => true
|
||||
]);
|
||||
$this->execute_remote_command([
|
||||
"docker images -q {$this->production_image_name} 2>/dev/null", "hidden" => true, "save" => "local_image_found"
|
||||
]);
|
||||
}
|
||||
}
|
||||
private function save_environment_variables()
|
||||
{
|
||||
$envs = collect([]);
|
||||
if ($this->pull_request_id !== 0) {
|
||||
foreach ($this->application->environment_variables_preview as $env) {
|
||||
$envs->push($env->key . '=' . $env->real_value);
|
||||
}
|
||||
} else {
|
||||
foreach ($this->application->environment_variables as $env) {
|
||||
$envs->push($env->key . '=' . $env->real_value);
|
||||
}
|
||||
}
|
||||
$envs_base64 = base64_encode($envs->implode("\n"));
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, "echo '$envs_base64' | base64 -d > $this->workdir/.env")
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
private function deploy_simple_dockerfile()
|
||||
{
|
||||
if ($this->use_build_server) {
|
||||
$this->server = $this->build_server;
|
||||
}
|
||||
$dockerfile_base64 = base64_encode($this->application->dockerfile);
|
||||
$this->application_deployment_queue->addLogEntry("Starting deployment of {$this->application->name}.");
|
||||
$this->application_deployment_queue->addLogEntry("Starting deployment of {$this->application->name} to {$this->server->name}.");
|
||||
$this->prepare_builder_image();
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
@@ -444,19 +310,30 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
],
|
||||
);
|
||||
$this->generate_image_names();
|
||||
if (!$this->force_rebuild) {
|
||||
$this->check_image_locally_or_remotely();
|
||||
if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) {
|
||||
$this->create_workdir();
|
||||
$this->application_deployment_queue->addLogEntry("No configuration changed & image found ({$this->production_image_name}) with the same Git Commit SHA. Build step skipped.");
|
||||
$this->generate_compose_file();
|
||||
$this->push_to_docker_registry();
|
||||
$this->rolling_update();
|
||||
return;
|
||||
}
|
||||
}
|
||||
$this->generate_compose_file();
|
||||
$this->generate_build_env_variables();
|
||||
$this->add_build_env_variables_to_dockerfile();
|
||||
$this->build_image();
|
||||
$this->push_to_docker_registry();
|
||||
$this->rolling_update();
|
||||
}
|
||||
|
||||
private function deploy_dockerimage_buildpack()
|
||||
{
|
||||
$this->dockerImage = $this->application->docker_registry_image_name;
|
||||
$this->dockerImageTag = $this->application->docker_registry_image_tag;
|
||||
ray("echo 'Starting deployment of {$this->dockerImage}:{$this->dockerImageTag}.'");
|
||||
$this->application_deployment_queue->addLogEntry("Starting deployment of {$this->dockerImage}:{$this->dockerImageTag}.");
|
||||
ray("echo 'Starting deployment of {$this->dockerImage}:{$this->dockerImageTag} to {$this->server->name}.'");
|
||||
$this->application_deployment_queue->addLogEntry("Starting deployment of {$this->dockerImage}:{$this->dockerImageTag} to {$this->server->name}.");
|
||||
$this->generate_image_names();
|
||||
$this->prepare_builder_image();
|
||||
$this->generate_compose_file();
|
||||
@@ -474,14 +351,11 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
$this->docker_compose_custom_build_command = $this->application->docker_compose_custom_build_command;
|
||||
}
|
||||
if ($this->pull_request_id === 0) {
|
||||
$this->application_deployment_queue->addLogEntry("Starting deployment of {$this->application->name}.");
|
||||
$this->application_deployment_queue->addLogEntry("Starting deployment of {$this->application->name} to {$this->server->name}.");
|
||||
} else {
|
||||
$this->application_deployment_queue->addLogEntry("Starting pull request (#{$this->pull_request_id}) deployment of {$this->customRepository}:{$this->application->git_branch}.");
|
||||
$this->application_deployment_queue->addLogEntry("Starting pull request (#{$this->pull_request_id}) deployment of {$this->customRepository}:{$this->application->git_branch} to {$this->server->name}.");
|
||||
}
|
||||
$this->server->executeRemoteCommand(
|
||||
commands: $this->application->prepareHelperImage($this->deployment_uuid),
|
||||
loggingModel: $this->application_deployment_queue
|
||||
);
|
||||
$this->prepare_builder_image();
|
||||
$this->check_git_if_build_needed();
|
||||
$this->clone_repository();
|
||||
$this->generate_image_names();
|
||||
@@ -547,30 +421,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
if (data_get($this->application, 'dockerfile_location')) {
|
||||
$this->dockerfile_location = $this->application->dockerfile_location;
|
||||
}
|
||||
$this->application_deployment_queue->addLogEntry("Starting deployment of {$this->customRepository}:{$this->application->git_branch}.");
|
||||
$this->prepare_builder_image();
|
||||
$this->check_git_if_build_needed();
|
||||
$this->clone_repository();
|
||||
$this->set_base_dir();
|
||||
$this->generate_image_names();
|
||||
$this->cleanup_git();
|
||||
$this->generate_compose_file();
|
||||
$this->generate_build_env_variables();
|
||||
$this->add_build_env_variables_to_dockerfile();
|
||||
$this->build_image();
|
||||
// if ($this->application->additional_destinations) {
|
||||
// $this->push_to_docker_registry();
|
||||
// $this->deploy_to_additional_destinations();
|
||||
// } else {
|
||||
$this->rolling_update();
|
||||
// }
|
||||
}
|
||||
private function deploy_nixpacks_buildpack()
|
||||
{
|
||||
if ($this->use_build_server) {
|
||||
$this->server = $this->build_server;
|
||||
}
|
||||
$this->application_deployment_queue->addLogEntry("Starting deployment of {$this->customRepository}:{$this->application->git_branch}.");
|
||||
$this->application_deployment_queue->addLogEntry("Starting deployment of {$this->customRepository}:{$this->application->git_branch} to {$this->server->name}.");
|
||||
$this->prepare_builder_image();
|
||||
$this->check_git_if_build_needed();
|
||||
$this->set_base_dir();
|
||||
@@ -581,6 +432,38 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
$this->create_workdir();
|
||||
$this->application_deployment_queue->addLogEntry("No configuration changed & image found ({$this->production_image_name}) with the same Git Commit SHA. Build step skipped.");
|
||||
$this->generate_compose_file();
|
||||
$this->push_to_docker_registry();
|
||||
$this->rolling_update();
|
||||
return;
|
||||
}
|
||||
}
|
||||
$this->clone_repository();
|
||||
$this->cleanup_git();
|
||||
$this->generate_compose_file();
|
||||
$this->generate_build_env_variables();
|
||||
$this->add_build_env_variables_to_dockerfile();
|
||||
$this->build_image();
|
||||
$this->push_to_docker_registry();
|
||||
$this->rolling_update();
|
||||
}
|
||||
private function deploy_nixpacks_buildpack()
|
||||
{
|
||||
if ($this->use_build_server) {
|
||||
$this->server = $this->build_server;
|
||||
}
|
||||
$this->application_deployment_queue->addLogEntry("Starting deployment of {$this->customRepository}:{$this->application->git_branch} to {$this->server->name}.");
|
||||
$this->prepare_builder_image();
|
||||
$this->check_git_if_build_needed();
|
||||
$this->set_base_dir();
|
||||
$this->generate_image_names();
|
||||
if (!$this->force_rebuild) {
|
||||
$this->check_image_locally_or_remotely();
|
||||
if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) {
|
||||
$this->create_workdir();
|
||||
$this->application_deployment_queue->addLogEntry("No configuration changed & image found ({$this->production_image_name}) with the same Git Commit SHA. Build step skipped.");
|
||||
$this->generate_compose_file();
|
||||
ray('pushing to docker registry');
|
||||
$this->push_to_docker_registry();
|
||||
$this->rolling_update();
|
||||
return;
|
||||
}
|
||||
@@ -593,8 +476,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
$this->generate_nixpacks_confs();
|
||||
$this->generate_compose_file();
|
||||
$this->generate_build_env_variables();
|
||||
// $this->add_build_env_variables_to_dockerfile();
|
||||
$this->build_image();
|
||||
$this->push_to_docker_registry();
|
||||
$this->rolling_update();
|
||||
}
|
||||
private function deploy_static_buildpack()
|
||||
@@ -602,18 +485,205 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
if ($this->use_build_server) {
|
||||
$this->server = $this->build_server;
|
||||
}
|
||||
$this->application_deployment_queue->addLogEntry("Starting deployment of {$this->customRepository}:{$this->application->git_branch}.");
|
||||
$this->application_deployment_queue->addLogEntry("Starting deployment of {$this->customRepository}:{$this->application->git_branch} to {$this->server->name}.");
|
||||
$this->prepare_builder_image();
|
||||
$this->check_git_if_build_needed();
|
||||
$this->set_base_dir();
|
||||
$this->generate_image_names();
|
||||
if (!$this->force_rebuild) {
|
||||
$this->check_image_locally_or_remotely();
|
||||
if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) {
|
||||
$this->create_workdir();
|
||||
$this->application_deployment_queue->addLogEntry("No configuration changed & image found ({$this->production_image_name}) with the same Git Commit SHA. Build step skipped.");
|
||||
$this->generate_compose_file();
|
||||
$this->push_to_docker_registry();
|
||||
$this->rolling_update();
|
||||
return;
|
||||
}
|
||||
}
|
||||
$this->clone_repository();
|
||||
$this->cleanup_git();
|
||||
$this->build_image();
|
||||
$this->generate_compose_file();
|
||||
$this->build_image();
|
||||
$this->push_to_docker_registry();
|
||||
$this->rolling_update();
|
||||
}
|
||||
|
||||
private function write_deployment_configurations()
|
||||
{
|
||||
if (isset($this->docker_compose_base64)) {
|
||||
if ($this->use_build_server) {
|
||||
$this->server = $this->original_server;
|
||||
}
|
||||
$readme = generate_readme_file($this->application->name, $this->application_deployment_queue->updated_at);
|
||||
$composeFileName = "$this->configuration_dir/docker-compose.yml";
|
||||
if ($this->pull_request_id !== 0) {
|
||||
$composeFileName = "$this->configuration_dir/docker-compose-pr-{$this->pull_request_id}.yml";
|
||||
}
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
"mkdir -p $this->configuration_dir"
|
||||
],
|
||||
[
|
||||
"echo '{$this->docker_compose_base64}' | base64 -d > $composeFileName",
|
||||
],
|
||||
[
|
||||
"echo '{$readme}' > $this->configuration_dir/README.md",
|
||||
]
|
||||
);
|
||||
if ($this->use_build_server) {
|
||||
$this->server = $this->build_server;
|
||||
}
|
||||
}
|
||||
}
|
||||
private function push_to_docker_registry()
|
||||
{
|
||||
$forceFail = true;
|
||||
if (str($this->application->docker_registry_image_name)->isEmpty()) {
|
||||
ray('empty docker_registry_image_name');
|
||||
return;
|
||||
}
|
||||
if ($this->restart_only) {
|
||||
ray('restart_only');
|
||||
return;
|
||||
}
|
||||
if ($this->application->build_pack === 'dockerimage') {
|
||||
ray('dockerimage');
|
||||
return;
|
||||
}
|
||||
if ($this->use_build_server) {
|
||||
ray('use_build_server');
|
||||
$forceFail = true;
|
||||
}
|
||||
if ($this->server->isSwarm() && $this->build_pack !== 'dockerimage') {
|
||||
ray('isSwarm');
|
||||
$forceFail = true;
|
||||
}
|
||||
if ($this->application->additional_servers->count() > 0) {
|
||||
ray('additional_servers');
|
||||
$forceFail = true;
|
||||
}
|
||||
if ($this->application->additional_servers()->wherePivot('server_id', $this->server->id)->count() > 0) {
|
||||
ray('this is an additional_servers, no pushy pushy');
|
||||
return;
|
||||
}
|
||||
ray('push_to_docker_registry noww: ' . $this->production_image_name);
|
||||
try {
|
||||
instant_remote_process(["docker images --format '{{json .}}' {$this->production_image_name}"], $this->server);
|
||||
$this->application_deployment_queue->addLogEntry("----------------------------------------");
|
||||
$this->application_deployment_queue->addLogEntry("Pushing image to docker registry ({$this->production_image_name}).");
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, "docker push {$this->production_image_name}"), 'hidden' => true
|
||||
],
|
||||
);
|
||||
if ($this->application->docker_registry_image_tag) {
|
||||
// Tag image with latest
|
||||
$this->application_deployment_queue->addLogEntry("Tagging and pushing image with latest tag.");
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, "docker tag {$this->production_image_name} {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"), 'ignore_errors' => true, 'hidden' => true
|
||||
],
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, "docker push {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"), 'ignore_errors' => true, 'hidden' => true
|
||||
],
|
||||
);
|
||||
}
|
||||
$this->application_deployment_queue->addLogEntry("Image pushed to docker registry.");
|
||||
} catch (Exception $e) {
|
||||
$this->application_deployment_queue->addLogEntry("Failed to push image to docker registry. Please check debug logs for more information.");
|
||||
if ($forceFail) {
|
||||
throw new RuntimeException($e->getMessage(), 69420);
|
||||
}
|
||||
ray($e);
|
||||
}
|
||||
}
|
||||
private function generate_image_names()
|
||||
{
|
||||
if ($this->application->dockerfile) {
|
||||
if ($this->application->docker_registry_image_name) {
|
||||
$this->build_image_name = Str::lower("{$this->application->docker_registry_image_name}:build");
|
||||
$this->production_image_name = Str::lower("{$this->application->docker_registry_image_name}:latest");
|
||||
} else {
|
||||
$this->build_image_name = Str::lower("{$this->application->uuid}: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) {
|
||||
if ($this->application->docker_registry_image_name) {
|
||||
$this->build_image_name = Str::lower("{$this->application->docker_registry_image_name}:pr-{$this->pull_request_id}-build");
|
||||
$this->production_image_name = Str::lower("{$this->application->docker_registry_image_name}:pr-{$this->pull_request_id}");
|
||||
} else {
|
||||
$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 {
|
||||
$this->dockerImageTag = str($this->commit)->substr(0, 128);
|
||||
if ($this->application->docker_registry_image_tag) {
|
||||
$this->dockerImageTag = $this->application->docker_registry_image_tag;
|
||||
}
|
||||
if ($this->application->docker_registry_image_name) {
|
||||
$this->build_image_name = Str::lower("{$this->application->docker_registry_image_name}:{$this->dockerImageTag}-build");
|
||||
$this->production_image_name = Str::lower("{$this->application->docker_registry_image_name}:{$this->dockerImageTag}");
|
||||
} else {
|
||||
$this->build_image_name = Str::lower("{$this->application->uuid}:{$this->dockerImageTag}-build");
|
||||
$this->production_image_name = Str::lower("{$this->application->uuid}:{$this->dockerImageTag}");
|
||||
}
|
||||
}
|
||||
}
|
||||
private function just_restart()
|
||||
{
|
||||
$this->application_deployment_queue->addLogEntry("Starting deployment of {$this->customRepository}:{$this->application->git_branch} to {$this->server->name}.");
|
||||
$this->prepare_builder_image();
|
||||
$this->check_git_if_build_needed();
|
||||
$this->set_base_dir();
|
||||
$this->generate_image_names();
|
||||
$this->check_image_locally_or_remotely();
|
||||
if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty()) {
|
||||
$this->application_deployment_queue->addLogEntry("Image found ({$this->production_image_name}) with the same Git Commit SHA. Restarting container.");
|
||||
$this->create_workdir();
|
||||
$this->generate_compose_file();
|
||||
$this->rolling_update();
|
||||
return;
|
||||
}
|
||||
throw new RuntimeException('Cannot find image anywhere. Please redeploy the application.');
|
||||
}
|
||||
private function check_image_locally_or_remotely()
|
||||
{
|
||||
$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'))->isEmpty() && $this->application->docker_registry_image_name) {
|
||||
$this->execute_remote_command([
|
||||
"docker pull {$this->production_image_name} 2>/dev/null", "ignore_errors" => true, "hidden" => true
|
||||
]);
|
||||
$this->execute_remote_command([
|
||||
"docker images -q {$this->production_image_name} 2>/dev/null", "hidden" => true, "save" => "local_image_found"
|
||||
]);
|
||||
}
|
||||
}
|
||||
private function save_environment_variables()
|
||||
{
|
||||
$envs = collect([]);
|
||||
if ($this->pull_request_id !== 0) {
|
||||
foreach ($this->application->environment_variables_preview as $env) {
|
||||
$envs->push($env->key . '=' . $env->real_value);
|
||||
}
|
||||
} else {
|
||||
foreach ($this->application->environment_variables as $env) {
|
||||
$envs->push($env->key . '=' . $env->real_value);
|
||||
}
|
||||
}
|
||||
$envs_base64 = base64_encode($envs->implode("\n"));
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, "echo '$envs_base64' | base64 -d > $this->workdir/.env")
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
private function framework_based_notification()
|
||||
{
|
||||
// Laravel old env variables
|
||||
@@ -631,9 +701,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
private function rolling_update()
|
||||
{
|
||||
if ($this->server->isSwarm()) {
|
||||
if ($this->build_pack !== 'dockerimage') {
|
||||
$this->push_to_docker_registry(forceFail: true);
|
||||
}
|
||||
$this->application_deployment_queue->addLogEntry("Rolling update started.");
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
@@ -643,13 +710,17 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
$this->application_deployment_queue->addLogEntry("Rolling update completed.");
|
||||
} else {
|
||||
if ($this->use_build_server) {
|
||||
$this->push_to_docker_registry(forceFail: true);
|
||||
$this->write_deployment_configurations();
|
||||
$this->server = $this->original_server;
|
||||
}
|
||||
if (count($this->application->ports_mappings_array) > 0) {
|
||||
if (count($this->application->ports_mappings_array) > 0 || (bool) $this->application->settings->is_consistent_container_name_enabled) {
|
||||
$this->application_deployment_queue->addLogEntry("----------------------------------------");
|
||||
$this->application_deployment_queue->addLogEntry("Application has ports mapped to the host system, rolling update is not supported.");
|
||||
if (count($this->application->ports_mappings_array) > 0) {
|
||||
$this->application_deployment_queue->addLogEntry("Application has ports mapped to the host system, rolling update is not supported.");
|
||||
}
|
||||
if ((bool) $this->application->settings->is_consistent_container_name_enabled) {
|
||||
$this->application_deployment_queue->addLogEntry("Consistent container name feature enabled, rolling update is not supported.");
|
||||
}
|
||||
$this->stop_running_container(force: true);
|
||||
$this->start_by_compose_file();
|
||||
} else {
|
||||
@@ -679,7 +750,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
if ($this->full_healthcheck_url) {
|
||||
$this->application_deployment_queue->addLogEntry("Healthcheck URL (inside the container): {$this->full_healthcheck_url}");
|
||||
}
|
||||
while ($counter < $this->application->health_check_retries) {
|
||||
while ($counter <= $this->application->health_check_retries) {
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
"docker inspect --format='{{json .State.Health.Status}}' {$this->container_name}",
|
||||
@@ -722,10 +793,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
$this->generate_nixpacks_confs();
|
||||
}
|
||||
$this->generate_compose_file();
|
||||
|
||||
// Needs separate preview variables
|
||||
$this->generate_build_env_variables();
|
||||
if ($this->application->build_pack !== 'nixpacks') {
|
||||
if ($this->application->build_pack === 'dockerfile') {
|
||||
$this->add_build_env_variables_to_dockerfile();
|
||||
}
|
||||
$this->build_image();
|
||||
@@ -790,7 +859,18 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
}
|
||||
private function deploy_to_additional_destinations()
|
||||
{
|
||||
$destination_ids = collect(str($this->application->additional_destinations)->explode(','));
|
||||
if ($this->application->additional_networks->count() === 0) {
|
||||
return;
|
||||
}
|
||||
$destination_ids = $this->application->additional_networks->pluck('id');
|
||||
if ($this->server->isSwarm()) {
|
||||
$this->application_deployment_queue->addLogEntry("Additional destinations are not supported in swarm mode.");
|
||||
return;
|
||||
}
|
||||
if ($destination_ids->contains($this->destination->id)) {
|
||||
ray('Same destination found in additional destinations. Skipping.');
|
||||
return;
|
||||
}
|
||||
foreach ($destination_ids as $destination_id) {
|
||||
$destination = StandaloneDocker::find($destination_id);
|
||||
$server = $destination->server;
|
||||
@@ -798,11 +878,21 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
$this->application_deployment_queue->addLogEntry("Skipping deployment to {$server->name}. Not in the same team?!");
|
||||
continue;
|
||||
}
|
||||
$this->server = $server;
|
||||
$this->application_deployment_queue->addLogEntry("Deploying to {$this->server->name}.");
|
||||
$this->prepare_builder_image();
|
||||
$this->generate_image_names();
|
||||
$this->rolling_update();
|
||||
// ray('Deploying to additional destination: ', $server->name);
|
||||
$deployment_uuid = new Cuid2();
|
||||
queue_application_deployment(
|
||||
deployment_uuid: $deployment_uuid,
|
||||
application: $this->application,
|
||||
server: $server,
|
||||
destination: $destination,
|
||||
no_questions_asked: true,
|
||||
);
|
||||
$this->application_deployment_queue->addLogEntry("Deploying to additional server: {$server->name}. Click here to see the deployment status: " . route('project.application.deployment.show', [
|
||||
'project_uuid' => data_get($this->application, 'environment.project.uuid'),
|
||||
'application_uuid' => data_get($this->application, 'uuid'),
|
||||
'deployment_uuid' => $deployment_uuid,
|
||||
'environment_name' => data_get($this->application, 'environment.name'),
|
||||
]));
|
||||
}
|
||||
}
|
||||
private function set_base_dir()
|
||||
@@ -861,7 +951,12 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
|
||||
private function generate_git_import_commands()
|
||||
{
|
||||
['commands' => $commands, 'branch' => $this->branch, 'fullRepoUrl' => $this->fullRepoUrl] = $this->application->generateGitImportCommands($this->deployment_uuid, $this->pull_request_id, $this->git_type);
|
||||
['commands' => $commands, 'branch' => $this->branch, 'fullRepoUrl' => $this->fullRepoUrl] = $this->application->generateGitImportCommands(
|
||||
deployment_uuid: $this->deployment_uuid,
|
||||
pull_request_id: $this->pull_request_id,
|
||||
git_type: $this->git_type,
|
||||
commit: $this->commit
|
||||
);
|
||||
return $commands;
|
||||
}
|
||||
|
||||
@@ -1111,9 +1206,19 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
// ];
|
||||
// }
|
||||
|
||||
$docker_compose['services'][$this->application->uuid] = $docker_compose['services'][$this->container_name];
|
||||
|
||||
data_forget($docker_compose, 'services.' . $this->container_name);
|
||||
if ((bool)$this->application->settings->is_consistent_container_name_enabled) {
|
||||
$custom_compose = convert_docker_run_to_compose($this->application->custom_docker_run_options);
|
||||
if (count($custom_compose) > 0) {
|
||||
$docker_compose['services'][$this->container_name] = array_merge_recursive($docker_compose['services'][$this->container_name], $custom_compose);
|
||||
}
|
||||
} else {
|
||||
$docker_compose['services'][$this->application->uuid] = $docker_compose['services'][$this->container_name];
|
||||
data_forget($docker_compose, 'services.' . $this->container_name);
|
||||
$custom_compose = convert_docker_run_to_compose($this->application->custom_docker_run_options);
|
||||
if (count($custom_compose) > 0) {
|
||||
$docker_compose['services'][$this->application->uuid] = array_merge_recursive($docker_compose['services'][$this->application->uuid], $custom_compose);
|
||||
}
|
||||
}
|
||||
|
||||
$this->docker_compose = Yaml::dump($docker_compose, 10);
|
||||
$this->docker_compose_base64 = base64_encode($this->docker_compose);
|
||||
@@ -1397,6 +1502,11 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
[executeInDocker($this->deployment_uuid, "docker rm -f $containerName >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true],
|
||||
);
|
||||
});
|
||||
if ($this->application->settings->is_consistent_container_name_enabled) {
|
||||
$this->execute_remote_command(
|
||||
[executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true],
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$this->application_deployment_queue->addLogEntry("New container is not healthy, rolling back to the old container.");
|
||||
$this->application_deployment_queue->update([
|
||||
@@ -1489,31 +1599,39 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
|
||||
private function next(string $status)
|
||||
{
|
||||
queue_next_deployment($this->application);
|
||||
// If the deployment is cancelled by the user, don't update the status
|
||||
if ($this->application_deployment_queue->status !== ApplicationDeploymentStatus::CANCELLED_BY_USER->value) {
|
||||
if ($this->application_deployment_queue->status !== ApplicationDeploymentStatus::CANCELLED_BY_USER->value && $this->application_deployment_queue->status !== ApplicationDeploymentStatus::FAILED->value) {
|
||||
$this->application_deployment_queue->update([
|
||||
'status' => $status,
|
||||
]);
|
||||
}
|
||||
queue_next_deployment($this->application);
|
||||
if ($status === ApplicationDeploymentStatus::FINISHED->value) {
|
||||
$this->application->environment->project->team?->notify(new DeploymentSuccess($this->application, $this->deployment_uuid, $this->preview));
|
||||
}
|
||||
if ($status === ApplicationDeploymentStatus::FAILED->value) {
|
||||
$this->application->environment->project->team?->notify(new DeploymentFailed($this->application, $this->deployment_uuid, $this->preview));
|
||||
return;
|
||||
}
|
||||
if ($status === ApplicationDeploymentStatus::FINISHED->value) {
|
||||
$this->deploy_to_additional_destinations();
|
||||
$this->application->environment->project->team?->notify(new DeploymentSuccess($this->application, $this->deployment_uuid, $this->preview));
|
||||
}
|
||||
}
|
||||
|
||||
public function failed(Throwable $exception): void
|
||||
{
|
||||
$this->application_deployment_queue->addLogEntry("Oops something is not okay, are you okay? 😢", 'stderr');
|
||||
$this->application_deployment_queue->addLogEntry($exception->getMessage(), 'stderr');
|
||||
if (str($exception->getMessage())->isNotEmpty()) {
|
||||
$this->application_deployment_queue->addLogEntry($exception->getMessage(), 'stderr');
|
||||
}
|
||||
|
||||
if ($this->application->build_pack !== 'dockercompose') {
|
||||
$this->application_deployment_queue->addLogEntry("Deployment failed. Removing the new version of your application.", 'stderr');
|
||||
$this->execute_remote_command(
|
||||
[executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true]
|
||||
);
|
||||
$code = $exception->getCode();
|
||||
if ($code !== 69420) {
|
||||
// 69420 means failed to push the image to the registry, so we don't need to remove the new version as it is the currently running one
|
||||
$this->application_deployment_queue->addLogEntry("Deployment failed. Removing the new version of your application.", 'stderr');
|
||||
$this->execute_remote_command(
|
||||
[executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$this->next(ApplicationDeploymentStatus::FAILED->value);
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Enums\ApplicationDeploymentStatus;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationDeploymentQueue;
|
||||
use App\Models\Server;
|
||||
use App\Traits\ExecuteRemoteCommand;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
class ApplicationDeploymentNewJob implements ShouldQueue, ShouldBeEncrypted
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, ExecuteRemoteCommand;
|
||||
|
||||
public $timeout = 3600;
|
||||
public $tries = 1;
|
||||
|
||||
public static int $batch_counter = 0;
|
||||
public Server $mainServer;
|
||||
public $servers;
|
||||
public string $basedir;
|
||||
public string $workdir;
|
||||
|
||||
public string $deploymentUuid;
|
||||
public int $pullRequestId = 0;
|
||||
|
||||
// Git related
|
||||
public string $gitImportCommands;
|
||||
public ?string $gitType = null;
|
||||
public string $gitRepository;
|
||||
public string $gitBranch;
|
||||
public int $gitPort;
|
||||
public string $gitFullRepoUrl;
|
||||
|
||||
public function __construct(public ApplicationDeploymentQueue $deployment, public Application $application)
|
||||
{
|
||||
$this->mainServer = data_get($this->application, 'destination.server');
|
||||
$this->deploymentUuid = data_get($this->deployment, 'deployment_uuid');
|
||||
$this->pullRequestId = data_get($this->deployment, 'pull_request_id', 0);
|
||||
$this->gitType = data_get($this->deployment, 'git_type');
|
||||
|
||||
$this->basedir = $this->application->generateBaseDir($this->deploymentUuid);
|
||||
$this->workdir = $this->basedir . rtrim($this->application->base_directory, '/');
|
||||
}
|
||||
public function handle()
|
||||
{
|
||||
try {
|
||||
ray()->clearAll();
|
||||
$this->deployment->setStatus(ApplicationDeploymentStatus::IN_PROGRESS->value);
|
||||
|
||||
$hostIpMappings = $this->mainServer->getHostIPMappings($this->application->destination->network);
|
||||
if ($this->application->dockerfile_target_build) {
|
||||
$buildTarget = " --target {$this->application->dockerfile_target_build} ";
|
||||
}
|
||||
|
||||
// Get the git repository and port (custom port or default port)
|
||||
[
|
||||
'repository' => $this->gitRepository,
|
||||
'port' => $this->gitPort
|
||||
] = $this->application->customRepository();
|
||||
|
||||
// Get the git branch and git import commands
|
||||
[
|
||||
'commands' => $this->gitImportCommands,
|
||||
'branch' => $this->gitBranch,
|
||||
'fullRepoUrl' => $this->gitFullRepoUrl
|
||||
] = $this->application->generateGitImportCommands($this->deploymentUuid, $this->pullRequestId, $this->gitType);
|
||||
|
||||
$this->servers = $this->application->servers();
|
||||
|
||||
if ($this->deployment->restart_only) {
|
||||
if ($this->application->build_pack === 'dockerimage') {
|
||||
throw new \Exception('Restart only is not supported for docker image based deployments');
|
||||
}
|
||||
$this->deployment->addLogEntry("Starting deployment of {$this->application->name}.");
|
||||
$this->servers->each(function ($server) {
|
||||
$this->deployment->addLogEntry("Restarting {$this->application->name} on {$server->name}.");
|
||||
$this->restartOnly($server);
|
||||
});
|
||||
}
|
||||
$this->next(ApplicationDeploymentStatus::FINISHED->value);
|
||||
} catch (Throwable $exception) {
|
||||
$this->fail($exception);
|
||||
} finally {
|
||||
$this->servers->each(function ($server) {
|
||||
$this->deployment->addLogEntry("Cleaning up temporary containers on {$server->name}.");
|
||||
$server->executeRemoteCommand(
|
||||
commands: collect([])->push([
|
||||
"command" => "docker rm -f {$this->deploymentUuid}",
|
||||
"hidden" => true,
|
||||
"ignoreErrors" => true,
|
||||
]),
|
||||
loggingModel: $this->deployment
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
public function restartOnly(Server $server)
|
||||
{
|
||||
$server->executeRemoteCommand(
|
||||
commands: $this->application->prepareHelperImage($this->deploymentUuid),
|
||||
loggingModel: $this->deployment
|
||||
);
|
||||
|
||||
$privateKey = data_get($this->application, 'private_key.private_key', null);
|
||||
$gitLsRemoteCommand = collect([]);
|
||||
if ($privateKey) {
|
||||
$privateKey = base64_decode($privateKey);
|
||||
$gitLsRemoteCommand
|
||||
->push([
|
||||
"command" => executeInDocker($this->deploymentUuid, "mkdir -p /root/.ssh")
|
||||
])
|
||||
->push([
|
||||
"command" => executeInDocker($this->deploymentUuid, "echo '{$privateKey}' | base64 -d > /root/.ssh/id_rsa")
|
||||
])
|
||||
->push([
|
||||
"command" => executeInDocker($this->deploymentUuid, "chmod 600 /root/.ssh/id_rsa")
|
||||
])
|
||||
->push([
|
||||
"name" => "git_commit_sha",
|
||||
"command" => executeInDocker($this->deploymentUuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->gitPort} -o Port={$this->gitPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git ls-remote {$this->gitFullRepoUrl} {$this->gitBranch}"),
|
||||
"hidden" => true,
|
||||
]);
|
||||
} else {
|
||||
$gitLsRemoteCommand->push([
|
||||
"name" => "git_commit_sha",
|
||||
"command" => executeInDocker($this->deploymentUuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->gitPort} -o Port={$this->gitPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git ls-remote {$this->gitFullRepoUrl} {$this->gitBranch}"),
|
||||
"hidden" => true,
|
||||
]);
|
||||
}
|
||||
$this->deployment->addLogEntry("Checking if there is any new commit on {$this->gitBranch} branch.");
|
||||
|
||||
$server->executeRemoteCommand(
|
||||
commands: $gitLsRemoteCommand,
|
||||
loggingModel: $this->deployment
|
||||
);
|
||||
$commit = str($this->deployment->getOutput('git_commit_sha'))->before("\t");
|
||||
|
||||
[
|
||||
'productionImageName' => $productionImageName
|
||||
] = $this->application->generateImageNames($commit, $this->pullRequestId);
|
||||
|
||||
$this->deployment->addLogEntry("Checking if the image {$productionImageName} already exists.");
|
||||
$server->checkIfDockerImageExists($productionImageName, $this->deployment);
|
||||
|
||||
if (str($this->deployment->getOutput('local_image_found'))->isNotEmpty()) {
|
||||
$this->deployment->addLogEntry("Image {$productionImageName} already exists. Skipping the build.");
|
||||
|
||||
$server->createWorkDirForDeployment($this->workdir, $this->deployment);
|
||||
|
||||
$this->application->generateDockerComposeFile($server, $this->deployment, $this->workdir);
|
||||
$this->application->rollingUpdateApplication($server, $this->deployment, $this->workdir);
|
||||
return;
|
||||
}
|
||||
throw new RuntimeException('Cannot find image anywhere. Please redeploy the application.');
|
||||
}
|
||||
public function failed(Throwable $exception): void
|
||||
{
|
||||
ray($exception);
|
||||
$this->next(ApplicationDeploymentStatus::FAILED->value);
|
||||
}
|
||||
private function next(string $status)
|
||||
{
|
||||
// If the deployment is cancelled by the user, don't update the status
|
||||
if ($this->deployment->status !== ApplicationDeploymentStatus::CANCELLED_BY_USER->value) {
|
||||
$this->deployment->update([
|
||||
'status' => $status,
|
||||
]);
|
||||
}
|
||||
queue_next_deployment($this->application, isNew: true);
|
||||
}
|
||||
}
|
||||
@@ -17,38 +17,34 @@ class ApplicationPullRequestUpdateJob implements ShouldQueue, ShouldBeEncrypted
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public string $build_logs_url;
|
||||
public Application $application;
|
||||
public ApplicationPreview $preview;
|
||||
public string $body;
|
||||
|
||||
public function __construct(
|
||||
public string $application_id,
|
||||
public int $pull_request_id,
|
||||
public string $deployment_uuid,
|
||||
public string $status
|
||||
public Application $application,
|
||||
public ApplicationPreview $preview,
|
||||
public ProcessStatus $status,
|
||||
public ?string $deployment_uuid = null
|
||||
) {
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
try {
|
||||
$this->application = Application::findOrFail($this->application_id);
|
||||
$this->preview = ApplicationPreview::findPreviewByApplicationAndPullId($this->application->id, $this->pull_request_id);
|
||||
|
||||
$this->build_logs_url = base_url() . "/project/{$this->application->environment->project->uuid}/{$this->application->environment->name}/application/{$this->application->uuid}/deployment/{$this->deployment_uuid}";
|
||||
|
||||
if ($this->status === ProcessStatus::IN_PROGRESS->value) {
|
||||
if ($this->status === ProcessStatus::CLOSED) {
|
||||
$this->delete_comment();
|
||||
return;
|
||||
} else if ($this->status === ProcessStatus::IN_PROGRESS) {
|
||||
$this->body = "The preview deployment is in progress. 🟡\n\n";
|
||||
}
|
||||
if ($this->status === ProcessStatus::FINISHED->value) {
|
||||
} else if ($this->status === ProcessStatus::FINISHED) {
|
||||
$this->body = "The preview deployment is ready. 🟢\n\n";
|
||||
if ($this->preview->fqdn) {
|
||||
$this->body .= "[Open Preview]({$this->preview->fqdn}) | ";
|
||||
}
|
||||
}
|
||||
if ($this->status === ProcessStatus::ERROR->value) {
|
||||
} else if ($this->status === ProcessStatus::ERROR) {
|
||||
$this->body = "The preview deployment failed. 🔴\n\n";
|
||||
}
|
||||
$this->build_logs_url = base_url() . "/project/{$this->application->environment->project->uuid}/{$this->application->environment->name}/application/{$this->application->uuid}/deployment/{$this->deployment_uuid}";
|
||||
|
||||
$this->body .= "[Open Build Logs](" . $this->build_logs_url . ")\n\n\n";
|
||||
$this->body .= "Last updated at: " . now()->toDateTimeString() . " CET";
|
||||
|
||||
@@ -77,10 +73,14 @@ class ApplicationPullRequestUpdateJob implements ShouldQueue, ShouldBeEncrypted
|
||||
|
||||
private function create_comment()
|
||||
{
|
||||
['data' => $data] = githubApi(source: $this->application->source, endpoint: "/repos/{$this->application->git_repository}/issues/{$this->pull_request_id}/comments", method: 'post', data: [
|
||||
['data' => $data] = githubApi(source: $this->application->source, endpoint: "/repos/{$this->application->git_repository}/issues/{$this->preview->pull_request_id}/comments", method: 'post', data: [
|
||||
'body' => $this->body,
|
||||
]);
|
||||
$this->preview->pull_request_issue_comment_id = $data['id'];
|
||||
$this->preview->save();
|
||||
}
|
||||
private function delete_comment()
|
||||
{
|
||||
githubApi(source: $this->application->source, endpoint: "/repos/{$this->application->git_repository}/issues/comments/{$this->preview->pull_request_issue_comment_id}", method: 'delete');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace App\Jobs;
|
||||
use App\Actions\Database\StartDatabaseProxy;
|
||||
use App\Actions\Proxy\CheckProxy;
|
||||
use App\Actions\Proxy\StartProxy;
|
||||
use App\Actions\Shared\ComplexStatusCheck;
|
||||
use App\Models\ApplicationPreview;
|
||||
use App\Models\Server;
|
||||
use App\Notifications\Container\ContainerRestarted;
|
||||
@@ -42,6 +43,19 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$applications = $this->server->applications();
|
||||
foreach ($applications as $application) {
|
||||
if ($application->additional_servers->count() > 0) {
|
||||
$is_main_server = $application->destination->server->id === $this->server->id;
|
||||
if ($is_main_server) {
|
||||
ComplexStatusCheck::run($application);
|
||||
$applications = $applications->filter(function ($value, $key) use ($application) {
|
||||
return $value->id !== $application->id;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$this->server->isFunctional()) {
|
||||
return 'Server is not ready.';
|
||||
};
|
||||
@@ -83,7 +97,6 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
|
||||
});
|
||||
}
|
||||
}
|
||||
$applications = $this->server->applications();
|
||||
$databases = $this->server->databases();
|
||||
$services = $this->server->services()->get();
|
||||
$previews = $this->server->previews();
|
||||
@@ -160,10 +173,9 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
|
||||
// Notify user that this container should not be there.
|
||||
}
|
||||
}
|
||||
if (data_get($container,'Name') === '/coolify-db') {
|
||||
if (data_get($container, 'Name') === '/coolify-db') {
|
||||
$foundDatabases[] = 0;
|
||||
}
|
||||
|
||||
}
|
||||
$serviceLabelId = data_get($labels, 'coolify.serviceId');
|
||||
if ($serviceLabelId) {
|
||||
@@ -209,7 +221,7 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
|
||||
}
|
||||
$exitedServices = $exitedServices->unique('id');
|
||||
foreach ($exitedServices as $exitedService) {
|
||||
if ($exitedService->status === 'exited') {
|
||||
if (str($exitedService->status)->startsWith('exited')) {
|
||||
continue;
|
||||
}
|
||||
$name = data_get($exitedService, 'name');
|
||||
@@ -231,7 +243,7 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
|
||||
$notRunningApplications = $applications->pluck('id')->diff($foundApplications);
|
||||
foreach ($notRunningApplications as $applicationId) {
|
||||
$application = $applications->where('id', $applicationId)->first();
|
||||
if ($application->status === 'exited') {
|
||||
if (str($application->status)->startsWith('exited')) {
|
||||
continue;
|
||||
}
|
||||
$application->update(['status' => 'exited']);
|
||||
@@ -256,7 +268,7 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
|
||||
$notRunningApplicationPreviews = $previews->pluck('id')->diff($foundApplicationPreviews);
|
||||
foreach ($notRunningApplicationPreviews as $previewId) {
|
||||
$preview = $previews->where('id', $previewId)->first();
|
||||
if ($preview->status === 'exited') {
|
||||
if (str($preview->status)->startsWith('exited')) {
|
||||
continue;
|
||||
}
|
||||
$preview->update(['status' => 'exited']);
|
||||
@@ -281,7 +293,7 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
|
||||
$notRunningDatabases = $databases->pluck('id')->diff($foundDatabases);
|
||||
foreach ($notRunningDatabases as $database) {
|
||||
$database = $databases->where('id', $database)->first();
|
||||
if ($database->status === 'exited') {
|
||||
if (str($database->status)->startsWith('exited')) {
|
||||
continue;
|
||||
}
|
||||
$database->update(['status' => 'exited']);
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace App\Jobs;
|
||||
use App\Actions\Application\StopApplication;
|
||||
use App\Actions\Database\StopDatabase;
|
||||
use App\Actions\Service\DeleteService;
|
||||
use App\Actions\Service\StopService;
|
||||
use App\Models\Application;
|
||||
use App\Models\Service;
|
||||
use App\Models\StandaloneMariadb;
|
||||
@@ -18,6 +19,7 @@ use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
|
||||
class DeleteResourceJob implements ShouldQueue, ShouldBeEncrypted
|
||||
{
|
||||
@@ -30,45 +32,29 @@ class DeleteResourceJob implements ShouldQueue, ShouldBeEncrypted
|
||||
public function handle()
|
||||
{
|
||||
try {
|
||||
$server = $this->resource->destination->server;
|
||||
$this->resource->delete();
|
||||
if (!$server->isFunctional()) {
|
||||
if ($this->resource->type() === 'service') {
|
||||
ray('dispatching delete service');
|
||||
DeleteService::dispatch($this->resource);
|
||||
} else {
|
||||
$this->resource->forceDelete();
|
||||
}
|
||||
return 'Server is not functional';
|
||||
}
|
||||
$this->resource->forceDelete();
|
||||
switch ($this->resource->type()) {
|
||||
case 'application':
|
||||
StopApplication::run($this->resource);
|
||||
break;
|
||||
case 'standalone-postgresql':
|
||||
StopDatabase::run($this->resource);
|
||||
break;
|
||||
case 'standalone-redis':
|
||||
StopDatabase::run($this->resource);
|
||||
break;
|
||||
case 'standalone-mongodb':
|
||||
StopDatabase::run($this->resource);
|
||||
break;
|
||||
case 'standalone-mysql':
|
||||
StopDatabase::run($this->resource);
|
||||
break;
|
||||
case 'standalone-mariadb':
|
||||
StopDatabase::run($this->resource);
|
||||
break;
|
||||
}
|
||||
if ($this->resource->type() === 'service') {
|
||||
DeleteService::dispatch($this->resource);
|
||||
} else {
|
||||
$this->resource->forceDelete();
|
||||
case 'service':
|
||||
StopService::run($this->resource);
|
||||
DeleteService::run($this->resource);
|
||||
break;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
ray($e->getMessage());
|
||||
send_internal_notification('ContainerStoppingJob failed with: ' . $e->getMessage());
|
||||
throw $e;
|
||||
} finally {
|
||||
Artisan::queue('cleanup:stucked-resources');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ class ScheduledTaskJob implements ShouldQueue
|
||||
} else if ($application = $task->application()->first()) {
|
||||
$this->resource = $application;
|
||||
} else {
|
||||
throw new \Exception('ScheduledTaskJob failed: No resource found.');
|
||||
throw new \RuntimeException('ScheduledTaskJob failed: No resource found.');
|
||||
}
|
||||
$this->team = Team::find($task->team_id);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ class ServerStatusJob implements ShouldQueue, ShouldBeEncrypted
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public ?int $disk_usage = null;
|
||||
public int|string|null $disk_usage = null;
|
||||
public $tries = 4;
|
||||
public function backoff(): int
|
||||
{
|
||||
@@ -41,6 +41,15 @@ class ServerStatusJob implements ShouldQueue, ShouldBeEncrypted
|
||||
throw new \RuntimeException('Server is not ready.');
|
||||
};
|
||||
try {
|
||||
// $this->server->validateConnection();
|
||||
// $this->server->validateOS();
|
||||
// $docker_installed = $this->server->validateDockerEngine();
|
||||
// if (!$docker_installed) {
|
||||
// $this->server->installDocker();
|
||||
// $this->server->validateDockerEngine();
|
||||
// }
|
||||
|
||||
// $this->server->validateDockerEngineVersion();
|
||||
if ($this->server->isFunctional()) {
|
||||
$this->cleanup(notify: false);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ class ActivityMonitor extends Component
|
||||
public $isPollingActive = false;
|
||||
|
||||
protected $activity;
|
||||
protected $listeners = ['newMonitorActivity'];
|
||||
protected $listeners = ['activityMonitor' => 'newMonitorActivity'];
|
||||
|
||||
public function newMonitorActivity($activityId, $eventToDispatch = 'activityFinished')
|
||||
{
|
||||
|
||||
44
app/Livewire/Admin/Index.php
Normal file
44
app/Livewire/Admin/Index.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Admin;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Livewire\Component;
|
||||
|
||||
class Index extends Component
|
||||
{
|
||||
public $users = [];
|
||||
public function mount()
|
||||
{
|
||||
if (!isCloud()) {
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
if (auth()->user()->id !== 0 && session('adminToken') === null) {
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
$this->users = User::whereHas('teams', function ($query) {
|
||||
$query->whereRelation('subscription', 'stripe_subscription_id', '!=', null);
|
||||
})->get();
|
||||
}
|
||||
public function switchUser(int $user_id)
|
||||
{
|
||||
$user = User::find($user_id);
|
||||
auth()->login($user);
|
||||
|
||||
if ($user_id === 0) {
|
||||
session()->forget('adminToken');
|
||||
} else {
|
||||
$token_payload = [
|
||||
'valid' => true,
|
||||
];
|
||||
$token = Crypt::encrypt($token_payload);
|
||||
session(['adminToken' => $token]);
|
||||
}
|
||||
return refreshSession();
|
||||
}
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.admin.index');
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Livewire\Boarding;
|
||||
|
||||
use App\Actions\Server\InstallDocker;
|
||||
use App\Enums\ProxyTypes;
|
||||
use App\Models\PrivateKey;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
@@ -12,6 +13,7 @@ use Livewire\Component;
|
||||
|
||||
class Index extends Component
|
||||
{
|
||||
protected $listeners = ['serverInstalled' => 'validateServer'];
|
||||
public string $currentState = 'welcome';
|
||||
|
||||
public ?string $selectedServerType = null;
|
||||
@@ -93,7 +95,11 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
|
||||
$this->serverPublicKey = $this->createdServer->privateKey->publicKey();
|
||||
return $this->validateServer('localhost');
|
||||
} elseif ($this->selectedServerType === 'remote') {
|
||||
$this->privateKeys = PrivateKey::ownedByCurrentTeam(['name'])->where('id', '!=', 0)->get();
|
||||
if (isDev()) {
|
||||
$this->privateKeys = PrivateKey::ownedByCurrentTeam(['name'])->get();
|
||||
} else {
|
||||
$this->privateKeys = PrivateKey::ownedByCurrentTeam(['name'])->where('id', '!=', 0)->get();
|
||||
}
|
||||
if ($this->privateKeys->count() > 0) {
|
||||
$this->selectedExistingPrivateKey = $this->privateKeys->first()->id;
|
||||
}
|
||||
@@ -116,15 +122,16 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
|
||||
}
|
||||
$this->selectedExistingPrivateKey = $this->createdServer->privateKey->id;
|
||||
$this->serverPublicKey = $this->createdServer->privateKey->publicKey();
|
||||
$this->validateServer();
|
||||
$this->installServer();
|
||||
}
|
||||
public function getProxyType()
|
||||
{
|
||||
$proxyTypeSet = $this->createdServer->proxy->type;
|
||||
if (!$proxyTypeSet) {
|
||||
$this->currentState = 'select-proxy';
|
||||
return;
|
||||
}
|
||||
$this->selectProxy(ProxyTypes::TRAEFIK_V2->value);
|
||||
// $proxyTypeSet = $this->createdServer->proxy->type;
|
||||
// if (!$proxyTypeSet) {
|
||||
// $this->currentState = 'select-proxy';
|
||||
// return;
|
||||
// }
|
||||
$this->getProjects();
|
||||
}
|
||||
public function selectExistingPrivateKey()
|
||||
@@ -188,7 +195,11 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
|
||||
$this->createdServer->settings->is_cloudflare_tunnel = $this->isCloudflareTunnel;
|
||||
$this->createdServer->settings->save();
|
||||
$this->createdServer->addInitialNetwork();
|
||||
$this->validateServer();
|
||||
$this->currentState = 'validate-server';
|
||||
}
|
||||
public function installServer()
|
||||
{
|
||||
$this->dispatch('validateServer', true);
|
||||
}
|
||||
public function validateServer()
|
||||
{
|
||||
@@ -210,7 +221,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
|
||||
$dockerVersion = instant_remote_process(["docker version|head -2|grep -i version| awk '{print $2}'"], $this->createdServer, true);
|
||||
$dockerVersion = checkMinimumDockerEngineVersion($dockerVersion);
|
||||
if (is_null($dockerVersion)) {
|
||||
$this->currentState = 'install-docker';
|
||||
$this->currentState = 'validate-server';
|
||||
throw new \Exception('Docker not found or old version is installed.');
|
||||
}
|
||||
$this->createdServer->settings()->update([
|
||||
@@ -218,27 +229,10 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
|
||||
]);
|
||||
$this->getProxyType();
|
||||
} catch (\Throwable $e) {
|
||||
// $this->dockerInstallationStarted = false;
|
||||
return handleError(error: $e, livewire: $this);
|
||||
}
|
||||
}
|
||||
public function installDocker()
|
||||
{
|
||||
try {
|
||||
$this->dockerInstallationStarted = true;
|
||||
$activity = InstallDocker::run($this->createdServer);
|
||||
$this->dispatch('installDocker');
|
||||
$this->dispatch('newMonitorActivity', $activity->id);
|
||||
} catch (\Throwable $e) {
|
||||
$this->dockerInstallationStarted = false;
|
||||
return handleError(error: $e, livewire: $this);
|
||||
}
|
||||
}
|
||||
public function dockerInstalledOrSkipped()
|
||||
{
|
||||
$this->validateServer();
|
||||
}
|
||||
public function selectProxy(string|null $proxyType = null)
|
||||
public function selectProxy(?string $proxyType = null)
|
||||
{
|
||||
if (!$proxyType) {
|
||||
return $this->getProjects();
|
||||
|
||||
@@ -2,18 +2,43 @@
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\ApplicationDeploymentQueue;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Livewire\Component;
|
||||
|
||||
class Dashboard extends Component
|
||||
{
|
||||
public $projects = [];
|
||||
public $servers = [];
|
||||
public Collection $servers;
|
||||
public $deployments_per_server;
|
||||
public function mount()
|
||||
{
|
||||
$this->servers = Server::ownedByCurrentTeam()->get();
|
||||
$this->projects = Project::ownedByCurrentTeam()->get();
|
||||
$this->get_deployments();
|
||||
}
|
||||
public function cleanup_queue()
|
||||
{
|
||||
$this->dispatch('success', 'Cleanup started.');
|
||||
Artisan::queue('app:init', [
|
||||
'--cleanup-deployments' => 'true'
|
||||
]);
|
||||
}
|
||||
public function get_deployments()
|
||||
{
|
||||
$this->deployments_per_server = ApplicationDeploymentQueue::whereIn("status", ["in_progress", "queued"])->whereIn("server_id", $this->servers->pluck("id"))->get([
|
||||
"id",
|
||||
"application_id",
|
||||
"application_name",
|
||||
"deployment_url",
|
||||
"pull_request_id",
|
||||
"server_name",
|
||||
"server_id",
|
||||
"status"
|
||||
])->sortBy('id')->groupBy('server_name')->toArray();
|
||||
}
|
||||
// public function getIptables()
|
||||
// {
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
use DanHarrin\LivewireRateLimiting\WithRateLimiting;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Livewire\Component;
|
||||
|
||||
@@ -28,9 +30,8 @@ class Help extends Component
|
||||
public function submit()
|
||||
{
|
||||
try {
|
||||
$this->rateLimit(3, 60);
|
||||
$this->rateLimit(3, 30);
|
||||
$this->validate();
|
||||
$subscriptionType = auth()->user()?->subscription?->type() ?? 'Free';
|
||||
$debug = "Route: {$this->path}";
|
||||
$mail = new MailMessage();
|
||||
$mail->view(
|
||||
@@ -40,9 +41,21 @@ class Help extends Component
|
||||
'debug' => $debug
|
||||
]
|
||||
);
|
||||
$mail->subject("[HELP - {$subscriptionType}]: {$this->subject}");
|
||||
send_user_an_email($mail, auth()->user()?->email, 'hi@coollabs.io');
|
||||
$this->dispatch('success', 'Your message has been sent successfully. <br>We will get in touch with you as soon as possible.');
|
||||
$mail->subject("[HELP]: {$this->subject}");
|
||||
$settings = InstanceSettings::get();
|
||||
$type = set_transanctional_email_settings($settings);
|
||||
if (!$type) {
|
||||
$url = "https://app.coolify.io/api/feedback";
|
||||
if (isDev()) {
|
||||
$url = "http://localhost:80/api/feedback";
|
||||
}
|
||||
Http::post($url, [
|
||||
'content' => "User: `" . auth()->user()?->email . "` with subject: `" . $this->subject . "` has the following problem: `" . $this->description . "`"
|
||||
]);
|
||||
} else {
|
||||
send_user_an_email($mail, auth()->user()?->email, 'hi@coollabs.io');
|
||||
}
|
||||
$this->dispatch('success', 'Feedback sent.', 'We will get in touch with you as soon as possible.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
63
app/Livewire/NewActivityMonitor.php
Normal file
63
app/Livewire/NewActivityMonitor.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\User;
|
||||
use Livewire\Component;
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
|
||||
class NewActivityMonitor extends Component
|
||||
{
|
||||
public ?string $header = null;
|
||||
public $activityId;
|
||||
public $eventToDispatch = 'activityFinished';
|
||||
public $isPollingActive = false;
|
||||
|
||||
protected $activity;
|
||||
protected $listeners = ['newActivityMonitor' => 'newMonitorActivity'];
|
||||
|
||||
public function newMonitorActivity($activityId, $eventToDispatch = 'activityFinished')
|
||||
{
|
||||
$this->activityId = $activityId;
|
||||
$this->eventToDispatch = $eventToDispatch;
|
||||
|
||||
$this->hydrateActivity();
|
||||
|
||||
$this->isPollingActive = true;
|
||||
}
|
||||
|
||||
public function hydrateActivity()
|
||||
{
|
||||
$this->activity = Activity::find($this->activityId);
|
||||
}
|
||||
|
||||
public function polling()
|
||||
{
|
||||
$this->hydrateActivity();
|
||||
// $this->setStatus(ProcessStatus::IN_PROGRESS);
|
||||
$exit_code = data_get($this->activity, 'properties.exitCode');
|
||||
if ($exit_code !== null) {
|
||||
// if ($exit_code === 0) {
|
||||
// // $this->setStatus(ProcessStatus::FINISHED);
|
||||
// } else {
|
||||
// // $this->setStatus(ProcessStatus::ERROR);
|
||||
// }
|
||||
$this->isPollingActive = false;
|
||||
if ($this->eventToDispatch !== null) {
|
||||
if (str($this->eventToDispatch)->startsWith('App\\Events\\')) {
|
||||
$causer_id = data_get($this->activity, 'causer_id');
|
||||
$user = User::find($causer_id);
|
||||
if ($user) {
|
||||
foreach ($user->teams as $team) {
|
||||
$teamId = $team->id;
|
||||
$this->eventToDispatch::dispatch($teamId);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
$this->dispatch($this->eventToDispatch);
|
||||
ray('Dispatched event: ' . $this->eventToDispatch);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,20 +8,25 @@ use Livewire\Component;
|
||||
class Advanced extends Component
|
||||
{
|
||||
public Application $application;
|
||||
public bool $is_force_https_enabled;
|
||||
protected $rules = [
|
||||
'application.settings.is_git_submodules_enabled' => 'boolean|required',
|
||||
'application.settings.is_git_lfs_enabled' => 'boolean|required',
|
||||
'application.settings.is_preview_deployments_enabled' => 'boolean|required',
|
||||
'application.settings.is_auto_deploy_enabled' => 'boolean|required',
|
||||
'application.settings.is_force_https_enabled' => 'boolean|required',
|
||||
'is_force_https_enabled' => 'boolean|required',
|
||||
'application.settings.is_log_drain_enabled' => 'boolean|required',
|
||||
'application.settings.is_gpu_enabled' => 'boolean|required',
|
||||
'application.settings.is_build_server_enabled' => 'boolean|required',
|
||||
'application.settings.is_consistent_container_name_enabled' => 'boolean|required',
|
||||
'application.settings.gpu_driver' => 'string|required',
|
||||
'application.settings.gpu_count' => 'string|required',
|
||||
'application.settings.gpu_device_ids' => 'string|required',
|
||||
'application.settings.gpu_options' => 'string|required',
|
||||
];
|
||||
public function mount() {
|
||||
$this->is_force_https_enabled = $this->application->settings->is_force_https_enabled;
|
||||
}
|
||||
public function instantSave()
|
||||
{
|
||||
if ($this->application->isLogDrainEnabled()) {
|
||||
@@ -31,7 +36,8 @@ class Advanced extends Component
|
||||
return;
|
||||
}
|
||||
}
|
||||
if ($this->application->settings->is_force_https_enabled) {
|
||||
if ($this->application->settings->is_force_https_enabled !== $this->is_force_https_enabled) {
|
||||
$this->application->settings->is_force_https_enabled = $this->is_force_https_enabled;
|
||||
$this->dispatch('resetDefaultLabels', false);
|
||||
}
|
||||
$this->application->settings->save();
|
||||
|
||||
@@ -15,6 +15,7 @@ class Index extends Component
|
||||
public int $skip = 0;
|
||||
public int $default_take = 40;
|
||||
public bool $show_next = false;
|
||||
public bool $show_prev = false;
|
||||
public ?string $pull_request_id = null;
|
||||
protected $queryString = ['pull_request_id'];
|
||||
public function mount()
|
||||
@@ -60,15 +61,30 @@ class Index extends Component
|
||||
{
|
||||
$this->load_deployments();
|
||||
}
|
||||
public function previous_page(?int $take = null)
|
||||
{
|
||||
|
||||
public function load_deployments(int|null $take = null)
|
||||
if ($take) {
|
||||
$this->skip = $this->skip - $take;
|
||||
}
|
||||
$this->skip = $this->skip - $this->default_take;
|
||||
if ($this->skip < 0) {
|
||||
$this->show_prev = false;
|
||||
$this->skip = 0;
|
||||
}
|
||||
$this->load_deployments();
|
||||
}
|
||||
public function next_page(?int $take = null)
|
||||
{
|
||||
if ($take) {
|
||||
$this->skip = $this->skip + $take;
|
||||
}
|
||||
$take = $this->default_take;
|
||||
|
||||
['deployments' => $deployments, 'count' => $count] = $this->application->deployments($this->skip, $take);
|
||||
$this->show_prev = true;
|
||||
$this->load_deployments();
|
||||
}
|
||||
public function load_deployments()
|
||||
{
|
||||
['deployments' => $deployments, 'count' => $count] = $this->application->deployments($this->skip, $this->default_take);
|
||||
$this->deployments = $deployments;
|
||||
$this->deployments_count = $count;
|
||||
$this->show_pull_request_only();
|
||||
|
||||
@@ -7,8 +7,6 @@ use App\Models\Application;
|
||||
use App\Models\ApplicationDeploymentQueue;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\Component;
|
||||
|
||||
class DeploymentNavbar extends Component
|
||||
@@ -37,11 +35,21 @@ class DeploymentNavbar extends Component
|
||||
$this->is_debug_enabled = $this->application->settings->is_debug_enabled;
|
||||
$this->dispatch('refreshQueue');
|
||||
}
|
||||
|
||||
public function force_start()
|
||||
{
|
||||
try {
|
||||
force_start_deployment($this->application_deployment_queue);
|
||||
} catch (\Throwable $e) {
|
||||
ray($e);
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
public function cancel()
|
||||
{
|
||||
try {
|
||||
$kill_command = "docker rm -f {$this->application_deployment_queue->deployment_uuid}";
|
||||
$server_id = $this->application_deployment_queue->server_id ?? $this->application->destination->server_id;
|
||||
$server = Server::find($server_id);
|
||||
if ($this->application_deployment_queue->logs) {
|
||||
$previous_logs = json_decode($this->application_deployment_queue->logs, associative: true, flags: JSON_THROW_ON_ERROR);
|
||||
|
||||
@@ -57,8 +65,8 @@ class DeploymentNavbar extends Component
|
||||
$this->application_deployment_queue->update([
|
||||
'logs' => json_encode($previous_logs, flags: JSON_THROW_ON_ERROR),
|
||||
]);
|
||||
instant_remote_process([$kill_command], $this->server);
|
||||
}
|
||||
instant_remote_process([$kill_command], $server);
|
||||
} catch (\Throwable $e) {
|
||||
ray($e);
|
||||
return handleError($e, $this);
|
||||
@@ -67,7 +75,6 @@ class DeploymentNavbar extends Component
|
||||
'current_process_id' => null,
|
||||
'status' => ApplicationDeploymentStatus::CANCELLED_BY_USER->value,
|
||||
]);
|
||||
// queue_next_deployment($this->application);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,11 +61,12 @@ class General extends Component
|
||||
'application.docker_compose_pr' => 'nullable',
|
||||
'application.docker_compose_raw' => 'nullable',
|
||||
'application.docker_compose_pr_raw' => 'nullable',
|
||||
'application.custom_labels' => 'nullable',
|
||||
'application.dockerfile_target_build' => 'nullable',
|
||||
'application.settings.is_static' => 'boolean|required',
|
||||
'application.docker_compose_custom_start_command' => 'nullable',
|
||||
'application.docker_compose_custom_build_command' => 'nullable',
|
||||
'application.custom_labels' => 'nullable',
|
||||
'application.custom_docker_run_options' => 'nullable',
|
||||
'application.settings.is_static' => 'boolean|required',
|
||||
'application.settings.is_raw_compose_deployment_enabled' => 'boolean|required',
|
||||
'application.settings.is_build_server_enabled' => 'boolean|required',
|
||||
];
|
||||
@@ -97,9 +98,10 @@ class General extends Component
|
||||
'application.docker_compose_pr_raw' => 'Docker compose raw',
|
||||
'application.custom_labels' => 'Custom labels',
|
||||
'application.dockerfile_target_build' => 'Dockerfile target build',
|
||||
'application.settings.is_static' => 'Is static',
|
||||
'application.custom_docker_run_options' => 'Custom docker run commands',
|
||||
'application.docker_compose_custom_start_command' => 'Docker compose custom start command',
|
||||
'application.docker_compose_custom_build_command' => 'Docker compose custom build command',
|
||||
'application.settings.is_static' => 'Is static',
|
||||
'application.settings.is_raw_compose_deployment_enabled' => 'Is raw compose deployment enabled',
|
||||
'application.settings.is_build_server_enabled' => 'Is build server enabled',
|
||||
];
|
||||
@@ -124,7 +126,6 @@ class General extends Component
|
||||
$this->application->save();
|
||||
}
|
||||
$this->initialDockerComposeLocation = $this->application->docker_compose_location;
|
||||
$this->checkLabelUpdates();
|
||||
}
|
||||
public function instantSave()
|
||||
{
|
||||
@@ -162,6 +163,7 @@ class General extends Component
|
||||
}
|
||||
return $domain;
|
||||
}
|
||||
|
||||
public function updatedApplicationBuildPack()
|
||||
{
|
||||
if ($this->application->build_pack !== 'nixpacks') {
|
||||
@@ -182,15 +184,6 @@ class General extends Component
|
||||
$this->submit();
|
||||
$this->dispatch('build_pack_updated');
|
||||
}
|
||||
public function checkLabelUpdates()
|
||||
{
|
||||
if (md5($this->application->custom_labels) !== md5(implode("|", generateLabelsApplication($this->application)))) {
|
||||
$this->labelsChanged = true;
|
||||
} else {
|
||||
$this->labelsChanged = false;
|
||||
}
|
||||
}
|
||||
|
||||
public function getWildcardDomain()
|
||||
{
|
||||
$server = data_get($this->application, 'destination.server');
|
||||
@@ -210,6 +203,13 @@ class General extends Component
|
||||
|
||||
public function updatedApplicationFqdn()
|
||||
{
|
||||
$this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
|
||||
$this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
|
||||
$this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
|
||||
return str($domain)->trim()->lower();
|
||||
});
|
||||
$this->application->fqdn = $this->application->fqdn->unique()->implode(',');
|
||||
$this->application->save();
|
||||
$this->resetDefaultLabels(false);
|
||||
// $this->dispatch('success', 'Labels reset to default!');
|
||||
}
|
||||
@@ -236,19 +236,20 @@ class General extends Component
|
||||
]);
|
||||
}
|
||||
if (data_get($this->application, 'fqdn')) {
|
||||
$this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
|
||||
$domains = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
|
||||
return str($domain)->trim()->lower();
|
||||
});
|
||||
$domains = $domains->unique();
|
||||
foreach ($domains as $domain) {
|
||||
if (!validate_dns_entry($domain, $this->application->destination->server)) {
|
||||
$showToaster && $this->dispatch('error', "Validating DNS settings for: $domain failed.<br>Make sure you have added the DNS records correctly.<br><br>Check this <a target='_blank' class='underline' href='https://coolify.io/docs/dns-settings'>documentation</a> for further help.");
|
||||
$domains = str($this->application->fqdn)->trim()->explode(',');
|
||||
if ($this->application->additional_servers->count() === 0) {
|
||||
foreach ($domains as $domain) {
|
||||
if (!validate_dns_entry($domain, $this->application->destination->server)) {
|
||||
$showToaster && $this->dispatch('error', "Validating DNS ($domain) failed.", "Make sure you have added the DNS records correctly.<br><br>Check this <a target='_blank' class='text-white underline' href='https://coolify.io/docs/dns-settings'>documentation</a> for further help.");
|
||||
}
|
||||
}
|
||||
}
|
||||
check_fqdn_usage($this->application);
|
||||
$this->application->fqdn = $domains->implode(',');
|
||||
}
|
||||
|
||||
if (data_get($this->application, 'custom_docker_run_options')) {
|
||||
$this->application->custom_docker_run_options = str($this->application->custom_docker_run_options)->trim();
|
||||
}
|
||||
if (data_get($this->application, 'dockerfile')) {
|
||||
$port = get_port_from_dockerfile($this->application->dockerfile);
|
||||
if ($port && !$this->application->ports_exposes) {
|
||||
@@ -271,7 +272,6 @@ class General extends Component
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
} finally {
|
||||
$this->checkLabelUpdates();
|
||||
$this->isConfigurationChanged = $this->application->isConfigurationChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
namespace App\Livewire\Project\Application;
|
||||
|
||||
use App\Actions\Application\StopApplication;
|
||||
use App\Events\ApplicationStatusChanged;
|
||||
use App\Jobs\ComplexContainerStatusJob;
|
||||
use App\Jobs\ContainerStatusJob;
|
||||
use App\Jobs\ServerStatusJob;
|
||||
use App\Models\Application;
|
||||
@@ -31,13 +33,14 @@ class Heading extends Component
|
||||
{
|
||||
if ($this->application->destination->server->isFunctional()) {
|
||||
dispatch(new ContainerStatusJob($this->application->destination->server));
|
||||
$this->application->refresh();
|
||||
$this->application->previews->each(function ($preview) {
|
||||
$preview->refresh();
|
||||
});
|
||||
// $this->application->refresh();
|
||||
// $this->application->previews->each(function ($preview) {
|
||||
// $preview->refresh();
|
||||
// });
|
||||
} else {
|
||||
dispatch(new ServerStatusJob($this->application->destination->server));
|
||||
}
|
||||
|
||||
if ($showNotification) $this->dispatch('success', "Application status updated.");
|
||||
}
|
||||
|
||||
@@ -46,45 +49,27 @@ class Heading extends Component
|
||||
$this->deploy(force_rebuild: true);
|
||||
}
|
||||
|
||||
public function deployNew()
|
||||
{
|
||||
if ($this->application->build_pack === 'dockercompose' && is_null($this->application->docker_compose_raw)) {
|
||||
$this->dispatch('error', 'Please load a Compose file first.');
|
||||
return;
|
||||
}
|
||||
$this->setDeploymentUuid();
|
||||
queue_application_deployment(
|
||||
application_id: $this->application->id,
|
||||
server_id: $this->application->destination->server->id,
|
||||
deployment_uuid: $this->deploymentUuid,
|
||||
force_rebuild: false,
|
||||
is_new_deployment: true,
|
||||
);
|
||||
return redirect()->route('project.application.deployment.show', [
|
||||
'project_uuid' => $this->parameters['project_uuid'],
|
||||
'application_uuid' => $this->parameters['application_uuid'],
|
||||
'deployment_uuid' => $this->deploymentUuid,
|
||||
'environment_name' => $this->parameters['environment_name'],
|
||||
]);
|
||||
}
|
||||
public function deploy(bool $force_rebuild = false)
|
||||
{
|
||||
if ($this->application->build_pack === 'dockercompose' && is_null($this->application->docker_compose_raw)) {
|
||||
$this->dispatch('error', 'Please load a Compose file first.');
|
||||
$this->dispatch('error', 'Failed to deploy', 'Please load a Compose file first.');
|
||||
return;
|
||||
}
|
||||
if ($this->application->destination->server->isSwarm() && is_null($this->application->docker_registry_image_name)) {
|
||||
$this->dispatch('error', 'To deploy to a Swarm cluster you must set a Docker image name first.');
|
||||
if ($this->application->destination->server->isSwarm() && str($this->application->docker_registry_image_name)->isEmpty()) {
|
||||
$this->dispatch('error', 'Failed to deploy.', 'To deploy to a Swarm cluster you must set a Docker image name first.');
|
||||
return;
|
||||
}
|
||||
if (data_get($this->application, 'settings.is_build_server_enabled') && is_null($this->application->docker_registry_image_name)) {
|
||||
$this->dispatch('error', 'To use a build server you must set a Docker image name first.<br>More information here: <a target="_blank" class="underline" href="https://coolify.io/docs/server/build-server">documentation</a>');
|
||||
if (data_get($this->application, 'settings.is_build_server_enabled') && str($this->application->docker_registry_image_name)->isEmpty()) {
|
||||
$this->dispatch('error', 'Failed to deploy.', 'To use a build server, you must first set a Docker image.<br>More information here: <a target="_blank" class="underline" href="https://coolify.io/docs/server/build-server">documentation</a>');
|
||||
return;
|
||||
}
|
||||
if ($this->application->additional_servers->count() > 0 && str($this->application->docker_registry_image_name)->isEmpty()) {
|
||||
$this->dispatch('error', 'Failed to deploy.', 'Before deploying to multiple servers, you must first set a Docker image in the General tab.<br>More information here: <a target="_blank" class="underline" href="https://coolify.io/docs/server/multiple-servers">documentation</a>');
|
||||
return;
|
||||
}
|
||||
$this->setDeploymentUuid();
|
||||
queue_application_deployment(
|
||||
application_id: $this->application->id,
|
||||
server_id: $this->application->destination->server->id,
|
||||
application: $this->application,
|
||||
deployment_uuid: $this->deploymentUuid,
|
||||
force_rebuild: $force_rebuild,
|
||||
);
|
||||
@@ -107,31 +92,23 @@ class Heading extends Component
|
||||
StopApplication::run($this->application);
|
||||
$this->application->status = 'exited';
|
||||
$this->application->save();
|
||||
$this->application->refresh();
|
||||
}
|
||||
public function restartNew()
|
||||
{
|
||||
$this->setDeploymentUuid();
|
||||
queue_application_deployment(
|
||||
application_id: $this->application->id,
|
||||
server_id: $this->application->destination->server->id,
|
||||
deployment_uuid: $this->deploymentUuid,
|
||||
restart_only: true,
|
||||
is_new_deployment: true,
|
||||
);
|
||||
return redirect()->route('project.application.deployment.show', [
|
||||
'project_uuid' => $this->parameters['project_uuid'],
|
||||
'application_uuid' => $this->parameters['application_uuid'],
|
||||
'deployment_uuid' => $this->deploymentUuid,
|
||||
'environment_name' => $this->parameters['environment_name'],
|
||||
]);
|
||||
if ($this->application->additional_servers->count() > 0) {
|
||||
$this->application->additional_servers->each(function ($server) {
|
||||
$server->pivot->status = "exited:unhealthy";
|
||||
$server->pivot->save();
|
||||
});
|
||||
}
|
||||
ApplicationStatusChanged::dispatch(data_get($this->application, 'environment.project.team.id'));
|
||||
}
|
||||
public function restart()
|
||||
{
|
||||
if ($this->application->additional_servers->count() > 0 && str($this->application->docker_registry_image_name)->isEmpty()) {
|
||||
$this->dispatch('error', 'Failed to deploy', 'Before deploying to multiple servers, you must first set a Docker image in the General tab.<br>More information here: <a target="_blank" class="underline" href="https://coolify.io/docs/server/multiple-servers">documentation</a>');
|
||||
return;
|
||||
}
|
||||
$this->setDeploymentUuid();
|
||||
queue_application_deployment(
|
||||
application_id: $this->application->id,
|
||||
server_id: $this->application->destination->server->id,
|
||||
application: $this->application,
|
||||
deployment_uuid: $this->deploymentUuid,
|
||||
restart_only: true,
|
||||
);
|
||||
|
||||
@@ -47,8 +47,7 @@ class Previews extends Component
|
||||
]);
|
||||
}
|
||||
queue_application_deployment(
|
||||
application_id: $this->application->id,
|
||||
server_id: $this->application->destination->server->id,
|
||||
application: $this->application,
|
||||
deployment_uuid: $this->deployment_uuid,
|
||||
force_rebuild: false,
|
||||
pull_request_id: $pull_request_id,
|
||||
|
||||
@@ -24,8 +24,7 @@ class Rollback extends Component
|
||||
$deployment_uuid = new Cuid2(7);
|
||||
|
||||
queue_application_deployment(
|
||||
application_id: $this->application->id,
|
||||
server_id: $this->application->destination->server->id,
|
||||
application: $this->application,
|
||||
deployment_uuid: $deployment_uuid,
|
||||
commit: $commit,
|
||||
force_rebuild: false,
|
||||
|
||||
@@ -22,12 +22,12 @@ class CloneMe extends Component
|
||||
public ?int $selectedDestination = null;
|
||||
public ?Server $server = null;
|
||||
public $resources = [];
|
||||
public string $newProjectName = '';
|
||||
public string $newName = '';
|
||||
|
||||
protected $messages = [
|
||||
'selectedServer' => 'Please select a server.',
|
||||
'selectedDestination' => 'Please select a server & destination.',
|
||||
'newProjectName' => 'Please enter a name for the new project.',
|
||||
'newName' => 'Please enter a name for the new project or environment.',
|
||||
];
|
||||
public function mount($project_uuid)
|
||||
{
|
||||
@@ -36,7 +36,7 @@ class CloneMe extends Component
|
||||
$this->environment = $this->project->environments->where('name', $this->environment_name)->first();
|
||||
$this->project_id = $this->project->id;
|
||||
$this->servers = currentTeam()->servers;
|
||||
$this->newProjectName = str($this->project->name . '-clone-' . (string)new Cuid2(7))->slug();
|
||||
$this->newName = str($this->project->name . '-clone-' . (string)new Cuid2(7))->slug();
|
||||
}
|
||||
|
||||
public function render()
|
||||
@@ -46,34 +46,50 @@ class CloneMe extends Component
|
||||
|
||||
public function selectServer($server_id, $destination_id)
|
||||
{
|
||||
if ($server_id == $this->selectedServer && $destination_id == $this->selectedDestination) {
|
||||
$this->selectedServer = null;
|
||||
$this->selectedDestination = null;
|
||||
$this->server = null;
|
||||
return;
|
||||
}
|
||||
$this->selectedServer = $server_id;
|
||||
$this->selectedDestination = $destination_id;
|
||||
$this->server = $this->servers->where('id', $server_id)->first();
|
||||
}
|
||||
|
||||
public function clone()
|
||||
public function clone(string $type)
|
||||
{
|
||||
try {
|
||||
$this->validate([
|
||||
'selectedDestination' => 'required',
|
||||
'newProjectName' => 'required',
|
||||
'newName' => 'required',
|
||||
]);
|
||||
$foundProject = Project::where('name', $this->newProjectName)->first();
|
||||
if ($foundProject) {
|
||||
throw new \Exception('Project with the same name already exists.');
|
||||
}
|
||||
$newProject = Project::create([
|
||||
'name' => $this->newProjectName,
|
||||
'team_id' => currentTeam()->id,
|
||||
'description' => $this->project->description . ' (clone)',
|
||||
]);
|
||||
if ($this->environment->name !== 'production') {
|
||||
$newProject->environments()->create([
|
||||
'name' => $this->environment->name,
|
||||
if ($type === 'project') {
|
||||
$foundProject = Project::where('name', $this->newName)->first();
|
||||
if ($foundProject) {
|
||||
throw new \Exception('Project with the same name already exists.');
|
||||
}
|
||||
$project = Project::create([
|
||||
'name' => $this->newName,
|
||||
'team_id' => currentTeam()->id,
|
||||
'description' => $this->project->description . ' (clone)',
|
||||
]);
|
||||
if ($this->environment->name !== 'production') {
|
||||
$project->environments()->create([
|
||||
'name' => $this->environment->name,
|
||||
]);
|
||||
}
|
||||
$environment = $project->environments->where('name', $this->environment->name)->first();
|
||||
} else {
|
||||
$foundEnv = $this->project->environments()->where('name', $this->newName)->first();
|
||||
if ($foundEnv) {
|
||||
throw new \Exception('Environment with the same name already exists.');
|
||||
}
|
||||
$project = $this->project;
|
||||
$environment = $this->project->environments()->create([
|
||||
'name' => $this->newName,
|
||||
]);
|
||||
}
|
||||
$newEnvironment = $newProject->environments->where('name', $this->environment->name)->first();
|
||||
// Clone Applications
|
||||
$applications = $this->environment->applications;
|
||||
$databases = $this->environment->databases();
|
||||
$services = $this->environment->services;
|
||||
@@ -83,7 +99,7 @@ class CloneMe extends Component
|
||||
'uuid' => $uuid,
|
||||
'fqdn' => generateFqdn($this->server, $uuid),
|
||||
'status' => 'exited',
|
||||
'environment_id' => $newEnvironment->id,
|
||||
'environment_id' => $environment->id,
|
||||
// This is not correct, but we need to set it to something
|
||||
'destination_id' => $this->selectedDestination,
|
||||
]);
|
||||
@@ -110,7 +126,7 @@ class CloneMe extends Component
|
||||
'uuid' => $uuid,
|
||||
'status' => 'exited',
|
||||
'started_at' => null,
|
||||
'environment_id' => $newEnvironment->id,
|
||||
'environment_id' => $environment->id,
|
||||
'destination_id' => $this->selectedDestination,
|
||||
]);
|
||||
$newDatabase->save();
|
||||
@@ -136,7 +152,7 @@ class CloneMe extends Component
|
||||
$uuid = (string)new Cuid2(7);
|
||||
$newService = $service->replicate()->fill([
|
||||
'uuid' => $uuid,
|
||||
'environment_id' => $newEnvironment->id,
|
||||
'environment_id' => $environment->id,
|
||||
'destination_id' => $this->selectedDestination,
|
||||
]);
|
||||
$newService->save();
|
||||
@@ -153,8 +169,8 @@ class CloneMe extends Component
|
||||
$newService->parse();
|
||||
}
|
||||
return redirect()->route('project.resource.index', [
|
||||
'project_uuid' => $newProject->uuid,
|
||||
'environment_name' => $newEnvironment->name,
|
||||
'project_uuid' => $project->uuid,
|
||||
'environment_name' => $environment->name,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return handleError($e, $this);
|
||||
|
||||
@@ -58,19 +58,19 @@ class Heading extends Component
|
||||
{
|
||||
if ($this->database->type() === 'standalone-postgresql') {
|
||||
$activity = StartPostgresql::run($this->database);
|
||||
$this->dispatch('newMonitorActivity', $activity->id);
|
||||
$this->dispatch('activityMonitor', $activity->id);
|
||||
} else if ($this->database->type() === 'standalone-redis') {
|
||||
$activity = StartRedis::run($this->database);
|
||||
$this->dispatch('newMonitorActivity', $activity->id);
|
||||
$this->dispatch('activityMonitor', $activity->id);
|
||||
} else if ($this->database->type() === 'standalone-mongodb') {
|
||||
$activity = StartMongodb::run($this->database);
|
||||
$this->dispatch('newMonitorActivity', $activity->id);
|
||||
$this->dispatch('activityMonitor', $activity->id);
|
||||
} else if ($this->database->type() === 'standalone-mysql') {
|
||||
$activity = StartMysql::run($this->database);
|
||||
$this->dispatch('newMonitorActivity', $activity->id);
|
||||
$this->dispatch('activityMonitor', $activity->id);
|
||||
} else if ($this->database->type() === 'standalone-mariadb') {
|
||||
$activity = StartMariadb::run($this->database);
|
||||
$this->dispatch('newMonitorActivity', $activity->id);
|
||||
$this->dispatch('activityMonitor', $activity->id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ class Import extends Component
|
||||
|
||||
if (!empty($this->importCommands)) {
|
||||
$activity = remote_process($this->importCommands, $this->server, ignore_errors: true);
|
||||
$this->dispatch('newMonitorActivity', $activity->id);
|
||||
$this->dispatch('activityMonitor', $activity->id);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$this->validated = false;
|
||||
|
||||
@@ -9,6 +9,7 @@ class DeleteEnvironment extends Component
|
||||
{
|
||||
public array $parameters;
|
||||
public int $environment_id;
|
||||
public bool $disabled = false;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
|
||||
@@ -9,6 +9,7 @@ class DeleteProject extends Component
|
||||
{
|
||||
public array $parameters;
|
||||
public int $project_id;
|
||||
public bool $disabled = false;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
|
||||
@@ -9,6 +9,7 @@ use App\Models\PrivateKey;
|
||||
use App\Models\Project;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\SwarmDocker;
|
||||
use Illuminate\Support\Collection;
|
||||
use Livewire\Component;
|
||||
use Spatie\Url\Url;
|
||||
use Illuminate\Support\Str;
|
||||
@@ -18,7 +19,7 @@ class GithubPrivateRepositoryDeployKey extends Component
|
||||
public $current_step = 'private_keys';
|
||||
public $parameters;
|
||||
public $query;
|
||||
public $private_keys;
|
||||
public $private_keys =[];
|
||||
public int $private_key_id;
|
||||
|
||||
public int $port = 3000;
|
||||
@@ -33,6 +34,11 @@ class GithubPrivateRepositoryDeployKey extends Component
|
||||
public $build_pack = 'nixpacks';
|
||||
public bool $show_is_static = true;
|
||||
|
||||
private object $repository_url_parsed;
|
||||
private GithubApp|GitlabApp|string $git_source = 'other';
|
||||
private ?string $git_host = null;
|
||||
private string $git_repository;
|
||||
|
||||
protected $rules = [
|
||||
'repository_url' => 'required',
|
||||
'branch' => 'required|string',
|
||||
@@ -49,10 +55,7 @@ class GithubPrivateRepositoryDeployKey extends Component
|
||||
'publish_directory' => 'Publish directory',
|
||||
'build_pack' => 'Build pack',
|
||||
];
|
||||
private object $repository_url_parsed;
|
||||
private GithubApp|GitlabApp|string $git_source = 'other';
|
||||
private ?string $git_host = null;
|
||||
private string $git_repository;
|
||||
|
||||
|
||||
public function mount()
|
||||
{
|
||||
|
||||
@@ -29,7 +29,8 @@ class Index extends Component
|
||||
}
|
||||
$this->project = $project;
|
||||
$this->environment = $environment;
|
||||
$this->applications = $environment->applications->sortBy('name');
|
||||
|
||||
$this->applications = $this->environment->applications->load(['tags']);
|
||||
$this->applications = $this->applications->map(function ($application) {
|
||||
if (data_get($application, 'environment.project.uuid')) {
|
||||
$application->hrefLink = route('project.application.configuration', [
|
||||
@@ -40,8 +41,8 @@ class Index extends Component
|
||||
}
|
||||
return $application;
|
||||
});
|
||||
$this->postgresqls = $environment->postgresqls->sortBy('name');
|
||||
$this->postgresqls = $this->postgresqls->map(function ($postgresql) {
|
||||
$this->postgresqls = $this->environment->postgresqls->load(['tags'])->sortBy('name');
|
||||
$this->postgresqls = $this->postgresqls->map(function ($postgresql) {
|
||||
if (data_get($postgresql, 'environment.project.uuid')) {
|
||||
$postgresql->hrefLink = route('project.database.configuration', [
|
||||
'project_uuid' => data_get($postgresql, 'environment.project.uuid'),
|
||||
@@ -51,7 +52,7 @@ class Index extends Component
|
||||
}
|
||||
return $postgresql;
|
||||
});
|
||||
$this->redis = $environment->redis->sortBy('name');
|
||||
$this->redis = $this->environment->redis->load(['tags'])->sortBy('name');
|
||||
$this->redis = $this->redis->map(function ($redis) {
|
||||
if (data_get($redis, 'environment.project.uuid')) {
|
||||
$redis->hrefLink = route('project.database.configuration', [
|
||||
@@ -62,7 +63,7 @@ class Index extends Component
|
||||
}
|
||||
return $redis;
|
||||
});
|
||||
$this->mongodbs = $environment->mongodbs->sortBy('name');
|
||||
$this->mongodbs = $this->environment->mongodbs->load(['tags'])->sortBy('name');
|
||||
$this->mongodbs = $this->mongodbs->map(function ($mongodb) {
|
||||
if (data_get($mongodb, 'environment.project.uuid')) {
|
||||
$mongodb->hrefLink = route('project.database.configuration', [
|
||||
@@ -73,7 +74,7 @@ class Index extends Component
|
||||
}
|
||||
return $mongodb;
|
||||
});
|
||||
$this->mysqls = $environment->mysqls->sortBy('name');
|
||||
$this->mysqls = $this->environment->mysqls->load(['tags'])->sortBy('name');
|
||||
$this->mysqls = $this->mysqls->map(function ($mysql) {
|
||||
if (data_get($mysql, 'environment.project.uuid')) {
|
||||
$mysql->hrefLink = route('project.database.configuration', [
|
||||
@@ -84,7 +85,7 @@ class Index extends Component
|
||||
}
|
||||
return $mysql;
|
||||
});
|
||||
$this->mariadbs = $environment->mariadbs->sortBy('name');
|
||||
$this->mariadbs = $this->environment->mariadbs->load(['tags'])->sortBy('name');
|
||||
$this->mariadbs = $this->mariadbs->map(function ($mariadb) {
|
||||
if (data_get($mariadb, 'environment.project.uuid')) {
|
||||
$mariadb->hrefLink = route('project.database.configuration', [
|
||||
@@ -95,7 +96,7 @@ class Index extends Component
|
||||
}
|
||||
return $mariadb;
|
||||
});
|
||||
$this->services = $environment->services->sortBy('name');
|
||||
$this->services = $this->environment->services->load(['tags'])->sortBy('name');
|
||||
$this->services = $this->services->map(function ($service) {
|
||||
if (data_get($service, 'environment.project.uuid')) {
|
||||
$service->hrefLink = route('project.service.configuration', [
|
||||
|
||||
@@ -8,7 +8,7 @@ use Livewire\Component;
|
||||
|
||||
class Configuration extends Component
|
||||
{
|
||||
public Service $service;
|
||||
public ?Service $service = null;
|
||||
public $applications;
|
||||
public $databases;
|
||||
public array $parameters;
|
||||
|
||||
@@ -41,7 +41,7 @@ class FileStorage extends Component
|
||||
$this->fileStorage->content = null;
|
||||
}
|
||||
$this->fileStorage->save();
|
||||
$this->fileStorage->saveStorageOnServer($this->service);
|
||||
$this->fileStorage->saveStorageOnServer();
|
||||
$this->dispatch('success', 'File updated successfully.');
|
||||
} catch (\Throwable $e) {
|
||||
$this->fileStorage->setRawAttributes($original);
|
||||
|
||||
@@ -27,12 +27,12 @@ class Index extends Component
|
||||
$this->parameters = get_route_parameters();
|
||||
$this->query = request()->query();
|
||||
$this->service = Service::whereUuid($this->parameters['service_uuid'])->firstOrFail();
|
||||
$service = $this->service->applications()->whereName($this->parameters['service_name'])->first();
|
||||
$service = $this->service->applications()->whereUuid($this->parameters['stack_service_uuid'])->first();
|
||||
if ($service) {
|
||||
$this->serviceApplication = $service;
|
||||
$this->serviceApplication->getFilesFromServer();
|
||||
} else {
|
||||
$this->serviceDatabase = $this->service->databases()->whereName($this->parameters['service_name'])->first();
|
||||
$this->serviceDatabase = $this->service->databases()->whereUuid($this->parameters['stack_service_uuid'])->first();
|
||||
$this->serviceDatabase->getFilesFromServer();
|
||||
}
|
||||
$this->s3s = currentTeam()->s3s;
|
||||
|
||||
@@ -57,7 +57,7 @@ class Navbar extends Component
|
||||
}
|
||||
$this->service->parse();
|
||||
$activity = StartService::run($this->service);
|
||||
$this->dispatch('newMonitorActivity', $activity->id);
|
||||
$this->dispatch('activityMonitor', $activity->id);
|
||||
}
|
||||
public function stop(bool $forceCleanup = false)
|
||||
{
|
||||
@@ -82,6 +82,6 @@ class Navbar extends Component
|
||||
StopService::run($this->service);
|
||||
$this->service->parse();
|
||||
$activity = StartService::run($this->service);
|
||||
$this->dispatch('newMonitorActivity', $activity->id);
|
||||
$this->dispatch('activityMonitor', $activity->id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace App\Livewire\Project\Service;
|
||||
use App\Models\ServiceApplication;
|
||||
use Livewire\Component;
|
||||
|
||||
class Application extends Component
|
||||
class ServiceApplicationView extends Component
|
||||
{
|
||||
public ServiceApplication $application;
|
||||
public $parameters;
|
||||
@@ -20,7 +20,7 @@ class Application extends Component
|
||||
];
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.service.application');
|
||||
return view('livewire.project.service.service-application-view');
|
||||
}
|
||||
public function instantSave()
|
||||
{
|
||||
@@ -53,6 +53,7 @@ class Application extends Component
|
||||
public function submit()
|
||||
{
|
||||
try {
|
||||
check_fqdn_usage($this->application);
|
||||
$this->validate();
|
||||
$this->application->save();
|
||||
updateCompose($this->application);
|
||||
@@ -17,14 +17,15 @@ class Danger extends Component
|
||||
{
|
||||
$this->modalId = new Cuid2(7);
|
||||
$parameters = get_route_parameters();
|
||||
$this->projectUuid = $parameters['project_uuid'];
|
||||
$this->environmentName = $parameters['environment_name'];
|
||||
$this->projectUuid = data_get($parameters, 'project_uuid');
|
||||
$this->environmentName = data_get($parameters, 'environment_name');
|
||||
}
|
||||
|
||||
public function delete()
|
||||
{
|
||||
try {
|
||||
DeleteResourceJob::dispatchSync($this->resource);
|
||||
$this->resource->delete();
|
||||
DeleteResourceJob::dispatch($this->resource);
|
||||
return redirect()->route('project.resource.index', [
|
||||
'project_uuid' => $this->projectUuid,
|
||||
'environment_name' => $this->environmentName
|
||||
|
||||
@@ -2,11 +2,83 @@
|
||||
|
||||
namespace App\Livewire\Project\Shared;
|
||||
|
||||
use App\Actions\Application\StopApplicationOneServer;
|
||||
use App\Events\ApplicationStatusChanged;
|
||||
use App\Models\Server;
|
||||
use App\Models\StandaloneDocker;
|
||||
use Livewire\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class Destination extends Component
|
||||
{
|
||||
public $resource;
|
||||
public $servers = [];
|
||||
public $additional_servers = [];
|
||||
public $networks = [];
|
||||
|
||||
public function getListeners()
|
||||
{
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
return [
|
||||
"echo-private:team.{$teamId},ApplicationStatusChanged" => 'loadData',
|
||||
];
|
||||
}
|
||||
public function mount()
|
||||
{
|
||||
$this->loadData();
|
||||
}
|
||||
public function loadData()
|
||||
{
|
||||
$all_networks = collect([]);
|
||||
$all_networks = $all_networks->push($this->resource->destination);
|
||||
$all_networks = $all_networks->merge($this->resource->additional_networks);
|
||||
|
||||
$this->networks = Server::isUsable()->get()->map(function ($server) {
|
||||
return $server->standaloneDockers;
|
||||
})->flatten();
|
||||
$this->networks = $this->networks->reject(function ($network) use ($all_networks) {
|
||||
return $all_networks->pluck('id')->contains($network->id);
|
||||
});
|
||||
}
|
||||
public function redeploy(int $network_id, int $server_id)
|
||||
{
|
||||
if ($this->resource->additional_servers->count() > 0 && str($this->resource->docker_registry_image_name)->isEmpty()) {
|
||||
$this->dispatch('error', 'Failed to deploy.', 'Before deploying to multiple servers, you must first set a Docker image in the General tab.<br>More information here: <a target="_blank" class="underline" href="https://coolify.io/docs/server/multiple-servers">documentation</a>');
|
||||
return;
|
||||
}
|
||||
$deployment_uuid = new Cuid2(7);
|
||||
$server = Server::find($server_id);
|
||||
$destination = StandaloneDocker::find($network_id);
|
||||
queue_application_deployment(
|
||||
deployment_uuid: $deployment_uuid,
|
||||
application: $this->resource,
|
||||
server: $server,
|
||||
destination: $destination,
|
||||
no_questions_asked: true,
|
||||
);
|
||||
return redirect()->route('project.application.deployment.show', [
|
||||
'project_uuid' => data_get($this->resource, 'environment.project.uuid'),
|
||||
'application_uuid' => data_get($this->resource, 'uuid'),
|
||||
'deployment_uuid' => $deployment_uuid,
|
||||
'environment_name' => data_get($this->resource, 'environment.name'),
|
||||
]);
|
||||
}
|
||||
public function addServer(int $network_id, int $server_id)
|
||||
{
|
||||
$this->resource->additional_networks()->attach($network_id, ['server_id' => $server_id]);
|
||||
$this->resource->load(['additional_networks']);
|
||||
ApplicationStatusChanged::dispatch(data_get($this->resource, 'environment.project.team.id'));
|
||||
$this->loadData();
|
||||
}
|
||||
public function removeServer(int $network_id, int $server_id)
|
||||
{
|
||||
if ($this->resource->destination->server->id == $server_id) {
|
||||
$this->dispatch('error', 'You cannot remove this destination server.', 'You are trying to remove the main server.');
|
||||
return;
|
||||
}
|
||||
$server = Server::find($server_id);
|
||||
StopApplicationOneServer::run($this->resource, $server);
|
||||
$this->resource->additional_networks()->detach($network_id, ['server_id' => $server_id]);
|
||||
$this->resource->load(['additional_networks']);
|
||||
ApplicationStatusChanged::dispatch(data_get($this->resource, 'environment.project.team.id'));
|
||||
$this->loadData();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,13 @@ class Add extends Component
|
||||
public function submit()
|
||||
{
|
||||
$this->validate();
|
||||
if (str($this->value)->startsWith('{{') && str($this->value)->endsWith('}}')) {
|
||||
$type = str($this->value)->after("{{")->before(".")->value;
|
||||
if (!collect(SHARED_VARIABLE_TYPES)->contains($type)) {
|
||||
$this->dispatch('error', 'Invalid shared variable type.', "Valid types are: team, project, environment.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
$this->dispatch('saveKey', [
|
||||
'key' => $this->key,
|
||||
'value' => $this->value,
|
||||
|
||||
@@ -71,12 +71,26 @@ class All extends Component
|
||||
continue;
|
||||
}
|
||||
$found->value = $variable;
|
||||
if (str($found->value)->startsWith('{{') && str($found->value)->endsWith('}}')) {
|
||||
$type = str($found->value)->after("{{")->before(".")->value;
|
||||
if (!collect(SHARED_VARIABLE_TYPES)->contains($type)) {
|
||||
$this->dispatch('error', 'Invalid shared variable type.', "Valid types are: team, project, environment.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
$found->save();
|
||||
continue;
|
||||
} else {
|
||||
$environment = new EnvironmentVariable();
|
||||
$environment->key = $key;
|
||||
$environment->value = $variable;
|
||||
if (str($environment->value)->startsWith('{{') && str($environment->value)->endsWith('}}')) {
|
||||
$type = str($environment->value)->after("{{")->before(".")->value;
|
||||
if (!collect(SHARED_VARIABLE_TYPES)->contains($type)) {
|
||||
$this->dispatch('error', 'Invalid shared variable type.', "Valid types are: team, project, environment.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
$environment->is_build_time = false;
|
||||
$environment->is_preview = $isPreview ? true : false;
|
||||
switch ($this->resource->type()) {
|
||||
@@ -157,7 +171,7 @@ class All extends Component
|
||||
}
|
||||
$environment->save();
|
||||
$this->refreshEnvs();
|
||||
$this->dispatch('success', 'Environment variable added successfully.');
|
||||
$this->dispatch('success', 'Environment variable added.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
@@ -50,7 +50,8 @@ class Show extends Component
|
||||
$this->isLocked = true;
|
||||
}
|
||||
}
|
||||
public function serialize() {
|
||||
public function serialize()
|
||||
{
|
||||
data_forget($this->env, 'real_value');
|
||||
if ($this->env->getMorphClass() === 'App\Models\SharedEnvironmentVariable') {
|
||||
data_forget($this->env, 'is_build_time');
|
||||
@@ -80,11 +81,18 @@ class Show extends Component
|
||||
} else {
|
||||
$this->validate();
|
||||
}
|
||||
if (str($this->env->value)->startsWith('{{') && str($this->env->value)->endsWith('}}')) {
|
||||
$type = str($this->env->value)->after("{{")->before(".")->value;
|
||||
if (!collect(SHARED_VARIABLE_TYPES)->contains($type)) {
|
||||
$this->dispatch('error', 'Invalid shared variable type.', "Valid types are: team, project, environment.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
$this->serialize();
|
||||
$this->env->save();
|
||||
$this->dispatch('success', 'Environment variable updated successfully.');
|
||||
$this->dispatch('refreshEnvs');
|
||||
} catch(\Exception $e) {
|
||||
} catch (\Exception $e) {
|
||||
return handleError($e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ class ExecuteContainerCommand extends Component
|
||||
$exec = "docker exec {$this->container} {$cmd}";
|
||||
}
|
||||
$activity = remote_process([$exec], $this->server, ignore_errors: true);
|
||||
$this->dispatch('newMonitorActivity', $activity->id);
|
||||
$this->dispatch('activityMonitor', $activity->id);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
@@ -20,8 +20,8 @@ class ResourceOperations extends Component
|
||||
public function mount()
|
||||
{
|
||||
$parameters = get_route_parameters();
|
||||
$this->projectUuid = $parameters['project_uuid'];
|
||||
$this->environmentName = $parameters['environment_name'];
|
||||
$this->projectUuid = data_get($parameters, 'project_uuid');
|
||||
$this->environmentName = data_get($parameters, 'environment_name');
|
||||
$this->projects = Project::ownedByCurrentTeam()->get();
|
||||
$this->servers = currentTeam()->servers;
|
||||
}
|
||||
@@ -45,6 +45,11 @@ class ResourceOperations extends Component
|
||||
'destination_id' => $new_destination->id,
|
||||
]);
|
||||
$new_resource->save();
|
||||
if ($new_resource->destination->server->proxyType() === 'TRAEFIK_V2') {
|
||||
$customLabels = str(implode("|", generateLabelsApplication($new_resource)))->replace("|", "\n");
|
||||
$new_resource->custom_labels = base64_encode($customLabels);
|
||||
$new_resource->save();
|
||||
}
|
||||
$environmentVaribles = $this->resource->environment_variables()->get();
|
||||
foreach ($environmentVaribles as $environmentVarible) {
|
||||
$newEnvironmentVariable = $environmentVarible->replicate()->fill([
|
||||
|
||||
90
app/Livewire/Project/Shared/Tags.php
Normal file
90
app/Livewire/Project/Shared/Tags.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Project\Shared;
|
||||
|
||||
use App\Models\Tag;
|
||||
use Livewire\Component;
|
||||
|
||||
class Tags extends Component
|
||||
{
|
||||
public $resource = null;
|
||||
public ?string $new_tag = null;
|
||||
public $tags = [];
|
||||
protected $listeners = [
|
||||
'refresh' => '$refresh',
|
||||
];
|
||||
protected $rules = [
|
||||
'resource.tags.*.name' => 'required|string|min:2',
|
||||
'new_tag' => 'required|string|min:2'
|
||||
];
|
||||
protected $validationAttributes = [
|
||||
'new_tag' => 'tag'
|
||||
];
|
||||
public function mount()
|
||||
{
|
||||
$this->tags = Tag::ownedByCurrentTeam()->get();
|
||||
}
|
||||
public function addTag(string $id, string $name)
|
||||
{
|
||||
try {
|
||||
if ($this->resource->tags()->where('id', $id)->exists()) {
|
||||
$this->dispatch('error', 'Duplicate tags.', "Tag <span class='text-warning'>$name</span> already added.");
|
||||
return;
|
||||
}
|
||||
$this->resource->tags()->syncWithoutDetaching($id);
|
||||
$this->refresh();
|
||||
} catch (\Exception $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
public function deleteTag(string $id)
|
||||
{
|
||||
try {
|
||||
$this->resource->tags()->detach($id);
|
||||
|
||||
$found_more_tags = Tag::where(['id' => $id, 'team_id' => currentTeam()->id])->first();
|
||||
if ($found_more_tags->applications()->count() == 0 && $found_more_tags->services()->count() == 0){
|
||||
$found_more_tags->delete();
|
||||
}
|
||||
$this->refresh();
|
||||
} catch (\Exception $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
public function refresh()
|
||||
{
|
||||
$this->resource->load(['tags']);
|
||||
$this->tags = Tag::ownedByCurrentTeam()->get();
|
||||
$this->new_tag = null;
|
||||
}
|
||||
public function submit()
|
||||
{
|
||||
try {
|
||||
$this->validate([
|
||||
'new_tag' => 'required|string|min:2'
|
||||
]);
|
||||
$tags = str($this->new_tag)->trim()->explode(' ');
|
||||
foreach ($tags as $tag) {
|
||||
if ($this->resource->tags()->where('name', $tag)->exists()) {
|
||||
$this->dispatch('error', 'Duplicate tags.', "Tag <span class='text-warning'>$tag</span> already added.");
|
||||
continue;
|
||||
}
|
||||
$found = Tag::where(['name' => $tag, 'team_id' => currentTeam()->id])->first();
|
||||
if (!$found) {
|
||||
$found = Tag::create([
|
||||
'name' => $tag,
|
||||
'team_id' => currentTeam()->id
|
||||
]);
|
||||
}
|
||||
$this->resource->tags()->syncWithoutDetaching($found->id);
|
||||
}
|
||||
$this->refresh();
|
||||
} catch (\Exception $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.shared.tags');
|
||||
}
|
||||
}
|
||||
@@ -10,9 +10,11 @@ class Webhooks extends Component
|
||||
public ?string $deploywebhook = null;
|
||||
public ?string $githubManualWebhook = null;
|
||||
public ?string $gitlabManualWebhook = null;
|
||||
public ?string $bitbucketManualWebhook = null;
|
||||
protected $rules = [
|
||||
'resource.manual_webhook_secret_github' => 'nullable|string',
|
||||
'resource.manual_webhook_secret_gitlab' => 'nullable|string',
|
||||
'resource.manual_webhook_secret_bitbucket' => 'nullable|string',
|
||||
];
|
||||
public function saveSecret()
|
||||
{
|
||||
@@ -29,6 +31,7 @@ class Webhooks extends Component
|
||||
$this->deploywebhook = generateDeployWebhook($this->resource);
|
||||
$this->githubManualWebhook = generateGitManualWebhook($this->resource, 'github');
|
||||
$this->gitlabManualWebhook = generateGitManualWebhook($this->resource, 'gitlab');
|
||||
$this->bitbucketManualWebhook = generateGitManualWebhook($this->resource, 'bitbucket');
|
||||
}
|
||||
public function render()
|
||||
{
|
||||
|
||||
@@ -31,7 +31,7 @@ class RunCommand extends Component
|
||||
$this->validate();
|
||||
try {
|
||||
$activity = remote_process([$this->command], Server::where('uuid', $this->server)->first(), ignore_errors: true);
|
||||
$this->dispatch('newMonitorActivity', $activity->id);
|
||||
$this->dispatch('activityMonitor', $activity->id);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Livewire\Server;
|
||||
|
||||
use App\Actions\Server\InstallDocker;
|
||||
use App\Models\Server;
|
||||
use Livewire\Component;
|
||||
|
||||
@@ -14,7 +13,8 @@ class Form extends Component
|
||||
public ?string $wildcard_domain = null;
|
||||
public int $cleanup_after_percentage;
|
||||
public bool $dockerInstallationStarted = false;
|
||||
protected $listeners = ['serverRefresh'];
|
||||
|
||||
protected $listeners = ['serverInstalled'];
|
||||
|
||||
protected $rules = [
|
||||
'server.name' => 'required',
|
||||
@@ -28,6 +28,7 @@ class Form extends Component
|
||||
'server.settings.is_swarm_worker' => 'required|boolean',
|
||||
'server.settings.is_build_server' => 'required|boolean',
|
||||
'server.settings.concurrent_builds' => 'required|integer|min:1',
|
||||
'server.settings.dynamic_timeout' => 'required|integer|min:1',
|
||||
'wildcard_domain' => 'nullable|url',
|
||||
];
|
||||
protected $validationAttributes = [
|
||||
@@ -42,6 +43,8 @@ class Form extends Component
|
||||
'server.settings.is_swarm_worker' => 'Swarm Worker',
|
||||
'server.settings.is_build_server' => 'Build Server',
|
||||
'server.settings.concurrent_builds' => 'Concurrent Builds',
|
||||
'server.settings.dynamic_timeout' => 'Dynamic Timeout',
|
||||
|
||||
];
|
||||
|
||||
public function mount()
|
||||
@@ -49,9 +52,10 @@ class Form extends Component
|
||||
$this->wildcard_domain = $this->server->settings->wildcard_domain;
|
||||
$this->cleanup_after_percentage = $this->server->settings->cleanup_after_percentage;
|
||||
}
|
||||
public function serverRefresh($install = true)
|
||||
public function serverInstalled()
|
||||
{
|
||||
$this->validateServer($install);
|
||||
$this->server->refresh();
|
||||
$this->server->settings->refresh();
|
||||
}
|
||||
public function instantSave()
|
||||
{
|
||||
@@ -64,13 +68,6 @@ class Form extends Component
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
public function installDocker()
|
||||
{
|
||||
$this->dispatch('installDocker');
|
||||
$this->dockerInstallationStarted = true;
|
||||
$activity = InstallDocker::run($this->server);
|
||||
$this->dispatch('newMonitorActivity', $activity->id);
|
||||
}
|
||||
public function checkLocalhostConnection()
|
||||
{
|
||||
$uptime = $this->server->validateConnection();
|
||||
@@ -80,48 +77,13 @@ class Form extends Component
|
||||
$this->server->settings->is_usable = true;
|
||||
$this->server->settings->save();
|
||||
} else {
|
||||
$this->dispatch('error', 'Server is not reachable.<br>Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/server/openssh">documentation</a> for further help.');
|
||||
$this->dispatch('error', 'Server is not reachable.', 'Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/server/openssh">documentation</a> for further help.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
public function validateServer($install = true)
|
||||
{
|
||||
try {
|
||||
$uptime = $this->server->validateConnection();
|
||||
if (!$uptime) {
|
||||
$install && $this->dispatch('error', 'Server is not reachable.<br>Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/server/openssh">documentation</a> for further help.');
|
||||
return;
|
||||
}
|
||||
$supported_os_type = $this->server->validateOS();
|
||||
if (!$supported_os_type) {
|
||||
$install && $this->dispatch('error', 'Server OS type is not supported for automated installation. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://docs.docker.com/engine/install/#server">documentation</a>.');
|
||||
return;
|
||||
}
|
||||
$dockerInstalled = $this->server->validateDockerEngine();
|
||||
if ($dockerInstalled) {
|
||||
$install && $this->dispatch('success', 'Docker Engine is installed.<br> Checking version.');
|
||||
} else {
|
||||
$install && $this->installDocker();
|
||||
return;
|
||||
}
|
||||
$dockerVersion = $this->server->validateDockerEngineVersion();
|
||||
if ($dockerVersion) {
|
||||
$install && $this->dispatch('success', 'Docker Engine version is 22+.');
|
||||
} else {
|
||||
$install && $this->installDocker();
|
||||
return;
|
||||
}
|
||||
if ($this->server->isSwarm()) {
|
||||
$swarmInstalled = $this->server->validateDockerSwarm();
|
||||
if ($swarmInstalled) {
|
||||
$install && $this->dispatch('success', 'Docker Swarm is initiated.');
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
} finally {
|
||||
$this->dispatch('proxyStatusUpdated');
|
||||
}
|
||||
$this->dispatch('validateServer', $install);
|
||||
}
|
||||
|
||||
public function submit()
|
||||
|
||||
@@ -71,7 +71,7 @@ class Deploy extends Component
|
||||
{
|
||||
try {
|
||||
$activity = StartProxy::run($this->server);
|
||||
$this->dispatch('newMonitorActivity', $activity->id, ProxyStatusChanged::class);
|
||||
$this->dispatch('activityMonitor', $activity->id, ProxyStatusChanged::class);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
@@ -12,10 +12,8 @@ class Status extends Component
|
||||
public Server $server;
|
||||
public bool $polling = false;
|
||||
public int $numberOfPolls = 0;
|
||||
protected $listeners = ['proxyStatusUpdated' => '$refresh', 'startProxyPolling'];
|
||||
|
||||
protected $listeners = ['proxyStatusUpdated', 'startProxyPolling'];
|
||||
public function mount() {
|
||||
}
|
||||
public function startProxyPolling()
|
||||
{
|
||||
$this->checkProxy();
|
||||
|
||||
@@ -11,6 +11,7 @@ class Show extends Component
|
||||
use AuthorizesRequests;
|
||||
public ?Server $server = null;
|
||||
public $parameters = [];
|
||||
protected $listeners = ['serverInstalled' => '$refresh'];
|
||||
public function mount()
|
||||
{
|
||||
$this->parameters = get_route_parameters();
|
||||
@@ -19,14 +20,13 @@ class Show extends Component
|
||||
if (is_null($this->server)) {
|
||||
return redirect()->route('server.index');
|
||||
}
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
public function submit()
|
||||
{
|
||||
$this->dispatch('serverRefresh',false);
|
||||
$this->dispatch('serverRefresh', false);
|
||||
}
|
||||
public function render()
|
||||
{
|
||||
|
||||
@@ -39,7 +39,7 @@ class ShowPrivateKey extends Component
|
||||
if ($uptime) {
|
||||
$this->dispatch('success', 'Server is reachable.');
|
||||
} else {
|
||||
$this->dispatch('error', 'Server is not reachable.<br>Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/configuration#openssh-server">documentation</a> for further help.');
|
||||
$this->dispatch('error', 'Server is not reachable.<br>Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/server/openssh#openssh">documentation</a> for further help.');
|
||||
return;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
|
||||
116
app/Livewire/Server/ValidateAndInstall.php
Normal file
116
app/Livewire/Server/ValidateAndInstall.php
Normal file
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Server;
|
||||
|
||||
use App\Actions\Proxy\StartProxy;
|
||||
use App\Models\Server;
|
||||
use Livewire\Component;
|
||||
|
||||
class ValidateAndInstall extends Component
|
||||
{
|
||||
public Server $server;
|
||||
public int $number_of_tries = 0;
|
||||
public int $max_tries = 1;
|
||||
public bool $install = true;
|
||||
public $uptime = null;
|
||||
public $supported_os_type = null;
|
||||
public $docker_installed = null;
|
||||
public $docker_compose_installed = null;
|
||||
public $docker_version = null;
|
||||
public $proxy_started = false;
|
||||
public $error = null;
|
||||
|
||||
protected $listeners = ['validateServer' => 'init', 'validateDockerEngine', 'validateServerNow' => 'validateServer'];
|
||||
|
||||
public function init(bool $install = true)
|
||||
{
|
||||
|
||||
$this->install = $install;
|
||||
$this->uptime = null;
|
||||
$this->supported_os_type = null;
|
||||
$this->docker_installed = null;
|
||||
$this->docker_version = null;
|
||||
$this->docker_compose_installed = null;
|
||||
$this->proxy_started = null;
|
||||
$this->error = null;
|
||||
$this->number_of_tries = 0;
|
||||
$this->dispatch('validateServerNow');
|
||||
}
|
||||
|
||||
public function validateServer()
|
||||
{
|
||||
try {
|
||||
$this->validateConnection();
|
||||
$this->validateOS();
|
||||
$this->validateDockerEngine();
|
||||
|
||||
if ($this->server->isSwarm()) {
|
||||
$swarmInstalled = $this->server->validateDockerSwarm();
|
||||
if ($swarmInstalled) {
|
||||
$this->dispatch('success', 'Docker Swarm is initiated.');
|
||||
}
|
||||
} else {
|
||||
$proxy = StartProxy::run($this->server);
|
||||
if ($proxy) {
|
||||
$this->proxy_started = true;
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
public function validateConnection()
|
||||
{
|
||||
$this->uptime = $this->server->validateConnection();
|
||||
if (!$this->uptime) {
|
||||
$this->error = 'Server is not reachable. Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/server/openssh">documentation</a> for further help.';
|
||||
return;
|
||||
}
|
||||
}
|
||||
public function validateOS()
|
||||
{
|
||||
$this->supported_os_type = $this->server->validateOS();
|
||||
if (!$this->supported_os_type) {
|
||||
$this->error = 'Server OS type is not supported. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://docs.docker.com/engine/install/#server">documentation</a>.';
|
||||
return;
|
||||
}
|
||||
}
|
||||
public function validateDockerEngine()
|
||||
{
|
||||
$this->docker_installed = $this->server->validateDockerEngine();
|
||||
$this->docker_compose_installed = $this->server->validateDockerCompose();
|
||||
if (!$this->docker_installed || !$this->docker_compose_installed) {
|
||||
if ($this->install) {
|
||||
if ($this->number_of_tries == $this->max_tries) {
|
||||
$this->error = 'Docker Engine could not be installed. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://docs.docker.com/engine/install/#server">documentation</a>.';
|
||||
return;
|
||||
} else {
|
||||
$activity = $this->server->installDocker();
|
||||
$this->number_of_tries++;
|
||||
$this->dispatch('newActivityMonitor', $activity->id, 'validateDockerEngine');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
$this->error = 'Docker Engine is not installed. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://docs.docker.com/engine/install/#server">documentation</a>.';
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
$this->validateDockerVersion();
|
||||
}
|
||||
}
|
||||
public function validateDockerVersion()
|
||||
{
|
||||
$this->docker_version = $this->server->validateDockerEngineVersion();
|
||||
if ($this->docker_version) {
|
||||
$this->dispatch('serverInstalled');
|
||||
$this->dispatch('success', 'Server validated successfully.');
|
||||
} else {
|
||||
$this->error = 'Docker Engine version is not 22+. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://docs.docker.com/engine/install/#server">documentation</a>.';
|
||||
return;
|
||||
}
|
||||
}
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.server.validate-and-install');
|
||||
}
|
||||
}
|
||||
18
app/Livewire/Tags/Index.php
Normal file
18
app/Livewire/Tags/Index.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Tags;
|
||||
|
||||
use App\Models\Tag;
|
||||
use Livewire\Component;
|
||||
|
||||
class Index extends Component
|
||||
{
|
||||
public $tags = [];
|
||||
public function mount() {
|
||||
$this->tags = Tag::where('team_id', currentTeam()->id)->get()->unique('name')->sortBy('name');
|
||||
}
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.tags.index');
|
||||
}
|
||||
}
|
||||
72
app/Livewire/Tags/Show.php
Normal file
72
app/Livewire/Tags/Show.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Tags;
|
||||
|
||||
use App\Http\Controllers\Api\Deploy;
|
||||
use App\Models\ApplicationDeploymentQueue;
|
||||
use App\Models\Tag;
|
||||
use Livewire\Component;
|
||||
|
||||
class Show extends Component
|
||||
{
|
||||
public $tags;
|
||||
public Tag $tag;
|
||||
public $applications;
|
||||
public $services;
|
||||
public $webhook = null;
|
||||
public $deployments_per_tag_per_server = [];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->tags = Tag::ownedByCurrentTeam()->get()->unique('name')->sortBy('name');
|
||||
$tag = $this->tags->where('name', request()->tag_name)->first();
|
||||
if (!$tag) {
|
||||
return redirect()->route('tags.index');
|
||||
}
|
||||
$this->webhook = generatTagDeployWebhook($tag->name);
|
||||
$this->applications = $tag->applications()->get();
|
||||
$this->services = $tag->services()->get();
|
||||
$this->tag = $tag;
|
||||
$this->get_deployments();
|
||||
}
|
||||
public function get_deployments()
|
||||
{
|
||||
try {
|
||||
$resource_ids = $this->applications->pluck('id');
|
||||
$this->deployments_per_tag_per_server = ApplicationDeploymentQueue::whereIn("status", ["in_progress", "queued"])->whereIn('application_id', $resource_ids)->get([
|
||||
"id",
|
||||
"application_id",
|
||||
"application_name",
|
||||
"deployment_url",
|
||||
"pull_request_id",
|
||||
"server_name",
|
||||
"server_id",
|
||||
"status"
|
||||
])->sortBy('id')->groupBy('server_name')->toArray();
|
||||
} catch (\Exception $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
public function redeploy_all()
|
||||
{
|
||||
try {
|
||||
$message = collect([]);
|
||||
$this->applications->each(function ($resource) use ($message) {
|
||||
$deploy = new Deploy();
|
||||
$message->push($deploy->deploy_resource($resource));
|
||||
});
|
||||
$this->services->each(function ($resource) use ($message) {
|
||||
$deploy = new Deploy();
|
||||
$message->push($deploy->deploy_resource($resource));
|
||||
});
|
||||
$this->dispatch('success', 'Mass deployment started.');
|
||||
} catch (\Exception $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.tags.show');
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,11 @@ class Invitations extends Component
|
||||
|
||||
public function deleteInvitation(int $invitation_id)
|
||||
{
|
||||
TeamInvitation::find($invitation_id)->delete();
|
||||
$initiation_found = TeamInvitation::find($invitation_id);
|
||||
if (!$initiation_found) {
|
||||
return $this->dispatch('error', 'Invitation not found.');
|
||||
}
|
||||
$initiation_found->delete();
|
||||
$this->refreshInvitations();
|
||||
$this->dispatch('success', 'Invitation revoked.');
|
||||
}
|
||||
|
||||
@@ -67,7 +67,8 @@ class Create extends Component
|
||||
$this->storage->save();
|
||||
return redirect()->route('team.storage.show', $this->storage->uuid);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
$this->dispatch('error', 'Failed to create storage.', $e->getMessage());
|
||||
// return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,9 +33,9 @@ class Form extends Component
|
||||
{
|
||||
try {
|
||||
$this->storage->testConnection(shouldSave: true);
|
||||
return $this->dispatch('success', 'Connection is working. Tested with "ListObjectsV2" action.');
|
||||
return $this->dispatch('success', 'Connection is working.', 'Tested with "ListObjectsV2" action.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
$this->dispatch('error', 'Failed to create storage.', $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,18 +6,15 @@ use App\Enums\ApplicationDeploymentStatus;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Collection;
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
use Illuminate\Support\Str;
|
||||
use RuntimeException;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class Application extends BaseModel
|
||||
{
|
||||
use SoftDeletes;
|
||||
protected $guarded = [];
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
static::saving(function ($application) {
|
||||
@@ -39,6 +36,7 @@ class Application extends BaseModel
|
||||
]);
|
||||
});
|
||||
static::deleting(function ($application) {
|
||||
$application->update(['fqdn' => null]);
|
||||
$application->settings()->delete();
|
||||
$storages = $application->persistentStorages()->get();
|
||||
$server = data_get($application, 'destination.server');
|
||||
@@ -50,67 +48,30 @@ class Application extends BaseModel
|
||||
$application->persistentStorages()->delete();
|
||||
$application->environment_variables()->delete();
|
||||
$application->environment_variables_preview()->delete();
|
||||
foreach ($application->scheduled_tasks as $task) {
|
||||
$task->delete();
|
||||
}
|
||||
$application->tags()->detach();
|
||||
});
|
||||
}
|
||||
// Build packs / deployment types
|
||||
|
||||
|
||||
public function servers(): Collection
|
||||
public function additional_servers()
|
||||
{
|
||||
$mainServer = data_get($this, 'destination.server');
|
||||
$additionalDestinations = data_get($this, 'additional_destinations', null);
|
||||
$additionalServers = collect([]);
|
||||
if ($this->isMultipleServerDeployment()) {
|
||||
ray('asd');
|
||||
if (str($additionalDestinations)->isNotEmpty()) {
|
||||
$additionalDestinations = str($additionalDestinations)->explode(',');
|
||||
foreach ($additionalDestinations as $destinationId) {
|
||||
$destination = StandaloneDocker::find($destinationId)->whereNot('id', $mainServer->id)->first();
|
||||
$server = data_get($destination, 'server');
|
||||
$additionalServers->push($server);
|
||||
}
|
||||
}
|
||||
}
|
||||
return collect([$mainServer])->merge($additionalServers);
|
||||
return $this->belongsToMany(Server::class, 'additional_destinations')
|
||||
->withPivot('standalone_docker_id', 'status');
|
||||
}
|
||||
|
||||
public function generateImageNames(string $commit, int $pullRequestId)
|
||||
public function additional_networks()
|
||||
{
|
||||
if ($this->dockerfile) {
|
||||
if ($this->docker_registry_image_name) {
|
||||
$buildImageName = Str::lower("{$this->docker_registry_image_name}:build");
|
||||
$productionImageName = Str::lower("{$this->docker_registry_image_name}:latest");
|
||||
} else {
|
||||
$buildImageName = Str::lower("{$this->uuid}:build");
|
||||
$productionImageName = Str::lower("{$this->uuid}:latest");
|
||||
}
|
||||
} else if ($this->build_pack === 'dockerimage') {
|
||||
$productionImageName = Str::lower("{$this->docker_registry_image_name}:{$this->docker_registry_image_tag}");
|
||||
} else if ($pullRequestId === 0) {
|
||||
$dockerImageTag = str($commit)->substr(0, 128);
|
||||
if ($this->docker_registry_image_name) {
|
||||
$buildImageName = Str::lower("{$this->docker_registry_image_name}:{$dockerImageTag}-build");
|
||||
$productionImageName = Str::lower("{$this->docker_registry_image_name}:{$dockerImageTag}");
|
||||
} else {
|
||||
$buildImageName = Str::lower("{$this->uuid}:{$dockerImageTag}-build");
|
||||
$productionImageName = Str::lower("{$this->uuid}:{$dockerImageTag}");
|
||||
}
|
||||
} else if ($pullRequestId !== 0) {
|
||||
if ($this->docker_registry_image_name) {
|
||||
$buildImageName = Str::lower("{$this->docker_registry_image_name}:pr-{$pullRequestId}-build");
|
||||
$productionImageName = Str::lower("{$this->docker_registry_image_name}:pr-{$pullRequestId}");
|
||||
} else {
|
||||
$buildImageName = Str::lower("{$this->uuid}:pr-{$pullRequestId}-build");
|
||||
$productionImageName = Str::lower("{$this->uuid}:pr-{$pullRequestId}");
|
||||
}
|
||||
}
|
||||
return [
|
||||
'buildImageName' => $buildImageName,
|
||||
'productionImageName' => $productionImageName,
|
||||
];
|
||||
return $this->belongsToMany(StandaloneDocker::class, 'additional_destinations')
|
||||
->withPivot('server_id', 'status');
|
||||
}
|
||||
public function is_github_based(): bool
|
||||
{
|
||||
if (data_get($this, 'source')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// End of build packs / deployment types
|
||||
|
||||
public function link()
|
||||
{
|
||||
if (data_get($this, 'environment.project.uuid')) {
|
||||
@@ -242,9 +203,6 @@ class Application extends BaseModel
|
||||
set: fn ($value) => $value === "" ? null : $value,
|
||||
);
|
||||
}
|
||||
|
||||
// Normal Deployments
|
||||
|
||||
public function portsMappingsArray(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
@@ -254,6 +212,79 @@ class Application extends BaseModel
|
||||
|
||||
);
|
||||
}
|
||||
public function realStatus()
|
||||
{
|
||||
return $this->getRawOriginal('status');
|
||||
}
|
||||
public function status(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
set: function ($value) {
|
||||
if ($this->additional_servers->count() === 0) {
|
||||
if (str($value)->contains('(')) {
|
||||
$status = str($value)->before('(')->trim()->value();
|
||||
$health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy';
|
||||
} else if (str($value)->contains(':')) {
|
||||
$status = str($value)->before(':')->trim()->value();
|
||||
$health = str($value)->after(':')->trim()->value() ?? 'unhealthy';
|
||||
} else {
|
||||
$status = $value;
|
||||
$health = 'unhealthy';
|
||||
}
|
||||
return "$status:$health";
|
||||
} else {
|
||||
if (str($value)->contains('(')) {
|
||||
$status = str($value)->before('(')->trim()->value();
|
||||
$health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy';
|
||||
} else if (str($value)->contains(':')) {
|
||||
$status = str($value)->before(':')->trim()->value();
|
||||
$health = str($value)->after(':')->trim()->value() ?? 'unhealthy';
|
||||
} else {
|
||||
$status = $value;
|
||||
$health = 'unhealthy';
|
||||
}
|
||||
return "$status:$health";
|
||||
}
|
||||
},
|
||||
get: function ($value) {
|
||||
if ($this->additional_servers->count() === 0) {
|
||||
//running (healthy)
|
||||
if (str($value)->contains('(')) {
|
||||
$status = str($value)->before('(')->trim()->value();
|
||||
$health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy';
|
||||
} else if (str($value)->contains(':')) {
|
||||
$status = str($value)->before(':')->trim()->value();
|
||||
$health = str($value)->after(':')->trim()->value() ?? 'unhealthy';
|
||||
} else {
|
||||
$status = $value;
|
||||
$health = 'unhealthy';
|
||||
}
|
||||
return "$status:$health";
|
||||
} else {
|
||||
$complex_status = null;
|
||||
$complex_health = null;
|
||||
$complex_status = $main_server_status = str($value)->before(':')->value();
|
||||
$complex_health = $main_server_health = str($value)->after(':')->value() ?? 'unhealthy';
|
||||
$additional_servers_status = $this->additional_servers->pluck('pivot.status');
|
||||
foreach ($additional_servers_status as $status) {
|
||||
$server_status = str($status)->before(':')->value();
|
||||
$server_health = str($status)->after(':')->value() ?? 'unhealthy';
|
||||
if ($server_status !== 'running') {
|
||||
if ($main_server_status !== $server_status) {
|
||||
$complex_status = 'degraded';
|
||||
}
|
||||
}
|
||||
if ($server_health !== 'healthy') {
|
||||
if ($main_server_health !== $server_health) {
|
||||
$complex_health = 'unhealthy';
|
||||
}
|
||||
}
|
||||
}
|
||||
return "$complex_status:$complex_health";
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public function portsExposesArray(): Attribute
|
||||
{
|
||||
@@ -263,6 +294,14 @@ class Application extends BaseModel
|
||||
: explode(',', $this->ports_exposes)
|
||||
);
|
||||
}
|
||||
public function tags()
|
||||
{
|
||||
return $this->morphToMany(Tag::class, 'taggable');
|
||||
}
|
||||
public function project()
|
||||
{
|
||||
return data_get($this, 'environment.project');
|
||||
}
|
||||
public function team()
|
||||
{
|
||||
return data_get($this, 'environment.project.team');
|
||||
@@ -431,7 +470,7 @@ class Application extends BaseModel
|
||||
{
|
||||
return data_get($this, 'settings.is_log_drain_enabled', false);
|
||||
}
|
||||
public function isConfigurationChanged($save = false)
|
||||
public function isConfigurationChanged(bool $save = false)
|
||||
{
|
||||
$newConfigHash = $this->fqdn . $this->git_repository . $this->git_branch . $this->git_commit_sha . $this->build_pack . $this->static_image . $this->install_command . $this->build_command . $this->start_command . $this->port_exposes . $this->port_mappings . $this->base_directory . $this->publish_directory . $this->dockerfile . $this->dockerfile_location . $this->custom_labels;
|
||||
if ($this->pull_request_id === 0 || $this->pull_request_id === null) {
|
||||
@@ -458,31 +497,6 @@ class Application extends BaseModel
|
||||
return true;
|
||||
}
|
||||
}
|
||||
public function isMultipleServerDeployment()
|
||||
{
|
||||
return false;
|
||||
if (data_get($this, 'additional_destinations') && data_get($this, 'docker_registry_image_name')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
public function healthCheckUrl()
|
||||
{
|
||||
if ($this->dockerfile || $this->build_pack === 'dockerfile' || $this->build_pack === 'dockerimage') {
|
||||
return null;
|
||||
}
|
||||
if (!$this->health_check_port) {
|
||||
$health_check_port = $this->ports_exposes_array[0];
|
||||
} else {
|
||||
$health_check_port = $this->health_check_port;
|
||||
}
|
||||
if ($this->health_check_path) {
|
||||
$full_healthcheck_url = "{$this->health_check_scheme}://{$this->health_check_host}:{$health_check_port}{$this->health_check_path}";
|
||||
} else {
|
||||
$full_healthcheck_url = "{$this->health_check_scheme}://{$this->health_check_host}:{$health_check_port}/";
|
||||
}
|
||||
return $full_healthcheck_url;
|
||||
}
|
||||
function customRepository()
|
||||
{
|
||||
preg_match('/(?<=:)\d+(?=\/)/', $this->git_repository, $matches);
|
||||
@@ -504,295 +518,6 @@ class Application extends BaseModel
|
||||
{
|
||||
return "/artifacts/{$uuid}";
|
||||
}
|
||||
function generateHealthCheckCommands()
|
||||
{
|
||||
if ($this->dockerfile || $this->build_pack === 'dockerfile' || $this->build_pack === 'dockerimage') {
|
||||
// TODO: disabled HC because there are several ways to hc a simple docker image, hard to figure out a good way. Like some docker images (pocketbase) does not have curl.
|
||||
return 'exit 0';
|
||||
}
|
||||
if (!$this->health_check_port) {
|
||||
$health_check_port = $this->ports_exposes_array[0];
|
||||
} else {
|
||||
$health_check_port = $this->health_check_port;
|
||||
}
|
||||
if ($this->health_check_path) {
|
||||
$this->full_healthcheck_url = "{$this->health_check_method}: {$this->health_check_scheme}://{$this->health_check_host}:{$health_check_port}{$this->health_check_path}";
|
||||
$generated_healthchecks_commands = [
|
||||
"curl -s -X {$this->health_check_method} -f {$this->health_check_scheme}://{$this->health_check_host}:{$health_check_port}{$this->health_check_path} > /dev/null"
|
||||
];
|
||||
} else {
|
||||
$this->full_healthcheck_url = "{$this->health_check_method}: {$this->health_check_scheme}://{$this->health_check_host}:{$health_check_port}/";
|
||||
$generated_healthchecks_commands = [
|
||||
"curl -s -X {$this->health_check_method} -f {$this->health_check_scheme}://{$this->health_check_host}:{$health_check_port}/"
|
||||
];
|
||||
}
|
||||
return implode(' ', $generated_healthchecks_commands);
|
||||
}
|
||||
function generateLocalPersistentVolumes(int $pullRequestId)
|
||||
{
|
||||
$persistentStorages = [];
|
||||
$volumeNames = [];
|
||||
foreach ($this->persistentStorages as $persistentStorage) {
|
||||
$volume_name = $persistentStorage->host_path ?? $persistentStorage->name;
|
||||
if ($pullRequestId !== 0) {
|
||||
$volume_name = $volume_name . '-pr-' . $pullRequestId;
|
||||
}
|
||||
$persistentStorages[] = $volume_name . ':' . $persistentStorage->mount_path;
|
||||
|
||||
if ($persistentStorage->host_path) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$name = $persistentStorage->name;
|
||||
|
||||
if ($pullRequestId !== 0) {
|
||||
$name = $name . '-pr-' . $pullRequestId;
|
||||
}
|
||||
|
||||
$volumeNames[$name] = [
|
||||
'name' => $name,
|
||||
'external' => false,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'persistentStorages' => $persistentStorages,
|
||||
'volumeNames' => $volumeNames,
|
||||
];
|
||||
}
|
||||
public function generateEnvironmentVariables($ports)
|
||||
{
|
||||
$environmentVariables = collect();
|
||||
// ray('Generate Environment Variables')->green();
|
||||
if ($this->pull_request_id === 0) {
|
||||
// ray($this->runtime_environment_variables)->green();
|
||||
foreach ($this->runtime_environment_variables as $env) {
|
||||
$environmentVariables->push("$env->key=$env->value");
|
||||
}
|
||||
foreach ($this->nixpacks_environment_variables as $env) {
|
||||
$environmentVariables->push("$env->key=$env->value");
|
||||
}
|
||||
} else {
|
||||
// ray($this->runtime_environment_variables_preview)->green();
|
||||
foreach ($this->runtime_environment_variables_preview as $env) {
|
||||
$environmentVariables->push("$env->key=$env->value");
|
||||
}
|
||||
foreach ($this->nixpacks_environment_variables_preview as $env) {
|
||||
$environmentVariables->push("$env->key=$env->value");
|
||||
}
|
||||
}
|
||||
// Add PORT if not exists, use the first port as default
|
||||
if ($environmentVariables->filter(fn ($env) => Str::of($env)->contains('PORT'))->isEmpty()) {
|
||||
$environmentVariables->push("PORT={$ports[0]}");
|
||||
}
|
||||
return $environmentVariables->all();
|
||||
}
|
||||
function generateDockerComposeFile(Server $server, ApplicationDeploymentQueue $deployment, string $workdir)
|
||||
{
|
||||
$pullRequestId = $deployment->pull_request_id;
|
||||
$ports = $this->settings->is_static ? [80] : $this->ports_exposes_array;
|
||||
$container_name = generateApplicationContainerName($this, $this->pull_request_id);
|
||||
$commit = str($deployment->getOutput('git_commit_sha'))->before("\t");
|
||||
|
||||
[
|
||||
'productionImageName' => $productionImageName
|
||||
] = $this->generateImageNames($commit, $pullRequestId);
|
||||
|
||||
[
|
||||
'persistentStorages' => $persistentStorages,
|
||||
'volumeNames' => $volumeNames
|
||||
] = $this->generateLocalPersistentVolumes($pullRequestId);
|
||||
|
||||
$environmentVariables = $this->generateEnvironmentVariables($ports);
|
||||
|
||||
if (data_get($this, 'custom_labels')) {
|
||||
$labels = collect(str($this->custom_labels)->explode(','));
|
||||
$labels = $labels->filter(function ($value, $key) {
|
||||
return !Str::startsWith($value, 'coolify.');
|
||||
});
|
||||
$this->custom_labels = $labels->implode(',');
|
||||
$this->save();
|
||||
} else {
|
||||
$labels = collect(generateLabelsApplication($this, $this->preview));
|
||||
}
|
||||
if ($this->pull_request_id !== 0) {
|
||||
$labels = collect(generateLabelsApplication($this, $this->preview));
|
||||
}
|
||||
$labels = $labels->merge(defaultLabels($this->id, $this->uuid, $this->pull_request_id))->toArray();
|
||||
$docker_compose = [
|
||||
'version' => '3.8',
|
||||
'services' => [
|
||||
$container_name => [
|
||||
'image' => $productionImageName,
|
||||
'container_name' => $container_name,
|
||||
'restart' => RESTART_MODE,
|
||||
'environment' => $environmentVariables,
|
||||
'expose' => $ports,
|
||||
'networks' => [
|
||||
$this->destination->network,
|
||||
],
|
||||
'healthcheck' => [
|
||||
'test' => [
|
||||
'CMD-SHELL',
|
||||
$this->generateHealthCheckCommands()
|
||||
],
|
||||
'interval' => $this->health_check_interval . 's',
|
||||
'timeout' => $this->health_check_timeout . 's',
|
||||
'retries' => $this->health_check_retries,
|
||||
'start_period' => $this->health_check_start_period . 's'
|
||||
],
|
||||
'mem_limit' => $this->limits_memory,
|
||||
'memswap_limit' => $this->limits_memory_swap,
|
||||
'mem_swappiness' => $this->limits_memory_swappiness,
|
||||
'mem_reservation' => $this->limits_memory_reservation,
|
||||
'cpus' => (float) $this->limits_cpus,
|
||||
'cpu_shares' => $this->limits_cpu_shares,
|
||||
]
|
||||
],
|
||||
'networks' => [
|
||||
$this->destination->network => [
|
||||
'external' => true,
|
||||
'name' => $this->destination->network,
|
||||
'attachable' => true
|
||||
]
|
||||
]
|
||||
];
|
||||
if (!is_null($this->limits_cpuset)) {
|
||||
data_set($docker_compose, "services.{$container_name}.cpuset", $this->limits_cpuset);
|
||||
}
|
||||
if ($server->isSwarm()) {
|
||||
data_forget($docker_compose, 'services.' . $container_name . '.container_name');
|
||||
data_forget($docker_compose, 'services.' . $container_name . '.expose');
|
||||
data_forget($docker_compose, 'services.' . $container_name . '.restart');
|
||||
|
||||
data_forget($docker_compose, 'services.' . $container_name . '.mem_limit');
|
||||
data_forget($docker_compose, 'services.' . $container_name . '.memswap_limit');
|
||||
data_forget($docker_compose, 'services.' . $container_name . '.mem_swappiness');
|
||||
data_forget($docker_compose, 'services.' . $container_name . '.mem_reservation');
|
||||
data_forget($docker_compose, 'services.' . $container_name . '.cpus');
|
||||
data_forget($docker_compose, 'services.' . $container_name . '.cpuset');
|
||||
data_forget($docker_compose, 'services.' . $container_name . '.cpu_shares');
|
||||
|
||||
$docker_compose['services'][$container_name]['deploy'] = [
|
||||
'placement' => [
|
||||
'constraints' => [
|
||||
'node.role == worker'
|
||||
]
|
||||
],
|
||||
'mode' => 'replicated',
|
||||
'replicas' => 1,
|
||||
'update_config' => [
|
||||
'order' => 'start-first'
|
||||
],
|
||||
'rollback_config' => [
|
||||
'order' => 'start-first'
|
||||
],
|
||||
'labels' => $labels,
|
||||
'resources' => [
|
||||
'limits' => [
|
||||
'cpus' => $this->limits_cpus,
|
||||
'memory' => $this->limits_memory,
|
||||
],
|
||||
'reservations' => [
|
||||
'cpus' => $this->limits_cpus,
|
||||
'memory' => $this->limits_memory,
|
||||
]
|
||||
]
|
||||
];
|
||||
} else {
|
||||
$docker_compose['services'][$container_name]['labels'] = $labels;
|
||||
}
|
||||
if ($server->isLogDrainEnabled() && $this->isLogDrainEnabled()) {
|
||||
$docker_compose['services'][$container_name]['logging'] = [
|
||||
'driver' => 'fluentd',
|
||||
'options' => [
|
||||
'fluentd-address' => "tcp://127.0.0.1:24224",
|
||||
'fluentd-async' => "true",
|
||||
'fluentd-sub-second-precision' => "true",
|
||||
]
|
||||
];
|
||||
}
|
||||
if ($this->settings->is_gpu_enabled) {
|
||||
$docker_compose['services'][$container_name]['deploy']['resources']['reservations']['devices'] = [
|
||||
[
|
||||
'driver' => data_get($this, 'settings.gpu_driver', 'nvidia'),
|
||||
'capabilities' => ['gpu'],
|
||||
'options' => data_get($this, 'settings.gpu_options', [])
|
||||
]
|
||||
];
|
||||
if (data_get($this, 'settings.gpu_count')) {
|
||||
$count = data_get($this, 'settings.gpu_count');
|
||||
if ($count === 'all') {
|
||||
$docker_compose['services'][$container_name]['deploy']['resources']['reservations']['devices'][0]['count'] = $count;
|
||||
} else {
|
||||
$docker_compose['services'][$container_name]['deploy']['resources']['reservations']['devices'][0]['count'] = (int) $count;
|
||||
}
|
||||
} else if (data_get($this, 'settings.gpu_device_ids')) {
|
||||
$docker_compose['services'][$container_name]['deploy']['resources']['reservations']['devices'][0]['ids'] = data_get($this, 'settings.gpu_device_ids');
|
||||
}
|
||||
}
|
||||
if ($this->isHealthcheckDisabled()) {
|
||||
data_forget($docker_compose, 'services.' . $container_name . '.healthcheck');
|
||||
}
|
||||
if (count($this->ports_mappings_array) > 0 && $this->pull_request_id === 0) {
|
||||
$docker_compose['services'][$container_name]['ports'] = $this->ports_mappings_array;
|
||||
}
|
||||
if (count($persistentStorages) > 0) {
|
||||
$docker_compose['services'][$container_name]['volumes'] = $persistentStorages;
|
||||
}
|
||||
if (count($volumeNames) > 0) {
|
||||
$docker_compose['volumes'] = $volumeNames;
|
||||
}
|
||||
|
||||
$docker_compose['services'][$this->uuid] = $docker_compose['services'][$container_name];
|
||||
|
||||
data_forget($docker_compose, 'services.' . $container_name);
|
||||
|
||||
$docker_compose = Yaml::dump($docker_compose, 10);
|
||||
$docker_compose_base64 = base64_encode($docker_compose);
|
||||
$server->executeRemoteCommand(
|
||||
commands: collect([])->push([
|
||||
'command' => executeInDocker($deployment->deployment_uuid, "echo '{$docker_compose_base64}' | base64 -d > {$workdir}/docker-compose.yml"),
|
||||
'hidden' => true,
|
||||
'ignoreErrors' => true
|
||||
]),
|
||||
loggingModel: $deployment
|
||||
);
|
||||
}
|
||||
function rollingUpdateApplication(Server $server, ApplicationDeploymentQueue $deployment, string $workdir)
|
||||
{
|
||||
$pullRequestId = $deployment->pull_request_id;
|
||||
$containerName = generateApplicationContainerName($this, $pullRequestId);
|
||||
// if (count($this->ports_mappings_array) > 0) {
|
||||
// $deployment->addLogEntry('Application has ports mapped to the host system, rolling update is not supported.');
|
||||
$containers = getCurrentApplicationContainerStatus($server, $this->id, $pullRequestId);
|
||||
// if ($pullRequestId === 0) {
|
||||
// $containers = $containers->filter(function ($container) use ($containerName) {
|
||||
// return data_get($container, 'Names') !== $containerName;
|
||||
// });
|
||||
// }
|
||||
$containers->each(function ($container) use ($server, $deployment) {
|
||||
$removingContainerName = data_get($container, 'Names');
|
||||
$server->executeRemoteCommand(
|
||||
commands: collect([])->push([
|
||||
'command' => "docker rm -f $removingContainerName",
|
||||
'hidden' => true,
|
||||
'ignoreErrors' => true
|
||||
]),
|
||||
loggingModel: $deployment
|
||||
);
|
||||
});
|
||||
// }
|
||||
$server->executeRemoteCommand(
|
||||
commands: collect([])->push([
|
||||
'command' => executeInDocker($deployment->deployment_uuid, "docker compose --project-directory {$workdir} up --build -d"),
|
||||
'hidden' => true,
|
||||
'ignoreErrors' => true
|
||||
]),
|
||||
loggingModel: $deployment
|
||||
);
|
||||
$deployment->addLogEntry("New container started.");
|
||||
}
|
||||
function setGitImportSettings(string $deployment_uuid, string $git_clone_command)
|
||||
{
|
||||
$baseDir = $this->generateBaseDir($deployment_uuid);
|
||||
@@ -807,7 +532,7 @@ class Application extends BaseModel
|
||||
}
|
||||
return $git_clone_command;
|
||||
}
|
||||
function generateGitImportCommands(string $deployment_uuid, int $pull_request_id = 0, ?string $git_type = null, bool $exec_in_docker = true, bool $only_checkout = false, ?string $custom_base_dir = null)
|
||||
function generateGitImportCommands(string $deployment_uuid, int $pull_request_id = 0, ?string $git_type = null, bool $exec_in_docker = true, bool $only_checkout = false, ?string $custom_base_dir = null, ?string $commit = null)
|
||||
{
|
||||
$branch = $this->git_branch;
|
||||
['repository' => $customRepository, 'port' => $customPort] = $this->customRepository();
|
||||
@@ -820,7 +545,6 @@ class Application extends BaseModel
|
||||
if ($pull_request_id !== 0) {
|
||||
$pr_branch_name = "pr-{$pull_request_id}-coolify";
|
||||
}
|
||||
|
||||
if ($this->deploymentType() === 'source') {
|
||||
$source_html_url = data_get($this, 'source.html_url');
|
||||
$url = parse_url(filter_var($source_html_url, FILTER_SANITIZE_URL));
|
||||
@@ -899,8 +623,7 @@ class Application extends BaseModel
|
||||
$commands->push("echo 'Checking out $branch'");
|
||||
}
|
||||
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && git checkout $pr_branch_name";
|
||||
}
|
||||
if ($git_type === 'github') {
|
||||
} else if ($git_type === 'github') {
|
||||
$branch = "pull/{$pull_request_id}/head:$pr_branch_name";
|
||||
if ($exec_in_docker) {
|
||||
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
|
||||
@@ -908,6 +631,13 @@ class Application extends BaseModel
|
||||
$commands->push("echo 'Checking out $branch'");
|
||||
}
|
||||
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && git checkout $pr_branch_name";
|
||||
} else if ($git_type === 'bitbucket') {
|
||||
if ($exec_in_docker) {
|
||||
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
|
||||
} else {
|
||||
$commands->push("echo 'Checking out $branch'");
|
||||
}
|
||||
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git checkout $commit";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -926,6 +656,34 @@ class Application extends BaseModel
|
||||
$fullRepoUrl = $customRepository;
|
||||
$git_clone_command = "{$git_clone_command} {$customRepository} {$baseDir}";
|
||||
$git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command);
|
||||
|
||||
if ($pull_request_id !== 0) {
|
||||
if ($git_type === 'gitlab') {
|
||||
$branch = "merge-requests/{$pull_request_id}/head:$pr_branch_name";
|
||||
if ($exec_in_docker) {
|
||||
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
|
||||
} else {
|
||||
$commands->push("echo 'Checking out $branch'");
|
||||
}
|
||||
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && git checkout $pr_branch_name";
|
||||
} else if ($git_type === 'github') {
|
||||
$branch = "pull/{$pull_request_id}/head:$pr_branch_name";
|
||||
if ($exec_in_docker) {
|
||||
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
|
||||
} else {
|
||||
$commands->push("echo 'Checking out $branch'");
|
||||
}
|
||||
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && git checkout $pr_branch_name";
|
||||
} else if ($git_type === 'bitbucket') {
|
||||
if ($exec_in_docker) {
|
||||
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
|
||||
} else {
|
||||
$commands->push("echo 'Checking out $branch'");
|
||||
}
|
||||
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git checkout $commit";
|
||||
}
|
||||
}
|
||||
|
||||
if ($exec_in_docker) {
|
||||
$commands->push(executeInDocker($deployment_uuid, $git_clone_command));
|
||||
} else {
|
||||
@@ -938,34 +696,6 @@ class Application extends BaseModel
|
||||
];
|
||||
}
|
||||
}
|
||||
public function prepareHelperImage(string $deploymentUuid)
|
||||
{
|
||||
$basedir = $this->generateBaseDir($deploymentUuid);
|
||||
$helperImage = config('coolify.helper_image');
|
||||
$server = data_get($this, 'destination.server');
|
||||
$network = data_get($this, 'destination.network');
|
||||
|
||||
$serverUserHomeDir = instant_remote_process(["echo \$HOME"], $server);
|
||||
$dockerConfigFileExists = instant_remote_process(["test -f {$serverUserHomeDir}/.docker/config.json && echo 'OK' || echo 'NOK'"], $server);
|
||||
|
||||
$commands = collect([]);
|
||||
if ($dockerConfigFileExists === 'OK') {
|
||||
$commands->push([
|
||||
"command" => "docker run -d --network $network --name $deploymentUuid --rm -v {$serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock $helperImage",
|
||||
"hidden" => true,
|
||||
]);
|
||||
} else {
|
||||
$commands->push([
|
||||
"command" => "docker run -d --network {$network} --name {$deploymentUuid} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}",
|
||||
"hidden" => true,
|
||||
]);
|
||||
}
|
||||
$commands->push([
|
||||
"command" => executeInDocker($deploymentUuid, "mkdir -p {$basedir}"),
|
||||
"hidden" => true,
|
||||
]);
|
||||
return $commands;
|
||||
}
|
||||
function parseCompose(int $pull_request_id = 0)
|
||||
{
|
||||
if ($this->docker_compose_raw) {
|
||||
@@ -1076,4 +806,12 @@ class Application extends BaseModel
|
||||
$this->save();
|
||||
return $customLabels;
|
||||
}
|
||||
public function fqdns(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => is_null($this->fqdn)
|
||||
? []
|
||||
: explode(',', $this->fqdn),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ class Environment extends Model
|
||||
return $this->applications()->count() == 0 &&
|
||||
$this->redis()->count() == 0 &&
|
||||
$this->postgresqls()->count() == 0 &&
|
||||
$this->mysqls()->count() == 0 &&
|
||||
$this->mariadbs()->count() == 0 &&
|
||||
$this->mongodbs()->count() == 0 &&
|
||||
$this->services()->count() == 0;
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ class EnvironmentVariable extends Model
|
||||
}
|
||||
private function get_real_environment_variables(?string $environment_variable = null, $resource = null): string|null
|
||||
{
|
||||
if (!$environment_variable) {
|
||||
if (!$environment_variable || !$resource) {
|
||||
return null;
|
||||
}
|
||||
$environment_variable = trim($environment_variable);
|
||||
@@ -100,6 +100,9 @@ class EnvironmentVariable extends Model
|
||||
$variable = Str::after($environment_variable, "{$type}.");
|
||||
$variable = Str::before($variable, '}}');
|
||||
$variable = Str::of($variable)->trim()->value;
|
||||
if (!collect(SHARED_VARIABLE_TYPES)->contains($type)) {
|
||||
return $variable;
|
||||
}
|
||||
if ($type === 'environment') {
|
||||
$id = $resource->environment->id;
|
||||
} else if ($type === 'project') {
|
||||
|
||||
@@ -10,20 +10,37 @@ class LocalFileVolume extends BaseModel
|
||||
use HasFactory;
|
||||
protected $guarded = [];
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
static::created(function (LocalFileVolume $fileVolume) {
|
||||
$fileVolume->load(['service']);
|
||||
$fileVolume->saveStorageOnServer();
|
||||
});
|
||||
}
|
||||
public function service()
|
||||
{
|
||||
return $this->morphTo('resource');
|
||||
}
|
||||
public function saveStorageOnServer(ServiceApplication|ServiceDatabase $service)
|
||||
public function saveStorageOnServer()
|
||||
{
|
||||
$workdir = $service->service->workdir();
|
||||
$server = $service->service->server;
|
||||
$workdir = $this->resource->service->workdir();
|
||||
$server = $this->resource->service->server;
|
||||
$commands = collect([
|
||||
"mkdir -p $workdir > /dev/null 2>&1 || true",
|
||||
"cd $workdir"
|
||||
]);
|
||||
$is_directory = $this->is_directory;
|
||||
if ($is_directory) {
|
||||
$commands->push("mkdir -p $this->fs_path > /dev/null 2>&1 || true");
|
||||
}
|
||||
if (str($this->fs_path)->startsWith('.') || str($this->fs_path)->startsWith('/') || str($this->fs_path)->startsWith('~')) {
|
||||
$parent_dir = str($this->fs_path)->beforeLast('/');
|
||||
if ($parent_dir != '') {
|
||||
$commands->push("mkdir -p $parent_dir > /dev/null 2>&1 || true");
|
||||
}
|
||||
}
|
||||
$fileVolume = $this;
|
||||
$path = Str::of(data_get($fileVolume, 'fs_path'));
|
||||
$path = str(data_get($fileVolume, 'fs_path'));
|
||||
$content = data_get($fileVolume, 'content');
|
||||
if ($path->startsWith('.')) {
|
||||
$path = $path->after('.');
|
||||
@@ -32,17 +49,18 @@ class LocalFileVolume extends BaseModel
|
||||
$isFile = instant_remote_process(["test -f $path && echo OK || echo NOK"], $server);
|
||||
$isDir = instant_remote_process(["test -d $path && echo OK || echo NOK"], $server);
|
||||
if ($isFile == 'OK' && $fileVolume->is_directory) {
|
||||
throw new \Exception("File $path is a file on the server, but you are trying to mark it as a directory. Please delete the file on the server or mark it as directory.");
|
||||
throw new \Exception("The following file is a file on the server, but you are trying to mark it as a directory. Please delete the file on the server or mark it as directory.");
|
||||
} else if ($isDir == 'OK' && !$fileVolume->is_directory) {
|
||||
throw new \Exception("File $path is a directory on the server, but you are trying to mark it as a file. Please delete the directory on the server or mark it as directory.");
|
||||
throw new \Exception("The following file is a directory on the server, but you are trying to mark it as a file. <br><br>Please delete the directory on the server or mark it as directory.");
|
||||
}
|
||||
if (!$fileVolume->is_directory && $isDir == 'NOK') {
|
||||
$content = base64_encode($content);
|
||||
$commands->push("echo '$content' | base64 -d > $path");
|
||||
} else if ($isDir == 'NOK' && $fileVolume->is_directory) {
|
||||
if ($content) {
|
||||
$content = base64_encode($content);
|
||||
$commands->push("echo '$content' | base64 -d > $path");
|
||||
}
|
||||
} else if ($isDir == 'NOK' && $fileVolume->is_directory) {
|
||||
$commands->push("mkdir -p $path > /dev/null 2>&1 || true");
|
||||
}
|
||||
ray($commands->toArray());
|
||||
return instant_remote_process($commands, $server);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,10 +64,13 @@ class Project extends BaseModel
|
||||
}
|
||||
public function mysqls()
|
||||
{
|
||||
return $this->hasMany(StandaloneMysql::class, Environment::class);
|
||||
return $this->hasManyThrough(StandaloneMysql::class, Environment::class);
|
||||
}
|
||||
public function mariadbs()
|
||||
{
|
||||
return $this->hasMany(StandaloneMariadb::class, Environment::class);
|
||||
return $this->hasManyThrough(StandaloneMariadb::class, Environment::class);
|
||||
}
|
||||
public function resource_count() {
|
||||
return $this->applications()->count() + $this->postgresqls()->count() + $this->redis()->count() + $this->mongodbs()->count() + $this->mysqls()->count() + $this->mariadbs()->count();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,16 +2,14 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\ApplicationDeploymentStatus;
|
||||
use App\Actions\Server\InstallDocker;
|
||||
use App\Enums\ProxyStatus;
|
||||
use App\Enums\ProxyTypes;
|
||||
use App\Notifications\Server\Revived;
|
||||
use App\Notifications\Server\Unreachable;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Spatie\SchemalessAttributes\Casts\SchemalessAttributes;
|
||||
use Spatie\SchemalessAttributes\SchemalessAttributesTrait;
|
||||
use Illuminate\Support\Str;
|
||||
@@ -251,9 +249,17 @@ class Server extends BaseModel
|
||||
}
|
||||
public function applications()
|
||||
{
|
||||
return $this->destinations()->map(function ($standaloneDocker) {
|
||||
$applications = $this->destinations()->map(function ($standaloneDocker) {
|
||||
return $standaloneDocker->applications;
|
||||
})->flatten();
|
||||
$additionalApplicationIds = DB::table('additional_destinations')->where('server_id', $this->id)->get('application_id');
|
||||
$additionalApplicationIds = collect($additionalApplicationIds)->map(function ($item) {
|
||||
return $item->application_id;
|
||||
});
|
||||
Application::whereIn('id', $additionalApplicationIds)->get()->each(function ($application) use ($applications) {
|
||||
$applications->push($application);
|
||||
});
|
||||
return $applications;
|
||||
}
|
||||
public function dockerComposeBasedApplications()
|
||||
{
|
||||
@@ -303,7 +309,8 @@ class Server extends BaseModel
|
||||
{
|
||||
$standalone_docker = $this->hasMany(StandaloneDocker::class)->get();
|
||||
$swarm_docker = $this->hasMany(SwarmDocker::class)->get();
|
||||
return $standalone_docker->concat($swarm_docker);
|
||||
$asd = $this->belongsToMany(StandaloneDocker::class, 'additional_destinations')->withPivot('server_id')->get();
|
||||
return $standalone_docker->concat($swarm_docker)->concat($asd);
|
||||
}
|
||||
|
||||
public function standaloneDockers()
|
||||
@@ -335,20 +342,6 @@ class Server extends BaseModel
|
||||
if ($this->proxyType() === ProxyTypes::NONE->value || $this->settings->is_build_server) {
|
||||
return false;
|
||||
}
|
||||
// foreach ($this->applications() as $application) {
|
||||
// if (data_get($application, 'fqdn')) {
|
||||
// $shouldRun = true;
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// ray($this->services()->get());
|
||||
|
||||
// if ($this->id === 0) {
|
||||
// $settings = InstanceSettings::get();
|
||||
// if (data_get($settings, 'fqdn')) {
|
||||
// $shouldRun = true;
|
||||
// }
|
||||
// }
|
||||
return true;
|
||||
}
|
||||
public function isFunctional()
|
||||
@@ -429,6 +422,11 @@ class Server extends BaseModel
|
||||
|
||||
return true;
|
||||
}
|
||||
public function installDocker()
|
||||
{
|
||||
$activity = InstallDocker::run($this);
|
||||
return $activity;
|
||||
}
|
||||
public function validateDockerEngine($throwError = false)
|
||||
{
|
||||
$dockerBinary = instant_remote_process(["command -v docker"], $this, false);
|
||||
@@ -445,6 +443,21 @@ class Server extends BaseModel
|
||||
$this->validateCoolifyNetwork(isSwarm: false, isBuildServer: $this->settings->is_build_server);
|
||||
return true;
|
||||
}
|
||||
public function validateDockerCompose($throwError = false)
|
||||
{
|
||||
$dockerCompose = instant_remote_process(["docker compose version"], $this, false);
|
||||
if (is_null($dockerCompose)) {
|
||||
$this->settings->is_usable = false;
|
||||
$this->settings->save();
|
||||
if ($throwError) {
|
||||
throw new \Exception('Server is not usable. Docker Compose is not installed.');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
$this->settings->is_usable = true;
|
||||
$this->settings->save();
|
||||
return true;
|
||||
}
|
||||
public function validateDockerSwarm()
|
||||
{
|
||||
$swarmStatus = instant_remote_process(["docker info|grep -i swarm"], $this, false);
|
||||
@@ -483,153 +496,4 @@ class Server extends BaseModel
|
||||
return instant_remote_process(["docker network create coolify --attachable >/dev/null 2>&1 || true"], $this, false);
|
||||
}
|
||||
}
|
||||
public function executeRemoteCommand(Collection $commands, ?ApplicationDeploymentQueue $loggingModel = null)
|
||||
{
|
||||
static::$batch_counter++;
|
||||
foreach ($commands as $command) {
|
||||
$realCommand = data_get($command, 'command');
|
||||
if (is_null($realCommand)) {
|
||||
throw new \RuntimeException('Command is not set');
|
||||
}
|
||||
$hidden = data_get($command, 'hidden', false);
|
||||
$ignoreErrors = data_get($command, 'ignoreErrors', false);
|
||||
$customOutputType = data_get($command, 'customOutputType');
|
||||
$name = data_get($command, 'name');
|
||||
$remoteCommand = generateSshCommand($this, $realCommand);
|
||||
|
||||
$process = Process::timeout(3600)->idleTimeout(3600)->start($remoteCommand, function (string $type, string $output) use ($realCommand, $hidden, $customOutputType, $loggingModel, $name) {
|
||||
$output = str($output)->trim();
|
||||
if ($output->startsWith('╔')) {
|
||||
$output = "\n" . $output;
|
||||
}
|
||||
$newLogEntry = [
|
||||
'command' => remove_iip($realCommand),
|
||||
'output' => remove_iip($output),
|
||||
'type' => $customOutputType ?? $type === 'err' ? 'stderr' : 'stdout',
|
||||
'timestamp' => Carbon::now('UTC'),
|
||||
'hidden' => $hidden,
|
||||
'batch' => static::$batch_counter,
|
||||
];
|
||||
if ($loggingModel) {
|
||||
if (!$loggingModel->logs) {
|
||||
$newLogEntry['order'] = 1;
|
||||
} else {
|
||||
$previousLogs = json_decode($loggingModel->logs, associative: true, flags: JSON_THROW_ON_ERROR);
|
||||
$newLogEntry['order'] = count($previousLogs) + 1;
|
||||
}
|
||||
if ($name) {
|
||||
$newLogEntry['name'] = $name;
|
||||
}
|
||||
|
||||
$previousLogs[] = $newLogEntry;
|
||||
$loggingModel->logs = json_encode($previousLogs, flags: JSON_THROW_ON_ERROR);
|
||||
$loggingModel->save();
|
||||
}
|
||||
});
|
||||
if ($loggingModel) {
|
||||
$loggingModel->update([
|
||||
'current_process_id' => $process->id(),
|
||||
]);
|
||||
}
|
||||
$processResult = $process->wait();
|
||||
if ($processResult->exitCode() !== 0) {
|
||||
if (!$ignoreErrors) {
|
||||
if ($loggingModel) {
|
||||
$status = ApplicationDeploymentStatus::FAILED->value;
|
||||
$loggingModel->status = $status;
|
||||
$loggingModel->save();
|
||||
}
|
||||
throw new \RuntimeException($processResult->errorOutput());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
public function stopApplicationRelatedRunningContainers(string $applicationId, string $containerName)
|
||||
{
|
||||
$containers = getCurrentApplicationContainerStatus($this, $applicationId, 0);
|
||||
$containers = $containers->filter(function ($container) use ($containerName) {
|
||||
return data_get($container, 'Names') !== $containerName;
|
||||
});
|
||||
$containers->each(function ($container) {
|
||||
$removableContainer = data_get($container, 'Names');
|
||||
$this->server->executeRemoteCommand(
|
||||
commands: collect([
|
||||
'command' => "docker rm -f $removableContainer >/dev/null 2>&1",
|
||||
'hidden' => true,
|
||||
'ignoreErrors' => true
|
||||
]),
|
||||
loggingModel: $this->deploymentQueueEntry
|
||||
);
|
||||
});
|
||||
}
|
||||
public function getHostIPMappings($network)
|
||||
{
|
||||
$addHosts = null;
|
||||
$allContainers = instant_remote_process(["docker network inspect {$network} -f '{{json .Containers}}' "], $this);
|
||||
if (!is_null($allContainers)) {
|
||||
$allContainers = format_docker_command_output_to_json($allContainers);
|
||||
$ips = collect([]);
|
||||
if (count($allContainers) > 0) {
|
||||
$allContainers = $allContainers[0];
|
||||
foreach ($allContainers as $container) {
|
||||
$containerName = data_get($container, 'Name');
|
||||
if ($containerName === 'coolify-proxy') {
|
||||
continue;
|
||||
}
|
||||
$containerIp = data_get($container, 'IPv4Address');
|
||||
if ($containerName && $containerIp) {
|
||||
$containerIp = str($containerIp)->before('/');
|
||||
$ips->put($containerName, $containerIp->value());
|
||||
}
|
||||
}
|
||||
}
|
||||
$addHosts = $ips->map(function ($ip, $name) {
|
||||
return "--add-host $name:$ip";
|
||||
})->implode(' ');
|
||||
}
|
||||
return $addHosts;
|
||||
}
|
||||
public function checkIfDockerImageExists(string $imageName, ApplicationDeploymentQueue $deployment)
|
||||
{
|
||||
$this->executeRemoteCommand(
|
||||
commands: collect([
|
||||
[
|
||||
"name" => "local_image_found",
|
||||
"command" => "docker images -q {$imageName} 2>/dev/null",
|
||||
"hidden" => true,
|
||||
]
|
||||
]),
|
||||
loggingModel: $deployment
|
||||
);
|
||||
if (str($deployment->getOutput('local_image_found'))->isEmpty()) {
|
||||
$this->executeRemoteCommand(
|
||||
commands: collect([
|
||||
[
|
||||
"command" => "docker pull {$imageName} 2>/dev/null",
|
||||
"ignoreErrors" => true,
|
||||
"hidden" => true
|
||||
],
|
||||
[
|
||||
"name" => "local_image_found",
|
||||
"command" => "docker images -q {$imageName} 2>/dev/null",
|
||||
"hidden" => true,
|
||||
]
|
||||
]),
|
||||
loggingModel: $deployment
|
||||
);
|
||||
}
|
||||
}
|
||||
public function createWorkDirForDeployment(string $workdir, ApplicationDeploymentQueue $deployment)
|
||||
{
|
||||
$this->executeRemoteCommand(
|
||||
commands: collect([
|
||||
[
|
||||
"command" => executeInDocker($deployment->deployment_uuid, "mkdir -p {$workdir}"),
|
||||
"ignoreErrors" => true,
|
||||
"hidden" => true
|
||||
],
|
||||
]),
|
||||
loggingModel: $deployment
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,11 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Actions\Service\DeleteService;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
class Service extends BaseModel
|
||||
{
|
||||
@@ -18,10 +16,18 @@ class Service extends BaseModel
|
||||
{
|
||||
return 'service';
|
||||
}
|
||||
public function project()
|
||||
{
|
||||
return data_get($this, 'environment.project');
|
||||
}
|
||||
public function team()
|
||||
{
|
||||
return data_get($this, 'environment.project.team');
|
||||
}
|
||||
public function tags()
|
||||
{
|
||||
return $this->morphToMany(Tag::class, 'taggable');
|
||||
}
|
||||
public function extraFields()
|
||||
{
|
||||
$fields = collect([]);
|
||||
|
||||
@@ -5,7 +5,6 @@ namespace App\Models;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class ServiceApplication extends BaseModel
|
||||
{
|
||||
@@ -15,6 +14,7 @@ class ServiceApplication extends BaseModel
|
||||
protected static function booted()
|
||||
{
|
||||
static::deleting(function ($service) {
|
||||
$service->update(['fqdn' => null]);
|
||||
$service->persistentStorages()->delete();
|
||||
$service->fileStorages()->delete();
|
||||
});
|
||||
@@ -55,7 +55,6 @@ class ServiceApplication extends BaseModel
|
||||
get: fn () => is_null($this->fqdn)
|
||||
? []
|
||||
: explode(',', $this->fqdn),
|
||||
|
||||
);
|
||||
}
|
||||
public function getFilesFromServer(bool $isInit = false)
|
||||
|
||||
@@ -10,7 +10,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class StandaloneMariadb extends BaseModel
|
||||
{
|
||||
use HasFactory,SoftDeletes;
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $guarded = [];
|
||||
protected $casts = [
|
||||
@@ -40,8 +40,48 @@ class StandaloneMariadb extends BaseModel
|
||||
$database->scheduledBackups()->delete();
|
||||
$database->persistentStorages()->delete();
|
||||
$database->environment_variables()->delete();
|
||||
$database->tags()->detach();
|
||||
});
|
||||
}
|
||||
public function realStatus()
|
||||
{
|
||||
return $this->getRawOriginal('status');
|
||||
}
|
||||
public function status(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
set: function ($value) {
|
||||
if (str($value)->contains('(')) {
|
||||
$status = str($value)->before('(')->trim()->value();
|
||||
$health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy';
|
||||
} else if (str($value)->contains(':')) {
|
||||
$status = str($value)->before(':')->trim()->value();
|
||||
$health = str($value)->after(':')->trim()->value() ?? 'unhealthy';
|
||||
} else {
|
||||
$status = $value;
|
||||
$health = 'unhealthy';
|
||||
}
|
||||
return "$status:$health";
|
||||
},
|
||||
get: function ($value) {
|
||||
if (str($value)->contains('(')) {
|
||||
$status = str($value)->before('(')->trim()->value();
|
||||
$health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy';
|
||||
} else if (str($value)->contains(':')) {
|
||||
$status = str($value)->before(':')->trim()->value();
|
||||
$health = str($value)->after(':')->trim()->value() ?? 'unhealthy';
|
||||
} else {
|
||||
$status = $value;
|
||||
$health = 'unhealthy';
|
||||
}
|
||||
return "$status:$health";
|
||||
},
|
||||
);
|
||||
}
|
||||
public function tags()
|
||||
{
|
||||
return $this->morphToMany(Tag::class, 'taggable');
|
||||
}
|
||||
public function team()
|
||||
{
|
||||
return data_get($this, 'environment.project.team');
|
||||
|
||||
@@ -43,8 +43,48 @@ class StandaloneMongodb extends BaseModel
|
||||
$database->scheduledBackups()->delete();
|
||||
$database->persistentStorages()->delete();
|
||||
$database->environment_variables()->delete();
|
||||
$database->tags()->detach();
|
||||
});
|
||||
}
|
||||
public function realStatus()
|
||||
{
|
||||
return $this->getRawOriginal('status');
|
||||
}
|
||||
public function status(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
set: function ($value) {
|
||||
if (str($value)->contains('(')) {
|
||||
$status = str($value)->before('(')->trim()->value();
|
||||
$health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy';
|
||||
} else if (str($value)->contains(':')) {
|
||||
$status = str($value)->before(':')->trim()->value();
|
||||
$health = str($value)->after(':')->trim()->value() ?? 'unhealthy';
|
||||
} else {
|
||||
$status = $value;
|
||||
$health = 'unhealthy';
|
||||
}
|
||||
return "$status:$health";
|
||||
},
|
||||
get: function ($value) {
|
||||
if (str($value)->contains('(')) {
|
||||
$status = str($value)->before('(')->trim()->value();
|
||||
$health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy';
|
||||
} else if (str($value)->contains(':')) {
|
||||
$status = str($value)->before(':')->trim()->value();
|
||||
$health = str($value)->after(':')->trim()->value() ?? 'unhealthy';
|
||||
} else {
|
||||
$status = $value;
|
||||
$health = 'unhealthy';
|
||||
}
|
||||
return "$status:$health";
|
||||
},
|
||||
);
|
||||
}
|
||||
public function tags()
|
||||
{
|
||||
return $this->morphToMany(Tag::class, 'taggable');
|
||||
}
|
||||
public function team()
|
||||
{
|
||||
return data_get($this, 'environment.project.team');
|
||||
|
||||
@@ -40,8 +40,48 @@ class StandaloneMysql extends BaseModel
|
||||
$database->scheduledBackups()->delete();
|
||||
$database->persistentStorages()->delete();
|
||||
$database->environment_variables()->delete();
|
||||
$database->tags()->detach();
|
||||
});
|
||||
}
|
||||
public function realStatus()
|
||||
{
|
||||
return $this->getRawOriginal('status');
|
||||
}
|
||||
public function status(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
set: function ($value) {
|
||||
if (str($value)->contains('(')) {
|
||||
$status = str($value)->before('(')->trim()->value();
|
||||
$health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy';
|
||||
} else if (str($value)->contains(':')) {
|
||||
$status = str($value)->before(':')->trim()->value();
|
||||
$health = str($value)->after(':')->trim()->value() ?? 'unhealthy';
|
||||
} else {
|
||||
$status = $value;
|
||||
$health = 'unhealthy';
|
||||
}
|
||||
return "$status:$health";
|
||||
},
|
||||
get: function ($value) {
|
||||
if (str($value)->contains('(')) {
|
||||
$status = str($value)->before('(')->trim()->value();
|
||||
$health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy';
|
||||
} else if (str($value)->contains(':')) {
|
||||
$status = str($value)->before(':')->trim()->value();
|
||||
$health = str($value)->after(':')->trim()->value() ?? 'unhealthy';
|
||||
} else {
|
||||
$status = $value;
|
||||
$health = 'unhealthy';
|
||||
}
|
||||
return "$status:$health";
|
||||
},
|
||||
);
|
||||
}
|
||||
public function tags()
|
||||
{
|
||||
return $this->morphToMany(Tag::class, 'taggable');
|
||||
}
|
||||
public function team()
|
||||
{
|
||||
return data_get($this, 'environment.project.team');
|
||||
|
||||
@@ -40,8 +40,48 @@ class StandalonePostgresql extends BaseModel
|
||||
$database->scheduledBackups()->delete();
|
||||
$database->persistentStorages()->delete();
|
||||
$database->environment_variables()->delete();
|
||||
$database->tags()->detach();
|
||||
});
|
||||
}
|
||||
public function realStatus()
|
||||
{
|
||||
return $this->getRawOriginal('status');
|
||||
}
|
||||
public function status(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
set: function ($value) {
|
||||
if (str($value)->contains('(')) {
|
||||
$status = str($value)->before('(')->trim()->value();
|
||||
$health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy';
|
||||
} else if (str($value)->contains(':')) {
|
||||
$status = str($value)->before(':')->trim()->value();
|
||||
$health = str($value)->after(':')->trim()->value() ?? 'unhealthy';
|
||||
} else {
|
||||
$status = $value;
|
||||
$health = 'unhealthy';
|
||||
}
|
||||
return "$status:$health";
|
||||
},
|
||||
get: function ($value) {
|
||||
if (str($value)->contains('(')) {
|
||||
$status = str($value)->before('(')->trim()->value();
|
||||
$health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy';
|
||||
} else if (str($value)->contains(':')) {
|
||||
$status = str($value)->before(':')->trim()->value();
|
||||
$health = str($value)->after(':')->trim()->value() ?? 'unhealthy';
|
||||
} else {
|
||||
$status = $value;
|
||||
$health = 'unhealthy';
|
||||
}
|
||||
return "$status:$health";
|
||||
},
|
||||
);
|
||||
}
|
||||
public function tags()
|
||||
{
|
||||
return $this->morphToMany(Tag::class, 'taggable');
|
||||
}
|
||||
public function link()
|
||||
{
|
||||
if (data_get($this, 'environment.project.uuid')) {
|
||||
|
||||
@@ -35,8 +35,48 @@ class StandaloneRedis extends BaseModel
|
||||
}
|
||||
$database->persistentStorages()->delete();
|
||||
$database->environment_variables()->delete();
|
||||
$database->tags()->detach();
|
||||
});
|
||||
}
|
||||
public function realStatus()
|
||||
{
|
||||
return $this->getRawOriginal('status');
|
||||
}
|
||||
public function status(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
set: function ($value) {
|
||||
if (str($value)->contains('(')) {
|
||||
$status = str($value)->before('(')->trim()->value();
|
||||
$health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy';
|
||||
} else if (str($value)->contains(':')) {
|
||||
$status = str($value)->before(':')->trim()->value();
|
||||
$health = str($value)->after(':')->trim()->value() ?? 'unhealthy';
|
||||
} else {
|
||||
$status = $value;
|
||||
$health = 'unhealthy';
|
||||
}
|
||||
return "$status:$health";
|
||||
},
|
||||
get: function ($value) {
|
||||
if (str($value)->contains('(')) {
|
||||
$status = str($value)->before('(')->trim()->value();
|
||||
$health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy';
|
||||
} else if (str($value)->contains(':')) {
|
||||
$status = str($value)->before(':')->trim()->value();
|
||||
$health = str($value)->after(':')->trim()->value() ?? 'unhealthy';
|
||||
} else {
|
||||
$status = $value;
|
||||
$health = 'unhealthy';
|
||||
}
|
||||
return "$status:$health";
|
||||
},
|
||||
);
|
||||
}
|
||||
public function tags()
|
||||
{
|
||||
return $this->morphToMany(Tag::class, 'taggable');
|
||||
}
|
||||
public function team()
|
||||
{
|
||||
return data_get($this, 'environment.project.team');
|
||||
|
||||
31
app/Models/Tag.php
Normal file
31
app/Models/Tag.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
|
||||
class Tag extends BaseModel
|
||||
{
|
||||
protected $guarded = [];
|
||||
|
||||
|
||||
public function name(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn ($value) => strtolower($value),
|
||||
set: fn ($value) => strtolower($value)
|
||||
);
|
||||
}
|
||||
static public function ownedByCurrentTeam()
|
||||
{
|
||||
return Tag::whereTeamId(currentTeam()->id)->orderBy('name');
|
||||
}
|
||||
public function applications()
|
||||
{
|
||||
return $this->morphedByMany(Application::class, 'taggable');
|
||||
}
|
||||
public function services()
|
||||
{
|
||||
return $this->morphedByMany(Service::class, 'taggable');
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@ class DeploymentFailed extends Notification implements ShouldQueue
|
||||
if (Str::of($this->fqdn)->explode(',')->count() > 1) {
|
||||
$this->fqdn = Str::of($this->fqdn)->explode(',')->first();
|
||||
}
|
||||
$this->deployment_url = base_url() . "/project/{$this->project_uuid}/{$this->environment_name}/application/{$this->application->uuid}/deployment/{$this->deployment_uuid}";
|
||||
$this->deployment_url = base_url() . "/project/{$this->project_uuid}/" . urlencode($this->environment_name) . "/application/{$this->application->uuid}/deployment/{$this->deployment_uuid}";
|
||||
}
|
||||
|
||||
public function via(object $notifiable): array
|
||||
|
||||
@@ -38,7 +38,7 @@ class DeploymentSuccess extends Notification implements ShouldQueue
|
||||
if (Str::of($this->fqdn)->explode(',')->count() > 1) {
|
||||
$this->fqdn = Str::of($this->fqdn)->explode(',')->first();
|
||||
}
|
||||
$this->deployment_url = base_url() . "/project/{$this->project_uuid}/{$this->environment_name}/application/{$this->application->uuid}/deployment/{$this->deployment_uuid}";
|
||||
$this->deployment_url = base_url() . "/project/{$this->project_uuid}/" . urlencode($this->environment_name) . "/application/{$this->application->uuid}/deployment/{$this->deployment_uuid}";
|
||||
}
|
||||
|
||||
public function via(object $notifiable): array
|
||||
|
||||
@@ -31,7 +31,7 @@ class StatusChanged extends Notification implements ShouldQueue
|
||||
if (Str::of($this->fqdn)->explode(',')->count() > 1) {
|
||||
$this->fqdn = Str::of($this->fqdn)->explode(',')->first();
|
||||
}
|
||||
$this->resource_url = base_url() . "/project/{$this->project_uuid}/{$this->environment_name}/application/{$this->resource->uuid}";
|
||||
$this->resource_url = base_url() . "/project/{$this->project_uuid}/" . urlencode($this->environment_name) . "/application/{$this->resource->uuid}";
|
||||
}
|
||||
|
||||
public function via(object $notifiable): array
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Traits;
|
||||
|
||||
use App\Enums\ApplicationDeploymentStatus;
|
||||
use App\Models\Server;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
|
||||
trait ExecuteRemoteCommandNew
|
||||
{
|
||||
public static $batch_counter = 0;
|
||||
public function executeRemoteCommand(Server $server, $logModel, $commands)
|
||||
{
|
||||
static::$batch_counter++;
|
||||
if ($commands instanceof Collection) {
|
||||
$commandsText = $commands;
|
||||
} else {
|
||||
$commandsText = collect($commands);
|
||||
}
|
||||
$commandsText->each(function ($singleCommand) use ($server, $logModel) {
|
||||
$command = data_get($singleCommand, 'command') ?? $singleCommand[0] ?? null;
|
||||
if ($command === null) {
|
||||
throw new \RuntimeException('Command is not set');
|
||||
}
|
||||
$hidden = data_get($singleCommand, 'hidden', false);
|
||||
$customType = data_get($singleCommand, 'type');
|
||||
$ignoreErrors = data_get($singleCommand, 'ignore_errors', false);
|
||||
$save = data_get($singleCommand, 'save');
|
||||
|
||||
$remote_command = generateSshCommand($server, $command);
|
||||
$process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden, $customType, $logModel, $save) {
|
||||
$output = str($output)->trim();
|
||||
if ($output->startsWith('╔')) {
|
||||
$output = "\n" . $output;
|
||||
}
|
||||
$newLogEntry = [
|
||||
'command' => remove_iip($command),
|
||||
'output' => remove_iip($output),
|
||||
'type' => $customType ?? $type === 'err' ? 'stderr' : 'stdout',
|
||||
'timestamp' => Carbon::now('UTC'),
|
||||
'hidden' => $hidden,
|
||||
'batch' => static::$batch_counter,
|
||||
];
|
||||
|
||||
if (!$logModel->logs) {
|
||||
$newLogEntry['order'] = 1;
|
||||
} else {
|
||||
$previousLogs = json_decode($logModel->logs, associative: true, flags: JSON_THROW_ON_ERROR);
|
||||
$newLogEntry['order'] = count($previousLogs) + 1;
|
||||
}
|
||||
|
||||
$previousLogs[] = $newLogEntry;
|
||||
$logModel->logs = json_encode($previousLogs, flags: JSON_THROW_ON_ERROR);
|
||||
$logModel->save();
|
||||
|
||||
if ($save) {
|
||||
$this->remoteCommandOutputs[$save] = str($output)->trim();
|
||||
}
|
||||
});
|
||||
$logModel->update([
|
||||
'current_process_id' => $process->id(),
|
||||
]);
|
||||
|
||||
$processResult = $process->wait();
|
||||
if ($processResult->exitCode() !== 0) {
|
||||
if (!$ignoreErrors) {
|
||||
$status = ApplicationDeploymentStatus::FAILED->value;
|
||||
$logModel->status = $status;
|
||||
$logModel->save();
|
||||
throw new \RuntimeException($processResult->errorOutput());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,37 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\ApplicationDeploymentStatus;
|
||||
use App\Jobs\ApplicationDeploymentJob;
|
||||
use App\Jobs\ApplicationDeploymentNewJob;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationDeploymentQueue;
|
||||
use App\Models\ApplicationPreview;
|
||||
use App\Models\Server;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use App\Models\StandaloneDocker;
|
||||
use Spatie\Url\Url;
|
||||
|
||||
function queue_application_deployment(int $application_id, int $server_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, ?string $git_type = null, bool $is_new_deployment = false)
|
||||
function queue_application_deployment(Application $application, string $deployment_uuid, int | null $pull_request_id = 0, string $commit = 'HEAD', bool $force_rebuild = false, bool $is_webhook = false, bool $restart_only = false, ?string $git_type = null, bool $no_questions_asked = false, Server $server = null, StandaloneDocker $destination = null)
|
||||
{
|
||||
$server = Application::find($application_id)->destination->server;
|
||||
$application_id = $application->id;
|
||||
$deployment_link = Url::fromString($application->link() . "/deployment/{$deployment_uuid}");
|
||||
$deployment_url = $deployment_link->getPath();
|
||||
$server_id = $application->destination->server->id;
|
||||
$server_name = $application->destination->server->name;
|
||||
$destination_id = $application->destination->id;
|
||||
|
||||
if ($server) {
|
||||
$server_id = $server->id;
|
||||
$server_name = $server->name;
|
||||
}
|
||||
if ($destination) {
|
||||
$destination_id = $destination->id;
|
||||
}
|
||||
$deployment = ApplicationDeploymentQueue::create([
|
||||
'application_id' => $application_id,
|
||||
'application_name' => $application->name,
|
||||
'server_id' => $server_id,
|
||||
'server_name' => $server_name,
|
||||
'destination_id' => $destination_id,
|
||||
'deployment_uuid' => $deployment_uuid,
|
||||
'deployment_url' => $deployment_url,
|
||||
'pull_request_id' => $pull_request_id,
|
||||
'force_rebuild' => $force_rebuild,
|
||||
'is_webhook' => $is_webhook,
|
||||
@@ -22,317 +39,63 @@ function queue_application_deployment(int $application_id, int $server_id, strin
|
||||
'commit' => $commit,
|
||||
'git_type' => $git_type
|
||||
]);
|
||||
$deployments_per_server = ApplicationDeploymentQueue::where('server_id', $server_id)->whereIn('status', ['in_progress', 'queued'])->get();
|
||||
|
||||
$deployments = ApplicationDeploymentQueue::where('application_id', $application_id)->where('server_id', $server_id);
|
||||
$queued_deployments = $deployments->where('status', 'queued')->get()->sortByDesc('created_at');
|
||||
$running_deployments = $deployments->where('status', 'in_progress')->get()->sortByDesc('created_at');
|
||||
|
||||
ray("serverId:{$server->id}", "concurrentBuilds:{$server->settings->concurrent_builds}", "deployments:{$deployments_per_server->count()}", "queued:{$queued_deployments->count()}", "running:{$running_deployments->count()}");
|
||||
// ray('Q:' . $queued_deployments->count() . 'R:' . $running_deployments->count() . '| Queuing deployment: ' . $deployment_uuid . ' of applicationID: ' . $application_id . ' pull request: ' . $pull_request_id . ' with commit: ' . $commit . ' and is it forced: ' . $force_rebuild);
|
||||
|
||||
if ($queued_deployments->count() > 1) {
|
||||
$queued_deployments = $queued_deployments->skip(1);
|
||||
$queued_deployments->each(function ($queued_deployment, $key) {
|
||||
$queued_deployment->status = 'cancelled by system';
|
||||
$queued_deployment->save();
|
||||
});
|
||||
}
|
||||
if ($running_deployments->count() > 0) {
|
||||
return;
|
||||
}
|
||||
if ($deployments_per_server->count() > $server->settings->concurrent_builds) {
|
||||
return;
|
||||
}
|
||||
if ($is_new_deployment) {
|
||||
dispatch(new ApplicationDeploymentNewJob(
|
||||
deployment: $deployment,
|
||||
application: Application::find($application_id)
|
||||
if ($no_questions_asked) {
|
||||
$deployment->update([
|
||||
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
|
||||
]);
|
||||
dispatch(new ApplicationDeploymentJob(
|
||||
application_deployment_queue_id: $deployment->id,
|
||||
));
|
||||
} else {
|
||||
} else if (next_queuable($server_id, $application_id)) {
|
||||
$deployment->update([
|
||||
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
|
||||
]);
|
||||
dispatch(new ApplicationDeploymentJob(
|
||||
application_deployment_queue_id: $deployment->id,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
function queue_next_deployment(Application $application, bool $isNew = false)
|
||||
function force_start_deployment(ApplicationDeploymentQueue $deployment)
|
||||
{
|
||||
$deployment->update([
|
||||
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
|
||||
]);
|
||||
dispatch(new ApplicationDeploymentJob(
|
||||
application_deployment_queue_id: $deployment->id,
|
||||
));
|
||||
}
|
||||
function queue_next_deployment(Application $application)
|
||||
{
|
||||
$server_id = $application->destination->server_id;
|
||||
$next_found = ApplicationDeploymentQueue::where('server_id', $server_id)->where('status', 'queued')->get()->sortBy('created_at')->first();;
|
||||
// $next_found = ApplicationDeploymentQueue::where('status', 'queued')->get()->sortBy('created_at')->first();
|
||||
ray($next_found, $server_id);
|
||||
$next_found = ApplicationDeploymentQueue::where('server_id', $server_id)->where('status', 'queued')->get()->sortBy('created_at')->first();
|
||||
if ($next_found) {
|
||||
if ($isNew) {
|
||||
dispatch(new ApplicationDeploymentNewJob(
|
||||
deployment: $next_found,
|
||||
application: $application
|
||||
));
|
||||
} else {
|
||||
dispatch(new ApplicationDeploymentJob(
|
||||
application_deployment_queue_id: $next_found->id,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Deployment things
|
||||
function generateHostIpMapping(Server $server, string $network)
|
||||
{
|
||||
// Generate custom host<->ip hostnames
|
||||
$allContainers = instant_remote_process(["docker network inspect {$network} -f '{{json .Containers}}' "], $server);
|
||||
$allContainers = format_docker_command_output_to_json($allContainers);
|
||||
$ips = collect([]);
|
||||
if (count($allContainers) > 0) {
|
||||
$allContainers = $allContainers[0];
|
||||
foreach ($allContainers as $container) {
|
||||
$containerName = data_get($container, 'Name');
|
||||
if ($containerName === 'coolify-proxy') {
|
||||
continue;
|
||||
}
|
||||
$containerIp = data_get($container, 'IPv4Address');
|
||||
if ($containerName && $containerIp) {
|
||||
$containerIp = str($containerIp)->before('/');
|
||||
$ips->put($containerName, $containerIp->value());
|
||||
}
|
||||
}
|
||||
}
|
||||
return $ips->map(function ($ip, $name) {
|
||||
return "--add-host $name:$ip";
|
||||
})->implode(' ');
|
||||
}
|
||||
|
||||
function generateBaseDir(string $deplyomentUuid)
|
||||
{
|
||||
return "/artifacts/$deplyomentUuid";
|
||||
}
|
||||
function generateWorkdir(string $deplyomentUuid, Application $application)
|
||||
{
|
||||
return generateBaseDir($deplyomentUuid) . rtrim($application->base_directory, '/');
|
||||
}
|
||||
|
||||
function prepareHelperContainer(Server $server, string $network, string $deploymentUuid)
|
||||
{
|
||||
$basedir = generateBaseDir($deploymentUuid);
|
||||
$helperImage = config('coolify.helper_image');
|
||||
|
||||
$serverUserHomeDir = instant_remote_process(["echo \$HOME"], $server);
|
||||
$dockerConfigFileExists = instant_remote_process(["test -f {$serverUserHomeDir}/.docker/config.json && echo 'OK' || echo 'NOK'"], $server);
|
||||
|
||||
$commands = collect([]);
|
||||
if ($dockerConfigFileExists === 'OK') {
|
||||
$commands->push([
|
||||
"command" => "docker run -d --network $network --name $deploymentUuid --rm -v {$serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock $helperImage",
|
||||
"hidden" => true,
|
||||
]);
|
||||
} else {
|
||||
$commands->push([
|
||||
"command" => "docker run -d --network {$network} --name {$deploymentUuid} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}",
|
||||
"hidden" => true,
|
||||
$next_found->update([
|
||||
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
|
||||
]);
|
||||
dispatch(new ApplicationDeploymentJob(
|
||||
application_deployment_queue_id: $next_found->id,
|
||||
));
|
||||
}
|
||||
$commands->push([
|
||||
"command" => executeInDocker($deploymentUuid, "mkdir -p {$basedir}"),
|
||||
"hidden" => true,
|
||||
]);
|
||||
return $commands;
|
||||
}
|
||||
|
||||
function generateComposeFile(string $deploymentUuid, Server $server, string $network, Application $application, string $containerName, string $imageName, ?ApplicationPreview $preview = null, int $pullRequestId = 0)
|
||||
function next_queuable(string $server_id, string $application_id): bool
|
||||
{
|
||||
$ports = $application->settings->is_static ? [80] : $application->ports_exposes_array;
|
||||
$workDir = generateWorkdir($deploymentUuid, $application);
|
||||
$persistent_storages = generateLocalPersistentVolumes($application, $pullRequestId);
|
||||
$volume_names = generateLocalPersistentVolumesOnlyVolumeNames($application, $pullRequestId);
|
||||
$environment_variables = generateEnvironmentVariables($application, $ports, $pullRequestId);
|
||||
$deployments = ApplicationDeploymentQueue::where('server_id', $server_id)->whereIn('status', ['in_progress', 'queued'])->get()->sortByDesc('created_at');
|
||||
$same_application_deployments = $deployments->where('application_id', $application_id);
|
||||
$in_progress = $same_application_deployments->filter(function ($value, $key) {
|
||||
return $value->status === 'in_progress';
|
||||
});
|
||||
if ($in_progress->count() > 0) {
|
||||
return false;
|
||||
}
|
||||
$server = Server::find($server_id);
|
||||
$concurrent_builds = $server->settings->concurrent_builds;
|
||||
|
||||
if (data_get($application, 'custom_labels')) {
|
||||
$labels = collect(str($application->custom_labels)->explode(','));
|
||||
$labels = $labels->filter(function ($value, $key) {
|
||||
return !str($value)->startsWith('coolify.');
|
||||
});
|
||||
$application->custom_labels = $labels->implode(',');
|
||||
$application->save();
|
||||
} else {
|
||||
$labels = collect(generateLabelsApplication($application, $preview));
|
||||
}
|
||||
if ($pullRequestId !== 0) {
|
||||
$labels = collect(generateLabelsApplication($application, $preview));
|
||||
}
|
||||
$labels = $labels->merge(defaultLabels($application->id, $application->uuid, 0))->toArray();
|
||||
$docker_compose = [
|
||||
'version' => '3.8',
|
||||
'services' => [
|
||||
$containerName => [
|
||||
'image' => $imageName,
|
||||
'container_name' => $containerName,
|
||||
'restart' => RESTART_MODE,
|
||||
'environment' => $environment_variables,
|
||||
'labels' => $labels,
|
||||
'expose' => $ports,
|
||||
'networks' => [
|
||||
$network,
|
||||
],
|
||||
'mem_limit' => $application->limits_memory,
|
||||
'memswap_limit' => $application->limits_memory_swap,
|
||||
'mem_swappiness' => $application->limits_memory_swappiness,
|
||||
'mem_reservation' => $application->limits_memory_reservation,
|
||||
'cpus' => (int) $application->limits_cpus,
|
||||
'cpu_shares' => $application->limits_cpu_shares,
|
||||
]
|
||||
],
|
||||
'networks' => [
|
||||
$network => [
|
||||
'external' => true,
|
||||
'name' => $network,
|
||||
'attachable' => true
|
||||
]
|
||||
]
|
||||
];
|
||||
if (!is_null($application->limits_cpuset)) {
|
||||
data_set($docker_compose, "services.{$containerName}.cpuset", $application->limits_cpuset);
|
||||
}
|
||||
if ($server->isLogDrainEnabled() && $application->isLogDrainEnabled()) {
|
||||
$docker_compose['services'][$containerName]['logging'] = [
|
||||
'driver' => 'fluentd',
|
||||
'options' => [
|
||||
'fluentd-address' => "tcp://127.0.0.1:24224",
|
||||
'fluentd-async' => "true",
|
||||
'fluentd-sub-second-precision' => "true",
|
||||
]
|
||||
];
|
||||
}
|
||||
if ($application->settings->is_gpu_enabled) {
|
||||
$docker_compose['services'][$containerName]['deploy']['resources']['reservations']['devices'] = [
|
||||
[
|
||||
'driver' => data_get($application, 'settings.gpu_driver', 'nvidia'),
|
||||
'capabilities' => ['gpu'],
|
||||
'options' => data_get($application, 'settings.gpu_options', [])
|
||||
]
|
||||
];
|
||||
if (data_get($application, 'settings.gpu_count')) {
|
||||
$count = data_get($application, 'settings.gpu_count');
|
||||
if ($count === 'all') {
|
||||
$docker_compose['services'][$containerName]['deploy']['resources']['reservations']['devices'][0]['count'] = $count;
|
||||
} else {
|
||||
$docker_compose['services'][$containerName]['deploy']['resources']['reservations']['devices'][0]['count'] = (int) $count;
|
||||
}
|
||||
} else if (data_get($application, 'settings.gpu_device_ids')) {
|
||||
$docker_compose['services'][$containerName]['deploy']['resources']['reservations']['devices'][0]['ids'] = data_get($application, 'settings.gpu_device_ids');
|
||||
}
|
||||
}
|
||||
if ($application->isHealthcheckDisabled()) {
|
||||
data_forget($docker_compose, 'services.' . $containerName . '.healthcheck');
|
||||
}
|
||||
if (count($application->ports_mappings_array) > 0 && $pullRequestId === 0) {
|
||||
$docker_compose['services'][$containerName]['ports'] = $application->ports_mappings_array;
|
||||
}
|
||||
if (count($persistent_storages) > 0) {
|
||||
$docker_compose['services'][$containerName]['volumes'] = $persistent_storages;
|
||||
}
|
||||
if (count($volume_names) > 0) {
|
||||
$docker_compose['volumes'] = $volume_names;
|
||||
}
|
||||
$docker_compose = Yaml::dump($docker_compose, 10);
|
||||
$docker_compose_base64 = base64_encode($docker_compose);
|
||||
$commands = collect([]);
|
||||
$commands->push([
|
||||
"command" => executeInDocker($deploymentUuid, "echo '{$docker_compose_base64}' | base64 -d > {$workDir}/docker-compose.yml"),
|
||||
"hidden" => true,
|
||||
]);
|
||||
return $commands;
|
||||
}
|
||||
function generateLocalPersistentVolumes(Application $application, int $pullRequestId = 0)
|
||||
{
|
||||
$local_persistent_volumes = [];
|
||||
foreach ($application->persistentStorages as $persistentStorage) {
|
||||
$volume_name = $persistentStorage->host_path ?? $persistentStorage->name;
|
||||
if ($pullRequestId !== 0) {
|
||||
$volume_name = $volume_name . '-pr-' . $pullRequestId;
|
||||
}
|
||||
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
|
||||
}
|
||||
return $local_persistent_volumes;
|
||||
}
|
||||
ray("serverId:{$server->id}", "concurrentBuilds:{$concurrent_builds}", "deployments:{$deployments->count()}", "sameApplicationDeployments:{$same_application_deployments->count()}");
|
||||
|
||||
function generateLocalPersistentVolumesOnlyVolumeNames(Application $application, int $pullRequestId = 0)
|
||||
{
|
||||
$local_persistent_volumes_names = [];
|
||||
foreach ($application->persistentStorages as $persistentStorage) {
|
||||
if ($persistentStorage->host_path) {
|
||||
continue;
|
||||
}
|
||||
$name = $persistentStorage->name;
|
||||
|
||||
if ($pullRequestId !== 0) {
|
||||
$name = $name . '-pr-' . $pullRequestId;
|
||||
}
|
||||
|
||||
$local_persistent_volumes_names[$name] = [
|
||||
'name' => $name,
|
||||
'external' => false,
|
||||
];
|
||||
if ($deployments->count() > $concurrent_builds) {
|
||||
return false;
|
||||
}
|
||||
return $local_persistent_volumes_names;
|
||||
}
|
||||
function generateEnvironmentVariables(Application $application, $ports, int $pullRequestId = 0)
|
||||
{
|
||||
$environment_variables = collect();
|
||||
// ray('Generate Environment Variables')->green();
|
||||
if ($pullRequestId === 0) {
|
||||
// ray($this->application->runtime_environment_variables)->green();
|
||||
foreach ($application->runtime_environment_variables as $env) {
|
||||
$environment_variables->push("$env->key=$env->value");
|
||||
}
|
||||
foreach ($application->nixpacks_environment_variables as $env) {
|
||||
$environment_variables->push("$env->key=$env->value");
|
||||
}
|
||||
} else {
|
||||
// ray($this->application->runtime_environment_variables_preview)->green();
|
||||
foreach ($application->runtime_environment_variables_preview as $env) {
|
||||
$environment_variables->push("$env->key=$env->value");
|
||||
}
|
||||
foreach ($application->nixpacks_environment_variables_preview as $env) {
|
||||
$environment_variables->push("$env->key=$env->value");
|
||||
}
|
||||
}
|
||||
// Add PORT if not exists, use the first port as default
|
||||
if ($environment_variables->filter(fn ($env) => str($env)->contains('PORT'))->isEmpty()) {
|
||||
$environment_variables->push("PORT={$ports[0]}");
|
||||
}
|
||||
return $environment_variables->all();
|
||||
}
|
||||
|
||||
function startNewApplication(Application $application, string $deploymentUuid, ApplicationDeploymentQueue $loggingModel)
|
||||
{
|
||||
$commands = collect([]);
|
||||
$workDir = generateWorkdir($deploymentUuid, $application);
|
||||
if ($application->build_pack === 'dockerimage') {
|
||||
$loggingModel->addLogEntry('Pulling latest images from the registry.');
|
||||
$commands->push(
|
||||
[
|
||||
"command" => executeInDocker($deploymentUuid, "docker compose --project-directory {$workDir} pull"),
|
||||
"hidden" => true
|
||||
],
|
||||
[
|
||||
"command" => executeInDocker($deploymentUuid, "docker compose --project-directory {$workDir} up --build -d"),
|
||||
"hidden" => true
|
||||
],
|
||||
);
|
||||
} else {
|
||||
$commands->push(
|
||||
[
|
||||
"command" => executeInDocker($deploymentUuid, "docker compose --project-directory {$workDir} up --build -d"),
|
||||
"hidden" => true
|
||||
],
|
||||
);
|
||||
}
|
||||
return $commands;
|
||||
}
|
||||
function removeOldDeployment(string $containerName)
|
||||
{
|
||||
$commands = collect([]);
|
||||
$commands->push(
|
||||
["docker rm -f $containerName >/dev/null 2>&1"],
|
||||
);
|
||||
return $commands;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,11 @@ const VALID_CRON_STRINGS = [
|
||||
const RESTART_MODE = 'unless-stopped';
|
||||
|
||||
const DATABASE_DOCKER_IMAGES = [
|
||||
'bitnami/mariadb',
|
||||
'bitnami/mongodb',
|
||||
'bitnami/mysql',
|
||||
'bitnami/postgresql',
|
||||
'bitnami/redis',
|
||||
'mysql',
|
||||
'mariadb',
|
||||
'postgres',
|
||||
@@ -34,3 +39,5 @@ const SUPPORTED_OS = [
|
||||
'centos fedora rhel ol rocky',
|
||||
'sles opensuse-leap opensuse-tumbleweed'
|
||||
];
|
||||
|
||||
const SHARED_VARIABLE_TYPES = ['team', 'project', 'environment'];
|
||||
|
||||
@@ -3,9 +3,7 @@
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationPreview;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use App\Models\ServiceApplication;
|
||||
use App\Models\ServiceDatabase;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
use Spatie\Url\Url;
|
||||
@@ -125,10 +123,14 @@ function getContainerStatus(Server $server, string $container_id, bool $all_data
|
||||
|
||||
function generateApplicationContainerName(Application $application, $pull_request_id = 0)
|
||||
{
|
||||
$consistent_container_name = $application->settings->is_consistent_container_name_enabled;
|
||||
$now = now()->format('Hisu');
|
||||
if ($pull_request_id !== 0 && $pull_request_id !== null) {
|
||||
return $application->uuid . '-pr-' . $pull_request_id;
|
||||
} else {
|
||||
if ($consistent_container_name) {
|
||||
return $application->uuid;
|
||||
}
|
||||
return $application->uuid . '-' . $now;
|
||||
}
|
||||
}
|
||||
@@ -211,15 +213,48 @@ function generateServiceSpecificFqdns(ServiceApplication|Application $resource,
|
||||
}
|
||||
return $payload;
|
||||
}
|
||||
function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null)
|
||||
function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null)
|
||||
{
|
||||
$labels = collect([]);
|
||||
$labels->push('traefik.enable=true');
|
||||
$labels->push("traefik.http.middlewares.gzip.compress=true");
|
||||
$labels->push("traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https");
|
||||
|
||||
$basic_auth = false;
|
||||
$basic_auth_middleware = null;
|
||||
$redirect = false;
|
||||
$redirect_middleware = null;
|
||||
if ($serviceLabels) {
|
||||
$basic_auth = $serviceLabels->contains(function ($value) {
|
||||
return str_contains($value, 'basicauth');
|
||||
});
|
||||
if ($basic_auth) {
|
||||
$basic_auth_middleware = $serviceLabels
|
||||
->map(function ($item) {
|
||||
if (preg_match('/traefik\.http\.middlewares\.(.*?)\.basicauth\.users/', $item, $matches)) {
|
||||
return $matches[1];
|
||||
}
|
||||
})
|
||||
->filter()
|
||||
->first();
|
||||
}
|
||||
$redirect = $serviceLabels->contains(function ($value) {
|
||||
return str_contains($value, 'redirectregex');
|
||||
});
|
||||
if ($redirect) {
|
||||
$redirect_middleware = $serviceLabels
|
||||
->map(function ($item) {
|
||||
if (preg_match('/traefik\.http\.middlewares\.(.*?)\.redirectregex\.regex/', $item, $matches)) {
|
||||
return $matches[1];
|
||||
}
|
||||
})
|
||||
->filter()
|
||||
->first();
|
||||
}
|
||||
}
|
||||
foreach ($domains as $loop => $domain) {
|
||||
try {
|
||||
$uuid = new Cuid2(7);
|
||||
// $uuid = new Cuid2(7);
|
||||
$url = Url::fromString($domain);
|
||||
$host = $url->getHost();
|
||||
$path = $url->getPath();
|
||||
@@ -241,11 +276,24 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
|
||||
}
|
||||
if ($path !== '/') {
|
||||
$labels->push("traefik.http.middlewares.{$https_label}-stripprefix.stripprefix.prefixes={$path}");
|
||||
$labels->push("traefik.http.routers.{$https_label}.middlewares={$https_label}-stripprefix,gzip");
|
||||
$middlewares = "gzip,{$https_label}-stripprefix";
|
||||
if ($basic_auth && $basic_auth_middleware) {
|
||||
$middlewares = $middlewares . ',' . $basic_auth_middleware;
|
||||
}
|
||||
if ($redirect && $redirect_middleware) {
|
||||
$middlewares = $middlewares . ',' . $redirect_middleware;
|
||||
}
|
||||
$labels->push("traefik.http.routers.{$https_label}.middlewares={$middlewares}");
|
||||
} else {
|
||||
$labels->push("traefik.http.routers.{$https_label}.middlewares=gzip");
|
||||
$middlewares = "gzip";
|
||||
if ($basic_auth && $basic_auth_middleware) {
|
||||
$middlewares = $middlewares . ',' . $basic_auth_middleware;
|
||||
}
|
||||
if ($redirect && $redirect_middleware) {
|
||||
$middlewares = $middlewares . ',' . $redirect_middleware;
|
||||
}
|
||||
$labels->push("traefik.http.routers.{$https_label}.middlewares={$middlewares}");
|
||||
}
|
||||
|
||||
$labels->push("traefik.http.routers.{$https_label}.tls=true");
|
||||
$labels->push("traefik.http.routers.{$https_label}.tls.certresolver=letsencrypt");
|
||||
|
||||
@@ -269,16 +317,29 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
|
||||
}
|
||||
if ($path !== '/') {
|
||||
$labels->push("traefik.http.middlewares.{$http_label}-stripprefix.stripprefix.prefixes={$path}");
|
||||
$labels->push("traefik.http.routers.{$http_label}.middlewares={$http_label}-stripprefix,gzip");
|
||||
$middlewares = "gzip,{$http_label}-stripprefix";
|
||||
if ($basic_auth && $basic_auth_middleware) {
|
||||
$middlewares = $middlewares . ',' . $basic_auth_middleware;
|
||||
}
|
||||
if ($redirect && $redirect_middleware) {
|
||||
$middlewares = $middlewares . ',' . $redirect_middleware;
|
||||
}
|
||||
$labels->push("traefik.http.routers.{$http_label}.middlewares={$middlewares}");
|
||||
} else {
|
||||
$labels->push("traefik.http.routers.{$http_label}.middlewares=gzip");
|
||||
$middlewares = "gzip";
|
||||
if ($basic_auth && $basic_auth_middleware) {
|
||||
$middlewares = $middlewares . ',' . $basic_auth_middleware;
|
||||
}
|
||||
if ($redirect && $redirect_middleware) {
|
||||
$middlewares = $middlewares . ',' . $redirect_middleware;
|
||||
}
|
||||
$labels->push("traefik.http.routers.{$http_label}.middlewares={$middlewares}");
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return $labels->sort();
|
||||
}
|
||||
function generateLabelsApplication(Application $application, ?ApplicationPreview $preview = null): array
|
||||
@@ -323,3 +384,83 @@ function isDatabaseImage(?string $image = null)
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function convert_docker_run_to_compose(?string $custom_docker_run_options = null)
|
||||
{
|
||||
$options = [];
|
||||
$compose_options = collect([]);
|
||||
preg_match_all('/(--\w+(?:-\w+)*)(?:\s|=)?([^\s-]+)?/', $custom_docker_run_options, $matches, PREG_SET_ORDER);
|
||||
$list_options = collect([
|
||||
'--cap-add',
|
||||
'--cap-drop',
|
||||
'--security-opt',
|
||||
'--sysctl',
|
||||
'--ulimit',
|
||||
'--device'
|
||||
]);
|
||||
$mapping = collect([
|
||||
'--cap-add' => 'cap_add',
|
||||
'--cap-drop' => 'cap_drop',
|
||||
'--security-opt' => 'security_opt',
|
||||
'--sysctl' => 'sysctls',
|
||||
'--ulimit' => 'ulimits',
|
||||
'--device' => 'devices',
|
||||
'--init' => 'init',
|
||||
'--ulimit' => 'ulimits',
|
||||
'--privileged' => 'privileged',
|
||||
]);
|
||||
foreach ($matches as $match) {
|
||||
$option = $match[1];
|
||||
if (isset($match[2]) && $match[2] !== '') {
|
||||
$value = $match[2];
|
||||
$options[$option][] = $value;
|
||||
$options[$option] = array_unique($options[$option]);
|
||||
} else {
|
||||
$value = true;
|
||||
$options[$option] = $value;
|
||||
}
|
||||
}
|
||||
$options = collect($options);
|
||||
// Easily get mappings from https://github.com/composerize/composerize/blob/master/packages/composerize/src/mappings.js
|
||||
foreach ($options as $option => $value) {
|
||||
if (!data_get($mapping, $option)) {
|
||||
continue;
|
||||
}
|
||||
if ($option === '--ulimit') {
|
||||
$ulimits = collect([]);
|
||||
collect($value)->map(function ($ulimit) use ($ulimits) {
|
||||
$ulimit = explode('=', $ulimit);
|
||||
$type = $ulimit[0];
|
||||
$limits = explode(':', $ulimit[1]);
|
||||
if (count($limits) == 2) {
|
||||
$soft_limit = $limits[0];
|
||||
$hard_limit = $limits[1];
|
||||
$ulimits->put($type, [
|
||||
'soft' => $soft_limit,
|
||||
'hard' => $hard_limit
|
||||
]);
|
||||
} else {
|
||||
$soft_limit = $ulimit[1];
|
||||
$ulimits->put($type, [
|
||||
'soft' => $soft_limit,
|
||||
]);
|
||||
}
|
||||
});
|
||||
$compose_options->put($mapping[$option], $ulimits);
|
||||
} else {
|
||||
if ($list_options->contains($option)) {
|
||||
if ($compose_options->has($mapping[$option])) {
|
||||
$compose_options->put($mapping[$option], $options->get($mapping[$option]) . ',' . $value);
|
||||
} else {
|
||||
$compose_options->put($mapping[$option], $value);
|
||||
}
|
||||
continue;
|
||||
} else {
|
||||
$compose_options->put($mapping[$option], $value);
|
||||
continue;
|
||||
}
|
||||
$compose_options->forget($option);
|
||||
}
|
||||
}
|
||||
return $compose_options->toArray();
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ function generate_github_installation_token(GithubApp $source)
|
||||
'Accept' => 'application/vnd.github.machine-man-preview+json'
|
||||
])->post("{$source->api_url}/app/installations/{$source->installation_id}/access_tokens");
|
||||
if ($token->failed()) {
|
||||
throw new \Exception("Failed to get access token for " . $source->name . " with error: " . $token->json()['message']);
|
||||
throw new RuntimeException("Failed to get access token for " . $source->name . " with error: " . $token->json()['message']);
|
||||
}
|
||||
return $token->json()['token'];
|
||||
}
|
||||
@@ -69,6 +69,7 @@ function githubApi(GithubApp|GitlabApp|null $source, string $endpoint, string $m
|
||||
}
|
||||
$json = $response->json();
|
||||
if ($response->failed() && $throwError) {
|
||||
ray($json);
|
||||
throw new \Exception("Failed to get data from {$source->name} with error:<br><br>" . $json['message'] . "<br><br>Rate Limit resets at: " . Carbon::parse((int)$response->header('X-RateLimit-Reset'))->format('Y-m-d H:i:s') . 'UTC');
|
||||
}
|
||||
return [
|
||||
|
||||
@@ -22,6 +22,7 @@ use App\Notifications\Channels\EmailChannel;
|
||||
use App\Notifications\Channels\TelegramChannel;
|
||||
use App\Notifications\Internal\GeneralNotification;
|
||||
use DanHarrin\LivewireRateLimiting\Exceptions\TooManyRequestsException;
|
||||
use Illuminate\Database\UniqueConstraintViolationException;
|
||||
use Illuminate\Mail\Message;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
@@ -103,10 +104,16 @@ function handleError(?Throwable $error = null, ?Livewire\Component $livewire = n
|
||||
ray($error);
|
||||
if ($error instanceof TooManyRequestsException) {
|
||||
if (isset($livewire)) {
|
||||
return $livewire->dispatch('error', "Too many requests. Please try again in {$error->secondsUntilAvailable} seconds.");
|
||||
return $livewire->dispatch('error', "Too many requests.", "Please try again in {$error->secondsUntilAvailable} seconds.");
|
||||
}
|
||||
return "Too many requests. Please try again in {$error->secondsUntilAvailable} seconds.";
|
||||
}
|
||||
if ($error instanceof UniqueConstraintViolationException) {
|
||||
if (isset($livewire)) {
|
||||
return $livewire->dispatch('error', "Duplicate entry found.", "Please use a different name.");
|
||||
}
|
||||
return "Duplicate entry found. Please use a different name.";
|
||||
}
|
||||
|
||||
if ($error instanceof Throwable) {
|
||||
$message = $error->getMessage();
|
||||
@@ -118,6 +125,9 @@ function handleError(?Throwable $error = null, ?Livewire\Component $livewire = n
|
||||
}
|
||||
|
||||
if (isset($livewire)) {
|
||||
if (str($message)->length() > 20) {
|
||||
return $livewire->dispatch('error', 'Error occured', $message);
|
||||
}
|
||||
return $livewire->dispatch('error', $message);
|
||||
}
|
||||
throw new Exception($message);
|
||||
@@ -291,10 +301,8 @@ function validate_cron_expression($expression_to_validate): bool
|
||||
function send_internal_notification(string $message): void
|
||||
{
|
||||
try {
|
||||
$baseUrl = config('app.name');
|
||||
$team = Team::find(0);
|
||||
$team?->notify(new GeneralNotification("👀 {$baseUrl}: " . $message));
|
||||
ray("👀 {$baseUrl}: " . $message);
|
||||
$team?->notify(new GeneralNotification($message));
|
||||
} catch (\Throwable $e) {
|
||||
ray($e->getMessage());
|
||||
}
|
||||
@@ -474,7 +482,14 @@ function queryResourcesByUuid(string $uuid)
|
||||
if ($mariadb) return $mariadb;
|
||||
return $resource;
|
||||
}
|
||||
|
||||
function generatTagDeployWebhook($tag_name)
|
||||
{
|
||||
$baseUrl = base_url();
|
||||
$api = Url::fromString($baseUrl) . '/api/v1';
|
||||
$endpoint = "/deploy?tag=$tag_name";
|
||||
$url = $api . $endpoint;
|
||||
return $url;
|
||||
}
|
||||
function generateDeployWebhook($resource)
|
||||
{
|
||||
$baseUrl = base_url();
|
||||
@@ -515,28 +530,32 @@ function getTopLevelNetworks(Service|Application $resource)
|
||||
$definedNetwork = collect([$resource->uuid]);
|
||||
$services = collect($services)->map(function ($service, $_) use ($topLevelNetworks, $definedNetwork) {
|
||||
$serviceNetworks = collect(data_get($service, 'networks', []));
|
||||
$hasHostNetworkMode = data_get($service, 'network_mode') === 'host' ? true : false;
|
||||
|
||||
// Collect/create/update networks
|
||||
if ($serviceNetworks->count() > 0) {
|
||||
foreach ($serviceNetworks as $networkName => $networkDetails) {
|
||||
$networkExists = $topLevelNetworks->contains(function ($value, $key) use ($networkName) {
|
||||
return $value == $networkName || $key == $networkName;
|
||||
});
|
||||
if (!$networkExists) {
|
||||
$topLevelNetworks->put($networkDetails, null);
|
||||
// Only add 'networks' key if 'network_mode' is not 'host'
|
||||
if (!$hasHostNetworkMode) {
|
||||
// Collect/create/update networks
|
||||
if ($serviceNetworks->count() > 0) {
|
||||
foreach ($serviceNetworks as $networkName => $networkDetails) {
|
||||
$networkExists = $topLevelNetworks->contains(function ($value, $key) use ($networkName) {
|
||||
return $value == $networkName || $key == $networkName;
|
||||
});
|
||||
if (!$networkExists) {
|
||||
$topLevelNetworks->put($networkDetails, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$definedNetworkExists = $topLevelNetworks->contains(function ($value, $_) use ($definedNetwork) {
|
||||
return $value == $definedNetwork;
|
||||
});
|
||||
if (!$definedNetworkExists) {
|
||||
foreach ($definedNetwork as $network) {
|
||||
$topLevelNetworks->put($network, [
|
||||
'name' => $network,
|
||||
'external' => true
|
||||
]);
|
||||
$definedNetworkExists = $topLevelNetworks->contains(function ($value, $_) use ($definedNetwork) {
|
||||
return $value == $definedNetwork;
|
||||
});
|
||||
if (!$definedNetworkExists) {
|
||||
foreach ($definedNetwork as $network) {
|
||||
$topLevelNetworks->put($network, [
|
||||
'name' => $network,
|
||||
'external' => true
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -616,6 +635,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
|
||||
$serviceNetworks = collect(data_get($service, 'networks', []));
|
||||
$serviceVariables = collect(data_get($service, 'environment', []));
|
||||
$serviceLabels = collect(data_get($service, 'labels', []));
|
||||
$hasHostNetworkMode = data_get($service, 'network_mode') === 'host' ? true : false;
|
||||
if ($serviceLabels->count() > 0) {
|
||||
$removedLabels = collect([]);
|
||||
$serviceLabels = $serviceLabels->filter(function ($serviceLabel, $serviceLabelName) use ($removedLabels) {
|
||||
@@ -686,7 +706,6 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
|
||||
$savedService->image = $image;
|
||||
$savedService->save();
|
||||
}
|
||||
|
||||
// Collect/create/update networks
|
||||
if ($serviceNetworks->count() > 0) {
|
||||
foreach ($serviceNetworks as $networkName => $networkDetails) {
|
||||
@@ -717,37 +736,39 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
|
||||
$savedService->ports = $collectedPorts->implode(',');
|
||||
$savedService->save();
|
||||
|
||||
// Add Coolify specific networks
|
||||
$definedNetworkExists = $topLevelNetworks->contains(function ($value, $_) use ($definedNetwork) {
|
||||
return $value == $definedNetwork;
|
||||
});
|
||||
if (!$definedNetworkExists) {
|
||||
foreach ($definedNetwork as $network) {
|
||||
$topLevelNetworks->put($network, [
|
||||
'name' => $network,
|
||||
'external' => true
|
||||
]);
|
||||
if (!$hasHostNetworkMode) {
|
||||
// Add Coolify specific networks
|
||||
$definedNetworkExists = $topLevelNetworks->contains(function ($value, $_) use ($definedNetwork) {
|
||||
return $value == $definedNetwork;
|
||||
});
|
||||
if (!$definedNetworkExists) {
|
||||
foreach ($definedNetwork as $network) {
|
||||
$topLevelNetworks->put($network, [
|
||||
'name' => $network,
|
||||
'external' => true
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
$networks = collect();
|
||||
foreach ($serviceNetworks as $key => $serviceNetwork) {
|
||||
if (gettype($serviceNetwork) === 'string') {
|
||||
// networks:
|
||||
// - appwrite
|
||||
$networks->put($serviceNetwork, null);
|
||||
} else if (gettype($serviceNetwork) === 'array') {
|
||||
// networks:
|
||||
// default:
|
||||
// ipv4_address: 192.168.203.254
|
||||
// $networks->put($serviceNetwork, null);
|
||||
ray($key);
|
||||
$networks->put($key, $serviceNetwork);
|
||||
$networks = collect();
|
||||
foreach ($serviceNetworks as $key => $serviceNetwork) {
|
||||
if (gettype($serviceNetwork) === 'string') {
|
||||
// networks:
|
||||
// - appwrite
|
||||
$networks->put($serviceNetwork, null);
|
||||
} else if (gettype($serviceNetwork) === 'array') {
|
||||
// networks:
|
||||
// default:
|
||||
// ipv4_address: 192.168.203.254
|
||||
// $networks->put($serviceNetwork, null);
|
||||
ray($key);
|
||||
$networks->put($key, $serviceNetwork);
|
||||
}
|
||||
}
|
||||
foreach ($definedNetwork as $key => $network) {
|
||||
$networks->put($network, null);
|
||||
}
|
||||
data_set($service, 'networks', $networks->toArray());
|
||||
}
|
||||
foreach ($definedNetwork as $key => $network) {
|
||||
$networks->put($network, null);
|
||||
}
|
||||
data_set($service, 'networks', $networks->toArray());
|
||||
|
||||
// Collect/create/update volumes
|
||||
if ($serviceVolumes->count() > 0) {
|
||||
@@ -770,14 +791,14 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
|
||||
$source = data_get_str($volume, 'source');
|
||||
$target = data_get_str($volume, 'target');
|
||||
$content = data_get($volume, 'content');
|
||||
$isDirectory = (bool) data_get($volume, 'isDirectory', false);
|
||||
$isDirectory = (bool) data_get($volume, 'isDirectory', false) || (bool) data_get($volume, 'is_directory', false);
|
||||
$foundConfig = $savedService->fileStorages()->whereMountPath($target)->first();
|
||||
if ($foundConfig) {
|
||||
$contentNotNull = data_get($foundConfig, 'content');
|
||||
if ($contentNotNull) {
|
||||
$content = $contentNotNull;
|
||||
}
|
||||
$isDirectory = (bool) data_get($foundConfig, 'is_directory');
|
||||
$isDirectory = (bool) data_get($volume, 'isDirectory', false) || (bool) data_get($volume, 'is_directory', false);
|
||||
}
|
||||
}
|
||||
if ($type->value() === 'bind') {
|
||||
@@ -930,7 +951,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
|
||||
'service_id' => $resource->id,
|
||||
])->first();
|
||||
['command' => $command, 'forService' => $forService, 'generatedValue' => $generatedValue, 'port' => $port] = parseEnvVariable($value);
|
||||
if ($command->value() === 'FQDN' || $command->value() === 'URL') {
|
||||
if ($command?->value() === 'FQDN' || $command?->value() === 'URL') {
|
||||
if (Str::lower($forService) === $serviceName) {
|
||||
$fqdn = generateFqdn($resource->server, $containerName);
|
||||
} else {
|
||||
@@ -1018,7 +1039,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
|
||||
$serviceLabels = $serviceLabels->merge($defaultLabels);
|
||||
if (!$isDatabase && $fqdns->count() > 0) {
|
||||
if ($fqdns) {
|
||||
$serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($resource->uuid, $fqdns, true));
|
||||
$serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($resource->uuid, $fqdns, true, serviceLabels: $serviceLabels));
|
||||
}
|
||||
}
|
||||
if ($resource->server->isLogDrainEnabled() && $savedService->isLogDrainEnabled()) {
|
||||
@@ -1042,6 +1063,8 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
|
||||
data_set($service, 'container_name', $containerName);
|
||||
data_forget($service, 'volumes.*.content');
|
||||
data_forget($service, 'volumes.*.isDirectory');
|
||||
data_forget($service, 'volumes.*.is_directory');
|
||||
|
||||
// Remove unnecessary variables from service.environment
|
||||
// $withoutServiceEnvs = collect([]);
|
||||
// collect(data_get($service, 'environment'))->each(function ($value, $key) use ($withoutServiceEnvs) {
|
||||
@@ -1350,7 +1373,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
|
||||
'application_id' => $resource->id,
|
||||
])->first();
|
||||
['command' => $command, 'forService' => $forService, 'generatedValue' => $generatedValue, 'port' => $port] = parseEnvVariable($value);
|
||||
if ($command->value() === 'FQDN' || $command->value() === 'URL') {
|
||||
if ($command?->value() === 'FQDN' || $command?->value() === 'URL') {
|
||||
if (Str::lower($forService) === $serviceName) {
|
||||
$fqdn = generateFqdn($server, $containerName);
|
||||
} else {
|
||||
@@ -1457,7 +1480,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
|
||||
return $preview_fqdn;
|
||||
});
|
||||
}
|
||||
$serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($uuid, $fqdns));
|
||||
$serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($uuid, $fqdns,serviceLabels: $serviceLabels));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1660,3 +1683,42 @@ function ip_match($ip, $cidrs, &$match = null)
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function check_fqdn_usage(ServiceApplication|Application $own_resource)
|
||||
{
|
||||
$domains = collect($own_resource->fqdns)->map(function ($domain) {
|
||||
if (str($domain)->endsWith('/')) {
|
||||
$domain = str($domain)->beforeLast('/');
|
||||
}
|
||||
return str($domain)->replace('http://', '')->replace('https://', '');
|
||||
});
|
||||
$apps = Application::all();
|
||||
foreach ($apps as $app) {
|
||||
$list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== '');
|
||||
foreach ($list_of_domains as $domain) {
|
||||
if (str($domain)->endsWith('/')) {
|
||||
$domain = str($domain)->beforeLast('/');
|
||||
}
|
||||
$naked_domain = str($domain)->replace('http://', '')->replace('https://', '')->value();
|
||||
if ($domains->contains($naked_domain)) {
|
||||
if ($app->uuid !== $own_resource->uuid) {
|
||||
throw new \RuntimeException("Domain $naked_domain is already in use by another resource:<br> {$app->name}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$apps = ServiceApplication::all();
|
||||
foreach ($apps as $app) {
|
||||
$list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== '');
|
||||
foreach ($list_of_domains as $domain) {
|
||||
if (str($domain)->endsWith('/')) {
|
||||
$domain = str($domain)->beforeLast('/');
|
||||
}
|
||||
$naked_domain = str($domain)->replace('http://', '')->replace('https://', '')->value();
|
||||
if ($domains->contains($naked_domain)) {
|
||||
if ($app->uuid !== $own_resource->uuid) {
|
||||
throw new \RuntimeException("Domain $naked_domain is already in use by another resource.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1111
composer.lock
generated
1111
composer.lock
generated
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user