Files
coolify/app/Livewire/Project/Shared/ResourceOperations.php
T
Andras Bacsai bb1fada600 fix(clone): replace the clone suffix instead of adding one for each clone
Cloning a clone added another "-clone-<id>" to the name, so after about
seven clones the name was longer than 255 characters and the insert failed.

Closes #9976
2026-10-27 17:06:52 +01:00

406 lines
17 KiB
PHP

<?php
namespace App\Livewire\Project\Shared;
use App\Actions\Shared\CloneVolumeDataWithSourceStopped;
use App\Actions\Shared\MigrateResourceToDestination;
use App\Jobs\VolumeCloneJob;
use App\Models\Application;
use App\Models\Environment;
use App\Models\Project;
use App\Models\Server;
use App\Models\StandaloneClickhouse;
use App\Models\StandaloneDocker;
use App\Models\StandaloneDragonfly;
use App\Models\StandaloneKeydb;
use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use App\Models\StandaloneSqlite;
use App\Models\SwarmDocker;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Validation\ValidationException;
use Livewire\Component;
use RuntimeException;
class ResourceOperations extends Component
{
use AuthorizesRequests;
public $resource;
public $projectUuid;
public $environmentUuid;
public $projects;
public $servers;
public $buildServers;
public bool $cloneVolumeData = false;
public bool $migrateVolumeData = true;
public function mount()
{
$parameters = get_route_parameters();
$this->projectUuid = data_get($parameters, 'project_uuid');
$this->environmentUuid = data_get($parameters, 'environment_uuid');
$teamId = $this->resourceTeamId();
$this->projects = Project::query()->where('team_id', $teamId)->orderByRaw('LOWER(name)')->get();
$servers = Server::query()->where('team_id', $teamId)->get();
$this->servers = $servers->reject(fn ($server) => $server->isBuildServer());
$this->buildServers = $servers->filter(fn ($server) => $server->isBuildServer());
}
public function toggleVolumeCloning(bool $value)
{
$this->cloneVolumeData = $value;
}
public function toggleVolumeMigration(bool $value): void
{
$this->migrateVolumeData = $value;
}
public function cloneTo($destination_uuid, $environment_id = null)
{
try {
$this->authorize('update', $this->resource);
if ($this->cloneVolumeData) {
$this->authorize(CloneVolumeDataWithSourceStopped::sourceAbility($this->resource), $this->resource);
}
$new_environment = $this->resource->environment;
if ($environment_id !== null) {
$new_environment = $this->findResourceTeamEnvironment($environment_id);
if (! $new_environment) {
return $this->addError('environment_id', 'Environment not found.');
}
}
$new_destination = $this->findResourceTeamDestination($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.');
}
auditLog('ui.resource.clone_started', [
'team_id' => $this->resource->team()?->id,
'resource_uuid' => $this->resource->uuid,
'resource_name' => $this->resource->name,
'resource_type' => class_basename($this->resource),
'destination_uuid' => $new_destination->uuid,
'environment_id' => $new_environment->id,
]);
if ($this->resource->getMorphClass() === Application::class) {
$new_resource = clone_application($this->resource, $new_destination, [
'uuid' => $uuid,
'environment_id' => $new_environment->id,
], $this->cloneVolumeData);
$route = route('project.application.configuration', [
'project_uuid' => $new_environment->project->uuid,
'environment_uuid' => $new_environment->uuid,
'application_uuid' => $new_resource->uuid,
]).'#resource-operations';
return redirect()->to($route);
} elseif (
$this->resource->getMorphClass() === StandalonePostgresql::class ||
$this->resource->getMorphClass() === StandaloneMongodb::class ||
$this->resource->getMorphClass() === StandaloneMysql::class ||
$this->resource->getMorphClass() === StandaloneMariadb::class ||
$this->resource->getMorphClass() === StandaloneRedis::class ||
$this->resource->getMorphClass() === StandaloneKeydb::class ||
$this->resource->getMorphClass() === StandaloneDragonfly::class ||
$this->resource->getMorphClass() === StandaloneClickhouse::class ||
$this->resource->getMorphClass() === StandaloneSqlite::class
) {
$uuid = new_public_id();
$new_resource = $this->resource->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'uuid' => $uuid,
'name' => clone_resource_name($this->resource->name, $uuid),
'status' => 'exited',
'started_at' => null,
'environment_id' => $new_environment->id,
'destination_id' => $new_destination->id,
'destination_type' => $new_destination->getMorphClass(),
]);
$new_resource->save();
$tags = $this->resource->tags;
foreach ($tags as $tag) {
$new_resource->tags()->attach($tag->id);
}
$new_resource->persistentStorages()->delete();
$pendingVolumeClones = [];
$persistentVolumes = $this->resource->persistentStorages()->get();
foreach ($persistentVolumes as $volume) {
$originalName = $volume->name;
$newName = '';
if (str_starts_with($originalName, 'postgres-data-')) {
$newName = 'postgres-data-'.$new_resource->uuid;
} elseif (str_starts_with($originalName, 'mysql-data-')) {
$newName = 'mysql-data-'.$new_resource->uuid;
} elseif (str_starts_with($originalName, 'redis-data-')) {
$newName = 'redis-data-'.$new_resource->uuid;
} elseif (str_starts_with($originalName, 'clickhouse-data-')) {
$newName = 'clickhouse-data-'.$new_resource->uuid;
} elseif (str_starts_with($originalName, 'mariadb-data-')) {
$newName = 'mariadb-data-'.$new_resource->uuid;
} elseif (str_starts_with($originalName, 'mongodb-data-')) {
$newName = 'mongodb-data-'.$new_resource->uuid;
} elseif (str_starts_with($originalName, 'keydb-data-')) {
$newName = 'keydb-data-'.$new_resource->uuid;
} elseif (str_starts_with($originalName, 'dragonfly-data-')) {
$newName = 'dragonfly-data-'.$new_resource->uuid;
} elseif (str_starts_with($originalName, 'sqlite-data-')) {
$newName = 'sqlite-data-'.$new_resource->uuid;
} else {
if (str_starts_with($volume->name, $this->resource->uuid)) {
$newName = str($volume->name)->replace($this->resource->uuid, $new_resource->uuid);
} else {
$newName = $new_resource->uuid.'-'.$volume->name;
}
}
$newPersistentVolume = $volume->replicate([
'id',
'created_at',
'updated_at',
'uuid',
])->fill([
'name' => $newName,
'resource_id' => $new_resource->id,
]);
$newPersistentVolume->save();
if ($this->cloneVolumeData) {
$pendingVolumeClones[] = new VolumeCloneJob(
$volume->name,
$newPersistentVolume->name,
$this->resource->destination->server,
$new_resource->destination->server,
$newPersistentVolume,
);
}
}
CloneVolumeDataWithSourceStopped::run($this->resource, $pendingVolumeClones);
$fileStorages = $this->resource->fileStorages()->get();
foreach ($fileStorages as $storage) {
$newStorage = $storage->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'resource_id' => $new_resource->id,
]);
$newStorage->save();
}
$scheduledBackups = $this->resource->scheduledBackups()->get();
foreach ($scheduledBackups as $backup) {
$uuid = new_public_id();
$newBackup = $backup->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'uuid' => $uuid,
'database_id' => $new_resource->id,
'database_type' => $new_resource->getMorphClass(),
'team_id' => $this->resourceTeamId(),
]);
$newBackup->save();
$backup->copyS3StoragesTo($newBackup);
}
$environmentVaribles = $this->resource->environment_variables()->get();
foreach ($environmentVaribles as $environmentVarible) {
$payload = [
'resourceable_id' => $new_resource->id,
'resourceable_type' => $new_resource->getMorphClass(),
];
$newEnvironmentVariable = $environmentVarible->replicate([
'id',
'created_at',
'updated_at',
])->fill($payload);
$newEnvironmentVariable->save();
}
$this->resource->cloneSecretManagerLinkTo($new_resource);
$route = route('project.database.configuration', [
'project_uuid' => $new_environment->project->uuid,
'environment_uuid' => $new_environment->uuid,
'database_uuid' => $new_resource->uuid,
]).'#resource-operations';
return redirect()->to($route);
} elseif ($this->resource->type() === 'service') {
$new_resource = clone_service($this->resource, $new_destination, [
'environment_id' => $new_environment->id,
], $this->cloneVolumeData);
$route = route('project.service.configuration', [
'project_uuid' => $new_environment->project->uuid,
'environment_uuid' => $new_environment->uuid,
'service_uuid' => $new_resource->uuid,
]).'#resource-operations';
return redirect()->to($route);
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function moveTo($environment_id)
{
try {
$this->authorize('update', $this->resource);
$new_environment = $this->findResourceTeamEnvironment($environment_id);
if (! $new_environment) {
return $this->addError('environment_id', 'Environment not found.');
}
$this->resource->fill([
'environment_id' => $new_environment->id,
])->save();
if ($this->resource->type() === 'application') {
$route = route('project.application.configuration', [
'project_uuid' => $new_environment->project->uuid,
'environment_uuid' => $new_environment->uuid,
'application_uuid' => $this->resource->uuid,
]).'#resource-operations';
return redirect()->to($route);
} elseif (str($this->resource->type())->startsWith('standalone-')) {
$route = route('project.database.configuration', [
'project_uuid' => $new_environment->project->uuid,
'environment_uuid' => $new_environment->uuid,
'database_uuid' => $this->resource->uuid,
]).'#resource-operations';
return redirect()->to($route);
} elseif ($this->resource->type() === 'service') {
$route = route('project.service.configuration', [
'project_uuid' => $new_environment->project->uuid,
'environment_uuid' => $new_environment->uuid,
'service_uuid' => $this->resource->uuid,
]).'#resource-operations';
return redirect()->to($route);
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function migrateTo(string $destination_uuid)
{
try {
$this->authorize('update', $this->resource);
$new_destination = $this->findResourceTeamDestination($destination_uuid);
if (! $new_destination) {
return $this->dispatch('error', 'Destination not found.');
}
$result = MigrateResourceToDestination::run(
$this->resource,
$new_destination,
$this->migrateVolumeData,
);
$this->dispatch('success', $result['message']);
$this->resource->loadMissing('environment.project');
$projectUuid = $this->projectUuid ?? $this->resource->environment?->project?->uuid;
$environmentUuid = $this->environmentUuid ?? $this->resource->environment?->uuid;
if (! $projectUuid || ! $environmentUuid) {
return null;
}
if ($this->resource->type() === 'application') {
$route = route('project.application.configuration', [
'project_uuid' => $projectUuid,
'environment_uuid' => $environmentUuid,
'application_uuid' => $this->resource->uuid,
]).'#resource-operations';
} elseif (str($this->resource->type())->startsWith('standalone-')) {
$route = route('project.database.configuration', [
'project_uuid' => $projectUuid,
'environment_uuid' => $environmentUuid,
'database_uuid' => $this->resource->uuid,
]).'#resource-operations';
} elseif ($this->resource->type() === 'service') {
$route = route('project.service.configuration', [
'project_uuid' => $projectUuid,
'environment_uuid' => $environmentUuid,
'service_uuid' => $this->resource->uuid,
]).'#resource-operations';
} else {
return null;
}
return redirect()->to($route);
} catch (ValidationException $e) {
$message = collect($e->errors())->flatten()->first() ?? $e->getMessage();
return $this->dispatch('error', $message);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
private function resourceTeamId(): int
{
return $this->resource->team()?->id ?? throw new RuntimeException('The resource has no team.');
}
/** Cloning and moving stay inside the resource team; the session team can differ after a team switch. */
private function findResourceTeamEnvironment(int|string|null $environmentId): ?Environment
{
return Environment::query()
->whereRelation('project', 'team_id', $this->resourceTeamId())
->find($environmentId);
}
private function findResourceTeamDestination(?string $uuid): StandaloneDocker|SwarmDocker|null
{
if (blank($uuid)) {
return null;
}
$teamId = $this->resourceTeamId();
$destination = StandaloneDocker::query()->whereRelation('server', 'team_id', $teamId)->where('uuid', $uuid)->first()
?? SwarmDocker::query()->whereRelation('server', 'team_id', $teamId)->where('uuid', $uuid)->first();
return $destination?->server?->canHostResources() ? $destination : null;
}
public function render()
{
return view('livewire.project.shared.resource-operations');
}
}