Compare commits

...

45 Commits

Author SHA1 Message Date
Andras Bacsai
21e77bf0c1 Merge pull request #1691 from coollabsio/next
v4.0.0-beta.203
2024-01-31 10:02:04 +01:00
Andras Bacsai
0686e48e89 fix: service deletion 2024-01-31 09:58:41 +01:00
Andras Bacsai
1cef233db2 fix: regenerate labels on application clone 2024-01-31 09:03:54 +01:00
Andras Bacsai
907e52572c fix: validate server navbar upated 2024-01-31 08:56:49 +01:00
Andras Bacsai
795c8abf64 Refactor service and resource deletion logic 2024-01-30 17:38:07 +01:00
Andras Bacsai
cc641d8cba revert 2024-01-30 17:16:52 +01:00
Andras Bacsai
d4668ef44a refactor 2024-01-30 14:12:40 +01:00
Andras Bacsai
e8b539c3bd Refactor CleanupUnreachableServers command to update server IP address 2024-01-30 10:53:12 +01:00
Andras Bacsai
6555f0b50c Fix unreachable server cleanup and enable daily schedule 2024-01-30 10:52:57 +01:00
Andras Bacsai
bb05058dda feat: cleanup unreachable servers 2024-01-30 10:50:54 +01:00
Andras Bacsai
9667cd4a7a fix: handle duplicate error instead of sql error
fix: set fqdns to null if you delete an app or a serviceapp
fix: make stucked resources a separate command
2024-01-30 09:48:51 +01:00
Andras Bacsai
3ae9501814 fix: dns validation + duplicated fqdns 2024-01-30 09:22:34 +01:00
Andras Bacsai
73d0948734 fix: service deletion fix 2024-01-30 08:26:33 +01:00
Andras Bacsai
c3e2a741ea Update version numbers 2024-01-29 20:35:35 +01:00
Andras Bacsai
4792146f1d Merge pull request #1687 from coollabsio/next
v4.0.0-beta.202
2024-01-29 16:37:39 +01:00
Andras Bacsai
09b9305aa3 Refactor git_clone_command in generateGitImportCommands function 2024-01-29 16:33:06 +01:00
Andras Bacsai
ff7d0d442d Update button text in private key create view 2024-01-29 16:33:01 +01:00
Andras Bacsai
9a127bdc80 Refactor webhook handling logic to remove duplicate code and improve readability 2024-01-29 16:31:10 +01:00
Andras Bacsai
919e88afb4 Refactor docker run options to compose format 2024-01-29 16:21:23 +01:00
Andras Bacsai
1d1ec20cb7 Update version numbers 2024-01-29 16:13:04 +01:00
Andras Bacsai
5c29ecdf10 feat: add initial support for custom docker run commands 2024-01-29 16:07:00 +01:00
Andras Bacsai
9e09c449cf fix: service deletion function 2024-01-29 16:03:45 +01:00
Andras Bacsai
09bcd693f5 Merge pull request #1683 from coollabsio/next
v4.0.0-beta.201
2024-01-29 14:04:30 +01:00
Andras Bacsai
0c15e45419 Update WordPress database configuration 2024-01-29 14:03:59 +01:00
Andras Bacsai
31cbf552a2 Merge pull request #1677 from mraf/wordpress-template
fix: add env variables for wordpress template without database
2024-01-29 13:58:31 +01:00
Andras Bacsai
f7853ee174 Refactor deployments_per_server variable and update dashboard view
This commit refactors the `deployments_per_server` variable in the `Dashboard` class to remove the type hint and updates the corresponding view file to handle the changes. The `deployments_per_server` variable is now grouped by `server_name` and converted to an array. This improves the organization and readability of the code.
2024-01-29 13:26:50 +01:00
Andras Bacsai
de3a7b6eca Add previous page functionality to deployment index
This commit adds the functionality to navigate to the previous page in the deployment index. It includes changes to the `Index.php` and `index.blade.php` files.
2024-01-29 13:06:26 +01:00
Andras Bacsai
b56c7c34cb fix: unhealthy deployments should be failed 2024-01-29 12:51:20 +01:00
Andras Bacsai
49845f3da7 fix: webhooks for multiple apps 2024-01-29 11:23:04 +01:00
Andras Bacsai
987409bae4 fix: bitbucket manual deployments 2024-01-29 10:43:18 +01:00
Andras Bacsai
07d8461f96 Fix URL encoding in deployment and status notifications 2024-01-29 08:49:05 +01:00
Andras Bacsai
f255a71434 Merge pull request #1673 from Niki2k1/feat/bitbucket-manual-webhook
feat: added manual webhook support for bitbucket
2024-01-29 08:38:05 +01:00
Andras Bacsai
2a2818ac0d Merge pull request #1680 from iamEvanYT/discord-deployment-url-fix
fix: encode project name in discord webhook notifications
2024-01-29 08:32:57 +01:00
Andras Bacsai
fd3cdc2c7d Update deployment status border color 2024-01-29 08:32:04 +01:00
Andras Bacsai
84c3f832ae Add missing closing div tag in dashboard.blade.php 2024-01-29 08:30:00 +01:00
Andras Bacsai
70c28fceeb fix: change env variable length 2024-01-29 08:24:21 +01:00
iamEvan
f2c4f83f5a Fix 2024-01-27 20:33:22 +00:00
iamEvan
c8dd6f07ac Encode Project Name 2024-01-27 20:22:27 +00:00
Andras Bacsai
561e424a7d feat: dashboard live deployment view 2024-01-27 18:44:40 +01:00
Andras Bacsai
c46d38907e fix: queue 2024-01-27 17:18:13 +01:00
Andras Bacsai
5c334bbac6 feat: add PR comments 2024-01-26 18:46:50 +01:00
Andras Bacsai
9628072b0c Update dependencies in package.json and mix-manifest.json 2024-01-26 11:36:44 +01:00
Andras Bacsai
39ecff9f90 Update version numbers 2024-01-26 11:34:18 +01:00
mraf
053aa25d2c fix: add env variables for wordpress template without database 2024-01-25 12:27:41 +01:00
Niklas Lausch
54d8cb9027 feat: added manual webhook support for bitbucket 2024-01-24 10:56:24 +01:00
68 changed files with 1595 additions and 2131 deletions

View File

@@ -11,6 +11,9 @@ class StopApplication
public function handle(Application $application)
{
$server = $application->destination->server;
if (!$server->isFunctional()) {
return 'Server is not functional';
}
if ($server->isSwarm()) {
instant_remote_process(["docker stack rm {$application->uuid}" ], $server);
} else {

View File

@@ -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

View File

@@ -10,35 +10,41 @@ 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();
}
foreach ($service->databases()->get() as $database) {
$database->forceDelete();
}
$database->forceDelete();
}
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();
}
}

View File

@@ -10,6 +10,10 @@ class StopService
use AsAction;
public function handle(Service $service)
{
$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) {

View File

@@ -0,0 +1,295 @@
<?php
namespace App\Console\Commands;
use App\Models\Application;
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()
{
echo "Running cleanup stucked...\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";
}
// 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";
}
}
}

View 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'
]);
}
}
}
}

View File

@@ -4,19 +4,11 @@ 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;
@@ -31,7 +23,7 @@ class Init extends Command
$cleanup = $this->option('cleanup');
if ($cleanup) {
echo "Running cleanups...\n";
$this->cleanup_stucked_resources();
$this->call('cleanup:stucked-resources');
// Required for falsely deleted coolify db
$this->restore_coolify_db_backup();
@@ -137,273 +129,5 @@ 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";
}
}
}

View File

@@ -36,6 +36,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();

View File

@@ -10,4 +10,5 @@ enum ProcessStatus: string
case ERROR = 'error';
case KILLED = 'killed';
case CANCELLED = 'cancelled';
case CLOSED = 'closed';
}

View File

@@ -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;
@@ -158,6 +158,9 @@ 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);
}
}
}
@@ -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,11 +245,7 @@ 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));
@@ -254,8 +255,17 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$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 {
@@ -338,9 +348,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
],
);
}
$this->application_deployment_queue->addLogEntry("Image pushed to docker registry.'");
$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.'");
$this->application_deployment_queue->addLogEntry("Failed to push image to docker registry. Please check debug logs for more information.");
if ($forceFail) {
throw $e;
}
@@ -478,10 +488,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
} else {
$this->application_deployment_queue->addLogEntry("Starting pull request (#{$this->pull_request_id}) deployment of {$this->customRepository}:{$this->application->git_branch}.");
}
$this->server->executeRemoteCommand(
commands: $this->application->prepareHelperImage($this->deployment_uuid),
loggingModel: $this->application_deployment_queue
);
ray('asddf');
$this->prepare_builder_image();
$this->check_git_if_build_needed();
$this->clone_repository();
$this->generate_image_names();
@@ -679,7 +687,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 +730,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();
@@ -861,7 +867,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;
}
@@ -1115,6 +1126,11 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
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);
$this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d > {$this->workdir}/docker-compose.yml"), "hidden" => true]);
@@ -1489,13 +1505,13 @@ 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));
}
@@ -1507,7 +1523,9 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
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');

View File

@@ -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);
}
}

View File

@@ -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');
}
}

View File

