feat(v5): use resource uuids at boundaries

This commit is contained in:
Andras Bacsai 2026-07-02 21:26:41 +02:00
parent c9416abec6
commit fd5b2fec9a
15 changed files with 407 additions and 142 deletions

View file

@ -152,21 +152,16 @@ private function updateCaddyIngress(array $payload): ?V5Server
private function findApplication(array $payload): ?V5Application
{
$query = V5Application::query()->with('server');
$teamId = $this->intValue($payload, 'team_id');
$server = $this->findServer($payload);
if ($teamId !== null) {
$query->where('team_id', $teamId);
}
if ($server instanceof V5Server) {
$query->where('server_id', $server->id);
}
$applicationId = $this->intValue($payload, 'application_id') ?? $this->intValue($payload, 'resource_id');
$applicationUuid = $this->stringValue($payload, 'application_uuid') ?? $this->stringValue($payload, 'resource_uuid');
if ($applicationId !== null) {
return $query->whereKey($applicationId)->first();
if ($applicationUuid !== null) {
return $query->where('uuid', $applicationUuid)->first();
}
$containerName = $this->stringValue($payload, 'container_name') ?? $this->stringValue($payload, 'name');
@ -202,10 +197,10 @@ private function isCaddyIngressStatusUpdate(array $payload, string $resourceType
*/
private function findServer(array $payload): ?V5Server
{
$serverId = $this->intValue($payload, 'server_id') ?? $this->intValue($payload, 'host_server_id');
$serverUuid = $this->stringValue($payload, 'server_uuid') ?? $this->stringValue($payload, 'host_server_uuid');
if ($serverId !== null) {
return V5Server::query()->find($serverId);
if ($serverUuid !== null) {
return V5Server::query()->where('uuid', $serverUuid)->first();
}
$hostId = $this->stringValue($payload, 'host_id')

View file

@ -20,14 +20,18 @@ public function __invoke(Request $request): JsonResponse
$validated = Validator::make($request->all(), [
'resource_type' => ['required', 'string', 'max:64'],
'team_id' => ['nullable', 'integer'],
'application_id' => ['nullable', 'integer'],
'resource_id' => ['nullable', 'integer'],
'team_id' => ['prohibited'],
'application_id' => ['prohibited'],
'resource_id' => ['prohibited'],
'server_id' => ['prohibited'],
'host_server_id' => ['prohibited'],
'application_uuid' => ['nullable', 'string', 'max:255'],
'resource_uuid' => ['nullable', 'string', 'max:255'],
'server_uuid' => ['nullable', 'string', 'max:255'],
'host_server_uuid' => ['nullable', 'string', 'max:255'],
'host_id' => ['nullable', 'string', 'max:255'],
'node_id' => ['nullable', 'string', 'max:255'],
'server_host' => ['nullable', 'string', 'max:255'],
'server_id' => ['nullable', 'integer'],
'host_server_id' => ['nullable', 'integer'],
'container_id' => ['nullable', 'string', 'max:255'],
'runtime_container_id' => ['nullable', 'string', 'max:255'],
'container_name' => ['nullable', 'string', 'max:255'],

View file

@ -197,7 +197,7 @@ public function storeNginxApplication(Request $request): JsonResponse
}
$validated = $request->validate([
'server_id' => ['nullable', 'integer'],
'server_uuid' => ['nullable', 'string', 'max:255'],
'image' => ['nullable', 'string', 'max:255', 'regex:/^[a-zA-Z0-9][a-zA-Z0-9._\/:@-]*$/'],
]);
$image = trim($validated['image'] ?? '') ?: self::DEFAULT_NGINX_IMAGE;
@ -205,8 +205,8 @@ public function storeNginxApplication(Request $request): JsonResponse
$server = V5Server::query()
->where('team_id', $currentTeam->id)
->when(
isset($validated['server_id']),
fn (Builder $query) => $query->whereKey($validated['server_id']),
isset($validated['server_uuid']),
fn (Builder $query) => $query->where('uuid', $validated['server_uuid']),
fn (Builder $query) => $query
->orderByRaw('last_bootstrapped_at is null')
->orderBy('name')
@ -520,10 +520,10 @@ public function storeResourceConnection(Request $request): JsonResponse
$validated = $request->validate([
'resource_one' => ['required', 'array'],
'resource_one.type' => ['required', 'string', Rule::in(['application'])],
'resource_one.id' => ['required', 'integer'],
'resource_one.uuid' => ['required', 'string', 'max:255'],
'resource_two' => ['required', 'array'],
'resource_two.type' => ['required', 'string', Rule::in(['application'])],
'resource_two.id' => ['required', 'integer'],
'resource_two.uuid' => ['required', 'string', 'max:255'],
]);
$resourceOne = $this->resolveConnectableResource($currentTeam, $project, $environment, $validated['resource_one']);
@ -574,20 +574,23 @@ public function updateResourceConnection(Request $request, ResourceConnection $c
DB::transaction(function () use ($connection, $validated): void {
$connection->rules()->delete();
$resourcesByUuid = $this->connectionApplicationsByUuid($connection);
foreach ($validated['ports_by_direction'] as $direction => $ports) {
[$sourceResourceId, $targetResourceId] = array_pad(explode('->', (string) $direction, 2), 2, null);
[$sourceResourceUuid, $targetResourceUuid] = array_pad(explode('->', (string) $direction, 2), 2, null);
$sourceResource = is_string($sourceResourceUuid) ? $resourcesByUuid->get($sourceResourceUuid) : null;
$targetResource = is_string($targetResourceUuid) ? $resourcesByUuid->get($targetResourceUuid) : null;
if (! $this->connectionHasResourceId($connection, $sourceResourceId) || ! $this->connectionHasResourceId($connection, $targetResourceId)) {
if (! $sourceResource instanceof V5Application || ! $targetResource instanceof V5Application) {
continue;
}
foreach (array_unique($ports) as $port) {
$connection->rules()->create([
'source_resource_type' => $this->resourceTypeForConnectionId($connection, (int) $sourceResourceId),
'source_resource_id' => (int) $sourceResourceId,
'target_resource_type' => $this->resourceTypeForConnectionId($connection, (int) $targetResourceId),
'target_resource_id' => (int) $targetResourceId,
'source_resource_type' => $this->resourceTypeForConnectionUuid($connection, $sourceResource->uuid),
'source_resource_id' => $sourceResource->id,
'target_resource_type' => $this->resourceTypeForConnectionUuid($connection, $targetResource->uuid),
'target_resource_id' => $targetResource->id,
'protocol' => 'tcp',
'port' => (int) $port,
]);
@ -775,10 +778,10 @@ public function storeServer(Request $request, V5Cluster $cluster): JsonResponse
],
'ssh_user' => ['required', 'string', 'max:255'],
'ssh_port' => ['required', 'integer', 'min:1', 'max:65535'],
'private_key_id' => [
'private_key_uuid' => [
'required',
'integer',
Rule::exists('private_keys', 'id')->where('team_id', $currentTeam->id),
'string',
Rule::exists('private_keys', 'uuid')->where('team_id', $currentTeam->id),
],
'node_address' => ['nullable', 'string', 'max:255'],
'builder_enabled' => ['sometimes', 'boolean'],
@ -803,6 +806,10 @@ public function storeServer(Request $request, V5Cluster $cluster): JsonResponse
$builderCapacity = (int) ($validated['builder_capacity'] ?? $cluster->builder_capacity);
$builderCpuQuota = $validated['builder_cpu_quota'] ?? $cluster->builder_cpu_quota;
$devWireguardOverrides = $this->devLimaWireguardOverrides($validated['host'], (int) $validated['ssh_port']);
$privateKey = PrivateKey::query()
->where('team_id', $currentTeam->id)
->where('uuid', $validated['private_key_uuid'])
->firstOrFail();
V5Server::query()->create([
'team_id' => $currentTeam->id,
@ -812,7 +819,7 @@ public function storeServer(Request $request, V5Cluster $cluster): JsonResponse
'host' => $validated['host'],
'ssh_user' => $validated['ssh_user'],
'ssh_port' => $validated['ssh_port'],
'private_key_id' => $validated['private_key_id'] ?? null,
'private_key_id' => $privateKey->id,
'status' => 'added',
'ingress_type' => $ingressType,
'capabilities' => $this->serverCapabilities($builderEnabled, $ingressEnabled),
@ -1363,9 +1370,9 @@ private function nginxServers(mixed $currentTeam): array
->where('team_id', $currentTeam->id)
->orderByRaw('last_bootstrapped_at is null')
->orderBy('name')
->get(['id', 'name', 'host', 'status'])
->get(['id', 'uuid', 'name', 'host', 'status'])
->map(fn (V5Server $server) => [
'id' => (string) $server->id,
'id' => $server->uuid,
'name' => $server->name,
'host' => $server->host,
'status' => $server->status,
@ -1440,7 +1447,7 @@ private function serializeCaddyIngress(V5Server $server, int $index = 0): array
$isServerReachable = $this->isServerReachable($server);
return [
'id' => (string) $server->id,
'id' => $server->uuid,
'name' => $server->name,
'host' => $server->host,
'type' => $server->ingressType(),
@ -1456,16 +1463,27 @@ private function serializeCaddyIngress(V5Server $server, int $index = 0): array
*/
private function serializeResourceConnection(ResourceConnection $connection): array
{
$applications = $this->connectionApplicationsById($connection);
$resourceOneUuid = $applications->get($connection->resource_one_id)?->uuid;
$resourceTwoUuid = $applications->get($connection->resource_two_id)?->uuid;
$applicationsById = $applications;
return [
'id' => (string) $connection->id,
'applicationIds' => [
(string) $connection->resource_one_id,
(string) $connection->resource_two_id,
],
'fromApplicationId' => (string) $connection->resource_one_id,
'toApplicationId' => (string) $connection->resource_two_id,
'id' => $connection->uuid,
'applicationIds' => array_values(array_filter([
$resourceOneUuid,
$resourceTwoUuid,
])),
'fromApplicationId' => $resourceOneUuid,
'toApplicationId' => $resourceTwoUuid,
'portsByDirection' => $connection->rules
->groupBy(fn ($rule) => "{$rule->source_resource_id}->{$rule->target_resource_id}")
->groupBy(function ($rule) use ($applicationsById): string {
$sourceUuid = $applicationsById->get($rule->source_resource_id)?->uuid;
$targetUuid = $applicationsById->get($rule->target_resource_id)?->uuid;
return "{$sourceUuid}->{$targetUuid}";
})
->filter(fn (Collection $rules, string $direction): bool => ! str_starts_with($direction, '->') && ! str_ends_with($direction, '->'))
->map(fn (Collection $rules) => $rules
->sortBy('port')
->pluck('port')
@ -1477,7 +1495,7 @@ private function serializeResourceConnection(ResourceConnection $connection): ar
}
/**
* @param array{type: string, id: int} $resource
* @param array{type: string, uuid: string} $resource
*/
private function resolveConnectableResource(Team $team, Project $project, Environment $environment, array $resource): Model
{
@ -1486,7 +1504,7 @@ private function resolveConnectableResource(Team $team, Project $project, Enviro
->where('team_id', $team->id)
->where('project_id', $project->id)
->where('environment_id', $environment->id)
->whereKey($resource['id'])
->where('uuid', $resource['uuid'])
->firstOrFail(),
};
}
@ -1504,17 +1522,34 @@ private function resourceIdentity(Model $resource): string
return $resource->getMorphClass().':'.$resource->getKey();
}
private function connectionHasResourceId(ResourceConnection $connection, mixed $resourceId): bool
/**
* @return Collection<string, V5Application>
*/
private function connectionApplicationsByUuid(ResourceConnection $connection): Collection
{
return in_array((int) $resourceId, [
(int) $connection->resource_one_id,
(int) $connection->resource_two_id,
], true);
return $this->connectionApplicationsById($connection)->keyBy('uuid');
}
private function resourceTypeForConnectionId(ResourceConnection $connection, int $resourceId): string
/**
* @return Collection<int, V5Application>
*/
private function connectionApplicationsById(ResourceConnection $connection): Collection
{
return (int) $connection->resource_one_id === $resourceId
return V5Application::query()
->whereIn('id', [
(int) $connection->resource_one_id,
(int) $connection->resource_two_id,
])
->get()
->keyBy('id');
}
private function resourceTypeForConnectionUuid(ResourceConnection $connection, string $resourceUuid): string
{
$resourcesByUuid = $this->connectionApplicationsByUuid($connection);
$resource = $resourcesByUuid->get($resourceUuid);
return $resource instanceof V5Application && (int) $connection->resource_one_id === $resource->id
? $connection->resource_one_type
: $connection->resource_two_type;
}
@ -1686,7 +1721,7 @@ private function serializeApplication(V5Application $application): array
$isServerReachable = ! $server instanceof V5Server || $this->isServerReachable($server);
return [
'id' => (string) $application->id,
'id' => $application->uuid,
'name' => $application->name,
'image' => $application->image,
'containerName' => $application->container_name,
@ -1756,9 +1791,9 @@ private function privateKeys(mixed $currentTeam): array
->where('team_id', $currentTeam->id)
->where('is_git_related', false)
->orderBy('name')
->get(['id', 'name'])
->get(['id', 'uuid', 'name'])
->map(fn (PrivateKey $privateKey) => [
'id' => (string) $privateKey->id,
'id' => $privateKey->uuid,
'name' => $privateKey->name,
])
->all();
@ -1830,7 +1865,7 @@ private function friendlyIngressSyncError(string $message): string
private function serializeCluster(V5Cluster $cluster): array
{
return [
'id' => (string) $cluster->id,
'id' => $cluster->uuid,
'name' => $cluster->name,
'description' => $cluster->description,
'wireguardInterface' => $cluster->wireguard_interface,
@ -1855,7 +1890,7 @@ private function serializeCluster(V5Cluster $cluster): array
'lastCliRanAt' => $cluster->last_cli_ran_at?->toJSON(),
'serversCount' => $cluster->servers_count ?? $cluster->servers->count(),
'servers' => $cluster->servers->map(fn (V5Server $server) => [
'id' => (string) $server->id,
'id' => $server->uuid,
'name' => $server->name,
'host' => $server->host,
'status' => $server->status,

View file

@ -15,6 +15,7 @@ class Application extends V5Model
protected $table = 'v5_applications';
protected $fillable = [
'uuid',
'team_id',
'project_id',
'environment_id',

View file

@ -40,6 +40,7 @@ class Cluster extends V5Model
public const DEFAULT_BUILDER_TIMEOUT_SECS = 1800;
protected $fillable = [
'uuid',
'team_id',
'created_by_user_id',
'name',

View file

@ -15,6 +15,7 @@ class ResourceConnection extends V5Model
protected $table = 'v5_resource_connections';
protected $fillable = [
'uuid',
'team_id',
'project_id',
'environment_id',

View file

@ -7,6 +7,11 @@
abstract class V5Model extends Model
{
public function getRouteKeyName(): string
{
return 'uuid';
}
protected static function boot(): void
{
parent::boot();

View file

@ -13,6 +13,7 @@ public function up(): void
{
Schema::create('v5_clusters', function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->foreignId('team_id')->constrained('teams')->cascadeOnDelete();
$table->foreignId('created_by_user_id')->constrained('users')->cascadeOnDelete();
$table->string('name');

View file

@ -13,6 +13,7 @@ public function up(): void
{
Schema::create('v5_applications', function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->foreignId('team_id')->constrained('teams')->cascadeOnDelete();
$table->foreignId('project_id')->constrained('projects')->cascadeOnDelete();
$table->foreignId('environment_id')->constrained('environments')->cascadeOnDelete();

View file

@ -13,6 +13,7 @@ public function up(): void
{
Schema::create('v5_resource_connections', function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->foreignId('team_id')->constrained('teams')->cascadeOnDelete();
$table->foreignId('project_id')->constrained('projects')->cascadeOnDelete();
$table->foreignId('environment_id')->constrained('environments')->cascadeOnDelete();

View file

@ -59,7 +59,7 @@ type ServerFormErrors = {
host?: string[];
ssh_user?: string[];
ssh_port?: string[];
private_key_id?: string[];
private_key_uuid?: string[];
node_address?: string[];
builder_enabled?: string[];
builder_capacity?: string[];
@ -530,7 +530,7 @@ export default function Clusters({
host: serverHost,
ssh_user: serverSshUser,
ssh_port: Number(serverSshPort),
private_key_id: selectedPrivateKeyId === '' ? null : Number(selectedPrivateKeyId),
private_key_uuid: selectedPrivateKeyId === '' ? null : selectedPrivateKeyId,
node_address: serverNodeAddress.trim() === '' ? null : serverNodeAddress,
builder_enabled: serverBuilderEnabled,
ingress_enabled: serverIngressEnabled,
@ -1835,7 +1835,7 @@ export default function Clusters({
value={selectedPrivateKeyId}
onChange={(event) => setSelectedPrivateKeyId(event.target.value)}
className="appearance-none rounded-md border border-border bg-background bg-[length:1rem_1rem] bg-[position:right_0.75rem_center] bg-no-repeat px-3 py-2 pr-10 text-sm outline-none transition focus:border-ring focus:ring-0 aria-invalid:border-destructive aria-invalid:ring-0 dark:aria-invalid:border-destructive/50"
aria-invalid={serverErrors.private_key_id ? true : undefined}
aria-invalid={serverErrors.private_key_uuid ? true : undefined}
style={{
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 256 256' fill='none' stroke='%23ffffff' stroke-width='28' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m64 96 64 64 64-64'/%3E%3C/svg%3E")`,
}}
@ -1847,7 +1847,7 @@ export default function Clusters({
</option>
))}
</select>
<FieldError message={serverErrors.private_key_id?.[0]} />
<FieldError message={serverErrors.private_key_uuid?.[0]} />
</Field>
<div className="rounded-lg border border-border bg-muted/20 transition-colors focus-within:border-ring">

View file

@ -413,8 +413,8 @@ function normalizeConnection(connection: V5ResourceConnection): CanvasConnection
'X-CSRF-TOKEN': csrfToken(),
},
body: JSON.stringify({
resource_one: { type: 'application', id: Number(fromApplicationId) },
resource_two: { type: 'application', id: Number(toApplicationId) },
resource_one: { type: 'application', uuid: fromApplicationId },
resource_two: { type: 'application', uuid: toApplicationId },
}),
});
const payload = (await response.json()) as { connection?: V5ResourceConnection; message?: string; detail?: string };
@ -896,7 +896,7 @@ function normalizeConnection(connection: V5ResourceConnection): CanvasConnection
'X-CSRF-TOKEN': csrfToken(),
},
body: JSON.stringify({
server_id: selectedNginxServerId || null,
server_uuid: selectedNginxServerId || null,
image: nginxImage.trim() || DEFAULT_NGINX_IMAGE,
}),
});

View file

@ -2361,7 +2361,7 @@
"category": "automation",
"logo": "svgs/inngest.png",
"minversion": "0.0.0",
"template_last_updated_at": "2026-06-10T13:46:21+05:30",
"template_last_updated_at": "2026-07-02T13:25:47+02:00",
"port": "8288"
},
"invoice-ninja": {

View file

@ -2361,7 +2361,7 @@
"category": "automation",
"logo": "svgs/inngest.png",
"minversion": "0.0.0",
"template_last_updated_at": "2026-06-10T13:46:21+05:30",
"template_last_updated_at": "2026-07-02T13:25:47+02:00",
"port": "8288"
},
"invoice-ninja": {

View file

@ -155,6 +155,160 @@
->toContain("isSavingIngress ? 'Saving...' : 'Enable ingress'");
});
it('uses v5 resource uuids at http boundaries while keeping database ids internal', function () {
createSharedUserAndTeamTables();
[$user, $team] = createV5UserWithTeam();
[$project, $environment] = createV5ProjectWithEnvironment($team, 'Project', 'production');
$server = V5Server::query()->create([
'uuid' => 'server-public-uuid',
'team_id' => $team->id,
'created_by_user_id' => $user->id,
'name' => 'edge-01',
'host' => '203.0.113.20',
'ssh_user' => 'root',
'ssh_port' => 22,
'status' => 'installed',
'capabilities' => [],
'builder_enabled' => false,
'builder_capacity' => 0,
'builder_cpu_quota' => '200%',
'wireguard_management_ip' => '100.64.0.10',
'last_bootstrapped_at' => now(),
]);
$application = V5Application::query()->create([
'uuid' => 'application-public-uuid',
'team_id' => $team->id,
'project_id' => $project->id,
'environment_id' => $environment->id,
'server_id' => $server->id,
'created_by_user_id' => $user->id,
'name' => 'nginx-test',
'image' => 'docker.io/library/nginx:alpine',
'container_name' => 'coolify-v5-nginx-test',
'status' => 'running',
'mesh_namespace' => 'default',
]);
$this
->actingAs($user)
->withSession(['currentTeam' => $team])
->patchJson("/v5/applications/{$application->uuid}/position", [
'canvas_x' => 123,
'canvas_y' => 456,
])
->assertSuccessful()
->assertJsonPath('application.id', $application->uuid);
$this
->actingAs($user)
->withSession(['currentTeam' => $team])
->patchJson("/v5/applications/{$application->id}/position", [
'canvas_x' => 789,
'canvas_y' => 999,
])
->assertNotFound();
$fluxClient = Mockery::mock(FluxClient::class);
$fluxClient->shouldReceive('pullImage')->once()->andReturn('Image pulled.');
$fluxClient->shouldReceive('createContainer')->once()->andReturn('container-id');
$fluxClient->shouldReceive('startContainer')->once()->andReturn('Container started.');
$fluxClient->shouldReceive('inspectContainer')->once()->andReturn(['State' => ['Running' => true]]);
app()->instance(FluxClient::class, $fluxClient);
$this
->actingAs($user)
->withSession([
'currentTeam' => $team,
'v5.selected_project' => ['uuid' => $project->uuid, 'name' => $project->name],
'v5.selected_environment' => ['uuid' => $environment->uuid, 'name' => $environment->name],
])
->postJson('/v5/applications/nginx', [
'server_uuid' => $server->uuid,
'image' => 'docker.io/library/nginx:alpine',
])
->assertSuccessful()
->assertJsonPath('application.id', fn (string $id): bool => $id !== (string) V5Application::query()->latest('id')->value('id'));
});
it('uses v5 resource uuids for resource connection requests and responses', function () {
createSharedUserAndTeamTables();
[$user, $team] = createV5UserWithTeam();
[$project, $environment] = createV5ProjectWithEnvironment($team, 'Project', 'production');
$server = V5Server::query()->create([
'team_id' => $team->id,
'created_by_user_id' => $user->id,
'name' => 'edge-01',
'host' => '203.0.113.20',
'ssh_user' => 'root',
'ssh_port' => 22,
'status' => 'installed',
'capabilities' => [],
'wireguard_management_ip' => '100.64.0.10',
]);
$source = V5Application::query()->create([
'uuid' => 'source-application-uuid',
'team_id' => $team->id,
'project_id' => $project->id,
'environment_id' => $environment->id,
'server_id' => $server->id,
'created_by_user_id' => $user->id,
'name' => 'source',
'image' => 'docker.io/library/nginx:alpine',
'container_name' => 'source-container',
'status' => 'running',
]);
$target = V5Application::query()->create([
'uuid' => 'target-application-uuid',
'team_id' => $team->id,
'project_id' => $project->id,
'environment_id' => $environment->id,
'server_id' => $server->id,
'created_by_user_id' => $user->id,
'name' => 'target',
'image' => 'docker.io/library/nginx:alpine',
'container_name' => 'target-container',
'status' => 'running',
]);
$response = $this
->actingAs($user)
->withSession([
'currentTeam' => $team,
'v5.selected_project' => ['uuid' => $project->uuid, 'name' => $project->name],
'v5.selected_environment' => ['uuid' => $environment->uuid, 'name' => $environment->name],
])
->postJson('/v5/resource-connections', [
'resource_one' => ['type' => 'application', 'uuid' => $source->uuid],
'resource_two' => ['type' => 'application', 'uuid' => $target->uuid],
])
->assertCreated()
->assertJsonPath('connection.fromApplicationId', $source->uuid)
->assertJsonPath('connection.toApplicationId', $target->uuid);
$connectionUuid = $response->json('connection.id');
$fluxClient = Mockery::mock(FluxClient::class);
$fluxClient->shouldReceive('applyFirewallRule')->once()->andReturn('Firewall rule applied.');
app()->instance(FluxClient::class, $fluxClient);
$this
->actingAs($user)
->withSession(['currentTeam' => $team])
->patchJson("/v5/resource-connections/{$connectionUuid}", [
'ports_by_direction' => [
"{$source->uuid}->{$target->uuid}" => [8080],
],
])
->assertSuccessful()
->assertJsonPath("connection.portsByDirection.{$source->uuid}->{$target->uuid}.0", '8080');
expect(ResourceConnection::query()->first())
->resource_one_id->toBe($source->id)
->resource_two_id->toBe($target->id);
});
it('generates http-only caddy routes for application ingress', function () {
createSharedUserAndTeamTables();
@ -215,7 +369,7 @@
$this
->actingAs($user)
->withSession(['currentTeam' => $team])
->patchJson("/v5/applications/{$application->id}/ingress", [
->patchJson("/v5/applications/{$application->uuid}/ingress", [
'ingress_enabled' => true,
'internal_port' => 3000,
'domains' => ['app.example.com'],
@ -972,12 +1126,12 @@
])
->withHeader('X-CSRF-TOKEN', 'test-csrf-token')
->postJson('/v5/resource-connections', [
'resource_one' => ['type' => 'application', 'id' => $source->id],
'resource_two' => ['type' => 'application', 'id' => $target->id],
'resource_one' => ['type' => 'application', 'uuid' => $source->uuid],
'resource_two' => ['type' => 'application', 'uuid' => $target->uuid],
])
->assertCreated()
->assertJsonPath('connection.applicationIds.0', (string) $source->id)
->assertJsonPath('connection.applicationIds.1', (string) $target->id);
->assertJsonPath('connection.applicationIds.0', $source->uuid)
->assertJsonPath('connection.applicationIds.1', $target->uuid);
$connectionId = $response->json('connection.id');
@ -987,13 +1141,13 @@
->withHeader('X-CSRF-TOKEN', 'test-csrf-token')
->patchJson("/v5/resource-connections/{$connectionId}", [
'ports_by_direction' => [
"{$source->id}->{$target->id}" => [80],
"{$target->id}->{$source->id}" => [443],
"{$source->uuid}->{$target->uuid}" => [80],
"{$target->uuid}->{$source->uuid}" => [443],
],
])
->assertSuccessful()
->assertJsonPath("connection.portsByDirection.{$source->id}->{$target->id}.0", '80')
->assertJsonPath("connection.portsByDirection.{$target->id}->{$source->id}.0", '443');
->assertJsonPath("connection.portsByDirection.{$source->uuid}->{$target->uuid}.0", '80')
->assertJsonPath("connection.portsByDirection.{$target->uuid}->{$source->uuid}.0", '443');
$this
->actingAs($user)
@ -1006,8 +1160,8 @@
->assertSuccessful()
->assertSee('"resourceConnections":[', false)
->assertSee("\"id\":\"{$connectionId}\"", false)
->assertSee("\"{$source->id}->{$target->id}\":[\"80\"]", false)
->assertSee("\"{$target->id}->{$source->id}\":[\"443\"]", false);
->assertSee("\"{$source->uuid}->{$target->uuid}\":[\"80\"]", false)
->assertSee("\"{$target->uuid}->{$source->uuid}\":[\"443\"]", false);
});
it('reports flux failures when syncing v5 resource connection firewall rules', function () {
@ -1084,9 +1238,9 @@
->actingAs($user)
->withSession(['currentTeam' => $team, '_token' => 'test-csrf-token'])
->withHeader('X-CSRF-TOKEN', 'test-csrf-token')
->patchJson("/v5/resource-connections/{$connection->id}", [
->patchJson("/v5/resource-connections/{$connection->uuid}", [
'ports_by_direction' => [
"{$source->id}->{$target->id}" => [5432],
"{$source->uuid}->{$target->uuid}" => [5432],
],
])
->assertStatus(502)
@ -1196,9 +1350,9 @@
->actingAs($user)
->withSession(['currentTeam' => $team, '_token' => 'test-csrf-token'])
->withHeader('X-CSRF-TOKEN', 'test-csrf-token')
->patchJson("/v5/resource-connections/{$connection->id}", [
->patchJson("/v5/resource-connections/{$connection->uuid}", [
'ports_by_direction' => [
"{$source->id}->{$target->id}" => [5432],
"{$source->uuid}->{$target->uuid}" => [5432],
],
])
->assertSuccessful();
@ -1293,9 +1447,9 @@
->actingAs($user)
->withSession(['currentTeam' => $team, '_token' => 'test-csrf-token'])
->withHeader('X-CSRF-TOKEN', 'test-csrf-token')
->patchJson("/v5/resource-connections/{$connection->id}", [
->patchJson("/v5/resource-connections/{$connection->uuid}", [
'ports_by_direction' => [
"{$source->id}->{$target->id}" => [5432],
"{$source->uuid}->{$target->uuid}" => [5432],
],
])
->assertSuccessful();
@ -1304,7 +1458,7 @@
->actingAs($user)
->withSession(['currentTeam' => $team, '_token' => 'test-csrf-token'])
->withHeader('X-CSRF-TOKEN', 'test-csrf-token')
->deleteJson("/v5/resource-connections/{$connection->id}")
->deleteJson("/v5/resource-connections/{$connection->uuid}")
->assertNoContent();
});
@ -1426,7 +1580,7 @@
V5Server::query()->create([
'team_id' => $team->id,
'created_by_user_id' => $user->id,
'private_key_id' => $privateKey->id,
'private_key_uuid' => $privateKey->uuid,
'name' => 'edge-01',
'host' => '203.0.113.10',
'ssh_user' => 'root',
@ -1498,7 +1652,7 @@
'v5.selectedEnvironmentUuid' => $environment->uuid,
])
->postJson('/v5/applications/nginx', [
'server_id' => $selectedServer->id,
'server_uuid' => $selectedServer->uuid,
])
->assertCreated()
->assertJsonPath('application.serverName', 'edge-02');
@ -1568,7 +1722,7 @@
V5Server::query()->create([
'team_id' => $team->id,
'created_by_user_id' => $user->id,
'private_key_id' => $privateKey->id,
'private_key_uuid' => $privateKey->uuid,
'name' => 'edge-01',
'host' => '203.0.113.10',
'ssh_user' => 'root',
@ -1605,7 +1759,7 @@
$otherServer = V5Server::query()->create([
'team_id' => $otherTeam->id,
'created_by_user_id' => $otherUser->id,
'private_key_id' => $privateKey->id,
'private_key_uuid' => $privateKey->uuid,
'name' => 'other-edge-01',
'host' => '203.0.113.20',
'ssh_user' => 'root',
@ -1681,7 +1835,7 @@
$this
->actingAs($user)
->withSession(['currentTeam' => $team])
->deleteJson("/v5/applications/{$application->id}")
->deleteJson("/v5/applications/{$application->uuid}")
->assertNoContent();
expect(V5Application::query()->whereKey($application->id)->exists())->toBeFalse();
@ -1724,7 +1878,7 @@
$this
->actingAs($user)
->withSession(['currentTeam' => $team])
->deleteJson("/v5/applications/{$application->id}")
->deleteJson("/v5/applications/{$application->uuid}")
->assertNoContent();
Process::assertRan(function ($process): bool {
@ -1769,7 +1923,7 @@
$this
->actingAs($user)
->withSession(['currentTeam' => $team])
->deleteJson("/v5/applications/{$application->id}")
->deleteJson("/v5/applications/{$application->uuid}")
->assertNotFound();
expect(V5Application::query()->whereKey($application->id)->exists())->toBeTrue();
@ -1807,7 +1961,7 @@
$this
->actingAs($user)
->withSession(['currentTeam' => $team])
->patchJson("/v5/applications/{$application->id}/position", [
->patchJson("/v5/applications/{$application->uuid}/position", [
'canvas_x' => 320,
'canvas_y' => -160,
])
@ -1839,7 +1993,7 @@
$this
->actingAs($user)
->withSession(['currentTeam' => $team])
->patchJson("/v5/caddy-ingresses/{$server->id}/position", [
->patchJson("/v5/caddy-ingresses/{$server->uuid}/position", [
'canvas_x' => -160,
'canvas_y' => 240,
])
@ -2145,6 +2299,69 @@
->assertUnauthorized();
});
it('accepts flux resource status http updates identified by resource uuids', function () {
createSharedUserAndTeamTables();
Config::set('flux.laravel_api_token', 'test-flux-token');
[$user, $team] = createV5UserWithTeam();
[$project, $environment] = createV5ProjectWithEnvironment($team, 'Production Project', 'Production');
$server = V5Server::query()->create([
'uuid' => 'server-public-uuid',
'team_id' => $team->id,
'created_by_user_id' => $user->id,
'name' => 'edge-01',
'host' => '203.0.113.10',
'ssh_user' => 'root',
'ssh_port' => 22,
'status' => 'installed',
'capabilities' => [],
'wireguard_management_ip' => '100.64.0.5',
]);
$application = V5Application::query()->create([
'uuid' => 'application-public-uuid',
'team_id' => $team->id,
'project_id' => $project->id,
'environment_id' => $environment->id,
'server_id' => $server->id,
'created_by_user_id' => $user->id,
'name' => 'nginx-test',
'image' => 'docker.io/library/nginx:alpine',
'container_name' => 'coolify-v5-nginx-1',
'status' => 'starting',
'status_message' => 'Container starting.',
]);
$this
->withToken('test-flux-token')
->postJson('/api/v1/internal/flux/resource-status', [
'resource_type' => 'application',
'server_uuid' => $server->uuid,
'application_uuid' => $application->uuid,
'container_id' => 'new-container-id',
'status' => 'running',
'status_message' => 'Status received from coold through flux.',
])
->assertSuccessful()
->assertJsonPath('message', 'Resource status updated.');
expect($application->refresh()->status)->toBe('running')
->and($application->runtime_container_id)->toBe('new-container-id');
});
it('rejects numeric laravel ids in flux resource status http payloads', function () {
Config::set('flux.laravel_api_token', 'test-flux-token');
$this
->withToken('test-flux-token')
->postJson('/api/v1/internal/flux/resource-status', [
'resource_type' => 'application',
'server_id' => 1,
'application_id' => 1,
'status' => 'running',
])
->assertInvalid(['server_id', 'application_id']);
});
it('accepts flux resource status http updates and stores them in the database', function () {
createSharedUserAndTeamTables();
Config::set('flux.laravel_api_token', 'test-flux-token');
@ -2440,7 +2657,7 @@
])
->postJson('/v5/applications/refresh')
->assertSuccessful()
->assertJsonPath('applications.0.id', (string) $application->id)
->assertJsonPath('applications.0.id', $application->uuid)
->assertJsonPath('applications.0.status', 'exited')
->assertJsonPath('applications.0.statusMessage', 'Container state refreshed from coold.');
@ -2491,7 +2708,7 @@
])
->postJson('/v5/applications/refresh')
->assertSuccessful()
->assertJsonPath('caddyIngresses.0.id', (string) $server->id)
->assertJsonPath('caddyIngresses.0.id', $server->uuid)
->assertJsonPath('caddyIngresses.0.type', 'caddy')
->assertJsonPath('caddyIngresses.0.status', 'exited');
@ -2537,7 +2754,7 @@
$this
->actingAs($user)
->withSession(['currentTeam' => $team])
->postJson("/v5/clusters/{$cluster->id}/servers/{$server->id}/bootstrap")
->postJson("/v5/clusters/{$cluster->uuid}/servers/{$server->uuid}/bootstrap")
->assertAccepted();
Event::assertDispatched(V5ClusterUpdated::class, fn ($event): bool => $event->clusterId === $cluster->id
@ -2589,7 +2806,7 @@
$this
->actingAs($user)
->withSession(['currentTeam' => $team])
->getJson("/v5/clusters/{$cluster->id}/servers/{$server->id}/coold-logs?tail=200")
->getJson("/v5/clusters/{$cluster->uuid}/servers/{$server->uuid}/coold-logs?tail=200")
->assertSuccessful()
->assertJsonPath('output', 'Jun 22 coold[123]: started')
->assertJsonStructure(['output', 'fetchedAt']);
@ -2630,7 +2847,7 @@
$this
->actingAs($user)
->withSession(['currentTeam' => $team])
->getJson("/v5/clusters/{$cluster->id}/servers/{$server->id}/corrosion-tables?limit=200")
->getJson("/v5/clusters/{$cluster->uuid}/servers/{$server->uuid}/corrosion-tables?limit=200")
->assertSuccessful()
->assertJsonPath('output', '{"limit":200,"tables":[{"name":"service_endpoints","columns":["container_name"],"rows":[["coolify-v5-nginx"]]}]}')
->assertJsonStructure(['output', 'fetchedAt']);
@ -2678,7 +2895,7 @@
$this
->actingAs($user)
->withSession(['currentTeam' => $team])
->getJson("/v5/clusters/{$cluster->id}/servers/{$server->id}/firewall-rules")
->getJson("/v5/clusters/{$cluster->uuid}/servers/{$server->uuid}/firewall-rules")
->assertSuccessful()
->assertJsonPath('rules.0.id', 'v5-resource-connection:1:1:2:tcp:5432')
->assertJsonPath('rules.0.port', 5432)
@ -2742,10 +2959,10 @@
$this
->actingAs($user)
->withSession(['currentTeam' => $team])
->getJson("/v5/clusters/{$cluster->id}")
->getJson("/v5/clusters/{$cluster->uuid}")
->assertSuccessful()
->assertJsonPath('cluster.id', (string) $cluster->id)
->assertJsonPath('cluster.servers.0.id', (string) $server->id)
->assertJsonPath('cluster.id', $cluster->uuid)
->assertJsonPath('cluster.servers.0.id', $server->uuid)
->assertJsonPath('cluster.servers.0.lastBootstrapStatus', 'running')
->assertJsonPath('cluster.servers.0.lastBootstrapOutput', 'Starting Coolify CLI bootstrap for prod-01...');
});
@ -3035,12 +3252,12 @@
$this
->actingAs($user)
->withSession(['currentTeam' => $team])
->postJson("/v5/clusters/{$cluster->id}/servers", [
->postJson("/v5/clusters/{$cluster->uuid}/servers", [
'name' => 'prod-01',
'host' => '203.0.113.10',
'ssh_user' => 'root',
'ssh_port' => 22,
'private_key_id' => $privateKey->id,
'private_key_uuid' => $privateKey->uuid,
'node_address' => '203.0.113.10',
'builder_enabled' => true,
'builder_capacity' => 3,
@ -3102,12 +3319,12 @@
$this
->actingAs($user)
->withSession(['currentTeam' => $team])
->postJson("/v5/clusters/{$cluster->id}/servers", [
->postJson("/v5/clusters/{$cluster->uuid}/servers", [
'name' => 'edge-01',
'host' => '203.0.113.20',
'ssh_user' => 'root',
'ssh_port' => 22,
'private_key_id' => $privateKey->id,
'private_key_uuid' => $privateKey->uuid,
'builder_enabled' => false,
'builder_capacity' => 0,
'ingress_enabled' => true,
@ -3142,12 +3359,12 @@
$this
->actingAs($user)
->withSession(['currentTeam' => $team])
->postJson("/v5/clusters/{$cluster->id}/servers", [
->postJson("/v5/clusters/{$cluster->uuid}/servers", [
'name' => 'prod-01',
'host' => '203.0.113.10',
'ssh_user' => 'root',
'ssh_port' => 22,
'private_key_id' => $privateKey->id,
'private_key_uuid' => $privateKey->uuid,
'builder_enabled' => false,
'builder_capacity' => 3,
])
@ -3178,12 +3395,12 @@
$this
->actingAs($user)
->withSession(['currentTeam' => $team])
->postJson("/v5/clusters/{$cluster->id}/servers", [
->postJson("/v5/clusters/{$cluster->uuid}/servers", [
'name' => 'prod-01',
'host' => '203.0.113.10',
'ssh_user' => 'root',
'ssh_port' => 22,
'private_key_id' => $privateKey->id,
'private_key_uuid' => $privateKey->uuid,
'builder_enabled' => true,
'builder_capacity' => 0,
])
@ -3206,12 +3423,12 @@
$this
->actingAs($user)
->withSession(['currentTeam' => $team])
->postJson("/v5/clusters/{$cluster->id}/servers", [
->postJson("/v5/clusters/{$cluster->uuid}/servers", [
'name' => 'coolify-naked-test',
'host' => 'host.docker.internal',
'ssh_user' => 'root',
'ssh_port' => 60003,
'private_key_id' => $privateKey->id,
'private_key_uuid' => $privateKey->uuid,
])
->assertCreated()
->assertJsonPath('cluster.servers.0.wireguardListenPortOverride', 51823)
@ -3238,7 +3455,7 @@
$this
->actingAs($user)
->withSession(['currentTeam' => $team])
->postJson("/v5/clusters/{$cluster->id}/servers", [
->postJson("/v5/clusters/{$cluster->uuid}/servers", [
'name' => 'prod-01',
'host' => '203.0.113.10',
'ssh_user' => 'root',
@ -3261,12 +3478,12 @@
$this
->actingAs($user)
->withSession(['currentTeam' => $team])
->postJson("/v5/clusters/{$cluster->id}/servers", [
->postJson("/v5/clusters/{$cluster->uuid}/servers", [
'name' => '',
'host' => '',
'ssh_user' => '',
'ssh_port' => 70000,
'private_key_id' => null,
'private_key_uuid' => null,
'wireguard_listen_port_override' => 70000,
])
->assertUnprocessable()
@ -3275,7 +3492,7 @@
'host',
'ssh_user',
'ssh_port',
'private_key_id',
'private_key_uuid',
'wireguard_listen_port_override',
]);
});
@ -3301,15 +3518,15 @@
$this
->actingAs($user)
->withSession(['currentTeam' => $team])
->postJson("/v5/clusters/{$cluster->id}/servers", [
->postJson("/v5/clusters/{$cluster->uuid}/servers", [
'name' => 'prod-01',
'host' => '203.0.113.10',
'ssh_user' => 'root',
'ssh_port' => 22,
'private_key_id' => $otherPrivateKey->id,
'private_key_uuid' => $otherPrivateKey->uuid,
])
->assertUnprocessable()
->assertJsonValidationErrors(['private_key_id']);
->assertJsonValidationErrors(['private_key_uuid']);
});
it('checks v5 server ssh status without storing diagnostic output', function () {
@ -3344,7 +3561,7 @@
$this
->actingAs($user)
->withSession(['currentTeam' => $team])
->postJson("/v5/clusters/{$cluster->id}/servers/{$server->id}/check")
->postJson("/v5/clusters/{$cluster->uuid}/servers/{$server->uuid}/check")
->assertSuccessful()
->assertJsonPath('status', 'reachable')
->assertJsonPath('output', "SSH connection OK\nprod-01\nLinux aarch64\n/usr/bin/docker")
@ -3456,7 +3673,7 @@
$this
->actingAs($user)
->withSession(['currentTeam' => $team])
->postJson("/v5/clusters/{$cluster->id}/servers/{$server->id}/bootstrap")
->postJson("/v5/clusters/{$cluster->uuid}/servers/{$server->uuid}/bootstrap")
->assertAccepted()
->assertJsonPath('message', 'Bootstrap queued.')
->assertJsonPath('cluster.servers.0.status', 'added')
@ -3538,7 +3755,7 @@
$this
->actingAs($user)
->withSession(['currentTeam' => $team])
->postJson("/v5/clusters/{$cluster->id}/servers/{$server->id}/bootstrap")
->postJson("/v5/clusters/{$cluster->uuid}/servers/{$server->uuid}/bootstrap")
->assertAccepted();
Process::fake([
@ -3583,7 +3800,7 @@
$this
->actingAs($user)
->withSession(['currentTeam' => $team])
->postJson("/v5/clusters/{$cluster->id}/servers/{$server->id}/bootstrap")
->postJson("/v5/clusters/{$cluster->uuid}/servers/{$server->uuid}/bootstrap")
->assertAccepted()
->assertJsonPath('cluster.servers.0.status', 'added')
->assertJsonPath('cluster.servers.0.lastBootstrappedAt', null)
@ -3653,7 +3870,7 @@
$this
->actingAs($user)
->withSession(['currentTeam' => $team])
->postJson("/v5/clusters/{$cluster->id}/servers/{$newServer->id}/bootstrap")
->postJson("/v5/clusters/{$cluster->uuid}/servers/{$newServer->uuid}/bootstrap")
->assertAccepted()
->assertJsonPath('cluster.servers.1.lastBootstrapAction', 'extend')
->assertJsonPath('cluster.servers.1.lastBootstrapStatus', 'queued');
@ -3817,7 +4034,7 @@
$this
->actingAs($user)
->withSession(['currentTeam' => $team])
->deleteJson("/v5/clusters/{$cluster->id}/servers/{$server->id}")
->deleteJson("/v5/clusters/{$cluster->uuid}/servers/{$server->uuid}")
->assertSuccessful()
->assertJsonPath('cluster.serversCount', 0)
->assertJsonPath('cluster.servers', []);
@ -3852,7 +4069,7 @@
$this
->actingAs($user)
->withSession(['currentTeam' => $team])
->deleteJson("/v5/clusters/{$cluster->id}/servers/{$server->id}")
->deleteJson("/v5/clusters/{$cluster->uuid}/servers/{$server->uuid}")
->assertSuccessful()
->assertJsonPath('cluster.serversCount', 0)
->assertJsonPath('cluster.servers', []);
@ -3891,7 +4108,7 @@
$this
->actingAs($user)
->withSession(['currentTeam' => $team])
->patchJson("/v5/clusters/{$cluster->id}/servers/{$server->id}", [
->patchJson("/v5/clusters/{$cluster->uuid}/servers/{$server->uuid}", [
'builder_enabled' => true,
'builder_capacity' => 5,
'builder_cpu_quota' => '350%',
@ -3953,7 +4170,7 @@
$this
->actingAs($user)
->withSession(['currentTeam' => $team])
->patchJson("/v5/clusters/{$cluster->id}/servers/{$server->id}", [
->patchJson("/v5/clusters/{$cluster->uuid}/servers/{$server->uuid}", [
'builder_enabled' => false,
'builder_capacity' => 2,
'builder_cpu_quota' => '200%',
@ -4021,7 +4238,7 @@
$this
->actingAs($user)
->withSession(['currentTeam' => $team])
->patchJson("/v5/applications/{$application->id}/ingress", [
->patchJson("/v5/applications/{$application->uuid}/ingress", [
'ingress_enabled' => true,
'internal_port' => 3000,
'domains' => ['app.example.com'],
@ -4094,7 +4311,7 @@
$this
->actingAs($user)
->withSession(['currentTeam' => $team])
->patchJson("/v5/applications/{$application->id}/ingress", [
->patchJson("/v5/applications/{$application->uuid}/ingress", [
'ingress_enabled' => false,
'internal_port' => 8080,
])
@ -4153,7 +4370,7 @@
$this
->actingAs($user)
->withSession(['currentTeam' => $team])
->patchJson("/v5/applications/{$application->id}/ingress", [
->patchJson("/v5/applications/{$application->uuid}/ingress", [
'ingress_enabled' => true,
'internal_port' => 3000,
'domains' => ['https://bad.example.com'],
@ -4223,7 +4440,7 @@
$this
->actingAs($user)
->withSession(['currentTeam' => $team])
->patchJson("/v5/applications/{$application->id}/ingress", [
->patchJson("/v5/applications/{$application->uuid}/ingress", [
'ingress_enabled' => true,
'internal_port' => 3000,
'domains' => ['app.example.com'],
@ -4288,7 +4505,7 @@
$this
->actingAs($user)
->withSession(['currentTeam' => $team])
->patchJson("/v5/applications/{$application->id}/ingress", [
->patchJson("/v5/applications/{$application->uuid}/ingress", [
'ingress_enabled' => true,
'internal_port' => 3000,
'domains' => ['app.example.com'],
@ -4368,7 +4585,7 @@
$this
->actingAs($user)
->withSession(['currentTeam' => $team])
->patchJson("/v5/clusters/{$cluster->id}/servers/{$server->id}", [
->patchJson("/v5/clusters/{$cluster->uuid}/servers/{$server->uuid}", [
'builder_enabled' => false,
'builder_capacity' => 0,
'builder_cpu_quota' => '200%',
@ -4439,7 +4656,7 @@
$this
->actingAs($user)
->withSession(['currentTeam' => $team])
->patchJson("/v5/clusters/{$cluster->id}/servers/{$server->id}", [
->patchJson("/v5/clusters/{$cluster->uuid}/servers/{$server->uuid}", [
'builder_enabled' => false,
'builder_capacity' => 0,
'builder_cpu_quota' => '200%',
@ -4479,7 +4696,7 @@
$this
->actingAs($user)
->withSession(['currentTeam' => $team])
->patchJson("/v5/clusters/{$cluster->id}/servers/{$server->id}", [
->patchJson("/v5/clusters/{$cluster->uuid}/servers/{$server->uuid}", [
'builder_enabled' => false,
'builder_capacity' => 5,
'builder_cpu_quota' => '350%',
@ -4524,7 +4741,7 @@
$this
->actingAs($user)
->withSession(['currentTeam' => $team])
->patchJson("/v5/clusters/{$cluster->id}/servers/{$server->id}", [
->patchJson("/v5/clusters/{$cluster->uuid}/servers/{$server->uuid}", [
'builder_enabled' => true,
'builder_capacity' => 1001,
'builder_cpu_quota' => str_repeat('a', 33),
@ -4561,7 +4778,7 @@
$this
->actingAs($user)
->withSession(['currentTeam' => $team])
->patchJson("/v5/clusters/{$cluster->id}/servers/{$server->id}", [
->patchJson("/v5/clusters/{$cluster->uuid}/servers/{$server->uuid}", [
'builder_enabled' => true,
'builder_capacity' => 0,
'builder_cpu_quota' => '200%',
@ -4622,7 +4839,7 @@
$this
->actingAs($user)
->withSession(['currentTeam' => $team])
->deleteJson('/v5/clusters/'.$cluster->id)
->deleteJson('/v5/clusters/'.$cluster->uuid)
->assertNoContent();
expect(Cluster::query()->whereKey($cluster->id)->exists())->toBeFalse();
@ -4658,7 +4875,7 @@
$this
->actingAs($user)
->withSession(['currentTeam' => $team])
->deleteJson('/v5/clusters/'.$cluster->id)
->deleteJson('/v5/clusters/'.$cluster->uuid)
->assertUnprocessable()
->assertJsonPath('message', 'Only empty clusters can be deleted.');
@ -4691,7 +4908,7 @@
$this
->actingAs($user)
->withSession(['currentTeam' => $team])
->deleteJson('/v5/clusters/'.$cluster->id)
->deleteJson('/v5/clusters/'.$cluster->uuid)
->assertNotFound();
expect(Cluster::query()->whereKey($cluster->id)->exists())->toBeTrue();
@ -4869,7 +5086,7 @@
->toContain('selectedNginxServerId')
->toContain('nginxImage')
->toContain('docker.io/library/nginx:alpine')
->toContain('server_id: selectedNginxServerId || null')
->toContain('server_uuid: selectedNginxServerId || null')
->toContain('image: nginxImage.trim() || DEFAULT_NGINX_IMAGE')
->toContain('Center')
->toContain('Delete')
@ -5284,7 +5501,7 @@
->toContain("import { Input } from '@/components/ui/input';")
->toContain("import { Textarea } from '@/components/ui/textarea';")
->toContain('<FieldError message={errors.name?.[0]} />')
->toContain('<FieldError message={serverErrors.private_key_id?.[0]} />')
->toContain('<FieldError message={serverErrors.private_key_uuid?.[0]} />')
->not->toContain('{errors.name ? <span className="text-xs text-destructive">{errors.name[0]}</span> : null}')
->not->toContain('{serverErrors.name ? (');
});
@ -5675,6 +5892,7 @@ function createSharedUserAndTeamTables(): void
Schema::create('v5_clusters', function ($table) {
$table->id();
$table->string('uuid')->nullable()->unique();
$table->foreignId('team_id');
$table->foreignId('created_by_user_id');
$table->string('name');
@ -5756,6 +5974,7 @@ function createSharedUserAndTeamTables(): void
Schema::create('v5_applications', function ($table) {
$table->id();
$table->string('uuid')->nullable()->unique();
$table->foreignId('team_id');
$table->foreignId('project_id');
$table->foreignId('environment_id');
@ -5786,6 +6005,7 @@ function createSharedUserAndTeamTables(): void
Schema::create('v5_resource_connections', function ($table) {
$table->id();
$table->string('uuid')->nullable()->unique();
$table->foreignId('team_id');
$table->foreignId('project_id');
$table->foreignId('environment_id');