feat(api): add tag management endpoints for applications, databases, and services (#9275)
This commit is contained in:
commit
5dc697c428
13 changed files with 2577 additions and 15 deletions
|
|
@ -33,6 +33,18 @@
|
||||||
|
|
||||||
class ApplicationsController extends Controller
|
class ApplicationsController extends Controller
|
||||||
{
|
{
|
||||||
|
use Concerns\HandlesTagsApi;
|
||||||
|
|
||||||
|
protected function findTaggableResource(string $uuid, int|string $teamId): mixed
|
||||||
|
{
|
||||||
|
return Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $uuid)->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tagResourceNotFoundMessage(): string
|
||||||
|
{
|
||||||
|
return 'Application not found.';
|
||||||
|
}
|
||||||
|
|
||||||
private function exposeFileStorageContentIfAllowed(LocalFileVolume|LocalPersistentVolume $storage): LocalFileVolume|LocalPersistentVolume
|
private function exposeFileStorageContentIfAllowed(LocalFileVolume|LocalPersistentVolume $storage): LocalFileVolume|LocalPersistentVolume
|
||||||
{
|
{
|
||||||
if (request()->attributes->get('can_read_sensitive', false) === true) {
|
if (request()->attributes->get('can_read_sensitive', false) === true) {
|
||||||
|
|
@ -280,6 +292,7 @@ public function applications(Request $request)
|
||||||
'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'],
|
'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'],
|
||||||
'autogenerate_domain' => ['type' => 'boolean', 'default' => true, 'description' => 'If true and domains is empty, auto-generate a domain using the server\'s wildcard domain or sslip.io fallback. Default: true.'],
|
'autogenerate_domain' => ['type' => 'boolean', 'default' => true, 'description' => 'If true and domains is empty, auto-generate a domain using the server\'s wildcard domain or sslip.io fallback. Default: true.'],
|
||||||
'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.'],
|
'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.'],
|
||||||
|
'tags' => ['type' => 'array', 'items' => new OA\Items(type: 'string'), 'description' => 'Tags to assign to the application.'],
|
||||||
'is_preserve_repository_enabled' => ['type' => 'boolean', 'default' => false, 'description' => 'Preserve repository during deployment.'],
|
'is_preserve_repository_enabled' => ['type' => 'boolean', 'default' => false, 'description' => 'Preserve repository during deployment.'],
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
@ -447,6 +460,7 @@ public function create_public_application(Request $request)
|
||||||
'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'],
|
'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'],
|
||||||
'autogenerate_domain' => ['type' => 'boolean', 'default' => true, 'description' => 'If true and domains is empty, auto-generate a domain using the server\'s wildcard domain or sslip.io fallback. Default: true.'],
|
'autogenerate_domain' => ['type' => 'boolean', 'default' => true, 'description' => 'If true and domains is empty, auto-generate a domain using the server\'s wildcard domain or sslip.io fallback. Default: true.'],
|
||||||
'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.'],
|
'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.'],
|
||||||
|
'tags' => ['type' => 'array', 'items' => new OA\Items(type: 'string'), 'description' => 'Tags to assign to the application.'],
|
||||||
'is_preserve_repository_enabled' => ['type' => 'boolean', 'default' => false, 'description' => 'Preserve repository during deployment.'],
|
'is_preserve_repository_enabled' => ['type' => 'boolean', 'default' => false, 'description' => 'Preserve repository during deployment.'],
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
@ -614,6 +628,7 @@ public function create_private_gh_app_application(Request $request)
|
||||||
'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'],
|
'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'],
|
||||||
'autogenerate_domain' => ['type' => 'boolean', 'default' => true, 'description' => 'If true and domains is empty, auto-generate a domain using the server\'s wildcard domain or sslip.io fallback. Default: true.'],
|
'autogenerate_domain' => ['type' => 'boolean', 'default' => true, 'description' => 'If true and domains is empty, auto-generate a domain using the server\'s wildcard domain or sslip.io fallback. Default: true.'],
|
||||||
'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.'],
|
'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.'],
|
||||||
|
'tags' => ['type' => 'array', 'items' => new OA\Items(type: 'string'), 'description' => 'Tags to assign to the application.'],
|
||||||
'is_preserve_repository_enabled' => ['type' => 'boolean', 'default' => false, 'description' => 'Preserve repository during deployment.'],
|
'is_preserve_repository_enabled' => ['type' => 'boolean', 'default' => false, 'description' => 'Preserve repository during deployment.'],
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
@ -753,6 +768,7 @@ public function create_private_deploy_key_application(Request $request)
|
||||||
'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'],
|
'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'],
|
||||||
'autogenerate_domain' => ['type' => 'boolean', 'default' => true, 'description' => 'If true and domains is empty, auto-generate a domain using the server\'s wildcard domain or sslip.io fallback. Default: true.'],
|
'autogenerate_domain' => ['type' => 'boolean', 'default' => true, 'description' => 'If true and domains is empty, auto-generate a domain using the server\'s wildcard domain or sslip.io fallback. Default: true.'],
|
||||||
'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.'],
|
'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.'],
|
||||||
|
'tags' => ['type' => 'array', 'items' => new OA\Items(type: 'string'), 'description' => 'Tags to assign to the application.'],
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
|
@ -888,6 +904,7 @@ public function create_dockerfile_application(Request $request)
|
||||||
'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'],
|
'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'],
|
||||||
'autogenerate_domain' => ['type' => 'boolean', 'default' => true, 'description' => 'If true and domains is empty, auto-generate a domain using the server\'s wildcard domain or sslip.io fallback. Default: true.'],
|
'autogenerate_domain' => ['type' => 'boolean', 'default' => true, 'description' => 'If true and domains is empty, auto-generate a domain using the server\'s wildcard domain or sslip.io fallback. Default: true.'],
|
||||||
'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.'],
|
'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.'],
|
||||||
|
'tags' => ['type' => 'array', 'items' => new OA\Items(type: 'string'), 'description' => 'Tags to assign to the application.'],
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
|
@ -964,7 +981,7 @@ private function create_application(Request $request, $type)
|
||||||
if ($return instanceof JsonResponse) {
|
if ($return instanceof JsonResponse) {
|
||||||
return $return;
|
return $return;
|
||||||
}
|
}
|
||||||
$allowedFields = ['project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'is_spa', 'is_auto_deploy_enabled', 'is_force_https_enabled', 'is_preview_deployments_enabled', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'custom_network_aliases', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_type', 'health_check_command', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'dockerfile_location', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server', 'static_image', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'autogenerate_domain', 'is_container_label_escape_enabled', 'is_preserve_repository_enabled'];
|
$allowedFields = ['project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'is_spa', 'is_auto_deploy_enabled', 'is_force_https_enabled', 'is_preview_deployments_enabled', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'custom_network_aliases', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_type', 'health_check_command', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'dockerfile_location', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server', 'static_image', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'autogenerate_domain', 'is_container_label_escape_enabled', 'tags', 'is_preserve_repository_enabled'];
|
||||||
|
|
||||||
$validator = customApiValidator($request->all(), [
|
$validator = customApiValidator($request->all(), [
|
||||||
'name' => 'string|max:255',
|
'name' => 'string|max:255',
|
||||||
|
|
@ -978,6 +995,8 @@ private function create_application(Request $request, $type)
|
||||||
'http_basic_auth_username' => 'string|nullable',
|
'http_basic_auth_username' => 'string|nullable',
|
||||||
'http_basic_auth_password' => 'string|nullable',
|
'http_basic_auth_password' => 'string|nullable',
|
||||||
'autogenerate_domain' => 'boolean',
|
'autogenerate_domain' => 'boolean',
|
||||||
|
'tags' => 'array|nullable',
|
||||||
|
'tags.*' => 'string|min:2',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
|
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
|
||||||
|
|
@ -995,6 +1014,13 @@ private function create_application(Request $request, $type)
|
||||||
], 422);
|
], 422);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$return = $this->validateTagsParameter($request);
|
||||||
|
if ($return instanceof JsonResponse) {
|
||||||
|
return $return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tagNames = $request->input('tags') ?? [];
|
||||||
|
|
||||||
$environmentUuid = $request->environment_uuid;
|
$environmentUuid = $request->environment_uuid;
|
||||||
$environmentName = $request->environment_name;
|
$environmentName = $request->environment_name;
|
||||||
if (blank($environmentUuid) && blank($environmentName)) {
|
if (blank($environmentUuid) && blank($environmentName)) {
|
||||||
|
|
@ -1252,6 +1278,9 @@ private function create_application(Request $request, $type)
|
||||||
$application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
|
$application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
|
||||||
$application->save();
|
$application->save();
|
||||||
}
|
}
|
||||||
|
if ($tagNames !== []) {
|
||||||
|
$this->attachTagsToResource($application, $tagNames, $teamId);
|
||||||
|
}
|
||||||
$application->isConfigurationChanged(true);
|
$application->isConfigurationChanged(true);
|
||||||
|
|
||||||
if ($instantDeploy) {
|
if ($instantDeploy) {
|
||||||
|
|
@ -1495,6 +1524,9 @@ private function create_application(Request $request, $type)
|
||||||
$application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
|
$application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
|
||||||
$application->save();
|
$application->save();
|
||||||
}
|
}
|
||||||
|
if ($tagNames !== []) {
|
||||||
|
$this->attachTagsToResource($application, $tagNames, $teamId);
|
||||||
|
}
|
||||||
$application->isConfigurationChanged(true);
|
$application->isConfigurationChanged(true);
|
||||||
|
|
||||||
if ($instantDeploy) {
|
if ($instantDeploy) {
|
||||||
|
|
@ -1708,6 +1740,9 @@ private function create_application(Request $request, $type)
|
||||||
$application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
|
$application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
|
||||||
$application->save();
|
$application->save();
|
||||||
}
|
}
|
||||||
|
if ($tagNames !== []) {
|
||||||
|
$this->attachTagsToResource($application, $tagNames, $teamId);
|
||||||
|
}
|
||||||
$application->isConfigurationChanged(true);
|
$application->isConfigurationChanged(true);
|
||||||
|
|
||||||
if ($instantDeploy) {
|
if ($instantDeploy) {
|
||||||
|
|
@ -1832,6 +1867,9 @@ private function create_application(Request $request, $type)
|
||||||
$application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
|
$application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
|
||||||
$application->save();
|
$application->save();
|
||||||
}
|
}
|
||||||
|
if ($tagNames !== []) {
|
||||||
|
$this->attachTagsToResource($application, $tagNames, $teamId);
|
||||||
|
}
|
||||||
$application->isConfigurationChanged(true);
|
$application->isConfigurationChanged(true);
|
||||||
|
|
||||||
if ($instantDeploy) {
|
if ($instantDeploy) {
|
||||||
|
|
@ -1955,6 +1993,9 @@ private function create_application(Request $request, $type)
|
||||||
$application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
|
$application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
|
||||||
$application->save();
|
$application->save();
|
||||||
}
|
}
|
||||||
|
if ($tagNames !== []) {
|
||||||
|
$this->attachTagsToResource($application, $tagNames, $teamId);
|
||||||
|
}
|
||||||
$application->isConfigurationChanged(true);
|
$application->isConfigurationChanged(true);
|
||||||
|
|
||||||
if ($instantDeploy) {
|
if ($instantDeploy) {
|
||||||
|
|
@ -1986,6 +2027,7 @@ private function create_application(Request $request, $type)
|
||||||
'uuid' => data_get($application, 'uuid'),
|
'uuid' => data_get($application, 'uuid'),
|
||||||
'domains' => data_get($application, 'fqdn'),
|
'domains' => data_get($application, 'fqdn'),
|
||||||
]))->setStatusCode(201);
|
]))->setStatusCode(201);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return response()->json(['message' => 'Invalid type.'], 400);
|
return response()->json(['message' => 'Invalid type.'], 400);
|
||||||
|
|
@ -4654,4 +4696,148 @@ public function delete_preview_by_pull_request_id(Request $request): JsonRespons
|
||||||
|
|
||||||
return response()->json(['message' => 'Preview deletion request queued.']);
|
return response()->json(['message' => 'Preview deletion request queued.']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Get(
|
||||||
|
summary: 'List Tags',
|
||||||
|
description: 'List tags for an application by UUID.',
|
||||||
|
path: '/applications/{uuid}/tags',
|
||||||
|
operationId: 'list-tags-by-application-uuid',
|
||||||
|
security: [
|
||||||
|
['bearerAuth' => []],
|
||||||
|
],
|
||||||
|
tags: ['Applications'],
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(
|
||||||
|
name: 'uuid',
|
||||||
|
in: 'path',
|
||||||
|
description: 'UUID of the application.',
|
||||||
|
required: true,
|
||||||
|
schema: new OA\Schema(type: 'string')
|
||||||
|
),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'List of tags.',
|
||||||
|
content: [
|
||||||
|
new OA\MediaType(
|
||||||
|
mediaType: 'application/json',
|
||||||
|
schema: new OA\Schema(
|
||||||
|
type: 'array',
|
||||||
|
items: new OA\Items(ref: '#/components/schemas/Tag')
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, ref: '#/components/responses/401'),
|
||||||
|
new OA\Response(response: 400, ref: '#/components/responses/400'),
|
||||||
|
new OA\Response(response: 404, ref: '#/components/responses/404'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
|
public function tags(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
return $this->listTags($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[OA\Post(
|
||||||
|
summary: 'Create Tag',
|
||||||
|
description: 'Add tag(s) to an application by UUID.',
|
||||||
|
path: '/applications/{uuid}/tags',
|
||||||
|
operationId: 'create-tag-by-application-uuid',
|
||||||
|
security: [
|
||||||
|
['bearerAuth' => []],
|
||||||
|
],
|
||||||
|
tags: ['Applications'],
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(
|
||||||
|
name: 'uuid',
|
||||||
|
in: 'path',
|
||||||
|
description: 'UUID of the application.',
|
||||||
|
required: true,
|
||||||
|
schema: new OA\Schema(type: 'string')
|
||||||
|
),
|
||||||
|
],
|
||||||
|
requestBody: new OA\RequestBody(
|
||||||
|
required: true,
|
||||||
|
content: [
|
||||||
|
new OA\MediaType(
|
||||||
|
mediaType: 'application/json',
|
||||||
|
schema: new OA\Schema(
|
||||||
|
type: 'object',
|
||||||
|
properties: [
|
||||||
|
'tag_name' => ['type' => 'string', 'description' => 'The tag name (min 2 characters). Required if tag_names is not provided.'],
|
||||||
|
'tag_names' => [
|
||||||
|
'type' => 'array',
|
||||||
|
'items' => new OA\Items(type: 'string'),
|
||||||
|
'description' => 'Array of tag names (each min 2 characters). Required if tag_name is not provided.',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 201,
|
||||||
|
description: 'Tags added successfully.',
|
||||||
|
content: [
|
||||||
|
new OA\MediaType(
|
||||||
|
mediaType: 'application/json',
|
||||||
|
schema: new OA\Schema(
|
||||||
|
type: 'array',
|
||||||
|
items: new OA\Items(ref: '#/components/schemas/Tag')
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, ref: '#/components/responses/401'),
|
||||||
|
new OA\Response(response: 400, ref: '#/components/responses/400'),
|
||||||
|
new OA\Response(response: 404, ref: '#/components/responses/404'),
|
||||||
|
new OA\Response(response: 422, ref: '#/components/responses/422'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
|
public function create_tag(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
return $this->createTag($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[OA\Delete(
|
||||||
|
summary: 'Delete Tag',
|
||||||
|
description: 'Remove a tag from an application by UUID.',
|
||||||
|
path: '/applications/{uuid}/tags/{tag_uuid}',
|
||||||
|
operationId: 'delete-tag-by-application-uuid',
|
||||||
|
security: [
|
||||||
|
['bearerAuth' => []],
|
||||||
|
],
|
||||||
|
tags: ['Applications'],
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(
|
||||||
|
name: 'uuid',
|
||||||
|
in: 'path',
|
||||||
|
description: 'UUID of the application.',
|
||||||
|
required: true,
|
||||||
|
schema: new OA\Schema(type: 'string')
|
||||||
|
),
|
||||||
|
new OA\Parameter(
|
||||||
|
name: 'tag_uuid',
|
||||||
|
in: 'path',
|
||||||
|
description: 'UUID of the tag.',
|
||||||
|
required: true,
|
||||||
|
schema: new OA\Schema(type: 'string')
|
||||||
|
),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Tag removed.',
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, ref: '#/components/responses/401'),
|
||||||
|
new OA\Response(response: 400, ref: '#/components/responses/400'),
|
||||||
|
new OA\Response(response: 404, ref: '#/components/responses/404'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
|
public function delete_tag(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
return $this->deleteTag($request);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
174
app/Http/Controllers/Api/Concerns/HandlesTagsApi.php
Normal file
174
app/Http/Controllers/Api/Concerns/HandlesTagsApi.php
Normal file
|
|
@ -0,0 +1,174 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\Concerns;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Api\TagsController;
|
||||||
|
use App\Models\Tag;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
|
||||||
|
trait HandlesTagsApi
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Find the taggable resource by UUID within the team.
|
||||||
|
*/
|
||||||
|
abstract protected function findTaggableResource(string $uuid, int|string $teamId): mixed;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the 404 message for the taggable resource.
|
||||||
|
*/
|
||||||
|
abstract protected function tagResourceNotFoundMessage(): string;
|
||||||
|
|
||||||
|
public function listTags(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$teamId = getTeamIdFromToken();
|
||||||
|
if (is_null($teamId)) {
|
||||||
|
return invalidTokenResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
$resource = $this->findTaggableResource($request->route('uuid'), $teamId);
|
||||||
|
if (! $resource) {
|
||||||
|
return response()->json(['message' => $this->tagResourceNotFoundMessage()], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->authorize('view', $resource);
|
||||||
|
|
||||||
|
return response()->json($resource->tags->map(TagsController::serializeTag(...)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createTag(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$teamId = getTeamIdFromToken();
|
||||||
|
if (is_null($teamId)) {
|
||||||
|
return invalidTokenResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
$return = validateIncomingRequest($request);
|
||||||
|
if ($return instanceof JsonResponse) {
|
||||||
|
return $return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$resource = $this->findTaggableResource($request->route('uuid'), $teamId);
|
||||||
|
if (! $resource) {
|
||||||
|
return response()->json(['message' => $this->tagResourceNotFoundMessage()], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->authorize('update', $resource);
|
||||||
|
|
||||||
|
if ($request->has('tag_name') && $request->has('tag_names')) {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Validation failed.',
|
||||||
|
'errors' => ['tag_name' => ['Provide either tag_name or tag_names, not both.']],
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
$validator = Validator::make($request->all(), [
|
||||||
|
'tag_name' => 'required_without:tag_names|string',
|
||||||
|
'tag_names' => 'required_without:tag_name|array|min:1',
|
||||||
|
'tag_names.*' => 'string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$extraFields = array_diff(array_keys($request->all()), ['tag_name', 'tag_names']);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
$tagNames = $this->normalizeTagNames($request->has('tag_names') ? $request->tag_names : [$request->tag_name]);
|
||||||
|
$invalidTags = array_filter($tagNames, fn (string $tagName): bool => mb_strlen($tagName) < 2);
|
||||||
|
if (! empty($invalidTags)) {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Validation failed.',
|
||||||
|
'errors' => ['tag_name' => ['Each tag name must be at least 2 characters after sanitization.']],
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->attachTagsToResource($resource, $tagNames, $teamId);
|
||||||
|
|
||||||
|
return response()->json($resource->refresh()->tags->map(TagsController::serializeTag(...)))->setStatusCode(201);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleteTag(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$teamId = getTeamIdFromToken();
|
||||||
|
if (is_null($teamId)) {
|
||||||
|
return invalidTokenResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
$resource = $this->findTaggableResource($request->route('uuid'), $teamId);
|
||||||
|
if (! $resource) {
|
||||||
|
return response()->json(['message' => $this->tagResourceNotFoundMessage()], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->authorize('update', $resource);
|
||||||
|
|
||||||
|
$tag = Tag::where('team_id', $teamId)->where('uuid', $request->route('tag_uuid'))->first();
|
||||||
|
if (! $tag) {
|
||||||
|
return response()->json(['message' => 'Tag not found.'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $resource->tags()->whereKey($tag->id)->exists()) {
|
||||||
|
return response()->json(['message' => 'Tag not found on resource.'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$resource->tags()->detach($tag->id);
|
||||||
|
$tag->deleteIfOrphaned();
|
||||||
|
|
||||||
|
return response()->json(['message' => 'Tag removed.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function attachTagsToResource($resource, array $tagNames, int|string $teamId): void
|
||||||
|
{
|
||||||
|
foreach ($this->normalizeTagNames($tagNames) as $tagName) {
|
||||||
|
if (mb_strlen($tagName) < 2) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tag = Tag::query()->createOrFirst([
|
||||||
|
'team_id' => $teamId,
|
||||||
|
'name' => $tagName,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$resource->tags()->syncWithoutDetaching([$tag->id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function validateTagsParameter(Request $request): ?JsonResponse
|
||||||
|
{
|
||||||
|
if (! $request->has('tags')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tagNames = $this->normalizeTagNames($request->input('tags', []));
|
||||||
|
$invalidTags = array_filter($tagNames, fn (string $tagName): bool => mb_strlen($tagName) < 2);
|
||||||
|
if (! empty($invalidTags)) {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Validation failed.',
|
||||||
|
'errors' => ['tags' => ['Each tag name must be at least 2 characters after sanitization.']],
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
$request->merge(['tags' => $tagNames]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function normalizeTagNames(array $tagNames): array
|
||||||
|
{
|
||||||
|
return collect($tagNames)
|
||||||
|
->map(fn ($tagName): string => strtolower(trim(strip_tags((string) $tagName))))
|
||||||
|
->unique()
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -28,6 +28,18 @@
|
||||||
|
|
||||||
class DatabasesController extends Controller
|
class DatabasesController extends Controller
|
||||||
{
|
{
|
||||||
|
use Concerns\HandlesTagsApi;
|
||||||
|
|
||||||
|
protected function findTaggableResource(string $uuid, int|string $teamId): mixed
|
||||||
|
{
|
||||||
|
return queryDatabaseByUuidWithinTeam($uuid, $teamId);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tagResourceNotFoundMessage(): string
|
||||||
|
{
|
||||||
|
return 'Database not found.';
|
||||||
|
}
|
||||||
|
|
||||||
private function exposeFileStorageContentIfAllowed(LocalFileVolume|LocalPersistentVolume $storage): LocalFileVolume|LocalPersistentVolume
|
private function exposeFileStorageContentIfAllowed(LocalFileVolume|LocalPersistentVolume $storage): LocalFileVolume|LocalPersistentVolume
|
||||||
{
|
{
|
||||||
if (request()->attributes->get('can_read_sensitive', false) === true) {
|
if (request()->attributes->get('can_read_sensitive', false) === true) {
|
||||||
|
|
@ -1220,6 +1232,7 @@ public function update_backup(Request $request)
|
||||||
'limits_cpuset' => ['type' => 'string', 'description' => 'CPU set of the database'],
|
'limits_cpuset' => ['type' => 'string', 'description' => 'CPU set of the database'],
|
||||||
'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares of the database'],
|
'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares of the database'],
|
||||||
'instant_deploy' => ['type' => 'boolean', 'description' => 'Instant deploy the database'],
|
'instant_deploy' => ['type' => 'boolean', 'description' => 'Instant deploy the database'],
|
||||||
|
'tags' => ['type' => 'array', 'items' => new OA\Items(type: 'string'), 'description' => 'Tags to assign to the database.'],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -1288,6 +1301,7 @@ public function create_database_postgresql(Request $request)
|
||||||
'limits_cpuset' => ['type' => 'string', 'description' => 'CPU set of the database'],
|
'limits_cpuset' => ['type' => 'string', 'description' => 'CPU set of the database'],
|
||||||
'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares of the database'],
|
'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares of the database'],
|
||||||
'instant_deploy' => ['type' => 'boolean', 'description' => 'Instant deploy the database'],
|
'instant_deploy' => ['type' => 'boolean', 'description' => 'Instant deploy the database'],
|
||||||
|
'tags' => ['type' => 'array', 'items' => new OA\Items(type: 'string'), 'description' => 'Tags to assign to the database.'],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -1355,6 +1369,7 @@ public function create_database_clickhouse(Request $request)
|
||||||
'limits_cpuset' => ['type' => 'string', 'description' => 'CPU set of the database'],
|
'limits_cpuset' => ['type' => 'string', 'description' => 'CPU set of the database'],
|
||||||
'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares of the database'],
|
'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares of the database'],
|
||||||
'instant_deploy' => ['type' => 'boolean', 'description' => 'Instant deploy the database'],
|
'instant_deploy' => ['type' => 'boolean', 'description' => 'Instant deploy the database'],
|
||||||
|
'tags' => ['type' => 'array', 'items' => new OA\Items(type: 'string'), 'description' => 'Tags to assign to the database.'],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -1423,6 +1438,7 @@ public function create_database_dragonfly(Request $request)
|
||||||
'limits_cpuset' => ['type' => 'string', 'description' => 'CPU set of the database'],
|
'limits_cpuset' => ['type' => 'string', 'description' => 'CPU set of the database'],
|
||||||
'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares of the database'],
|
'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares of the database'],
|
||||||
'instant_deploy' => ['type' => 'boolean', 'description' => 'Instant deploy the database'],
|
'instant_deploy' => ['type' => 'boolean', 'description' => 'Instant deploy the database'],
|
||||||
|
'tags' => ['type' => 'array', 'items' => new OA\Items(type: 'string'), 'description' => 'Tags to assign to the database.'],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -1491,6 +1507,7 @@ public function create_database_redis(Request $request)
|
||||||
'limits_cpuset' => ['type' => 'string', 'description' => 'CPU set of the database'],
|
'limits_cpuset' => ['type' => 'string', 'description' => 'CPU set of the database'],
|
||||||
'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares of the database'],
|
'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares of the database'],
|
||||||
'instant_deploy' => ['type' => 'boolean', 'description' => 'Instant deploy the database'],
|
'instant_deploy' => ['type' => 'boolean', 'description' => 'Instant deploy the database'],
|
||||||
|
'tags' => ['type' => 'array', 'items' => new OA\Items(type: 'string'), 'description' => 'Tags to assign to the database.'],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -1562,6 +1579,7 @@ public function create_database_keydb(Request $request)
|
||||||
'limits_cpuset' => ['type' => 'string', 'description' => 'CPU set of the database'],
|
'limits_cpuset' => ['type' => 'string', 'description' => 'CPU set of the database'],
|
||||||
'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares of the database'],
|
'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares of the database'],
|
||||||
'instant_deploy' => ['type' => 'boolean', 'description' => 'Instant deploy the database'],
|
'instant_deploy' => ['type' => 'boolean', 'description' => 'Instant deploy the database'],
|
||||||
|
'tags' => ['type' => 'array', 'items' => new OA\Items(type: 'string'), 'description' => 'Tags to assign to the database.'],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -1633,6 +1651,7 @@ public function create_database_mariadb(Request $request)
|
||||||
'limits_cpuset' => ['type' => 'string', 'description' => 'CPU set of the database'],
|
'limits_cpuset' => ['type' => 'string', 'description' => 'CPU set of the database'],
|
||||||
'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares of the database'],
|
'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares of the database'],
|
||||||
'instant_deploy' => ['type' => 'boolean', 'description' => 'Instant deploy the database'],
|
'instant_deploy' => ['type' => 'boolean', 'description' => 'Instant deploy the database'],
|
||||||
|
'tags' => ['type' => 'array', 'items' => new OA\Items(type: 'string'), 'description' => 'Tags to assign to the database.'],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -1701,6 +1720,7 @@ public function create_database_mysql(Request $request)
|
||||||
'limits_cpuset' => ['type' => 'string', 'description' => 'CPU set of the database'],
|
'limits_cpuset' => ['type' => 'string', 'description' => 'CPU set of the database'],
|
||||||
'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares of the database'],
|
'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares of the database'],
|
||||||
'instant_deploy' => ['type' => 'boolean', 'description' => 'Instant deploy the database'],
|
'instant_deploy' => ['type' => 'boolean', 'description' => 'Instant deploy the database'],
|
||||||
|
'tags' => ['type' => 'array', 'items' => new OA\Items(type: 'string'), 'description' => 'Tags to assign to the database.'],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -1731,7 +1751,7 @@ public function create_database_mongodb(Request $request)
|
||||||
|
|
||||||
public function create_database(Request $request, NewDatabaseTypes $type)
|
public function create_database(Request $request, NewDatabaseTypes $type)
|
||||||
{
|
{
|
||||||
$allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
|
$allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf', 'tags'];
|
||||||
|
|
||||||
$teamId = getTeamIdFromToken();
|
$teamId = getTeamIdFromToken();
|
||||||
if (is_null($teamId)) {
|
if (is_null($teamId)) {
|
||||||
|
|
@ -1830,6 +1850,8 @@ public function create_database(Request $request, NewDatabaseTypes $type)
|
||||||
'limits_cpuset' => 'string|nullable',
|
'limits_cpuset' => 'string|nullable',
|
||||||
'limits_cpu_shares' => 'numeric',
|
'limits_cpu_shares' => 'numeric',
|
||||||
'instant_deploy' => 'boolean',
|
'instant_deploy' => 'boolean',
|
||||||
|
'tags' => 'array|nullable',
|
||||||
|
'tags.*' => 'string|min:2',
|
||||||
]);
|
]);
|
||||||
if ($validator->failed()) {
|
if ($validator->failed()) {
|
||||||
return response()->json([
|
return response()->json([
|
||||||
|
|
@ -1837,6 +1859,13 @@ public function create_database(Request $request, NewDatabaseTypes $type)
|
||||||
'errors' => $validator->errors(),
|
'errors' => $validator->errors(),
|
||||||
], 422);
|
], 422);
|
||||||
}
|
}
|
||||||
|
$return = $this->validateTagsParameter($request);
|
||||||
|
if ($return instanceof JsonResponse) {
|
||||||
|
return $return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tagNames = $request->input('tags') ?? [];
|
||||||
|
|
||||||
if ($request->public_port) {
|
if ($request->public_port) {
|
||||||
if ($request->public_port < 1024 || $request->public_port > 65535) {
|
if ($request->public_port < 1024 || $request->public_port > 65535) {
|
||||||
return response()->json([
|
return response()->json([
|
||||||
|
|
@ -1848,7 +1877,7 @@ public function create_database(Request $request, NewDatabaseTypes $type)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($type === NewDatabaseTypes::POSTGRESQL) {
|
if ($type === NewDatabaseTypes::POSTGRESQL) {
|
||||||
$allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf'];
|
$allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'tags'];
|
||||||
$validator = customApiValidator($request->all(), [
|
$validator = customApiValidator($request->all(), [
|
||||||
'postgres_user' => ValidationPatterns::databaseIdentifierRules(required: false),
|
'postgres_user' => ValidationPatterns::databaseIdentifierRules(required: false),
|
||||||
'postgres_password' => ValidationPatterns::databasePasswordRules(required: false),
|
'postgres_password' => ValidationPatterns::databasePasswordRules(required: false),
|
||||||
|
|
@ -1896,6 +1925,9 @@ public function create_database(Request $request, NewDatabaseTypes $type)
|
||||||
if ($instantDeploy) {
|
if ($instantDeploy) {
|
||||||
StartDatabase::dispatch($database);
|
StartDatabase::dispatch($database);
|
||||||
}
|
}
|
||||||
|
if ($tagNames !== []) {
|
||||||
|
$this->attachTagsToResource($database, $tagNames, $teamId);
|
||||||
|
}
|
||||||
$database->refresh();
|
$database->refresh();
|
||||||
$payload = [
|
$payload = [
|
||||||
'uuid' => $database->uuid,
|
'uuid' => $database->uuid,
|
||||||
|
|
@ -1917,7 +1949,7 @@ public function create_database(Request $request, NewDatabaseTypes $type)
|
||||||
|
|
||||||
return response()->json(serializeApiResponse($payload))->setStatusCode(201);
|
return response()->json(serializeApiResponse($payload))->setStatusCode(201);
|
||||||
} elseif ($type === NewDatabaseTypes::MARIADB) {
|
} elseif ($type === NewDatabaseTypes::MARIADB) {
|
||||||
$allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database'];
|
$allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'tags'];
|
||||||
$validator = customApiValidator($request->all(), [
|
$validator = customApiValidator($request->all(), [
|
||||||
'mariadb_conf' => 'string',
|
'mariadb_conf' => 'string',
|
||||||
'mariadb_root_password' => ValidationPatterns::databasePasswordRules(required: false),
|
'mariadb_root_password' => ValidationPatterns::databasePasswordRules(required: false),
|
||||||
|
|
@ -1964,6 +1996,9 @@ public function create_database(Request $request, NewDatabaseTypes $type)
|
||||||
if ($instantDeploy) {
|
if ($instantDeploy) {
|
||||||
StartDatabase::dispatch($database);
|
StartDatabase::dispatch($database);
|
||||||
}
|
}
|
||||||
|
if ($tagNames !== []) {
|
||||||
|
$this->attachTagsToResource($database, $tagNames, $teamId);
|
||||||
|
}
|
||||||
|
|
||||||
$database->refresh();
|
$database->refresh();
|
||||||
$payload = [
|
$payload = [
|
||||||
|
|
@ -1986,7 +2021,7 @@ public function create_database(Request $request, NewDatabaseTypes $type)
|
||||||
|
|
||||||
return response()->json(serializeApiResponse($payload))->setStatusCode(201);
|
return response()->json(serializeApiResponse($payload))->setStatusCode(201);
|
||||||
} elseif ($type === NewDatabaseTypes::MYSQL) {
|
} elseif ($type === NewDatabaseTypes::MYSQL) {
|
||||||
$allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
|
$allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf', 'tags'];
|
||||||
$validator = customApiValidator($request->all(), [
|
$validator = customApiValidator($request->all(), [
|
||||||
'mysql_root_password' => ValidationPatterns::databasePasswordRules(required: false),
|
'mysql_root_password' => ValidationPatterns::databasePasswordRules(required: false),
|
||||||
'mysql_password' => ValidationPatterns::databasePasswordRules(required: false),
|
'mysql_password' => ValidationPatterns::databasePasswordRules(required: false),
|
||||||
|
|
@ -2033,6 +2068,9 @@ public function create_database(Request $request, NewDatabaseTypes $type)
|
||||||
if ($instantDeploy) {
|
if ($instantDeploy) {
|
||||||
StartDatabase::dispatch($database);
|
StartDatabase::dispatch($database);
|
||||||
}
|
}
|
||||||
|
if ($tagNames !== []) {
|
||||||
|
$this->attachTagsToResource($database, $tagNames, $teamId);
|
||||||
|
}
|
||||||
|
|
||||||
$database->refresh();
|
$database->refresh();
|
||||||
$payload = [
|
$payload = [
|
||||||
|
|
@ -2055,7 +2093,7 @@ public function create_database(Request $request, NewDatabaseTypes $type)
|
||||||
|
|
||||||
return response()->json(serializeApiResponse($payload))->setStatusCode(201);
|
return response()->json(serializeApiResponse($payload))->setStatusCode(201);
|
||||||
} elseif ($type === NewDatabaseTypes::REDIS) {
|
} elseif ($type === NewDatabaseTypes::REDIS) {
|
||||||
$allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'redis_password', 'redis_conf'];
|
$allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'redis_password', 'redis_conf', 'tags'];
|
||||||
$validator = customApiValidator($request->all(), [
|
$validator = customApiValidator($request->all(), [
|
||||||
'redis_password' => ValidationPatterns::databasePasswordRules(required: false),
|
'redis_password' => ValidationPatterns::databasePasswordRules(required: false),
|
||||||
'redis_conf' => 'string',
|
'redis_conf' => 'string',
|
||||||
|
|
@ -2099,6 +2137,9 @@ public function create_database(Request $request, NewDatabaseTypes $type)
|
||||||
if ($instantDeploy) {
|
if ($instantDeploy) {
|
||||||
StartDatabase::dispatch($database);
|
StartDatabase::dispatch($database);
|
||||||
}
|
}
|
||||||
|
if ($tagNames !== []) {
|
||||||
|
$this->attachTagsToResource($database, $tagNames, $teamId);
|
||||||
|
}
|
||||||
|
|
||||||
$database->refresh();
|
$database->refresh();
|
||||||
$payload = [
|
$payload = [
|
||||||
|
|
@ -2121,7 +2162,7 @@ public function create_database(Request $request, NewDatabaseTypes $type)
|
||||||
|
|
||||||
return response()->json(serializeApiResponse($payload))->setStatusCode(201);
|
return response()->json(serializeApiResponse($payload))->setStatusCode(201);
|
||||||
} elseif ($type === NewDatabaseTypes::DRAGONFLY) {
|
} elseif ($type === NewDatabaseTypes::DRAGONFLY) {
|
||||||
$allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'dragonfly_password'];
|
$allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'dragonfly_password', 'tags'];
|
||||||
$validator = customApiValidator($request->all(), [
|
$validator = customApiValidator($request->all(), [
|
||||||
'dragonfly_password' => ValidationPatterns::databasePasswordRules(required: false),
|
'dragonfly_password' => ValidationPatterns::databasePasswordRules(required: false),
|
||||||
]);
|
]);
|
||||||
|
|
@ -2146,12 +2187,15 @@ public function create_database(Request $request, NewDatabaseTypes $type)
|
||||||
if ($instantDeploy) {
|
if ($instantDeploy) {
|
||||||
StartDatabase::dispatch($database);
|
StartDatabase::dispatch($database);
|
||||||
}
|
}
|
||||||
|
if ($tagNames !== []) {
|
||||||
|
$this->attachTagsToResource($database, $tagNames, $teamId);
|
||||||
|
}
|
||||||
|
|
||||||
return response()->json(serializeApiResponse([
|
return response()->json(serializeApiResponse([
|
||||||
'uuid' => $database->uuid,
|
'uuid' => $database->uuid,
|
||||||
]))->setStatusCode(201);
|
]))->setStatusCode(201);
|
||||||
} elseif ($type === NewDatabaseTypes::KEYDB) {
|
} elseif ($type === NewDatabaseTypes::KEYDB) {
|
||||||
$allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'keydb_password', 'keydb_conf'];
|
$allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'keydb_password', 'keydb_conf', 'tags'];
|
||||||
$validator = customApiValidator($request->all(), [
|
$validator = customApiValidator($request->all(), [
|
||||||
'keydb_password' => ValidationPatterns::databasePasswordRules(required: false),
|
'keydb_password' => ValidationPatterns::databasePasswordRules(required: false),
|
||||||
'keydb_conf' => 'string',
|
'keydb_conf' => 'string',
|
||||||
|
|
@ -2195,6 +2239,9 @@ public function create_database(Request $request, NewDatabaseTypes $type)
|
||||||
if ($instantDeploy) {
|
if ($instantDeploy) {
|
||||||
StartDatabase::dispatch($database);
|
StartDatabase::dispatch($database);
|
||||||
}
|
}
|
||||||
|
if ($tagNames !== []) {
|
||||||
|
$this->attachTagsToResource($database, $tagNames, $teamId);
|
||||||
|
}
|
||||||
|
|
||||||
$database->refresh();
|
$database->refresh();
|
||||||
$payload = [
|
$payload = [
|
||||||
|
|
@ -2217,7 +2264,7 @@ public function create_database(Request $request, NewDatabaseTypes $type)
|
||||||
|
|
||||||
return response()->json(serializeApiResponse($payload))->setStatusCode(201);
|
return response()->json(serializeApiResponse($payload))->setStatusCode(201);
|
||||||
} elseif ($type === NewDatabaseTypes::CLICKHOUSE) {
|
} elseif ($type === NewDatabaseTypes::CLICKHOUSE) {
|
||||||
$allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'clickhouse_admin_user', 'clickhouse_admin_password'];
|
$allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'clickhouse_admin_user', 'clickhouse_admin_password', 'tags'];
|
||||||
$validator = customApiValidator($request->all(), [
|
$validator = customApiValidator($request->all(), [
|
||||||
'clickhouse_admin_user' => ValidationPatterns::databaseIdentifierRules(required: false),
|
'clickhouse_admin_user' => ValidationPatterns::databaseIdentifierRules(required: false),
|
||||||
'clickhouse_admin_password' => ValidationPatterns::databasePasswordRules(required: false),
|
'clickhouse_admin_password' => ValidationPatterns::databasePasswordRules(required: false),
|
||||||
|
|
@ -2241,6 +2288,9 @@ public function create_database(Request $request, NewDatabaseTypes $type)
|
||||||
if ($instantDeploy) {
|
if ($instantDeploy) {
|
||||||
StartDatabase::dispatch($database);
|
StartDatabase::dispatch($database);
|
||||||
}
|
}
|
||||||
|
if ($tagNames !== []) {
|
||||||
|
$this->attachTagsToResource($database, $tagNames, $teamId);
|
||||||
|
}
|
||||||
|
|
||||||
$database->refresh();
|
$database->refresh();
|
||||||
$payload = [
|
$payload = [
|
||||||
|
|
@ -2263,7 +2313,7 @@ public function create_database(Request $request, NewDatabaseTypes $type)
|
||||||
|
|
||||||
return response()->json(serializeApiResponse($payload))->setStatusCode(201);
|
return response()->json(serializeApiResponse($payload))->setStatusCode(201);
|
||||||
} elseif ($type === NewDatabaseTypes::MONGODB) {
|
} elseif ($type === NewDatabaseTypes::MONGODB) {
|
||||||
$allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database'];
|
$allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database', 'tags'];
|
||||||
$validator = customApiValidator($request->all(), [
|
$validator = customApiValidator($request->all(), [
|
||||||
'mongo_conf' => 'string',
|
'mongo_conf' => 'string',
|
||||||
'mongo_initdb_root_username' => ValidationPatterns::databaseIdentifierRules(required: false),
|
'mongo_initdb_root_username' => ValidationPatterns::databaseIdentifierRules(required: false),
|
||||||
|
|
@ -2309,6 +2359,9 @@ public function create_database(Request $request, NewDatabaseTypes $type)
|
||||||
if ($instantDeploy) {
|
if ($instantDeploy) {
|
||||||
StartDatabase::dispatch($database);
|
StartDatabase::dispatch($database);
|
||||||
}
|
}
|
||||||
|
if ($tagNames !== []) {
|
||||||
|
$this->attachTagsToResource($database, $tagNames, $teamId);
|
||||||
|
}
|
||||||
|
|
||||||
$database->refresh();
|
$database->refresh();
|
||||||
$payload = [
|
$payload = [
|
||||||
|
|
@ -4342,4 +4395,148 @@ public function delete_storage(Request $request): JsonResponse
|
||||||
|
|
||||||
return response()->json(['message' => 'Storage deleted.']);
|
return response()->json(['message' => 'Storage deleted.']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Get(
|
||||||
|
summary: 'List Tags',
|
||||||
|
description: 'List tags for a database by UUID.',
|
||||||
|
path: '/databases/{uuid}/tags',
|
||||||
|
operationId: 'list-tags-by-database-uuid',
|
||||||
|
security: [
|
||||||
|
['bearerAuth' => []],
|
||||||
|
],
|
||||||
|
tags: ['Databases'],
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(
|
||||||
|
name: 'uuid',
|
||||||
|
in: 'path',
|
||||||
|
description: 'UUID of the database.',
|
||||||
|
required: true,
|
||||||
|
schema: new OA\Schema(type: 'string')
|
||||||
|
),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'List of tags.',
|
||||||
|
content: [
|
||||||
|
new OA\MediaType(
|
||||||
|
mediaType: 'application/json',
|
||||||
|
schema: new OA\Schema(
|
||||||
|
type: 'array',
|
||||||
|
items: new OA\Items(ref: '#/components/schemas/Tag')
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, ref: '#/components/responses/401'),
|
||||||
|
new OA\Response(response: 400, ref: '#/components/responses/400'),
|
||||||
|
new OA\Response(response: 404, ref: '#/components/responses/404'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
|
public function tags(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
return $this->listTags($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[OA\Post(
|
||||||
|
summary: 'Create Tag',
|
||||||
|
description: 'Add tag(s) to a database by UUID.',
|
||||||
|
path: '/databases/{uuid}/tags',
|
||||||
|
operationId: 'create-tag-by-database-uuid',
|
||||||
|
security: [
|
||||||
|
['bearerAuth' => []],
|
||||||
|
],
|
||||||
|
tags: ['Databases'],
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(
|
||||||
|
name: 'uuid',
|
||||||
|
in: 'path',
|
||||||
|
description: 'UUID of the database.',
|
||||||
|
required: true,
|
||||||
|
schema: new OA\Schema(type: 'string')
|
||||||
|
),
|
||||||
|
],
|
||||||
|
requestBody: new OA\RequestBody(
|
||||||
|
required: true,
|
||||||
|
content: [
|
||||||
|
new OA\MediaType(
|
||||||
|
mediaType: 'application/json',
|
||||||
|
schema: new OA\Schema(
|
||||||
|
type: 'object',
|
||||||
|
properties: [
|
||||||
|
'tag_name' => ['type' => 'string', 'description' => 'The tag name (min 2 characters). Required if tag_names is not provided.'],
|
||||||
|
'tag_names' => [
|
||||||
|
'type' => 'array',
|
||||||
|
'items' => new OA\Items(type: 'string'),
|
||||||
|
'description' => 'Array of tag names (each min 2 characters). Required if tag_name is not provided.',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 201,
|
||||||
|
description: 'Tags added successfully.',
|
||||||
|
content: [
|
||||||
|
new OA\MediaType(
|
||||||
|
mediaType: 'application/json',
|
||||||
|
schema: new OA\Schema(
|
||||||
|
type: 'array',
|
||||||
|
items: new OA\Items(ref: '#/components/schemas/Tag')
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, ref: '#/components/responses/401'),
|
||||||
|
new OA\Response(response: 400, ref: '#/components/responses/400'),
|
||||||
|
new OA\Response(response: 404, ref: '#/components/responses/404'),
|
||||||
|
new OA\Response(response: 422, ref: '#/components/responses/422'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
|
public function create_tag(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
return $this->createTag($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[OA\Delete(
|
||||||
|
summary: 'Delete Tag',
|
||||||
|
description: 'Remove a tag from a database by UUID.',
|
||||||
|
path: '/databases/{uuid}/tags/{tag_uuid}',
|
||||||
|
operationId: 'delete-tag-by-database-uuid',
|
||||||
|
security: [
|
||||||
|
['bearerAuth' => []],
|
||||||
|
],
|
||||||
|
tags: ['Databases'],
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(
|
||||||
|
name: 'uuid',
|
||||||
|
in: 'path',
|
||||||
|
description: 'UUID of the database.',
|
||||||
|
required: true,
|
||||||
|
schema: new OA\Schema(type: 'string')
|
||||||
|
),
|
||||||
|
new OA\Parameter(
|
||||||
|
name: 'tag_uuid',
|
||||||
|
in: 'path',
|
||||||
|
description: 'UUID of the tag.',
|
||||||
|
required: true,
|
||||||
|
schema: new OA\Schema(type: 'string')
|
||||||
|
),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Tag removed.',
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, ref: '#/components/responses/401'),
|
||||||
|
new OA\Response(response: 400, ref: '#/components/responses/400'),
|
||||||
|
new OA\Response(response: 404, ref: '#/components/responses/404'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
|
public function delete_tag(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
return $this->deleteTag($request);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,18 @@
|
||||||
|
|
||||||
class ServicesController extends Controller
|
class ServicesController extends Controller
|
||||||
{
|
{
|
||||||
|
use Concerns\HandlesTagsApi;
|
||||||
|
|
||||||
|
protected function findTaggableResource(string $uuid, int|string $teamId): mixed
|
||||||
|
{
|
||||||
|
return Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($uuid)->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tagResourceNotFoundMessage(): string
|
||||||
|
{
|
||||||
|
return 'Service not found.';
|
||||||
|
}
|
||||||
|
|
||||||
private function exposeFileStorageContentIfAllowed(LocalFileVolume|LocalPersistentVolume $storage): LocalFileVolume|LocalPersistentVolume
|
private function exposeFileStorageContentIfAllowed(LocalFileVolume|LocalPersistentVolume $storage): LocalFileVolume|LocalPersistentVolume
|
||||||
{
|
{
|
||||||
if (request()->attributes->get('can_read_sensitive', false) === true) {
|
if (request()->attributes->get('can_read_sensitive', false) === true) {
|
||||||
|
|
@ -276,6 +288,7 @@ public function services(Request $request)
|
||||||
],
|
],
|
||||||
'force_domain_override' => ['type' => 'boolean', 'default' => false, 'description' => 'Force domain override even if conflicts are detected.'],
|
'force_domain_override' => ['type' => 'boolean', 'default' => false, 'description' => 'Force domain override even if conflicts are detected.'],
|
||||||
'is_container_label_escape_enabled' => ['type' => 'boolean', 'default' => true, 'description' => 'Escape special characters in labels. By default, $ (and other chars) is escaped. If you want to use env variables inside the labels, turn this off.'],
|
'is_container_label_escape_enabled' => ['type' => 'boolean', 'default' => true, 'description' => 'Escape special characters in labels. By default, $ (and other chars) is escaped. If you want to use env variables inside the labels, turn this off.'],
|
||||||
|
'tags' => ['type' => 'array', 'items' => new OA\Items(type: 'string'), 'description' => 'Tags to assign to the service.'],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -342,7 +355,7 @@ public function services(Request $request)
|
||||||
)]
|
)]
|
||||||
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', 'urls', 'force_domain_override', 'is_container_label_escape_enabled'];
|
$allowedFields = ['type', 'name', 'description', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'docker_compose_raw', 'urls', 'force_domain_override', 'is_container_label_escape_enabled', 'tags'];
|
||||||
|
|
||||||
$teamId = getTeamIdFromToken();
|
$teamId = getTeamIdFromToken();
|
||||||
if (is_null($teamId)) {
|
if (is_null($teamId)) {
|
||||||
|
|
@ -372,6 +385,8 @@ public function create_service(Request $request)
|
||||||
'urls.*.url' => 'string|nullable',
|
'urls.*.url' => 'string|nullable',
|
||||||
'force_domain_override' => 'boolean',
|
'force_domain_override' => 'boolean',
|
||||||
'is_container_label_escape_enabled' => 'boolean',
|
'is_container_label_escape_enabled' => 'boolean',
|
||||||
|
'tags' => 'array|nullable',
|
||||||
|
'tags.*' => 'string|min:2',
|
||||||
];
|
];
|
||||||
$validationMessages = [
|
$validationMessages = [
|
||||||
'urls.*.array' => 'An item in the urls array has invalid fields. Only name and url fields are supported.',
|
'urls.*.array' => 'An item in the urls array has invalid fields. Only name and url fields are supported.',
|
||||||
|
|
@ -393,6 +408,11 @@ public function create_service(Request $request)
|
||||||
], 422);
|
], 422);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$return = $this->validateTagsParameter($request);
|
||||||
|
if ($return instanceof JsonResponse) {
|
||||||
|
return $return;
|
||||||
|
}
|
||||||
|
|
||||||
if (filled($request->type) && filled($request->docker_compose_raw)) {
|
if (filled($request->type) && filled($request->docker_compose_raw)) {
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'message' => 'You cannot provide both service type and docker_compose_raw. Use one or the other.',
|
'message' => 'You cannot provide both service type and docker_compose_raw. Use one or the other.',
|
||||||
|
|
@ -531,6 +551,10 @@ public function create_service(Request $request)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($request->has('tags')) {
|
||||||
|
$this->attachTagsToResource($service, $request->tags, $teamId);
|
||||||
|
}
|
||||||
|
|
||||||
if ($instantDeploy) {
|
if ($instantDeploy) {
|
||||||
StartService::dispatch($service);
|
StartService::dispatch($service);
|
||||||
}
|
}
|
||||||
|
|
@ -551,7 +575,7 @@ public function create_service(Request $request)
|
||||||
|
|
||||||
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', 'urls', 'force_domain_override', 'is_container_label_escape_enabled'];
|
$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', 'is_container_label_escape_enabled', 'tags'];
|
||||||
|
|
||||||
$validationRules = [
|
$validationRules = [
|
||||||
'project_uuid' => 'string|required',
|
'project_uuid' => 'string|required',
|
||||||
|
|
@ -570,6 +594,8 @@ public function create_service(Request $request)
|
||||||
'urls.*.url' => 'string|nullable',
|
'urls.*.url' => 'string|nullable',
|
||||||
'force_domain_override' => 'boolean',
|
'force_domain_override' => 'boolean',
|
||||||
'is_container_label_escape_enabled' => 'boolean',
|
'is_container_label_escape_enabled' => 'boolean',
|
||||||
|
'tags' => 'array|nullable',
|
||||||
|
'tags.*' => 'string|min:2',
|
||||||
];
|
];
|
||||||
$validationMessages = [
|
$validationMessages = [
|
||||||
'urls.*.array' => 'An item in the urls array has invalid fields. Only name and url fields are supported.',
|
'urls.*.array' => 'An item in the urls array has invalid fields. Only name and url fields are supported.',
|
||||||
|
|
@ -703,6 +729,10 @@ public function create_service(Request $request)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($request->has('tags')) {
|
||||||
|
$this->attachTagsToResource($service, $request->tags, $teamId);
|
||||||
|
}
|
||||||
|
|
||||||
if ($instantDeploy) {
|
if ($instantDeploy) {
|
||||||
StartService::dispatch($service);
|
StartService::dispatch($service);
|
||||||
}
|
}
|
||||||
|
|
@ -2798,4 +2828,148 @@ public function delete_storage(Request $request): JsonResponse
|
||||||
|
|
||||||
return response()->json(['message' => 'Storage deleted.']);
|
return response()->json(['message' => 'Storage deleted.']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[OA\Get(
|
||||||
|
summary: 'List Tags',
|
||||||
|
description: 'List tags for a service by UUID.',
|
||||||
|
path: '/services/{uuid}/tags',
|
||||||
|
operationId: 'list-tags-by-service-uuid',
|
||||||
|
security: [
|
||||||
|
['bearerAuth' => []],
|
||||||
|
],
|
||||||
|
tags: ['Services'],
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(
|
||||||
|
name: 'uuid',
|
||||||
|
in: 'path',
|
||||||
|
description: 'UUID of the service.',
|
||||||
|
required: true,
|
||||||
|
schema: new OA\Schema(type: 'string')
|
||||||
|
),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'List of tags.',
|
||||||
|
content: [
|
||||||
|
new OA\MediaType(
|
||||||
|
mediaType: 'application/json',
|
||||||
|
schema: new OA\Schema(
|
||||||
|
type: 'array',
|
||||||
|
items: new OA\Items(ref: '#/components/schemas/Tag')
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, ref: '#/components/responses/401'),
|
||||||
|
new OA\Response(response: 400, ref: '#/components/responses/400'),
|
||||||
|
new OA\Response(response: 404, ref: '#/components/responses/404'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
|
public function tags(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
return $this->listTags($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[OA\Post(
|
||||||
|
summary: 'Create Tag',
|
||||||
|
description: 'Add tag(s) to a service by UUID.',
|
||||||
|
path: '/services/{uuid}/tags',
|
||||||
|
operationId: 'create-tag-by-service-uuid',
|
||||||
|
security: [
|
||||||
|
['bearerAuth' => []],
|
||||||
|
],
|
||||||
|
tags: ['Services'],
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(
|
||||||
|
name: 'uuid',
|
||||||
|
in: 'path',
|
||||||
|
description: 'UUID of the service.',
|
||||||
|
required: true,
|
||||||
|
schema: new OA\Schema(type: 'string')
|
||||||
|
),
|
||||||
|
],
|
||||||
|
requestBody: new OA\RequestBody(
|
||||||
|
required: true,
|
||||||
|
content: [
|
||||||
|
new OA\MediaType(
|
||||||
|
mediaType: 'application/json',
|
||||||
|
schema: new OA\Schema(
|
||||||
|
type: 'object',
|
||||||
|
properties: [
|
||||||
|
'tag_name' => ['type' => 'string', 'description' => 'The tag name (min 2 characters). Required if tag_names is not provided.'],
|
||||||
|
'tag_names' => [
|
||||||
|
'type' => 'array',
|
||||||
|
'items' => new OA\Items(type: 'string'),
|
||||||
|
'description' => 'Array of tag names (each min 2 characters). Required if tag_name is not provided.',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 201,
|
||||||
|
description: 'Tags added successfully.',
|
||||||
|
content: [
|
||||||
|
new OA\MediaType(
|
||||||
|
mediaType: 'application/json',
|
||||||
|
schema: new OA\Schema(
|
||||||
|
type: 'array',
|
||||||
|
items: new OA\Items(ref: '#/components/schemas/Tag')
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, ref: '#/components/responses/401'),
|
||||||
|
new OA\Response(response: 400, ref: '#/components/responses/400'),
|
||||||
|
new OA\Response(response: 404, ref: '#/components/responses/404'),
|
||||||
|
new OA\Response(response: 422, ref: '#/components/responses/422'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
|
public function create_tag(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
return $this->createTag($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[OA\Delete(
|
||||||
|
summary: 'Delete Tag',
|
||||||
|
description: 'Remove a tag from a service by UUID.',
|
||||||
|
path: '/services/{uuid}/tags/{tag_uuid}',
|
||||||
|
operationId: 'delete-tag-by-service-uuid',
|
||||||
|
security: [
|
||||||
|
['bearerAuth' => []],
|
||||||
|
],
|
||||||
|
tags: ['Services'],
|
||||||
|
parameters: [
|
||||||
|
new OA\Parameter(
|
||||||
|
name: 'uuid',
|
||||||
|
in: 'path',
|
||||||
|
description: 'UUID of the service.',
|
||||||
|
required: true,
|
||||||
|
schema: new OA\Schema(type: 'string')
|
||||||
|
),
|
||||||
|
new OA\Parameter(
|
||||||
|
name: 'tag_uuid',
|
||||||
|
in: 'path',
|
||||||
|
description: 'UUID of the tag.',
|
||||||
|
required: true,
|
||||||
|
schema: new OA\Schema(type: 'string')
|
||||||
|
),
|
||||||
|
],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'Tag removed.',
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, ref: '#/components/responses/401'),
|
||||||
|
new OA\Response(response: 400, ref: '#/components/responses/400'),
|
||||||
|
new OA\Response(response: 404, ref: '#/components/responses/404'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
|
public function delete_tag(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
return $this->deleteTag($request);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
61
app/Http/Controllers/Api/TagsController.php
Normal file
61
app/Http/Controllers/Api/TagsController.php
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Tag;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use OpenApi\Attributes as OA;
|
||||||
|
|
||||||
|
class TagsController extends Controller
|
||||||
|
{
|
||||||
|
public static function serializeTag(Tag $tag): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'uuid' => $tag->uuid,
|
||||||
|
'name' => $tag->name,
|
||||||
|
'created_at' => $tag->created_at,
|
||||||
|
'updated_at' => $tag->updated_at,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[OA\Get(
|
||||||
|
summary: 'List',
|
||||||
|
description: 'List all tags for the current team.',
|
||||||
|
path: '/tags',
|
||||||
|
operationId: 'list-tags',
|
||||||
|
security: [
|
||||||
|
['bearerAuth' => []],
|
||||||
|
],
|
||||||
|
tags: ['Tags'],
|
||||||
|
responses: [
|
||||||
|
new OA\Response(
|
||||||
|
response: 200,
|
||||||
|
description: 'All tags for the current team.',
|
||||||
|
content: [
|
||||||
|
new OA\MediaType(
|
||||||
|
mediaType: 'application/json',
|
||||||
|
schema: new OA\Schema(
|
||||||
|
type: 'array',
|
||||||
|
items: new OA\Items(ref: '#/components/schemas/Tag')
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
new OA\Response(response: 401, ref: '#/components/responses/401'),
|
||||||
|
new OA\Response(response: 400, ref: '#/components/responses/400'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
|
public function tags(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$teamId = getTeamIdFromToken();
|
||||||
|
if (is_null($teamId)) {
|
||||||
|
return invalidTokenResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
$tags = Tag::where('team_id', $teamId)->orderBy('name')->get();
|
||||||
|
|
||||||
|
return response()->json($tags->map(self::serializeTag(...)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -91,9 +91,7 @@ public function deleteTag(string $id)
|
||||||
$this->authorize('update', $this->resource);
|
$this->authorize('update', $this->resource);
|
||||||
$this->resource->tags()->detach($id);
|
$this->resource->tags()->detach($id);
|
||||||
$found_more_tags = Tag::ownedByCurrentTeam()->find($id);
|
$found_more_tags = Tag::ownedByCurrentTeam()->find($id);
|
||||||
if ($found_more_tags && $found_more_tags->applications()->count() == 0 && $found_more_tags->services()->count() == 0) {
|
$found_more_tags?->deleteIfOrphaned();
|
||||||
$found_more_tags->delete();
|
|
||||||
}
|
|
||||||
$this->refresh();
|
$this->refresh();
|
||||||
$this->dispatch('success', 'Tag deleted.');
|
$this->dispatch('success', 'Tag deleted.');
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,19 @@
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use App\Traits\HasSafeStringAttribute;
|
use App\Traits\HasSafeStringAttribute;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use OpenApi\Attributes as OA;
|
||||||
|
|
||||||
|
#[OA\Schema(
|
||||||
|
description: 'Tag model',
|
||||||
|
type: 'object',
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'uuid', type: 'string'),
|
||||||
|
new OA\Property(property: 'name', type: 'string'),
|
||||||
|
new OA\Property(property: 'created_at', type: 'string'),
|
||||||
|
new OA\Property(property: 'updated_at', type: 'string'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
class Tag extends BaseModel
|
class Tag extends BaseModel
|
||||||
{
|
{
|
||||||
use HasSafeStringAttribute;
|
use HasSafeStringAttribute;
|
||||||
|
|
@ -23,6 +35,13 @@ public static function ownedByCurrentTeam()
|
||||||
return Tag::whereTeamId(currentTeam()->id)->orderBy('name');
|
return Tag::whereTeamId(currentTeam()->id)->orderBy('name');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function deleteIfOrphaned(): void
|
||||||
|
{
|
||||||
|
if (DB::table('taggables')->where('tag_id', $this->id)->doesntExist()) {
|
||||||
|
$this->delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function applications()
|
public function applications()
|
||||||
{
|
{
|
||||||
return $this->morphedByMany(Application::class, 'taggable');
|
return $this->morphedByMany(Application::class, 'taggable');
|
||||||
|
|
|
||||||
|
|
@ -222,4 +222,5 @@ function removeUnnecessaryFieldsFromRequest(Request $request)
|
||||||
$request->offsetUnset('is_preserve_repository_enabled');
|
$request->offsetUnset('is_preserve_repository_enabled');
|
||||||
$request->offsetUnset('include_source_commit_in_build');
|
$request->offsetUnset('include_source_commit_in_build');
|
||||||
$request->offsetUnset('docker_compose_raw');
|
$request->offsetUnset('docker_compose_raw');
|
||||||
|
$request->offsetUnset('tags');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
if ($this->indexExists()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::table('tags')
|
||||||
|
->select('team_id', 'name', DB::raw('MIN(id) as keep_id'), DB::raw('COUNT(*) as tag_count'))
|
||||||
|
->whereNotNull('team_id')
|
||||||
|
->groupBy('team_id', 'name')
|
||||||
|
->havingRaw('COUNT(*) > 1')
|
||||||
|
->orderBy('keep_id')
|
||||||
|
->cursor()
|
||||||
|
->each(function ($duplicate): void {
|
||||||
|
DB::table('tags')
|
||||||
|
->select('id')
|
||||||
|
->where('team_id', $duplicate->team_id)
|
||||||
|
->where('name', $duplicate->name)
|
||||||
|
->where('id', '!=', $duplicate->keep_id)
|
||||||
|
->orderBy('id')
|
||||||
|
->cursor()
|
||||||
|
->each(function ($duplicateTag) use ($duplicate): void {
|
||||||
|
DB::table('taggables')
|
||||||
|
->where('tag_id', $duplicateTag->id)
|
||||||
|
->orderBy('taggable_id')
|
||||||
|
->cursor()
|
||||||
|
->each(function ($taggable) use ($duplicate): void {
|
||||||
|
DB::table('taggables')->updateOrInsert([
|
||||||
|
'tag_id' => $duplicate->keep_id,
|
||||||
|
'taggable_id' => $taggable->taggable_id,
|
||||||
|
'taggable_type' => $taggable->taggable_type,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
DB::table('taggables')->where('tag_id', $duplicateTag->id)->delete();
|
||||||
|
DB::table('tags')->where('id', $duplicateTag->id)->delete();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('tags', function (Blueprint $table) {
|
||||||
|
$table->unique(['team_id', 'name'], 'tags_team_id_name_unique');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
if (! $this->indexExists()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Schema::table('tags', function (Blueprint $table) {
|
||||||
|
$table->dropUnique('tags_team_id_name_unique');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private function indexExists(): bool
|
||||||
|
{
|
||||||
|
return collect(Schema::getIndexes('tags'))
|
||||||
|
->contains(fn (array $index): bool => $index['name'] === 'tags_team_id_name_unique');
|
||||||
|
}
|
||||||
|
};
|
||||||
675
openapi.json
675
openapi.json
|
|
@ -412,6 +412,13 @@
|
||||||
"default": true,
|
"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."
|
"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."
|
||||||
},
|
},
|
||||||
|
"tags": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "Tags to assign to the application."
|
||||||
|
},
|
||||||
"is_preserve_repository_enabled": {
|
"is_preserve_repository_enabled": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"default": false,
|
"default": false,
|
||||||
|
|
@ -866,6 +873,13 @@
|
||||||
"default": true,
|
"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."
|
"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."
|
||||||
},
|
},
|
||||||
|
"tags": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "Tags to assign to the application."
|
||||||
|
},
|
||||||
"is_preserve_repository_enabled": {
|
"is_preserve_repository_enabled": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"default": false,
|
"default": false,
|
||||||
|
|
@ -1320,6 +1334,13 @@
|
||||||
"default": true,
|
"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."
|
"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."
|
||||||
},
|
},
|
||||||
|
"tags": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "Tags to assign to the application."
|
||||||
|
},
|
||||||
"is_preserve_repository_enabled": {
|
"is_preserve_repository_enabled": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"default": false,
|
"default": false,
|
||||||
|
|
@ -1678,6 +1699,13 @@
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"default": true,
|
"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."
|
"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."
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "Tags to assign to the application."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "object"
|
"type": "object"
|
||||||
|
|
@ -2017,6 +2045,13 @@
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"default": true,
|
"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."
|
"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."
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "Tags to assign to the application."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "object"
|
"type": "object"
|
||||||
|
|
@ -3720,6 +3755,179 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"\/applications\/{uuid}\/tags": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Applications"
|
||||||
|
],
|
||||||
|
"summary": "List Tags",
|
||||||
|
"description": "List tags for an application by UUID.",
|
||||||
|
"operationId": "list-tags-by-application-uuid",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "uuid",
|
||||||
|
"in": "path",
|
||||||
|
"description": "UUID of the application.",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "List of tags.",
|
||||||
|
"content": {
|
||||||
|
"application\/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#\/components\/schemas\/Tag"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"$ref": "#\/components\/responses\/401"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"$ref": "#\/components\/responses\/400"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#\/components\/responses\/404"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearerAuth": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"Applications"
|
||||||
|
],
|
||||||
|
"summary": "Create Tag",
|
||||||
|
"description": "Add tag(s) to an application by UUID.",
|
||||||
|
"operationId": "create-tag-by-application-uuid",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "uuid",
|
||||||
|
"in": "path",
|
||||||
|
"description": "UUID of the application.",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application\/json": {
|
||||||
|
"schema": {
|
||||||
|
"properties": {
|
||||||
|
"tag_name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The tag name (min 2 characters). Required if tag_names is not provided."
|
||||||
|
},
|
||||||
|
"tag_names": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "Array of tag names (each min 2 characters). Required if tag_name is not provided."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"description": "Tags added successfully.",
|
||||||
|
"content": {
|
||||||
|
"application\/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#\/components\/schemas\/Tag"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"$ref": "#\/components\/responses\/401"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"$ref": "#\/components\/responses\/400"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#\/components\/responses\/404"
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"$ref": "#\/components\/responses\/422"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearerAuth": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"\/applications\/{uuid}\/tags\/{tag_uuid}": {
|
||||||
|
"delete": {
|
||||||
|
"tags": [
|
||||||
|
"Applications"
|
||||||
|
],
|
||||||
|
"summary": "Delete Tag",
|
||||||
|
"description": "Remove a tag from an application by UUID.",
|
||||||
|
"operationId": "delete-tag-by-application-uuid",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "uuid",
|
||||||
|
"in": "path",
|
||||||
|
"description": "UUID of the application.",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tag_uuid",
|
||||||
|
"in": "path",
|
||||||
|
"description": "UUID of the tag.",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Tag removed."
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"$ref": "#\/components\/responses\/401"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"$ref": "#\/components\/responses\/400"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#\/components\/responses\/404"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearerAuth": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"\/cloud-tokens": {
|
"\/cloud-tokens": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
|
|
@ -5018,6 +5226,13 @@
|
||||||
"instant_deploy": {
|
"instant_deploy": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "Instant deploy the database"
|
"description": "Instant deploy the database"
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "Tags to assign to the database."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "object"
|
"type": "object"
|
||||||
|
|
@ -5150,6 +5365,13 @@
|
||||||
"instant_deploy": {
|
"instant_deploy": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "Instant deploy the database"
|
"description": "Instant deploy the database"
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "Tags to assign to the database."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "object"
|
"type": "object"
|
||||||
|
|
@ -5278,6 +5500,13 @@
|
||||||
"instant_deploy": {
|
"instant_deploy": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "Instant deploy the database"
|
"description": "Instant deploy the database"
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "Tags to assign to the database."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "object"
|
"type": "object"
|
||||||
|
|
@ -5410,6 +5639,13 @@
|
||||||
"instant_deploy": {
|
"instant_deploy": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "Instant deploy the database"
|
"description": "Instant deploy the database"
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "Tags to assign to the database."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "object"
|
"type": "object"
|
||||||
|
|
@ -5542,6 +5778,13 @@
|
||||||
"instant_deploy": {
|
"instant_deploy": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "Instant deploy the database"
|
"description": "Instant deploy the database"
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "Tags to assign to the database."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "object"
|
"type": "object"
|
||||||
|
|
@ -5686,6 +5929,13 @@
|
||||||
"instant_deploy": {
|
"instant_deploy": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "Instant deploy the database"
|
"description": "Instant deploy the database"
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "Tags to assign to the database."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "object"
|
"type": "object"
|
||||||
|
|
@ -5830,6 +6080,13 @@
|
||||||
"instant_deploy": {
|
"instant_deploy": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "Instant deploy the database"
|
"description": "Instant deploy the database"
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "Tags to assign to the database."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "object"
|
"type": "object"
|
||||||
|
|
@ -5962,6 +6219,13 @@
|
||||||
"instant_deploy": {
|
"instant_deploy": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "Instant deploy the database"
|
"description": "Instant deploy the database"
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "Tags to assign to the database."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "object"
|
"type": "object"
|
||||||
|
|
@ -7106,6 +7370,179 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"\/databases\/{uuid}\/tags": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Databases"
|
||||||
|
],
|
||||||
|
"summary": "List Tags",
|
||||||
|
"description": "List tags for a database by UUID.",
|
||||||
|
"operationId": "list-tags-by-database-uuid",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "uuid",
|
||||||
|
"in": "path",
|
||||||
|
"description": "UUID of the database.",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "List of tags.",
|
||||||
|
"content": {
|
||||||
|
"application\/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#\/components\/schemas\/Tag"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"$ref": "#\/components\/responses\/401"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"$ref": "#\/components\/responses\/400"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#\/components\/responses\/404"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearerAuth": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"Databases"
|
||||||
|
],
|
||||||
|
"summary": "Create Tag",
|
||||||
|
"description": "Add tag(s) to a database by UUID.",
|
||||||
|
"operationId": "create-tag-by-database-uuid",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "uuid",
|
||||||
|
"in": "path",
|
||||||
|
"description": "UUID of the database.",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application\/json": {
|
||||||
|
"schema": {
|
||||||
|
"properties": {
|
||||||
|
"tag_name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The tag name (min 2 characters). Required if tag_names is not provided."
|
||||||
|
},
|
||||||
|
"tag_names": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "Array of tag names (each min 2 characters). Required if tag_name is not provided."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"description": "Tags added successfully.",
|
||||||
|
"content": {
|
||||||
|
"application\/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#\/components\/schemas\/Tag"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"$ref": "#\/components\/responses\/401"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"$ref": "#\/components\/responses\/400"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#\/components\/responses\/404"
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"$ref": "#\/components\/responses\/422"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearerAuth": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"\/databases\/{uuid}\/tags\/{tag_uuid}": {
|
||||||
|
"delete": {
|
||||||
|
"tags": [
|
||||||
|
"Databases"
|
||||||
|
],
|
||||||
|
"summary": "Delete Tag",
|
||||||
|
"description": "Remove a tag from a database by UUID.",
|
||||||
|
"operationId": "delete-tag-by-database-uuid",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "uuid",
|
||||||
|
"in": "path",
|
||||||
|
"description": "UUID of the database.",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tag_uuid",
|
||||||
|
"in": "path",
|
||||||
|
"description": "UUID of the tag.",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Tag removed."
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"$ref": "#\/components\/responses\/401"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"$ref": "#\/components\/responses\/400"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#\/components\/responses\/404"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearerAuth": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"\/deployments": {
|
"\/deployments": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
|
|
@ -10968,6 +11405,13 @@
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"default": true,
|
"default": true,
|
||||||
"description": "Escape special characters in labels. By default, $ (and other chars) is escaped. If you want to use env variables inside the labels, turn this off."
|
"description": "Escape special characters in labels. By default, $ (and other chars) is escaped. If you want to use env variables inside the labels, turn this off."
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "Tags to assign to the service."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "object"
|
"type": "object"
|
||||||
|
|
@ -12390,6 +12834,215 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"\/services\/{uuid}\/tags": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Services"
|
||||||
|
],
|
||||||
|
"summary": "List Tags",
|
||||||
|
"description": "List tags for a service by UUID.",
|
||||||
|
"operationId": "list-tags-by-service-uuid",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "uuid",
|
||||||
|
"in": "path",
|
||||||
|
"description": "UUID of the service.",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "List of tags.",
|
||||||
|
"content": {
|
||||||
|
"application\/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#\/components\/schemas\/Tag"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"$ref": "#\/components\/responses\/401"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"$ref": "#\/components\/responses\/400"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#\/components\/responses\/404"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearerAuth": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"Services"
|
||||||
|
],
|
||||||
|
"summary": "Create Tag",
|
||||||
|
"description": "Add tag(s) to a service by UUID.",
|
||||||
|
"operationId": "create-tag-by-service-uuid",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "uuid",
|
||||||
|
"in": "path",
|
||||||
|
"description": "UUID of the service.",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application\/json": {
|
||||||
|
"schema": {
|
||||||
|
"properties": {
|
||||||
|
"tag_name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The tag name (min 2 characters). Required if tag_names is not provided."
|
||||||
|
},
|
||||||
|
"tag_names": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "Array of tag names (each min 2 characters). Required if tag_name is not provided."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"description": "Tags added successfully.",
|
||||||
|
"content": {
|
||||||
|
"application\/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#\/components\/schemas\/Tag"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"$ref": "#\/components\/responses\/401"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"$ref": "#\/components\/responses\/400"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#\/components\/responses\/404"
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"$ref": "#\/components\/responses\/422"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearerAuth": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"\/services\/{uuid}\/tags\/{tag_uuid}": {
|
||||||
|
"delete": {
|
||||||
|
"tags": [
|
||||||
|
"Services"
|
||||||
|
],
|
||||||
|
"summary": "Delete Tag",
|
||||||
|
"description": "Remove a tag from a service by UUID.",
|
||||||
|
"operationId": "delete-tag-by-service-uuid",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "uuid",
|
||||||
|
"in": "path",
|
||||||
|
"description": "UUID of the service.",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tag_uuid",
|
||||||
|
"in": "path",
|
||||||
|
"description": "UUID of the tag.",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Tag removed."
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"$ref": "#\/components\/responses\/401"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"$ref": "#\/components\/responses\/400"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#\/components\/responses\/404"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearerAuth": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"\/tags": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Tags"
|
||||||
|
],
|
||||||
|
"summary": "List",
|
||||||
|
"description": "List all tags for the current team.",
|
||||||
|
"operationId": "list-tags",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "All tags for the current team.",
|
||||||
|
"content": {
|
||||||
|
"application\/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#\/components\/schemas\/Tag"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"$ref": "#\/components\/responses\/401"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"$ref": "#\/components\/responses\/400"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearerAuth": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"\/teams": {
|
"\/teams": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
|
|
@ -13610,6 +14263,24 @@
|
||||||
},
|
},
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"Tag": {
|
||||||
|
"description": "Tag model",
|
||||||
|
"properties": {
|
||||||
|
"uuid": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"Team": {
|
"Team": {
|
||||||
"description": "Team model",
|
"description": "Team model",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
@ -13864,6 +14535,10 @@
|
||||||
"name": "Services",
|
"name": "Services",
|
||||||
"description": "Services"
|
"description": "Services"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Tags",
|
||||||
|
"description": "Tags"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Teams",
|
"name": "Teams",
|
||||||
"description": "Teams"
|
"description": "Teams"
|
||||||
|
|
|
||||||
439
openapi.yaml
439
openapi.yaml
|
|
@ -293,6 +293,10 @@ paths:
|
||||||
type: boolean
|
type: boolean
|
||||||
default: true
|
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.'
|
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.'
|
||||||
|
tags:
|
||||||
|
type: array
|
||||||
|
items: { type: string }
|
||||||
|
description: 'Tags to assign to the application.'
|
||||||
is_preserve_repository_enabled:
|
is_preserve_repository_enabled:
|
||||||
type: boolean
|
type: boolean
|
||||||
default: false
|
default: false
|
||||||
|
|
@ -583,6 +587,10 @@ paths:
|
||||||
type: boolean
|
type: boolean
|
||||||
default: true
|
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.'
|
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.'
|
||||||
|
tags:
|
||||||
|
type: array
|
||||||
|
items: { type: string }
|
||||||
|
description: 'Tags to assign to the application.'
|
||||||
is_preserve_repository_enabled:
|
is_preserve_repository_enabled:
|
||||||
type: boolean
|
type: boolean
|
||||||
default: false
|
default: false
|
||||||
|
|
@ -873,6 +881,10 @@ paths:
|
||||||
type: boolean
|
type: boolean
|
||||||
default: true
|
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.'
|
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.'
|
||||||
|
tags:
|
||||||
|
type: array
|
||||||
|
items: { type: string }
|
||||||
|
description: 'Tags to assign to the application.'
|
||||||
is_preserve_repository_enabled:
|
is_preserve_repository_enabled:
|
||||||
type: boolean
|
type: boolean
|
||||||
default: false
|
default: false
|
||||||
|
|
@ -1104,6 +1116,10 @@ paths:
|
||||||
type: boolean
|
type: boolean
|
||||||
default: true
|
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.'
|
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.'
|
||||||
|
tags:
|
||||||
|
type: array
|
||||||
|
items: { type: string }
|
||||||
|
description: 'Tags to assign to the application.'
|
||||||
type: object
|
type: object
|
||||||
responses:
|
responses:
|
||||||
'201':
|
'201':
|
||||||
|
|
@ -1321,6 +1337,10 @@ paths:
|
||||||
type: boolean
|
type: boolean
|
||||||
default: true
|
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.'
|
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.'
|
||||||
|
tags:
|
||||||
|
type: array
|
||||||
|
items: { type: string }
|
||||||
|
description: 'Tags to assign to the application.'
|
||||||
type: object
|
type: object
|
||||||
responses:
|
responses:
|
||||||
'201':
|
'201':
|
||||||
|
|
@ -2376,6 +2396,121 @@ paths:
|
||||||
security:
|
security:
|
||||||
-
|
-
|
||||||
bearerAuth: []
|
bearerAuth: []
|
||||||
|
'/applications/{uuid}/tags':
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- Applications
|
||||||
|
summary: 'List Tags'
|
||||||
|
description: 'List tags for an application by UUID.'
|
||||||
|
operationId: list-tags-by-application-uuid
|
||||||
|
parameters:
|
||||||
|
-
|
||||||
|
name: uuid
|
||||||
|
in: path
|
||||||
|
description: 'UUID of the application.'
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 'List of tags.'
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/Tag'
|
||||||
|
'401':
|
||||||
|
$ref: '#/components/responses/401'
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/responses/400'
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/404'
|
||||||
|
security:
|
||||||
|
-
|
||||||
|
bearerAuth: []
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- Applications
|
||||||
|
summary: 'Create Tag'
|
||||||
|
description: 'Add tag(s) to an application by UUID.'
|
||||||
|
operationId: create-tag-by-application-uuid
|
||||||
|
parameters:
|
||||||
|
-
|
||||||
|
name: uuid
|
||||||
|
in: path
|
||||||
|
description: 'UUID of the application.'
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
properties:
|
||||||
|
tag_name:
|
||||||
|
type: string
|
||||||
|
description: 'The tag name (min 2 characters). Required if tag_names is not provided.'
|
||||||
|
tag_names:
|
||||||
|
type: array
|
||||||
|
items: { type: string }
|
||||||
|
description: 'Array of tag names (each min 2 characters). Required if tag_name is not provided.'
|
||||||
|
type: object
|
||||||
|
responses:
|
||||||
|
'201':
|
||||||
|
description: 'Tags added successfully.'
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/Tag'
|
||||||
|
'401':
|
||||||
|
$ref: '#/components/responses/401'
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/responses/400'
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/404'
|
||||||
|
'422':
|
||||||
|
$ref: '#/components/responses/422'
|
||||||
|
security:
|
||||||
|
-
|
||||||
|
bearerAuth: []
|
||||||
|
'/applications/{uuid}/tags/{tag_uuid}':
|
||||||
|
delete:
|
||||||
|
tags:
|
||||||
|
- Applications
|
||||||
|
summary: 'Delete Tag'
|
||||||
|
description: 'Remove a tag from an application by UUID.'
|
||||||
|
operationId: delete-tag-by-application-uuid
|
||||||
|
parameters:
|
||||||
|
-
|
||||||
|
name: uuid
|
||||||
|
in: path
|
||||||
|
description: 'UUID of the application.'
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
-
|
||||||
|
name: tag_uuid
|
||||||
|
in: path
|
||||||
|
description: 'UUID of the tag.'
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 'Tag removed.'
|
||||||
|
'401':
|
||||||
|
$ref: '#/components/responses/401'
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/responses/400'
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/404'
|
||||||
|
security:
|
||||||
|
-
|
||||||
|
bearerAuth: []
|
||||||
/cloud-tokens:
|
/cloud-tokens:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
|
|
@ -3244,6 +3379,10 @@ paths:
|
||||||
instant_deploy:
|
instant_deploy:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: 'Instant deploy the database'
|
description: 'Instant deploy the database'
|
||||||
|
tags:
|
||||||
|
type: array
|
||||||
|
items: { type: string }
|
||||||
|
description: 'Tags to assign to the database.'
|
||||||
type: object
|
type: object
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
|
|
@ -3339,6 +3478,10 @@ paths:
|
||||||
instant_deploy:
|
instant_deploy:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: 'Instant deploy the database'
|
description: 'Instant deploy the database'
|
||||||
|
tags:
|
||||||
|
type: array
|
||||||
|
items: { type: string }
|
||||||
|
description: 'Tags to assign to the database.'
|
||||||
type: object
|
type: object
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
|
|
@ -3431,6 +3574,10 @@ paths:
|
||||||
instant_deploy:
|
instant_deploy:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: 'Instant deploy the database'
|
description: 'Instant deploy the database'
|
||||||
|
tags:
|
||||||
|
type: array
|
||||||
|
items: { type: string }
|
||||||
|
description: 'Tags to assign to the database.'
|
||||||
type: object
|
type: object
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
|
|
@ -3526,6 +3673,10 @@ paths:
|
||||||
instant_deploy:
|
instant_deploy:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: 'Instant deploy the database'
|
description: 'Instant deploy the database'
|
||||||
|
tags:
|
||||||
|
type: array
|
||||||
|
items: { type: string }
|
||||||
|
description: 'Tags to assign to the database.'
|
||||||
type: object
|
type: object
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
|
|
@ -3621,6 +3772,10 @@ paths:
|
||||||
instant_deploy:
|
instant_deploy:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: 'Instant deploy the database'
|
description: 'Instant deploy the database'
|
||||||
|
tags:
|
||||||
|
type: array
|
||||||
|
items: { type: string }
|
||||||
|
description: 'Tags to assign to the database.'
|
||||||
type: object
|
type: object
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
|
|
@ -3725,6 +3880,10 @@ paths:
|
||||||
instant_deploy:
|
instant_deploy:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: 'Instant deploy the database'
|
description: 'Instant deploy the database'
|
||||||
|
tags:
|
||||||
|
type: array
|
||||||
|
items: { type: string }
|
||||||
|
description: 'Tags to assign to the database.'
|
||||||
type: object
|
type: object
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
|
|
@ -3829,6 +3988,10 @@ paths:
|
||||||
instant_deploy:
|
instant_deploy:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: 'Instant deploy the database'
|
description: 'Instant deploy the database'
|
||||||
|
tags:
|
||||||
|
type: array
|
||||||
|
items: { type: string }
|
||||||
|
description: 'Tags to assign to the database.'
|
||||||
type: object
|
type: object
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
|
|
@ -3924,6 +4087,10 @@ paths:
|
||||||
instant_deploy:
|
instant_deploy:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: 'Instant deploy the database'
|
description: 'Instant deploy the database'
|
||||||
|
tags:
|
||||||
|
type: array
|
||||||
|
items: { type: string }
|
||||||
|
description: 'Tags to assign to the database.'
|
||||||
type: object
|
type: object
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
|
|
@ -4636,6 +4803,121 @@ paths:
|
||||||
security:
|
security:
|
||||||
-
|
-
|
||||||
bearerAuth: []
|
bearerAuth: []
|
||||||
|
'/databases/{uuid}/tags':
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- Databases
|
||||||
|
summary: 'List Tags'
|
||||||
|
description: 'List tags for a database by UUID.'
|
||||||
|
operationId: list-tags-by-database-uuid
|
||||||
|
parameters:
|
||||||
|
-
|
||||||
|
name: uuid
|
||||||
|
in: path
|
||||||
|
description: 'UUID of the database.'
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 'List of tags.'
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/Tag'
|
||||||
|
'401':
|
||||||
|
$ref: '#/components/responses/401'
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/responses/400'
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/404'
|
||||||
|
security:
|
||||||
|
-
|
||||||
|
bearerAuth: []
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- Databases
|
||||||
|
summary: 'Create Tag'
|
||||||
|
description: 'Add tag(s) to a database by UUID.'
|
||||||
|
operationId: create-tag-by-database-uuid
|
||||||
|
parameters:
|
||||||
|
-
|
||||||
|
name: uuid
|
||||||
|
in: path
|
||||||
|
description: 'UUID of the database.'
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
properties:
|
||||||
|
tag_name:
|
||||||
|
type: string
|
||||||
|
description: 'The tag name (min 2 characters). Required if tag_names is not provided.'
|
||||||
|
tag_names:
|
||||||
|
type: array
|
||||||
|
items: { type: string }
|
||||||
|
description: 'Array of tag names (each min 2 characters). Required if tag_name is not provided.'
|
||||||
|
type: object
|
||||||
|
responses:
|
||||||
|
'201':
|
||||||
|
description: 'Tags added successfully.'
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/Tag'
|
||||||
|
'401':
|
||||||
|
$ref: '#/components/responses/401'
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/responses/400'
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/404'
|
||||||
|
'422':
|
||||||
|
$ref: '#/components/responses/422'
|
||||||
|
security:
|
||||||
|
-
|
||||||
|
bearerAuth: []
|
||||||
|
'/databases/{uuid}/tags/{tag_uuid}':
|
||||||
|
delete:
|
||||||
|
tags:
|
||||||
|
- Databases
|
||||||
|
summary: 'Delete Tag'
|
||||||
|
description: 'Remove a tag from a database by UUID.'
|
||||||
|
operationId: delete-tag-by-database-uuid
|
||||||
|
parameters:
|
||||||
|
-
|
||||||
|
name: uuid
|
||||||
|
in: path
|
||||||
|
description: 'UUID of the database.'
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
-
|
||||||
|
name: tag_uuid
|
||||||
|
in: path
|
||||||
|
description: 'UUID of the tag.'
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 'Tag removed.'
|
||||||
|
'401':
|
||||||
|
$ref: '#/components/responses/401'
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/responses/400'
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/404'
|
||||||
|
security:
|
||||||
|
-
|
||||||
|
bearerAuth: []
|
||||||
/deployments:
|
/deployments:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
|
|
@ -7008,6 +7290,10 @@ paths:
|
||||||
type: boolean
|
type: boolean
|
||||||
default: true
|
default: true
|
||||||
description: 'Escape special characters in labels. By default, $ (and other chars) is escaped. If you want to use env variables inside the labels, turn this off.'
|
description: 'Escape special characters in labels. By default, $ (and other chars) is escaped. If you want to use env variables inside the labels, turn this off.'
|
||||||
|
tags:
|
||||||
|
type: array
|
||||||
|
items: { type: string }
|
||||||
|
description: 'Tags to assign to the service.'
|
||||||
type: object
|
type: object
|
||||||
responses:
|
responses:
|
||||||
'201':
|
'201':
|
||||||
|
|
@ -7860,6 +8146,144 @@ paths:
|
||||||
security:
|
security:
|
||||||
-
|
-
|
||||||
bearerAuth: []
|
bearerAuth: []
|
||||||
|
'/services/{uuid}/tags':
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- Services
|
||||||
|
summary: 'List Tags'
|
||||||
|
description: 'List tags for a service by UUID.'
|
||||||
|
operationId: list-tags-by-service-uuid
|
||||||
|
parameters:
|
||||||
|
-
|
||||||
|
name: uuid
|
||||||
|
in: path
|
||||||
|
description: 'UUID of the service.'
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 'List of tags.'
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/Tag'
|
||||||
|
'401':
|
||||||
|
$ref: '#/components/responses/401'
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/responses/400'
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/404'
|
||||||
|
security:
|
||||||
|
-
|
||||||
|
bearerAuth: []
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- Services
|
||||||
|
summary: 'Create Tag'
|
||||||
|
description: 'Add tag(s) to a service by UUID.'
|
||||||
|
operationId: create-tag-by-service-uuid
|
||||||
|
parameters:
|
||||||
|
-
|
||||||
|
name: uuid
|
||||||
|
in: path
|
||||||
|
description: 'UUID of the service.'
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
properties:
|
||||||
|
tag_name:
|
||||||
|
type: string
|
||||||
|
description: 'The tag name (min 2 characters). Required if tag_names is not provided.'
|
||||||
|
tag_names:
|
||||||
|
type: array
|
||||||
|
items: { type: string }
|
||||||
|
description: 'Array of tag names (each min 2 characters). Required if tag_name is not provided.'
|
||||||
|
type: object
|
||||||
|
responses:
|
||||||
|
'201':
|
||||||
|
description: 'Tags added successfully.'
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/Tag'
|
||||||
|
'401':
|
||||||
|
$ref: '#/components/responses/401'
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/responses/400'
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/404'
|
||||||
|
'422':
|
||||||
|
$ref: '#/components/responses/422'
|
||||||
|
security:
|
||||||
|
-
|
||||||
|
bearerAuth: []
|
||||||
|
'/services/{uuid}/tags/{tag_uuid}':
|
||||||
|
delete:
|
||||||
|
tags:
|
||||||
|
- Services
|
||||||
|
summary: 'Delete Tag'
|
||||||
|
description: 'Remove a tag from a service by UUID.'
|
||||||
|
operationId: delete-tag-by-service-uuid
|
||||||
|
parameters:
|
||||||
|
-
|
||||||
|
name: uuid
|
||||||
|
in: path
|
||||||
|
description: 'UUID of the service.'
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
-
|
||||||
|
name: tag_uuid
|
||||||
|
in: path
|
||||||
|
description: 'UUID of the tag.'
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 'Tag removed.'
|
||||||
|
'401':
|
||||||
|
$ref: '#/components/responses/401'
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/responses/400'
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/404'
|
||||||
|
security:
|
||||||
|
-
|
||||||
|
bearerAuth: []
|
||||||
|
/tags:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- Tags
|
||||||
|
summary: List
|
||||||
|
description: 'List all tags for the current team.'
|
||||||
|
operationId: list-tags
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 'All tags for the current team.'
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/Tag'
|
||||||
|
'401':
|
||||||
|
$ref: '#/components/responses/401'
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/responses/400'
|
||||||
|
security:
|
||||||
|
-
|
||||||
|
bearerAuth: []
|
||||||
/teams:
|
/teams:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
|
|
@ -8740,6 +9164,18 @@ components:
|
||||||
type: string
|
type: string
|
||||||
description: 'The date and time when the service was deleted.'
|
description: 'The date and time when the service was deleted.'
|
||||||
type: object
|
type: object
|
||||||
|
Tag:
|
||||||
|
description: 'Tag model'
|
||||||
|
properties:
|
||||||
|
uuid:
|
||||||
|
type: string
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
created_at:
|
||||||
|
type: string
|
||||||
|
updated_at:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
Team:
|
Team:
|
||||||
description: 'Team model'
|
description: 'Team model'
|
||||||
properties:
|
properties:
|
||||||
|
|
@ -8911,6 +9347,9 @@ tags:
|
||||||
-
|
-
|
||||||
name: Services
|
name: Services
|
||||||
description: Services
|
description: Services
|
||||||
|
-
|
||||||
|
name: Tags
|
||||||
|
description: Tags
|
||||||
-
|
-
|
||||||
name: Teams
|
name: Teams
|
||||||
description: Teams
|
description: Teams
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
use App\Http\Controllers\Api\SentinelController;
|
use App\Http\Controllers\Api\SentinelController;
|
||||||
use App\Http\Controllers\Api\ServersController;
|
use App\Http\Controllers\Api\ServersController;
|
||||||
use App\Http\Controllers\Api\ServicesController;
|
use App\Http\Controllers\Api\ServicesController;
|
||||||
|
use App\Http\Controllers\Api\TagsController;
|
||||||
use App\Http\Controllers\Api\TeamController;
|
use App\Http\Controllers\Api\TeamController;
|
||||||
use App\Http\Middleware\ApiAllowed;
|
use App\Http\Middleware\ApiAllowed;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
@ -108,6 +109,8 @@
|
||||||
|
|
||||||
Route::get('/resources', [ResourcesController::class, 'resources'])->middleware(['api.ability:read']);
|
Route::get('/resources', [ResourcesController::class, 'resources'])->middleware(['api.ability:read']);
|
||||||
|
|
||||||
|
Route::get('/tags', [TagsController::class, 'tags'])->middleware(['api.ability:read']);
|
||||||
|
|
||||||
Route::get('/applications', [ApplicationsController::class, 'applications'])->middleware(['api.ability:read']);
|
Route::get('/applications', [ApplicationsController::class, 'applications'])->middleware(['api.ability:read']);
|
||||||
Route::post('/applications/public', [ApplicationsController::class, 'create_public_application'])->middleware(['api.ability:write']);
|
Route::post('/applications/public', [ApplicationsController::class, 'create_public_application'])->middleware(['api.ability:write']);
|
||||||
Route::post('/applications/private-github-app', [ApplicationsController::class, 'create_private_gh_app_application'])->middleware(['api.ability:write']);
|
Route::post('/applications/private-github-app', [ApplicationsController::class, 'create_private_gh_app_application'])->middleware(['api.ability:write']);
|
||||||
|
|
@ -130,6 +133,10 @@
|
||||||
Route::patch('/applications/{uuid}/storages', [ApplicationsController::class, 'update_storage'])->middleware(['api.ability:write']);
|
Route::patch('/applications/{uuid}/storages', [ApplicationsController::class, 'update_storage'])->middleware(['api.ability:write']);
|
||||||
Route::delete('/applications/{uuid}/storages/{storage_uuid}', [ApplicationsController::class, 'delete_storage'])->middleware(['api.ability:write']);
|
Route::delete('/applications/{uuid}/storages/{storage_uuid}', [ApplicationsController::class, 'delete_storage'])->middleware(['api.ability:write']);
|
||||||
|
|
||||||
|
Route::get('/applications/{uuid}/tags', [ApplicationsController::class, 'tags'])->middleware(['api.ability:read']);
|
||||||
|
Route::post('/applications/{uuid}/tags', [ApplicationsController::class, 'create_tag'])->middleware(['api.ability:write']);
|
||||||
|
Route::delete('/applications/{uuid}/tags/{tag_uuid}', [ApplicationsController::class, 'delete_tag'])->middleware(['api.ability:write']);
|
||||||
|
|
||||||
Route::match(['get', 'post'], '/applications/{uuid}/start', [ApplicationsController::class, 'action_deploy'])->middleware(['api.ability:deploy']);
|
Route::match(['get', 'post'], '/applications/{uuid}/start', [ApplicationsController::class, 'action_deploy'])->middleware(['api.ability:deploy']);
|
||||||
Route::match(['get', 'post'], '/applications/{uuid}/restart', [ApplicationsController::class, 'action_restart'])->middleware(['api.ability:deploy']);
|
Route::match(['get', 'post'], '/applications/{uuid}/restart', [ApplicationsController::class, 'action_restart'])->middleware(['api.ability:deploy']);
|
||||||
Route::match(['get', 'post'], '/applications/{uuid}/stop', [ApplicationsController::class, 'action_stop'])->middleware(['api.ability:deploy']);
|
Route::match(['get', 'post'], '/applications/{uuid}/stop', [ApplicationsController::class, 'action_stop'])->middleware(['api.ability:deploy']);
|
||||||
|
|
@ -175,6 +182,10 @@
|
||||||
Route::patch('/databases/{uuid}/envs', [DatabasesController::class, 'update_env_by_uuid'])->middleware(['api.ability:write']);
|
Route::patch('/databases/{uuid}/envs', [DatabasesController::class, 'update_env_by_uuid'])->middleware(['api.ability:write']);
|
||||||
Route::delete('/databases/{uuid}/envs/{env_uuid}', [DatabasesController::class, 'delete_env_by_uuid'])->middleware(['api.ability:write']);
|
Route::delete('/databases/{uuid}/envs/{env_uuid}', [DatabasesController::class, 'delete_env_by_uuid'])->middleware(['api.ability:write']);
|
||||||
|
|
||||||
|
Route::get('/databases/{uuid}/tags', [DatabasesController::class, 'tags'])->middleware(['api.ability:read']);
|
||||||
|
Route::post('/databases/{uuid}/tags', [DatabasesController::class, 'create_tag'])->middleware(['api.ability:write']);
|
||||||
|
Route::delete('/databases/{uuid}/tags/{tag_uuid}', [DatabasesController::class, 'delete_tag'])->middleware(['api.ability:write']);
|
||||||
|
|
||||||
Route::match(['get', 'post'], '/databases/{uuid}/start', [DatabasesController::class, 'action_deploy'])->middleware(['api.ability:deploy']);
|
Route::match(['get', 'post'], '/databases/{uuid}/start', [DatabasesController::class, 'action_deploy'])->middleware(['api.ability:deploy']);
|
||||||
Route::match(['get', 'post'], '/databases/{uuid}/restart', [DatabasesController::class, 'action_restart'])->middleware(['api.ability:deploy']);
|
Route::match(['get', 'post'], '/databases/{uuid}/restart', [DatabasesController::class, 'action_restart'])->middleware(['api.ability:deploy']);
|
||||||
Route::match(['get', 'post'], '/databases/{uuid}/stop', [DatabasesController::class, 'action_stop'])->middleware(['api.ability:deploy']);
|
Route::match(['get', 'post'], '/databases/{uuid}/stop', [DatabasesController::class, 'action_stop'])->middleware(['api.ability:deploy']);
|
||||||
|
|
@ -198,6 +209,10 @@
|
||||||
Route::delete('/services/{uuid}/envs/{env_uuid}', [ServicesController::class, 'delete_env_by_uuid'])->middleware(['api.ability:write']);
|
Route::delete('/services/{uuid}/envs/{env_uuid}', [ServicesController::class, 'delete_env_by_uuid'])->middleware(['api.ability:write']);
|
||||||
Route::get('/services/{uuid}/logs', [ServicesController::class, 'logs_by_uuid'])->middleware(['api.ability:read']);
|
Route::get('/services/{uuid}/logs', [ServicesController::class, 'logs_by_uuid'])->middleware(['api.ability:read']);
|
||||||
|
|
||||||
|
Route::get('/services/{uuid}/tags', [ServicesController::class, 'tags'])->middleware(['api.ability:read']);
|
||||||
|
Route::post('/services/{uuid}/tags', [ServicesController::class, 'create_tag'])->middleware(['api.ability:write']);
|
||||||
|
Route::delete('/services/{uuid}/tags/{tag_uuid}', [ServicesController::class, 'delete_tag'])->middleware(['api.ability:write']);
|
||||||
|
|
||||||
Route::match(['get', 'post'], '/services/{uuid}/start', [ServicesController::class, 'action_deploy'])->middleware(['api.ability:deploy']);
|
Route::match(['get', 'post'], '/services/{uuid}/start', [ServicesController::class, 'action_deploy'])->middleware(['api.ability:deploy']);
|
||||||
Route::match(['get', 'post'], '/services/{uuid}/restart', [ServicesController::class, 'action_restart'])->middleware(['api.ability:deploy']);
|
Route::match(['get', 'post'], '/services/{uuid}/restart', [ServicesController::class, 'action_restart'])->middleware(['api.ability:deploy']);
|
||||||
Route::match(['get', 'post'], '/services/{uuid}/stop', [ServicesController::class, 'action_stop'])->middleware(['api.ability:deploy']);
|
Route::match(['get', 'post'], '/services/{uuid}/stop', [ServicesController::class, 'action_stop'])->middleware(['api.ability:deploy']);
|
||||||
|
|
|
||||||
547
tests/Feature/TagApiTest.php
Normal file
547
tests/Feature/TagApiTest.php
Normal file
|
|
@ -0,0 +1,547 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Livewire\Project\Shared\Tags as TagsComponent;
|
||||||
|
use App\Models\Application;
|
||||||
|
use App\Models\Environment;
|
||||||
|
use App\Models\InstanceSettings;
|
||||||
|
use App\Models\Project;
|
||||||
|
use App\Models\Server;
|
||||||
|
use App\Models\Service;
|
||||||
|
use App\Models\StandaloneDocker;
|
||||||
|
use App\Models\StandalonePostgresql;
|
||||||
|
use App\Models\Tag;
|
||||||
|
use App\Models\Team;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Database\QueryException;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Livewire\Livewire;
|
||||||
|
|
||||||
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
InstanceSettings::unguarded(fn () => InstanceSettings::updateOrCreate(['id' => 0], ['is_api_enabled' => true]));
|
||||||
|
|
||||||
|
$this->team = Team::factory()->create();
|
||||||
|
$this->user = User::factory()->create();
|
||||||
|
$this->team->members()->attach($this->user->id, ['role' => 'owner']);
|
||||||
|
|
||||||
|
session(['currentTeam' => $this->team]);
|
||||||
|
|
||||||
|
$this->token = $this->user->createToken('test-token', ['*']);
|
||||||
|
$this->bearerToken = $this->token->plainTextToken;
|
||||||
|
|
||||||
|
$this->server = Server::factory()->create(['team_id' => $this->team->id]);
|
||||||
|
$this->destination = StandaloneDocker::where('server_id', $this->server->id)->first();
|
||||||
|
$this->project = Project::factory()->create(['team_id' => $this->team->id]);
|
||||||
|
$this->environment = Environment::factory()->create(['project_id' => $this->project->id]);
|
||||||
|
|
||||||
|
$this->application = Application::factory()->create([
|
||||||
|
'environment_id' => $this->environment->id,
|
||||||
|
'destination_id' => $this->destination->id,
|
||||||
|
'destination_type' => $this->destination->getMorphClass(),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
function tagApiAuthHeaders($bearerToken): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'Authorization' => 'Bearer '.$bearerToken,
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('GET /api/v1/tags', function () {
|
||||||
|
test('returns all tags for current team', function () {
|
||||||
|
Tag::create(['name' => 'production', 'team_id' => $this->team->id]);
|
||||||
|
Tag::create(['name' => 'staging', 'team_id' => $this->team->id]);
|
||||||
|
|
||||||
|
$response = $this->withHeaders(tagApiAuthHeaders($this->bearerToken))
|
||||||
|
->getJson('/api/v1/tags');
|
||||||
|
|
||||||
|
$response->assertOk();
|
||||||
|
$response->assertJsonCount(2);
|
||||||
|
$response->assertJsonFragment(['name' => 'production']);
|
||||||
|
$response->assertJsonFragment(['name' => 'staging']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns empty array when no tags exist', function () {
|
||||||
|
$response = $this->withHeaders(tagApiAuthHeaders($this->bearerToken))
|
||||||
|
->getJson('/api/v1/tags');
|
||||||
|
|
||||||
|
$response->assertOk();
|
||||||
|
$response->assertJsonCount(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does not return tags from other teams', function () {
|
||||||
|
$otherTeam = Team::factory()->create();
|
||||||
|
Tag::create(['name' => 'other-team-tag', 'team_id' => $otherTeam->id]);
|
||||||
|
Tag::create(['name' => 'my-tag', 'team_id' => $this->team->id]);
|
||||||
|
|
||||||
|
$response = $this->withHeaders(tagApiAuthHeaders($this->bearerToken))
|
||||||
|
->getJson('/api/v1/tags');
|
||||||
|
|
||||||
|
$response->assertOk();
|
||||||
|
$response->assertJsonCount(1);
|
||||||
|
$response->assertJsonFragment(['name' => 'my-tag']);
|
||||||
|
$response->assertJsonMissing(['name' => 'other-team-tag']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /api/v1/applications/{uuid}/tags', function () {
|
||||||
|
test('returns tags for an application', function () {
|
||||||
|
$tag = Tag::create(['name' => 'production', 'team_id' => $this->team->id]);
|
||||||
|
$this->application->tags()->attach($tag->id);
|
||||||
|
|
||||||
|
$response = $this->withHeaders(tagApiAuthHeaders($this->bearerToken))
|
||||||
|
->getJson("/api/v1/applications/{$this->application->uuid}/tags");
|
||||||
|
|
||||||
|
$response->assertOk();
|
||||||
|
$response->assertJsonCount(1);
|
||||||
|
$response->assertJsonFragment(['name' => 'production']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns 404 for non-existent application', function () {
|
||||||
|
$response = $this->withHeaders(tagApiAuthHeaders($this->bearerToken))
|
||||||
|
->getJson('/api/v1/applications/non-existent-uuid/tags');
|
||||||
|
|
||||||
|
$response->assertNotFound();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns empty array when application has no tags', function () {
|
||||||
|
$response = $this->withHeaders(tagApiAuthHeaders($this->bearerToken))
|
||||||
|
->getJson("/api/v1/applications/{$this->application->uuid}/tags");
|
||||||
|
|
||||||
|
$response->assertOk();
|
||||||
|
$response->assertJsonCount(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /api/v1/applications/{uuid}/tags', function () {
|
||||||
|
test('adds a single tag via tag_name', function () {
|
||||||
|
$response = $this->withHeaders(tagApiAuthHeaders($this->bearerToken))
|
||||||
|
->postJson("/api/v1/applications/{$this->application->uuid}/tags", [
|
||||||
|
'tag_name' => 'production',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertCreated();
|
||||||
|
$response->assertJsonCount(1);
|
||||||
|
$response->assertJsonFragment(['name' => 'production']);
|
||||||
|
|
||||||
|
expect($this->application->tags()->count())->toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('adds multiple tags via tag_names array', function () {
|
||||||
|
$response = $this->withHeaders(tagApiAuthHeaders($this->bearerToken))
|
||||||
|
->postJson("/api/v1/applications/{$this->application->uuid}/tags", [
|
||||||
|
'tag_names' => ['production', 'frontend'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertCreated();
|
||||||
|
$response->assertJsonCount(2);
|
||||||
|
|
||||||
|
expect($this->application->tags()->count())->toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('reuses existing team tag instead of creating duplicate', function () {
|
||||||
|
Tag::create(['name' => 'production', 'team_id' => $this->team->id]);
|
||||||
|
|
||||||
|
$response = $this->withHeaders(tagApiAuthHeaders($this->bearerToken))
|
||||||
|
->postJson("/api/v1/applications/{$this->application->uuid}/tags", [
|
||||||
|
'tag_name' => 'production',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertCreated();
|
||||||
|
expect(Tag::where('team_id', $this->team->id)->where('name', 'production')->count())->toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects tag_name shorter than 2 characters', function () {
|
||||||
|
$response = $this->withHeaders(tagApiAuthHeaders($this->bearerToken))
|
||||||
|
->postJson("/api/v1/applications/{$this->application->uuid}/tags", [
|
||||||
|
'tag_name' => 'x',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertUnprocessable();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects both tag_name and tag_names provided simultaneously', function () {
|
||||||
|
$response = $this->withHeaders(tagApiAuthHeaders($this->bearerToken))
|
||||||
|
->postJson("/api/v1/applications/{$this->application->uuid}/tags", [
|
||||||
|
'tag_name' => 'production',
|
||||||
|
'tag_names' => ['staging'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertUnprocessable();
|
||||||
|
$response->assertJsonFragment(['tag_name' => ['Provide either tag_name or tag_names, not both.']]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('skips duplicate tag already on resource', function () {
|
||||||
|
$tag = Tag::create(['name' => 'production', 'team_id' => $this->team->id]);
|
||||||
|
$this->application->tags()->attach($tag->id);
|
||||||
|
|
||||||
|
$response = $this->withHeaders(tagApiAuthHeaders($this->bearerToken))
|
||||||
|
->postJson("/api/v1/applications/{$this->application->uuid}/tags", [
|
||||||
|
'tag_name' => 'production',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertCreated();
|
||||||
|
expect($this->application->tags()->count())->toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns 404 for non-existent application', function () {
|
||||||
|
$response = $this->withHeaders(tagApiAuthHeaders($this->bearerToken))
|
||||||
|
->postJson('/api/v1/applications/non-existent-uuid/tags', [
|
||||||
|
'tag_name' => 'production',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertNotFound();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('DELETE /api/v1/applications/{uuid}/tags/{tag_uuid}', function () {
|
||||||
|
test('removes tag from application', function () {
|
||||||
|
$tag = Tag::create(['name' => 'production', 'team_id' => $this->team->id]);
|
||||||
|
$this->application->tags()->attach($tag->id);
|
||||||
|
|
||||||
|
$response = $this->withHeaders(tagApiAuthHeaders($this->bearerToken))
|
||||||
|
->deleteJson("/api/v1/applications/{$this->application->uuid}/tags/{$tag->uuid}");
|
||||||
|
|
||||||
|
$response->assertOk();
|
||||||
|
expect($this->application->tags()->count())->toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('garbage-collects orphaned tag', function () {
|
||||||
|
$tag = Tag::create(['name' => 'production', 'team_id' => $this->team->id]);
|
||||||
|
$this->application->tags()->attach($tag->id);
|
||||||
|
|
||||||
|
$this->withHeaders(tagApiAuthHeaders($this->bearerToken))
|
||||||
|
->deleteJson("/api/v1/applications/{$this->application->uuid}/tags/{$tag->uuid}");
|
||||||
|
|
||||||
|
expect(Tag::find($tag->id))->toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns 404 when deleting a tag that is not attached to the application', function () {
|
||||||
|
$tag = Tag::create(['name' => 'production', 'team_id' => $this->team->id]);
|
||||||
|
|
||||||
|
$response = $this->withHeaders(tagApiAuthHeaders($this->bearerToken))
|
||||||
|
->deleteJson("/api/v1/applications/{$this->application->uuid}/tags/{$tag->uuid}");
|
||||||
|
|
||||||
|
$response->assertNotFound();
|
||||||
|
expect(Tag::find($tag->id))->not->toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('keeps tag if still used by other resources', function () {
|
||||||
|
$tag = Tag::create(['name' => 'production', 'team_id' => $this->team->id]);
|
||||||
|
$this->application->tags()->attach($tag->id);
|
||||||
|
|
||||||
|
$otherApp = Application::factory()->create([
|
||||||
|
'environment_id' => $this->environment->id,
|
||||||
|
'destination_id' => $this->destination->id,
|
||||||
|
'destination_type' => $this->destination->getMorphClass(),
|
||||||
|
]);
|
||||||
|
$otherApp->tags()->attach($tag->id);
|
||||||
|
|
||||||
|
$this->withHeaders(tagApiAuthHeaders($this->bearerToken))
|
||||||
|
->deleteJson("/api/v1/applications/{$this->application->uuid}/tags/{$tag->uuid}");
|
||||||
|
|
||||||
|
expect(Tag::find($tag->id))->not->toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns 404 for non-existent tag', function () {
|
||||||
|
$response = $this->withHeaders(tagApiAuthHeaders($this->bearerToken))
|
||||||
|
->deleteJson("/api/v1/applications/{$this->application->uuid}/tags/non-existent-uuid");
|
||||||
|
|
||||||
|
$response->assertNotFound();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /api/v1/databases/{uuid}/tags', function () {
|
||||||
|
test('returns tags for a database', function () {
|
||||||
|
$database = StandalonePostgresql::create([
|
||||||
|
'name' => 'test-pg',
|
||||||
|
'postgres_password' => 'testpassword',
|
||||||
|
'environment_id' => $this->environment->id,
|
||||||
|
'destination_id' => $this->destination->id,
|
||||||
|
'destination_type' => $this->destination->getMorphClass(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$tag = Tag::create(['name' => 'database-tag', 'team_id' => $this->team->id]);
|
||||||
|
$database->tags()->attach($tag->id);
|
||||||
|
|
||||||
|
$response = $this->withHeaders(tagApiAuthHeaders($this->bearerToken))
|
||||||
|
->getJson("/api/v1/databases/{$database->uuid}/tags");
|
||||||
|
|
||||||
|
$response->assertOk();
|
||||||
|
$response->assertJsonCount(1);
|
||||||
|
$response->assertJsonFragment(['name' => 'database-tag']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /api/v1/databases/{uuid}/tags', function () {
|
||||||
|
test('adds tag to database', function () {
|
||||||
|
$database = StandalonePostgresql::create([
|
||||||
|
'name' => 'test-pg',
|
||||||
|
'postgres_password' => 'testpassword',
|
||||||
|
'environment_id' => $this->environment->id,
|
||||||
|
'destination_id' => $this->destination->id,
|
||||||
|
'destination_type' => $this->destination->getMorphClass(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->withHeaders(tagApiAuthHeaders($this->bearerToken))
|
||||||
|
->postJson("/api/v1/databases/{$database->uuid}/tags", [
|
||||||
|
'tag_name' => 'database-tag',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertCreated();
|
||||||
|
expect($database->tags()->count())->toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('DELETE /api/v1/databases/{uuid}/tags/{tag_uuid}', function () {
|
||||||
|
test('removes tag from database', function () {
|
||||||
|
$database = StandalonePostgresql::create([
|
||||||
|
'name' => 'test-pg',
|
||||||
|
'postgres_password' => 'testpassword',
|
||||||
|
'environment_id' => $this->environment->id,
|
||||||
|
'destination_id' => $this->destination->id,
|
||||||
|
'destination_type' => $this->destination->getMorphClass(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$tag = Tag::create(['name' => 'database-tag', 'team_id' => $this->team->id]);
|
||||||
|
$database->tags()->attach($tag->id);
|
||||||
|
|
||||||
|
$response = $this->withHeaders(tagApiAuthHeaders($this->bearerToken))
|
||||||
|
->deleteJson("/api/v1/databases/{$database->uuid}/tags/{$tag->uuid}");
|
||||||
|
|
||||||
|
$response->assertOk();
|
||||||
|
expect($database->tags()->count())->toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /api/v1/services/{uuid}/tags', function () {
|
||||||
|
test('returns tags for a service', function () {
|
||||||
|
$service = Service::factory()->create([
|
||||||
|
'server_id' => $this->server->id,
|
||||||
|
'destination_id' => $this->destination->id,
|
||||||
|
'destination_type' => $this->destination->getMorphClass(),
|
||||||
|
'environment_id' => $this->environment->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$tag = Tag::create(['name' => 'service-tag', 'team_id' => $this->team->id]);
|
||||||
|
$service->tags()->attach($tag->id);
|
||||||
|
|
||||||
|
$response = $this->withHeaders(tagApiAuthHeaders($this->bearerToken))
|
||||||
|
->getJson("/api/v1/services/{$service->uuid}/tags");
|
||||||
|
|
||||||
|
$response->assertOk();
|
||||||
|
$response->assertJsonCount(1);
|
||||||
|
$response->assertJsonFragment(['name' => 'service-tag']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /api/v1/services/{uuid}/tags', function () {
|
||||||
|
test('adds tag to service', function () {
|
||||||
|
$service = Service::factory()->create([
|
||||||
|
'server_id' => $this->server->id,
|
||||||
|
'destination_id' => $this->destination->id,
|
||||||
|
'destination_type' => $this->destination->getMorphClass(),
|
||||||
|
'environment_id' => $this->environment->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->withHeaders(tagApiAuthHeaders($this->bearerToken))
|
||||||
|
->postJson("/api/v1/services/{$service->uuid}/tags", [
|
||||||
|
'tag_name' => 'service-tag',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertCreated();
|
||||||
|
expect($service->tags()->count())->toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('DELETE /api/v1/services/{uuid}/tags/{tag_uuid}', function () {
|
||||||
|
test('removes tag from service', function () {
|
||||||
|
$service = Service::factory()->create([
|
||||||
|
'server_id' => $this->server->id,
|
||||||
|
'destination_id' => $this->destination->id,
|
||||||
|
'destination_type' => $this->destination->getMorphClass(),
|
||||||
|
'environment_id' => $this->environment->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$tag = Tag::create(['name' => 'service-tag', 'team_id' => $this->team->id]);
|
||||||
|
$service->tags()->attach($tag->id);
|
||||||
|
|
||||||
|
$response = $this->withHeaders(tagApiAuthHeaders($this->bearerToken))
|
||||||
|
->deleteJson("/api/v1/services/{$service->uuid}/tags/{$tag->uuid}");
|
||||||
|
|
||||||
|
$response->assertOk();
|
||||||
|
expect($service->tags()->count())->toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Resource creation tags', function () {
|
||||||
|
test('adds tags while creating a public application', function () {
|
||||||
|
$response = $this->withHeaders(tagApiAuthHeaders($this->bearerToken))
|
||||||
|
->postJson('/api/v1/applications/public', [
|
||||||
|
'project_uuid' => $this->project->uuid,
|
||||||
|
'environment_uuid' => $this->environment->uuid,
|
||||||
|
'server_uuid' => $this->server->uuid,
|
||||||
|
'git_repository' => 'https://gitlab.com/coolify/test-static-app',
|
||||||
|
'git_branch' => 'main',
|
||||||
|
'build_pack' => 'static',
|
||||||
|
'ports_exposes' => '80',
|
||||||
|
'autogenerate_domain' => false,
|
||||||
|
'tags' => ['production', 'frontend'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertSuccessful();
|
||||||
|
|
||||||
|
$application = Application::whereUuid($response->json('uuid'))->firstOrFail();
|
||||||
|
|
||||||
|
expect($application->tags()->pluck('name')->sort()->values()->all())
|
||||||
|
->toBe(['frontend', 'production']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('adds tags while creating a postgresql database', function () {
|
||||||
|
$response = $this->withHeaders(tagApiAuthHeaders($this->bearerToken))
|
||||||
|
->postJson('/api/v1/databases/postgresql', [
|
||||||
|
'server_uuid' => $this->server->uuid,
|
||||||
|
'project_uuid' => $this->project->uuid,
|
||||||
|
'environment_uuid' => $this->environment->uuid,
|
||||||
|
'instant_deploy' => false,
|
||||||
|
'tags' => ['database', 'production'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertSuccessful();
|
||||||
|
|
||||||
|
$database = StandalonePostgresql::whereUuid($response->json('uuid'))->firstOrFail();
|
||||||
|
|
||||||
|
expect($database->tags()->pluck('name')->sort()->values()->all())
|
||||||
|
->toBe(['database', 'production']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('adds tags while creating a docker compose service', function () {
|
||||||
|
$compose = base64_encode(<<<'YAML'
|
||||||
|
services:
|
||||||
|
nginx:
|
||||||
|
image: nginx:alpine
|
||||||
|
YAML);
|
||||||
|
|
||||||
|
$response = $this->withHeaders(tagApiAuthHeaders($this->bearerToken))
|
||||||
|
->postJson('/api/v1/services', [
|
||||||
|
'server_uuid' => $this->server->uuid,
|
||||||
|
'project_uuid' => $this->project->uuid,
|
||||||
|
'environment_uuid' => $this->environment->uuid,
|
||||||
|
'docker_compose_raw' => $compose,
|
||||||
|
'instant_deploy' => false,
|
||||||
|
'tags' => ['service', 'production'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertCreated();
|
||||||
|
|
||||||
|
$service = Service::whereUuid($response->json('uuid'))->firstOrFail();
|
||||||
|
|
||||||
|
expect($service->tags()->pluck('name')->sort()->values()->all())
|
||||||
|
->toBe(['production', 'service']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects creation tags that are too short after sanitization', function () {
|
||||||
|
$response = $this->withHeaders(tagApiAuthHeaders($this->bearerToken))
|
||||||
|
->postJson('/api/v1/databases/postgresql', [
|
||||||
|
'server_uuid' => $this->server->uuid,
|
||||||
|
'project_uuid' => $this->project->uuid,
|
||||||
|
'environment_uuid' => $this->environment->uuid,
|
||||||
|
'instant_deploy' => false,
|
||||||
|
'tags' => ['<b>x</b>'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertUnprocessable();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Tag name sanitization', function () {
|
||||||
|
test('strips HTML tags from tag names', function () {
|
||||||
|
$response = $this->withHeaders(tagApiAuthHeaders($this->bearerToken))
|
||||||
|
->postJson("/api/v1/applications/{$this->application->uuid}/tags", [
|
||||||
|
'tag_name' => '<script>alert("xss")</script>production',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertCreated();
|
||||||
|
$response->assertJsonFragment(['name' => 'alert("xss")production']);
|
||||||
|
$response->assertJsonMissing(['name' => '<script>']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('lowercases tag names', function () {
|
||||||
|
$response = $this->withHeaders(tagApiAuthHeaders($this->bearerToken))
|
||||||
|
->postJson("/api/v1/applications/{$this->application->uuid}/tags", [
|
||||||
|
'tag_name' => 'Production',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertCreated();
|
||||||
|
$response->assertJsonFragment(['name' => 'production']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects tag names that are too short after sanitization', function () {
|
||||||
|
$response = $this->withHeaders(tagApiAuthHeaders($this->bearerToken))
|
||||||
|
->postJson("/api/v1/applications/{$this->application->uuid}/tags", [
|
||||||
|
'tag_name' => '<b>x</b>',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertUnprocessable();
|
||||||
|
expect($this->application->tags()->count())->toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Tag uniqueness', function () {
|
||||||
|
test('prevents duplicate tag names per team at the database level', function () {
|
||||||
|
Tag::create(['name' => 'production', 'team_id' => $this->team->id]);
|
||||||
|
|
||||||
|
expect(fn () => Tag::create(['name' => 'production', 'team_id' => $this->team->id]))
|
||||||
|
->toThrow(QueryException::class);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Tag cleanup across resource types', function () {
|
||||||
|
test('does not delete a tag still attached to a database when removed from an application in the UI', function () {
|
||||||
|
$this->actingAs($this->user);
|
||||||
|
|
||||||
|
$database = StandalonePostgresql::create([
|
||||||
|
'name' => 'test-pg',
|
||||||
|
'postgres_password' => 'testpassword',
|
||||||
|
'environment_id' => $this->environment->id,
|
||||||
|
'destination_id' => $this->destination->id,
|
||||||
|
'destination_type' => $this->destination->getMorphClass(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$tag = Tag::create(['name' => 'shared-tag', 'team_id' => $this->team->id]);
|
||||||
|
$this->application->tags()->attach($tag->id);
|
||||||
|
$database->tags()->attach($tag->id);
|
||||||
|
|
||||||
|
Livewire::test(TagsComponent::class, ['resource' => $this->application])
|
||||||
|
->call('deleteTag', (string) $tag->id);
|
||||||
|
|
||||||
|
expect(Tag::find($tag->id))->not->toBeNull()
|
||||||
|
->and($database->tags()->count())->toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Cross-resource tag isolation', function () {
|
||||||
|
test('cannot delete tag via wrong resource', function () {
|
||||||
|
$tag = Tag::create(['name' => 'shared-tag', 'team_id' => $this->team->id]);
|
||||||
|
|
||||||
|
$otherApp = Application::factory()->create([
|
||||||
|
'environment_id' => $this->environment->id,
|
||||||
|
'destination_id' => $this->destination->id,
|
||||||
|
'destination_type' => $this->destination->getMorphClass(),
|
||||||
|
]);
|
||||||
|
$otherApp->tags()->attach($tag->id);
|
||||||
|
|
||||||
|
// Tag is not attached to $this->application, but we try to delete via it
|
||||||
|
$response = $this->withHeaders(tagApiAuthHeaders($this->bearerToken))
|
||||||
|
->deleteJson("/api/v1/applications/{$this->application->uuid}/tags/{$tag->uuid}");
|
||||||
|
|
||||||
|
$response->assertNotFound();
|
||||||
|
// Tag should still exist on the other app (detach on non-attached is a no-op)
|
||||||
|
expect($otherApp->tags()->count())->toBe(1);
|
||||||
|
// Tag should NOT be garbage-collected since otherApp still uses it
|
||||||
|
expect(Tag::find($tag->id))->not->toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue