diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 5dbdbf215..6cf642f27 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -329,7 +329,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue } else { $this->write_deployment_configurations(); } - $this->application_deployment_queue->addLogEntry("Starting graceful shutdown container: {$this->deployment_uuid}"); + $this->application_deployment_queue->addLogEntry("Gracefully shutting down build container: {$this->deployment_uuid}"); $this->graceful_shutdown_container($this->deployment_uuid); ApplicationStatusChanged::dispatch(data_get($this->application, 'environment.project.team.id')); @@ -1361,7 +1361,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue } } $this->application_deployment_queue->addLogEntry("Preparing container with helper image: $helperImage."); - $this->application_deployment_queue->addLogEntry("Starting graceful shutdown container: {$this->deployment_uuid}"); $this->graceful_shutdown_container($this->deployment_uuid); $this->execute_remote_command( [ @@ -1710,6 +1709,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue ]); $this->application->parseHealthcheckFromDockerfile($this->saved_outputs->get('dockerfile_from_repo')); } + $custom_network_aliases = []; + if (is_array($this->application->custom_network_aliases) && count($this->application->custom_network_aliases) > 0) { + $custom_network_aliases = $this->application->custom_network_aliases; + } $docker_compose = [ 'services' => [ $this->container_name => [ @@ -1719,9 +1722,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue 'expose' => $ports, 'networks' => [ $this->destination->network => [ - 'aliases' => [ - $this->container_name, - ], + 'aliases' => array_merge( + [$this->container_name], + $custom_network_aliases + ), ], ], 'mem_limit' => $this->application->limits_memory, diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index 9e5126281..eaa988b99 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -68,6 +68,7 @@ class General extends Component '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', @@ -121,6 +122,7 @@ class General extends Component 'application.custom_labels' => 'Custom labels', 'application.dockerfile_target_build' => 'Dockerfile target build', 'application.custom_docker_run_options' => 'Custom docker run commands', + 'application.custom_network_aliases' => 'Custom docker network aliases', 'application.docker_compose_custom_start_command' => 'Docker compose custom start command', 'application.docker_compose_custom_build_command' => 'Docker compose custom build command', 'application.custom_nginx_configuration' => 'Custom Nginx configuration', diff --git a/app/Livewire/Project/Database/Redis/General.php b/app/Livewire/Project/Database/Redis/General.php index f301d912e..f03f1256d 100644 --- a/app/Livewire/Project/Database/Redis/General.php +++ b/app/Livewire/Project/Database/Redis/General.php @@ -21,7 +21,7 @@ class General extends Component public string $redis_username; - public string $redis_password; + public ?string $redis_password; public string $redis_version; diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/All.php b/app/Livewire/Project/Shared/EnvironmentVariable/All.php index 35e585c82..57952ddb3 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/All.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/All.php @@ -3,10 +3,13 @@ namespace App\Livewire\Project\Shared\EnvironmentVariable; use App\Models\EnvironmentVariable; +use App\Traits\EnvironmentVariableProtection; use Livewire\Component; class All extends Component { + use EnvironmentVariableProtection; + public $resource; public string $resourceClass; @@ -138,17 +141,57 @@ class All extends Component private function handleBulkSubmit() { $variables = parseEnvFormatToArray($this->variables); + $changesMade = false; + $errorOccurred = false; - $this->deleteRemovedVariables(false, $variables); - $this->updateOrCreateVariables(false, $variables); + // Try to delete removed variables + $deletedCount = $this->deleteRemovedVariables(false, $variables); + if ($deletedCount > 0) { + $changesMade = true; + } elseif ($deletedCount === 0 && $this->resource->environment_variables()->whereNotIn('key', array_keys($variables))->exists()) { + // If we tried to delete but couldn't (due to Docker Compose), mark as error + $errorOccurred = true; + } + + // Update or create variables + $updatedCount = $this->updateOrCreateVariables(false, $variables); + if ($updatedCount > 0) { + $changesMade = true; + } if ($this->showPreview) { $previewVariables = parseEnvFormatToArray($this->variablesPreview); - $this->deleteRemovedVariables(true, $previewVariables); - $this->updateOrCreateVariables(true, $previewVariables); + + // Try to delete removed preview variables + $deletedPreviewCount = $this->deleteRemovedVariables(true, $previewVariables); + if ($deletedPreviewCount > 0) { + $changesMade = true; + } elseif ($deletedPreviewCount === 0 && $this->resource->environment_variables_preview()->whereNotIn('key', array_keys($previewVariables))->exists()) { + // If we tried to delete but couldn't (due to Docker Compose), mark as error + $errorOccurred = true; + } + + // Update or create preview variables + $updatedPreviewCount = $this->updateOrCreateVariables(true, $previewVariables); + if ($updatedPreviewCount > 0) { + $changesMade = true; + } } - $this->dispatch('success', 'Environment variables updated.'); + // Debug information + \Log::info('Environment variables update status', [ + 'deletedCount' => $deletedCount, + 'updatedCount' => $updatedCount, + 'deletedPreviewCount' => $deletedPreviewCount ?? 0, + 'updatedPreviewCount' => $updatedPreviewCount ?? 0, + 'changesMade' => $changesMade, + 'errorOccurred' => $errorOccurred, + ]); + + // Only show success message if changes were actually made and no errors occurred + if ($changesMade && ! $errorOccurred) { + $this->dispatch('success', 'Environment variables updated.'); + } } private function handleSingleSubmit($data) @@ -184,11 +227,46 @@ class All extends Component private function deleteRemovedVariables($isPreview, $variables) { $method = $isPreview ? 'environment_variables_preview' : 'environment_variables'; + + // Get all environment variables that will be deleted + $variablesToDelete = $this->resource->$method()->whereNotIn('key', array_keys($variables))->get(); + + // If there are no variables to delete, return 0 + if ($variablesToDelete->isEmpty()) { + return 0; + } + + // Check for system variables that shouldn't be deleted + foreach ($variablesToDelete as $envVar) { + if ($this->isProtectedEnvironmentVariable($envVar->key)) { + $this->dispatch('error', "Cannot delete system environment variable '{$envVar->key}'."); + + return 0; + } + } + + // Check if any of these variables are used in Docker Compose + if ($this->resource->type() === 'service' || $this->resource->build_pack === 'dockercompose') { + foreach ($variablesToDelete as $envVar) { + [$isUsed, $reason] = $this->isEnvironmentVariableUsedInDockerCompose($envVar->key, $this->resource->docker_compose); + + if ($isUsed) { + $this->dispatch('error', "Cannot delete environment variable '{$envVar->key}'

Please remove it from the Docker Compose file first."); + + return 0; + } + } + } + + // If we get here, no variables are used in Docker Compose, so we can delete them $this->resource->$method()->whereNotIn('key', array_keys($variables))->delete(); + + return $variablesToDelete->count(); } private function updateOrCreateVariables($isPreview, $variables) { + $count = 0; foreach ($variables as $key => $value) { if (str($key)->startsWith('SERVICE_FQDN') || str($key)->startsWith('SERVICE_URL')) { continue; @@ -198,8 +276,12 @@ class All extends Component if ($found) { if (! $found->is_shown_once && ! $found->is_multiline) { - $found->value = $value; - $found->save(); + // Only count as a change if the value actually changed + if ($found->value !== $value) { + $found->value = $value; + $found->save(); + $count++; + } } } else { $environment = new EnvironmentVariable; @@ -212,8 +294,11 @@ class All extends Component $environment->resourceable_type = $this->resource->getMorphClass(); $environment->save(); + $count++; } } + + return $count; } public function refreshEnvs() diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php index 3a7d0faa5..d58151abf 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php @@ -4,10 +4,13 @@ namespace App\Livewire\Project\Shared\EnvironmentVariable; use App\Models\EnvironmentVariable as ModelsEnvironmentVariable; use App\Models\SharedEnvironmentVariable; +use App\Traits\EnvironmentVariableProtection; use Livewire\Component; class Show extends Component { + use EnvironmentVariableProtection; + public $parameters; public ModelsEnvironmentVariable|SharedEnvironmentVariable $env; @@ -40,6 +43,8 @@ class Show extends Component public bool $is_really_required = false; + public bool $is_redis_credential = false; + protected $listeners = [ 'refreshEnvs' => 'refresh', 'refresh', @@ -65,7 +70,9 @@ class Show extends Component } $this->parameters = get_route_parameters(); $this->checkEnvs(); - + if ($this->type === 'standalone-redis' && ($this->env->key === 'REDIS_PASSWORD' || $this->env->key === 'REDIS_USERNAME')) { + $this->is_redis_credential = true; + } } public function refresh() @@ -171,6 +178,24 @@ class Show extends Component public function delete() { try { + // Check if the variable is protected + if ($this->isProtectedEnvironmentVariable($this->env->key)) { + $this->dispatch('error', "Cannot delete system environment variable '{$this->env->key}'."); + + return; + } + + // Check if the variable is used in Docker Compose + if ($this->type === 'service' || $this->type === 'application' && $this->env->resource()?->docker_compose) { + [$isUsed, $reason] = $this->isEnvironmentVariableUsedInDockerCompose($this->env->key, $this->env->resource()?->docker_compose); + + if ($isUsed) { + $this->dispatch('error', "Cannot delete environment variable '{$this->env->key}'

Please remove it from the Docker Compose file first."); + + return; + } + } + $this->env->delete(); $this->dispatch('environmentVariableDeleted'); $this->dispatch('success', 'Environment variable deleted successfully.'); diff --git a/app/Models/Application.php b/app/Models/Application.php index d07577cc7..2feaebf94 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -45,6 +45,7 @@ use Visus\Cuid2\Cuid2; 'start_command' => ['type' => 'string', 'description' => 'Start command.'], 'ports_exposes' => ['type' => 'string', 'description' => 'Ports exposes.'], 'ports_mappings' => ['type' => 'string', 'nullable' => true, 'description' => 'Ports mappings.'], + 'custom_network_aliases' => ['type' => 'string', 'nullable' => true, 'description' => 'Network aliases for Docker container.'], 'base_directory' => ['type' => 'string', 'description' => 'Base directory for all commands.'], 'publish_directory' => ['type' => 'string', 'description' => 'Publish directory.'], 'health_check_enabled' => ['type' => 'boolean', 'description' => 'Health check enabled.'], @@ -115,6 +116,68 @@ class Application extends BaseModel protected $appends = ['server_status']; + protected $casts = ['custom_network_aliases' => 'array']; + + public function customNetworkAliases(): Attribute + { + return Attribute::make( + set: function ($value) { + if (is_null($value) || $value === '') { + return null; + } + + // If it's already a JSON string, decode it + if (is_string($value) && $this->isJson($value)) { + $value = json_decode($value, true); + } + + // If it's a string but not JSON, treat it as a comma-separated list + if (is_string($value) && ! is_array($value)) { + $value = explode(',', $value); + } + + $value = collect($value) + ->map(function ($alias) { + if (is_string($alias)) { + return str_replace(' ', '-', trim($alias)); + } + + return null; + }) + ->filter() + ->unique() // Remove duplicate values + ->values() + ->toArray(); + + return empty($value) ? null : json_encode($value); + }, + get: function ($value) { + if (is_null($value)) { + return null; + } + + if (is_string($value) && $this->isJson($value)) { + return json_decode($value, true); + } + + return is_array($value) ? $value : []; + } + ); + } + + /** + * Check if a string is a valid JSON + */ + private function isJson($string) + { + if (! is_string($string)) { + return false; + } + json_decode($string); + + return json_last_error() === JSON_ERROR_NONE; + } + protected static function booted() { static::addGlobalScope('withRelations', function ($builder) { diff --git a/app/Traits/EnvironmentVariableProtection.php b/app/Traits/EnvironmentVariableProtection.php new file mode 100644 index 000000000..b6b8d2687 --- /dev/null +++ b/app/Traits/EnvironmentVariableProtection.php @@ -0,0 +1,63 @@ +startsWith('SERVICE_FQDN') || str($key)->startsWith('SERVICE_URL'); + } + + /** + * Check if an environment variable is used in Docker Compose + * + * @param string $key The environment variable key to check + * @param string|null $dockerCompose The Docker Compose YAML content + * @return array [bool $isUsed, string $reason] Whether the variable is used and the reason if it is + */ + protected function isEnvironmentVariableUsedInDockerCompose(string $key, ?string $dockerCompose): array + { + if (empty($dockerCompose)) { + return [false, '']; + } + + try { + $dockerComposeData = Yaml::parse($dockerCompose); + $dockerEnvVars = data_get($dockerComposeData, 'services.*.environment'); + + foreach ($dockerEnvVars as $serviceEnvs) { + if (! is_array($serviceEnvs)) { + continue; + } + + // Check for direct variable usage + foreach ($serviceEnvs as $env => $value) { + if ($env === $key) { + return [true, "Environment variable '{$key}' is used directly in the Docker Compose file."]; + } + } + + // Check for variable references in values + foreach ($serviceEnvs as $env => $value) { + if (is_string($value) && str_contains($value, '$'.$key)) { + return [true, "Environment variable '{$key}' is referenced in the Docker Compose file."]; + } + } + } + } catch (\Exception $e) { + // If there's an error parsing the Docker Compose file, we'll assume it's not used + return [false, '']; + } + + return [false, '']; + } +} diff --git a/config/constants.php b/config/constants.php index d2d635033..439f32940 100644 --- a/config/constants.php +++ b/config/constants.php @@ -2,7 +2,7 @@ return [ 'coolify' => [ - 'version' => '4.0.0-beta.406', + 'version' => '4.0.0-beta.407', 'helper_version' => '1.0.8', 'realtime_version' => '1.0.6', 'self_hosted' => env('SELF_HOSTED', true), diff --git a/database/migrations/2025_01_05_050736_add_network_aliases_to_applications_table.php b/database/migrations/2025_01_05_050736_add_network_aliases_to_applications_table.php new file mode 100644 index 000000000..61fadd0e5 --- /dev/null +++ b/database/migrations/2025_01_05_050736_add_network_aliases_to_applications_table.php @@ -0,0 +1,22 @@ +text('custom_network_aliases')->nullable(); + }); + } + + public function down() + { + Schema::table('applications', function (Blueprint $table) { + $table->dropColumn('custom_network_aliases'); + }); + } +}; diff --git a/other/nightly/versions.json b/other/nightly/versions.json index af1c3e997..50a917a8d 100644 --- a/other/nightly/versions.json +++ b/other/nightly/versions.json @@ -1,10 +1,10 @@ { "coolify": { "v4": { - "version": "4.0.0-beta.406" + "version": "4.0.0-beta.407" }, "nightly": { - "version": "4.0.0-beta.407" + "version": "4.0.0-beta.408" }, "helper": { "version": "1.0.8" diff --git a/resources/views/livewire/project/application/general.blade.php b/resources/views/livewire/project/application/general.blade.php index 8c12d1d62..f971c8a4f 100644 --- a/resources/views/livewire/project/application/general.blade.php +++ b/resources/views/livewire/project/application/general.blade.php @@ -342,6 +342,11 @@ @endif + @if (!$application->destination->server->isSwarm()) + + @endif @if ($application->settings->is_container_label_readonly_enabled) diff --git a/resources/views/livewire/project/shared/environment-variable/show.blade.php b/resources/views/livewire/project/shared/environment-variable/show.blade.php index e3130e805..4d54df3bd 100644 --- a/resources/views/livewire/project/shared/environment-variable/show.blade.php +++ b/resources/views/livewire/project/shared/environment-variable/show.blade.php @@ -31,46 +31,48 @@ @else
@if ($is_multiline) - - + + @else - - + + @endif @if ($is_shared) - + @endif
@endif
- @if ($type === 'service') - - - - @else - @if ($is_shared) + @if (!$is_redis_credential) + @if ($type === 'service') + @else - @if ($isSharedVariable) - - @else + @if ($is_shared) - - @if ($is_multiline === false) - + + @else + @if ($isSharedVariable) + + @else + + + @if ($is_multiline === false) + + @endif @endif @endif @endif diff --git a/versions.json b/versions.json index af1c3e997..50a917a8d 100644 --- a/versions.json +++ b/versions.json @@ -1,10 +1,10 @@ { "coolify": { "v4": { - "version": "4.0.0-beta.406" + "version": "4.0.0-beta.407" }, "nightly": { - "version": "4.0.0-beta.407" + "version": "4.0.0-beta.408" }, "helper": { "version": "1.0.8"