mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-16 16:15:36 +00:00
Merge branch 'next' into shadow/fix-docker-time-command
This commit is contained in:
310 files changed
+22764
-11444
No files matched your search
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in new issue
Block a user