fix(api): remove deprecated docker compose application endpoint
Drop the unstable applications/dockercompose route and controller path now that service creation is handled by POST /api/v1/services. Add coverage to ensure the deprecated endpoint stays unregistered while the services endpoint remains available.
This commit is contained in:
parent
61b124b434
commit
6ee75cfa65
7 changed files with 32 additions and 463 deletions
|
|
@ -5,7 +5,6 @@
|
|||
use App\Actions\Application\CleanupPreviewDeployment;
|
||||
use App\Actions\Application\LoadComposeFile;
|
||||
use App\Actions\Application\StopApplication;
|
||||
use App\Actions\Service\StartService;
|
||||
use App\Enums\BuildPackTypes;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Jobs\DeleteResourceJob;
|
||||
|
|
@ -18,7 +17,6 @@
|
|||
use App\Models\PrivateKey;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use App\Rules\ValidGitBranch;
|
||||
use App\Rules\ValidGitRepositoryUrl;
|
||||
use App\Services\DockerImageParser;
|
||||
|
|
@ -899,105 +897,6 @@ public function create_dockerimage_application(Request $request)
|
|||
return $this->create_application($request, 'dockerimage');
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use POST /api/v1/services instead. This endpoint creates a Service, not an Application and is an unstable duplicate of POST /api/v1/services.
|
||||
*/
|
||||
#[OA\Post(
|
||||
summary: 'Create (Docker Compose)',
|
||||
description: 'Deprecated: Use POST /api/v1/services instead.',
|
||||
path: '/applications/dockercompose',
|
||||
operationId: 'create-dockercompose-application',
|
||||
deprecated: true,
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Applications'],
|
||||
requestBody: new OA\RequestBody(
|
||||
description: 'Application object that needs to be created.',
|
||||
required: true,
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
required: ['project_uuid', 'server_uuid', 'environment_name', 'environment_uuid', 'docker_compose_raw'],
|
||||
properties: [
|
||||
'project_uuid' => ['type' => 'string', 'description' => 'The project UUID.'],
|
||||
'server_uuid' => ['type' => 'string', 'description' => 'The server UUID.'],
|
||||
'environment_name' => ['type' => 'string', 'description' => 'The environment name. You need to provide at least one of environment_name or environment_uuid.'],
|
||||
'environment_uuid' => ['type' => 'string', 'description' => 'The environment UUID. You need to provide at least one of environment_name or environment_uuid.'],
|
||||
'docker_compose_raw' => ['type' => 'string', 'description' => 'The Docker Compose raw content.'],
|
||||
'destination_uuid' => ['type' => 'string', 'description' => 'The destination UUID if the server has more than one destinations.'],
|
||||
'name' => ['type' => 'string', 'description' => 'The application name.'],
|
||||
'description' => ['type' => 'string', 'description' => 'The application description.'],
|
||||
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'],
|
||||
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
|
||||
'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'],
|
||||
'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'],
|
||||
'is_container_label_escape_enabled' => ['type' => 'boolean', 'default' => true, 'description' => 'Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off.'],
|
||||
],
|
||||
)
|
||||
),
|
||||
]
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 201,
|
||||
description: 'Application created successfully.',
|
||||
content: new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'uuid' => ['type' => 'string'],
|
||||
]
|
||||
)
|
||||
)
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
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\''],
|
||||
]
|
||||
),
|
||||
],
|
||||
]
|
||||
)
|
||||
),
|
||||
]
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function create_dockercompose_application(Request $request)
|
||||
{
|
||||
return $this->create_application($request, 'dockercompose');
|
||||
}
|
||||
|
||||
private function create_application(Request $request, $type)
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
|
|
@ -2005,97 +1904,6 @@ private function create_application(Request $request, $type)
|
|||
'uuid' => data_get($application, 'uuid'),
|
||||
'domains' => data_get($application, 'fqdn'),
|
||||
]))->setStatusCode(201);
|
||||
} elseif ($type === 'dockercompose') {
|
||||
$allowedFields = ['project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'instant_deploy', 'docker_compose_raw', 'force_domain_override', 'is_container_label_escape_enabled'];
|
||||
|
||||
$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('name')) {
|
||||
$request->offsetSet('name', 'service'.new Cuid2);
|
||||
}
|
||||
$validationRules = [
|
||||
'docker_compose_raw' => 'string|required',
|
||||
];
|
||||
$validationRules = array_merge(sharedDataApplications(), $validationRules);
|
||||
$validator = customApiValidator($request->all(), $validationRules);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
$return = $this->validateDataApplications($request, $server);
|
||||
if ($return instanceof JsonResponse) {
|
||||
return $return;
|
||||
}
|
||||
if (! isBase64Encoded($request->docker_compose_raw)) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => [
|
||||
'docker_compose_raw' => 'The docker_compose_raw should be base64 encoded.',
|
||||
],
|
||||
], 422);
|
||||
}
|
||||
$dockerComposeRaw = base64_decode($request->docker_compose_raw);
|
||||
if (mb_detect_encoding($dockerComposeRaw, 'UTF-8', true) === false) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => [
|
||||
'docker_compose_raw' => 'The docker_compose_raw should be base64 encoded.',
|
||||
],
|
||||
], 422);
|
||||
}
|
||||
$dockerCompose = base64_decode($request->docker_compose_raw);
|
||||
$dockerComposeRaw = Yaml::dump(Yaml::parse($dockerCompose), 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK);
|
||||
|
||||
$service = new Service;
|
||||
removeUnnecessaryFieldsFromRequest($request);
|
||||
$service->fill($request->only($allowedFields));
|
||||
|
||||
$service->docker_compose_raw = $dockerComposeRaw;
|
||||
$service->environment_id = $environment->id;
|
||||
$service->server_id = $server->id;
|
||||
$service->destination_id = $destination->id;
|
||||
$service->destination_type = $destination->getMorphClass();
|
||||
if (isset($isContainerLabelEscapeEnabled)) {
|
||||
$service->is_container_label_escape_enabled = $isContainerLabelEscapeEnabled;
|
||||
}
|
||||
$service->save();
|
||||
|
||||
$service->parse(isNew: true);
|
||||
|
||||
// Apply service-specific application prerequisites
|
||||
applyServiceApplicationPrerequisites($service);
|
||||
|
||||
if ($instantDeploy) {
|
||||
StartService::dispatch($service);
|
||||
}
|
||||
|
||||
auditLog('api.application.created', [
|
||||
'team_id' => $teamId,
|
||||
'service_uuid' => data_get($service, 'uuid'),
|
||||
'service_name' => data_get($service, 'name'),
|
||||
'application_type' => $type,
|
||||
'instant_deploy' => (bool) ($instantDeploy ?? false),
|
||||
]);
|
||||
|
||||
return response()->json(serializeApiResponse([
|
||||
'uuid' => data_get($service, 'uuid'),
|
||||
'domains' => data_get($service, 'domains'),
|
||||
]))->setStatusCode(201);
|
||||
}
|
||||
|
||||
return response()->json(['message' => 'Invalid type.'], 400);
|
||||
|
|
|
|||
167
openapi.json
167
openapi.json
|
|
@ -2092,173 +2092,6 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"\/applications\/dockercompose": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Applications"
|
||||
],
|
||||
"summary": "Create (Docker Compose)",
|
||||
"description": "Deprecated: Use POST \/api\/v1\/services instead.",
|
||||
"operationId": "create-dockercompose-application",
|
||||
"requestBody": {
|
||||
"description": "Application object that needs to be created.",
|
||||
"required": true,
|
||||
"content": {
|
||||
"application\/json": {
|
||||
"schema": {
|
||||
"required": [
|
||||
"project_uuid",
|
||||
"server_uuid",
|
||||
"environment_name",
|
||||
"environment_uuid",
|
||||
"docker_compose_raw"
|
||||
],
|
||||
"properties": {
|
||||
"project_uuid": {
|
||||
"type": "string",
|
||||
"description": "The project UUID."
|
||||
},
|
||||
"server_uuid": {
|
||||
"type": "string",
|
||||
"description": "The server UUID."
|
||||
},
|
||||
"environment_name": {
|
||||
"type": "string",
|
||||
"description": "The environment name. You need to provide at least one of environment_name or environment_uuid."
|
||||
},
|
||||
"environment_uuid": {
|
||||
"type": "string",
|
||||
"description": "The environment UUID. You need to provide at least one of environment_name or environment_uuid."
|
||||
},
|
||||
"docker_compose_raw": {
|
||||
"type": "string",
|
||||
"description": "The Docker Compose raw content."
|
||||
},
|
||||
"destination_uuid": {
|
||||
"type": "string",
|
||||
"description": "The destination UUID if the server has more than one destinations."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The application name."
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "The application description."
|
||||
},
|
||||
"instant_deploy": {
|
||||
"type": "boolean",
|
||||
"description": "The flag to indicate if the application should be deployed instantly."
|
||||
},
|
||||
"use_build_server": {
|
||||
"type": "boolean",
|
||||
"nullable": true,
|
||||
"description": "Use build server."
|
||||
},
|
||||
"connect_to_docker_network": {
|
||||
"type": "boolean",
|
||||
"description": "The flag to connect the service to the predefined Docker network."
|
||||
},
|
||||
"force_domain_override": {
|
||||
"type": "boolean",
|
||||
"description": "Force domain usage even if conflicts are detected. Default is false."
|
||||
},
|
||||
"is_container_label_escape_enabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off."
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Application created successfully.",
|
||||
"content": {
|
||||
"application\/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"uuid": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"deprecated": true,
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"\/applications\/{uuid}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
|
|
|
|||
89
openapi.yaml
89
openapi.yaml
|
|
@ -1337,95 +1337,6 @@ paths:
|
|||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
/applications/dockercompose:
|
||||
post:
|
||||
tags:
|
||||
- Applications
|
||||
summary: 'Create (Docker Compose)'
|
||||
description: 'Deprecated: Use POST /api/v1/services instead.'
|
||||
operationId: create-dockercompose-application
|
||||
requestBody:
|
||||
description: 'Application object that needs to be created.'
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
required:
|
||||
- project_uuid
|
||||
- server_uuid
|
||||
- environment_name
|
||||
- environment_uuid
|
||||
- docker_compose_raw
|
||||
properties:
|
||||
project_uuid:
|
||||
type: string
|
||||
description: 'The project UUID.'
|
||||
server_uuid:
|
||||
type: string
|
||||
description: 'The server UUID.'
|
||||
environment_name:
|
||||
type: string
|
||||
description: 'The environment name. You need to provide at least one of environment_name or environment_uuid.'
|
||||
environment_uuid:
|
||||
type: string
|
||||
description: 'The environment UUID. You need to provide at least one of environment_name or environment_uuid.'
|
||||
docker_compose_raw:
|
||||
type: string
|
||||
description: 'The Docker Compose raw content.'
|
||||
destination_uuid:
|
||||
type: string
|
||||
description: 'The destination UUID if the server has more than one destinations.'
|
||||
name:
|
||||
type: string
|
||||
description: 'The application name.'
|
||||
description:
|
||||
type: string
|
||||
description: 'The application description.'
|
||||
instant_deploy:
|
||||
type: boolean
|
||||
description: 'The flag to indicate if the application should be deployed instantly.'
|
||||
use_build_server:
|
||||
type: boolean
|
||||
nullable: true
|
||||
description: 'Use build server.'
|
||||
connect_to_docker_network:
|
||||
type: boolean
|
||||
description: 'The flag to connect the service to the predefined Docker network.'
|
||||
force_domain_override:
|
||||
type: boolean
|
||||
description: 'Force domain usage even if conflicts are detected. Default is false.'
|
||||
is_container_label_escape_enabled:
|
||||
type: boolean
|
||||
default: true
|
||||
description: 'Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off.'
|
||||
type: object
|
||||
responses:
|
||||
'201':
|
||||
description: 'Application created successfully.'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
uuid: { type: string }
|
||||
type: object
|
||||
'401':
|
||||
$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
|
||||
deprecated: true
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
'/applications/{uuid}':
|
||||
get:
|
||||
tags:
|
||||
|
|
|
|||
|
|
@ -108,11 +108,6 @@
|
|||
Route::post('/applications/dockerfile', [ApplicationsController::class, 'create_dockerfile_application'])->middleware(['api.ability:write']);
|
||||
Route::post('/applications/dockerimage', [ApplicationsController::class, 'create_dockerimage_application'])->middleware(['api.ability:write']);
|
||||
|
||||
/**
|
||||
* @deprecated Use POST /api/v1/services instead. This endpoint creates a Service, not an Application and is a unstable duplicate of POST /api/v1/services.
|
||||
*/
|
||||
Route::post('/applications/dockercompose', [ApplicationsController::class, 'create_dockercompose_application'])->middleware(['api.ability:write']);
|
||||
|
||||
Route::get('/applications/{uuid}', [ApplicationsController::class, 'application_by_uuid'])->middleware(['api.ability:read']);
|
||||
Route::patch('/applications/{uuid}', [ApplicationsController::class, 'update_by_uuid'])->middleware(['api.ability:write']);
|
||||
Route::delete('/applications/{uuid}', [ApplicationsController::class, 'delete_by_uuid'])->middleware(['api.ability:write']);
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
use App\Http\Controllers\Api\ServicesController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
test('deprecated docker compose application endpoint is not registered', function () {
|
||||
$routes = collect(Route::getRoutes()->getRoutes())
|
||||
->filter(fn ($route) => in_array('POST', $route->methods(), true))
|
||||
->filter(fn ($route) => $route->uri() === 'api/v1/applications/dockercompose');
|
||||
|
||||
expect($routes)->toBeEmpty();
|
||||
|
||||
$this->postJson('/api/v1/applications/dockercompose')->assertNotFound();
|
||||
});
|
||||
|
||||
test('custom docker compose services endpoint remains registered', function () {
|
||||
$route = collect(Route::getRoutes()->getRoutes())
|
||||
->first(fn ($route) => in_array('POST', $route->methods(), true) && $route->uri() === 'api/v1/services');
|
||||
|
||||
expect($route)->not->toBeNull()
|
||||
->and($route->getActionName())->toBe(ServicesController::class.'@create_service');
|
||||
});
|
||||
Loading…
Reference in a new issue