Compare commits

...

63 Commits

Author SHA1 Message Date
Andras Bacsai
93fb14884e Merge pull request #1711 from coollabsio/next
Refactor server validation and installation logic
2024-02-05 15:13:56 +01:00
Andras Bacsai
26ccc4afb4 Refactor server validation and installation logic 2024-02-05 15:13:39 +01:00
Andras Bacsai
5fda1bb932 Merge pull request #1710 from coollabsio/next
v4.0.0-beta.207
2024-02-05 14:57:21 +01:00
Andras Bacsai
409ba8a1bb Refactor application deployment logic 2024-02-05 14:47:06 +01:00
Andras Bacsai
49f5240ff8 fix: better server validation and installation process
fix: add destination to queue deployment
feat: force start deployment
2024-02-05 14:40:54 +01:00
Andras Bacsai
0c3ed3d393 Update BunnyCDN sync and version numbers 2024-02-05 10:17:40 +01:00
Andras Bacsai
6e3dc474f2 Merge pull request #1702 from coollabsio/next
v4.0.0-beta.206
2024-02-05 10:06:59 +01:00
Andras Bacsai
d3eb87561e Fix styling issue in tag links 2024-02-05 10:00:53 +01:00
Andras Bacsai
8b58c8f856 Add tags to show and index views 2024-02-05 09:51:44 +01:00
Andras Bacsai
8c60ef5bd6 Update link in error message to the correct documentation 2024-02-04 17:00:13 +01:00
Andras Bacsai
1d59383c78 feat: clone to env 2024-02-04 16:54:12 +01:00
Andras Bacsai
60f590454d Update application deployment status in job handling 2024-02-04 14:40:23 +01:00
Andras Bacsai
dcb61a553e Merge pull request #1706 from piscis/patch-1
fix: Wrap tags and avoid horizontal overflow
2024-02-04 14:39:55 +01:00
Andras Bacsai
e06e31642f Refactor modal component and add new functionality 2024-02-04 14:07:08 +01:00
Andras Bacsai
9dfce48380 Add private_keys array initialization and define additional private properties 2024-02-04 13:50:24 +01:00
Andras Bacsai
8eed87e2f7 Update main class with mx-auto 2024-02-04 13:50:16 +01:00
Alex
d56d4eb8fc fix: Wrap tags and avoid horizontal overflow 2024-02-04 13:15:39 +01:00
Andras Bacsai
fd32cd04ab Refactor invoice payment failure handling in webhooks.php 2024-02-04 12:23:00 +01:00
Andras Bacsai
1d3b7ffd3b Refactor tags functionality and improve user experience 2024-02-03 12:44:18 +01:00
Andras Bacsai
0b5baf60a5 fix: tags 2024-02-03 12:39:07 +01:00
Andras Bacsai
bc31df6fb2 Update version numbers to 4.0.0-beta.206 2024-02-02 14:52:24 +01:00
Andras Bacsai
818399bc23 Merge pull request #1700 from coollabsio/next
v4.0.0-beta.205
2024-02-02 12:41:46 +01:00
Andras Bacsai
e7fdff0f69 feat: tags
ui: improvements
2024-02-02 11:50:28 +01:00
Andras Bacsai
6312c0ba84 feat: tags and tag deploy webhooks 2024-02-01 15:38:12 +01:00
Andras Bacsai
44efe0b5e1 Update versions and fix code formatting 2024-02-01 11:59:20 +01:00
Andras Bacsai
de7d584648 Merge pull request #1694 from coollabsio/next
v4.0.0-beta.204
2024-02-01 10:54:27 +01:00
Andras Bacsai
b9f12d2586 fix: duplicate domain check 2024-02-01 10:53:05 +01:00
Andras Bacsai
c76e8bb0de fix: migrate to new modal 2024-01-31 16:14:12 +01:00
Andras Bacsai
3b655f8e3f Update filebrowser image tag to use filebrowser/filebrowser:s6 2024-01-31 15:16:06 +01:00
Andras Bacsai
2b9df41444 fix: create dynamic directory 2024-01-31 15:04:08 +01:00
Andras Bacsai
628fec6904 fix: sentry error 2024-01-31 14:22:48 +01:00
Andras Bacsai
f36135cbfc fix: sentry 2024-01-31 14:20:57 +01:00
Andras Bacsai
75fe005055 fix: sentry error 2024-01-31 14:19:45 +01:00
Andras Bacsai
8ff7aeb78b ui: new modal component 2024-01-31 14:18:59 +01:00
Andras Bacsai
f1a9e28d5a fix: sentry 2024-01-31 14:18:51 +01:00
Andras Bacsai
843cd90ee5 Update exception type in generate_github_installation_token function 2024-01-31 13:47:16 +01:00
Andras Bacsai
1cbfd03912 fix: sentry fix 2024-01-31 13:46:40 +01:00
Andras Bacsai
ce60a39dc5 Throw RuntimeException instead of Exception when no resource is found in ScheduledTaskJob 2024-01-31 13:45:58 +01:00
Andras Bacsai
f1e4395a83 Refactor shared variable type validation 2024-01-31 13:43:23 +01:00
Andras Bacsai
52fd7ad571 fix: not able to use other shared envs 2024-01-31 13:40:15 +01:00
Andras Bacsai
5f797ec0ae Update version and release numbers 2024-01-31 10:28:18 +01:00
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
130 changed files with 2326 additions and 2314 deletions

View File

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

View File

