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:
parent
764d8861f6
commit
0628268875
3 changed files with 287 additions and 31 deletions
|
|
@ -38,13 +38,14 @@ private function removeSensitiveData($service)
|
|||
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 @@ private function applyServiceUrls(Service $service, array $urls, string $teamId)
|
|||
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 @@ private function applyServiceUrls(Service $service, array $urls, string $teamId)
|
|||
}
|
||||
|
||||
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 @@ private function applyServiceUrls(Service $service, array $urls, string $teamId)
|
|||
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 @@ public function services(Request $request)
|
|||
],
|
||||
),
|
||||
],
|
||||
'force_domain_override' => ['type' => 'boolean', 'default' => false, 'description' => 'Force domain override even if conflicts are detected.'],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -217,6 +236,35 @@ public function services(Request $request)
|
|||
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 @@ public function services(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', '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 @@ public function create_service(Request $request)
|
|||
'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,12 +427,22 @@ public function create_service(Request $request)
|
|||
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) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $urlResult['errors'],
|
||||
], 422);
|
||||
$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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -399,7 +458,7 @@ public function create_service(Request $request)
|
|||
|
||||
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 @@ public function create_service(Request $request)
|
|||
'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,12 +576,22 @@ public function create_service(Request $request)
|
|||
$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) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $urlResult['errors'],
|
||||
], 422);
|
||||
$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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -726,6 +796,7 @@ public function delete_by_uuid(Request $request)
|
|||
],
|
||||
),
|
||||
],
|
||||
'force_domain_override' => ['type' => 'boolean', 'default' => false, 'description' => 'Force domain override even if conflicts are detected.'],
|
||||
],
|
||||
)
|
||||
),
|
||||
|
|
@ -760,6 +831,35 @@ public function delete_by_uuid(Request $request)
|
|||
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 @@ public function update_by_uuid(Request $request)
|
|||
|
||||
$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 @@ public function update_by_uuid(Request $request)
|
|||
'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,12 +968,21 @@ public function update_by_uuid(Request $request)
|
|||
$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) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $urlResult['errors'],
|
||||
], 422);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
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:
|
||||
|
|
|
|||
Loading…
Reference in a new issue