Files
coolify/tests/Feature/ComposeVolumeDriverOptionsTest.php
T
Andras Bacsai 9e7cb60b0d feat: add import restore options, OAuth confirmation, and fixes
- database import: add keep_owners (restore PostgreSQL owners and
  privileges) and sqlite_database (pick the target SQLite file) to the
  API, OpenAPI spec, and import form
- oauth: require password confirmation and support team selection on
  OAuth login
- remote process: send team broadcast events to the team of the server
  that ran the process instead of the causer's user id
- servers API: handle the legacy build server setting
- compose: support parent bind paths and volume driver options
- dns: improve record hints and provider management
- notifications: fix throttling and missing backup notifications
- ui: rework the confirmation modal
- add feature and unit tests for the new behavior
2026-09-30 13:35:22 +02:00

298 lines
10 KiB
PHP

<?php
use App\Models\Application;
use App\Models\ApplicationPreview;
use App\Models\Environment;
use App\Models\InstanceSettings;
use App\Models\PrivateKey;
use App\Models\Project;
use App\Models\Server;
use App\Models\Service;
use App\Models\StandaloneDocker;
use App\Models\Team;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Facades\Queue;
uses(RefreshDatabase::class);
const DRIVER_OPTIONS_SHORT_COMPOSE = <<<'YAML'
services:
web:
image: nginx:alpine
volumes:
- 'opts:/data'
volumes:
opts:
driver: local
driver_opts:
type: none
o: bind
device: ${OPTS_DEVICE}
labels:
com.example.purpose: host-folder
YAML;
const DRIVER_OPTIONS_LONG_COMPOSE = <<<'YAML'
services:
web:
image: nginx:alpine
volumes:
- type: volume
source: opts
target: /data
volumes:
opts:
driver: local
driver_opts:
type: none
o: bind
device: ${OPTS_DEVICE}
labels:
com.example.purpose: host-folder
YAML;
const DRIVER_OPTIONS_NAMED_COMPOSE = <<<'YAML'
services:
web:
image: nginx:alpine
volumes:
- 'opts:/data'
volumes:
opts:
name: custom-name
driver: local
driver_opts:
type: none
o: bind
device: /srv/data
YAML;
const DRIVER_OPTIONS_NFS_MIXED_COMPOSE = <<<'YAML'
services:
web:
image: nginx:alpine
volumes:
- 'nfs-data:/nfs'
- 'app-data:/app'
volumes:
nfs-data:
driver: local
driver_opts:
type: nfs
o: addr=10.0.0.1,rw
device: ':/exports/data'
app-data: {}
YAML;
beforeEach(function () {
Server::flushIdentityMap();
InstanceSettings::unguarded(fn () => InstanceSettings::firstOrCreate(['id' => 0]));
config(['constants.ssh.mux_enabled' => false]);
Bus::fake();
Queue::fake();
Process::fake(['*' => Process::result(output: '')]);
$team = Team::factory()->create();
$privateKey = PrivateKey::factory()->create(['team_id' => $team->id]);
$server = Server::factory()->create([
'team_id' => $team->id,
'private_key_id' => $privateKey->id,
]);
$server->settings()->update(['is_reachable' => true, 'is_usable' => true]);
$this->destination = StandaloneDocker::where('server_id', $server->id)->firstOrFail();
$project = Project::factory()->create(['team_id' => $team->id]);
$this->environment = $project->environments()->first()
?? Environment::factory()->create(['project_id' => $project->id]);
});
function driverOptionsApplication(string $compose, ?string $parsingVersion = null): Application
{
$application = Application::factory()->create([
'build_pack' => 'dockercompose',
'docker_compose_raw' => $compose,
'environment_id' => test()->environment->id,
'destination_id' => test()->destination->id,
'destination_type' => test()->destination->getMorphClass(),
]);
if ($parsingVersion !== null) {
$application->update(['compose_parsing_version' => $parsingVersion]);
}
return $application->fresh();
}
function driverOptionsService(string $compose, ?string $parsingVersion = null): Service
{
$service = Service::factory()->create([
'docker_compose_raw' => $compose,
'environment_id' => test()->environment->id,
'server_id' => test()->destination->server_id,
'destination_id' => test()->destination->id,
'destination_type' => test()->destination->getMorphClass(),
]);
if ($parsingVersion !== null) {
$service->update(['compose_parsing_version' => $parsingVersion]);
}
return $service->fresh();
}
function driverOptionsPreview(Application $application, int $pullRequestId = 42): ApplicationPreview
{
return ApplicationPreview::create([
'uuid' => 'driver-options-preview-'.$pullRequestId,
'application_id' => $application->id,
'pull_request_id' => $pullRequestId,
'pull_request_html_url' => 'https://github.com/example/repository/pull/'.$pullRequestId,
]);
}
/**
* @return array<string, mixed>
*/
function expectedDriverOptionsVolume(string $name): array
{
return [
'driver' => 'local',
'driver_opts' => [
'type' => 'none',
'o' => 'bind',
'device' => '${OPTS_DEVICE}',
],
'labels' => [
'com.example.purpose' => 'host-folder',
],
'name' => $name,
];
}
/**
* @param array<int, mixed> $volumes
* @return list<string>
*/
function driverOptionsVolumeSources(array $volumes): array
{
return collect($volumes)
->map(fn (mixed $volume): string => is_array($volume) ? (string) $volume['source'] : explode(':', $volume)[0])
->values()
->all();
}
describe('applicationParser', function () {
it('keeps driver, driver_opts and labels on the renamed volume', function (string $compose) {
$application = driverOptionsApplication($compose);
$uuid = $application->uuid;
$parsed = applicationParser($application)->toArray();
expect(driverOptionsVolumeSources($parsed['services']['web']['volumes']))->toBe(["{$uuid}_opts"])
->and($parsed['volumes']["{$uuid}_opts"])->toBe(expectedDriverOptionsVolume("{$uuid}_opts"));
})->with([
'short syntax' => [DRIVER_OPTIONS_SHORT_COMPOSE],
'long syntax' => [DRIVER_OPTIONS_LONG_COMPOSE],
]);
it('keeps driver options on the preview volume', function () {
$application = driverOptionsApplication(DRIVER_OPTIONS_SHORT_COMPOSE);
$uuid = $application->uuid;
$preview = driverOptionsPreview($application);
$parsed = applicationParser($application, 42, $preview->id)->toArray();
$name = addPreviewDeploymentSuffix("{$uuid}_opts", 42);
expect(driverOptionsVolumeSources($parsed['services']['web-pr-42']['volumes']))->toBe([$name])
->and($parsed['volumes'][$name])->toBe(expectedDriverOptionsVolume($name));
});
it('replaces the name of the declaration with the renamed volume', function () {
$application = driverOptionsApplication(DRIVER_OPTIONS_NAMED_COMPOSE);
$uuid = $application->uuid;
$parsed = applicationParser($application)->toArray();
expect($parsed['volumes']["{$uuid}_opts"])->toBe([
'driver' => 'local',
'driver_opts' => ['type' => 'none', 'o' => 'bind', 'device' => '/srv/data'],
'name' => "{$uuid}_opts",
]);
});
it('keeps an nfs volume next to other volumes', function () {
$application = driverOptionsApplication(DRIVER_OPTIONS_NFS_MIXED_COMPOSE);
$uuid = $application->uuid;
$parsed = applicationParser($application)->toArray();
expect($parsed['services']['web']['volumes'])->toBe(['nfs-data:/nfs', "{$uuid}_app-data:/app"])
->and($parsed['volumes']['nfs-data']['driver_opts']['type'])->toBe('nfs');
});
});
describe('serviceParser', function () {
it('keeps driver, driver_opts and labels on the renamed volume', function (string $compose) {
$service = driverOptionsService($compose);
$uuid = $service->uuid;
$parsed = serviceParser($service)->toArray();
expect(driverOptionsVolumeSources($parsed['services']['web']['volumes']))->toBe(["{$uuid}_opts"])
->and($parsed['volumes']["{$uuid}_opts"])->toBe(expectedDriverOptionsVolume("{$uuid}_opts"));
})->with([
'short syntax' => [DRIVER_OPTIONS_SHORT_COMPOSE],
'long syntax' => [DRIVER_OPTIONS_LONG_COMPOSE],
]);
it('keeps an nfs volume next to other volumes', function () {
$service = driverOptionsService(DRIVER_OPTIONS_NFS_MIXED_COMPOSE);
$uuid = $service->uuid;
$parsed = serviceParser($service)->toArray();
expect($parsed['services']['web']['volumes'])->toBe(['nfs-data:/nfs', "{$uuid}_app-data:/app"])
->and($parsed['volumes']['nfs-data']['driver_opts']['type'])->toBe('nfs');
});
});
describe('legacy parsers', function () {
it('keeps driver options on the renamed volume of a legacy service', function (string $compose) {
$service = driverOptionsService($compose, '2');
$uuid = $service->uuid;
$parsed = parseDockerComposeFile($service)->toArray();
expect($parsed['volumes']["{$uuid}_opts"])->toBe(expectedDriverOptionsVolume("{$uuid}_opts"));
})->with([
'short syntax' => [DRIVER_OPTIONS_SHORT_COMPOSE],
'long syntax' => [DRIVER_OPTIONS_LONG_COMPOSE],
]);
it('keeps driver options on the renamed volume of a legacy application', function (string $parsingVersion, int $pullRequestId, string $compose, string $expectedName) {
$application = driverOptionsApplication($compose, $parsingVersion);
$uuid = $application->uuid;
$previewId = $pullRequestId === 0 ? null : driverOptionsPreview($application, $pullRequestId)->id;
$expectedName = str_replace('{uuid}', $uuid, $expectedName);
$parsed = parseDockerComposeFile($application, pull_request_id: $pullRequestId, preview_id: $previewId)->toArray();
expect($parsed['volumes'][$expectedName])->toBe(expectedDriverOptionsVolume($expectedName));
})->with([
'v1 short preview' => ['1', 42, DRIVER_OPTIONS_SHORT_COMPOSE, 'opts-pr-42'],
'v1 long preview' => ['1', 42, DRIVER_OPTIONS_LONG_COMPOSE, 'opts-pr-42'],
'v2 short' => ['2', 0, DRIVER_OPTIONS_SHORT_COMPOSE, '{uuid}-opts'],
'v2 short preview' => ['2', 42, DRIVER_OPTIONS_SHORT_COMPOSE, '{uuid}-opts-pr-42'],
'v2 long' => ['2', 0, DRIVER_OPTIONS_LONG_COMPOSE, '{uuid}-opts'],
'v2 long preview' => ['2', 42, DRIVER_OPTIONS_LONG_COMPOSE, '{uuid}-opts-pr-42'],
]);
it('keeps the volume of a legacy application as written', function () {
$application = driverOptionsApplication(DRIVER_OPTIONS_SHORT_COMPOSE, '1');
$parsed = parseDockerComposeFile($application)->toArray();
expect($parsed['services']['web']['volumes'])->toBe(['opts:/data'])
->and($parsed['volumes']['opts']['driver_opts']['device'])->toBe('${OPTS_DEVICE}');
});
});