fix(api): applications post and patch endpoints

- remove `docker_compose_raw` from post and patch endpoints, as the compose file is sourced from git and should not be manually settable via the api
- improve the documentation for `docker_compose_domains` (URLs)
- enhanced array validation for `docker_compose_domains` by validating each array field and verifying which fields are allowed
- set a custom array validation error message, as the default message is not really clear
- show an error if the user attempts to set `domains` when the build pack is `dockercompose`
- validate that the `domains` in `docker_compose_domains` are proper URLs and include a valid scheme (`http` or `https`)
This commit is contained in:
peaklabs-dev
2026-01-12 21:42:20 +01:00
parent 7c0cb2f59d
commit ce3cae3ff9
2 changed files with 254 additions and 112 deletions

View File

@@ -19,6 +19,7 @@ use App\Rules\ValidGitBranch;
use App\Rules\ValidGitRepositoryUrl; use App\Rules\ValidGitRepositoryUrl;
use App\Services\DockerImageParser; use App\Services\DockerImageParser;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
use Spatie\Url\Url; use Spatie\Url\Url;
@@ -198,10 +199,19 @@ class ApplicationsController extends Controller
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'], 'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'],
'dockerfile' => ['type' => 'string', 'description' => 'The Dockerfile content.'], 'dockerfile' => ['type' => 'string', 'description' => 'The Dockerfile content.'],
'docker_compose_location' => ['type' => 'string', 'description' => 'The Docker Compose location.'], 'docker_compose_location' => ['type' => 'string', 'description' => 'The Docker Compose location.'],
'docker_compose_raw' => ['type' => 'string', 'description' => 'The Docker Compose raw content.'],
'docker_compose_custom_start_command' => ['type' => 'string', 'description' => 'The Docker Compose custom start command.'], 'docker_compose_custom_start_command' => ['type' => 'string', 'description' => 'The Docker Compose custom start command.'],
'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'], 'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'],
'docker_compose_domains' => ['type' => 'array', 'description' => 'The Docker Compose domains.'], 'docker_compose_domains' => [
'type' => 'array',
'description' => 'Array of URLs to be applied to containers of a dockercompose application.',
'items' => new OA\Schema(
type: 'object',
properties: [
'name' => ['type' => 'string', 'description' => 'The service name as defined in docker-compose.'],
'domain' => ['type' => 'string', 'description' => 'Comma-separated list of URLs (e.g. "http://app.coolify.io,https://app2.coolify.io")'],
],
),
],
'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'], 'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'],
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'], 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
'is_http_basic_auth_enabled' => ['type' => 'boolean', 'description' => 'HTTP Basic Authentication enabled.'], 'is_http_basic_auth_enabled' => ['type' => 'boolean', 'description' => 'HTTP Basic Authentication enabled.'],
@@ -350,10 +360,19 @@ class ApplicationsController extends Controller
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'], 'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'],
'dockerfile' => ['type' => 'string', 'description' => 'The Dockerfile content.'], 'dockerfile' => ['type' => 'string', 'description' => 'The Dockerfile content.'],
'docker_compose_location' => ['type' => 'string', 'description' => 'The Docker Compose location.'], 'docker_compose_location' => ['type' => 'string', 'description' => 'The Docker Compose location.'],
'docker_compose_raw' => ['type' => 'string', 'description' => 'The Docker Compose raw content.'],
'docker_compose_custom_start_command' => ['type' => 'string', 'description' => 'The Docker Compose custom start command.'], 'docker_compose_custom_start_command' => ['type' => 'string', 'description' => 'The Docker Compose custom start command.'],
'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'], 'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'],
'docker_compose_domains' => ['type' => 'array', 'description' => 'The Docker Compose domains.'], 'docker_compose_domains' => [
'type' => 'array',
'description' => 'Array of URLs to be applied to containers of a dockercompose application.',
'items' => new OA\Schema(
type: 'object',
properties: [
'name' => ['type' => 'string', 'description' => 'The service name as defined in docker-compose.'],
'domain' => ['type' => 'string', 'description' => 'Comma-separated list of URLs (e.g. "http://app.coolify.io,https://app2.coolify.io")'],
],
),
],
'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'], 'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'],
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'], 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
'is_http_basic_auth_enabled' => ['type' => 'boolean', 'description' => 'HTTP Basic Authentication enabled.'], 'is_http_basic_auth_enabled' => ['type' => 'boolean', 'description' => 'HTTP Basic Authentication enabled.'],
@@ -502,10 +521,19 @@ class ApplicationsController extends Controller
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'], 'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'],
'dockerfile' => ['type' => 'string', 'description' => 'The Dockerfile content.'], 'dockerfile' => ['type' => 'string', 'description' => 'The Dockerfile content.'],
'docker_compose_location' => ['type' => 'string', 'description' => 'The Docker Compose location.'], 'docker_compose_location' => ['type' => 'string', 'description' => 'The Docker Compose location.'],
'docker_compose_raw' => ['type' => 'string', 'description' => 'The Docker Compose raw content.'],
'docker_compose_custom_start_command' => ['type' => 'string', 'description' => 'The Docker Compose custom start command.'], 'docker_compose_custom_start_command' => ['type' => 'string', 'description' => 'The Docker Compose custom start command.'],
'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'], 'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'],
'docker_compose_domains' => ['type' => 'array', 'description' => 'The Docker Compose domains.'], 'docker_compose_domains' => [
'type' => 'array',
'description' => 'Array of URLs to be applied to containers of a dockercompose application.',
'items' => new OA\Schema(
type: 'object',
properties: [
'name' => ['type' => 'string', 'description' => 'The service name as defined in docker-compose.'],
'domain' => ['type' => 'string', 'description' => 'Comma-separated list of URLs (e.g. "http://app.coolify.io,https://app2.coolify.io")'],
],
),
],
'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'], 'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'],
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'], 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
'is_http_basic_auth_enabled' => ['type' => 'boolean', 'description' => 'HTTP Basic Authentication enabled.'], 'is_http_basic_auth_enabled' => ['type' => 'boolean', 'description' => 'HTTP Basic Authentication enabled.'],
@@ -1056,8 +1084,10 @@ class ApplicationsController extends Controller
'build_pack' => ['required', Rule::enum(BuildPackTypes::class)], 'build_pack' => ['required', Rule::enum(BuildPackTypes::class)],
'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required', 'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required',
'docker_compose_location' => 'string', 'docker_compose_location' => 'string',
'docker_compose_raw' => 'string|nullable',
'docker_compose_domains' => 'array|nullable', 'docker_compose_domains' => 'array|nullable',
'docker_compose_domains.*' => 'array:name,domain',
'docker_compose_domains.*.name' => 'string|required',
'docker_compose_domains.*.domain' => 'string|nullable',
]; ];
// ports_exposes is not required for dockercompose // ports_exposes is not required for dockercompose
if ($request->build_pack === 'dockercompose') { if ($request->build_pack === 'dockercompose') {
@@ -1065,13 +1095,26 @@ class ApplicationsController extends Controller
$request->offsetSet('ports_exposes', '80'); $request->offsetSet('ports_exposes', '80');
} }
$validationRules = array_merge(sharedDataApplications(), $validationRules); $validationRules = array_merge(sharedDataApplications(), $validationRules);
$validator = customApiValidator($request->all(), $validationRules); $validationMessages = [
'docker_compose_domains.*.array' => 'An item in the docker_compose_domains array has invalid fields. Only a name and domain field are supported.',
];
$validator = Validator::make($request->all(), $validationRules, $validationMessages);
if ($validator->fails()) { if ($validator->fails()) {
return response()->json([ return response()->json([
'message' => 'Validation failed.', 'message' => 'Validation failed.',
'errors' => $validator->errors(), 'errors' => $validator->errors(),
], 422); ], 422);
} }
// For dockercompose applications, domains (fqdn) field should not be used
// Only docker_compose_domains should be used to set domains for individual services
if ($request->build_pack === 'dockercompose' && $request->has('domains')) {
return response()->json([
'message' => 'Validation failed.',
'errors' => [
'domains' => 'The domains field cannot be used for dockercompose applications. Use docker_compose_domains instead to set domains for individual services.',
],
], 422);
}
if (! $request->has('name')) { if (! $request->has('name')) {
$request->offsetSet('name', generate_application_name($request->git_repository, $request->git_branch)); $request->offsetSet('name', generate_application_name($request->git_repository, $request->git_branch));
} }
@@ -1087,11 +1130,42 @@ class ApplicationsController extends Controller
$dockerComposeDomainsJson = collect(); $dockerComposeDomainsJson = collect();
if ($request->has('docker_compose_domains')) { if ($request->has('docker_compose_domains')) {
$dockerComposeDomains = collect($request->docker_compose_domains); $dockerComposeDomains = collect($request->docker_compose_domains);
if ($dockerComposeDomains->count() > 0) { $domainErrors = [];
$dockerComposeDomains->each(function ($domain, $key) use ($dockerComposeDomainsJson) {
$dockerComposeDomainsJson->put(data_get($domain, 'name'), ['domain' => data_get($domain, 'domain')]); foreach ($dockerComposeDomains as $index => $item) {
}); $domainValue = data_get($item, 'domain');
if (filled($domainValue)) {
$urls = str($domainValue)->replaceStart(',', '')->replaceEnd(',', '')->trim();
str($urls)->explode(',')->each(function ($url) use (&$domainErrors) {
$url = trim($url);
if (empty($url)) {
return;
}
if (! filter_var($url, FILTER_VALIDATE_URL)) {
$domainErrors[] = "Invalid URL: {$url}";
return;
}
$scheme = parse_url($url, PHP_URL_SCHEME) ?? '';
if (! in_array(strtolower($scheme), ['http', 'https'])) {
$domainErrors[] = "Invalid URL scheme: {$scheme} for URL: {$url}. Only http and https are supported.";
}
});
}
} }
if (! empty($domainErrors)) {
return response()->json([
'message' => 'Validation failed.',
'errors' => [
'docker_compose_domains' => $domainErrors,
],
], 422);
}
$dockerComposeDomains->each(function ($domain) use ($dockerComposeDomainsJson) {
$dockerComposeDomainsJson->put(data_get($domain, 'name'), ['domain' => data_get($domain, 'domain')]);
});
$request->offsetUnset('docker_compose_domains'); $request->offsetUnset('docker_compose_domains');
} }
if ($dockerComposeDomainsJson->count() > 0) { if ($dockerComposeDomainsJson->count() > 0) {
@@ -1170,17 +1244,32 @@ class ApplicationsController extends Controller
'github_app_uuid' => 'string|required', 'github_app_uuid' => 'string|required',
'watch_paths' => 'string|nullable', 'watch_paths' => 'string|nullable',
'docker_compose_location' => 'string', 'docker_compose_location' => 'string',
'docker_compose_raw' => 'string|nullable', 'docker_compose_domains' => 'array|nullable',
'docker_compose_domains.*' => 'array:name,domain',
'docker_compose_domains.*.name' => 'string|required',
'docker_compose_domains.*.domain' => 'string|nullable',
]; ];
$validationRules = array_merge(sharedDataApplications(), $validationRules); $validationRules = array_merge(sharedDataApplications(), $validationRules);
$validationMessages = [
$validator = customApiValidator($request->all(), $validationRules); 'docker_compose_domains.*.array' => 'An item in the docker_compose_domains array has invalid fields. Only a name and domain field are supported.',
];
$validator = Validator::make($request->all(), $validationRules, $validationMessages);
if ($validator->fails()) { if ($validator->fails()) {
return response()->json([ return response()->json([
'message' => 'Validation failed.', 'message' => 'Validation failed.',
'errors' => $validator->errors(), 'errors' => $validator->errors(),
], 422); ], 422);
} }
// For dockercompose applications, domains (fqdn) field should not be used
// Only docker_compose_domains should be used to set domains for individual services
if ($request->build_pack === 'dockercompose' && $request->has('domains')) {
return response()->json([
'message' => 'Validation failed.',
'errors' => [
'domains' => 'The domains field cannot be used for dockercompose applications. Use docker_compose_domains instead to set domains for individual services.',
],
], 422);
}
if (! $request->has('name')) { if (! $request->has('name')) {
$request->offsetSet('name', generate_application_name($request->git_repository, $request->git_branch)); $request->offsetSet('name', generate_application_name($request->git_repository, $request->git_branch));
@@ -1229,43 +1318,43 @@ class ApplicationsController extends Controller
$dockerComposeDomainsJson = collect(); $dockerComposeDomainsJson = collect();
if ($request->has('docker_compose_domains')) { if ($request->has('docker_compose_domains')) {
if (! $request->has('docker_compose_raw')) { $dockerComposeDomains = collect($request->docker_compose_domains);
$domainErrors = [];
foreach ($dockerComposeDomains as $index => $item) {
$domainValue = data_get($item, 'domain');
if (filled($domainValue)) {
$urls = str($domainValue)->replaceStart(',', '')->replaceEnd(',', '')->trim();
str($urls)->explode(',')->each(function ($url) use (&$domainErrors) {
$url = trim($url);
if (empty($url)) {
return;
}
if (! filter_var($url, FILTER_VALIDATE_URL)) {
$domainErrors[] = "Invalid URL: {$url}";
return;
}
$scheme = parse_url($url, PHP_URL_SCHEME) ?? '';
if (! in_array(strtolower($scheme), ['http', 'https'])) {
$domainErrors[] = "Invalid URL scheme: {$scheme} for URL: {$url}. Only http and https are supported.";
}
});
}
}
if (! empty($domainErrors)) {
return response()->json([ return response()->json([
'message' => 'Validation failed.', 'message' => 'Validation failed.',
'errors' => [ 'errors' => [
'docker_compose_raw' => 'The base64 encoded docker_compose_raw is required.', 'docker_compose_domains' => $domainErrors,
], ],
], 422); ], 422);
} }
if (! isBase64Encoded($request->docker_compose_raw)) { $dockerComposeDomains->each(function ($domain) use ($dockerComposeDomainsJson) {
return response()->json([ $dockerComposeDomainsJson->put(data_get($domain, 'name'), ['domain' => data_get($domain, 'domain')]);
'message' => 'Validation failed.', });
'errors' => [
'docker_compose_raw' => 'The docker_compose_raw should be base64 encoded.',
],
], 422);
}
$dockerComposeRaw = base64_decode($request->docker_compose_raw);
if (mb_detect_encoding($dockerComposeRaw, 'ASCII', true) === false) {
return response()->json([
'message' => 'Validation failed.',
'errors' => [
'docker_compose_raw' => 'The docker_compose_raw should be base64 encoded.',
],
], 422);
}
$yaml = Yaml::parse($dockerComposeRaw);
$services = data_get($yaml, 'services');
$dockerComposeDomains = collect($request->docker_compose_domains);
if ($dockerComposeDomains->count() > 0) {
$dockerComposeDomains->each(function ($domain, $key) use ($services, $dockerComposeDomainsJson) {
$name = data_get($domain, 'name');
if (data_get($services, $name)) {
$dockerComposeDomainsJson->put($name, ['domain' => data_get($domain, 'domain')]);
}
});
}
$request->offsetUnset('docker_compose_domains'); $request->offsetUnset('docker_compose_domains');
} }
if ($dockerComposeDomainsJson->count() > 0) { if ($dockerComposeDomainsJson->count() > 0) {
@@ -1335,11 +1424,17 @@ class ApplicationsController extends Controller
'private_key_uuid' => 'string|required', 'private_key_uuid' => 'string|required',
'watch_paths' => 'string|nullable', 'watch_paths' => 'string|nullable',
'docker_compose_location' => 'string', 'docker_compose_location' => 'string',
'docker_compose_raw' => 'string|nullable', 'docker_compose_domains' => 'array|nullable',
'docker_compose_domains.*' => 'array:name,domain',
'docker_compose_domains.*.name' => 'string|required',
'docker_compose_domains.*.domain' => 'string|nullable',
]; ];
$validationRules = array_merge(sharedDataApplications(), $validationRules); $validationRules = array_merge(sharedDataApplications(), $validationRules);
$validator = customApiValidator($request->all(), $validationRules); $validationMessages = [
'docker_compose_domains.*.array' => 'An item in the docker_compose_domains array has invalid fields. Only a name and domain field are supported.',
];
$validator = Validator::make($request->all(), $validationRules, $validationMessages);
if ($validator->fails()) { if ($validator->fails()) {
return response()->json([ return response()->json([
@@ -1347,6 +1442,16 @@ class ApplicationsController extends Controller
'errors' => $validator->errors(), 'errors' => $validator->errors(),
], 422); ], 422);
} }
// For dockercompose applications, domains (fqdn) field should not be used
// Only docker_compose_domains should be used to set domains for individual services
if ($request->build_pack === 'dockercompose' && $request->has('domains')) {
return response()->json([
'message' => 'Validation failed.',
'errors' => [
'domains' => 'The domains field cannot be used for dockercompose applications. Use docker_compose_domains instead to set domains for individual services.',
],
], 422);
}
if (! $request->has('name')) { if (! $request->has('name')) {
$request->offsetSet('name', generate_application_name($request->git_repository, $request->git_branch)); $request->offsetSet('name', generate_application_name($request->git_repository, $request->git_branch));
} }
@@ -1370,44 +1475,43 @@ class ApplicationsController extends Controller
$dockerComposeDomainsJson = collect(); $dockerComposeDomainsJson = collect();
if ($request->has('docker_compose_domains')) { if ($request->has('docker_compose_domains')) {
if (! $request->has('docker_compose_raw')) { $dockerComposeDomains = collect($request->docker_compose_domains);
$domainErrors = [];
foreach ($dockerComposeDomains as $index => $item) {
$domainValue = data_get($item, 'domain');
if (filled($domainValue)) {
$urls = str($domainValue)->replaceStart(',', '')->replaceEnd(',', '')->trim();
str($urls)->explode(',')->each(function ($url) use (&$domainErrors) {
$url = trim($url);
if (empty($url)) {
return;
}
if (! filter_var($url, FILTER_VALIDATE_URL)) {
$domainErrors[] = "Invalid URL: {$url}";
return;
}
$scheme = parse_url($url, PHP_URL_SCHEME) ?? '';
if (! in_array(strtolower($scheme), ['http', 'https'])) {
$domainErrors[] = "Invalid URL scheme: {$scheme} for URL: {$url}. Only http and https are supported.";
}
});
}
}
if (! empty($domainErrors)) {
return response()->json([ return response()->json([
'message' => 'Validation failed.', 'message' => 'Validation failed.',
'errors' => [ 'errors' => [
'docker_compose_raw' => 'The base64 encoded docker_compose_raw is required.', 'docker_compose_domains' => $domainErrors,
], ],
], 422); ], 422);
} }
if (! isBase64Encoded($request->docker_compose_raw)) { $dockerComposeDomains->each(function ($domain) use ($dockerComposeDomainsJson) {
return response()->json([ $dockerComposeDomainsJson->put(data_get($domain, 'name'), ['domain' => data_get($domain, 'domain')]);
'message' => 'Validation failed.', });
'errors' => [
'docker_compose_raw' => 'The docker_compose_raw should be base64 encoded.',
],
], 422);
}
$dockerComposeRaw = base64_decode($request->docker_compose_raw);
if (mb_detect_encoding($dockerComposeRaw, 'ASCII', true) === false) {
return response()->json([
'message' => 'Validation failed.',
'errors' => [
'docker_compose_raw' => 'The docker_compose_raw should be base64 encoded.',
],
], 422);
}
$dockerComposeRaw = base64_decode($request->docker_compose_raw);
$yaml = Yaml::parse($dockerComposeRaw);
$services = data_get($yaml, 'services');
$dockerComposeDomains = collect($request->docker_compose_domains);
if ($dockerComposeDomains->count() > 0) {
$dockerComposeDomains->each(function ($domain, $key) use ($services, $dockerComposeDomainsJson) {
$name = data_get($domain, 'name');
if (data_get($services, $name)) {
$dockerComposeDomainsJson->put($name, ['domain' => data_get($domain, 'domain')]);
}
});
}
$request->offsetUnset('docker_compose_domains'); $request->offsetUnset('docker_compose_domains');
} }
if ($dockerComposeDomainsJson->count() > 0) { if ($dockerComposeDomainsJson->count() > 0) {
@@ -2090,10 +2194,19 @@ class ApplicationsController extends Controller
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'], 'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'],
'dockerfile' => ['type' => 'string', 'description' => 'The Dockerfile content.'], 'dockerfile' => ['type' => 'string', 'description' => 'The Dockerfile content.'],
'docker_compose_location' => ['type' => 'string', 'description' => 'The Docker Compose location.'], 'docker_compose_location' => ['type' => 'string', 'description' => 'The Docker Compose location.'],
'docker_compose_raw' => ['type' => 'string', 'description' => 'The Docker Compose raw content.'],
'docker_compose_custom_start_command' => ['type' => 'string', 'description' => 'The Docker Compose custom start command.'], 'docker_compose_custom_start_command' => ['type' => 'string', 'description' => 'The Docker Compose custom start command.'],
'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'], 'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'],
'docker_compose_domains' => ['type' => 'array', 'description' => 'The Docker Compose domains.'], 'docker_compose_domains' => [
'type' => 'array',
'description' => 'Array of URLs to be applied to containers of a dockercompose application.',
'items' => new OA\Schema(
type: 'object',
properties: [
'name' => ['type' => 'string', 'description' => 'The service name as defined in docker-compose.'],
'domain' => ['type' => 'string', 'description' => 'Comma-separated list of URLs (e.g. "http://app.coolify.io,https://app2.coolify.io")'],
],
),
],
'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'], 'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'],
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'], 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'], 'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'],
@@ -2192,8 +2305,10 @@ class ApplicationsController extends Controller
'static_image' => 'string', 'static_image' => 'string',
'watch_paths' => 'string|nullable', 'watch_paths' => 'string|nullable',
'docker_compose_location' => 'string', 'docker_compose_location' => 'string',
'docker_compose_raw' => 'string|nullable',
'docker_compose_domains' => 'array|nullable', 'docker_compose_domains' => 'array|nullable',
'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_start_command' => 'string|nullable',
'docker_compose_custom_build_command' => 'string|nullable', 'docker_compose_custom_build_command' => 'string|nullable',
'custom_nginx_configuration' => 'string|nullable', 'custom_nginx_configuration' => 'string|nullable',
@@ -2202,7 +2317,10 @@ class ApplicationsController extends Controller
'http_basic_auth_password' => 'string', 'http_basic_auth_password' => 'string',
]; ];
$validationRules = array_merge(sharedDataApplications(), $validationRules); $validationRules = array_merge(sharedDataApplications(), $validationRules);
$validator = customApiValidator($request->all(), $validationRules); $validationMessages = [
'docker_compose_domains.*.array' => 'An item in the docker_compose_domains array has invalid fields. Only a name and domain field are supported.',
];
$validator = Validator::make($request->all(), $validationRules, $validationMessages);
// Validate ports_exposes // Validate ports_exposes
if ($request->has('ports_exposes')) { if ($request->has('ports_exposes')) {
@@ -2278,6 +2396,17 @@ class ApplicationsController extends Controller
$application->save(); $application->save();
} }
// For dockercompose applications, domains (fqdn) field should not be used
// Only docker_compose_domains should be used to set domains for individual services
if ($application->build_pack === 'dockercompose' && $request->has('domains')) {
return response()->json([
'message' => 'Validation failed.',
'errors' => [
'domains' => 'The domains field cannot be used for dockercompose applications. Use docker_compose_domains instead to set domains for individual services.',
],
], 422);
}
$domains = $request->domains; $domains = $request->domains;
$requestHasDomains = $request->has('domains'); $requestHasDomains = $request->has('domains');
if ($requestHasDomains && $server->isProxyShouldRun()) { if ($requestHasDomains && $server->isProxyShouldRun()) {
@@ -2321,44 +2450,57 @@ class ApplicationsController extends Controller
$dockerComposeDomainsJson = collect(); $dockerComposeDomainsJson = collect();
if ($request->has('docker_compose_domains')) { if ($request->has('docker_compose_domains')) {
if (! $request->has('docker_compose_raw')) { if (empty($application->docker_compose_raw)) {
return response()->json([ return response()->json([
'message' => 'Validation failed.', 'message' => 'Validation failed.',
'errors' => [ 'errors' => [
'docker_compose_raw' => 'The base64 encoded docker_compose_raw is required.', 'docker_compose_domains' => 'Cannot set docker_compose_domains without docker_compose_raw. Reload the compose file from the git repository first.',
], ],
], 422); ], 422);
} }
if (! isBase64Encoded($request->docker_compose_raw)) {
return response()->json([
'message' => 'Validation failed.',
'errors' => [
'docker_compose_raw' => 'The docker_compose_raw should be base64 encoded.',
],
], 422);
}
$dockerComposeRaw = base64_decode($request->docker_compose_raw);
if (mb_detect_encoding($dockerComposeRaw, 'ASCII', true) === false) {
return response()->json([
'message' => 'Validation failed.',
'errors' => [
'docker_compose_raw' => 'The docker_compose_raw should be base64 encoded.',
],
], 422);
}
$dockerComposeRaw = base64_decode($request->docker_compose_raw);
$yaml = Yaml::parse($dockerComposeRaw);
$services = data_get($yaml, 'services');
$dockerComposeDomains = collect($request->docker_compose_domains); $dockerComposeDomains = collect($request->docker_compose_domains);
if ($dockerComposeDomains->count() > 0) { $domainErrors = [];
$dockerComposeDomains->each(function ($domain, $key) use ($services, $dockerComposeDomainsJson) {
$name = data_get($domain, 'name'); foreach ($dockerComposeDomains as $item) {
if (data_get($services, $name)) { $domainValue = data_get($item, 'domain');
$dockerComposeDomainsJson->put($name, ['domain' => data_get($domain, 'domain')]); if (filled($domainValue)) {
} $urls = str($domainValue)->replaceStart(',', '')->replaceEnd(',', '')->trim();
}); str($urls)->explode(',')->each(function ($url) use (&$domainErrors) {
$url = trim($url);
if (empty($url)) {
return;
}
if (! filter_var($url, FILTER_VALIDATE_URL)) {
$domainErrors[] = "Invalid URL: {$url}";
return;
}
$scheme = parse_url($url, PHP_URL_SCHEME) ?? '';
if (! in_array(strtolower($scheme), ['http', 'https'])) {
$domainErrors[] = "Invalid URL scheme: {$scheme} for URL: {$url}. Only http and https are supported.";
}
});
}
} }
if (! empty($domainErrors)) {
return response()->json([
'message' => 'Validation failed.',
'errors' => [
'docker_compose_domains' => $domainErrors,
],
], 422);
}
$yaml = Yaml::parse($application->docker_compose_raw);
$services = data_get($yaml, 'services', []);
$dockerComposeDomains->each(function ($domain) use ($services, $dockerComposeDomainsJson) {
$name = data_get($domain, 'name');
if ($name && is_array($services) && isset($services[$name])) {
$dockerComposeDomainsJson->put($name, ['domain' => data_get($domain, 'domain')]);
}
});
$request->offsetUnset('docker_compose_domains'); $request->offsetUnset('docker_compose_domains');
} }
$instantDeploy = $request->instant_deploy; $instantDeploy = $request->instant_deploy;

View File

@@ -131,7 +131,6 @@ function sharedDataApplications()
'manual_webhook_secret_gitea' => 'string|nullable', 'manual_webhook_secret_gitea' => 'string|nullable',
'docker_compose_location' => 'string', 'docker_compose_location' => 'string',
'docker_compose' => 'string|nullable', 'docker_compose' => 'string|nullable',
'docker_compose_raw' => 'string|nullable',
'docker_compose_domains' => 'array|nullable', 'docker_compose_domains' => 'array|nullable',
'docker_compose_custom_start_command' => 'string|nullable', 'docker_compose_custom_start_command' => 'string|nullable',
'docker_compose_custom_build_command' => 'string|nullable', 'docker_compose_custom_build_command' => 'string|nullable',
@@ -181,4 +180,5 @@ function removeUnnecessaryFieldsFromRequest(Request $request)
$request->offsetUnset('force_domain_override'); $request->offsetUnset('force_domain_override');
$request->offsetUnset('autogenerate_domain'); $request->offsetUnset('autogenerate_domain');
$request->offsetUnset('is_container_label_escape_enabled'); $request->offsetUnset('is_container_label_escape_enabled');
$request->offsetUnset('docker_compose_raw');
} }