diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index 593207735..468589a1d 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -1237,6 +1237,12 @@ private function create_application(Request $request, $type) if (! $server) { return response()->json(['message' => 'Server not found.'], 404); } + if (! $server->canHostResources()) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => ['server_uuid' => ['The specified server is configured as a build server and cannot host resources.']], + ], 422); + } $destinations = $server->destinations(); if ($destinations->count() == 0) { return response()->json(['message' => 'Server has no destinations.'], 400); diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index d06e2eac5..f2c7f2226 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -1813,6 +1813,12 @@ public function create_database(Request $request, NewDatabaseTypes $type) if (! $server) { return response()->json(['message' => 'Server not found.'], 404); } + if (! $server->canHostResources()) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => ['server_uuid' => ['The specified server is configured as a build server and cannot host resources.']], + ], 422); + } $destinations = $server->destinations(); if ($destinations->count() == 0) { return response()->json(['message' => 'Server has no destinations.'], 400); diff --git a/app/Http/Controllers/Api/ServersController.php b/app/Http/Controllers/Api/ServersController.php index fa0017f87..4343971ed 100644 --- a/app/Http/Controllers/Api/ServersController.php +++ b/app/Http/Controllers/Api/ServersController.php @@ -736,6 +736,13 @@ public function update_server(Request $request) ], 422); } + if ($request->boolean('is_build_server') && ! $server->isBuildServer() && ! $server->isEmpty()) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => ['is_build_server' => ['A server with existing resources cannot be configured as a build server.']], + ], 422); + } + $server->update($updateFields); if ($request->has('is_build_server')) { $server->settings()->update([ diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php index 9fa512c6b..9c074c9a1 100644 --- a/app/Http/Controllers/Api/ServicesController.php +++ b/app/Http/Controllers/Api/ServicesController.php @@ -444,6 +444,12 @@ public function create_service(Request $request) if (! $server) { return response()->json(['message' => 'Server not found.'], 404); } + if (! $server->canHostResources()) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => ['server_uuid' => ['The specified server is configured as a build server and cannot host resources.']], + ], 422); + } $destinations = $server->destinations(); if ($destinations->count() == 0) { return response()->json(['message' => 'Server has no destinations.'], 400); @@ -501,7 +507,8 @@ public function create_service(Request $request) if (in_array($oneClickServiceName, NEEDS_TO_CONNECT_TO_PREDEFINED_NETWORK)) { data_set($servicePayload, 'connect_to_docker_network', true); } - $service = Service::create($servicePayload); + $service = new Service($servicePayload); + $service->save(); $service->name = $request->name ?? "$oneClickServiceName-".$service->uuid; $service->description = $request->description; if ($request->has('is_container_label_escape_enabled')) { @@ -639,6 +646,12 @@ public function create_service(Request $request) if (! $server) { return response()->json(['message' => 'Server not found.'], 404); } + if (! $server->canHostResources()) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => ['server_uuid' => ['The specified server is configured as a build server and cannot host resources.']], + ], 422); + } $destinations = $server->destinations(); if ($destinations->count() == 0) { return response()->json(['message' => 'Server has no destinations.'], 400); diff --git a/app/Livewire/Project/CloneMe.php b/app/Livewire/Project/CloneMe.php index 0a6e3d8ec..fff2b7fbf 100644 --- a/app/Livewire/Project/CloneMe.php +++ b/app/Livewire/Project/CloneMe.php @@ -34,7 +34,7 @@ class CloneMe extends Component public ?int $selectedServer = null; - public ?int $selectedDestination = null; + public ?string $selectedDestination = null; public ?Server $server = null; @@ -76,9 +76,9 @@ public function render() return view('livewire.project.clone-me'); } - public function selectServer($server_id, $destination_id) + public function selectServer($server_id, $destination_uuid) { - if ($server_id == $this->selectedServer && $destination_id == $this->selectedDestination) { + if ($server_id == $this->selectedServer && $destination_uuid === $this->selectedDestination) { $this->selectedServer = null; $this->selectedDestination = null; $this->server = null; @@ -86,7 +86,7 @@ public function selectServer($server_id, $destination_id) return; } $this->selectedServer = $server_id; - $this->selectedDestination = $destination_id; + $this->selectedDestination = $destination_uuid; $this->server = $this->servers->where('id', $server_id)->first(); } @@ -98,6 +98,10 @@ public function clone(string $type) 'selectedDestination' => 'required', 'newName' => ValidationPatterns::nameRules(), ]); + $selectedDestination = find_resource_destination_for_current_team($this->selectedDestination); + if (! $selectedDestination) { + throw new \Exception('Destination not found.'); + } if ($type === 'project') { $foundProject = Project::where('name', $this->newName)->first(); if ($foundProject) { @@ -130,7 +134,6 @@ public function clone(string $type) $databases = $this->environment->databases(); $services = $this->environment->services; foreach ($applications as $application) { - $selectedDestination = $this->servers->flatMap(fn ($server) => $server->destinations())->where('id', $this->selectedDestination)->first(); clone_application($application, $selectedDestination, [ 'environment_id' => $environment->id, ], $this->cloneVolumeData); @@ -147,7 +150,8 @@ public function clone(string $type) 'status' => 'exited', 'started_at' => null, 'environment_id' => $environment->id, - 'destination_id' => $this->selectedDestination, + 'destination_id' => $selectedDestination->id, + 'destination_type' => $selectedDestination->getMorphClass(), ]); $newDatabase->save(); @@ -265,7 +269,9 @@ public function clone(string $type) ])->fill([ 'uuid' => $uuid, 'environment_id' => $environment->id, - 'destination_id' => $this->selectedDestination, + 'destination_id' => $selectedDestination->id, + 'destination_type' => $selectedDestination->getMorphClass(), + 'server_id' => $selectedDestination->server_id, ]); $newService->save(); diff --git a/app/Livewire/Project/New/DockerCompose.php b/app/Livewire/Project/New/DockerCompose.php index 55ed8941c..2f9264730 100644 --- a/app/Livewire/Project/New/DockerCompose.php +++ b/app/Livewire/Project/New/DockerCompose.php @@ -47,19 +47,20 @@ public function submit() $environment = $project->environments()->where('uuid', $this->parameters['environment_uuid'])->firstOrFail(); $destination_uuid = $this->query['destination'] ?? null; - $destination = find_destination_for_current_team($destination_uuid); + $destination = find_resource_destination_for_current_team($destination_uuid); if (! $destination) { throw new \Exception('Destination not found.'); } $destination_class = $destination->getMorphClass(); - $service = Service::create([ + $service = new Service([ 'docker_compose_raw' => $this->dockerComposeRaw, 'environment_id' => $environment->id, 'server_id' => $destination->server_id, 'destination_id' => $destination->id, 'destination_type' => $destination_class, ]); + $service->save(); $variables = parseEnvFormatToArray($this->envFile); foreach ($variables as $key => $data) { diff --git a/app/Livewire/Project/New/DockerImage.php b/app/Livewire/Project/New/DockerImage.php index 68ee0d055..ab6063e09 100644 --- a/app/Livewire/Project/New/DockerImage.php +++ b/app/Livewire/Project/New/DockerImage.php @@ -115,7 +115,7 @@ public function submit() $parser->parse($dockerImage); $destination_uuid = $this->query['destination'] ?? null; - $destination = find_destination_for_current_team($destination_uuid); + $destination = find_resource_destination_for_current_team($destination_uuid); if (! $destination) { throw new \Exception('Destination not found.'); } @@ -133,7 +133,7 @@ public function submit() // Determine the image tag based on whether it's a hash or regular tag $imageTag = $parser->isImageHash() ? 'sha256-'.$parser->getTag() : $parser->getTag(); - $application = Application::create([ + $application = new Application([ 'name' => 'docker-image-'.new_public_id(), 'repository_project_id' => 0, 'git_repository' => 'coollabsio/coolify', @@ -147,6 +147,7 @@ public function submit() 'destination_type' => $destination_class, 'health_check_enabled' => false, ]); + $application->save(); $fqdn = generateUrl(server: $destination->server, random: $application->uuid); $application->update([ diff --git a/app/Livewire/Project/New/GithubPrivateRepository.php b/app/Livewire/Project/New/GithubPrivateRepository.php index 35e9b186e..479c2a1f5 100644 --- a/app/Livewire/Project/New/GithubPrivateRepository.php +++ b/app/Livewire/Project/New/GithubPrivateRepository.php @@ -192,7 +192,7 @@ public function submit() } $destination_uuid = $this->query['destination'] ?? null; - $destination = find_destination_for_current_team($destination_uuid); + $destination = find_resource_destination_for_current_team($destination_uuid); if (! $destination) { throw new \Exception('Destination not found.'); } @@ -201,7 +201,7 @@ public function submit() $project = Project::ownedByCurrentTeam()->where('uuid', $this->parameters['project_uuid'])->firstOrFail(); $environment = $project->environments()->where('uuid', $this->parameters['environment_uuid'])->firstOrFail(); - $application = Application::create([ + $application = new Application([ 'name' => generate_application_name($this->selected_repository_owner.'/'.$this->selected_repository_repo, $this->selected_branch_name), 'repository_project_id' => $this->selected_repository_id, 'git_repository' => str($this->selected_repository_owner)->trim()->toString().'/'.str($this->selected_repository_repo)->trim()->toString(), @@ -216,6 +216,7 @@ public function submit() 'source_id' => $this->github_app->id, 'source_type' => $this->github_app->getMorphClass(), ]); + $application->save(); $application->settings->is_static = $this->is_static; $application->settings->save(); diff --git a/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php b/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php index d5b4bbef8..fb3c0b2c3 100644 --- a/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php +++ b/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php @@ -136,7 +136,7 @@ public function submit() $this->validate(); try { $destination_uuid = $this->query['destination'] ?? null; - $destination = find_destination_for_current_team($destination_uuid); + $destination = find_resource_destination_for_current_team($destination_uuid); if (! $destination) { throw new \Exception('Destination not found.'); } @@ -185,7 +185,8 @@ public function submit() $application_init['docker_compose_location'] = $this->docker_compose_location; $application_init['base_directory'] = $this->base_directory; } - $application = Application::create($application_init); + $application = new Application($application_init); + $application->save(); $application->settings->is_static = $this->is_static; $application->settings->save(); diff --git a/app/Livewire/Project/New/PublicGitRepository.php b/app/Livewire/Project/New/PublicGitRepository.php index 4fddd744b..fdae52f7c 100644 --- a/app/Livewire/Project/New/PublicGitRepository.php +++ b/app/Livewire/Project/New/PublicGitRepository.php @@ -290,7 +290,7 @@ public function submit() $project_uuid = $this->parameters['project_uuid']; $environment_uuid = $this->parameters['environment_uuid']; - $destination = find_destination_for_current_team($destination_uuid); + $destination = find_resource_destination_for_current_team($destination_uuid); if (! $destination) { throw new \Exception('Destination not found.'); } @@ -336,7 +336,8 @@ public function submit() $application_init['docker_compose_location'] = $this->docker_compose_location; $application_init['base_directory'] = $this->base_directory; } - $application = Application::create($application_init); + $application = new Application($application_init); + $application->save(); $application->settings->is_static = $this->isStatic; $application->settings->save(); diff --git a/app/Livewire/Project/New/Select.php b/app/Livewire/Project/New/Select.php index 34601f5dd..08047fc79 100644 --- a/app/Livewire/Project/New/Select.php +++ b/app/Livewire/Project/New/Select.php @@ -24,6 +24,8 @@ class Select extends Component public Collection|null|Server $servers; + public ?Collection $buildServers = null; + public bool $onlyBuildServerAvailable = false; public ?Collection $standaloneDockers; @@ -380,7 +382,7 @@ public function setType(string $type) return; } - if (count($this->servers) === 1) { + if (count($this->servers) === 1 && $this->buildServers?->isEmpty()) { $server = $this->servers->first(); if ($server instanceof Server) { $this->setServer($server); @@ -452,12 +454,8 @@ public function whatToDoNext() public function loadServers() { $this->servers = Server::isUsable()->get()->sortBy('name'); - $this->allServers = $this->servers; - - if ($this->allServers && $this->allServers->isNotEmpty()) { - $this->onlyBuildServerAvailable = $this->allServers->every(function ($server) { - return $server->isBuildServer(); - }); - } + $this->buildServers = Server::isUsableBuildServer()->get()->sortBy('name'); + $this->allServers = $this->servers->concat($this->buildServers); + $this->onlyBuildServerAvailable = $this->servers->isEmpty() && $this->buildServers->isNotEmpty(); } } diff --git a/app/Livewire/Project/New/SimpleDockerfile.php b/app/Livewire/Project/New/SimpleDockerfile.php index 3328c5db3..24f21b4cb 100644 --- a/app/Livewire/Project/New/SimpleDockerfile.php +++ b/app/Livewire/Project/New/SimpleDockerfile.php @@ -38,7 +38,7 @@ public function submit() 'dockerfile' => 'required', ]); $destination_uuid = $this->query['destination'] ?? null; - $destination = find_destination_for_current_team($destination_uuid); + $destination = find_resource_destination_for_current_team($destination_uuid); if (! $destination) { throw new \Exception('Destination not found.'); } @@ -51,7 +51,7 @@ public function submit() if (! $port) { $port = 80; } - $application = Application::create([ + $application = new Application([ 'name' => 'dockerfile-'.new_public_id(), 'repository_project_id' => 0, 'git_repository' => 'coollabsio/coolify', @@ -66,6 +66,7 @@ public function submit() 'source_id' => 0, 'source_type' => GithubApp::class, ]); + $application->save(); $fqdn = generateUrl(server: $destination->server, random: $application->uuid); $application->update([ diff --git a/app/Livewire/Project/Resource/Create.php b/app/Livewire/Project/Resource/Create.php index e0b45eea0..19ffad55c 100644 --- a/app/Livewire/Project/Resource/Create.php +++ b/app/Livewire/Project/Resource/Create.php @@ -33,7 +33,7 @@ public function mount() return redirect()->route('dashboard'); } if (isset($type) && isset($destination_uuid)) { - $destination = find_destination_for_current_team($destination_uuid); + $destination = find_resource_destination_for_current_team($destination_uuid); if (! $destination) { return redirect()->route('dashboard'); } @@ -96,7 +96,8 @@ public function mount() if (in_array($oneClickServiceName, NEEDS_TO_CONNECT_TO_PREDEFINED_NETWORK)) { data_set($service_payload, 'connect_to_docker_network', true); } - $service = Service::create($service_payload); + $service = new Service($service_payload); + $service->save(); $service->name = "$oneClickServiceName-".$service->uuid; $service->save(); if ($oneClickDotEnvs?->count() > 0) { diff --git a/app/Livewire/Project/Shared/Destination.php b/app/Livewire/Project/Shared/Destination.php index 4f3e659da..94fb4b4eb 100644 --- a/app/Livewire/Project/Shared/Destination.php +++ b/app/Livewire/Project/Shared/Destination.php @@ -118,9 +118,8 @@ public function promote(int $network_id, int $server_id) $server = Server::ownedByCurrentTeam()->findOrFail($server_id); $network = StandaloneDocker::ownedByCurrentTeam()->where('server_id', $server->id)->findOrFail($network_id); $this->authorize('update', $this->resource); - $this->resource->getConnection()->transaction(function () use ($network, $server) { - $main_destination = $this->resource->destination; + $mainDestination = $this->resource->destination; $this->resource->update([ 'destination_id' => $network->id, 'destination_type' => StandaloneDocker::class, @@ -128,7 +127,7 @@ public function promote(int $network_id, int $server_id) $this->resource->additional_networks() ->wherePivot('server_id', $server->id) ->detach($network->id); - $this->resource->additional_networks()->attach($main_destination->id, ['server_id' => $main_destination->server->id]); + $this->resource->additional_networks()->attach($mainDestination->id, ['server_id' => $mainDestination->server->id]); }); $this->resource->refresh(); $this->refreshServers(); diff --git a/app/Livewire/Project/Shared/ResourceOperations.php b/app/Livewire/Project/Shared/ResourceOperations.php index 02171af8d..ba6a6e03d 100644 --- a/app/Livewire/Project/Shared/ResourceOperations.php +++ b/app/Livewire/Project/Shared/ResourceOperations.php @@ -11,7 +11,6 @@ use App\Models\Environment; use App\Models\Project; use App\Models\StandaloneClickhouse; -use App\Models\StandaloneDocker; use App\Models\StandaloneDragonfly; use App\Models\StandaloneKeydb; use App\Models\StandaloneMariadb; @@ -19,7 +18,6 @@ use App\Models\StandaloneMysql; use App\Models\StandalonePostgresql; use App\Models\StandaloneRedis; -use App\Models\SwarmDocker; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Livewire\Component; @@ -37,6 +35,8 @@ class ResourceOperations extends Component public $servers; + public $buildServers; + public bool $cloneVolumeData = false; public function mount() @@ -45,7 +45,9 @@ public function mount() $this->projectUuid = data_get($parameters, 'project_uuid'); $this->environmentUuid = data_get($parameters, 'environment_uuid'); $this->projects = Project::ownedByCurrentTeamCached(); - $this->servers = currentTeam()->servers->filter(fn ($server) => ! $server->isBuildServer()); + $servers = currentTeam()->servers()->get(); + $this->servers = $servers->reject(fn ($server) => $server->isBuildServer()); + $this->buildServers = $servers->filter(fn ($server) => $server->isBuildServer()); } public function toggleVolumeCloning(bool $value) @@ -53,20 +55,20 @@ public function toggleVolumeCloning(bool $value) $this->cloneVolumeData = $value; } - public function cloneTo($destination_id) + public function cloneTo($destination_uuid) { try { $this->authorize('update', $this->resource); - $new_destination = StandaloneDocker::ownedByCurrentTeam()->find($destination_id); - if (! $new_destination) { - $new_destination = SwarmDocker::ownedByCurrentTeam()->find($destination_id); - } + $new_destination = find_resource_destination_for_current_team($destination_uuid); if (! $new_destination) { return $this->addError('destination_id', 'Destination not found.'); } $uuid = new_public_id(); $server = $new_destination->server; + if (! $server->canHostResources()) { + return $this->addError('destination_id', 'The selected server cannot host resources.'); + } if ($this->resource->getMorphClass() === Application::class) { $new_resource = clone_application($this->resource, $new_destination, ['uuid' => $uuid], $this->cloneVolumeData); @@ -99,6 +101,7 @@ public function cloneTo($destination_id) 'status' => 'exited', 'started_at' => null, 'destination_id' => $new_destination->id, + 'destination_type' => $new_destination->getMorphClass(), ]); $new_resource->save(); diff --git a/app/Livewire/Server/Show.php b/app/Livewire/Server/Show.php index 6cd004362..1090e9892 100644 --- a/app/Livewire/Server/Show.php +++ b/app/Livewire/Server/Show.php @@ -202,7 +202,7 @@ public function mount(string $server_uuid) try { $this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail(); $this->syncData(); - if (! $this->server->isEmpty()) { + if (! $this->server->isBuildServer() && ! $this->server->isEmpty()) { $this->isBuildServerLocked = true; } // Load saved Hetzner status and validation state @@ -409,6 +409,12 @@ public function updatedIsBuildServer($value) { try { $this->authorize('update', $this->server); + if ($value === true && ! $this->server->isEmpty()) { + $this->isBuildServer = false; + $this->dispatch('error', 'A server with existing resources cannot be configured as a build server.'); + + return; + } if ($value === true && $this->isSentinelEnabled) { $this->isSentinelEnabled = false; $this->isMetricsEnabled = false; diff --git a/app/Models/Server.php b/app/Models/Server.php index d55df0179..3acaf3507 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -509,9 +509,29 @@ public static function ownedByCurrentTeamCached() }); } - public static function isUsable() + public static function isUsable(): Builder { - return Server::ownedByCurrentTeam()->whereRelation('settings', 'is_reachable', true)->whereRelation('settings', 'is_usable', true)->whereRelation('settings', 'is_swarm_worker', false)->whereRelation('settings', 'is_build_server', false)->whereRelation('settings', 'force_disabled', false); + return self::usableByBuildServerStatus(false); + } + + public static function isUsableBuildServer(): Builder + { + return self::usableByBuildServerStatus(true); + } + + private static function usableByBuildServerStatus(bool $isBuildServer): Builder + { + return Server::ownedByCurrentTeam() + ->whereRelation('settings', 'is_reachable', true) + ->whereRelation('settings', 'is_usable', true) + ->whereRelation('settings', 'is_swarm_worker', false) + ->whereRelation('settings', 'is_build_server', $isBuildServer) + ->whereRelation('settings', 'force_disabled', false); + } + + public function canHostResources(): bool + { + return ! $this->isBuildServer(); } public function settings() diff --git a/app/Models/ServerSetting.php b/app/Models/ServerSetting.php index e96aab4a3..0453dc793 100644 --- a/app/Models/ServerSetting.php +++ b/app/Models/ServerSetting.php @@ -109,6 +109,7 @@ class ServerSetting extends Model 'sentinel_token' => 'encrypted', 'is_reachable' => 'boolean', 'is_usable' => 'boolean', + 'is_build_server' => 'boolean', 'is_terminal_enabled' => 'boolean', 'disable_application_image_retention' => 'boolean', 'connection_timeout' => 'integer', diff --git a/bootstrap/helpers/applications.php b/bootstrap/helpers/applications.php index b7e4af7ab..339a0bcf7 100644 --- a/bootstrap/helpers/applications.php +++ b/bootstrap/helpers/applications.php @@ -220,6 +220,7 @@ function clone_application(Application $source, $destination, array $overrides = 'fqdn' => $url, 'status' => 'exited', 'destination_id' => $destination->id, + 'destination_type' => $destination->getMorphClass(), ], $overrides)); $newApplication->save(); diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 10de1f86f..8900c0cd3 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -535,6 +535,17 @@ function find_destination_for_current_team(?string $uuid): StandaloneDocker|Swar ?? SwarmDocker::ownedByCurrentTeam()->where('uuid', $uuid)->first(); } +function find_resource_destination_for_current_team(?string $uuid): StandaloneDocker|SwarmDocker|null +{ + $destination = find_destination_for_current_team($uuid); + + if (! $destination?->server?->canHostResources()) { + return null; + } + + return $destination; +} + function showBoarding(): bool { if (isDev()) { diff --git a/resources/views/livewire/project/clone-me.blade.php b/resources/views/livewire/project/clone-me.blade.php index 3c7f874ce..f150b5525 100644 --- a/resources/views/livewire/project/clone-me.blade.php +++ b/resources/views/livewire/project/clone-me.blade.php @@ -25,13 +25,13 @@ @foreach ($servers->sortBy('id') as $server) @foreach ($server->destinations() as $destination)