Ensure that the uuid field is preserved during clone operations for persistent volumes across all clone methods (CloneMe, ResourceOperations, and the clone_application helper). This prevents UUID conflicts and ensures cloned volumes receive new unique identifiers as intended. Adds test coverage validating that cloned persistent volumes receive new UUIDs distinct from the original volumes.
406 lines
17 KiB
PHP
406 lines
17 KiB
PHP
<?php
|
|
|
|
namespace App\Livewire\Project\Shared;
|
|
|
|
use App\Actions\Database\StartDatabase;
|
|
use App\Actions\Database\StopDatabase;
|
|
use App\Actions\Service\StartService;
|
|
use App\Actions\Service\StopService;
|
|
use App\Jobs\VolumeCloneJob;
|
|
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;
|
|
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;
|
|
use Visus\Cuid2\Cuid2;
|
|
|
|
class ResourceOperations extends Component
|
|
{
|
|
use AuthorizesRequests;
|
|
|
|
public $resource;
|
|
|
|
public $projectUuid;
|
|
|
|
public $environmentUuid;
|
|
|
|
public $projects;
|
|
|
|
public $servers;
|
|
|
|
public bool $cloneVolumeData = false;
|
|
|
|
public function mount()
|
|
{
|
|
$parameters = get_route_parameters();
|
|
$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());
|
|
}
|
|
|
|
public function toggleVolumeCloning(bool $value)
|
|
{
|
|
$this->cloneVolumeData = $value;
|
|
}
|
|
|
|
public function cloneTo($destination_id)
|
|
{
|
|
$this->authorize('update', $this->resource);
|
|
|
|
$teamScope = fn ($q) => $q->where('team_id', currentTeam()->id);
|
|
$new_destination = StandaloneDocker::whereHas('server', $teamScope)->find($destination_id);
|
|
if (! $new_destination) {
|
|
$new_destination = SwarmDocker::whereHas('server', $teamScope)->find($destination_id);
|
|
}
|
|
if (! $new_destination) {
|
|
return $this->addError('destination_id', 'Destination not found.');
|
|
}
|
|
$uuid = (string) new Cuid2;
|
|
$server = $new_destination->server;
|
|
|
|
if ($this->resource->getMorphClass() === Application::class) {
|
|
$new_resource = clone_application($this->resource, $new_destination, ['uuid' => $uuid], $this->cloneVolumeData);
|
|
|
|
$route = route('project.application.configuration', [
|
|
'project_uuid' => $this->projectUuid,
|
|
'environment_uuid' => $this->environmentUuid,
|
|
'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
|
|
) {
|
|
$uuid = (string) new Cuid2;
|
|
$new_resource = $this->resource->replicate([
|
|
'id',
|
|
'created_at',
|
|
'updated_at',
|
|
])->forceFill([
|
|
'uuid' => $uuid,
|
|
'name' => $this->resource->name.'-clone-'.$uuid,
|
|
'status' => 'exited',
|
|
'started_at' => null,
|
|
'destination_id' => $new_destination->id,
|
|
]);
|
|
$new_resource->save();
|
|
|
|
$tags = $this->resource->tags;
|
|
foreach ($tags as $tag) {
|
|
$new_resource->tags()->attach($tag->id);
|
|
}
|
|
|
|
$new_resource->persistentStorages()->delete();
|
|
$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;
|
|
} 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',
|
|
])->forceFill([
|
|
'name' => $newName,
|
|
'resource_id' => $new_resource->id,
|
|
]);
|
|
$newPersistentVolume->save();
|
|
|
|
if ($this->cloneVolumeData) {
|
|
try {
|
|
StopDatabase::dispatch($this->resource);
|
|
$sourceVolume = $volume->name;
|
|
$targetVolume = $newPersistentVolume->name;
|
|
$sourceServer = $this->resource->destination->server;
|
|
$targetServer = $new_resource->destination->server;
|
|
|
|
VolumeCloneJob::dispatch($sourceVolume, $targetVolume, $sourceServer, $targetServer, $newPersistentVolume);
|
|
|
|
StartDatabase::dispatch($this->resource);
|
|
} catch (\Exception $e) {
|
|
\Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage());
|
|
}
|
|
}
|
|
}
|
|
|
|
$fileStorages = $this->resource->fileStorages()->get();
|
|
foreach ($fileStorages as $storage) {
|
|
$newStorage = $storage->replicate([
|
|
'id',
|
|
'created_at',
|
|
'updated_at',
|
|
])->forceFill([
|
|
'resource_id' => $new_resource->id,
|
|
]);
|
|
$newStorage->save();
|
|
}
|
|
|
|
$scheduledBackups = $this->resource->scheduledBackups()->get();
|
|
foreach ($scheduledBackups as $backup) {
|
|
$uuid = (string) new Cuid2;
|
|
$newBackup = $backup->replicate([
|
|
'id',
|
|
'created_at',
|
|
'updated_at',
|
|
])->forceFill([
|
|
'uuid' => $uuid,
|
|
'database_id' => $new_resource->id,
|
|
'database_type' => $new_resource->getMorphClass(),
|
|
'team_id' => currentTeam()->id,
|
|
]);
|
|
$newBackup->save();
|
|
}
|
|
|
|
$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',
|
|
])->forceFill($payload);
|
|
$newEnvironmentVariable->save();
|
|
}
|
|
|
|
$route = route('project.database.configuration', [
|
|
'project_uuid' => $this->projectUuid,
|
|
'environment_uuid' => $this->environmentUuid,
|
|
'database_uuid' => $new_resource->uuid,
|
|
]).'#resource-operations';
|
|
|
|
return redirect()->to($route);
|
|
} elseif ($this->resource->type() === 'service') {
|
|
$uuid = (string) new Cuid2;
|
|
$new_resource = $this->resource->replicate([
|
|
'id',
|
|
'created_at',
|
|
'updated_at',
|
|
])->forceFill([
|
|
'uuid' => $uuid,
|
|
'name' => $this->resource->name.'-clone-'.$uuid,
|
|
'destination_id' => $new_destination->id,
|
|
'destination_type' => $new_destination->getMorphClass(),
|
|
'server_id' => $new_destination->server_id, // server_id is probably not needed anymore because of the new polymorphic relationships (here it is needed for clone to a different server to work - but maybe we can drop the column)
|
|
]);
|
|
|
|
$new_resource->save();
|
|
|
|
$tags = $this->resource->tags;
|
|
foreach ($tags as $tag) {
|
|
$new_resource->tags()->attach($tag->id);
|
|
}
|
|
|
|
$scheduledTasks = $this->resource->scheduled_tasks()->get();
|
|
foreach ($scheduledTasks as $task) {
|
|
$newTask = $task->replicate([
|
|
'id',
|
|
'created_at',
|
|
'updated_at',
|
|
])->forceFill([
|
|
'uuid' => (string) new Cuid2,
|
|
'service_id' => $new_resource->id,
|
|
'team_id' => currentTeam()->id,
|
|
]);
|
|
$newTask->save();
|
|
}
|
|
|
|
$environmentVariables = $this->resource->environment_variables()->get();
|
|
foreach ($environmentVariables as $environmentVariable) {
|
|
$newEnvironmentVariable = $environmentVariable->replicate([
|
|
'id',
|
|
'created_at',
|
|
'updated_at',
|
|
])->forceFill([
|
|
'resourceable_id' => $new_resource->id,
|
|
'resourceable_type' => $new_resource->getMorphClass(),
|
|
]);
|
|
$newEnvironmentVariable->save();
|
|
}
|
|
|
|
foreach ($new_resource->applications() as $application) {
|
|
$application->forceFill([
|
|
'status' => 'exited',
|
|
])->save();
|
|
|
|
$persistentVolumes = $application->persistentStorages()->get();
|
|
foreach ($persistentVolumes as $volume) {
|
|
$newName = '';
|
|
if (str_starts_with($volume->name, $volume->resource->uuid)) {
|
|
$newName = str($volume->name)->replace($volume->resource->uuid, $application->uuid);
|
|
} else {
|
|
$newName = $application->uuid.'-'.str($volume->name)->afterLast('-');
|
|
}
|
|
|
|
$newPersistentVolume = $volume->replicate([
|
|
'id',
|
|
'created_at',
|
|
'updated_at',
|
|
'uuid',
|
|
])->forceFill([
|
|
'name' => $newName,
|
|
'resource_id' => $application->id,
|
|
]);
|
|
$newPersistentVolume->save();
|
|
|
|
if ($this->cloneVolumeData) {
|
|
try {
|
|
StopService::dispatch($application);
|
|
$sourceVolume = $volume->name;
|
|
$targetVolume = $newPersistentVolume->name;
|
|
$sourceServer = $application->service->destination->server;
|
|
$targetServer = $new_resource->destination->server;
|
|
|
|
VolumeCloneJob::dispatch($sourceVolume, $targetVolume, $sourceServer, $targetServer, $newPersistentVolume);
|
|
|
|
StartService::dispatch($application);
|
|
} catch (\Exception $e) {
|
|
\Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach ($new_resource->databases() as $database) {
|
|
$database->forceFill([
|
|
'status' => 'exited',
|
|
])->save();
|
|
|
|
$persistentVolumes = $database->persistentStorages()->get();
|
|
foreach ($persistentVolumes as $volume) {
|
|
$newName = '';
|
|
if (str_starts_with($volume->name, $volume->resource->uuid)) {
|
|
$newName = str($volume->name)->replace($volume->resource->uuid, $database->uuid);
|
|
} else {
|
|
$newName = $database->uuid.'-'.str($volume->name)->afterLast('-');
|
|
}
|
|
|
|
$newPersistentVolume = $volume->replicate([
|
|
'id',
|
|
'created_at',
|
|
'updated_at',
|
|
'uuid',
|
|
])->forceFill([
|
|
'name' => $newName,
|
|
'resource_id' => $database->id,
|
|
]);
|
|
$newPersistentVolume->save();
|
|
|
|
if ($this->cloneVolumeData) {
|
|
try {
|
|
StopService::dispatch($database->service);
|
|
$sourceVolume = $volume->name;
|
|
$targetVolume = $newPersistentVolume->name;
|
|
$sourceServer = $database->service->destination->server;
|
|
$targetServer = $new_resource->destination->server;
|
|
|
|
VolumeCloneJob::dispatch($sourceVolume, $targetVolume, $sourceServer, $targetServer, $newPersistentVolume);
|
|
|
|
StartService::dispatch($database->service);
|
|
} catch (\Exception $e) {
|
|
\Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$new_resource->parse();
|
|
|
|
$route = route('project.service.configuration', [
|
|
'project_uuid' => $this->projectUuid,
|
|
'environment_uuid' => $this->environmentUuid,
|
|
'service_uuid' => $new_resource->uuid,
|
|
]).'#resource-operations';
|
|
|
|
return redirect()->to($route);
|
|
}
|
|
}
|
|
|
|
public function moveTo($environment_id)
|
|
{
|
|
try {
|
|
$this->authorize('update', $this->resource);
|
|
$new_environment = Environment::ownedByCurrentTeam()->findOrFail($environment_id);
|
|
$this->resource->forceFill([
|
|
'environment_id' => $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 render()
|
|
{
|
|
return view('livewire.project.shared.resource-operations');
|
|
}
|
|
}
|