mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-13 06:15:13 +00:00
fix(servers): retain cloud instances awaiting IPs
Persist DigitalOcean, Hetzner, and Vultr servers before public IP assignment, then backfill placeholder addresses from provider state. Treat partial Sentinel snapshots as non-authoritative and document the destinations API with OpenAPI schemas.
This commit is contained in:
1 parent
d3fbb32c52
commit
e01b8a057e
29 files changed
+1275
-59
No files matched your search
@@ -3,12 +3,14 @@
|
||||
use App\Exceptions\DeploymentException;
|
||||
use App\Jobs\ApplicationDeploymentJob;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationDeploymentQueue;
|
||||
use App\Models\ApplicationSetting;
|
||||
use App\Models\EnvironmentVariable;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Collection;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class);
|
||||
uses(TestCase::class, RefreshDatabase::class);
|
||||
|
||||
class TestableRailpackDeploymentJob extends ApplicationDeploymentJob
|
||||
{
|
||||
@@ -373,9 +375,9 @@ it('creates an empty build-time env file for railpack when there are no generate
|
||||
]));
|
||||
|
||||
foreach ([
|
||||
'application_deployment_queue' => new class
|
||||
'application_deployment_queue' => new class extends ApplicationDeploymentQueue
|
||||
{
|
||||
public function addLogEntry(string $message, string $type = 'info', bool $hidden = false): void {}
|
||||
public function addLogEntry(string $message, string $type = 'stdout', bool $hidden = false): void {}
|
||||
},
|
||||
'build_pack' => 'railpack',
|
||||
'pull_request_id' => 0,
|
||||
|
||||
@@ -86,6 +86,7 @@ it('detects environment variable value changes without exposing secret values',
|
||||
'is_buildtime' => false,
|
||||
'is_runtime' => true,
|
||||
'is_preview' => false,
|
||||
'is_shown_once' => true,
|
||||
'resourceable_type' => Application::class,
|
||||
'resourceable_id' => $application->id,
|
||||
]);
|
||||
@@ -112,6 +113,7 @@ it('describes added environment variables as set without exposing secret values'
|
||||
'is_buildtime' => false,
|
||||
'is_runtime' => true,
|
||||
'is_preview' => false,
|
||||
'is_shown_once' => true,
|
||||
'resourceable_type' => Application::class,
|
||||
'resourceable_id' => $application->id,
|
||||
]);
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
it('defines OpenAPI documentation for every destinations endpoint', function () {
|
||||
$controller = file_get_contents(__DIR__.'/../../app/Http/Controllers/Api/DestinationsController.php');
|
||||
$model = file_get_contents(__DIR__.'/../../app/Models/StandaloneDocker.php');
|
||||
|
||||
expect($model)->toContain("schema: 'Destination'")
|
||||
->and($controller)->toContain("operationId: 'list-destinations'")
|
||||
->and($controller)->toContain("operationId: 'get-destination-by-uuid'")
|
||||
->and($controller)->toContain("operationId: 'delete-destination-by-uuid'")
|
||||
->and($controller)->toContain("operationId: 'list-server-destinations'")
|
||||
->and($controller)->toContain("operationId: 'create-server-destination'");
|
||||
});
|
||||
@@ -10,7 +10,7 @@ use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class, RefreshDatabase::class);
|
||||
|
||||
function createDigitalOceanServerForStateTest(string $status = 'active'): Server
|
||||
function createDigitalOceanServerForStateTest(string $status = 'active', array $attributes = []): Server
|
||||
{
|
||||
$team = Team::create([
|
||||
'name' => 'Test Team',
|
||||
@@ -24,13 +24,13 @@ function createDigitalOceanServerForStateTest(string $status = 'active'): Server
|
||||
]);
|
||||
$privateKey = PrivateKey::factory()->create(['team_id' => $team->id]);
|
||||
|
||||
return Server::factory()->create([
|
||||
return Server::factory()->create(array_merge([
|
||||
'team_id' => $team->id,
|
||||
'private_key_id' => $privateKey->id,
|
||||
'cloud_provider_token_id' => $token->id,
|
||||
'digitalocean_droplet_id' => 987,
|
||||
'digitalocean_droplet_status' => $status,
|
||||
]);
|
||||
], $attributes));
|
||||
}
|
||||
|
||||
it('marks a DigitalOcean droplet as deleted when the provider returns 404', function () {
|
||||
@@ -45,6 +45,48 @@ it('marks a DigitalOcean droplet as deleted when the provider returns 404', func
|
||||
expect($server->fresh()->digitalocean_droplet_status)->toBe('deleted');
|
||||
});
|
||||
|
||||
it('backfills a placeholder IP from the DigitalOcean droplet state', function () {
|
||||
$server = createDigitalOceanServerForStateTest('new', ['ip' => Server::PLACEHOLDER_IP]);
|
||||
|
||||
Http::fake([
|
||||
'https://api.digitalocean.com/v2/droplets/987' => Http::response([
|
||||
'droplet' => [
|
||||
'id' => 987,
|
||||
'status' => 'active',
|
||||
'networks' => [
|
||||
'v4' => [
|
||||
['type' => 'public', 'ip_address' => '203.0.113.10'],
|
||||
],
|
||||
],
|
||||
],
|
||||
], 200),
|
||||
]);
|
||||
|
||||
expect($server->refreshDigitalOceanState())->toBe('active');
|
||||
expect($server->fresh()->ip)->toBe('203.0.113.10');
|
||||
});
|
||||
|
||||
it('does not overwrite a real IP from the DigitalOcean droplet state', function () {
|
||||
$server = createDigitalOceanServerForStateTest('active', ['ip' => '198.51.100.20']);
|
||||
|
||||
Http::fake([
|
||||
'https://api.digitalocean.com/v2/droplets/987' => Http::response([
|
||||
'droplet' => [
|
||||
'id' => 987,
|
||||
'status' => 'active',
|
||||
'networks' => [
|
||||
'v4' => [
|
||||
['type' => 'public', 'ip_address' => '203.0.113.10'],
|
||||
],
|
||||
],
|
||||
],
|
||||
], 200),
|
||||
]);
|
||||
|
||||
$server->refreshDigitalOceanState();
|
||||
expect($server->fresh()->ip)->toBe('198.51.100.20');
|
||||
});
|
||||
|
||||
it('does not mark a DigitalOcean droplet as deleted on transient provider errors', function () {
|
||||
$server = createDigitalOceanServerForStateTest();
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
<?php
|
||||
|
||||
test('expanded navbar theme item cycles themes without a right side selector', function () {
|
||||
test('navbar does not render the removed theme switcher', function () {
|
||||
$navbar = file_get_contents(__DIR__.'/../../resources/views/components/navbar.blade.php');
|
||||
|
||||
expect(substr_count($navbar, '@click.stop="cycleTheme()"'))->toBe(2)
|
||||
expect($navbar)->not->toContain('cycleTheme()')
|
||||
->and($navbar)->not->toContain('aria-label="Theme switcher"')
|
||||
->and($navbar)->not->toContain('ml-auto flex items-center gap-0.5')
|
||||
->and($navbar)->not->toContain('@click.stop="setTheme(\'light\')"')
|
||||
->and($navbar)->not->toContain('@click.stop="setTheme(\'system\')"')
|
||||
->and($navbar)->not->toContain('@click.stop="setTheme(\'dark\')"')
|
||||
->and($navbar)->toContain('<span class="text-left menu-item-label">Theme</span>');
|
||||
->and($navbar)->not->toContain('<span class="text-left menu-item-label">Theme</span>');
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
use App\Models\PrivateKey;
|
||||
use App\Models\Server;
|
||||
use App\Models\Team;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class, RefreshDatabase::class);
|
||||
|
||||
function createServerForPlaceholderIpTest(string $ip): Server
|
||||
{
|
||||
$team = Team::create([
|
||||
'name' => 'Test Team',
|
||||
'personal_team' => false,
|
||||
]);
|
||||
$privateKey = PrivateKey::factory()->create(['team_id' => $team->id]);
|
||||
|
||||
return Server::factory()->create([
|
||||
'team_id' => $team->id,
|
||||
'private_key_id' => $privateKey->id,
|
||||
'ip' => $ip,
|
||||
]);
|
||||
}
|
||||
|
||||
it('detects placeholder IPs', function () {
|
||||
$server = createServerForPlaceholderIpTest(Server::PLACEHOLDER_IP);
|
||||
|
||||
foreach ([Server::PLACEHOLDER_IP, '0.0.0.0', '::'] as $ip) {
|
||||
$server->update(['ip' => $ip]);
|
||||
|
||||
expect($server->fresh()->hasPlaceholderIp())->toBeTrue();
|
||||
}
|
||||
});
|
||||
|
||||
it('does not treat a real IP as a placeholder', function () {
|
||||
$server = createServerForPlaceholderIpTest('203.0.113.5');
|
||||
|
||||
expect($server->hasPlaceholderIp())->toBeFalse();
|
||||
});
|
||||
|
||||
it('backfills a placeholder IP with the real address', function () {
|
||||
$server = createServerForPlaceholderIpTest(Server::PLACEHOLDER_IP);
|
||||
|
||||
expect($server->backfillPlaceholderIp('203.0.113.5'))->toBeTrue();
|
||||
expect($server->fresh()->ip)->toBe('203.0.113.5');
|
||||
});
|
||||
|
||||
it('does not overwrite a real IP when backfilling', function () {
|
||||
$server = createServerForPlaceholderIpTest('203.0.113.5');
|
||||
|
||||
expect($server->backfillPlaceholderIp('198.51.100.9'))->toBeFalse();
|
||||
expect($server->fresh()->ip)->toBe('203.0.113.5');
|
||||
});
|
||||
|
||||
it('ignores a missing IP when backfilling', function () {
|
||||
$server = createServerForPlaceholderIpTest(Server::PLACEHOLDER_IP);
|
||||
|
||||
expect($server->backfillPlaceholderIp(null))->toBeFalse();
|
||||
expect($server->fresh()->ip)->toBe(Server::PLACEHOLDER_IP);
|
||||
});
|
||||
|
||||
it('skips servers with a placeholder IP in scheduled jobs', function () {
|
||||
$server = createServerForPlaceholderIpTest(Server::PLACEHOLDER_IP);
|
||||
|
||||
expect($server->skipServer())->toBeTrue();
|
||||
});
|
||||
Reference in new issue
Block a user