feat(v5): add mesh app canvas

This commit is contained in:
Andras Bacsai committed 2026-06-20 09:23:16 +02:00
1 parent 155b07378c
commit 86156b6f7a
53 files changed
+6037 -113

No files matched your search

+24
View File
@@ -51,3 +51,27 @@ function runContainerRoleHelper(string $roles, string $serviceRole, ?string $wor
->env(['COOLIFY_CONTAINER_ROLE' => $roles])
->run($command);
}
it('does not register a separate v5 flux status listener service', function () {
foreach (['development', 'production'] as $environment) {
$base = base_path("docker/{$environment}/etc/s6-overlay/s6-rc.d");
expect(file_exists("{$base}/v5-flux-status-listener"))->toBeFalse()
->and(file_exists("{$base}/user/contents.d/v5-flux-status-listener"))->toBeFalse();
}
});
it('configures flux to publish v5 resource statuses to laravel over local http by default', function () {
foreach (['development', 'production'] as $environment) {
$runScript = file_get_contents(base_path("docker/{$environment}/etc/s6-overlay/s6-rc.d/flux/run"));
expect($runScript)
->toContain('COOLIFY_FLUX_LARAVEL_API_URL')
->toContain('http://127.0.0.1:8080')
->toContain('COOLIFY_FLUX_LARAVEL_API_TOKEN')
->not->toContain('APP_KEY')
->not->toContain("grep -E '^APP_KEY=' .env")
->not->toContain('COOLIFY_FLUX_REDIS_URL')
->not->toContain('COOLIFY_FLUX_RESOURCE_STATUS_CHANNEL');
}
});
@@ -75,7 +75,7 @@ it('seeds bootstrapped Lima VMs into v5 development server state', function () {
->and($script)->toContain('lima_ssh_port "$index"')
->and($script)->toContain('host.docker.internal')
->and($script)->toContain('v5:sync-dev-lima-servers')
->and($script)->toContain('--server="${instance}|host.docker.internal|${ssh_user}|${ssh_port}"')
->and($script)->toContain('--server="${instance}|host.docker.internal|${ssh_user}|${ssh_port}|$(coold_vm_wg_ip "$index")"')
->and($compose)->toContain('COOLIFY_CLI_SSH_USER: "${COOLIFY_CLI_SSH_USER:-}"')
->and($script)->not->toContain('db:seed --class=V5DevLimaSeeder --force')
->and($script)->not->toContain('--server "${instance}|${node}|$(coolify_ssh_user)|22"');
File diff suppressed because it is too large. Load diff
+12
View File
@@ -44,6 +44,18 @@ it('downloads postgres upgrade script during install and upgrade without auto-ru
'nightly upgrade' => 'other/nightly/upgrade.sh',
]);
it('generates a dedicated flux laravel api token during install and upgrade', function (string $path) {
$script = file_get_contents(getcwd().'/'.$path);
expect($script)
->toContain('update_env_var "COOLIFY_FLUX_LARAVEL_API_TOKEN" "$(openssl rand -hex 32)"');
})->with([
'stable install' => 'scripts/install.sh',
'nightly install' => 'other/nightly/install.sh',
'stable upgrade' => 'scripts/upgrade.sh',
'nightly upgrade' => 'other/nightly/upgrade.sh',
]);
it('keeps postgres upgrade compose override in future upgrade compose commands', function (string $path) {
$script = file_get_contents(getcwd().'/'.$path);
@@ -0,0 +1,149 @@
<?php
use App\Actions\V5\Proxy\GenerateCaddyIngressConfiguration;
use App\Actions\V5\Proxy\StartCaddyIngress;
use App\Actions\V5\Proxy\StopCaddyIngress;
use App\Models\PrivateKey;
use App\Models\V5\Server;
use Illuminate\Support\Facades\Process;
use Tests\TestCase;
uses(TestCase::class);
it('generates a caddy ingress compose file with health endpoint', function () {
$configuration = GenerateCaddyIngressConfiguration::run();
expect($configuration['compose'])->toContain('container_name: coolify-v5-caddy')
->and($configuration['compose'])->toContain("image: 'docker.io/library/caddy:2-alpine'")
->and($configuration['compose'])->toContain('80:80')
->and($configuration['compose'])->toContain('443:443')
->and($configuration['compose'])->toContain('./Caddyfile:/etc/caddy/Caddyfile:ro')
->and($configuration['caddyfile'])->toContain('respond /coolify-health 200')
->and($configuration['caddyfile'])->toContain('respond 404');
});
it('builds caddy ingress install commands with sudo fallback for non-root ssh users', function () {
$configuration = GenerateCaddyIngressConfiguration::run('/tmp/coolify-caddy');
$script = implode("\n", $configuration['commands']);
expect($configuration['commands'])->toHaveCount(6)
->and($script)->toContain('sudo mkdir -p /tmp/coolify-caddy/data /tmp/coolify-caddy/config')
->and($script)->toContain('sudo tee /tmp/coolify-caddy/docker-compose.yml')
->and($script)->toContain('sudo tee /tmp/coolify-caddy/Caddyfile')
->and($script)->toContain('command -v podman')
->and($script)->toContain('command -v docker')
->and(strpos($script, 'command -v podman'))->toBeLessThan(strpos($script, 'command -v docker'))
->and($script)->toContain('coolify-v5-caddy')
->and($script)->toContain('-v /tmp/coolify-caddy/Caddyfile:/etc/caddy/Caddyfile:ro');
});
it('throws when the caddy ingress start command fails', function () {
$privateKey = new PrivateKey([
'private_key' => "-----BEGIN OPENSSH PRIVATE KEY-----\ntest-key\n-----END OPENSSH PRIVATE KEY-----\n",
]);
$server = new Server([
'host' => '203.0.113.10',
'ssh_user' => 'root',
'ssh_port' => 22,
'capabilities' => ['coold', 'ingress'],
]);
$server->setRelation('privateKey', $privateKey);
Process::fake([
'*' => Process::result(errorOutput: 'mkdir: Permission denied', exitCode: 1),
]);
StartCaddyIngress::run($server);
})->throws(RuntimeException::class, 'Failed to start Caddy ingress: mkdir: Permission denied');
it('starts caddy ingress over ssh for ingress servers', function () {
$privateKey = new PrivateKey([
'private_key' => "-----BEGIN OPENSSH PRIVATE KEY-----\ntest-key\n-----END OPENSSH PRIVATE KEY-----\n",
]);
$server = new Server([
'host' => '203.0.113.10',
'ssh_user' => 'root',
'ssh_port' => 22,
'capabilities' => ['coold', 'ingress'],
]);
$server->setRelation('privateKey', $privateKey);
Process::fake([
'*' => Process::result(output: ''),
]);
$result = StartCaddyIngress::run($server);
expect($result)->toBe('Caddy ingress started.');
Process::assertRan(function ($process): bool {
$command = is_array($process->command) ? implode(' ', $process->command) : $process->command;
return is_string($command)
&& str_contains($command, 'command -v podman')
&& str_contains($command, 'command -v docker')
&& strpos($command, 'command -v podman') < strpos($command, 'command -v docker')
&& str_contains($command, 'coolify-v5-caddy');
});
});
it('prefers podman for every caddy ingress runtime command', function () {
$configuration = GenerateCaddyIngressConfiguration::run('/tmp/coolify-caddy');
$runtimeCommands = collect($configuration['commands'])
->filter(fn (string $command) => str_contains($command, 'command -v podman') && str_contains($command, 'command -v docker'));
expect($runtimeCommands)->toHaveCount(3);
$runtimeCommands->each(function (string $command): void {
expect(strpos($command, 'command -v podman'))->toBeLessThan(strpos($command, 'command -v docker'));
});
});
it('does not start caddy ingress for non-ingress servers', function () {
$server = new Server([
'capabilities' => ['coold'],
]);
Process::fake();
$result = StartCaddyIngress::run($server);
expect($result)->toBe('Server is not an ingress server.');
Process::assertNothingRan();
});
it('stops caddy ingress over ssh', function () {
$privateKey = new PrivateKey([
'private_key' => "-----BEGIN OPENSSH PRIVATE KEY-----\ntest-key\n-----END OPENSSH PRIVATE KEY-----\n",
]);
$server = new Server([
'host' => '203.0.113.10',
'ssh_user' => 'root',
'ssh_port' => 22,
'capabilities' => ['coold'],
]);
$server->setRelation('privateKey', $privateKey);
Process::fake([
'*' => Process::result(output: ''),
]);
$result = StopCaddyIngress::run($server);
expect($result)->toBe('Caddy ingress stopped.');
Process::assertRan(function ($process): bool {
$command = is_array($process->command) ? implode(' ', $process->command) : $process->command;
return is_string($command)
&& str_contains($command, 'command -v podman')
&& str_contains($command, 'command -v docker')
&& strpos($command, 'command -v podman') < strpos($command, 'command -v docker')
&& str_contains($command, 'coolify-v5-caddy');
});
});
@@ -0,0 +1,57 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
import { resolveCanvasNodeLayout, resolveCanvasNodePosition } from '../../../../resources/js/v5/lib/canvas-collision.ts';
test('moves a dragged canvas node to the closest non-overlapping side with a gap', () => {
const settledPosition = resolveCanvasNodePosition(
{ id: 'dragged', x: 110, y: 10, width: 320, height: 136 },
[{ id: 'existing', x: 0, y: 0, width: 320, height: 136 }],
16,
);
assert.deepEqual(settledPosition, { x: 110, y: 152 });
});
test('keeps moving until the closest side is clear', () => {
const settledPosition = resolveCanvasNodePosition(
{ id: 'dragged', x: 110, y: 10, width: 320, height: 136 },
[
{ id: 'left-blocker', x: 0, y: 0, width: 320, height: 136 },
{ id: 'bottom-blocker', x: 110, y: 152, width: 320, height: 136 },
{ id: 'top-blocker', x: 110, y: -152, width: 320, height: 136 },
],
16,
);
assert.deepEqual(settledPosition, { x: -336, y: 10 });
});
test('ignores the dragged node when comparing canvas collisions', () => {
const settledPosition = resolveCanvasNodePosition(
{ id: 'app-1', x: 24, y: 32, width: 320, height: 136 },
[{ id: 'app-1', x: 24, y: 32, width: 320, height: 136 }],
16,
);
assert.deepEqual(settledPosition, { x: 24, y: 32 });
});
test('spreads an overlapping canvas layout in order', () => {
const settledNodes = resolveCanvasNodeLayout(
[
{ id: 'first', x: 0, y: 0, width: 320, height: 136 },
{ id: 'second', x: 110, y: 10, width: 320, height: 136 },
],
16,
);
assert.deepEqual(
settledNodes.map((node) => ({ id: node.id, x: node.x, y: node.y })),
[
{ id: 'first', x: 0, y: 0 },
{ id: 'second', x: 110, y: 152 },
],
);
});
@@ -0,0 +1,32 @@
<?php
use App\Actions\V5\Application\DeployNginxApplication;
use App\Models\V5\Application;
use Tests\TestCase;
uses(TestCase::class);
it('verifies nginx is running before marking the application running', function () {
$application = new Application([
'name' => 'nginx-test',
'image' => 'docker.io/library/nginx:alpine',
'container_name' => 'coolify-v5-nginx-test',
'status' => 'creating',
]);
$action = new DeployNginxApplication;
$method = new ReflectionMethod($action, 'remoteCommand');
$method->setAccessible(true);
$remoteCommand = $method->invoke($action, $application);
expect($remoteCommand)
->toContain('if [ "$(id -u)" = "0" ]; then podman=podman; else podman="sudo -n podman"; fi')
->toContain("--network 'coolify-default-mesh'")
->toContain("--network-alias 'coolify-v5-nginx-test'")
->toContain('$podman inspect')
->not->toContain('docker run')
->not->toContain('docker inspect')
->toContain('.State.Running')
->toContain('Container did not stay running')
->toContain('exit 1');
});