mirror of
https://github.com/coollabsio/coolify.git
synced 2026-10-28 17:44:36 +08:00
385 lines
18 KiB
PHP
385 lines
18 KiB
PHP
<?php
|
|
|
|
use Symfony\Component\Process\Process;
|
|
|
|
beforeEach(function () {
|
|
$this->devRoot = sys_get_temp_dir().'/coolify-dev-script-'.bin2hex(random_bytes(4));
|
|
$main = $this->devRoot.'/main';
|
|
$this->devLog = $this->devRoot.'/commands.log';
|
|
$this->qemuStorage = $this->devRoot.'/qemu';
|
|
|
|
mkdir($main.'/scripts', 0777, true);
|
|
mkdir($this->devRoot.'/bin');
|
|
mkdir($this->qemuStorage);
|
|
copy(base_path('scripts/dev'), $main.'/scripts/dev');
|
|
chmod($main.'/scripts/dev', 0755);
|
|
copy(base_path('docker-compose.dev-multi.yml'), $main.'/docker-compose.dev-multi.yml');
|
|
mkdir($main.'/config');
|
|
copy(base_path('config/development-qemu.php'), $main.'/config/development-qemu.php');
|
|
file_put_contents($main.'/.gitignore', ".env\n.dev-instances/\n");
|
|
|
|
$git = Process::fromShellCommandline(implode(' && ', [
|
|
'git init -q -b main',
|
|
'git add -A',
|
|
'git -c user.name=test -c user.email=test@example.com commit -q -m init',
|
|
'git worktree add -q -b fix/Some_thing ../wt',
|
|
]), $main);
|
|
$git->run();
|
|
expect($git->isSuccessful())->toBeTrue($git->getErrorOutput());
|
|
|
|
$env = "APP_URL=https://devserver.example.ts.net:8000\nDEV_BIND_ADDRESS=127.0.0.1\n";
|
|
file_put_contents($main.'/.env', $env);
|
|
file_put_contents($this->devRoot.'/wt/.env', $env);
|
|
|
|
$fake = <<<'BASH'
|
|
#!/usr/bin/env bash
|
|
printf '%s %s\n' "$(basename "$0")" "$*" >> "$DEV_TEST_LOG"
|
|
case "$(basename "$0") $1" in
|
|
'docker inspect') echo "${DEV_TEST_HEALTH:-running healthy}" ;;
|
|
'docker ps') [[ -n "${DEV_TEST_RUNNING:-}" && "$*" == *"$DEV_TEST_RUNNING"* ]] && echo abc123 ;;
|
|
esac
|
|
if [[ "$(basename "$0")" == virsh && "$*" == *' list '* ]]; then
|
|
printf '%s\n' coolify-dev-fix-some_thing--ubuntu-root coolify-dev-fix-some_thing-2--ubuntu-root coolify-dev-ubuntu-root
|
|
fi
|
|
exit 0
|
|
BASH;
|
|
foreach (['docker', 'virsh', 'php', 'tailscale', 'iptables'] as $binary) {
|
|
file_put_contents($this->devRoot.'/bin/'.$binary, $fake);
|
|
chmod($this->devRoot.'/bin/'.$binary, 0755);
|
|
}
|
|
|
|
$this->limaConfig = $this->devRoot.'/lima.yaml';
|
|
$this->fakeLima = function (): void {
|
|
file_put_contents($this->devRoot.'/bin/limactl', <<<'BASH'
|
|
#!/usr/bin/env bash
|
|
printf 'limactl %s\n' "$*" >> "$DEV_TEST_LOG"
|
|
case "$*" in
|
|
create*) cat > "$DEV_TEST_LIMA_CONFIG" ;;
|
|
*SSHLocalPort*) echo 53022 ;;
|
|
*'{{.Name}} {{.Status}}'*) printf '%s\n' 'coolify-dev-2-debian-root Running' 'coolify-dev-2-ubuntu-root Stopped' 'coolify-dev-21-debian-root Running' 'other Running' ;;
|
|
esac
|
|
exit 0
|
|
BASH);
|
|
file_put_contents($this->devRoot.'/bin/uname', "#!/usr/bin/env bash\necho Darwin\n");
|
|
chmod($this->devRoot.'/bin/limactl', 0755);
|
|
chmod($this->devRoot.'/bin/uname', 0755);
|
|
};
|
|
});
|
|
|
|
afterEach(function () {
|
|
Process::fromShellCommandline('rm -rf '.escapeshellarg($this->devRoot))->run();
|
|
});
|
|
|
|
function runDevScript(string $directory, array $arguments, array $env = []): Process
|
|
{
|
|
$process = new Process(['bash', 'scripts/dev', ...$arguments], test()->devRoot.'/'.$directory, [
|
|
'PATH' => test()->devRoot.'/bin:'.getenv('PATH'),
|
|
'DEV_TEST_LOG' => test()->devLog,
|
|
'DEVELOPMENT_QEMU_STORAGE_PATH' => test()->qemuStorage,
|
|
'COOLIFY_DEV_SERVER_BACKEND' => 'testing-host',
|
|
'DEV_TEST_LIMA_CONFIG' => test()->limaConfig,
|
|
...$env,
|
|
]);
|
|
$process->run();
|
|
|
|
return $process;
|
|
}
|
|
|
|
function devScriptLog(): string
|
|
{
|
|
return file_exists(test()->devLog) ? file_get_contents(test()->devLog) : '';
|
|
}
|
|
|
|
function devInstanceEnv(string $name): string
|
|
{
|
|
return file_get_contents(test()->devRoot."/main/.dev-instances/{$name}.env");
|
|
}
|
|
|
|
it('shows usage and rejects invalid arguments', function () {
|
|
$help = runDevScript('main', ['--help']);
|
|
$extra = runDevScript('main', ['start', 'ubuntu-root', 'extra']);
|
|
$unknown = runDevScript('main', ['restart']);
|
|
$destroyWithoutName = runDevScript('main', ['destroy']);
|
|
|
|
expect($help->isSuccessful())->toBeTrue()
|
|
->and($help->getOutput())->toContain('start [qemu-profile]', 'stop [qemu-profile]', 'run [qemu-profile]', 'destroy <name>')
|
|
->and($extra->isSuccessful())->toBeFalse()
|
|
->and($unknown->isSuccessful())->toBeFalse()
|
|
->and($destroyWithoutName->isSuccessful())->toBeFalse();
|
|
});
|
|
|
|
it('starts the branch instance of the main checkout on the default ports', function () {
|
|
$process = runDevScript('main', ['start']);
|
|
|
|
expect($process->isSuccessful())->toBeTrue($process->getErrorOutput())
|
|
->and(devInstanceEnv('main'))
|
|
->toContain('COMPOSE_PROJECT_NAME=coolify-dev-main')
|
|
->toContain('DEVELOPMENT_QEMU_INSTANCE=main')
|
|
->toContain('DEVELOPMENT_QEMU_SLOT=1')
|
|
->toContain('APP_URL=https://devserver.example.ts.net:8000')
|
|
->toContain('APP_PORT=8000')
|
|
->toContain('FORWARD_DB_PORT=5432')
|
|
->and(devScriptLog())
|
|
->toContain('docker stop coolify coolify-db coolify-redis')
|
|
->toContain('docker compose -p coolify-dev-main -f docker-compose.dev-multi.yml --env-file .env --env-file')
|
|
->toContain('--profile vite --profile mailpit --profile minio --profile testing-host up -d --build')
|
|
->toContain('coolify-dev-main')
|
|
->toContain('exec -T coolify php artisan db:seed --class=ServerSeeder --force')
|
|
->not->toContain('dev:qemu');
|
|
});
|
|
|
|
it('gives a worktree branch its own port block, tailscale ports, and stable data', function () {
|
|
runDevScript('main', ['start']);
|
|
file_put_contents($this->devLog, '');
|
|
|
|
$first = runDevScript('wt', ['start']);
|
|
$appKey = preg_replace('/.*^APP_KEY=(\S+)$.*/ms', '$1', devInstanceEnv('fix-some_thing'));
|
|
$second = runDevScript('wt', ['start']);
|
|
|
|
expect($first->isSuccessful())->toBeTrue($first->getErrorOutput())
|
|
->and($second->isSuccessful())->toBeTrue($second->getErrorOutput())
|
|
->and(devInstanceEnv('fix-some_thing'))
|
|
->toContain('COMPOSE_PROJECT_NAME=coolify-dev-fix-some_thing')
|
|
->toContain('DEVELOPMENT_QEMU_SLOT=2')
|
|
->toContain('APP_URL=https://devserver.example.ts.net:20020')
|
|
->toContain('FORWARD_PUSHER_PORT=20021')
|
|
->toContain('FORWARD_TERMINAL_PORT=20022')
|
|
->toContain('FORWARD_DB_PORT=20023')
|
|
->toContain('VITE_PORT=20025')
|
|
->toContain("APP_KEY={$appKey}")
|
|
->toContain('DEV_CHECKOUT='.realpath($this->devRoot.'/wt'))
|
|
->and(file_get_contents($this->devRoot.'/main/.dev-instances/slots'))->toBe("main 1\nfix-some_thing 2\n")
|
|
->and(devScriptLog())
|
|
->toContain('tailscale serve --bg --https=20020 http://127.0.0.1:20020')
|
|
->toContain('tailscale serve --bg --https=20021 http://127.0.0.1:20021')
|
|
->toContain('tailscale serve --bg --https=20022 http://127.0.0.1:20022')
|
|
->toContain('tailscale serve --bg --https=20025 http://127.0.0.1:20025')
|
|
->not->toContain('docker stop coolify ')
|
|
->and($this->devRoot.'/wt/.dev-instances')->not->toBeDirectory();
|
|
});
|
|
|
|
it('stops the other running instance of the same checkout', function () {
|
|
runDevScript('main', ['start'], ['COOLIFY_DEV_INSTANCE' => 'older']);
|
|
file_put_contents($this->devLog, '');
|
|
|
|
$process = runDevScript('main', ['start'], ['DEV_TEST_RUNNING' => 'coolify-dev-older']);
|
|
|
|
expect($process->isSuccessful())->toBeTrue($process->getErrorOutput())
|
|
->and(devScriptLog())->toContain('docker compose -p coolify-dev-older stop');
|
|
});
|
|
|
|
it('fails when the coolify container exits', function () {
|
|
$process = runDevScript('main', ['start'], ['DEV_TEST_HEALTH' => 'exited unhealthy']);
|
|
|
|
expect($process->isSuccessful())->toBeFalse()
|
|
->and($process->getErrorOutput())->toContain('did not become healthy');
|
|
});
|
|
|
|
it('removes the containers when a run ends', function () {
|
|
$process = runDevScript('wt', ['run']);
|
|
|
|
expect($process->isSuccessful())->toBeTrue($process->getErrorOutput())
|
|
->and(devScriptLog())->toContain('logs -f --tail=100')
|
|
->toContain(' stop')
|
|
->toContain('down --remove-orphans');
|
|
});
|
|
|
|
it('lists instances with their urls and checkouts', function () {
|
|
runDevScript('main', ['start']);
|
|
runDevScript('wt', ['start']);
|
|
|
|
$process = runDevScript('wt', ['urls']);
|
|
|
|
expect($process->isSuccessful())->toBeTrue($process->getErrorOutput())
|
|
->and($process->getOutput())
|
|
->toMatch('/^main\s+https:\/\/devserver\.example\.ts\.net:8000\s+running healthy\s+5432/m')
|
|
->toMatch('/^fix-some_thing\s+https:\/\/devserver\.example\.ts\.net:20020\s+running healthy\s+20023/m');
|
|
});
|
|
|
|
it('destroys only the named instance data, vms, and slot', function () {
|
|
runDevScript('main', ['start']);
|
|
runDevScript('wt', ['start']);
|
|
foreach (['coolify-dev-fix-some_thing--ubuntu-root.qcow2', 'coolify-dev-fix-some_thing-2--ubuntu-root.qcow2', 'coolify-dev-ubuntu-root-prepared.qcow2'] as $file) {
|
|
touch($this->qemuStorage.'/'.$file);
|
|
}
|
|
file_put_contents($this->devLog, '');
|
|
|
|
$process = runDevScript('main', ['destroy', 'fix-some_thing']);
|
|
|
|
expect($process->isSuccessful())->toBeTrue($process->getErrorOutput())
|
|
->and(devScriptLog())
|
|
->toContain('down --remove-orphans --volumes')
|
|
->toContain('virsh --connect qemu:///system undefine coolify-dev-fix-some_thing--ubuntu-root')
|
|
->not->toContain('undefine coolify-dev-fix-some_thing-2--ubuntu-root')
|
|
->not->toContain('undefine coolify-dev-ubuntu-root')
|
|
->toContain('virsh --connect qemu:///system net-undefine coolify-dev-2')
|
|
->toContain('tailscale serve --https=20020 off')
|
|
->and($this->qemuStorage.'/coolify-dev-fix-some_thing--ubuntu-root.qcow2')->not->toBeFile()
|
|
->and($this->qemuStorage.'/coolify-dev-fix-some_thing-2--ubuntu-root.qcow2')->toBeFile()
|
|
->and($this->qemuStorage.'/coolify-dev-ubuntu-root-prepared.qcow2')->toBeFile()
|
|
->and($this->devRoot.'/main/.dev-instances/fix-some_thing.env')->not->toBeFile()
|
|
->and(file_get_contents($this->devRoot.'/main/.dev-instances/slots'))->toBe("main 1\n");
|
|
});
|
|
|
|
it('starts the instance kvm vm with the instance database and names', function () {
|
|
if (! function_exists('posix_geteuid') || posix_geteuid() !== 0 || ! file_exists('/dev/kvm') || filetype('/dev/kvm') !== 'char' || ! is_readable('/dev/kvm') || ! is_writable('/dev/kvm')) {
|
|
$this->markTestSkipped('KVM and root access are required for this launcher branch.');
|
|
}
|
|
|
|
file_put_contents($this->devRoot.'/bin/php', <<<'BASH'
|
|
#!/usr/bin/env bash
|
|
printf 'php %s DB=%s:%s LOG=%s QEMU=%s/%s/%s/%s\n' "$*" "$DB_HOST" "$DB_PORT" "$LOG_CHANNEL" "$DEVELOPMENT_QEMU_INSTANCE" "$DEVELOPMENT_QEMU_SLOT" "$DEVELOPMENT_QEMU_DOCKER_NETWORK" "$DEVELOPMENT_QEMU_COOLIFY_CONTAINER" >> "$DEV_TEST_LOG"
|
|
BASH);
|
|
|
|
runDevScript('main', ['start'], ['COOLIFY_DEV_SERVER_BACKEND' => 'auto']);
|
|
$start = runDevScript('wt', ['start', 'debian-root'], ['COOLIFY_DEV_SERVER_BACKEND' => 'auto']);
|
|
$stop = runDevScript('wt', ['stop'], ['COOLIFY_DEV_SERVER_BACKEND' => 'auto']);
|
|
|
|
expect($start->isSuccessful())->toBeTrue($start->getErrorOutput())
|
|
->and($stop->isSuccessful())->toBeTrue($stop->getErrorOutput())
|
|
->and(devScriptLog())
|
|
->toContain('php artisan dev:qemu debian-root --as-localhost DB=127.0.0.1:20023 LOG=stderr QEMU=fix-some_thing/2/coolify-dev-fix-some_thing/coolify-dev-fix-some_thing')
|
|
->toContain('virsh --connect qemu:///system shutdown coolify-dev-fix-some_thing--ubuntu-root')
|
|
->not->toContain('shutdown coolify-dev-fix-some_thing-2--ubuntu-root')
|
|
->not->toContain('shutdown coolify-dev-ubuntu-root')
|
|
->not->toContain('--profile testing-host up');
|
|
});
|
|
|
|
it('runs the host qemu command with a clean local environment', function () {
|
|
if (! function_exists('posix_geteuid') || posix_geteuid() !== 0 || ! file_exists('/dev/kvm') || filetype('/dev/kvm') !== 'char' || ! is_readable('/dev/kvm') || ! is_writable('/dev/kvm')) {
|
|
$this->markTestSkipped('KVM and root access are required for this launcher branch.');
|
|
}
|
|
|
|
file_put_contents($this->devRoot.'/bin/php', <<<'BASH'
|
|
#!/usr/bin/env bash
|
|
[[ "$APP_ENV" == local && "$APP_CONFIG_CACHE" == */bootstrap/cache/dev-qemu-*.php ]] || {
|
|
echo 'This command may only run in development mode.' >&2
|
|
exit 1
|
|
}
|
|
printf 'php %s ENV=%s CACHE=%s\n' "$*" "$APP_ENV" "$APP_CONFIG_CACHE" >> "$DEV_TEST_LOG"
|
|
BASH);
|
|
|
|
$process = runDevScript('wt', ['start', 'ubuntu-root'], [
|
|
'COOLIFY_DEV_SERVER_BACKEND' => 'auto',
|
|
'APP_ENV' => 'production',
|
|
'APP_CONFIG_CACHE' => '/tmp/stale-config.php',
|
|
]);
|
|
|
|
expect($process->isSuccessful())->toBeTrue($process->getErrorOutput())
|
|
->and(devScriptLog())->toContain('php artisan dev:qemu ubuntu-root --as-localhost ENV=local CACHE=');
|
|
});
|
|
|
|
it('passes the gpu option to the host qemu command when COOLIFY_DEV_GPU is 1', function () {
|
|
if (! function_exists('posix_geteuid') || posix_geteuid() !== 0 || ! file_exists('/dev/kvm') || filetype('/dev/kvm') !== 'char' || ! is_readable('/dev/kvm') || ! is_writable('/dev/kvm')) {
|
|
$this->markTestSkipped('KVM and root access are required for this launcher branch.');
|
|
}
|
|
|
|
file_put_contents($this->devRoot.'/bin/php', <<<'BASH'
|
|
#!/usr/bin/env bash
|
|
printf 'php %s|\n' "$*" >> "$DEV_TEST_LOG"
|
|
BASH);
|
|
|
|
$withGpu = runDevScript('wt', ['start', 'ubuntu-root'], ['COOLIFY_DEV_SERVER_BACKEND' => 'auto', 'COOLIFY_DEV_GPU' => '1']);
|
|
$withoutGpu = runDevScript('wt', ['start', 'ubuntu-root'], ['COOLIFY_DEV_SERVER_BACKEND' => 'auto', 'COOLIFY_DEV_GPU' => '0']);
|
|
|
|
expect($withGpu->isSuccessful())->toBeTrue($withGpu->getErrorOutput())
|
|
->and($withoutGpu->isSuccessful())->toBeTrue($withoutGpu->getErrorOutput())
|
|
->and(devScriptLog())
|
|
->toContain('php artisan dev:qemu ubuntu-root --as-localhost --gpu|')
|
|
->toContain('php artisan dev:qemu ubuntu-root --as-localhost|');
|
|
});
|
|
|
|
it('starts a lima vm as localhost on macos and seeds its forwarded ssh port', function () {
|
|
($this->fakeLima)();
|
|
runDevScript('main', ['start']);
|
|
file_put_contents($this->devLog, '');
|
|
|
|
$process = runDevScript('wt', ['start', 'debian-non-root'], ['COOLIFY_DEV_SERVER_BACKEND' => 'auto']);
|
|
|
|
expect($process->isSuccessful())->toBeTrue($process->getErrorOutput())
|
|
->and(devScriptLog())
|
|
->toContain('limactl create --tty=false --name=coolify-dev-2-debian-non-root -')
|
|
->toContain('limactl start --tty=false coolify-dev-2-debian-non-root')
|
|
->toContain('exec -T coolify php artisan dev:qemu:seed debian-non-root --as-localhost --ip=host.docker.internal --port=53022')
|
|
->not->toContain('--profile testing-host up')
|
|
->not->toContain('dev:qemu debian-non-root')
|
|
->and(file_get_contents($this->limaConfig))
|
|
->toContain('base: template:_images/debian-12')
|
|
->toContain('mounts: []')
|
|
->toContain("user='coolify'")
|
|
->toContain(config('development-qemu.public_key'))
|
|
->toContain('"$user ALL=(ALL) NOPASSWD:ALL"');
|
|
});
|
|
|
|
it('rejects an unknown lima profile before it creates a vm', function () {
|
|
($this->fakeLima)();
|
|
|
|
$process = runDevScript('wt', ['start', 'arch-root'], ['COOLIFY_DEV_SERVER_BACKEND' => 'lima']);
|
|
|
|
expect($process->isSuccessful())->toBeFalse()
|
|
->and($process->getErrorOutput())->toContain("Unknown profile 'arch-root'")
|
|
->and(devScriptLog())->not->toContain('limactl create');
|
|
});
|
|
|
|
it('stops and destroys only the lima vms of the instance slot', function () {
|
|
($this->fakeLima)();
|
|
runDevScript('main', ['start']);
|
|
runDevScript('wt', ['start']);
|
|
file_put_contents($this->devLog, '');
|
|
|
|
$stop = runDevScript('wt', ['stop'], ['COOLIFY_DEV_SERVER_BACKEND' => 'lima']);
|
|
$stopLog = devScriptLog();
|
|
$destroy = runDevScript('main', ['destroy', 'fix-some_thing']);
|
|
|
|
expect($stop->isSuccessful())->toBeTrue($stop->getErrorOutput())
|
|
->and($destroy->isSuccessful())->toBeTrue($destroy->getErrorOutput())
|
|
->and($stopLog)
|
|
->toContain('limactl stop coolify-dev-2-debian-root')
|
|
->not->toContain('limactl stop coolify-dev-2-ubuntu-root')
|
|
->not->toContain('limactl stop coolify-dev-21-debian-root')
|
|
->and(devScriptLog())
|
|
->toContain('limactl delete --force coolify-dev-2-debian-root')
|
|
->toContain('limactl delete --force coolify-dev-2-ubuntu-root')
|
|
->not->toContain('delete --force coolify-dev-21-debian-root')
|
|
->not->toContain('delete --force other');
|
|
});
|
|
|
|
it('keeps fresh worktree instances writable and stable during the first composer install', function () {
|
|
$initSetup = file_get_contents(base_path('docker/development/etc/s6-overlay/scripts/init-setup.sh'));
|
|
|
|
expect(strpos($initSetup, 'touch storage/logs/laravel.log'))->toBeLessThan(strpos($initSetup, 'chown -R www-data:www-data storage'))
|
|
->and(file_get_contents(base_path('vite.config.js')))->toContain('"**/vendor/**"');
|
|
});
|
|
|
|
it('tears down the instance of a deleted worktree', function () {
|
|
runDevScript('main', ['start']);
|
|
runDevScript('wt', ['start']);
|
|
file_put_contents($this->devLog, '');
|
|
|
|
$process = runDevScript('wt', ['teardown']);
|
|
|
|
expect($process->isSuccessful())->toBeTrue($process->getErrorOutput())
|
|
->and(devScriptLog())->toContain('docker compose -p coolify-dev-fix-some_thing')
|
|
->toContain('down --remove-orphans --volumes')
|
|
->not->toContain('-p coolify-dev-main ')
|
|
->and($this->devRoot.'/main/.dev-instances/fix-some_thing.env')->not->toBeFile()
|
|
->and($this->devRoot.'/main/.dev-instances/main.env')->toBeFile();
|
|
});
|
|
|
|
it('lets jean delete a worktree that never started an instance', function () {
|
|
$process = runDevScript('wt', ['teardown']);
|
|
|
|
expect($process->isSuccessful())->toBeTrue($process->getErrorOutput())
|
|
->and($process->getOutput())->toContain('nothing to destroy')
|
|
->and(devScriptLog())->not->toContain('docker compose');
|
|
});
|
|
|
|
it('never tears down the main checkout instance', function () {
|
|
runDevScript('main', ['start']);
|
|
|
|
$process = runDevScript('main', ['teardown']);
|
|
|
|
expect($process->isSuccessful())->toBeFalse()
|
|
->and($this->devRoot.'/main/.dev-instances/main.env')->toBeFile();
|
|
});
|