diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index 6b4f1efee..ea3998f8a 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -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); } diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php index 7440cc16a..587f49fa5 100644 --- a/app/Http/Controllers/Api/ServicesController.php +++ b/app/Http/Controllers/Api/ServicesController.php @@ -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); } diff --git a/app/Livewire/Project/New/DockerCompose.php b/app/Livewire/Project/New/DockerCompose.php index a88a62d88..18bb237af 100644 --- a/app/Livewire/Project/New/DockerCompose.php +++ b/app/Livewire/Project/New/DockerCompose.php @@ -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, diff --git a/app/Livewire/Project/Resource/Create.php b/app/Livewire/Project/Resource/Create.php index f4c5f81b0..1158fb3f7 100644 --- a/app/Livewire/Project/Resource/Create.php +++ b/app/Livewire/Project/Resource/Create.php @@ -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(); diff --git a/app/Livewire/Server/Proxy.php b/app/Livewire/Server/Proxy.php index 49d872210..0264eb1eb 100644 --- a/app/Livewire/Server/Proxy.php +++ b/app/Livewire/Server/Proxy.php @@ -92,7 +92,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() diff --git a/bootstrap/helpers/constants.php b/bootstrap/helpers/constants.php index 6d9136b02..178876b89 100644 --- a/bootstrap/helpers/constants.php +++ b/bootstrap/helpers/constants.php @@ -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']; diff --git a/bootstrap/helpers/services.php b/bootstrap/helpers/services.php index 3fff2c090..3d2b61b86 100644 --- a/bootstrap/helpers/services.php +++ b/bootstrap/helpers/services.php @@ -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(), + ]); + } +} diff --git a/tests/Unit/CheckForUpdatesJobTest.php b/tests/Unit/CheckForUpdatesJobTest.php index 649ecdeb8..1dbc73e44 100644 --- a/tests/Unit/CheckForUpdatesJobTest.php +++ b/tests/Unit/CheckForUpdatesJobTest.php @@ -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(); }); diff --git a/tests/Unit/ServiceApplicationPrerequisitesTest.php b/tests/Unit/ServiceApplicationPrerequisitesTest.php new file mode 100644 index 000000000..19b1c5c8c --- /dev/null +++ b/tests/Unit/ServiceApplicationPrerequisitesTest.php @@ -0,0 +1,149 @@ +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(); +}); diff --git a/tests/Unit/UpdateCoolifyTest.php b/tests/Unit/UpdateCoolifyTest.php index 3a89d7ea9..b3f496d68 100644 --- a/tests/Unit/UpdateCoolifyTest.php +++ b/tests/Unit/UpdateCoolifyTest.php @@ -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()