mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
fix(migration): block server migration when the target proxy is not running
This commit is contained in:
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Actions\Development;
|
||||
|
||||
use App\Enums\ProxyStatus;
|
||||
use App\Enums\ProxyTypes;
|
||||
use App\Models\PrivateKey;
|
||||
use App\Models\Server;
|
||||
use InvalidArgumentException;
|
||||
@@ -50,6 +52,10 @@ class SeedDevelopmentQemuServer
|
||||
'team_id' => 0,
|
||||
'private_key_id' => $privateKey->id,
|
||||
]);
|
||||
if (! $server->proxyType()) {
|
||||
$server->proxy->set('type', ProxyTypes::CADDY->value);
|
||||
$server->proxy->set('status', ProxyStatus::EXITED->value);
|
||||
}
|
||||
$server->deleted_at = null;
|
||||
$server->save();
|
||||
|
||||
|
||||
@@ -86,6 +86,12 @@ class MigrateResourceToDestination
|
||||
]);
|
||||
}
|
||||
|
||||
if ($this->hasDomains($resource) && $targetServer->isProxyShouldRun() && $targetServer->proxy->get('status') !== 'running') {
|
||||
throw ValidationException::withMessages([
|
||||
'destination_id' => 'The proxy of the target server is not running. Start it on the server page before you migrate a resource with domains.',
|
||||
]);
|
||||
}
|
||||
|
||||
$crossServer = $sourceServer && (int) $sourceServer->id !== (int) $targetServer->id;
|
||||
|
||||
if (! $crossServer) {
|
||||
@@ -169,6 +175,20 @@ class MigrateResourceToDestination
|
||||
$this->resaveFileStorages($resource->fresh());
|
||||
}
|
||||
|
||||
protected function hasDomains(
|
||||
Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse|StandaloneSqlite $resource,
|
||||
): bool {
|
||||
if ($resource instanceof Application) {
|
||||
return $resource->maintenanceDomains()->isNotEmpty();
|
||||
}
|
||||
|
||||
if ($resource instanceof Service) {
|
||||
return $resource->applications()->get()->contains(fn ($application) => filled($application->fqdn));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function stopResource(
|
||||
Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse|StandaloneSqlite $resource,
|
||||
): void {
|
||||
|
||||
@@ -458,7 +458,7 @@ class ResourceOperations extends Component
|
||||
|
||||
$new_destination = $this->findResourceTeamDestination($destination_uuid);
|
||||
if (! $new_destination) {
|
||||
return $this->addError('destination_id', 'Destination not found.');
|
||||
return $this->dispatch('error', 'Destination not found.');
|
||||
}
|
||||
|
||||
$result = MigrateResourceToDestination::run(
|
||||
@@ -503,7 +503,7 @@ class ResourceOperations extends Component
|
||||
} catch (ValidationException $e) {
|
||||
$message = collect($e->errors())->flatten()->first() ?? $e->getMessage();
|
||||
|
||||
return $this->addError('destination_id', $message);
|
||||
return $this->dispatch('error', $message);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
@@ -171,7 +171,9 @@ describe('after the session team changes', function () {
|
||||
test('migrateTo rejects a destination of the session team', function () {
|
||||
resourceOperationsAfterTeamSwitch($this, $this->applicationA)
|
||||
->call('migrateTo', $this->destinationB->uuid)
|
||||
->assertHasErrors(['destination_id' => 'Destination not found.']);
|
||||
->assertDispatched('error', 'Destination not found.');
|
||||
|
||||
expect($this->applicationA->fresh()->destination_id)->toBe($this->destinationA->id);
|
||||
});
|
||||
|
||||
test('cloneTo and moveTo still work inside the resource team', function () {
|
||||
|
||||
@@ -145,6 +145,21 @@ it('seeds one predefined root qemu server', function () {
|
||||
->and(Server::query()->where('uuid', 'like', 'development-qemu-%')->count())->toBe(1);
|
||||
});
|
||||
|
||||
it('seeds a new qemu server with the caddy proxy', function () {
|
||||
$server = SeedDevelopmentQemuServer::run('debian-root');
|
||||
|
||||
expect($server->proxyType())->toBe('CADDY')
|
||||
->and($server->proxy->get('status'))->toBe('exited');
|
||||
});
|
||||
|
||||
it('keeps the proxy of an existing server when it seeds a qemu vm for it', function () {
|
||||
$this->seed(ServerSeeder::class);
|
||||
|
||||
$server = SeedDevelopmentQemuServer::run('ubuntu-root', true, true);
|
||||
|
||||
expect($server->proxyType())->toBe('TRAEFIK');
|
||||
});
|
||||
|
||||
it('uses a qemu vm as the localhost sentinel without changing its identity', function () {
|
||||
$this->seed(ServerSeeder::class);
|
||||
|
||||
|
||||
@@ -240,7 +240,7 @@ test('livewire migrateTo rejects cross-team destination', function () {
|
||||
Livewire::test(ResourceOperations::class, ['resource' => $application])
|
||||
->set('migrateVolumeData', false)
|
||||
->call('migrateTo', $otherDestination->uuid)
|
||||
->assertHasErrors('destination_id');
|
||||
->assertDispatched('error', 'Destination not found.');
|
||||
|
||||
$application->refresh();
|
||||
expect($application->destination_id)->toBe($this->destination->id);
|
||||
@@ -322,6 +322,73 @@ test('rejects migration to a server that is not validated and reachable', functi
|
||||
MigrateResourceToDestination::run($application, $this->targetDestination, migrateVolumes: false);
|
||||
})->throws(ValidationException::class);
|
||||
|
||||
test('rejects migrating an application with a domain when the target proxy is not running', function () {
|
||||
StopApplication::shouldNotRun();
|
||||
|
||||
$this->targetServer->proxy->set('type', 'TRAEFIK');
|
||||
$this->targetServer->proxy->set('status', 'exited');
|
||||
$this->targetServer->save();
|
||||
|
||||
$application = createMigrateTestApplication($this);
|
||||
$application->update(['fqdn' => 'http://app.example.com']);
|
||||
|
||||
MigrateResourceToDestination::run($application, $this->targetDestination, migrateVolumes: false);
|
||||
})->throws(ValidationException::class, 'The proxy of the target server is not running.');
|
||||
|
||||
test('rejects migrating a service with a domain when the target proxy is not running', function () {
|
||||
StopService::shouldNotRun();
|
||||
|
||||
$service = Service::create([
|
||||
'name' => 'svc-migrate',
|
||||
'uuid' => new_public_id(),
|
||||
'environment_id' => $this->environment->id,
|
||||
'server_id' => $this->server->id,
|
||||
'destination_id' => $this->destination->id,
|
||||
'destination_type' => $this->destination->getMorphClass(),
|
||||
'docker_compose_raw' => base64_encode("services:\n app:\n image: nginx\n"),
|
||||
'docker_compose' => base64_encode("services:\n app:\n image: nginx\n"),
|
||||
]);
|
||||
$service->applications()->create(['name' => 'app', 'image' => 'nginx', 'fqdn' => 'http://svc.example.com']);
|
||||
|
||||
MigrateResourceToDestination::run($service, $this->targetDestination, migrateVolumes: false);
|
||||
})->throws(ValidationException::class, 'The proxy of the target server is not running.');
|
||||
|
||||
test('livewire migrateTo shows why the migration was rejected', function () {
|
||||
$this->actingAs($this->user);
|
||||
session(['currentTeam' => $this->team]);
|
||||
StopApplication::shouldNotRun();
|
||||
|
||||
$this->targetServer->proxy->set('type', 'TRAEFIK');
|
||||
$this->targetServer->proxy->set('status', 'exited');
|
||||
$this->targetServer->save();
|
||||
|
||||
$application = createMigrateTestApplication($this);
|
||||
$application->update(['fqdn' => 'http://app.example.com']);
|
||||
|
||||
Livewire::test(ResourceOperations::class, ['resource' => $application])
|
||||
->call('migrateTo', $this->targetDestination->uuid)
|
||||
->assertDispatched('error', 'The proxy of the target server is not running. Start it on the server page before you migrate a resource with domains.')
|
||||
->assertNoRedirect();
|
||||
});
|
||||
|
||||
test('migrates an application with a domain when the target proxy does not block it', function (string $proxyType, string $proxyStatus) {
|
||||
StopApplication::shouldRun()->once();
|
||||
|
||||
$this->targetServer->proxy->set('type', $proxyType);
|
||||
$this->targetServer->proxy->set('status', $proxyStatus);
|
||||
$this->targetServer->save();
|
||||
|
||||
$application = createMigrateTestApplication($this);
|
||||
$application->update(['fqdn' => 'http://app.example.com']);
|
||||
|
||||
MigrateResourceToDestination::run($application, $this->targetDestination, migrateVolumes: false);
|
||||
|
||||
expect($application->refresh()->destination_id)->toBe($this->targetDestination->id);
|
||||
})->with([
|
||||
'proxy running' => ['TRAEFIK', 'running'],
|
||||
'no managed proxy' => ['NONE', 'exited'],
|
||||
]);
|
||||
|
||||
test('migration is unavailable outside development mode', function () {
|
||||
config(['app.env' => 'production']);
|
||||
StopApplication::shouldNotRun();
|
||||
|
||||
Reference in New Issue
Block a user