@@ -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;
@@ -30,41 +31,22 @@ 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) {
send_internal_notification('ContainerStoppingJob failed with: ' . $e->getMessage());

View File

@@ -2,18 +2,35 @@
namespace App\Livewire;
use App\Models\ApplicationDeploymentQueue;
use App\Models\Project;
use App\Models\Server;
use Illuminate\Support\Collection;
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 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()
// {

View File

@@ -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();

View File

@@ -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',
];
@@ -243,12 +245,16 @@ class General extends Component
$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.");
$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) {

View File

@@ -54,8 +54,7 @@ class Heading extends Component
}
$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: false,
is_new_deployment: true,
@@ -83,8 +82,7 @@ class Heading extends Component
}
$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,
);
@@ -113,8 +111,7 @@ class Heading extends Component
{
$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,
is_new_deployment: true,
@@ -130,8 +127,7 @@ class Heading extends Component
{
$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,
);

View File

@@ -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,

View File

@@ -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,

View File

@@ -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);

View File

@@ -24,7 +24,8 @@ class Danger extends Component
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

View File

@@ -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([

View File

@@ -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()
{

View File

@@ -11,6 +11,7 @@ class Show extends Component
use AuthorizesRequests;
public ?Server $server = null;
public $parameters = [];
protected $listeners = ['proxyStatusUpdated' => '$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()
{

View File

@@ -6,11 +6,9 @@ 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
@@ -39,6 +37,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');
@@ -52,65 +51,14 @@ class Application extends BaseModel
$application->environment_variables_preview()->delete();
});
}
// Build packs / deployment types
public function servers(): Collection
public function is_github_based(): bool
{
$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);
}
}
if (data_get($this, 'source')) {
return true;
}
return collect([$mainServer])->merge($additionalServers);
return false;
}
public function generateImageNames(string $commit, int $pullRequestId)
{
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,
];
}
// End of build packs / deployment types
public function link()
{
if (data_get($this, 'environment.project.uuid')) {
@@ -458,31 +406,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,296 +427,7 @@ 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)
function setGitImportSettings(string $deployment_uuid, string $git_clone_command)
{
$baseDir = $this->generateBaseDir($deployment_uuid);
if ($this->git_commit_sha !== 'HEAD') {
@@ -807,7 +441,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 +454,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 +532,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 +540,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 +565,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 +605,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 +715,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),
);
}
}

View File

@@ -2,16 +2,12 @@
namespace App\Models;
use App\Enums\ApplicationDeploymentStatus;
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 Spatie\SchemalessAttributes\Casts\SchemalessAttributes;
use Spatie\SchemalessAttributes\SchemalessAttributesTrait;
use Illuminate\Support\Str;
@@ -335,20 +331,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()
@@ -483,153 +465,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
);
}
}

View File

@@ -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
{

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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());
}
}
});
}
}

View File

@@ -1,20 +1,25 @@
<?php
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 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)
{
$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;
$deployment = ApplicationDeploymentQueue::create([
'application_id' => $application_id,
'application_name' => $application->name,
'server_id' => $server_id,
'server_name' => $server_name,
'deployment_uuid' => $deployment_uuid,
'deployment_url' => $deployment_url,
'pull_request_id' => $pull_request_id,
'force_rebuild' => $force_rebuild,
'is_webhook' => $is_webhook,
@@ -22,317 +27,42 @@ 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)
));
} else {
if (next_queuable($server_id, $application_id)) {
dispatch(new ApplicationDeploymentJob(
application_deployment_queue_id: $deployment->id,
));
}
}
function queue_next_deployment(Application $application, bool $isNew = false)
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,
));
}
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)
function next_queuable(string $server_id, string $application_id): bool
{
return "/artifacts/$deplyomentUuid";
}
function generateWorkdir(string $deplyomentUuid, Application $application)
{
return generateBaseDir($deplyomentUuid) . rtrim($application->base_directory, '/');
}
$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;
function prepareHelperContainer(Server $server, string $network, string $deploymentUuid)
{
$basedir = generateBaseDir($deploymentUuid);
$helperImage = config('coolify.helper_image');
ray("serverId:{$server->id}", "concurrentBuilds:{$concurrent_builds}", "deployments:{$deployments->count()}", "sameApplicationDeployments:{$same_application_deployments->count()}");
$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,
]);
if ($deployments->count() > $concurrent_builds) {
return false;
}
$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)
{
$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);
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;
}
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,
];
}
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;
}

View File

@@ -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;
@@ -323,3 +321,89 @@ 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',
'--device' => 'devices',
'--ulimit' => 'ulimits',
'--init' => 'init',
'--ulimit' => 'ulimits',
'--privileged' => 'privileged',
]);
foreach ($matches as $match) {
$option = $match[1];
$value = isset($match[2]) && $match[2] !== '' ? $match[2] : true;
if ($list_options->contains($option)) {
$value = explode(',', $value);
}
if (array_key_exists($option, $options)) {
if (is_array($options[$option])) {
$options[$option][] = $value;
} else {
$options[$option] = [$options[$option], $value];
}
} else {
$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();
}

View File

@@ -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 [

View File

@@ -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;
@@ -107,6 +108,12 @@ function handleError(?Throwable $error = null, ?Livewire\Component $livewire = n
}
return "Too many requests. Please try again in {$error->secondsUntilAvailable} seconds.";
}
if ($error instanceof UniqueConstraintViolationException) {
if (isset($livewire)) {
return $livewire->dispatch('error', "A resource with the same name already exists.");
}
return "A resource with the same name already exists.";
}
if ($error instanceof Throwable) {
$message = $error->getMessage();
@@ -1660,3 +1667,33 @@ 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) {
return Url::fromString($domain)->getHost();
});
$apps = Application::all();
foreach ($apps as $app) {
$list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== '');
foreach ($list_of_domains as $domain) {
$naked_domain = Url::fromString($domain)->getHost();
if ($domains->contains($naked_domain)) {
if ($app->uuid !== $own_resource->uuid ) {
throw new \RuntimeException("Domain $naked_domain is already in use by another resource.");
}
}
}
}
$apps = ServiceApplication::all();
foreach ($apps as $app) {
$list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== '');
foreach ($list_of_domains as $domain) {
$naked_domain = Url::fromString($domain)->getHost();
if ($domains->contains($naked_domain)) {
if ($app->uuid !== $own_resource->uuid) {
throw new \RuntimeException("Domain $naked_domain is already in use by another resource.");
}
}
}
}
}

