50
app/Console/Commands/CheckApplicationDeploymentQueue.php
Normal file
50
app/Console/Commands/CheckApplicationDeploymentQueue.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,10 +61,12 @@ 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->team = Team::findOrFail($this->backup->team_id);
|
||||||
if (data_get($this->backup, 'database_type') === 'App\Models\ServiceDatabase') {
|
if (data_get($this->backup, 'database_type') === 'App\Models\ServiceDatabase') {
|
||||||
$this->database = data_get($this->backup, 'database');
|
$this->database = data_get($this->backup, 'database');
|
||||||
$this->server = $this->database->service->server;
|
$this->server = $this->database->service->server;
|
||||||
@@ -76,17 +76,11 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
|||||||
$this->server = $this->database->destination->server;
|
$this->server = $this->database->destination->server;
|
||||||
$this->s3 = $this->backup->s3;
|
$this->s3 = $this->backup->s3;
|
||||||
}
|
}
|
||||||
|
if (is_null($this->server)) {
|
||||||
|
throw new \Exception('Server not found?!');
|
||||||
}
|
}
|
||||||
|
if (is_null($this->database)) {
|
||||||
public function handle(): void
|
throw new \Exception('Database not found?!');
|
||||||
{
|
|
||||||
try {
|
|
||||||
// Check if team is exists
|
|
||||||
if (is_null($this->team)) {
|
|
||||||
StopDatabase::run($this->database);
|
|
||||||
$this->database->delete();
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
BackupCreated::dispatch($this->team->id);
|
BackupCreated::dispatch($this->team->id);
|
||||||
@@ -237,7 +231,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';
|
||||||
@@ -325,9 +318,11 @@ 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 {
|
||||||
|
if ($this->team) {
|
||||||
BackupCreated::dispatch($this->team->id);
|
BackupCreated::dispatch($this->team->id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private function backup_standalone_mongodb(string $databaseWithCollections): void
|
private function backup_standalone_mongodb(string $databaseWithCollections): void
|
||||||
{
|
{
|
||||||
@@ -466,34 +461,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 +482,27 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
|||||||
$this->ensureHelperImageAvailable();
|
$this->ensureHelperImageAvailable();
|
||||||
|
|
||||||
$fullImageName = $this->getFullImageName();
|
$fullImageName = $this->getFullImageName();
|
||||||
|
|
||||||
|
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}";
|
$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 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());
|
||||||
|
|||||||
@@ -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,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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('.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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([
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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(':');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1184,14 +1184,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 (
|
||||||
|
! str(trim($line))->startsWith([
|
||||||
'cd',
|
'cd',
|
||||||
'command',
|
'command',
|
||||||
'echo',
|
'echo',
|
||||||
'true',
|
'true',
|
||||||
'if',
|
'if',
|
||||||
'fi',
|
'fi',
|
||||||
])) {
|
])
|
||||||
|
) {
|
||||||
return "sudo $line";
|
return "sudo $line";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3863,9 +3865,11 @@ 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) {
|
||||||
|
if (is_numeric($key)) {
|
||||||
$parts = explode('=', $value, 2);
|
$parts = explode('=', $value, 2);
|
||||||
if (count($parts) === 2) {
|
if (count($parts) === 2) {
|
||||||
$key = $parts[0];
|
$key = $parts[0];
|
||||||
@@ -3874,12 +3878,16 @@ function convertComposeEnvironmentToArray($environment)
|
|||||||
} else {
|
} else {
|
||||||
$changedEnvironment->put($key, $value);
|
$changedEnvironment->put($key, $value);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
$changedEnvironment->put($key, $value);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return $changedEnvironment;
|
return $changedEnvironment;
|
||||||
}
|
}
|
||||||
$convertedServiceVariables = $environment;
|
$convertedServiceVariables = $environment;
|
||||||
} else {
|
} else {
|
||||||
|
// Example: $environment = ['FOO=bar', 'BAZ=qux'];
|
||||||
foreach ($environment as $value) {
|
foreach ($environment as $value) {
|
||||||
$parts = explode('=', $value, 2);
|
$parts = explode('=', $value, 2);
|
||||||
$key = $parts[0];
|
$key = $parts[0];
|
||||||
|
|||||||
@@ -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.349',
|
'release' => '4.0.0-beta.350',
|
||||||
// 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'),
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
return '4.0.0-beta.349';
|
return '4.0.0-beta.350';
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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!');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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') {
|
||||||
|
|||||||
@@ -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">
|
||||||
|
<input type="password" id="password-confirm" x-model="password" class="w-full input"
|
||||||
placeholder="Enter your password">
|
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 class="w-auto" isError
|
|
||||||
@click="$wire.delete('hello')">
|
|
||||||
<span x-text="step3ButtonText"></span>
|
|
||||||
</x-forms.button>
|
|
||||||
@else
|
|
||||||
<x-forms.button @click="step++" class="w-auto" isError>
|
<x-forms.button @click="step++" class="w-auto" isError>
|
||||||
<span x-text="step1ButtonText"></span>
|
<span x-text="step1ButtonText"></span>
|
||||||
</x-forms.button>
|
</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) {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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?"
|
|
||||||
buttonTitle="Delete"
|
|
||||||
isErrorButton
|
|
||||||
submitAction="deleteBackup({{ data_get($execution, 'id') }})"
|
submitAction="deleteBackup({{ data_get($execution, 'id') }})"
|
||||||
{{-- :checkboxes="$checkboxes" --}}
|
:actions="['This backup will be permanently deleted from local storage.']" confirmationText="{{ data_get($execution, 'filename') }}"
|
||||||
: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"
|
confirmationLabel="Please confirm the execution of the actions by entering the Backup Filename below"
|
||||||
shortConfirmationLabel="Backup Filename"
|
shortConfirmationLabel="Backup Filename" step3ButtonText="Permanently Delete" />
|
||||||
step3ButtonText="Permanently Delete"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@empty
|
@empty
|
||||||
|
|||||||
@@ -2,16 +2,18 @@
|
|||||||
<x-forms.input 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" />
|
|
||||||
<x-forms.select id="selected_storage_id">
|
|
||||||
@if ($s3s->count() === 0)
|
@if ($s3s->count() === 0)
|
||||||
<option value="0">No S3 Storages found.</option>
|
<div class="text-red-500">No validated S3 Storages found.</div>
|
||||||
@else
|
@else
|
||||||
|
<x-forms.checkbox wire:model.live="save_s3" label="Save to S3" />
|
||||||
|
@if ($save_s3)
|
||||||
|
<x-forms.select id="selected_storage_id" label="Select a validated S3 storage">
|
||||||
@foreach ($s3s as $s3)
|
@foreach ($s3s as $s3)
|
||||||
<option value="{{ $s3->id }}">{{ $s3->name }}</option>
|
<option value="{{ $s3->id }}">{{ $s3->name }}</option>
|
||||||
@endforeach
|
@endforeach
|
||||||
@endif
|
|
||||||
</x-forms.select>
|
</x-forms.select>
|
||||||
|
@endif
|
||||||
|
@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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
{
|
{
|
||||||
"coolify": {
|
"coolify": {
|
||||||
"v4": {
|
"v4": {
|
||||||
"version": "4.0.0-beta.349"
|
"version": "4.0.0-beta.350"
|
||||||
},
|
},
|
||||||
"nightly": {
|
"nightly": {
|
||||||
"version": "4.0.0-beta.350"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user