@@ -17,6 +17,9 @@ class StopDatabase
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb $database) public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb $database)
{ {
$server = $database->destination->server; $server = $database->destination->server;
if (!$server->isFunctional()) {
return 'Server is not functional';
}
instant_remote_process( instant_remote_process(
["docker rm -f {$database->uuid}"], ["docker rm -f {$database->uuid}"],
$server $server

View File

@@ -27,7 +27,7 @@ class StartProxy
$server->save(); $server->save();
if ($server->isSwarm()) { if ($server->isSwarm()) {
$commands = $commands->merge([ $commands = $commands->merge([
"mkdir -p $proxy_path && cd $proxy_path", "mkdir -p $proxy_path/dynamic && cd $proxy_path",
"echo 'Creating required Docker Compose file.'", "echo 'Creating required Docker Compose file.'",
"echo 'Starting coolify-proxy.'", "echo 'Starting coolify-proxy.'",
"cd $proxy_path && docker stack deploy -c docker-compose.yml coolify-proxy", "cd $proxy_path && docker stack deploy -c docker-compose.yml coolify-proxy",
@@ -35,7 +35,7 @@ class StartProxy
]); ]);
} else { } else {
$commands = $commands->merge([ $commands = $commands->merge([
"mkdir -p $proxy_path && cd $proxy_path", "mkdir -p $proxy_path/dynamic && cd $proxy_path",
"echo 'Creating required Docker Compose file.'", "echo 'Creating required Docker Compose file.'",
"echo 'Pulling docker image.'", "echo 'Pulling docker image.'",
'docker compose pull', 'docker compose pull',

View File

@@ -43,6 +43,7 @@ class InstallDocker
"echo 'Restarting Docker Engine...'", "echo 'Restarting Docker Engine...'",
"ls -l /tmp" "ls -l /tmp"
]); ]);
return remote_process($command, $server);
} else { } else {
if ($supported_os_type->contains('debian')) { if ($supported_os_type->contains('debian')) {
$command = $command->merge([ $command = $command->merge([
@@ -89,7 +90,6 @@ class InstallDocker
"echo 'Done!'", "echo 'Done!'",
]); ]);
} }
return remote_process($command, $server); return remote_process($command, $server);
} }
} }

View File

@@ -10,35 +10,42 @@ class DeleteService
use AsAction; use AsAction;
public function handle(Service $service) public function handle(Service $service)
{ {
$server = data_get($service, 'server'); try {
if ($server->isFunctional()) { $server = data_get($service, 'server');
StopService::run($service); if ($server->isFunctional()) {
} $storagesToDelete = collect([]);
$storagesToDelete = collect([]);
$service->environment_variables()->delete(); $service->environment_variables()->delete();
$commands = []; $commands = [];
foreach ($service->applications()->get() as $application) { foreach ($service->applications()->get() as $application) {
$storages = $application->persistentStorages()->get(); $storages = $application->persistentStorages()->get();
foreach ($storages as $storage) { foreach ($storages as $storage) {
$storagesToDelete->push($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(); } catch (\Exception $e) {
} throw new \Exception($e->getMessage());
foreach ($service->databases()->get() as $database) { } finally {
$storages = $database->persistentStorages()->get(); foreach ($service->applications()->get() as $application) {
foreach ($storages as $storage) { $application->forceDelete();
$storagesToDelete->push($storage);
} }
$database->forceDelete(); foreach ($service->databases()->get() as $database) {
$database->forceDelete();
}
$service->tags()->detach();
} }
foreach ($storagesToDelete as $storage) {
$commands[] = "docker volume rm -f $storage->name";
}
$commands[] = "docker rm -f $service->uuid";
instant_remote_process($commands, $server, false);
$service->forceDelete();
} }
} }

View File

@@ -10,6 +10,10 @@ class StopService
use AsAction; use AsAction;
public function handle(Service $service) public function handle(Service $service)
{ {
$server = $service->destination->server;
if (!$server->isFunctional()) {
return 'Server is not functional';
}
ray('Stopping service: ' . $service->name); ray('Stopping service: ' . $service->name);
$applications = $service->applications()->get(); $applications = $service->applications()->get();
foreach ($applications as $application) { 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\Enums\ApplicationDeploymentStatus;
use App\Jobs\CleanupHelperContainersJob; use App\Jobs\CleanupHelperContainersJob;
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue; use App\Models\ApplicationDeploymentQueue;
use App\Models\InstanceSettings; use App\Models\InstanceSettings;
use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledDatabaseBackup;
use App\Models\Server; 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\StandalonePostgresql;
use App\Models\StandaloneRedis;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
@@ -31,7 +23,7 @@ class Init extends Command
$cleanup = $this->option('cleanup'); $cleanup = $this->option('cleanup');
if ($cleanup) { if ($cleanup) {
echo "Running cleanups...\n"; echo "Running cleanups...\n";
$this->cleanup_stucked_resources(); $this->call('cleanup:stucked-resources');
// Required for falsely deleted coolify db // Required for falsely deleted coolify db
$this->restore_coolify_db_backup(); $this->restore_coolify_db_backup();
@@ -137,273 +129,5 @@ class Init extends Command
echo "Error: {$e->getMessage()}\n"; 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

@@ -48,7 +48,7 @@ class SyncBunny extends Command
$versions = "versions.json"; $versions = "versions.json";
PendingRequest::macro('storage', function ($fileName) use($that) { PendingRequest::macro('storage', function ($fileName) use ($that) {
$headers = [ $headers = [
'AccessKey' => env('BUNNY_STORAGE_API_KEY'), 'AccessKey' => env('BUNNY_STORAGE_API_KEY'),
'Accept' => 'application/json', 'Accept' => 'application/json',
@@ -76,23 +76,26 @@ class SyncBunny extends Command
} }
if ($only_template) { if ($only_template) {
$this->info('About to sync service-templates.json to BunnyCDN.'); $this->info('About to sync service-templates.json to BunnyCDN.');
} $confirmed = confirm("Are you sure you want to sync?");
if ($only_version) { if (!$confirmed) {
$this->info('About to sync versions.json to BunnyCDN.'); return;
} }
$confirmed = confirm('Are you sure you want to sync?');
if (!$confirmed) {
return;
}
if ($only_template) {
Http::pool(fn (Pool $pool) => [ Http::pool(fn (Pool $pool) => [
$pool->storage(fileName: "$parent_dir/templates/$service_template")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$service_template"), $pool->storage(fileName: "$parent_dir/templates/$service_template")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$service_template"),
$pool->purge("$bunny_cdn/$bunny_cdn_path/$service_template"), $pool->purge("$bunny_cdn/$bunny_cdn_path/$service_template"),
]); ]);
$this->info('Service template uploaded & purged...'); $this->info('Service template uploaded & purged...');
return; return;
} } else if ($only_version) {
if ($only_version) { $this->info('About to sync versions.json to BunnyCDN.');
$file = file_get_contents("$parent_dir/$versions");
$json = json_decode($file, true);
$actual_version = data_get($json, 'coolify.v4.version');
$confirmed = confirm("Are you sure you want to sync to {$actual_version}?");
if (!$confirmed) {
return;
}
Http::pool(fn (Pool $pool) => [ Http::pool(fn (Pool $pool) => [
$pool->storage(fileName: "$parent_dir/$versions")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$versions"), $pool->storage(fileName: "$parent_dir/$versions")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$versions"),
$pool->purge("$bunny_cdn/$bunny_cdn_path/$versions"), $pool->purge("$bunny_cdn/$bunny_cdn_path/$versions"),
@@ -101,6 +104,7 @@ class SyncBunny extends Command
return; return;
} }
Http::pool(fn (Pool $pool) => [ Http::pool(fn (Pool $pool) => [
$pool->storage(fileName: "$parent_dir/$compose_file")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$compose_file"), $pool->storage(fileName: "$parent_dir/$compose_file")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$compose_file"),
$pool->storage(fileName: "$parent_dir/$compose_file_prod")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$compose_file_prod"), $pool->storage(fileName: "$parent_dir/$compose_file_prod")->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$compose_file_prod"),

View File

@@ -36,6 +36,8 @@ class Kernel extends ConsoleKernel
} else { } else {
// Instance Jobs // Instance Jobs
$schedule->command('horizon:snapshot')->everyFiveMinutes(); $schedule->command('horizon:snapshot')->everyFiveMinutes();
$schedule->command('cleanup:unreachable-servers')->daily();
$schedule->job(new CleanupInstanceStuffsJob)->everyTwoMinutes()->onOneServer(); $schedule->job(new CleanupInstanceStuffsJob)->everyTwoMinutes()->onOneServer();
// $schedule->job(new CheckResaleLicenseJob)->hourly()->onOneServer(); // $schedule->job(new CheckResaleLicenseJob)->hourly()->onOneServer();

View File

@@ -0,0 +1,159 @@
<?php
namespace App\Http\Controllers\Api;
use App\Actions\Database\StartMariadb;
use App\Actions\Database\StartMongodb;
use App\Actions\Database\StartMysql;
use App\Actions\Database\StartPostgresql;
use App\Actions\Database\StartRedis;
use App\Actions\Service\StartService;
use App\Http\Controllers\Controller;
use App\Models\Tag;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Visus\Cuid2\Cuid2;
class Deploy extends Controller
{
public function deploy(Request $request)
{
$token = auth()->user()->currentAccessToken();
$teamId = data_get($token, 'team_id');
$uuids = $request->query->get('uuid');
$tags = $request->query->get('tag');
$force = $request->query->get('force') ?? false;
if ($uuids && $tags) {
return response()->json(['error' => 'You can only use uuid or tag, not both.', 'docs' => 'https://coolify.io/docs/api/deploy-webhook'], 400);
}
if (is_null($teamId)) {
return response()->json(['error' => 'Invalid token.', 'docs' => 'https://coolify.io/docs/api/authentication'], 400);
}
if ($tags) {
return $this->by_tags($tags, $teamId, $force);
} else if ($uuids) {
return $this->by_uuids($uuids, $teamId, $force);
}
return response()->json(['error' => 'You must provide uuid or tag.', 'docs' => 'https://coolify.io/docs/api/deploy-webhook'], 400);
}
private function by_uuids(string $uuid, int $teamId, bool $force = false)
{
$uuids = explode(',', $uuid);
$uuids = collect(array_filter($uuids));
if (count($uuids) === 0) {
return response()->json(['error' => 'No UUIDs provided.', 'docs' => 'https://coolify.io/docs/api/deploy-webhook'], 400);
}
$message = collect([]);
foreach ($uuids as $uuid) {
$resource = getResourceByUuid($uuid, $teamId);
if ($resource) {
$return_message = $this->deploy_resource($resource, $force);
$message = $message->merge($return_message);
}
}
if ($message->count() > 0) {
return response()->json(['message' => $message->toArray()], 200);
}
return response()->json(['error' => "No resources found.", 'docs' => 'https://coolify.io/docs/api/deploy-webhook'], 404);
}
public function by_tags(string $tags, int $team_id, bool $force = false)
{
$tags = explode(',', $tags);
$tags = collect(array_filter($tags));
if (count($tags) === 0) {
return response()->json(['error' => 'No TAGs provided.', 'docs' => 'https://coolify.io/docs/api/deploy-webhook'], 400);
}
$message = collect([]);
foreach ($tags as $tag) {
$found_tag = Tag::where(['name' => $tag, 'team_id' => $team_id])->first();
if (!$found_tag) {
$message->push("Tag {$tag} not found.");
continue;
}
$applications = $found_tag->applications();
$services = $found_tag->services();
if ($applications->count() === 0 && $services->count() === 0) {
$message->push("No resources found for tag {$tag}.");
continue;
}
foreach ($applications as $resource) {
$return_message = $this->deploy_resource($resource, $force);
$message = $message->merge($return_message);
}
foreach ($services as $resource) {
$return_message = $this->deploy_resource($resource, $force);
$message = $message->merge($return_message);
}
}
if ($message->count() > 0) {
return response()->json(['message' => $message->toArray()], 200);
}
return response()->json(['error' => "No resources found.", 'docs' => 'https://coolify.io/docs/api/deploy-webhook'], 404);
}
public function deploy_resource($resource, bool $force = false): Collection
{
$message = collect([]);
$type = $resource->getMorphClass();
if ($type === 'App\Models\Application') {
queue_application_deployment(
application: $resource,
deployment_uuid: new Cuid2(7),
force_rebuild: $force,
);
$message->push("Application {$resource->name} deployment queued.");
} else if ($type === 'App\Models\StandalonePostgresql') {
if (str($resource->status)->startsWith('running')) {
$message->push("Database {$resource->name} already running.");
}
StartPostgresql::run($resource);
$resource->update([
'started_at' => now(),
]);
$message->push("Database {$resource->name} started.");
} else if ($type === 'App\Models\StandaloneRedis') {
if (str($resource->status)->startsWith('running')) {
$message->push("Database {$resource->name} already running.");
}
StartRedis::run($resource);
$resource->update([
'started_at' => now(),
]);
$message->push("Database {$resource->name} started.");
} else if ($type === 'App\Models\StandaloneMongodb') {
if (str($resource->status)->startsWith('running')) {
$message->push("Database {$resource->name} already running.");
}
StartMongodb::run($resource);
$resource->update([
'started_at' => now(),
]);
$message->push("Database {$resource->name} started.");
} else if ($type === 'App\Models\StandaloneMysql') {
if (str($resource->status)->startsWith('running')) {
$message->push("Database {$resource->name} already running.");
}
StartMysql::run($resource);
$resource->update([
'started_at' => now(),
]);
$message->push("Database {$resource->name} started.");
} else if ($type === 'App\Models\StandaloneMariadb') {
if (str($resource->status)->startsWith('running')) {
$message->push("Database {$resource->name} already running.");
}
StartMariadb::run($resource);
$resource->update([
'started_at' => now(),
]);
$message->push("Database {$resource->name} started.");
} else if ($type === 'App\Models\Service') {
StartService::run($resource);
$message->push("Service {$resource->name} started. It could take a while, be patient.");
}
return $message;
}
}

View File

@@ -122,7 +122,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if ($source) { if ($source) {
$this->source = $source->getMorphClass()::where('id', $this->application->source->id)->first(); $this->source = $source->getMorphClass()::where('id', $this->application->source->id)->first();
} }
$this->destination = $this->application->destination->getMorphClass()::where('id', $this->application->destination->id)->first(); $this->server = Server::find($this->application_deployment_queue->server_id);
$this->destination = $this->server->destinations()->where('id', $this->application_deployment_queue->destination_id)->first();
$this->server = $this->mainServer = $this->destination->server; $this->server = $this->mainServer = $this->destination->server;
$this->serverUser = $this->server->user; $this->serverUser = $this->server->user;
$this->basedir = $this->application->generateBaseDir($this->deployment_uuid); $this->basedir = $this->application->generateBaseDir($this->deployment_uuid);
@@ -166,10 +167,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
public function handle(): void public function handle(): void
{ {
$this->application_deployment_queue->update([
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
]);
// Generate custom host<->ip mapping // Generate custom host<->ip mapping
$allContainers = instant_remote_process(["docker network inspect {$this->destination->network} -f '{{json .Containers}}' "], $this->server); $allContainers = instant_remote_process(["docker network inspect {$this->destination->network} -f '{{json .Containers}}' "], $this->server);
if (!is_null($allContainers)) { if (!is_null($allContainers)) {
@@ -348,9 +345,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) { } 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) { if ($forceFail) {
throw $e; throw $e;
} }
@@ -488,10 +485,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
} else { } else {
$this->application_deployment_queue->addLogEntry("Starting pull request (#{$this->pull_request_id}) deployment of {$this->customRepository}:{$this->application->git_branch}."); $this->application_deployment_queue->addLogEntry("Starting pull request (#{$this->pull_request_id}) deployment of {$this->customRepository}:{$this->application->git_branch}.");
} }
$this->server->executeRemoteCommand( $this->prepare_builder_image();
commands: $this->application->prepareHelperImage($this->deployment_uuid),
loggingModel: $this->application_deployment_queue
);
$this->check_git_if_build_needed(); $this->check_git_if_build_needed();
$this->clone_repository(); $this->clone_repository();
$this->generate_image_names(); $this->generate_image_names();
@@ -568,12 +562,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->generate_build_env_variables(); $this->generate_build_env_variables();
$this->add_build_env_variables_to_dockerfile(); $this->add_build_env_variables_to_dockerfile();
$this->build_image(); $this->build_image();
// if ($this->application->additional_destinations) {
// $this->push_to_docker_registry();
// $this->deploy_to_additional_destinations();
// } else {
$this->rolling_update(); $this->rolling_update();
// }
} }
private function deploy_nixpacks_buildpack() private function deploy_nixpacks_buildpack()
{ {
@@ -798,7 +787,18 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
} }
private function deploy_to_additional_destinations() private function deploy_to_additional_destinations()
{ {
if (str($this->application->additional_destinations)->isEmpty()) {
return;
}
$destination_ids = collect(str($this->application->additional_destinations)->explode(',')); $destination_ids = collect(str($this->application->additional_destinations)->explode(','));
if ($this->server->isSwarm()) {
$this->application_deployment_queue->addLogEntry("Additional destinations are not supported in swarm mode.");
return;
}
if ($destination_ids->contains($this->destination->id)) {
ray('Same destination found in additional destinations. Skipping.');
return;
}
foreach ($destination_ids as $destination_id) { foreach ($destination_ids as $destination_id) {
$destination = StandaloneDocker::find($destination_id); $destination = StandaloneDocker::find($destination_id);
$server = $destination->server; $server = $destination->server;
@@ -806,11 +806,21 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->application_deployment_queue->addLogEntry("Skipping deployment to {$server->name}. Not in the same team?!"); $this->application_deployment_queue->addLogEntry("Skipping deployment to {$server->name}. Not in the same team?!");
continue; continue;
} }
$this->server = $server; // ray('Deploying to additional destination: ', $server->name);
$this->application_deployment_queue->addLogEntry("Deploying to {$this->server->name}."); $deployment_uuid = new Cuid2();
$this->prepare_builder_image(); queue_application_deployment(
$this->generate_image_names(); deployment_uuid: $deployment_uuid,
$this->rolling_update(); application: $this->application,
server: $server,
destination: $destination,
no_questions_asked: true,
);
$this->application_deployment_queue->addLogEntry("Deploying to additional server: {$server->name}. Click here to see the deployment status: " . route('project.application.deployment.show', [
'project_uuid' => data_get($this->application, 'environment.project.uuid'),
'application_uuid' => data_get($this->application, 'uuid'),
'deployment_uuid' => $deployment_uuid,
'environment_name' => data_get($this->application, 'environment.name'),
]));
} }
} }
private function set_base_dir() private function set_base_dir()
@@ -1128,6 +1138,11 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
data_forget($docker_compose, 'services.' . $this->container_name); data_forget($docker_compose, 'services.' . $this->container_name);
$custom_compose = convert_docker_run_to_compose($this->application->custom_docker_run_options);
if (count($custom_compose) > 0) {
$docker_compose['services'][$this->application->uuid] = array_merge_recursive($docker_compose['services'][$this->application->uuid], $custom_compose);
}
$this->docker_compose = Yaml::dump($docker_compose, 10); $this->docker_compose = Yaml::dump($docker_compose, 10);
$this->docker_compose_base64 = base64_encode($this->docker_compose); $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]); $this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d > {$this->workdir}/docker-compose.yml"), "hidden" => true]);
@@ -1509,11 +1524,13 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
'status' => $status, 'status' => $status,
]); ]);
} }
if ($status === ApplicationDeploymentStatus::FINISHED->value) {
$this->application->environment->project->team?->notify(new DeploymentSuccess($this->application, $this->deployment_uuid, $this->preview));
}
if ($status === ApplicationDeploymentStatus::FAILED->value) { if ($status === ApplicationDeploymentStatus::FAILED->value) {
$this->application->environment->project->team?->notify(new DeploymentFailed($this->application, $this->deployment_uuid, $this->preview)); $this->application->environment->project->team?->notify(new DeploymentFailed($this->application, $this->deployment_uuid, $this->preview));
return;
}
if ($status === ApplicationDeploymentStatus::FINISHED->value) {
// $this->deploy_to_additional_destinations();
$this->application->environment->project->team?->notify(new DeploymentSuccess($this->application, $this->deployment_uuid, $this->preview));
} }
} }

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

@@ -5,6 +5,7 @@ namespace App\Jobs;
use App\Actions\Application\StopApplication; use App\Actions\Application\StopApplication;
use App\Actions\Database\StopDatabase; use App\Actions\Database\StopDatabase;
use App\Actions\Service\DeleteService; use App\Actions\Service\DeleteService;
use App\Actions\Service\StopService;
use App\Models\Application; use App\Models\Application;
use App\Models\Service; use App\Models\Service;
use App\Models\StandaloneMariadb; use App\Models\StandaloneMariadb;
@@ -30,41 +31,22 @@ class DeleteResourceJob implements ShouldQueue, ShouldBeEncrypted
public function handle() public function handle()
{ {
try { try {
$server = $this->resource->destination->server; $this->resource->forceDelete();
$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';
}
switch ($this->resource->type()) { switch ($this->resource->type()) {
case 'application': case 'application':
StopApplication::run($this->resource); StopApplication::run($this->resource);
break; break;
case 'standalone-postgresql': case 'standalone-postgresql':
StopDatabase::run($this->resource);
break;
case 'standalone-redis': case 'standalone-redis':
StopDatabase::run($this->resource);
break;
case 'standalone-mongodb': case 'standalone-mongodb':
StopDatabase::run($this->resource);
break;
case 'standalone-mysql': case 'standalone-mysql':
StopDatabase::run($this->resource);
break;
case 'standalone-mariadb': case 'standalone-mariadb':
StopDatabase::run($this->resource); StopDatabase::run($this->resource);
break; break;
} case 'service':
if ($this->resource->type() === 'service') { StopService::run($this->resource);
DeleteService::dispatch($this->resource); DeleteService::run($this->resource);
} else { break;
$this->resource->forceDelete();
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
send_internal_notification('ContainerStoppingJob failed with: ' . $e->getMessage()); send_internal_notification('ContainerStoppingJob failed with: ' . $e->getMessage());

View File

@@ -39,7 +39,7 @@ class ScheduledTaskJob implements ShouldQueue
} else if ($application = $task->application()->first()) { } else if ($application = $task->application()->first()) {
$this->resource = $application; $this->resource = $application;
} else { } else {
throw new \Exception('ScheduledTaskJob failed: No resource found.'); throw new \RuntimeException('ScheduledTaskJob failed: No resource found.');
} }
$this->team = Team::find($task->team_id); $this->team = Team::find($task->team_id);
} }

View File

@@ -15,7 +15,7 @@ class ActivityMonitor extends Component
public $isPollingActive = false; public $isPollingActive = false;
protected $activity; protected $activity;
protected $listeners = ['newMonitorActivity']; protected $listeners = ['activityMonitor' => 'newMonitorActivity'];
public function newMonitorActivity($activityId, $eventToDispatch = 'activityFinished') public function newMonitorActivity($activityId, $eventToDispatch = 'activityFinished')
{ {

View File

@@ -12,6 +12,7 @@ use Livewire\Component;
class Index extends Component class Index extends Component
{ {
protected $listeners = ['serverInstalled' => 'validateServer'];
public string $currentState = 'welcome'; public string $currentState = 'welcome';
public ?string $selectedServerType = null; public ?string $selectedServerType = null;
@@ -93,7 +94,11 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
$this->serverPublicKey = $this->createdServer->privateKey->publicKey(); $this->serverPublicKey = $this->createdServer->privateKey->publicKey();
return $this->validateServer('localhost'); return $this->validateServer('localhost');
} elseif ($this->selectedServerType === 'remote') { } elseif ($this->selectedServerType === 'remote') {
$this->privateKeys = PrivateKey::ownedByCurrentTeam(['name'])->where('id', '!=', 0)->get(); if (isDev()) {
$this->privateKeys = PrivateKey::ownedByCurrentTeam(['name'])->get();
} else {
$this->privateKeys = PrivateKey::ownedByCurrentTeam(['name'])->where('id', '!=', 0)->get();
}
if ($this->privateKeys->count() > 0) { if ($this->privateKeys->count() > 0) {
$this->selectedExistingPrivateKey = $this->privateKeys->first()->id; $this->selectedExistingPrivateKey = $this->privateKeys->first()->id;
} }
@@ -190,6 +195,10 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
$this->createdServer->addInitialNetwork(); $this->createdServer->addInitialNetwork();
$this->validateServer(); $this->validateServer();
} }
public function installServer()
{
$this->dispatch('validateServer', true);
}
public function validateServer() public function validateServer()
{ {
try { try {
@@ -228,7 +237,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
$this->dockerInstallationStarted = true; $this->dockerInstallationStarted = true;
$activity = InstallDocker::run($this->createdServer); $activity = InstallDocker::run($this->createdServer);
$this->dispatch('installDocker'); $this->dispatch('installDocker');
$this->dispatch('newMonitorActivity', $activity->id); $this->dispatch('activityMonitor', $activity->id);
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->dockerInstallationStarted = false; $this->dockerInstallationStarted = false;
return handleError(error: $e, livewire: $this); return handleError(error: $e, livewire: $this);

View File

@@ -0,0 +1,63 @@
<?php
namespace App\Livewire;
use App\Models\User;
use Livewire\Component;
use Spatie\Activitylog\Models\Activity;
class NewActivityMonitor extends Component
{
public ?string $header = null;
public $activityId;
public $eventToDispatch = 'activityFinished';
public $isPollingActive = false;
protected $activity;
protected $listeners = ['newActivityMonitor' => 'newMonitorActivity'];
public function newMonitorActivity($activityId, $eventToDispatch = 'activityFinished')
{
$this->activityId = $activityId;
$this->eventToDispatch = $eventToDispatch;
$this->hydrateActivity();
$this->isPollingActive = true;
}
public function hydrateActivity()
{
$this->activity = Activity::find($this->activityId);
}
public function polling()
{
$this->hydrateActivity();
// $this->setStatus(ProcessStatus::IN_PROGRESS);
$exit_code = data_get($this->activity, 'properties.exitCode');
if ($exit_code !== null) {
// if ($exit_code === 0) {
// // $this->setStatus(ProcessStatus::FINISHED);
// } else {
// // $this->setStatus(ProcessStatus::ERROR);
// }
$this->isPollingActive = false;
if ($this->eventToDispatch !== null) {
if (str($this->eventToDispatch)->startsWith('App\\Events\\')) {
$causer_id = data_get($this->activity, 'causer_id');
$user = User::find($causer_id);
if ($user) {
foreach ($user->teams as $team) {
$teamId = $team->id;
$this->eventToDispatch::dispatch($teamId);
}
}
return;
}
$this->dispatch($this->eventToDispatch);
ray('Dispatched event: ' . $this->eventToDispatch);
}
}
}
}

View File

@@ -7,8 +7,6 @@ use App\Models\Application;
use App\Models\ApplicationDeploymentQueue; use App\Models\ApplicationDeploymentQueue;
use App\Models\Server; use App\Models\Server;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Str;
use Livewire\Component; use Livewire\Component;
class DeploymentNavbar extends Component class DeploymentNavbar extends Component
@@ -37,7 +35,15 @@ class DeploymentNavbar extends Component
$this->is_debug_enabled = $this->application->settings->is_debug_enabled; $this->is_debug_enabled = $this->application->settings->is_debug_enabled;
$this->dispatch('refreshQueue'); $this->dispatch('refreshQueue');
} }
public function force_start()
{
try {
force_start_deployment($this->application_deployment_queue);
} catch (\Throwable $e) {
ray($e);
return handleError($e, $this);
}
}
public function cancel() public function cancel()
{ {
try { try {
@@ -67,7 +73,6 @@ class DeploymentNavbar extends Component
'current_process_id' => null, 'current_process_id' => null,
'status' => ApplicationDeploymentStatus::CANCELLED_BY_USER->value, 'status' => ApplicationDeploymentStatus::CANCELLED_BY_USER->value,
]); ]);
// queue_next_deployment($this->application);
} }
} }
} }

View File

@@ -61,11 +61,12 @@ class General extends Component
'application.docker_compose_pr' => 'nullable', 'application.docker_compose_pr' => 'nullable',
'application.docker_compose_raw' => 'nullable', 'application.docker_compose_raw' => 'nullable',
'application.docker_compose_pr_raw' => 'nullable', 'application.docker_compose_pr_raw' => 'nullable',
'application.custom_labels' => 'nullable',
'application.dockerfile_target_build' => 'nullable', 'application.dockerfile_target_build' => 'nullable',
'application.settings.is_static' => 'boolean|required',
'application.docker_compose_custom_start_command' => 'nullable', 'application.docker_compose_custom_start_command' => 'nullable',
'application.docker_compose_custom_build_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_raw_compose_deployment_enabled' => 'boolean|required',
'application.settings.is_build_server_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.docker_compose_pr_raw' => 'Docker compose raw',
'application.custom_labels' => 'Custom labels', 'application.custom_labels' => 'Custom labels',
'application.dockerfile_target_build' => 'Dockerfile target build', '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_start_command' => 'Docker compose custom start command',
'application.docker_compose_custom_build_command' => 'Docker compose custom build 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_raw_compose_deployment_enabled' => 'Is raw compose deployment enabled',
'application.settings.is_build_server_enabled' => 'Is build server enabled', 'application.settings.is_build_server_enabled' => 'Is build server enabled',
]; ];
@@ -243,12 +245,16 @@ class General extends Component
$domains = $domains->unique(); $domains = $domains->unique();
foreach ($domains as $domain) { foreach ($domains as $domain) {
if (!validate_dns_entry($domain, $this->application->destination->server)) { 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(','); $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')) { if (data_get($this->application, 'dockerfile')) {
$port = get_port_from_dockerfile($this->application->dockerfile); $port = get_port_from_dockerfile($this->application->dockerfile);
if ($port && !$this->application->ports_exposes) { if ($port && !$this->application->ports_exposes) {

View File

@@ -46,26 +46,6 @@ class Heading extends Component
$this->deploy(force_rebuild: true); $this->deploy(force_rebuild: true);
} }
public function deployNew()
{
if ($this->application->build_pack === 'dockercompose' && is_null($this->application->docker_compose_raw)) {
$this->dispatch('error', 'Please load a Compose file first.');
return;
}
$this->setDeploymentUuid();
queue_application_deployment(
application: $this->application,
deployment_uuid: $this->deploymentUuid,
force_rebuild: false,
is_new_deployment: true,
);
return redirect()->route('project.application.deployment.show', [
'project_uuid' => $this->parameters['project_uuid'],
'application_uuid' => $this->parameters['application_uuid'],
'deployment_uuid' => $this->deploymentUuid,
'environment_name' => $this->parameters['environment_name'],
]);
}
public function deploy(bool $force_rebuild = false) public function deploy(bool $force_rebuild = false)
{ {
if ($this->application->build_pack === 'dockercompose' && is_null($this->application->docker_compose_raw)) { if ($this->application->build_pack === 'dockercompose' && is_null($this->application->docker_compose_raw)) {

View File

@@ -22,12 +22,12 @@ class CloneMe extends Component
public ?int $selectedDestination = null; public ?int $selectedDestination = null;
public ?Server $server = null; public ?Server $server = null;
public $resources = []; public $resources = [];
public string $newProjectName = ''; public string $newName = '';
protected $messages = [ protected $messages = [
'selectedServer' => 'Please select a server.', 'selectedServer' => 'Please select a server.',
'selectedDestination' => 'Please select a server & destination.', 'selectedDestination' => 'Please select a server & destination.',
'newProjectName' => 'Please enter a name for the new project.', 'newName' => 'Please enter a name for the new project or environment.',
]; ];
public function mount($project_uuid) public function mount($project_uuid)
{ {
@@ -36,7 +36,7 @@ class CloneMe extends Component
$this->environment = $this->project->environments->where('name', $this->environment_name)->first(); $this->environment = $this->project->environments->where('name', $this->environment_name)->first();
$this->project_id = $this->project->id; $this->project_id = $this->project->id;
$this->servers = currentTeam()->servers; $this->servers = currentTeam()->servers;
$this->newProjectName = str($this->project->name . '-clone-' . (string)new Cuid2(7))->slug(); $this->newName = str($this->project->name . '-clone-' . (string)new Cuid2(7))->slug();
} }
public function render() public function render()
@@ -46,34 +46,50 @@ class CloneMe extends Component
public function selectServer($server_id, $destination_id) public function selectServer($server_id, $destination_id)
{ {
if ($server_id == $this->selectedServer && $destination_id == $this->selectedDestination) {
$this->selectedServer = null;
$this->selectedDestination = null;
$this->server = null;
return;
}
$this->selectedServer = $server_id; $this->selectedServer = $server_id;
$this->selectedDestination = $destination_id; $this->selectedDestination = $destination_id;
$this->server = $this->servers->where('id', $server_id)->first(); $this->server = $this->servers->where('id', $server_id)->first();
} }
public function clone() public function clone(string $type)
{ {
try { try {
$this->validate([ $this->validate([
'selectedDestination' => 'required', 'selectedDestination' => 'required',
'newProjectName' => 'required', 'newName' => 'required',
]); ]);
$foundProject = Project::where('name', $this->newProjectName)->first(); if ($type === 'project') {
if ($foundProject) { $foundProject = Project::where('name', $this->newName)->first();
throw new \Exception('Project with the same name already exists.'); if ($foundProject) {
} throw new \Exception('Project with the same name already exists.');
$newProject = Project::create([ }
'name' => $this->newProjectName, $project = Project::create([
'team_id' => currentTeam()->id, 'name' => $this->newName,
'description' => $this->project->description . ' (clone)', 'team_id' => currentTeam()->id,
]); 'description' => $this->project->description . ' (clone)',
if ($this->environment->name !== 'production') { ]);
$newProject->environments()->create([ if ($this->environment->name !== 'production') {
'name' => $this->environment->name, $project->environments()->create([
'name' => $this->environment->name,
]);
}
$environment = $project->environments->where('name', $this->environment->name)->first();
} else {
$foundEnv = $this->project->environments()->where('name', $this->newName)->first();
if ($foundEnv) {
throw new \Exception('Environment with the same name already exists.');
}
$project = $this->project;
$environment = $this->project->environments()->create([
'name' => $this->newName,
]); ]);
} }
$newEnvironment = $newProject->environments->where('name', $this->environment->name)->first();
// Clone Applications
$applications = $this->environment->applications; $applications = $this->environment->applications;
$databases = $this->environment->databases(); $databases = $this->environment->databases();
$services = $this->environment->services; $services = $this->environment->services;
@@ -83,7 +99,7 @@ class CloneMe extends Component
'uuid' => $uuid, 'uuid' => $uuid,
'fqdn' => generateFqdn($this->server, $uuid), 'fqdn' => generateFqdn($this->server, $uuid),
'status' => 'exited', 'status' => 'exited',
'environment_id' => $newEnvironment->id, 'environment_id' => $environment->id,
// This is not correct, but we need to set it to something // This is not correct, but we need to set it to something
'destination_id' => $this->selectedDestination, 'destination_id' => $this->selectedDestination,
]); ]);
@@ -110,7 +126,7 @@ class CloneMe extends Component
'uuid' => $uuid, 'uuid' => $uuid,
'status' => 'exited', 'status' => 'exited',
'started_at' => null, 'started_at' => null,
'environment_id' => $newEnvironment->id, 'environment_id' => $environment->id,
'destination_id' => $this->selectedDestination, 'destination_id' => $this->selectedDestination,
]); ]);
$newDatabase->save(); $newDatabase->save();
@@ -136,7 +152,7 @@ class CloneMe extends Component
$uuid = (string)new Cuid2(7); $uuid = (string)new Cuid2(7);
$newService = $service->replicate()->fill([ $newService = $service->replicate()->fill([
'uuid' => $uuid, 'uuid' => $uuid,
'environment_id' => $newEnvironment->id, 'environment_id' => $environment->id,
'destination_id' => $this->selectedDestination, 'destination_id' => $this->selectedDestination,
]); ]);
$newService->save(); $newService->save();
@@ -153,8 +169,8 @@ class CloneMe extends Component
$newService->parse(); $newService->parse();
} }
return redirect()->route('project.resource.index', [ return redirect()->route('project.resource.index', [
'project_uuid' => $newProject->uuid, 'project_uuid' => $project->uuid,
'environment_name' => $newEnvironment->name, 'environment_name' => $environment->name,
]); ]);
} catch (\Exception $e) { } catch (\Exception $e) {
return handleError($e, $this); return handleError($e, $this);

View File

@@ -58,19 +58,19 @@ class Heading extends Component
{ {
if ($this->database->type() === 'standalone-postgresql') { if ($this->database->type() === 'standalone-postgresql') {
$activity = StartPostgresql::run($this->database); $activity = StartPostgresql::run($this->database);
$this->dispatch('newMonitorActivity', $activity->id); $this->dispatch('activityMonitor', $activity->id);
} else if ($this->database->type() === 'standalone-redis') { } else if ($this->database->type() === 'standalone-redis') {
$activity = StartRedis::run($this->database); $activity = StartRedis::run($this->database);
$this->dispatch('newMonitorActivity', $activity->id); $this->dispatch('activityMonitor', $activity->id);
} else if ($this->database->type() === 'standalone-mongodb') { } else if ($this->database->type() === 'standalone-mongodb') {
$activity = StartMongodb::run($this->database); $activity = StartMongodb::run($this->database);
$this->dispatch('newMonitorActivity', $activity->id); $this->dispatch('activityMonitor', $activity->id);
} else if ($this->database->type() === 'standalone-mysql') { } else if ($this->database->type() === 'standalone-mysql') {
$activity = StartMysql::run($this->database); $activity = StartMysql::run($this->database);
$this->dispatch('newMonitorActivity', $activity->id); $this->dispatch('activityMonitor', $activity->id);
} else if ($this->database->type() === 'standalone-mariadb') { } else if ($this->database->type() === 'standalone-mariadb') {
$activity = StartMariadb::run($this->database); $activity = StartMariadb::run($this->database);
$this->dispatch('newMonitorActivity', $activity->id); $this->dispatch('activityMonitor', $activity->id);
} }
} }
} }

