mirror of
https://github.com/tiennm99/coolify.git
synced 2026-04-17 17:21:04 +00:00
feat(api): add url update support to services api (#7929)
This commit is contained in:
@@ -12,6 +12,7 @@ use App\Models\Project;
|
|||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
use App\Models\Service;
|
use App\Models\Service;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
use OpenApi\Attributes as OA;
|
use OpenApi\Attributes as OA;
|
||||||
use Symfony\Component\Yaml\Yaml;
|
use Symfony\Component\Yaml\Yaml;
|
||||||
|
|
||||||
@@ -37,6 +38,100 @@ class ServicesController extends Controller
|
|||||||
return serializeApiResponse($service);
|
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(
|
#[OA\Get(
|
||||||
summary: 'List',
|
summary: 'List',
|
||||||
description: 'List all services.',
|
description: 'List all services.',
|
||||||
@@ -115,6 +210,18 @@ class ServicesController extends Controller
|
|||||||
'destination_uuid' => ['type' => 'string', 'description' => 'Destination UUID. Required if server has multiple destinations.'],
|
'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.'],
|
'instant_deploy' => ['type' => 'boolean', 'default' => false, 'description' => 'Start the service immediately after creation.'],
|
||||||
'docker_compose_raw' => ['type' => 'string', 'description' => 'The base64 encoded Docker Compose 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.'],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -144,6 +251,35 @@ class ServicesController extends Controller
|
|||||||
response: 400,
|
response: 400,
|
||||||
ref: '#/components/responses/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(
|
new OA\Response(
|
||||||
response: 422,
|
response: 422,
|
||||||
ref: '#/components/responses/422',
|
ref: '#/components/responses/422',
|
||||||
@@ -152,7 +288,7 @@ class ServicesController extends Controller
|
|||||||
)]
|
)]
|
||||||
public function create_service(Request $request)
|
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();
|
$teamId = getTeamIdFromToken();
|
||||||
if (is_null($teamId)) {
|
if (is_null($teamId)) {
|
||||||
@@ -165,7 +301,7 @@ class ServicesController extends Controller
|
|||||||
if ($return instanceof \Illuminate\Http\JsonResponse) {
|
if ($return instanceof \Illuminate\Http\JsonResponse) {
|
||||||
return $return;
|
return $return;
|
||||||
}
|
}
|
||||||
$validator = customApiValidator($request->all(), [
|
$validationRules = [
|
||||||
'type' => 'string|required_without:docker_compose_raw',
|
'type' => 'string|required_without:docker_compose_raw',
|
||||||
'docker_compose_raw' => 'string|required_without:type',
|
'docker_compose_raw' => 'string|required_without:type',
|
||||||
'project_uuid' => 'string|required',
|
'project_uuid' => 'string|required',
|
||||||
@@ -176,7 +312,16 @@ class ServicesController extends Controller
|
|||||||
'name' => 'string|max:255',
|
'name' => 'string|max:255',
|
||||||
'description' => 'string|nullable',
|
'description' => 'string|nullable',
|
||||||
'instant_deploy' => 'boolean',
|
'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);
|
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
|
||||||
if ($validator->fails() || ! empty($extraFields)) {
|
if ($validator->fails() || ! empty($extraFields)) {
|
||||||
@@ -297,29 +442,41 @@ class ServicesController extends Controller
|
|||||||
// Apply service-specific application prerequisites
|
// Apply service-specific application prerequisites
|
||||||
applyServiceApplicationPrerequisites($service);
|
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) {
|
if ($instantDeploy) {
|
||||||
StartService::dispatch($service);
|
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([
|
return response()->json([
|
||||||
'uuid' => $service->uuid,
|
'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);
|
return response()->json(['message' => 'Service not found.', 'valid_service_types' => $serviceKeys], 404);
|
||||||
} elseif (filled($request->docker_compose_raw)) {
|
} 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',
|
'project_uuid' => 'string|required',
|
||||||
'environment_name' => 'string|nullable',
|
'environment_name' => 'string|nullable',
|
||||||
'environment_uuid' => 'string|nullable',
|
'environment_uuid' => 'string|nullable',
|
||||||
@@ -330,7 +487,16 @@ class ServicesController extends Controller
|
|||||||
'instant_deploy' => 'boolean',
|
'instant_deploy' => 'boolean',
|
||||||
'connect_to_docker_network' => 'boolean',
|
'connect_to_docker_network' => 'boolean',
|
||||||
'docker_compose_raw' => 'string|required',
|
'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);
|
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
|
||||||
if ($validator->fails() || ! empty($extraFields)) {
|
if ($validator->fails() || ! empty($extraFields)) {
|
||||||
@@ -424,22 +590,34 @@ class ServicesController extends Controller
|
|||||||
$service->save();
|
$service->save();
|
||||||
|
|
||||||
$service->parse(isNew: true);
|
$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) {
|
if ($instantDeploy) {
|
||||||
StartService::dispatch($service);
|
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([
|
return response()->json([
|
||||||
'uuid' => $service->uuid,
|
'uuid' => $service->uuid,
|
||||||
'domains' => $domains,
|
'domains' => $service->applications()->pluck('fqdn')->filter()->sort()->values(),
|
||||||
])->setStatusCode(201);
|
])->setStatusCode(201);
|
||||||
} elseif (filled($request->type)) {
|
} elseif (filled($request->type)) {
|
||||||
return response()->json([
|
return response()->json([
|
||||||
@@ -623,6 +801,18 @@ class ServicesController extends Controller
|
|||||||
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the service should be deployed instantly.'],
|
'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.'],
|
'connect_to_docker_network' => ['type' => 'boolean', 'default' => false, 'description' => 'Connect the service to the predefined docker network.'],
|
||||||
'docker_compose_raw' => ['type' => 'string', 'description' => 'The base64 encoded Docker Compose 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.'],
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
@@ -657,6 +847,35 @@ class ServicesController extends Controller
|
|||||||
response: 404,
|
response: 404,
|
||||||
ref: '#/components/responses/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(
|
new OA\Response(
|
||||||
response: 422,
|
response: 422,
|
||||||
ref: '#/components/responses/422',
|
ref: '#/components/responses/422',
|
||||||
@@ -682,15 +901,24 @@ class ServicesController extends Controller
|
|||||||
|
|
||||||
$this->authorize('update', $service);
|
$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',
|
'name' => 'string|max:255',
|
||||||
'description' => 'string|nullable',
|
'description' => 'string|nullable',
|
||||||
'instant_deploy' => 'boolean',
|
'instant_deploy' => 'boolean',
|
||||||
'connect_to_docker_network' => 'boolean',
|
'connect_to_docker_network' => 'boolean',
|
||||||
'docker_compose_raw' => 'string|nullable',
|
'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);
|
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
|
||||||
if ($validator->fails() || ! empty($extraFields)) {
|
if ($validator->fails() || ! empty($extraFields)) {
|
||||||
@@ -754,22 +982,33 @@ class ServicesController extends Controller
|
|||||||
$service->save();
|
$service->save();
|
||||||
|
|
||||||
$service->parse();
|
$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) {
|
if ($request->instant_deploy) {
|
||||||
StartService::dispatch($service);
|
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([
|
return response()->json([
|
||||||
'uuid' => $service->uuid,
|
'uuid' => $service->uuid,
|
||||||
'domains' => $domains,
|
'domains' => $service->applications()->pluck('fqdn')->filter()->sort()->values(),
|
||||||
])->setStatusCode(200);
|
])->setStatusCode(200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
152
openapi.json
152
openapi.json
@@ -8887,6 +8887,28 @@
|
|||||||
"docker_compose_raw": {
|
"docker_compose_raw": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "The base64 encoded Docker Compose content."
|
"description": "The base64 encoded Docker Compose content."
|
||||||
|
},
|
||||||
|
"urls": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Array of URLs to be applied to containers of a service.",
|
||||||
|
"items": {
|
||||||
|
"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\")."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"force_domain_override": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"description": "Force domain override even if conflicts are detected."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "object"
|
"type": "object"
|
||||||
@@ -8924,6 +8946,60 @@
|
|||||||
"400": {
|
"400": {
|
||||||
"$ref": "#\/components\/responses\/400"
|
"$ref": "#\/components\/responses\/400"
|
||||||
},
|
},
|
||||||
|
"409": {
|
||||||
|
"description": "Domain conflicts detected.",
|
||||||
|
"content": {
|
||||||
|
"application\/json": {
|
||||||
|
"schema": {
|
||||||
|
"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": {
|
||||||
|
"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'"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"422": {
|
"422": {
|
||||||
"$ref": "#\/components\/responses\/422"
|
"$ref": "#\/components\/responses\/422"
|
||||||
}
|
}
|
||||||
@@ -9137,6 +9213,28 @@
|
|||||||
"docker_compose_raw": {
|
"docker_compose_raw": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "The base64 encoded Docker Compose content."
|
"description": "The base64 encoded Docker Compose content."
|
||||||
|
},
|
||||||
|
"urls": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Array of URLs to be applied to containers of a service.",
|
||||||
|
"items": {
|
||||||
|
"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\")."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"force_domain_override": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"description": "Force domain override even if conflicts are detected."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "object"
|
"type": "object"
|
||||||
@@ -9177,6 +9275,60 @@
|
|||||||
"404": {
|
"404": {
|
||||||
"$ref": "#\/components\/responses\/404"
|
"$ref": "#\/components\/responses\/404"
|
||||||
},
|
},
|
||||||
|
"409": {
|
||||||
|
"description": "Domain conflicts detected.",
|
||||||
|
"content": {
|
||||||
|
"application\/json": {
|
||||||
|
"schema": {
|
||||||
|
"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": {
|
||||||
|
"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'"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"422": {
|
"422": {
|
||||||
"$ref": "#\/components\/responses\/422"
|
"$ref": "#\/components\/responses\/422"
|
||||||
}
|
}
|
||||||
|
|||||||
36
openapi.yaml
36
openapi.yaml
@@ -5606,6 +5606,14 @@ paths:
|
|||||||
docker_compose_raw:
|
docker_compose_raw:
|
||||||
type: string
|
type: string
|
||||||
description: 'The base64 encoded Docker Compose content.'
|
description: 'The base64 encoded Docker Compose content.'
|
||||||
|
urls:
|
||||||
|
type: array
|
||||||
|
description: 'Array of URLs to be applied to containers of a service.'
|
||||||
|
items: { 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").' } }, type: object }
|
||||||
|
force_domain_override:
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
description: 'Force domain override even if conflicts are detected.'
|
||||||
type: object
|
type: object
|
||||||
responses:
|
responses:
|
||||||
'201':
|
'201':
|
||||||
@@ -5621,6 +5629,16 @@ paths:
|
|||||||
$ref: '#/components/responses/401'
|
$ref: '#/components/responses/401'
|
||||||
'400':
|
'400':
|
||||||
$ref: '#/components/responses/400'
|
$ref: '#/components/responses/400'
|
||||||
|
'409':
|
||||||
|
description: 'Domain conflicts detected.'
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
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: { 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'" } }, type: object } }
|
||||||
|
type: object
|
||||||
'422':
|
'422':
|
||||||
$ref: '#/components/responses/422'
|
$ref: '#/components/responses/422'
|
||||||
security:
|
security:
|
||||||
@@ -5773,6 +5791,14 @@ paths:
|
|||||||
docker_compose_raw:
|
docker_compose_raw:
|
||||||
type: string
|
type: string
|
||||||
description: 'The base64 encoded Docker Compose content.'
|
description: 'The base64 encoded Docker Compose content.'
|
||||||
|
urls:
|
||||||
|
type: array
|
||||||
|
description: 'Array of URLs to be applied to containers of a service.'
|
||||||
|
items: { 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").' } }, type: object }
|
||||||
|
force_domain_override:
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
description: 'Force domain override even if conflicts are detected.'
|
||||||
type: object
|
type: object
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
@@ -5790,6 +5816,16 @@ paths:
|
|||||||
$ref: '#/components/responses/400'
|
$ref: '#/components/responses/400'
|
||||||
'404':
|
'404':
|
||||||
$ref: '#/components/responses/404'
|
$ref: '#/components/responses/404'
|
||||||
|
'409':
|
||||||
|
description: 'Domain conflicts detected.'
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
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: { 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'" } }, type: object } }
|
||||||
|
type: object
|
||||||
'422':
|
'422':
|
||||||
$ref: '#/components/responses/422'
|
$ref: '#/components/responses/422'
|
||||||
security:
|
security:
|
||||||
|
|||||||
Reference in New Issue
Block a user