Merge branch 'next' into fix-traefik-startup
This commit is contained in:
commit
627cec16fa
12 changed files with 290 additions and 50 deletions
|
|
@ -1652,6 +1652,10 @@ private function create_application(Request $request, $type)
|
|||
$service->save();
|
||||
|
||||
$service->parse(isNew: true);
|
||||
|
||||
// Apply service-specific application prerequisites
|
||||
applyServiceApplicationPrerequisites($service);
|
||||
|
||||
if ($instantDeploy) {
|
||||
StartService::dispatch($service);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -376,6 +376,10 @@ public function create_service(Request $request)
|
|||
});
|
||||
}
|
||||
$service->parse(isNew: true);
|
||||
|
||||
// Apply service-specific application prerequisites
|
||||
applyServiceApplicationPrerequisites($service);
|
||||
|
||||
if ($instantDeploy) {
|
||||
StartService::dispatch($service);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,6 +74,9 @@ public function submit()
|
|||
}
|
||||
$service->parse(isNew: true);
|
||||
|
||||
// Apply service-specific application prerequisites
|
||||
applyServiceApplicationPrerequisites($service);
|
||||
|
||||
return redirect()->route('project.service.configuration', [
|
||||
'service_uuid' => $service->uuid,
|
||||
'environment_uuid' => $environment->uuid,
|
||||
|
|
|
|||
|
|
@ -102,33 +102,16 @@ public function mount()
|
|||
}
|
||||
});
|
||||
}
|
||||
$service->parse(isNew: true);
|
||||
$service->parse(isNew: true);
|
||||
|
||||
// For Beszel service disable gzip (fixes realtime not working issue)
|
||||
if ($oneClickServiceName === 'beszel') {
|
||||
$appService = $service->applications()->whereName('beszel')->first();
|
||||
if ($appService) {
|
||||
$appService->is_gzip_enabled = false;
|
||||
$appService->save();
|
||||
}
|
||||
}
|
||||
// For Appwrite services, disable strip prefix for services that handle domain requests
|
||||
if ($oneClickServiceName === 'appwrite') {
|
||||
$servicesToDisableStripPrefix = ['appwrite', 'appwrite-console', 'appwrite-realtime'];
|
||||
foreach ($servicesToDisableStripPrefix as $serviceName) {
|
||||
$appService = $service->applications()->whereName($serviceName)->first();
|
||||
if ($appService) {
|
||||
$appService->is_stripprefix_enabled = false;
|
||||
$appService->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
// Apply service-specific application prerequisites
|
||||
applyServiceApplicationPrerequisites($service);
|
||||
|
||||
return redirect()->route('project.service.configuration', [
|
||||
'service_uuid' => $service->uuid,
|
||||
'environment_uuid' => $environment->uuid,
|
||||
'project_uuid' => $project->uuid,
|
||||
]);
|
||||
return redirect()->route('project.service.configuration', [
|
||||
'service_uuid' => $service->uuid,
|
||||
'environment_uuid' => $environment->uuid,
|
||||
'project_uuid' => $project->uuid,
|
||||
]);
|
||||
}
|
||||
}
|
||||
$this->type = $type->value();
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ protected function getTraefikVersions(): ?array
|
|||
|
||||
public function getConfigurationFilePathProperty(): string
|
||||
{
|
||||
return rtrim($this->server->proxyPath(), '/') . '/docker-compose.yml';
|
||||
return rtrim($this->server->proxyPath(), '/').'/docker-compose.yml';
|
||||
}
|
||||
|
||||
public function changeProxy()
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@
|
|||
|
||||
class Upgrade extends Component
|
||||
{
|
||||
public bool $showProgress = false;
|
||||
|
||||
public bool $updateInProgress = false;
|
||||
|
||||
public bool $isUpgradeAvailable = false;
|
||||
|
|
|
|||
|
|
@ -71,4 +71,10 @@
|
|||
'pgadmin',
|
||||
'postgresus',
|
||||
];
|
||||
const NEEDS_TO_DISABLE_GZIP = [
|
||||
'beszel' => ['beszel'],
|
||||
];
|
||||
const NEEDS_TO_DISABLE_STRIPPREFIX = [
|
||||
'appwrite' => ['appwrite', 'appwrite-console', 'appwrite-realtime'],
|
||||
];
|
||||
const SHARED_VARIABLE_TYPES = ['team', 'project', 'environment'];
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
use App\Models\Service;
|
||||
use App\Models\ServiceApplication;
|
||||
use App\Models\ServiceDatabase;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Stringable;
|
||||
use Spatie\Url\Url;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
|
@ -339,3 +340,54 @@ function parseServiceEnvironmentVariable(string $key): array
|
|||
'has_port' => $hasPort,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply service-specific application prerequisites after service parse.
|
||||
*
|
||||
* This function configures application-level settings that are required for
|
||||
* specific one-click services to work correctly (e.g., disabling gzip for Beszel,
|
||||
* disabling strip prefix for Appwrite services).
|
||||
*
|
||||
* Must be called AFTER $service->parse() since it requires applications to exist.
|
||||
*
|
||||
* @param Service $service The service to apply prerequisites to
|
||||
*/
|
||||
function applyServiceApplicationPrerequisites(Service $service): void
|
||||
{
|
||||
try {
|
||||
// Extract service name from service name (format: "servicename-uuid")
|
||||
$serviceName = str($service->name)->beforeLast('-')->value();
|
||||
|
||||
// Apply gzip disabling if needed
|
||||
if (array_key_exists($serviceName, NEEDS_TO_DISABLE_GZIP)) {
|
||||
$applicationNames = NEEDS_TO_DISABLE_GZIP[$serviceName];
|
||||
foreach ($applicationNames as $applicationName) {
|
||||
$application = $service->applications()->whereName($applicationName)->first();
|
||||
if ($application) {
|
||||
$application->is_gzip_enabled = false;
|
||||
$application->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply stripprefix disabling if needed
|
||||
if (array_key_exists($serviceName, NEEDS_TO_DISABLE_STRIPPREFIX)) {
|
||||
$applicationNames = NEEDS_TO_DISABLE_STRIPPREFIX[$serviceName];
|
||||
foreach ($applicationNames as $applicationName) {
|
||||
$application = $service->applications()->whereName($applicationName)->first();
|
||||
if ($application) {
|
||||
$application->is_stripprefix_enabled = false;
|
||||
$application->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Log error but don't throw - prerequisites are nice-to-have, not critical
|
||||
Log::error('Failed to apply service application prerequisites', [
|
||||
'service_id' => $service->id,
|
||||
'service_name' => $service->name,
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -93,6 +93,10 @@ class="w-24 dark:bg-coolgray-200 dark:hover:bg-coolgray-300">Cancel
|
|||
modalOpen: false,
|
||||
showProgress: false,
|
||||
currentStatus: '',
|
||||
checkHealthInterval: null,
|
||||
checkIfIamDeadInterval: null,
|
||||
healthCheckAttempts: 0,
|
||||
startTime: null,
|
||||
confirmed() {
|
||||
this.showProgress = true;
|
||||
this.$wire.$call('upgrade')
|
||||
|
|
@ -102,43 +106,78 @@ class="w-24 dark:bg-coolgray-200 dark:hover:bg-coolgray-300">Cancel
|
|||
event.returnValue = '';
|
||||
});
|
||||
},
|
||||
getReviveStatusMessage(elapsedMinutes, attempts) {
|
||||
if (elapsedMinutes === 0) {
|
||||
return `Waiting for Coolify to come back online... (attempt ${attempts})`;
|
||||
} else if (elapsedMinutes < 2) {
|
||||
return `Waiting for Coolify to come back online... (${elapsedMinutes} minute${elapsedMinutes !== 1 ? 's' : ''} elapsed)`;
|
||||
} else if (elapsedMinutes < 5) {
|
||||
return `Update in progress, this may take several minutes... (${elapsedMinutes} minutes elapsed)`;
|
||||
} else if (elapsedMinutes < 10) {
|
||||
return `Large updates can take 10+ minutes. Please be patient... (${elapsedMinutes} minutes elapsed)`;
|
||||
} else {
|
||||
return `Still updating. If this takes longer than 15 minutes, please check server logs... (${elapsedMinutes} minutes elapsed)`;
|
||||
}
|
||||
},
|
||||
revive() {
|
||||
if (checkHealthInterval) return true;
|
||||
if (this.checkHealthInterval) return true;
|
||||
this.healthCheckAttempts = 0;
|
||||
this.startTime = Date.now();
|
||||
console.log('Checking server\'s health...')
|
||||
checkHealthInterval = setInterval(() => {
|
||||
this.checkHealthInterval = setInterval(() => {
|
||||
this.healthCheckAttempts++;
|
||||
const elapsedMinutes = Math.floor((Date.now() - this.startTime) / 60000);
|
||||
fetch('/api/health')
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
this.currentStatus =
|
||||
'Coolify is back online. Reloading this page (you can manually reload if its not done automatically)...';
|
||||
if (checkHealthInterval) clearInterval(
|
||||
checkHealthInterval);
|
||||
'Coolify is back online. Reloading this page in 5 seconds...';
|
||||
if (this.checkHealthInterval) {
|
||||
clearInterval(this.checkHealthInterval);
|
||||
this.checkHealthInterval = null;
|
||||
}
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 5000)
|
||||
} else {
|
||||
this.currentStatus =
|
||||
"Waiting for Coolify to come back from the dead..."
|
||||
this.currentStatus = this.getReviveStatusMessage(elapsedMinutes, this
|
||||
.healthCheckAttempts);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Health check failed:', error);
|
||||
this.currentStatus = this.getReviveStatusMessage(elapsedMinutes, this
|
||||
.healthCheckAttempts);
|
||||
});
|
||||
}, 2000);
|
||||
},
|
||||
upgrade() {
|
||||
if (checkIfIamDeadInterval || this.$wire.showProgress) return true;
|
||||
this.currentStatus = 'Pulling new images and updating Coolify.';
|
||||
checkIfIamDeadInterval = setInterval(() => {
|
||||
if (this.checkIfIamDeadInterval || this.showProgress) return true;
|
||||
this.currentStatus = 'Update in progress. Pulling new images and preparing to restart Coolify...';
|
||||
this.checkIfIamDeadInterval = setInterval(() => {
|
||||
fetch('/api/health')
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
this.currentStatus = "Waiting for the update process..."
|
||||
} else {
|
||||
this.currentStatus =
|
||||
"Update done, restarting Coolify & waiting until it is revived!"
|
||||
if (checkIfIamDeadInterval) clearInterval(
|
||||
checkIfIamDeadInterval);
|
||||
"Update in progress. Pulling new images and preparing to restart Coolify..."
|
||||
} else {
|
||||
this.currentStatus = "Coolify is restarting with the new version..."
|
||||
if (this.checkIfIamDeadInterval) {
|
||||
clearInterval(this.checkIfIamDeadInterval);
|
||||
this.checkIfIamDeadInterval = null;
|
||||
}
|
||||
this.revive();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Health check failed:', error);
|
||||
this.currentStatus = "Coolify is restarting with the new version..."
|
||||
if (this.checkIfIamDeadInterval) {
|
||||
clearInterval(this.checkIfIamDeadInterval);
|
||||
this.checkIfIamDeadInterval = null;
|
||||
}
|
||||
this.revive();
|
||||
});
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@
|
|||
->once()
|
||||
->with(base_path('versions.json'), Mockery::on(function ($json) {
|
||||
$data = json_decode($json, true);
|
||||
|
||||
// Should use cached version (4.0.10), not CDN version (4.0.0)
|
||||
return $data['coolify']['v4']['version'] === '4.0.10';
|
||||
}));
|
||||
|
|
@ -61,7 +62,7 @@
|
|||
return $this->settings;
|
||||
});
|
||||
|
||||
$job = new CheckForUpdatesJob();
|
||||
$job = new CheckForUpdatesJob;
|
||||
$job->handle();
|
||||
});
|
||||
|
||||
|
|
@ -87,6 +88,7 @@
|
|||
->once()
|
||||
->with(base_path('versions.json'), Mockery::on(function ($json) {
|
||||
$data = json_decode($json, true);
|
||||
|
||||
// Should use running version (4.0.10), not CDN (4.0.0) or cache (4.0.5)
|
||||
return $data['coolify']['v4']['version'] === '4.0.10';
|
||||
}));
|
||||
|
|
@ -104,7 +106,7 @@
|
|||
return $this->settings;
|
||||
});
|
||||
|
||||
$job = new CheckForUpdatesJob();
|
||||
$job = new CheckForUpdatesJob;
|
||||
$job->handle();
|
||||
});
|
||||
|
||||
|
|
@ -125,7 +127,7 @@
|
|||
return $this->settings;
|
||||
});
|
||||
|
||||
$job = new CheckForUpdatesJob();
|
||||
$job = new CheckForUpdatesJob;
|
||||
|
||||
// Should not throw even if structure is unexpected
|
||||
// data_set() handles nested path creation
|
||||
|
|
@ -159,6 +161,7 @@
|
|||
expect($data['traefik']['v3.6'])->toBe('3.6.2');
|
||||
// Sentinel should use CDN version
|
||||
expect($data['sentinel']['version'])->toBe('1.0.5');
|
||||
|
||||
return true;
|
||||
}));
|
||||
|
||||
|
|
@ -178,6 +181,6 @@
|
|||
return $this->settings;
|
||||
});
|
||||
|
||||
$job = new CheckForUpdatesJob();
|
||||
$job = new CheckForUpdatesJob;
|
||||
$job->handle();
|
||||
});
|
||||
|
|
|
|||
149
tests/Unit/ServiceApplicationPrerequisitesTest.php
Normal file
149
tests/Unit/ServiceApplicationPrerequisitesTest.php
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Service;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
beforeEach(function () {
|
||||
Log::shouldReceive('error')->andReturn(null);
|
||||
});
|
||||
|
||||
it('applies beszel gzip prerequisite correctly', function () {
|
||||
// Create a simple object to track the property change
|
||||
$application = new class
|
||||
{
|
||||
public $is_gzip_enabled = true;
|
||||
|
||||
public function save() {}
|
||||
};
|
||||
|
||||
$query = Mockery::mock();
|
||||
$query->shouldReceive('whereName')
|
||||
->with('beszel')
|
||||
->once()
|
||||
->andReturnSelf();
|
||||
$query->shouldReceive('first')
|
||||
->once()
|
||||
->andReturn($application);
|
||||
|
||||
$service = Mockery::mock(Service::class)->makePartial();
|
||||
$service->name = 'beszel-clx1ab2cd3ef4g5hi6jk7l8m9n0o1p2q3'; // CUID2 format
|
||||
$service->id = 1;
|
||||
$service->shouldReceive('applications')
|
||||
->once()
|
||||
->andReturn($query);
|
||||
|
||||
applyServiceApplicationPrerequisites($service);
|
||||
|
||||
expect($application->is_gzip_enabled)->toBeFalse();
|
||||
});
|
||||
|
||||
it('applies appwrite stripprefix prerequisite correctly', function () {
|
||||
$applications = [];
|
||||
|
||||
foreach (['appwrite', 'appwrite-console', 'appwrite-realtime'] as $name) {
|
||||
$app = new class
|
||||
{
|
||||
public $is_stripprefix_enabled = true;
|
||||
|
||||
public function save() {}
|
||||
};
|
||||
$applications[$name] = $app;
|
||||
}
|
||||
|
||||
$service = Mockery::mock(Service::class)->makePartial();
|
||||
$service->name = 'appwrite-clx1ab2cd3ef4g5hi6jk7l8m9n0o1p2q3'; // CUID2 format
|
||||
$service->id = 1;
|
||||
|
||||
$service->shouldReceive('applications')->times(3)->andReturnUsing(function () use (&$applications) {
|
||||
static $callCount = 0;
|
||||
$names = ['appwrite', 'appwrite-console', 'appwrite-realtime'];
|
||||
$currentName = $names[$callCount++];
|
||||
|
||||
$query = Mockery::mock();
|
||||
$query->shouldReceive('whereName')
|
||||
->with($currentName)
|
||||
->once()
|
||||
->andReturnSelf();
|
||||
$query->shouldReceive('first')
|
||||
->once()
|
||||
->andReturn($applications[$currentName]);
|
||||
|
||||
return $query;
|
||||
});
|
||||
|
||||
applyServiceApplicationPrerequisites($service);
|
||||
|
||||
foreach ($applications as $app) {
|
||||
expect($app->is_stripprefix_enabled)->toBeFalse();
|
||||
}
|
||||
});
|
||||
|
||||
it('handles missing applications gracefully', function () {
|
||||
$query = Mockery::mock();
|
||||
$query->shouldReceive('whereName')
|
||||
->with('beszel')
|
||||
->once()
|
||||
->andReturnSelf();
|
||||
$query->shouldReceive('first')
|
||||
->once()
|
||||
->andReturn(null);
|
||||
|
||||
$service = Mockery::mock(Service::class)->makePartial();
|
||||
$service->name = 'beszel-clx1ab2cd3ef4g5hi6jk7l8m9n0o1p2q3'; // CUID2 format
|
||||
$service->id = 1;
|
||||
$service->shouldReceive('applications')
|
||||
->once()
|
||||
->andReturn($query);
|
||||
|
||||
// Should not throw exception
|
||||
applyServiceApplicationPrerequisites($service);
|
||||
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
|
||||
it('skips services without prerequisites', function () {
|
||||
$service = Mockery::mock(Service::class)->makePartial();
|
||||
$service->name = 'unknown-clx1ab2cd3ef4g5hi6jk7l8m9n0o1p2q3'; // CUID2 format
|
||||
$service->id = 1;
|
||||
$service->shouldNotReceive('applications');
|
||||
|
||||
applyServiceApplicationPrerequisites($service);
|
||||
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
|
||||
it('correctly parses service name with single hyphen', function () {
|
||||
$service = Mockery::mock(Service::class)->makePartial();
|
||||
$service->name = 'docker-registry-clx1ab2cd3ef4g5hi6jk7l8m9n0o1p2q3'; // CUID2 format
|
||||
$service->id = 1;
|
||||
$service->shouldNotReceive('applications');
|
||||
|
||||
// Should not throw exception - validates that 'docker-registry' is correctly parsed
|
||||
applyServiceApplicationPrerequisites($service);
|
||||
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
|
||||
it('correctly parses service name with multiple hyphens', function () {
|
||||
$service = Mockery::mock(Service::class)->makePartial();
|
||||
$service->name = 'elasticsearch-with-kibana-clx1ab2cd3ef4g5hi6jk7l8m9n0o1p2q3'; // CUID2 format
|
||||
$service->id = 1;
|
||||
$service->shouldNotReceive('applications');
|
||||
|
||||
// Should not throw exception - validates that 'elasticsearch-with-kibana' is correctly parsed
|
||||
applyServiceApplicationPrerequisites($service);
|
||||
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
|
||||
it('correctly parses service name with hyphens in template name', function () {
|
||||
$service = Mockery::mock(Service::class)->makePartial();
|
||||
$service->name = 'apprise-api-clx1ab2cd3ef4g5hi6jk7l8m9n0o1p2q3'; // CUID2 format
|
||||
$service->id = 1;
|
||||
$service->shouldNotReceive('applications');
|
||||
|
||||
// Should not throw exception - validates that 'apprise-api' is correctly parsed
|
||||
applyServiceApplicationPrerequisites($service);
|
||||
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
|
|
@ -4,7 +4,6 @@
|
|||
use App\Models\InstanceSettings;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
beforeEach(function () {
|
||||
|
|
@ -46,7 +45,7 @@
|
|||
|
||||
config(['constants.coolify.version' => '4.0.10']);
|
||||
|
||||
$action = new UpdateCoolify();
|
||||
$action = new UpdateCoolify;
|
||||
|
||||
// Should throw exception - cache is older than running
|
||||
try {
|
||||
|
|
@ -115,7 +114,7 @@
|
|||
// Current version is newer
|
||||
config(['constants.coolify.version' => '4.0.10']);
|
||||
|
||||
$action = new UpdateCoolify();
|
||||
$action = new UpdateCoolify;
|
||||
|
||||
\Illuminate\Support\Facades\Log::shouldReceive('error')
|
||||
->once()
|
||||
|
|
|
|||
Loading…
Reference in a new issue