View File

@@ -129,7 +129,7 @@ class Import extends Component
if (!empty($this->importCommands)) { if (!empty($this->importCommands)) {
$activity = remote_process($this->importCommands, $this->server, ignore_errors: true); $activity = remote_process($this->importCommands, $this->server, ignore_errors: true);
$this->dispatch('newMonitorActivity', $activity->id); $this->dispatch('activityMonitor', $activity->id);
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->validated = false; $this->validated = false;

View File

@@ -9,6 +9,7 @@ class DeleteEnvironment extends Component
{ {
public array $parameters; public array $parameters;
public int $environment_id; public int $environment_id;
public bool $disabled = false;
public function mount() public function mount()
{ {

View File

@@ -9,6 +9,7 @@ class DeleteProject extends Component
{ {
public array $parameters; public array $parameters;
public int $project_id; public int $project_id;
public bool $disabled = false;
public function mount() public function mount()
{ {

View File

@@ -9,6 +9,7 @@ use App\Models\PrivateKey;
use App\Models\Project; use App\Models\Project;
use App\Models\StandaloneDocker; use App\Models\StandaloneDocker;
use App\Models\SwarmDocker; use App\Models\SwarmDocker;
use Illuminate\Support\Collection;
use Livewire\Component; use Livewire\Component;
use Spatie\Url\Url; use Spatie\Url\Url;
use Illuminate\Support\Str; use Illuminate\Support\Str;
@@ -18,7 +19,7 @@ class GithubPrivateRepositoryDeployKey extends Component
public $current_step = 'private_keys'; public $current_step = 'private_keys';
public $parameters; public $parameters;
public $query; public $query;
public $private_keys; public $private_keys =[];
public int $private_key_id; public int $private_key_id;
public int $port = 3000; public int $port = 3000;
@@ -33,6 +34,11 @@ class GithubPrivateRepositoryDeployKey extends Component
public $build_pack = 'nixpacks'; public $build_pack = 'nixpacks';
public bool $show_is_static = true; public bool $show_is_static = true;
private object $repository_url_parsed;
private GithubApp|GitlabApp|string $git_source = 'other';
private ?string $git_host = null;
private string $git_repository;
protected $rules = [ protected $rules = [
'repository_url' => 'required', 'repository_url' => 'required',
'branch' => 'required|string', 'branch' => 'required|string',
@@ -49,10 +55,7 @@ class GithubPrivateRepositoryDeployKey extends Component
'publish_directory' => 'Publish directory', 'publish_directory' => 'Publish directory',
'build_pack' => 'Build pack', 'build_pack' => 'Build pack',
]; ];
private object $repository_url_parsed;
private GithubApp|GitlabApp|string $git_source = 'other';
private ?string $git_host = null;
private string $git_repository;
public function mount() public function mount()
{ {

View File

@@ -29,7 +29,7 @@ class Index extends Component
} }
$this->project = $project; $this->project = $project;
$this->environment = $environment; $this->environment = $environment;
$this->applications = $environment->applications->sortBy('name'); $this->applications = $environment->applications->load(['tags']);
$this->applications = $this->applications->map(function ($application) { $this->applications = $this->applications->map(function ($application) {
if (data_get($application, 'environment.project.uuid')) { if (data_get($application, 'environment.project.uuid')) {
$application->hrefLink = route('project.application.configuration', [ $application->hrefLink = route('project.application.configuration', [
@@ -40,8 +40,9 @@ class Index extends Component
} }
return $application; return $application;
}); });
$this->postgresqls = $environment->postgresqls->sortBy('name'); ray($this->applications);
$this->postgresqls = $this->postgresqls->map(function ($postgresql) { $this->postgresqls = $environment->postgresqls->load(['tags'])->sortBy('name');
$this->postgresqls = $this->postgresqls->map(function ($postgresql) {
if (data_get($postgresql, 'environment.project.uuid')) { if (data_get($postgresql, 'environment.project.uuid')) {
$postgresql->hrefLink = route('project.database.configuration', [ $postgresql->hrefLink = route('project.database.configuration', [
'project_uuid' => data_get($postgresql, 'environment.project.uuid'), 'project_uuid' => data_get($postgresql, 'environment.project.uuid'),
@@ -51,7 +52,7 @@ class Index extends Component
} }
return $postgresql; return $postgresql;
}); });
$this->redis = $environment->redis->sortBy('name'); $this->redis = $environment->redis->load(['tags'])->sortBy('name');
$this->redis = $this->redis->map(function ($redis) { $this->redis = $this->redis->map(function ($redis) {
if (data_get($redis, 'environment.project.uuid')) { if (data_get($redis, 'environment.project.uuid')) {
$redis->hrefLink = route('project.database.configuration', [ $redis->hrefLink = route('project.database.configuration', [
@@ -62,7 +63,7 @@ class Index extends Component
} }
return $redis; return $redis;
}); });
$this->mongodbs = $environment->mongodbs->sortBy('name'); $this->mongodbs = $environment->mongodbs->load(['tags'])->sortBy('name');
$this->mongodbs = $this->mongodbs->map(function ($mongodb) { $this->mongodbs = $this->mongodbs->map(function ($mongodb) {
if (data_get($mongodb, 'environment.project.uuid')) { if (data_get($mongodb, 'environment.project.uuid')) {
$mongodb->hrefLink = route('project.database.configuration', [ $mongodb->hrefLink = route('project.database.configuration', [
@@ -73,7 +74,7 @@ class Index extends Component
} }
return $mongodb; return $mongodb;
}); });
$this->mysqls = $environment->mysqls->sortBy('name'); $this->mysqls = $environment->mysqls->load(['tags'])->sortBy('name');
$this->mysqls = $this->mysqls->map(function ($mysql) { $this->mysqls = $this->mysqls->map(function ($mysql) {
if (data_get($mysql, 'environment.project.uuid')) { if (data_get($mysql, 'environment.project.uuid')) {
$mysql->hrefLink = route('project.database.configuration', [ $mysql->hrefLink = route('project.database.configuration', [
@@ -84,7 +85,7 @@ class Index extends Component
} }
return $mysql; return $mysql;
}); });
$this->mariadbs = $environment->mariadbs->sortBy('name'); $this->mariadbs = $environment->mariadbs->load(['tags'])->sortBy('name');
$this->mariadbs = $this->mariadbs->map(function ($mariadb) { $this->mariadbs = $this->mariadbs->map(function ($mariadb) {
if (data_get($mariadb, 'environment.project.uuid')) { if (data_get($mariadb, 'environment.project.uuid')) {
$mariadb->hrefLink = route('project.database.configuration', [ $mariadb->hrefLink = route('project.database.configuration', [
@@ -95,7 +96,7 @@ class Index extends Component
} }
return $mariadb; return $mariadb;
}); });
$this->services = $environment->services->sortBy('name'); $this->services = $environment->services->load(['tags'])->sortBy('name');
$this->services = $this->services->map(function ($service) { $this->services = $this->services->map(function ($service) {
if (data_get($service, 'environment.project.uuid')) { if (data_get($service, 'environment.project.uuid')) {
$service->hrefLink = route('project.service.configuration', [ $service->hrefLink = route('project.service.configuration', [

View File

@@ -8,7 +8,7 @@ use Livewire\Component;
class Configuration extends Component class Configuration extends Component
{ {
public Service $service; public ?Service $service = null;
public $applications; public $applications;
public $databases; public $databases;
public array $parameters; public array $parameters;

View File

@@ -57,7 +57,7 @@ class Navbar extends Component
} }
$this->service->parse(); $this->service->parse();
$activity = StartService::run($this->service); $activity = StartService::run($this->service);
$this->dispatch('newMonitorActivity', $activity->id); $this->dispatch('activityMonitor', $activity->id);
} }
public function stop(bool $forceCleanup = false) public function stop(bool $forceCleanup = false)
{ {
@@ -82,6 +82,6 @@ class Navbar extends Component
StopService::run($this->service); StopService::run($this->service);
$this->service->parse(); $this->service->parse();
$activity = StartService::run($this->service); $activity = StartService::run($this->service);
$this->dispatch('newMonitorActivity', $activity->id); $this->dispatch('activityMonitor', $activity->id);
} }
} }

View File

@@ -5,7 +5,7 @@ namespace App\Livewire\Project\Service;
use App\Models\ServiceApplication; use App\Models\ServiceApplication;
use Livewire\Component; use Livewire\Component;
class Application extends Component class ServiceApplicationView extends Component
{ {
public ServiceApplication $application; public ServiceApplication $application;
public $parameters; public $parameters;
@@ -20,7 +20,7 @@ class Application extends Component
]; ];
public function render() public function render()
{ {
return view('livewire.project.service.application'); return view('livewire.project.service.service-application-view');
} }
public function instantSave() public function instantSave()
{ {
@@ -53,6 +53,7 @@ class Application extends Component
public function submit() public function submit()
{ {
try { try {
check_fqdn_usage($this->application);
$this->validate(); $this->validate();
$this->application->save(); $this->application->save();
updateCompose($this->application); updateCompose($this->application);

View File

@@ -17,14 +17,15 @@ class Danger extends Component
{ {
$this->modalId = new Cuid2(7); $this->modalId = new Cuid2(7);
$parameters = get_route_parameters(); $parameters = get_route_parameters();
$this->projectUuid = $parameters['project_uuid']; $this->projectUuid = data_get($parameters, 'project_uuid');
$this->environmentName = $parameters['environment_name']; $this->environmentName = data_get($parameters, 'environment_name');
} }
public function delete() public function delete()
{ {
try { try {
DeleteResourceJob::dispatchSync($this->resource); $this->resource->delete();
DeleteResourceJob::dispatch($this->resource);
return redirect()->route('project.resource.index', [ return redirect()->route('project.resource.index', [
'project_uuid' => $this->projectUuid, 'project_uuid' => $this->projectUuid,
'environment_name' => $this->environmentName 'environment_name' => $this->environmentName

View File

@@ -32,6 +32,13 @@ class Add extends Component
public function submit() public function submit()
{ {
$this->validate(); $this->validate();
if (str($this->value)->startsWith('{{') && str($this->value)->endsWith('}}')) {
$type = str($this->value)->after("{{")->before(".")->value;
if (!collect(SHARED_VARIABLE_TYPES)->contains($type)) {
$this->dispatch('error', 'Invalid shared variable type.', "Valid types are: team, project, environment.");
return;
}
}
$this->dispatch('saveKey', [ $this->dispatch('saveKey', [
'key' => $this->key, 'key' => $this->key,
'value' => $this->value, 'value' => $this->value,

View File

@@ -71,12 +71,26 @@ class All extends Component
continue; continue;
} }
$found->value = $variable; $found->value = $variable;
if (str($found->value)->startsWith('{{') && str($found->value)->endsWith('}}')) {
$type = str($found->value)->after("{{")->before(".")->value;
if (!collect(SHARED_VARIABLE_TYPES)->contains($type)) {
$this->dispatch('error', 'Invalid shared variable type.', "Valid types are: team, project, environment.");
return;
}
}
$found->save(); $found->save();
continue; continue;
} else { } else {
$environment = new EnvironmentVariable(); $environment = new EnvironmentVariable();
$environment->key = $key; $environment->key = $key;
$environment->value = $variable; $environment->value = $variable;
if (str($environment->value)->startsWith('{{') && str($environment->value)->endsWith('}}')) {
$type = str($environment->value)->after("{{")->before(".")->value;
if (!collect(SHARED_VARIABLE_TYPES)->contains($type)) {
$this->dispatch('error', 'Invalid shared variable type.', "Valid types are: team, project, environment.");
return;
}
}
$environment->is_build_time = false; $environment->is_build_time = false;
$environment->is_preview = $isPreview ? true : false; $environment->is_preview = $isPreview ? true : false;
switch ($this->resource->type()) { switch ($this->resource->type()) {

View File

@@ -50,7 +50,8 @@ class Show extends Component
$this->isLocked = true; $this->isLocked = true;
} }
} }
public function serialize() { public function serialize()
{
data_forget($this->env, 'real_value'); data_forget($this->env, 'real_value');
if ($this->env->getMorphClass() === 'App\Models\SharedEnvironmentVariable') { if ($this->env->getMorphClass() === 'App\Models\SharedEnvironmentVariable') {
data_forget($this->env, 'is_build_time'); data_forget($this->env, 'is_build_time');
@@ -80,11 +81,18 @@ class Show extends Component
} else { } else {
$this->validate(); $this->validate();
} }
if (str($this->env->value)->startsWith('{{') && str($this->env->value)->endsWith('}}')) {
$type = str($this->env->value)->after("{{")->before(".")->value;
if (!collect(SHARED_VARIABLE_TYPES)->contains($type)) {
$this->dispatch('error', 'Invalid shared variable type.', "Valid types are: team, project, environment.");
return;
}
}
$this->serialize(); $this->serialize();
$this->env->save(); $this->env->save();
$this->dispatch('success', 'Environment variable updated successfully.'); $this->dispatch('success', 'Environment variable updated successfully.');
$this->dispatch('refreshEnvs'); $this->dispatch('refreshEnvs');
} catch(\Exception $e) { } catch (\Exception $e) {
return handleError($e); return handleError($e);
} }
} }

View File

@@ -115,7 +115,7 @@ class ExecuteContainerCommand extends Component
$exec = "docker exec {$this->container} {$cmd}"; $exec = "docker exec {$this->container} {$cmd}";
} }
$activity = remote_process([$exec], $this->server, ignore_errors: true); $activity = remote_process([$exec], $this->server, ignore_errors: true);
$this->dispatch('newMonitorActivity', $activity->id); $this->dispatch('activityMonitor', $activity->id);
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

@@ -20,8 +20,8 @@ class ResourceOperations extends Component
public function mount() public function mount()
{ {
$parameters = get_route_parameters(); $parameters = get_route_parameters();
$this->projectUuid = $parameters['project_uuid']; $this->projectUuid = data_get($parameters, 'project_uuid');
$this->environmentName = $parameters['environment_name']; $this->environmentName = data_get($parameters, 'environment_name');
$this->projects = Project::ownedByCurrentTeam()->get(); $this->projects = Project::ownedByCurrentTeam()->get();
$this->servers = currentTeam()->servers; $this->servers = currentTeam()->servers;
} }
@@ -45,6 +45,11 @@ class ResourceOperations extends Component
'destination_id' => $new_destination->id, 'destination_id' => $new_destination->id,
]); ]);
$new_resource->save(); $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(); $environmentVaribles = $this->resource->environment_variables()->get();
foreach ($environmentVaribles as $environmentVarible) { foreach ($environmentVaribles as $environmentVarible) {
$newEnvironmentVariable = $environmentVarible->replicate()->fill([ $newEnvironmentVariable = $environmentVarible->replicate()->fill([

View File

@@ -0,0 +1,90 @@
<?php
namespace App\Livewire\Project\Shared;
use App\Models\Tag;
use Livewire\Component;
class Tags extends Component
{
public $resource = null;
public ?string $new_tag = null;
public $tags = [];
protected $listeners = [
'refresh' => '$refresh',
];
protected $rules = [
'resource.tags.*.name' => 'required|string|min:2',
'new_tag' => 'required|string|min:2'
];
protected $validationAttributes = [
'new_tag' => 'tag'
];
public function mount()
{
$this->tags = Tag::ownedByCurrentTeam()->get();
}
public function addTag(string $id, string $name)
{
try {
if ($this->resource->tags()->where('id', $id)->exists()) {
$this->dispatch('error', 'Duplicate tags.', "Tag <span class='text-warning'>$name</span> already added.");
return;
}
$this->resource->tags()->syncWithoutDetaching($id);
$this->refresh();
} catch (\Exception $e) {
return handleError($e, $this);
}
}
public function deleteTag(string $id)
{
try {
$this->resource->tags()->detach($id);
$found_more_tags = Tag::where(['id' => $id, 'team_id' => currentTeam()->id])->first();
if ($found_more_tags->applications()->count() == 0 && $found_more_tags->services()->count() == 0){
$found_more_tags->delete();
}
$this->refresh();
} catch (\Exception $e) {
return handleError($e, $this);
}
}
public function refresh()
{
$this->resource->load(['tags']);
$this->tags = Tag::ownedByCurrentTeam()->get();
$this->new_tag = null;
}
public function submit()
{
try {
$this->validate([
'new_tag' => 'required|string|min:2'
]);
$tags = str($this->new_tag)->trim()->explode(' ');
foreach ($tags as $tag) {
if ($this->resource->tags()->where('name', $tag)->exists()) {
$this->dispatch('error', 'Duplicate tags.', "Tag <span class='text-warning'>$tag</span> already added.");
continue;
}
$found = Tag::where(['name' => $tag, 'team_id' => currentTeam()->id])->first();
if (!$found) {
$found = Tag::create([
'name' => $tag,
'team_id' => currentTeam()->id
]);
}
$this->resource->tags()->syncWithoutDetaching($found->id);
}
$this->refresh();
} catch (\Exception $e) {
return handleError($e, $this);
}
}
public function render()
{
return view('livewire.project.shared.tags');
}
}

View File

@@ -31,7 +31,7 @@ class RunCommand extends Component
$this->validate(); $this->validate();
try { try {
$activity = remote_process([$this->command], Server::where('uuid', $this->server)->first(), ignore_errors: true); $activity = remote_process([$this->command], Server::where('uuid', $this->server)->first(), ignore_errors: true);
$this->dispatch('newMonitorActivity', $activity->id); $this->dispatch('activityMonitor', $activity->id);
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

@@ -2,7 +2,6 @@
namespace App\Livewire\Server; namespace App\Livewire\Server;
use App\Actions\Server\InstallDocker;
use App\Models\Server; use App\Models\Server;
use Livewire\Component; use Livewire\Component;
@@ -14,7 +13,8 @@ class Form extends Component
public ?string $wildcard_domain = null; public ?string $wildcard_domain = null;
public int $cleanup_after_percentage; public int $cleanup_after_percentage;
public bool $dockerInstallationStarted = false; public bool $dockerInstallationStarted = false;
protected $listeners = ['serverRefresh'];
protected $listeners = ['serverInstalled'];
protected $rules = [ protected $rules = [
'server.name' => 'required', 'server.name' => 'required',
@@ -49,9 +49,10 @@ class Form extends Component
$this->wildcard_domain = $this->server->settings->wildcard_domain; $this->wildcard_domain = $this->server->settings->wildcard_domain;
$this->cleanup_after_percentage = $this->server->settings->cleanup_after_percentage; $this->cleanup_after_percentage = $this->server->settings->cleanup_after_percentage;
} }
public function serverRefresh($install = true) public function serverInstalled()
{ {
$this->validateServer($install); $this->server->refresh();
$this->server->settings->refresh();
} }
public function instantSave() public function instantSave()
{ {
@@ -64,13 +65,6 @@ class Form extends Component
return handleError($e, $this); return handleError($e, $this);
} }
} }
public function installDocker()
{
$this->dispatch('installDocker');
$this->dockerInstallationStarted = true;
$activity = InstallDocker::run($this->server);
$this->dispatch('newMonitorActivity', $activity->id);
}
public function checkLocalhostConnection() public function checkLocalhostConnection()
{ {
$uptime = $this->server->validateConnection(); $uptime = $this->server->validateConnection();
@@ -80,48 +74,13 @@ class Form extends Component
$this->server->settings->is_usable = true; $this->server->settings->is_usable = true;
$this->server->settings->save(); $this->server->settings->save();
} else { } else {
$this->dispatch('error', 'Server is not reachable.<br>Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/server/openssh">documentation</a> for further help.'); $this->dispatch('error', 'Server is not reachable.', 'Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/server/openssh">documentation</a> for further help.');
return; return;
} }
} }
public function validateServer($install = true) public function validateServer($install = true)
{ {
try { $this->dispatch('validateServer', $install);
$uptime = $this->server->validateConnection();
if (!$uptime) {
$install && $this->dispatch('error', 'Server is not reachable.<br>Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/server/openssh">documentation</a> for further help.');
return;
}
$supported_os_type = $this->server->validateOS();
if (!$supported_os_type) {
$install && $this->dispatch('error', 'Server OS type is not supported for automated installation. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://docs.docker.com/engine/install/#server">documentation</a>.');
return;
}
$dockerInstalled = $this->server->validateDockerEngine();
if ($dockerInstalled) {
$install && $this->dispatch('success', 'Docker Engine is installed.<br> Checking version.');
} else {
$install && $this->installDocker();
return;
}
$dockerVersion = $this->server->validateDockerEngineVersion();
if ($dockerVersion) {
$install && $this->dispatch('success', 'Docker Engine version is 22+.');
} else {
$install && $this->installDocker();
return;
}
if ($this->server->isSwarm()) {
$swarmInstalled = $this->server->validateDockerSwarm();
if ($swarmInstalled) {
$install && $this->dispatch('success', 'Docker Swarm is initiated.');
}
}
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {
$this->dispatch('proxyStatusUpdated');
}
} }
public function submit() public function submit()

View File

@@ -71,7 +71,7 @@ class Deploy extends Component
{ {
try { try {
$activity = StartProxy::run($this->server); $activity = StartProxy::run($this->server);
$this->dispatch('newMonitorActivity', $activity->id, ProxyStatusChanged::class); $this->dispatch('activityMonitor', $activity->id, ProxyStatusChanged::class);
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

@@ -12,10 +12,8 @@ class Status extends Component
public Server $server; public Server $server;
public bool $polling = false; public bool $polling = false;
public int $numberOfPolls = 0; public int $numberOfPolls = 0;
protected $listeners = ['proxyStatusUpdated', 'startProxyPolling']; protected $listeners = ['proxyStatusUpdated', 'startProxyPolling'];
public function mount() {
}
public function startProxyPolling() public function startProxyPolling()
{ {
$this->checkProxy(); $this->checkProxy();

View File

@@ -11,6 +11,7 @@ class Show extends Component
use AuthorizesRequests; use AuthorizesRequests;
public ?Server $server = null; public ?Server $server = null;
public $parameters = []; public $parameters = [];
protected $listeners = ['serverInstalled' => '$refresh'];
public function mount() public function mount()
{ {
$this->parameters = get_route_parameters(); $this->parameters = get_route_parameters();
@@ -19,14 +20,13 @@ class Show extends Component
if (is_null($this->server)) { if (is_null($this->server)) {
return redirect()->route('server.index'); return redirect()->route('server.index');
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }
} }
public function submit() public function submit()
{ {
$this->dispatch('serverRefresh',false); $this->dispatch('serverRefresh', false);
} }
public function render() public function render()
{ {

View File

@@ -39,7 +39,7 @@ class ShowPrivateKey extends Component
if ($uptime) { if ($uptime) {
$this->dispatch('success', 'Server is reachable.'); $this->dispatch('success', 'Server is reachable.');
} else { } else {
$this->dispatch('error', 'Server is not reachable.<br>Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/configuration#openssh-server">documentation</a> for further help.'); $this->dispatch('error', 'Server is not reachable.<br>Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/server/openssh#openssh">documentation</a> for further help.');
return; return;
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {

View File

@@ -0,0 +1,105 @@
<?php
namespace App\Livewire\Server;
use App\Models\Server;
use Livewire\Component;
class ValidateAndInstall extends Component
{
public Server $server;
public int $number_of_tries = 0;
public int $max_tries = 1;
public bool $install = true;
public $uptime = null;
public $supported_os_type = null;
public $docker_installed = null;
public $docker_version = null;
public $error = null;
protected $listeners = ['validateServer' => 'init', 'validateDockerEngine', 'validateServerNow' => 'validateServer'];
public function init(bool $install = true)
{
$this->install = $install;
$this->uptime = null;
$this->supported_os_type = null;
$this->docker_installed = null;
$this->docker_version = null;
$this->error = null;
$this->number_of_tries = 0;
$this->dispatch('validateServerNow');
}
public function validateServer()
{
try {
$this->validateConnection();
$this->validateOS();
$this->validateDockerEngine();
if ($this->server->isSwarm()) {
$swarmInstalled = $this->server->validateDockerSwarm();
if ($swarmInstalled) {
$this->dispatch('success', 'Docker Swarm is initiated.');
}
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function validateConnection()
{
$this->uptime = $this->server->validateConnection();
if (!$this->uptime) {
$this->error = 'Server is not reachable. Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/server/openssh">documentation</a> for further help.';
return;
}
}
public function validateOS()
{
$this->supported_os_type = $this->server->validateOS();
if (!$this->supported_os_type) {
$this->error = 'Server OS type is not supported. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://docs.docker.com/engine/install/#server">documentation</a>.';
return;
}
}
public function validateDockerEngine()
{
$this->docker_installed = $this->server->validateDockerEngine();
if (!$this->docker_installed) {
if ($this->install) {
ray($this->number_of_tries, $this->max_tries);
if ($this->number_of_tries == $this->max_tries) {
$this->error = 'Docker Engine could not be installed. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://docs.docker.com/engine/install/#server">documentation</a>.';
return;
} else {
$activity = $this->server->installDocker();
$this->number_of_tries++;
$this->dispatch('newActivityMonitor', $activity->id, 'validateDockerEngine');
return;
}
} else {
$this->error = 'Docker Engine is not installed. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://docs.docker.com/engine/install/#server">documentation</a>.';
return;
}
} else {
$this->validateDockerVersion();
}
}
public function validateDockerVersion()
{
$this->docker_version = $this->server->validateDockerEngineVersion();
if ($this->docker_version) {
$this->dispatch('serverInstalled');
$this->dispatch('success', 'Server validated successfully.');
} else {
$this->error = 'Docker Engine version is not 22+. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://docs.docker.com/engine/install/#server">documentation</a>.';
return;
}
}
public function render()
{
return view('livewire.server.validate-and-install');
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace App\Livewire\Tags;
use App\Models\Tag;
use Livewire\Component;
class Index extends Component
{
public $tags = [];
public function mount() {
$this->tags = Tag::where('team_id', currentTeam()->id)->get()->unique('name')->sortBy('name');
}
public function render()
{
return view('livewire.tags.index');
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace App\Livewire\Tags;
use App\Http\Controllers\Api\Deploy;
use App\Models\ApplicationDeploymentQueue;
use App\Models\Tag;
use Livewire\Component;
class Show extends Component
{
public $tags;
public Tag $tag;
public $applications;
public $services;
public $webhook = null;
public $deployments_per_tag_per_server = [];
public function mount()
{
$this->tags = Tag::ownedByCurrentTeam()->get()->unique('name')->sortBy('name');
$tag = $this->tags->where('name', request()->tag_name)->first();
if (!$tag) {
return redirect()->route('tags.index');
}
$this->webhook = generatTagDeployWebhook($tag->name);
$this->applications = $tag->applications()->get();
$this->services = $tag->services()->get();
$this->tag = $tag;
$this->get_deployments();
}
public function get_deployments()
{
try {
$resource_ids = $this->applications->pluck('id');
$this->deployments_per_tag_per_server = ApplicationDeploymentQueue::whereIn("status", ["in_progress", "queued"])->whereIn('application_id', $resource_ids)->get([
"id",
"application_id",
"application_name",
"deployment_url",
"pull_request_id",
"server_name",
"server_id",
"status"
])->sortBy('id')->groupBy('server_name')->toArray();
} catch (\Exception $e) {
return handleError($e, $this);
}
}
public function redeploy_all()
{
try {
$this->applications->each(function ($resource) {
$deploy = new Deploy();
$deploy->deploy_resource($resource);
});
$this->services->each(function ($resource) {
$deploy = new Deploy();
$deploy->deploy_resource($resource);
});
$this->dispatch('success', 'Mass deployment started.');
} catch (\Exception $e) {
return handleError($e, $this);
}
}
public function render()
{
return view('livewire.tags.show');
}
}

View File

@@ -12,7 +12,11 @@ class Invitations extends Component
public function deleteInvitation(int $invitation_id) public function deleteInvitation(int $invitation_id)
{ {
TeamInvitation::find($invitation_id)->delete(); $initiation_found = TeamInvitation::find($invitation_id);
if (!$initiation_found) {
return $this->dispatch('error', 'Invitation not found.');
}
$initiation_found->delete();
$this->refreshInvitations(); $this->refreshInvitations();
$this->dispatch('success', 'Invitation revoked.'); $this->dispatch('success', 'Invitation revoked.');
} }

View File

@@ -67,7 +67,8 @@ class Create extends Component
$this->storage->save(); $this->storage->save();
return redirect()->route('team.storage.show', $this->storage->uuid); return redirect()->route('team.storage.show', $this->storage->uuid);
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); $this->dispatch('error', 'Failed to create storage.', $e->getMessage());
// return handleError($e, $this);
} }
} }
} }

View File

@@ -33,9 +33,9 @@ class Form extends Component
{ {
try { try {
$this->storage->testConnection(shouldSave: true); $this->storage->testConnection(shouldSave: true);
return $this->dispatch('success', 'Connection is working. Tested with "ListObjectsV2" action.'); return $this->dispatch('success', 'Connection is working.', 'Tested with "ListObjectsV2" action.');
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); $this->dispatch('error', 'Failed to create storage.', $e->getMessage());
} }
} }

View File

@@ -6,11 +6,9 @@ use App\Enums\ApplicationDeploymentStatus;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Spatie\Activitylog\Models\Activity; use Spatie\Activitylog\Models\Activity;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use RuntimeException; use RuntimeException;
use Symfony\Component\Yaml\Yaml;
use Visus\Cuid2\Cuid2; use Visus\Cuid2\Cuid2;
class Application extends BaseModel class Application extends BaseModel
@@ -39,6 +37,7 @@ class Application extends BaseModel
]); ]);
}); });
static::deleting(function ($application) { static::deleting(function ($application) {
$application->update(['fqdn' => null]);
$application->settings()->delete(); $application->settings()->delete();
$storages = $application->persistentStorages()->get(); $storages = $application->persistentStorages()->get();
$server = data_get($application, 'destination.server'); $server = data_get($application, 'destination.server');
@@ -50,66 +49,9 @@ class Application extends BaseModel
$application->persistentStorages()->delete(); $application->persistentStorages()->delete();
$application->environment_variables()->delete(); $application->environment_variables()->delete();
$application->environment_variables_preview()->delete(); $application->environment_variables_preview()->delete();
$application->tags()->detach();
}); });
} }
// Build packs / deployment types
public function servers(): Collection
{
$mainServer = data_get($this, 'destination.server');
$additionalDestinations = data_get($this, 'additional_destinations', null);
$additionalServers = collect([]);
if ($this->isMultipleServerDeployment()) {
ray('asd');
if (str($additionalDestinations)->isNotEmpty()) {
$additionalDestinations = str($additionalDestinations)->explode(',');
foreach ($additionalDestinations as $destinationId) {
$destination = StandaloneDocker::find($destinationId)->whereNot('id', $mainServer->id)->first();
$server = data_get($destination, 'server');
$additionalServers->push($server);
}
}
}
return collect([$mainServer])->merge($additionalServers);
}
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 is_github_based(): bool public function is_github_based(): bool
{ {
@@ -270,6 +212,13 @@ class Application extends BaseModel
: explode(',', $this->ports_exposes) : explode(',', $this->ports_exposes)
); );
} }
public function tags()
{
return $this->morphToMany(Tag::class, 'taggable');
}
public function project() {
return data_get($this, 'environment.project');
}
public function team() public function team()
{ {
return data_get($this, 'environment.project.team'); return data_get($this, 'environment.project.team');
@@ -465,31 +414,6 @@ class Application extends BaseModel
return true; 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() function customRepository()
{ {
preg_match('/(?<=:)\d+(?=\/)/', $this->git_repository, $matches); preg_match('/(?<=:)\d+(?=\/)/', $this->git_repository, $matches);
@@ -511,296 +435,7 @@ class Application extends BaseModel
{ {
return "/artifacts/{$uuid}"; return "/artifacts/{$uuid}";
} }
function generateHealthCheckCommands() function setGitImportSettings(string $deployment_uuid, string $git_clone_command)
{
if ($this->dockerfile || $this->build_pack === 'dockerfile' || $this->build_pack === 'dockerimage') {
// TODO: disabled HC because there are several ways to hc a simple docker image, hard to figure out a good way. Like some docker images (pocketbase) does not have curl.
return 'exit 0';
}
if (!$this->health_check_port) {
$health_check_port = $this->ports_exposes_array[0];
} else {
$health_check_port = $this->health_check_port;
}
if ($this->health_check_path) {
$this->full_healthcheck_url = "{$this->health_check_method}: {$this->health_check_scheme}://{$this->health_check_host}:{$health_check_port}{$this->health_check_path}";
$generated_healthchecks_commands = [
"curl -s -X {$this->health_check_method} -f {$this->health_check_scheme}://{$this->health_check_host}:{$health_check_port}{$this->health_check_path} > /dev/null"
];
} else {
$this->full_healthcheck_url = "{$this->health_check_method}: {$this->health_check_scheme}://{$this->health_check_host}:{$health_check_port}/";
$generated_healthchecks_commands = [
"curl -s -X {$this->health_check_method} -f {$this->health_check_scheme}://{$this->health_check_host}:{$health_check_port}/"
];
}
return implode(' ', $generated_healthchecks_commands);
}
function generateLocalPersistentVolumes(int $pullRequestId)
{
$persistentStorages = [];
$volumeNames = [];
foreach ($this->persistentStorages as $persistentStorage) {
$volume_name = $persistentStorage->host_path ?? $persistentStorage->name;
if ($pullRequestId !== 0) {
$volume_name = $volume_name . '-pr-' . $pullRequestId;
}
$persistentStorages[] = $volume_name . ':' . $persistentStorage->mount_path;
if ($persistentStorage->host_path) {
continue;
}
$name = $persistentStorage->name;
if ($pullRequestId !== 0) {
$name = $name . '-pr-' . $pullRequestId;
}
$volumeNames[$name] = [
'name' => $name,
'external' => false,
];
}
return [
'persistentStorages' => $persistentStorages,
'volumeNames' => $volumeNames,
];
}
public function generateEnvironmentVariables($ports)
{
$environmentVariables = collect();
// ray('Generate Environment Variables')->green();
if ($this->pull_request_id === 0) {
// ray($this->runtime_environment_variables)->green();
foreach ($this->runtime_environment_variables as $env) {
$environmentVariables->push("$env->key=$env->value");
}
foreach ($this->nixpacks_environment_variables as $env) {
$environmentVariables->push("$env->key=$env->value");
}
} else {
// ray($this->runtime_environment_variables_preview)->green();
foreach ($this->runtime_environment_variables_preview as $env) {
$environmentVariables->push("$env->key=$env->value");
}
foreach ($this->nixpacks_environment_variables_preview as $env) {
$environmentVariables->push("$env->key=$env->value");
}
}
// Add PORT if not exists, use the first port as default
if ($environmentVariables->filter(fn ($env) => Str::of($env)->contains('PORT'))->isEmpty()) {
$environmentVariables->push("PORT={$ports[0]}");
}
return $environmentVariables->all();
}
function generateDockerComposeFile(Server $server, ApplicationDeploymentQueue $deployment, string $workdir)
{
$pullRequestId = $deployment->pull_request_id;
$ports = $this->settings->is_static ? [80] : $this->ports_exposes_array;
$container_name = generateApplicationContainerName($this, $this->pull_request_id);
$commit = str($deployment->getOutput('git_commit_sha'))->before("\t");
[
'productionImageName' => $productionImageName
] = $this->generateImageNames($commit, $pullRequestId);
[
'persistentStorages' => $persistentStorages,
'volumeNames' => $volumeNames
] = $this->generateLocalPersistentVolumes($pullRequestId);
$environmentVariables = $this->generateEnvironmentVariables($ports);
if (data_get($this, 'custom_labels')) {
$labels = collect(str($this->custom_labels)->explode(','));
$labels = $labels->filter(function ($value, $key) {
return !Str::startsWith($value, 'coolify.');
});
$this->custom_labels = $labels->implode(',');
$this->save();
} else {
$labels = collect(generateLabelsApplication($this, $this->preview));
}
if ($this->pull_request_id !== 0) {
$labels = collect(generateLabelsApplication($this, $this->preview));
}
$labels = $labels->merge(defaultLabels($this->id, $this->uuid, $this->pull_request_id))->toArray();
$docker_compose = [
'version' => '3.8',
'services' => [
$container_name => [
'image' => $productionImageName,
'container_name' => $container_name,
'restart' => RESTART_MODE,
'environment' => $environmentVariables,
'expose' => $ports,
'networks' => [
$this->destination->network,
],
'healthcheck' => [
'test' => [
'CMD-SHELL',
$this->generateHealthCheckCommands()
],
'interval' => $this->health_check_interval . 's',
'timeout' => $this->health_check_timeout . 's',
'retries' => $this->health_check_retries,
'start_period' => $this->health_check_start_period . 's'
],
'mem_limit' => $this->limits_memory,
'memswap_limit' => $this->limits_memory_swap,
'mem_swappiness' => $this->limits_memory_swappiness,
'mem_reservation' => $this->limits_memory_reservation,
'cpus' => (float) $this->limits_cpus,
'cpu_shares' => $this->limits_cpu_shares,
]
],
'networks' => [
$this->destination->network => [
'external' => true,
'name' => $this->destination->network,
'attachable' => true
]
]
];
if (!is_null($this->limits_cpuset)) {
data_set($docker_compose, "services.{$container_name}.cpuset", $this->limits_cpuset);
}
if ($server->isSwarm()) {
data_forget($docker_compose, 'services.' . $container_name . '.container_name');
data_forget($docker_compose, 'services.' . $container_name . '.expose');
data_forget($docker_compose, 'services.' . $container_name . '.restart');
data_forget($docker_compose, 'services.' . $container_name . '.mem_limit');
data_forget($docker_compose, 'services.' . $container_name . '.memswap_limit');
data_forget($docker_compose, 'services.' . $container_name . '.mem_swappiness');
data_forget($docker_compose, 'services.' . $container_name . '.mem_reservation');
data_forget($docker_compose, 'services.' . $container_name . '.cpus');
data_forget($docker_compose, 'services.' . $container_name . '.cpuset');
data_forget($docker_compose, 'services.' . $container_name . '.cpu_shares');
$docker_compose['services'][$container_name]['deploy'] = [
'placement' => [
'constraints' => [
'node.role == worker'
]
],
'mode' => 'replicated',
'replicas' => 1,
'update_config' => [
'order' => 'start-first'
],
'rollback_config' => [
'order' => 'start-first'
],
'labels' => $labels,
'resources' => [
'limits' => [
'cpus' => $this->limits_cpus,
'memory' => $this->limits_memory,
],
'reservations' => [
'cpus' => $this->limits_cpus,
'memory' => $this->limits_memory,
]
]
];
} else {
$docker_compose['services'][$container_name]['labels'] = $labels;
}
if ($server->isLogDrainEnabled() && $this->isLogDrainEnabled()) {
$docker_compose['services'][$container_name]['logging'] = [
'driver' => 'fluentd',
'options' => [
'fluentd-address' => "tcp://127.0.0.1:24224",
'fluentd-async' => "true",
'fluentd-sub-second-precision' => "true",
]
];
}
if ($this->settings->is_gpu_enabled) {
$docker_compose['services'][$container_name]['deploy']['resources']['reservations']['devices'] = [
[
'driver' => data_get($this, 'settings.gpu_driver', 'nvidia'),
'capabilities' => ['gpu'],
'options' => data_get($this, 'settings.gpu_options', [])
]
];
if (data_get($this, 'settings.gpu_count')) {
$count = data_get($this, 'settings.gpu_count');
if ($count === 'all') {
$docker_compose['services'][$container_name]['deploy']['resources']['reservations']['devices'][0]['count'] = $count;
} else {
$docker_compose['services'][$container_name]['deploy']['resources']['reservations']['devices'][0]['count'] = (int) $count;
}
} else if (data_get($this, 'settings.gpu_device_ids')) {
$docker_compose['services'][$container_name]['deploy']['resources']['reservations']['devices'][0]['ids'] = data_get($this, 'settings.gpu_device_ids');
}
}
if ($this->isHealthcheckDisabled()) {
data_forget($docker_compose, 'services.' . $container_name . '.healthcheck');
}
if (count($this->ports_mappings_array) > 0 && $this->pull_request_id === 0) {
$docker_compose['services'][$container_name]['ports'] = $this->ports_mappings_array;
}
if (count($persistentStorages) > 0) {
$docker_compose['services'][$container_name]['volumes'] = $persistentStorages;
}
if (count($volumeNames) > 0) {
$docker_compose['volumes'] = $volumeNames;
}
$docker_compose['services'][$this->uuid] = $docker_compose['services'][$container_name];
data_forget($docker_compose, 'services.' . $container_name);
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$server->executeRemoteCommand(
commands: collect([])->push([
'command' => executeInDocker($deployment->deployment_uuid, "echo '{$docker_compose_base64}' | base64 -d > {$workdir}/docker-compose.yml"),
'hidden' => true,
'ignoreErrors' => true
]),
loggingModel: $deployment
);
}
function rollingUpdateApplication(Server $server, ApplicationDeploymentQueue $deployment, string $workdir)
{
$pullRequestId = $deployment->pull_request_id;
$containerName = generateApplicationContainerName($this, $pullRequestId);
// if (count($this->ports_mappings_array) > 0) {
// $deployment->addLogEntry('Application has ports mapped to the host system, rolling update is not supported.');
$containers = getCurrentApplicationContainerStatus($server, $this->id, $pullRequestId);
// if ($pullRequestId === 0) {
// $containers = $containers->filter(function ($container) use ($containerName) {
// return data_get($container, 'Names') !== $containerName;
// });
// }
$containers->each(function ($container) use ($server, $deployment) {
$removingContainerName = data_get($container, 'Names');
$server->executeRemoteCommand(
commands: collect([])->push([
'command' => "docker rm -f $removingContainerName",
'hidden' => true,
'ignoreErrors' => true
]),
loggingModel: $deployment
);
});
// }
$server->executeRemoteCommand(
commands: collect([])->push([
'command' => executeInDocker($deployment->deployment_uuid, "docker compose --project-directory {$workdir} up --build -d"),
'hidden' => true,
'ignoreErrors' => true
]),
loggingModel: $deployment
);
$deployment->addLogEntry("New container started.");
}
function setGitImportSettings(string $deployment_uuid, string $git_clone_command)
{ {
$baseDir = $this->generateBaseDir($deployment_uuid); $baseDir = $this->generateBaseDir($deployment_uuid);
if ($this->git_commit_sha !== 'HEAD') { if ($this->git_commit_sha !== 'HEAD') {
@@ -905,8 +540,7 @@ class Application extends BaseModel
$commands->push("echo 'Checking out $branch'"); $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"; $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') {
if ($git_type === 'github') {
$branch = "pull/{$pull_request_id}/head:$pr_branch_name"; $branch = "pull/{$pull_request_id}/head:$pr_branch_name";
if ($exec_in_docker) { if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'")); $commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
@@ -914,6 +548,13 @@ class Application extends BaseModel
$commands->push("echo 'Checking out $branch'"); $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"; $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";
} }
} }
@@ -972,34 +613,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) function parseCompose(int $pull_request_id = 0)
{ {
if ($this->docker_compose_raw) { if ($this->docker_compose_raw) {
@@ -1110,4 +723,12 @@ class Application extends BaseModel
$this->save(); $this->save();
return $customLabels; return $customLabels;
} }
public function fqdns(): Attribute
{
return Attribute::make(
get: fn () => is_null($this->fqdn)
? []
: explode(',', $this->fqdn),
);
}
} }

View File

@@ -91,7 +91,7 @@ class EnvironmentVariable extends Model
} }
private function get_real_environment_variables(?string $environment_variable = null, $resource = null): string|null private function get_real_environment_variables(?string $environment_variable = null, $resource = null): string|null
{ {
if (!$environment_variable) { if (!$environment_variable || !$resource) {
return null; return null;
} }
$environment_variable = trim($environment_variable); $environment_variable = trim($environment_variable);
@@ -100,6 +100,9 @@ class EnvironmentVariable extends Model
$variable = Str::after($environment_variable, "{$type}."); $variable = Str::after($environment_variable, "{$type}.");
$variable = Str::before($variable, '}}'); $variable = Str::before($variable, '}}');
$variable = Str::of($variable)->trim()->value; $variable = Str::of($variable)->trim()->value;
if (!collect(SHARED_VARIABLE_TYPES)->contains($type)) {
return $variable;
}
if ($type === 'environment') { if ($type === 'environment') {
$id = $resource->environment->id; $id = $resource->environment->id;
} else if ($type === 'project') { } else if ($type === 'project') {

View File

@@ -64,10 +64,13 @@ class Project extends BaseModel
} }
public function mysqls() public function mysqls()
{ {
return $this->hasMany(StandaloneMysql::class, Environment::class); return $this->hasManyThrough(StandaloneMysql::class, Environment::class);
} }
public function mariadbs() public function mariadbs()
{ {
return $this->hasMany(StandaloneMariadb::class, Environment::class); return $this->hasManyThrough(StandaloneMariadb::class, Environment::class);
}
public function resource_count() {
return $this->applications()->count() + $this->postgresqls()->count() + $this->redis()->count() + $this->mongodbs()->count() + $this->mysqls()->count() + $this->mariadbs()->count();
} }
} }

View File

@@ -2,16 +2,13 @@
namespace App\Models; namespace App\Models;
use App\Enums\ApplicationDeploymentStatus; use App\Actions\Server\InstallDocker;
use App\Enums\ProxyStatus; use App\Enums\ProxyStatus;
use App\Enums\ProxyTypes; use App\Enums\ProxyTypes;
use App\Notifications\Server\Revived; use App\Notifications\Server\Revived;
use App\Notifications\Server\Unreachable; use App\Notifications\Server\Unreachable;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute; 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\Casts\SchemalessAttributes;
use Spatie\SchemalessAttributes\SchemalessAttributesTrait; use Spatie\SchemalessAttributes\SchemalessAttributesTrait;
use Illuminate\Support\Str; use Illuminate\Support\Str;
@@ -335,20 +332,6 @@ class Server extends BaseModel
if ($this->proxyType() === ProxyTypes::NONE->value || $this->settings->is_build_server) { if ($this->proxyType() === ProxyTypes::NONE->value || $this->settings->is_build_server) {
return false; 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; return true;
} }
public function isFunctional() public function isFunctional()
@@ -429,6 +412,11 @@ class Server extends BaseModel
return true; return true;
} }
public function installDocker()
{
$activity = InstallDocker::run($this);
return $activity;
}
public function validateDockerEngine($throwError = false) public function validateDockerEngine($throwError = false)
{ {
$dockerBinary = instant_remote_process(["command -v docker"], $this, false); $dockerBinary = instant_remote_process(["command -v docker"], $this, false);
@@ -483,153 +471,4 @@ class Server extends BaseModel
return instant_remote_process(["docker network create coolify --attachable >/dev/null 2>&1 || true"], $this, false); return instant_remote_process(["docker network create coolify --attachable >/dev/null 2>&1 || true"], $this, false);
} }
} }
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; namespace App\Models;
use App\Actions\Service\DeleteService;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Symfony\Component\Yaml\Yaml;
class Service extends BaseModel class Service extends BaseModel
{ {
@@ -18,10 +16,18 @@ class Service extends BaseModel
{ {
return 'service'; return 'service';
} }
public function project()
{
return data_get($this, 'environment.project');
}
public function team() public function team()
{ {
return data_get($this, 'environment.project.team'); return data_get($this, 'environment.project.team');
} }
public function tags()
{
return $this->morphToMany(Tag::class, 'taggable');
}
public function extraFields() public function extraFields()
{ {
$fields = collect([]); $fields = collect([]);

View File

@@ -5,7 +5,6 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Cache;
class ServiceApplication extends BaseModel class ServiceApplication extends BaseModel
{ {
@@ -15,6 +14,7 @@ class ServiceApplication extends BaseModel
protected static function booted() protected static function booted()
{ {
static::deleting(function ($service) { static::deleting(function ($service) {
$service->update(['fqdn' => null]);
$service->persistentStorages()->delete(); $service->persistentStorages()->delete();
$service->fileStorages()->delete(); $service->fileStorages()->delete();
}); });
@@ -55,7 +55,6 @@ class ServiceApplication extends BaseModel
get: fn () => is_null($this->fqdn) get: fn () => is_null($this->fqdn)
? [] ? []
: explode(',', $this->fqdn), : explode(',', $this->fqdn),
); );
} }
public function getFilesFromServer(bool $isInit = false) public function getFilesFromServer(bool $isInit = false)

View File

@@ -40,8 +40,14 @@ class StandaloneMariadb extends BaseModel
$database->scheduledBackups()->delete(); $database->scheduledBackups()->delete();
$database->persistentStorages()->delete(); $database->persistentStorages()->delete();
$database->environment_variables()->delete(); $database->environment_variables()->delete();
$database->tags()->detach();
}); });
} }
public function tags()
{
return $this->morphToMany(Tag::class, 'taggable');
}
public function team() public function team()
{ {
return data_get($this, 'environment.project.team'); return data_get($this, 'environment.project.team');

View File

@@ -43,8 +43,14 @@ class StandaloneMongodb extends BaseModel
$database->scheduledBackups()->delete(); $database->scheduledBackups()->delete();
$database->persistentStorages()->delete(); $database->persistentStorages()->delete();
$database->environment_variables()->delete(); $database->environment_variables()->delete();
$database->tags()->detach();
}); });
} }
public function tags()
{
return $this->morphToMany(Tag::class, 'taggable');
}
public function team() public function team()
{ {
return data_get($this, 'environment.project.team'); return data_get($this, 'environment.project.team');

View File

@@ -40,8 +40,14 @@ class StandaloneMysql extends BaseModel
$database->scheduledBackups()->delete(); $database->scheduledBackups()->delete();
$database->persistentStorages()->delete(); $database->persistentStorages()->delete();
$database->environment_variables()->delete(); $database->environment_variables()->delete();
$database->tags()->detach();
}); });
} }
public function tags()
{
return $this->morphToMany(Tag::class, 'taggable');
}
public function team() public function team()
{ {
return data_get($this, 'environment.project.team'); return data_get($this, 'environment.project.team');

View File

@@ -40,8 +40,14 @@ class StandalonePostgresql extends BaseModel
$database->scheduledBackups()->delete(); $database->scheduledBackups()->delete();
$database->persistentStorages()->delete(); $database->persistentStorages()->delete();
$database->environment_variables()->delete(); $database->environment_variables()->delete();
$database->tags()->detach();
}); });
} }
public function tags()
{
return $this->morphToMany(Tag::class, 'taggable');
}
public function link() public function link()
{ {
if (data_get($this, 'environment.project.uuid')) { if (data_get($this, 'environment.project.uuid')) {

View File

@@ -35,8 +35,14 @@ class StandaloneRedis extends BaseModel
} }
$database->persistentStorages()->delete(); $database->persistentStorages()->delete();
$database->environment_variables()->delete(); $database->environment_variables()->delete();
$database->tags()->detach();
}); });
} }
public function tags()
{
return $this->morphToMany(Tag::class, 'taggable');
}
public function team() public function team()
{ {
return data_get($this, 'environment.project.team'); return data_get($this, 'environment.project.team');

31
app/Models/Tag.php Normal file
View File

@@ -0,0 +1,31 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
class Tag extends BaseModel
{
protected $guarded = [];
public function name(): Attribute
{
return Attribute::make(
get: fn ($value) => strtolower($value),
set: fn ($value) => strtolower($value)
);
}
static public function ownedByCurrentTeam()
{
return Tag::whereTeamId(currentTeam()->id)->orderBy('name');
}
public function applications()
{
return $this->morphedByMany(Application::class, 'taggable');
}
public function services()
{
return $this->morphedByMany(Service::class, 'taggable');
}
}

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,26 +1,35 @@
<?php <?php
use App\Enums\ApplicationDeploymentStatus;
use App\Jobs\ApplicationDeploymentJob; use App\Jobs\ApplicationDeploymentJob;
use App\Jobs\ApplicationDeploymentNewJob;
use App\Models\Application; use App\Models\Application;
use App\Models\ApplicationDeploymentQueue; use App\Models\ApplicationDeploymentQueue;
use App\Models\ApplicationPreview;
use App\Models\Server; use App\Models\Server;
use App\Models\StandaloneDocker;
use Spatie\Url\Url; use Spatie\Url\Url;
use Symfony\Component\Yaml\Yaml;
function queue_application_deployment(Application $application, string $deployment_uuid, int | null $pull_request_id = 0, string $commit = 'HEAD', bool $force_rebuild = false, bool $is_webhook = false, bool $restart_only = false, ?string $git_type = null, bool $is_new_deployment = false) function queue_application_deployment(Application $application, string $deployment_uuid, int | null $pull_request_id = 0, string $commit = 'HEAD', bool $force_rebuild = false, bool $is_webhook = false, bool $restart_only = false, ?string $git_type = null, bool $no_questions_asked = false, Server $server = null, StandaloneDocker $destination = null)
{ {
$application_id = $application->id; $application_id = $application->id;
$deployment_link = Url::fromString($application->link() . "/deployment/{$deployment_uuid}"); $deployment_link = Url::fromString($application->link() . "/deployment/{$deployment_uuid}");
$deployment_url = $deployment_link->getPath(); $deployment_url = $deployment_link->getPath();
$server_id = $application->destination->server->id; $server_id = $application->destination->server->id;
$server_name = $application->destination->server->name; $server_name = $application->destination->server->name;
$destination_id = $application->destination->id;
if ($server) {
$server_id = $server->id;
$server_name = $server->name;
}
if ($destination) {
$destination_id = $destination->id;
}
$deployment = ApplicationDeploymentQueue::create([ $deployment = ApplicationDeploymentQueue::create([
'application_id' => $application_id, 'application_id' => $application_id,
'application_name' => $application->name, 'application_name' => $application->name,
'server_id' => $server_id, 'server_id' => $server_id,
'server_name' => $server_name, 'server_name' => $server_name,
'destination_id' => $destination_id,
'deployment_uuid' => $deployment_uuid, 'deployment_uuid' => $deployment_uuid,
'deployment_url' => $deployment_url, 'deployment_url' => $deployment_url,
'pull_request_id' => $pull_request_id, 'pull_request_id' => $pull_request_id,
@@ -31,13 +40,54 @@ function queue_application_deployment(Application $application, string $deployme
'git_type' => $git_type 'git_type' => $git_type
]); ]);
if ($no_questions_asked) {
$deployment->update([
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
]);
dispatch(new ApplicationDeploymentJob(
application_deployment_queue_id: $deployment->id,
));
} else if (next_queuable($server_id, $application_id)) {
$deployment->update([
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
]);
dispatch(new ApplicationDeploymentJob(
application_deployment_queue_id: $deployment->id,
));
}
}
function force_start_deployment(ApplicationDeploymentQueue $deployment)
{
$deployment->update([
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
]);
dispatch(new ApplicationDeploymentJob(
application_deployment_queue_id: $deployment->id,
));
}
function queue_next_deployment(Application $application)
{
$server_id = $application->destination->server_id;
$next_found = ApplicationDeploymentQueue::where('server_id', $server_id)->where('status', 'queued')->get()->sortBy('created_at')->first();
if ($next_found) {
$next_found->update([
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
]);
dispatch(new ApplicationDeploymentJob(
application_deployment_queue_id: $next_found->id,
));
}
}
function next_queuable(string $server_id, string $application_id): bool
{
$deployments = ApplicationDeploymentQueue::where('server_id', $server_id)->whereIn('status', ['in_progress', 'queued'])->get()->sortByDesc('created_at'); $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); $same_application_deployments = $deployments->where('application_id', $application_id);
$in_progress = $same_application_deployments->filter(function ($value, $key) { $in_progress = $same_application_deployments->filter(function ($value, $key) {
return $value->status === 'in_progress'; return $value->status === 'in_progress';
}); });
if ($in_progress->count() > 0) { if ($in_progress->count() > 0) {
return; return false;
} }
$server = Server::find($server_id); $server = Server::find($server_id);
$concurrent_builds = $server->settings->concurrent_builds; $concurrent_builds = $server->settings->concurrent_builds;
@@ -45,296 +95,7 @@ function queue_application_deployment(Application $application, string $deployme
ray("serverId:{$server->id}", "concurrentBuilds:{$concurrent_builds}", "deployments:{$deployments->count()}", "sameApplicationDeployments:{$same_application_deployments->count()}"); ray("serverId:{$server->id}", "concurrentBuilds:{$concurrent_builds}", "deployments:{$deployments->count()}", "sameApplicationDeployments:{$same_application_deployments->count()}");
if ($deployments->count() > $concurrent_builds) { if ($deployments->count() > $concurrent_builds) {
return; return false;
}
if ($is_new_deployment) {
dispatch(new ApplicationDeploymentNewJob(
deployment: $deployment,
application: Application::find($application_id)
));
} else {
dispatch(new ApplicationDeploymentJob(
application_deployment_queue_id: $deployment->id,
));
} }
} return true;
function queue_next_deployment(Application $application, bool $isNew = false)
{
$server_id = $application->destination->server_id;
$next_found = ApplicationDeploymentQueue::where('server_id', $server_id)->where('status', 'queued')->get()->sortBy('created_at')->first();;
// ray($next_found, $server_id);
if ($next_found) {
if ($isNew) {
dispatch(new ApplicationDeploymentNewJob(
deployment: $next_found,
application: $application
));
} else {
dispatch(new ApplicationDeploymentJob(
application_deployment_queue_id: $next_found->id,
));
}
}
}
// Deployment things
function generateHostIpMapping(Server $server, string $network)
{
// Generate custom host<->ip hostnames
$allContainers = instant_remote_process(["docker network inspect {$network} -f '{{json .Containers}}' "], $server);
$allContainers = format_docker_command_output_to_json($allContainers);
$ips = collect([]);
if (count($allContainers) > 0) {
$allContainers = $allContainers[0];
foreach ($allContainers as $container) {
$containerName = data_get($container, 'Name');
if ($containerName === 'coolify-proxy') {
continue;
}
$containerIp = data_get($container, 'IPv4Address');
if ($containerName && $containerIp) {
$containerIp = str($containerIp)->before('/');
$ips->put($containerName, $containerIp->value());
}
}
}
return $ips->map(function ($ip, $name) {
return "--add-host $name:$ip";
})->implode(' ');
}
function generateBaseDir(string $deplyomentUuid)
{
return "/artifacts/$deplyomentUuid";
}
function generateWorkdir(string $deplyomentUuid, Application $application)
{
return generateBaseDir($deplyomentUuid) . rtrim($application->base_directory, '/');
}
function prepareHelperContainer(Server $server, string $network, string $deploymentUuid)
{
$basedir = generateBaseDir($deploymentUuid);
$helperImage = config('coolify.helper_image');
$serverUserHomeDir = instant_remote_process(["echo \$HOME"], $server);
$dockerConfigFileExists = instant_remote_process(["test -f {$serverUserHomeDir}/.docker/config.json && echo 'OK' || echo 'NOK'"], $server);
$commands = collect([]);
if ($dockerConfigFileExists === 'OK') {
$commands->push([
"command" => "docker run -d --network $network --name $deploymentUuid --rm -v {$serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock $helperImage",
"hidden" => true,
]);
} else {
$commands->push([
"command" => "docker run -d --network {$network} --name {$deploymentUuid} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}",
"hidden" => true,
]);
}
$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;
} }

View File

@@ -34,3 +34,5 @@ const SUPPORTED_OS = [
'centos fedora rhel ol rocky', 'centos fedora rhel ol rocky',
'sles opensuse-leap opensuse-tumbleweed' 'sles opensuse-leap opensuse-tumbleweed'
]; ];
const SHARED_VARIABLE_TYPES = ['team', 'project', 'environment'];

View File

@@ -3,9 +3,7 @@
use App\Models\Application; use App\Models\Application;
use App\Models\ApplicationPreview; use App\Models\ApplicationPreview;
use App\Models\Server; use App\Models\Server;
use App\Models\Service;
use App\Models\ServiceApplication; use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Spatie\Url\Url; use Spatie\Url\Url;
@@ -323,3 +321,89 @@ function isDatabaseImage(?string $image = null)
} }
return false; 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

@@ -29,7 +29,7 @@ function generate_github_installation_token(GithubApp $source)
'Accept' => 'application/vnd.github.machine-man-preview+json' 'Accept' => 'application/vnd.github.machine-man-preview+json'
])->post("{$source->api_url}/app/installations/{$source->installation_id}/access_tokens"); ])->post("{$source->api_url}/app/installations/{$source->installation_id}/access_tokens");
if ($token->failed()) { if ($token->failed()) {
throw new \Exception("Failed to get access token for " . $source->name . " with error: " . $token->json()['message']); throw new RuntimeException("Failed to get access token for " . $source->name . " with error: " . $token->json()['message']);
} }
return $token->json()['token']; return $token->json()['token'];
} }

View File

@@ -22,6 +22,7 @@ use App\Notifications\Channels\EmailChannel;
use App\Notifications\Channels\TelegramChannel; use App\Notifications\Channels\TelegramChannel;
use App\Notifications\Internal\GeneralNotification; use App\Notifications\Internal\GeneralNotification;
use DanHarrin\LivewireRateLimiting\Exceptions\TooManyRequestsException; use DanHarrin\LivewireRateLimiting\Exceptions\TooManyRequestsException;
use Illuminate\Database\UniqueConstraintViolationException;
use Illuminate\Mail\Message; use Illuminate\Mail\Message;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\Facades\Cache; 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."; return "Too many requests. Please try again in {$error->secondsUntilAvailable} seconds.";
} }
if ($error instanceof UniqueConstraintViolationException) {
if (isset($livewire)) {
return $livewire->dispatch('error', "Duplicate entry found.", "Please use a different name.");
}
return "Duplicate entry found. Please use a different name.";
}
if ($error instanceof Throwable) { if ($error instanceof Throwable) {
$message = $error->getMessage(); $message = $error->getMessage();
@@ -474,7 +481,14 @@ function queryResourcesByUuid(string $uuid)
if ($mariadb) return $mariadb; if ($mariadb) return $mariadb;
return $resource; return $resource;
} }
function generatTagDeployWebhook($tag_name)
{
$baseUrl = base_url();
$api = Url::fromString($baseUrl) . '/api/v1';
$endpoint = "/deploy?tag=$tag_name";
$url = $api . $endpoint;
return $url;
}
function generateDeployWebhook($resource) function generateDeployWebhook($resource)
{ {
$baseUrl = base_url(); $baseUrl = base_url();
@@ -930,7 +944,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
'service_id' => $resource->id, 'service_id' => $resource->id,
])->first(); ])->first();
['command' => $command, 'forService' => $forService, 'generatedValue' => $generatedValue, 'port' => $port] = parseEnvVariable($value); ['command' => $command, 'forService' => $forService, 'generatedValue' => $generatedValue, 'port' => $port] = parseEnvVariable($value);
if ($command->value() === 'FQDN' || $command->value() === 'URL') { if ($command?->value() === 'FQDN' || $command?->value() === 'URL') {
if (Str::lower($forService) === $serviceName) { if (Str::lower($forService) === $serviceName) {
$fqdn = generateFqdn($resource->server, $containerName); $fqdn = generateFqdn($resource->server, $containerName);
} else { } else {
@@ -1350,7 +1364,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
'application_id' => $resource->id, 'application_id' => $resource->id,
])->first(); ])->first();
['command' => $command, 'forService' => $forService, 'generatedValue' => $generatedValue, 'port' => $port] = parseEnvVariable($value); ['command' => $command, 'forService' => $forService, 'generatedValue' => $generatedValue, 'port' => $port] = parseEnvVariable($value);
if ($command->value() === 'FQDN' || $command->value() === 'URL') { if ($command?->value() === 'FQDN' || $command?->value() === 'URL') {
if (Str::lower($forService) === $serviceName) { if (Str::lower($forService) === $serviceName) {
$fqdn = generateFqdn($server, $containerName); $fqdn = generateFqdn($server, $containerName);
} else { } else {
@@ -1660,3 +1674,42 @@ function ip_match($ip, $cidrs, &$match = null)
} }
return false; return false;
} }
function check_fqdn_usage(ServiceApplication|Application $own_resource)
{
$domains = collect($own_resource->fqdns)->map(function ($domain) {
if (str($domain)->endsWith('/')) {
$domain = str($domain)->beforeLast('/');
}
return str($domain)->replace('http://', '')->replace('https://', '');
});
$apps = Application::all();
foreach ($apps as $app) {
$list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== '');
foreach ($list_of_domains as $domain) {
if (str($domain)->endsWith('/')) {
$domain = str($domain)->beforeLast('/');
}
$naked_domain = str($domain)->replace('http://', '')->replace('https://', '')->value();
if ($domains->contains($naked_domain)) {
if ($app->uuid !== $own_resource->uuid) {
throw new \RuntimeException("Domain $naked_domain is already in use by another resource.");
}
}
}
}
$apps = ServiceApplication::all();
foreach ($apps as $app) {
$list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== '');
foreach ($list_of_domains as $domain) {
if (str($domain)->endsWith('/')) {
$domain = str($domain)->beforeLast('/');
}
$naked_domain = str($domain)->replace('http://', '')->replace('https://', '')->value();
if ($domains->contains($naked_domain)) {
if ($app->uuid !== $own_resource->uuid) {
throw new \RuntimeException("Domain $naked_domain is already in use by another resource.");
}
}
}
}
}

View File

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

View File

@@ -1,48 +0,0 @@
<?php
declare(strict_types=1);
return [
/**
* Add an additional second for every 100th word of the toast messages.
*
* Supported: true | false
*/
'accessibility' => true,
/**
* The vertical alignment of the toast container.
*
* Supported: "bottom", "middle" or "top"
*/
'alignment' => 'top',
/**
* Allow users to close toast messages prematurely.
*
* Supported: true | false
*/
'closeable' => true,
/**
* The on-screen duration of each toast.
*
* Minimum: 3000 (in milliseconds)
*/
'duration' => 5000,
/**
* The horizontal position of each toast.
*
* Supported: "center", "left" or "right"
*/
'position' => 'center',
/**
* Whether messages passed as translation keys should be translated automatically.
*
* Supported: true | false
*/
'translate' => true,
];

View File

@@ -1,3 +1,3 @@
<?php <?php
return '4.0.0-beta.201'; return '4.0.0-beta.207';

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

@@ -0,0 +1,39 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('tags', function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->string('name')->unique();
$table->foreignId('team_id')->nullable()->constrained()->onDelete('cascade');
$table->timestamps();
});
Schema::create('taggables', function (Blueprint $table) {
$table->unsignedBigInteger('tag_id');
$table->unsignedBigInteger('taggable_id');
$table->string('taggable_type');
$table->foreign('tag_id')->references('id')->on('tags')->onDelete('cascade');
$table->unique(['tag_id', 'taggable_id', 'taggable_type'], 'taggable_unique'); // Composite unique index
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('taggables');
Schema::dropIfExists('tags');
}
};

View File

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

View File

@@ -14,6 +14,10 @@ button[isError] {
@apply bg-red-600 hover:bg-red-700; @apply bg-red-600 hover:bg-red-700;
} }
button[isHighlighted] {
@apply bg-coollabs hover:bg-coollabs-100;
}
.scrollbar { .scrollbar {
@apply scrollbar-thumb-coollabs-100 scrollbar-track-coolgray-200 scrollbar-w-2; @apply scrollbar-thumb-coollabs-100 scrollbar-track-coolgray-200 scrollbar-w-2;
} }
@@ -72,11 +76,11 @@ a {
} }
.box { .box {
@apply flex p-2 transition-colors cursor-pointer min-h-[4rem] bg-coolgray-100 hover:bg-coollabs-100 hover:text-white hover:no-underline min-w-[24rem]; @apply flex p-2 transition-colors cursor-pointer min-h-[4rem] bg-coolgray-100 hover:bg-coollabs-100 hover:text-white hover:no-underline;
} }
.box-without-bg { .box-without-bg {
@apply flex p-2 transition-colors min-h-full hover:text-white hover:no-underline min-h-[4rem]; @apply flex p-2 transition-colors hover:text-white hover:no-underline min-h-[4rem];
} }
.description { .description {

View File

@@ -4,7 +4,7 @@
class="transition rounded w-11 h-11" src="{{ asset('coolify-transparent.png') }}"></a> class="transition rounded w-11 h-11" src="{{ asset('coolify-transparent.png') }}"></a>
<ul class="flex flex-col h-full gap-4 menu flex-nowrap"> <ul class="flex flex-col h-full gap-4 menu flex-nowrap">
<li title="Dashboard"> <li title="Dashboard">
<a class="hover:bg-transparent" href="/"> <a class="hover:bg-transparent" href="/">
<svg xmlns="http://www.w3.org/2000/svg" class="{{ request()->is('/') ? 'text-warning icon' : 'icon' }}" <svg xmlns="http://www.w3.org/2000/svg" class="{{ request()->is('/') ? 'text-warning icon' : 'icon' }}"
fill="none" viewBox="0 0 24 24" stroke="currentColor"> fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
@@ -13,7 +13,7 @@
</a> </a>
</li> </li>
<li title="Servers"> <li title="Servers">
<a class="hover:bg-transparent" href="/servers"> <a class="hover:bg-transparent" href="/servers">
<svg xmlns="http://www.w3.org/2000/svg" <svg xmlns="http://www.w3.org/2000/svg"
class="{{ request()->is('server/*') || request()->is('servers') ? 'text-warning icon' : 'icon' }}" class="{{ request()->is('server/*') || request()->is('servers') ? 'text-warning icon' : 'icon' }}"
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
@@ -28,7 +28,7 @@
</a> </a>
</li> </li>
<li title="Projects"> <li title="Projects">
<a class="hover:bg-transparent" href="/projects"> <a class="hover:bg-transparent" href="/projects">
<svg xmlns="http://www.w3.org/2000/svg" <svg xmlns="http://www.w3.org/2000/svg"
class="{{ request()->is('project/*') || request()->is('projects') ? 'text-warning icon' : 'icon' }}" class="{{ request()->is('project/*') || request()->is('projects') ? 'text-warning icon' : 'icon' }}"
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
@@ -41,7 +41,7 @@
</a> </a>
</li> </li>
<li title="Command Center"> <li title="Command Center">
<a class="hover:bg-transparent" href="/command-center"> <a class="hover:bg-transparent" href="/command-center">
<svg xmlns="http://www.w3.org/2000/svg" <svg xmlns="http://www.w3.org/2000/svg"
class="{{ request()->is('command-center') ? 'text-warning icon' : 'icon' }}" viewBox="0 0 24 24" class="{{ request()->is('command-center') ? 'text-warning icon' : 'icon' }}" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
@@ -53,7 +53,7 @@
</a> </a>
</li> </li>
<li title="Source"> <li title="Source">
<a class="hover:bg-transparent" href="{{ route('source.all') }}"> <a class="hover:bg-transparent" href="{{ route('source.all') }}">
<svg class="icon" viewBox="0 0 15 15" xmlns="http://www.w3.org/2000/svg"> <svg class="icon" viewBox="0 0 15 15" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor" <path fill="currentColor"
d="m6.793 1.207l.353.354l-.353-.354ZM1.207 6.793l-.353-.354l.353.354Zm0 1.414l.354-.353l-.354.353Zm5.586 5.586l-.354.353l.354-.353Zm1.414 0l-.353-.354l.353.354Zm5.586-5.586l.353.354l-.353-.354Zm0-1.414l-.354.353l.354-.353ZM8.207 1.207l.354-.353l-.354.353ZM6.44.854L.854 6.439l.707.707l5.585-5.585L6.44.854ZM.854 8.56l5.585 5.585l.707-.707l-5.585-5.585l-.707.707Zm7.707 5.585l5.585-5.585l-.707-.707l-5.585 5.585l.707.707Zm5.585-7.707L8.561.854l-.707.707l5.585 5.585l.707-.707Zm0 2.122a1.5 1.5 0 0 0 0-2.122l-.707.707a.5.5 0 0 1 0 .708l.707.707ZM6.44 14.146a1.5 1.5 0 0 0 2.122 0l-.707-.707a.5.5 0 0 1-.708 0l-.707.707ZM.854 6.44a1.5 1.5 0 0 0 0 2.122l.707-.707a.5.5 0 0 1 0-.708L.854 6.44Zm6.292-4.878a.5.5 0 0 1 .708 0L8.56.854a1.5 1.5 0 0 0-2.122 0l.707.707Zm-2 1.293l1 1l.708-.708l-1-1l-.708.708ZM7.5 5a.5.5 0 0 1-.5-.5H6A1.5 1.5 0 0 0 7.5 6V5Zm.5-.5a.5.5 0 0 1-.5.5v1A1.5 1.5 0 0 0 9 4.5H8ZM7.5 4a.5.5 0 0 1 .5.5h1A1.5 1.5 0 0 0 7.5 3v1Zm0-1A1.5 1.5 0 0 0 6 4.5h1a.5.5 0 0 1 .5-.5V3Zm.646 2.854l1.5 1.5l.707-.708l-1.5-1.5l-.707.708ZM10.5 8a.5.5 0 0 1-.5-.5H9A1.5 1.5 0 0 0 10.5 9V8Zm.5-.5a.5.5 0 0 1-.5.5v1A1.5 1.5 0 0 0 12 7.5h-1Zm-.5-.5a.5.5 0 0 1 .5.5h1A1.5 1.5 0 0 0 10.5 6v1Zm0-1A1.5 1.5 0 0 0 9 7.5h1a.5.5 0 0 1 .5-.5V6ZM7 5.5v4h1v-4H7Zm.5 5.5a.5.5 0 0 1-.5-.5H6A1.5 1.5 0 0 0 7.5 12v-1Zm.5-.5a.5.5 0 0 1-.5.5v1A1.5 1.5 0 0 0 9 10.5H8Zm-.5-.5a.5.5 0 0 1 .5.5h1A1.5 1.5 0 0 0 7.5 9v1Zm0-1A1.5 1.5 0 0 0 6 10.5h1a.5.5 0 0 1 .5-.5V9Z" /> d="m6.793 1.207l.353.354l-.353-.354ZM1.207 6.793l-.353-.354l.353.354Zm0 1.414l.354-.353l-.354.353Zm5.586 5.586l-.354.353l.354-.353Zm1.414 0l-.353-.354l.353.354Zm5.586-5.586l.353.354l-.353-.354Zm0-1.414l-.354.353l.354-.353ZM8.207 1.207l.354-.353l-.354.353ZM6.44.854L.854 6.439l.707.707l5.585-5.585L6.44.854ZM.854 8.56l5.585 5.585l.707-.707l-5.585-5.585l-.707.707Zm7.707 5.585l5.585-5.585l-.707-.707l-5.585 5.585l.707.707Zm5.585-7.707L8.561.854l-.707.707l5.585 5.585l.707-.707Zm0 2.122a1.5 1.5 0 0 0 0-2.122l-.707.707a.5.5 0 0 1 0 .708l.707.707ZM6.44 14.146a1.5 1.5 0 0 0 2.122 0l-.707-.707a.5.5 0 0 1-.708 0l-.707.707ZM.854 6.44a1.5 1.5 0 0 0 0 2.122l.707-.707a.5.5 0 0 1 0-.708L.854 6.44Zm6.292-4.878a.5.5 0 0 1 .708 0L8.56.854a1.5 1.5 0 0 0-2.122 0l.707.707Zm-2 1.293l1 1l.708-.708l-1-1l-.708.708ZM7.5 5a.5.5 0 0 1-.5-.5H6A1.5 1.5 0 0 0 7.5 6V5Zm.5-.5a.5.5 0 0 1-.5.5v1A1.5 1.5 0 0 0 9 4.5H8ZM7.5 4a.5.5 0 0 1 .5.5h1A1.5 1.5 0 0 0 7.5 3v1Zm0-1A1.5 1.5 0 0 0 6 4.5h1a.5.5 0 0 1 .5-.5V3Zm.646 2.854l1.5 1.5l.707-.708l-1.5-1.5l-.707.708ZM10.5 8a.5.5 0 0 1-.5-.5H9A1.5 1.5 0 0 0 10.5 9V8Zm.5-.5a.5.5 0 0 1-.5.5v1A1.5 1.5 0 0 0 12 7.5h-1Zm-.5-.5a.5.5 0 0 1 .5.5h1A1.5 1.5 0 0 0 10.5 6v1Zm0-1A1.5 1.5 0 0 0 9 7.5h1a.5.5 0 0 1 .5-.5V6ZM7 5.5v4h1v-4H7Zm.5 5.5a.5.5 0 0 1-.5-.5H6A1.5 1.5 0 0 0 7.5 12v-1Zm.5-.5a.5.5 0 0 1-.5.5v1A1.5 1.5 0 0 0 9 10.5H8Zm-.5-.5a.5.5 0 0 1 .5.5h1A1.5 1.5 0 0 0 7.5 9v1Zm0-1A1.5 1.5 0 0 0 6 10.5h1a.5.5 0 0 1 .5-.5V9Z" />
@@ -61,7 +61,7 @@
</a> </a>
</li> </li>
<li title="Security"> <li title="Security">
<a class="hover:bg-transparent" href="{{ route('security.private-key.index') }}"> <a class="hover:bg-transparent" href="{{ route('security.private-key.index') }}">
<svg class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <svg class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" stroke-width="2"
@@ -70,7 +70,7 @@
</a> </a>
</li> </li>
<li title="Teams"> <li title="Teams">
<a class="hover:bg-transparent" href="{{ route('team.index') }}"> <a class="hover:bg-transparent" href="{{ route('team.index') }}">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" stroke-width="1.5" <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> <path stroke="none" d="M0 0h24v24H0z" fill="none" />
@@ -83,6 +83,18 @@
</svg> </svg>
</a> </a>
</li> </li>
<li title="Tags">
<a class="hover:bg-transparent" href="{{ route('tags.index') }}">
<svg class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2">
<path
d="M3 8v4.172a2 2 0 0 0 .586 1.414l5.71 5.71a2.41 2.41 0 0 0 3.408 0l3.592-3.592a2.41 2.41 0 0 0 0-3.408l-5.71-5.71A2 2 0 0 0 9.172 6H5a2 2 0 0 0-2 2" />
<path d="m18 19l1.592-1.592a4.82 4.82 0 0 0 0-6.816L15 6m-8 4h-.01" />
</g>
</svg>
</a>
</li>
<div class="flex-1"></div> <div class="flex-1"></div>
@if (isInstanceAdmin() && !isCloud()) @if (isInstanceAdmin() && !isCloud())
@@ -103,7 +115,7 @@
</a> </a>
</li> </li>
<li title="Profile"> <li title="Profile">
<a class="hover:bg-transparent" href="/profile"> <a class="hover:bg-transparent" href="/profile">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" stroke-width="1.5" <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> <path stroke="none" d="M0 0h24v24H0z" fill="none" />
@@ -116,7 +128,7 @@
@if (isInstanceAdmin()) @if (isInstanceAdmin())
<li title="Settings" class="mt-auto"> <li title="Settings" class="mt-auto">
<a class="hover:bg-transparent" href="/settings"> <a class="hover:bg-transparent" href="/settings">
<svg xmlns="http://www.w3.org/2000/svg" <svg xmlns="http://www.w3.org/2000/svg"
class="{{ request()->is('settings*') ? 'text-warning icon' : 'icon' }}" viewBox="0 0 24 24" class="{{ request()->is('settings*') ? 'text-warning icon' : 'icon' }}" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"

View File

@@ -0,0 +1,69 @@
@props([
'title' => 'Are you sure?',
'buttonTitle' => 'Open Modal',
'isErrorButton' => false,
'disabled' => false,
'action' => 'delete',
'content' => null,
])
<div x-data="{ modalOpen: false }" @keydown.escape.window="modalOpen = false" :class="{ 'z-40': modalOpen }"
class="relative w-auto h-auto">
@if ($content)
<div @click="modalOpen=true">
{{ $content }}
</div>
@else
@if ($disabled)
<x-forms.button isError disabled>{{ $buttonTitle }}</x-forms.button>
@elseif ($isErrorButton)
<x-forms.button isError @click="modalOpen=true">{{ $buttonTitle }}</x-forms.button>
@else
<x-forms.button @click="modalOpen=true">{{ $buttonTitle }}</x-forms.button>
@endif
@endif
<template x-teleport="body">
<div x-show="modalOpen" class="fixed top-0 left-0 z-[99] flex items-center justify-center w-screen h-screen"
x-cloak>
<div x-show="modalOpen" x-transition:enter="ease-out duration-100" x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100" x-transition:leave="ease-in duration-100"
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" @click="modalOpen=false"
class="absolute inset-0 w-full h-full bg-black bg-opacity-20 backdrop-blur-sm"></div>
<div x-show="modalOpen" x-trap.inert.noscroll="modalOpen" x-transition:enter="ease-out duration-100"
x-transition:enter-start="opacity-0 -translate-y-2 sm:scale-95"
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave="ease-in duration-100"
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave-end="opacity-0 -translate-y-2 sm:scale-95"
class="relative w-full py-6 border rounded shadow-lg bg-coolgray-100 px-7 border-coolgray-300 sm:max-w-lg">
<div class="flex items-center justify-between pb-3">
<h3 class="text-2xl font-bold">{{ $title }}</h3>
{{-- <button @click="modalOpen=false"
class="absolute top-0 right-0 flex items-center justify-center w-8 h-8 mt-5 mr-5 text-white rounded-full hover:bg-coolgray-300">
<svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button> --}}
</div>
<div class="relative w-auto pb-8">
{{ $slot }}
</div>
<div class="flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2">
<x-forms.button @click="modalOpen=false" class="w-24 bg-coolgray-200 hover:bg-coolgray-300"
type="button">Cancel
</x-forms.button>
<div class="flex-1"></div>
@if ($isErrorButton)
<x-forms.button @click="modalOpen=false" class="w-24" isError type="button"
wire:click.prevent="{{ $action }}">Continue
</x-forms.button>
@else
<x-forms.button @click="modalOpen=false" class="w-24" isHighlighted type="button"
wire:click.prevent="{{ $action }}">Continue
</x-forms.button>
@endif
</div>
</div>
</div>
</template>
</div>

View File

@@ -258,7 +258,8 @@
<div class="flex items-start gap-4 text-xl tracking-tight">Need official support for <div class="flex items-start gap-4 text-xl tracking-tight">Need official support for
your self-hosted instance? your self-hosted instance?
<x-forms.button> <x-forms.button>
<a class="font-bold text-white hover:no-underline" href="{{ config('coolify.contact') }}">Contact <a class="font-bold text-white hover:no-underline"
href="{{ config('coolify.contact') }}">Contact
Us</a> Us</a>
</x-forms.button> </x-forms.button>
</div> </div>

View File

@@ -1,31 +1,37 @@
@props(['closeWithX' => 'false', 'fullScreen' => 'false'])
<div x-data="{ <div x-data="{
slideOverOpen: false slideOverOpen: false
}" class="relative w-auto h-auto"> }" class="relative w-auto h-auto">
{{ $slot }} {{ $slot }}
<template x-teleport="body"> <template x-teleport="body">
<div x-show="slideOverOpen" @keydown.window.escape="slideOverOpen=false" class="relative z-[99]"> <div x-show="slideOverOpen" @if (!$closeWithX) @keydown.window.escape="slideOverOpen=false" @endif
<div x-show="slideOverOpen" @click="slideOverOpen = false" class="fixed inset-0 bg-black bg-opacity-60"></div> class="relative z-[99]">
<div x-show="slideOverOpen" @if (!$closeWithX) @click="slideOverOpen = false" @endif
class="fixed inset-0 bg-black bg-opacity-60"></div>
<div class="fixed inset-0 overflow-hidden"> <div class="fixed inset-0 overflow-hidden">
<div class="absolute inset-0 overflow-hidden"> <div class="absolute inset-0 overflow-hidden">
<div class="fixed inset-y-0 right-0 flex max-w-full pl-10"> <div class="fixed inset-y-0 right-0 flex max-w-full pl-10">
<div x-show="slideOverOpen" @click.away="slideOverOpen = false" <div x-show="slideOverOpen"
@if (!$closeWithX) @click.away="slideOverOpen = false" @endif
x-transition:enter="transform transition ease-in-out duration-100 sm:duration-300" x-transition:enter="transform transition ease-in-out duration-100 sm:duration-300"
x-transition:enter-start="translate-x-full" x-transition:enter-end="translate-x-0" x-transition:enter-start="translate-x-full" x-transition:enter-end="translate-x-0"
x-transition:leave="transform transition ease-in-out duration-100 sm:duration-300" x-transition:leave="transform transition ease-in-out duration-100 sm:duration-300"
x-transition:leave-start="translate-x-0" x-transition:leave-end="translate-x-full" x-transition:leave-start="translate-x-0" x-transition:leave-end="translate-x-full"
class="w-screen max-w-md"> @class([
'max-w-md w-screen' => !$fullScreen,
'max-w-7xl w-screen' => $fullScreen,
])>
<div <div
class="flex flex-col h-full py-6 overflow-hidden border-l shadow-lg bg-base-100 border-neutral-800"> class="flex flex-col h-full py-6 overflow-hidden border-l shadow-lg bg-base-100 border-neutral-800">
<div class="px-4 pb-10 sm:px-5"> <div class="px-4 pb-4 sm:px-5">
<div class="flex items-start justify-between pb-1"> <div class="flex items-start justify-between pb-1">
<h2 class="text-2xl leading-6" id="slide-over-title"> <h2 class="text-3xl leading-6" id="slide-over-title">
{{ $title }}</h2> {{ $title }}</h2>
<div class="flex items-center h-auto ml-3"> <div class="flex items-center h-auto ml-3">
<button class="icon" @click="slideOverOpen=false" <button class="icon" @click="slideOverOpen=false"
class="absolute top-0 right-0 z-30 flex items-center justify-center px-3 py-2 mt-4 mr-2 space-x-1 text-xs font-normal border-none rounded"> class="absolute top-0 right-0 z-30 flex items-center justify-center px-3 py-2 mt-4 mr-2 space-x-1 text-xs font-normal border-none rounded">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" fill="none" <svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" fill="none"
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
>
<path stroke-linecap="round" stroke-linejoin="round" <path stroke-linecap="round" stroke-linejoin="round"
d="M6 18L18 6M6 6l12 12"></path> d="M6 18L18 6M6 6l12 12"></path>
</svg> </svg>

View File

@@ -22,20 +22,7 @@
if (typeof options.html != 'undefined') html = options.html; if (typeof options.html != 'undefined') html = options.html;
window.dispatchEvent(new CustomEvent('toast-show', { detail: { type: type, message: message, description: description, position: position, html: html } })); window.dispatchEvent(new CustomEvent('toast-show', { detail: { type: type, message: message, description: description, position: position, html: html } }));
} }" class="relative space-y-5">
window.customToastHTML = `
<div class='relative flex items-start justify-center p-4'>
<div class='flex flex-col'>
<p class='text-sm font-medium text-gray-800'>New Friend Request</p>
<p class='mt-1 text-xs leading-none text-gray-800'>Friend request from John Doe.</p>
<div class='flex mt-3'>
<button type='button' @click='burnToast(toast.id)' class='inline-flex items-center px-2 py-1 text-xs font-semibold text-white bg-indigo-600 rounded shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600'>Accept</button>
<button type='button' @click='burnToast(toast.id)' class='inline-flex items-center px-2 py-1 ml-3 text-xs font-semibold text-gray-900 bg-white rounded shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50'>Decline</button>
</div>
</div>
</div>
`" class="relative space-y-5">
<template x-teleport="body"> <template x-teleport="body">
<ul x-data="{ <ul x-data="{
toasts: [], toasts: [],
@@ -369,14 +356,14 @@ window.customToastHTML = `
}, 5); }, 5);
}, 4000);" }, 4000);"
@mouseover="toastHovered=true" @mouseout="toastHovered=false" @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-100 ease-out sm:max-w-xs"
:class="{ 'toast-no-description': !toast.description }"> :class="{ 'toast-no-description': !toast.description }">
<span <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-100 ease-out bg-coolgray-100 border border-coolgray-200 rounded sm:max-w-xs group"
:class="{ 'p-4': !toast.html, 'p-0': toast.html }"> :class="{ 'p-4': !toast.html, 'p-0': toast.html }">
<template x-if="!toast.html"> <template x-if="!toast.html">
<div class="relative"> <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' }"> :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" <svg x-show="toast.type=='success'" class="w-[18px] h-[18px] mr-1.5 -ml-1"
@@ -403,12 +390,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" 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> fill="currentColor"></path>
</svg> </svg>
<p class="font-medium leading-none text-neutral-200" <p class="leading-2 text-neutral-200"
x-html="toast.message"> x-html="toast.message">
</p> </p>
</div> </div>
<p x-show="toast.description" :class="{ 'pl-5': toast.type!='default' }" <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 whitespace-pre-wrap" x-html="toast.description"></p>
</div> </div>
</template> </template>
<template x-if="toast.html"> <template x-if="toast.html">

View File

@@ -170,6 +170,7 @@
} }
}) })
window.Livewire.on('installDocker', () => { window.Livewire.on('installDocker', () => {
console.log('Installing Docker...');
installDocker.showModal(); installDocker.showModal();
}) })
}); });

View File

@@ -1,15 +1,5 @@
@extends('layouts.base') @extends('layouts.base')
@section('body') @section('body')
<x-modal noSubmit modalId="installDocker">
<x-slot:modalBody>
<livewire:activity-monitor header="Docker Installation Logs" />
</x-slot:modalBody>
<x-slot:modalSubmit>
<x-forms.button onclick="installDocker.close()" type="submit">
Close
</x-forms.button>
</x-slot:modalSubmit>
</x-modal>
@if (isSubscriptionActive() || isDev()) @if (isSubscriptionActive() || isDev())
<div title="Send us feedback or get help!" class="fixed top-0 right-0 p-2 px-4 pt-4 mt-auto text-xs"> <div title="Send us feedback or get help!" class="fixed top-0 right-0 p-2 px-4 pt-4 mt-auto text-xs">
<button class="flex items-center justify-center gap-2" wire:click="help" onclick="help.showModal()"> <button class="flex items-center justify-center gap-2" wire:click="help" onclick="help.showModal()">

View File

@@ -12,7 +12,7 @@
<x-navbar-subscription /> <x-navbar-subscription />
@endif @endif
<main class="main max-w-screen-2xl"> <main class="mx-auto main max-w-screen-2xl">
{{ $slot }} {{ $slot }}
</main> </main>
@endsection @endsection

View File

@@ -5,7 +5,8 @@
<h1 class="text-5xl font-bold">Welcome to Coolify</h1> <h1 class="text-5xl font-bold">Welcome to Coolify</h1>
<p class="py-6 text-xl text-center">Let me help you to set the basics.</p> <p class="py-6 text-xl text-center">Let me help you to set the basics.</p>
<div class="flex justify-center "> <div class="flex justify-center ">
<x-forms.button class="justify-center box" wire:click="$set('currentState','explanation')">Get Started <x-forms.button class="justify-center w-64 box" wire:click="$set('currentState','explanation')">Get
Started
</x-forms.button> </x-forms.button>
</div> </div>
@endif @endif
@@ -31,7 +32,7 @@
Telegram, Email, etc.) when something goes wrong, or an action needed from your side.</p> Telegram, Email, etc.) when something goes wrong, or an action needed from your side.</p>
</x-slot:explanation> </x-slot:explanation>
<x-slot:actions> <x-slot:actions>
<x-forms.button class="justify-center box" wire:click="explanation">Next <x-forms.button class="justify-center w-64 box" wire:click="explanation">Next
</x-forms.button> </x-forms.button>
</x-slot:actions> </x-slot:actions>
</x-boarding-step> </x-boarding-step>
@@ -43,11 +44,11 @@
or on a <x-highlighted text="Remote Server" />? or on a <x-highlighted text="Remote Server" />?
</x-slot:question> </x-slot:question>
<x-slot:actions> <x-slot:actions>
<x-forms.button class="justify-center box" wire:target="setServerType('localhost')" <x-forms.button class="justify-center w-64 box" wire:target="setServerType('localhost')"
wire:click="setServerType('localhost')">Localhost wire:click="setServerType('localhost')">Localhost
</x-forms.button> </x-forms.button>
<x-forms.button class="justify-center box" wire:target="setServerType('remote')" <x-forms.button class="justify-center w-64 box " wire:target="setServerType('remote')"
wire:click="setServerType('remote')">Remote Server wire:click="setServerType('remote')">Remote Server
</x-forms.button> </x-forms.button>
@if (!$serverReachable) @if (!$serverReachable)
@@ -57,9 +58,10 @@
'root' or skip the boarding process and add a new private key manually to Coolify and to the 'root' or skip the boarding process and add a new private key manually to Coolify and to the
server. server.
<br /> <br />
Check this <a target="_blank" class="underline" href="https://coolify.io/docs/server/openssh">documentation</a> for further help. Check this <a target="_blank" class="underline"
href="https://coolify.io/docs/server/openssh">documentation</a> for further help.
<x-forms.input readonly id="serverPublicKey"></x-forms.input> <x-forms.input readonly id="serverPublicKey"></x-forms.input>
<x-forms.button class="box" wire:target="setServerType('localhost')" <x-forms.button class="w-64 box" wire:target="setServerType('localhost')"
wire:click="setServerType('localhost')">Check again wire:click="setServerType('localhost')">Check again
</x-forms.button> </x-forms.button>
@endif @endif
@@ -83,10 +85,10 @@
Do you have your own SSH Private Key? Do you have your own SSH Private Key?
</x-slot:question> </x-slot:question>
<x-slot:actions> <x-slot:actions>
<x-forms.button class="justify-center box" wire:target="setPrivateKey('own')" <x-forms.button class="justify-center w-64 box" wire:target="setPrivateKey('own')"
wire:click="setPrivateKey('own')">Yes wire:click="setPrivateKey('own')">Yes
</x-forms.button> </x-forms.button>
<x-forms.button class="justify-center box" wire:target="setPrivateKey('create')" <x-forms.button class="justify-center w-64 box" wire:target="setPrivateKey('create')"
wire:click="setPrivateKey('create')">No (create one for me) wire:click="setPrivateKey('create')">No (create one for me)
</x-forms.button> </x-forms.button>
@if (count($privateKeys) > 0) @if (count($privateKeys) > 0)
@@ -119,7 +121,7 @@
There are already servers available for your Team. Do you want to use one of them? There are already servers available for your Team. Do you want to use one of them?
</x-slot:question> </x-slot:question>
<x-slot:actions> <x-slot:actions>
<x-forms.button class="justify-center box" wire:click="createNewServer">No (create one for me) <x-forms.button class="justify-center w-64 box" wire:click="createNewServer">No (create one for me)
</x-forms.button> </x-forms.button>
<div> <div>
<form wire:submit='selectExistingServer' class="flex flex-col w-full gap-4 lg:w-96"> <form wire:submit='selectExistingServer' class="flex flex-col w-full gap-4 lg:w-96">
@@ -139,7 +141,7 @@
'root' or skip the boarding process and add a new private key manually to Coolify and to the 'root' or skip the boarding process and add a new private key manually to Coolify and to the
server. server.
<x-forms.input readonly id="serverPublicKey"></x-forms.input> <x-forms.input readonly id="serverPublicKey"></x-forms.input>
<x-forms.button class="box" wire:target="validateServer" wire:click="validateServer">Check <x-forms.button class="w-64 box" wire:target="validateServer" wire:click="validateServer">Check
again again
</x-forms.button> </x-forms.button>
@endif @endif
@@ -231,12 +233,16 @@
Could not find Docker Engine on your server. Do you want me to install it for you? Could not find Docker Engine on your server. Do you want me to install it for you?
</x-slot:question> </x-slot:question>
<x-slot:actions> <x-slot:actions>
<x-forms.button class="justify-center box" wire:click="installDocker"> <x-slide-over closeWithX fullScreen>
Let's do it!</x-forms.button> <x-slot:title>Configuring Server</x-slot:title>
@if ($dockerInstallationStarted) <x-slot:content>
<x-forms.button class="justify-center box" wire:click="dockerInstalledOrSkipped"> <livewire:server.validate-and-install :server="$this->createdServer" />
Validate Server & Continue</x-forms.button> </x-slot:content>
@endif <x-forms.button @click="slideOverOpen=true" class="font-bold box w-96"
wire:click.prevent='installServer' isHighlighted>
Let's do it!
</x-forms.button>
</x-slide-over>
</x-slot:actions> </x-slot:actions>
<x-slot:explanation> <x-slot:explanation>
<p>This will install the latest Docker Engine on your server, configure a few things to be able <p>This will install the latest Docker Engine on your server, configure a few things to be able
@@ -246,7 +252,6 @@
documentation</a>.</p> documentation</a>.</p>
</x-slot:explanation> </x-slot:explanation>
</x-boarding-step> </x-boarding-step>
@endif @endif
</div> </div>
<div> <div>
@@ -289,7 +294,7 @@
@endif @endif
</x-slot:question> </x-slot:question>
<x-slot:actions> <x-slot:actions>
<x-forms.button class="justify-center box" wire:click="createNewProject">Let's create a new <x-forms.button class="justify-center w-64 box" wire:click="createNewProject">Let's create a new
one!</x-forms.button> one!</x-forms.button>
<div> <div>
@if (count($projects) > 0) @if (count($projects) > 0)
@@ -322,7 +327,7 @@
I will redirect you to the new resource page, where you can create your first resource. I will redirect you to the new resource page, where you can create your first resource.
</x-slot:question> </x-slot:question>
<x-slot:actions> <x-slot:actions>
<div class="items-center justify-center box" wire:click="showNewResource">Let's do <div class="items-center justify-center w-64 box" wire:click="showNewResource">Let's do
it!</div> it!</div>
</x-slot:actions> </x-slot:actions>
<x-slot:explanation> <x-slot:explanation>

View File

@@ -102,41 +102,40 @@
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<h3 class="py-4">Deployments </h3> <h3 class="py-4">Deployments</h3>
@if (count($deployments_per_server) > 0) @if (count($deployments_per_server) > 0)
<x-loading /> <x-loading />
@endif @endif
</div> </div>
{{-- <div wire:poll.4000ms="get_deployments" class="grid grid-cols-1 gap-2 lg:grid-cols-3"> --}} <div wire:poll.1000ms="get_deployments" class="grid grid-cols-1">
<div class="grid grid-cols-1">
@forelse ($deployments_per_server as $server_name => $deployments) @forelse ($deployments_per_server as $server_name => $deployments)
<h4 class="py-4">{{ $server_name }}</h4> <h4 class="py-4">{{ $server_name }}</h4>
<div class="grid grid-cols-1 gap-2 lg:grid-cols-3"> <div class="grid grid-cols-1 gap-2 lg:grid-cols-3">
@foreach ($deployments as $deployment) @foreach ($deployments as $deployment)
<a href="{{ data_get($deployment, 'deployment_url') }}" @class([ <a href="{{ data_get($deployment, 'deployment_url') }}" @class([
'gap-2 cursor-pointer box group border-l-2 border-dotted', 'gap-2 cursor-pointer box group border-l-2 border-dotted',
'border-white' => data_get($deployment, 'status') === 'queued', 'border-coolgray-500' => data_get($deployment, 'status') === 'queued',
'border-yellow-500' => data_get($deployment, 'status') === 'in_progress', 'border-yellow-500' => data_get($deployment, 'status') === 'in_progress',
])> ])>
<div class="flex flex-col mx-6"> <div class="flex flex-col mx-6">
<div class="font-bold text-white"> <div class="font-bold text-white">
{{ data_get($deployment, 'application_name') }} {{ data_get($deployment, 'application_name') }}
</div> </div>
@if (data_get($deployment, 'pull_request_id') !== 0) @if (data_get($deployment, 'pull_request_id') !== 0)
<div class="description"> <div class="description">
PR #{{ data_get($deployment, 'pull_request_id') }} PR #{{ data_get($deployment, 'pull_request_id') }}
</div>
@endif
<div class="description">
{{ str(data_get($deployment, 'status'))->headline() }}
</div> </div>
@endif
<div class="description">
{{ str(data_get($deployment, 'status'))->headline() }}
</div> </div>
</div> <div class="flex-1"></div>
<div class="flex-1"></div> </a>
</a> @endforeach
@endforeach
</div> </div>
@empty @empty
<div>No queued / in progress deployments</div> <div>No deployments running.</div>
@endforelse @endforelse
</div> </div>
<script> <script>

View File

@@ -1,9 +1,4 @@
<div> <div>
<x-modal yesOrNo modalId="deleteDestination" modalTitle="Delete Destination">
<x-slot:modalBody>
<p>This destination will be deleted. It is not reversible. <br>Please think again.</p>
</x-slot:modalBody>
</x-modal>
<form class="flex flex-col"> <form class="flex flex-col">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<h1>Destination</h1> <h1>Destination</h1>
@@ -11,9 +6,9 @@
Save Save
</x-forms.button> </x-forms.button>
@if ($destination->network !== 'coolify') @if ($destination->network !== 'coolify')
<x-forms.button isError isModal modalId="deleteDestination"> <x-new-modal isErrorButton buttonTitle="Delete Destination">
Delete This destination will be deleted. It is not reversible. <br>Please think again.
</x-forms.button> </x-new-modal>
@endif @endif
</div> </div>

View File

@@ -14,7 +14,7 @@
</x-forms.select> </x-forms.select>
{{-- <x-forms.checkbox type="checkbox" id="is_swarm" label="Is it a Swarm network?" /> --}} {{-- <x-forms.checkbox type="checkbox" id="is_swarm" label="Is it a Swarm network?" /> --}}
<x-forms.button type="submit"> <x-forms.button type="submit">
Save Destination Continue
</x-forms.button> </x-forms.button>
</form> </form>
</div> </div>

View File

@@ -0,0 +1,18 @@
@php use App\Actions\CoolifyTask\RunRemoteProcess; @endphp
<div>
@if ($this->activity)
@if (isset($header))
<div class="flex gap-2 pb-2">
{{ $header }}
@if ($isPollingActive)
<x-loading />
@endif
</div>
@endif
<div
class="scrollbar flex flex-col-reverse w-full overflow-y-auto border border-solid rounded border-coolgray-300 max-h-[32rem] p-4 pt-6 text-xs text-white">
<pre class="font-mono whitespace-pre-wrap" @if ($isPollingActive) wire:poll.1000ms="polling" @endif>{{ RunRemoteProcess::decodeOutput($this->activity) }}</pre>
</div>
@endif
</div>

View File

@@ -63,6 +63,9 @@
@click.prevent="activeTab = 'resource-operations'; window.location.hash = 'resource-operations'" @click.prevent="activeTab = 'resource-operations'; window.location.hash = 'resource-operations'"
href="#">Resource Operations href="#">Resource Operations
</a> </a>
<a :class="activeTab === 'tags' && 'text-white'"
@click.prevent="activeTab = 'tags'; window.location.hash = 'tags'" href="#">Tags
</a>
<a :class="activeTab === 'danger' && 'text-white'" <a :class="activeTab === 'danger' && 'text-white'"
@click.prevent="activeTab = 'danger'; window.location.hash = 'danger'" href="#">Danger Zone @click.prevent="activeTab = 'danger'; window.location.hash = 'danger'" href="#">Danger Zone
</a> </a>
@@ -112,6 +115,9 @@
<div x-cloak x-show="activeTab === 'resource-operations'"> <div x-cloak x-show="activeTab === 'resource-operations'">
<livewire:project.shared.resource-operations :resource="$application" /> <livewire:project.shared.resource-operations :resource="$application" />
</div> </div>
<div x-cloak x-show="activeTab === 'tags'">
<livewire:project.shared.tags :resource="$application" />
</div>
<div x-cloak x-show="activeTab === 'danger'"> <div x-cloak x-show="activeTab === 'danger'">
<livewire:project.shared.danger :resource="$application" /> <livewire:project.shared.danger :resource="$application" />
</div> </div>

View File

@@ -5,8 +5,12 @@
@else @else
<x-forms.button wire:click.prevent="show_debug">Show Debug Logs</x-forms.button> <x-forms.button wire:click.prevent="show_debug">Show Debug Logs</x-forms.button>
@endif @endif
@if (data_get($application_deployment_queue, 'status') === 'queued')
<x-forms.button wire:click.prevent="force_start">Force Start</x-forms.button>
@endif
@if (data_get($application_deployment_queue, 'status') === 'in_progress' || @if (data_get($application_deployment_queue, 'status') === 'in_progress' ||
data_get($application_deployment_queue, 'status') === 'queued') data_get($application_deployment_queue, 'status') === 'queued')
<x-forms.button isError wire:click.prevent="cancel">Cancel Deployment</x-forms.button> <x-forms.button isError wire:click.prevent="cancel">Cancel</x-forms.button>
@endif @endif
</div> </div>

View File

@@ -112,9 +112,7 @@
helper="If set, it will tag the built image with this tag too. <br><br>Example: If you set it to 'latest', it will push the image with the commit sha tag + with the latest tag." helper="If set, it will tag the built image with this tag too. <br><br>Example: If you set it to 'latest', it will push the image with the commit sha tag + with the latest tag."
label="Docker Image Tag" /> label="Docker Image Tag" />
@endif @endif
@endif @endif
</div> </div>
@endif @endif
@@ -130,7 +128,6 @@
@endif @endif
@if ($application->could_set_build_commands()) @if ($application->could_set_build_commands())
@if ($application->build_pack === 'nixpacks') @if ($application->build_pack === 'nixpacks')
<div class="flex flex-col gap-2 xl:flex-row"> <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" <x-forms.input placeholder="If you modify this, you probably need to have a nixpacks.toml"
id="application.install_command" label="Install Command" /> id="application.install_command" label="Install Command" />
@@ -194,6 +191,13 @@
@endif @endif
@endif @endif
</div> </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
@endif @endif

View File

@@ -106,18 +106,6 @@
</svg> </svg>
Deploy Deploy
</button> </button>
{{-- @if (isDev())
<button wire:click='deployNew'
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-warning" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" />
</svg>
Deploy (new)
</button>
@endif --}}
@endif @endif
@endif @endif
</div> </div>

View File

@@ -1,18 +1,17 @@
<form wire:submit='clone'> <form>
<div class="flex flex-col"> <div class="flex flex-col">
<h1>Clone</h1> <h1>Clone</h1>
<div class="subtitle ">Quickly clone all resources to a new project</div> <div class="subtitle ">Quickly clone all resources to a new project or environment</div>
</div>
<div class="flex items-end gap-2">
<x-forms.input required id="newProjectName" label="New Project Name" />
<x-forms.button type="submit">Clone</x-forms.button>
</div> </div>
<x-forms.input required id="newName" label="New Name" />
<x-forms.button isHighlighted wire:click="clone('project')" class="mt-4">Clone to a new Project</x-forms.button>
<x-forms.button isHighlighted wire:click="clone('environment')" class="mt-4">Clone to a new Environment</x-forms.button>
<h3 class="pt-4 pb-2">Servers</h3> <h3 class="pt-4 pb-2">Servers</h3>
<div>Choose the server and network to clone the resources to.</div>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
@foreach ($servers->sortBy('id') as $server) @foreach ($servers->sortBy('id') as $server)
<div class="p-4 border border-coolgray-500"> <div class="p-4">
<h3>{{ $server->name }}</h3> <h4>{{ $server->name }}</h4>
<h5>{{ $server->description }}</h5>
<div class="pt-4 pb-2">Docker Networks</div> <div class="pt-4 pb-2">Docker Networks</div>
<div class="grid grid-cols-1 gap-2 pb-4 lg:grid-cols-4"> <div class="grid grid-cols-1 gap-2 pb-4 lg:grid-cols-4">
@foreach ($server->destinations() as $destination) @foreach ($server->destinations() as $destination)
@@ -28,9 +27,10 @@
</div> </div>
<h3 class="pt-4 pb-2">Resources</h3> <h3 class="pt-4 pb-2">Resources</h3>
<div class="grid grid-cols-1 gap-2 p-4 border border-coolgray-500"> <div>These will be cloned to the new project</div>
<div class="grid grid-cols-1 gap-2 pt-4 opacity-95 lg:grid-cols-2 xl:grid-cols-3">
@foreach ($environment->applications->sortBy('name') as $application) @foreach ($environment->applications->sortBy('name') as $application)
<div> <div class="cursor-default box-without-bg bg-coolgray-100 group">
<div class="flex flex-col"> <div class="flex flex-col">
<div class="font-bold text-white">{{ $application->name }}</div> <div class="font-bold text-white">{{ $application->name }}</div>
<div class="description">{{ $application->description }}</div> <div class="description">{{ $application->description }}</div>
@@ -38,7 +38,7 @@
</div> </div>
@endforeach @endforeach
@foreach ($environment->databases()->sortBy('name') as $database) @foreach ($environment->databases()->sortBy('name') as $database)
<div> <div class="cursor-default box-without-bg bg-coolgray-100 group">
<div class="flex flex-col"> <div class="flex flex-col">
<div class="font-bold text-white">{{ $database->name }}</div> <div class="font-bold text-white">{{ $database->name }}</div>
<div class="description">{{ $database->description }}</div> <div class="description">{{ $database->description }}</div>
@@ -46,7 +46,7 @@
</div> </div>
@endforeach @endforeach
@foreach ($environment->services->sortBy('name') as $service) @foreach ($environment->services->sortBy('name') as $service)
<div> <div class="cursor-default box-without-bg bg-coolgray-100 group">
<div class="flex flex-col"> <div class="flex flex-col">
<div class="font-bold text-white">{{ $service->name }}</div> <div class="font-bold text-white">{{ $service->name }}</div>
<div class="description">{{ $service->description }}</div> <div class="description">{{ $service->description }}</div>

View File

@@ -48,6 +48,9 @@
@click.prevent="activeTab = 'resource-operations'; window.location.hash = 'resource-operations'" @click.prevent="activeTab = 'resource-operations'; window.location.hash = 'resource-operations'"
href="#">Resource Operations href="#">Resource Operations
</a> </a>
<a :class="activeTab === 'tags' && 'text-white'"
@click.prevent="activeTab = 'tags'; window.location.hash = 'tags'" href="#">Tags
</a>
<a :class="activeTab === 'danger' && 'text-white'" <a :class="activeTab === 'danger' && 'text-white'"
@click.prevent="activeTab = 'danger'; @click.prevent="activeTab = 'danger';
window.location.hash = 'danger'" window.location.hash = 'danger'"
@@ -89,6 +92,9 @@
<div x-cloak x-show="activeTab === 'resource-operations'"> <div x-cloak x-show="activeTab === 'resource-operations'">
<livewire:project.shared.resource-operations :resource="$database" /> <livewire:project.shared.resource-operations :resource="$database" />
</div> </div>
<div x-cloak x-show="activeTab === 'tags'">
<livewire:project.shared.tags :resource="$database" />
</div>
<div x-cloak x-show="activeTab === 'danger'"> <div x-cloak x-show="activeTab === 'danger'">
<livewire:project.shared.danger :resource="$database" /> <livewire:project.shared.danger :resource="$database" />
</div> </div>

View File

@@ -1,8 +1,3 @@
<div> <x-new-modal isErrorButton buttonTitle="Delete Environment" disabled="{{ $disabled }}">
<x-modal yesOrNo modalId="deleteEnvironment" modalTitle="Delete Environment"> This environment will be deleted. It is not reversible. <br>Please think again.
<x-slot:modalBody> </x-new-modal>
<p>This environment will be deleted. It is not reversible. <br>Please think again.</p>
</x-slot:modalBody>
</x-modal>
<x-forms.button isError isModal modalId="deleteEnvironment"> Delete Environment</x-forms.button>
</div>

View File

@@ -1,10 +1,3 @@
<div> <x-new-modal isErrorButton buttonTitle="Delete Project" disabled="{{ $disabled }}">
<x-modal yesOrNo modalId="deleteProject" modalTitle="Delete Project"> This project will be deleted. It is not reversible. <br>Please think again.
<x-slot:modalBody> </x-new-modal>
<p>This project will be deleted. It is not reversible. <br>Please think again.</p>
</x-slot:modalBody>
</x-modal>
<x-forms.button isError isModal modalId="deleteProject">
Delete Project
</x-forms.button>
</div>

View File

@@ -5,6 +5,7 @@
<div class="flex items-end gap-2"> <div class="flex items-end gap-2">
<h2>General</h2> <h2>General</h2>
<x-forms.button type="submit">Save</x-forms.button> <x-forms.button type="submit">Save</x-forms.button>
<livewire:project.delete-project :disabled="$project->resource_count() > 0" :project_id="$project->id" />
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<x-forms.input label="Name" id="project.name" /> <x-forms.input label="Name" id="project.name" />
@@ -23,7 +24,8 @@
Add</button> Add</button>
</x-slide-over> </x-slide-over>
</div> </div>
<div class="flex items-center gap-2 pb-4">You can use these variables anywhere with <span class="text-warning">@{{project.VARIABLENAME}}</span><x-helper <div class="flex items-center gap-2 pb-4">You can use these variables anywhere with <span
class="text-warning">@{{ project.VARIABLENAME }}</span><x-helper
helper="More info <a class='text-white underline' href='https://coolify.io/docs/environment-variables#shared-variables' target='_blank'>here</a>."></x-helper> helper="More info <a class='text-white underline' href='https://coolify.io/docs/environment-variables#shared-variables' target='_blank'>here</a>."></x-helper>
</div> </div>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">

View File

@@ -3,6 +3,7 @@
<div class="flex items-end gap-2"> <div class="flex items-end gap-2">
<h1>Environment: {{ data_get($environment, 'name') }}</h1> <h1>Environment: {{ data_get($environment, 'name') }}</h1>
<x-forms.button type="submit">Save</x-forms.button> <x-forms.button type="submit">Save</x-forms.button>
<livewire:project.delete-environment :disabled="!$environment->isEmpty()" :environment_id="$environment->id" />
</div> </div>
<nav class="flex pt-2 pb-10"> <nav class="flex pt-2 pb-10">
<ol class="flex items-center"> <ol class="flex items-center">

View File

@@ -8,7 +8,7 @@
<li class="step">Select a Repository, Branch & Save</li> <li class="step">Select a Repository, Branch & Save</li>
</ul> </ul>
<div class="flex flex-col justify-center gap-2 text-left xl:flex-row"> <div class="flex flex-col justify-center gap-2 text-left xl:flex-row">
@foreach ($private_keys as $key) @forelse ($private_keys as $key)
@if ($private_key_id == $key->id) @if ($private_key_id == $key->id)
<div class="gap-2 py-4 cursor-pointer group hover:bg-coollabs bg-coolgray-200" <div class="gap-2 py-4 cursor-pointer group hover:bg-coollabs bg-coolgray-200"
wire:click.defer="setPrivateKey('{{ $key->id }}')" wire:key="{{ $key->id }}"> wire:click.defer="setPrivateKey('{{ $key->id }}')" wire:key="{{ $key->id }}">
@@ -32,7 +32,16 @@
</div> </div>
</div> </div>
@endif @endif
@endforeach @empty
<div class="flex flex-col items-center justify-center gap-2">
<div class="text-neutral-500">
No private keys found.
</div>
<a href="{{ route('security.private-key.index') }}">
<x-forms.button>Create a new private key</x-forms.button>
</a>
</div>
@endforelse
</div> </div>
@endif @endif
@if ($current_step === 'repository') @if ($current_step === 'repository')

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