Merge branch 'next' into shadow/fix-docker-time-command

This commit is contained in:
ShadowArcanist authored and GitHub committed 2025-10-17 18:52:40 +05:30
commit 9620455be0
310 files changed
+22764 -11444

No files matched your search

+280 -120
View File
@@ -3,6 +3,7 @@
namespace App\Livewire\Project\Application;
use App\Actions\Application\GenerateConfig;
use App\Livewire\Concerns\SynchronizesModelData;
use App\Models\Application;
use App\Support\ValidationPatterns;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
@@ -14,6 +15,7 @@ use Visus\Cuid2\Cuid2;
class General extends Component
{
use AuthorizesRequests;
use SynchronizesModelData;
public string $applicationId;
@@ -23,6 +25,8 @@ class General extends Component
public string $name;
public ?string $description = null;
public ?string $fqdn = null;
public string $git_repository;
@@ -31,14 +35,82 @@ class General extends Component
public ?string $git_commit_sha = null;
public ?string $install_command = null;
public ?string $build_command = null;
public ?string $start_command = null;
public string $build_pack;
public string $static_image;
public string $base_directory;
public ?string $publish_directory = null;
public ?string $ports_exposes = null;
public ?string $ports_mappings = null;
public ?string $custom_network_aliases = null;
public ?string $dockerfile = null;
public ?string $dockerfile_location = null;
public ?string $dockerfile_target_build = null;
public ?string $docker_registry_image_name = null;
public ?string $docker_registry_image_tag = null;
public ?string $docker_compose_location = null;
public ?string $docker_compose = null;
public ?string $docker_compose_raw = null;
public ?string $docker_compose_custom_start_command = null;
public ?string $docker_compose_custom_build_command = null;
public ?string $custom_labels = null;
public ?string $custom_docker_run_options = null;
public ?string $pre_deployment_command = null;
public ?string $pre_deployment_command_container = null;
public ?string $post_deployment_command = null;
public ?string $post_deployment_command_container = null;
public ?string $custom_nginx_configuration = null;
public bool $is_static = false;
public bool $is_spa = false;
public bool $is_build_server_enabled = false;
public bool $is_preserve_repository_enabled = false;
public bool $is_container_label_escape_enabled = true;
public bool $is_container_label_readonly_enabled = false;
public bool $is_http_basic_auth_enabled = false;
public ?string $http_basic_auth_username = null;
public ?string $http_basic_auth_password = null;
public ?string $watch_paths = null;
public string $redirect;
public $customLabels;
public bool $labelsChanged = false;
@@ -66,50 +138,50 @@ class General extends Component
protected function rules(): array
{
return [
'application.name' => ValidationPatterns::nameRules(),
'application.description' => ValidationPatterns::descriptionRules(),
'application.fqdn' => 'nullable',
'application.git_repository' => 'required',
'application.git_branch' => 'required',
'application.git_commit_sha' => 'nullable',
'application.install_command' => 'nullable',
'application.build_command' => 'nullable',
'application.start_command' => 'nullable',
'application.build_pack' => 'required',
'application.static_image' => 'required',
'application.base_directory' => 'required',
'application.publish_directory' => 'nullable',
'application.ports_exposes' => 'required',
'application.ports_mappings' => 'nullable',
'application.custom_network_aliases' => 'nullable',
'application.dockerfile' => 'nullable',
'application.docker_registry_image_name' => 'nullable',
'application.docker_registry_image_tag' => 'nullable',
'application.dockerfile_location' => 'nullable',
'application.docker_compose_location' => 'nullable',
'application.docker_compose' => 'nullable',
'application.docker_compose_raw' => 'nullable',
'application.dockerfile_target_build' => 'nullable',
'application.docker_compose_custom_start_command' => 'nullable',
'application.docker_compose_custom_build_command' => 'nullable',
'application.custom_labels' => 'nullable',
'application.custom_docker_run_options' => 'nullable',
'application.pre_deployment_command' => 'nullable',
'application.pre_deployment_command_container' => 'nullable',
'application.post_deployment_command' => 'nullable',
'application.post_deployment_command_container' => 'nullable',
'application.custom_nginx_configuration' => 'nullable',
'application.settings.is_static' => 'boolean|required',
'application.settings.is_spa' => 'boolean|required',
'application.settings.is_build_server_enabled' => 'boolean|required',
'application.settings.is_container_label_escape_enabled' => 'boolean|required',
'application.settings.is_container_label_readonly_enabled' => 'boolean|required',
'application.settings.is_preserve_repository_enabled' => 'boolean|required',
'application.is_http_basic_auth_enabled' => 'boolean|required',
'application.http_basic_auth_username' => 'string|nullable',
'application.http_basic_auth_password' => 'string|nullable',
'application.watch_paths' => 'nullable',
'application.redirect' => 'string|required',
'name' => ValidationPatterns::nameRules(),
'description' => ValidationPatterns::descriptionRules(),
'fqdn' => 'nullable',
'git_repository' => 'required',
'git_branch' => 'required',
'git_commit_sha' => 'nullable',
'install_command' => 'nullable',
'build_command' => 'nullable',
'start_command' => 'nullable',
'build_pack' => 'required',
'static_image' => 'required',
'base_directory' => 'required',
'publish_directory' => 'nullable',
'ports_exposes' => 'required',
'ports_mappings' => 'nullable',
'custom_network_aliases' => 'nullable',
'dockerfile' => 'nullable',
'docker_registry_image_name' => 'nullable',
'docker_registry_image_tag' => 'nullable',
'dockerfile_location' => 'nullable',
'docker_compose_location' => 'nullable',
'docker_compose' => 'nullable',
'docker_compose_raw' => 'nullable',
'dockerfile_target_build' => 'nullable',
'docker_compose_custom_start_command' => 'nullable',
'docker_compose_custom_build_command' => 'nullable',
'custom_labels' => 'nullable',
'custom_docker_run_options' => 'nullable',
'pre_deployment_command' => 'nullable',
'pre_deployment_command_container' => 'nullable',
'post_deployment_command' => 'nullable',
'post_deployment_command_container' => 'nullable',
'custom_nginx_configuration' => 'nullable',
'is_static' => 'boolean|required',
'is_spa' => 'boolean|required',
'is_build_server_enabled' => 'boolean|required',
'is_container_label_escape_enabled' => 'boolean|required',
'is_container_label_readonly_enabled' => 'boolean|required',
'is_preserve_repository_enabled' => 'boolean|required',
'is_http_basic_auth_enabled' => 'boolean|required',
'http_basic_auth_username' => 'string|nullable',
'http_basic_auth_password' => 'string|nullable',
'watch_paths' => 'nullable',
'redirect' => 'string|required',
];
}
@@ -118,31 +190,31 @@ class General extends Component
return array_merge(
ValidationPatterns::combinedMessages(),
[
'application.name.required' => 'The Name field is required.',
'application.name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().',
'application.description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.',
'application.git_repository.required' => 'The Git Repository field is required.',
'application.git_branch.required' => 'The Git Branch field is required.',
'application.build_pack.required' => 'The Build Pack field is required.',
'application.static_image.required' => 'The Static Image field is required.',
'application.base_directory.required' => 'The Base Directory field is required.',
'application.ports_exposes.required' => 'The Exposed Ports field is required.',
'application.settings.is_static.required' => 'The Static setting is required.',
'application.settings.is_static.boolean' => 'The Static setting must be true or false.',
'application.settings.is_spa.required' => 'The SPA setting is required.',
'application.settings.is_spa.boolean' => 'The SPA setting must be true or false.',
'application.settings.is_build_server_enabled.required' => 'The Build Server setting is required.',
'application.settings.is_build_server_enabled.boolean' => 'The Build Server setting must be true or false.',
'application.settings.is_container_label_escape_enabled.required' => 'The Container Label Escape setting is required.',
'application.settings.is_container_label_escape_enabled.boolean' => 'The Container Label Escape setting must be true or false.',
'application.settings.is_container_label_readonly_enabled.required' => 'The Container Label Readonly setting is required.',
'application.settings.is_container_label_readonly_enabled.boolean' => 'The Container Label Readonly setting must be true or false.',
'application.settings.is_preserve_repository_enabled.required' => 'The Preserve Repository setting is required.',
'application.settings.is_preserve_repository_enabled.boolean' => 'The Preserve Repository setting must be true or false.',
'application.is_http_basic_auth_enabled.required' => 'The HTTP Basic Auth setting is required.',
'application.is_http_basic_auth_enabled.boolean' => 'The HTTP Basic Auth setting must be true or false.',
'application.redirect.required' => 'The Redirect setting is required.',
'application.redirect.string' => 'The Redirect setting must be a string.',
'name.required' => 'The Name field is required.',
'name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().',
'description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.',
'git_repository.required' => 'The Git Repository field is required.',
'git_branch.required' => 'The Git Branch field is required.',
'build_pack.required' => 'The Build Pack field is required.',
'static_image.required' => 'The Static Image field is required.',
'base_directory.required' => 'The Base Directory field is required.',
'ports_exposes.required' => 'The Exposed Ports field is required.',
'is_static.required' => 'The Static setting is required.',
'is_static.boolean' => 'The Static setting must be true or false.',
'is_spa.required' => 'The SPA setting is required.',
'is_spa.boolean' => 'The SPA setting must be true or false.',
'is_build_server_enabled.required' => 'The Build Server setting is required.',
'is_build_server_enabled.boolean' => 'The Build Server setting must be true or false.',
'is_container_label_escape_enabled.required' => 'The Container Label Escape setting is required.',
'is_container_label_escape_enabled.boolean' => 'The Container Label Escape setting must be true or false.',
'is_container_label_readonly_enabled.required' => 'The Container Label Readonly setting is required.',
'is_container_label_readonly_enabled.boolean' => 'The Container Label Readonly setting must be true or false.',
'is_preserve_repository_enabled.required' => 'The Preserve Repository setting is required.',
'is_preserve_repository_enabled.boolean' => 'The Preserve Repository setting must be true or false.',
'is_http_basic_auth_enabled.required' => 'The HTTP Basic Auth setting is required.',
'is_http_basic_auth_enabled.boolean' => 'The HTTP Basic Auth setting must be true or false.',
'redirect.required' => 'The Redirect setting is required.',
'redirect.string' => 'The Redirect setting must be a string.',
]
);
}
@@ -193,11 +265,15 @@ class General extends Component
$this->parsedServices = $this->application->parse();
if (is_null($this->parsedServices) || empty($this->parsedServices)) {
$this->dispatch('error', 'Failed to parse your docker-compose file. Please check the syntax and try again.');
// Still sync data even if parse fails, so form fields are populated
$this->syncFromModel();
return;
}
} catch (\Throwable $e) {
$this->dispatch('error', $e->getMessage());
// Still sync data even on error, so form fields are populated
$this->syncFromModel();
}
if ($this->application->build_pack === 'dockercompose') {
// Only update if user has permission
@@ -218,9 +294,6 @@ class General extends Component
}
$this->parsedServiceDomains = $sanitizedDomains;
$this->ports_exposes = $this->application->ports_exposes;
$this->is_preserve_repository_enabled = $this->application->settings->is_preserve_repository_enabled;
$this->is_container_label_escape_enabled = $this->application->settings->is_container_label_escape_enabled;
$this->customLabels = $this->application->parseContainerLabels();
if (! $this->customLabels && $this->application->destination->server->proxyType() !== 'NONE' && $this->application->settings->is_container_label_readonly_enabled === true) {
// Only update custom labels if user has permission
@@ -249,6 +322,60 @@ class General extends Component
if (str($this->application->status)->startsWith('running') && is_null($this->application->config_hash)) {
$this->dispatch('configurationChanged');
}
// Sync data from model to properties at the END, after all business logic
// This ensures any modifications to $this->application during mount() are reflected in properties
$this->syncFromModel();
}
protected function getModelBindings(): array
{
return [
'name' => 'application.name',
'description' => 'application.description',
'fqdn' => 'application.fqdn',
'git_repository' => 'application.git_repository',
'git_branch' => 'application.git_branch',
'git_commit_sha' => 'application.git_commit_sha',
'install_command' => 'application.install_command',
'build_command' => 'application.build_command',
'start_command' => 'application.start_command',
'build_pack' => 'application.build_pack',
'static_image' => 'application.static_image',
'base_directory' => 'application.base_directory',
'publish_directory' => 'application.publish_directory',
'ports_exposes' => 'application.ports_exposes',
'ports_mappings' => 'application.ports_mappings',
'custom_network_aliases' => 'application.custom_network_aliases',
'dockerfile' => 'application.dockerfile',
'dockerfile_location' => 'application.dockerfile_location',
'dockerfile_target_build' => 'application.dockerfile_target_build',
'docker_registry_image_name' => 'application.docker_registry_image_name',
'docker_registry_image_tag' => 'application.docker_registry_image_tag',
'docker_compose_location' => 'application.docker_compose_location',
'docker_compose' => 'application.docker_compose',
'docker_compose_raw' => 'application.docker_compose_raw',
'docker_compose_custom_start_command' => 'application.docker_compose_custom_start_command',
'docker_compose_custom_build_command' => 'application.docker_compose_custom_build_command',
'custom_labels' => 'application.custom_labels',
'custom_docker_run_options' => 'application.custom_docker_run_options',
'pre_deployment_command' => 'application.pre_deployment_command',
'pre_deployment_command_container' => 'application.pre_deployment_command_container',
'post_deployment_command' => 'application.post_deployment_command',
'post_deployment_command_container' => 'application.post_deployment_command_container',
'custom_nginx_configuration' => 'application.custom_nginx_configuration',
'is_static' => 'application.settings.is_static',
'is_spa' => 'application.settings.is_spa',
'is_build_server_enabled' => 'application.settings.is_build_server_enabled',
'is_preserve_repository_enabled' => 'application.settings.is_preserve_repository_enabled',
'is_container_label_escape_enabled' => 'application.settings.is_container_label_escape_enabled',
'is_container_label_readonly_enabled' => 'application.settings.is_container_label_readonly_enabled',
'is_http_basic_auth_enabled' => 'application.is_http_basic_auth_enabled',
'http_basic_auth_username' => 'application.http_basic_auth_username',
'http_basic_auth_password' => 'application.http_basic_auth_password',
'watch_paths' => 'application.watch_paths',
'redirect' => 'application.redirect',
];
}
public function instantSave()
@@ -256,6 +383,12 @@ class General extends Component
try {
$this->authorize('update', $this->application);
$oldPortsExposes = $this->application->ports_exposes;
$oldIsContainerLabelEscapeEnabled = $this->application->settings->is_container_label_escape_enabled;
$oldIsPreserveRepositoryEnabled = $this->application->settings->is_preserve_repository_enabled;
$this->syncToModel();
if ($this->application->settings->isDirty('is_spa')) {
$this->generateNginxConfiguration($this->application->settings->is_spa ? 'spa' : 'static');
}
@@ -265,20 +398,21 @@ class General extends Component
$this->application->settings->save();
$this->dispatch('success', 'Settings saved.');
$this->application->refresh();
$this->syncFromModel();
// If port_exposes changed, reset default labels
if ($this->ports_exposes !== $this->application->ports_exposes || $this->is_container_label_escape_enabled !== $this->application->settings->is_container_label_escape_enabled) {
if ($oldPortsExposes !== $this->ports_exposes || $oldIsContainerLabelEscapeEnabled !== $this->is_container_label_escape_enabled) {
$this->resetDefaultLabels(false);
}
if ($this->is_preserve_repository_enabled !== $this->application->settings->is_preserve_repository_enabled) {
if ($this->application->settings->is_preserve_repository_enabled === false) {
if ($oldIsPreserveRepositoryEnabled !== $this->is_preserve_repository_enabled) {
if ($this->is_preserve_repository_enabled === false) {
$this->application->fileStorages->each(function ($storage) {
$storage->is_based_on_git = $this->application->settings->is_preserve_repository_enabled;
$storage->is_based_on_git = $this->is_preserve_repository_enabled;
$storage->save();
});
}
}
if ($this->application->settings->is_container_label_readonly_enabled) {
if ($this->is_container_label_readonly_enabled) {
$this->resetDefaultLabels(false);
}
} catch (\Throwable $e) {
@@ -366,21 +500,21 @@ class General extends Component
}
}
public function updatedApplicationBaseDirectory()
public function updatedBaseDirectory()
{
if ($this->application->build_pack === 'dockercompose') {
if ($this->build_pack === 'dockercompose') {
$this->loadComposeFile();
}
}
public function updatedApplicationSettingsIsStatic($value)
public function updatedIsStatic($value)
{
if ($value) {
$this->generateNginxConfiguration();
}
}
public function updatedApplicationBuildPack()
public function updatedBuildPack()
{
// Check if user has permission to update
try {
@@ -388,21 +522,28 @@ class General extends Component
} catch (\Illuminate\Auth\Access\AuthorizationException $e) {
// User doesn't have permission, revert the change and return
$this->application->refresh();
$this->syncFromModel();
return;
}
if ($this->application->build_pack !== 'nixpacks') {
// Sync property to model before checking/modifying
$this->syncToModel();
if ($this->build_pack !== 'nixpacks') {
$this->is_static = false;
$this->application->settings->is_static = false;
$this->application->settings->save();
} else {
$this->application->ports_exposes = $this->ports_exposes = 3000;
$this->ports_exposes = 3000;
$this->application->ports_exposes = 3000;
$this->resetDefaultLabels(false);
}
if ($this->application->build_pack === 'dockercompose') {
if ($this->build_pack === 'dockercompose') {
// Only update if user has permission
try {
$this->authorize('update', $this->application);
$this->fqdn = null;
$this->application->fqdn = null;
$this->application->settings->save();
} catch (\Illuminate\Auth\Access\AuthorizationException $e) {
@@ -421,8 +562,9 @@ class General extends Component
$this->application->environment_variables_preview()->where('key', 'LIKE', 'SERVICE_URL_%')->delete();
}
}
if ($this->application->build_pack === 'static') {
$this->application->ports_exposes = $this->ports_exposes = 80;
if ($this->build_pack === 'static') {
$this->ports_exposes = 80;
$this->application->ports_exposes = 80;
$this->resetDefaultLabels(false);
$this->generateNginxConfiguration();
}
@@ -438,8 +580,11 @@ class General extends Component
$server = data_get($this->application, 'destination.server');
if ($server) {
$fqdn = generateUrl(server: $server, random: $this->application->uuid);
$this->application->fqdn = $fqdn;
$this->fqdn = $fqdn;
$this->syncToModel();
$this->application->save();
$this->application->refresh();
$this->syncFromModel();
$this->resetDefaultLabels();
$this->dispatch('success', 'Wildcard domain generated.');
}
@@ -453,8 +598,11 @@ class General extends Component
try {
$this->authorize('update', $this->application);
$this->application->custom_nginx_configuration = defaultNginxConfiguration($type);
$this->custom_nginx_configuration = defaultNginxConfiguration($type);
$this->syncToModel();
$this->application->save();
$this->application->refresh();
$this->syncFromModel();
$this->dispatch('success', 'Nginx configuration generated.');
} catch (\Throwable $e) {
return handleError($e, $this);
@@ -464,15 +612,16 @@ class General extends Component
public function resetDefaultLabels($manualReset = false)
{
try {
if (! $this->application->settings->is_container_label_readonly_enabled && ! $manualReset) {
if (! $this->is_container_label_readonly_enabled && ! $manualReset) {
return;
}
$this->customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n");
$this->ports_exposes = $this->application->ports_exposes;
$this->is_container_label_escape_enabled = $this->application->settings->is_container_label_escape_enabled;
$this->application->custom_labels = base64_encode($this->customLabels);
$this->custom_labels = base64_encode($this->customLabels);
$this->syncToModel();
$this->application->save();
if ($this->application->build_pack === 'dockercompose') {
$this->application->refresh();
$this->syncFromModel();
if ($this->build_pack === 'dockercompose') {
$this->loadComposeFile(showToast: false);
}
$this->dispatch('configurationChanged');
@@ -483,8 +632,8 @@ class General extends Component
public function checkFqdns($showToaster = true)
{
if (data_get($this->application, 'fqdn')) {
$domains = str($this->application->fqdn)->trim()->explode(',');
if ($this->fqdn) {
$domains = str($this->fqdn)->trim()->explode(',');
if ($this->application->additional_servers->count() === 0) {
foreach ($domains as $domain) {
if (! validateDNSEntry($domain, $this->application->destination->server)) {
@@ -507,7 +656,8 @@ class General extends Component
$this->forceSaveDomains = false;
}
$this->application->fqdn = $domains->implode(',');
$this->fqdn = $domains->implode(',');
$this->application->fqdn = $this->fqdn;
$this->resetDefaultLabels(false);
}
@@ -547,21 +697,27 @@ class General extends Component
$this->validate();
$this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
$oldPortsExposes = $this->application->ports_exposes;
$oldIsContainerLabelEscapeEnabled = $this->application->settings->is_container_label_escape_enabled;
$oldDockerComposeLocation = $this->initialDockerComposeLocation;
// Process FQDN with intermediate variable to avoid Collection/string confusion
$this->fqdn = str($this->fqdn)->replaceEnd(',', '')->trim()->toString();
$this->fqdn = str($this->fqdn)->replaceStart(',', '')->trim()->toString();
$domains = str($this->fqdn)->trim()->explode(',')->map(function ($domain) {
$domain = trim($domain);
Url::fromString($domain, ['http', 'https']);
return str($domain)->lower();
});
$this->application->fqdn = $this->application->fqdn->unique()->implode(',');
$warning = sslipDomainWarning($this->application->fqdn);
$this->fqdn = $domains->unique()->implode(',');
$warning = sslipDomainWarning($this->fqdn);
if ($warning) {
$this->dispatch('warning', __('warning.sslipdomain'));
}
// $this->resetDefaultLabels();
$this->syncToModel();
if ($this->application->isDirty('redirect')) {
$this->setRedirect();
@@ -581,38 +737,42 @@ class General extends Component
$this->application->save();
}
if ($this->application->build_pack === 'dockercompose' && $this->initialDockerComposeLocation !== $this->application->docker_compose_location) {
if ($this->build_pack === 'dockercompose' && $oldDockerComposeLocation !== $this->docker_compose_location) {
$compose_return = $this->loadComposeFile(showToast: false);
if ($compose_return instanceof \Livewire\Features\SupportEvents\Event) {
return;
}
}
if ($this->ports_exposes !== $this->application->ports_exposes || $this->is_container_label_escape_enabled !== $this->application->settings->is_container_label_escape_enabled) {
if ($oldPortsExposes !== $this->ports_exposes || $oldIsContainerLabelEscapeEnabled !== $this->is_container_label_escape_enabled) {
$this->resetDefaultLabels();
}
if (data_get($this->application, 'build_pack') === 'dockerimage') {
if ($this->build_pack === 'dockerimage') {
$this->validate([
'application.docker_registry_image_name' => 'required',
'docker_registry_image_name' => 'required',
]);
}
if (data_get($this->application, 'custom_docker_run_options')) {
$this->application->custom_docker_run_options = str($this->application->custom_docker_run_options)->trim();
if ($this->custom_docker_run_options) {
$this->custom_docker_run_options = str($this->custom_docker_run_options)->trim()->toString();
$this->application->custom_docker_run_options = $this->custom_docker_run_options;
}
if (data_get($this->application, 'dockerfile')) {
$port = get_port_from_dockerfile($this->application->dockerfile);
if ($port && ! $this->application->ports_exposes) {
if ($this->dockerfile) {
$port = get_port_from_dockerfile($this->dockerfile);
if ($port && ! $this->ports_exposes) {
$this->ports_exposes = $port;
$this->application->ports_exposes = $port;
}
}
if ($this->application->base_directory && $this->application->base_directory !== '/') {
$this->application->base_directory = rtrim($this->application->base_directory, '/');
if ($this->base_directory && $this->base_directory !== '/') {
$this->base_directory = rtrim($this->base_directory, '/');
$this->application->base_directory = $this->base_directory;
}
if ($this->application->publish_directory && $this->application->publish_directory !== '/') {
$this->application->publish_directory = rtrim($this->application->publish_directory, '/');
if ($this->publish_directory && $this->publish_directory !== '/') {
$this->publish_directory = rtrim($this->publish_directory, '/');
$this->application->publish_directory = $this->publish_directory;
}
if ($this->application->build_pack === 'dockercompose') {
if ($this->build_pack === 'dockercompose') {
$this->application->docker_compose_domains = json_encode($this->parsedServiceDomains);
if ($this->application->isDirty('docker_compose_domains')) {
foreach ($this->parsedServiceDomains as $service) {
@@ -643,12 +803,12 @@ class General extends Component
}
$this->application->custom_labels = base64_encode($this->customLabels);
$this->application->save();
$this->application->refresh();
$this->syncFromModel();
$showToaster && ! $warning && $this->dispatch('success', 'Application settings updated!');
} catch (\Throwable $e) {
$originalFqdn = $this->application->getOriginal('fqdn');
if ($originalFqdn !== $this->application->fqdn) {
$this->application->fqdn = $originalFqdn;
}
$this->application->refresh();
$this->syncFromModel();
return handleError($e, $this);
} finally {
+67 -26
View File
@@ -33,14 +33,34 @@ class Previews extends Component
public $pendingPreviewId = null;
public array $previewFqdns = [];
protected $rules = [
'application.previews.*.fqdn' => 'string|nullable',
'previewFqdns.*' => 'string|nullable',
];
public function mount()
{
$this->pull_requests = collect();
$this->parameters = get_route_parameters();
$this->syncData(false);
}
private function syncData(bool $toModel = false): void
{
if ($toModel) {
foreach ($this->previewFqdns as $key => $fqdn) {
$preview = $this->application->previews->get($key);
if ($preview) {
$preview->fqdn = $fqdn;
}
}
} else {
$this->previewFqdns = [];
foreach ($this->application->previews as $key => $preview) {
$this->previewFqdns[$key] = $preview->fqdn;
}
}
}
public function load_prs()
@@ -73,35 +93,52 @@ class Previews extends Component
$this->authorize('update', $this->application);
$success = true;
$preview = $this->application->previews->find($preview_id);
if (data_get_str($preview, 'fqdn')->isNotEmpty()) {
$preview->fqdn = str($preview->fqdn)->replaceEnd(',', '')->trim();
$preview->fqdn = str($preview->fqdn)->replaceStart(',', '')->trim();
$preview->fqdn = str($preview->fqdn)->trim()->lower();
if (! validateDNSEntry($preview->fqdn, $this->application->destination->server)) {
$this->dispatch('error', 'Validating DNS failed.', "Make sure you have added the DNS records correctly.<br><br>$preview->fqdn->{$this->application->destination->server->ip}<br><br>Check this <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/dns-configuration'>documentation</a> for further help.");
$success = false;
}
// Check for domain conflicts if not forcing save
if (! $this->forceSaveDomains) {
$result = checkDomainUsage(resource: $this->application, domain: $preview->fqdn);
if ($result['hasConflicts']) {
$this->domainConflicts = $result['conflicts'];
$this->showDomainConflictModal = true;
$this->pendingPreviewId = $preview_id;
return;
}
} else {
// Reset the force flag after using it
$this->forceSaveDomains = false;
}
}
if (! $preview) {
throw new \Exception('Preview not found');
}
$success && $preview->save();
$success && $this->dispatch('success', 'Preview saved.<br><br>Do not forget to redeploy the preview to apply the changes.');
// Find the key for this preview in the collection
$previewKey = $this->application->previews->search(function ($item) use ($preview_id) {
return $item->id == $preview_id;
});
if ($previewKey !== false && isset($this->previewFqdns[$previewKey])) {
$fqdn = $this->previewFqdns[$previewKey];
if (! empty($fqdn)) {
$fqdn = str($fqdn)->replaceEnd(',', '')->trim();
$fqdn = str($fqdn)->replaceStart(',', '')->trim();
$fqdn = str($fqdn)->trim()->lower();
$this->previewFqdns[$previewKey] = $fqdn;
if (! validateDNSEntry($fqdn, $this->application->destination->server)) {
$this->dispatch('error', 'Validating DNS failed.', "Make sure you have added the DNS records correctly.<br><br>$fqdn->{$this->application->destination->server->ip}<br><br>Check this <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/dns-configuration'>documentation</a> for further help.");
$success = false;
}
// Check for domain conflicts if not forcing save
if (! $this->forceSaveDomains) {
$result = checkDomainUsage(resource: $this->application, domain: $fqdn);
if ($result['hasConflicts']) {
$this->domainConflicts = $result['conflicts'];
$this->showDomainConflictModal = true;
$this->pendingPreviewId = $preview_id;
return;
}
} else {
// Reset the force flag after using it
$this->forceSaveDomains = false;
}
}
}
if ($success) {
$this->syncData(true);
$preview->save();
$this->dispatch('success', 'Preview saved.<br><br>Do not forget to redeploy the preview to apply the changes.');
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
@@ -121,6 +158,7 @@ class Previews extends Component
if ($this->application->build_pack === 'dockercompose') {
$preview->generate_preview_fqdn_compose();
$this->application->refresh();
$this->syncData(false);
$this->dispatch('success', 'Domain generated.');
return;
@@ -128,6 +166,7 @@ class Previews extends Component
$preview->generate_preview_fqdn();
$this->application->refresh();
$this->syncData(false);
$this->dispatch('update_links');
$this->dispatch('success', 'Domain generated.');
} catch (\Throwable $e) {
@@ -152,6 +191,7 @@ class Previews extends Component
}
$found->generate_preview_fqdn_compose();
$this->application->refresh();
$this->syncData(false);
} else {
$this->setDeploymentUuid();
$found = ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first();
@@ -164,6 +204,7 @@ class Previews extends Component
}
$found->generate_preview_fqdn();
$this->application->refresh();
$this->syncData(false);
$this->dispatch('update_links');
$this->dispatch('success', 'Preview added.');
}
@@ -18,6 +18,13 @@ class PreviewsCompose extends Component
public ApplicationPreview $preview;
public ?string $domain = null;
public function mount()
{
$this->domain = data_get($this->service, 'domain');
}
public function render()
{
return view('livewire.project.application.previews-compose');
@@ -28,10 +35,10 @@ class PreviewsCompose extends Component
try {
$this->authorize('update', $this->preview->application);
$domain = data_get($this->service, 'domain');
$docker_compose_domains = data_get($this->preview, 'docker_compose_domains');
$docker_compose_domains = json_decode($docker_compose_domains, true);
$docker_compose_domains[$this->serviceName]['domain'] = $domain;
$docker_compose_domains = json_decode($docker_compose_domains, true) ?: [];
$docker_compose_domains[$this->serviceName] = $docker_compose_domains[$this->serviceName] ?? [];
$docker_compose_domains[$this->serviceName]['domain'] = $this->domain;
$this->preview->docker_compose_domains = json_encode($docker_compose_domains);
$this->preview->save();
$this->dispatch('update_links');
@@ -46,7 +53,7 @@ class PreviewsCompose extends Component
try {
$this->authorize('update', $this->preview->application);
$domains = collect(json_decode($this->preview->application->docker_compose_domains)) ?? collect();
$domains = collect(json_decode($this->preview->application->docker_compose_domains, true) ?: []);
$domain = $domains->first(function ($_, $key) {
return $key === $this->serviceName;
});
@@ -68,24 +75,40 @@ class PreviewsCompose extends Component
$preview_fqdn = str($generated_fqdn)->before('://').'://'.$preview_fqdn;
} else {
// Use the existing domain from the main application
$url = Url::fromString($domain_string);
// Handle multiple domains separated by commas
$domain_list = explode(',', $domain_string);
$preview_fqdns = [];
$template = $this->preview->application->preview_url_template;
$host = $url->getHost();
$schema = $url->getScheme();
$portInt = $url->getPort();
$port = $portInt !== null ? ':'.$portInt : '';
$random = new Cuid2;
$preview_fqdn = str_replace('{{random}}', $random, $template);
$preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn);
$preview_fqdn = str_replace('{{pr_id}}', $this->preview->pull_request_id, $preview_fqdn);
$preview_fqdn = str_replace('{{port}}', $port, $preview_fqdn);
$preview_fqdn = "$schema://$preview_fqdn";
foreach ($domain_list as $single_domain) {
$single_domain = trim($single_domain);
if (empty($single_domain)) {
continue;
}
$url = Url::fromString($single_domain);
$host = $url->getHost();
$schema = $url->getScheme();
$portInt = $url->getPort();
$port = $portInt !== null ? ':'.$portInt : '';
$preview_fqdn = str_replace('{{random}}', $random, $template);
$preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn);
$preview_fqdn = str_replace('{{pr_id}}', $this->preview->pull_request_id, $preview_fqdn);
$preview_fqdn = str_replace('{{port}}', $port, $preview_fqdn);
$preview_fqdns[] = "$schema://$preview_fqdn";
}
$preview_fqdn = implode(',', $preview_fqdns);
}
// Save the generated domain
$this->domain = $preview_fqdn;
$docker_compose_domains = data_get($this->preview, 'docker_compose_domains');
$docker_compose_domains = json_decode($docker_compose_domains, true);
$docker_compose_domains[$this->serviceName]['domain'] = $this->service->domain = $preview_fqdn;
$docker_compose_domains = json_decode($docker_compose_domains, true) ?: [];
$docker_compose_domains[$this->serviceName] = $docker_compose_domains[$this->serviceName] ?? [];
$docker_compose_domains[$this->serviceName]['domain'] = $this->domain;
$this->preview->docker_compose_domains = json_encode($docker_compose_domains);
$this->preview->save();