Compare commits

...

58 Commits

Author SHA1 Message Date
Andras Bacsai
885b3bdea7 Merge pull request #3701 from coollabsio/next
v4.0.0-beta.352
2024-10-03 20:53:17 +02:00
Andras Bacsai
d84d0a816b chore: Refactor DatabaseBackupJob to handle missing team 2024-10-03 20:51:18 +02:00
Andras Bacsai
0da31c34b5 fix: add new supported database images 2024-10-03 20:47:22 +02:00
Andras Bacsai
ccdaf59ecb fix: service application view 2024-10-03 20:47:02 +02:00
Andras Bacsai
3fc9cf90ab chore: update version to 4.0.0-beta.352 2024-10-03 20:46:45 +02:00
Andras Bacsai
a8982379c9 Merge pull request #3683 from coollabsio/next
v4.0.0-beta.351
2024-10-03 15:15:21 +02:00
Andras Bacsai
6dd0bd0742 fix: api useBuildServer 2024-10-03 15:09:56 +02:00
Andras Bacsai
1d3494a6ba fix: network handling
fix: environment variable handling
2024-10-03 15:04:40 +02:00
Andras Bacsai
ee5eb427c9 fix: bitcoin core template 2024-10-03 14:07:58 +02:00
Andras Bacsai
ad7b5e6e1c Merge pull request #3689 from ALsJourney/bitcoin_core_service
Basic Bitcoin Core node service
2024-10-03 14:00:52 +02:00
Andras Bacsai
73bd344147 fix: strapi template 2024-10-03 13:49:50 +02:00
Andras Bacsai
ef448280d8 fix: able to support more database dynamically from Coolify's UI 2024-10-03 13:49:43 +02:00
Andras Bacsai
5282248cb4 Merge pull request #3685 from statickidz/add-strapi-template
Add Strapi template
2024-10-03 13:18:40 +02:00
Andras Bacsai
14c9f25c57 feat: restart service without pulling the latest image 2024-10-03 13:17:35 +02:00
ALsJourney
44b3d08d86 Finished basic bitcoin core service 2024-10-03 13:13:25 +02:00
Andras Bacsai
5da1f48ae1 fix typo 2024-10-03 12:43:41 +02:00
Andras Bacsai
2bc1b9027c Merge pull request #3679 from alepouna/typo-fix
Update docker-registry.yaml
2024-10-03 12:43:46 +02:00
Andras Bacsai
1c7ca56756 feat: backup all databases for mysql,mariadb,postgresql 2024-10-03 12:39:45 +02:00
Adrian Barrio
d70faea845 Merge remote-tracking branch 'origin/next' into add-strapi-template 2024-10-03 11:33:35 +02:00
Adrian Barrio
fdb5cab875 feat: add strapi template 2024-10-03 11:28:27 +02:00
Andras Bacsai
bb6cb8edc9 improvement: show backup button on supported db service stacks 2024-10-03 10:48:25 +02:00
Andras Bacsai
a6ec2b92fb version++ 2024-10-03 10:23:38 +02:00
Andras Bacsai
436a1d945f Merge pull request #3663 from coollabsio/next
v4.0.0-beta.350
2024-10-03 10:09:05 +02:00
Andras Bacsai
a6a3abc273 chore: Update soketi service image to version 1.0.3 2024-10-03 10:05:54 +02:00
Andras Bacsai
c4e702f096 fix: able to select root permission easier 2024-10-03 09:57:37 +02:00
alepouna
31df222798 Update docker-registry.yaml 2024-10-03 04:04:37 +03:00
Andras Bacsai
e91939a4ea refactor: Remove unnecessary watch command from soketi service entrypoint 2024-10-02 21:24:09 +02:00
Andras Bacsai
ceccd093d2 refactor: Improve socket reconnection interval in terminal.js 2024-10-02 21:24:05 +02:00
Andras Bacsai
66bb4e0fc1 refactor: Remove inactivity timer in terminal-server.js 2024-10-02 21:23:59 +02:00
Andras Bacsai
dd3ff38df7 refactor: Encode delimiter in SshMultiplexingHelper 2024-10-02 21:23:46 +02:00
Andras Bacsai
69553ec314 refactor: Improve popup component styling and button behavior 2024-10-02 18:26:51 +02:00
Andras Bacsai
7bb1bf0ae3 refactor: Improve parsing of commands for sudo in parseCommandsByLineForSudo 2024-10-02 18:26:40 +02:00
Andras Bacsai
a1a8f1336a refactor: Fix indentation in modal-confirmation.blade.php 2024-10-02 17:35:48 +02:00
Andras Bacsai
207fe1d709 wtf wtf wtf 2024-10-02 17:27:02 +02:00
Andras Bacsai
059535a676 chore: Remove commented out code for uploading to S3 in DatabaseBackupJob 2024-10-02 16:43:01 +02:00
Andras Bacsai
765a74ca4f handle errors in databasebackupjob 2024-10-02 15:33:14 +02:00
Andras Bacsai
e03e4f2e91 refactor: Improve SSH command generation in Terminal.php and terminal-server.js 2024-10-02 15:16:55 +02:00
Andras Bacsai
0ab432d5e6 chore: Remove unnecessary command from SshMultiplexingHelper 2024-10-02 14:54:48 +02:00
Andras Bacsai
6f25b548c7 fix: realtime watch in development mode 2024-10-02 14:18:52 +02:00
Andras Bacsai
d55e4bf381 feat: Handle HTTPS domain in ConfigureCloudflareTunnels 2024-10-02 13:36:25 +02:00
Andras Bacsai
ec216254b5 chore: Update modal input in server form to prevent closing on outside click 2024-10-02 13:36:09 +02:00
Andras Bacsai
fbb36bfe8e revert modal-confirmation in dev 2024-10-02 12:01:12 +02:00
Andras Bacsai
dd782e75f5 fix: local dev s3 uploads
fix: hetzner s3 uploads (mc alias instead of mc host)
2024-10-02 11:45:30 +02:00
Andras Bacsai
2be2f0ac79 feat: support Hetzner S3 2024-10-02 10:25:45 +02:00
Andras Bacsai
97943db5f4 chore: Add missing import for Attribute class in ApplicationDeploymentQueue model 2024-10-02 09:21:54 +02:00
Andras Bacsai
bbd2748ad7 chore: Update command signature and description for cleanup application deployment queue 2024-10-02 09:21:50 +02:00
Andras Bacsai
a530804a71 feat: Add command to check application deployment queue 2024-10-02 09:21:28 +02:00
Andras Bacsai
8ca8ab82b0 refactor: Remove deployment queue when deleting an application 2024-10-02 09:20:49 +02:00
Andras Bacsai
024ad8e943 fix: cleanup stucked applicationdeploymentqueue 2024-10-02 09:20:08 +02:00
Andras Bacsai
4d86b556a4 fix: ipv6 scp should use -6 flag 2024-10-02 08:15:03 +02:00
Andras Bacsai
73e38ff951 chore: Update version numbers to 4.0.0-beta.350 in configuration files 2024-10-02 08:13:49 +02:00
Andras Bacsai
5354561c39 Merge pull request #3659 from coollabsio/next
v4.0.0-beta.349
2024-10-01 21:36:50 +02:00
Andras Bacsai
223cd37031 fix: remove autofocuses 2024-10-01 21:36:16 +02:00
Andras Bacsai
93a0725a4e Merge pull request #3613 from nfnot/main
refactor: Update search input placeholder in resource index view
2024-10-01 21:32:21 +02:00
Andras Bacsai
4d1d4598b6 chore: Update version numbers to 4.0.0-beta.349 and 4.0.0-beta.350 2024-10-01 21:31:54 +02:00
Norman
888e96448c Merge branch 'main' into main 2024-10-01 13:33:02 +03:00
Norman
b84576ce87 Merge branch 'main' into main 2024-09-28 18:18:47 +03:00
Norman
f2a9a04461 refactor: Update search input placeholder in resource index view 2024-09-28 01:14:54 +00:00
75 changed files with 763 additions and 272 deletions

View File

@@ -0,0 +1,50 @@
<?php
namespace App\Console\Commands;
use App\Enums\ApplicationDeploymentStatus;
use App\Models\ApplicationDeploymentQueue;
use Illuminate\Console\Command;
class CheckApplicationDeploymentQueue extends Command
{
protected $signature = 'check:deployment-queue {--force} {--seconds=3600}';
protected $description = 'Check application deployment queue.';
public function handle()
{
$seconds = $this->option('seconds');
$deployments = ApplicationDeploymentQueue::whereIn('status', [
ApplicationDeploymentStatus::IN_PROGRESS,
ApplicationDeploymentStatus::QUEUED,
])->where('created_at', '>=', now()->subSeconds($seconds))->get();
if ($deployments->isEmpty()) {
$this->info('No deployments found in the last '.$seconds.' seconds.');
return;
}
$this->info('Found '.$deployments->count().' deployments created in the last '.$seconds.' seconds.');
foreach ($deployments as $deployment) {
if ($this->option('force')) {
$this->info('Deployment '.$deployment->id.' created at '.$deployment->created_at.' is older than '.$seconds.' seconds. Setting status to failed.');
$this->cancelDeployment($deployment);
} else {
$this->info('Deployment '.$deployment->id.' created at '.$deployment->created_at.' is older than '.$seconds.' seconds. Setting status to failed.');
if ($this->confirm('Do you want to cancel this deployment?', true)) {
$this->cancelDeployment($deployment);
}
}
}
}
private function cancelDeployment(ApplicationDeploymentQueue $deployment)
{
$deployment->update(['status' => ApplicationDeploymentStatus::FAILED]);
if ($deployment->server?->isFunctional()) {
remote_process(['docker rm -f '.$deployment->deployment_uuid], $deployment->server, false);
}
}
}

View File

