mirror of
https://github.com/tiennm99/coolify.git
synced 2026-08-20 12:23:48 +00:00
feat(servers): add cross-instance server migration (#11075)
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\Environment;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\PrivateKey;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\Team;
|
||||
use App\Services\ServerTransfer\ServerTransferClaimer;
|
||||
use App\Services\ServerTransfer\ServerTransferMigrator;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class, RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
InstanceSettings::forceCreate(['id' => 0, 'is_api_enabled' => true, 'fqdn' => 'https://coolify-a.test']);
|
||||
|
||||
$this->team = Team::factory()->create();
|
||||
$this->privateKey = PrivateKey::factory()->create(['team_id' => $this->team->id]);
|
||||
$this->server = Server::factory()->create([
|
||||
'team_id' => $this->team->id,
|
||||
'private_key_id' => $this->privateKey->id,
|
||||
'ip' => '10.88.0.10',
|
||||
'name' => 'migrate-me',
|
||||
]);
|
||||
$destination = StandaloneDocker::where('server_id', $this->server->id)->firstOrFail();
|
||||
$project = Project::factory()->create(['team_id' => $this->team->id]);
|
||||
$environment = $project->environments()->first()
|
||||
?? Environment::factory()->create(['project_id' => $project->id]);
|
||||
Application::factory()->create([
|
||||
'environment_id' => $environment->id,
|
||||
'destination_id' => $destination->id,
|
||||
'destination_type' => $destination->getMorphClass(),
|
||||
'name' => 'app-on-server',
|
||||
'ports_exposes' => '3000',
|
||||
]);
|
||||
});
|
||||
|
||||
test('migrate exports imports via http and completes locally', function () {
|
||||
Http::fake([
|
||||
'http://target.test/api/v1/servers/import' => Http::response([
|
||||
'dry_run' => false,
|
||||
'server_uuid' => $this->server->uuid,
|
||||
'claimed' => true,
|
||||
'export_id' => 'remote-export',
|
||||
'created' => ['applications' => 1],
|
||||
'warnings' => [],
|
||||
], 201),
|
||||
]);
|
||||
|
||||
$result = app(ServerTransferMigrator::class)->migrate(
|
||||
server: $this->server,
|
||||
targetUrl: 'http://target.test',
|
||||
targetToken: 'target-token-xyz',
|
||||
writeRemote: false,
|
||||
);
|
||||
|
||||
expect($result['server_uuid'])->toBe($this->server->uuid)
|
||||
->and($result['target_url'])->toBe('http://target.test')
|
||||
->and($result['import']['claimed'])->toBeTrue()
|
||||
->and($result['message'])->toContain('migrated');
|
||||
|
||||
$this->server->refresh();
|
||||
expect(data_get($this->server->server_metadata, 'transfer.status'))->toBe('transferred')
|
||||
->and((bool) $this->server->settings->force_disabled)->toBeTrue();
|
||||
|
||||
Http::assertSent(function ($request) {
|
||||
return $request->url() === 'http://target.test/api/v1/servers/import'
|
||||
&& $request->hasHeader('Authorization', 'Bearer target-token-xyz')
|
||||
&& data_get($request->data(), 'claim') === true
|
||||
&& data_get($request->data(), 'bundle.server.uuid') === $this->server->uuid;
|
||||
});
|
||||
});
|
||||
|
||||
test('migrate rewrites localhost target when running in docker style env', function () {
|
||||
// Simulate container: create a temp marker if missing is hard; instead assert host rewrite helper via migrate call
|
||||
// with Http fake matching host.docker.internal when /.dockerenv exists — skip if not in docker.
|
||||
if (! file_exists('/.dockerenv') && ! is_file('/run/.containerenv')) {
|
||||
expect(true)->toBeTrue();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Http::fake([
|
||||
'http://host.docker.internal:8001/api/v1/servers/import' => Http::response([
|
||||
'dry_run' => false,
|
||||
'server_uuid' => $this->server->uuid,
|
||||
'claimed' => true,
|
||||
'warnings' => [],
|
||||
], 201),
|
||||
]);
|
||||
|
||||
app(ServerTransferMigrator::class)->migrate(
|
||||
$this->server,
|
||||
'http://localhost:8001',
|
||||
'token',
|
||||
);
|
||||
|
||||
Http::assertSent(fn ($request) => str_contains($request->url(), 'host.docker.internal:8001'));
|
||||
});
|
||||
|
||||
test('migrate fails clearly when target is unreachable', function () {
|
||||
Http::fake([
|
||||
'http://down.test/*' => Http::failedConnection(),
|
||||
]);
|
||||
|
||||
expect(fn () => app(ServerTransferMigrator::class)->migrate(
|
||||
$this->server,
|
||||
'http://down.test',
|
||||
'token',
|
||||
))->toThrow(RuntimeException::class, 'Could not reach target');
|
||||
});
|
||||
|
||||
test('migrate fails when target returns error', function () {
|
||||
Http::fake([
|
||||
'http://target.test/api/v1/servers/import' => Http::response([
|
||||
'message' => 'A server with IP/domain already exists',
|
||||
], 422),
|
||||
]);
|
||||
|
||||
expect(fn () => app(ServerTransferMigrator::class)->migrate(
|
||||
$this->server,
|
||||
'http://target.test',
|
||||
'token',
|
||||
))->toThrow(RuntimeException::class, 'Target import failed');
|
||||
|
||||
// Source must remain unmanaged-away only after successful remote import+complete.
|
||||
$this->server->refresh();
|
||||
expect(data_get($this->server->server_metadata, 'transfer.status'))->not->toBe('transferred')
|
||||
->and((bool) $this->server->settings->force_disabled)->toBeFalse();
|
||||
});
|
||||
|
||||
test('migrate surfaces recovery guidance when complete fails after successful remote import', function () {
|
||||
Http::fake([
|
||||
'http://target.test/api/v1/servers/import' => Http::response([
|
||||
'dry_run' => false,
|
||||
'server_uuid' => $this->server->uuid,
|
||||
'claimed' => true,
|
||||
'warnings' => [],
|
||||
], 201),
|
||||
]);
|
||||
|
||||
$claimer = Mockery::mock(ServerTransferClaimer::class)->makePartial();
|
||||
$claimer->shouldReceive('markTransferred')
|
||||
->once()
|
||||
->andThrow(new RuntimeException('simulated complete failure'));
|
||||
app()->instance(ServerTransferClaimer::class, $claimer);
|
||||
|
||||
expect(fn () => app(ServerTransferMigrator::class)->migrate(
|
||||
$this->server,
|
||||
'http://target.test',
|
||||
'token',
|
||||
))->toThrow(RuntimeException::class, 'Retry complete');
|
||||
});
|
||||
Reference in New Issue
Block a user