mirror of
https://github.com/coollabsio/coolify.git
synced 2026-08-24 10:05:47 -05:00
feat(resources): add cross-server resource migration (dev-only) (#11165)
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
<?php
|
||||
|
||||
use App\Actions\Shared\MigrateResourceToDestination;
|
||||
use App\Enums\BuildPackTypes;
|
||||
use App\Enums\RedirectTypes;
|
||||
use App\Enums\StaticImageTypes;
|
||||
use App\Models\Environment;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\SwarmDocker;
|
||||
use App\Rules\ValidGitBranch;
|
||||
use App\Support\ValidationPatterns;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
@@ -13,6 +16,7 @@ use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
function getTeamIdFromToken()
|
||||
{
|
||||
@@ -258,6 +262,80 @@ function moveResourceToEnvironment(Request $request, $resource, string $resource
|
||||
]);
|
||||
}
|
||||
|
||||
function migrateResourceToDestination(Request $request, $resource, string $resourceType, int $teamId): JsonResponse
|
||||
{
|
||||
if (! isDev()) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$validator = Validator::make($request->all(), [
|
||||
'destination_uuid' => 'required|string',
|
||||
'migrate_volumes' => 'boolean',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$allowedFields = ['destination_uuid', 'migrate_volumes'];
|
||||
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
|
||||
if (! empty($extraFields)) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => collect($extraFields)->mapWithKeys(fn ($field) => [$field => 'This field is not allowed.'])->toArray(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
Gate::authorize('update', $resource);
|
||||
|
||||
$destination = StandaloneDocker::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->destination_uuid)->first()
|
||||
?? SwarmDocker::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->destination_uuid)->first();
|
||||
|
||||
if (! $destination || ! $destination->server?->canHostResources()) {
|
||||
return response()->json(['message' => 'Destination not found.'], 404);
|
||||
}
|
||||
|
||||
$sourceDestination = $resource->destination;
|
||||
$migrateVolumes = $request->boolean('migrate_volumes', true);
|
||||
|
||||
try {
|
||||
$result = MigrateResourceToDestination::run(
|
||||
$resource,
|
||||
$destination,
|
||||
$migrateVolumes,
|
||||
);
|
||||
} catch (ValidationException $e) {
|
||||
return response()->json([
|
||||
'message' => collect($e->errors())->flatten()->first() ?? $e->getMessage(),
|
||||
'errors' => $e->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
auditLog('api.'.str($resourceType)->lower()->value().'.migrated', [
|
||||
'team_id' => $teamId,
|
||||
'resource_uuid' => $resource->uuid,
|
||||
'resource_type' => str($resourceType)->lower()->value(),
|
||||
'from_destination_uuid' => $sourceDestination?->uuid,
|
||||
'to_destination_uuid' => $destination->uuid,
|
||||
'from_server_id' => $sourceDestination?->server_id,
|
||||
'to_server_id' => $destination->server_id,
|
||||
'migrate_volumes' => $migrateVolumes,
|
||||
'async' => $result['async'],
|
||||
'volume_jobs' => $result['volume_jobs'],
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'message' => $result['message'],
|
||||
'uuid' => $resource->uuid,
|
||||
'destination_uuid' => $destination->uuid,
|
||||
'async' => $result['async'],
|
||||
'volume_jobs' => $result['volume_jobs'],
|
||||
]);
|
||||
}
|
||||
|
||||
function validateIncomingRequest(Request $request)
|
||||
{
|
||||
// check if request is json
|
||||
|
||||
@@ -102,6 +102,35 @@ function instant_scp(string $source, string $dest, Server $server, $throwError =
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a remote file from a managed server onto the Coolify host via SCP.
|
||||
*/
|
||||
function instant_scp_from_server(string $remoteSource, string $localDest, Server $server, $throwError = true)
|
||||
{
|
||||
return SshRetryHandler::retry(
|
||||
function () use ($remoteSource, $localDest, $server) {
|
||||
$scp_command = SshMultiplexingHelper::generateScpDownloadCommand($server, $remoteSource, $localDest);
|
||||
$process = Process::timeout(config('constants.ssh.command_timeout'))->run($scp_command);
|
||||
|
||||
$output = trim($process->output());
|
||||
$exitCode = $process->exitCode();
|
||||
|
||||
if ($exitCode !== 0) {
|
||||
excludeCertainErrors($process->errorOutput(), $exitCode);
|
||||
}
|
||||
|
||||
return $output === 'null' ? null : $output;
|
||||
},
|
||||
[
|
||||
'server' => $server->ip,
|
||||
'source' => $remoteSource,
|
||||
'dest' => $localDest,
|
||||
'function' => 'instant_scp_from_server',
|
||||
],
|
||||
$throwError
|
||||
);
|
||||
}
|
||||
|
||||
function instant_remote_process_with_timeout(Collection|array $command, Server $server, bool $throwError = true, bool $no_sudo = false): ?string
|
||||
{
|
||||
$command = $command instanceof Collection ? $command->toArray() : $command;
|
||||
|
||||
Reference in New Issue
Block a user