@@ -7,9 +7,9 @@ use Illuminate\Console\Command;
class CleanupApplicationDeploymentQueue extends Command class CleanupApplicationDeploymentQueue extends Command
{ {
protected $signature = 'cleanup:application-deployment-queue {--team-id=}'; protected $signature = 'cleanup:deployment-queue {--team-id=}';
protected $description = 'CleanupApplicationDeploymentQueue'; protected $description = 'Cleanup application deployment queue.';
public function handle() public function handle()
{ {

View File

@@ -4,6 +4,7 @@ namespace App\Console\Commands;
use App\Jobs\CleanupHelperContainersJob; use App\Jobs\CleanupHelperContainersJob;
use App\Models\Application; use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
use App\Models\ApplicationPreview; use App\Models\ApplicationPreview;
use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledDatabaseBackup;
use App\Models\ScheduledTask; use App\Models\ScheduledTask;
@@ -47,6 +48,17 @@ class CleanupStuckedResources extends Command
} catch (\Throwable $e) { } catch (\Throwable $e) {
echo "Error in cleaning stucked resources: {$e->getMessage()}\n"; echo "Error in cleaning stucked resources: {$e->getMessage()}\n";
} }
try {
$applicationsDeploymentQueue = ApplicationDeploymentQueue::get();
foreach ($applicationsDeploymentQueue as $applicationDeploymentQueue) {
if (is_null($applicationDeploymentQueue->application)) {
echo "Deleting stuck application deployment queue: {$applicationDeploymentQueue->id}\n";
$applicationDeploymentQueue->delete();
}
}
} catch (\Throwable $e) {
echo "Error in cleaning stuck application deployment queue: {$e->getMessage()}\n";
}
try { try {
$applications = Application::withTrashed()->whereNotNull('deleted_at')->get(); $applications = Application::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($applications as $application) { foreach ($applications as $application) {

View File

@@ -94,7 +94,9 @@ class SshMultiplexingHelper
$muxPersistTime = config('constants.ssh.mux_persist_time'); $muxPersistTime = config('constants.ssh.mux_persist_time');
$scp_command = "timeout $timeout scp "; $scp_command = "timeout $timeout scp ";
if ($server->isIpv6()) {
$scp_command .= '-6 ';
}
if (self::isMultiplexingEnabled()) { if (self::isMultiplexingEnabled()) {
$scp_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} "; $scp_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
self::ensureMultiplexedConnection($server); self::ensureMultiplexedConnection($server);
@@ -136,8 +138,8 @@ class SshMultiplexingHelper
$ssh_command .= self::getCommonSshOptions($server, $sshKeyLocation, config('constants.ssh.connection_timeout'), config('constants.ssh.server_interval')); $ssh_command .= self::getCommonSshOptions($server, $sshKeyLocation, config('constants.ssh.connection_timeout'), config('constants.ssh.server_interval'));
$command = "PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/host/usr/local/sbin:/host/usr/local/bin:/host/usr/sbin:/host/usr/bin:/host/sbin:/host/bin && $command";
$delimiter = Hash::make($command); $delimiter = Hash::make($command);
$delimiter = base64_encode($delimiter);
$command = str_replace($delimiter, '', $command); $command = str_replace($delimiter, '', $command);
$ssh_command .= "{$server->user}@{$server->ip} 'bash -se' << \\$delimiter".PHP_EOL $ssh_command .= "{$server->user}@{$server->ip} 'bash -se' << \\$delimiter".PHP_EOL

View File

@@ -744,8 +744,10 @@ class ApplicationsController extends Controller
$application->destination_id = $destination->id; $application->destination_id = $destination->id;
$application->destination_type = $destination->getMorphClass(); $application->destination_type = $destination->getMorphClass();
$application->environment_id = $environment->id; $application->environment_id = $environment->id;
$application->settings->is_build_server_enabled = $useBuildServer; if (isset($useBuildServer)) {
$application->settings->save(); $application->settings->is_build_server_enabled = $useBuildServer;
$application->settings->save();
}
$application->save(); $application->save();
$application->refresh(); $application->refresh();
if (! $application->settings->is_container_label_readonly_enabled) { if (! $application->settings->is_container_label_readonly_enabled) {
@@ -842,8 +844,10 @@ class ApplicationsController extends Controller
$application->environment_id = $environment->id; $application->environment_id = $environment->id;
$application->source_type = $githubApp->getMorphClass(); $application->source_type = $githubApp->getMorphClass();
$application->source_id = $githubApp->id; $application->source_id = $githubApp->id;
$application->settings->is_build_server_enabled = $useBuildServer; if (isset($useBuildServer)) {
$application->settings->save(); $application->settings->is_build_server_enabled = $useBuildServer;
$application->settings->save();
}
$application->save(); $application->save();
$application->refresh(); $application->refresh();
if (! $application->settings->is_container_label_readonly_enabled) { if (! $application->settings->is_container_label_readonly_enabled) {
@@ -936,8 +940,10 @@ class ApplicationsController extends Controller
$application->destination_id = $destination->id; $application->destination_id = $destination->id;
$application->destination_type = $destination->getMorphClass(); $application->destination_type = $destination->getMorphClass();
$application->environment_id = $environment->id; $application->environment_id = $environment->id;
$application->settings->is_build_server_enabled = $useBuildServer; if (isset($useBuildServer)) {
$application->settings->save(); $application->settings->is_build_server_enabled = $useBuildServer;
$application->settings->save();
}
$application->save(); $application->save();
$application->refresh(); $application->refresh();
if (! $application->settings->is_container_label_readonly_enabled) { if (! $application->settings->is_container_label_readonly_enabled) {
@@ -1017,8 +1023,10 @@ class ApplicationsController extends Controller
$application->destination_id = $destination->id; $application->destination_id = $destination->id;
$application->destination_type = $destination->getMorphClass(); $application->destination_type = $destination->getMorphClass();
$application->environment_id = $environment->id; $application->environment_id = $environment->id;
$application->settings->is_build_server_enabled = $useBuildServer; if (isset($useBuildServer)) {
$application->settings->save(); $application->settings->is_build_server_enabled = $useBuildServer;
$application->settings->save();
}
$application->git_repository = 'coollabsio/coolify'; $application->git_repository = 'coollabsio/coolify';
$application->git_branch = 'main'; $application->git_branch = 'main';
@@ -1077,8 +1085,10 @@ class ApplicationsController extends Controller
$application->destination_id = $destination->id; $application->destination_id = $destination->id;
$application->destination_type = $destination->getMorphClass(); $application->destination_type = $destination->getMorphClass();
$application->environment_id = $environment->id; $application->environment_id = $environment->id;
$application->settings->is_build_server_enabled = $useBuildServer; if (isset($useBuildServer)) {
$application->settings->save(); $application->settings->is_build_server_enabled = $useBuildServer;
$application->settings->save();
}
$application->git_repository = 'coollabsio/coolify'; $application->git_repository = 'coollabsio/coolify';
$application->git_branch = 'main'; $application->git_branch = 'main';
@@ -1555,8 +1565,11 @@ class ApplicationsController extends Controller
$instantDeploy = $request->instant_deploy; $instantDeploy = $request->instant_deploy;
$use_build_server = $request->use_build_server; $use_build_server = $request->use_build_server;
$application->settings->is_build_server_enabled = $use_build_server;
$application->settings->save(); if (isset($use_build_server)) {
$application->settings->is_build_server_enabled = $use_build_server;
$application->settings->save();
}
removeUnnecessaryFieldsFromRequest($request); removeUnnecessaryFieldsFromRequest($request);

View File

@@ -2,7 +2,6 @@
namespace App\Jobs; namespace App\Jobs;
use App\Actions\Database\StopDatabase;
use App\Events\BackupCreated; use App\Events\BackupCreated;
use App\Models\S3Storage; use App\Models\S3Storage;
use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledDatabaseBackup;
@@ -24,7 +23,6 @@ use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Visus\Cuid2\Cuid2;
class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
{ {
@@ -63,31 +61,32 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
public function __construct($backup) public function __construct($backup)
{ {
$this->backup = $backup; $this->backup = $backup;
$this->team = Team::find($backup->team_id);
if (is_null($this->team)) {
return;
}
if (data_get($this->backup, 'database_type') === 'App\Models\ServiceDatabase') {
$this->database = data_get($this->backup, 'database');
$this->server = $this->database->service->server;
$this->s3 = $this->backup->s3;
} else {
$this->database = data_get($this->backup, 'database');
$this->server = $this->database->destination->server;
$this->s3 = $this->backup->s3;
}
} }
public function handle(): void public function handle(): void
{ {
try { try {
// Check if team is exists $this->team = Team::find($this->backup->team_id);
if (is_null($this->team)) { if (! $this->team) {
StopDatabase::run($this->database); $this->backup->delete();
$this->database->delete();
return; return;
} }
if (data_get($this->backup, 'database_type') === 'App\Models\ServiceDatabase') {
$this->database = data_get($this->backup, 'database');
$this->server = $this->database->service->server;
$this->s3 = $this->backup->s3;
} else {
$this->database = data_get($this->backup, 'database');
$this->server = $this->database->destination->server;
$this->s3 = $this->backup->s3;
}
if (is_null($this->server)) {
throw new \Exception('Server not found?!');
}
if (is_null($this->database)) {
throw new \Exception('Database not found?!');
}
BackupCreated::dispatch($this->team->id); BackupCreated::dispatch($this->team->id);
@@ -237,7 +236,6 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
} }
} }
$this->backup_dir = backup_dir().'/databases/'.str($this->team->name)->slug().'-'.$this->team->id.'/'.$this->directory_name; $this->backup_dir = backup_dir().'/databases/'.str($this->team->name)->slug().'-'.$this->team->id.'/'.$this->directory_name;
if ($this->database->name === 'coolify-db') { if ($this->database->name === 'coolify-db') {
$databasesToBackup = ['coolify']; $databasesToBackup = ['coolify'];
$this->directory_name = $this->container_name = 'coolify-db'; $this->directory_name = $this->container_name = 'coolify-db';
@@ -250,6 +248,9 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
try { try {
if (str($databaseType)->contains('postgres')) { if (str($databaseType)->contains('postgres')) {
$this->backup_file = "/pg-dump-$database-".Carbon::now()->timestamp.'.dmp'; $this->backup_file = "/pg-dump-$database-".Carbon::now()->timestamp.'.dmp';
if ($this->backup->dump_all) {
$this->backup_file = '/pg-dump-all-'.Carbon::now()->timestamp.'.gz';
}
$this->backup_location = $this->backup_dir.$this->backup_file; $this->backup_location = $this->backup_dir.$this->backup_file;
$this->backup_log = ScheduledDatabaseBackupExecution::create([ $this->backup_log = ScheduledDatabaseBackupExecution::create([
'database_name' => $database, 'database_name' => $database,
@@ -278,6 +279,9 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
$this->backup_standalone_mongodb($database); $this->backup_standalone_mongodb($database);
} elseif (str($databaseType)->contains('mysql')) { } elseif (str($databaseType)->contains('mysql')) {
$this->backup_file = "/mysql-dump-$database-".Carbon::now()->timestamp.'.dmp'; $this->backup_file = "/mysql-dump-$database-".Carbon::now()->timestamp.'.dmp';
if ($this->backup->dump_all) {
$this->backup_file = '/mysql-dump-all-'.Carbon::now()->timestamp.'.gz';
}
$this->backup_location = $this->backup_dir.$this->backup_file; $this->backup_location = $this->backup_dir.$this->backup_file;
$this->backup_log = ScheduledDatabaseBackupExecution::create([ $this->backup_log = ScheduledDatabaseBackupExecution::create([
'database_name' => $database, 'database_name' => $database,
@@ -287,6 +291,9 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
$this->backup_standalone_mysql($database); $this->backup_standalone_mysql($database);
} elseif (str($databaseType)->contains('mariadb')) { } elseif (str($databaseType)->contains('mariadb')) {
$this->backup_file = "/mariadb-dump-$database-".Carbon::now()->timestamp.'.dmp'; $this->backup_file = "/mariadb-dump-$database-".Carbon::now()->timestamp.'.dmp';
if ($this->backup->dump_all) {
$this->backup_file = '/mariadb-dump-all-'.Carbon::now()->timestamp.'.gz';
}
$this->backup_location = $this->backup_dir.$this->backup_file; $this->backup_location = $this->backup_dir.$this->backup_file;
$this->backup_log = ScheduledDatabaseBackupExecution::create([ $this->backup_log = ScheduledDatabaseBackupExecution::create([
'database_name' => $database, 'database_name' => $database,
@@ -325,7 +332,9 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
send_internal_notification('DatabaseBackupJob failed with: '.$e->getMessage()); send_internal_notification('DatabaseBackupJob failed with: '.$e->getMessage());
throw $e; throw $e;
} finally { } finally {
BackupCreated::dispatch($this->team->id); if ($this->team) {
BackupCreated::dispatch($this->team->id);
}
} }
} }
@@ -384,7 +393,11 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
if ($this->postgres_password) { if ($this->postgres_password) {
$backupCommand .= " -e PGPASSWORD=$this->postgres_password"; $backupCommand .= " -e PGPASSWORD=$this->postgres_password";
} }
$backupCommand .= " $this->container_name pg_dump --format=custom --no-acl --no-owner --username {$this->database->postgres_user} $database > $this->backup_location"; if ($this->backup->dump_all) {
$backupCommand .= " $this->container_name pg_dumpall --username {$this->database->postgres_user} | gzip > $this->backup_location";
} else {
$backupCommand .= " $this->container_name pg_dump --format=custom --no-acl --no-owner --username {$this->database->postgres_user} $database > $this->backup_location";
}
$commands[] = $backupCommand; $commands[] = $backupCommand;
ray($commands); ray($commands);
@@ -405,8 +418,11 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
{ {
try { try {
$commands[] = 'mkdir -p '.$this->backup_dir; $commands[] = 'mkdir -p '.$this->backup_dir;
$commands[] = "docker exec $this->container_name mysqldump -u root -p{$this->database->mysql_root_password} $database > $this->backup_location"; if ($this->backup->dump_all) {
ray($commands); $commands[] = "docker exec $this->container_name mysqldump -u root -p{$this->database->mysql_root_password} --all-databases --single-transaction --quick --lock-tables=false --compress | gzip > $this->backup_location";
} else {
$commands[] = "docker exec $this->container_name mysqldump -u root -p{$this->database->mysql_root_password} $database > $this->backup_location";
}
$this->backup_output = instant_remote_process($commands, $this->server); $this->backup_output = instant_remote_process($commands, $this->server);
$this->backup_output = trim($this->backup_output); $this->backup_output = trim($this->backup_output);
if ($this->backup_output === '') { if ($this->backup_output === '') {
@@ -424,7 +440,11 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
{ {
try { try {
$commands[] = 'mkdir -p '.$this->backup_dir; $commands[] = 'mkdir -p '.$this->backup_dir;
$commands[] = "docker exec $this->container_name mariadb-dump -u root -p{$this->database->mariadb_root_password} $database > $this->backup_location"; if ($this->backup->dump_all) {
$commands[] = "docker exec $this->container_name mariadb-dump -u root -p{$this->database->mariadb_root_password} --all-databases --single-transaction --quick --lock-tables=false --compress > $this->backup_location";
} else {
$commands[] = "docker exec $this->container_name mariadb-dump -u root -p{$this->database->mariadb_root_password} $database > $this->backup_location";
}
ray($commands); ray($commands);
$this->backup_output = instant_remote_process($commands, $this->server); $this->backup_output = instant_remote_process($commands, $this->server);
$this->backup_output = trim($this->backup_output); $this->backup_output = trim($this->backup_output);
@@ -466,34 +486,6 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
} }
} }
// private function upload_to_s3(): void
// {
// try {
// if (is_null($this->s3)) {
// return;
// }
// $key = $this->s3->key;
// $secret = $this->s3->secret;
// // $region = $this->s3->region;
// $bucket = $this->s3->bucket;
// $endpoint = $this->s3->endpoint;
// $this->s3->testConnection(shouldSave: true);
// $configName = new Cuid2;
// $s3_copy_dir = str($this->backup_location)->replace(backup_dir(), '/var/www/html/storage/app/backups/');
// $commands[] = "docker exec coolify bash -c 'mc config host add {$configName} {$endpoint} $key $secret'";
// $commands[] = "docker exec coolify bash -c 'mc cp $s3_copy_dir {$configName}/{$bucket}{$this->backup_dir}/'";
// instant_remote_process($commands, $this->server);
// $this->add_to_backup_output('Uploaded to S3.');
// } catch (\Throwable $e) {
// $this->add_to_backup_output($e->getMessage());
// throw $e;
// } finally {
// $removeConfigCommands[] = "docker exec coolify bash -c 'mc config remove {$configName}'";
// $removeConfigCommands[] = "docker exec coolify bash -c 'mc alias rm {$configName}'";
// instant_remote_process($removeConfigCommands, $this->server, false);
// }
// }
private function upload_to_s3(): void private function upload_to_s3(): void
{ {
try { try {
@@ -515,10 +507,27 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
$this->ensureHelperImageAvailable(); $this->ensureHelperImageAvailable();
$fullImageName = $this->getFullImageName(); $fullImageName = $this->getFullImageName();
$commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $this->backup_location:$this->backup_location:ro {$fullImageName}";
$commands[] = "docker exec backup-of-{$this->backup->uuid} mc config host add temporary {$endpoint} $key $secret"; if (isDev()) {
if ($this->database->name === 'coolify-db') {
$backup_location_from = '/var/lib/docker/volumes/coolify_dev_backups_data/_data/coolify/coolify-db-'.$this->server->ip.$this->backup_file;
$commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $backup_location_from:$this->backup_location:ro {$fullImageName}";
} else {
$backup_location_from = '/var/lib/docker/volumes/coolify_dev_backups_data/_data/databases/'.str($this->team->name)->slug().'-'.$this->team->id.'/'.$this->directory_name.$this->backup_file;
$commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $backup_location_from:$this->backup_location:ro {$fullImageName}";
}
} else {
$commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $this->backup_location:$this->backup_location:ro {$fullImageName}";
}
if ($this->s3->isHetzner()) {
$endpointWithoutBucket = 'https://'.str($endpoint)->after('https://')->after('.')->value();
$commands[] = "docker exec backup-of-{$this->backup->uuid} mc alias set --path=off --api=S3v4 temporary {$endpointWithoutBucket} $key $secret";
} else {
$commands[] = "docker exec backup-of-{$this->backup->uuid} mc config host add temporary {$endpoint} $key $secret";
}
$commands[] = "docker exec backup-of-{$this->backup->uuid} mc cp $this->backup_location temporary/$bucket{$this->backup_dir}/"; $commands[] = "docker exec backup-of-{$this->backup->uuid} mc cp $this->backup_location temporary/$bucket{$this->backup_dir}/";
instant_remote_process($commands, $this->server); instant_remote_process($commands, $this->server);
$this->add_to_backup_output('Uploaded to S3.'); $this->add_to_backup_output('Uploaded to S3.');
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->add_to_backup_output($e->getMessage()); $this->add_to_backup_output($e->getMessage());

View File

@@ -30,7 +30,7 @@ class Dashboard extends Component
public function cleanup_queue() public function cleanup_queue()
{ {
Artisan::queue('cleanup:application-deployment-queue', [ Artisan::queue('cleanup:deployment-queue', [
'--team-id' => currentTeam()->id, '--team-id' => currentTeam()->id,
]); ]);
} }

View File

@@ -31,6 +31,7 @@ class BackupEdit extends Component
'backup.save_s3' => 'required|boolean', 'backup.save_s3' => 'required|boolean',
'backup.s3_storage_id' => 'nullable|integer', 'backup.s3_storage_id' => 'nullable|integer',
'backup.databases_to_backup' => 'nullable', 'backup.databases_to_backup' => 'nullable',
'backup.dump_all' => 'required|boolean',
]; ];
protected $validationAttributes = [ protected $validationAttributes = [
@@ -40,6 +41,7 @@ class BackupEdit extends Component
'backup.save_s3' => 'Save to S3', 'backup.save_s3' => 'Save to S3',
'backup.s3_storage_id' => 'S3 Storage', 'backup.s3_storage_id' => 'S3 Storage',
'backup.databases_to_backup' => 'Databases to Backup', 'backup.databases_to_backup' => 'Databases to Backup',
'backup.dump_all' => 'Backup All Databases',
]; ];
protected $messages = [ protected $messages = [

View File

@@ -108,6 +108,21 @@ class Navbar extends Component
return; return;
} }
StopService::run(service: $this->service, dockerCleanup: false);
$this->service->parse();
$this->dispatch('imagePulled');
$activity = StartService::run($this->service);
$this->dispatch('activityMonitor', $activity->id);
}
public function pullAndRestartEvent()
{
$this->checkDeployments();
if ($this->isDeploymentProgress) {
$this->dispatch('error', 'There is a deployment in progress.');
return;
}
PullImage::run($this->service); PullImage::run($this->service);
StopService::run(service: $this->service, dockerCleanup: false); StopService::run(service: $this->service, dockerCleanup: false);
$this->service->parse(); $this->service->parse();

View File

@@ -34,9 +34,9 @@ class Terminal extends Component
if ($status !== 'running') { if ($status !== 'running') {
return; return;
} }
$command = SshMultiplexingHelper::generateSshCommand($server, "docker exec -it {$identifier} sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'"); $command = SshMultiplexingHelper::generateSshCommand($server, "docker exec -it {$identifier} sh -c 'PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin && if [ -f ~/.profile ]; then . ~/.profile; fi && if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'");
} else { } else {
$command = SshMultiplexingHelper::generateSshCommand($server, "sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'"); $command = SshMultiplexingHelper::generateSshCommand($server, 'PATH=$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin && if [ -f ~/.profile ]; then . ~/.profile; fi && if [ -n "$SHELL" ]; then exec $SHELL; else sh; fi');
} }
// ssh command is sent back to frontend then to websocket // ssh command is sent back to frontend then to websocket

View File

@@ -15,6 +15,8 @@ class ApiTokens extends Component
public bool $readOnly = true; public bool $readOnly = true;
public bool $rootAccess = false;
public array $permissions = ['read-only']; public array $permissions = ['read-only'];
public $isApiEnabled; public $isApiEnabled;
@@ -35,12 +37,11 @@ class ApiTokens extends Component
if ($this->viewSensitiveData) { if ($this->viewSensitiveData) {
$this->permissions[] = 'view:sensitive'; $this->permissions[] = 'view:sensitive';
$this->permissions = array_diff($this->permissions, ['*']); $this->permissions = array_diff($this->permissions, ['*']);
$this->rootAccess = false;
} else { } else {
$this->permissions = array_diff($this->permissions, ['view:sensitive']); $this->permissions = array_diff($this->permissions, ['view:sensitive']);
} }
if (count($this->permissions) == 0) { $this->makeSureOneIsSelected();
$this->permissions = ['*'];
}
} }
public function updatedReadOnly() public function updatedReadOnly()
@@ -48,11 +49,30 @@ class ApiTokens extends Component
if ($this->readOnly) { if ($this->readOnly) {
$this->permissions[] = 'read-only'; $this->permissions[] = 'read-only';
$this->permissions = array_diff($this->permissions, ['*']); $this->permissions = array_diff($this->permissions, ['*']);
$this->rootAccess = false;
} else { } else {
$this->permissions = array_diff($this->permissions, ['read-only']); $this->permissions = array_diff($this->permissions, ['read-only']);
} }
if (count($this->permissions) == 0) { $this->makeSureOneIsSelected();
}
public function updatedRootAccess()
{
if ($this->rootAccess) {
$this->permissions = ['*']; $this->permissions = ['*'];
$this->readOnly = false;
$this->viewSensitiveData = false;
} else {
$this->readOnly = true;
$this->permissions = ['read-only'];
}
}
public function makeSureOneIsSelected()
{
if (count($this->permissions) == 0) {
$this->permissions = ['read-only'];
$this->readOnly = true;
} }
} }
@@ -62,12 +82,6 @@ class ApiTokens extends Component
$this->validate([ $this->validate([
'description' => 'required|min:3|max:255', 'description' => 'required|min:3|max:255',
]); ]);
// if ($this->viewSensitiveData) {
// $this->permissions[] = 'view:sensitive';
// }
// if ($this->readOnly) {
// $this->permissions[] = 'read-only';
// }
$token = auth()->user()->createToken($this->description, $this->permissions); $token = auth()->user()->createToken($this->description, $this->permissions);
$this->tokens = auth()->user()->tokens; $this->tokens = auth()->user()->tokens;
session()->flash('token', $token->plainTextToken); session()->flash('token', $token->plainTextToken);

View File

@@ -30,6 +30,11 @@ class ConfigureCloudflareTunnels extends Component
public function submit() public function submit()
{ {
try { try {
if (str($this->ssh_domain)->contains('https://')) {
$this->ssh_domain = str($this->ssh_domain)->replace('https://', '')->replace('http://', '')->trim();
// remove / from the end
$this->ssh_domain = str($this->ssh_domain)->replace('/', '');
}
$server = Server::ownedByCurrentTeam()->where('id', $this->server_id)->firstOrFail(); $server = Server::ownedByCurrentTeam()->where('id', $this->server_id)->firstOrFail();
ConfigureCloudflared::dispatch($server, $this->cloudflare_token); ConfigureCloudflared::dispatch($server, $this->cloudflare_token);
$server->settings->is_cloudflare_tunnel = true; $server->settings->is_cloudflare_tunnel = true;

View File

@@ -43,15 +43,17 @@ class Create extends Component
'endpoint' => 'Endpoint', 'endpoint' => 'Endpoint',
]; ];
public function mount() public function updatedEndpoint($value)
{ {
if (isDev()) { if (! str($value)->startsWith('https://') && ! str($value)->startsWith('http://')) {
$this->name = 'Local MinIO'; $this->endpoint = 'https://'.$value;
$this->description = 'Local MinIO'; $value = $this->endpoint;
$this->key = 'minioadmin'; }
$this->secret = 'minioadmin';
$this->bucket = 'local'; if (str($value)->contains('your-objectstorage.com') && ! isset($this->bucket)) {
$this->endpoint = 'http://coolify-minio:9000'; $this->bucket = str($value)->after('//')->before('.');
} elseif (str($value)->contains('your-objectstorage.com')) {
$this->bucket = $this->bucket ?: str($value)->after('//')->before('.');
} }
} }

View File

@@ -143,6 +143,9 @@ class Application extends BaseModel
} }
$application->tags()->detach(); $application->tags()->detach();
$application->previews()->delete(); $application->previews()->delete();
foreach ($application->deployment_queue as $deployment) {
$deployment->delete();
}
}); });
} }
@@ -710,6 +713,11 @@ class Application extends BaseModel
return $this->hasMany(ApplicationPreview::class); return $this->hasMany(ApplicationPreview::class);
} }
public function deployment_queue()
{
return $this->hasMany(ApplicationDeploymentQueue::class);
}
public function destination() public function destination()
{ {
return $this->morphTo(); return $this->morphTo();

View File

@@ -2,6 +2,7 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
@@ -39,6 +40,20 @@ class ApplicationDeploymentQueue extends Model
{ {
protected $guarded = []; protected $guarded = [];
public function application(): Attribute
{
return Attribute::make(
get: fn () => Application::find($this->application_id),
);
}
public function server(): Attribute
{
return Attribute::make(
get: fn () => Server::find($this->server_id),
);
}
public function setStatus(string $status) public function setStatus(string $status)
{ {
$this->update([ $this->update([

View File

@@ -40,6 +40,16 @@ class S3Storage extends BaseModel
return "{$this->endpoint}/{$this->bucket}"; return "{$this->endpoint}/{$this->bucket}";
} }
public function isHetzner()
{
return str($this->endpoint)->contains('your-objectstorage.com');
}
public function isDigitalOcean()
{
return str($this->endpoint)->contains('digitaloceanspaces.com');
}
public function testConnection(bool $shouldSave = false) public function testConnection(bool $shouldSave = false)
{ {
try { try {

View File

@@ -1221,4 +1221,9 @@ $schema://$host {
return instant_remote_process($commands, $this, false); return instant_remote_process($commands, $this, false);
} }
public function isIpv6(): bool
{
return str($this->ip)->contains(':');
}
} }

View File

@@ -770,9 +770,34 @@ class Service extends BaseModel
} }
$fields->put('Code Server', $data->toArray()); $fields->put('Code Server', $data->toArray());
break; break;
case str($image)->contains('elestio/strapi'):
$data = collect([]);
$license = $this->environment_variables()->where('key', 'STRAPI_LICENSE')->first();
if ($license) {
$data = $data->merge([
'License' => [
'key' => data_get($license, 'key'),
'value' => data_get($license, 'value'),
],
]);
}
$nodeEnv = $this->environment_variables()->where('key', 'NODE_ENV')->first();
if ($nodeEnv) {
$data = $data->merge([
'Node Environment' => [
'key' => data_get($nodeEnv, 'key'),
'value' => data_get($nodeEnv, 'value'),
],
]);
}
$fields->put('Strapi', $data->toArray());
break;
} }
} }
$databases = $this->databases()->get(); $databases = $this->databases()->get();
ray($databases);
foreach ($databases as $database) { foreach ($databases as $database) {
$image = str($database->image)->before(':')->value(); $image = str($database->image)->before(':')->value();
@@ -1108,7 +1133,6 @@ class Service extends BaseModel
$real_value = escapeEnvVariables($env->real_value); $real_value = escapeEnvVariables($env->real_value);
} }
} }
ray("echo \"{$env->key}={$real_value}\" >> .env");
$commands[] = "echo \"{$env->key}={$real_value}\" >> .env"; $commands[] = "echo \"{$env->key}={$real_value}\" >> .env";
} }
} }

View File

@@ -112,4 +112,9 @@ class ServiceApplication extends BaseModel
{ {
getFilesystemVolumesFromServer($this, $isInit); getFilesystemVolumesFromServer($this, $isInit);
} }
public function isBackupSolutionAvailable()
{
return false;
}
} }

View File

@@ -115,4 +115,13 @@ class ServiceDatabase extends BaseModel
{ {
return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
} }
public function isBackupSolutionAvailable()
{
return str($this->databaseType())->contains('mysql') ||
str($this->databaseType())->contains('postgres') ||
str($this->databaseType())->contains('postgis') ||
str($this->databaseType())->contains('mariadb') ||
str($this->databaseType())->contains('mongodb');
}
} }

View File

@@ -294,4 +294,9 @@ class StandaloneClickhouse extends BaseModel
return $parsedCollection->toArray(); return $parsedCollection->toArray();
} }
} }
public function isBackupSolutionAvailable()
{
return false;
}
} }

View File

@@ -294,4 +294,9 @@ class StandaloneDragonfly extends BaseModel
return $parsedCollection->toArray(); return $parsedCollection->toArray();
} }
} }
public function isBackupSolutionAvailable()
{
return false;
}
} }

View File

@@ -294,4 +294,9 @@ class StandaloneKeydb extends BaseModel
return $parsedCollection->toArray(); return $parsedCollection->toArray();
} }
} }
public function isBackupSolutionAvailable()
{
return false;
}
} }

