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 */ 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/", $process->command, $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' => '', '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($process->command, 'docker inspect') ? "volume {$volumeName}" : 'OK', )); } function ranRemoteCommandContaining(string $needle): bool { $ran = false; Process::assertRan(function ($process) use ($needle, &$ran) { $ran = $ran || str_contains($process->command, $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(); });