mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-04 10:42:23 +00:00
218 lines
8.9 KiB
PHP
218 lines
8.9 KiB
PHP
<?php
|
|
|
|
use App\Actions\Database\RestartDatabase;
|
|
use App\Actions\Database\StartClickhouse;
|
|
use App\Actions\Database\StartDatabase;
|
|
use App\Actions\Database\StopDatabase;
|
|
use App\Jobs\DatabaseStartJob;
|
|
use App\Livewire\Project\Database\Clickhouse\General as ClickhouseGeneral;
|
|
use App\Models\Environment;
|
|
use App\Models\InstanceSettings;
|
|
use App\Models\LocalFileVolume;
|
|
use App\Models\PrivateKey;
|
|
use App\Models\Project;
|
|
use App\Models\Server;
|
|
use App\Models\StandaloneClickhouse;
|
|
use App\Models\StandaloneDocker;
|
|
use App\Models\Team;
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Process;
|
|
use Illuminate\Support\Facades\Queue;
|
|
use Illuminate\Support\Str;
|
|
use Livewire\Livewire;
|
|
use Spatie\Activitylog\Models\Activity;
|
|
use Symfony\Component\Yaml\Yaml;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
beforeEach(function () {
|
|
Server::flushIdentityMap();
|
|
InstanceSettings::forceCreate(['id' => 0]);
|
|
config(['app.maintenance.store' => 'array', 'constants.ssh.mux_enabled' => false]);
|
|
Process::fake();
|
|
Queue::fake();
|
|
|
|
$team = Team::factory()->create();
|
|
$this->team = $team;
|
|
$this->owner = User::factory()->create();
|
|
$this->member = User::factory()->create();
|
|
$team->members()->attach($this->owner->id, ['role' => 'owner']);
|
|
$team->members()->attach($this->member->id, ['role' => 'member']);
|
|
$this->server = Server::factory()->create([
|
|
'team_id' => $team->id,
|
|
'private_key_id' => PrivateKey::factory()->create(['team_id' => $team->id])->id,
|
|
]);
|
|
$this->server->settings->update(['is_reachable' => true, 'is_usable' => true, 'force_disabled' => false]);
|
|
$this->destination = StandaloneDocker::query()->where('server_id', $this->server->id)->firstOrFail();
|
|
$project = Project::factory()->create(['team_id' => $team->id]);
|
|
$this->environment = Environment::factory()->create(['project_id' => $project->id]);
|
|
});
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
function startedClickhouseCompose(): array
|
|
{
|
|
$compose = null;
|
|
Process::assertRan(function ($process) use (&$compose) {
|
|
if (preg_match("/echo '([A-Za-z0-9+\/=]+)' \| base64 -d \| (?:sudo )?tee \S+\/docker-compose\.yml/", remoteProcessScript($process), $matches)) {
|
|
$compose = Yaml::parse(base64_decode($matches[1]));
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
return $compose;
|
|
}
|
|
|
|
it('keeps the data volume when a file mount is added', function () {
|
|
$database = create_standalone_clickhouse($this->environment->id, $this->destination);
|
|
LocalFileVolume::create([
|
|
'fs_path' => database_configuration_dir().'/'.$database->uuid.'/etc/clickhouse-server/users.d/extra.xml',
|
|
'mount_path' => '/etc/clickhouse-server/users.d/extra.xml',
|
|
'content' => '<clickhouse></clickhouse>',
|
|
'is_directory' => false,
|
|
'resource_id' => $database->id,
|
|
'resource_type' => $database->getMorphClass(),
|
|
]);
|
|
|
|
StartClickhouse::run($database->fresh(), activity()->log('Starting database'));
|
|
|
|
expect(startedClickhouseCompose()['services'][$database->uuid]['volumes'])->toBe([
|
|
"clickhouse-data-{$database->uuid}:/var/lib/clickhouse",
|
|
database_configuration_dir()."/{$database->uuid}/etc/clickhouse-server/users.d/extra.xml:/etc/clickhouse-server/users.d/extra.xml",
|
|
]);
|
|
});
|
|
|
|
/**
|
|
* Fake the remote `docker inspect` so the container's data directory is mounted from $volumeName.
|
|
*/
|
|
function fakeClickhouseDataMount(string $volumeName): void
|
|
{
|
|
Process::fake(fn ($process) => Process::result(
|
|
output: str_contains(remoteProcessScript($process), 'docker inspect') ? "volume {$volumeName}" : 'OK',
|
|
));
|
|
}
|
|
|
|
function ranRemoteCommandContaining(string $needle): bool
|
|
{
|
|
$ran = false;
|
|
Process::assertRan(function ($process) use ($needle, &$ran) {
|
|
$ran = $ran || str_contains(remoteProcessScript($process), $needle);
|
|
|
|
return true;
|
|
});
|
|
|
|
return $ran;
|
|
}
|
|
|
|
it('blocks start and restart while the current data is in an unnamed volume', function () {
|
|
$database = create_standalone_clickhouse($this->environment->id, $this->destination);
|
|
fakeClickhouseDataMount('3f1c0ffee');
|
|
|
|
expect(StartDatabase::run($database->fresh()))->toContain('unnamed Docker volume 3f1c0ffee')
|
|
->and(RestartDatabase::run($database->fresh()))->toContain('unnamed Docker volume 3f1c0ffee')
|
|
->and(ranRemoteCommandContaining("docker rm -f {$database->uuid}"))->toBeFalse();
|
|
Queue::assertNotPushed(DatabaseStartJob::class);
|
|
});
|
|
|
|
it('starts a database that uses its data volume', function () {
|
|
$database = create_standalone_clickhouse($this->environment->id, $this->destination);
|
|
fakeClickhouseDataMount("clickhouse-data-{$database->uuid}");
|
|
|
|
expect(StartDatabase::run($database->fresh()))->toBeInstanceOf(Activity::class);
|
|
Queue::assertPushed(DatabaseStartJob::class);
|
|
});
|
|
|
|
it('keeps the container on stop so the unnamed volume stays linked, except on delete', function (bool $keepAnonymousDataVolume, bool $removesContainer) {
|
|
$database = create_standalone_clickhouse($this->environment->id, $this->destination);
|
|
fakeClickhouseDataMount('3f1c0ffee');
|
|
|
|
StopDatabase::run($database->fresh(), dockerCleanup: false, keepAnonymousDataVolume: $keepAnonymousDataVolume);
|
|
|
|
expect(ranRemoteCommandContaining("docker rm -f {$database->uuid}"))->toBe($removesContainer);
|
|
})->with([
|
|
'stop' => [true, false],
|
|
'delete' => [false, true],
|
|
]);
|
|
|
|
it('lets an admin keep the current data volume and warns about it', function () {
|
|
$database = create_standalone_clickhouse($this->environment->id, $this->destination);
|
|
fakeClickhouseDataMount('3f1c0ffee');
|
|
$this->actingAs($this->owner);
|
|
session(['currentTeam' => $this->team]);
|
|
|
|
Livewire::test(ClickhouseGeneral::class, ['database' => $database->fresh()])
|
|
->assertSet('anonymousDataVolume', '3f1c0ffee')
|
|
->assertSee('Keep current data')
|
|
->call('keepCurrentDataVolume')
|
|
->assertSet('anonymousDataVolume', null);
|
|
|
|
expect($database->persistentStorages()->where('mount_path', StandaloneClickhouse::DATA_DIRECTORY)->value('name'))->toBe('3f1c0ffee')
|
|
->and(StartDatabase::prerequisiteError($database->fresh()))->toBeNull();
|
|
});
|
|
|
|
it('does not let a member keep the current data volume', function () {
|
|
$database = create_standalone_clickhouse($this->environment->id, $this->destination);
|
|
fakeClickhouseDataMount('3f1c0ffee');
|
|
$this->actingAs($this->member);
|
|
session(['currentTeam' => $this->team]);
|
|
|
|
Livewire::test(ClickhouseGeneral::class, ['database' => $database->fresh()])
|
|
->assertSee('Current data is not in the data volume')
|
|
->call('keepCurrentDataVolume');
|
|
|
|
expect($database->persistentStorages()->where('mount_path', StandaloneClickhouse::DATA_DIRECTORY)->value('name'))
|
|
->toBe("clickhouse-data-{$database->uuid}");
|
|
});
|
|
|
|
it('reads the data mount with sudo on a non-root server', function () {
|
|
$this->server->update(['user' => 'cooluser']);
|
|
$database = create_standalone_clickhouse($this->environment->id, $this->destination);
|
|
fakeClickhouseDataMount('3f1c0ffee');
|
|
|
|
expect($database->fresh()->anonymousDataVolume())->toBe('3f1c0ffee')
|
|
->and(ranRemoteCommandContaining("sudo docker inspect --format '{{range .Mounts}}{{if eq .Destination \"/var/lib/clickhouse\"}}{{.Type}} {{.Name}}{{end}}{{end}}' '{$database->uuid}' 2>/dev/null || sudo true"))->toBeTrue();
|
|
});
|
|
|
|
it('rejects an API start or restart of a blocked database at once', function (string $method, string $path) {
|
|
$database = create_standalone_clickhouse($this->environment->id, $this->destination);
|
|
fakeClickhouseDataMount('3f1c0ffee');
|
|
$plainToken = Str::random(40);
|
|
$token = $this->owner->tokens()->create([
|
|
'name' => 'clickhouse-test',
|
|
'token' => hash('sha256', $plainToken),
|
|
'abilities' => ['*'],
|
|
'team_id' => $this->team->id,
|
|
]);
|
|
|
|
$response = $this->withHeader('Authorization', 'Bearer '.$token->getKey().'|'.$plainToken)
|
|
->json($method, str_replace('{uuid}', $database->uuid, $path));
|
|
|
|
$response->assertStatus(422);
|
|
expect($response->json('message'))->toContain('unnamed Docker volume 3f1c0ffee');
|
|
Queue::assertNothingPushed();
|
|
})->with([
|
|
'start' => ['POST', '/api/v1/databases/{uuid}/start'],
|
|
'restart' => ['POST', '/api/v1/databases/{uuid}/restart'],
|
|
]);
|
|
|
|
it('reports a blocked database in the deploy API', function () {
|
|
$database = create_standalone_clickhouse($this->environment->id, $this->destination);
|
|
fakeClickhouseDataMount('3f1c0ffee');
|
|
$plainToken = Str::random(40);
|
|
$token = $this->owner->tokens()->create([
|
|
'name' => 'clickhouse-test',
|
|
'token' => hash('sha256', $plainToken),
|
|
'abilities' => ['*'],
|
|
'team_id' => $this->team->id,
|
|
]);
|
|
|
|
$response = $this->withHeader('Authorization', 'Bearer '.$token->getKey().'|'.$plainToken)
|
|
->postJson('/api/v1/deploy', ['uuid' => $database->uuid]);
|
|
|
|
expect($response->json('deployments.0.message'))->toContain('unnamed Docker volume 3f1c0ffee');
|
|
Queue::assertNothingPushed();
|
|
});
|