View File

@@ -294,4 +294,9 @@ class StandaloneMariadb extends BaseModel
return $parsedCollection->toArray(); return $parsedCollection->toArray();
} }
} }
public function isBackupSolutionAvailable()
{
return true;
}
} }

View File

@@ -314,4 +314,9 @@ class StandaloneMongodb extends BaseModel
return $parsedCollection->toArray(); return $parsedCollection->toArray();
} }
} }
public function isBackupSolutionAvailable()
{
return true;
}
} }

View File

@@ -295,4 +295,9 @@ class StandaloneMysql extends BaseModel
return $parsedCollection->toArray(); return $parsedCollection->toArray();
} }
} }
public function isBackupSolutionAvailable()
{
return true;
}
} }

View File

@@ -296,4 +296,9 @@ class StandalonePostgresql extends BaseModel
return $parsedCollection->toArray(); return $parsedCollection->toArray();
} }
} }
public function isBackupSolutionAvailable()
{
return true;
}
} }

View File

@@ -290,4 +290,9 @@ class StandaloneRedis extends BaseModel
return $parsedCollection->toArray(); return $parsedCollection->toArray();
} }
} }
public function isBackupSolutionAvailable()
{
return false;
}
} }

View File

@@ -20,12 +20,16 @@ const RESTART_MODE = 'unless-stopped';
const DATABASE_DOCKER_IMAGES = [ const DATABASE_DOCKER_IMAGES = [
'bitnami/mariadb', 'bitnami/mariadb',
'bitnami/mongodb', 'bitnami/mongodb',
'bitnami/mysql',
'bitnami/postgresql',
'bitnami/redis', 'bitnami/redis',
'mysql', 'mysql',
'bitnami/mysql',
'mysql/mysql-server',
'mariadb', 'mariadb',
'postgis/postgis',
'postgres', 'postgres',
'bitnami/postgresql',
'supabase/postgres',
'elestio/postgres',
'mongo', 'mongo',
'redis', 'redis',
'memcached', 'memcached',
@@ -33,7 +37,6 @@ const DATABASE_DOCKER_IMAGES = [
'neo4j', 'neo4j',
'influxdb', 'influxdb',
'clickhouse/clickhouse-server', 'clickhouse/clickhouse-server',
'supabase/postgres',
]; ];
const SPECIFIC_SERVICES = [ const SPECIFIC_SERVICES = [
'quay.io/minio/minio', 'quay.io/minio/minio',

View File

@@ -1,14 +1,11 @@
<?php <?php
use App\Models\S3Storage; use App\Models\S3Storage;
use Illuminate\Support\Str;
function set_s3_target(S3Storage $s3) function set_s3_target(S3Storage $s3)
{ {
$is_digital_ocean = false; $is_digital_ocean = false;
if ($s3->endpoint) {
$is_digital_ocean = Str::contains($s3->endpoint, 'digitaloceanspaces.com');
}
config()->set('filesystems.disks.custom-s3', [ config()->set('filesystems.disks.custom-s3', [
'driver' => 's3', 'driver' => 's3',
'region' => $s3['region'], 'region' => $s3['region'],
@@ -17,7 +14,7 @@ function set_s3_target(S3Storage $s3)
'bucket' => $s3['bucket'], 'bucket' => $s3['bucket'],
'endpoint' => $s3['endpoint'], 'endpoint' => $s3['endpoint'],
'use_path_style_endpoint' => true, 'use_path_style_endpoint' => true,
'bucket_endpoint' => $is_digital_ocean, 'bucket_endpoint' => $s3->isHetzner() || $s3->isDigitalOcean(),
'aws_url' => $s3->awsUrl(), 'aws_url' => $s3->awsUrl(),
]); ]);
} }

View File

@@ -708,7 +708,9 @@ function getTopLevelNetworks(Service|Application $resource)
return $value == $networkName || $key == $networkName; return $value == $networkName || $key == $networkName;
}); });
if (! $networkExists) { if (! $networkExists) {
$topLevelNetworks->put($networkDetails, null); if (is_string($networkDetails) || is_int($networkDetails)) {
$topLevelNetworks->put($networkDetails, null);
}
} }
} }
} }
@@ -758,7 +760,9 @@ function getTopLevelNetworks(Service|Application $resource)
return $value == $networkName || $key == $networkName; return $value == $networkName || $key == $networkName;
}); });
if (! $networkExists) { if (! $networkExists) {
$topLevelNetworks->put($networkDetails, null); if (is_string($networkDetails) || is_int($networkDetails)) {
$topLevelNetworks->put($networkDetails, null);
}
} }
} }
} }
@@ -1184,14 +1188,16 @@ function check_domain_usage(ServiceApplication|Application|null $resource = null
function parseCommandsByLineForSudo(Collection $commands, Server $server): array function parseCommandsByLineForSudo(Collection $commands, Server $server): array
{ {
$commands = $commands->map(function ($line) { $commands = $commands->map(function ($line) {
if (! str(trim($line))->startsWith([ if (
'cd', ! str(trim($line))->startsWith([
'command', 'cd',
'echo', 'command',
'true', 'echo',
'if', 'true',
'fi', 'if',
])) { 'fi',
])
) {
return "sudo $line"; return "sudo $line";
} }
@@ -1606,7 +1612,9 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
return $value == $networkName || $key == $networkName; return $value == $networkName || $key == $networkName;
}); });
if (! $networkExists) { if (! $networkExists) {
$topLevelNetworks->put($networkDetails, null); if (is_string($networkDetails) || is_int($networkDetails)) {
$topLevelNetworks->put($networkDetails, null);
}
} }
} }
} }
@@ -2521,7 +2529,9 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
return $value == $networkName || $key == $networkName; return $value == $networkName || $key == $networkName;
}); });
if (! $networkExists) { if (! $networkExists) {
$topLevelNetworks->put($networkDetails, null); if (is_string($networkDetails) || is_int($networkDetails)) {
$topLevelNetworks->put($networkDetails, null);
}
} }
} }
} }
@@ -2982,11 +2992,22 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
$predefinedPort = '8000'; $predefinedPort = '8000';
} }
if ($isDatabase) { if ($isDatabase) {
$savedService = ServiceDatabase::firstOrCreate([ $applicationFound = ServiceApplication::where('name', $serviceName)->where('image', $image)->where('service_id', $resource->id)->first();
'name' => $serviceName, if ($applicationFound) {
'image' => $image, $savedService = $applicationFound;
'service_id' => $resource->id, $savedService = ServiceDatabase::firstOrCreate([
]); 'name' => $applicationFound->name,
'image' => $applicationFound->image,
'service_id' => $applicationFound->service_id,
]);
$applicationFound->delete();
} else {
$savedService = ServiceDatabase::firstOrCreate([
'name' => $serviceName,
'image' => $image,
'service_id' => $resource->id,
]);
}
} else { } else {
$savedService = ServiceApplication::firstOrCreate([ $savedService = ServiceApplication::firstOrCreate([
'name' => $serviceName, 'name' => $serviceName,
@@ -3207,12 +3228,24 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
if ($serviceName === 'plausible') { if ($serviceName === 'plausible') {
$predefinedPort = '8000'; $predefinedPort = '8000';
} }
if ($isDatabase) { if ($isDatabase) {
$savedService = ServiceDatabase::firstOrCreate([ $applicationFound = ServiceApplication::where('name', $serviceName)->where('image', $image)->where('service_id', $resource->id)->first();
'name' => $serviceName, if ($applicationFound) {
'image' => $image, $savedService = $applicationFound;
'service_id' => $resource->id, $savedService = ServiceDatabase::firstOrCreate([
]); 'name' => $applicationFound->name,
'image' => $applicationFound->image,
'service_id' => $applicationFound->service_id,
]);
$applicationFound->delete();
} else {
$savedService = ServiceDatabase::firstOrCreate([
'name' => $serviceName,
'image' => $image,
'service_id' => $resource->id,
]);
}
} else { } else {
$savedService = ServiceApplication::firstOrCreate([ $savedService = ServiceApplication::firstOrCreate([
'name' => $serviceName, 'name' => $serviceName,
@@ -3863,14 +3896,19 @@ function convertComposeEnvironmentToArray($environment)
{ {
$convertedServiceVariables = collect([]); $convertedServiceVariables = collect([]);
if (isAssociativeArray($environment)) { if (isAssociativeArray($environment)) {
// Example: $environment = ['FOO' => 'bar', 'BAZ' => 'qux'];
if ($environment instanceof Collection) { if ($environment instanceof Collection) {
$changedEnvironment = collect([]); $changedEnvironment = collect([]);
$environment->each(function ($value, $key) use ($changedEnvironment) { $environment->each(function ($value, $key) use ($changedEnvironment) {
$parts = explode('=', $value, 2); if (is_numeric($key)) {
if (count($parts) === 2) { $parts = explode('=', $value, 2);
$key = $parts[0]; if (count($parts) === 2) {
$realValue = $parts[1] ?? ''; $key = $parts[0];
$changedEnvironment->put($key, $realValue); $realValue = $parts[1] ?? '';
$changedEnvironment->put($key, $realValue);
} else {
$changedEnvironment->put($key, $value);
}
} else { } else {
$changedEnvironment->put($key, $value); $changedEnvironment->put($key, $value);
} }
@@ -3880,12 +3918,15 @@ function convertComposeEnvironmentToArray($environment)
} }
$convertedServiceVariables = $environment; $convertedServiceVariables = $environment;
} else { } else {
// Example: $environment = ['FOO=bar', 'BAZ=qux'];
foreach ($environment as $value) { foreach ($environment as $value) {
$parts = explode('=', $value, 2); if (is_string($value)) {
$key = $parts[0]; $parts = explode('=', $value, 2);
$realValue = $parts[1] ?? ''; $key = $parts[0];
if ($key) { $realValue = $parts[1] ?? '';
$convertedServiceVariables->put($key, $realValue); if ($key) {
$convertedServiceVariables->put($key, $realValue);
}
} }
} }
} }

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.348', 'release' => '4.0.0-beta.352',
// When left empty or `null` the Laravel environment will be used // When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'), 'environment' => config('app.env'),

View File

@@ -1,3 +1,3 @@
<?php <?php
return '4.0.0-beta.348'; return '4.0.0-beta.352';

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('scheduled_database_backups', function (Blueprint $table) {
$table->boolean('dump_all')->default(false);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('scheduled_database_backups', function (Blueprint $table) {
$table->dropColumn('dump_all');
});
}
};

View File

@@ -58,6 +58,7 @@ services:
SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID:-coolify}" SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID:-coolify}"
SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY:-coolify}" SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY:-coolify}"
SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET:-coolify}" SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET:-coolify}"
entrypoint: ["/bin/sh", "/soketi-entrypoint.sh"]
vite: vite:
image: node:20 image: node:20
pull_policy: always pull_policy: always

View File

@@ -113,7 +113,7 @@ services:
retries: 10 retries: 10
timeout: 2s timeout: 2s
soketi: soketi:
image: 'ghcr.io/coollabsio/coolify-realtime:1.0.2' image: 'ghcr.io/coollabsio/coolify-realtime:1.0.3'
ports: ports:
- "${SOKETI_PORT:-6001}:6001" - "${SOKETI_PORT:-6001}:6001"
- "6002:6002" - "6002:6002"

View File

@@ -1,9 +1,27 @@
FROM quay.io/soketi/soketi:1.6-16-alpine FROM quay.io/soketi/soketi:1.6-16-alpine
ARG TARGETPLATFORM
# https://github.com/cloudflare/cloudflared/releases
ARG CLOUDFLARED_VERSION=2024.4.1
WORKDIR /terminal WORKDIR /terminal
RUN apk add --no-cache openssh-client make g++ python3 RUN apk add --no-cache openssh-client make g++ python3 curl
COPY docker/coolify-realtime/package.json ./ COPY docker/coolify-realtime/package.json ./
RUN npm i RUN npm i
RUN npm rebuild node-pty --update-binary RUN npm rebuild node-pty --update-binary
COPY docker/coolify-realtime/soketi-entrypoint.sh /soketi-entrypoint.sh COPY docker/coolify-realtime/soketi-entrypoint.sh /soketi-entrypoint.sh
COPY docker/coolify-realtime/terminal-server.js /terminal/terminal-server.js COPY docker/coolify-realtime/terminal-server.js /terminal/terminal-server.js
RUN /bin/sh -c "if [[ ${TARGETPLATFORM} == 'linux/amd64' ]]; then \
echo 'amd64' && \
curl -sSL https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared \
;fi"
RUN /bin/sh -c "if [[ ${TARGETPLATFORM} == 'linux/arm64' ]]; then \
echo 'arm64' && \
curl -L https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-arm64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared \
;fi"
ENTRYPOINT ["/bin/sh", "/soketi-entrypoint.sh"] ENTRYPOINT ["/bin/sh", "/soketi-entrypoint.sh"]

View File

@@ -1,11 +1,19 @@
#!/bin/sh #!/bin/sh
# Function to timestamp logs # Function to timestamp logs
# Check if the first argument is 'watch'
if [ "$1" = "watch" ]; then
WATCH_MODE="--watch"
else
WATCH_MODE=""
fi
timestamp() { timestamp() {
date "+%Y-%m-%d %H:%M:%S" date "+%Y-%m-%d %H:%M:%S"
} }
# Start the terminal server in the background with logging # Start the terminal server in the background with logging
node /terminal/terminal-server.js > >(while read line; do echo "$(timestamp) [TERMINAL] $line"; done) 2>&1 & node $WATCH_MODE /terminal/terminal-server.js > >(while read line; do echo "$(timestamp) [TERMINAL] $line"; done) 2>&1 &
TERMINAL_PID=$! TERMINAL_PID=$!
# Start the Soketi process in the background with logging # Start the Soketi process in the background with logging

View File

@@ -61,9 +61,13 @@ wss.on('connection', (ws) => {
const userSession = { ws, userId, ptyProcess: null, isActive: false }; const userSession = { ws, userId, ptyProcess: null, isActive: false };
userSessions.set(userId, userSession); userSessions.set(userId, userSession);
ws.on('message', (message) => handleMessage(userSession, message)); ws.on('message', (message) => {
handleMessage(userSession, message);
});
ws.on('error', (err) => handleError(err, userId)); ws.on('error', (err) => handleError(err, userId));
ws.on('close', () => handleClose(userId)); ws.on('close', () => handleClose(userId));
}); });
const messageHandlers = { const messageHandlers = {
@@ -108,7 +112,6 @@ function parseMessage(message) {
async function handleCommand(ws, command, userId) { async function handleCommand(ws, command, userId) {
const userSession = userSessions.get(userId); const userSession = userSessions.get(userId);
if (userSession && userSession.isActive) { if (userSession && userSession.isActive) {
const result = await killPtyProcess(userId); const result = await killPtyProcess(userId);
if (!result) { if (!result) {
@@ -127,6 +130,7 @@ async function handleCommand(ws, command, userId) {
cols: 80, cols: 80,
rows: 30, rows: 30,
cwd: process.env.HOME, cwd: process.env.HOME,
env: {},
}; };
// NOTE: - Initiates a process within the Terminal container // NOTE: - Initiates a process within the Terminal container
@@ -139,13 +143,16 @@ async function handleCommand(ws, command, userId) {
ws.send('pty-ready'); ws.send('pty-ready');
ptyProcess.onData((data) => ws.send(data)); ptyProcess.onData((data) => {
ws.send(data);
});
// when parent closes // when parent closes
ptyProcess.onExit(({ exitCode, signal }) => { ptyProcess.onExit(({ exitCode, signal }) => {
console.error(`Process exited with code ${exitCode} and signal ${signal}`); console.error(`Process exited with code ${exitCode} and signal ${signal}`);
ws.send('pty-exited'); ws.send('pty-exited');
userSession.isActive = false; userSession.isActive = false;
}); });
if (timeout) { if (timeout) {
@@ -179,7 +186,7 @@ async function killPtyProcess(userId) {
// session.ptyProcess.kill() wont work here because of https://github.com/moby/moby/issues/9098 // session.ptyProcess.kill() wont work here because of https://github.com/moby/moby/issues/9098
// patch with https://github.com/moby/moby/issues/9098#issuecomment-189743947 // patch with https://github.com/moby/moby/issues/9098#issuecomment-189743947
session.ptyProcess.write('kill -TERM -$$ && exit\n'); session.ptyProcess.write('set +o history\nkill -TERM -$$ && exit\nset -o history\n');
setTimeout(() => { setTimeout(() => {
if (!session.isActive || !session.ptyProcess) { if (!session.isActive || !session.ptyProcess) {
@@ -228,5 +235,5 @@ function extractHereDocContent(commandString) {
} }
server.listen(6002, () => { server.listen(6002, () => {
console.log('Server listening on port 6002'); console.log('Coolify realtime terminal server listening on port 6002. Let the hacking begin!');
}); });

View File

@@ -46,6 +46,9 @@ services:
- PUSHER_APP_ID - PUSHER_APP_ID
- PUSHER_APP_KEY - PUSHER_APP_KEY
- PUSHER_APP_SECRET - PUSHER_APP_SECRET
- TERMINAL_PROTOCOL
- TERMINAL_HOST
- TERMINAL_PORT
- AUTOUPDATE - AUTOUPDATE
- SELF_HOSTED - SELF_HOSTED
- SSH_MUX_ENABLED - SSH_MUX_ENABLED
@@ -110,7 +113,7 @@ services:
retries: 10 retries: 10
timeout: 2s timeout: 2s
soketi: soketi:
image: 'ghcr.io/coollabsio/coolify-realtime:1.0.2' image: 'ghcr.io/coollabsio/coolify-realtime:1.0.3'
ports: ports:
- "${SOKETI_PORT:-6001}:6001" - "${SOKETI_PORT:-6001}:6001"
- "6002:6002" - "6002:6002"

View File

@@ -1,16 +1,16 @@
{ {
"coolify": { "coolify": {
"v4": { "v4": {
"version": "4.0.0-beta.348" "version": "4.0.0-beta.350"
}, },
"nightly": { "nightly": {
"version": "4.0.0-beta.349" "version": "4.0.0-beta.351"
}, },
"helper": { "helper": {
"version": "1.0.1" "version": "1.0.1"
}, },
"realtime": { "realtime": {
"version": "1.0.2" "version": "1.0.3"
} }
} }
} }

15
public/svgs/bitcoin.svg Normal file
View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Creator: CorelDRAW 2019 (64-Bit) -->
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="100%" height="100%" version="1.1" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" image-rendering="optimizeQuality" fill-rule="evenodd" clip-rule="evenodd"
viewBox="0 0 4091.27 4091.73"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:xodm="http://www.corel.com/coreldraw/odm/2003">
<g id="Layer_x0020_1">
<metadata id="CorelCorpID_0Corel-Layer"/>
<g id="_1421344023328">
<path fill="#F7931A" fill-rule="nonzero" d="M4030.06 2540.77c-273.24,1096.01 -1383.32,1763.02 -2479.46,1489.71 -1095.68,-273.24 -1762.69,-1383.39 -1489.33,-2479.31 273.12,-1096.13 1383.2,-1763.19 2479,-1489.95 1096.06,273.24 1763.03,1383.51 1489.76,2479.57l0.02 -0.02z"/>
<path fill="white" fill-rule="nonzero" d="M2947.77 1754.38c40.72,-272.26 -166.56,-418.61 -450,-516.24l91.95 -368.8 -224.5 -55.94 -89.51 359.09c-59.02,-14.72 -119.63,-28.59 -179.87,-42.34l90.16 -361.46 -224.36 -55.94 -92 368.68c-48.84,-11.12 -96.81,-22.11 -143.35,-33.69l0.26 -1.16 -309.59 -77.31 -59.72 239.78c0,0 166.56,38.18 163.05,40.53 90.91,22.69 107.35,82.87 104.62,130.57l-104.74 420.15c6.26,1.59 14.38,3.89 23.34,7.49 -7.49,-1.86 -15.46,-3.89 -23.73,-5.87l-146.81 588.57c-11.11,27.62 -39.31,69.07 -102.87,53.33 2.25,3.26 -163.17,-40.72 -163.17,-40.72l-111.46 256.98 292.15 72.83c54.35,13.63 107.61,27.89 160.06,41.3l-92.9 373.03 224.24 55.94 92 -369.07c61.26,16.63 120.71,31.97 178.91,46.43l-91.69 367.33 224.51 55.94 92.89 -372.33c382.82,72.45 670.67,43.24 791.83,-303.02 97.63,-278.78 -4.86,-439.58 -206.26,-544.44 146.69,-33.83 257.18,-130.31 286.64,-329.61l-0.07 -0.05zm-512.93 719.26c-69.38,278.78 -538.76,128.08 -690.94,90.29l123.28 -494.2c152.17,37.99 640.17,113.17 567.67,403.91zm69.43 -723.3c-63.29,253.58 -453.96,124.75 -580.69,93.16l111.77 -448.21c126.73,31.59 534.85,90.55 468.94,355.05l-0.02 0z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

8
public/svgs/strapi.svg Normal file
View File

@@ -0,0 +1,8 @@
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 22.1867C0 11.7278 0 6.49832 3.24916 3.24916C6.49832 0 11.7278 0 22.1867 0H41.8133C52.2722 0 57.5017 0 60.7508 3.24916C64 6.49832 64 11.7278 64 22.1867V41.8133C64 52.2722 64 57.5017 60.7508 60.7508C57.5017 64 52.2722 64 41.8133 64H22.1867C11.7278 64 6.49832 64 3.24916 60.7508C0 57.5017 0 52.2722 0 41.8133V22.1867Z" fill="#4945FF"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M44.156 19.4131H22.6094V30.4004H33.596V41.3864H44.5827V19.8398C44.5827 19.6041 44.3917 19.4131 44.156 19.4131Z" fill="white"/>
<rect x="33.1719" y="30.4004" width="0.426667" height="0.426667" fill="white"/>
<path d="M22.6172 30.4004H33.1772C33.4128 30.4004 33.6039 30.5914 33.6039 30.8271V41.3871H23.0439C22.8082 41.3871 22.6172 41.196 22.6172 40.9604V30.4004Z" fill="#9593FF"/>
<path d="M33.6016 41.3867H44.5882L33.9657 52.0092C33.8314 52.1436 33.6016 52.0484 33.6016 51.8584V41.3867Z" fill="#9593FF"/>
<path d="M22.6151 30.3998H12.1434C11.9534 30.3998 11.8582 30.17 11.9926 30.0356L22.6151 19.4131V30.3998Z" fill="#9593FF"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -16,6 +16,7 @@ export function initializeTerminalComponent() {
paused: false, paused: false,
MAX_PENDING_WRITES: 5, MAX_PENDING_WRITES: 5,
keepAliveInterval: null, keepAliveInterval: null,
reconnectInterval: null,
init() { init() {
this.setupTerminal(); this.setupTerminal();
@@ -48,6 +49,9 @@ export function initializeTerminalComponent() {
document.addEventListener(event, () => { document.addEventListener(event, () => {
this.checkIfProcessIsRunningAndKillIt(); this.checkIfProcessIsRunningAndKillIt();
clearInterval(this.keepAliveInterval); clearInterval(this.keepAliveInterval);
if (this.reconnectInterval) {
clearInterval(this.reconnectInterval);
}
}, { once: true }); }, { once: true });
}); });
@@ -103,11 +107,27 @@ export function initializeTerminalComponent() {
}; };
this.socket.onclose = () => { this.socket.onclose = () => {
console.log('WebSocket connection closed'); console.log('WebSocket connection closed');
this.reconnect();
}; };
} }
}, },
reconnect() {
if (this.reconnectInterval) {
clearInterval(this.reconnectInterval);
}
this.reconnectInterval = setInterval(() => {
console.log('Attempting to reconnect...');
this.initializeWebSocket();
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
console.log('Reconnected successfully');
clearInterval(this.reconnectInterval);
this.reconnectInterval = null;
window.location.reload();
}
}, 2000);
},
handleSocketMessage(event) { handleSocketMessage(event) {
this.message = '(connection closed)'; this.message = '(connection closed)';
if (event.data === 'pty-ready') { if (event.data === 'pty-ready') {

View File

@@ -8,7 +8,7 @@
<div class="w-96"> <div class="w-96">
<form action="/user/confirm-password" method="POST" class="flex flex-col gap-2"> <form action="/user/confirm-password" method="POST" class="flex flex-col gap-2">
@csrf @csrf
<x-forms.input required type="password" name="password" label="{{ __('input.password') }}" autofocus /> <x-forms.input required type="password" name="password" label="{{ __('input.password') }}" />
<x-forms.button type="submit">{{ __('auth.confirm_password') }}</x-forms.button> <x-forms.button type="submit">{{ __('auth.confirm_password') }}</x-forms.button>
</form> </form>
@if ($errors->any()) @if ($errors->any())

View File

@@ -12,7 +12,7 @@
@if (is_transactional_emails_active()) @if (is_transactional_emails_active())
<form action="/forgot-password" method="POST" class="flex flex-col gap-2"> <form action="/forgot-password" method="POST" class="flex flex-col gap-2">
@csrf @csrf
<x-forms.input required type="email" name="email" label="{{ __('input.email') }}" autofocus /> <x-forms.input required type="email" name="email" label="{{ __('input.email') }}" />
<x-forms.button type="submit">{{ __('auth.forgot_password_send_email') }}</x-forms.button> <x-forms.button type="submit">{{ __('auth.forgot_password_send_email') }}</x-forms.button>
</form> </form>
@else @else

View File

@@ -10,7 +10,7 @@
@csrf @csrf
@env('local') @env('local')
<x-forms.input value="test@example.com" type="email" autocomplete="email" name="email" <x-forms.input value="test@example.com" type="email" autocomplete="email" name="email"
required label="{{ __('input.email') }}" autofocus /> required label="{{ __('input.email') }}" />
<x-forms.input value="password" type="password" autocomplete="current-password" name="password" <x-forms.input value="password" type="password" autocomplete="current-password" name="password"
required label="{{ __('input.password') }}" /> required label="{{ __('input.password') }}" />
@@ -20,7 +20,7 @@
</a> </a>
@else @else
<x-forms.input type="email" name="email" autocomplete="email" required <x-forms.input type="email" name="email" autocomplete="email" required
label="{{ __('input.email') }}" autofocus /> label="{{ __('input.email') }}" />
<x-forms.input type="password" name="password" autocomplete="current-password" required <x-forms.input type="password" name="password" autocomplete="current-password" required
label="{{ __('input.password') }}" /> label="{{ __('input.password') }}" />
<a href="/forgot-password" class="text-xs"> <a href="/forgot-password" class="text-xs">

View File

@@ -16,7 +16,7 @@
label="{{ __('input.email') }}" /> label="{{ __('input.email') }}" />
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<x-forms.input required type="password" id="password" name="password" <x-forms.input required type="password" id="password" name="password"
label="{{ __('input.password') }}" autofocus /> label="{{ __('input.password') }}" />
<x-forms.input required type="password" id="password_confirmation" <x-forms.input required type="password" id="password_confirmation"
name="password_confirmation" label="{{ __('input.password.again') }}" /> name="password_confirmation" label="{{ __('input.password.again') }}" />
</div> </div>

View File

@@ -9,7 +9,7 @@
<form action="/two-factor-challenge" method="POST" class="flex flex-col gap-2"> <form action="/two-factor-challenge" method="POST" class="flex flex-col gap-2">
@csrf @csrf
<div> <div>
<x-forms.input type="number" name="code" autocomplete="one-time-code" label="{{ __('input.code') }}" autofocus /> <x-forms.input type="number" name="code" autocomplete="one-time-code" label="{{ __('input.code') }}" />
<div x-show="!showRecovery" <div x-show="!showRecovery"
class="pt-2 text-xs cursor-pointer hover:underline hover:dark:text-white" class="pt-2 text-xs cursor-pointer hover:underline hover:dark:text-white"
x-on:click="showRecovery = !showRecovery">Enter x-on:click="showRecovery = !showRecovery">Enter

View File

@@ -151,9 +151,9 @@
@endif @endif
@endif @endif
<template x-teleport="body"> <template x-teleport="body">
<div x-show="modalOpen" <div x-show="modalOpen" @click.away="modalOpen = false; resetModal()"
class="fixed top-0 lg:pt-10 left-0 z-[99] flex items-start justify-center w-screen h-screen" x-cloak> class="fixed top-0 lg:pt-10 left-0 z-[99] flex items-start justify-center w-screen h-screen" x-cloak>
<div x-show="modalOpen" <div x-show="modalOpen" @click="modalOpen = false; resetModal()"
class="absolute inset-0 w-full h-full bg-black bg-opacity-20 backdrop-blur-sm"></div> 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" <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-start="opacity-0 -translate-y-2 sm:scale-95"
@@ -222,12 +222,12 @@
</template> </template>
@endforeach @endforeach
</ul> </ul>
@if ($confirmWithText && $confirmationText) @if ($confirmWithText)
<div class="mb-4"> <div class="mb-4">
<h4 class="mb-2 text-lg font-semibold">Confirm Actions</h4> <h4 class="mb-2 text-lg font-semibold">Confirm Actions</h4>
<p class="mb-2 text-sm">{{ $confirmationLabel }}</p> <p class="mb-2 text-sm">{{ $confirmationLabel }}</p>
<div class="relative mb-2"> <div class="relative mb-2">
<input autocomplete="off" type="text" x-model="confirmationText" <input type="text" x-model="confirmationText"
class="p-2 pr-10 w-full text-black rounded cursor-text input" readonly> class="p-2 pr-10 w-full text-black rounded cursor-text input" readonly>
<button @click="copyConfirmationText()" <button @click="copyConfirmationText()"
class="absolute right-2 top-1/2 text-gray-500 transform -translate-y-1/2 hover:text-gray-700" class="absolute right-2 top-1/2 text-gray-500 transform -translate-y-1/2 hover:text-gray-700"
@@ -255,7 +255,7 @@
class="block mt-4 text-sm font-medium text-gray-700 dark:text-gray-300"> class="block mt-4 text-sm font-medium text-gray-700 dark:text-gray-300">
{{ $shortConfirmationLabel }} {{ $shortConfirmationLabel }}
</label> </label>
<input autocomplete="off" type="text" x-model="userConfirmationText" <input type="text" x-model="userConfirmationText"
class="p-2 mt-1 w-full text-black rounded input"> class="p-2 mt-1 w-full text-black rounded input">
</div> </div>
@endif @endif
@@ -272,8 +272,10 @@
class="block text-sm font-medium text-gray-700 dark:text-gray-300"> class="block text-sm font-medium text-gray-700 dark:text-gray-300">
Your Password Your Password
</label> </label>
<input autocomplete="off" type="password" id="password-confirm" x-model="password" class="w-full input" <form action="return false">
placeholder="Enter your password"> <input type="password" id="password-confirm" x-model="password" class="w-full input"
placeholder="Enter your password">
</form>
<p x-show="passwordError" x-text="passwordError" class="mt-1 text-sm text-red-500"></p> <p x-show="passwordError" x-text="passwordError" class="mt-1 text-sm text-red-500"></p>
@error('password') @error('password')
<p class="mt-1 text-sm text-red-500">{{ $message }}</p> <p class="mt-1 text-sm text-red-500">{{ $message }}</p>
@@ -296,20 +298,13 @@
</template> </template>
<template x-if="step === 1"> <template x-if="step === 1">
@if(isDev() && $submitAction === 'delete') <x-forms.button @click="step++" class="w-auto" isError>
<x-forms.button class="w-auto" isError <span x-text="step1ButtonText"></span>
@click="$wire.delete('hello')"> </x-forms.button>
<span x-text="step3ButtonText"></span>
</x-forms.button>
@else
<x-forms.button @click="step++" class="w-auto" isError>
<span x-text="step1ButtonText"></span>
</x-forms.button>
@endif
</template> </template>
<template x-if="step === 2"> <template x-if="step === 2">
<x-forms.button x-bind:disabled="confirmationText !== '' && confirmWithText && userConfirmationText !== confirmationText" <x-forms.button x-bind:disabled="confirmWithText && userConfirmationText !== confirmationText"
class="w-auto" isError class="w-auto" isError
@click=" @click="
if (dispatchEvent) { if (dispatchEvent) {

View File

@@ -6,10 +6,10 @@
x-transition:enter-start="translate-y-full" x-transition:enter-end="translate-y-0" x-transition:enter-start="translate-y-full" x-transition:enter-end="translate-y-0"
x-transition:leave="transition ease-in duration-300" x-transition:leave-start="translate-y-0" x-transition:leave="transition ease-in duration-300" x-transition:leave-start="translate-y-0"
x-transition:leave-end="translate-y-full" x-init="setTimeout(() => { bannerVisible = true }, bannerVisibleAfter);" x-transition:leave-end="translate-y-full" x-init="setTimeout(() => { bannerVisible = true }, bannerVisibleAfter);"
class="fixed bottom-0 right-0 w-full h-auto duration-300 ease-out sm:px-5 sm:pb-5 sm:w-[26rem] lg:w-full z-[999]" class="fixed bottom-0 right-0 w-full h-auto duration-300 ease-out sm:px-5 sm:pb-5 w-full z-[999]"
x-cloak> x-cloak>
<div <div
class="flex flex-col items-center justify-between w-full h-full max-w-4xl p-6 mx-auto bg-white border shadow-lg lg:border-t dark:border-coolgray-300 dark:bg-coolgray-100 lg:p-8 lg:flex-row sm:rounded"> class="flex items-center flex-col justify-between w-full h-full max-w-4xl p-6 mx-auto bg-white border shadow-lg lg:border-t dark:border-coolgray-300 dark:bg-coolgray-100 lg:p-8 lg:flex-row sm:rounded">
<div <div
class="flex flex-col items-start h-full pb-6 text-xs lg:items-center lg:flex-row lg:pb-0 lg:pr-6 lg:space-x-5 dark:text-neutral-300"> class="flex flex-col items-start h-full pb-6 text-xs lg:items-center lg:flex-row lg:pb-0 lg:pr-6 lg:space-x-5 dark:text-neutral-300">
@if (isset($icon)) @if (isset($icon))
@@ -23,14 +23,12 @@
<p class="">{{ $description }}</span></p> <p class="">{{ $description }}</span></p>
</div> </div>
</div> </div>
<div class="flex items-end justify-end w-full pl-3 space-x-3 lg:flex-shrink-0 lg:w-auto">
<button <button
@if ($buttonText->attributes->whereStartsWith('@click')->first()) @click="bannerVisible=false;{{ $buttonText->attributes->get('@click') }}" @if ($buttonText->attributes->whereStartsWith('@click')->first()) @click="bannerVisible=false;{{ $buttonText->attributes->get('@click') }}"
@else @else
@click="bannerVisible=false;" @endif @click="bannerVisible=false;" @endif
class="inline-flex items-center justify-center flex-shrink-0 w-1/2 px-4 py-2 text-sm font-medium tracking-wide transition-colors duration-200 rounded-md bg-neutral-100 hover:bg-neutral-200 dark:bg-coolgray-200 lg:w-auto dark:text-neutral-200 dark:hover:bg-coolgray-300 focus:shadow-outline focus:outline-none"> class="w-full px-4 py-2 text-sm font-medium tracking-wide transition-colors duration-200 rounded-md bg-neutral-100 hover:bg-neutral-200 dark:bg-coolgray-200 lg:w-auto dark:text-neutral-200 dark:hover:bg-coolgray-300 focus:shadow-outline focus:outline-none">
{{ $buttonText }} {{ $buttonText }}
</button> </button>
</div>
</div> </div>
</div> </div>

View File

@@ -32,21 +32,16 @@
@if (!isCloud()) @if (!isCloud())
<x-popup> <x-popup>
<x-slot:title> <x-slot:title>
<span class="font-bold text-left text-red-500">WARNING: </span>Realtime Error?! <span class="font-bold text-left text-red-500">WARNING: </span> Cannot connect to real-time service
</x-slot:title> </x-slot:title>
<x-slot:description> <x-slot:description>
<span>Coolify could not connect to its real-time service.<br>This will cause unusual problems on the <div>This will cause unusual problems on the
UI UI! <br><br>
if
not fixed! <br><br>
Please ensure that you have opened the Please ensure that you have opened the
<a class="underline" href='https://coolify.io/docs/knowledge-base/server/firewall' <a class="underline" href='https://coolify.io/docs/knowledge-base/server/firewall'
target='_blank'>required ports</a>, target='_blank'>required ports</a> or get
check the
related <a class="underline" href='https://coolify.io/docs/knowledge-base/cloudflare/tunnels'
target='_blank'>documentation</a> or get
help on <a class="underline" href='https://coollabs.io/discord' target='_blank'>Discord</a>. help on <a class="underline" href='https://coollabs.io/discord' target='_blank'>Discord</a>.
</span> </div>
</x-slot:description> </x-slot:description>
<x-slot:button-text @click="disableRealtime()"> <x-slot:button-text @click="disableRealtime()">
Acknowledge & Disable This Popup Acknowledge & Disable This Popup

View File

@@ -1,5 +1,5 @@
<form class="flex flex-col w-full gap-2 rounded" wire:submit='submit'> <form class="flex flex-col w-full gap-2 rounded" wire:submit='submit'>
<x-forms.input autofocus placeholder="Your Cool Project" id="name" label="Name" required /> <x-forms.input placeholder="Your Cool Project" id="name" label="Name" required />
<x-forms.input placeholder="This is my cool project everyone knows about" id="description" label="Description" /> <x-forms.input placeholder="This is my cool project everyone knows about" id="description" label="Description" />
<div class="subtitle">New project will have a default production environment.</div> <div class="subtitle">New project will have a default production environment.</div>
<x-forms.button type="submit" @click="slideOverOpen=false"> <x-forms.button type="submit" @click="slideOverOpen=false">

View File

@@ -1,5 +1,5 @@
<form class="flex flex-col w-full gap-2 rounded" wire:submit='submit'> <form class="flex flex-col w-full gap-2 rounded" wire:submit='submit'>
<x-forms.input autofocus placeholder="production" id="name" label="Name" required /> <x-forms.input placeholder="production" id="name" label="Name" required />
<x-forms.button type="submit" @click="slideOverOpen=false"> <x-forms.button type="submit" @click="slideOverOpen=false">
Save Save
</x-forms.button> </x-forms.button>

View File

@@ -8,17 +8,14 @@
<livewire:project.database.backup-now :backup="$backup" /> <livewire:project.database.backup-now :backup="$backup" />
@endif @endif
@if ($backup->database_id !== 0) @if ($backup->database_id !== 0)
<x-modal-confirmation <x-modal-confirmation title="Confirm Backup Schedule Deletion?" buttonTitle="Delete Backups and Schedule"
title="Confirm Backup Schedule Deletion?" isErrorButton submitAction="delete" :checkboxes="$checkboxes" :actions="[
buttonTitle="Delete Backups and Schedule" 'The selected backup schedule will be deleted.',
isErrorButton 'Scheduled backups for this database will be stopped (if this is the only backup schedule for this database).',
submitAction="delete" ]"
:checkboxes="$checkboxes" confirmationText="{{ $backup->database->name }}"
:actions="['The selected backup schedule will be deleted.', 'Scheduled backups for this database will be stopped (if this is the only backup schedule for this database).']" confirmationLabel="Please confirm the execution of the actions by entering the Database Name of the scheduled backups below"
confirmationText="{{ $backup->database->name }}" shortConfirmationLabel="Database Name" />
confirmationLabel="Please confirm the execution of the actions by entering the Database Name of the scheduled backups below"
shortConfirmationLabel="Database Name"
/>
@endif @endif
</div> </div>
<div class="w-48 pb-2"> <div class="w-48 pb-2">
@@ -36,23 +33,39 @@
</div> </div>
@endif @endif
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<div class="flex gap-2"> <h3>Settings</h3>
<div class="flex gap-2 flex-col ">
@if ($backup->database_type === 'App\Models\StandalonePostgresql') @if ($backup->database_type === 'App\Models\StandalonePostgresql')
<x-forms.input label="Databases To Backup" <div class="w-48">
helper="Comma separated list of databases to backup. Empty will include the default one." <x-forms.checkbox label="Backup All Databases" id="backup.dump_all" instantSave />
id="backup.databases_to_backup" /> </div>
@if (!$backup->dump_all)
<x-forms.input label="Databases To Backup"
helper="Comma separated list of databases to backup. Empty will include the default one."
id="backup.databases_to_backup" />
@endif
@elseif($backup->database_type === 'App\Models\StandaloneMongodb') @elseif($backup->database_type === 'App\Models\StandaloneMongodb')
<x-forms.input label="Databases To Include" <x-forms.input label="Databases To Include"
helper="A list of databases to backup. You can specify which collection(s) per database to exclude from the backup. Empty will include all databases and collections.<br><br>Example:<br><br>database1:collection1,collection2|database2:collection3,collection4<br><br> database1 will include all collections except collection1 and collection2. <br>database2 will include all collections except collection3 and collection4.<br><br>Another Example:<br><br>database1:collection1|database2<br><br> database1 will include all collections except collection1.<br>database2 will include ALL collections." helper="A list of databases to backup. You can specify which collection(s) per database to exclude from the backup. Empty will include all databases and collections.<br><br>Example:<br><br>database1:collection1,collection2|database2:collection3,collection4<br><br> database1 will include all collections except collection1 and collection2. <br>database2 will include all collections except collection3 and collection4.<br><br>Another Example:<br><br>database1:collection1|database2<br><br> database1 will include all collections except collection1.<br>database2 will include ALL collections."
id="backup.databases_to_backup" /> id="backup.databases_to_backup" />
@elseif($backup->database_type === 'App\Models\StandaloneMysql') @elseif($backup->database_type === 'App\Models\StandaloneMysql')
<x-forms.input label="Databases To Backup" <div class="w-48">
helper="Comma separated list of databases to backup. Empty will include the default one." <x-forms.checkbox label="Backup All Databases" id="backup.dump_all" instantSave />
id="backup.databases_to_backup" /> </div>
@if (!$backup->dump_all)
<x-forms.input label="Databases To Backup"
helper="Comma separated list of databases to backup. Empty will include the default one."
id="backup.databases_to_backup" />
@endif
@elseif($backup->database_type === 'App\Models\StandaloneMariadb') @elseif($backup->database_type === 'App\Models\StandaloneMariadb')
<x-forms.input label="Databases To Backup" <div class="w-48">
helper="Comma separated list of databases to backup. Empty will include the default one." <x-forms.checkbox label="Backup All Databases" id="backup.dump_all" instantSave />
id="backup.databases_to_backup" /> </div>
@if (!$backup->dump_all)
<x-forms.input label="Databases To Backup"
helper="Comma separated list of databases to backup. Empty will include the default one."
id="backup.databases_to_backup" />
@endif
@endif @endif
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">

View File

@@ -45,20 +45,11 @@
<x-forms.button class="dark:hover:bg-coolgray-400" <x-forms.button class="dark:hover:bg-coolgray-400"
x-on:click="download_file('{{ data_get($execution, 'id') }}')">Download</x-forms.button> x-on:click="download_file('{{ data_get($execution, 'id') }}')">Download</x-forms.button>
@endif @endif
<x-modal-confirmation <x-modal-confirmation title="Confirm Backup Deletion?" buttonTitle="Delete" isErrorButton
title="Confirm Backup Deletion?" submitAction="deleteBackup({{ data_get($execution, 'id') }})"
buttonTitle="Delete" :actions="['This backup will be permanently deleted from local storage.']" confirmationText="{{ data_get($execution, 'filename') }}"
isErrorButton confirmationLabel="Please confirm the execution of the actions by entering the Backup Filename below"
submitAction="deleteBackup({{ data_get($execution, 'id') }})" shortConfirmationLabel="Backup Filename" step3ButtonText="Permanently Delete" />
{{-- :checkboxes="$checkboxes" --}}
:actions="[
'This backup will be permanently deleted from local storage.'
]"
confirmationText="{{ data_get($execution, 'filename') }}"
confirmationLabel="Please confirm the execution of the actions by entering the Backup Filename below"
shortConfirmationLabel="Backup Filename"
step3ButtonText="Permanently Delete"
/>
</div> </div>
</div> </div>
@empty @empty

View File

@@ -1,17 +1,19 @@
<form class="flex flex-col w-full gap-2 rounded" wire:submit='submit'> <form class="flex flex-col w-full gap-2 rounded" wire:submit='submit'>
<x-forms.input autofocus placeholder="0 0 * * * or daily" id="frequency" <x-forms.input placeholder="0 0 * * * or daily" id="frequency"
helper="You can use every_minute, hourly, daily, weekly, monthly, yearly or a cron expression." label="Frequency" helper="You can use every_minute, hourly, daily, weekly, monthly, yearly or a cron expression." label="Frequency"
required /> required />
<x-forms.checkbox id="save_s3" label="Save to S3" /> @if ($s3s->count() === 0)
<x-forms.select id="selected_storage_id"> <div class="text-red-500">No validated S3 Storages found.</div>
@if ($s3s->count() === 0) @else
<option value="0">No S3 Storages found.</option> <x-forms.checkbox wire:model.live="save_s3" label="Save to S3" />
@else @if ($save_s3)
@foreach ($s3s as $s3) <x-forms.select id="selected_storage_id" label="Select a validated S3 storage">
<option value="{{ $s3->id }}">{{ $s3->name }}</option> @foreach ($s3s as $s3)
@endforeach <option value="{{ $s3->id }}">{{ $s3->name }}</option>
@endforeach
</x-forms.select>
@endif @endif
</x-forms.select> @endif
<x-forms.button type="submit" @click="modalOpen=false"> <x-forms.button type="submit" @click="modalOpen=false">
Save Save
</x-forms.button> </x-forms.button>

View File

@@ -102,7 +102,7 @@
<h3>Initialization scripts</h3> <h3>Initialization scripts</h3>
<x-modal-input buttonTitle="+ Add" title="New Init Script"> <x-modal-input buttonTitle="+ Add" title="New Init Script">
<form class="flex flex-col w-full gap-2 rounded" wire:submit='save_new_init_script'> <form class="flex flex-col w-full gap-2 rounded" wire:submit='save_new_init_script'>
<x-forms.input autofocus placeholder="create_test_db.sql" id="new_filename" label="Filename" <x-forms.input placeholder="create_test_db.sql" id="new_filename" label="Filename"
required /> required />
<x-forms.textarea rows="20" placeholder="CREATE DATABASE test;" id="new_content" <x-forms.textarea rows="20" placeholder="CREATE DATABASE test;" id="new_content"
label="Content" required /> label="Content" required />

View File

@@ -48,7 +48,7 @@
class="items-center justify-center box">+ Add New Resource</a> class="items-center justify-center box">+ Add New Resource</a>
@else @else
<div x-data="searchComponent()"> <div x-data="searchComponent()">
<x-forms.input autofocus placeholder="Search for name, fqdn..." x-model="search" id="null" /> <x-forms.input placeholder="Search for name, fqdn..." x-model="search" id="null" />
<div class="grid grid-cols-1 gap-4 pt-4 lg:grid-cols-2 xl:grid-cols-3"> <div class="grid grid-cols-1 gap-4 pt-4 lg:grid-cols-2 xl:grid-cols-3">
<template x-for="item in allFilteredItems" :key="item.uuid"> <template x-for="item in allFilteredItems" :key="item.uuid">
<span> <span>

View File

@@ -149,6 +149,12 @@
<div class="text-xs">{{ $database->status }}</div> <div class="text-xs">{{ $database->status }}</div>
</div> </div>
<div class="flex items-center px-4"> <div class="flex items-center px-4">
@if ($database->isBackupSolutionAvailable())
<a class="mx-4 text-xs font-bold hover:underline"
href="{{ route('project.service.index', [...$parameters, 'stack_service_uuid' => $database->uuid]) }}#backups">
Backups
</a>
@endif
<a class="mx-4 text-xs font-bold hover:underline" <a class="mx-4 text-xs font-bold hover:underline"
href="{{ route('project.service.index', [...$parameters, 'stack_service_uuid' => $database->uuid]) }}"> href="{{ route('project.service.index', [...$parameters, 'stack_service_uuid' => $database->uuid]) }}">
Settings Settings

View File

@@ -10,9 +10,7 @@
<a class="menu-item" :class="activeTab === 'general' && 'menu-item-active'" <a class="menu-item" :class="activeTab === 'general' && 'menu-item-active'"
@click.prevent="activeTab = 'general'; window.location.hash = 'general'; if(window.location.search) window.location.search = ''" @click.prevent="activeTab = 'general'; window.location.hash = 'general'; if(window.location.search) window.location.search = ''"
href="#">General</a> href="#">General</a>
@if (str($serviceDatabase?->databaseType())->contains('mysql') || @if ($serviceDatabase->isBackupSolutionAvailable())
str($serviceDatabase?->databaseType())->contains('postgres') ||
str($serviceDatabase?->databaseType())->contains('mariadb'))
<a :class="activeTab === 'backups' && 'menu-item-active'" class="menu-item" <a :class="activeTab === 'backups' && 'menu-item-active'" class="menu-item"
@click.prevent="activeTab = 'backups'; window.location.hash = 'backups'" href="#">Backups</a> @click.prevent="activeTab = 'backups'; window.location.hash = 'backups'" href="#">Backups</a>
@endif @endif
@@ -28,22 +26,25 @@
</div> </div>
@endisset @endisset
@isset($serviceDatabase) @isset($serviceDatabase)
<x-slot:title> <x-slot:title>
{{ data_get_str($service, 'name')->limit(10) }} > {{ data_get_str($serviceDatabase, 'name')->limit(10) }} | Coolify {{ data_get_str($service, 'name')->limit(10) }} >
</x-slot> {{ data_get_str($serviceDatabase, 'name')->limit(10) }} | Coolify
</x-slot>
<div x-cloak x-show="activeTab === 'general'" class="h-full"> <div x-cloak x-show="activeTab === 'general'" class="h-full">
<livewire:project.service.database :database="$serviceDatabase" /> <livewire:project.service.database :database="$serviceDatabase" />
</div> </div>
<div x-cloak x-show="activeTab === 'backups'"> @if ($serviceDatabase->isBackupSolutionAvailable())
<div class="flex gap-2 "> <div x-cloak x-show="activeTab === 'backups'">
<h2 class="pb-4">Scheduled Backups</h2> <div class="flex gap-2 ">
<x-modal-input buttonTitle="+ Add" title="New Scheduled Backup"> <h2 class="pb-4">Scheduled Backups</h2>
<livewire:project.database.create-scheduled-backup :database="$serviceDatabase" :s3s="$s3s" /> <x-modal-input buttonTitle="+ Add" title="New Scheduled Backup">
</x-modal-input> <livewire:project.database.create-scheduled-backup :database="$serviceDatabase" :s3s="$s3s" />
</div> </x-modal-input>
<livewire:project.database.scheduled-backups :database="$serviceDatabase" /> </div>
</div> <livewire:project.database.scheduled-backups :database="$serviceDatabase" />
@endisset @endif
</div> </div>
@endisset
</div> </div>
</div> </div>
</div>

View File

@@ -22,7 +22,25 @@
</nav> </nav>
<div class="flex flex-wrap order-first gap-2 items-center sm:order-last"> <div class="flex flex-wrap order-first gap-2 items-center sm:order-last">
@if (str($service->status())->contains('running')) @if (str($service->status())->contains('running'))
<button @click="$wire.dispatch('restartEvent')" class="gap-2 button"> <x-dropdown>
<x-slot:title>
Advanced
</x-slot>
<div class="dropdown-item" @click="$wire.dispatch('pullAndRestartEvent')">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" 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="M12.983 8.978c3.955 -.182 7.017 -1.446 7.017 -2.978c0 -1.657 -3.582 -3 -8 -3c-1.661 0 -3.204 .19 -4.483 .515m-2.783 1.228c-.471 .382 -.734 .808 -.734 1.257c0 1.22 1.944 2.271 4.734 2.74" />
<path
d="M4 6v6c0 1.657 3.582 3 8 3c.986 0 1.93 -.067 2.802 -.19m3.187 -.82c1.251 -.53 2.011 -1.228 2.011 -1.99v-6" />
<path d="M4 12v6c0 1.657 3.582 3 8 3c3.217 0 5.991 -.712 7.261 -1.74m.739 -3.26v-4" />
<path d="M3 3l18 18" />
</svg>
Pull Latest Images & Restart
</div>
</x-dropdown>
<x-forms.button title="Restart" @click="$wire.dispatch('restartEvent')">
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2"> stroke-width="2">
@@ -30,8 +48,8 @@
<path d="M20 4v5h-5" /> <path d="M20 4v5h-5" />
</g> </g>
</svg> </svg>
Pull Latest Images & Restart Restart
</button> </x-forms.button>
<x-modal-confirmation title="Confirm Service Stopping?" buttonTitle="Stop" submitAction="stop" <x-modal-confirmation title="Confirm Service Stopping?" buttonTitle="Stop" submitAction="stop"
:checkboxes="$checkboxes" :actions="[__('service.stop'), __('resource.non_persistent')]" :confirmWithText="false" :confirmWithPassword="false" step1ButtonText="Continue" :checkboxes="$checkboxes" :actions="[__('service.stop'), __('resource.non_persistent')]" :confirmWithText="false" :confirmWithPassword="false" step1ButtonText="Continue"
step2ButtonText="Stop Service" :dispatchEvent="true" dispatchEventType="stopEvent"> step2ButtonText="Stop Service" :dispatchEvent="true" dispatchEventType="stopEvent">
@@ -135,9 +153,13 @@
$wire.$call('start'); $wire.$call('start');
}); });
$wire.$on('restartEvent', () => { $wire.$on('restartEvent', () => {
$wire.$dispatch('info', 'Pulling new images.'); $wire.$dispatch('info', 'Service restart in progress.');
$wire.$call('restart'); $wire.$call('restart');
}); });
$wire.$on('pullAndRestartEvent', () => {
$wire.$dispatch('info', 'Pulling new images.');
$wire.$call('pullAndRestartEvent');
});
$wire.on('imagePulled', () => { $wire.on('imagePulled', () => {
window.dispatchEvent(new CustomEvent('startservice')); window.dispatchEvent(new CustomEvent('startservice'));
$wire.$dispatch('info', 'Restarting service.'); $wire.$dispatch('info', 'Restarting service.');

View File

@@ -1,5 +1,5 @@
<form class="flex flex-col w-full gap-2 rounded" wire:submit='submit'> <form class="flex flex-col w-full gap-2 rounded" wire:submit='submit'>
<x-forms.input autofocus placeholder="NODE_ENV" id="key" label="Name" required /> <x-forms.input placeholder="NODE_ENV" id="key" label="Name" required />
<x-forms.textarea x-show="$wire.is_multiline === true" x-cloak id="value" label="Value" required /> <x-forms.textarea x-show="$wire.is_multiline === true" x-cloak id="value" label="Value" required />
<x-forms.input x-show="$wire.is_multiline === false" x-cloak placeholder="production" id="value" <x-forms.input x-show="$wire.is_multiline === false" x-cloak placeholder="production" id="value"
x-bind:label="$wire.is_multiline === false && 'Value'" required /> x-bind:label="$wire.is_multiline === false && 'Value'" required />

View File

@@ -1,5 +1,5 @@
<form class="flex flex-col w-full gap-2 rounded" wire:submit='submit'> <form class="flex flex-col w-full gap-2 rounded" wire:submit='submit'>
<x-forms.input autofocus placeholder="Run cron" id="name" label="Name" /> <x-forms.input placeholder="Run cron" id="name" label="Name" />
<x-forms.input placeholder="php artisan schedule:run" id="command" label="Command" /> <x-forms.input placeholder="php artisan schedule:run" id="command" label="Command" />
<x-forms.input placeholder="0 0 * * * or daily" <x-forms.input placeholder="0 0 * * * or daily"
helper="You can use every_minute, hourly, daily, weekly, monthly, yearly or a cron expression." id="frequency" helper="You can use every_minute, hourly, daily, weekly, monthly, yearly or a cron expression." id="frequency"

View File

@@ -6,7 +6,8 @@
<div class="pb-4"> <div class="pb-4">
<h2>API Tokens</h2> <h2>API Tokens</h2>
@if (!$isApiEnabled) @if (!$isApiEnabled)
<div>API is disabled. If you want to use the API, please enable it in the <a href="{{ route('settings.index') }}" class="underline dark:text-white">Settings</a> menu.</div> <div>API is disabled. If you want to use the API, please enable it in the <a
href="{{ route('settings.index') }}" class="underline dark:text-white">Settings</a> menu.</div>
@else @else
<div>Tokens are created with the current team as scope. You will only have access to this team's resources. <div>Tokens are created with the current team as scope. You will only have access to this team's resources.
</div> </div>
@@ -25,7 +26,7 @@
@if ($permissions) @if ($permissions)
@foreach ($permissions as $permission) @foreach ($permissions as $permission)
@if ($permission === '*') @if ($permission === '*')
<div>All (root/admin access), be careful!</div> <div>Root access, be careful!</div>
@else @else
<div>{{ $permission }}</div> <div>{{ $permission }}</div>
@endif @endif
@@ -35,6 +36,7 @@
</div> </div>
<h4>Token Permissions</h4> <h4>Token Permissions</h4>
<div class="w-64"> <div class="w-64">
<x-forms.checkbox label="Root Access" wire:model.live="rootAccess"></x-forms.checkbox>
<x-forms.checkbox label="Read-only" wire:model.live="readOnly"></x-forms.checkbox> <x-forms.checkbox label="Read-only" wire:model.live="readOnly"></x-forms.checkbox>
<x-forms.checkbox label="View Sensitive Data" wire:model.live="viewSensitiveData"></x-forms.checkbox> <x-forms.checkbox label="View Sensitive Data" wire:model.live="viewSensitiveData"></x-forms.checkbox>
</div> </div>

View File

@@ -141,7 +141,7 @@
</div> </div>
@endif @endif
@if (!$server->settings->is_cloudflare_tunnel && $server->isFunctional()) @if (!$server->settings->is_cloudflare_tunnel && $server->isFunctional())
<x-modal-input buttonTitle="Automated Configuration" title="Cloudflare Tunnels" class="w-full"> <x-modal-input buttonTitle="Automated Configuration" title="Cloudflare Tunnels" class="w-full" :closeOutside="false">
<livewire:server.configure-cloudflare-tunnels :server_id="$server->id" /> <livewire:server.configure-cloudflare-tunnels :server_id="$server->id" />
</x-modal-input> </x-modal-input>
@endif @endif

View File

@@ -4,7 +4,7 @@
@else @else
<form class="flex flex-col w-full gap-2" wire:submit='submit'> <form class="flex flex-col w-full gap-2" wire:submit='submit'>
<div class="flex w-full gap-2 flex-wrap sm:flex-nowrap"> <div class="flex w-full gap-2 flex-wrap sm:flex-nowrap">
<x-forms.input autofocus id="name" label="Name" required /> <x-forms.input id="name" label="Name" required />
<x-forms.input id="description" label="Description" /> <x-forms.input id="description" label="Description" />
</div> </div>
<div class="flex gap-2 flex-wrap sm:flex-nowrap"> <div class="flex gap-2 flex-wrap sm:flex-nowrap">

View File

@@ -1,5 +1,5 @@
<form wire:submit.prevent="addDynamicConfiguration" class="flex flex-col w-full gap-4"> <form wire:submit.prevent="addDynamicConfiguration" class="flex flex-col w-full gap-4">
<x-forms.input autofocus id="fileName" label="Filename" required /> <x-forms.input id="fileName" label="Filename" required />
<x-forms.textarea allowTab useMonacoEditor id="value" label="Configuration" required rows="20" /> <x-forms.textarea allowTab useMonacoEditor id="value" label="Configuration" required rows="20" />
<x-forms.button type="submit" @click="slideOverOpen=false">Save</x-forms.button> <x-forms.button type="submit" @click="slideOverOpen=false">Save</x-forms.button>
</form> </form>

View File

@@ -4,7 +4,7 @@
<x-forms.input required label="Name" id="name" /> <x-forms.input required label="Name" id="name" />
<x-forms.input label="Description" id="description" /> <x-forms.input label="Description" id="description" />
</div> </div>
<x-forms.input required type="url" label="Endpoint" id="endpoint" /> <x-forms.input required type="url" label="Endpoint" wire:model.blur="endpoint" />
<div class="flex gap-2"> <div class="flex gap-2">
<x-forms.input required label="Bucket" id="bucket" /> <x-forms.input required label="Bucket" id="bucket" />
<x-forms.input required label="Region" id="region" /> <x-forms.input required label="Region" id="region" />

View File

@@ -1,5 +1,5 @@
<form class="flex flex-col w-full gap-2" wire:submit='submit'> <form class="flex flex-col w-full gap-2" wire:submit='submit'>
<x-forms.input autofocus id="name" label="Name" required /> <x-forms.input id="name" label="Name" required />
<x-forms.input id="description" label="Description" /> <x-forms.input id="description" label="Description" />
<x-forms.button type="submit"> <x-forms.button type="submit">
Continue Continue

View File

@@ -0,0 +1,17 @@
# documentation: https://hub.docker.com/r/ruimarinho/bitcoin-core/
# slogan: A self-hosted Bitcoin Core full node.
# tags: cryptocurrency,node,blockchain,bitcoin
# logo: svgs/bitcoin.svg
services:
bitcoin-core:
image: ruimarinho/bitcoin-core:latest
environment:
- BITCOIN_RPCUSER=${BITCOIN_RPCUSER:-bitcoinuser}
- BITCOIN_RPCPASSWORD=${SERVICE_PASSWORD_PASSWORD64}
- BITCOIN_NETWORK=${BITCOIN_NETWORK:-mainnet}
- BITCOIN_PRINTTOCONSOLE=${BITCOIN_PRINTTOCONSOLE:-1}
- BITCOIN_TXINDEX=${BITCOIN_TXINDEX:-1}
volumes:
- bitcoin_data:/home/bitcoin/.bitcoin

View File

@@ -1,5 +1,5 @@
# documentation: https://docs.docker.com/registry/ # documentation: https://docs.docker.com/registry/
# slogan: The Docker Registry is lets you distribute Docker images. # slogan: The Docker Registry lets you distribute Docker images.
# tags: registry,images,docker # tags: registry,images,docker
# logo: svgs/docker-registry.png # logo: svgs/docker-registry.png
# port: 5000 # port: 5000

View File

@@ -0,0 +1,60 @@
# documentation: https://docs.strapi.io/
# slogan: Open-source headless CMS to build powerful APIs with built-in content management.
# tags: cms, headless, mysql, api
# logo: svgs/strapi.svg
# port: 1337
services:
strapi:
image: "elestio/strapi-development:latest"
environment:
- SERVICE_FQDN_STRAPI_1337
- DATABASE_CLIENT=postgres
- DATABASE_HOST=postgresql
- DATABASE_PORT=5432
- "DATABASE_NAME=${POSTGRESQL_DATABASE:-strapi}"
- DATABASE_USERNAME=$SERVICE_USER_POSTGRESQL
- DATABASE_PASSWORD=$SERVICE_PASSWORD_POSTGRESQL
- JWT_SECRET=$SERVICE_BASE64_64_SECRET
- ADMIN_JWT_SECRET=$SERVICE_BASE64_64_SECRET
- APP_KEYS=$SERVICE_BASE64_64_KEY
- STRAPI_TELEMETRY_DISABLED=${STRAPI_TELEMETRY_DISABLED:-true}
- STRAPI_LICENSE=${STRAPI_LICENSE}
- NODE_ENV=${NODE_ENV:-development}
- BROWSER=${BROWSER:-true}
- STRAPI_PLUGIN_I18N_INIT_LOCALE_CODE=${STRAPI_PLUGIN_I18N_INIT_LOCALE_CODE:-en}
- STRAPI_ENFORCE_SOURCEMAPS=${STRAPI_ENFORCE_SOURCEMAPS:-false}
- FAST_REFRESH=${FAST_REFRESH:-true}
volumes:
- "strapi-config:/opt/app/config"
- "strapi-src:/opt/app/src"
- "strapi-uploads:/opt/app/public/uploads"
healthcheck:
test:
- CMD
- wget
- "-q"
- "--spider"
- "http://127.0.0.1:1337/"
interval: 5s
timeout: 20s
retries: 10
depends_on:
postgresql:
condition: service_healthy
postgresql:
image: "elestio/postgres:latest"
environment:
- "POSTGRES_DB=${POSTGRESQL_DATABASE:-strapi}"
- POSTGRES_USER=$SERVICE_USER_POSTGRESQL
- POSTGRES_PASSWORD=$SERVICE_PASSWORD_POSTGRESQL
- PGDATA=/var/lib/postgresql/data
volumes:
- "strapi-postgresql-data:/var/lib/postgresql/data"
healthcheck:
test:
- CMD-SHELL
- "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"
interval: 5s
timeout: 20s
retries: 10

File diff suppressed because one or more lines are too long

View File

@@ -1,16 +1,16 @@
{ {
"coolify": { "coolify": {
"v4": { "v4": {
"version": "4.0.0-beta.348" "version": "4.0.0-beta.352"
}, },
"nightly": { "nightly": {
"version": "4.0.0-beta.349" "version": "4.0.0-beta.353"
}, },
"helper": { "helper": {
"version": "1.0.1" "version": "1.0.1"
}, },
"realtime": { "realtime": {
"version": "1.0.2" "version": "1.0.3"
} }
} }
} }