fix(resources): clarify build server hosting restrictions (#10961)

This commit is contained in:
Andras Bacsai authored and GitHub committed 2026-07-16 21:35:23 +02:00
1 parent 7d699818e8
commit 913d033c75
25 files changed
+621 -70

No files matched your search

@@ -1237,6 +1237,12 @@ class ApplicationsController extends Controller
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);
@@ -1813,6 +1813,12 @@ class DatabasesController extends Controller
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);
@@ -736,6 +736,13 @@ class ServersController extends Controller
], 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([
@@ -444,6 +444,12 @@ class ServicesController extends Controller
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 @@ class ServicesController extends Controller
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 @@ class ServicesController extends Controller
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);
+13 -7
View File
@@ -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 @@ class CloneMe extends Component
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 @@ class CloneMe extends Component
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 @@ class CloneMe extends Component
'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 @@ class CloneMe extends Component
$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 @@ class CloneMe extends Component
'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 @@ class CloneMe extends Component
])->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();
+3 -2
View File
@@ -47,19 +47,20 @@ class DockerCompose extends Component
$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) {
+3 -2
View File
@@ -115,7 +115,7 @@ class DockerImage extends Component
$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 @@ class DockerImage extends Component
// 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 @@ class DockerImage extends Component
'destination_type' => $destination_class,
'health_check_enabled' => false,
]);
$application->save();
$fqdn = generateUrl(server: $destination->server, random: $application->uuid);
$application->update([
@@ -192,7 +192,7 @@ class GithubPrivateRepository extends Component
}
$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 @@ class GithubPrivateRepository extends Component
$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 @@ class GithubPrivateRepository extends Component
'source_id' => $this->github_app->id,
'source_type' => $this->github_app->getMorphClass(),
]);
$application->save();
$application->settings->is_static = $this->is_static;
$application->settings->save();
@@ -136,7 +136,7 @@ class GithubPrivateRepositoryDeployKey extends Component
$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 @@ class GithubPrivateRepositoryDeployKey extends Component
$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();
@@ -290,7 +290,7 @@ class PublicGitRepository extends Component
$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 @@ class PublicGitRepository extends Component
$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();
+6 -8
View File
@@ -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 @@ class Select extends Component
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 @@ class Select extends Component
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();
}
}
@@ -38,7 +38,7 @@ CMD ["nginx", "-g", "daemon off;"]
'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 @@ CMD ["nginx", "-g", "daemon off;"]
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 @@ CMD ["nginx", "-g", "daemon off;"]
'source_id' => 0,
'source_type' => GithubApp::class,
]);
$application->save();
$fqdn = generateUrl(server: $destination->server, random: $application->uuid);
$application->update([
+3 -2
View File
@@ -33,7 +33,7 @@ class Create extends Component
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 @@ class Create extends Component
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) {
+2 -3
View File
@@ -118,9 +118,8 @@ class Destination extends Component
$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 @@ class Destination extends Component
$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();
@@ -11,7 +11,6 @@ use App\Models\Application;
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\StandaloneMongodb;
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 @@ class ResourceOperations extends Component
$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 @@ class ResourceOperations extends Component
$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 @@ class ResourceOperations extends Component
'status' => 'exited',
'started_at' => null,
'destination_id' => $new_destination->id,
'destination_type' => $new_destination->getMorphClass(),
]);
$new_resource->save();
+7 -1
View File
@@ -202,7 +202,7 @@ class Show extends Component
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 @@ class Show extends Component
{
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;
+22 -2
View File
@@ -509,9 +509,29 @@ class Server extends BaseModel
});
}
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()
+1
View File
@@ -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',