mirror of
https://github.com/tiennm99/coolify.git
synced 2026-04-17 19:21:36 +00:00
feat(api): improve service urls update
- add force_domain_override functionality and docs - delete service on creation if there is URL conflicts as otherwise we will have stale services (we need to create the service because we need to parse it and more)
This commit is contained in:
@@ -38,13 +38,14 @@ class ServicesController extends Controller
|
||||
return serializeApiResponse($service);
|
||||
}
|
||||
|
||||
private function applyServiceUrls(Service $service, array $urls, string $teamId): ?array
|
||||
private function applyServiceUrls(Service $service, array $urls, string $teamId, bool $forceDomainOverride = false): ?array
|
||||
{
|
||||
$errors = [];
|
||||
$conflicts = [];
|
||||
|
||||
foreach ($urls as $item) {
|
||||
$name = data_get($item, 'name');
|
||||
$urls = data_get($item, 'url');
|
||||
foreach ($urls as $url) {
|
||||
$name = data_get($url, 'name');
|
||||
$urls = data_get($url, 'url');
|
||||
|
||||
if (blank($name)) {
|
||||
$errors[] = 'Service container name is required to apply URLs.';
|
||||
@@ -66,7 +67,7 @@ class ServicesController extends Controller
|
||||
if (! filter_var($url, FILTER_VALIDATE_URL)) {
|
||||
$errors[] = 'Invalid URL: '.$url;
|
||||
|
||||
return str($url)->lower();
|
||||
return $url;
|
||||
}
|
||||
$scheme = parse_url($url, PHP_URL_SCHEME) ?? '';
|
||||
if (! in_array(strtolower($scheme), ['http', 'https'])) {
|
||||
@@ -74,16 +75,26 @@ class ServicesController extends Controller
|
||||
}
|
||||
|
||||
return str($url)->lower();
|
||||
})->filter(fn ($u) => $u->isNotEmpty())->unique()->implode(',');
|
||||
});
|
||||
|
||||
if ($urls && empty($errors)) {
|
||||
$result = checkIfDomainIsAlreadyUsedViaAPI(collect(explode(',', $urls)), $teamId, $application->uuid);
|
||||
if ($result['hasConflicts']) {
|
||||
foreach ($result['conflicts'] as $conflict) {
|
||||
$errors[] = $conflict['message'];
|
||||
if (count($errors) > 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$result = checkIfDomainIsAlreadyUsedViaAPI($urls, $teamId, $application->uuid);
|
||||
if (isset($result['error'])) {
|
||||
$errors[] = $result['error'];
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($result['hasConflicts'] && ! $forceDomainOverride) {
|
||||
$conflicts = array_merge($conflicts, $result['conflicts']);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$urls = $urls->filter(fn ($u) => filled($u))->unique()->implode(',');
|
||||
} else {
|
||||
$urls = null;
|
||||
}
|
||||
@@ -96,6 +107,13 @@ class ServicesController extends Controller
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -188,6 +206,7 @@ class ServicesController extends Controller
|
||||
],
|
||||
),
|
||||
],
|
||||
'force_domain_override' => ['type' => 'boolean', 'default' => false, 'description' => 'Force domain override even if conflicts are detected.'],
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -217,6 +236,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',
|
||||
@@ -225,7 +273,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', 'urls'];
|
||||
$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)) {
|
||||
@@ -253,6 +301,7 @@ class ServicesController extends Controller
|
||||
'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.',
|
||||
@@ -378,13 +427,23 @@ class ServicesController extends Controller
|
||||
applyServiceApplicationPrerequisites($service);
|
||||
|
||||
if ($request->has('urls') && is_array($request->urls)) {
|
||||
$urlResult = $this->applyServiceUrls($service, $request->urls, $teamId);
|
||||
$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) {
|
||||
@@ -399,7 +458,7 @@ class ServicesController extends Controller
|
||||
|
||||
return response()->json(['message' => 'Service not found.', 'valid_service_types' => $serviceKeys], 404);
|
||||
} elseif (filled($request->docker_compose_raw)) {
|
||||
$allowedFields = ['name', 'description', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'docker_compose_raw', 'connect_to_docker_network', 'urls'];
|
||||
$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'];
|
||||
|
||||
$validationRules = [
|
||||
'project_uuid' => 'string|required',
|
||||
@@ -416,6 +475,7 @@ class ServicesController extends Controller
|
||||
'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.',
|
||||
@@ -516,13 +576,23 @@ class ServicesController extends Controller
|
||||
$service->parse(isNew: true);
|
||||
|
||||
if ($request->has('urls') && is_array($request->urls)) {
|
||||
$urlResult = $this->applyServiceUrls($service, $request->urls, $teamId);
|
||||
$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) {
|
||||
@@ -726,6 +796,7 @@ class ServicesController extends Controller
|
||||
],
|
||||
),
|
||||
],
|
||||
'force_domain_override' => ['type' => 'boolean', 'default' => false, 'description' => 'Force domain override even if conflicts are detected.'],
|
||||
],
|
||||
)
|
||||
),
|
||||
@@ -760,6 +831,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',
|
||||
@@ -785,7 +885,7 @@ class ServicesController extends Controller
|
||||
|
||||
$this->authorize('update', $service);
|
||||
|
||||
$allowedFields = ['name', 'description', 'instant_deploy', 'docker_compose_raw', 'connect_to_docker_network', 'urls'];
|
||||
$allowedFields = ['name', 'description', 'instant_deploy', 'docker_compose_raw', 'connect_to_docker_network', 'urls', 'force_domain_override'];
|
||||
|
||||
$validationRules = [
|
||||
'name' => 'string|max:255',
|
||||
@@ -797,6 +897,7 @@ class ServicesController extends Controller
|
||||
'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.',
|
||||
@@ -867,13 +968,22 @@ class ServicesController extends Controller
|
||||
$service->parse();
|
||||
|
||||
if ($request->has('urls') && is_array($request->urls)) {
|
||||
$urlResult = $this->applyServiceUrls($service, $request->urls, $teamId);
|
||||
$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) {
|
||||
|
||||
118
openapi.json
118
openapi.json
@@ -8904,6 +8904,11 @@
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"force_domain_override": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Force domain override even if conflicts are detected."
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
@@ -8941,6 +8946,60 @@
|
||||
"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": {
|
||||
"$ref": "#\/components\/responses\/422"
|
||||
}
|
||||
@@ -9171,6 +9230,11 @@
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"force_domain_override": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Force domain override even if conflicts are detected."
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
@@ -9211,6 +9275,60 @@
|
||||
"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": {
|
||||
"$ref": "#\/components\/responses\/422"
|
||||
}
|
||||
|
||||
28
openapi.yaml
28
openapi.yaml
@@ -5610,6 +5610,10 @@ paths:
|
||||
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 domains (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
|
||||
responses:
|
||||
'201':
|
||||
@@ -5625,6 +5629,16 @@ paths:
|
||||
$ref: '#/components/responses/401'
|
||||
'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':
|
||||
$ref: '#/components/responses/422'
|
||||
security:
|
||||
@@ -5781,6 +5795,10 @@ paths:
|
||||
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 domains (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
|
||||
responses:
|
||||
'200':
|
||||
@@ -5798,6 +5816,16 @@ paths:
|
||||
$ref: '#/components/responses/400'
|
||||
'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':
|
||||
$ref: '#/components/responses/422'
|
||||
security:
|
||||
|
||||
Reference in New Issue
Block a user