mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-13 20:14:45 +00:00
Merge remote-tracking branch 'origin/next' into env-var-descriptions
This commit is contained in:
commit
d9e39ba211
404 files changed
+29406
-26356
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -411,7 +411,6 @@ class CloudProviderTokensController extends Controller
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
)
|
||||
),
|
||||
],
|
||||
|
||||
@@ -117,7 +117,6 @@ class DatabasesController extends Controller
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
)
|
||||
),
|
||||
],
|
||||
@@ -182,7 +181,6 @@ class DatabasesController extends Controller
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
)
|
||||
),
|
||||
],
|
||||
@@ -245,7 +243,6 @@ class DatabasesController extends Controller
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
)
|
||||
),
|
||||
],
|
||||
@@ -392,7 +389,7 @@ class DatabasesController extends Controller
|
||||
], 422);
|
||||
}
|
||||
$postgresConf = base64_decode($request->postgres_conf);
|
||||
if (mb_detect_encoding($postgresConf, 'ASCII', true) === false) {
|
||||
if (mb_detect_encoding($postgresConf, 'UTF-8', true) === false) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => [
|
||||
@@ -432,7 +429,7 @@ class DatabasesController extends Controller
|
||||
], 422);
|
||||
}
|
||||
$redisConf = base64_decode($request->redis_conf);
|
||||
if (mb_detect_encoding($redisConf, 'ASCII', true) === false) {
|
||||
if (mb_detect_encoding($redisConf, 'UTF-8', true) === false) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => [
|
||||
@@ -459,7 +456,7 @@ class DatabasesController extends Controller
|
||||
], 422);
|
||||
}
|
||||
$keydbConf = base64_decode($request->keydb_conf);
|
||||
if (mb_detect_encoding($keydbConf, 'ASCII', true) === false) {
|
||||
if (mb_detect_encoding($keydbConf, 'UTF-8', true) === false) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => [
|
||||
@@ -489,7 +486,7 @@ class DatabasesController extends Controller
|
||||
], 422);
|
||||
}
|
||||
$mariadbConf = base64_decode($request->mariadb_conf);
|
||||
if (mb_detect_encoding($mariadbConf, 'ASCII', true) === false) {
|
||||
if (mb_detect_encoding($mariadbConf, 'UTF-8', true) === false) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => [
|
||||
@@ -518,7 +515,7 @@ class DatabasesController extends Controller
|
||||
], 422);
|
||||
}
|
||||
$mongoConf = base64_decode($request->mongo_conf);
|
||||
if (mb_detect_encoding($mongoConf, 'ASCII', true) === false) {
|
||||
if (mb_detect_encoding($mongoConf, 'UTF-8', true) === false) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => [
|
||||
@@ -549,7 +546,7 @@ class DatabasesController extends Controller
|
||||
], 422);
|
||||
}
|
||||
$mysqlConf = base64_decode($request->mysql_conf);
|
||||
if (mb_detect_encoding($mysqlConf, 'ASCII', true) === false) {
|
||||
if (mb_detect_encoding($mysqlConf, 'UTF-8', true) === false) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => [
|
||||
@@ -614,7 +611,6 @@ class DatabasesController extends Controller
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
)
|
||||
),
|
||||
],
|
||||
@@ -832,7 +828,6 @@ class DatabasesController extends Controller
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
)
|
||||
),
|
||||
new OA\Parameter(
|
||||
@@ -842,7 +837,6 @@ class DatabasesController extends Controller
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
)
|
||||
),
|
||||
],
|
||||
@@ -1707,7 +1701,7 @@ class DatabasesController extends Controller
|
||||
], 422);
|
||||
}
|
||||
$postgresConf = base64_decode($request->postgres_conf);
|
||||
if (mb_detect_encoding($postgresConf, 'ASCII', true) === false) {
|
||||
if (mb_detect_encoding($postgresConf, 'UTF-8', true) === false) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => [
|
||||
@@ -1762,7 +1756,7 @@ class DatabasesController extends Controller
|
||||
], 422);
|
||||
}
|
||||
$mariadbConf = base64_decode($request->mariadb_conf);
|
||||
if (mb_detect_encoding($mariadbConf, 'ASCII', true) === false) {
|
||||
if (mb_detect_encoding($mariadbConf, 'UTF-8', true) === false) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => [
|
||||
@@ -1821,7 +1815,7 @@ class DatabasesController extends Controller
|
||||
], 422);
|
||||
}
|
||||
$mysqlConf = base64_decode($request->mysql_conf);
|
||||
if (mb_detect_encoding($mysqlConf, 'ASCII', true) === false) {
|
||||
if (mb_detect_encoding($mysqlConf, 'UTF-8', true) === false) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => [
|
||||
@@ -1877,7 +1871,7 @@ class DatabasesController extends Controller
|
||||
], 422);
|
||||
}
|
||||
$redisConf = base64_decode($request->redis_conf);
|
||||
if (mb_detect_encoding($redisConf, 'ASCII', true) === false) {
|
||||
if (mb_detect_encoding($redisConf, 'UTF-8', true) === false) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => [
|
||||
@@ -1963,7 +1957,7 @@ class DatabasesController extends Controller
|
||||
], 422);
|
||||
}
|
||||
$keydbConf = base64_decode($request->keydb_conf);
|
||||
if (mb_detect_encoding($keydbConf, 'ASCII', true) === false) {
|
||||
if (mb_detect_encoding($keydbConf, 'UTF-8', true) === false) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => [
|
||||
@@ -2057,7 +2051,7 @@ class DatabasesController extends Controller
|
||||
], 422);
|
||||
}
|
||||
$mongoConf = base64_decode($request->mongo_conf);
|
||||
if (mb_detect_encoding($mongoConf, 'ASCII', true) === false) {
|
||||
if (mb_detect_encoding($mongoConf, 'UTF-8', true) === false) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => [
|
||||
@@ -2104,7 +2098,6 @@ class DatabasesController extends Controller
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
)
|
||||
),
|
||||
new OA\Parameter(name: 'delete_configurations', in: 'query', required: false, description: 'Delete configurations.', schema: new OA\Schema(type: 'boolean', default: true)),
|
||||
@@ -2193,7 +2186,7 @@ class DatabasesController extends Controller
|
||||
in: 'path',
|
||||
required: true,
|
||||
description: 'UUID of the backup configuration to delete',
|
||||
schema: new OA\Schema(type: 'string', format: 'uuid')
|
||||
schema: new OA\Schema(type: 'string')
|
||||
),
|
||||
new OA\Parameter(
|
||||
name: 'delete_s3',
|
||||
@@ -2310,14 +2303,14 @@ class DatabasesController extends Controller
|
||||
in: 'path',
|
||||
required: true,
|
||||
description: 'UUID of the backup configuration',
|
||||
schema: new OA\Schema(type: 'string', format: 'uuid')
|
||||
schema: new OA\Schema(type: 'string')
|
||||
),
|
||||
new OA\Parameter(
|
||||
name: 'execution_uuid',
|
||||
in: 'path',
|
||||
required: true,
|
||||
description: 'UUID of the backup execution to delete',
|
||||
schema: new OA\Schema(type: 'string', format: 'uuid')
|
||||
schema: new OA\Schema(type: 'string')
|
||||
),
|
||||
new OA\Parameter(
|
||||
name: 'delete_s3',
|
||||
@@ -2430,7 +2423,7 @@ class DatabasesController extends Controller
|
||||
in: 'path',
|
||||
required: true,
|
||||
description: 'UUID of the backup configuration',
|
||||
schema: new OA\Schema(type: 'string', format: 'uuid')
|
||||
schema: new OA\Schema(type: 'string')
|
||||
),
|
||||
],
|
||||
responses: [
|
||||
@@ -2527,7 +2520,6 @@ class DatabasesController extends Controller
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
)
|
||||
),
|
||||
],
|
||||
@@ -2608,7 +2600,6 @@ class DatabasesController extends Controller
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
)
|
||||
),
|
||||
],
|
||||
@@ -2689,7 +2680,6 @@ class DatabasesController extends Controller
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
)
|
||||
),
|
||||
],
|
||||
|
||||
@@ -127,6 +127,10 @@ class DeployController extends Controller
|
||||
if (! $deployment) {
|
||||
return response()->json(['message' => 'Deployment not found.'], 404);
|
||||
}
|
||||
$application = $deployment->application;
|
||||
if (! $application || data_get($application->team(), 'id') !== $teamId) {
|
||||
return response()->json(['message' => 'Deployment not found.'], 404);
|
||||
}
|
||||
|
||||
return response()->json($this->removeSensitiveData($deployment));
|
||||
}
|
||||
@@ -538,7 +542,6 @@ class DeployController extends Controller
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
)
|
||||
),
|
||||
new OA\Parameter(
|
||||
|
||||
@@ -285,7 +285,6 @@ class ProjectController extends Controller
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
)
|
||||
),
|
||||
],
|
||||
@@ -406,7 +405,6 @@ class ProjectController extends Controller
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
)
|
||||
),
|
||||
],
|
||||
|
||||
@@ -0,0 +1,922 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Application;
|
||||
use App\Models\ScheduledTask;
|
||||
use App\Models\Service;
|
||||
use Illuminate\Http\Request;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
class ScheduledTasksController extends Controller
|
||||
{
|
||||
private function removeSensitiveData($task)
|
||||
{
|
||||
$task->makeHidden([
|
||||
'id',
|
||||
'team_id',
|
||||
'application_id',
|
||||
'service_id',
|
||||
]);
|
||||
|
||||
return serializeApiResponse($task);
|
||||
}
|
||||
|
||||
private function resolveApplication(Request $request, int $teamId): ?Application
|
||||
{
|
||||
return Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first();
|
||||
}
|
||||
|
||||
private function resolveService(Request $request, int $teamId): ?Service
|
||||
{
|
||||
return Service::whereRelation('environment.project.team', 'id', $teamId)->where('uuid', $request->uuid)->first();
|
||||
}
|
||||
|
||||
private function listTasks(Application|Service $resource): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
$this->authorize('view', $resource);
|
||||
|
||||
$tasks = $resource->scheduled_tasks->map(function ($task) {
|
||||
return $this->removeSensitiveData($task);
|
||||
});
|
||||
|
||||
return response()->json($tasks);
|
||||
}
|
||||
|
||||
private function createTask(Request $request, Application|Service $resource): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
$this->authorize('update', $resource);
|
||||
|
||||
$return = validateIncomingRequest($request);
|
||||
if ($return instanceof \Illuminate\Http\JsonResponse) {
|
||||
return $return;
|
||||
}
|
||||
|
||||
$allowedFields = ['name', 'command', 'frequency', 'container', 'timeout', 'enabled'];
|
||||
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'name' => 'required|string|max:255',
|
||||
'command' => 'required|string',
|
||||
'frequency' => 'required|string',
|
||||
'container' => 'string|nullable',
|
||||
'timeout' => 'integer|min:1',
|
||||
'enabled' => 'boolean',
|
||||
]);
|
||||
|
||||
$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);
|
||||
}
|
||||
|
||||
if (! validate_cron_expression($request->frequency)) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => ['frequency' => ['Invalid cron expression or frequency format.']],
|
||||
], 422);
|
||||
}
|
||||
|
||||
$teamId = getTeamIdFromToken();
|
||||
|
||||
$task = new ScheduledTask;
|
||||
$task->name = $request->name;
|
||||
$task->command = $request->command;
|
||||
$task->frequency = $request->frequency;
|
||||
$task->container = $request->container;
|
||||
$task->timeout = $request->has('timeout') ? $request->timeout : 300;
|
||||
$task->enabled = $request->has('enabled') ? $request->enabled : true;
|
||||
$task->team_id = $teamId;
|
||||
|
||||
if ($resource instanceof Application) {
|
||||
$task->application_id = $resource->id;
|
||||
} elseif ($resource instanceof Service) {
|
||||
$task->service_id = $resource->id;
|
||||
}
|
||||
|
||||
$task->save();
|
||||
|
||||
return response()->json($this->removeSensitiveData($task), 201);
|
||||
}
|
||||
|
||||
private function updateTask(Request $request, Application|Service $resource): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
$this->authorize('update', $resource);
|
||||
|
||||
$return = validateIncomingRequest($request);
|
||||
if ($return instanceof \Illuminate\Http\JsonResponse) {
|
||||
return $return;
|
||||
}
|
||||
|
||||
if ($request->all() === []) {
|
||||
return response()->json(['message' => 'At least one field must be provided.'], 422);
|
||||
}
|
||||
|
||||
$allowedFields = ['name', 'command', 'frequency', 'container', 'timeout', 'enabled'];
|
||||
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'name' => 'string|max:255',
|
||||
'command' => 'string',
|
||||
'frequency' => 'string',
|
||||
'container' => 'string|nullable',
|
||||
'timeout' => 'integer|min:1',
|
||||
'enabled' => 'boolean',
|
||||
]);
|
||||
|
||||
$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);
|
||||
}
|
||||
|
||||
if ($request->has('frequency') && ! validate_cron_expression($request->frequency)) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => ['frequency' => ['Invalid cron expression or frequency format.']],
|
||||
], 422);
|
||||
}
|
||||
|
||||
$task = $resource->scheduled_tasks()->where('uuid', $request->task_uuid)->first();
|
||||
if (! $task) {
|
||||
return response()->json(['message' => 'Scheduled task not found.'], 404);
|
||||
}
|
||||
|
||||
$task->update($request->only($allowedFields));
|
||||
|
||||
return response()->json($this->removeSensitiveData($task), 200);
|
||||
}
|
||||
|
||||
private function deleteTask(Request $request, Application|Service $resource): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
$this->authorize('update', $resource);
|
||||
|
||||
$deleted = $resource->scheduled_tasks()->where('uuid', $request->task_uuid)->delete();
|
||||
if (! $deleted) {
|
||||
return response()->json(['message' => 'Scheduled task not found.'], 404);
|
||||
}
|
||||
|
||||
return response()->json(['message' => 'Scheduled task deleted.']);
|
||||
}
|
||||
|
||||
private function getExecutions(Request $request, Application|Service $resource): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
$this->authorize('view', $resource);
|
||||
|
||||
$task = $resource->scheduled_tasks()->where('uuid', $request->task_uuid)->first();
|
||||
if (! $task) {
|
||||
return response()->json(['message' => 'Scheduled task not found.'], 404);
|
||||
}
|
||||
|
||||
$executions = $task->executions()->get()->map(function ($execution) {
|
||||
$execution->makeHidden(['id', 'scheduled_task_id']);
|
||||
|
||||
return serializeApiResponse($execution);
|
||||
});
|
||||
|
||||
return response()->json($executions);
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
summary: 'List Tasks',
|
||||
description: 'List all scheduled tasks for an application.',
|
||||
path: '/applications/{uuid}/scheduled-tasks',
|
||||
operationId: 'list-scheduled-tasks-by-application-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Scheduled Tasks'],
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the application.',
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
)
|
||||
),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Get all scheduled tasks for an application.',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'array',
|
||||
items: new OA\Items(ref: '#/components/schemas/ScheduledTask')
|
||||
)
|
||||
),
|
||||
]
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 404,
|
||||
ref: '#/components/responses/404',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function scheduled_tasks_by_application_uuid(Request $request): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
$application = $this->resolveApplication($request, $teamId);
|
||||
if (! $application) {
|
||||
return response()->json(['message' => 'Application not found.'], 404);
|
||||
}
|
||||
|
||||
return $this->listTasks($application);
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
summary: 'Create Task',
|
||||
description: 'Create a new scheduled task for an application.',
|
||||
path: '/applications/{uuid}/scheduled-tasks',
|
||||
operationId: 'create-scheduled-task-by-application-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Scheduled Tasks'],
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the application.',
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
)
|
||||
),
|
||||
],
|
||||
requestBody: new OA\RequestBody(
|
||||
description: 'Scheduled task data',
|
||||
required: true,
|
||||
content: new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
required: ['name', 'command', 'frequency'],
|
||||
properties: [
|
||||
'name' => ['type' => 'string', 'description' => 'The name of the scheduled task.'],
|
||||
'command' => ['type' => 'string', 'description' => 'The command to execute.'],
|
||||
'frequency' => ['type' => 'string', 'description' => 'The frequency of the scheduled task.'],
|
||||
'container' => ['type' => 'string', 'nullable' => true, 'description' => 'The container where the command should be executed.'],
|
||||
'timeout' => ['type' => 'integer', 'description' => 'The timeout of the scheduled task in seconds.', 'default' => 300],
|
||||
'enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if the scheduled task is enabled.', 'default' => true],
|
||||
],
|
||||
),
|
||||
)
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 201,
|
||||
description: 'Scheduled task created.',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(ref: '#/components/schemas/ScheduledTask')
|
||||
),
|
||||
]
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 404,
|
||||
ref: '#/components/responses/404',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 422,
|
||||
ref: '#/components/responses/422',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function create_scheduled_task_by_application_uuid(Request $request): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
$application = $this->resolveApplication($request, $teamId);
|
||||
if (! $application) {
|
||||
return response()->json(['message' => 'Application not found.'], 404);
|
||||
}
|
||||
|
||||
return $this->createTask($request, $application);
|
||||
}
|
||||
|
||||
#[OA\Patch(
|
||||
summary: 'Update Task',
|
||||
description: 'Update a scheduled task for an application.',
|
||||
path: '/applications/{uuid}/scheduled-tasks/{task_uuid}',
|
||||
operationId: 'update-scheduled-task-by-application-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Scheduled Tasks'],
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the application.',
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
)
|
||||
),
|
||||
new OA\Parameter(
|
||||
name: 'task_uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the scheduled task.',
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
)
|
||||
),
|
||||
],
|
||||
requestBody: new OA\RequestBody(
|
||||
description: 'Scheduled task data',
|
||||
required: true,
|
||||
content: new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'name' => ['type' => 'string', 'description' => 'The name of the scheduled task.'],
|
||||
'command' => ['type' => 'string', 'description' => 'The command to execute.'],
|
||||
'frequency' => ['type' => 'string', 'description' => 'The frequency of the scheduled task.'],
|
||||
'container' => ['type' => 'string', 'nullable' => true, 'description' => 'The container where the command should be executed.'],
|
||||
'timeout' => ['type' => 'integer', 'description' => 'The timeout of the scheduled task in seconds.', 'default' => 300],
|
||||
'enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if the scheduled task is enabled.', 'default' => true],
|
||||
],
|
||||
),
|
||||
)
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Scheduled task updated.',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(ref: '#/components/schemas/ScheduledTask')
|
||||
),
|
||||
]
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 404,
|
||||
ref: '#/components/responses/404',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 422,
|
||||
ref: '#/components/responses/422',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function update_scheduled_task_by_application_uuid(Request $request): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
$application = $this->resolveApplication($request, $teamId);
|
||||
if (! $application) {
|
||||
return response()->json(['message' => 'Application not found.'], 404);
|
||||
}
|
||||
|
||||
return $this->updateTask($request, $application);
|
||||
}
|
||||
|
||||
#[OA\Delete(
|
||||
summary: 'Delete Task',
|
||||
description: 'Delete a scheduled task for an application.',
|
||||
path: '/applications/{uuid}/scheduled-tasks/{task_uuid}',
|
||||
operationId: 'delete-scheduled-task-by-application-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Scheduled Tasks'],
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the application.',
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
)
|
||||
),
|
||||
new OA\Parameter(
|
||||
name: 'task_uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the scheduled task.',
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
)
|
||||
),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Scheduled task deleted.',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'message' => ['type' => 'string', 'example' => 'Scheduled task deleted.'],
|
||||
]
|
||||
)
|
||||
),
|
||||
]
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 404,
|
||||
ref: '#/components/responses/404',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function delete_scheduled_task_by_application_uuid(Request $request): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
$application = $this->resolveApplication($request, $teamId);
|
||||
if (! $application) {
|
||||
return response()->json(['message' => 'Application not found.'], 404);
|
||||
}
|
||||
|
||||
return $this->deleteTask($request, $application);
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
summary: 'List Executions',
|
||||
description: 'List all executions for a scheduled task on an application.',
|
||||
path: '/applications/{uuid}/scheduled-tasks/{task_uuid}/executions',
|
||||
operationId: 'list-scheduled-task-executions-by-application-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Scheduled Tasks'],
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the application.',
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
)
|
||||
),
|
||||
new OA\Parameter(
|
||||
name: 'task_uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the scheduled task.',
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
)
|
||||
),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Get all executions for a scheduled task.',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'array',
|
||||
items: new OA\Items(ref: '#/components/schemas/ScheduledTaskExecution')
|
||||
)
|
||||
),
|
||||
]
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 404,
|
||||
ref: '#/components/responses/404',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function executions_by_application_uuid(Request $request): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
$application = $this->resolveApplication($request, $teamId);
|
||||
if (! $application) {
|
||||
return response()->json(['message' => 'Application not found.'], 404);
|
||||
}
|
||||
|
||||
return $this->getExecutions($request, $application);
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
summary: 'List Tasks',
|
||||
description: 'List all scheduled tasks for a service.',
|
||||
path: '/services/{uuid}/scheduled-tasks',
|
||||
operationId: 'list-scheduled-tasks-by-service-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Scheduled Tasks'],
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the service.',
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
)
|
||||
),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Get all scheduled tasks for a service.',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'array',
|
||||
items: new OA\Items(ref: '#/components/schemas/ScheduledTask')
|
||||
)
|
||||
),
|
||||
]
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 404,
|
||||
ref: '#/components/responses/404',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function scheduled_tasks_by_service_uuid(Request $request): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
$service = $this->resolveService($request, $teamId);
|
||||
if (! $service) {
|
||||
return response()->json(['message' => 'Service not found.'], 404);
|
||||
}
|
||||
|
||||
return $this->listTasks($service);
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
summary: 'Create Task',
|
||||
description: 'Create a new scheduled task for a service.',
|
||||
path: '/services/{uuid}/scheduled-tasks',
|
||||
operationId: 'create-scheduled-task-by-service-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Scheduled Tasks'],
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the service.',
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
)
|
||||
),
|
||||
],
|
||||
requestBody: new OA\RequestBody(
|
||||
description: 'Scheduled task data',
|
||||
required: true,
|
||||
content: new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
required: ['name', 'command', 'frequency'],
|
||||
properties: [
|
||||
'name' => ['type' => 'string', 'description' => 'The name of the scheduled task.'],
|
||||
'command' => ['type' => 'string', 'description' => 'The command to execute.'],
|
||||
'frequency' => ['type' => 'string', 'description' => 'The frequency of the scheduled task.'],
|
||||
'container' => ['type' => 'string', 'nullable' => true, 'description' => 'The container where the command should be executed.'],
|
||||
'timeout' => ['type' => 'integer', 'description' => 'The timeout of the scheduled task in seconds.', 'default' => 300],
|
||||
'enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if the scheduled task is enabled.', 'default' => true],
|
||||
],
|
||||
),
|
||||
)
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 201,
|
||||
description: 'Scheduled task created.',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(ref: '#/components/schemas/ScheduledTask')
|
||||
),
|
||||
]
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 404,
|
||||
ref: '#/components/responses/404',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 422,
|
||||
ref: '#/components/responses/422',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function create_scheduled_task_by_service_uuid(Request $request): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
$service = $this->resolveService($request, $teamId);
|
||||
if (! $service) {
|
||||
return response()->json(['message' => 'Service not found.'], 404);
|
||||
}
|
||||
|
||||
return $this->createTask($request, $service);
|
||||
}
|
||||
|
||||
#[OA\Patch(
|
||||
summary: 'Update Task',
|
||||
description: 'Update a scheduled task for a service.',
|
||||
path: '/services/{uuid}/scheduled-tasks/{task_uuid}',
|
||||
operationId: 'update-scheduled-task-by-service-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Scheduled Tasks'],
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the service.',
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
)
|
||||
),
|
||||
new OA\Parameter(
|
||||
name: 'task_uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the scheduled task.',
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
)
|
||||
),
|
||||
],
|
||||
requestBody: new OA\RequestBody(
|
||||
description: 'Scheduled task data',
|
||||
required: true,
|
||||
content: new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'name' => ['type' => 'string', 'description' => 'The name of the scheduled task.'],
|
||||
'command' => ['type' => 'string', 'description' => 'The command to execute.'],
|
||||
'frequency' => ['type' => 'string', 'description' => 'The frequency of the scheduled task.'],
|
||||
'container' => ['type' => 'string', 'nullable' => true, 'description' => 'The container where the command should be executed.'],
|
||||
'timeout' => ['type' => 'integer', 'description' => 'The timeout of the scheduled task in seconds.', 'default' => 300],
|
||||
'enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if the scheduled task is enabled.', 'default' => true],
|
||||
],
|
||||
),
|
||||
)
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Scheduled task updated.',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(ref: '#/components/schemas/ScheduledTask')
|
||||
),
|
||||
]
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 404,
|
||||
ref: '#/components/responses/404',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 422,
|
||||
ref: '#/components/responses/422',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function update_scheduled_task_by_service_uuid(Request $request): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
$service = $this->resolveService($request, $teamId);
|
||||
if (! $service) {
|
||||
return response()->json(['message' => 'Service not found.'], 404);
|
||||
}
|
||||
|
||||
return $this->updateTask($request, $service);
|
||||
}
|
||||
|
||||
#[OA\Delete(
|
||||
summary: 'Delete Task',
|
||||
description: 'Delete a scheduled task for a service.',
|
||||
path: '/services/{uuid}/scheduled-tasks/{task_uuid}',
|
||||
operationId: 'delete-scheduled-task-by-service-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Scheduled Tasks'],
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the service.',
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
)
|
||||
),
|
||||
new OA\Parameter(
|
||||
name: 'task_uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the scheduled task.',
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
)
|
||||
),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Scheduled task deleted.',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'message' => ['type' => 'string', 'example' => 'Scheduled task deleted.'],
|
||||
]
|
||||
)
|
||||
),
|
||||
]
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 404,
|
||||
ref: '#/components/responses/404',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function delete_scheduled_task_by_service_uuid(Request $request): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
$service = $this->resolveService($request, $teamId);
|
||||
if (! $service) {
|
||||
return response()->json(['message' => 'Service not found.'], 404);
|
||||
}
|
||||
|
||||
return $this->deleteTask($request, $service);
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
summary: 'List Executions',
|
||||
description: 'List all executions for a scheduled task on a service.',
|
||||
path: '/services/{uuid}/scheduled-tasks/{task_uuid}/executions',
|
||||
operationId: 'list-scheduled-task-executions-by-service-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Scheduled Tasks'],
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the service.',
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
)
|
||||
),
|
||||
new OA\Parameter(
|
||||
name: 'task_uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the scheduled task.',
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
)
|
||||
),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Get all executions for a scheduled task.',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'array',
|
||||
items: new OA\Items(ref: '#/components/schemas/ScheduledTaskExecution')
|
||||
)
|
||||
),
|
||||
]
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 404,
|
||||
ref: '#/components/responses/404',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function executions_by_service_uuid(Request $request): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
$service = $this->resolveService($request, $teamId);
|
||||
if (! $service) {
|
||||
return response()->json(['message' => 'Service not found.'], 404);
|
||||
}
|
||||
|
||||
return $this->getExecutions($request, $service);
|
||||
}
|
||||
}
|
||||
@@ -290,9 +290,12 @@ class ServersController extends Controller
|
||||
}
|
||||
$uuid = $request->get('uuid');
|
||||
if ($uuid) {
|
||||
$domains = Application::getDomainsByUuid($uuid);
|
||||
$application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $uuid)->first();
|
||||
if (! $application) {
|
||||
return response()->json(['message' => 'Application not found.'], 404);
|
||||
}
|
||||
|
||||
return response()->json(serializeApiResponse($domains));
|
||||
return response()->json(serializeApiResponse($application->fqdns));
|
||||
}
|
||||
$projects = Project::where('team_id', $teamId)->get();
|
||||
$domains = collect();
|
||||
@@ -519,9 +522,13 @@ class ServersController extends Controller
|
||||
if (! $privateKey) {
|
||||
return response()->json(['message' => 'Private key not found.'], 404);
|
||||
}
|
||||
$allServers = ModelsServer::whereIp($request->ip)->get();
|
||||
if ($allServers->count() > 0) {
|
||||
return response()->json(['message' => 'Server with this IP already exists.'], 400);
|
||||
$foundServer = ModelsServer::whereIp($request->ip)->first();
|
||||
if ($foundServer) {
|
||||
if ($foundServer->team_id === $teamId) {
|
||||
return response()->json(['message' => 'A server with this IP/Domain already exists in your team.'], 400);
|
||||
}
|
||||
|
||||
return response()->json(['message' => 'A server with this IP/Domain is already in use by another team.'], 400);
|
||||
}
|
||||
|
||||
$proxyType = $request->proxy_type ? str($request->proxy_type)->upper() : ProxyTypes::TRAEFIK->value;
|
||||
@@ -699,7 +706,6 @@ class ServersController extends Controller
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
)
|
||||
),
|
||||
],
|
||||
|
||||
@@ -12,6 +12,7 @@ use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
@@ -37,6 +38,100 @@ class ServicesController extends Controller
|
||||
return serializeApiResponse($service);
|
||||
}
|
||||
|
||||
private function applyServiceUrls(Service $service, array $urlsArray, string $teamId, bool $forceDomainOverride = false): ?array
|
||||
{
|
||||
$errors = [];
|
||||
$conflicts = [];
|
||||
|
||||
$urls = collect($urlsArray)->flatMap(function ($item) {
|
||||
$urlValue = data_get($item, 'url');
|
||||
if (blank($urlValue)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return str($urlValue)->replaceStart(',', '')->replaceEnd(',', '')->trim()->explode(',')->map(fn ($url) => trim($url))->filter();
|
||||
});
|
||||
|
||||
$urls = $urls->map(function ($url) use (&$errors) {
|
||||
if (! filter_var($url, FILTER_VALIDATE_URL)) {
|
||||
$errors[] = "Invalid URL: {$url}";
|
||||
|
||||
return $url;
|
||||
}
|
||||
$scheme = parse_url($url, PHP_URL_SCHEME) ?? '';
|
||||
if (! in_array(strtolower($scheme), ['http', 'https'])) {
|
||||
$errors[] = "Invalid URL scheme: {$scheme} for URL: {$url}. Only http and https are supported.";
|
||||
}
|
||||
|
||||
return $url;
|
||||
});
|
||||
|
||||
$duplicates = $urls->duplicates()->unique()->values();
|
||||
if ($duplicates->isNotEmpty() && ! $forceDomainOverride) {
|
||||
$errors[] = 'The current request contains conflicting URLs across containers: '.implode(', ', $duplicates->toArray()).'. Use force_domain_override=true to proceed.';
|
||||
}
|
||||
|
||||
if (count($errors) > 0) {
|
||||
return ['errors' => $errors];
|
||||
}
|
||||
|
||||
collect($urlsArray)->each(function ($item) use ($service, $teamId, $forceDomainOverride, &$errors, &$conflicts) {
|
||||
$name = data_get($item, 'name');
|
||||
$containerUrls = data_get($item, 'url');
|
||||
|
||||
if (blank($name)) {
|
||||
$errors[] = 'Service container name is required to apply URLs.';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$application = $service->applications()->where('name', $name)->first();
|
||||
if (! $application) {
|
||||
$errors[] = "Service container with '{$name}' not found.";
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (filled($containerUrls)) {
|
||||
$containerUrls = str($containerUrls)->replaceStart(',', '')->replaceEnd(',', '')->trim();
|
||||
$containerUrls = str($containerUrls)->explode(',')->map(fn ($url) => str(trim($url))->lower());
|
||||
|
||||
$result = checkIfDomainIsAlreadyUsedViaAPI($containerUrls, $teamId, $application->uuid);
|
||||
if (isset($result['error'])) {
|
||||
$errors[] = $result['error'];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($result['hasConflicts'] && ! $forceDomainOverride) {
|
||||
$conflicts = array_merge($conflicts, $result['conflicts']);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$containerUrls = $containerUrls->filter(fn ($u) => filled($u))->unique()->implode(',');
|
||||
} else {
|
||||
$containerUrls = null;
|
||||
}
|
||||
|
||||
$application->fqdn = $containerUrls;
|
||||
$application->save();
|
||||
});
|
||||
|
||||
if (! empty($errors)) {
|
||||
return ['errors' => $errors];
|
||||
}
|
||||
|
||||
if (! empty($conflicts)) {
|
||||
return [
|
||||
'conflicts' => $conflicts,
|
||||
'warning' => 'Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior.',
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
summary: 'List',
|
||||
description: 'List all services.',
|
||||
@@ -105,98 +200,7 @@ class ServicesController extends Controller
|
||||
type: 'object',
|
||||
required: ['server_uuid', 'project_uuid', 'environment_name', 'environment_uuid'],
|
||||
properties: [
|
||||
'type' => [
|
||||
'description' => 'The one-click service type',
|
||||
'type' => 'string',
|
||||
'enum' => [
|
||||
'activepieces',
|
||||
'appsmith',
|
||||
'appwrite',
|
||||
'authentik',
|
||||
'babybuddy',
|
||||
'budge',
|
||||
'changedetection',
|
||||
'chatwoot',
|
||||
'classicpress-with-mariadb',
|
||||
'classicpress-with-mysql',
|
||||
'classicpress-without-database',
|
||||
'cloudflared',
|
||||
'code-server',
|
||||
'dashboard',
|
||||
'directus',
|
||||
'directus-with-postgresql',
|
||||
'docker-registry',
|
||||
'docuseal',
|
||||
'docuseal-with-postgres',
|
||||
'dokuwiki',
|
||||
'duplicati',
|
||||
'emby',
|
||||
'embystat',
|
||||
'fider',
|
||||
'filebrowser',
|
||||
'firefly',
|
||||
'formbricks',
|
||||
'ghost',
|
||||
'gitea',
|
||||
'gitea-with-mariadb',
|
||||
'gitea-with-mysql',
|
||||
'gitea-with-postgresql',
|
||||
'glance',
|
||||
'glances',
|
||||
'glitchtip',
|
||||
'grafana',
|
||||
'grafana-with-postgresql',
|
||||
'grocy',
|
||||
'heimdall',
|
||||
'homepage',
|
||||
'jellyfin',
|
||||
'kuzzle',
|
||||
'listmonk',
|
||||
'logto',
|
||||
'mediawiki',
|
||||
'meilisearch',
|
||||
'metabase',
|
||||
'metube',
|
||||
'minio',
|
||||
'moodle',
|
||||
'n8n',
|
||||
'n8n-with-postgresql',
|
||||
'next-image-transformation',
|
||||
'nextcloud',
|
||||
'nocodb',
|
||||
'odoo',
|
||||
'openblocks',
|
||||
'pairdrop',
|
||||
'penpot',
|
||||
'phpmyadmin',
|
||||
'pocketbase',
|
||||
'posthog',
|
||||
'reactive-resume',
|
||||
'rocketchat',
|
||||
'shlink',
|
||||
'slash',
|
||||
'snapdrop',
|
||||
'statusnook',
|
||||
'stirling-pdf',
|
||||
'supabase',
|
||||
'syncthing',
|
||||
'tolgee',
|
||||
'trigger',
|
||||
'trigger-with-external-database',
|
||||
'twenty',
|
||||
'umami',
|
||||
'unleash-with-postgresql',
|
||||
'unleash-without-database',
|
||||
'uptime-kuma',
|
||||
'vaultwarden',
|
||||
'vikunja',
|
||||
'weblate',
|
||||
'whoogle',
|
||||
'wordpress-with-mariadb',
|
||||
'wordpress-with-mysql',
|
||||
'wordpress-without-database',
|
||||
],
|
||||
],
|
||||
'type' => ['description' => 'The one-click service type (e.g. "actualbudget", "calibre-web", "gitea-with-mysql" ...)', 'type' => 'string'],
|
||||
'name' => ['type' => 'string', 'maxLength' => 255, 'description' => 'Name of the service.'],
|
||||
'description' => ['type' => 'string', 'nullable' => true, 'description' => 'Description of the service.'],
|
||||
'project_uuid' => ['type' => 'string', 'description' => 'Project UUID.'],
|
||||
@@ -205,7 +209,19 @@ class ServicesController extends Controller
|
||||
'server_uuid' => ['type' => 'string', 'description' => 'Server UUID.'],
|
||||
'destination_uuid' => ['type' => 'string', 'description' => 'Destination UUID. Required if server has multiple destinations.'],
|
||||
'instant_deploy' => ['type' => 'boolean', 'default' => false, 'description' => 'Start the service immediately after creation.'],
|
||||
'docker_compose_raw' => ['type' => 'string', 'description' => 'The Docker Compose raw content.'],
|
||||
'docker_compose_raw' => ['type' => 'string', 'description' => 'The base64 encoded Docker Compose content.'],
|
||||
'urls' => [
|
||||
'type' => 'array',
|
||||
'description' => 'Array of URLs to be applied to containers of a service.',
|
||||
'items' => new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'name' => ['type' => 'string', 'description' => 'The service name as defined in docker-compose.'],
|
||||
'url' => ['type' => 'string', 'description' => 'Comma-separated list of URLs (e.g. "http://app.coolify.io,https://app2.coolify.io").'],
|
||||
],
|
||||
),
|
||||
],
|
||||
'force_domain_override' => ['type' => 'boolean', 'default' => false, 'description' => 'Force domain override even if conflicts are detected.'],
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -235,6 +251,35 @@ class ServicesController extends Controller
|
||||
response: 400,
|
||||
ref: '#/components/responses/400',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 409,
|
||||
description: 'Domain conflicts detected.',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'message' => ['type' => 'string', 'example' => 'Domain conflicts detected. Use force_domain_override=true to proceed.'],
|
||||
'warning' => ['type' => 'string', 'example' => 'Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior.'],
|
||||
'conflicts' => [
|
||||
'type' => 'array',
|
||||
'items' => new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'domain' => ['type' => 'string', 'example' => 'example.com'],
|
||||
'resource_name' => ['type' => 'string', 'example' => 'My Application'],
|
||||
'resource_uuid' => ['type' => 'string', 'nullable' => true, 'example' => 'abc123-def456'],
|
||||
'resource_type' => ['type' => 'string', 'enum' => ['application', 'service', 'instance'], 'example' => 'application'],
|
||||
'message' => ['type' => 'string', 'example' => 'Domain example.com is already in use by application \'My Application\''],
|
||||
]
|
||||
),
|
||||
],
|
||||
]
|
||||
)
|
||||
),
|
||||
]
|
||||
),
|
||||
new OA\Response(
|
||||
response: 422,
|
||||
ref: '#/components/responses/422',
|
||||
@@ -243,7 +288,7 @@ class ServicesController extends Controller
|
||||
)]
|
||||
public function create_service(Request $request)
|
||||
{
|
||||
$allowedFields = ['type', 'name', 'description', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'docker_compose_raw'];
|
||||
$allowedFields = ['type', 'name', 'description', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'docker_compose_raw', 'urls', 'force_domain_override'];
|
||||
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
@@ -256,7 +301,7 @@ class ServicesController extends Controller
|
||||
if ($return instanceof \Illuminate\Http\JsonResponse) {
|
||||
return $return;
|
||||
}
|
||||
$validator = customApiValidator($request->all(), [
|
||||
$validationRules = [
|
||||
'type' => 'string|required_without:docker_compose_raw',
|
||||
'docker_compose_raw' => 'string|required_without:type',
|
||||
'project_uuid' => 'string|required',
|
||||
@@ -267,7 +312,16 @@ class ServicesController extends Controller
|
||||
'name' => 'string|max:255',
|
||||
'description' => 'string|nullable',
|
||||
'instant_deploy' => 'boolean',
|
||||
]);
|
||||
'urls' => 'array|nullable',
|
||||
'urls.*' => 'array:name,url',
|
||||
'urls.*.name' => 'string|required',
|
||||
'urls.*.url' => 'string|nullable',
|
||||
'force_domain_override' => 'boolean',
|
||||
];
|
||||
$validationMessages = [
|
||||
'urls.*.array' => 'An item in the urls array has invalid fields. Only name and url fields are supported.',
|
||||
];
|
||||
$validator = Validator::make($request->all(), $validationRules, $validationMessages);
|
||||
|
||||
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
|
||||
if ($validator->fails() || ! empty($extraFields)) {
|
||||
@@ -283,6 +337,13 @@ class ServicesController extends Controller
|
||||
'errors' => $errors,
|
||||
], 422);
|
||||
}
|
||||
|
||||
if (filled($request->type) && filled($request->docker_compose_raw)) {
|
||||
return response()->json([
|
||||
'message' => 'You cannot provide both service type and docker_compose_raw. Use one or the other.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
$environmentUuid = $request->environment_uuid;
|
||||
$environmentName = $request->environment_name;
|
||||
if (blank($environmentUuid) && blank($environmentName)) {
|
||||
@@ -355,7 +416,8 @@ class ServicesController extends Controller
|
||||
data_set($servicePayload, 'connect_to_docker_network', true);
|
||||
}
|
||||
$service = Service::create($servicePayload);
|
||||
$service->name = "$oneClickServiceName-".$service->uuid;
|
||||
$service->name = $request->name ?? "$oneClickServiceName-".$service->uuid;
|
||||
$service->description = $request->description;
|
||||
$service->save();
|
||||
if ($oneClickDotEnvs?->count() > 0) {
|
||||
$oneClickDotEnvs->each(function ($value) use ($service) {
|
||||
@@ -380,29 +442,41 @@ class ServicesController extends Controller
|
||||
// Apply service-specific application prerequisites
|
||||
applyServiceApplicationPrerequisites($service);
|
||||
|
||||
if ($request->has('urls') && is_array($request->urls)) {
|
||||
$urlResult = $this->applyServiceUrls($service, $request->urls, $teamId, $request->boolean('force_domain_override'));
|
||||
if ($urlResult !== null) {
|
||||
$service->delete();
|
||||
if (isset($urlResult['errors'])) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $urlResult['errors'],
|
||||
], 422);
|
||||
}
|
||||
if (isset($urlResult['conflicts'])) {
|
||||
return response()->json([
|
||||
'message' => 'Domain conflicts detected. Use force_domain_override=true to proceed.',
|
||||
'conflicts' => $urlResult['conflicts'],
|
||||
'warning' => $urlResult['warning'],
|
||||
], 409);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'uuid' => $service->uuid,
|
||||
'domains' => $domains,
|
||||
]);
|
||||
'domains' => $service->applications()->pluck('fqdn')->filter()->sort()->values(),
|
||||
])->setStatusCode(201);
|
||||
}
|
||||
|
||||
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'];
|
||||
$allowedFields = ['name', 'description', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'docker_compose_raw', 'connect_to_docker_network', 'urls', 'force_domain_override'];
|
||||
|
||||
$validator = customApiValidator($request->all(), [
|
||||
$validationRules = [
|
||||
'project_uuid' => 'string|required',
|
||||
'environment_name' => 'string|nullable',
|
||||
'environment_uuid' => 'string|nullable',
|
||||
@@ -413,7 +487,16 @@ class ServicesController extends Controller
|
||||
'instant_deploy' => 'boolean',
|
||||
'connect_to_docker_network' => 'boolean',
|
||||
'docker_compose_raw' => 'string|required',
|
||||
]);
|
||||
'urls' => 'array|nullable',
|
||||
'urls.*' => 'array:name,url',
|
||||
'urls.*.name' => 'string|required',
|
||||
'urls.*.url' => 'string|nullable',
|
||||
'force_domain_override' => 'boolean',
|
||||
];
|
||||
$validationMessages = [
|
||||
'urls.*.array' => 'An item in the urls array has invalid fields. Only name and url fields are supported.',
|
||||
];
|
||||
$validator = Validator::make($request->all(), $validationRules, $validationMessages);
|
||||
|
||||
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
|
||||
if ($validator->fails() || ! empty($extraFields)) {
|
||||
@@ -469,7 +552,7 @@ class ServicesController extends Controller
|
||||
], 422);
|
||||
}
|
||||
$dockerComposeRaw = base64_decode($request->docker_compose_raw);
|
||||
if (mb_detect_encoding($dockerComposeRaw, 'ASCII', true) === false) {
|
||||
if (mb_detect_encoding($dockerComposeRaw, 'UTF-8', true) === false) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => [
|
||||
@@ -507,25 +590,40 @@ class ServicesController extends Controller
|
||||
$service->save();
|
||||
|
||||
$service->parse(isNew: true);
|
||||
|
||||
if ($request->has('urls') && is_array($request->urls)) {
|
||||
$urlResult = $this->applyServiceUrls($service, $request->urls, $teamId, $request->boolean('force_domain_override'));
|
||||
if ($urlResult !== null) {
|
||||
$service->delete();
|
||||
if (isset($urlResult['errors'])) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $urlResult['errors'],
|
||||
], 422);
|
||||
}
|
||||
if (isset($urlResult['conflicts'])) {
|
||||
return response()->json([
|
||||
'message' => 'Domain conflicts detected. Use force_domain_override=true to proceed.',
|
||||
'conflicts' => $urlResult['conflicts'],
|
||||
'warning' => $urlResult['warning'],
|
||||
], 409);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
'domains' => $service->applications()->pluck('fqdn')->filter()->sort()->values(),
|
||||
])->setStatusCode(201);
|
||||
} else {
|
||||
return response()->json(['message' => 'No service type or docker_compose_raw provided.'], 400);
|
||||
} elseif (filled($request->type)) {
|
||||
return response()->json([
|
||||
'message' => 'Invalid service type.',
|
||||
'valid_service_types' => $serviceKeys,
|
||||
], 404);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -681,7 +779,6 @@ class ServicesController extends Controller
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
)
|
||||
),
|
||||
],
|
||||
@@ -703,7 +800,19 @@ class ServicesController extends Controller
|
||||
'destination_uuid' => ['type' => 'string', 'description' => 'The destination UUID.'],
|
||||
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the service should be deployed instantly.'],
|
||||
'connect_to_docker_network' => ['type' => 'boolean', 'default' => false, 'description' => 'Connect the service to the predefined docker network.'],
|
||||
'docker_compose_raw' => ['type' => 'string', 'description' => 'The Docker Compose raw content.'],
|
||||
'docker_compose_raw' => ['type' => 'string', 'description' => 'The base64 encoded Docker Compose content.'],
|
||||
'urls' => [
|
||||
'type' => 'array',
|
||||
'description' => 'Array of URLs to be applied to containers of a service.',
|
||||
'items' => new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'name' => ['type' => 'string', 'description' => 'The service name as defined in docker-compose.'],
|
||||
'url' => ['type' => 'string', 'description' => 'Comma-separated list of URLs (e.g. "http://app.coolify.io,https://app2.coolify.io").'],
|
||||
],
|
||||
),
|
||||
],
|
||||
'force_domain_override' => ['type' => 'boolean', 'default' => false, 'description' => 'Force domain override even if conflicts are detected.'],
|
||||
],
|
||||
)
|
||||
),
|
||||
@@ -738,6 +847,35 @@ class ServicesController extends Controller
|
||||
response: 404,
|
||||
ref: '#/components/responses/404',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 409,
|
||||
description: 'Domain conflicts detected.',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'message' => ['type' => 'string', 'example' => 'Domain conflicts detected. Use force_domain_override=true to proceed.'],
|
||||
'warning' => ['type' => 'string', 'example' => 'Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior.'],
|
||||
'conflicts' => [
|
||||
'type' => 'array',
|
||||
'items' => new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'domain' => ['type' => 'string', 'example' => 'example.com'],
|
||||
'resource_name' => ['type' => 'string', 'example' => 'My Application'],
|
||||
'resource_uuid' => ['type' => 'string', 'nullable' => true, 'example' => 'abc123-def456'],
|
||||
'resource_type' => ['type' => 'string', 'enum' => ['application', 'service', 'instance'], 'example' => 'application'],
|
||||
'message' => ['type' => 'string', 'example' => 'Domain example.com is already in use by application \'My Application\''],
|
||||
]
|
||||
),
|
||||
],
|
||||
]
|
||||
)
|
||||
),
|
||||
]
|
||||
),
|
||||
new OA\Response(
|
||||
response: 422,
|
||||
ref: '#/components/responses/422',
|
||||
@@ -763,15 +901,24 @@ class ServicesController extends Controller
|
||||
|
||||
$this->authorize('update', $service);
|
||||
|
||||
$allowedFields = ['name', 'description', 'instant_deploy', 'docker_compose_raw', 'connect_to_docker_network'];
|
||||
$allowedFields = ['name', 'description', 'instant_deploy', 'docker_compose_raw', 'connect_to_docker_network', 'urls', 'force_domain_override'];
|
||||
|
||||
$validator = customApiValidator($request->all(), [
|
||||
$validationRules = [
|
||||
'name' => 'string|max:255',
|
||||
'description' => 'string|nullable',
|
||||
'instant_deploy' => 'boolean',
|
||||
'connect_to_docker_network' => 'boolean',
|
||||
'docker_compose_raw' => 'string|nullable',
|
||||
]);
|
||||
'urls' => 'array|nullable',
|
||||
'urls.*' => 'array:name,url',
|
||||
'urls.*.name' => 'string|required',
|
||||
'urls.*.url' => 'string|nullable',
|
||||
'force_domain_override' => 'boolean',
|
||||
];
|
||||
$validationMessages = [
|
||||
'urls.*.array' => 'An item in the urls array has invalid fields. Only name and url fields are supported.',
|
||||
];
|
||||
$validator = Validator::make($request->all(), $validationRules, $validationMessages);
|
||||
|
||||
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
|
||||
if ($validator->fails() || ! empty($extraFields)) {
|
||||
@@ -797,7 +944,7 @@ class ServicesController extends Controller
|
||||
], 422);
|
||||
}
|
||||
$dockerComposeRaw = base64_decode($request->docker_compose_raw);
|
||||
if (mb_detect_encoding($dockerComposeRaw, 'ASCII', true) === false) {
|
||||
if (mb_detect_encoding($dockerComposeRaw, 'UTF-8', true) === false) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => [
|
||||
@@ -835,22 +982,33 @@ class ServicesController extends Controller
|
||||
$service->save();
|
||||
|
||||
$service->parse();
|
||||
|
||||
if ($request->has('urls') && is_array($request->urls)) {
|
||||
$urlResult = $this->applyServiceUrls($service, $request->urls, $teamId, $request->boolean('force_domain_override'));
|
||||
if ($urlResult !== null) {
|
||||
if (isset($urlResult['errors'])) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $urlResult['errors'],
|
||||
], 422);
|
||||
}
|
||||
if (isset($urlResult['conflicts'])) {
|
||||
return response()->json([
|
||||
'message' => 'Domain conflicts detected. Use force_domain_override=true to proceed.',
|
||||
'conflicts' => $urlResult['conflicts'],
|
||||
'warning' => $urlResult['warning'],
|
||||
], 409);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($request->instant_deploy) {
|
||||
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,
|
||||
'domains' => $service->applications()->pluck('fqdn')->filter()->sort()->values(),
|
||||
])->setStatusCode(200);
|
||||
}
|
||||
|
||||
@@ -871,7 +1029,6 @@ class ServicesController extends Controller
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
)
|
||||
),
|
||||
],
|
||||
@@ -952,7 +1109,6 @@ class ServicesController extends Controller
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
)
|
||||
),
|
||||
],
|
||||
@@ -985,10 +1141,7 @@ class ServicesController extends Controller
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'message' => ['type' => 'string', 'example' => 'Environment variable updated.'],
|
||||
]
|
||||
ref: '#/components/schemas/EnvironmentVariable'
|
||||
)
|
||||
),
|
||||
]
|
||||
@@ -1082,7 +1235,6 @@ class ServicesController extends Controller
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
)
|
||||
),
|
||||
],
|
||||
@@ -1123,10 +1275,8 @@ class ServicesController extends Controller
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'message' => ['type' => 'string', 'example' => 'Environment variables updated.'],
|
||||
]
|
||||
type: 'array',
|
||||
items: new OA\Items(ref: '#/components/schemas/EnvironmentVariable')
|
||||
)
|
||||
),
|
||||
]
|
||||
@@ -1213,7 +1363,6 @@ class ServicesController extends Controller
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
)
|
||||
),
|
||||
],
|
||||
@@ -1336,7 +1485,6 @@ class ServicesController extends Controller
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
)
|
||||
),
|
||||
new OA\Parameter(
|
||||
@@ -1346,7 +1494,6 @@ class ServicesController extends Controller
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
)
|
||||
),
|
||||
],
|
||||
@@ -1425,7 +1572,6 @@ class ServicesController extends Controller
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
)
|
||||
),
|
||||
],
|
||||
@@ -1506,7 +1652,6 @@ class ServicesController extends Controller
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
)
|
||||
),
|
||||
],
|
||||
@@ -1587,7 +1732,6 @@ class ServicesController extends Controller
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
)
|
||||
),
|
||||
new OA\Parameter(
|
||||
|
||||
@@ -218,7 +218,10 @@ class TeamController extends Controller
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
$team = auth()->user()->currentTeam();
|
||||
$team = auth()->user()->teams->where('id', $teamId)->first();
|
||||
if (is_null($team)) {
|
||||
return response()->json(['message' => 'Team not found.'], 404);
|
||||
}
|
||||
|
||||
return response()->json(
|
||||
$this->removeSensitiveData($team),
|
||||
@@ -263,7 +266,10 @@ class TeamController extends Controller
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
$team = auth()->user()->currentTeam();
|
||||
$team = auth()->user()->teams->where('id', $teamId)->first();
|
||||
if (is_null($team)) {
|
||||
return response()->json(['message' => 'Team not found.'], 404);
|
||||
}
|
||||
$team->members->makeHidden([
|
||||
'pivot',
|
||||
'email_change_code',
|
||||
|
||||
@@ -13,7 +13,8 @@ class UploadController extends BaseController
|
||||
{
|
||||
public function upload(Request $request)
|
||||
{
|
||||
$resource = getResourceByUuid(request()->route('databaseUuid'), data_get(auth()->user()->currentTeam(), 'id'));
|
||||
$databaseIdentifier = request()->route('databaseUuid');
|
||||
$resource = getResourceByUuid($databaseIdentifier, data_get(auth()->user()->currentTeam(), 'id'));
|
||||
if (is_null($resource)) {
|
||||
return response()->json(['error' => 'You do not have permission for this database'], 500);
|
||||
}
|
||||
@@ -26,7 +27,10 @@ class UploadController extends BaseController
|
||||
$save = $receiver->receive();
|
||||
|
||||
if ($save->isFinished()) {
|
||||
return $this->saveFile($save->getFile(), $resource);
|
||||
// Use the original identifier from the route to maintain path consistency
|
||||
// For ServiceDatabase: {name}-{service_uuid}
|
||||
// For standalone databases: {uuid}
|
||||
return $this->saveFile($save->getFile(), $databaseIdentifier);
|
||||
}
|
||||
|
||||
$handler = $save->handler();
|
||||
@@ -57,10 +61,10 @@ class UploadController extends BaseController
|
||||
// 'mime_type' => $mime
|
||||
// ]);
|
||||
// }
|
||||
protected function saveFile(UploadedFile $file, $resource)
|
||||
protected function saveFile(UploadedFile $file, string $resourceIdentifier)
|
||||
{
|
||||
$mime = str_replace('/', '-', $file->getMimeType());
|
||||
$filePath = "upload/{$resource->uuid}";
|
||||
$filePath = "upload/{$resourceIdentifier}";
|
||||
$finalPath = storage_path('app/'.$filePath);
|
||||
$file->move($finalPath, 'restore');
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ class Gitea extends Controller
|
||||
if ($x_gitea_event === 'push') {
|
||||
if ($application->isDeployable()) {
|
||||
$is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files);
|
||||
if ($is_watch_path_triggered || is_null($application->watch_paths)) {
|
||||
if ($is_watch_path_triggered || blank($application->watch_paths)) {
|
||||
$deployment_uuid = new Cuid2;
|
||||
$result = queue_application_deployment(
|
||||
application: $application,
|
||||
|
||||
@@ -2,13 +2,10 @@
|
||||
|
||||
namespace App\Http\Controllers\Webhook;
|
||||
|
||||
use App\Actions\Application\CleanupPreviewDeployment;
|
||||
use App\Enums\ProcessStatus;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Jobs\ApplicationPullRequestUpdateJob;
|
||||
use App\Jobs\GithubAppPermissionJob;
|
||||
use App\Jobs\ProcessGithubPullRequestWebhook;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationPreview;
|
||||
use App\Models\GithubApp;
|
||||
use App\Models\PrivateKey;
|
||||
use Exception;
|
||||
@@ -54,6 +51,8 @@ class Github extends Controller
|
||||
$pull_request_html_url = data_get($payload, 'pull_request.html_url');
|
||||
$branch = data_get($payload, 'pull_request.head.ref');
|
||||
$base_branch = data_get($payload, 'pull_request.base.ref');
|
||||
$before_sha = data_get($payload, 'before');
|
||||
$after_sha = data_get($payload, 'after', data_get($payload, 'pull_request.head.sha'));
|
||||
$author_association = data_get($payload, 'pull_request.author_association');
|
||||
}
|
||||
if (! $branch) {
|
||||
@@ -69,7 +68,7 @@ class Github extends Controller
|
||||
if ($x_github_event === 'pull_request') {
|
||||
$applications = $applications->where('git_branch', $base_branch)->get();
|
||||
if ($applications->isEmpty()) {
|
||||
return response("Nothing to do. No applications found with branch '$base_branch'.");
|
||||
return response("Nothing to do. No applications found for repo $full_name and branch '$base_branch'.");
|
||||
}
|
||||
}
|
||||
$applicationsByServer = $applications->groupBy(function ($app) {
|
||||
@@ -102,7 +101,7 @@ class Github extends Controller
|
||||
if ($x_github_event === 'push') {
|
||||
if ($application->isDeployable()) {
|
||||
$is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files);
|
||||
if ($is_watch_path_triggered || is_null($application->watch_paths)) {
|
||||
if ($is_watch_path_triggered || blank($application->watch_paths)) {
|
||||
$deployment_uuid = new Cuid2;
|
||||
$result = queue_application_deployment(
|
||||
application: $application,
|
||||
@@ -152,96 +151,35 @@ class Github extends Controller
|
||||
}
|
||||
}
|
||||
if ($x_github_event === 'pull_request') {
|
||||
if ($action === 'opened' || $action === 'synchronize' || $action === 'reopened') {
|
||||
if ($application->isPRDeployable()) {
|
||||
// Check if PR deployments from public contributors are restricted
|
||||
if (! $application->settings->is_pr_deployments_public_enabled) {
|
||||
$trustedAssociations = ['OWNER', 'MEMBER', 'COLLABORATOR', 'CONTRIBUTOR'];
|
||||
if (! in_array($author_association, $trustedAssociations)) {
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'failed',
|
||||
'message' => 'PR deployments are restricted to repository members and contributors. Author association: '.$author_association,
|
||||
]);
|
||||
// Check if PR deployments are enabled (but allow 'closed' action to cleanup)
|
||||
if (! $application->isPRDeployable() && $action !== 'closed') {
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'failed',
|
||||
'message' => 'Preview deployments disabled.',
|
||||
]);
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
$deployment_uuid = new Cuid2;
|
||||
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
|
||||
if (! $found) {
|
||||
if ($application->build_pack === 'dockercompose') {
|
||||
$pr_app = ApplicationPreview::create([
|
||||
'git_type' => 'github',
|
||||
'application_id' => $application->id,
|
||||
'pull_request_id' => $pull_request_id,
|
||||
'pull_request_html_url' => $pull_request_html_url,
|
||||
'docker_compose_domains' => $application->docker_compose_domains,
|
||||
]);
|
||||
$pr_app->generate_preview_fqdn_compose();
|
||||
} else {
|
||||
$pr_app = ApplicationPreview::create([
|
||||
'git_type' => 'github',
|
||||
'application_id' => $application->id,
|
||||
'pull_request_id' => $pull_request_id,
|
||||
'pull_request_html_url' => $pull_request_html_url,
|
||||
]);
|
||||
$pr_app->generate_preview_fqdn();
|
||||
}
|
||||
}
|
||||
|
||||
$result = queue_application_deployment(
|
||||
application: $application,
|
||||
pull_request_id: $pull_request_id,
|
||||
deployment_uuid: $deployment_uuid,
|
||||
force_rebuild: false,
|
||||
commit: data_get($payload, 'head.sha', 'HEAD'),
|
||||
is_webhook: true,
|
||||
git_type: 'github'
|
||||
);
|
||||
if ($result['status'] === 'queue_full') {
|
||||
return response($result['message'], 429)->header('Retry-After', 60);
|
||||
} elseif ($result['status'] === 'skipped') {
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'skipped',
|
||||
'message' => $result['message'],
|
||||
]);
|
||||
} else {
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'success',
|
||||
'message' => 'Preview deployment queued.',
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'failed',
|
||||
'message' => 'Preview deployments disabled.',
|
||||
]);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if ($action === 'closed') {
|
||||
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
|
||||
if ($found) {
|
||||
// Use comprehensive cleanup that cancels active deployments,
|
||||
// kills helper containers, and removes all PR containers
|
||||
CleanupPreviewDeployment::run($application, $pull_request_id, $found);
|
||||
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'success',
|
||||
'message' => 'Preview deployment closed.',
|
||||
]);
|
||||
} else {
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'failed',
|
||||
'message' => 'No preview deployment found.',
|
||||
]);
|
||||
}
|
||||
}
|
||||
ProcessGithubPullRequestWebhook::dispatch(
|
||||
applicationId: $application->id,
|
||||
githubAppId: null,
|
||||
action: $action,
|
||||
pullRequestId: $pull_request_id,
|
||||
pullRequestHtmlUrl: $pull_request_html_url,
|
||||
beforeSha: $before_sha,
|
||||
afterSha: $after_sha,
|
||||
commitSha: data_get($payload, 'pull_request.head.sha', 'HEAD'),
|
||||
authorAssociation: $author_association,
|
||||
fullName: $full_name,
|
||||
);
|
||||
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'queued',
|
||||
'message' => 'PR webhook received, processing queued.',
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -304,6 +242,8 @@ class Github extends Controller
|
||||
$pull_request_html_url = data_get($payload, 'pull_request.html_url');
|
||||
$branch = data_get($payload, 'pull_request.head.ref');
|
||||
$base_branch = data_get($payload, 'pull_request.base.ref');
|
||||
$before_sha = data_get($payload, 'before');
|
||||
$after_sha = data_get($payload, 'after', data_get($payload, 'pull_request.head.sha'));
|
||||
$author_association = data_get($payload, 'pull_request.author_association');
|
||||
}
|
||||
if (! $id || ! $branch) {
|
||||
@@ -344,7 +284,7 @@ class Github extends Controller
|
||||
if ($x_github_event === 'push') {
|
||||
if ($application->isDeployable()) {
|
||||
$is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files);
|
||||
if ($is_watch_path_triggered || is_null($application->watch_paths)) {
|
||||
if ($is_watch_path_triggered || blank($application->watch_paths)) {
|
||||
$deployment_uuid = new Cuid2;
|
||||
$result = queue_application_deployment(
|
||||
application: $application,
|
||||
@@ -386,86 +326,37 @@ class Github extends Controller
|
||||
}
|
||||
}
|
||||
if ($x_github_event === 'pull_request') {
|
||||
if ($action === 'opened' || $action === 'synchronize' || $action === 'reopened') {
|
||||
if ($application->isPRDeployable()) {
|
||||
// Check if PR deployments from public contributors are restricted
|
||||
if (! $application->settings->is_pr_deployments_public_enabled) {
|
||||
$trustedAssociations = ['OWNER', 'MEMBER', 'COLLABORATOR', 'CONTRIBUTOR'];
|
||||
if (! in_array($author_association, $trustedAssociations)) {
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'failed',
|
||||
'message' => 'PR deployments are restricted to repository members and contributors. Author association: '.$author_association,
|
||||
]);
|
||||
// Check if PR deployments are enabled (but allow 'closed' action to cleanup)
|
||||
if (! $application->isPRDeployable() && $action !== 'closed') {
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'failed',
|
||||
'message' => 'Preview deployments disabled.',
|
||||
]);
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
$deployment_uuid = new Cuid2;
|
||||
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
|
||||
if (! $found) {
|
||||
ApplicationPreview::create([
|
||||
'git_type' => 'github',
|
||||
'application_id' => $application->id,
|
||||
'pull_request_id' => $pull_request_id,
|
||||
'pull_request_html_url' => $pull_request_html_url,
|
||||
]);
|
||||
}
|
||||
$result = queue_application_deployment(
|
||||
application: $application,
|
||||
pull_request_id: $pull_request_id,
|
||||
deployment_uuid: $deployment_uuid,
|
||||
force_rebuild: false,
|
||||
commit: data_get($payload, 'head.sha', 'HEAD'),
|
||||
is_webhook: true,
|
||||
git_type: 'github'
|
||||
);
|
||||
if ($result['status'] === 'queue_full') {
|
||||
return response($result['message'], 429)->header('Retry-After', 60);
|
||||
} elseif ($result['status'] === 'skipped') {
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'skipped',
|
||||
'message' => $result['message'],
|
||||
]);
|
||||
} else {
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'success',
|
||||
'message' => 'Preview deployment queued.',
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'failed',
|
||||
'message' => 'Preview deployments disabled.',
|
||||
]);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if ($action === 'closed' || $action === 'close') {
|
||||
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
|
||||
if ($found) {
|
||||
// Delete the PR comment on GitHub (GitHub-specific feature)
|
||||
ApplicationPullRequestUpdateJob::dispatchSync(application: $application, preview: $found, status: ProcessStatus::CLOSED);
|
||||
|
||||
// Use comprehensive cleanup that cancels active deployments,
|
||||
// kills helper containers, and removes all PR containers
|
||||
CleanupPreviewDeployment::run($application, $pull_request_id, $found);
|
||||
$full_name = data_get($payload, 'repository.full_name');
|
||||
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'success',
|
||||
'message' => 'Preview deployment closed.',
|
||||
]);
|
||||
} else {
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'failed',
|
||||
'message' => 'No preview deployment found.',
|
||||
]);
|
||||
}
|
||||
}
|
||||
ProcessGithubPullRequestWebhook::dispatch(
|
||||
applicationId: $application->id,
|
||||
githubAppId: $github_app->id,
|
||||
action: $action,
|
||||
pullRequestId: $pull_request_id,
|
||||
pullRequestHtmlUrl: $pull_request_html_url,
|
||||
beforeSha: $before_sha,
|
||||
afterSha: $after_sha,
|
||||
commitSha: data_get($payload, 'pull_request.head.sha', 'HEAD'),
|
||||
authorAssociation: $author_association,
|
||||
fullName: $full_name,
|
||||
);
|
||||
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'queued',
|
||||
'message' => 'PR webhook received, processing queued.',
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ class Gitlab extends Controller
|
||||
}
|
||||
foreach ($applications as $application) {
|
||||
$webhook_secret = data_get($application, 'manual_webhook_secret_gitlab');
|
||||
if ($webhook_secret !== $x_gitlab_token) {
|
||||
if (! hash_equals($webhook_secret ?? '', $x_gitlab_token ?? '')) {
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'failed',
|
||||
@@ -122,7 +122,7 @@ class Gitlab extends Controller
|
||||
if ($x_gitlab_event === 'push') {
|
||||
if ($application->isDeployable()) {
|
||||
$is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files);
|
||||
if ($is_watch_path_triggered || is_null($application->watch_paths)) {
|
||||
if ($is_watch_path_triggered || blank($application->watch_paths)) {
|
||||
$deployment_uuid = new Cuid2;
|
||||
$result = queue_application_deployment(
|
||||
application: $application,
|
||||
|
||||
Reference in new issue
Block a user