From ad8f4423180332d1732da0076ed226ab5ca91c8c Mon Sep 17 00:00:00 2001 From: Yanluis Fermin <32645451+Jacxk@users.noreply.github.com> Date: Wed, 30 Jul 2025 15:59:35 -0400 Subject: [PATCH 1/3] refactor(services): update validation rules to be optional --- .../Controllers/Api/ServicesController.php | 71 ++++++++++--------- 1 file changed, 39 insertions(+), 32 deletions(-) diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php index 542be83de..f385b4d0e 100644 --- a/app/Http/Controllers/Api/ServicesController.php +++ b/app/Http/Controllers/Api/ServicesController.php @@ -7,6 +7,7 @@ use App\Actions\Service\StartService; use App\Actions\Service\StopService; use App\Http\Controllers\Controller; use App\Jobs\DeleteResourceJob; +use App\Models\Environment; use App\Models\EnvironmentVariable; use App\Models\Project; use App\Models\Server; @@ -550,7 +551,6 @@ class ServicesController extends Controller mediaType: 'application/json', schema: new OA\Schema( type: 'object', - required: ['server_uuid', 'project_uuid', 'environment_name', 'environment_uuid', 'docker_compose_raw'], properties: [ 'name' => ['type' => 'string', 'description' => 'The service name.'], 'description' => ['type' => 'string', 'description' => 'The service description.'], @@ -627,16 +627,16 @@ class ServicesController extends Controller { $allowedFields = ['name', 'description', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'docker_compose_raw', 'connect_to_docker_network']; $validator = customApiValidator($request->all(), [ - 'project_uuid' => 'string|required', + 'project_uuid' => 'string|nullable', 'environment_name' => 'string|nullable', 'environment_uuid' => 'string|nullable', - 'server_uuid' => 'string|required', + 'server_uuid' => 'string|nullable', 'destination_uuid' => 'string', 'name' => 'string|max:255', 'description' => 'string|nullable', 'instant_deploy' => 'boolean', 'connect_to_docker_network' => 'boolean', - 'docker_compose_raw' => 'string|required', + 'docker_compose_raw' => 'string|nullable', ]); $extraFields = array_diff(array_keys($request->all()), $allowedFields); @@ -654,14 +654,19 @@ class ServicesController extends Controller ], 422); } - $environmentUuid = $request->environment_uuid; - $environmentName = $request->environment_name; - if (blank($environmentUuid) && blank($environmentName)) { - return response()->json(['message' => 'You need to provide at least one of environment_name or environment_uuid.'], 422); + $environmentUuid = null; + $environmentName = null; + + if ($request->environment_uuid) { + $environmentUuid = $request->environment_uuid; + } elseif ($request->environment_name) { + $environmentName = $request->environment_name; + } else { + $environmentUuid = $service->environment->uuid; } - $serverUuid = $request->server_uuid; - $instantDeploy = $request->instant_deploy ?? false; - $project = Project::whereTeamId($teamId)->whereUuid($request->project_uuid)->first(); + $serverUuid = $request->server_uuid ?? $service->server->uuid; + $projectUuid = $request->project_uuid ?? $service->environment->project->uuid; + $project = Project::whereTeamId($teamId)->whereUuid($projectUuid)->first(); if (! $project) { return response()->json(['message' => 'Project not found.'], 404); } @@ -684,39 +689,41 @@ class ServicesController extends Controller return response()->json(['message' => 'Server has multiple destinations and you do not set destination_uuid.'], 400); } $destination = $destinations->first(); - if (! isBase64Encoded($request->docker_compose_raw)) { - return response()->json([ - 'message' => 'Validation failed.', - 'errors' => [ - 'docker_compose_raw' => 'The docker_compose_raw should be base64 encoded.', - ], - ], 422); + if ($request->has('docker_compose_raw')) { + if (! isBase64Encoded($request->docker_compose_raw)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'docker_compose_raw' => 'The docker_compose_raw should be base64 encoded.', + ], + ], 422); + } + $dockerComposeRaw = base64_decode($request->docker_compose_raw); + if (mb_detect_encoding($dockerComposeRaw, 'ASCII', true) === false) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'docker_compose_raw' => 'The docker_compose_raw should be base64 encoded.', + ], + ], 422); + } + $dockerCompose = base64_decode($request->docker_compose_raw); + $dockerComposeRaw = Yaml::dump(Yaml::parse($dockerCompose), 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK); + $service->docker_compose_raw = $dockerComposeRaw; } - $dockerComposeRaw = base64_decode($request->docker_compose_raw); - if (mb_detect_encoding($dockerComposeRaw, 'ASCII', true) === false) { - return response()->json([ - 'message' => 'Validation failed.', - 'errors' => [ - 'docker_compose_raw' => 'The docker_compose_raw should be base64 encoded.', - ], - ], 422); - } - $dockerCompose = base64_decode($request->docker_compose_raw); - $dockerComposeRaw = Yaml::dump(Yaml::parse($dockerCompose), 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK); $connectToDockerNetwork = $request->connect_to_docker_network ?? false; $service->name = $request->name ?? null; $service->description = $request->description ?? null; - $service->docker_compose_raw = $dockerComposeRaw; $service->environment_id = $environment->id; $service->server_id = $server->id; $service->destination_id = $destination->id; $service->destination_type = $destination->getMorphClass(); $service->connect_to_docker_network = $connectToDockerNetwork; $service->save(); - + $service->parse(); - if ($instantDeploy) { + if ($request->instant_deploy) { StartService::dispatch($service); } From 9b0fd2073a06a1f470d5b2b36bae9f43f4ab19c7 Mon Sep 17 00:00:00 2001 From: Yanluis Fermin <32645451+Jacxk@users.noreply.github.com> Date: Mon, 11 Aug 2025 12:31:12 -0400 Subject: [PATCH 2/3] fix(api): update service upsert to retain name and description values if not set --- app/Http/Controllers/Api/ServicesController.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php index f2d6c4dcf..1db2a3663 100644 --- a/app/Http/Controllers/Api/ServicesController.php +++ b/app/Http/Controllers/Api/ServicesController.php @@ -713,8 +713,8 @@ class ServicesController extends Controller } $connectToDockerNetwork = $request->connect_to_docker_network ?? false; - $service->name = $request->name ?? null; - $service->description = $request->description ?? null; + $service->name = $request->name ?? $service->name; + $service->description = $request->description ?? $service->description; $service->environment_id = $environment->id; $service->server_id = $server->id; $service->destination_id = $destination->id; From 0e014ce2130f00c3563b20295f83ad4e1aaf0614 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sun, 17 Aug 2025 19:45:12 +0200 Subject: [PATCH 3/3] fix(service api): separate create and update service functionalities --- .../Controllers/Api/ServicesController.php | 190 +++++++++++------- 1 file changed, 122 insertions(+), 68 deletions(-) diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php index 1db2a3663..2c831879b 100644 --- a/app/Http/Controllers/Api/ServicesController.php +++ b/app/Http/Controllers/Api/ServicesController.php @@ -7,7 +7,6 @@ use App\Actions\Service\StartService; use App\Actions\Service\StopService; use App\Http\Controllers\Controller; use App\Jobs\DeleteResourceJob; -use App\Models\Environment; use App\Models\EnvironmentVariable; use App\Models\Project; use App\Models\Server; @@ -378,14 +377,118 @@ class ServicesController extends Controller return response()->json(['message' => 'Service not found.', 'valid_service_types' => $serviceKeys], 404); } elseif (filled($request->docker_compose_raw)) { + $allowedFields = ['name', 'description', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'docker_compose_raw', 'connect_to_docker_network']; - $service = new Service; - $result = $this->upsert_service($request, $service, $teamId); - if ($result instanceof \Illuminate\Http\JsonResponse) { - return $result; + $validator = customApiValidator($request->all(), [ + 'project_uuid' => 'string|required', + 'environment_name' => 'string|nullable', + 'environment_uuid' => 'string|nullable', + 'server_uuid' => 'string|required', + 'destination_uuid' => 'string', + 'name' => 'string|max:255', + 'description' => 'string|nullable', + 'instant_deploy' => 'boolean', + 'connect_to_docker_network' => 'boolean', + 'docker_compose_raw' => 'string|required', + ]); + + $extraFields = array_diff(array_keys($request->all()), $allowedFields); + if ($validator->fails() || ! empty($extraFields)) { + $errors = $validator->errors(); + if (! empty($extraFields)) { + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + } + + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); } - return response()->json(serializeApiResponse($result))->setStatusCode(201); + $environmentUuid = $request->environment_uuid; + $environmentName = $request->environment_name; + if (blank($environmentUuid) && blank($environmentName)) { + return response()->json(['message' => 'You need to provide at least one of environment_name or environment_uuid.'], 422); + } + $serverUuid = $request->server_uuid; + $projectUuid = $request->project_uuid; + $project = Project::whereTeamId($teamId)->whereUuid($projectUuid)->first(); + if (! $project) { + return response()->json(['message' => 'Project not found.'], 404); + } + $environment = $project->environments()->where('name', $environmentName)->first(); + if (! $environment) { + $environment = $project->environments()->where('uuid', $environmentUuid)->first(); + } + if (! $environment) { + return response()->json(['message' => 'Environment not found.'], 404); + } + $server = Server::whereTeamId($teamId)->whereUuid($serverUuid)->first(); + if (! $server) { + return response()->json(['message' => 'Server not found.'], 404); + } + $destinations = $server->destinations(); + if ($destinations->count() == 0) { + return response()->json(['message' => 'Server has no destinations.'], 400); + } + if ($destinations->count() > 1 && ! $request->has('destination_uuid')) { + return response()->json(['message' => 'Server has multiple destinations and you do not set destination_uuid.'], 400); + } + $destination = $destinations->first(); + if (! isBase64Encoded($request->docker_compose_raw)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'docker_compose_raw' => 'The docker_compose_raw should be base64 encoded.', + ], + ], 422); + } + $dockerComposeRaw = base64_decode($request->docker_compose_raw); + if (mb_detect_encoding($dockerComposeRaw, 'ASCII', true) === false) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'docker_compose_raw' => 'The docker_compose_raw should be base64 encoded.', + ], + ], 422); + } + $dockerCompose = base64_decode($request->docker_compose_raw); + $dockerComposeRaw = Yaml::dump(Yaml::parse($dockerCompose), 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK); + + $connectToDockerNetwork = $request->connect_to_docker_network ?? false; + $instantDeploy = $request->instant_deploy ?? false; + + $service = new Service; + $service->name = $request->name ?? 'service-'.str()->random(10); + $service->description = $request->description; + $service->docker_compose_raw = $dockerComposeRaw; + $service->environment_id = $environment->id; + $service->server_id = $server->id; + $service->destination_id = $destination->id; + $service->destination_type = $destination->getMorphClass(); + $service->connect_to_docker_network = $connectToDockerNetwork; + $service->save(); + + $service->parse(isNew: true); + if ($instantDeploy) { + StartService::dispatch($service); + } + + $domains = $service->applications()->get()->pluck('fqdn')->sort(); + $domains = $domains->map(function ($domain) { + if (count(explode(':', $domain)) > 2) { + return str($domain)->beforeLast(':')->value(); + } + + return $domain; + })->values(); + + return response()->json([ + 'uuid' => $service->uuid, + 'domains' => $domains, + ])->setStatusCode(201); } else { return response()->json(['message' => 'No service type or docker_compose_raw provided.'], 400); } @@ -615,23 +718,9 @@ class ServicesController extends Controller return response()->json(['message' => 'Service not found.'], 404); } - $result = $this->upsert_service($request, $service, $teamId); - if ($result instanceof \Illuminate\Http\JsonResponse) { - return $result; - } + $allowedFields = ['name', 'description', 'instant_deploy', 'docker_compose_raw', 'connect_to_docker_network']; - return response()->json(serializeApiResponse($result))->setStatusCode(200); - } - - private function upsert_service(Request $request, Service $service, string $teamId) - { - $allowedFields = ['name', 'description', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'docker_compose_raw', 'connect_to_docker_network']; $validator = customApiValidator($request->all(), [ - 'project_uuid' => 'string|nullable', - 'environment_name' => 'string|nullable', - 'environment_uuid' => 'string|nullable', - 'server_uuid' => 'string|nullable', - 'destination_uuid' => 'string', 'name' => 'string|max:255', 'description' => 'string|nullable', 'instant_deploy' => 'boolean', @@ -653,42 +742,6 @@ class ServicesController extends Controller 'errors' => $errors, ], 422); } - - $environmentUuid = null; - $environmentName = null; - - if ($request->environment_uuid) { - $environmentUuid = $request->environment_uuid; - } elseif ($request->environment_name) { - $environmentName = $request->environment_name; - } else { - $environmentUuid = $service->environment->uuid; - } - $serverUuid = $request->server_uuid ?? $service->server->uuid; - $projectUuid = $request->project_uuid ?? $service->environment->project->uuid; - $project = Project::whereTeamId($teamId)->whereUuid($projectUuid)->first(); - if (! $project) { - return response()->json(['message' => 'Project not found.'], 404); - } - $environment = $project->environments()->where('name', $environmentName)->first(); - if (! $environment) { - $environment = $project->environments()->where('uuid', $environmentUuid)->first(); - } - if (! $environment) { - return response()->json(['message' => 'Environment not found.'], 404); - } - $server = Server::whereTeamId($teamId)->whereUuid($serverUuid)->first(); - if (! $server) { - return response()->json(['message' => 'Server not found.'], 404); - } - $destinations = $server->destinations(); - if ($destinations->count() == 0) { - return response()->json(['message' => 'Server has no destinations.'], 400); - } - if ($destinations->count() > 1 && ! $request->has('destination_uuid')) { - return response()->json(['message' => 'Server has multiple destinations and you do not set destination_uuid.'], 400); - } - $destination = $destinations->first(); if ($request->has('docker_compose_raw')) { if (! isBase64Encoded($request->docker_compose_raw)) { return response()->json([ @@ -711,17 +764,18 @@ class ServicesController extends Controller $dockerComposeRaw = Yaml::dump(Yaml::parse($dockerCompose), 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK); $service->docker_compose_raw = $dockerComposeRaw; } - $connectToDockerNetwork = $request->connect_to_docker_network ?? false; - $service->name = $request->name ?? $service->name; - $service->description = $request->description ?? $service->description; - $service->environment_id = $environment->id; - $service->server_id = $server->id; - $service->destination_id = $destination->id; - $service->destination_type = $destination->getMorphClass(); - $service->connect_to_docker_network = $connectToDockerNetwork; + if ($request->has('name')) { + $service->name = $request->name; + } + if ($request->has('description')) { + $service->description = $request->description; + } + if ($request->has('connect_to_docker_network')) { + $service->connect_to_docker_network = $request->connect_to_docker_network; + } $service->save(); - + $service->parse(); if ($request->instant_deploy) { StartService::dispatch($service); @@ -736,10 +790,10 @@ class ServicesController extends Controller return $domain; })->values(); - return [ + return response()->json([ 'uuid' => $service->uuid, 'domains' => $domains, - ]; + ])->setStatusCode(200); } #[OA\Get(