diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php index 6070ad16a..3276711c5 100644 --- a/app/Jobs/DatabaseBackupJob.php +++ b/app/Jobs/DatabaseBackupJob.php @@ -484,6 +484,11 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue $fullImageName = $this->getFullImageName(); + $containerExists = instant_remote_process(["docker ps -a -q -f name=backup-of-{$this->backup->uuid}"], $this->server, false); + if (filled($containerExists)) { + instant_remote_process(["docker rm -f backup-of-{$this->backup->uuid}"], $this->server, false); + } + 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; diff --git a/app/Livewire/Project/Service/FileStorage.php b/app/Livewire/Project/Service/FileStorage.php index 4d070bc0c..5b88c15eb 100644 --- a/app/Livewire/Project/Service/FileStorage.php +++ b/app/Livewire/Project/Service/FileStorage.php @@ -49,7 +49,6 @@ class FileStorage extends Component $this->workdir = null; $this->fs_path = $this->fileStorage->fs_path; } - $this->fileStorage->loadStorageOnServer(); } public function convertToDirectory() @@ -68,6 +67,18 @@ class FileStorage extends Component } } + public function loadStorageOnServer() + { + try { + $this->fileStorage->loadStorageOnServer(); + $this->dispatch('success', 'File storage loaded from server.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } finally { + $this->dispatch('refreshStorages'); + } + } + public function convertToFile() { try { diff --git a/app/Livewire/Server/Proxy/Deploy.php b/app/Livewire/Server/Proxy/Deploy.php index f823ff3d4..4a7e4124e 100644 --- a/app/Livewire/Server/Proxy/Deploy.php +++ b/app/Livewire/Server/Proxy/Deploy.php @@ -65,7 +65,6 @@ class Deploy extends Component public function restart() { try { - $this->stop(); $this->dispatch('checkProxy'); } catch (\Throwable $e) { return handleError($e, $this); diff --git a/app/Livewire/SettingsEmail.php b/app/Livewire/SettingsEmail.php index 4205594a5..858f3b187 100644 --- a/app/Livewire/SettingsEmail.php +++ b/app/Livewire/SettingsEmail.php @@ -235,7 +235,7 @@ class SettingsEmail extends Component throw new \Exception('Too many messages sent!'); } } catch (\Throwable $e) { - return handleError($e); + return handleError($e, $this); } } } diff --git a/app/Models/LocalFileVolume.php b/app/Models/LocalFileVolume.php index a3bbbc64a..c56cd7694 100644 --- a/app/Models/LocalFileVolume.php +++ b/app/Models/LocalFileVolume.php @@ -9,8 +9,8 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; class LocalFileVolume extends BaseModel { protected $casts = [ - 'fs_path' => 'encrypted', - 'mount_path' => 'encrypted', + // 'fs_path' => 'encrypted', + // 'mount_path' => 'encrypted', 'content' => 'encrypted', 'is_directory' => 'boolean', ]; @@ -176,4 +176,19 @@ class LocalFileVolume extends BaseModel return instant_remote_process($commands, $server); } + + // Accessor for convenient access + protected function plainMountPath(): Attribute + { + return Attribute::make( + get: fn () => $this->mount_path, + set: fn ($value) => $this->mount_path = $value + ); + } + + // Scope for searching + public function scopeWherePlainMountPath($query, $path) + { + return $query->get()->where('plain_mount_path', $path); + } } diff --git a/app/Notifications/Channels/EmailChannel.php b/app/Notifications/Channels/EmailChannel.php index 4cdf8d08f..a452ef8dd 100644 --- a/app/Notifications/Channels/EmailChannel.php +++ b/app/Notifications/Channels/EmailChannel.php @@ -2,69 +2,68 @@ namespace App\Notifications\Channels; -use App\Services\ConfigurationRepository; -use Exception; -use Illuminate\Mail\Message; use Illuminate\Notifications\Notification; -use Illuminate\Support\Facades\Mail; +use Resend; class EmailChannel { - private ConfigurationRepository $configRepository; - - public function __construct(ConfigurationRepository $configRepository) - { - $this->configRepository = $configRepository; - } + public function __construct() {} public function send(SendsEmail $notifiable, Notification $notification): void { - try { - $this->bootConfigs($notifiable); + $useInstanceEmailSettings = $notifiable->emailNotificationSettings->use_instance_email_settings; + $customEmails = data_get($notification, 'emails', null); + if ($useInstanceEmailSettings) { + $settings = instanceSettings(); + } else { + $settings = $notifiable->emailNotificationSettings; + } + $isResendEnabled = $settings->resend_enabled; + $isSmtpEnabled = $settings->smtp_enabled; + if ($customEmails) { + $recipients = [$customEmails]; + } else { $recipients = $notifiable->getRecipients(); - if (count($recipients) === 0) { - throw new Exception('No email recipients found'); - } + } + $mailMessage = $notification->toMail($notifiable); - $mailMessage = $notification->toMail($notifiable); - Mail::send( - [], - [], - fn (Message $message) => $message - ->to($recipients) - ->subject($mailMessage->subject) - ->html((string) $mailMessage->render()) + if ($isResendEnabled) { + $resend = Resend::client($settings->resend_api_key); + $from = "{$settings->smtp_from_name} <{$settings->smtp_from_address}>"; + $resend->emails->send([ + 'from' => $from, + 'to' => $recipients, + 'subject' => $mailMessage->subject, + 'html' => (string) $mailMessage->render(), + ]); + } elseif ($isSmtpEnabled) { + $encryption = match (strtolower($settings->smtp_encryption)) { + 'starttls' => null, + 'tls' => 'tls', + 'none' => null, + default => null, + }; + + $transport = new \Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport( + $settings->smtp_host, + $settings->smtp_port, + $encryption ); - } catch (Exception $e) { - $error = $e->getMessage(); - if ($error === 'No email settings found.') { - throw $e; - } - $message = "EmailChannel error: {$e->getMessage()}. Failed to send email to:"; - if (isset($recipients)) { - $message .= implode(', ', $recipients); - } - if (isset($mailMessage)) { - $message .= " with subject: {$mailMessage->subject}"; - } - send_internal_notification($message); - throw $e; + $transport->setUsername($settings->smtp_username); + $transport->setPassword($settings->smtp_password); + + $mailer = new \Symfony\Component\Mailer\Mailer($transport); + + $fromEmail = $settings->smtp_from_address ?? 'noreply@localhost'; + $fromName = $settings->smtp_from_name ?? 'System'; + $from = new \Symfony\Component\Mime\Address($fromEmail, $fromName); + $email = (new \Symfony\Component\Mime\Email) + ->from($from) + ->to(...$recipients) + ->subject($mailMessage->subject) + ->html((string) $mailMessage->render()); + + $mailer->send($email); } } - - private function bootConfigs($notifiable): void - { - $emailSettings = $notifiable->emailNotificationSettings; - - if ($emailSettings->use_instance_email_settings) { - $type = set_transanctional_email_settings(); - if (blank($type)) { - throw new Exception('No email settings found.'); - } - - return; - } - - $this->configRepository->updateMailConfig($emailSettings); - } } diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php index 80e19d80f..de80adbef 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -8,6 +8,7 @@ use App\Models\ServiceApplication; use Illuminate\Support\Collection; use Illuminate\Support\Str; use Spatie\Url\Url; +use Symfony\Component\Yaml\Yaml; use Visus\Cuid2\Cuid2; function getCurrentApplicationContainerStatus(Server $server, int $id, ?int $pullRequestId = null, ?bool $includePullrequests = false): Collection @@ -834,7 +835,15 @@ function validateComposeFile(string $compose, int $server_id): string|Throwable if (! $server) { throw new \Exception('Server not found'); } - $base64_compose = base64_encode($compose); + $yaml_compose = Yaml::parse($compose); + foreach ($yaml_compose['services'] as $service_name => $service) { + foreach ($service['volumes'] as $volume_name => $volume) { + if (data_get($volume, 'type') === 'bind' && data_get($volume, 'content')) { + unset($yaml_compose['services'][$service_name]['volumes'][$volume_name]['content']); + } + } + } + $base64_compose = base64_encode(Yaml::dump($yaml_compose)); instant_remote_process([ "echo {$base64_compose} | base64 -d | tee /tmp/{$uuid}.yml > /dev/null", "chmod 600 /tmp/{$uuid}.yml", diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 60e71cd9c..a020e7558 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -1250,13 +1250,23 @@ function get_public_ips() function isAnyDeploymentInprogress() { $runningJobs = ApplicationDeploymentQueue::where('horizon_job_worker', gethostname())->where('status', ApplicationDeploymentStatus::IN_PROGRESS->value)->get(); + $basicDetails = $runningJobs->map(function ($job) { + return [ + 'id' => $job->id, + 'created_at' => $job->created_at, + 'application_id' => $job->application_id, + 'server_id' => $job->server_id, + 'horizon_job_id' => $job->horizon_job_id, + 'status' => $job->status, + ]; + }); + echo 'Running jobs: '.json_encode($basicDetails)."\n"; $horizonJobIds = []; foreach ($runningJobs as $runningJob) { $horizonJobStatus = getJobStatus($runningJob->horizon_job_id); - if ($horizonJobStatus === 'unknown') { - return true; + if ($horizonJobStatus === 'unknown' || $horizonJobStatus === 'reserved') { + $horizonJobIds[] = $runningJob->horizon_job_id; } - $horizonJobIds[] = $runningJob->horizon_job_id; } if (count($horizonJobIds) === 0) { echo "No deployments in progress.\n"; @@ -1665,6 +1675,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal if ($source->value() === '/tmp' || $source->value() === '/tmp/') { return $volume; } + LocalFileVolume::updateOrCreate( [ 'mount_path' => $target, diff --git a/config/constants.php b/config/constants.php index c3f177f67..c674fe3c7 100644 --- a/config/constants.php +++ b/config/constants.php @@ -2,7 +2,7 @@ return [ 'coolify' => [ - 'version' => '4.0.0-beta.401', + 'version' => '4.0.0-beta.402', 'helper_version' => '1.0.7', 'realtime_version' => '1.0.6', 'self_hosted' => env('SELF_HOSTED', true), diff --git a/database/migrations/2025_03_29_204400_revert_some_local_volume_encryption.php b/database/migrations/2025_03_29_204400_revert_some_local_volume_encryption.php new file mode 100644 index 000000000..683f1be3d --- /dev/null +++ b/database/migrations/2025_03_29_204400_revert_some_local_volume_encryption.php @@ -0,0 +1,96 @@ +exists()) { + DB::table('local_file_volumes') + ->orderBy('id') + ->chunk(100, function ($volumes) { + foreach ($volumes as $volume) { + DB::beginTransaction(); + + try { + $fs_path = $volume->fs_path; + $mount_path = $volume->mount_path; + try { + if ($fs_path) { + $fs_path = Crypt::decryptString($fs_path); + } + } catch (\Exception $e) { + } + + try { + if ($mount_path) { + $mount_path = Crypt::decryptString($mount_path); + } + } catch (\Exception $e) { + } + + DB::table('local_file_volumes')->where('id', $volume->id)->update([ + 'fs_path' => $fs_path, + 'mount_path' => $mount_path, + ]); + echo "Updated volume {$volume->id}\n"; + } catch (\Exception $e) { + echo "Error encrypting local file volume fields: {$e->getMessage()}\n"; + Log::error('Error encrypting local file volume fields: '.$e->getMessage()); + } + DB::commit(); + } + }); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + if (DB::table('local_file_volumes')->exists()) { + DB::table('local_file_volumes') + ->orderBy('id') + ->chunk(100, function ($volumes) { + foreach ($volumes as $volume) { + DB::beginTransaction(); + try { + $fs_path = $volume->fs_path; + $mount_path = $volume->mount_path; + try { + if ($fs_path) { + $fs_path = Crypt::encrypt($fs_path); + } + } catch (\Exception $e) { + } + + try { + if ($mount_path) { + $mount_path = Crypt::encrypt($mount_path); + } + } catch (\Exception $e) { + } + + DB::table('local_file_volumes')->where('id', $volume->id)->update([ + 'fs_path' => $fs_path, + 'mount_path' => $mount_path, + ]); + echo "Updated volume {$volume->id}\n"; + } catch (\Exception $e) { + echo "Error decrypting local file volume fields: {$e->getMessage()}\n"; + Log::error('Error decrypting local file volume fields: '.$e->getMessage()); + } + DB::commit(); + } + }); + } + } +}; diff --git a/other/nightly/versions.json b/other/nightly/versions.json index 03c56756d..1ad2d31cf 100644 --- a/other/nightly/versions.json +++ b/other/nightly/versions.json @@ -1,10 +1,10 @@ { "coolify": { "v4": { - "version": "4.0.0-beta.401" + "version": "4.0.0-beta.402" }, "nightly": { - "version": "4.0.0-beta.402" + "version": "4.0.0-beta.403" }, "helper": { "version": "1.0.7" diff --git a/resources/views/livewire/notifications/email.blade.php b/resources/views/livewire/notifications/email.blade.php index 7b0878b25..194cb3b43 100644 --- a/resources/views/livewire/notifications/email.blade.php +++ b/resources/views/livewire/notifications/email.blade.php @@ -14,7 +14,7 @@
+ id="testEmailAddress" label="Recipient" required /> Send Email diff --git a/resources/views/livewire/project/service/file-storage.blade.php b/resources/views/livewire/project/service/file-storage.blade.php index 6010000c4..594f9f7cc 100644 --- a/resources/views/livewire/project/service/file-storage.blade.php +++ b/resources/views/livewire/project/service/file-storage.blade.php @@ -1,16 +1,9 @@ -
+
- {{-- @if (data_get($resource, 'build_pack') === 'dockercompose') -

{{ data_get($resource, 'name', 'unknown') }}

- @endif --}} - @if ($fileStorage->is_directory) -

Directory Mount

- @else -

File Mount

- @endif - - - +
+ + +
@@ -39,6 +32,7 @@ confirmationLabel="Please confirm the execution of the actions by entering the Filepath below" shortConfirmationLabel="Filepath" :confirmWithPassword="false" step2ButtonText="Convert to directory" /> @endif + Load from server @if ($isReadOnly) @if ($isFirst) - @if ( - $storage->resource_type === 'App\Models\ServiceApplication' || - $storage->resource_type === 'App\Models\ServiceDatabase') - - @else - - @endif - @if ($isService || $startedAt) - - - @else - - - - Update - - @endif +
+ @if ( + $storage->resource_type === 'App\Models\ServiceApplication' || + $storage->resource_type === 'App\Models\ServiceDatabase') + + @else + + @endif + @if ($isService || $startedAt) + + + @else + + + + Update + + @endif +
@else - - - +
+ + + +
@endif @else @if ($isFirst) - - - +
+ + + +
@else - - - +
+ + + +
@endif
diff --git a/resources/views/livewire/settings-email.blade.php b/resources/views/livewire/settings-email.blade.php index 02523bbcc..4376cbf92 100644 --- a/resources/views/livewire/settings-email.blade.php +++ b/resources/views/livewire/settings-email.blade.php @@ -13,7 +13,7 @@ + label="Recipient" required /> Send Email diff --git a/versions.json b/versions.json index 03c56756d..1ad2d31cf 100644 --- a/versions.json +++ b/versions.json @@ -1,10 +1,10 @@ { "coolify": { "v4": { - "version": "4.0.0-beta.401" + "version": "4.0.0-beta.402" }, "nightly": { - "version": "4.0.0-beta.402" + "version": "4.0.0-beta.403" }, "helper": { "version": "1.0.7"