diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 833e6bfe8..3c52e03a1 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -1282,7 +1282,7 @@ private function generate_runtime_environment_variables() }); foreach ($runtime_environment_variables as $env) { - $envs->push($env->key.'='.$env->real_value); + $envs->push($env->key.'='.$env->getResolvedValueWithServer($this->mainServer)); } // Check for PORT environment variable mismatch with ports_exposes @@ -1348,7 +1348,7 @@ private function generate_runtime_environment_variables() }); foreach ($runtime_environment_variables_preview as $env) { - $envs->push($env->key.'='.$env->real_value); + $envs->push($env->key.'='.$env->getResolvedValueWithServer($this->mainServer)); } // Fall back to production env vars for keys not overridden by preview vars, @@ -1362,7 +1362,7 @@ private function generate_runtime_environment_variables() return $env->is_runtime && ! in_array($env->key, $previewKeys); }); foreach ($fallback_production_vars as $env) { - $envs->push($env->key.'='.$env->real_value); + $envs->push($env->key.'='.$env->getResolvedValueWithServer($this->mainServer)); } } @@ -1604,10 +1604,11 @@ private function generate_buildtime_environment_variables() } foreach ($sorted_environment_variables as $env) { + $resolvedValue = $env->getResolvedValueWithServer($this->mainServer); // For literal/multiline vars, real_value includes quotes that we need to remove if ($env->is_literal || $env->is_multiline) { // Strip outer quotes from real_value and apply proper bash escaping - $value = trim($env->real_value, "'"); + $value = trim($resolvedValue, "'"); $escapedValue = escapeBashEnvValue($value); if (isDev() && isset($envs_dict[$env->key])) { @@ -1619,13 +1620,13 @@ private function generate_buildtime_environment_variables() if (isDev()) { $this->application_deployment_queue->addLogEntry("[DEBUG] Build-time env: {$env->key}"); $this->application_deployment_queue->addLogEntry('[DEBUG] Type: literal/multiline'); - $this->application_deployment_queue->addLogEntry("[DEBUG] raw real_value: {$env->real_value}"); + $this->application_deployment_queue->addLogEntry("[DEBUG] raw real_value: {$resolvedValue}"); $this->application_deployment_queue->addLogEntry("[DEBUG] stripped value: {$value}"); $this->application_deployment_queue->addLogEntry("[DEBUG] final escaped: {$escapedValue}"); } } else { // For normal vars, use double quotes to allow $VAR expansion - $escapedValue = escapeBashDoubleQuoted($env->real_value); + $escapedValue = escapeBashDoubleQuoted($resolvedValue); if (isDev() && isset($envs_dict[$env->key])) { $this->application_deployment_queue->addLogEntry("[DEBUG] User override: {$env->key} (was: {$envs_dict[$env->key]}, now: {$escapedValue})"); @@ -1636,7 +1637,7 @@ private function generate_buildtime_environment_variables() if (isDev()) { $this->application_deployment_queue->addLogEntry("[DEBUG] Build-time env: {$env->key}"); $this->application_deployment_queue->addLogEntry('[DEBUG] Type: normal (allows expansion)'); - $this->application_deployment_queue->addLogEntry("[DEBUG] real_value: {$env->real_value}"); + $this->application_deployment_queue->addLogEntry("[DEBUG] real_value: {$resolvedValue}"); $this->application_deployment_queue->addLogEntry("[DEBUG] final escaped: {$escapedValue}"); } } @@ -1655,10 +1656,11 @@ private function generate_buildtime_environment_variables() } foreach ($sorted_environment_variables as $env) { + $resolvedValue = $env->getResolvedValueWithServer($this->mainServer); // For literal/multiline vars, real_value includes quotes that we need to remove if ($env->is_literal || $env->is_multiline) { // Strip outer quotes from real_value and apply proper bash escaping - $value = trim($env->real_value, "'"); + $value = trim($resolvedValue, "'"); $escapedValue = escapeBashEnvValue($value); if (isDev() && isset($envs_dict[$env->key])) { @@ -1670,13 +1672,13 @@ private function generate_buildtime_environment_variables() if (isDev()) { $this->application_deployment_queue->addLogEntry("[DEBUG] Build-time env: {$env->key}"); $this->application_deployment_queue->addLogEntry('[DEBUG] Type: literal/multiline'); - $this->application_deployment_queue->addLogEntry("[DEBUG] raw real_value: {$env->real_value}"); + $this->application_deployment_queue->addLogEntry("[DEBUG] raw real_value: {$resolvedValue}"); $this->application_deployment_queue->addLogEntry("[DEBUG] stripped value: {$value}"); $this->application_deployment_queue->addLogEntry("[DEBUG] final escaped: {$escapedValue}"); } } else { // For normal vars, use double quotes to allow $VAR expansion - $escapedValue = escapeBashDoubleQuoted($env->real_value); + $escapedValue = escapeBashDoubleQuoted($resolvedValue); if (isDev() && isset($envs_dict[$env->key])) { $this->application_deployment_queue->addLogEntry("[DEBUG] User override: {$env->key} (was: {$envs_dict[$env->key]}, now: {$escapedValue})"); @@ -1687,7 +1689,7 @@ private function generate_buildtime_environment_variables() if (isDev()) { $this->application_deployment_queue->addLogEntry("[DEBUG] Build-time env: {$env->key}"); $this->application_deployment_queue->addLogEntry('[DEBUG] Type: normal (allows expansion)'); - $this->application_deployment_queue->addLogEntry("[DEBUG] real_value: {$env->real_value}"); + $this->application_deployment_queue->addLogEntry("[DEBUG] real_value: {$resolvedValue}"); $this->application_deployment_queue->addLogEntry("[DEBUG] final escaped: {$escapedValue}"); } } @@ -2392,15 +2394,17 @@ private function generate_nixpacks_env_variables() $this->env_nixpacks_args = collect([]); if ($this->pull_request_id === 0) { foreach ($this->application->nixpacks_environment_variables as $env) { - if (! is_null($env->real_value) && $env->real_value !== '') { - $value = ($env->is_literal || $env->is_multiline) ? trim($env->real_value, "'") : $env->real_value; + $resolvedValue = $env->getResolvedValueWithServer($this->mainServer); + if (! is_null($resolvedValue) && $resolvedValue !== '') { + $value = ($env->is_literal || $env->is_multiline) ? trim($resolvedValue, "'") : $resolvedValue; $this->env_nixpacks_args->push('--env '.escapeShellValue("{$env->key}={$value}")); } } } else { foreach ($this->application->nixpacks_environment_variables_preview as $env) { - if (! is_null($env->real_value) && $env->real_value !== '') { - $value = ($env->is_literal || $env->is_multiline) ? trim($env->real_value, "'") : $env->real_value; + $resolvedValue = $env->getResolvedValueWithServer($this->mainServer); + if (! is_null($resolvedValue) && $resolvedValue !== '') { + $value = ($env->is_literal || $env->is_multiline) ? trim($resolvedValue, "'") : $resolvedValue; $this->env_nixpacks_args->push('--env '.escapeShellValue("{$env->key}={$value}")); } } @@ -2539,8 +2543,9 @@ private function generate_env_variables() ->get(); foreach ($envs as $env) { - if (! is_null($env->real_value)) { - $this->env_args->put($env->key, $env->real_value); + $resolvedValue = $env->getResolvedValueWithServer($this->mainServer); + if (! is_null($resolvedValue)) { + $this->env_args->put($env->key, $resolvedValue); } } } else { @@ -2550,8 +2555,9 @@ private function generate_env_variables() ->get(); foreach ($envs as $env) { - if (! is_null($env->real_value)) { - $this->env_args->put($env->key, $env->real_value); + $resolvedValue = $env->getResolvedValueWithServer($this->mainServer); + if (! is_null($resolvedValue)) { + $this->env_args->put($env->key, $resolvedValue); } } } @@ -3566,7 +3572,7 @@ private function generate_secrets_hash($variables) } else { $secrets_string = $variables ->map(function ($env) { - return "{$env->key}={$env->real_value}"; + return "{$env->key}={$env->getResolvedValueWithServer($this->mainServer)}"; }) ->sort() ->implode('|'); @@ -3632,7 +3638,7 @@ private function add_build_env_variables_to_dockerfile() if (data_get($env, 'is_multiline') === true) { $argsToInsert->push("ARG {$env->key}"); } else { - $argsToInsert->push("ARG {$env->key}={$env->real_value}"); + $argsToInsert->push("ARG {$env->key}={$env->getResolvedValueWithServer($this->mainServer)}"); } } // Add Coolify variables as ARGs @@ -3654,7 +3660,7 @@ private function add_build_env_variables_to_dockerfile() if (data_get($env, 'is_multiline') === true) { $argsToInsert->push("ARG {$env->key}"); } else { - $argsToInsert->push("ARG {$env->key}={$env->real_value}"); + $argsToInsert->push("ARG {$env->key}={$env->getResolvedValueWithServer($this->mainServer)}"); } } // Add Coolify variables as ARGs @@ -3690,7 +3696,7 @@ private function add_build_env_variables_to_dockerfile() } } $envs_mapped = $envs->mapWithKeys(function ($env) { - return [$env->key => $env->real_value]; + return [$env->key => $env->getResolvedValueWithServer($this->mainServer)]; }); $secrets_hash = $this->generate_secrets_hash($envs_mapped); $argsToInsert->push("ARG COOLIFY_BUILD_SECRETS_HASH={$secrets_hash}"); diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/Add.php b/app/Livewire/Project/Shared/EnvironmentVariable/Add.php index 73d5393b0..c51b27b6a 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/Add.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/Add.php @@ -71,6 +71,7 @@ public function availableSharedVariables(): array 'team' => [], 'project' => [], 'environment' => [], + 'server' => [], ]; // Early return if no team @@ -126,6 +127,66 @@ public function availableSharedVariables(): array } } + // Get server variables + $serverUuid = data_get($this->parameters, 'server_uuid'); + if ($serverUuid) { + // If we have a specific server_uuid, show variables for that server + $server = \App\Models\Server::where('team_id', $team->id) + ->where('uuid', $serverUuid) + ->first(); + + if ($server) { + try { + $this->authorize('view', $server); + $result['server'] = $server->environment_variables() + ->pluck('key') + ->toArray(); + } catch (\Illuminate\Auth\Access\AuthorizationException $e) { + // User not authorized to view server variables + } + } + } else { + // For application environment variables, try to use the application's destination server + $applicationUuid = data_get($this->parameters, 'application_uuid'); + if ($applicationUuid) { + $application = \App\Models\Application::whereRelation('environment.project.team', 'id', $team->id) + ->where('uuid', $applicationUuid) + ->with('destination.server') + ->first(); + + if ($application && $application->destination && $application->destination->server) { + try { + $this->authorize('view', $application->destination->server); + $result['server'] = $application->destination->server->environment_variables() + ->pluck('key') + ->toArray(); + } catch (\Illuminate\Auth\Access\AuthorizationException $e) { + // User not authorized to view server variables + } + } + } else { + // For service environment variables, try to use the service's server + $serviceUuid = data_get($this->parameters, 'service_uuid'); + if ($serviceUuid) { + $service = \App\Models\Service::whereRelation('environment.project.team', 'id', $team->id) + ->where('uuid', $serviceUuid) + ->with('server') + ->first(); + + if ($service && $service->server) { + try { + $this->authorize('view', $service->server); + $result['server'] = $service->server->environment_variables() + ->pluck('key') + ->toArray(); + } catch (\Illuminate\Auth\Access\AuthorizationException $e) { + // User not authorized to view server variables + } + } + } + } + } + return $result; } diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php index c567d96aa..4e8521f27 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php @@ -219,6 +219,7 @@ public function availableSharedVariables(): array 'team' => [], 'project' => [], 'environment' => [], + 'server' => [], ]; // Early return if no team @@ -274,6 +275,66 @@ public function availableSharedVariables(): array } } + // Get server variables + $serverUuid = data_get($this->parameters, 'server_uuid'); + if ($serverUuid) { + // If we have a specific server_uuid, show variables for that server + $server = \App\Models\Server::where('team_id', $team->id) + ->where('uuid', $serverUuid) + ->first(); + + if ($server) { + try { + $this->authorize('view', $server); + $result['server'] = $server->environment_variables() + ->pluck('key') + ->toArray(); + } catch (\Illuminate\Auth\Access\AuthorizationException $e) { + // User not authorized to view server variables + } + } + } else { + // For application environment variables, try to use the application's destination server + $applicationUuid = data_get($this->parameters, 'application_uuid'); + if ($applicationUuid) { + $application = \App\Models\Application::whereRelation('environment.project.team', 'id', $team->id) + ->where('uuid', $applicationUuid) + ->with('destination.server') + ->first(); + + if ($application && $application->destination && $application->destination->server) { + try { + $this->authorize('view', $application->destination->server); + $result['server'] = $application->destination->server->environment_variables() + ->pluck('key') + ->toArray(); + } catch (\Illuminate\Auth\Access\AuthorizationException $e) { + // User not authorized to view server variables + } + } + } else { + // For service environment variables, try to use the service's server + $serviceUuid = data_get($this->parameters, 'service_uuid'); + if ($serviceUuid) { + $service = \App\Models\Service::whereRelation('environment.project.team', 'id', $team->id) + ->where('uuid', $serviceUuid) + ->with('server') + ->first(); + + if ($service && $service->server) { + try { + $this->authorize('view', $service->server); + $result['server'] = $service->server->environment_variables() + ->pluck('key') + ->toArray(); + } catch (\Illuminate\Auth\Access\AuthorizationException $e) { + // User not authorized to view server variables + } + } + } + } + } + return $result; } diff --git a/app/Livewire/SharedVariables/Environment/Show.php b/app/Livewire/SharedVariables/Environment/Show.php index 9405b452a..bfbdf9212 100644 --- a/app/Livewire/SharedVariables/Environment/Show.php +++ b/app/Livewire/SharedVariables/Environment/Show.php @@ -51,11 +51,14 @@ public function saveKey($data) } } - public function mount() + public function mount(?string $project_uuid = null, ?string $environment_uuid = null) { $this->parameters = get_route_parameters(); - $this->project = Project::ownedByCurrentTeam()->where('uuid', request()->route('project_uuid'))->firstOrFail(); - $this->environment = $this->project->environments()->where('uuid', request()->route('environment_uuid'))->firstOrFail(); + $projectUuid = $project_uuid ?? request()->route('project_uuid'); + $environmentUuid = $environment_uuid ?? request()->route('environment_uuid'); + + $this->project = Project::ownedByCurrentTeam()->where('uuid', $projectUuid)->firstOrFail(); + $this->environment = $this->project->environments()->where('uuid', $environmentUuid)->firstOrFail(); $this->getDevView(); } diff --git a/app/Livewire/SharedVariables/Project/Show.php b/app/Livewire/SharedVariables/Project/Show.php index 7753a4027..c9f0dcd8e 100644 --- a/app/Livewire/SharedVariables/Project/Show.php +++ b/app/Livewire/SharedVariables/Project/Show.php @@ -44,9 +44,9 @@ public function saveKey($data) } } - public function mount() + public function mount(?string $project_uuid = null) { - $projectUuid = request()->route('project_uuid'); + $projectUuid = $project_uuid ?? request()->route('project_uuid'); $teamId = currentTeam()->id; $project = Project::where('team_id', $teamId)->where('uuid', $projectUuid)->first(); if (! $project) { diff --git a/app/Livewire/SharedVariables/Server/Index.php b/app/Livewire/SharedVariables/Server/Index.php new file mode 100644 index 000000000..cd10e510a --- /dev/null +++ b/app/Livewire/SharedVariables/Server/Index.php @@ -0,0 +1,22 @@ +servers = Server::ownedByCurrentTeamCached(); + } + + public function render() + { + return view('livewire.shared-variables.server.index'); + } +} diff --git a/app/Livewire/SharedVariables/Server/Show.php b/app/Livewire/SharedVariables/Server/Show.php new file mode 100644 index 000000000..a0498b2b7 --- /dev/null +++ b/app/Livewire/SharedVariables/Server/Show.php @@ -0,0 +1,190 @@ + 'refreshEnvs', 'saveKey' => 'saveKey', 'environmentVariableDeleted' => 'refreshEnvs']; + + public function saveKey($data) + { + try { + $this->authorize('update', $this->server); + + if (in_array($data['key'], ['COOLIFY_SERVER_UUID', 'COOLIFY_SERVER_NAME'])) { + throw new \Exception('Cannot create predefined variable.'); + } + + $found = $this->server->environment_variables()->where('key', $data['key'])->first(); + if ($found) { + throw new \Exception('Variable already exists.'); + } + $this->server->environment_variables()->create([ + 'key' => $data['key'], + 'value' => $data['value'], + 'is_multiline' => $data['is_multiline'], + 'is_literal' => $data['is_literal'], + 'comment' => $data['comment'] ?? null, + 'type' => 'server', + 'team_id' => currentTeam()->id, + ]); + $this->server->refresh(); + $this->getDevView(); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function mount(?string $server_uuid = null) + { + $serverUuid = $server_uuid ?? request()->route('server_uuid'); + $teamId = currentTeam()->id; + $server = Server::where('team_id', $teamId)->where('uuid', $serverUuid)->first(); + if (! $server) { + return redirect()->route('dashboard'); + } + $this->authorize('view', $server); + $this->server = $server; + $this->getDevView(); + } + + public function switch() + { + $this->authorize('view', $this->server); + $this->view = $this->view === 'normal' ? 'dev' : 'normal'; + $this->getDevView(); + } + + public function getDevView() + { + $this->variables = $this->formatEnvironmentVariables($this->server->environment_variables->whereNotIn('key', ['COOLIFY_SERVER_UUID', 'COOLIFY_SERVER_NAME'])->sortBy('key')); + } + + private function formatEnvironmentVariables($variables) + { + return $variables->map(function ($item) { + if ($item->is_shown_once) { + return "$item->key=(Locked Secret, delete and add again to change)"; + } + if ($item->is_multiline) { + return "$item->key=(Multiline environment variable, edit in normal view)"; + } + + return "$item->key=$item->value"; + })->join("\n"); + } + + public function submit() + { + try { + $this->authorize('update', $this->server); + $this->handleBulkSubmit(); + $this->getDevView(); + } catch (\Throwable $e) { + return handleError($e, $this); + } finally { + $this->refreshEnvs(); + } + } + + private function handleBulkSubmit() + { + $variables = parseEnvFormatToArray($this->variables); + + $changesMade = DB::transaction(function () use ($variables) { + // Delete removed variables + $deletedCount = $this->deleteRemovedVariables($variables); + + // Update or create variables + $updatedCount = $this->updateOrCreateVariables($variables); + + return $deletedCount > 0 || $updatedCount > 0; + }); + + if ($changesMade) { + $this->dispatch('success', 'Environment variables updated.'); + } + } + + private function deleteRemovedVariables($variables) + { + $variablesToDelete = $this->server->environment_variables() + ->whereNotIn('key', array_keys($variables)) + ->whereNotIn('key', ['COOLIFY_SERVER_UUID', 'COOLIFY_SERVER_NAME']) + ->get(); + + if ($variablesToDelete->isEmpty()) { + return 0; + } + + $this->server->environment_variables() + ->whereNotIn('key', array_keys($variables)) + ->whereNotIn('key', ['COOLIFY_SERVER_UUID', 'COOLIFY_SERVER_NAME']) + ->delete(); + + return $variablesToDelete->count(); + } + + private function updateOrCreateVariables($variables) + { + $count = 0; + foreach ($variables as $key => $data) { + $value = is_array($data) ? ($data['value'] ?? '') : $data; + $comment = is_array($data) ? ($data['comment'] ?? null) : null; + + // Skip predefined variables + if (in_array($key, ['COOLIFY_SERVER_UUID', 'COOLIFY_SERVER_NAME'])) { + continue; + } + $found = $this->server->environment_variables()->where('key', $key)->first(); + + if ($found) { + if (! $found->is_shown_once && ! $found->is_multiline) { + if ($found->value !== $value || $found->comment !== $comment) { + $found->value = $value; + $found->comment = $comment; + $found->save(); + $count++; + } + } + } else { + $this->server->environment_variables()->create([ + 'key' => $key, + 'value' => $value, + 'comment' => $comment, + 'is_multiline' => false, + 'is_literal' => false, + 'type' => 'server', + 'team_id' => currentTeam()->id, + ]); + $count++; + } + } + + return $count; + } + + public function refreshEnvs() + { + $this->server->refresh(); + $this->getDevView(); + } + + public function render() + { + return view('livewire.shared-variables.server.show'); + } +} diff --git a/app/Models/Environment.php b/app/Models/Environment.php index 65ffaf579..55830f889 100644 --- a/app/Models/Environment.php +++ b/app/Models/Environment.php @@ -63,7 +63,7 @@ public function isEmpty() public function environment_variables() { - return $this->hasMany(SharedEnvironmentVariable::class); + return $this->hasMany(SharedEnvironmentVariable::class)->where('type', 'environment'); } public function applications() diff --git a/app/Models/EnvironmentVariable.php b/app/Models/EnvironmentVariable.php index 5acd4c1e4..83212267c 100644 --- a/app/Models/EnvironmentVariable.php +++ b/app/Models/EnvironmentVariable.php @@ -152,6 +152,17 @@ public function realValue(): Attribute return null; } + // Load relationships needed for shared variable resolution + if (! $resource->relationLoaded('environment')) { + $resource->load('environment'); + } + if (! $resource->relationLoaded('server') && method_exists($resource, 'server')) { + $resource->load('server'); + } + if (! $resource->relationLoaded('destination') && method_exists($resource, 'destination')) { + $resource->load('destination.server'); + } + $real_value = $this->get_real_environment_variables($this->value, $resource); // Skip escaping for valid JSON objects/arrays to prevent quote corruption (see #6160) @@ -217,9 +228,99 @@ protected function isShared(): Attribute ); } + public function get_real_environment_variables_with_server(?string $environment_variable = null, $resource = null, $server = null) + { + return $this->get_real_environment_variables_internal($environment_variable, $resource, $server); + } + + public function getResolvedValueWithServer($server = null) + { + if (! $this->relationLoaded('resourceable')) { + $this->load('resourceable'); + } + $resource = $this->resourceable; + if (! $resource) { + return null; + } + + // Load relationships needed for shared variable resolution + if (! $resource->relationLoaded('environment')) { + $resource->load('environment'); + } + if (! $resource->relationLoaded('server') && method_exists($resource, 'server')) { + $resource->load('server'); + } + if (! $resource->relationLoaded('destination') && method_exists($resource, 'destination')) { + $resource->load('destination.server'); + } + + $real_value = $this->get_real_environment_variables_internal($this->value, $resource, $server); + + // Skip escaping for valid JSON objects/arrays to prevent quote corruption (see #6160) + if (json_validate($real_value) && (str_starts_with($real_value, '{') || str_starts_with($real_value, '['))) { + return $real_value; + } + + if ($this->is_literal || $this->is_multiline) { + $real_value = '\''.$real_value.'\''; + } else { + $real_value = escapeEnvVariables($real_value); + } + + return $real_value; + } + private function get_real_environment_variables(?string $environment_variable = null, $resource = null) { - return resolveSharedEnvironmentVariables($environment_variable, $resource); + return $this->get_real_environment_variables_internal($environment_variable, $resource); + } + + private function get_real_environment_variables_internal(?string $environment_variable = null, $resource = null, $serverOverride = null) + { + if (is_null($environment_variable) || $environment_variable === '' || is_null($resource)) { + return $environment_variable; + } + $environment_variable = trim($environment_variable); + $sharedEnvsFound = str($environment_variable)->matchAll('/{{(.*?)}}/'); + if ($sharedEnvsFound->isEmpty()) { + return $environment_variable; + } + foreach ($sharedEnvsFound as $sharedEnv) { + $type = str($sharedEnv)->trim()->match('/(.*?)\./'); + if (! collect(SHARED_VARIABLE_TYPES)->contains($type)) { + continue; + } + $variable = str($sharedEnv)->trim()->match('/\.(.*)/'); + $id = null; + if ($type->value() === 'environment') { + $id = $resource->environment->id; + } elseif ($type->value() === 'project') { + $id = $resource->environment->project->id; + } elseif ($type->value() === 'team') { + $id = $resource->team()->id; + } elseif ($type->value() === 'server') { + if ($serverOverride) { + $id = $serverOverride->id; + } elseif (isset($resource->server) && $resource->server) { + $id = $resource->server->id; + } elseif (isset($resource->destination) && $resource->destination && isset($resource->destination->server)) { + $id = $resource->destination->server->id; + } + } + if (is_null($id)) { + continue; + } + $found = SharedEnvironmentVariable::where('type', $type) + ->where('key', $variable) + ->where('team_id', $resource->team()->id) + ->where("{$type}_id", $id) + ->first(); + if ($found) { + $environment_variable = str($environment_variable)->replace("{{{$sharedEnv}}}", $found->value); + } + } + + return str($environment_variable)->value(); } private function get_environment_variables(?string $environment_variable = null): ?string diff --git a/app/Models/Project.php b/app/Models/Project.php index 15628892e..632787a07 100644 --- a/app/Models/Project.php +++ b/app/Models/Project.php @@ -74,7 +74,7 @@ protected static function booted() public function environment_variables() { - return $this->hasMany(SharedEnvironmentVariable::class); + return $this->hasMany(SharedEnvironmentVariable::class)->where('type', 'project'); } public function environments() diff --git a/app/Models/Server.php b/app/Models/Server.php index 32100a775..06426f211 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -155,13 +155,7 @@ protected static function booted() 'server_id' => $server->id, ])->save(); } else { - (new StandaloneDocker)->forceFill([ - 'id' => 0, - 'name' => 'coolify', - 'uuid' => (string) new Cuid2, - 'network' => 'coolify', - 'server_id' => $server->id, - ])->saveQuietly(); + (new StandaloneDocker)->forceFill($server->defaultStandaloneDockerAttributes(id: 0))->saveQuietly(); } } else { if ($server->isSwarm()) { @@ -172,18 +166,31 @@ protected static function booted() ]); } else { $standaloneDocker = new StandaloneDocker; - $standaloneDocker->forceFill([ - 'name' => 'coolify', - 'uuid' => (string) new Cuid2, - 'network' => 'coolify', - 'server_id' => $server->id, - ]); + $standaloneDocker->forceFill($server->defaultStandaloneDockerAttributes()); $standaloneDocker->saveQuietly(); } } if (! isset($server->proxy->redirect_enabled)) { $server->proxy->redirect_enabled = true; } + + // Create predefined server shared variables + SharedEnvironmentVariable::create([ + 'key' => 'COOLIFY_SERVER_UUID', + 'value' => $server->uuid, + 'type' => 'server', + 'server_id' => $server->id, + 'team_id' => $server->team_id, + 'is_literal' => true, + ]); + SharedEnvironmentVariable::create([ + 'key' => 'COOLIFY_SERVER_NAME', + 'value' => $server->name, + 'type' => 'server', + 'server_id' => $server->id, + 'team_id' => $server->team_id, + 'is_literal' => true, + ]); }); static::retrieved(function ($server) { if (! isset($server->proxy->redirect_enabled)) { @@ -1026,6 +1033,30 @@ public function team() return $this->belongsTo(Team::class); } + /** + * @return array{id?: int, name: string, uuid: string, network: string, server_id: int} + */ + public function defaultStandaloneDockerAttributes(?int $id = null): array + { + $attributes = [ + 'name' => 'coolify', + 'uuid' => (string) new Cuid2, + 'network' => 'coolify', + 'server_id' => $this->id, + ]; + + if (! is_null($id)) { + $attributes['id'] = $id; + } + + return $attributes; + } + + public function environment_variables() + { + return $this->hasMany(SharedEnvironmentVariable::class)->where('type', 'server'); + } + public function isProxyShouldRun() { // TODO: Do we need "|| $this->proxy->force_stop" here? diff --git a/app/Models/SharedEnvironmentVariable.php b/app/Models/SharedEnvironmentVariable.php index 158140b12..fa6fd45e0 100644 --- a/app/Models/SharedEnvironmentVariable.php +++ b/app/Models/SharedEnvironmentVariable.php @@ -17,6 +17,7 @@ class SharedEnvironmentVariable extends Model 'team_id', 'project_id', 'environment_id', + 'server_id', // Boolean flags 'is_multiline', @@ -46,4 +47,9 @@ public function environment() { return $this->belongsTo(Environment::class); } + + public function server() + { + return $this->belongsTo(Server::class); + } } diff --git a/app/Models/Team.php b/app/Models/Team.php index 8eb8fa050..8a54a9dee 100644 --- a/app/Models/Team.php +++ b/app/Models/Team.php @@ -232,7 +232,7 @@ public function subscriptionEnded() public function environment_variables() { - return $this->hasMany(SharedEnvironmentVariable::class)->whereNull('project_id')->whereNull('environment_id'); + return $this->hasMany(SharedEnvironmentVariable::class)->where('type', 'team'); } public function members() diff --git a/app/View/Components/Forms/EnvVarInput.php b/app/View/Components/Forms/EnvVarInput.php index 4a98e4a51..faef64a36 100644 --- a/app/View/Components/Forms/EnvVarInput.php +++ b/app/View/Components/Forms/EnvVarInput.php @@ -38,6 +38,7 @@ public function __construct( public array $availableVars = [], public ?string $projectUuid = null, public ?string $environmentUuid = null, + public ?string $serverUuid = null, ) { // Handle authorization-based disabling if ($this->canGate && $this->canResource && $this->autoDisable) { @@ -86,6 +87,9 @@ public function render(): View|Closure|string 'environment_uuid' => $this->environmentUuid, ]) : route('shared-variables.environment.index'), + 'server' => $this->serverUuid + ? route('shared-variables.server.show', ['server_uuid' => $this->serverUuid]) + : route('shared-variables.server.index'), 'default' => route('shared-variables.index'), ]; diff --git a/bootstrap/helpers/constants.php b/bootstrap/helpers/constants.php index 30ca36f2e..bae2573de 100644 --- a/bootstrap/helpers/constants.php +++ b/bootstrap/helpers/constants.php @@ -81,4 +81,4 @@ const NEEDS_TO_DISABLE_STRIPPREFIX = [ 'appwrite' => ['appwrite', 'appwrite-console', 'appwrite-realtime'], ]; -const SHARED_VARIABLE_TYPES = ['team', 'project', 'environment']; +const SHARED_VARIABLE_TYPES = ['team', 'project', 'environment', 'server']; diff --git a/database/migrations/2025_12_24_095507_add_server_to_shared_environment_variables_table.php b/database/migrations/2025_12_24_095507_add_server_to_shared_environment_variables_table.php new file mode 100644 index 000000000..66d585069 --- /dev/null +++ b/database/migrations/2025_12_24_095507_add_server_to_shared_environment_variables_table.php @@ -0,0 +1,47 @@ +foreignId('server_id')->nullable()->constrained()->onDelete('cascade'); + // NULL != NULL in PostgreSQL unique indexes, so this only enforces uniqueness + // for server-scoped rows (where server_id is non-null). Other scopes are covered + // by existing unique constraints on ['key', 'project_id', 'team_id'] and ['key', 'environment_id', 'team_id']. + $table->unique(['key', 'server_id', 'team_id']); + }); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + DB::transaction(function () { + Schema::table('shared_environment_variables', function (Blueprint $table) { + $table->dropUnique(['key', 'server_id', 'team_id']); + $table->dropForeign(['server_id']); + $table->dropColumn('server_id'); + }); + if (DB::getDriverName() !== 'sqlite') { + DB::statement('ALTER TABLE shared_environment_variables DROP CONSTRAINT IF EXISTS shared_environment_variables_type_check'); + DB::statement("ALTER TABLE shared_environment_variables ADD CONSTRAINT shared_environment_variables_type_check CHECK (type IN ('team', 'project', 'environment'))"); + } + }); + } +}; diff --git a/database/migrations/2025_12_24_133707_add_predefined_server_variables_to_existing_servers.php b/database/migrations/2025_12_24_133707_add_predefined_server_variables_to_existing_servers.php new file mode 100644 index 000000000..c67987e67 --- /dev/null +++ b/database/migrations/2025_12_24_133707_add_predefined_server_variables_to_existing_servers.php @@ -0,0 +1,56 @@ +chunk(100, function ($servers) { + foreach ($servers as $server) { + $existingKeys = SharedEnvironmentVariable::where('type', 'server') + ->where('server_id', $server->id) + ->whereIn('key', ['COOLIFY_SERVER_UUID', 'COOLIFY_SERVER_NAME']) + ->pluck('key') + ->toArray(); + + if (! in_array('COOLIFY_SERVER_UUID', $existingKeys)) { + SharedEnvironmentVariable::create([ + 'key' => 'COOLIFY_SERVER_UUID', + 'value' => $server->uuid, + 'type' => 'server', + 'server_id' => $server->id, + 'team_id' => $server->team_id, + 'is_literal' => true, + ]); + } + + if (! in_array('COOLIFY_SERVER_NAME', $existingKeys)) { + SharedEnvironmentVariable::create([ + 'key' => 'COOLIFY_SERVER_NAME', + 'value' => $server->name, + 'type' => 'server', + 'server_id' => $server->id, + 'team_id' => $server->team_id, + 'is_literal' => true, + ]); + } + } + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + SharedEnvironmentVariable::where('type', 'server') + ->whereIn('key', ['COOLIFY_SERVER_UUID', 'COOLIFY_SERVER_NAME']) + ->delete(); + } +}; diff --git a/database/seeders/SharedEnvironmentVariableSeeder.php b/database/seeders/SharedEnvironmentVariableSeeder.php index 54643fe3b..7a17fbd10 100644 --- a/database/seeders/SharedEnvironmentVariableSeeder.php +++ b/database/seeders/SharedEnvironmentVariableSeeder.php @@ -2,6 +2,7 @@ namespace Database\Seeders; +use App\Models\Server; use App\Models\SharedEnvironmentVariable; use Illuminate\Database\Seeder; @@ -32,5 +33,29 @@ public function run(): void 'project_id' => 1, 'team_id' => 0, ]); + + // Add predefined server variables to all existing servers + $servers = \App\Models\Server::all(); + foreach ($servers as $server) { + SharedEnvironmentVariable::firstOrCreate([ + 'key' => 'COOLIFY_SERVER_UUID', + 'type' => 'server', + 'server_id' => $server->id, + 'team_id' => $server->team_id, + ], [ + 'value' => $server->uuid, + 'is_literal' => true, + ]); + + SharedEnvironmentVariable::firstOrCreate([ + 'key' => 'COOLIFY_SERVER_NAME', + 'type' => 'server', + 'server_id' => $server->id, + 'team_id' => $server->team_id, + ], [ + 'value' => $server->name, + 'is_literal' => true, + ]); + } } } diff --git a/resources/views/components/forms/env-var-input.blade.php b/resources/views/components/forms/env-var-input.blade.php index d26e248c1..642bbcfb0 100644 --- a/resources/views/components/forms/env-var-input.blade.php +++ b/resources/views/components/forms/env-var-input.blade.php @@ -17,8 +17,15 @@ selectedIndex: 0, cursorPosition: 0, currentScope: null, - availableScopes: ['team', 'project', 'environment'], availableVars: @js($availableVars), + get availableScopes() { + // Only include scopes that have at least one variable + const allScopes = ['team', 'project', 'environment', 'server']; + return allScopes.filter(scope => { + const vars = this.availableVars[scope]; + return vars && vars.length > 0; + }); + }, scopeUrls: @js($scopeUrls), handleInput() { @@ -54,6 +61,11 @@ if (content === '') { this.currentScope = null; + // Only show dropdown if there are available scopes with variables + if (this.availableScopes.length === 0) { + this.showDropdown = false; + return; + } this.suggestions = this.availableScopes.map(scope => ({ type: 'scope', value: scope, diff --git a/resources/views/components/navbar.blade.php b/resources/views/components/navbar.blade.php index 48b544ebb..da9a112f8 100644 --- a/resources/views/components/navbar.blade.php +++ b/resources/views/components/navbar.blade.php @@ -79,7 +79,7 @@ }">