494
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ return [
// The release version of your application
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
'release' => '4.0.0-beta.200',
'release' => '4.0.0-beta.203',
// When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'),

View File

@@ -1,3 +1,3 @@
<?php
return '4.0.0-beta.200';
return '4.0.0-beta.203';

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
version: '3.8'
services:
coolify:
image: "ghcr.io/coollabsio/coolify:${LATEST_IMAGE:-4.0.0-beta.190}"
image: "ghcr.io/coollabsio/coolify:${LATEST_IMAGE:latest}"
volumes:
- type: bind
source: /data/coolify/source/.env

222
package-lock.json generated
View File

@@ -6,22 +6,22 @@
"": {
"dependencies": {
"@tailwindcss/typography": "0.5.10",
"alpinejs": "3.13.3",
"alpinejs": "3.13.5",
"daisyui": "4.4.19",
"ioredis": "5.3.2",
"tailwindcss-scrollbar": "0.1.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "4.5.1",
"autoprefixer": "10.4.16",
"axios": "1.6.5",
"autoprefixer": "10.4.17",
"axios": "1.6.7",
"laravel-echo": "1.15.3",
"laravel-vite-plugin": "0.8.1",
"postcss": "8.4.33",
"pusher-js": "8.4.0-rc2",
"tailwindcss": "3.4.1",
"vite": "4.5.2",
"vue": "3.4.13"
"vue": "3.4.15"
}
},
"node_modules/@alloc/quick-lru": {
@@ -36,9 +36,9 @@
}
},
"node_modules/@babel/parser": {
"version": "7.23.6",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz",
"integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==",
"version": "7.23.9",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz",
"integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==",
"dev": true,
"bin": {
"parser": "bin/babel-parser.js"
@@ -524,77 +524,77 @@
}
},
"node_modules/@vue/compiler-core": {
"version": "3.4.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.13.tgz",
"integrity": "sha512-zGUdmB3j3Irn9z51GXLJ5s0EAHxmsm5/eXl0y6MBaajMeOAaiT4+zaDoxui4Ets98dwIRr8BBaqXXHtHSfm+KA==",
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.15.tgz",
"integrity": "sha512-XcJQVOaxTKCnth1vCxEChteGuwG6wqnUHxAm1DO3gCz0+uXKaJNx8/digSz4dLALCy8n2lKq24jSUs8segoqIw==",
"dev": true,
"dependencies": {
"@babel/parser": "^7.23.6",
"@vue/shared": "3.4.13",
"@vue/shared": "3.4.15",
"entities": "^4.5.0",
"estree-walker": "^2.0.2",
"source-map-js": "^1.0.2"
}
},
"node_modules/@vue/compiler-core/node_modules/@vue/shared": {
"version": "3.4.13",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.13.tgz",
"integrity": "sha512-56crFKLPpzk85WXX1L1c0QzPOuoapWlPVys8eMG8kkRmqdMjWUqK8KpFdE2d7BQA4CEbXwyyHPq6MpFr8H9rcg==",
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.15.tgz",
"integrity": "sha512-KzfPTxVaWfB+eGcGdbSf4CWdaXcGDqckoeXUh7SB3fZdEtzPCK2Vq9B/lRRL3yutax/LWITz+SwvgyOxz5V75g==",
"dev": true
},
"node_modules/@vue/compiler-dom": {
"version": "3.4.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.13.tgz",
"integrity": "sha512-XSNbpr5Rs3kCfVAmBqMu/HDwOS+RL6y28ZZjDlnDUuf146pRWt2sQkwhsOYc9uu2lxjjJy2NcyOkK7MBLVEc7w==",
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.15.tgz",
"integrity": "sha512-wox0aasVV74zoXyblarOM3AZQz/Z+OunYcIHe1OsGclCHt8RsRm04DObjefaI82u6XDzv+qGWZ24tIsRAIi5MQ==",
"dev": true,
"dependencies": {
"@vue/compiler-core": "3.4.13",
"@vue/shared": "3.4.13"
"@vue/compiler-core": "3.4.15",
"@vue/shared": "3.4.15"
}
},
"node_modules/@vue/compiler-dom/node_modules/@vue/shared": {
"version": "3.4.13",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.13.tgz",
"integrity": "sha512-56crFKLPpzk85WXX1L1c0QzPOuoapWlPVys8eMG8kkRmqdMjWUqK8KpFdE2d7BQA4CEbXwyyHPq6MpFr8H9rcg==",
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.15.tgz",
"integrity": "sha512-KzfPTxVaWfB+eGcGdbSf4CWdaXcGDqckoeXUh7SB3fZdEtzPCK2Vq9B/lRRL3yutax/LWITz+SwvgyOxz5V75g==",
"dev": true
},
"node_modules/@vue/compiler-sfc": {
"version": "3.4.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.13.tgz",
"integrity": "sha512-SkpmQN8xIFBd5onT413DFSDdjxULJf6jmJg/t3w/DZ9I8ZzyNlLIBLO0qFLewVHyHCiAgpPZlWqSRZXYrawk3Q==",
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.15.tgz",
"integrity": "sha512-LCn5M6QpkpFsh3GQvs2mJUOAlBQcCco8D60Bcqmf3O3w5a+KWS5GvYbrrJBkgvL1BDnTp+e8q0lXCLgHhKguBA==",
"dev": true,
"dependencies": {
"@babel/parser": "^7.23.6",
"@vue/compiler-core": "3.4.13",
"@vue/compiler-dom": "3.4.13",
"@vue/compiler-ssr": "3.4.13",
"@vue/shared": "3.4.13",
"@vue/compiler-core": "3.4.15",
"@vue/compiler-dom": "3.4.15",
"@vue/compiler-ssr": "3.4.15",
"@vue/shared": "3.4.15",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.5",
"postcss": "^8.4.32",
"postcss": "^8.4.33",
"source-map-js": "^1.0.2"
}
},
"node_modules/@vue/compiler-sfc/node_modules/@vue/shared": {
"version": "3.4.13",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.13.tgz",
"integrity": "sha512-56crFKLPpzk85WXX1L1c0QzPOuoapWlPVys8eMG8kkRmqdMjWUqK8KpFdE2d7BQA4CEbXwyyHPq6MpFr8H9rcg==",
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.15.tgz",
"integrity": "sha512-KzfPTxVaWfB+eGcGdbSf4CWdaXcGDqckoeXUh7SB3fZdEtzPCK2Vq9B/lRRL3yutax/LWITz+SwvgyOxz5V75g==",
"dev": true
},
"node_modules/@vue/compiler-ssr": {
"version": "3.4.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.13.tgz",
"integrity": "sha512-rwnw9SVBgD6eGKh8UucnwztieQo/R3RQrEGpE0b0cxb2xxvJeLs/fe7DoYlhEfaSyzM/qD5odkK87hl3G3oW+A==",
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.15.tgz",
"integrity": "sha512-1jdeQyiGznr8gjFDadVmOJqZiLNSsMa5ZgqavkPZ8O2wjHv0tVuAEsw5hTdUoUW4232vpBbL/wJhzVW/JwY1Uw==",
"dev": true,
"dependencies": {
"@vue/compiler-dom": "3.4.13",
"@vue/shared": "3.4.13"
"@vue/compiler-dom": "3.4.15",
"@vue/shared": "3.4.15"
}
},
"node_modules/@vue/compiler-ssr/node_modules/@vue/shared": {
"version": "3.4.13",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.13.tgz",
"integrity": "sha512-56crFKLPpzk85WXX1L1c0QzPOuoapWlPVys8eMG8kkRmqdMjWUqK8KpFdE2d7BQA4CEbXwyyHPq6MpFr8H9rcg==",
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.15.tgz",
"integrity": "sha512-KzfPTxVaWfB+eGcGdbSf4CWdaXcGDqckoeXUh7SB3fZdEtzPCK2Vq9B/lRRL3yutax/LWITz+SwvgyOxz5V75g==",
"dev": true
},
"node_modules/@vue/reactivity": {
@@ -606,64 +606,64 @@
}
},
"node_modules/@vue/runtime-core": {
"version": "3.4.13",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.13.tgz",
"integrity": "sha512-Ov4d4At7z3goxqzSqQxdfVYEcN5HY4dM1uDYL6Hu/Es9Za9BEN602zyjWhhi2+BEki5F9NizRSvn02k/tqNWlg==",
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.15.tgz",
"integrity": "sha512-6E3by5m6v1AkW0McCeAyhHTw+3y17YCOKG0U0HDKDscV4Hs0kgNT5G+GCHak16jKgcCDHpI9xe5NKb8sdLCLdw==",
"dev": true,
"dependencies": {
"@vue/reactivity": "3.4.13",
"@vue/shared": "3.4.13"
"@vue/reactivity": "3.4.15",
"@vue/shared": "3.4.15"
}
},
"node_modules/@vue/runtime-core/node_modules/@vue/reactivity": {
"version": "3.4.13",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.13.tgz",
"integrity": "sha512-/ZdUOrGKkGVONzVJkfDqNcn2fLMvaa5VlYx2KwTbnRbX06YZ4GJE0PVTmWzIxtBYdpSTLLXgw3pDggO+96KXzg==",
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.15.tgz",
"integrity": "sha512-55yJh2bsff20K5O84MxSvXKPHHt17I2EomHznvFiJCAZpJTNW8IuLj1xZWMLELRhBK3kkFV/1ErZGHJfah7i7w==",
"dev": true,
"dependencies": {
"@vue/shared": "3.4.13"
"@vue/shared": "3.4.15"
}
},
"node_modules/@vue/runtime-core/node_modules/@vue/shared": {
"version": "3.4.13",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.13.tgz",
"integrity": "sha512-56crFKLPpzk85WXX1L1c0QzPOuoapWlPVys8eMG8kkRmqdMjWUqK8KpFdE2d7BQA4CEbXwyyHPq6MpFr8H9rcg==",
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.15.tgz",
"integrity": "sha512-KzfPTxVaWfB+eGcGdbSf4CWdaXcGDqckoeXUh7SB3fZdEtzPCK2Vq9B/lRRL3yutax/LWITz+SwvgyOxz5V75g==",
"dev": true
},
"node_modules/@vue/runtime-dom": {
"version": "3.4.13",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.13.tgz",
"integrity": "sha512-ynde9p16eEV3u1VCxUre2e0nKzD0l3NzH0r599+bXeLT1Yhac8Atcot3iL9XNqwolxYCI89KBII+2MSVzfrz6w==",
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.15.tgz",
"integrity": "sha512-EVW8D6vfFVq3V/yDKNPBFkZKGMFSvZrUQmx196o/v2tHKdwWdiZjYUBS+0Ez3+ohRyF8Njwy/6FH5gYJ75liUw==",
"dev": true,
"dependencies": {
"@vue/runtime-core": "3.4.13",
"@vue/shared": "3.4.13",
"@vue/runtime-core": "3.4.15",
"@vue/shared": "3.4.15",
"csstype": "^3.1.3"
}
},
"node_modules/@vue/runtime-dom/node_modules/@vue/shared": {
"version": "3.4.13",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.13.tgz",
"integrity": "sha512-56crFKLPpzk85WXX1L1c0QzPOuoapWlPVys8eMG8kkRmqdMjWUqK8KpFdE2d7BQA4CEbXwyyHPq6MpFr8H9rcg==",
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.15.tgz",
"integrity": "sha512-KzfPTxVaWfB+eGcGdbSf4CWdaXcGDqckoeXUh7SB3fZdEtzPCK2Vq9B/lRRL3yutax/LWITz+SwvgyOxz5V75g==",
"dev": true
},
"node_modules/@vue/server-renderer": {
"version": "3.4.13",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.13.tgz",
"integrity": "sha512-hkw+UQyDZZtSn1q30nObMfc8beVEQv2pG08nghigxGw+iOWodR+tWSuJak0mzWAHlP/xt/qLc//dG6igfgvGEA==",
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.15.tgz",
"integrity": "sha512-3HYzaidu9cHjrT+qGUuDhFYvF/j643bHC6uUN9BgM11DVy+pM6ATsG6uPBLnkwOgs7BpJABReLmpL3ZPAsUaqw==",
"dev": true,
"dependencies": {
"@vue/compiler-ssr": "3.4.13",
"@vue/shared": "3.4.13"
"@vue/compiler-ssr": "3.4.15",
"@vue/shared": "3.4.15"
},
"peerDependencies": {
"vue": "3.4.13"
"vue": "3.4.15"
}
},
"node_modules/@vue/server-renderer/node_modules/@vue/shared": {
"version": "3.4.13",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.13.tgz",
"integrity": "sha512-56crFKLPpzk85WXX1L1c0QzPOuoapWlPVys8eMG8kkRmqdMjWUqK8KpFdE2d7BQA4CEbXwyyHPq6MpFr8H9rcg==",
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.15.tgz",
"integrity": "sha512-KzfPTxVaWfB+eGcGdbSf4CWdaXcGDqckoeXUh7SB3fZdEtzPCK2Vq9B/lRRL3yutax/LWITz+SwvgyOxz5V75g==",
"dev": true
},
"node_modules/@vue/shared": {
@@ -672,9 +672,9 @@
"integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA=="
},
"node_modules/alpinejs": {
"version": "3.13.3",
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.13.3.tgz",
"integrity": "sha512-WZ6WQjkAOl+WdW/jukzNHq9zHFDNKmkk/x6WF7WdyNDD6woinrfXCVsZXm0galjbco+pEpYmJLtwlZwcOfIVdg==",
"version": "3.13.5",
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.13.5.tgz",
"integrity": "sha512-1d2XeNGN+Zn7j4mUAKXtAgdc4/rLeadyTMWeJGXF5DzwawPBxwTiBhFFm6w/Ei8eJxUZeyNWWSD9zknfdz1kEw==",
"dependencies": {
"@vue/reactivity": "~3.1.1"
}
@@ -708,9 +708,9 @@
"dev": true
},
"node_modules/autoprefixer": {
"version": "10.4.16",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.16.tgz",
"integrity": "sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==",
"version": "10.4.17",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.17.tgz",
"integrity": "sha512-/cpVNRLSfhOtcGflT13P2794gVSgmPgTR+erw5ifnMLZb0UnSlkK4tquLmkd3BhA+nLo5tX8Cu0upUsGKvKbmg==",
"dev": true,
"funding": [
{
@@ -727,9 +727,9 @@
}
],
"dependencies": {
"browserslist": "^4.21.10",
"caniuse-lite": "^1.0.30001538",
"fraction.js": "^4.3.6",
"browserslist": "^4.22.2",
"caniuse-lite": "^1.0.30001578",
"fraction.js": "^4.3.7",
"normalize-range": "^0.1.2",
"picocolors": "^1.0.0",
"postcss-value-parser": "^4.2.0"
@@ -745,9 +745,9 @@
}
},
"node_modules/axios": {
"version": "1.6.5",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz",
"integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==",
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz",
"integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==",
"dev": true,
"dependencies": {
"follow-redirects": "^1.15.4",
@@ -789,9 +789,9 @@
}
},
"node_modules/browserslist": {
"version": "4.21.11",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.11.tgz",
"integrity": "sha512-xn1UXOKUz7DjdGlg9RrUr0GGiWzI97UQJnugHtH0OLDfJB7jMgoIkYvRIEO1l9EeEERVqeqLYOcFBW9ldjypbQ==",
"version": "4.22.2",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz",
"integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==",
"dev": true,
"funding": [
{
@@ -808,9 +808,9 @@
}
],
"dependencies": {
"caniuse-lite": "^1.0.30001538",
"electron-to-chromium": "^1.4.526",
"node-releases": "^2.0.13",
"caniuse-lite": "^1.0.30001565",
"electron-to-chromium": "^1.4.601",
"node-releases": "^2.0.14",
"update-browserslist-db": "^1.0.13"
},
"bin": {
@@ -829,9 +829,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001539",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001539.tgz",
"integrity": "sha512-hfS5tE8bnNiNvEOEkm8HElUHroYwlqMMENEzELymy77+tJ6m+gA2krtHl5hxJaj71OlpC2cHZbdSMX1/YEqEkA==",
"version": "1.0.30001580",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001580.tgz",
"integrity": "sha512-mtj5ur2FFPZcCEpXFy8ADXbDACuNFXg6mxVDqp7tqooX6l3zwm+d8EPoeOSIFRDvHs8qu7/SLFOGniULkcH2iA==",
"dev": true,
"funding": [
{
@@ -1014,9 +1014,9 @@
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="
},
"node_modules/electron-to-chromium": {
"version": "1.4.528",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.528.tgz",
"integrity": "sha512-UdREXMXzLkREF4jA8t89FQjA8WHI6ssP38PMY4/4KhXFQbtImnghh4GkCgrtiZwLKUKVD2iTVXvDVQjfomEQuA==",
"version": "1.4.647",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.647.tgz",
"integrity": "sha512-Z/fTNGwc45WrYQhPaEcz5tAJuZZ8G7S/DBnhS6Kgp4BxnS40Z/HqlJ0hHg3Z79IGVzuVartIlTcjw/cQbPLgOw==",
"dev": true
},
"node_modules/entities": {
@@ -1168,9 +1168,9 @@
}
},
"node_modules/fraction.js": {
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.6.tgz",
"integrity": "sha512-n2aZ9tNfYDwaHhvFTkhFErqOMIb8uyzSQ+vGJBjZyanAKZVbGUQ1sngfk9FdkBw7G26O7AgNjLcecLffD1c7eg==",
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
"integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==",
"dev": true,
"engines": {
"node": "*"
@@ -1506,9 +1506,9 @@
}
},
"node_modules/node-releases": {
"version": "2.0.13",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz",
"integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==",
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
"integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==",
"dev": true
},
"node_modules/normalize-path": {
@@ -2106,16 +2106,16 @@
}
},
"node_modules/vue": {
"version": "3.4.13",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.4.13.tgz",
"integrity": "sha512-FE3UZ0p+oUZTwz+SzlH/hDFg+XsVRFvwmx0LXjdD1pRK/cO4fu5v6ltAZji4za4IBih3dV78elUK3di8v3pWIg==",
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.4.15.tgz",
"integrity": "sha512-jC0GH4KkWLWJOEQjOpkqU1bQsBwf4R1rsFtw5GQJbjHVKWDzO6P0nWWBTmjp1xSemAioDFj1jdaK1qa3DnMQoQ==",
"dev": true,
"dependencies": {
"@vue/compiler-dom": "3.4.13",
"@vue/compiler-sfc": "3.4.13",
"@vue/runtime-dom": "3.4.13",
"@vue/server-renderer": "3.4.13",
"@vue/shared": "3.4.13"
"@vue/compiler-dom": "3.4.15",
"@vue/compiler-sfc": "3.4.15",
"@vue/runtime-dom": "3.4.15",
"@vue/server-renderer": "3.4.15",
"@vue/shared": "3.4.15"
},
"peerDependencies": {
"typescript": "*"
@@ -2127,9 +2127,9 @@
}
},
"node_modules/vue/node_modules/@vue/shared": {
"version": "3.4.13",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.13.tgz",
"integrity": "sha512-56crFKLPpzk85WXX1L1c0QzPOuoapWlPVys8eMG8kkRmqdMjWUqK8KpFdE2d7BQA4CEbXwyyHPq6MpFr8H9rcg==",
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.15.tgz",
"integrity": "sha512-KzfPTxVaWfB+eGcGdbSf4CWdaXcGDqckoeXUh7SB3fZdEtzPCK2Vq9B/lRRL3yutax/LWITz+SwvgyOxz5V75g==",
"dev": true
},
"node_modules/wrappy": {

View File

@@ -7,19 +7,19 @@
},
"devDependencies": {
"@vitejs/plugin-vue": "4.5.1",
"autoprefixer": "10.4.16",
"axios": "1.6.5",
"autoprefixer": "10.4.17",
"axios": "1.6.7",
"laravel-echo": "1.15.3",
"laravel-vite-plugin": "0.8.1",
"postcss": "8.4.33",
"pusher-js": "8.4.0-rc2",
"tailwindcss": "3.4.1",
"vite": "4.5.2",
"vue": "3.4.13"
"vue": "3.4.15"
},
"dependencies": {
"@tailwindcss/typography": "0.5.10",
"alpinejs": "3.13.3",
"alpinejs": "3.13.5",
"daisyui": "4.4.19",
"ioredis": "5.3.2",
"tailwindcss-scrollbar": "0.1.0"

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,5 @@
{
"/app.js": "/app.js?id=79bae40dcb18de9ca1b5d0008c577471",
"/app.js": "/app.js?id=b4f3f08e60211bd6948ec35e5e9de9a1",
"/app-dark.css": "/app-dark.css?id=15c72df05e2b1147fa3e4b0670cfb435",
"/app.css": "/app.css?id=4d6a1a7fe095eedc2cb2a4ce822ea8a5",
"/img/favicon.png": "/img/favicon.png?id=1542bfe8a0010dcbee710da13cce367f",

View File

@@ -19,7 +19,7 @@ button[isError] {
}
.main {
@apply pt-4 pl-24 pr-10 mx-auto;
@apply pt-4 pl-24 pr-10 lg:pr-32 lg:pl-44;
}
.custom-modal {

View File

@@ -369,14 +369,14 @@ window.customToastHTML = `
}, 5);
}, 4000);"
@mouseover="toastHovered=true" @mouseout="toastHovered=false"
class="absolute w-full duration-300 ease-out select-none sm:max-w-xs"
class="absolute w-full duration-200 ease-out select-none sm:max-w-xs"
:class="{ 'toast-no-description': !toast.description }">
<span
class="relative flex flex-col items-start shadow-[0_5px_15px_-3px_rgb(0_0_0_/_0.08)] w-full transition-all duration-300 ease-out bg-coolgray-200 border border-coolgray-100 sm:rounded-md sm:max-w-xs group"
class="relative flex flex-col items-start shadow-[0_5px_15px_-3px_rgb(0_0_0_/_0.08)] w-full transition-all duration-200 ease-out bg-coolgray-200 border border-coolgray-100 sm:rounded-md sm:max-w-xs group"
:class="{ 'p-4': !toast.html, 'p-0': toast.html }">
<template x-if="!toast.html">
<div class="relative">
<div class="flex items-center"
<div class="flex items-start"
:class="{ 'text-green-500': toast.type=='success', 'text-blue-500': toast.type=='info', 'text-orange-400': toast.type=='warning', 'text-red-500': toast.type=='danger', 'text-gray-800': toast.type=='default' }">
<svg x-show="toast.type=='success'" class="w-[18px] h-[18px] mr-1.5 -ml-1"
@@ -403,12 +403,12 @@ window.customToastHTML = `
d="M2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12ZM11.9996 7C12.5519 7 12.9996 7.44772 12.9996 8V12C12.9996 12.5523 12.5519 13 11.9996 13C11.4474 13 10.9996 12.5523 10.9996 12V8C10.9996 7.44772 11.4474 7 11.9996 7ZM12.001 14.99C11.4488 14.9892 11.0004 15.4363 10.9997 15.9886L10.9996 15.9986C10.9989 16.5509 11.446 16.9992 11.9982 17C12.5505 17.0008 12.9989 16.5537 12.9996 16.0014L12.9996 15.9914C13.0004 15.4391 12.5533 14.9908 12.001 14.99Z"
fill="currentColor"></path>
</svg>
<p class="font-medium leading-none text-neutral-200"
<p class="font-bold leading-2 text-neutral-200"
x-html="toast.message">
</p>
</div>
<p x-show="toast.description" :class="{ 'pl-5': toast.type!='default' }"
class="mt-1.5 text-xs leading-none opacity-70" x-html="toast.description"></p>
class="mt-1.5 text-xs leading-2 opacity-90" x-html="toast.description"></p>
</div>
</template>
<template x-if="toast.html">

View File

@@ -11,7 +11,7 @@
@auth
<livewire:realtime-connection />
@endauth
<main class="pb-10 main max-w-screen-2xl">
<main class="pb-10 main">
{{ $slot }}
</main>
@endsection

View File

@@ -11,7 +11,8 @@
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>Your subscription has been activated! Welcome onboard! <br>It could take a few seconds before your subscription is activated.<br> Please be patient.</span>
<span>Your subscription has been activated! Welcome onboard! <br>It could take a few seconds before your
subscription is activated.<br> Please be patient.</span>
</div>
@endif
@if ($projects->count() === 0 && $servers->count() === 0)
@@ -29,14 +30,14 @@
@foreach ($projects as $project)
<div class="gap-2 border border-transparent cursor-pointer box group">
@if (data_get($project, 'environments.0.name'))
<a class="flex flex-col flex-1 mx-6 hover:no-underline"
<a class="flex flex-col flex-1 mx-6 hover:no-underline"
href="{{ route('project.resource.index', ['project_uuid' => data_get($project, 'uuid'), 'environment_name' => data_get($project, 'environments.0.name', 'production')]) }}">
<div class="font-bold text-white">{{ $project->name }}</div>
<div class="description">
{{ $project->description }}</div>
</a>
@else
<a class="flex flex-col flex-1 mx-6 hover:no-underline"
<a class="flex flex-col flex-1 mx-6 hover:no-underline"
href="{{ route('project.show', ['project_uuid' => data_get($project, 'uuid')]) }}">
<div class="font-bold text-white">{{ $project->name }}</div>
<div class="description">
@@ -44,11 +45,11 @@
</a>
@endif
<div class="flex items-center">
<a class="mx-4 rounded group-hover:text-white hover:no-underline"
<a class="mx-4 rounded group-hover:text-white hover:no-underline"
href="{{ route('project.resource.create', ['project_uuid' => data_get($project, 'uuid'), 'environment_name' => data_get($project, 'environments.0.name', 'production')]) }}">
<span class="font-bold hover:text-warning">+ New Resource</span>
</a>
<a class="mx-4 rounded group-hover:text-white"
<a class="mx-4 rounded group-hover:text-white"
href="{{ route('project.edit', ['project_uuid' => data_get($project, 'uuid')]) }}">
<svg xmlns="http://www.w3.org/2000/svg" class="icon hover:text-warning" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
@@ -72,12 +73,11 @@
<div class="grid grid-cols-1 gap-2 xl:grid-cols-2">
@endif
@foreach ($servers as $server)
<a href="{{ route('server.show', ['server_uuid' => data_get($server, 'uuid')]) }}"
@class([
'gap-2 border cursor-pointer box group',
'border-transparent' => $server->settings->is_reachable,
'border-red-500' => !$server->settings->is_reachable,
])>
<a href="{{ route('server.show', ['server_uuid' => data_get($server, 'uuid')]) }}" @class([
'gap-2 border cursor-pointer box group',
'border-transparent' => $server->settings->is_reachable,
'border-red-500' => !$server->settings->is_reachable,
])>
<div class="flex flex-col mx-6">
<div class="font-bold text-white">
{{ $server->name }}
@@ -100,6 +100,45 @@
</a>
@endforeach
</div>
<div class="flex items-center gap-2">
<h3 class="py-4">Deployments </h3>
@if (count($deployments_per_server) > 0)
<x-loading />
@endif
</div>
{{-- <div wire:poll.4000ms="get_deployments" class="grid grid-cols-1 gap-2 lg:grid-cols-3"> --}}
<div class="grid grid-cols-1">
@forelse ($deployments_per_server as $server_name => $deployments)
<h4 class="py-4">{{ $server_name }}</h4>
<div class="grid grid-cols-1 gap-2 lg:grid-cols-3">
@foreach ($deployments as $deployment)
<a href="{{ data_get($deployment, 'deployment_url') }}" @class([
'gap-2 cursor-pointer box group border-l-2 border-dotted',
'border-white' => data_get($deployment, 'status') === 'queued',
'border-yellow-500' => data_get($deployment, 'status') === 'in_progress',
])>
<div class="flex flex-col mx-6">
<div class="font-bold text-white">
{{ data_get($deployment, 'application_name') }}
</div>
@if (data_get($deployment, 'pull_request_id') !== 0)
<div class="description">
PR #{{ data_get($deployment, 'pull_request_id') }}
</div>
@endif
<div class="description">
{{ str(data_get($deployment, 'status'))->headline() }}
</div>
</div>
<div class="flex-1"></div>
</a>
@endforeach
</div>
@empty
<div>No queued / in progress deployments</div>
@endforelse
</div>
<script>
function gotoProject(uuid, environment = 'production') {
window.location.href = '/project/' + uuid + '/' + environment;

View File

@@ -39,7 +39,7 @@
<a :class="activeTab === 'webhooks' && 'text-white'"
@click.prevent="activeTab = 'webhooks'; window.location.hash = 'webhooks'" href="#">Webhooks
</a>
@if ($application->git_based() && $application->build_pack !== 'static')
@if ($application->git_based())
<a :class="activeTab === 'previews' && 'text-white'"
@click.prevent="activeTab = 'previews'; window.location.hash = 'previews'" href="#">Preview
Deployments

View File

@@ -7,6 +7,6 @@
@endif
@if (data_get($application_deployment_queue, 'status') === 'in_progress' ||
data_get($application_deployment_queue, 'status') === 'queued')
<x-forms.button wire:click.prevent="cancel">Cancel Deployment</x-forms.button>
<x-forms.button isError wire:click.prevent="cancel">Cancel Deployment</x-forms.button>
@endif
</div>

View File

@@ -2,12 +2,33 @@
<h1>Deployments</h1>
<livewire:project.application.heading :application="$application" />
{{-- <livewire:project.application.deployment.show :application="$application" :deployments="$deployments" :deployments_count="$deployments_count" /> --}}
<div class="flex flex-col gap-2 pb-10" @if ($skip == 0) wire:poll.5000ms='reload_deployments' @endif>
<div class="flex flex-col gap-2 pb-10"
@if ($skip == 0) wire:poll.5000ms='reload_deployments' @endif>
<div class="flex items-end gap-2 pt-4">
<h2>Deployments <span class="text-xs">({{ $deployments_count }})</span></h2>
@if ($show_prev)
<x-forms.button wire:click="previous_page({{ $default_take }})"><svg class="w-6 h-6" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="m14 6l-6 6l6 6z" />
</svg></x-forms.button>
@else
<x-forms.button disabled><svg class="w-6 h-6" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="m14 6l-6 6l6 6z" />
</svg></x-forms.button>
@endif
@if ($show_next)
<x-forms.button wire:click="load_deployments({{ $default_take }})">Next Page
</x-forms.button>
<x-forms.button wire:click="next_page({{ $default_take }})"><svg class="w-6 h-6" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="m10 18l6-6l-6-6z" />
</svg></x-forms.button>
@else
<x-forms.button disabled><svg class="w-6 h-6" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="m10 18l6-6l-6-6z" />
</svg></x-forms.button>
@endif
</div>
<form class="flex items-end gap-2">
@@ -15,7 +36,7 @@
<x-forms.button type="submit">Filter</x-forms.button>
</form>
@forelse ($deployments as $deployment)
<a @class([
<a @class([
'bg-coolgray-100 p-2 border-l border-dashed transition-colors hover:no-underline',
'hover:bg-coolgray-200' => data_get($deployment, 'status') === 'queued',
'border-warning hover:bg-warning hover:text-black' =>
@@ -33,20 +54,28 @@
<span class=" text-warning">></span>
{{ $deployment->status }}
</div>
@if (data_get($deployment, 'pull_request_id'))
<div>
<span class=" text-warning">></span>
Pull Request #{{ data_get($deployment, 'pull_request_id') }}
@if (data_get($deployment, 'is_webhook') || data_get($deployment, 'pull_request_id'))
<div class="flex gap-1">
@if (data_get($deployment, 'is_webhook'))
(Webhook)
Webhook
@endif
Webhook (SHA
@if (data_get($deployment, 'commit'))
{{ data_get($deployment, 'commit') }})
@else
HEAD)
@if (data_get($deployment, 'pull_request_id'))
@if (data_get($deployment, 'is_webhook'))
|
@endif
Pull Request #{{ data_get($deployment, 'pull_request_id') }}
(SHA
@if (data_get($deployment, 'commit'))
{{ data_get($deployment, 'commit') }})
@else
HEAD)
@endif
@endif
</div>
@else
<div class="flex gap-1">
Manual
</div>
@endif
</div>

View File

@@ -130,7 +130,6 @@
@endif
@if ($application->could_set_build_commands())
@if ($application->build_pack === 'nixpacks')
<div class="flex flex-col gap-2 xl:flex-row">
<x-forms.input placeholder="If you modify this, you probably need to have a nixpacks.toml"
id="application.install_command" label="Install Command" />
@@ -194,6 +193,13 @@
@endif
@endif
</div>
<div>The following options are for advanced use cases. Only modify them if you
know what are
you doing.</div>
<x-forms.input
helper="You can add custom docker run options that will be used when your container is started.<br>Note: Not all options are supported, as they could mess up Coolify's automation and could cause bad experience for users.<br><br>Check the <a class='text-white underline' href='https://coolify.io/docs/custom-docker-options'>docs.</a>"
placeholder="--cap-add SYS_ADMIN --device=/dev/fuse --security-opt apparmor:unconfined --ulimit nofile=1024:1024 --tmpfs /run:rw,noexec,nosuid,size=65536k"
id="application.custom_docker_run_options" label="Custom Docker Options" />
@endif
@endif

View File

@@ -1,11 +1,13 @@
<div>
<livewire:project.application.preview.form :application="$application" />
<div>
<div class="flex items-center gap-2">
<h3>Pull Requests on Git</h3>
<x-forms.button wire:click="load_prs">Load Pull Requests
</x-forms.button>
</div>
@if ($application->is_github_based())
<div class="flex items-center gap-2">
<h3>Pull Requests on Git</h3>
<x-forms.button wire:click="load_prs">Load Pull Requests
</x-forms.button>
</div>
@endif
@isset($rate_limit_remaining)
<div class="pt-1 ">Requests remaining till rate limited by Git: {{ $rate_limit_remaining }}</div>
@endisset

View File

@@ -33,6 +33,10 @@
helper="Need to set a secret to be able to use this webhook. It should match with the secret in GitLab."
label="GitLab Webhook Secret" id="resource.manual_webhook_secret_gitlab"></x-forms.input>
</div>
<div class="flex gap-2">
<x-forms.input readonly label="Bitbucket" id="bitbucketManualWebhook"></x-forms.input>
<x-forms.input type="password" helper="Need to set a secret to be able to use this webhook. It should match with the secret in Bitbucket." label="Bitbucket Webhook Secret" id="resource.manual_webhook_secret_bitbucket"></x-forms.input>
</div>
<x-forms.button type="submit">Save</x-forms.button>
</form>
@else

View File

@@ -14,7 +14,7 @@
~/.ssh/authorized_keys
file</span>
<x-forms.button type="submit">
Save Private Key
Continue
</x-forms.button>
</form>
</div>

View File

@@ -70,8 +70,7 @@ Route::group([
$type = $resource->getMorphClass();
if ($type === 'App\Models\Application') {
queue_application_deployment(
server_id: $resource->destination->server->id,
application_id: $resource->id,
application: $resource,
deployment_uuid: new Cuid2(7),
force_rebuild: $force,
);

View File

@@ -1,5 +1,7 @@
<?php
use App\Enums\ProcessStatus;
use App\Jobs\ApplicationPullRequestUpdateJob;
use App\Jobs\SubscriptionInvoiceFailedJob;
use App\Jobs\SubscriptionTrialEndedJob;
use App\Jobs\SubscriptionTrialEndsSoonJob;
@@ -149,8 +151,7 @@ Route::post('/source/gitlab/events/manual', function () {
ray('Deploying ' . $application->name . ' with branch ' . $branch);
$deployment_uuid = new Cuid2(7);
queue_application_deployment(
server_id: $application->destination->server->id,
application_id: $application->id,
application: $application,
deployment_uuid: $deployment_uuid,
force_rebuild: false,
is_webhook: true
@@ -178,8 +179,7 @@ Route::post('/source/gitlab/events/manual', function () {
]);
}
queue_application_deployment(
server_id: $application->destination->server->id,
application_id: $application->id,
application: $application,
pull_request_id: $pull_request_id,
deployment_uuid: $deployment_uuid,
force_rebuild: false,
@@ -234,8 +234,162 @@ Route::post('/source/gitlab/events/manual', function () {
return handleError($e);
}
});
Route::post('/source/bitbucket/events/manual', function () {
try {
$return_payloads = collect([]);
$payload = request()->collect();
$headers = request()->headers->all();
$x_bitbucket_token = data_get($headers, 'x-hub-signature.0', "");
$x_bitbucket_event = data_get($headers, 'x-event-key.0', "");
$handled_events = collect(['repo:push', 'pullrequest:created', 'pullrequest:rejected', 'pullrequest:fulfilled']);
if (!$handled_events->contains($x_bitbucket_event)) {
return response([
'status' => 'failed',
'message' => 'Nothing to do. Event not handled.',
]);
}
if ($x_bitbucket_event === 'repo:push') {
$branch = data_get($payload, 'push.changes.0.new.name');
$full_name = data_get($payload, 'repository.full_name');
if (!$branch) {
return response([
'status' => 'failed',
'message' => 'Nothing to do. No branch found in the request.',
]);
}
ray('Manual webhook bitbucket push event with branch: ' . $branch);
}
if ($x_bitbucket_event === 'pullrequest:created' || $x_bitbucket_event === 'pullrequest:rejected' || $x_bitbucket_event === 'pullrequest:fulfilled') {
$branch = data_get($payload, 'pullrequest.destination.branch.name');
$base_branch = data_get($payload, 'pullrequest.source.branch.name');
$full_name = data_get($payload, 'repository.full_name');
$pull_request_id = data_get($payload, 'pullrequest.id');
$pull_request_html_url = data_get($payload, 'pullrequest.links.html.href');
$commit = data_get($payload, 'pullrequest.source.commit.hash');
}
$applications = Application::where('git_repository', 'like', "%$full_name%");
$applications = $applications->where('git_branch', $branch)->get();
if ($applications->isEmpty()) {
return response([
'status' => 'failed',
'message' => "Nothing to do. No applications found with deploy key set, branch is '$branch' and Git Repository name has $full_name.",
]);
}
foreach ($applications as $application) {
$webhook_secret = data_get($application, 'manual_webhook_secret_bitbucket');
$payload = request()->getContent();
list($algo, $hash) = explode('=', $x_bitbucket_token, 2);
$payloadHash = hash_hmac($algo, $payload, $webhook_secret);
if (!hash_equals($hash, $payloadHash) && !isDev()) {
$return_payloads->push([
'application' => $application->name,
'status' => 'failed',
'message' => 'Invalid token.',
]);
ray('Invalid signature');
continue;
}
$isFunctional = $application->destination->server->isFunctional();
if (!$isFunctional) {
$return_payloads->push([
'application' => $application->name,
'status' => 'failed',
'message' => 'Server is not functional.',
]);
ray('Server is not functional: ' . $application->destination->server->name);
continue;
}
if ($x_bitbucket_event === 'repo:push') {
if ($application->isDeployable()) {
ray('Deploying ' . $application->name . ' with branch ' . $branch);
$deployment_uuid = new Cuid2(7);
queue_application_deployment(
application: $application,
deployment_uuid: $deployment_uuid,
force_rebuild: false,
is_webhook: true
);
$return_payloads->push([
'application' => $application->name,
'status' => 'success',
'message' => 'Preview deployment queued.',
]);
} else {
$return_payloads->push([
'application' => $application->name,
'status' => 'failed',
'message' => 'Auto deployment disabled.',
]);
}
}
if ($x_bitbucket_event === 'pullrequest:created') {
if ($application->isPRDeployable()) {
ray('Deploying preview for ' . $application->name . ' with branch ' . $branch . ' and base branch ' . $base_branch . ' and pull request id ' . $pull_request_id);
$deployment_uuid = new Cuid2(7);
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if (!$found) {
ApplicationPreview::create([
'git_type' => 'bitbucket',
'application_id' => $application->id,
'pull_request_id' => $pull_request_id,
'pull_request_html_url' => $pull_request_html_url,
]);
}
queue_application_deployment(
application: $application,
pull_request_id: $pull_request_id,
deployment_uuid: $deployment_uuid,
force_rebuild: false,
commit: $commit,
is_webhook: true,
git_type: 'bitbucket'
);
$return_payloads->push([
'application' => $application->name,
'status' => 'success',
'message' => 'Preview deployment queued.',
]);
} else {
$return_payloads->push([
'application' => $application->name,
'status' => 'failed',
'message' => 'Preview deployments disabled.',
]);
}
}
if ($x_bitbucket_event === 'pullrequest:rejected' || $x_bitbucket_event === 'pullrequest:fulfilled') {
ray('Pull request rejected');
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if ($found) {
$found->delete();
$container_name = generateApplicationContainerName($application, $pull_request_id);
instant_remote_process(["docker rm -f $container_name"], $application->destination->server);
$return_payloads->push([
'application' => $application->name,
'status' => 'success',
'message' => 'Preview deployment closed.',
]);
} else {
$return_payloads->push([
'application' => $application->name,
'status' => 'failed',
'message' => 'No preview deployment found.',
]);
}
}
}
ray($return_payloads);
return response($return_payloads);
} catch (Exception $e) {
ray($e);
return handleError($e);
}
});
Route::post('/source/github/events/manual', function () {
try {
$return_payloads = collect([]);
$x_github_event = Str::lower(request()->header('X-GitHub-Event'));
$x_hub_signature_256 = Str::after(request()->header('X-Hub-Signature-256'), 'sha256=');
$content_type = request()->header('Content-Type');
@@ -284,13 +438,22 @@ Route::post('/source/github/events/manual', function () {
foreach ($applications as $application) {
$webhook_secret = data_get($application, 'manual_webhook_secret_github');
$hmac = hash_hmac('sha256', request()->getContent(), $webhook_secret);
if (!hash_equals($x_hub_signature_256, $hmac)) {
if (!hash_equals($x_hub_signature_256, $hmac) && !isDev()) {
ray('Invalid signature');
$return_payloads->push([
'application' => $application->name,
'status' => 'failed',
'message' => 'Invalid token.',
]);
continue;
}
$isFunctional = $application->destination->server->isFunctional();
if (!$isFunctional) {
ray('Server is not functional: ' . $application->destination->server->name);
$return_payloads->push([
'application' => $application->name,
'status' => 'failed',
'message' => 'Server is not functional.',
]);
continue;
}
if ($x_github_event === 'push') {
@@ -298,14 +461,22 @@ Route::post('/source/github/events/manual', function () {
ray('Deploying ' . $application->name . ' with branch ' . $branch);
$deployment_uuid = new Cuid2(7);
queue_application_deployment(
server_id: $application->destination->server->id,
application_id: $application->id,
application: $application,
deployment_uuid: $deployment_uuid,
force_rebuild: false,
is_webhook: true
is_webhook: true,
);
$return_payloads->push([
'application' => $application->name,
'status' => 'success',
'message' => 'Deployment queued.',
]);
} else {
ray('Deployments disabled for ' . $application->name);
$return_payloads->push([
'application' => $application->name,
'status' => 'failed',
'message' => 'Deployments disabled.',
]);
}
}
if ($x_github_event === 'pull_request') {
@@ -322,19 +493,24 @@ Route::post('/source/github/events/manual', function () {
]);
}
queue_application_deployment(
server_id: $application->destination->server->id,
application_id: $application->id,
application: $application,
pull_request_id: $pull_request_id,
deployment_uuid: $deployment_uuid,
force_rebuild: false,
is_webhook: true,
git_type: 'github'
);
ray('Deploying preview for ' . $application->name . ' with branch ' . $branch . ' and base branch ' . $base_branch . ' and pull request id ' . $pull_request_id);
return response('Preview Deployment queued.');
$return_payloads->push([
'application' => $application->name,
'status' => 'success',
'message' => 'Preview deployment queued.',
]);
} else {
ray('Preview deployments disabled for ' . $application->name);
return response('Nothing to do. Preview Deployments disabled.');
$return_payloads->push([
'application' => $application->name,
'status' => 'failed',
'message' => 'Preview deployments disabled.',
]);
}
}
if ($action === 'closed') {
@@ -344,12 +520,23 @@ Route::post('/source/github/events/manual', function () {
$container_name = generateApplicationContainerName($application, $pull_request_id);
// ray('Stopping container: ' . $container_name);
instant_remote_process(["docker rm -f $container_name"], $application->destination->server);
return response('Preview Deployment closed.');
$return_payloads->push([
'application' => $application->name,
'status' => 'success',
'message' => 'Preview deployment closed.',
]);
} else {
$return_payloads->push([
'application' => $application->name,
'status' => 'failed',
'message' => 'No preview deployment found.',
]);
}
return response('Nothing to do. No Preview Deployment found');
}
}
}
ray($return_payloads);
return response($return_payloads);
} catch (Exception $e) {
ray($e->getMessage());
return handleError($e);
@@ -357,6 +544,7 @@ Route::post('/source/github/events/manual', function () {
});
Route::post('/source/github/events', function () {
try {
$return_payloads = collect([]);
$id = null;
$x_github_delivery = request()->header('X-GitHub-Delivery');
$x_github_event = Str::lower(request()->header('X-GitHub-Event'));
@@ -380,7 +568,7 @@ Route::post('/source/github/events', function () {
$hmac = hash_hmac('sha256', request()->getContent(), $webhook_secret);
if (config('app.env') !== 'local') {
if (!hash_equals($x_hub_signature_256, $hmac)) {
return response('not cool');
return response('Invalid signature.');
}
}
if ($x_github_event === 'push') {
@@ -420,7 +608,11 @@ Route::post('/source/github/events', function () {
foreach ($applications as $application) {
$isFunctional = $application->destination->server->isFunctional();
if (!$isFunctional) {
ray('Server is not functional: ' . $application->destination->server->name);
$return_payloads->push([
'application' => $application->name,
'status' => 'failed',
'message' => 'Server is not functional.',
]);
continue;
}
if ($x_github_event === 'push') {
@@ -428,14 +620,22 @@ Route::post('/source/github/events', function () {
ray('Deploying ' . $application->name . ' with branch ' . $branch);
$deployment_uuid = new Cuid2(7);
queue_application_deployment(
server_id: $application->destination->server->id,
application_id: $application->id,
application: $application,
deployment_uuid: $deployment_uuid,
force_rebuild: false,
is_webhook: true
);
$return_payloads->push([
'application' => $application->name,
'status' => 'success',
'message' => 'Deployment queued.',
]);
} else {
ray('Deployments disabled for ' . $application->name);
$return_payloads->push([
'application' => $application->name,
'status' => 'failed',
'message' => 'Deployments disabled.',
]);
}
}
if ($x_github_event === 'pull_request') {
@@ -452,34 +652,51 @@ Route::post('/source/github/events', function () {
]);
}
queue_application_deployment(
server_id: $application->destination->server->id,
application_id: $application->id,
application: $application,
pull_request_id: $pull_request_id,
deployment_uuid: $deployment_uuid,
force_rebuild: false,
is_webhook: true,
git_type: 'github'
);
ray('Deploying preview for ' . $application->name . ' with branch ' . $branch . ' and base branch ' . $base_branch . ' and pull request id ' . $pull_request_id);
return response('Preview Deployment queued.');
$return_payloads->push([
'application' => $application->name,
'status' => 'success',
'message' => 'Preview deployment queued.',
]);
} else {
ray('Preview deployments disabled for ' . $application->name);
return response('Nothing to do. Preview Deployments disabled.');
$return_payloads->push([
'application' => $application->name,
'status' => 'failed',
'message' => 'Preview deployments disabled.',
]);
}
}
if ($action === 'closed' || $action === 'close') {
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if ($found) {
ApplicationPullRequestUpdateJob::dispatchSync(application: $application, preview: $found, status: ProcessStatus::CLOSED);
$found->delete();
$container_name = generateApplicationContainerName($application, $pull_request_id);
// ray('Stopping container: ' . $container_name);
instant_remote_process(["docker rm -f $container_name"], $application->destination->server);
return response('Preview Deployment closed.');
$return_payloads->push([
'application' => $application->name,
'status' => 'success',
'message' => 'Preview deployment closed.',
]);
} else {
$return_payloads->push([
'application' => $application->name,
'status' => 'failed',
'message' => 'No preview deployment found.',
]);
}
return response('Nothing to do. No Preview Deployment found');
}
}
}
ray($return_payloads);
return response($return_payloads);
} catch (Exception $e) {
ray($e->getMessage());
return handleError($e);

View File

@@ -9,3 +9,7 @@ services:
- wordpress-files:/var/www/html
environment:
SERVICE_FQDN:
WORDPRESS_DB_HOST: $WORDPRESS_DB_HOST
WORDPRESS_DB_USER: $WORDPRESS_DB_USER
WORDPRESS_DB_PASSWORD: $WORDPRESS_DB_PASSWORD
WORDPRESS_DB_NAME: $WORDPRESS_DB_NAME

View File

@@ -603,7 +603,7 @@
"wordpress-without-database": {
"documentation": "https:\/\/wordpress.org\/documentation\/",
"slogan": "WordPress with external database. Wordpress is open source software you can use to create a beautiful website, blog, or app.",
"compose": "c2VydmljZXM6CiAgd29yZHByZXNzOgogICAgaW1hZ2U6ICd3b3JkcHJlc3M6bGF0ZXN0JwogICAgdm9sdW1lczoKICAgICAgLSAnd29yZHByZXNzLWZpbGVzOi92YXIvd3d3L2h0bWwnCiAgICBlbnZpcm9ubWVudDoKICAgICAgU0VSVklDRV9GUUROOiBudWxsCg==",
"compose": "c2VydmljZXM6CiAgd29yZHByZXNzOgogICAgaW1hZ2U6ICd3b3JkcHJlc3M6bGF0ZXN0JwogICAgdm9sdW1lczoKICAgICAgLSAnd29yZHByZXNzLWZpbGVzOi92YXIvd3d3L2h0bWwnCiAgICBlbnZpcm9ubWVudDoKICAgICAgU0VSVklDRV9GUUROOiBudWxsCiAgICAgIFdPUkRQUkVTU19EQl9IT1NUOiAkV09SRFBSRVNTX0RCX0hPU1QKICAgICAgV09SRFBSRVNTX0RCX1VTRVI6ICRXT1JEUFJFU1NfREJfVVNFUgogICAgICBXT1JEUFJFU1NfREJfUEFTU1dPUkQ6ICRXT1JEUFJFU1NfREJfUEFTU1dPUkQKICAgICAgV09SRFBSRVNTX0RCX05BTUU6ICRXT1JEUFJFU1NfREJfTkFNRQo=",
"tags": [
"cms",
"blog",

View File

@@ -1 +0,0 @@
[]

View File

@@ -1,17 +0,0 @@
[
{
"_id": "e6458286-eef1-401c-be84-860b111d66f0",
"colName": "Webhooks",
"created": "2023-05-09T11:45:36.504Z",
"sortNum": 10000,
"folders": [
{
"_id": "b8cfd093-5467-44a2-9221-ad0207717310",
"name": "GitHub",
"containerId": "",
"created": "2023-05-09T11:45:40.630Z",
"sortNum": 10000
}
]
}
]

View File

@@ -1,29 +0,0 @@
[
{
"_id": "e3fbfa6d-da5a-422c-95c5-904c27da8e5a",
"name": "(Global Env)",
"default": false,
"global": true,
"sortNum": -2,
"created": "2023-05-31T08:28:50.859Z",
"modified": "2023-05-31T08:28:50.859Z",
"data": [
{
"name": "repository_id",
"value": "603035348"
},
{
"name": "repository_ref",
"value": "nodejs-fastify"
},
{
"name": "repository_name",
"value": "coollabsio/coolify-examples"
},
{
"name": "repository_ref_pr",
"value": "nodejs-fastify-pr"
}
]
}
]

View File

@@ -1,146 +0,0 @@
[
{
"_id": "b3d379ab-e5e4-4ba4-991d-b6c8c6bbcb98",
"colId": "e6458286-eef1-401c-be84-860b111d66f0",
"containerId": "b8cfd093-5467-44a2-9221-ad0207717310",
"name": "Public Push",
"url": "http://localhost:8000/webhooks/source/github/events",
"method": "POST",
"sortNum": 10000,
"created": "2023-05-09T11:45:50.227Z",
"modified": "2023-05-09T12:22:27.192Z",
"headers": [
{
"name": "X-GitHub-Delivery",
"value": "9b4bc300-ee63-11ed-9133-5f71dd83487d"
},
{
"name": "X-GitHub-Event",
"value": "push"
},
{
"name": "X-GitHub-Hook-ID",
"value": "400873078"
},
{
"name": "X-GitHub-Hook-Installation-Target-ID",
"value": "292941"
},
{
"name": "X-GitHub-Hook-Installation-Target-Type",
"value": "integration"
},
{
"name": "X-Hub-Signature-256",
"value": "sha256=d5c8d05cc6de14422ab3661d37ec4b98e71f4fdd63d1116f5dedfcb0213ee03d"
},
{
"name": "Content-Type",
"value": "application/json"
}
],
"params": [],
"body": {
"type": "json",
"raw": "{\n \"ref\": \"{{repository_ref}}\",\n \"repository\": {\n \"id\": \"{{repository_id}}\",\n \"full_name\": \"{{repository_name}}\"\n }\n}",
"form": []
},
"tests": []
},
{
"_id": "b5386afc-ad91-428f-88ac-0f449c5c26fd",
"colId": "e6458286-eef1-401c-be84-860b111d66f0",
"containerId": "b8cfd093-5467-44a2-9221-ad0207717310",
"name": "Public PR - Opened",
"url": "http://localhost:8000/webhooks/source/github/events",
"method": "POST",
"sortNum": 20000,
"created": "2023-05-31T08:23:28.904Z",
"modified": "2023-06-13T14:11:50.286Z",
"headers": [
{
"name": "X-GitHub-Delivery",
"value": "e4c43c10-09cf-11ee-8879-0a481c473173"
},
{
"name": "X-GitHub-Event",
"value": "pull_request"
},
{
"name": "X-GitHub-Hook-ID",
"value": "400873078"
},
{
"name": "X-GitHub-Hook-Installation-Target-ID",
"value": "292941"
},
{
"name": "X-GitHub-Hook-Installation-Target-Type",
"value": "integration"
},
{
"name": "X-Hub-Signature-256",
"value": "sha256=d02e35ae379a528076710322fcf9386b23bc14d61fd671259ae1d9d20488b36f"
},
{
"name": "Content-Type",
"value": "application/json"
}
],
"params": [],
"body": {
"type": "json",
"raw": "{\r\n \"action\": \"opened\",\r\n \"number\": 1,\r\n \"pull_request\": {\r\n \"html_url\": \"https://github.com/{{repository_name}}/pull/1\",\r\n \"head\": {\r\n \"ref\":\"{{repository_ref_pr}}\"\r\n },\r\n \"base\": {\r\n \"ref\":\"{{repository_ref}}\"\r\n }\r\n },\r\n \"repository\": {\r\n \"id\": \"{{repository_id}}\",\r\n \"full_name\": \"{{repository_name}}\"\r\n }\r\n}",
"form": []
},
"tests": []
},
{
"_id": "7e7a3abd-dc01-454f-aa80-eaeb2c18aa56",
"colId": "e6458286-eef1-401c-be84-860b111d66f0",
"containerId": "b8cfd093-5467-44a2-9221-ad0207717310",
"name": "Public PR - Closed",
"url": "http://localhost:8000/webhooks/source/github/events",
"method": "POST",
"sortNum": 30000,
"created": "2023-05-31T09:15:15.833Z",
"modified": "2023-06-13T08:34:27.203Z",
"headers": [
{
"name": "X-GitHub-Delivery",
"value": "9b4bc300-ee63-11ed-9133-5f71dd83487d"
},
{
"name": "X-GitHub-Event",
"value": "pull_request"
},
{
"name": "X-GitHub-Hook-ID",
"value": "400873078"
},
{
"name": "X-GitHub-Hook-Installation-Target-ID",
"value": "292941"
},
{
"name": "X-GitHub-Hook-Installation-Target-Type",
"value": "integration"
},
{
"name": "X-Hub-Signature-256",
"value": "sha256=d5c8d05cc6de14422ab3661d37ec4b98e71f4fdd63d1116f5dedfcb0213ee03d"
},
{
"name": "Content-Type",
"value": "application/json"
}
],
"params": [],
"body": {
"type": "json",
"raw": "{\n \"action\": \"closed\",\n \"number\": 1,\n \"pull_request\": {\n \"html_url\": \"https://github.com/{{repository_name}}/pull/1\",\n \"head\": {\n \"ref\":\"{{repository_ref_pr}}\"\n },\n \"base\": {\n \"ref\":\"{{repository_ref}}\"\n }\n },\n \"repository\": {\n \"id\": \"{{repository_id}}\",\n \"full_name\": \"{{repository_name}}\"\n }\n}",
"form": []
},
"tests": []
}
]

View File

@@ -4,7 +4,7 @@
"version": "3.12.36"
},
"v4": {
"version": "4.0.0-beta.200"
"version": "4.0.0-beta.203"
}
}
}