mirror of
https://github.com/tiennm99/coolify.git
synced 2026-06-30 15:03:49 +00:00
Merge remote-tracking branch 'origin/next' into 2731-investigate-failed-git-clone
This commit is contained in:
@@ -11,6 +11,8 @@ use App\Jobs\DeleteResourceJob;
|
||||
use App\Models\Application;
|
||||
use App\Models\EnvironmentVariable;
|
||||
use App\Models\GithubApp;
|
||||
use App\Models\LocalFileVolume;
|
||||
use App\Models\LocalPersistentVolume;
|
||||
use App\Models\PrivateKey;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
@@ -18,6 +20,8 @@ use App\Models\Service;
|
||||
use App\Rules\ValidGitBranch;
|
||||
use App\Rules\ValidGitRepositoryUrl;
|
||||
use App\Services\DockerImageParser;
|
||||
use App\Support\ValidationPatterns;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
@@ -226,6 +230,7 @@ class ApplicationsController extends Controller
|
||||
'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.'],
|
||||
'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_preserve_repository_enabled' => ['type' => 'boolean', 'default' => false, 'description' => 'Preserve repository during deployment.'],
|
||||
],
|
||||
)
|
||||
),
|
||||
@@ -391,6 +396,7 @@ class ApplicationsController extends Controller
|
||||
'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.'],
|
||||
'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_preserve_repository_enabled' => ['type' => 'boolean', 'default' => false, 'description' => 'Preserve repository during deployment.'],
|
||||
],
|
||||
)
|
||||
),
|
||||
@@ -556,6 +562,7 @@ class ApplicationsController extends Controller
|
||||
'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.'],
|
||||
'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_preserve_repository_enabled' => ['type' => 'boolean', 'default' => false, 'description' => 'Preserve repository during deployment.'],
|
||||
],
|
||||
)
|
||||
),
|
||||
@@ -999,10 +1006,10 @@ class ApplicationsController extends Controller
|
||||
$this->authorize('create', Application::class);
|
||||
|
||||
$return = validateIncomingRequest($request);
|
||||
if ($return instanceof \Illuminate\Http\JsonResponse) {
|
||||
if ($return instanceof JsonResponse) {
|
||||
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', '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'];
|
||||
$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', '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'];
|
||||
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'name' => 'string|max:255',
|
||||
@@ -1051,6 +1058,7 @@ class ApplicationsController extends Controller
|
||||
$connectToDockerNetwork = $request->connect_to_docker_network;
|
||||
$customNginxConfiguration = $request->custom_nginx_configuration;
|
||||
$isContainerLabelEscapeEnabled = $request->boolean('is_container_label_escape_enabled', true);
|
||||
$isPreserveRepositoryEnabled = $request->boolean('is_preserve_repository_enabled', false);
|
||||
|
||||
if (! is_null($customNginxConfiguration)) {
|
||||
if (! isBase64Encoded($customNginxConfiguration)) {
|
||||
@@ -1147,14 +1155,14 @@ class ApplicationsController extends Controller
|
||||
$request->offsetSet('name', generate_application_name($request->git_repository, $request->git_branch));
|
||||
}
|
||||
$return = $this->validateDataApplications($request, $server);
|
||||
if ($return instanceof \Illuminate\Http\JsonResponse) {
|
||||
if ($return instanceof JsonResponse) {
|
||||
return $return;
|
||||
}
|
||||
|
||||
$application = new Application;
|
||||
removeUnnecessaryFieldsFromRequest($request);
|
||||
|
||||
$application->fill($request->all());
|
||||
$application->fill($request->only($allowedFields));
|
||||
$dockerComposeDomainsJson = collect();
|
||||
if ($request->has('docker_compose_domains')) {
|
||||
$dockerComposeDomains = collect($request->docker_compose_domains);
|
||||
@@ -1263,6 +1271,10 @@ class ApplicationsController extends Controller
|
||||
$application->settings->is_container_label_escape_enabled = $isContainerLabelEscapeEnabled;
|
||||
$application->settings->save();
|
||||
}
|
||||
if (isset($isPreserveRepositoryEnabled)) {
|
||||
$application->settings->is_preserve_repository_enabled = $isPreserveRepositoryEnabled;
|
||||
$application->settings->save();
|
||||
}
|
||||
$application->refresh();
|
||||
// Auto-generate domain if requested and no custom domain provided
|
||||
if ($autogenerateDomain && blank($fqdn)) {
|
||||
@@ -1342,7 +1354,7 @@ class ApplicationsController extends Controller
|
||||
}
|
||||
|
||||
$return = $this->validateDataApplications($request, $server);
|
||||
if ($return instanceof \Illuminate\Http\JsonResponse) {
|
||||
if ($return instanceof JsonResponse) {
|
||||
return $return;
|
||||
}
|
||||
$githubApp = GithubApp::whereTeamId($teamId)->where('uuid', $githubAppUuid)->first();
|
||||
@@ -1381,7 +1393,7 @@ class ApplicationsController extends Controller
|
||||
$application = new Application;
|
||||
removeUnnecessaryFieldsFromRequest($request);
|
||||
|
||||
$application->fill($request->all());
|
||||
$application->fill($request->only($allowedFields));
|
||||
|
||||
$dockerComposeDomainsJson = collect();
|
||||
if ($request->has('docker_compose_domains')) {
|
||||
@@ -1495,6 +1507,10 @@ class ApplicationsController extends Controller
|
||||
$application->settings->is_container_label_escape_enabled = $isContainerLabelEscapeEnabled;
|
||||
$application->settings->save();
|
||||
}
|
||||
if (isset($isPreserveRepositoryEnabled)) {
|
||||
$application->settings->is_preserve_repository_enabled = $isPreserveRepositoryEnabled;
|
||||
$application->settings->save();
|
||||
}
|
||||
if ($application->settings->is_container_label_readonly_enabled) {
|
||||
$application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
|
||||
$application->save();
|
||||
@@ -1570,7 +1586,7 @@ class ApplicationsController extends Controller
|
||||
}
|
||||
|
||||
$return = $this->validateDataApplications($request, $server);
|
||||
if ($return instanceof \Illuminate\Http\JsonResponse) {
|
||||
if ($return instanceof JsonResponse) {
|
||||
return $return;
|
||||
}
|
||||
$privateKey = PrivateKey::whereTeamId($teamId)->where('uuid', $request->private_key_uuid)->first();
|
||||
@@ -1581,7 +1597,7 @@ class ApplicationsController extends Controller
|
||||
$application = new Application;
|
||||
removeUnnecessaryFieldsFromRequest($request);
|
||||
|
||||
$application->fill($request->all());
|
||||
$application->fill($request->only($allowedFields));
|
||||
|
||||
$dockerComposeDomainsJson = collect();
|
||||
if ($request->has('docker_compose_domains')) {
|
||||
@@ -1691,6 +1707,10 @@ class ApplicationsController extends Controller
|
||||
$application->settings->is_container_label_escape_enabled = $isContainerLabelEscapeEnabled;
|
||||
$application->settings->save();
|
||||
}
|
||||
if (isset($isPreserveRepositoryEnabled)) {
|
||||
$application->settings->is_preserve_repository_enabled = $isPreserveRepositoryEnabled;
|
||||
$application->settings->save();
|
||||
}
|
||||
if ($application->settings->is_container_label_readonly_enabled) {
|
||||
$application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
|
||||
$application->save();
|
||||
@@ -1739,7 +1759,7 @@ class ApplicationsController extends Controller
|
||||
}
|
||||
|
||||
$return = $this->validateDataApplications($request, $server);
|
||||
if ($return instanceof \Illuminate\Http\JsonResponse) {
|
||||
if ($return instanceof JsonResponse) {
|
||||
return $return;
|
||||
}
|
||||
if (! isBase64Encoded($request->dockerfile)) {
|
||||
@@ -1768,7 +1788,7 @@ class ApplicationsController extends Controller
|
||||
}
|
||||
|
||||
$application = new Application;
|
||||
$application->fill($request->all());
|
||||
$application->fill($request->only($allowedFields));
|
||||
$application->fqdn = $fqdn;
|
||||
$application->ports_exposes = $port;
|
||||
$application->build_pack = 'dockerfile';
|
||||
@@ -1847,7 +1867,7 @@ class ApplicationsController extends Controller
|
||||
$request->offsetSet('name', 'docker-image-'.new Cuid2);
|
||||
}
|
||||
$return = $this->validateDataApplications($request, $server);
|
||||
if ($return instanceof \Illuminate\Http\JsonResponse) {
|
||||
if ($return instanceof JsonResponse) {
|
||||
return $return;
|
||||
}
|
||||
// Process docker image name and tag using DockerImageParser
|
||||
@@ -1880,7 +1900,7 @@ class ApplicationsController extends Controller
|
||||
$application = new Application;
|
||||
removeUnnecessaryFieldsFromRequest($request);
|
||||
|
||||
$application->fill($request->all());
|
||||
$application->fill($request->only($allowedFields));
|
||||
$application->fqdn = $fqdn;
|
||||
$application->build_pack = 'dockerimage';
|
||||
$application->destination_id = $destination->id;
|
||||
@@ -1971,7 +1991,7 @@ class ApplicationsController extends Controller
|
||||
], 422);
|
||||
}
|
||||
$return = $this->validateDataApplications($request, $server);
|
||||
if ($return instanceof \Illuminate\Http\JsonResponse) {
|
||||
if ($return instanceof JsonResponse) {
|
||||
return $return;
|
||||
}
|
||||
if (! isBase64Encoded($request->docker_compose_raw)) {
|
||||
@@ -1996,7 +2016,7 @@ class ApplicationsController extends Controller
|
||||
|
||||
$service = new Service;
|
||||
removeUnnecessaryFieldsFromRequest($request);
|
||||
$service->fill($request->all());
|
||||
$service->fill($request->only($allowedFields));
|
||||
|
||||
$service->docker_compose_raw = $dockerComposeRaw;
|
||||
$service->environment_id = $environment->id;
|
||||
@@ -2386,6 +2406,7 @@ class ApplicationsController extends Controller
|
||||
'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'],
|
||||
'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'],
|
||||
'is_container_label_escape_enabled' => ['type' => 'boolean', 'default' => true, 'description' => 'Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off.'],
|
||||
'is_preserve_repository_enabled' => ['type' => 'boolean', 'description' => 'Preserve git repository during application update. If false, the existing repository will be removed and replaced with the new one. If true, the existing repository will be kept and the new one will be ignored. Default is false.'],
|
||||
],
|
||||
)
|
||||
),
|
||||
@@ -2457,7 +2478,7 @@ class ApplicationsController extends Controller
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
$return = validateIncomingRequest($request);
|
||||
if ($return instanceof \Illuminate\Http\JsonResponse) {
|
||||
if ($return instanceof JsonResponse) {
|
||||
return $return;
|
||||
}
|
||||
|
||||
@@ -2471,7 +2492,7 @@ class ApplicationsController extends Controller
|
||||
$this->authorize('update', $application);
|
||||
|
||||
$server = $application->destination->server;
|
||||
$allowedFields = ['name', 'description', 'is_static', 'is_spa', 'is_auto_deploy_enabled', 'is_force_https_enabled', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', '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', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'dockerfile_location', 'docker_compose_location', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'is_container_label_escape_enabled'];
|
||||
$allowedFields = ['name', 'description', 'is_static', 'is_spa', 'is_auto_deploy_enabled', 'is_force_https_enabled', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', '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', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'dockerfile_location', 'dockerfile_target_build', 'docker_compose_location', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'is_container_label_escape_enabled', 'is_preserve_repository_enabled'];
|
||||
|
||||
$validationRules = [
|
||||
'name' => 'string|max:255',
|
||||
@@ -2482,8 +2503,6 @@ class ApplicationsController extends Controller
|
||||
'docker_compose_domains.*' => 'array:name,domain',
|
||||
'docker_compose_domains.*.name' => 'string|required',
|
||||
'docker_compose_domains.*.domain' => 'string|nullable',
|
||||
'docker_compose_custom_start_command' => 'string|nullable',
|
||||
'docker_compose_custom_build_command' => 'string|nullable',
|
||||
'custom_nginx_configuration' => 'string|nullable',
|
||||
'is_http_basic_auth_enabled' => 'boolean|nullable',
|
||||
'http_basic_auth_username' => 'string',
|
||||
@@ -2529,7 +2548,7 @@ class ApplicationsController extends Controller
|
||||
}
|
||||
}
|
||||
$return = $this->validateDataApplications($request, $server);
|
||||
if ($return instanceof \Illuminate\Http\JsonResponse) {
|
||||
if ($return instanceof JsonResponse) {
|
||||
return $return;
|
||||
}
|
||||
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
|
||||
@@ -2720,7 +2739,7 @@ class ApplicationsController extends Controller
|
||||
$connectToDockerNetwork = $request->connect_to_docker_network;
|
||||
$useBuildServer = $request->use_build_server;
|
||||
$isContainerLabelEscapeEnabled = $request->boolean('is_container_label_escape_enabled');
|
||||
|
||||
$isPreserveRepositoryEnabled = $request->boolean('is_preserve_repository_enabled');
|
||||
if (isset($useBuildServer)) {
|
||||
$application->settings->is_build_server_enabled = $useBuildServer;
|
||||
$application->settings->save();
|
||||
@@ -2755,10 +2774,13 @@ class ApplicationsController extends Controller
|
||||
$application->settings->is_container_label_escape_enabled = $isContainerLabelEscapeEnabled;
|
||||
$application->settings->save();
|
||||
}
|
||||
|
||||
if ($request->has('is_preserve_repository_enabled')) {
|
||||
$application->settings->is_preserve_repository_enabled = $isPreserveRepositoryEnabled;
|
||||
$application->settings->save();
|
||||
}
|
||||
removeUnnecessaryFieldsFromRequest($request);
|
||||
|
||||
$data = $request->all();
|
||||
$data = $request->only($allowedFields);
|
||||
if ($requestHasDomains && $server->isProxyShouldRun()) {
|
||||
data_set($data, 'fqdn', $domains);
|
||||
}
|
||||
@@ -2955,10 +2977,10 @@ class ApplicationsController extends Controller
|
||||
}
|
||||
|
||||
$return = validateIncomingRequest($request);
|
||||
if ($return instanceof \Illuminate\Http\JsonResponse) {
|
||||
if ($return instanceof JsonResponse) {
|
||||
return $return;
|
||||
}
|
||||
$application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first();
|
||||
$application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->route('uuid'))->first();
|
||||
|
||||
if (! $application) {
|
||||
return response()->json([
|
||||
@@ -3156,10 +3178,10 @@ class ApplicationsController extends Controller
|
||||
}
|
||||
|
||||
$return = validateIncomingRequest($request);
|
||||
if ($return instanceof \Illuminate\Http\JsonResponse) {
|
||||
if ($return instanceof JsonResponse) {
|
||||
return $return;
|
||||
}
|
||||
$application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first();
|
||||
$application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->route('uuid'))->first();
|
||||
|
||||
if (! $application) {
|
||||
return response()->json([
|
||||
@@ -3176,7 +3198,7 @@ class ApplicationsController extends Controller
|
||||
], 400);
|
||||
}
|
||||
$bulk_data = collect($bulk_data)->map(function ($item) {
|
||||
return collect($item)->only(['key', 'value', 'is_preview', 'is_literal', 'is_multiline', 'is_shown_once', 'is_runtime', 'is_buildtime']);
|
||||
return collect($item)->only(['key', 'value', 'is_preview', 'is_literal', 'is_multiline', 'is_shown_once', 'is_runtime', 'is_buildtime', 'comment']);
|
||||
});
|
||||
$returnedEnvs = collect();
|
||||
foreach ($bulk_data as $item) {
|
||||
@@ -3189,6 +3211,7 @@ class ApplicationsController extends Controller
|
||||
'is_shown_once' => 'boolean',
|
||||
'is_runtime' => 'boolean',
|
||||
'is_buildtime' => 'boolean',
|
||||
'comment' => 'string|nullable|max:256',
|
||||
]);
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
@@ -3221,6 +3244,9 @@ class ApplicationsController extends Controller
|
||||
if ($item->has('is_buildtime') && $env->is_buildtime != $item->get('is_buildtime')) {
|
||||
$env->is_buildtime = $item->get('is_buildtime');
|
||||
}
|
||||
if ($item->has('comment') && $env->comment != $item->get('comment')) {
|
||||
$env->comment = $item->get('comment');
|
||||
}
|
||||
$env->save();
|
||||
} else {
|
||||
$env = $application->environment_variables()->create([
|
||||
@@ -3232,6 +3258,7 @@ class ApplicationsController extends Controller
|
||||
'is_shown_once' => $is_shown_once,
|
||||
'is_runtime' => $item->get('is_runtime', true),
|
||||
'is_buildtime' => $item->get('is_buildtime', true),
|
||||
'comment' => $item->get('comment'),
|
||||
'resourceable_type' => get_class($application),
|
||||
'resourceable_id' => $application->id,
|
||||
]);
|
||||
@@ -3255,6 +3282,9 @@ class ApplicationsController extends Controller
|
||||
if ($item->has('is_buildtime') && $env->is_buildtime != $item->get('is_buildtime')) {
|
||||
$env->is_buildtime = $item->get('is_buildtime');
|
||||
}
|
||||
if ($item->has('comment') && $env->comment != $item->get('comment')) {
|
||||
$env->comment = $item->get('comment');
|
||||
}
|
||||
$env->save();
|
||||
} else {
|
||||
$env = $application->environment_variables()->create([
|
||||
@@ -3266,6 +3296,7 @@ class ApplicationsController extends Controller
|
||||
'is_shown_once' => $is_shown_once,
|
||||
'is_runtime' => $item->get('is_runtime', true),
|
||||
'is_buildtime' => $item->get('is_buildtime', true),
|
||||
'comment' => $item->get('comment'),
|
||||
'resourceable_type' => get_class($application),
|
||||
'resourceable_id' => $application->id,
|
||||
]);
|
||||
@@ -3353,7 +3384,7 @@ class ApplicationsController extends Controller
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
$application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first();
|
||||
$application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->route('uuid'))->first();
|
||||
|
||||
if (! $application) {
|
||||
return response()->json([
|
||||
@@ -3510,7 +3541,7 @@ class ApplicationsController extends Controller
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
$application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first();
|
||||
$application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->route('uuid'))->first();
|
||||
|
||||
if (! $application) {
|
||||
return response()->json([
|
||||
@@ -3520,7 +3551,7 @@ class ApplicationsController extends Controller
|
||||
|
||||
$this->authorize('manageEnvironment', $application);
|
||||
|
||||
$found_env = EnvironmentVariable::where('uuid', $request->env_uuid)
|
||||
$found_env = EnvironmentVariable::where('uuid', $request->route('env_uuid'))
|
||||
->where('resourceable_type', Application::class)
|
||||
->where('resourceable_id', $application->id)
|
||||
->first();
|
||||
@@ -3919,4 +3950,528 @@ class ApplicationsController extends Controller
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
summary: 'List Storages',
|
||||
description: 'List all persistent storages and file storages by application UUID.',
|
||||
path: '/applications/{uuid}/storages',
|
||||
operationId: 'list-storages-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: 'All storages by application UUID.',
|
||||
content: new OA\JsonContent(
|
||||
properties: [
|
||||
new OA\Property(property: 'persistent_storages', type: 'array', items: new OA\Items(type: 'object')),
|
||||
new OA\Property(property: 'file_storages', type: 'array', items: new OA\Items(type: 'object')),
|
||||
],
|
||||
),
|
||||
),
|
||||
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 storages(Request $request): JsonResponse
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
$application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first();
|
||||
|
||||
if (! $application) {
|
||||
return response()->json([
|
||||
'message' => 'Application not found',
|
||||
], 404);
|
||||
}
|
||||
|
||||
$this->authorize('view', $application);
|
||||
|
||||
$persistentStorages = $application->persistentStorages->sortBy('id')->values();
|
||||
$fileStorages = $application->fileStorages->sortBy('id')->values();
|
||||
|
||||
return response()->json([
|
||||
'persistent_storages' => $persistentStorages,
|
||||
'file_storages' => $fileStorages,
|
||||
]);
|
||||
}
|
||||
|
||||
#[OA\Patch(
|
||||
summary: 'Update Storage',
|
||||
description: 'Update a persistent storage or file storage by application UUID.',
|
||||
path: '/applications/{uuid}/storages',
|
||||
operationId: 'update-storage-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(
|
||||
description: 'Storage updated. For read-only storages (from docker-compose or services), only is_preview_suffix_enabled can be updated.',
|
||||
required: true,
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
required: ['type'],
|
||||
properties: [
|
||||
'uuid' => ['type' => 'string', 'description' => 'The UUID of the storage (preferred).'],
|
||||
'id' => ['type' => 'integer', 'description' => 'The ID of the storage (deprecated, use uuid instead).'],
|
||||
'type' => ['type' => 'string', 'enum' => ['persistent', 'file'], 'description' => 'The type of storage: persistent or file.'],
|
||||
'is_preview_suffix_enabled' => ['type' => 'boolean', 'description' => 'Whether to add -pr-N suffix for preview deployments.'],
|
||||
'name' => ['type' => 'string', 'description' => 'The volume name (persistent only, not allowed for read-only storages).'],
|
||||
'mount_path' => ['type' => 'string', 'description' => 'The container mount path (not allowed for read-only storages).'],
|
||||
'host_path' => ['type' => 'string', 'nullable' => true, 'description' => 'The host path (persistent only, not allowed for read-only storages).'],
|
||||
'content' => ['type' => 'string', 'nullable' => true, 'description' => 'The file content (file only, not allowed for read-only storages).'],
|
||||
],
|
||||
additionalProperties: false,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Storage updated.',
|
||||
content: new OA\JsonContent(type: 'object'),
|
||||
),
|
||||
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 update_storage(Request $request): JsonResponse
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
$return = validateIncomingRequest($request);
|
||||
if ($return instanceof JsonResponse) {
|
||||
return $return;
|
||||
}
|
||||
|
||||
$application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->route('uuid'))->first();
|
||||
|
||||
if (! $application) {
|
||||
return response()->json([
|
||||
'message' => 'Application not found',
|
||||
], 404);
|
||||
}
|
||||
|
||||
$this->authorize('update', $application);
|
||||
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'uuid' => 'string',
|
||||
'id' => 'integer',
|
||||
'type' => 'required|string|in:persistent,file',
|
||||
'is_preview_suffix_enabled' => 'boolean',
|
||||
'name' => ['string', 'regex:'.ValidationPatterns::VOLUME_NAME_PATTERN],
|
||||
'mount_path' => 'string',
|
||||
'host_path' => 'string|nullable',
|
||||
'content' => 'string|nullable',
|
||||
]);
|
||||
|
||||
$allAllowedFields = ['uuid', 'id', 'type', 'is_preview_suffix_enabled', 'name', 'mount_path', 'host_path', 'content'];
|
||||
$extraFields = array_diff(array_keys($request->all()), $allAllowedFields);
|
||||
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);
|
||||
}
|
||||
|
||||
$storageUuid = $request->input('uuid');
|
||||
$storageId = $request->input('id');
|
||||
|
||||
if (! $storageUuid && ! $storageId) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => ['uuid' => 'Either uuid or id is required.'],
|
||||
], 422);
|
||||
}
|
||||
|
||||
$lookupField = $storageUuid ? 'uuid' : 'id';
|
||||
$lookupValue = $storageUuid ?? $storageId;
|
||||
|
||||
if ($request->type === 'persistent') {
|
||||
$storage = $application->persistentStorages->where($lookupField, $lookupValue)->first();
|
||||
} else {
|
||||
$storage = $application->fileStorages->where($lookupField, $lookupValue)->first();
|
||||
}
|
||||
|
||||
if (! $storage) {
|
||||
return response()->json([
|
||||
'message' => 'Storage not found.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
$isReadOnly = $storage->shouldBeReadOnlyInUI();
|
||||
$editableOnlyFields = ['name', 'mount_path', 'host_path', 'content'];
|
||||
$requestedEditableFields = array_intersect($editableOnlyFields, array_keys($request->all()));
|
||||
|
||||
if ($isReadOnly && ! empty($requestedEditableFields)) {
|
||||
return response()->json([
|
||||
'message' => 'This storage is read-only (managed by docker-compose or service definition). Only is_preview_suffix_enabled can be updated.',
|
||||
'read_only_fields' => array_values($requestedEditableFields),
|
||||
], 422);
|
||||
}
|
||||
|
||||
// Reject fields that don't apply to the given storage type
|
||||
if (! $isReadOnly) {
|
||||
$typeSpecificInvalidFields = $request->type === 'persistent'
|
||||
? array_intersect(['content'], array_keys($request->all()))
|
||||
: array_intersect(['name', 'host_path'], array_keys($request->all()));
|
||||
|
||||
if (! empty($typeSpecificInvalidFields)) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => collect($typeSpecificInvalidFields)
|
||||
->mapWithKeys(fn ($field) => [$field => "Field '{$field}' is not valid for type '{$request->type}'."]),
|
||||
], 422);
|
||||
}
|
||||
}
|
||||
|
||||
// Always allowed
|
||||
if ($request->has('is_preview_suffix_enabled')) {
|
||||
$storage->is_preview_suffix_enabled = $request->is_preview_suffix_enabled;
|
||||
}
|
||||
|
||||
// Only for editable storages
|
||||
if (! $isReadOnly) {
|
||||
if ($request->type === 'persistent') {
|
||||
if ($request->has('name')) {
|
||||
$storage->name = $request->name;
|
||||
}
|
||||
if ($request->has('mount_path')) {
|
||||
$storage->mount_path = $request->mount_path;
|
||||
}
|
||||
if ($request->has('host_path')) {
|
||||
$storage->host_path = $request->host_path;
|
||||
}
|
||||
} else {
|
||||
if ($request->has('mount_path')) {
|
||||
$storage->mount_path = $request->mount_path;
|
||||
}
|
||||
if ($request->has('content')) {
|
||||
$storage->content = $request->content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$storage->save();
|
||||
|
||||
return response()->json($storage);
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
summary: 'Create Storage',
|
||||
description: 'Create a persistent storage or file storage for an application.',
|
||||
path: '/applications/{uuid}/storages',
|
||||
operationId: 'create-storage-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',
|
||||
required: ['type', 'mount_path'],
|
||||
properties: [
|
||||
'type' => ['type' => 'string', 'enum' => ['persistent', 'file'], 'description' => 'The type of storage.'],
|
||||
'name' => ['type' => 'string', 'description' => 'Volume name (persistent only, required for persistent).'],
|
||||
'mount_path' => ['type' => 'string', 'description' => 'The container mount path.'],
|
||||
'host_path' => ['type' => 'string', 'nullable' => true, 'description' => 'The host path (persistent only, optional).'],
|
||||
'content' => ['type' => 'string', 'nullable' => true, 'description' => 'File content (file only, optional).'],
|
||||
'is_directory' => ['type' => 'boolean', 'description' => 'Whether this is a directory mount (file only, default false).'],
|
||||
'fs_path' => ['type' => 'string', 'description' => 'Host directory path (required when is_directory is true).'],
|
||||
],
|
||||
additionalProperties: false,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 201,
|
||||
description: 'Storage created.',
|
||||
content: new OA\JsonContent(type: 'object'),
|
||||
),
|
||||
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_storage(Request $request): JsonResponse
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
$return = validateIncomingRequest($request);
|
||||
if ($return instanceof JsonResponse) {
|
||||
return $return;
|
||||
}
|
||||
|
||||
$application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first();
|
||||
if (! $application) {
|
||||
return response()->json(['message' => 'Application not found.'], 404);
|
||||
}
|
||||
|
||||
$this->authorize('update', $application);
|
||||
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'type' => 'required|string|in:persistent,file',
|
||||
'name' => ['string', 'regex:'.ValidationPatterns::VOLUME_NAME_PATTERN],
|
||||
'mount_path' => 'required|string',
|
||||
'host_path' => 'string|nullable',
|
||||
'content' => 'string|nullable',
|
||||
'is_directory' => 'boolean',
|
||||
'fs_path' => 'string',
|
||||
]);
|
||||
|
||||
$allAllowedFields = ['type', 'name', 'mount_path', 'host_path', 'content', 'is_directory', 'fs_path'];
|
||||
$extraFields = array_diff(array_keys($request->all()), $allAllowedFields);
|
||||
if ($validator->fails() || ! empty($extraFields)) {
|
||||
$errors = $validator->errors();
|
||||
if (! empty($extraFields)) {
|
||||
foreach ($extraFields as $field) {
|
||||
$errors->add($field, 'This field is not allowed.');
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $errors,
|
||||
], 422);
|
||||
}
|
||||
|
||||
if ($request->type === 'persistent') {
|
||||
if (! $request->name) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => ['name' => 'The name field is required for persistent storages.'],
|
||||
], 422);
|
||||
}
|
||||
|
||||
$typeSpecificInvalidFields = array_intersect(['content', 'is_directory', 'fs_path'], array_keys($request->all()));
|
||||
if (! empty($typeSpecificInvalidFields)) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => collect($typeSpecificInvalidFields)
|
||||
->mapWithKeys(fn ($field) => [$field => "Field '{$field}' is not valid for type 'persistent'."]),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$storage = LocalPersistentVolume::create([
|
||||
'name' => $application->uuid.'-'.$request->name,
|
||||
'mount_path' => $request->mount_path,
|
||||
'host_path' => $request->host_path,
|
||||
'resource_id' => $application->id,
|
||||
'resource_type' => $application->getMorphClass(),
|
||||
]);
|
||||
|
||||
return response()->json($storage, 201);
|
||||
}
|
||||
|
||||
// File storage
|
||||
$typeSpecificInvalidFields = array_intersect(['name', 'host_path'], array_keys($request->all()));
|
||||
if (! empty($typeSpecificInvalidFields)) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => collect($typeSpecificInvalidFields)
|
||||
->mapWithKeys(fn ($field) => [$field => "Field '{$field}' is not valid for type 'file'."]),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$isDirectory = $request->boolean('is_directory', false);
|
||||
|
||||
if ($isDirectory) {
|
||||
if (! $request->fs_path) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => ['fs_path' => 'The fs_path field is required for directory mounts.'],
|
||||
], 422);
|
||||
}
|
||||
|
||||
$fsPath = str($request->fs_path)->trim()->start('/')->value();
|
||||
$mountPath = str($request->mount_path)->trim()->start('/')->value();
|
||||
|
||||
validateShellSafePath($fsPath, 'storage source path');
|
||||
validateShellSafePath($mountPath, 'storage destination path');
|
||||
|
||||
$storage = LocalFileVolume::create([
|
||||
'fs_path' => $fsPath,
|
||||
'mount_path' => $mountPath,
|
||||
'is_directory' => true,
|
||||
'resource_id' => $application->id,
|
||||
'resource_type' => get_class($application),
|
||||
]);
|
||||
} else {
|
||||
$mountPath = str($request->mount_path)->trim()->start('/')->value();
|
||||
|
||||
validateShellSafePath($mountPath, 'file storage path');
|
||||
|
||||
$fsPath = application_configuration_dir().'/'.$application->uuid.$mountPath;
|
||||
|
||||
$storage = LocalFileVolume::create([
|
||||
'fs_path' => $fsPath,
|
||||
'mount_path' => $mountPath,
|
||||
'content' => $request->content,
|
||||
'is_directory' => false,
|
||||
'resource_id' => $application->id,
|
||||
'resource_type' => get_class($application),
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json($storage, 201);
|
||||
}
|
||||
|
||||
#[OA\Delete(
|
||||
summary: 'Delete Storage',
|
||||
description: 'Delete a persistent storage or file storage by application UUID.',
|
||||
path: '/applications/{uuid}/storages/{storage_uuid}',
|
||||
operationId: 'delete-storage-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: 'storage_uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the storage.',
|
||||
required: true,
|
||||
schema: new OA\Schema(type: 'string')
|
||||
),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(response: 200, description: 'Storage deleted.', content: new OA\JsonContent(
|
||||
properties: [new OA\Property(property: 'message', type: 'string')],
|
||||
)),
|
||||
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 delete_storage(Request $request): JsonResponse
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
$application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first();
|
||||
if (! $application) {
|
||||
return response()->json(['message' => 'Application not found.'], 404);
|
||||
}
|
||||
|
||||
$this->authorize('update', $application);
|
||||
|
||||
$storageUuid = $request->route('storage_uuid');
|
||||
|
||||
$storage = $application->persistentStorages->where('uuid', $storageUuid)->first();
|
||||
if (! $storage) {
|
||||
$storage = $application->fileStorages->where('uuid', $storageUuid)->first();
|
||||
}
|
||||
|
||||
if (! $storage) {
|
||||
return response()->json(['message' => 'Storage not found.'], 404);
|
||||
}
|
||||
|
||||
if ($storage->shouldBeReadOnlyInUI()) {
|
||||
return response()->json([
|
||||
'message' => 'This storage is read-only (managed by docker-compose or service definition) and cannot be deleted.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
if ($storage instanceof LocalFileVolume) {
|
||||
$storage->deleteStorageOnServer();
|
||||
}
|
||||
|
||||
$storage->delete();
|
||||
|
||||
return response()->json(['message' => 'Storage deleted.']);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user