Merge branch 'next' into main

This commit is contained in:
Karan V
2025-03-31 22:35:28 +09:00
committed by GitHub
16 changed files with 269 additions and 28 deletions

View File

@@ -57,6 +57,17 @@ class StartDragonfly
$server = $this->database->destination->server; $server = $this->database->destination->server;
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first(); $caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
if (! $caCert) {
$server->generateCaCertificate();
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
}
if (! $caCert) {
$this->dispatch('error', 'No CA certificate found for this database. Please generate a CA certificate for this server in the server/advanced page.');
return;
}
$this->ssl_certificate = $this->database->sslCertificates()->first(); $this->ssl_certificate = $this->database->sslCertificates()->first();
if (! $this->ssl_certificate) { if (! $this->ssl_certificate) {

View File

@@ -58,6 +58,17 @@ class StartKeydb
$server = $this->database->destination->server; $server = $this->database->destination->server;
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first(); $caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
if (! $caCert) {
$server->generateCaCertificate();
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
}
if (! $caCert) {
$this->dispatch('error', 'No CA certificate found for this database. Please generate a CA certificate for this server in the server/advanced page.');
return;
}
$this->ssl_certificate = $this->database->sslCertificates()->first(); $this->ssl_certificate = $this->database->sslCertificates()->first();
if (! $this->ssl_certificate) { if (! $this->ssl_certificate) {

View File

@@ -59,6 +59,17 @@ class StartMariadb
$server = $this->database->destination->server; $server = $this->database->destination->server;
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first(); $caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
if (! $caCert) {
$server->generateCaCertificate();
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
}
if (! $caCert) {
$this->dispatch('error', 'No CA certificate found for this database. Please generate a CA certificate for this server in the server/advanced page.');
return;
}
$this->ssl_certificate = $this->database->sslCertificates()->first(); $this->ssl_certificate = $this->database->sslCertificates()->first();
if (! $this->ssl_certificate) { if (! $this->ssl_certificate) {

View File

@@ -63,6 +63,16 @@ class StartMongodb
$server = $this->database->destination->server; $server = $this->database->destination->server;
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first(); $caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
if (! $caCert) {
$server->generateCaCertificate();
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
}
if (! $caCert) {
$this->dispatch('error', 'No CA certificate found for this database. Please generate a CA certificate for this server in the server/advanced page.');
return;
}
$this->ssl_certificate = $this->database->sslCertificates()->first(); $this->ssl_certificate = $this->database->sslCertificates()->first();
if (! $this->ssl_certificate) { if (! $this->ssl_certificate) {

View File

@@ -59,6 +59,17 @@ class StartMysql
$server = $this->database->destination->server; $server = $this->database->destination->server;
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first(); $caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
if (! $caCert) {
$server->generateCaCertificate();
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
}
if (! $caCert) {
$this->dispatch('error', 'No CA certificate found for this database. Please generate a CA certificate for this server in the server/advanced page.');
return;
}
$this->ssl_certificate = $this->database->sslCertificates()->first(); $this->ssl_certificate = $this->database->sslCertificates()->first();
if (! $this->ssl_certificate) { if (! $this->ssl_certificate) {

View File

@@ -64,6 +64,17 @@ class StartPostgresql
$server = $this->database->destination->server; $server = $this->database->destination->server;
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first(); $caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
if (! $caCert) {
$server->generateCaCertificate();
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
}
if (! $caCert) {
$this->dispatch('error', 'No CA certificate found for this database. Please generate a CA certificate for this server in the server/advanced page.');
return;
}
$this->ssl_certificate = $this->database->sslCertificates()->first(); $this->ssl_certificate = $this->database->sslCertificates()->first();
if (! $this->ssl_certificate) { if (! $this->ssl_certificate) {

View File

@@ -58,6 +58,17 @@ class StartRedis
$server = $this->database->destination->server; $server = $this->database->destination->server;
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first(); $caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
if (! $caCert) {
$server->generateCaCertificate();
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
}
if (! $caCert) {
$this->dispatch('error', 'No CA certificate found for this database. Please generate a CA certificate for this server in the server/advanced page.');
return;
}
$this->ssl_certificate = $this->database->sslCertificates()->first(); $this->ssl_certificate = $this->database->sslCertificates()->first();
if (! $this->ssl_certificate) { if (! $this->ssl_certificate) {

View File

@@ -2027,7 +2027,11 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
if (str($this->application->custom_nginx_configuration)->isNotEmpty()) { if (str($this->application->custom_nginx_configuration)->isNotEmpty()) {
$nginx_config = base64_encode($this->application->custom_nginx_configuration); $nginx_config = base64_encode($this->application->custom_nginx_configuration);
} else { } else {
$nginx_config = base64_encode(defaultNginxConfiguration()); if ($this->application->settings->is_spa) {
$nginx_config = base64_encode(defaultNginxConfiguration('spa'));
} else {
$nginx_config = base64_encode(defaultNginxConfiguration());
}
} }
} else { } else {
if ($this->application->build_pack === 'nixpacks') { if ($this->application->build_pack === 'nixpacks') {
@@ -2094,7 +2098,11 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
if (str($this->application->custom_nginx_configuration)->isNotEmpty()) { if (str($this->application->custom_nginx_configuration)->isNotEmpty()) {
$nginx_config = base64_encode($this->application->custom_nginx_configuration); $nginx_config = base64_encode($this->application->custom_nginx_configuration);
} else { } else {
$nginx_config = base64_encode(defaultNginxConfiguration()); if ($this->application->settings->is_spa) {
$nginx_config = base64_encode(defaultNginxConfiguration('spa'));
} else {
$nginx_config = base64_encode(defaultNginxConfiguration());
}
} }
} }
$build_command = "docker build {$this->addHosts} --network host -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}"; $build_command = "docker build {$this->addHosts} --network host -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}";

View File

@@ -86,6 +86,7 @@ class General extends Component
'application.post_deployment_command_container' => 'nullable', 'application.post_deployment_command_container' => 'nullable',
'application.custom_nginx_configuration' => 'nullable', 'application.custom_nginx_configuration' => 'nullable',
'application.settings.is_static' => 'boolean|required', 'application.settings.is_static' => 'boolean|required',
'application.settings.is_spa' => 'boolean|required',
'application.settings.is_build_server_enabled' => 'boolean|required', 'application.settings.is_build_server_enabled' => 'boolean|required',
'application.settings.is_container_label_escape_enabled' => 'boolean|required', 'application.settings.is_container_label_escape_enabled' => 'boolean|required',
'application.settings.is_container_label_readonly_enabled' => 'boolean|required', 'application.settings.is_container_label_readonly_enabled' => 'boolean|required',
@@ -124,6 +125,7 @@ class General extends Component
'application.docker_compose_custom_build_command' => 'Docker compose custom build command', 'application.docker_compose_custom_build_command' => 'Docker compose custom build command',
'application.custom_nginx_configuration' => 'Custom Nginx configuration', 'application.custom_nginx_configuration' => 'Custom Nginx configuration',
'application.settings.is_static' => 'Is static', 'application.settings.is_static' => 'Is static',
'application.settings.is_spa' => 'Is SPA',
'application.settings.is_build_server_enabled' => 'Is build server enabled', 'application.settings.is_build_server_enabled' => 'Is build server enabled',
'application.settings.is_container_label_escape_enabled' => 'Is container label escape enabled', 'application.settings.is_container_label_escape_enabled' => 'Is container label escape enabled',
'application.settings.is_container_label_readonly_enabled' => 'Is container label readonly', 'application.settings.is_container_label_readonly_enabled' => 'Is container label readonly',
@@ -171,6 +173,9 @@ class General extends Component
public function instantSave() public function instantSave()
{ {
if ($this->application->settings->isDirty('is_spa')) {
$this->generateNginxConfiguration($this->application->settings->is_spa ? 'spa' : 'static');
}
$this->application->settings->save(); $this->application->settings->save();
$this->dispatch('success', 'Settings saved.'); $this->dispatch('success', 'Settings saved.');
$this->application->refresh(); $this->application->refresh();
@@ -190,6 +195,7 @@ class General extends Component
if ($this->application->settings->is_container_label_readonly_enabled) { if ($this->application->settings->is_container_label_readonly_enabled) {
$this->resetDefaultLabels(false); $this->resetDefaultLabels(false);
} }
} }
public function loadComposeFile($isInit = false) public function loadComposeFile($isInit = false)
@@ -287,9 +293,9 @@ class General extends Component
} }
} }
public function generateNginxConfiguration() public function generateNginxConfiguration($type = 'static')
{ {
$this->application->custom_nginx_configuration = defaultNginxConfiguration(); $this->application->custom_nginx_configuration = defaultNginxConfiguration($type);
$this->application->save(); $this->application->save();
$this->dispatch('success', 'Nginx configuration generated.'); $this->dispatch('success', 'Nginx configuration generated.');
} }

View File

@@ -214,10 +214,23 @@ class General extends Component
return; return;
} }
$caCert = SslCertificate::where('server_id', $existingCert->server_id) $server = $this->database->destination->server;
$caCert = SslCertificate::where('server_id', $server->id)
->where('is_ca_certificate', true) ->where('is_ca_certificate', true)
->first(); ->first();
if (! $caCert) {
$server->generateCaCertificate();
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
}
if (! $caCert) {
$this->dispatch('error', 'No CA certificate found for this database. Please generate a CA certificate for this server in the server/advanced page.');
return;
}
SslHelper::generateSslCertificate( SslHelper::generateSslCertificate(
commonName: $existingCert->commonName, commonName: $existingCert->commonName,
subjectAlternativeNames: $existingCert->subjectAlternativeNames ?? [], subjectAlternativeNames: $existingCert->subjectAlternativeNames ?? [],

View File

@@ -1530,7 +1530,6 @@ class Application extends BaseModel
$interval = str($healthcheckCommand)->match('/--interval=([0-9]+[a-zµ]*)/'); $interval = str($healthcheckCommand)->match('/--interval=([0-9]+[a-zµ]*)/');
$timeout = str($healthcheckCommand)->match('/--timeout=([0-9]+[a-zµ]*)/'); $timeout = str($healthcheckCommand)->match('/--timeout=([0-9]+[a-zµ]*)/');
$start_period = str($healthcheckCommand)->match('/--start-period=([0-9]+[a-zµ]*)/'); $start_period = str($healthcheckCommand)->match('/--start-period=([0-9]+[a-zµ]*)/');
$start_interval = str($healthcheckCommand)->match('/--start-interval=([0-9]+[a-zµ]*)/');
$retries = str($healthcheckCommand)->match('/--retries=(\d+)/'); $retries = str($healthcheckCommand)->match('/--retries=(\d+)/');
if ($interval->isNotEmpty()) { if ($interval->isNotEmpty()) {
@@ -1542,13 +1541,10 @@ class Application extends BaseModel
if ($start_period->isNotEmpty()) { if ($start_period->isNotEmpty()) {
$this->health_check_start_period = parseDockerfileInterval($start_period); $this->health_check_start_period = parseDockerfileInterval($start_period);
} }
if ($start_interval->isNotEmpty()) {
$this->health_check_start_interval = parseDockerfileInterval($start_interval);
}
if ($retries->isNotEmpty()) { if ($retries->isNotEmpty()) {
$this->health_check_retries = $retries->toInteger(); $this->health_check_retries = $retries->toInteger();
} }
if ($interval || $timeout || $start_period || $start_interval || $retries) { if ($interval || $timeout || $start_period || $retries) {
$this->custom_healthcheck_found = true; $this->custom_healthcheck_found = true;
$this->save(); $this->save();
} }

View File

@@ -7,7 +7,9 @@ use App\Actions\Server\InstallDocker;
use App\Actions\Server\StartSentinel; use App\Actions\Server\StartSentinel;
use App\Enums\ProxyTypes; use App\Enums\ProxyTypes;
use App\Events\ServerReachabilityChanged; use App\Events\ServerReachabilityChanged;
use App\Helpers\SslHelper;
use App\Jobs\CheckAndStartSentinelJob; use App\Jobs\CheckAndStartSentinelJob;
use App\Jobs\RegenerateSslCertJob;
use App\Notifications\Server\Reachable; use App\Notifications\Server\Reachable;
use App\Notifications\Server\Unreachable; use App\Notifications\Server\Unreachable;
use App\Services\ConfigurationRepository; use App\Services\ConfigurationRepository;
@@ -1337,4 +1339,41 @@ $schema://$host {
$configRepository = app(ConfigurationRepository::class); $configRepository = app(ConfigurationRepository::class);
$configRepository->disableSshMux(); $configRepository->disableSshMux();
} }
public function generateCaCertificate()
{
try {
ray('Generating CA certificate for server', $this->id);
SslHelper::generateSslCertificate(
commonName: 'Coolify CA Certificate',
serverId: $this->id,
isCaCertificate: true,
validityDays: 10 * 365
);
$caCertificate = SslCertificate::where('server_id', $this->id)->where('is_ca_certificate', true)->first();
ray('CA certificate generated', $caCertificate);
if ($caCertificate) {
$certificateContent = $caCertificate->ssl_certificate;
$caCertPath = config('constants.coolify.base_config_path').'/ssl/';
$commands = collect([
"mkdir -p $caCertPath",
"chown -R 9999:root $caCertPath",
"chmod -R 700 $caCertPath",
"rm -rf $caCertPath/coolify-ca.crt",
"echo '{$certificateContent}' > $caCertPath/coolify-ca.crt",
"chmod 644 $caCertPath/coolify-ca.crt",
]);
instant_remote_process($commands, $this, false);
dispatch(new RegenerateSslCertJob(
server_id: $this->id,
force_regeneration: true
));
}
} catch (\Throwable $e) {
return handleError($e);
}
}
} }

View File

@@ -4061,9 +4061,35 @@ function isEmailRateLimited(string $limiterKey, int $decaySeconds = 3600, ?calla
return $rateLimited; return $rateLimited;
} }
function defaultNginxConfiguration(): string function defaultNginxConfiguration(string $type = 'static'): string
{ {
return 'server { if ($type === 'spa') {
return <<<'NGINX'
server {
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
# Handle 404 errors
error_page 404 /404.html;
location = /404.html {
root /usr/share/nginx/html;
internal;
}
# Handle server errors (50x)
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
internal;
}
}
NGINX;
} else {
return <<<'NGINX'
server {
location / { location / {
root /usr/share/nginx/html; root /usr/share/nginx/html;
index index.html index.htm; index index.html index.htm;
@@ -4083,7 +4109,9 @@ function defaultNginxConfiguration(): string
root /usr/share/nginx/html; root /usr/share/nginx/html;
internal; internal;
} }
}'; }
NGINX;
}
} }
function convertGitUrl(string $gitRepository, string $deploymentType, ?GithubApp $source = null): array function convertGitUrl(string $gitRepository, string $deploymentType, ?GithubApp $source = null): array

View File

@@ -13,15 +13,17 @@ return new class extends Migration
public function up(): void public function up(): void
{ {
if (DB::table('local_file_volumes')->exists()) { if (DB::table('local_file_volumes')->exists()) {
// First, get all volumes and decrypt their values
$decryptedVolumes = collect();
DB::table('local_file_volumes') DB::table('local_file_volumes')
->orderBy('id') ->orderBy('id')
->chunk(100, function ($volumes) { ->chunk(100, function ($volumes) use (&$decryptedVolumes) {
foreach ($volumes as $volume) { foreach ($volumes as $volume) {
DB::beginTransaction();
try { try {
$fs_path = $volume->fs_path; $fs_path = $volume->fs_path;
$mount_path = $volume->mount_path; $mount_path = $volume->mount_path;
try { try {
if ($fs_path) { if ($fs_path) {
$fs_path = Crypt::decryptString($fs_path); $fs_path = Crypt::decryptString($fs_path);
@@ -36,18 +38,59 @@ return new class extends Migration
} catch (\Exception $e) { } catch (\Exception $e) {
} }
DB::table('local_file_volumes')->where('id', $volume->id)->update([ $decryptedVolumes->push([
'id' => $volume->id,
'fs_path' => $fs_path, 'fs_path' => $fs_path,
'mount_path' => $mount_path, 'mount_path' => $mount_path,
'resource_id' => $volume->resource_id,
'resource_type' => $volume->resource_type,
]); ]);
echo "Updated volume {$volume->id}\n";
} catch (\Exception $e) { } catch (\Exception $e) {
echo "Error encrypting local file volume fields: {$e->getMessage()}\n"; echo "Error decrypting volume {$volume->id}: {$e->getMessage()}\n";
Log::error('Error encrypting local file volume fields: '.$e->getMessage()); Log::error("Error decrypting volume {$volume->id}: ".$e->getMessage());
} }
DB::commit();
} }
}); });
// Group by the unique constraint fields and keep only the first occurrence
$uniqueVolumes = $decryptedVolumes->groupBy(function ($volume) {
return $volume['mount_path'].'|'.$volume['resource_id'].'|'.$volume['resource_type'];
})->map(function ($group) {
return $group->first();
});
// Get IDs to delete (all except the ones we're keeping)
$idsToKeep = $uniqueVolumes->pluck('id')->toArray();
$idsToDelete = $decryptedVolumes->pluck('id')->diff($idsToKeep)->toArray();
// Delete duplicate records
if (! empty($idsToDelete)) {
// Show details of volumes being deleted
$volumesToDelete = $decryptedVolumes->whereIn('id', $idsToDelete);
echo "\nVolumes to be deleted:\n";
foreach ($volumesToDelete as $volume) {
echo "ID: {$volume['id']}, Mount Path: {$volume['mount_path']}, Resource ID: {$volume['resource_id']}, Resource Type: {$volume['resource_type']}\n";
echo "FS Path: {$volume['fs_path']}\n";
echo "-------------------\n";
}
DB::table('local_file_volumes')->whereIn('id', $idsToDelete)->delete();
echo 'Deleted '.count($idsToDelete)." duplicate volume(s)\n";
}
// Update the remaining records with decrypted values
foreach ($uniqueVolumes as $volume) {
try {
DB::table('local_file_volumes')->where('id', $volume['id'])->update([
'fs_path' => $volume['fs_path'],
'mount_path' => $volume['mount_path'],
]);
} catch (\Exception $e) {
echo "Error updating volume {$volume['id']}: {$e->getMessage()}\n";
Log::error("Error updating volume {$volume['id']}: ".$e->getMessage());
}
}
} }
} }

View File

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

View File

@@ -69,6 +69,17 @@
<x-forms.button wire:click="generateNginxConfiguration">Generate Default Nginx <x-forms.button wire:click="generateNginxConfiguration">Generate Default Nginx
Configuration</x-forms.button> Configuration</x-forms.button>
@endif @endif
<div class="w-96 pb-8">
@if ($application->could_set_build_commands())
<x-forms.checkbox instantSave id="application.settings.is_static" label="Is it a static site?"
helper="If your application is a static site or the final build assets should be served as a static site, enable this." />
@endif
@if ($application->settings->is_static && $application->build_pack !== 'static')
<x-forms.checkbox label="Is it a SPA (Single Page Application)?"
helper="If your application is a SPA, enable this." id="application.settings.is_spa"
instantSave></x-forms.checkbox>
@endif
</div>
@if ($application->build_pack !== 'dockercompose') @if ($application->build_pack !== 'dockercompose')
<div class="flex items-end gap-2"> <div class="flex items-end gap-2">
@if ($application->settings->is_container_label_readonly_enabled == false) @if ($application->settings->is_container_label_readonly_enabled == false)
@@ -274,13 +285,6 @@
label="Use a Build Server?" /> label="Use a Build Server?" />
</div> </div>
@endif @endif
@if ($application->could_set_build_commands())
<div class="w-96">
<x-forms.checkbox instantSave id="application.settings.is_static"
label="Is it a static site?"
helper="If your application is a static site or the final build assets should be served as a static site, enable this." />
</div>
@endif
@endif @endif
</div> </div>
@endif @endif