mirror of
https://github.com/coollabsio/coolify.git
synced 2026-10-06 14:07:37 -04:00
262 lines
7.4 KiB
PHP
262 lines
7.4 KiB
PHP
<?php
|
|
|
|
use App\Models\Application;
|
|
use App\Models\Server;
|
|
use App\Models\StandaloneDocker;
|
|
use Illuminate\Support\Facades\Process;
|
|
use Tests\TestCase;
|
|
|
|
uses(TestCase::class);
|
|
|
|
function makeRawComposeApplication(string $compose): Application
|
|
{
|
|
$application = new Application;
|
|
$application->uuid = 'compose-volume-test';
|
|
$application->docker_compose_raw = $compose;
|
|
|
|
$server = new Server;
|
|
$server->ip = '127.0.0.1';
|
|
$server->user = 'root';
|
|
|
|
$destination = new StandaloneDocker;
|
|
$destination->setRelation('server', $server);
|
|
$application->setRelation('destination', $destination);
|
|
|
|
return $application;
|
|
}
|
|
|
|
function rawComposeBindSource(string $source, string $syntax = 'long'): string
|
|
{
|
|
if ($syntax === 'short') {
|
|
$volume = trim($source, '"');
|
|
|
|
return <<<YAML
|
|
services:
|
|
web:
|
|
image: nginx:alpine
|
|
volumes:
|
|
- "{$volume}:/usr/share/nginx/html"
|
|
YAML;
|
|
}
|
|
|
|
return <<<YAML
|
|
services:
|
|
web:
|
|
image: nginx:alpine
|
|
volumes:
|
|
- type: bind
|
|
source: {$source}
|
|
target: /usr/share/nginx/html
|
|
YAML;
|
|
}
|
|
|
|
it('rejects invalid long-form bind sources before remote work', function () {
|
|
Process::fake();
|
|
|
|
$application = makeRawComposeApplication(rawComposeBindSource('"/srv/data;extra"'));
|
|
|
|
expect(fn () => $application->oldRawParser())
|
|
->toThrow(Exception::class, 'forbidden character');
|
|
|
|
Process::assertNothingRan();
|
|
});
|
|
|
|
it('rejects invalid short-form bind sources before remote work', function () {
|
|
Process::fake();
|
|
|
|
$application = makeRawComposeApplication(rawComposeBindSource('"/srv/data;extra"', 'short'));
|
|
|
|
expect(fn () => $application->oldRawParser())
|
|
->toThrow(Exception::class, 'forbidden character');
|
|
|
|
Process::assertNothingRan();
|
|
});
|
|
|
|
it('rejects unsupported expansion syntax in long-form bind sources', function () {
|
|
Process::fake();
|
|
|
|
$application = makeRawComposeApplication(rawComposeBindSource('"/srv/data$(value)"'));
|
|
|
|
expect(fn () => $application->oldRawParser())
|
|
->toThrow(Exception::class, 'command substitution');
|
|
|
|
Process::assertNothingRan();
|
|
});
|
|
|
|
it('rejects unsupported quoted syntax in long-form bind sources', function () {
|
|
Process::fake();
|
|
|
|
$application = makeRawComposeApplication(rawComposeBindSource('"/srv/data`value`"'));
|
|
|
|
expect(fn () => $application->oldRawParser())
|
|
->toThrow(Exception::class, 'backtick');
|
|
|
|
Process::assertNothingRan();
|
|
});
|
|
|
|
it('rejects unsupported operators in bind sources', function (string $source) {
|
|
Process::fake();
|
|
|
|
$application = makeRawComposeApplication(rawComposeBindSource($source));
|
|
|
|
expect(fn () => $application->oldRawParser())
|
|
->toThrow(Exception::class, 'forbidden character');
|
|
|
|
Process::assertNothingRan();
|
|
})->with([
|
|
'"/srv/data | extra"',
|
|
'"/srv/data && extra"',
|
|
'"/srv/data || extra"',
|
|
'"/srv/data > extra"',
|
|
'"/srv/data < extra"',
|
|
]);
|
|
|
|
it('rejects newlines and control characters in volume sources', function () {
|
|
Process::fake();
|
|
|
|
$application = makeRawComposeApplication(<<<'YAML'
|
|
services:
|
|
web:
|
|
image: nginx:alpine
|
|
volumes:
|
|
- type: bind
|
|
source: "/srv/data
|
|
extra"
|
|
target: /usr/share/nginx/html
|
|
YAML);
|
|
|
|
expect(fn () => $application->oldRawParser())
|
|
->toThrow(Exception::class);
|
|
|
|
Process::assertNothingRan();
|
|
});
|
|
|
|
it('rejects invalid Compose variable defaults in bind sources', function () {
|
|
Process::fake();
|
|
|
|
$application = makeRawComposeApplication(rawComposeBindSource('"${DATA:-$(value)}"'));
|
|
|
|
expect(fn () => $application->oldRawParser())
|
|
->toThrow(Exception::class);
|
|
|
|
Process::assertNothingRan();
|
|
});
|
|
|
|
it('quotes safe absolute bind sources at the mkdir execution boundary', function () {
|
|
expect(rawComposeBindMkdirCommand('/srv/appdata'))
|
|
->toBe("mkdir -p -- '/srv/appdata' > /dev/null 2>&1 || true");
|
|
|
|
expect(fn () => rawComposeBindMkdirCommand('/srv/appdata;extra'))
|
|
->toThrow(Exception::class);
|
|
});
|
|
|
|
it('quotes paths that begin with a command option', function () {
|
|
expect(rawComposeBindMkdirCommand('-evil'))
|
|
->toBe("mkdir -p -- '-evil' > /dev/null 2>&1 || true");
|
|
});
|
|
|
|
it('rejects blank and null-byte bind sources before a remote command runs', function (string $source) {
|
|
Process::fake();
|
|
|
|
$application = makeRawComposeApplication(rawComposeBindSource(json_encode($source)));
|
|
|
|
expect(fn () => $application->oldRawParser())->toThrow(Exception::class, 'Invalid volume source');
|
|
Process::assertNothingRan();
|
|
})->with([' ', "\0"]);
|
|
|
|
it('quotes quotes and backslashes so they cannot change the mkdir command', function () {
|
|
expect(rawComposeBindMkdirCommand("/tmp/foo'bar"))
|
|
->toBe("mkdir -p -- '/tmp/foo'\\''bar' > /dev/null 2>&1 || true")
|
|
->and(rawComposeBindMkdirCommand('/tmp/foo\\bar'))
|
|
->toBe("mkdir -p -- '/tmp/foo\\bar' > /dev/null 2>&1 || true")
|
|
->and(rawComposeBindMkdirCommand('$HOME'))
|
|
->toBe("mkdir -p -- '\$HOME' > /dev/null 2>&1 || true");
|
|
});
|
|
|
|
it('does not expand compose environment interpolations through the server shell', function () {
|
|
expect(rawComposeBindMkdirCommand('${DATA_PATH}'))->toBeNull()
|
|
->and(rawComposeBindMkdirCommand('${DATA_PATH}/mysql'))->toBeNull()
|
|
->and(rawComposeBindMkdirCommand('${DATA_PATH:-/srv/appdata}'))->toBeNull();
|
|
});
|
|
|
|
it('preserves legitimate long-form bind mounts', function () {
|
|
Process::fake();
|
|
|
|
$application = makeRawComposeApplication(rawComposeBindSource('"/srv/appdata"'));
|
|
|
|
$application->oldRawParser();
|
|
|
|
expect($application->docker_compose_raw)->toContain('/srv/appdata');
|
|
});
|
|
|
|
it('preserves legitimate short-form relative bind mounts', function () {
|
|
Process::fake();
|
|
|
|
$application = makeRawComposeApplication(rawComposeBindSource('./data', 'short'));
|
|
|
|
$application->oldRawParser();
|
|
|
|
expect($application->docker_compose_raw)->toContain('./data');
|
|
});
|
|
|
|
it('preserves nested relative bind mounts', function () {
|
|
Process::fake();
|
|
|
|
$application = makeRawComposeApplication(rawComposeBindSource('./data/nested/dir', 'short'));
|
|
|
|
$application->oldRawParser();
|
|
|
|
expect($application->docker_compose_raw)->toContain('./data/nested/dir');
|
|
});
|
|
|
|
it('does not treat named volumes as bind sources', function () {
|
|
Process::fake();
|
|
|
|
$application = makeRawComposeApplication(<<<'YAML'
|
|
services:
|
|
web:
|
|
image: nginx:alpine
|
|
volumes:
|
|
- appdata:/var/lib/data
|
|
volumes:
|
|
appdata:
|
|
YAML);
|
|
|
|
$application->oldRawParser();
|
|
|
|
expect($application->docker_compose_raw)->toContain('appdata:/var/lib/data');
|
|
});
|
|
|
|
it('does not treat long-form named volumes as bind sources', function () {
|
|
Process::fake();
|
|
|
|
$application = makeRawComposeApplication(<<<'YAML'
|
|
services:
|
|
web:
|
|
image: nginx:alpine
|
|
volumes:
|
|
- type: volume
|
|
source: appdata
|
|
target: /var/lib/data
|
|
volumes:
|
|
appdata:
|
|
YAML);
|
|
|
|
$application->oldRawParser();
|
|
|
|
expect($application->docker_compose_raw)->toContain('appdata');
|
|
});
|
|
|
|
it('still uses oldRawParser for raw compose deployments after git compose load', function () {
|
|
$jobSource = file_get_contents(base_path('app/Jobs/ApplicationDeploymentJob.php'));
|
|
$parserSource = file_get_contents(base_path('app/Models/Application.php'));
|
|
|
|
expect($jobSource)
|
|
->toContain('loadComposeFile(isInit: false)')
|
|
->toContain('is_raw_compose_deployment_enabled')
|
|
->toContain('oldRawParser()')
|
|
->and($parserSource)
|
|
->toContain('rawComposeBindMkdirCommand(')
|
|
->toContain('escapeshellarg');
|
|
});
|